Token exchange grant handler SPI

1. Introduction

OAuth 2.0 token exchange (RFC 8693) is a generic mechanism for client applications to obtain a token from the token endpoint of an authorisation server by presenting a grant that is also a token.

Example token exchange scenarios:

  • Obtaining tokens for backend and downstream services.
  • Obtaining tokens in a delegation (on-behalf-of) scenario.
  • Obtaining tokens in a impersonation (act-as) scenario.
  • Obtaining tokens using a credential from a different security domain.
  • Obtaining tokens for special purposes.

The decision what scope and properties to give the newly issued token is based on the authorisation encoded in the submitted token, and may also take into account the identity and registered metadata of the requesting client.

The nature of the OAuth 2.0 token exchange grant is similar to the JWT assertion grants, but giving architects complete freedom to specify the type, encodings and other properties of the submitted and issued tokens. In the common case the submitted and the issued tokens are both access tokens, but they can also be combinations of a generic JWT, an ID token, a SAML assertion or something else.

1.1 Example token exchange request

This example illustrates the request for an access token with scope https://api.example.com/get-customer-address using another token with scope https://api.example.com/order-delivery.

Note that with token exchange the submitted token can originate from a different security domain (provided it's trusted).

The client submits the original token in the subject_token form parameter, indicating its type (access token) in the subject_token_type parameter. The grant_type and scope parameters have their standard meanings from the core OAuth 2.0 spec.

The form parameters also include a private_key_jwt client authentication.

POST /token HTTP/1.1
Host: c2id.com
Content-Type: application/x-www-form-urlencoded

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange
 &subject_token=Eexungahcaetaizoh7ingait3Ur9ya1b
 &subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token
 &scope=https%3A%2F%2Fapi.example.com%2Fget-customer-address
 &client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
 &client_assertion=eyJhbGciOiJSUzI1NiIsImtpZCI6IjIyIn...

The Connect2id server authenticates the client and checks the submitted token. If the exchange is permitted it returns a successful token response:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  "access_token"     : "2YotnFZFEjr1zCsicMWpAA",
  "token_type"       : "DPoP",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "expires_in"       : 60,
  "scope"            : "https://api.example.com/get-customer-address"
}

Example error response if the submitted token is found to be invalid / expired, or the exchange was denied due to a violated policy:

HTTP/1.1 400 Bad Request
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  "error"             : "invalid_request",
  "error_description" : "Invalid token exchange request"
}

1.2 Prerequisites

For a client to make a token exchange request it must be registered for the urn:ietf:params:oauth:grant-type:token-exchange grant type.

1.3 Security considerations

Whenever possible, a client performing token exchange should be registered as confidential (with authenticating credentials) rather than public. This will limit the scope of potential token exploitation in case of a leak of a token that is exchange capable.

Clients that request highly privileged tokens should use a strong authentication method, such as private_key_jwt or one based on the mutual TLS profile.

2. Token exchange grant handler SPI

The Connect2id server comes with a flexible plugin interface (SPI) for verifying token exchange grants and specifying the properties of the access token to be issued.

Features of the grant handler SPI:

  • Provides access to the token request parameters, including the subject_token and the optional actor_token.

  • If the subject_token is a valid locally issued access token, self-contained or identifier-based, the Connect2id server makes its introspected authorisation details available to the handler.

  • The client_id and registered metadata of the requesting client is also made available, which may be as inputs in determining the properties of the issued access token (scope values, etc).

  • Provides optional methods for initialising, configuring and shutting down the handler.

At present the token exchange handler SPI is capable of ingesting access tokens (locally or externally issued) as well as other arbitrary tokens. The issued token can be a local access token. Future Connect2id server releases may add support for issuing refresh tokens and generic JWTs.

Access token JWT Refresh token ID token SAML 1.1 SAML 2.0
Submitted token
Issued token

How does the Connect2id server process token exchange requests?

  1. The Connect2id server validates the client_id and its credentials (for a confidential client). On failure to authenticate an invalid_client error is returned.

  2. The Connect2id server then invokes the token exchange grant handler plugin interface (SPI), which has 2 tasks to perform:

    1. Verify that the submitted subject_token and optional actor_token are valid and carry the necessary authorisation for an exchange. If not the handler must return an invalid_request error.
    2. Determine the scope of the access token to issue to the client. This is up to the handler's policy, and will typically be based on the scope of the submitted token, with the client metadata as potentially additional decision input. The grant handler can also set other token properties, such as expiration, encoding, etc.
  3. If the handler accepted the grant as valid it returns an access token spec which the Connect2id server will then use to mint the actual token and return it in a appropriate token response to the client.

The token exchange handler SPI was introduced in v12.14.

3. Available implementations

3.1 Web-based handler

The Connect2id server comes with a ready SPI implementation which delegates processing of the token exchange grant to a web service.

Benefits of the web service approach:

  • You can use any language or framework to implement the subject token verification and the authorisation logic, leveraging your existing IT resources.

  • You can update the handler logic without having to restart or otherwise affect the availability of the Connect2id server.

The web service interface is simple -- a single HTTP POST, using JSON to convey the request and response parameters.

The web-based handler connector and API are provided as an open source (Apache 2.0) package.

Git repohttps://bitbucket.org/connect2id/grant-handlers-web

3.2.1 Configuration

The Connect2id server is shipped with a base configuration for the token exchange grant handler located in WEB-INF/tokenExchangeGrantHandlerWebAPI.properties. It can be replaced or overridden with Java system properties.

Content of the shipped handler configuration file, with explanation of the properties:

# Enables / disables the web-based token exchange grant handler. Disabled
# (false) by default.
op.grantHandler.tokenExchange.webAPI.enable=false

# The endpoint URL of the web API.
op.grantHandler.tokenExchange.webAPI.url=

# Access token of type Bearer for the web API.
op.grantHandler.tokenExchange.webAPI.apiAccessToken=

# Names of custom token request parameters to include as top-level members in
# the handler request JSON object. The default value is none.
op.grantHandler.tokenExchange.customParams=

# Names of client metadata fields to include in the "client" JSON object which
# is part of handler request. If not specified the following client metadata
# fields are included by default: "scope", "application_type",
# "sector_identifier_uri", "subject_type", "default_max_age",
# "require_auth_time", "default_acr_values", "data".
op.grantHandler.tokenExchange.clientMetadata=

# The accepted subject token types, as comma and / or space separated list.
# The default value is "*" (asterisk) indicating all types.
op.grantHandler.tokenExchange.webAPI.subjectToken.types=*

# Enables / disables introspection of subject tokens of type access token as
# locally issued access tokens. If enabled the introspection takes place before
# invoking the web API and on success will include the token introspection
# response from the Connect2id server. The default value is false (no
# introspection of local access tokens).
op.grantHandler.tokenExchange.webAPI.subjectToken.accessTokenIntrospection.local.enable=false

# One or more optional remote introspections to perform on subject tokens of
# type access token. If specified the introspection takes place sequentially
# before invoking the web API and on success at any one of the configured
# introspection endpoints will include its token introspection response.
#
# The configured introspection endpoints must be compliant with RFC 7662.
#
# If a client authentication method is specified, it must be
# "client_secret_basic" and include a client ID and client secret for the
# caller. Other authentication methods may be supported in future.
#
# If no client authentication method is specified the default value is "none"
# (no client authentication).
#
# An HTTP connect and response read timeout, in milliseconds, may be specified.
# The default timeouts are zero, implying none or determined by the underlying
# HTTP client.
#
# Example with two remote introspection endpoints:
#
# op.grantHandler.tokenExchange.webAPI.subjectToken.accessTokenIntrospection.remote.1.endpoint=https://op.example.com/token/introspect
# op.grantHandler.tokenExchange.webAPI.subjectToken.accessTokenIntrospection.remote.1.authMethod=client_secret_basic
# op.grantHandler.tokenExchange.webAPI.subjectToken.accessTokenIntrospection.remote.1.clientID=123
# op.grantHandler.tokenExchange.webAPI.subjectToken.accessTokenIntrospection.remote.1.clientSecret=airo8Muawi5tuuLo
# op.grantHandler.tokenExchange.webAPI.subjectToken.accessTokenIntrospection.remote.1.connectTimeout=1000
# op.grantHandler.tokenExchange.webAPI.subjectToken.accessTokenIntrospection.remote.1.readTimeout=2000
# op.grantHandler.tokenExchange.webAPI.subjectToken.accessTokenIntrospection.remote.2.endpoint=https://oauth.example.org/token/introspect
# op.grantHandler.tokenExchange.webAPI.subjectToken.accessTokenIntrospection.remote.2.authMethod=none
# op.grantHandler.tokenExchange.webAPI.subjectToken.accessTokenIntrospection.remote.2.connectTimeout=0
# op.grantHandler.tokenExchange.webAPI.subjectToken.accessTokenIntrospection.remote.2.readTimeout=0

# Enables / disables the "must pass" requirement for introspected subject
# tokens of type access token. The default value is true, meaning access tokens
# must successfully pass local or remote introspection before the Connect2id
# server invokes the web API, else an "invalid_request" error will be returned
# to the client. If false the web API will also be invoked for access tokens
# that failed the configured introspection, for example to let the web-based
# handler perform its own introspection.
op.grantHandler.tokenExchange.webAPI.subjectToken.accessTokenIntrospection.mustPass=

# One or more optional JWK set URLs to perform JWT signature verification of
# subject types of type explicit JWT, access token or ID token. If specified
# the JWK sets URLs will be used sequentially to retrieve key candidates for
# the signature verification. After retrieval the JWK sets are cached locally.
#
# An HTTP connect and response read timeout, in milliseconds, may be specified.
# The default timeouts are zero, implying none or determined by the underlying
# HTTP client.
#
# Example with two JWK set URLs:
#
# op.grantHandler.tokenExchange.webAPI.subjectToken.jwtVerification.1.jwkSetURI=https://op.example.com/jwks.json
# op.grantHandler.tokenExchange.webAPI.subjectToken.jwtVerification.1.connectTimeout=1000
# op.grantHandler.tokenExchange.webAPI.subjectToken.jwtVerification.1.readTimeout=1000
# op.grantHandler.tokenExchange.webAPI.subjectToken.jwtVerification.2.jwkSetURI=https://sts.example.org/jwks.json

# Enables / disables the "must pass" requirement for JWT verification of
# subject tokens of type explicit JWT, access token or ID token. The default
# value is true, meaning JWTs must successfully pass the signature verification
# and optional claims validation before the Connect2id server invokes the web
# API, else an "invalid_request" error will be returned to the client. If false
# the web API will also be invoked for JWTs that failed the verification, for
# example to let the web-based handler perform its own.
op.grantHandler.tokenExchange.webAPI.subjectToken.jwtVerification.mustPass=

# The accepted actor token types, as comma and / or space separated list. An
# "*" (asterisk) indicates all types. The default value is none (empty).
op.grantHandler.tokenExchange.webAPI.actorToken.types=*

# The accepted requested token types, as comma and / or space separated list.
# The default value is "*" (asterisk) indicating all types.
op.grantHandler.tokenExchange.webAPI.requestedToken.types=*

# The HTTP connect timeout, in milliseconds. The default value is zero, implies
# none or determined by the underlying HTTP client.
op.grantHandler.tokenExchange.webAPI.connectTimeout=250

# The HTTP response read timeout, in milliseconds. The default value is zero,
# implies none or determined by the underlying HTTP client.
op.grantHandler.tokenExchange.webAPI.readTimeout=500

To set up the handler the following minimal configuration must be provided:

  • Enabling of the handler.
  • The URL of the web service.
  • A long-lived bearer token to access the web service.

Example minimal configuration:

op.grantHandler.tokenExchange.webAPI.enable=true
op.grantHandler.tokenExchange.webAPI.url=https://login.example.com/client-credentials-grant-handler
op.grantHandler.tokenExchange.webAPI.apiAccessToken=ztucZS1ZyFKgh0tUEruUtiSTXhnexmd6

Example configuration limiting the accepted subject tokens to access tokens only and enabling their local introspection:

op.grantHandler.tokenExchange.webAPI.enable=true
op.grantHandler.tokenExchange.webAPI.url=https://login.example.com/client-credentials-grant-handler
op.grantHandler.tokenExchange.webAPI.apiAccessToken=ztucZS1ZyFKgh0tUEruUtiSTXhnexmd6
op.grantHandler.tokenExchange.webAPI.subjectToken.types=urn:ietf:params:oauth:token-type:access_token
op.grantHandler.tokenExchange.webAPI.subjectToken.accessTokenIntrospection.local.enable=true

Example configuration limiting the accepted subject tokens to access tokens only and enabling JWT signature and expiration verification for them:

op.grantHandler.tokenExchange.webAPI.enable=true
op.grantHandler.tokenExchange.webAPI.url=https://login.example.com/client-credentials-grant-handler
op.grantHandler.tokenExchange.webAPI.apiAccessToken=ztucZS1ZyFKgh0tUEruUtiSTXhnexmd6
op.grantHandler.tokenExchange.webAPI.subjectToken.types=urn:ietf:params:oauth:token-type:access_token
op.grantHandler.tokenExchange.webAPI.subjectToken.jwtVerification.1.jwkSetURI=https://op.example.com/jwks.json

To verify the handler loading and configuration check the server logs for lines with the OP7106 and TEWxxxx identifiers.

Example logged configuration:

INFO MAIN [main] [OP7106] Loaded and initialized urn:ietf:params:oauth:grant-type:token-exchange grant handler com.nimbusds.openid.connect.provider.spi.grants.handlers.web.tokenexchange.TokenExchangeGrantDelegator
INFO MAIN [main] [PWW0000] Token exchange grant handler enabled: true
INFO MAIN [main] [PWW0001] Token exchange grant handler: Endpoint URL: http://localhost:45277
INFO MAIN [main] [PWW0002] Token exchange grant handler: Accepted custom token request parameters: []
INFO MAIN [main] [PWW0005] Token exchange grant handler: Included client metadata parameters: [default_acr_values, sector_identifier_uri, scope, default_max_age, application_type, data, require_auth_time, subject_type]
INFO MAIN [main] [PWW0003] Token exchange grant handler: HTTP connect timeout: 150 ms
INFO MAIN [main] [PWW0004] Token exchange grant handler: HTTP read timeout: 250 ms

3.1.2 Web API

For each access token request with a token exchange grant the Connect2id server will identify or authenticate the client according to its type (public or confidential) and check whether the client registration permits use of the grant. Only then will the server call the SPI (the connector to the web service) to process the grant.

The web service is expected to perform the following:

  • Verify the received subject_token and optional actor_token:

    • If access token introspection (RFC 7662) for the subject token was configured and was successfully passed the call to the handler will include the introspection response.

    • If signed JWT verification for the subject token was configured and was successfully passed the call to the handler will include the parsed JWS header and the JWT claims. Note that this verification will only check the JWS signature against the configured JWK set URL and that the token has not expired if an exp claim is present. The handler must ensure the JWT header and claims match the expected profile, for example the RFC 9068 profile of a JWT-encoded access token.

    • If none of the above were configured the web service must perform the entire verification of the subject token.

    • Verify the actor token if accepted or required.

  • Determine if the received subject_token is eligible for exchange. The logic for that can be based on the following inputs:

    • The claims of the verified subject token and optional actor token (if the latter token is accepted or required);

    • The requested scope (if any) and other token request parameters.

    • The client ID and selected client metadata.

  • If the subject_token is eligible return the scope and other properties of the access token to be issued.

The web service interface is specified as follows:

HTTP POST

Header parameters:

  • Authorization The configured bearer access token for the web service (see op.grantHandler.tokenExchange.webAPI.apiAccessToken).

  • Content-Type Set to application/json.

  • Issuer The issuer URL.

Body:

  • A JSON object with the following members:

    • subject_token {string} The subject token.

    • subject_token_type {string} The subject token type.

    • [ subject_token_introspection ] {object} If the subject token was successfully introspected as an active access token:

      • [ endpoint ] {string} The remote token introspection endpoint URL, omitted if the introspection was for a token issued locally by the Connect2id server.
      • response {object} The token introspection response, with the active member being true.
    • [ subject_token_verification ] {object} If the subject token is a signed JWT which signature was successfully verified and is not expired when the exp claim is present (further checks are not performed):

      • jws_header {object} The JWS header.
      • claims {object} The JWT claims.
    • [ actor_token ] {string} The actor token, omitted if none.

    • [ actor_token_type ] {string} The actor token type, omitted if no actor token.

    • [ requested_token_type ] {string} The type of the requested token, omitted if not specified.

    • [ scope ] {string array} The requested scope values, as specified in the access token request, empty array or omitted if none.

    • [ resources ] {string array} The requested target resource URI(s), omitted if none.

    • [ audience ] {string array} The requested token audience, omitted if none. Similar to resources, but with the client providing a logical name instead of a URI for the target resource.

    • client {object} JSON object containing the client_id and selected client metadata according to the op.grantHandler.password.webAPI.clientMetadata configuration. A boolean confidential parameter is also included, to indicate whether the client is confidential or public.

    • [ ... ] {string} Other custom token request parameters according to the op.grantHandler.password.webAPI.customParams configuration.

Success:

  • Code: 200

  • Content-Type: application/json

  • Body: {object} A JSON with object with the following authorisation properties:

    • sub {string} The identifier of the authenticated subject (end-user).

    • issued_token_type {string} Must be set to urn:ietf:params:oauth:token-type:access_token.

    • scope {string array} An array of one or more authorised scope values for the access token. May be different from the originally requested scope values.

    • [ access_token ] {object} Optional access token settings:

      • [ lifetime = 0 ] {integer} The access token lifetime in seconds. If zero or omitted defaults to the configured access token lifetime.
      • [ encoding = "SELF_CONTAINED" ] {"SELF_CONTAINED"|"IDENTIFIER"} The access token encoding. If omitted defaults to self-contained (JWT-encoded).
      • [ audience ] {string array} Optional explicit list of audiences for the access token, omitted if none.
      • [ sub_type = "PUBLIC" ] {"PUBLIC"|"PAIRWISE"} The access token subject type. If PAIRWISE the access token with be issued with a pairwise subject identifier. This requires the audience to be set (if multiple audience values are set the first in the list will used to compute the pairwise identifier). Defaults to PUBLIC.
      • [ encrypt = false ] {true|false} Encryption flag. Applies only to self-contained (JWT-encoded) access tokens. If true the JWT will be encrypted with a shared AES key after signing for confidentiality.
    • [ claims ] {string array} Optional array of names of consented OpenID claims.

      Special keywords and prefixes:

      access_token: - For a non-requested claim, this prefix will cause the claim to be delivered in the issued access token. If the access token is self-contained (JWT) the claim will be added at the top-level. If the access token is identifier-based the claim will appear at the top-level in the token introspection response.

      access_token:uip: - For a non-requested claim, this prefix will cause the claim to be merged into the top-level "uip" (optional preset UserInfo claims) JSON object claim of the access token.

      access_token:dat: - For a non-requested claim, this prefix will cause the claim to be merged into the top-level "dat" (optional data) JSON object claim of the access token.

      verified: - If Identity Assurance is enabled indicates a verified claim. Must be after any other prefixes.

    • [ claims_locales ] {string array} Optional array of the claims locales, omitted if not specified.

    • [ claims_data ] {object} Optional data to be passed in the request to retrieve the consented OpenID claims from the configured source(s). The claims data will be included in a "cld" (claims data) field in the issued access token(s) and in the long-lived authorisations if the consent is persisted. If the claims data must be kept confidential from the client either an identifier access token encoding must be chosen or if a self-contained (JWT) access token is chosen it must be additionally encrypted. An AdvancedClaimsSource SPI implementation can retrieve the claims data JSON object by a call to the ClaimsSourceRequestContext.getClaimsData method.

    • [ preset_claims ] {object} Optional JSON object specifying additional preset OpenID claims to include in the UserInfo response:

      • [ userinfo ] {object} Preset claims to include in the UserInfo response, omitted or empty JSON object if none.
    • [ data ] {object} Optional additional information to be stored in the dat field of the authorisation record and self-contained (JWT-encoded) access tokens.

Errors:

  • 400 Bad Request

    • With an encoded invalid_request or invalid_grant error indicates a subject_token or actor_token that is invalid or not eligible for exchange. See RFC 8693, section 2.2.2 for details.
    • With an encoded invalid_scope error indicates the requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner. See OAuth 2.0, section 5.2 on error responses for details.
    • With an encoded invalid_target error indicates an invalid resource or audience parameter. See RFC 8693, section 2.2.2 for details.
    • Any other bad request.
  • 401 Unauthorized -- With an encoded bearer token error indicates the access token is invalid. See OAuth 2.0 Bearer Token, section 3.1 on error responses for details.

  • 500 Internal Server Error -- Indicates an internal grant processing error.

Example request from a confidential client with ID 123, the subject token was not introspected or verified as JWT:

POST /token-exchange-grant-handler HTTP/1.1
Authorization: Bearer ztucZS1ZyFKgh0tUEruUtiSTXhnexmd6
Content-Type: application/json
Issuer: https://c2id.com

{
  "subject_token"      : "Eexungahcaetaizoh7ingait3Ur9ya1b",
  "subject_token_type" : "urn:ietf:params:oauth:token-type:access_token",
  "scope"              : [ "https://api.example.com/get-customer-address" ],
  "client"             : { "client_id"        : "123",
                           "confidential"     : true,
                           "application_type" : "web" }
}

Example request with a successful introspection of the subject token as an active access token issued by some 3rd party:

POST /token-exchange-grant-handler HTTP/1.1
Authorization: Bearer ztucZS1ZyFKgh0tUEruUtiSTXhnexmd6
Content-Type: application/json
Issuer: https://c2id.com

{
  "subject_token"               : "Eexungahcaetaizoh7ingait3Ur9ya1b",
  "subject_token_type"          : "urn:ietf:params:oauth:token-type:access_token",
  "subject_token_introspection" : { "endpoint" : "https://op.example.com/token/introspect",
                                    "response" : {
                                        "active"    : true,
                                        "sub"       : "164476e0-5c10-4cf0-bf75-b30fec2ba925",
                                        "client_id" : "foo6ulei",
                                        "scope"     : "https://api.example.com/get-customer-address",
                                        "exp"       : 1660654529
                                    } },
  "scope"                       : [ "https://api.example.com/get-customer-address" ],
  "client"                      : { "client_id"        : "123",
                                    "confidential"     : true,
                                    "application_type" : "web" }
}

Example response for a successful token exchange authorisation:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "sub"               : "164476e0-5c10-4cf0-bf75-b30fec2ba925",
  "issued_token_type" : "urn:ietf:params:oauth:token-type:access_token",
  "scope"             : [ "https://api.example.com/get-customer-address" ]
}

Example response indicating an invalid subject token:

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "error"             : "invalid_request",
  "error_description" : "Invalid subject token"
}

4. How to develop your own token exchange grant handler

First, read our general guide for developing, annotating and packaging an SPI-based plugin.

The connector must implement the TokenExchangeGrantHandler SPI defined in the Connect2id server SDK:

Git repohttps://bitbucket.org/connect2id/server-sdk

If the Connect2id server detects an SPI implementation it will log its loading under OP7106.

INFO main MAIN - [OP7106] Loaded and initialized urn:ietf:params:oauth:grant-type:token-exchange grant handler com.nimbusds.openid.connect.provider.spi.grants.handlers.web.tokenexchange.TokenExchangeGrantDelegator

Note, the Connect2id server can load multiple token exchange grant handlers at startup, but only one may be enabled at a time.