diff --git a/pkg/oidc/authorization.go b/pkg/oidc/authorization.go index 79d0c1e..1e8795f 100644 --- a/pkg/oidc/authorization.go +++ b/pkg/oidc/authorization.go @@ -59,27 +59,28 @@ const ( //AuthRequest according to: //https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest type AuthRequest struct { - ID string - Scopes SpaceDelimitedArray `schema:"scope"` - ResponseType ResponseType `schema:"response_type"` - ClientID string `schema:"client_id"` - RedirectURI string `schema:"redirect_uri"` //TODO: type + Scopes SpaceDelimitedArray `json:"scope" schema:"scope"` + ResponseType ResponseType `json:"response_type" schema:"response_type"` + ClientID string `json:"client_id" schema:"client_id"` + RedirectURI string `json:"redirect_uri" schema:"redirect_uri"` - State string `schema:"state"` + State string `json:"state" schema:"state"` + Nonce string `json:"nonce" schema:"nonce"` - // ResponseMode TODO: ? + ResponseMode ResponseMode `json:"response_mode" schema:"response_mode"` + Display Display `json:"display" schema:"display"` + Prompt SpaceDelimitedArray `json:"prompt" schema:"prompt"` + MaxAge *uint `json:"max_age" schema:"max_age"` + UILocales Locales `json:"ui_locales" schema:"ui_locales"` + IDTokenHint string `json:"id_token_hint" schema:"id_token_hint"` + LoginHint string `json:"login_hint" schema:"login_hint"` + ACRValues []string `json:"acr_values" schema:"acr_values"` - Nonce string `schema:"nonce"` - Display Display `schema:"display"` - Prompt SpaceDelimitedArray `schema:"prompt"` - MaxAge *uint `schema:"max_age"` - UILocales Locales `schema:"ui_locales"` - IDTokenHint string `schema:"id_token_hint"` - LoginHint string `schema:"login_hint"` - ACRValues []string `schema:"acr_values"` + CodeChallenge string `json:"code_challenge" schema:"code_challenge"` + CodeChallengeMethod CodeChallengeMethod `json:"code_challenge_method" schema:"code_challenge_method"` - CodeChallenge string `schema:"code_challenge"` - CodeChallengeMethod CodeChallengeMethod `schema:"code_challenge_method"` + //RequestParam enables OIDC requests to be passed in a single, self-contained parameter (as JWT, called Request Object) + RequestParam string `schema:"request"` } //GetRedirectURI returns the redirect_uri value for the ErrAuthRequest interface diff --git a/pkg/oidc/error.go b/pkg/oidc/error.go index f1224de..1f4fa7c 100644 --- a/pkg/oidc/error.go +++ b/pkg/oidc/error.go @@ -64,6 +64,7 @@ const ( ServerError errorType = "server_error" InteractionRequired errorType = "interaction_required" LoginRequired errorType = "login_required" + RequestNotSupported errorType = "request_not_supported" ) var ( @@ -118,6 +119,11 @@ var ( ErrorType: LoginRequired, } } + ErrRequestNotSupported = func() *Error { + return &Error{ + ErrorType: RequestNotSupported, + } + } ) // DefaultToServerError checks if the error is an Error diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index e72d67c..0bee12b 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -6,6 +6,7 @@ import ( "time" "golang.org/x/text/language" + "gopkg.in/square/go-jose.v2" ) type Audience []string @@ -106,3 +107,16 @@ func (t *Time) UnmarshalJSON(data []byte) error { func (t *Time) MarshalJSON() ([]byte, error) { return json.Marshal(time.Time(*t).UTC().Unix()) } + +type RequestObject struct { + Issuer string `json:"iss"` + Audience Audience `json:"aud"` + AuthRequest +} + +func (r *RequestObject) GetIssuer() string { + return r.Issuer +} + +func (r *RequestObject) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) { +} diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index a6348bd..79dc97f 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -2,7 +2,6 @@ package op import ( "context" - "fmt" "net" "net/http" "net/url" @@ -41,6 +40,7 @@ type Authorizer interface { IDTokenHintVerifier() IDTokenHintVerifier Crypto() Crypto Issuer() string + RequestObjectSupported() bool } //AuthorizeValidator is an extension of Authorizer interface @@ -70,6 +70,13 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { AuthRequestError(w, r, authReq, err, authorizer.Encoder()) return } + if authReq.RequestParam != "" && authorizer.RequestObjectSupported() { + authReq, err = ParseRequestObject(r.Context(), authReq, authorizer.Storage(), authorizer.Issuer()) + if err != nil { + AuthRequestError(w, r, authReq, err, authorizer.Encoder()) + return + } + } validation := ValidateAuthRequest if validater, ok := authorizer.(AuthorizeValidator); ok { validation = validater.ValidateAuthRequest @@ -79,6 +86,10 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { AuthRequestError(w, r, authReq, err, authorizer.Encoder()) return } + if authReq.RequestParam != "" { + AuthRequestError(w, r, authReq, oidc.ErrRequestNotSupported(), authorizer.Encoder()) + return + } req, err := authorizer.Storage().CreateAuthRequest(r.Context(), authReq, userID) if err != nil { AuthRequestError(w, r, authReq, oidc.DefaultToServerError(err, "unable to save auth request"), authorizer.Encoder()) @@ -92,7 +103,7 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { RedirectToLogin(req.GetID(), client, w, r) } -//ParseAuthorizeRequest parsed the http request into a oidc.AuthRequest +//ParseAuthorizeRequest parsed the http request into an oidc.AuthRequest func ParseAuthorizeRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.AuthRequest, error) { err := r.ParseForm() if err != nil { @@ -106,6 +117,83 @@ func ParseAuthorizeRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.A return authReq, nil } +//ParseRequestObject parse the `request` parameter, validates the token including the signature +//and copies the token claims into the auth request +func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, issuer string) (*oidc.AuthRequest, error) { + requestObject := new(oidc.RequestObject) + payload, err := oidc.ParseToken(authReq.RequestParam, requestObject) + if err != nil { + return nil, err + } + + if requestObject.ClientID != "" && requestObject.ClientID != authReq.ClientID { + return authReq, oidc.ErrInvalidRequest() + } + if requestObject.ResponseType != "" && requestObject.ResponseType != authReq.ResponseType { + return authReq, oidc.ErrInvalidRequest() + } + if requestObject.Issuer != requestObject.ClientID { + return authReq, oidc.ErrInvalidRequest() + } + if !str.Contains(requestObject.Audience, issuer) { + return authReq, oidc.ErrInvalidRequest() + } + keySet := &jwtProfileKeySet{storage, requestObject.Issuer} + if err = oidc.CheckSignature(ctx, authReq.RequestParam, payload, requestObject, nil, keySet); err != nil { + return authReq, err + } + CopyRequestObjectToAuthRequest(authReq, requestObject) + return authReq, nil +} + +//CopyRequestObjectToAuthRequest overwrites present values from the Request Object into the auth request +//and clears the `RequestParam` of the auth request +func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oidc.RequestObject) { + if str.Contains(authReq.Scopes, oidc.ScopeOpenID) && len(requestObject.Scopes) > 0 { + authReq.Scopes = requestObject.Scopes + } + if requestObject.RedirectURI != "" { + authReq.RedirectURI = requestObject.RedirectURI + } + if requestObject.State != "" { + authReq.State = requestObject.State + } + if requestObject.ResponseMode != "" { + authReq.ResponseMode = requestObject.ResponseMode + } + if requestObject.Nonce != "" { + authReq.Nonce = requestObject.Nonce + } + if requestObject.Display != "" { + authReq.Display = requestObject.Display + } + if len(requestObject.Prompt) > 0 { + authReq.Prompt = requestObject.Prompt + } + if requestObject.MaxAge != nil { + authReq.MaxAge = requestObject.MaxAge + } + if len(requestObject.UILocales) > 0 { + authReq.UILocales = requestObject.UILocales + } + if requestObject.IDTokenHint != "" { + authReq.IDTokenHint = requestObject.IDTokenHint + } + if requestObject.LoginHint != "" { + authReq.LoginHint = requestObject.LoginHint + } + if len(requestObject.ACRValues) > 0 { + authReq.ACRValues = requestObject.ACRValues + } + if requestObject.CodeChallenge != "" { + authReq.CodeChallenge = requestObject.CodeChallenge + } + if requestObject.CodeChallengeMethod != "" { + authReq.CodeChallengeMethod = requestObject.CodeChallengeMethod + } + authReq.RequestParam = "" +} + //ValidateAuthRequest validates the authorize parameters and returns the userID of the id_token_hint if passed func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier IDTokenHintVerifier) (sub string, err error) { authReq.MaxAge, err = ValidateAuthReqPrompt(authReq.Prompt, authReq.MaxAge) diff --git a/pkg/op/config.go b/pkg/op/config.go index 39c84c8..1ec99c9 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -22,9 +22,14 @@ type Configuration interface { AuthMethodPostSupported() bool CodeMethodS256Supported() bool AuthMethodPrivateKeyJWTSupported() bool + TokenEndpointSigningAlgorithmsSupported() []string GrantTypeRefreshTokenSupported() bool GrantTypeTokenExchangeSupported() bool GrantTypeJWTAuthorizationSupported() bool + IntrospectionAuthMethodPrivateKeyJWTSupported() bool + IntrospectionEndpointSigningAlgorithmsSupported() []string + RequestObjectSupported() bool + RequestObjectSigningAlgorithmsSupported() []string SupportedUILocales() []language.Tag } diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index c407ffc..c625abc 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -19,23 +19,27 @@ func Discover(w http.ResponseWriter, config *oidc.DiscoveryConfiguration) { func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfiguration { return &oidc.DiscoveryConfiguration{ - Issuer: c.Issuer(), - AuthorizationEndpoint: c.AuthorizationEndpoint().Absolute(c.Issuer()), - TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()), - IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()), - UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()), - EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), - JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), - ScopesSupported: Scopes(c), - ResponseTypesSupported: ResponseTypes(c), - GrantTypesSupported: GrantTypes(c), - SubjectTypesSupported: SubjectTypes(c), - IDTokenSigningAlgValuesSupported: SigAlgorithms(s), - TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(c), - IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(c), - ClaimsSupported: SupportedClaims(c), - CodeChallengeMethodsSupported: CodeChallengeMethods(c), - UILocalesSupported: c.SupportedUILocales(), + Issuer: c.Issuer(), + AuthorizationEndpoint: c.AuthorizationEndpoint().Absolute(c.Issuer()), + TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()), + IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()), + UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()), + EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), + JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), + ScopesSupported: Scopes(c), + ResponseTypesSupported: ResponseTypes(c), + GrantTypesSupported: GrantTypes(c), + SubjectTypesSupported: SubjectTypes(c), + IDTokenSigningAlgValuesSupported: SigAlgorithms(s), + RequestObjectSigningAlgValuesSupported: RequestObjectSigAlgorithms(c), + TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(c), + TokenEndpointAuthSigningAlgValuesSupported: TokenSigAlgorithms(c), + IntrospectionEndpointAuthSigningAlgValuesSupported: IntrospectionSigAlgorithms(c), + IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(c), + ClaimsSupported: SupportedClaims(c), + CodeChallengeMethodsSupported: CodeChallengeMethods(c), + UILocalesSupported: c.SupportedUILocales(), + RequestParameterSupported: c.RequestObjectSupported(), } } @@ -128,6 +132,13 @@ func AuthMethodsTokenEndpoint(c Configuration) []oidc.AuthMethod { return authMethods } +func TokenSigAlgorithms(c Configuration) []string { + if !c.AuthMethodPrivateKeyJWTSupported() { + return nil + } + return c.TokenEndpointSigningAlgorithmsSupported() +} + func AuthMethodsIntrospectionEndpoint(c Configuration) []oidc.AuthMethod { authMethods := []oidc.AuthMethod{ oidc.AuthMethodBasic, @@ -145,3 +156,17 @@ func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod { } return codeMethods } + +func IntrospectionSigAlgorithms(c Configuration) []string { + if !c.IntrospectionAuthMethodPrivateKeyJWTSupported() { + return nil + } + return c.IntrospectionEndpointSigningAlgorithmsSupported() +} + +func RequestObjectSigAlgorithms(c Configuration) []string { + if !c.RequestObjectSupported() { + return nil + } + return c.RequestObjectSigningAlgorithmsSupported() +} diff --git a/pkg/op/op.go b/pkg/op/op.go index 84657cb..c3508bd 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -86,6 +86,7 @@ type Config struct { CodeMethodS256 bool AuthMethodPrivateKeyJWT bool GrantTypeRefreshToken bool + RequestObjectSupported bool SupportedUILocales []language.Tag } @@ -191,6 +192,10 @@ func (o *openidProvider) AuthMethodPrivateKeyJWTSupported() bool { return o.config.AuthMethodPrivateKeyJWT } +func (o *openidProvider) TokenEndpointSigningAlgorithmsSupported() []string { + return []string{"RS256"} +} + func (o *openidProvider) GrantTypeRefreshTokenSupported() bool { return o.config.GrantTypeRefreshToken } @@ -203,6 +208,22 @@ func (o *openidProvider) GrantTypeJWTAuthorizationSupported() bool { return true } +func (o *openidProvider) IntrospectionAuthMethodPrivateKeyJWTSupported() bool { + return true +} + +func (o *openidProvider) IntrospectionEndpointSigningAlgorithmsSupported() []string { + return []string{"RS256"} +} + +func (o *openidProvider) RequestObjectSupported() bool { + return o.config.RequestObjectSupported +} + +func (o *openidProvider) RequestObjectSigningAlgorithmsSupported() []string { + return []string{"RS256"} +} + func (o *openidProvider) SupportedUILocales() []language.Tag { return o.config.SupportedUILocales }