fix token validation and error messages

This commit is contained in:
Livio Amstutz 2020-08-03 15:32:49 +02:00
parent 3663b9d1af
commit e46029eebd
7 changed files with 57 additions and 31 deletions

View file

@ -2,7 +2,6 @@ package op
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
@ -95,19 +94,19 @@ func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage
func ValidateAuthReqScopes(scopes []string) error { func ValidateAuthReqScopes(scopes []string) error {
if len(scopes) == 0 { if len(scopes) == 0 {
return ErrInvalidRequest("Unforuntately, the scope of your request is missing. Please ensure your scope value is not 0, and try again. If you have any questions, you may contact the administrator of the application.") return ErrInvalidRequest("Unfortunately, the scope parameter of your request is missing. Please ensure your scope value is not empty, and try again. If you have any questions, you may contact the administrator of the application.")
} }
if !utils.Contains(scopes, oidc.ScopeOpenID) { if !utils.Contains(scopes, oidc.ScopeOpenID) {
return ErrInvalidRequest("Unfortunately, the scope openid of your request is missing. Please ensure your scope openid is complete and accurate, and try again. If you have any questions, you may contact the administrator of the application.") return ErrInvalidRequest("Unfortunately, the scope `openid` is missing. Please ensure your scope configuration is correct (containing the `openid` value), and try again. If you have any questions, you may contact the administrator of the application.")
} }
return nil return nil
} }
func ValidateAuthReqRedirectURI(ctx context.Context, uri, client_id string, responseType oidc.ResponseType, storage OPStorage) error { func ValidateAuthReqRedirectURI(ctx context.Context, uri, clientID string, responseType oidc.ResponseType, storage OPStorage) error {
if uri == "" { if uri == "" {
return ErrInvalidRequestRedirectURI("Unfortunately, the client's redirect_uri is missing. Please ensure your redirect_uri is included in the request, and try again. If you have any questions, you may contact the administrator of the application.") return ErrInvalidRequestRedirectURI("Unfortunately, the client's redirect_uri is missing. Please ensure your redirect_uri is included in the request, and try again. If you have any questions, you may contact the administrator of the application.")
} }
client, err := storage.GetClientByClientID(ctx, client_id) client, err := storage.GetClientByClientID(ctx, clientID)
if err != nil { if err != nil {
return ErrServerError(err.Error()) return ErrServerError(err.Error())
} }
@ -143,9 +142,9 @@ func ValidateAuthReqResponseType(responseType oidc.ResponseType) error {
oidc.ResponseTypeIDTokenOnly: oidc.ResponseTypeIDTokenOnly:
return nil return nil
case "": case "":
return ErrInvalidRequest("Unfortunately, a response type is missing in your request. Please ensure the response type is complete and accurate, and try again. If you have any questions, you may contact the administrator of the application.") return ErrInvalidRequest("Unfortunately, the response type is missing in your request. Please ensure the response type is complete and accurate, and try again. If you have any questions, you may contact the administrator of the application.")
default: default:
return ErrInvalidRequest("response_type invalid") return ErrInvalidRequest("Unfortunately, the response type provided in your request is invalid. Please ensure the response type is valid, and try again. If you have any questions, you may contact the administrator of the application.")
} }
} }
@ -153,7 +152,7 @@ func ValidateAuthReqIDTokenHint(ctx context.Context, idTokenHint string, verifie
if idTokenHint == "" { if idTokenHint == "" {
return "", nil return "", nil
} }
claims, err := verifier.Verify(ctx, "", idTokenHint) claims, err := verifier.VerifyIdToken(ctx, idTokenHint)
if err != nil { if err != nil {
return "", ErrInvalidRequest("Unfortunately, the id_token_hint is invalid. Please ensure the id_token_hint is complete and accurate, and try again. If you have any questions, you may contact the administrator of the application.") return "", ErrInvalidRequest("Unfortunately, the id_token_hint is invalid. Please ensure the id_token_hint is complete and accurate, and try again. If you have any questions, you may contact the administrator of the application.")
} }
@ -175,7 +174,7 @@ func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Author
return return
} }
if !authReq.Done() { if !authReq.Done() {
AuthRequestError(w, r, authReq, errors.New("user not logged in"), authorizer.Encoder()) AuthRequestError(w, r, authReq, ErrInteractionRequired("Unfortunately, the user may is not logged in and/or additional interaction is required."), authorizer.Encoder())
return return
} }
AuthResponse(authReq, authorizer, w, r) AuthResponse(authReq, authorizer, w, r)

View file

@ -54,9 +54,9 @@ func TestValidateIssuer(t *testing.T) {
false, false,
}, },
{ {
"localhost with http ok", "localhost with http fails",
args{"http://localhost:9999"}, args{"http://localhost:9999"},
false, true,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
@ -78,7 +78,7 @@ func TestValidateIssuerDevLocalAllowed(t *testing.T) {
wantErr bool wantErr bool
}{ }{
{ {
"localhost with http ok", "localhost with http with dev ok",
args{"http://localhost:9999"}, args{"http://localhost:9999"},
false, false,
}, },

View file

@ -9,8 +9,10 @@ import (
) )
const ( const (
InvalidRequest errorType = "invalid_request" InvalidRequest errorType = "invalid_request"
ServerError errorType = "server_error" InvalidRequestUri errorType = "invalid_request_uri"
InteractionRequired errorType = "interaction_required"
ServerError errorType = "server_error"
) )
var ( var (
@ -22,11 +24,17 @@ var (
} }
ErrInvalidRequestRedirectURI = func(description string) *OAuthError { ErrInvalidRequestRedirectURI = func(description string) *OAuthError {
return &OAuthError{ return &OAuthError{
ErrorType: InvalidRequest, ErrorType: InvalidRequestUri,
Description: description, Description: description,
redirectDisabled: true, redirectDisabled: true,
} }
} }
ErrInteractionRequired = func(description string) *OAuthError {
return &OAuthError{
ErrorType: InteractionRequired,
Description: description,
}
}
ErrServerError = func(description string) *OAuthError { ErrServerError = func(description string) *OAuthError {
return &OAuthError{ return &OAuthError{
ErrorType: ServerError, ErrorType: ServerError,
@ -54,7 +62,7 @@ func AuthRequestError(w http.ResponseWriter, r *http.Request, authReq ErrAuthReq
e.ErrorType = ServerError e.ErrorType = ServerError
e.Description = err.Error() e.Description = err.Error()
} }
e.state = authReq.GetState() e.State = authReq.GetState()
if authReq.GetRedirectURI() == "" || e.redirectDisabled { if authReq.GetRedirectURI() == "" || e.redirectDisabled {
http.Error(w, e.Description, http.StatusBadRequest) http.Error(w, e.Description, http.StatusBadRequest)
return return
@ -87,9 +95,9 @@ func RequestError(w http.ResponseWriter, r *http.Request, err error) {
type OAuthError struct { type OAuthError struct {
ErrorType errorType `json:"error" schema:"error"` ErrorType errorType `json:"error" schema:"error"`
Description string `json:"error_description" schema:"error_description"` Description string `json:"error_description,omitempty" schema:"error_description,omitempty"`
state string `json:"state" schema:"state"` State string `json:"state,omitempty" schema:"state,omitempty"`
redirectDisabled bool redirectDisabled bool `json:"-" schema:"-"`
} }
func (e *OAuthError) Error() string { func (e *OAuthError) Error() string {

View file

@ -57,7 +57,7 @@ func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest,
if req.IdTokenHint == "" { if req.IdTokenHint == "" {
return session, nil return session, nil
} }
claims, err := ender.IDTokenVerifier().Verify(ctx, "", req.IdTokenHint) claims, err := ender.IDTokenVerifier().VerifyIdToken(ctx, req.IdTokenHint)
if err != nil { if err != nil {
return nil, ErrInvalidRequest("id_token_hint invalid") return nil, ErrInvalidRequest("id_token_hint invalid")
} }

View file

@ -148,7 +148,7 @@ func DefaultACRVerifier(possibleValues []string) ACRVerifier {
//and https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation //and https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation
func (v *DefaultVerifier) Verify(ctx context.Context, accessToken, idTokenString string) (*oidc.IDTokenClaims, error) { func (v *DefaultVerifier) Verify(ctx context.Context, accessToken, idTokenString string) (*oidc.IDTokenClaims, error) {
v.config.now = time.Now().UTC() v.config.now = time.Now().UTC()
idToken, err := v.VerifyIDToken(ctx, idTokenString) idToken, err := v.VerifyIdToken(ctx, idTokenString)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -158,15 +158,9 @@ func (v *DefaultVerifier) Verify(ctx context.Context, accessToken, idTokenString
return idToken, nil return idToken, nil
} }
func (v *DefaultVerifier) now() time.Time { //Verify implements the `VerifyIdToken` method of the `Verifier` interface
if v.config.now.IsZero() { //according to https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
v.config.now = time.Now().UTC().Round(time.Second) func (v *DefaultVerifier) VerifyIdToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error) {
}
return v.config.now
}
//VerifyIDToken: https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
func (v *DefaultVerifier) VerifyIDToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error) {
//1. if encrypted --> decrypt //1. if encrypted --> decrypt
decrypted, err := v.decryptToken(idTokenString) decrypted, err := v.decryptToken(idTokenString)
if err != nil { if err != nil {
@ -227,6 +221,13 @@ func (v *DefaultVerifier) VerifyIDToken(ctx context.Context, idTokenString strin
return claims, nil return claims, nil
} }
func (v *DefaultVerifier) now() time.Time {
if v.config.now.IsZero() {
v.config.now = time.Now().UTC().Round(time.Second)
}
return v.config.now
}
func (v *DefaultVerifier) parseToken(tokenString string) (*oidc.IDTokenClaims, []byte, error) { func (v *DefaultVerifier) parseToken(tokenString string) (*oidc.IDTokenClaims, []byte, error) {
parts := strings.Split(tokenString, ".") parts := strings.Split(tokenString, ".")
if len(parts) != 3 { if len(parts) != 3 {
@ -372,7 +373,7 @@ func (v *DefaultVerifier) decryptToken(tokenString string) (string, error) {
} }
func (v *DefaultVerifier) verifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAlgorithm) error { func (v *DefaultVerifier) verifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAlgorithm) error {
if accessToken == "" { if atHash == "" {
return nil return nil
} }

View file

@ -48,3 +48,18 @@ func (mr *MockVerifierMockRecorder) Verify(arg0, arg1, arg2 interface{}) *gomock
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockVerifier)(nil).Verify), arg0, arg1, arg2) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockVerifier)(nil).Verify), arg0, arg1, arg2)
} }
// VerifyIdToken mocks base method
func (m *MockVerifier) VerifyIdToken(arg0 context.Context, arg1 string) (*oidc.IDTokenClaims, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VerifyIdToken", arg0, arg1)
ret0, _ := ret[0].(*oidc.IDTokenClaims)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VerifyIdToken indicates an expected call of VerifyIdToken
func (mr *MockVerifierMockRecorder) VerifyIdToken(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyIdToken", reflect.TypeOf((*MockVerifier)(nil).VerifyIdToken), arg0, arg1)
}

View file

@ -12,4 +12,7 @@ type Verifier interface {
//Verify checks the access_token and id_token and returns the `id token claims` //Verify checks the access_token and id_token and returns the `id token claims`
Verify(ctx context.Context, accessToken, idTokenString string) (*oidc.IDTokenClaims, error) Verify(ctx context.Context, accessToken, idTokenString string) (*oidc.IDTokenClaims, error)
//VerifyIdToken checks the id_token only and returns its `id token claims`
VerifyIdToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error)
} }