From 8e6dd999123c23a1abde58bf6956b0bb2da77277 Mon Sep 17 00:00:00 2001 From: Vivek Shankar Date: Sat, 27 May 2023 10:40:50 +0800 Subject: [PATCH 01/10] feat: Token exchange handlers --- handler/openid/strategy.go | 5 + handler/rfc8693/access_token_type_handler.go | 219 ++++++++++++++++ .../rfc8693/actor_token_validation_handler.go | 61 +++++ handler/rfc8693/client.go | 13 + handler/rfc8693/config.go | 28 +++ handler/rfc8693/custom_jwt_type_handler.go | 235 ++++++++++++++++++ handler/rfc8693/flow_token_exchange.go | 190 ++++++++++++++ handler/rfc8693/id_token_type_handler.go | 139 +++++++++++ handler/rfc8693/refresh_token_type_handler.go | 194 +++++++++++++++ handler/rfc8693/session.go | 19 ++ handler/rfc8693/storage.go | 23 ++ handler/rfc8693/token_type.go | 23 ++ handler/rfc8693/token_type_jwt.go | 34 +++ 13 files changed, 1183 insertions(+) create mode 100644 handler/rfc8693/access_token_type_handler.go create mode 100644 handler/rfc8693/actor_token_validation_handler.go create mode 100644 handler/rfc8693/client.go create mode 100644 handler/rfc8693/config.go create mode 100644 handler/rfc8693/custom_jwt_type_handler.go create mode 100644 handler/rfc8693/flow_token_exchange.go create mode 100644 handler/rfc8693/id_token_type_handler.go create mode 100644 handler/rfc8693/refresh_token_type_handler.go create mode 100644 handler/rfc8693/session.go create mode 100644 handler/rfc8693/storage.go create mode 100644 handler/rfc8693/token_type.go create mode 100644 handler/rfc8693/token_type_jwt.go diff --git a/handler/openid/strategy.go b/handler/openid/strategy.go index c61738b80..0b48c232e 100644 --- a/handler/openid/strategy.go +++ b/handler/openid/strategy.go @@ -8,8 +8,13 @@ import ( "time" "github.com/ory/fosite" + "github.com/ory/fosite/token/jwt" ) type OpenIDConnectTokenStrategy interface { GenerateIDToken(ctx context.Context, lifespan time.Duration, requester fosite.Requester) (token string, err error) } + +type OpenIDConnectTokenValidationStrategy interface { + ValidateIDToken(ctx context.Context, requester fosite.Requester, token string) (jwt.MapClaims, error) +} diff --git a/handler/rfc8693/access_token_type_handler.go b/handler/rfc8693/access_token_type_handler.go new file mode 100644 index 000000000..0fe5ed195 --- /dev/null +++ b/handler/rfc8693/access_token_type_handler.go @@ -0,0 +1,219 @@ +package rfc8693 + +import ( + "context" + "time" + + "github.com/ory/fosite" + "github.com/ory/fosite/handler/oauth2" + "github.com/ory/fosite/storage" + "github.com/ory/x/errorsx" + "github.com/pkg/errors" +) + +type AccessTokenTypeHandler struct { + Config ConfigProvider + AccessTokenLifespan time.Duration + RefreshTokenLifespan time.Duration + RefreshTokenScopes []string + oauth2.CoreStrategy + ScopeStrategy fosite.ScopeStrategy + Storage +} + +// HandleTokenEndpointRequest implements https://tools.ietf.org/html/rfc6749#section-4.3.2 +func (c *AccessTokenTypeHandler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error { + if !c.CanHandleTokenEndpointRequest(request) { + return errorsx.WithStack(fosite.ErrUnknownRequest) + } + + session, _ := request.GetSession().(Session) + if session == nil { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the session is not of the right type.")) + } + + form := request.GetRequestForm() + if form.Get("subject_token_type") != AccessTokenType && form.Get("actor_token_type") != AccessTokenType { + return nil + } + + if form.Get("actor_token_type") == AccessTokenType { + token := form.Get("actor_token") + if _, unpacked, err := c.validate(ctx, request, token); err != nil { + return err + } else { + session.SetActorToken(unpacked) + } + } + + if form.Get("subject_token_type") == AccessTokenType { + token := form.Get("subject_token") + if subjectTokenSession, unpacked, err := c.validate(ctx, request, token); err != nil { + return err + } else { + session.SetSubjectToken(unpacked) + session.SetSubject(subjectTokenSession.GetSubject()) + } + } + + return nil +} + +// PopulateTokenEndpointResponse implements https://tools.ietf.org/html/rfc6749#section-4.3.3 +func (c *AccessTokenTypeHandler) PopulateTokenEndpointResponse(ctx context.Context, request fosite.AccessRequester, responder fosite.AccessResponder) error { + + if !c.CanHandleTokenEndpointRequest(request) { + return errorsx.WithStack(fosite.ErrUnknownRequest) + } + + session, _ := request.GetSession().(Session) + if session == nil { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the session is not of the right type.")) + } + + form := request.GetRequestForm() + requestedTokenType := form.Get("requested_token_type") + if requestedTokenType == "" { + requestedTokenType = c.Config.GetDefaultRequestedTokenType(ctx) + } + + if requestedTokenType != AccessTokenType { + return nil + } + + if err := c.issue(ctx, request, responder); err != nil { + return err + } + + return nil +} + +// CanSkipClientAuth indicates if client auth can be skipped +func (c *AccessTokenTypeHandler) CanSkipClientAuth(requester fosite.AccessRequester) bool { + return false +} + +// CanHandleTokenEndpointRequest indicates if the token endpoint request can be handled +func (c *AccessTokenTypeHandler) CanHandleTokenEndpointRequest(requester fosite.AccessRequester) bool { + // grant_type REQUIRED. + // Value MUST be set to "password". + return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:token-exchange") +} + +func (c *AccessTokenTypeHandler) validate(ctx context.Context, request fosite.AccessRequester, token string) (fosite.Session, map[string]interface{}, error) { + + session, _ := request.GetSession().(Session) + if session == nil { + return nil, nil, errorsx.WithStack(fosite.ErrServerError.WithDebug( + "Failed to perform token exchange because the session is not of the right type.")) + } + + client := request.GetClient() + + sig := c.CoreStrategy.AccessTokenSignature(ctx, token) + or, err := c.Storage.GetAccessTokenSession(ctx, sig, request.GetSession()) + if err != nil { + return nil, nil, errors.WithStack(fosite.ErrInvalidRequest.WithHint("Token is not valid or has expired.").WithDebug(err.Error())) + } else if err := c.CoreStrategy.ValidateAccessToken(ctx, or, token); err != nil { + return nil, nil, err + } + + subjectTokenClientID := or.GetClient().GetID() + // forbid original subjects client to exchange its own token + if client.GetID() == subjectTokenClientID { + return nil, nil, errors.WithStack(fosite.ErrRequestForbidden.WithHint("Clients are not allowed to perform a token exchange on their own tokens.")) + } + + // Check if the client is allowed to exchange this token + if subjectTokenClient, ok := or.GetClient().(Client); ok { + allowedClientIDs := subjectTokenClient.GetAllowedClientIDsForTokenExchange() + allowed := false + if len(allowedClientIDs) == 0 { + allowed = true + } else { + for _, cid := range allowedClientIDs { + if client.GetID() == cid { + allowed = true + break + } + } + } + + if !allowed { + return nil, nil, errors.WithStack(fosite.ErrRequestForbidden.WithHintf( + "The OAuth 2.0 client is not permitted to exchange a subject token issued to client %s", subjectTokenClientID)) + } + } + + // Scope check + for _, scope := range request.GetRequestedScopes() { + if !c.ScopeStrategy(or.GetGrantedScopes(), scope) { + return nil, nil, errors.WithStack(fosite.ErrInvalidScope.WithHintf("The subject token is not granted \"%s\" and so this scope cannot be requested.", scope)) + } + } + + // Convert to flat session with only access token claims + tokenObject := session.AccessTokenClaimsMap() + tokenObject["client_id"] = or.GetClient().GetID() + tokenObject["scope"] = or.GetGrantedScopes() + tokenObject["aud"] = or.GetGrantedAudience() + + return or.GetSession(), tokenObject, nil +} + +func (c *AccessTokenTypeHandler) issue(ctx context.Context, request fosite.AccessRequester, response fosite.AccessResponder) error { + request.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(c.AccessTokenLifespan)) + + token, signature, err := c.CoreStrategy.GenerateAccessToken(ctx, request) + if err != nil { + return err + } else if err := c.Storage.CreateAccessTokenSession(ctx, signature, request.Sanitize([]string{})); err != nil { + return err + } + + issueRefreshToken := c.canIssueRefreshToken(request) + if issueRefreshToken { + request.GetSession().SetExpiresAt(fosite.RefreshToken, time.Now().UTC().Add(c.RefreshTokenLifespan).Round(time.Second)) + refresh, refreshSignature, err := c.CoreStrategy.GenerateRefreshToken(ctx, request) + if err != nil { + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) + } + + if refreshSignature != "" { + if err := c.Storage.CreateRefreshTokenSession(ctx, refreshSignature, request.Sanitize([]string{})); err != nil { + if rollBackTxnErr := storage.MaybeRollbackTx(ctx, c.Storage); rollBackTxnErr != nil { + err = rollBackTxnErr + } + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) + } + } + + response.SetExtra("refresh_token", refresh) + } + + response.SetAccessToken(token) + response.SetTokenType("bearer") + response.SetExpiresIn(c.getExpiresIn(request, fosite.AccessToken, c.AccessTokenLifespan, time.Now().UTC())) + response.SetScopes(request.GetGrantedScopes()) + + return nil +} + +func (c *AccessTokenTypeHandler) canIssueRefreshToken(request fosite.Requester) bool { + // Require one of the refresh token scopes, if set. + if len(c.RefreshTokenScopes) > 0 && !request.GetGrantedScopes().HasOneOf(c.RefreshTokenScopes...) { + return false + } + // Do not issue a refresh token to clients that cannot use the refresh token grant type. + if !request.GetClient().GetGrantTypes().Has("refresh_token") { + return false + } + return true +} + +func (c *AccessTokenTypeHandler) getExpiresIn(r fosite.Requester, key fosite.TokenType, defaultLifespan time.Duration, now time.Time) time.Duration { + if r.GetSession().GetExpiresAt(key).IsZero() { + return defaultLifespan + } + return time.Duration(r.GetSession().GetExpiresAt(key).UnixNano() - now.UnixNano()) +} diff --git a/handler/rfc8693/actor_token_validation_handler.go b/handler/rfc8693/actor_token_validation_handler.go new file mode 100644 index 000000000..daa86db9a --- /dev/null +++ b/handler/rfc8693/actor_token_validation_handler.go @@ -0,0 +1,61 @@ +package rfc8693 + +import ( + "context" + + "github.com/ory/fosite" + "github.com/ory/x/errorsx" + "github.com/pkg/errors" +) + +type ActorTokenValidationHandler struct{} + +// HandleTokenEndpointRequest implements https://tools.ietf.org/html/rfc6749#section-4.3.2 +func (c *ActorTokenValidationHandler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error { + if !c.CanHandleTokenEndpointRequest(request) { + return errorsx.WithStack(fosite.ErrUnknownRequest) + } + + client := request.GetClient() + session, _ := request.GetSession().(Session) + if session == nil { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the session is not of the right type.")) + } + + // Validate that the actor or client is allowed to make this request + subjectTokenObject := session.GetSubjectToken() + if mayAct, _ := subjectTokenObject["may_act"].(map[string]interface{}); mayAct != nil { + actorTokenObject := session.GetActorToken() + if actorTokenObject == nil { + actorTokenObject = map[string]interface{}{ + "sub": client.GetID(), + "client_id": client.GetID(), + } + } + + for k, v := range mayAct { + if actorTokenObject[k] != v { + return errors.WithStack(fosite.ErrInvalidRequest.WithHint("The actor or client is not authorized to act on behalf of the subject.")) + } + } + } + + return nil +} + +// PopulateTokenEndpointResponse implements https://tools.ietf.org/html/rfc6749#section-4.3.3 +func (c *ActorTokenValidationHandler) PopulateTokenEndpointResponse(ctx context.Context, request fosite.AccessRequester, responder fosite.AccessResponder) error { + return nil +} + +// CanSkipClientAuth indicates if client auth can be skipped +func (c *ActorTokenValidationHandler) CanSkipClientAuth(requester fosite.AccessRequester) bool { + return false +} + +// CanHandleTokenEndpointRequest indicates if the token endpoint request can be handled +func (c *ActorTokenValidationHandler) CanHandleTokenEndpointRequest(requester fosite.AccessRequester) bool { + // grant_type REQUIRED. + // Value MUST be set to "password". + return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:token-exchange") +} diff --git a/handler/rfc8693/client.go b/handler/rfc8693/client.go new file mode 100644 index 000000000..21948e834 --- /dev/null +++ b/handler/rfc8693/client.go @@ -0,0 +1,13 @@ +package rfc8693 + +type Client interface { + // GetSupportedSubjectTokenTypes indicates the token types allowed for subject_token + GetSupportedSubjectTokenTypes() []string + // GetSupportedActorTokenTypes indicates the token types allowed for subject_token + GetSupportedActorTokenTypes() []string + // GetSupportedRequestTokenTypes indicates the token types allowed for requested_token_type + GetSupportedRequestTokenTypes() []string + // GetAllowedClientIDsForTokenExchange indicates the clients that are allowed to + // exchange the subject token for an impersonated or delegated token. + GetAllowedClientIDsForTokenExchange() []string +} diff --git a/handler/rfc8693/config.go b/handler/rfc8693/config.go new file mode 100644 index 000000000..34997480f --- /dev/null +++ b/handler/rfc8693/config.go @@ -0,0 +1,28 @@ +package rfc8693 + +import ( + "context" + "time" +) + +const ( + // AccessTokenType is the access token type issued by the same provider + AccessTokenType string = "urn:ietf:params:oauth:token-type:access_token" + // RefreshTokenType is the refresh token type issued by the same provider + RefreshTokenType string = "urn:ietf:params:oauth:token-type:refresh_token" + // IDTokenType is the id_token type issued by the same provider + IDTokenType string = "urn:ietf:params:oauth:token-type:id_token" + // JWTTokenType is the JWT type that may be issued by a different provider + JWTTokenType string = "urn:ietf:params:oauth:token-type:jwt" +) + +type ConfigProvider interface { + GetTokenTypes(ctx context.Context) map[string]TokenType + + GetDefaultRequestedTokenType(ctx context.Context) string + + GetIssuer(ctx context.Context) string + + // GetIDTokenLifespan returns the ID token lifespan. + GetIDTokenLifespan(ctx context.Context) time.Duration +} diff --git a/handler/rfc8693/custom_jwt_type_handler.go b/handler/rfc8693/custom_jwt_type_handler.go new file mode 100644 index 000000000..32985ffe5 --- /dev/null +++ b/handler/rfc8693/custom_jwt_type_handler.go @@ -0,0 +1,235 @@ +package rfc8693 + +import ( + "context" + "encoding/json" + "time" + + "github.com/google/uuid" + "github.com/ory/fosite" + "github.com/ory/fosite/handler/openid" + "github.com/ory/fosite/token/jwt" + "github.com/ory/x/errorsx" +) + +type CustomJWTTypeHandler struct { + Config ConfigProvider + JWTStrategy jwt.Signer + Storage +} + +// HandleTokenEndpointRequest implements https://tools.ietf.org/html/rfc6749#section-4.3.2 +func (c *CustomJWTTypeHandler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error { + if !c.CanHandleTokenEndpointRequest(request) { + return errorsx.WithStack(fosite.ErrUnknownRequest) + } + + session, _ := request.GetSession().(Session) + if session == nil { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the session is not of the right type.")) + } + + form := request.GetRequestForm() + tokenTypes := c.Config.GetTokenTypes(ctx) + actorTokenType := tokenTypes[form.Get("actor_token_type")] + subjectTokenType := tokenTypes[form.Get("subject_token_type")] + if actorTokenType != nil && actorTokenType.GetType(ctx) == JWTTokenType { + token := form.Get("actor_token") + if unpacked, err := c.validate(ctx, request, actorTokenType, token); err != nil { + return err + } else { + session.SetActorToken(unpacked) + } + } + + if subjectTokenType != nil && subjectTokenType.GetType(ctx) == JWTTokenType { + token := form.Get("subject_token") + if unpacked, err := c.validate(ctx, request, subjectTokenType, token); err != nil { + return err + } else { + session.SetSubjectToken(unpacked) + // Get the subject and populate session + if subject, err := c.Storage.GetSubjectForTokenExchange(ctx, request); err != nil { + return err + } else { + session.SetSubject(subject) + } + } + } + + return nil +} + +// PopulateTokenEndpointResponse implements https://tools.ietf.org/html/rfc6749#section-4.3.3 +func (c *CustomJWTTypeHandler) PopulateTokenEndpointResponse(ctx context.Context, request fosite.AccessRequester, responder fosite.AccessResponder) error { + + if !c.CanHandleTokenEndpointRequest(request) { + return errorsx.WithStack(fosite.ErrUnknownRequest) + } + + session, _ := request.GetSession().(Session) + if session == nil { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the session is not of the right type.")) + } + + form := request.GetRequestForm() + requestedTokenType := form.Get("requested_token_type") + if requestedTokenType == "" { + requestedTokenType = c.Config.GetDefaultRequestedTokenType(ctx) + } + + tokenTypes := c.Config.GetTokenTypes(ctx) + tokenType := tokenTypes[requestedTokenType] + if tokenType == nil || tokenType.GetType(ctx) != JWTTokenType { + return nil + } + + if err := c.issue(ctx, request, tokenType, responder); err != nil { + return err + } + + return nil +} + +// CanSkipClientAuth indicates if client auth can be skipped +func (c *CustomJWTTypeHandler) CanSkipClientAuth(requester fosite.AccessRequester) bool { + return false +} + +// CanHandleTokenEndpointRequest indicates if the token endpoint request can be handled +func (c *CustomJWTTypeHandler) CanHandleTokenEndpointRequest(requester fosite.AccessRequester) bool { + // grant_type REQUIRED. + // Value MUST be set to "password". + return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:token-exchange") +} + +func (c *CustomJWTTypeHandler) validate(ctx context.Context, request fosite.AccessRequester, tokenType TokenType, token string) (map[string]interface{}, error) { + + jwtType, _ := tokenType.(*JWTType) + if jwtType == nil { + return nil, errorsx.WithStack( + fosite.ErrServerError.WithDebugf( + "Token type '%s' is supposed to be of type JWT but is not castable to 'JWTType'", tokenType.GetName(ctx))) + } + + // Parse the token + ftoken, err := jwt.ParseWithClaims(token, jwt.MapClaims{}, jwtType.ValidateFunc) + if err != nil { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("Unable to parse the JSON web token").WithWrap(err).WithDebug(err.Error())) + } + + window := jwtType.JWTLifetimeToleranceWindow + if window == 0 { + window = 1 * time.Hour + } + claims := ftoken.Claims + + if issued, exists := claims["iat"]; exists { + if time.Unix(toInt64(issued), 0).Add(window).Before(time.Now()) { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("Claim 'iat' from token is too far in the past.")) + } + } + + if _, exists := claims["exp"]; !exists { // Validate 'exp' is mandatory + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("Claim 'exp' from token is missing.")) + } + expiry := toInt64(claims["exp"]) + if time.Now().Add(window).Before(time.Unix(expiry, 0)) { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("Claim 'exp' from token is too far in the future.")) + } + + if !claims.VerifyIssuer(jwtType.Issuer, true) { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("Claim 'iss' from token must match the '%s'.", jwtType.Issuer)) + } + + // Validate the JTI is unique if required + if jwtType.ValidateJTI { + jti, _ := claims["jti"].(string) + if jti == "" { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("Claim 'jti' from token is missing.")) + } + + if c.Storage.SetTokenExchangeCustomJWT(ctx, jti, time.Unix(expiry, 0)) != nil { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("Claim 'jti' from the token must be used only once.")) + } + } + + return map[string]interface{}(claims), nil +} + +func (c *CustomJWTTypeHandler) issue(ctx context.Context, request fosite.AccessRequester, tokenType TokenType, response fosite.AccessResponder) error { + jwtType, _ := tokenType.(*JWTType) + if jwtType == nil { + return errorsx.WithStack( + fosite.ErrServerError.WithDebugf( + "Token type '%s' is supposed to be of type JWT but is not castable to 'JWTType'", tokenType.GetName(ctx))) + } + + sess, ok := request.GetSession().(openid.Session) + if !ok { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to generate JWT because session must be of type fosite/handler/openid.Session.")) + } + + claims := sess.IDTokenClaims() + if claims.Subject == "" { + claims.Subject = request.GetClient().GetID() + } + + if claims.ExpiresAt.IsZero() { + claims.ExpiresAt = time.Now().UTC().Add(jwtType.Expiry) + } + + if claims.Issuer == "" { + claims.Issuer = jwtType.Issuer + } + + if len(request.GetRequestedAudience()) > 0 { + claims.Audience = append(claims.Audience, request.GetRequestedAudience()...) + } + + if len(claims.Audience) == 0 { + aud := jwtType.JWTIssueConfig.Audience + if len(aud) == 0 { + aud = append(aud, request.GetClient().GetID()) + } + + claims.Audience = append(claims.Audience, aud...) + } + + if claims.JTI == "" { + claims.JTI = uuid.New().String() + } + + claims.IssuedAt = time.Now().UTC() + + token, _, err := c.JWTStrategy.Generate(ctx, claims.ToMapClaims(), sess.IDTokenHeaders()) + if err != nil { + return err + } + + response.SetAccessToken(token) + response.SetTokenType("N_A") + response.SetExpiresIn(time.Duration(claims.ExpiresAt.UnixNano() - time.Now().UTC().UnixNano())) + return nil +} + +// type conversion according to jwt.MapClaims.toInt64 - ignore error +func toInt64(claim interface{}) int64 { + switch t := claim.(type) { + case float64: + return int64(t) + case int64: + return t + case json.Number: + v, err := t.Int64() + if err == nil { + return v + } + vf, err := t.Float64() + if err != nil { + return 0 + } + return int64(vf) + } + return 0 +} diff --git a/handler/rfc8693/flow_token_exchange.go b/handler/rfc8693/flow_token_exchange.go new file mode 100644 index 000000000..786c62938 --- /dev/null +++ b/handler/rfc8693/flow_token_exchange.go @@ -0,0 +1,190 @@ +package rfc8693 + +import ( + "context" + + "github.com/ory/fosite" + "github.com/ory/x/errorsx" + "github.com/pkg/errors" +) + +// TokenExchangeGrantHandler is the grant handler for RFC8693 +type TokenExchangeGrantHandler struct { + Config ConfigProvider + ScopeStrategy fosite.ScopeStrategy + AudienceMatchingStrategy fosite.AudienceMatchingStrategy +} + +// HandleTokenEndpointRequest implements https://tools.ietf.org/html/rfc6749#section-4.3.2 +func (c *TokenExchangeGrantHandler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error { + if !c.CanHandleTokenEndpointRequest(request) { + return errorsx.WithStack(fosite.ErrUnknownRequest) + } + + client := request.GetClient() + if client.IsPublic() { + return errors.WithStack(fosite.ErrInvalidGrant.WithHint("The OAuth 2.0 Client is marked as public and is thus not allowed to use authorization grant \"urn:ietf:params:oauth:grant-type:token-exchange\".")) + } + + // Check whether client is allowed to use token exchange + if !client.GetGrantTypes().Has("urn:ietf:params:oauth:grant-type:token-exchange") { + return errors.WithStack(fosite.ErrUnauthorizedClient.WithHintf( + "The OAuth 2.0 Client is not allowed to use authorization grant \"%s\".", "urn:ietf:params:oauth:grant-type:token-exchange")) + } + + session, _ := request.GetSession().(Session) + if session == nil { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the session is not of the right type.")) + } + + form := request.GetRequestForm() + configTypesSupported := c.Config.GetTokenTypes(ctx) + var supportedSubjectTypes, supportedActorTypes, supportedRequestTypes fosite.Arguments + if teClient, ok := client.(Client); ok { + supportedRequestTypes = fosite.Arguments(teClient.GetSupportedRequestTokenTypes()) + supportedActorTypes = fosite.Arguments(teClient.GetSupportedActorTokenTypes()) + supportedSubjectTypes = fosite.Arguments(teClient.GetSupportedSubjectTokenTypes()) + } + + // From https://tools.ietf.org/html/rfc8693#section-2.1: + // + // subject_token + // REQUIRED. A security token that represents the identity of the + // party on behalf of whom the request is being made. Typically, the + // subject of this token will be the subject of the security token + // issued in response to the request. + subjectToken := form.Get("subject_token") + if subjectToken == "" { + return errors.WithStack(fosite.ErrInvalidRequest.WithHintf("Mandatory parameter \"%s\" is missing.", "subject_token")) + } + + // From https://tools.ietf.org/html/rfc8693#section-2.1: + // + // subject_token_type + // REQUIRED. An identifier, as described in Section 3, that + // indicates the type of the security token in the "subject_token" + // parameter. + subjectTokenType := form.Get("subject_token_type") + if subjectTokenType == "" { + return errors.WithStack(fosite.ErrInvalidRequest.WithHintf("Mandatory parameter \"%s\" is missing.", "subject_token_type")) + } + + if tt := configTypesSupported[subjectTokenType]; tt == nil { + return errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("\"%s\" token type is not supported as a \"%s\".", subjectTokenType, "subject_token_type")) + } + + if len(supportedSubjectTypes) > 0 && !supportedSubjectTypes.Has(subjectTokenType) { + return errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf( + "The OAuth 2.0 client is not allowed to use \"%s\" as \"%s\".", subjectTokenType, "subject_token_type")) + } + + // From https://tools.ietf.org/html/rfc8693#section-2.1: + // + // actor_token + // OPTIONAL . A security token that represents the identity of the acting party. + // Typically, this will be the party that is authorized to use the requested security + // token and act on behalf of the subject. + actorToken := form.Get("actor_token") + actorTokenType := form.Get("actor_token_type") + if actorToken != "" { + // From https://tools.ietf.org/html/rfc8693#section-2.1: + // + // actor_token_type + // An identifier, as described in Section 3, that indicates the type of the security token + // in the actor_token parameter. This is REQUIRED when the actor_token parameter is present + // in the request but MUST NOT be included otherwise. + if actorTokenType == "" { + return errors.WithStack(fosite.ErrInvalidRequest.WithHintf("\"actor_token_type\" is empty even though the \"actor_token\" is not empty.")) + } + + if tt := configTypesSupported[actorTokenType]; tt == nil { + return errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf( + "\"%s\" token type is not supported as a \"%s\".", actorTokenType, "actor_token_type")) + } + + if len(supportedActorTypes) > 0 && !supportedActorTypes.Has(actorTokenType) { + return errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf( + "The OAuth 2.0 client is not allowed to use \"%s\" as \"%s\".", actorTokenType, "actor_token_type")) + } + } else if actorTokenType != "" { + return errors.WithStack(fosite.ErrInvalidRequest.WithHintf("\"actor_token_type\" is not empty even though the \"actor_token\" is empty.")) + } + + // check if supported + requestedTokenType := form.Get("requested_token_type") + if requestedTokenType == "" { + requestedTokenType = c.Config.GetDefaultRequestedTokenType(ctx) + } + + if tt := configTypesSupported[requestedTokenType]; tt == nil { + return errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf( + "\"%s\" token type is not supported as a \"%s\".", requestedTokenType, "requested_token_type")) + } + + if len(supportedRequestTypes) > 0 && !supportedRequestTypes.Has(requestedTokenType) { + return errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("The OAuth 2.0 client is not allowed to use \"%s\" as \"%s\".", requestedTokenType, "requested_token_type")) + } + + // Check scope + for _, scope := range request.GetRequestedScopes() { + if !c.ScopeStrategy(client.GetScopes(), scope) { + return errors.WithStack(fosite.ErrInvalidScope.WithHintf("The OAuth 2.0 Client is not allowed to request scope '%s'.", scope)) + } + } + + // Check audience + if err := c.AudienceMatchingStrategy(client.GetAudience(), request.GetRequestedAudience()); err != nil { + // TODO: Need to convert to using invalid_target + return err + } + + return nil +} + +// PopulateTokenEndpointResponse implements https://tools.ietf.org/html/rfc6749#section-4.3.3 +func (c *TokenExchangeGrantHandler) PopulateTokenEndpointResponse(ctx context.Context, request fosite.AccessRequester, responder fosite.AccessResponder) error { + if !c.CanHandleTokenEndpointRequest(request) { + return errorsx.WithStack(fosite.ErrUnknownRequest) + } + + session, _ := request.GetSession().(Session) + if session == nil { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the session is not of the right type.")) + } + + form := request.GetRequestForm() + requestedTokenType := form.Get("requested_token_type") + if requestedTokenType == "" { + requestedTokenType = c.Config.GetDefaultRequestedTokenType(ctx) + } + + configTypesSupported := c.Config.GetTokenTypes(ctx) + if tt := configTypesSupported[requestedTokenType]; tt == nil { + return errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf( + "\"%s\" token type is not supported as a \"%s\".", requestedTokenType, "requested_token_type")) + } + + // chain `act` if necessary + subjectTokenObject := session.GetSubjectToken() + if mayAct, _ := subjectTokenObject["may_act"].(map[string]interface{}); mayAct != nil { + if subjectActor, _ := subjectTokenObject["act"].(map[string]interface{}); subjectActor != nil { + mayAct["act"] = subjectActor + } + + session.SetAct(mayAct) + } + + return nil +} + +// CanSkipClientAuth indicates if client auth can be skipped +func (c *TokenExchangeGrantHandler) CanSkipClientAuth(requester fosite.AccessRequester) bool { + return false +} + +// CanHandleTokenEndpointRequest indicates if the token endpoint request can be handled +func (c *TokenExchangeGrantHandler) CanHandleTokenEndpointRequest(requester fosite.AccessRequester) bool { + // grant_type REQUIRED. + // Value MUST be set to "password". + return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:token-exchange") +} diff --git a/handler/rfc8693/id_token_type_handler.go b/handler/rfc8693/id_token_type_handler.go new file mode 100644 index 000000000..4cb1ea964 --- /dev/null +++ b/handler/rfc8693/id_token_type_handler.go @@ -0,0 +1,139 @@ +package rfc8693 + +import ( + "context" + + "github.com/ory/fosite" + "github.com/ory/fosite/handler/openid" + "github.com/ory/fosite/token/jwt" + "github.com/ory/x/errorsx" +) + +type IDTokenTypeHandler struct { + Config ConfigProvider + JWTStrategy jwt.Signer + IssueStrategy openid.OpenIDConnectTokenStrategy + ValidationStrategy openid.OpenIDConnectTokenValidationStrategy + Storage +} + +// HandleTokenEndpointRequest implements https://tools.ietf.org/html/rfc6749#section-4.3.2 +func (c *IDTokenTypeHandler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error { + if !c.CanHandleTokenEndpointRequest(request) { + return errorsx.WithStack(fosite.ErrUnknownRequest) + } + + session, _ := request.GetSession().(Session) + if session == nil { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the session is not of the right type.")) + } + + form := request.GetRequestForm() + if form.Get("subject_token_type") != IDTokenType && form.Get("actor_token_type") != IDTokenType { + return nil + } + + if form.Get("actor_token_type") == IDTokenType { + token := form.Get("actor_token") + if unpacked, err := c.validate(ctx, request, token); err != nil { + return err + } else { + session.SetActorToken(unpacked) + } + } + + if form.Get("subject_token_type") == IDTokenType { + token := form.Get("subject_token") + if unpacked, err := c.validate(ctx, request, token); err != nil { + return err + } else { + // Get the subject and populate session + session.SetSubject(unpacked["sub"].(string)) + session.SetSubjectToken(unpacked) + } + } + + return nil +} + +// PopulateTokenEndpointResponse implements https://tools.ietf.org/html/rfc6749#section-4.3.3 +func (c *IDTokenTypeHandler) PopulateTokenEndpointResponse(ctx context.Context, request fosite.AccessRequester, responder fosite.AccessResponder) error { + if !c.CanHandleTokenEndpointRequest(request) { + return errorsx.WithStack(fosite.ErrUnknownRequest) + } + + session, _ := request.GetSession().(Session) + if session == nil { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the session is not of the right type.")) + } + + form := request.GetRequestForm() + requestedTokenType := form.Get("requested_token_type") + if requestedTokenType == "" { + requestedTokenType = c.Config.GetDefaultRequestedTokenType(ctx) + } + + if requestedTokenType != IDTokenType { + return nil + } + + if err := c.issue(ctx, request, responder); err != nil { + return err + } + + return nil +} + +// CanSkipClientAuth indicates if client auth can be skipped +func (c *IDTokenTypeHandler) CanSkipClientAuth(requester fosite.AccessRequester) bool { + return false +} + +// CanHandleTokenEndpointRequest indicates if the token endpoint request can be handled +func (c *IDTokenTypeHandler) CanHandleTokenEndpointRequest(requester fosite.AccessRequester) bool { + // grant_type REQUIRED. + // Value MUST be set to "password". + return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:token-exchange") +} + +func (c *IDTokenTypeHandler) validate(ctx context.Context, request fosite.AccessRequester, token string) (map[string]interface{}, error) { + + claims, err := c.ValidationStrategy.ValidateIDToken(ctx, request, token) + if err != nil { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("Unable to parse the id_token").WithWrap(err).WithDebug(err.Error())) + } + + expectedIssuer := c.Config.GetIssuer(ctx) + if !claims.VerifyIssuer(expectedIssuer, true) { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("Claim 'iss' from token must match the '%s'.", expectedIssuer)) + } + + if _, ok := claims["sub"].(string); !ok { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("Claim 'sub' is missing.")) + } + + return map[string]interface{}(claims), nil +} + +func (c *IDTokenTypeHandler) issue(ctx context.Context, request fosite.AccessRequester, response fosite.AccessResponder) error { + sess, ok := request.GetSession().(openid.Session) + if !ok { + return errorsx.WithStack(fosite.ErrServerError.WithDebug( + "Failed to generate id token because session must be of type fosite/handler/openid.Session.")) + } + + claims := sess.IDTokenClaims() + if claims.Subject == "" { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to generate id token because subject is an empty string.")) + } + + token, err := c.IssueStrategy.GenerateIDToken(ctx, c.Config.GetIDTokenLifespan(ctx), request) + if err != nil { + return err + } + + response.SetAccessToken(token) + response.SetTokenType("N_A") + + return nil +} diff --git a/handler/rfc8693/refresh_token_type_handler.go b/handler/rfc8693/refresh_token_type_handler.go new file mode 100644 index 000000000..a4a89901d --- /dev/null +++ b/handler/rfc8693/refresh_token_type_handler.go @@ -0,0 +1,194 @@ +package rfc8693 + +import ( + "context" + "time" + + "github.com/ory/fosite" + "github.com/ory/fosite/handler/oauth2" + "github.com/ory/fosite/storage" + "github.com/ory/x/errorsx" + "github.com/pkg/errors" +) + +type RefreshTokenTypeHandler struct { + Config ConfigProvider + RefreshTokenLifespan time.Duration + RefreshTokenScopes []string + oauth2.CoreStrategy + ScopeStrategy fosite.ScopeStrategy + Storage +} + +// HandleTokenEndpointRequest implements https://tools.ietf.org/html/rfc6749#section-4.3.2 +func (c *RefreshTokenTypeHandler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error { + if !c.CanHandleTokenEndpointRequest(request) { + return errorsx.WithStack(fosite.ErrUnknownRequest) + } + + session, _ := request.GetSession().(Session) + if session == nil { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the session is not of the right type.")) + } + + form := request.GetRequestForm() + if form.Get("subject_token_type") != RefreshTokenType && form.Get("actor_token_type") != RefreshTokenType { + return nil + } + + if form.Get("actor_token_type") == RefreshTokenType { + token := form.Get("actor_token") + if _, unpacked, err := c.validate(ctx, request, token); err != nil { + return err + } else { + session.SetActorToken(unpacked) + } + } + + if form.Get("subject_token_type") == RefreshTokenType { + token := form.Get("subject_token") + if subjectTokenSession, unpacked, err := c.validate(ctx, request, token); err != nil { + return err + } else { + session.SetSubjectToken(unpacked) + session.SetSubject(subjectTokenSession.GetSubject()) + } + } + + return nil +} + +// PopulateTokenEndpointResponse implements https://tools.ietf.org/html/rfc6749#section-4.3.3 +func (c *RefreshTokenTypeHandler) PopulateTokenEndpointResponse(ctx context.Context, request fosite.AccessRequester, responder fosite.AccessResponder) error { + + if !c.CanHandleTokenEndpointRequest(request) { + return errorsx.WithStack(fosite.ErrUnknownRequest) + } + + session, _ := request.GetSession().(Session) + if session == nil { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the session is not of the right type.")) + } + + form := request.GetRequestForm() + requestedTokenType := form.Get("requested_token_type") + if requestedTokenType == "" { + requestedTokenType = c.Config.GetDefaultRequestedTokenType(ctx) + } + + if requestedTokenType != RefreshTokenType { + return nil + } + + if err := c.issue(ctx, request, responder); err != nil { + return err + } + + return nil +} + +// CanSkipClientAuth indicates if client auth can be skipped +func (c *RefreshTokenTypeHandler) CanSkipClientAuth(requester fosite.AccessRequester) bool { + return false +} + +// CanHandleTokenEndpointRequest indicates if the token endpoint request can be handled +func (c *RefreshTokenTypeHandler) CanHandleTokenEndpointRequest(requester fosite.AccessRequester) bool { + // grant_type REQUIRED. + // Value MUST be set to "password". + return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:token-exchange") +} + +func (c *RefreshTokenTypeHandler) validate(ctx context.Context, request fosite.AccessRequester, token string) ( + fosite.Session, map[string]interface{}, error) { + + session, _ := request.GetSession().(Session) + if session == nil { + return nil, nil, errorsx.WithStack(fosite.ErrServerError.WithDebug( + "Failed to perform token exchange because the session is not of the right type.")) + } + + client := request.GetClient() + + sig := c.CoreStrategy.RefreshTokenSignature(ctx, token) + or, err := c.Storage.GetRefreshTokenSession(ctx, sig, request.GetSession()) + if err != nil { + return nil, nil, errors.WithStack(fosite.ErrInvalidRequest.WithHint("Token is not valid or has expired.").WithDebug(err.Error())) + } else if err := c.CoreStrategy.ValidateRefreshToken(ctx, or, token); err != nil { + return nil, nil, err + } + + tokenClientID := or.GetClient().GetID() + // forbid original subjects client to exchange its own token + if client.GetID() == tokenClientID { + return nil, nil, errors.WithStack( + fosite.ErrRequestForbidden.WithHint("Clients are not allowed to perform a token exchange on their own tokens.")) + } + + // Check if the client is allowed to exchange this token + if subjectTokenClient, ok := or.GetClient().(Client); ok { + allowedClientIDs := subjectTokenClient.GetAllowedClientIDsForTokenExchange() + allowed := false + if len(allowedClientIDs) == 0 { + allowed = true + } else { + for _, cid := range allowedClientIDs { + if client.GetID() == cid { + allowed = true + break + } + } + } + + if !allowed { + return nil, nil, errors.WithStack(fosite.ErrRequestForbidden.WithHintf( + "The OAuth 2.0 client is not permitted to exchange a subject token issued to client %s", tokenClientID)) + } + } + + // Scope check + for _, scope := range request.GetRequestedScopes() { + if !c.ScopeStrategy(or.GetGrantedScopes(), scope) { + return nil, nil, errors.WithStack(fosite.ErrInvalidScope.WithHintf("The subject token is not granted \"%s\" and so this scope cannot be requested.", scope)) + } + } + + // Convert to flat session with only access token claims + tokenObject := session.AccessTokenClaimsMap() + tokenObject["client_id"] = or.GetClient().GetID() + tokenObject["scope"] = or.GetGrantedScopes() + tokenObject["aud"] = or.GetGrantedAudience() + + return or.GetSession(), tokenObject, nil +} + +func (c *RefreshTokenTypeHandler) issue(ctx context.Context, request fosite.AccessRequester, response fosite.AccessResponder) error { + request.GetSession().SetExpiresAt(fosite.RefreshToken, time.Now().UTC().Add(c.RefreshTokenLifespan).Round(time.Second)) + refresh, refreshSignature, err := c.CoreStrategy.GenerateRefreshToken(ctx, request) + if err != nil { + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) + } + + if refreshSignature != "" { + if err := c.Storage.CreateRefreshTokenSession(ctx, refreshSignature, request.Sanitize([]string{})); err != nil { + if rollBackTxnErr := storage.MaybeRollbackTx(ctx, c.Storage); rollBackTxnErr != nil { + err = rollBackTxnErr + } + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) + } + } + + response.SetAccessToken(refresh) + response.SetTokenType("N_A") + response.SetExpiresIn(c.getExpiresIn(request, fosite.RefreshToken, c.RefreshTokenLifespan, time.Now().UTC())) + response.SetScopes(request.GetGrantedScopes()) + + return nil +} + +func (c *RefreshTokenTypeHandler) getExpiresIn(r fosite.Requester, key fosite.TokenType, defaultLifespan time.Duration, now time.Time) time.Duration { + if r.GetSession().GetExpiresAt(key).IsZero() { + return defaultLifespan + } + return time.Duration(r.GetSession().GetExpiresAt(key).UnixNano() - now.UnixNano()) +} diff --git a/handler/rfc8693/session.go b/handler/rfc8693/session.go new file mode 100644 index 000000000..8de14514d --- /dev/null +++ b/handler/rfc8693/session.go @@ -0,0 +1,19 @@ +package rfc8693 + +// Session is required to support token exchange +type Session interface { + // SetSubject sets the session's subject. + SetSubject(subject string) + + SetActorToken(token map[string]interface{}) + + GetActorToken() map[string]interface{} + + SetSubjectToken(token map[string]interface{}) + + GetSubjectToken() map[string]interface{} + + SetAct(act map[string]interface{}) + + AccessTokenClaimsMap() map[string]interface{} +} diff --git a/handler/rfc8693/storage.go b/handler/rfc8693/storage.go new file mode 100644 index 000000000..d2b110498 --- /dev/null +++ b/handler/rfc8693/storage.go @@ -0,0 +1,23 @@ +package rfc8693 + +import ( + "context" + "time" + + "github.com/ory/fosite" + "github.com/ory/fosite/handler/oauth2" +) + +type Storage interface { + oauth2.CoreStorage + + // SetTokenExchangeCustomJWT marks a JTI as known for the given + // expiry time. It should atomically check if the JTI + // already exists and fail the request, if found. + SetTokenExchangeCustomJWT(ctx context.Context, jti string, exp time.Time) error + + // GetSubjectForTokenExchange computes the session subject and is used for token types where there is no way + // to know the subject value. For some token types, such as access and refresh tokens, the subject is well-defined + // and this function is not called. + GetSubjectForTokenExchange(ctx context.Context, requester fosite.Requester) (string, error) +} diff --git a/handler/rfc8693/token_type.go b/handler/rfc8693/token_type.go new file mode 100644 index 000000000..cf72b59b6 --- /dev/null +++ b/handler/rfc8693/token_type.go @@ -0,0 +1,23 @@ +package rfc8693 + +import ( + "context" +) + +type TokenType interface { + GetName(ctx context.Context) string + + GetType(ctx context.Context) string +} + +type DefaultTokenType struct { + Name string +} + +func (c *DefaultTokenType) GetName(ctx context.Context) string { + return c.Name +} + +func (c *DefaultTokenType) GetType(ctx context.Context) string { + return c.Name +} diff --git a/handler/rfc8693/token_type_jwt.go b/handler/rfc8693/token_type_jwt.go new file mode 100644 index 000000000..561843dc8 --- /dev/null +++ b/handler/rfc8693/token_type_jwt.go @@ -0,0 +1,34 @@ +package rfc8693 + +import ( + "context" + "time" + + "github.com/ory/fosite/token/jwt" +) + +type JWTType struct { + Name string `json:"name"` + Issuer string `json:"iss"` + JWTValidationConfig `json:"validate"` + JWTIssueConfig `json:"issue"` +} + +type JWTIssueConfig struct { + Audience []string `json:"aud"` + Expiry time.Duration `json:"exp"` +} + +type JWTValidationConfig struct { + ValidateJTI bool `json:"validate_jti"` + JWTLifetimeToleranceWindow time.Duration `json:"tolerance_window"` + ValidateFunc jwt.Keyfunc `json:"-"` +} + +func (c *JWTType) GetName(ctx context.Context) string { + return c.Name +} + +func (c *JWTType) GetType(ctx context.Context) string { + return JWTTokenType +} From 1d26736fe8f8514971ce1583a6d97c5c31e717d7 Mon Sep 17 00:00:00 2001 From: Vivek Shankar Date: Sat, 27 May 2023 18:28:01 +0800 Subject: [PATCH 02/10] fix: Add session functions and tests --- config.go | 6 + config_default.go | 12 + handler/rfc8693/access_token_type_handler.go | 10 +- handler/rfc8693/config.go | 28 -- handler/rfc8693/custom_jwt_type_handler.go | 8 +- handler/rfc8693/flow_token_exchange.go | 11 +- handler/rfc8693/id_token_type_handler.go | 12 +- handler/rfc8693/refresh_token_type_handler.go | 2 +- handler/rfc8693/session.go | 43 +++ handler/rfc8693/storage.go | 2 +- handler/rfc8693/token_exchange_test.go | 293 ++++++++++++++++++ handler/rfc8693/token_type.go | 15 +- oauth2.go | 10 +- storage/memory.go | 19 +- 14 files changed, 415 insertions(+), 56 deletions(-) delete mode 100644 handler/rfc8693/config.go create mode 100644 handler/rfc8693/token_exchange_test.go diff --git a/config.go b/config.go index 1b50eb70c..50ed58087 100644 --- a/config.go +++ b/config.go @@ -300,3 +300,9 @@ type PushedAuthorizeRequestConfigProvider interface { // must contain the PAR request_uri. EnforcePushedAuthorize(ctx context.Context) bool } + +type RFC8693ConfigProvider interface { + GetTokenTypes(ctx context.Context) map[string]RFC8693TokenType + + GetDefaultRequestedTokenType(ctx context.Context) string +} diff --git a/config_default.go b/config_default.go index 7f2e2487e..c0b2911e2 100644 --- a/config_default.go +++ b/config_default.go @@ -212,6 +212,10 @@ type Config struct { // IsPushedAuthorizeEnforced enforces pushed authorization request for /authorize IsPushedAuthorizeEnforced bool + + RFC8693TokenTypes map[string]RFC8693TokenType + + DefaultRequestedTokenType string } func (c *Config) GetGlobalSecret(ctx context.Context) ([]byte, error) { @@ -488,3 +492,11 @@ func (c *Config) GetPushedAuthorizeContextLifespan(ctx context.Context) time.Dur func (c *Config) EnforcePushedAuthorize(ctx context.Context) bool { return c.IsPushedAuthorizeEnforced } + +func (c *Config) GetTokenTypes(ctx context.Context) map[string]RFC8693TokenType { + return c.RFC8693TokenTypes +} + +func (c *Config) GetDefaultRequestedTokenType(ctx context.Context) string { + return c.DefaultRequestedTokenType +} diff --git a/handler/rfc8693/access_token_type_handler.go b/handler/rfc8693/access_token_type_handler.go index 0fe5ed195..5fba5b83d 100644 --- a/handler/rfc8693/access_token_type_handler.go +++ b/handler/rfc8693/access_token_type_handler.go @@ -12,7 +12,7 @@ import ( ) type AccessTokenTypeHandler struct { - Config ConfigProvider + Config fosite.RFC8693ConfigProvider AccessTokenLifespan time.Duration RefreshTokenLifespan time.Duration RefreshTokenScopes []string @@ -23,7 +23,7 @@ type AccessTokenTypeHandler struct { // HandleTokenEndpointRequest implements https://tools.ietf.org/html/rfc6749#section-4.3.2 func (c *AccessTokenTypeHandler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error { - if !c.CanHandleTokenEndpointRequest(request) { + if !c.CanHandleTokenEndpointRequest(ctx, request) { return errorsx.WithStack(fosite.ErrUnknownRequest) } @@ -62,7 +62,7 @@ func (c *AccessTokenTypeHandler) HandleTokenEndpointRequest(ctx context.Context, // PopulateTokenEndpointResponse implements https://tools.ietf.org/html/rfc6749#section-4.3.3 func (c *AccessTokenTypeHandler) PopulateTokenEndpointResponse(ctx context.Context, request fosite.AccessRequester, responder fosite.AccessResponder) error { - if !c.CanHandleTokenEndpointRequest(request) { + if !c.CanHandleTokenEndpointRequest(ctx, request) { return errorsx.WithStack(fosite.ErrUnknownRequest) } @@ -89,12 +89,12 @@ func (c *AccessTokenTypeHandler) PopulateTokenEndpointResponse(ctx context.Conte } // CanSkipClientAuth indicates if client auth can be skipped -func (c *AccessTokenTypeHandler) CanSkipClientAuth(requester fosite.AccessRequester) bool { +func (c *AccessTokenTypeHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { return false } // CanHandleTokenEndpointRequest indicates if the token endpoint request can be handled -func (c *AccessTokenTypeHandler) CanHandleTokenEndpointRequest(requester fosite.AccessRequester) bool { +func (c *AccessTokenTypeHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { // grant_type REQUIRED. // Value MUST be set to "password". return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:token-exchange") diff --git a/handler/rfc8693/config.go b/handler/rfc8693/config.go deleted file mode 100644 index 34997480f..000000000 --- a/handler/rfc8693/config.go +++ /dev/null @@ -1,28 +0,0 @@ -package rfc8693 - -import ( - "context" - "time" -) - -const ( - // AccessTokenType is the access token type issued by the same provider - AccessTokenType string = "urn:ietf:params:oauth:token-type:access_token" - // RefreshTokenType is the refresh token type issued by the same provider - RefreshTokenType string = "urn:ietf:params:oauth:token-type:refresh_token" - // IDTokenType is the id_token type issued by the same provider - IDTokenType string = "urn:ietf:params:oauth:token-type:id_token" - // JWTTokenType is the JWT type that may be issued by a different provider - JWTTokenType string = "urn:ietf:params:oauth:token-type:jwt" -) - -type ConfigProvider interface { - GetTokenTypes(ctx context.Context) map[string]TokenType - - GetDefaultRequestedTokenType(ctx context.Context) string - - GetIssuer(ctx context.Context) string - - // GetIDTokenLifespan returns the ID token lifespan. - GetIDTokenLifespan(ctx context.Context) time.Duration -} diff --git a/handler/rfc8693/custom_jwt_type_handler.go b/handler/rfc8693/custom_jwt_type_handler.go index 32985ffe5..9cb9b443d 100644 --- a/handler/rfc8693/custom_jwt_type_handler.go +++ b/handler/rfc8693/custom_jwt_type_handler.go @@ -13,7 +13,7 @@ import ( ) type CustomJWTTypeHandler struct { - Config ConfigProvider + Config fosite.RFC8693ConfigProvider JWTStrategy jwt.Signer Storage } @@ -49,7 +49,7 @@ func (c *CustomJWTTypeHandler) HandleTokenEndpointRequest(ctx context.Context, r } else { session.SetSubjectToken(unpacked) // Get the subject and populate session - if subject, err := c.Storage.GetSubjectForTokenExchange(ctx, request); err != nil { + if subject, err := c.Storage.GetSubjectForTokenExchange(ctx, request, unpacked); err != nil { return err } else { session.SetSubject(subject) @@ -103,7 +103,7 @@ func (c *CustomJWTTypeHandler) CanHandleTokenEndpointRequest(requester fosite.Ac return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:token-exchange") } -func (c *CustomJWTTypeHandler) validate(ctx context.Context, request fosite.AccessRequester, tokenType TokenType, token string) (map[string]interface{}, error) { +func (c *CustomJWTTypeHandler) validate(ctx context.Context, request fosite.AccessRequester, tokenType fosite.RFC8693TokenType, token string) (map[string]interface{}, error) { jwtType, _ := tokenType.(*JWTType) if jwtType == nil { @@ -157,7 +157,7 @@ func (c *CustomJWTTypeHandler) validate(ctx context.Context, request fosite.Acce return map[string]interface{}(claims), nil } -func (c *CustomJWTTypeHandler) issue(ctx context.Context, request fosite.AccessRequester, tokenType TokenType, response fosite.AccessResponder) error { +func (c *CustomJWTTypeHandler) issue(ctx context.Context, request fosite.AccessRequester, tokenType fosite.RFC8693TokenType, response fosite.AccessResponder) error { jwtType, _ := tokenType.(*JWTType) if jwtType == nil { return errorsx.WithStack( diff --git a/handler/rfc8693/flow_token_exchange.go b/handler/rfc8693/flow_token_exchange.go index 786c62938..e12edef5f 100644 --- a/handler/rfc8693/flow_token_exchange.go +++ b/handler/rfc8693/flow_token_exchange.go @@ -10,14 +10,14 @@ import ( // TokenExchangeGrantHandler is the grant handler for RFC8693 type TokenExchangeGrantHandler struct { - Config ConfigProvider + Config fosite.RFC8693ConfigProvider ScopeStrategy fosite.ScopeStrategy AudienceMatchingStrategy fosite.AudienceMatchingStrategy } // HandleTokenEndpointRequest implements https://tools.ietf.org/html/rfc6749#section-4.3.2 func (c *TokenExchangeGrantHandler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error { - if !c.CanHandleTokenEndpointRequest(request) { + if !c.CanHandleTokenEndpointRequest(ctx, request) { return errorsx.WithStack(fosite.ErrUnknownRequest) } @@ -143,7 +143,7 @@ func (c *TokenExchangeGrantHandler) HandleTokenEndpointRequest(ctx context.Conte // PopulateTokenEndpointResponse implements https://tools.ietf.org/html/rfc6749#section-4.3.3 func (c *TokenExchangeGrantHandler) PopulateTokenEndpointResponse(ctx context.Context, request fosite.AccessRequester, responder fosite.AccessResponder) error { - if !c.CanHandleTokenEndpointRequest(request) { + if !c.CanHandleTokenEndpointRequest(ctx, request) { return errorsx.WithStack(fosite.ErrUnknownRequest) } @@ -178,13 +178,12 @@ func (c *TokenExchangeGrantHandler) PopulateTokenEndpointResponse(ctx context.Co } // CanSkipClientAuth indicates if client auth can be skipped -func (c *TokenExchangeGrantHandler) CanSkipClientAuth(requester fosite.AccessRequester) bool { +func (c *TokenExchangeGrantHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { return false } // CanHandleTokenEndpointRequest indicates if the token endpoint request can be handled -func (c *TokenExchangeGrantHandler) CanHandleTokenEndpointRequest(requester fosite.AccessRequester) bool { +func (c *TokenExchangeGrantHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { // grant_type REQUIRED. - // Value MUST be set to "password". return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:token-exchange") } diff --git a/handler/rfc8693/id_token_type_handler.go b/handler/rfc8693/id_token_type_handler.go index 4cb1ea964..713684a31 100644 --- a/handler/rfc8693/id_token_type_handler.go +++ b/handler/rfc8693/id_token_type_handler.go @@ -10,7 +10,7 @@ import ( ) type IDTokenTypeHandler struct { - Config ConfigProvider + Config fosite.Configurator JWTStrategy jwt.Signer IssueStrategy openid.OpenIDConnectTokenStrategy ValidationStrategy openid.OpenIDConnectTokenValidationStrategy @@ -70,7 +70,9 @@ func (c *IDTokenTypeHandler) PopulateTokenEndpointResponse(ctx context.Context, form := request.GetRequestForm() requestedTokenType := form.Get("requested_token_type") if requestedTokenType == "" { - requestedTokenType = c.Config.GetDefaultRequestedTokenType(ctx) + if config, ok := c.Config.(fosite.RFC8693ConfigProvider); ok { + requestedTokenType = config.GetDefaultRequestedTokenType(ctx) + } } if requestedTokenType != IDTokenType { @@ -103,7 +105,11 @@ func (c *IDTokenTypeHandler) validate(ctx context.Context, request fosite.Access return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("Unable to parse the id_token").WithWrap(err).WithDebug(err.Error())) } - expectedIssuer := c.Config.GetIssuer(ctx) + expectedIssuer := "" + if config, ok := c.Config.(fosite.AccessTokenIssuerProvider); ok { + expectedIssuer = config.GetAccessTokenIssuer(ctx) + } + if !claims.VerifyIssuer(expectedIssuer, true) { return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("Claim 'iss' from token must match the '%s'.", expectedIssuer)) } diff --git a/handler/rfc8693/refresh_token_type_handler.go b/handler/rfc8693/refresh_token_type_handler.go index a4a89901d..eea9f30f8 100644 --- a/handler/rfc8693/refresh_token_type_handler.go +++ b/handler/rfc8693/refresh_token_type_handler.go @@ -12,7 +12,7 @@ import ( ) type RefreshTokenTypeHandler struct { - Config ConfigProvider + Config fosite.RFC8693ConfigProvider RefreshTokenLifespan time.Duration RefreshTokenScopes []string oauth2.CoreStrategy diff --git a/handler/rfc8693/session.go b/handler/rfc8693/session.go index 8de14514d..e14ac4625 100644 --- a/handler/rfc8693/session.go +++ b/handler/rfc8693/session.go @@ -1,5 +1,7 @@ package rfc8693 +import "github.com/ory/fosite/handler/openid" + // Session is required to support token exchange type Session interface { // SetSubject sets the session's subject. @@ -17,3 +19,44 @@ type Session interface { AccessTokenClaimsMap() map[string]interface{} } + +type DefaultSession struct { + *openid.DefaultSession + + ActorToken map[string]interface{} `json:"-"` + SubjectToken map[string]interface{} `json:"-"` + Extra map[string]interface{} `json:"extra,omitempty"` +} + +func (s *DefaultSession) SetActorToken(token map[string]interface{}) { + s.ActorToken = token +} + +func (s *DefaultSession) GetActorToken() map[string]interface{} { + return s.ActorToken +} + +func (s *DefaultSession) SetSubjectToken(token map[string]interface{}) { + s.SubjectToken = token +} + +func (s *DefaultSession) GetSubjectToken() map[string]interface{} { + return s.SubjectToken +} + +func (s *DefaultSession) SetAct(act map[string]interface{}) { + s.Extra["act"] = act +} + +func (s *DefaultSession) AccessTokenClaimsMap() map[string]interface{} { + tokenObject := map[string]interface{}{ + "sub": s.GetSubject(), + "username": s.GetUsername(), + } + + for k, v := range s.Extra { + tokenObject[k] = v + } + + return tokenObject +} diff --git a/handler/rfc8693/storage.go b/handler/rfc8693/storage.go index d2b110498..3d9e8b127 100644 --- a/handler/rfc8693/storage.go +++ b/handler/rfc8693/storage.go @@ -19,5 +19,5 @@ type Storage interface { // GetSubjectForTokenExchange computes the session subject and is used for token types where there is no way // to know the subject value. For some token types, such as access and refresh tokens, the subject is well-defined // and this function is not called. - GetSubjectForTokenExchange(ctx context.Context, requester fosite.Requester) (string, error) + GetSubjectForTokenExchange(ctx context.Context, requester fosite.Requester, subjectToken map[string]interface{}) (string, error) } diff --git a/handler/rfc8693/token_exchange_test.go b/handler/rfc8693/token_exchange_test.go new file mode 100644 index 000000000..59f5b0e49 --- /dev/null +++ b/handler/rfc8693/token_exchange_test.go @@ -0,0 +1,293 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package rfc8693_test + +import ( + "context" + "crypto/ecdsa" + "crypto/rsa" + "encoding/json" + "net/url" + "testing" + "time" + + "github.com/pborman/uuid" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/square/go-jose.v2" + + "github.com/ory/fosite/storage" + "github.com/ory/fosite/token/hmac" + "github.com/ory/fosite/token/jwt" + "github.com/ory/x/errorsx" + + "github.com/ory/fosite" + "github.com/ory/fosite/handler/oauth2" + "github.com/ory/fosite/handler/openid" + "github.com/ory/fosite/handler/rfc8693" + . "github.com/ory/fosite/handler/rfc8693" +) + +func TestAccessTokenExchangeImpersonation(t *testing.T) { + store := storage.NewExampleStore() + jwks := getJWKS() + customJWTType := &JWTType{ + Name: "urn:custom:jwt", + JWTValidationConfig: JWTValidationConfig{ + ValidateJTI: true, + ValidateFunc: jwt.Keyfunc(func(t *jwt.Token) (interface{}, error) { + if _, ok := t.Header["kid"].(string); !ok { + return nil, errors.New("invalid kid") + } + if _, ok := t.Claims["iss"].(string); !ok { + return nil, errors.New("invalid iss") + } + + return findPublicKey(t, jwks, true) + }), + JWTLifetimeToleranceWindow: 15 * time.Minute, + }, + JWTIssueConfig: JWTIssueConfig{ + Audience: []string{"https://resource1.com"}, + }, + Issuer: "https://customory.com", + } + + config := &fosite.Config{ + ScopeStrategy: fosite.HierarchicScopeStrategy, + AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, + GlobalSecret: []byte("some-secret-thats-random-some-secret-thats-random-"), + RFC8693TokenTypes: map[string]fosite.RFC8693TokenType{ + AccessTokenType: &DefaultTokenType{ + Name: AccessTokenType, + }, + IDTokenType: &DefaultTokenType{ + Name: IDTokenType, + }, + RefreshTokenType: &DefaultTokenType{ + Name: RefreshTokenType, + }, + customJWTType.GetName(nil): customJWTType, + }, + DefaultRequestedTokenType: AccessTokenType, + } + + coreStrategy := &oauth2.HMACSHAStrategy{ + Enigma: &hmac.HMACStrategy{Config: config}, + Config: config, + } + + genericTEHandler := &TokenExchangeGrantHandler{ + Config: config, + ScopeStrategy: config.ScopeStrategy, + AudienceMatchingStrategy: config.AudienceMatchingStrategy, + } + + accessTokenHandler := &AccessTokenTypeHandler{ + Config: config, + AccessTokenLifespan: 5 * time.Minute, + RefreshTokenLifespan: 5 * time.Minute, + RefreshTokenScopes: []string{"offline"}, + CoreStrategy: coreStrategy, + ScopeStrategy: config.ScopeStrategy, + Storage: store, + } + + for _, c := range []struct { + handlers []fosite.TokenEndpointHandler + areq *fosite.AccessRequest + description string + expectErr error + expect func(t *testing.T, areq *fosite.AccessRequest, aresp *fosite.AccessResponse) + }{ + { + handlers: []fosite.TokenEndpointHandler{genericTEHandler, accessTokenHandler}, + areq: &fosite.AccessRequest{ + Request: fosite.Request{ + ID: uuid.New(), + Client: store.Clients["my-client"], + Form: url.Values{ + "subject_token_type": []string{rfc8693.AccessTokenType}, + "subject_token": []string{createAccessToken(context.Background(), coreStrategy, store, + store.Clients["custom-lifespan-client"])}, + }, + Session: &rfc8693.DefaultSession{ + DefaultSession: &openid.DefaultSession{}, + Extra: map[string]interface{}{}, + }, + }, + }, + description: "should pass because a valid access token is exchanged for another access token", + expect: func(t *testing.T, areq *fosite.AccessRequest, aresp *fosite.AccessResponse) { + assert.NotEmpty(t, aresp.AccessToken, "Access token is empty; %+v", aresp) + }, + }, + } { + t.Run("case="+c.description, func(t *testing.T) { + ctx := context.Background() + aresp := fosite.NewAccessResponse() + found := false + var err error + c.areq.Form.Set("grant_type", string(fosite.GrantTypeTokenExchange)) + c.areq.GrantTypes = fosite.Arguments{"urn:ietf:params:oauth:grant-type:token-exchange"} + c.areq.Client = store.Clients["my-client"] + for _, loader := range c.handlers { + // Is the loader responsible for handling the request? + if !loader.CanHandleTokenEndpointRequest(ctx, c.areq) { + continue + } + + // The handler **is** responsible! + found = true + + if err = loader.HandleTokenEndpointRequest(ctx, c.areq); err == nil { + continue + } else if errors.Is(err, fosite.ErrUnknownRequest) { + // This is a duplicate because it should already have been handled by + // `loader.CanHandleTokenEndpointRequest(accessRequest)` but let's keep it for sanity. + // + err = nil + continue + } else if err != nil { + break + } + } + + if !found { + assert.Fail(t, "Unable to find a valid handler") + } + + // now execute the response + if err == nil { + for _, loader := range c.handlers { + // Is the loader responsible for handling the request? + if !loader.CanHandleTokenEndpointRequest(ctx, c.areq) { + continue + } + + // The handler **is** responsible! + + if err = loader.PopulateTokenEndpointResponse(ctx, c.areq, aresp); err == nil { + found = true + } else if errors.Is(err, fosite.ErrUnknownRequest) { + // This is a duplicate because it should already have been handled by + // `loader.CanHandleTokenEndpointRequest(accessRequest)` but let's keep it for sanity. + // + err = nil + continue + } else if err != nil { + break + } + } + } + + if c.expectErr != nil { + require.EqualError(t, err, c.expectErr.Error()) + } else { + require.NoError(t, err) + } + + if c.expect != nil { + c.expect(t, c.areq, aresp) + } + }) + } +} + +func findPublicKey(t *jwt.Token, set *jose.JSONWebKeySet, expectsRSAKey bool) (interface{}, error) { + keys := set.Keys + if len(keys) == 0 { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("The retrieved JSON Web Key Set does not contain any key.")) + } + + kid, ok := t.Header["kid"].(string) + if ok { + keys = set.Key(kid) + } + + if len(keys) == 0 { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("The JSON Web Token uses signing key with kid '%s', which could not be found.", kid)) + } + + for _, key := range keys { + if key.Use != "sig" { + continue + } + if expectsRSAKey { + if k, ok := key.Key.(*rsa.PublicKey); ok { + return k, nil + } + } else { + if k, ok := key.Key.(*ecdsa.PublicKey); ok { + return k, nil + } + } + } + + if expectsRSAKey { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("Unable to find RSA public key with use='sig' for kid '%s' in JSON Web Key Set.", kid)) + } else { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("Unable to find ECDSA public key with use='sig' for kid '%s' in JSON Web Key Set.", kid)) + } +} + +func getJWKS() *jose.JSONWebKeySet { + jwks := &jose.JSONWebKeySet{} + jwksStr := `{"keys":[ + { + "kty": "RSA", + "e": "AQAB", + "kid": "demojwtsigner", + "use": "sig", + "n": "nyEEwueLcSFRUSPdy9AL5Vf6X7QDuL8mFMOR2liM1LeluSHCSYIoN-h6xxMkwDfr6626EOhJVxMxeBuLaG-_3QWWjvicUdIpevj73U1jqQT7MaMPI3ms7rm0v1OHfabyLbrCjDniL_8Ym15H_RwVqF31kXIcKVqMtJWRWkeoOrSSqUq4h28rRDUi8HXUTAvSoQYnZ-J-sICME7G-ZYVJtIQObT6AjMuM_y54vCH8ViVE9aOQ2rV3Wi-TKEgiV9Ik1KB6EdzCB4CYK2HYy_OgheF0ggeWuwHOegBpVR4BqlQyZJKJyhKhWZhfYHmWkm_V-7KZtrWHoVQ_NhOAcT18qw" + }, + { + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "kid": "cibarsa", + "n": "mP6Zt6qN3YEE4asCoMmvVEJcXTv00I1AamJvmkUx0Ax9-w_AcBa7zeEgysEK0CQG2jXLGaRQ-W0D74Z5K_aAnx7dbRSmArxe-dlGm08_KoOwErh2dHq5_GezYURTWddv_2hjObJcoxQtzKmQQCbcLH_8_AGdVO6KZYfPElPqsEW1VEdiFkOgL3LPw2KRVPB3g6yj3t2Ot9edB8AnKwyD8eFDpV48Q-w9DfgqY_XlOYTDgtpBDGADP_XScL5Le7wZRfZp1N4qRYeak2NjKMDUpxPt0tX5d-GHjTG6ph9J-hzBFnSbUUpQEHol7fAVy6GFOwVbY9-yJkoV7CebstDryQ" + }, + { + "kty": "EC", + "use": "sig", + "crv": "P-521", + "kid": "cibaec521", + "x": "AVnfaEpeCrVt8mozqVaJ37hW7JBhHVu9q8BK0w6-wTAhJ8FBoWFxOPGT-Kc0-h0weNTh1UMGEoXmXFArN6qGp1yz", + "y": "AN6HK2bqfD2Y_3r6_WZa5Z6IyZao8Aw9OZBJ0IMrbnmay6z0-Oghqd7NChR6BORkizLetSe-4HbOxllPSztHFP2d" + } + ]}` + + if err := json.Unmarshal([]byte(jwksStr), jwks); err != nil { + panic(err.Error()) + } + + return jwks +} + +func createAccessToken(ctx context.Context, coreStrategy oauth2.CoreStrategy, storage oauth2.AccessTokenStorage, client fosite.Client) string { + request := &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"password"}, + Request: fosite.Request{ + Session: &fosite.DefaultSession{ + Username: "peter", + Subject: "peter", + ExpiresAt: map[fosite.TokenType]time.Time{ + fosite.AccessToken: time.Now().UTC().Add(10 * time.Minute), + }, + }, + Client: client, + }, + } + + token, signature, err := coreStrategy.GenerateAccessToken(ctx, request) + if err != nil { + panic(err.Error()) + } else if err := storage.CreateAccessTokenSession(ctx, signature, request.Sanitize([]string{})); err != nil { + panic(err.Error()) + } + + return token +} diff --git a/handler/rfc8693/token_type.go b/handler/rfc8693/token_type.go index cf72b59b6..f190389e1 100644 --- a/handler/rfc8693/token_type.go +++ b/handler/rfc8693/token_type.go @@ -4,11 +4,16 @@ import ( "context" ) -type TokenType interface { - GetName(ctx context.Context) string - - GetType(ctx context.Context) string -} +const ( + // AccessTokenType is the access token type issued by the same provider + AccessTokenType string = "urn:ietf:params:oauth:token-type:access_token" // #nosec G101 + // RefreshTokenType is the refresh token type issued by the same provider + RefreshTokenType string = "urn:ietf:params:oauth:token-type:refresh_token" // #nosec G101 + // IDTokenType is the id_token type issued by the same provider + IDTokenType string = "urn:ietf:params:oauth:token-type:id_token" // #nosec G101 + // JWTTokenType is the JWT type that may be issued by a different provider + JWTTokenType string = "urn:ietf:params:oauth:token-type:jwt" // #nosec G101 +) type DefaultTokenType struct { Name string diff --git a/oauth2.go b/oauth2.go index c25abf65a..eb676d500 100644 --- a/oauth2.go +++ b/oauth2.go @@ -32,8 +32,8 @@ const ( GrantTypePassword GrantType = "password" GrantTypeClientCredentials GrantType = "client_credentials" GrantTypeJWTBearer GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" //nolint:gosec // this is not a hardcoded credential - - BearerAccessToken string = "bearer" + GrantTypeTokenExchange GrantType = "urn:ietf:params:oauth:grant-type:token-exchange" + BearerAccessToken string = "bearer" ) // OAuth2Provider is an interface that enables you to write OAuth2 handlers with only a few lines of code. @@ -365,3 +365,9 @@ type G11NContext interface { // GetLang returns the current language in the context GetLang() language.Tag } + +type RFC8693TokenType interface { + GetName(ctx context.Context) string + + GetType(ctx context.Context) string +} diff --git a/storage/memory.go b/storage/memory.go index c08a28b2a..b2010c112 100644 --- a/storage/memory.go +++ b/storage/memory.go @@ -102,7 +102,7 @@ func NewExampleStore() *MemoryStore { RotatedSecrets: [][]byte{[]byte(`$2y$10$X51gLxUQJ.hGw1epgHTE5u0bt64xM0COU7K9iAp.OFg8p2pUd.1zC `)}, // = "foobaz", RedirectURIs: []string{"http://localhost:3846/callback"}, ResponseTypes: []string{"id_token", "code", "token", "id_token token", "code id_token", "code token", "code id_token token"}, - GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"}, + GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials", "urn:ietf:params:oauth:grant-type:token-exchange"}, Scopes: []string{"fosite", "openid", "photos", "offline"}, }, "custom-lifespan-client": &fosite.DefaultClientWithCustomTokenLifespans{ @@ -497,3 +497,20 @@ func (s *MemoryStore) DeletePARSession(ctx context.Context, requestURI string) ( delete(s.PARSessions, requestURI) return nil } + +func (s *MemoryStore) SetTokenExchangeCustomJWT(ctx context.Context, jti string, exp time.Time) error { + // the memory store implementation is generic, so just re-use + return s.SetClientAssertionJWT(ctx, jti, exp) +} + +// GetSubjectForTokenExchange computes the session subject and is used for token types where there is no way +// to know the subject value. For some token types, such as access and refresh tokens, the subject is well-defined +// and this function is not called. +func (s *MemoryStore) GetSubjectForTokenExchange(ctx context.Context, requester fosite.Requester, subjectToken map[string]interface{}) (string, error) { + sub, _ := subjectToken["subject"].(string) + if sub == "" { + return "", fosite.ErrInvalidRequest.WithHint("No subject found.") + } + + return sub, nil +} From 524a94a82241d8214016a41ee51e488644b98228 Mon Sep 17 00:00:00 2001 From: Vivek Shankar Date: Sat, 27 May 2023 20:56:24 +0800 Subject: [PATCH 03/10] fix: Handler funcs missing context --- handler/rfc8693/actor_token_validation_handler.go | 6 +++--- handler/rfc8693/custom_jwt_type_handler.go | 8 ++++---- handler/rfc8693/id_token_type_handler.go | 8 ++++---- handler/rfc8693/refresh_token_type_handler.go | 8 ++++---- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/handler/rfc8693/actor_token_validation_handler.go b/handler/rfc8693/actor_token_validation_handler.go index daa86db9a..5b4538095 100644 --- a/handler/rfc8693/actor_token_validation_handler.go +++ b/handler/rfc8693/actor_token_validation_handler.go @@ -12,7 +12,7 @@ type ActorTokenValidationHandler struct{} // HandleTokenEndpointRequest implements https://tools.ietf.org/html/rfc6749#section-4.3.2 func (c *ActorTokenValidationHandler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error { - if !c.CanHandleTokenEndpointRequest(request) { + if !c.CanHandleTokenEndpointRequest(ctx, request) { return errorsx.WithStack(fosite.ErrUnknownRequest) } @@ -49,12 +49,12 @@ func (c *ActorTokenValidationHandler) PopulateTokenEndpointResponse(ctx context. } // CanSkipClientAuth indicates if client auth can be skipped -func (c *ActorTokenValidationHandler) CanSkipClientAuth(requester fosite.AccessRequester) bool { +func (c *ActorTokenValidationHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { return false } // CanHandleTokenEndpointRequest indicates if the token endpoint request can be handled -func (c *ActorTokenValidationHandler) CanHandleTokenEndpointRequest(requester fosite.AccessRequester) bool { +func (c *ActorTokenValidationHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { // grant_type REQUIRED. // Value MUST be set to "password". return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:token-exchange") diff --git a/handler/rfc8693/custom_jwt_type_handler.go b/handler/rfc8693/custom_jwt_type_handler.go index 9cb9b443d..f467dbd08 100644 --- a/handler/rfc8693/custom_jwt_type_handler.go +++ b/handler/rfc8693/custom_jwt_type_handler.go @@ -20,7 +20,7 @@ type CustomJWTTypeHandler struct { // HandleTokenEndpointRequest implements https://tools.ietf.org/html/rfc6749#section-4.3.2 func (c *CustomJWTTypeHandler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error { - if !c.CanHandleTokenEndpointRequest(request) { + if !c.CanHandleTokenEndpointRequest(ctx, request) { return errorsx.WithStack(fosite.ErrUnknownRequest) } @@ -63,7 +63,7 @@ func (c *CustomJWTTypeHandler) HandleTokenEndpointRequest(ctx context.Context, r // PopulateTokenEndpointResponse implements https://tools.ietf.org/html/rfc6749#section-4.3.3 func (c *CustomJWTTypeHandler) PopulateTokenEndpointResponse(ctx context.Context, request fosite.AccessRequester, responder fosite.AccessResponder) error { - if !c.CanHandleTokenEndpointRequest(request) { + if !c.CanHandleTokenEndpointRequest(ctx, request) { return errorsx.WithStack(fosite.ErrUnknownRequest) } @@ -92,12 +92,12 @@ func (c *CustomJWTTypeHandler) PopulateTokenEndpointResponse(ctx context.Context } // CanSkipClientAuth indicates if client auth can be skipped -func (c *CustomJWTTypeHandler) CanSkipClientAuth(requester fosite.AccessRequester) bool { +func (c *CustomJWTTypeHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { return false } // CanHandleTokenEndpointRequest indicates if the token endpoint request can be handled -func (c *CustomJWTTypeHandler) CanHandleTokenEndpointRequest(requester fosite.AccessRequester) bool { +func (c *CustomJWTTypeHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { // grant_type REQUIRED. // Value MUST be set to "password". return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:token-exchange") diff --git a/handler/rfc8693/id_token_type_handler.go b/handler/rfc8693/id_token_type_handler.go index 713684a31..76d72bd30 100644 --- a/handler/rfc8693/id_token_type_handler.go +++ b/handler/rfc8693/id_token_type_handler.go @@ -19,7 +19,7 @@ type IDTokenTypeHandler struct { // HandleTokenEndpointRequest implements https://tools.ietf.org/html/rfc6749#section-4.3.2 func (c *IDTokenTypeHandler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error { - if !c.CanHandleTokenEndpointRequest(request) { + if !c.CanHandleTokenEndpointRequest(ctx, request) { return errorsx.WithStack(fosite.ErrUnknownRequest) } @@ -58,7 +58,7 @@ func (c *IDTokenTypeHandler) HandleTokenEndpointRequest(ctx context.Context, req // PopulateTokenEndpointResponse implements https://tools.ietf.org/html/rfc6749#section-4.3.3 func (c *IDTokenTypeHandler) PopulateTokenEndpointResponse(ctx context.Context, request fosite.AccessRequester, responder fosite.AccessResponder) error { - if !c.CanHandleTokenEndpointRequest(request) { + if !c.CanHandleTokenEndpointRequest(ctx, request) { return errorsx.WithStack(fosite.ErrUnknownRequest) } @@ -87,12 +87,12 @@ func (c *IDTokenTypeHandler) PopulateTokenEndpointResponse(ctx context.Context, } // CanSkipClientAuth indicates if client auth can be skipped -func (c *IDTokenTypeHandler) CanSkipClientAuth(requester fosite.AccessRequester) bool { +func (c *IDTokenTypeHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { return false } // CanHandleTokenEndpointRequest indicates if the token endpoint request can be handled -func (c *IDTokenTypeHandler) CanHandleTokenEndpointRequest(requester fosite.AccessRequester) bool { +func (c *IDTokenTypeHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { // grant_type REQUIRED. // Value MUST be set to "password". return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:token-exchange") diff --git a/handler/rfc8693/refresh_token_type_handler.go b/handler/rfc8693/refresh_token_type_handler.go index eea9f30f8..3bbe177f3 100644 --- a/handler/rfc8693/refresh_token_type_handler.go +++ b/handler/rfc8693/refresh_token_type_handler.go @@ -22,7 +22,7 @@ type RefreshTokenTypeHandler struct { // HandleTokenEndpointRequest implements https://tools.ietf.org/html/rfc6749#section-4.3.2 func (c *RefreshTokenTypeHandler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error { - if !c.CanHandleTokenEndpointRequest(request) { + if !c.CanHandleTokenEndpointRequest(ctx, request) { return errorsx.WithStack(fosite.ErrUnknownRequest) } @@ -61,7 +61,7 @@ func (c *RefreshTokenTypeHandler) HandleTokenEndpointRequest(ctx context.Context // PopulateTokenEndpointResponse implements https://tools.ietf.org/html/rfc6749#section-4.3.3 func (c *RefreshTokenTypeHandler) PopulateTokenEndpointResponse(ctx context.Context, request fosite.AccessRequester, responder fosite.AccessResponder) error { - if !c.CanHandleTokenEndpointRequest(request) { + if !c.CanHandleTokenEndpointRequest(ctx, request) { return errorsx.WithStack(fosite.ErrUnknownRequest) } @@ -88,12 +88,12 @@ func (c *RefreshTokenTypeHandler) PopulateTokenEndpointResponse(ctx context.Cont } // CanSkipClientAuth indicates if client auth can be skipped -func (c *RefreshTokenTypeHandler) CanSkipClientAuth(requester fosite.AccessRequester) bool { +func (c *RefreshTokenTypeHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { return false } // CanHandleTokenEndpointRequest indicates if the token endpoint request can be handled -func (c *RefreshTokenTypeHandler) CanHandleTokenEndpointRequest(requester fosite.AccessRequester) bool { +func (c *RefreshTokenTypeHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { // grant_type REQUIRED. // Value MUST be set to "password". return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:token-exchange") From 9bd4d0e839e66eaba16cd82049a281326e968631 Mon Sep 17 00:00:00 2001 From: Vivek Shankar Date: Sat, 27 May 2023 21:55:56 +0800 Subject: [PATCH 04/10] test: Added custom JWT test --- handler/rfc8693/token_exchange_test.go | 170 ++++++++++++------------- storage/memory.go | 1 + 2 files changed, 83 insertions(+), 88 deletions(-) diff --git a/handler/rfc8693/token_exchange_test.go b/handler/rfc8693/token_exchange_test.go index 59f5b0e49..a00987439 100644 --- a/handler/rfc8693/token_exchange_test.go +++ b/handler/rfc8693/token_exchange_test.go @@ -5,9 +5,6 @@ package rfc8693_test import ( "context" - "crypto/ecdsa" - "crypto/rsa" - "encoding/json" "net/url" "testing" "time" @@ -16,12 +13,11 @@ import ( "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gopkg.in/square/go-jose.v2" + "github.com/ory/fosite/internal/gen" "github.com/ory/fosite/storage" "github.com/ory/fosite/token/hmac" "github.com/ory/fosite/token/jwt" - "github.com/ory/x/errorsx" "github.com/ory/fosite" "github.com/ory/fosite/handler/oauth2" @@ -30,22 +26,25 @@ import ( . "github.com/ory/fosite/handler/rfc8693" ) +// expose key to verify id_token +var key = gen.MustRSAKey() + func TestAccessTokenExchangeImpersonation(t *testing.T) { store := storage.NewExampleStore() - jwks := getJWKS() + jwtName := "urn:custom:jwt" + + jwtSigner := &jwt.DefaultSigner{ + GetPrivateKey: func(_ context.Context) (interface{}, error) { + return key, nil + }, + } + customJWTType := &JWTType{ - Name: "urn:custom:jwt", + Name: jwtName, JWTValidationConfig: JWTValidationConfig{ ValidateJTI: true, ValidateFunc: jwt.Keyfunc(func(t *jwt.Token) (interface{}, error) { - if _, ok := t.Header["kid"].(string); !ok { - return nil, errors.New("invalid kid") - } - if _, ok := t.Claims["iss"].(string); !ok { - return nil, errors.New("invalid iss") - } - - return findPublicKey(t, jwks, true) + return key.PublicKey, nil }), JWTLifetimeToleranceWindow: 15 * time.Minute, }, @@ -95,6 +94,16 @@ func TestAccessTokenExchangeImpersonation(t *testing.T) { Storage: store, } + customJWTHandler := &CustomJWTTypeHandler{ + Config: config, + JWTStrategy: &jwt.DefaultSigner{ + GetPrivateKey: func(_ context.Context) (interface{}, error) { + return key, nil + }, + }, + Storage: store, + } + for _, c := range []struct { handlers []fosite.TokenEndpointHandler areq *fosite.AccessRequest @@ -122,6 +131,41 @@ func TestAccessTokenExchangeImpersonation(t *testing.T) { description: "should pass because a valid access token is exchanged for another access token", expect: func(t *testing.T, areq *fosite.AccessRequest, aresp *fosite.AccessResponse) { assert.NotEmpty(t, aresp.AccessToken, "Access token is empty; %+v", aresp) + req, err := introspectAccessToken(context.Background(), aresp.AccessToken, coreStrategy, store) + require.NoError(t, err, "Error occurred during introspection; err=%v", err) + + assert.EqualValues(t, "peter", req.GetSession().GetSubject(), "Subject did not match the expected value") + }, + }, + { + handlers: []fosite.TokenEndpointHandler{genericTEHandler, accessTokenHandler, customJWTHandler}, + areq: &fosite.AccessRequest{ + Request: fosite.Request{ + ID: uuid.New(), + Client: store.Clients["my-client"], + Form: url.Values{ + "subject_token_type": []string{jwtName}, + "subject_token": []string{createJWT(context.Background(), jwtSigner, jwt.MapClaims{ + "subject": "peter_for_jwt", + "jti": uuid.New(), + "iss": "https://customory.com", + "sub": "peter", + "exp": time.Now().Add(15 * time.Minute).Unix(), + })}, + }, + Session: &rfc8693.DefaultSession{ + DefaultSession: &openid.DefaultSession{}, + Extra: map[string]interface{}{}, + }, + }, + }, + description: "should pass because a valid custom JWT is exchanged for access token", + expect: func(t *testing.T, areq *fosite.AccessRequest, aresp *fosite.AccessResponse) { + assert.NotEmpty(t, aresp.AccessToken, "Access token is empty; %+v", aresp) + req, err := introspectAccessToken(context.Background(), aresp.AccessToken, coreStrategy, store) + require.NoError(t, err, "Error occurred during introspection; err=%v", err) + + assert.EqualValues(t, "peter_for_jwt", req.GetSession().GetSubject(), "Subject did not match the expected value") }, }, } { @@ -183,10 +227,15 @@ func TestAccessTokenExchangeImpersonation(t *testing.T) { } } + var rfcerr *fosite.RFC6749Error + rfcerr, _ = err.(*fosite.RFC6749Error) + if rfcerr == nil { + rfcerr = fosite.ErrServerError + } if c.expectErr != nil { - require.EqualError(t, err, c.expectErr.Error()) + require.EqualError(t, err, c.expectErr.Error(), "Error received: %v, rfcerr=%s", err, rfcerr.GetDescription()) } else { - require.NoError(t, err) + require.NoError(t, err, "Error received: %v, rfcerr=%s", err, rfcerr.GetDescription()) } if c.expect != nil { @@ -196,77 +245,6 @@ func TestAccessTokenExchangeImpersonation(t *testing.T) { } } -func findPublicKey(t *jwt.Token, set *jose.JSONWebKeySet, expectsRSAKey bool) (interface{}, error) { - keys := set.Keys - if len(keys) == 0 { - return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("The retrieved JSON Web Key Set does not contain any key.")) - } - - kid, ok := t.Header["kid"].(string) - if ok { - keys = set.Key(kid) - } - - if len(keys) == 0 { - return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("The JSON Web Token uses signing key with kid '%s', which could not be found.", kid)) - } - - for _, key := range keys { - if key.Use != "sig" { - continue - } - if expectsRSAKey { - if k, ok := key.Key.(*rsa.PublicKey); ok { - return k, nil - } - } else { - if k, ok := key.Key.(*ecdsa.PublicKey); ok { - return k, nil - } - } - } - - if expectsRSAKey { - return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("Unable to find RSA public key with use='sig' for kid '%s' in JSON Web Key Set.", kid)) - } else { - return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("Unable to find ECDSA public key with use='sig' for kid '%s' in JSON Web Key Set.", kid)) - } -} - -func getJWKS() *jose.JSONWebKeySet { - jwks := &jose.JSONWebKeySet{} - jwksStr := `{"keys":[ - { - "kty": "RSA", - "e": "AQAB", - "kid": "demojwtsigner", - "use": "sig", - "n": "nyEEwueLcSFRUSPdy9AL5Vf6X7QDuL8mFMOR2liM1LeluSHCSYIoN-h6xxMkwDfr6626EOhJVxMxeBuLaG-_3QWWjvicUdIpevj73U1jqQT7MaMPI3ms7rm0v1OHfabyLbrCjDniL_8Ym15H_RwVqF31kXIcKVqMtJWRWkeoOrSSqUq4h28rRDUi8HXUTAvSoQYnZ-J-sICME7G-ZYVJtIQObT6AjMuM_y54vCH8ViVE9aOQ2rV3Wi-TKEgiV9Ik1KB6EdzCB4CYK2HYy_OgheF0ggeWuwHOegBpVR4BqlQyZJKJyhKhWZhfYHmWkm_V-7KZtrWHoVQ_NhOAcT18qw" - }, - { - "kty": "RSA", - "e": "AQAB", - "use": "sig", - "kid": "cibarsa", - "n": "mP6Zt6qN3YEE4asCoMmvVEJcXTv00I1AamJvmkUx0Ax9-w_AcBa7zeEgysEK0CQG2jXLGaRQ-W0D74Z5K_aAnx7dbRSmArxe-dlGm08_KoOwErh2dHq5_GezYURTWddv_2hjObJcoxQtzKmQQCbcLH_8_AGdVO6KZYfPElPqsEW1VEdiFkOgL3LPw2KRVPB3g6yj3t2Ot9edB8AnKwyD8eFDpV48Q-w9DfgqY_XlOYTDgtpBDGADP_XScL5Le7wZRfZp1N4qRYeak2NjKMDUpxPt0tX5d-GHjTG6ph9J-hzBFnSbUUpQEHol7fAVy6GFOwVbY9-yJkoV7CebstDryQ" - }, - { - "kty": "EC", - "use": "sig", - "crv": "P-521", - "kid": "cibaec521", - "x": "AVnfaEpeCrVt8mozqVaJ37hW7JBhHVu9q8BK0w6-wTAhJ8FBoWFxOPGT-Kc0-h0weNTh1UMGEoXmXFArN6qGp1yz", - "y": "AN6HK2bqfD2Y_3r6_WZa5Z6IyZao8Aw9OZBJ0IMrbnmay6z0-Oghqd7NChR6BORkizLetSe-4HbOxllPSztHFP2d" - } - ]}` - - if err := json.Unmarshal([]byte(jwksStr), jwks); err != nil { - panic(err.Error()) - } - - return jwks -} - func createAccessToken(ctx context.Context, coreStrategy oauth2.CoreStrategy, storage oauth2.AccessTokenStorage, client fosite.Client) string { request := &fosite.AccessRequest{ GrantTypes: fosite.Arguments{"password"}, @@ -291,3 +269,19 @@ func createAccessToken(ctx context.Context, coreStrategy oauth2.CoreStrategy, st return token } + +func createJWT(ctx context.Context, signer jwt.Signer, claims jwt.MapClaims) string { + token, _, err := signer.Generate(ctx, claims, &jwt.Headers{}) + if err != nil { + panic(err.Error()) + } + + return token +} + +func introspectAccessToken(ctx context.Context, token string, coreStrategy oauth2.CoreStrategy, storage oauth2.CoreStorage) ( + fosite.Requester, error) { + sig := coreStrategy.AccessTokenSignature(ctx, token) + or, err := storage.GetAccessTokenSession(ctx, sig, &fosite.DefaultSession{}) + return or, err +} diff --git a/storage/memory.go b/storage/memory.go index b2010c112..7b4d61202 100644 --- a/storage/memory.go +++ b/storage/memory.go @@ -140,6 +140,7 @@ func NewExampleStore() *MemoryStore { RefreshTokens: map[string]StoreRefreshToken{}, PKCES: map[string]fosite.Requester{}, AccessTokenRequestIDs: map[string]string{}, + BlacklistedJTIs: map[string]time.Time{}, RefreshTokenRequestIDs: map[string]string{}, IssuerPublicKeys: map[string]IssuerPublicKeys{}, PARSessions: map[string]fosite.AuthorizeRequester{}, From 80cb32e3021c576f64335f96f62941d88a474658 Mon Sep 17 00:00:00 2001 From: Vivek Shankar Date: Sat, 27 May 2023 22:03:48 +0800 Subject: [PATCH 05/10] chore: Copyrights --- handler/rfc8693/access_token_type_handler.go | 3 +++ handler/rfc8693/actor_token_validation_handler.go | 3 +++ handler/rfc8693/client.go | 3 +++ handler/rfc8693/custom_jwt_type_handler.go | 3 +++ handler/rfc8693/flow_token_exchange.go | 3 +++ handler/rfc8693/id_token_type_handler.go | 3 +++ handler/rfc8693/refresh_token_type_handler.go | 3 +++ handler/rfc8693/session.go | 3 +++ handler/rfc8693/storage.go | 3 +++ handler/rfc8693/token_type.go | 3 +++ handler/rfc8693/token_type_jwt.go | 3 +++ 11 files changed, 33 insertions(+) diff --git a/handler/rfc8693/access_token_type_handler.go b/handler/rfc8693/access_token_type_handler.go index 5fba5b83d..27733f114 100644 --- a/handler/rfc8693/access_token_type_handler.go +++ b/handler/rfc8693/access_token_type_handler.go @@ -1,3 +1,6 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + package rfc8693 import ( diff --git a/handler/rfc8693/actor_token_validation_handler.go b/handler/rfc8693/actor_token_validation_handler.go index 5b4538095..ecc2b5add 100644 --- a/handler/rfc8693/actor_token_validation_handler.go +++ b/handler/rfc8693/actor_token_validation_handler.go @@ -1,3 +1,6 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + package rfc8693 import ( diff --git a/handler/rfc8693/client.go b/handler/rfc8693/client.go index 21948e834..5fbb8ab4c 100644 --- a/handler/rfc8693/client.go +++ b/handler/rfc8693/client.go @@ -1,3 +1,6 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + package rfc8693 type Client interface { diff --git a/handler/rfc8693/custom_jwt_type_handler.go b/handler/rfc8693/custom_jwt_type_handler.go index f467dbd08..f276d309b 100644 --- a/handler/rfc8693/custom_jwt_type_handler.go +++ b/handler/rfc8693/custom_jwt_type_handler.go @@ -1,3 +1,6 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + package rfc8693 import ( diff --git a/handler/rfc8693/flow_token_exchange.go b/handler/rfc8693/flow_token_exchange.go index e12edef5f..0092dad0f 100644 --- a/handler/rfc8693/flow_token_exchange.go +++ b/handler/rfc8693/flow_token_exchange.go @@ -1,3 +1,6 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + package rfc8693 import ( diff --git a/handler/rfc8693/id_token_type_handler.go b/handler/rfc8693/id_token_type_handler.go index 76d72bd30..6c46f9c11 100644 --- a/handler/rfc8693/id_token_type_handler.go +++ b/handler/rfc8693/id_token_type_handler.go @@ -1,3 +1,6 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + package rfc8693 import ( diff --git a/handler/rfc8693/refresh_token_type_handler.go b/handler/rfc8693/refresh_token_type_handler.go index 3bbe177f3..67eeb4e85 100644 --- a/handler/rfc8693/refresh_token_type_handler.go +++ b/handler/rfc8693/refresh_token_type_handler.go @@ -1,3 +1,6 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + package rfc8693 import ( diff --git a/handler/rfc8693/session.go b/handler/rfc8693/session.go index e14ac4625..5378e2db9 100644 --- a/handler/rfc8693/session.go +++ b/handler/rfc8693/session.go @@ -1,3 +1,6 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + package rfc8693 import "github.com/ory/fosite/handler/openid" diff --git a/handler/rfc8693/storage.go b/handler/rfc8693/storage.go index 3d9e8b127..ebc85c2da 100644 --- a/handler/rfc8693/storage.go +++ b/handler/rfc8693/storage.go @@ -1,3 +1,6 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + package rfc8693 import ( diff --git a/handler/rfc8693/token_type.go b/handler/rfc8693/token_type.go index f190389e1..d415f6981 100644 --- a/handler/rfc8693/token_type.go +++ b/handler/rfc8693/token_type.go @@ -1,3 +1,6 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + package rfc8693 import ( diff --git a/handler/rfc8693/token_type_jwt.go b/handler/rfc8693/token_type_jwt.go index 561843dc8..fa8d5757f 100644 --- a/handler/rfc8693/token_type_jwt.go +++ b/handler/rfc8693/token_type_jwt.go @@ -1,3 +1,6 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + package rfc8693 import ( From cfb4fe11e73964154ae3e0770cf90be9fb0042d2 Mon Sep 17 00:00:00 2001 From: Vivek Shankar Date: Wed, 31 May 2023 00:36:06 +0800 Subject: [PATCH 06/10] feat: Modify the token exchange client check to be more configurable based on implementation --- handler/rfc8693/access_token_type_handler.go | 14 +------------- handler/rfc8693/client.go | 8 +++++--- handler/rfc8693/refresh_token_type_handler.go | 14 +------------- 3 files changed, 7 insertions(+), 29 deletions(-) diff --git a/handler/rfc8693/access_token_type_handler.go b/handler/rfc8693/access_token_type_handler.go index 27733f114..fd833ff03 100644 --- a/handler/rfc8693/access_token_type_handler.go +++ b/handler/rfc8693/access_token_type_handler.go @@ -129,19 +129,7 @@ func (c *AccessTokenTypeHandler) validate(ctx context.Context, request fosite.Ac // Check if the client is allowed to exchange this token if subjectTokenClient, ok := or.GetClient().(Client); ok { - allowedClientIDs := subjectTokenClient.GetAllowedClientIDsForTokenExchange() - allowed := false - if len(allowedClientIDs) == 0 { - allowed = true - } else { - for _, cid := range allowedClientIDs { - if client.GetID() == cid { - allowed = true - break - } - } - } - + allowed := subjectTokenClient.TokenExchangeAllowed(client) if !allowed { return nil, nil, errors.WithStack(fosite.ErrRequestForbidden.WithHintf( "The OAuth 2.0 client is not permitted to exchange a subject token issued to client %s", subjectTokenClientID)) diff --git a/handler/rfc8693/client.go b/handler/rfc8693/client.go index 5fbb8ab4c..48a265901 100644 --- a/handler/rfc8693/client.go +++ b/handler/rfc8693/client.go @@ -3,6 +3,8 @@ package rfc8693 +import "github.com/ory/fosite" + type Client interface { // GetSupportedSubjectTokenTypes indicates the token types allowed for subject_token GetSupportedSubjectTokenTypes() []string @@ -10,7 +12,7 @@ type Client interface { GetSupportedActorTokenTypes() []string // GetSupportedRequestTokenTypes indicates the token types allowed for requested_token_type GetSupportedRequestTokenTypes() []string - // GetAllowedClientIDsForTokenExchange indicates the clients that are allowed to - // exchange the subject token for an impersonated or delegated token. - GetAllowedClientIDsForTokenExchange() []string + // TokenExchangeAllowed checks if the subject token client allows the specified client + // to perform the exchange + TokenExchangeAllowed(client fosite.Client) bool } diff --git a/handler/rfc8693/refresh_token_type_handler.go b/handler/rfc8693/refresh_token_type_handler.go index 67eeb4e85..38785c72d 100644 --- a/handler/rfc8693/refresh_token_type_handler.go +++ b/handler/rfc8693/refresh_token_type_handler.go @@ -130,19 +130,7 @@ func (c *RefreshTokenTypeHandler) validate(ctx context.Context, request fosite.A // Check if the client is allowed to exchange this token if subjectTokenClient, ok := or.GetClient().(Client); ok { - allowedClientIDs := subjectTokenClient.GetAllowedClientIDsForTokenExchange() - allowed := false - if len(allowedClientIDs) == 0 { - allowed = true - } else { - for _, cid := range allowedClientIDs { - if client.GetID() == cid { - allowed = true - break - } - } - } - + allowed := subjectTokenClient.TokenExchangeAllowed(client) if !allowed { return nil, nil, errors.WithStack(fosite.ErrRequestForbidden.WithHintf( "The OAuth 2.0 client is not permitted to exchange a subject token issued to client %s", tokenClientID)) From 2840d26b91fccc251b965b6e20084240d971f9ec Mon Sep 17 00:00:00 2001 From: Vivek Shankar Date: Sun, 1 Sep 2024 22:10:19 +0800 Subject: [PATCH 07/10] fix: minor changes --- handler/rfc8693/access_token_type_handler.go | 35 ++++++------- .../rfc8693/actor_token_validation_handler.go | 2 + handler/rfc8693/client.go | 3 ++ handler/rfc8693/flow_token_exchange.go | 50 +++++++++++++++---- handler/rfc8693/refresh_token_type_handler.go | 41 ++++++++------- handler/rfc8693/token_exchange_test.go | 18 +++---- 6 files changed, 87 insertions(+), 62 deletions(-) diff --git a/handler/rfc8693/access_token_type_handler.go b/handler/rfc8693/access_token_type_handler.go index fd833ff03..5175977db 100644 --- a/handler/rfc8693/access_token_type_handler.go +++ b/handler/rfc8693/access_token_type_handler.go @@ -14,13 +14,11 @@ import ( "github.com/pkg/errors" ) +var _ fosite.TokenEndpointHandler = (*AccessTokenTypeHandler)(nil) + type AccessTokenTypeHandler struct { - Config fosite.RFC8693ConfigProvider - AccessTokenLifespan time.Duration - RefreshTokenLifespan time.Duration - RefreshTokenScopes []string + Config fosite.Configurator oauth2.CoreStrategy - ScopeStrategy fosite.ScopeStrategy Storage } @@ -69,6 +67,11 @@ func (c *AccessTokenTypeHandler) PopulateTokenEndpointResponse(ctx context.Conte return errorsx.WithStack(fosite.ErrUnknownRequest) } + teConfig, _ := c.Config.(fosite.RFC8693ConfigProvider) + if teConfig == nil { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the config is not of the right type.")) + } + session, _ := request.GetSession().(Session) if session == nil { return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the session is not of the right type.")) @@ -77,7 +80,7 @@ func (c *AccessTokenTypeHandler) PopulateTokenEndpointResponse(ctx context.Conte form := request.GetRequestForm() requestedTokenType := form.Get("requested_token_type") if requestedTokenType == "" { - requestedTokenType = c.Config.GetDefaultRequestedTokenType(ctx) + requestedTokenType = teConfig.GetDefaultRequestedTokenType(ctx) } if requestedTokenType != AccessTokenType { @@ -136,13 +139,6 @@ func (c *AccessTokenTypeHandler) validate(ctx context.Context, request fosite.Ac } } - // Scope check - for _, scope := range request.GetRequestedScopes() { - if !c.ScopeStrategy(or.GetGrantedScopes(), scope) { - return nil, nil, errors.WithStack(fosite.ErrInvalidScope.WithHintf("The subject token is not granted \"%s\" and so this scope cannot be requested.", scope)) - } - } - // Convert to flat session with only access token claims tokenObject := session.AccessTokenClaimsMap() tokenObject["client_id"] = or.GetClient().GetID() @@ -153,7 +149,7 @@ func (c *AccessTokenTypeHandler) validate(ctx context.Context, request fosite.Ac } func (c *AccessTokenTypeHandler) issue(ctx context.Context, request fosite.AccessRequester, response fosite.AccessResponder) error { - request.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(c.AccessTokenLifespan)) + request.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(c.Config.GetAccessTokenLifespan(ctx))) token, signature, err := c.CoreStrategy.GenerateAccessToken(ctx, request) if err != nil { @@ -162,9 +158,9 @@ func (c *AccessTokenTypeHandler) issue(ctx context.Context, request fosite.Acces return err } - issueRefreshToken := c.canIssueRefreshToken(request) + issueRefreshToken := c.canIssueRefreshToken(ctx, request) if issueRefreshToken { - request.GetSession().SetExpiresAt(fosite.RefreshToken, time.Now().UTC().Add(c.RefreshTokenLifespan).Round(time.Second)) + request.GetSession().SetExpiresAt(fosite.RefreshToken, time.Now().UTC().Add(c.Config.GetRefreshTokenLifespan(ctx)).Round(time.Second)) refresh, refreshSignature, err := c.CoreStrategy.GenerateRefreshToken(ctx, request) if err != nil { return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) @@ -184,15 +180,16 @@ func (c *AccessTokenTypeHandler) issue(ctx context.Context, request fosite.Acces response.SetAccessToken(token) response.SetTokenType("bearer") - response.SetExpiresIn(c.getExpiresIn(request, fosite.AccessToken, c.AccessTokenLifespan, time.Now().UTC())) + response.SetExpiresIn(c.getExpiresIn(request, fosite.AccessToken, c.Config.GetAccessTokenLifespan(ctx), time.Now().UTC())) response.SetScopes(request.GetGrantedScopes()) return nil } -func (c *AccessTokenTypeHandler) canIssueRefreshToken(request fosite.Requester) bool { +func (c *AccessTokenTypeHandler) canIssueRefreshToken(ctx context.Context, request fosite.Requester) bool { // Require one of the refresh token scopes, if set. - if len(c.RefreshTokenScopes) > 0 && !request.GetGrantedScopes().HasOneOf(c.RefreshTokenScopes...) { + scopes := c.Config.GetRefreshTokenScopes(ctx) + if len(scopes) > 0 && !request.GetGrantedScopes().HasOneOf(scopes...) { return false } // Do not issue a refresh token to clients that cannot use the refresh token grant type. diff --git a/handler/rfc8693/actor_token_validation_handler.go b/handler/rfc8693/actor_token_validation_handler.go index ecc2b5add..75268e337 100644 --- a/handler/rfc8693/actor_token_validation_handler.go +++ b/handler/rfc8693/actor_token_validation_handler.go @@ -11,6 +11,8 @@ import ( "github.com/pkg/errors" ) +var _ fosite.TokenEndpointHandler = (*ActorTokenValidationHandler)(nil) + type ActorTokenValidationHandler struct{} // HandleTokenEndpointRequest implements https://tools.ietf.org/html/rfc6749#section-4.3.2 diff --git a/handler/rfc8693/client.go b/handler/rfc8693/client.go index 48a265901..1494cb8d5 100644 --- a/handler/rfc8693/client.go +++ b/handler/rfc8693/client.go @@ -15,4 +15,7 @@ type Client interface { // TokenExchangeAllowed checks if the subject token client allows the specified client // to perform the exchange TokenExchangeAllowed(client fosite.Client) bool + // ActorTokenRequired indicates that one of the allowed actor tokens must be provided + // in the request + ActorTokenRequired() bool } diff --git a/handler/rfc8693/flow_token_exchange.go b/handler/rfc8693/flow_token_exchange.go index 0092dad0f..b2455666b 100644 --- a/handler/rfc8693/flow_token_exchange.go +++ b/handler/rfc8693/flow_token_exchange.go @@ -11,11 +11,11 @@ import ( "github.com/pkg/errors" ) +var _ fosite.TokenEndpointHandler = (*TokenExchangeGrantHandler)(nil) + // TokenExchangeGrantHandler is the grant handler for RFC8693 type TokenExchangeGrantHandler struct { - Config fosite.RFC8693ConfigProvider - ScopeStrategy fosite.ScopeStrategy - AudienceMatchingStrategy fosite.AudienceMatchingStrategy + Config fosite.Configurator } // HandleTokenEndpointRequest implements https://tools.ietf.org/html/rfc6749#section-4.3.2 @@ -40,13 +40,20 @@ func (c *TokenExchangeGrantHandler) HandleTokenEndpointRequest(ctx context.Conte return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the session is not of the right type.")) } + teConfig, _ := c.Config.(fosite.RFC8693ConfigProvider) + if teConfig == nil { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the config is not of the right type.")) + } + form := request.GetRequestForm() - configTypesSupported := c.Config.GetTokenTypes(ctx) + configTypesSupported := teConfig.GetTokenTypes(ctx) var supportedSubjectTypes, supportedActorTypes, supportedRequestTypes fosite.Arguments + actorTokenRequired := false if teClient, ok := client.(Client); ok { supportedRequestTypes = fosite.Arguments(teClient.GetSupportedRequestTokenTypes()) supportedActorTypes = fosite.Arguments(teClient.GetSupportedActorTokenTypes()) supportedSubjectTypes = fosite.Arguments(teClient.GetSupportedSubjectTokenTypes()) + actorTokenRequired = teClient.ActorTokenRequired() } // From https://tools.ietf.org/html/rfc8693#section-2.1: @@ -111,12 +118,14 @@ func (c *TokenExchangeGrantHandler) HandleTokenEndpointRequest(ctx context.Conte } } else if actorTokenType != "" { return errors.WithStack(fosite.ErrInvalidRequest.WithHintf("\"actor_token_type\" is not empty even though the \"actor_token\" is empty.")) + } else if actorTokenRequired { + return errors.WithStack(fosite.ErrInvalidRequest.WithHintf("The OAuth 2.0 client must provide an actor token.")) } // check if supported requestedTokenType := form.Get("requested_token_type") if requestedTokenType == "" { - requestedTokenType = c.Config.GetDefaultRequestedTokenType(ctx) + requestedTokenType = teConfig.GetDefaultRequestedTokenType(ctx) } if tt := configTypesSupported[requestedTokenType]; tt == nil { @@ -129,14 +138,30 @@ func (c *TokenExchangeGrantHandler) HandleTokenEndpointRequest(ctx context.Conte } // Check scope - for _, scope := range request.GetRequestedScopes() { - if !c.ScopeStrategy(client.GetScopes(), scope) { + openIDIndex := -1 + for i, scope := range request.GetRequestedScopes() { + if !c.Config.GetScopeStrategy(ctx)(client.GetScopes(), scope) { return errors.WithStack(fosite.ErrInvalidScope.WithHintf("The OAuth 2.0 Client is not allowed to request scope '%s'.", scope)) } + + // making an assumption here that scope=openid is only present once. + // scope=openid makes no sense in the token exchange flow, so we are going + // to remove it. + if scope == "openid" { + openIDIndex = i + } + } + + if openIDIndex > -1 { + requestedScopes := request.GetRequestedScopes() + requestedScopes[openIDIndex] = requestedScopes[len(requestedScopes)-1] + requestedScopes = requestedScopes[:len(requestedScopes)-1] + + request.SetRequestedScopes(requestedScopes) } // Check audience - if err := c.AudienceMatchingStrategy(client.GetAudience(), request.GetRequestedAudience()); err != nil { + if err := c.Config.GetAudienceStrategy(ctx)(client.GetAudience(), request.GetRequestedAudience()); err != nil { // TODO: Need to convert to using invalid_target return err } @@ -155,13 +180,18 @@ func (c *TokenExchangeGrantHandler) PopulateTokenEndpointResponse(ctx context.Co return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the session is not of the right type.")) } + teConfig, _ := c.Config.(fosite.RFC8693ConfigProvider) + if teConfig == nil { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the config is not of the right type.")) + } + form := request.GetRequestForm() requestedTokenType := form.Get("requested_token_type") if requestedTokenType == "" { - requestedTokenType = c.Config.GetDefaultRequestedTokenType(ctx) + requestedTokenType = teConfig.GetDefaultRequestedTokenType(ctx) } - configTypesSupported := c.Config.GetTokenTypes(ctx) + configTypesSupported := teConfig.GetTokenTypes(ctx) if tt := configTypesSupported[requestedTokenType]; tt == nil { return errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf( "\"%s\" token type is not supported as a \"%s\".", requestedTokenType, "requested_token_type")) diff --git a/handler/rfc8693/refresh_token_type_handler.go b/handler/rfc8693/refresh_token_type_handler.go index 38785c72d..760f39b43 100644 --- a/handler/rfc8693/refresh_token_type_handler.go +++ b/handler/rfc8693/refresh_token_type_handler.go @@ -14,12 +14,11 @@ import ( "github.com/pkg/errors" ) +var _ fosite.TokenEndpointHandler = (*RefreshTokenTypeHandler)(nil) + type RefreshTokenTypeHandler struct { - Config fosite.RFC8693ConfigProvider - RefreshTokenLifespan time.Duration - RefreshTokenScopes []string + Config fosite.Configurator oauth2.CoreStrategy - ScopeStrategy fosite.ScopeStrategy Storage } @@ -73,10 +72,15 @@ func (c *RefreshTokenTypeHandler) PopulateTokenEndpointResponse(ctx context.Cont return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the session is not of the right type.")) } + teConfig, _ := c.Config.(fosite.RFC8693ConfigProvider) + if teConfig == nil { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the config is not of the right type.")) + } + form := request.GetRequestForm() requestedTokenType := form.Get("requested_token_type") if requestedTokenType == "" { - requestedTokenType = c.Config.GetDefaultRequestedTokenType(ctx) + requestedTokenType = teConfig.GetDefaultRequestedTokenType(ctx) } if requestedTokenType != RefreshTokenType { @@ -137,13 +141,6 @@ func (c *RefreshTokenTypeHandler) validate(ctx context.Context, request fosite.A } } - // Scope check - for _, scope := range request.GetRequestedScopes() { - if !c.ScopeStrategy(or.GetGrantedScopes(), scope) { - return nil, nil, errors.WithStack(fosite.ErrInvalidScope.WithHintf("The subject token is not granted \"%s\" and so this scope cannot be requested.", scope)) - } - } - // Convert to flat session with only access token claims tokenObject := session.AccessTokenClaimsMap() tokenObject["client_id"] = or.GetClient().GetID() @@ -154,24 +151,26 @@ func (c *RefreshTokenTypeHandler) validate(ctx context.Context, request fosite.A } func (c *RefreshTokenTypeHandler) issue(ctx context.Context, request fosite.AccessRequester, response fosite.AccessResponder) error { - request.GetSession().SetExpiresAt(fosite.RefreshToken, time.Now().UTC().Add(c.RefreshTokenLifespan).Round(time.Second)) + request.GetSession().SetExpiresAt(fosite.RefreshToken, time.Now().UTC().Add(c.Config.GetRefreshTokenLifespan(ctx)).Round(time.Second)) refresh, refreshSignature, err := c.CoreStrategy.GenerateRefreshToken(ctx, request) if err != nil { - return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) + return errorsx.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } - if refreshSignature != "" { - if err := c.Storage.CreateRefreshTokenSession(ctx, refreshSignature, request.Sanitize([]string{})); err != nil { - if rollBackTxnErr := storage.MaybeRollbackTx(ctx, c.Storage); rollBackTxnErr != nil { - err = rollBackTxnErr - } - return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) + if refreshSignature == "" { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Unable to generate the refresh token signature")) + } + + if err := c.Storage.CreateRefreshTokenSession(ctx, refreshSignature, request.Sanitize([]string{})); err != nil { + if rollBackTxnErr := storage.MaybeRollbackTx(ctx, c.Storage); rollBackTxnErr != nil { + err = rollBackTxnErr } + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } response.SetAccessToken(refresh) response.SetTokenType("N_A") - response.SetExpiresIn(c.getExpiresIn(request, fosite.RefreshToken, c.RefreshTokenLifespan, time.Now().UTC())) + response.SetExpiresIn(c.getExpiresIn(request, fosite.RefreshToken, c.Config.GetRefreshTokenLifespan(ctx), time.Now().UTC())) response.SetScopes(request.GetGrantedScopes()) return nil diff --git a/handler/rfc8693/token_exchange_test.go b/handler/rfc8693/token_exchange_test.go index a00987439..09cffa9c0 100644 --- a/handler/rfc8693/token_exchange_test.go +++ b/handler/rfc8693/token_exchange_test.go @@ -79,19 +79,13 @@ func TestAccessTokenExchangeImpersonation(t *testing.T) { } genericTEHandler := &TokenExchangeGrantHandler{ - Config: config, - ScopeStrategy: config.ScopeStrategy, - AudienceMatchingStrategy: config.AudienceMatchingStrategy, + Config: config, } accessTokenHandler := &AccessTokenTypeHandler{ - Config: config, - AccessTokenLifespan: 5 * time.Minute, - RefreshTokenLifespan: 5 * time.Minute, - RefreshTokenScopes: []string{"offline"}, - CoreStrategy: coreStrategy, - ScopeStrategy: config.ScopeStrategy, - Storage: store, + Config: config, + CoreStrategy: coreStrategy, + Storage: store, } customJWTHandler := &CustomJWTTypeHandler{ @@ -194,7 +188,7 @@ func TestAccessTokenExchangeImpersonation(t *testing.T) { // err = nil continue - } else if err != nil { + } else { break } } @@ -221,7 +215,7 @@ func TestAccessTokenExchangeImpersonation(t *testing.T) { // err = nil continue - } else if err != nil { + } else { break } } From ab318ffb78fefb657277b7aefe759efddbf04153 Mon Sep 17 00:00:00 2001 From: Vivek Shankar Date: Sun, 1 Sep 2024 22:27:49 +0800 Subject: [PATCH 08/10] fix: add compose functions --- compose/compose_rfc8693.go | 63 +++++++++++++++++++ handler/rfc8693/access_token_type_handler.go | 2 +- .../rfc8693/actor_token_validation_handler.go | 2 +- handler/rfc8693/client.go | 2 +- handler/rfc8693/custom_jwt_type_handler.go | 22 +++++-- handler/rfc8693/flow_token_exchange.go | 2 +- handler/rfc8693/id_token_type_handler.go | 2 +- handler/rfc8693/refresh_token_type_handler.go | 2 +- handler/rfc8693/session.go | 2 +- handler/rfc8693/storage.go | 2 +- handler/rfc8693/token_exchange_test.go | 2 +- handler/rfc8693/token_type.go | 2 +- handler/rfc8693/token_type_jwt.go | 2 +- 13 files changed, 90 insertions(+), 17 deletions(-) create mode 100644 compose/compose_rfc8693.go diff --git a/compose/compose_rfc8693.go b/compose/compose_rfc8693.go new file mode 100644 index 000000000..dd91c525c --- /dev/null +++ b/compose/compose_rfc8693.go @@ -0,0 +1,63 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package compose + +import ( + "github.com/ory/fosite" + "github.com/ory/fosite/handler/oauth2" + "github.com/ory/fosite/handler/openid" + "github.com/ory/fosite/handler/rfc8693" + "github.com/ory/fosite/token/jwt" +) + +// RFC8693AccessTokenTypeHandlerFactory creates a access token type handler. +func RFC8693AccessTokenTypeHandlerFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { + return &rfc8693.AccessTokenTypeHandler{ + CoreStrategy: strategy.(oauth2.CoreStrategy), + Storage: storage.(rfc8693.Storage), + Config: config, + } +} + +// RFC8693RefreshTokenTypeHandlerFactory creates a refresh token type handler. +func RFC8693RefreshTokenTypeHandlerFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { + return &rfc8693.RefreshTokenTypeHandler{ + CoreStrategy: strategy.(oauth2.CoreStrategy), + Storage: storage.(rfc8693.Storage), + Config: config, + } +} + +// RFC8693ActorTokenValidationHandlerFactory creates a actor token validation handler. +func RFC8693ActorTokenValidationHandlerFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { + return &rfc8693.ActorTokenValidationHandler{} +} + +// RFC8693CustomJWTTypeHandlerFactory creates a custom JWT token type handler. +func RFC8693CustomJWTTypeHandlerFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { + return &rfc8693.CustomJWTTypeHandler{ + JWTStrategy: strategy.(jwt.Signer), + Storage: storage.(rfc8693.Storage), + Config: config, + } +} + +// RFC8693TokenExchangeGrantHandlerFactory creates the request validation handler for token exchange. This should be the first +// in the list. +func RFC8693TokenExchangeGrantHandlerFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { + return &rfc8693.TokenExchangeGrantHandler{ + Config: config, + } +} + +// RFC8693IDTokenTypeHandlerFactory creates a ID token type handler. +func RFC8693IDTokenTypeHandlerFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { + return &rfc8693.IDTokenTypeHandler{ + JWTStrategy: strategy.(jwt.Signer), + Storage: storage.(rfc8693.Storage), + Config: config, + IssueStrategy: strategy.(openid.OpenIDConnectTokenStrategy), + ValidationStrategy: strategy.(openid.OpenIDConnectTokenValidationStrategy), + } +} diff --git a/handler/rfc8693/access_token_type_handler.go b/handler/rfc8693/access_token_type_handler.go index 5175977db..936b5430e 100644 --- a/handler/rfc8693/access_token_type_handler.go +++ b/handler/rfc8693/access_token_type_handler.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Ory Corp +// Copyright © 2024 Ory Corp // SPDX-License-Identifier: Apache-2.0 package rfc8693 diff --git a/handler/rfc8693/actor_token_validation_handler.go b/handler/rfc8693/actor_token_validation_handler.go index 75268e337..e65c0a889 100644 --- a/handler/rfc8693/actor_token_validation_handler.go +++ b/handler/rfc8693/actor_token_validation_handler.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Ory Corp +// Copyright © 2024 Ory Corp // SPDX-License-Identifier: Apache-2.0 package rfc8693 diff --git a/handler/rfc8693/client.go b/handler/rfc8693/client.go index 1494cb8d5..d94b1db30 100644 --- a/handler/rfc8693/client.go +++ b/handler/rfc8693/client.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Ory Corp +// Copyright © 2024 Ory Corp // SPDX-License-Identifier: Apache-2.0 package rfc8693 diff --git a/handler/rfc8693/custom_jwt_type_handler.go b/handler/rfc8693/custom_jwt_type_handler.go index f276d309b..3651ca55d 100644 --- a/handler/rfc8693/custom_jwt_type_handler.go +++ b/handler/rfc8693/custom_jwt_type_handler.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Ory Corp +// Copyright © 2024 Ory Corp // SPDX-License-Identifier: Apache-2.0 package rfc8693 @@ -16,7 +16,7 @@ import ( ) type CustomJWTTypeHandler struct { - Config fosite.RFC8693ConfigProvider + Config fosite.Configurator JWTStrategy jwt.Signer Storage } @@ -32,8 +32,13 @@ func (c *CustomJWTTypeHandler) HandleTokenEndpointRequest(ctx context.Context, r return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the session is not of the right type.")) } + teConfig, _ := c.Config.(fosite.RFC8693ConfigProvider) + if teConfig == nil { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the config is not of the right type.")) + } + form := request.GetRequestForm() - tokenTypes := c.Config.GetTokenTypes(ctx) + tokenTypes := teConfig.GetTokenTypes(ctx) actorTokenType := tokenTypes[form.Get("actor_token_type")] subjectTokenType := tokenTypes[form.Get("subject_token_type")] if actorTokenType != nil && actorTokenType.GetType(ctx) == JWTTokenType { @@ -75,13 +80,18 @@ func (c *CustomJWTTypeHandler) PopulateTokenEndpointResponse(ctx context.Context return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the session is not of the right type.")) } + teConfig, _ := c.Config.(fosite.RFC8693ConfigProvider) + if teConfig == nil { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to perform token exchange because the config is not of the right type.")) + } + form := request.GetRequestForm() requestedTokenType := form.Get("requested_token_type") if requestedTokenType == "" { - requestedTokenType = c.Config.GetDefaultRequestedTokenType(ctx) + requestedTokenType = teConfig.GetDefaultRequestedTokenType(ctx) } - tokenTypes := c.Config.GetTokenTypes(ctx) + tokenTypes := teConfig.GetTokenTypes(ctx) tokenType := tokenTypes[requestedTokenType] if tokenType == nil || tokenType.GetType(ctx) != JWTTokenType { return nil @@ -106,7 +116,7 @@ func (c *CustomJWTTypeHandler) CanHandleTokenEndpointRequest(ctx context.Context return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:token-exchange") } -func (c *CustomJWTTypeHandler) validate(ctx context.Context, request fosite.AccessRequester, tokenType fosite.RFC8693TokenType, token string) (map[string]interface{}, error) { +func (c *CustomJWTTypeHandler) validate(ctx context.Context, _ fosite.AccessRequester, tokenType fosite.RFC8693TokenType, token string) (map[string]interface{}, error) { jwtType, _ := tokenType.(*JWTType) if jwtType == nil { diff --git a/handler/rfc8693/flow_token_exchange.go b/handler/rfc8693/flow_token_exchange.go index b2455666b..d334e1502 100644 --- a/handler/rfc8693/flow_token_exchange.go +++ b/handler/rfc8693/flow_token_exchange.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Ory Corp +// Copyright © 2024 Ory Corp // SPDX-License-Identifier: Apache-2.0 package rfc8693 diff --git a/handler/rfc8693/id_token_type_handler.go b/handler/rfc8693/id_token_type_handler.go index 6c46f9c11..9b9bf9c1c 100644 --- a/handler/rfc8693/id_token_type_handler.go +++ b/handler/rfc8693/id_token_type_handler.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Ory Corp +// Copyright © 2024 Ory Corp // SPDX-License-Identifier: Apache-2.0 package rfc8693 diff --git a/handler/rfc8693/refresh_token_type_handler.go b/handler/rfc8693/refresh_token_type_handler.go index 760f39b43..6c46c8908 100644 --- a/handler/rfc8693/refresh_token_type_handler.go +++ b/handler/rfc8693/refresh_token_type_handler.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Ory Corp +// Copyright © 2024 Ory Corp // SPDX-License-Identifier: Apache-2.0 package rfc8693 diff --git a/handler/rfc8693/session.go b/handler/rfc8693/session.go index 5378e2db9..d2538279a 100644 --- a/handler/rfc8693/session.go +++ b/handler/rfc8693/session.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Ory Corp +// Copyright © 2024 Ory Corp // SPDX-License-Identifier: Apache-2.0 package rfc8693 diff --git a/handler/rfc8693/storage.go b/handler/rfc8693/storage.go index ebc85c2da..3626c413c 100644 --- a/handler/rfc8693/storage.go +++ b/handler/rfc8693/storage.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Ory Corp +// Copyright © 2024 Ory Corp // SPDX-License-Identifier: Apache-2.0 package rfc8693 diff --git a/handler/rfc8693/token_exchange_test.go b/handler/rfc8693/token_exchange_test.go index 09cffa9c0..352c68af6 100644 --- a/handler/rfc8693/token_exchange_test.go +++ b/handler/rfc8693/token_exchange_test.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Ory Corp +// Copyright © 2024 Ory Corp // SPDX-License-Identifier: Apache-2.0 package rfc8693_test diff --git a/handler/rfc8693/token_type.go b/handler/rfc8693/token_type.go index d415f6981..85ec5efb3 100644 --- a/handler/rfc8693/token_type.go +++ b/handler/rfc8693/token_type.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Ory Corp +// Copyright © 2024 Ory Corp // SPDX-License-Identifier: Apache-2.0 package rfc8693 diff --git a/handler/rfc8693/token_type_jwt.go b/handler/rfc8693/token_type_jwt.go index fa8d5757f..e00ca272b 100644 --- a/handler/rfc8693/token_type_jwt.go +++ b/handler/rfc8693/token_type_jwt.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Ory Corp +// Copyright © 2024 Ory Corp // SPDX-License-Identifier: Apache-2.0 package rfc8693 From 8def4cb98a7644e61bef9f674494b2c2d91d17ac Mon Sep 17 00:00:00 2001 From: Vivek Shankar Date: Sun, 1 Sep 2024 22:39:37 +0800 Subject: [PATCH 09/10] fix: use default uuid package --- handler/rfc8693/token_exchange_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/handler/rfc8693/token_exchange_test.go b/handler/rfc8693/token_exchange_test.go index 352c68af6..3b7764ddc 100644 --- a/handler/rfc8693/token_exchange_test.go +++ b/handler/rfc8693/token_exchange_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/pborman/uuid" + "github.com/google/uuid" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -109,7 +109,7 @@ func TestAccessTokenExchangeImpersonation(t *testing.T) { handlers: []fosite.TokenEndpointHandler{genericTEHandler, accessTokenHandler}, areq: &fosite.AccessRequest{ Request: fosite.Request{ - ID: uuid.New(), + ID: uuid.New().String(), Client: store.Clients["my-client"], Form: url.Values{ "subject_token_type": []string{rfc8693.AccessTokenType}, @@ -135,7 +135,7 @@ func TestAccessTokenExchangeImpersonation(t *testing.T) { handlers: []fosite.TokenEndpointHandler{genericTEHandler, accessTokenHandler, customJWTHandler}, areq: &fosite.AccessRequest{ Request: fosite.Request{ - ID: uuid.New(), + ID: uuid.New().String(), Client: store.Clients["my-client"], Form: url.Values{ "subject_token_type": []string{jwtName}, From 6f898fff4de9aab823af0e0097a767aa81a2cdce Mon Sep 17 00:00:00 2001 From: Vivek Shankar Date: Sun, 1 Sep 2024 22:41:23 +0800 Subject: [PATCH 10/10] fix: use the new HMACSHA strategy --- handler/rfc8693/token_exchange_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handler/rfc8693/token_exchange_test.go b/handler/rfc8693/token_exchange_test.go index 3b7764ddc..b70c2ef5d 100644 --- a/handler/rfc8693/token_exchange_test.go +++ b/handler/rfc8693/token_exchange_test.go @@ -73,7 +73,7 @@ func TestAccessTokenExchangeImpersonation(t *testing.T) { DefaultRequestedTokenType: AccessTokenType, } - coreStrategy := &oauth2.HMACSHAStrategy{ + coreStrategy := &oauth2.HMACSHAStrategyUnPrefixed{ Enigma: &hmac.HMACStrategy{Config: config}, Config: config, }