Skip to content
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