// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package oidc import ( "context" "net/url" "github.com/coreos/go-oidc/v3/oidc" "github.com/ory/fosite" "github.com/ory/fosite/compose" "github.com/ory/fosite/handler/oauth2" "github.com/ory/fosite/handler/openid" "github.com/pkg/errors" ) const ( tokenTypeAccessToken = "urn:ietf:params:oauth:token-type:access_token" //nolint:gosec tokenTypeJWT = "urn:ietf:params:oauth:token-type:jwt" //nolint:gosec pinnipedTokenExchangeScope = "pinniped:request-audience" //nolint:gosec ) type stsParams struct { subjectAccessToken string requestedAudience string } func TokenExchangeFactory(config *compose.Config, storage interface{}, strategy interface{}) interface{} { return &TokenExchangeHandler{ idTokenStrategy: strategy.(openid.OpenIDConnectTokenStrategy), accessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), accessTokenStorage: storage.(oauth2.AccessTokenStorage), } } type TokenExchangeHandler struct { idTokenStrategy openid.OpenIDConnectTokenStrategy accessTokenStrategy oauth2.AccessTokenStrategy accessTokenStorage oauth2.AccessTokenStorage } var _ fosite.TokenEndpointHandler = (*TokenExchangeHandler)(nil) func (t *TokenExchangeHandler) HandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) error { if !t.CanHandleTokenEndpointRequest(requester) { return errors.WithStack(fosite.ErrUnknownRequest) } return nil } func (t *TokenExchangeHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error { // Skip this request if it's for a different grant type. if err := t.HandleTokenEndpointRequest(ctx, requester); err != nil { return errors.WithStack(err) } // Validate the basic RFC8693 parameters we support. params, err := t.validateParams(requester.GetRequestForm()) if err != nil { return errors.WithStack(err) } // Validate the incoming access token and lookup the information about the original authorize request. originalRequester, err := t.validateAccessToken(ctx, requester, params.subjectAccessToken) if err != nil { return errors.WithStack(err) } // Require that the incoming access token has the pinniped:request-audience and OpenID scopes. if !originalRequester.GetGrantedScopes().Has(pinnipedTokenExchangeScope) { return errors.WithStack(fosite.ErrAccessDenied.WithHintf("Missing the %q scope.", pinnipedTokenExchangeScope)) } if !originalRequester.GetGrantedScopes().Has(oidc.ScopeOpenID) { return errors.WithStack(fosite.ErrAccessDenied.WithHintf("Missing the %q scope.", oidc.ScopeOpenID)) } // Use the original authorize request information, along with the requested audience, to mint a new JWT. responseToken, err := t.mintJWT(ctx, originalRequester, params.requestedAudience) if err != nil { return errors.WithStack(err) } // Format the response parameters according to RFC8693. responder.SetAccessToken(responseToken) responder.SetTokenType("N_A") responder.SetExtra("issued_token_type", tokenTypeJWT) return nil } func (t *TokenExchangeHandler) mintJWT(ctx context.Context, requester fosite.Requester, audience string) (string, error) { downscoped := fosite.NewAccessRequest(requester.GetSession()) downscoped.Client.(*fosite.DefaultClient).ID = audience return t.idTokenStrategy.GenerateIDToken(ctx, downscoped) } func (t *TokenExchangeHandler) validateParams(params url.Values) (*stsParams, error) { var result stsParams // Validate some required parameters. result.requestedAudience = params.Get("audience") if result.requestedAudience == "" { return nil, fosite.ErrInvalidRequest.WithHint("Missing 'audience' parameter.") } result.subjectAccessToken = params.Get("subject_token") if result.subjectAccessToken == "" { return nil, fosite.ErrInvalidRequest.WithHint("Missing 'subject_token' parameter.") } // Validate some parameters with hardcoded values we support. if params.Get("subject_token_type") != tokenTypeAccessToken { return nil, fosite.ErrInvalidRequest.WithHintf("Unsupported 'subject_token_type' parameter value, must be %q.", tokenTypeAccessToken) } if params.Get("requested_token_type") != tokenTypeJWT { return nil, fosite.ErrInvalidRequest.WithHintf("Unsupported 'requested_token_type' parameter value, must be %q.", tokenTypeJWT) } // Validate that none of these unsupported parameters were sent. These are optional and we do not currently support them. for _, param := range []string{ "resource", "scope", "actor_token", "actor_token_type", } { if params.Get(param) != "" { return nil, fosite.ErrInvalidRequest.WithHintf("Unsupported parameter %q.", param) } } return &result, nil } func (t *TokenExchangeHandler) validateAccessToken(ctx context.Context, requester fosite.AccessRequester, accessToken string) (fosite.Requester, error) { // Look up the access token's stored session data. signature := t.accessTokenStrategy.AccessTokenSignature(accessToken) originalRequester, err := t.accessTokenStorage.GetAccessTokenSession(ctx, signature, requester.GetSession()) if err != nil { // The access token was not found, or there was some other error while reading it. return nil, fosite.ErrRequestUnauthorized.WithWrap(err).WithHint("Invalid 'subject_token' parameter value.") } // Validate the access token using its stored session data, which includes its expiration time. if err := t.accessTokenStrategy.ValidateAccessToken(ctx, originalRequester, accessToken); err != nil { return nil, errors.WithStack(err) } return originalRequester, nil } func (t *TokenExchangeHandler) CanSkipClientAuth(_ fosite.AccessRequester) bool { return false } func (t *TokenExchangeHandler) CanHandleTokenEndpointRequest(requester fosite.AccessRequester) bool { return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:token-exchange") }