Token Exchange Flows

Use the token exchange flows to exchange access and ID tokens for impersonated or delegated access and ID tokens, as explained in the OAuth 2.0 Token Exchange specification. For implementation details and use case examples, see OAuth 2.0 Token Exchange.

Example Prerequisites

The example procedure in this section assumes the following configuration:

  • Identity Cloud is configured as an OAuth 2.0/OpenID Connect provider in a realm called mySubRealm, and it is also configured for token exchange.

  • Two clients are registered in Identity Cloud. One is the client that will create the subject token, and the other is the client that will request the token exchange.

    • customerConfidentialClient, the client that will make the requests for the subject tokens, has the following configuration:

      • Client secret: forgerock

      • Client type: Confidential

      • Redirect URIs: https://httpbin.org/anything

      • Scopes:read write openid

      • Token Endpoint Authentication Method:client_secret_post

        This is not required; confidential clients can authenticate in several ways, but client_secret_post makes the examples easier to read.

      • Response Type:code

        This is not required; depending on the flow you use to obtain the tokens, you will need a different response type configuration. This example assumes you will use the Authorization Code grant flow.

        Configure the OAuth 2.0 provider accordingly to the response type.

    • serviceConfidentialClient, the client that will make the token exchange requests, has the following configuration:

      • Client secret: forgerock

      • Client type: Confidential

      • Redirect URIs: https://httpbin.org/anything

      • Response Type:code

        This is not required; depending on the flow you use to obtain the tokens, you will need a different response type configuration. This example assumes you will use the Authorization Code grant flow.

        Configure the OAuth 2.0 provider accordingly to the response type.

      • Scopes: read write transfer

        The procedure will use the transfer scope to extend the scope of a token, which is why the client of the subject token does not have it configured.

      • Token Endpoint Authentication Method:client_secret_post

        This is not required; confidential clients can authenticate in several ways, but client_secret_post makes the examples easier to read.

      • Token Exchange Auth Level:10

  • The ForgerockDemo and ForgerockDemo2 identities are registered in Identity Cloud.


The following procedure demonstrates how to exchange tokens for both impersonation and delegation cases:

To Exchange Tokens
  1. Ensure you have configured Identity Cloud as per the "Example Prerequisites".

  2. Obtain an access token and an ID token for the ForgerockDemo/customerConfidentialClient user and client combination. Use, for example, the "Authorization Code Grant". Do not request the transfer scope; you will use it later in the procedure to expand the scopes of the exchanged token.

    Introspect the access token and retrieve the ID token information to check that they have the may_act claim.

    If they do not, review the "Configuring Identity Cloud for Token Exchange" section.

    {
        "active": true,
        "scope": "read write",
        "realm": "/mySubRealm",
        "client_id": "customerConfidentialClient",
        "user_id": "ForgerockDemo",
        "username":"ForgerockDemo",
        "token_type": "Bearer",
        "exp": 1610552102,
        "sub": "(usr!ForgerockDemo)",
        "subname": "ForgerockDemo",
        "iss": "https://openam.example.com:8443/openam/oauth2/mySubRealm",
        "auth_level": 0,
        "authGrantId": "Zcw9MEANrbPjz4tuDLBfzmYrZP0",
        "may_act": {
            "client_id": "serviceConfidentialClient",
            "sub": "(usr!ForgerockDemo2)"
        },
        "auditTrackingId": "961c12ad-0a56-471e-9017-036f3cb873ce-571823"
    }

    Note the value of the client_id and sub claims.

    {
        "at_hash": "XAhNBCa7Utuc5dujUUA5mQ",
        "sub": "(usr!ForgerockDemo)",
        "auditTrackingId": "961c12ad-0a56-471e-9017-036f3cb873ce-576398",
        "iss": "https://openam.example.com:8443/openam/oauth2/mySubRealm",
        "tokenName": "id_token",
        "nonce": "123abc",
        "sid": "I0GdWDfy1qhahDl1PpEA0v5LDspul+qW70biBhetUCk=",
        "aud": "customerConfidentialClient",
        "c_hash": "0EEiPQxhwezCGa2bPgKYDQ",
        "acr": "0",
        "org.forgerock.openidconnect.ops": "Sj07ATq01pVwzF7Kqop0ZuCCxFg",
        "s_hash": "bKE9UspwyIPg8LsQHkJaiQ",
        "azp": "customerConfidentialClient",
        "auth_time": 1610549052,
        "realm": "/mySubRealm",
        "may_act": {
            "client_id": "serviceConfidentialClient",
            "sub": "(usr!ForgerockDemo2)"
        },
        "exp": 1610552698,
        "tokenType": "JWTToken",
        "iat": 1610549098
    }

    Note the value of the aud and sub claims.

    Impersonation and delegation examples will use one of these tokens as the subject token.

  3. Obtain an access token and an ID token for the ForgerockDemo2/ customerConfidentialClient user and client combination.

    Delegation examples will use one of these tokens as the actor token.

    Use the examples and the information from the previous step, if needed.

  4. To perform a token exchange, the client makes an HTTP POST call to the authorization server's token endpoint. Identity Cloud will issue a delegation token if the request includes an actor token, and an impersonation token otherwise.

    The request must contain, at least, the following parameters:

    • grant_type=urn:ietf:params:oauth:grant-type:token-exchange

    • subject_token=token_to_be_exchanged

      For this example, this is the access token obtained for the ForgerockDemo/customerConfidentialClient user and client combination.

    • subject_token_type=type_of_subject_token

      The value of the subject_token_type parameter is one of the following, depending of the token you are exchanging:

      • urn:ietf:params:oauth:token-type:access_token

      • urn:ietf:params:oauth:token-type:id_token

    • requested_token_type=type_of_request_token

      The value of the requested_token_type parameter is one of the following, depending of the token you require:

      • urn:ietf:params:oauth:token-type:access_token

      • urn:ietf:params:oauth:token-type:id_token

    For delegation use cases, include the actor token as follows:

    • actor_token=token_that_acts_on_behalf_of_the_subject

      For this example, this is the access token obtained for the ForgerockDemo2/customerConfidentialClient user and client combination.

    • actor_token_type=type_of_actor_token

      The value of the actor_token_type parameter is one of the following, depending of the token you are exchanging:

      • urn:ietf:params:oauth:token-type:access_token

      • urn:ietf:params:oauth:token-type:id_token

    Confidential clients can authenticate to the /oauth2/access_token endpoint in several ways. This example uses the following parameters:

    • client_id=client_requesting_impersonation

    • client_secret=client_secret

    For more information, see OAuth 2.0 Client Authentication and "/oauth2/access_token".

    The client that makes the request must be the one authorized in the may_act claim of the subject token. In delegation cases, the subject of the actor token must also match the subject authorized in may_act claim of the subject token.

    The following is an example of a request for an impersonation token that exchanges an access token for another access token:

    $ curl --request POST \
    --data "client_id=serviceConfidentialClient" \
    --data "client_secret=forgerock" \
    --data "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
    --data "scope=read write transfer" \
    --data "subject_token=HTVnsWhZhmyM13b-fMsbUqowBqQ" \
    --data "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
    --data "requested_token_type=urn:ietf:params:oauth:token-type:access_token" \
    "https://openam.example.com:8443/openam/oauth2/realms/root/realms/mySubRealm/access_token"

    The following is an example of a request for a delegation token that exchanges two access tokens for an ID token:

    $ curl --request POST \
    --data "client_id=serviceConfidentialClient" \
    --data "client_secret=forgerock" \
    --data "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
    --data "scope=read write transfer" \
    --data "subject_token=HTVnsWhZhmyM13b-fMsbUqowBqQ" \
    --data "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
    --data "actor_token=f08f1fcf-3ecb-4120-820d-fb71e3f51c04" \
    --data "actor_token_type=urn:ietf:params:oauth:token-type:access_token" \
    --data "requested_token_type=urn:ietf:params:oauth:token-type:id_token" \
    "https://openam.example.com:8443/openam/oauth2/realms/root/realms/mySubRealm/access_token"

    Note that the scopes parameter has been added in both cases. Like in the regular OAuth 2.0/OpenID Connect flows, this parameter is not required, since it can be derived using the same mechanisms Identity Cloud uses for regular OAuth 2.0/OpenID Connect flows (see About Scopes).

    For example purposes, the call requested all the scopes configured in the subject token plus the transfer scope, which is only available to this client. This is an example of expanding scopes.

    The openid scope is not needed to request ID tokens.

    Caution

    There is no interaction with the user during token exchange, and therefore, it is impossible for Identity Cloud to request consent about the expanded scopes/claims. It is the responsibility of the client application to ensure that either the user has consented beforehand, or that the expanded scope/claim is unrelated to the user's resources.

    Clients should not request more scopes/claims than those required to perform the task requested by the user.

    To restrict the scopes/claims in the exchanged token, request fewer scopes/claims than those available to the subject token. For example, --data "scopes=write".

    Tip

    You can also use exchanged tokens as subject tokens.

    • Access token to access token // ID token to access token :

      {
          "access_token": "sq2MACbRu6eEGGvoO4_rAEOnNOQ",
          "refresh_token": "wFAPJKb57e-4EGBduApXqFOvyDw",
          "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
          "scope": "read write",
          "token_type": "Bearer",
          "expires_in": 3599
      }

      Refresh tokens are only issued if Identity Cloud is configured to issue them.

    • Access token to ID token // ID token to ID token:

      {
          "access_token": "eyJ0eXAiOiJKV1QiLCJraWQiOiJ3VTNpZkl...",
          "refresh_token": null,
          "issued_token_type": "urn:ietf:params:oauth:token-type:id_token",
          "scope": "profile email",
          "token_type": "Bearer",
          "expires_in": 3599
      }

      ID tokens are issued in the access_token object. Refresh tokens are never issued with ID tokens.

    • Invalid token exchange

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

      HTTP 400. The subject token, the actor token, or both, are invalid, of the wrong type, or cannot be exchanged due to the constrains imposed in the may_act claim.

    • Subject token type is required

      {
          "error_description": "Subject token type is required.",
          "error": "invalid_request"
      }

      HTTP 400. The subject token type has not been included in the request.

  5. Introspect the token you obtained, and compare it with the subject token you exchanged to understand the differences.

    Access token-specific claims are not populated unless the subject token contained that information. The only exception is the authentication level, as explained below:

    {
        "active": true,
        "scope": "read transfer write", (1)
        "realm": "/mySubRealm",
        "client_id": "serviceConfidentialClient", (2)
        "user_id": "ForgerockDemo",
        "username": "ForgerockDemo",
        "token_type": "Bearer",
        "exp": 1610551390,
        "sub": "(usr!ForgerockDemo)", (3)
        "subname": "ForgerockDemo",
        "iss": "https://openam.example.com:8443/openam/oauth2/mySubRealm",
        "auth_level": 10, (4)
        "authGrantId": "VokhnJCdMcvK-opywivldpKJyuk",
        "act": { (5)
            "sub": "(usr!ForgerockDemo2)"
        },
        "may_act": { (6)
            "client_id": "serviceConfidentialClient",
            "sub": "(usr!ForgerockDemo2)"
        },
        "auditTrackingId": "961c12ad-0a56-471e-9017-036f3cb873ce-565818"
    }

    1

    The exchanged token has the scopes requested in the token exchange call. In this case, the exchanged token has the same scopes as the subject token, and the additional transfer token, which expands the subject token's authorization.

    2

    The client ID has changed to that of the client that requested the exchanged token.

    3

    The subject of the exchanged token is the same as that of the subject token. The same is true for any other claims related to the subject, such as username and subname.

    The subject claim is in the format (type!subject), where:

    • subject is the identifier of the user/identity, or the name of the OAuth 2.0/OpenID Connect client that is the subject of the token.

    • type can be one of the following:

      • age. Specifies that the subject is an OAuth 2.0/OpenID Connect-related user-agent or client. For example, an OAuth 2.0 client, a Remote Consent Service agent, and a Web and Java Agent internal client.

      • usr. Specifies that the subject is a user/identity.

    For example, (usr!demo), or (age!myOAuth2Client).

    The value of the subname claim matches the value of the subject portion of the sub claim.

    4

    When exchanging an access token for an access token, the authentication level is copied across. When exchanging an ID token for an access token, the authentication level is the value of the Token Exchange Auth Level field in the client profiles of the serviceConfidentialClient client.

    5

    The act claim is only present in delegated tokens, and contains the subject of the actor token.

    When using delegated tokens as subject tokens, you may see the act claim nesting. For example:

    "act": {
      "sub": "(usr!ForgerockDemo2)",
      "act": {
        "sub": "(usr!ForgerockDemo3)"
      }
    }

    6

    The may_act claim will only be present if the token meet the criteria established in the May Act script.

    ID token-specific claims, such as acr and nonce, are not populated unless the subject token contained that information.

    {
       "sub": "(usr!ForgerockDemo)", (1)
       "auditTrackingId": "961c12ad-0a56-471e-9017-036f3cb873ce-607580",
       "iss": "https://openam.example.com:8443/openam/oauth2/mySubRealm",
       "tokenName": "id_token",
       "nonce": "123abc",
       "sid": "I0GdWDfy1qhahDl1PpEA0v5LDspul+qW70biBhetUCk=",
       "aud": "serviceConfidentialClient", (2)
       "acr": "0",
       "org.forgerock.openidconnect.ops": "eVzAOcG-UAyweUaGeEhggWDDWvU",
       "azp": "serviceConfidentialClient",
       "auth_time": 1610552780,
       "realm": "/mySubRealm",
       "may_act": {  (3)
           "client_id": "serviceConfidentialClient",
           "sub": "(usr!ForgerockDemo2)"
       },
       "act": { (4)
           "sub": "(usr!ForgerockDemo2)"
       },
       "exp": 1610556942,
       "tokenType": "JWTToken",
       "iat": 1610553342
       }

    1

    The subject of the exchanged token is the same as that of the subject token.

    2

    The audience is the client ID that requested the exchanged token. Note that the authorized party (azp) is also this client ID.

    3

    The may_act claim will only be present if the token meet the criteria established in the May Act script.

    4

    The act claim is only present in delegated tokens, and contains the subject of the actor token.

    When using delegated tokens as subject tokens, you may see the act claim nesting. For example:

    "act": {
      "sub": "(usr!ForgerockDemo2)",
      "act": {
        "sub": "(usr!ForgerockDemo3)"
      }
    }
Read a different version of :