2022-06-07 23:32:19 +00:00
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
2020-12-09 01:33:08 +00:00
// SPDX-License-Identifier: Apache-2.0
2020-12-08 01:28:51 +00:00
package oidc
import (
"context"
2020-12-09 16:04:58 +00:00
"net/url"
2022-06-07 23:32:19 +00:00
"strings"
2020-12-08 01:28:51 +00:00
2021-01-20 17:54:44 +00:00
"github.com/coreos/go-oidc/v3/oidc"
2020-12-09 16:04:58 +00:00
"github.com/ory/fosite"
"github.com/ory/fosite/compose"
2020-12-08 18:17:03 +00:00
"github.com/ory/fosite/handler/oauth2"
"github.com/ory/fosite/handler/openid"
"github.com/pkg/errors"
2022-07-14 16:51:11 +00:00
"go.pinniped.dev/internal/oidc/clientregistry"
2020-12-09 16:04:58 +00:00
)
2020-12-08 18:17:03 +00:00
2020-12-09 16:04:58 +00:00
const (
2022-07-20 20:55:56 +00:00
tokenExchangeGrantType = "urn:ietf:params:oauth:grant-type:token-exchange" //nolint: gosec
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
2020-12-08 01:28:51 +00:00
)
2020-12-09 16:04:58 +00:00
type stsParams struct {
subjectAccessToken string
requestedAudience string
}
2020-12-08 01:28:51 +00:00
func TokenExchangeFactory ( config * compose . Config , storage interface { } , strategy interface { } ) interface { } {
2020-12-08 18:17:03 +00:00
return & TokenExchangeHandler {
2020-12-09 16:04:58 +00:00
idTokenStrategy : strategy . ( openid . OpenIDConnectTokenStrategy ) ,
accessTokenStrategy : strategy . ( oauth2 . AccessTokenStrategy ) ,
accessTokenStorage : storage . ( oauth2 . AccessTokenStorage ) ,
2020-12-08 18:17:03 +00:00
}
2020-12-08 01:28:51 +00:00
}
type TokenExchangeHandler struct {
2020-12-08 18:17:03 +00:00
idTokenStrategy openid . OpenIDConnectTokenStrategy
accessTokenStrategy oauth2 . AccessTokenStrategy
accessTokenStorage oauth2 . AccessTokenStorage
2020-12-08 01:28:51 +00:00
}
2021-03-01 19:08:41 +00:00
var _ fosite . TokenEndpointHandler = ( * TokenExchangeHandler ) ( nil )
2020-12-08 01:28:51 +00:00
func ( t * TokenExchangeHandler ) HandleTokenEndpointRequest ( ctx context . Context , requester fosite . AccessRequester ) error {
2021-03-01 19:08:41 +00:00
if ! t . CanHandleTokenEndpointRequest ( requester ) {
2020-12-08 18:17:03 +00:00
return errors . WithStack ( fosite . ErrUnknownRequest )
}
2020-12-08 01:28:51 +00:00
return nil
}
func ( t * TokenExchangeHandler ) PopulateTokenEndpointResponse ( ctx context . Context , requester fosite . AccessRequester , responder fosite . AccessResponder ) error {
2020-12-09 16:04:58 +00:00
// Skip this request if it's for a different grant type.
if err := t . HandleTokenEndpointRequest ( ctx , requester ) ; err != nil {
2020-12-08 18:17:03 +00:00
return errors . WithStack ( err )
}
2020-12-09 16:04:58 +00:00
// Validate the basic RFC8693 parameters we support.
params , err := t . validateParams ( requester . GetRequestForm ( ) )
2020-12-08 18:17:03 +00:00
if err != nil {
return errors . WithStack ( err )
}
2020-12-09 16:04:58 +00:00
// 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 )
2020-12-08 18:17:03 +00:00
}
2020-12-09 16:04:58 +00:00
2022-07-20 20:55:56 +00:00
// Check that the currently authenticated client and the client which was originally used to get the access token are the same.
if originalRequester . GetClient ( ) . GetID ( ) != requester . GetClient ( ) . GetID ( ) {
// This error message is copied from the similar check in fosite's flow_authorize_code_token.go.
2022-07-21 20:50:33 +00:00
return errors . WithStack ( fosite . ErrInvalidGrant . WithHint ( "The OAuth 2.0 Client ID from this request does not match the one from the authorize request." ) )
2022-07-20 20:55:56 +00:00
}
// Check that the client is allowed to perform this grant type.
if ! requester . GetClient ( ) . GetGrantTypes ( ) . Has ( tokenExchangeGrantType ) {
// This error message is trying to be similar to the analogous one in fosite's flow_authorize_code_token.go.
2022-07-21 20:50:33 +00:00
return errors . WithStack ( fosite . ErrUnauthorizedClient . WithHintf ( ` The OAuth 2.0 Client is not allowed to use token exchange grant "%s". ` , tokenExchangeGrantType ) )
2022-07-20 20:55:56 +00:00
}
2020-12-16 03:59:57 +00:00
// Require that the incoming access token has the pinniped:request-audience and OpenID scopes.
2020-12-09 23:15:50 +00:00
if ! originalRequester . GetGrantedScopes ( ) . Has ( pinnipedTokenExchangeScope ) {
2020-12-09 19:56:53 +00:00
return errors . WithStack ( fosite . ErrAccessDenied . WithHintf ( "missing the %q scope" , pinnipedTokenExchangeScope ) )
}
2020-12-09 23:15:50 +00:00
if ! originalRequester . GetGrantedScopes ( ) . Has ( oidc . ScopeOpenID ) {
return errors . WithStack ( fosite . ErrAccessDenied . WithHintf ( "missing the %q scope" , oidc . ScopeOpenID ) )
}
2020-12-09 19:56:53 +00:00
2020-12-09 16:04:58 +00:00
// Use the original authorize request information, along with the requested audience, to mint a new JWT.
responseToken , err := t . mintJWT ( ctx , originalRequester , params . requestedAudience )
2020-12-08 18:17:03 +00:00
if err != nil {
return errors . WithStack ( err )
}
2020-12-09 16:04:58 +00:00
// Format the response parameters according to RFC8693.
responder . SetAccessToken ( responseToken )
2020-12-08 18:17:03 +00:00
responder . SetTokenType ( "N_A" )
2020-12-09 19:56:53 +00:00
responder . SetExtra ( "issued_token_type" , tokenTypeJWT )
2020-12-08 01:28:51 +00:00
return nil
}
2020-12-09 16:04:58 +00:00
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 == "" {
2020-12-09 19:56:53 +00:00
return nil , fosite . ErrInvalidRequest . WithHint ( "missing audience parameter" )
2020-12-09 16:04:58 +00:00
}
result . subjectAccessToken = params . Get ( "subject_token" )
if result . subjectAccessToken == "" {
2020-12-09 19:56:53 +00:00
return nil , fosite . ErrInvalidRequest . WithHint ( "missing subject_token parameter" )
2020-12-09 16:04:58 +00:00
}
// Validate some parameters with hardcoded values we support.
if params . Get ( "subject_token_type" ) != tokenTypeAccessToken {
2020-12-09 19:56:53 +00:00
return nil , fosite . ErrInvalidRequest . WithHintf ( "unsupported subject_token_type parameter value, must be %q" , tokenTypeAccessToken )
2020-12-09 16:04:58 +00:00
}
if params . Get ( "requested_token_type" ) != tokenTypeJWT {
2020-12-09 19:56:53 +00:00
return nil , fosite . ErrInvalidRequest . WithHintf ( "unsupported requested_token_type parameter value, must be %q" , tokenTypeJWT )
2020-12-09 16:04:58 +00:00
}
// 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 ) != "" {
2020-12-09 19:56:53 +00:00
return nil , fosite . ErrInvalidRequest . WithHintf ( "unsupported parameter %s" , param )
2020-12-09 16:04:58 +00:00
}
}
2022-06-07 23:32:19 +00:00
// Validate that the requested audience is not one of the reserved strings. All possible requested audience strings
// are subdivided into these classifications:
// 1. pinniped-cli is reserved for the statically defined OAuth client, which is disallowed for this token exchange.
2022-06-13 19:08:11 +00:00
// 2. client.oauth.pinniped.dev-* is reserved to be the names of user-defined dynamic OAuth clients, which is also
2022-06-07 23:32:19 +00:00
// disallowed for this token exchange.
2022-06-13 19:08:11 +00:00
// 3. Anything else matching *.pinniped.dev* is reserved for future use, in case we want to create more
// buckets of names some day, e.g. something.pinniped.dev/*. These names are also disallowed for this
2022-06-07 23:32:19 +00:00
// token exchange.
// 4. Any other string is reserved to conceptually mean the name of a workload cluster (technically, it's the
// configured audience of its Concierge JWTAuthenticator or other OIDC JWT validator). These are the only
// allowed values for this token exchange.
2022-06-13 19:08:11 +00:00
if strings . Contains ( result . requestedAudience , ".pinniped.dev" ) {
return nil , fosite . ErrInvalidRequest . WithHintf ( "requested audience cannot contain '.pinniped.dev'" )
2022-06-07 23:32:19 +00:00
}
2022-07-14 16:51:11 +00:00
if result . requestedAudience == clientregistry . PinnipedCLIClientID {
return nil , fosite . ErrInvalidRequest . WithHintf ( "requested audience cannot equal '%s'" , clientregistry . PinnipedCLIClientID )
2022-06-07 23:32:19 +00:00
}
2020-12-09 16:04:58 +00:00
return & result , nil
}
func ( t * TokenExchangeHandler ) validateAccessToken ( ctx context . Context , requester fosite . AccessRequester , accessToken string ) ( fosite . Requester , error ) {
if err := t . accessTokenStrategy . ValidateAccessToken ( ctx , requester , accessToken ) ; err != nil {
return nil , errors . WithStack ( err )
}
signature := t . accessTokenStrategy . AccessTokenSignature ( accessToken )
originalRequester , err := t . accessTokenStorage . GetAccessTokenSession ( ctx , signature , requester . GetSession ( ) )
if err != nil {
2020-12-17 20:09:19 +00:00
return nil , fosite . ErrRequestUnauthorized . WithWrap ( err ) . WithHint ( "invalid subject_token" )
2020-12-09 16:04:58 +00:00
}
return originalRequester , nil
}
2021-03-01 19:08:41 +00:00
func ( t * TokenExchangeHandler ) CanSkipClientAuth ( _ fosite . AccessRequester ) bool {
return false
}
func ( t * TokenExchangeHandler ) CanHandleTokenEndpointRequest ( requester fosite . AccessRequester ) bool {
2022-07-20 20:55:56 +00:00
return requester . GetGrantTypes ( ) . ExactOne ( tokenExchangeGrantType )
2021-03-01 19:08:41 +00:00
}