Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: (rfc8693) Token exchange #821

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
63 changes: 63 additions & 0 deletions compose/compose_rfc8693.go
Original file line number Diff line number Diff line change
@@ -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),
}
}
6 changes: 6 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,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
}
12 changes: 12 additions & 0 deletions config_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,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) {
Expand Down Expand Up @@ -499,3 +503,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
}
5 changes: 5 additions & 0 deletions handler/openid/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
207 changes: 207 additions & 0 deletions handler/rfc8693/access_token_type_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

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"
)

var _ fosite.TokenEndpointHandler = (*AccessTokenTypeHandler)(nil)

type AccessTokenTypeHandler struct {
Config fosite.Configurator
oauth2.CoreStrategy
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(ctx, 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(ctx, request) {
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."))
}

form := request.GetRequestForm()
requestedTokenType := form.Get("requested_token_type")
if requestedTokenType == "" {
requestedTokenType = teConfig.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(ctx context.Context, requester fosite.AccessRequester) bool {
return false
}

// CanHandleTokenEndpointRequest indicates if the token endpoint request can be handled
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")
}

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 {
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))
}
}

// 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.Config.GetAccessTokenLifespan(ctx)))

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(ctx, request)
if issueRefreshToken {
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()))
}

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.Config.GetAccessTokenLifespan(ctx), time.Now().UTC()))
response.SetScopes(request.GetGrantedScopes())

return nil
}

func (c *AccessTokenTypeHandler) canIssueRefreshToken(ctx context.Context, request fosite.Requester) bool {
// Require one of the refresh token scopes, if set.
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.
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())
}
66 changes: 66 additions & 0 deletions handler/rfc8693/actor_token_validation_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package rfc8693

import (
"context"

"github.com/ory/fosite"
"github.com/ory/x/errorsx"
"github.com/pkg/errors"
)

var _ fosite.TokenEndpointHandler = (*ActorTokenValidationHandler)(nil)

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(ctx, 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(ctx context.Context, requester fosite.AccessRequester) bool {
return false
}

// CanHandleTokenEndpointRequest indicates if the token endpoint request can be handled
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")
}
Loading
Loading