IG 7.2.0

OAuth 2.0 token exchange

The following sections describe how to exchange an OAuth 2.0 access token for another access token, with AM as an authorization server. Other authorization providers can be used instead of AM.

Token exchange requires a subject token and provides an issued token. The subject token is the original access token, obtained using the OAuth 2.0/OpenID Connect flow. The issued token is provided in exchange for the subject token.

The token exchange can be conducted only by an OAuth 2.0 client that "may act" on the subject token, as configured in the authorization service.

This example is a typical scenario for token impersonation. For more information, see OAuth 2.0 token exchange in AM’s OAuth 2.0 guide.

The following sequence diagram shows the flow of information during token exchange between IG and AM:

sso
This procedure uses the Resource Owner Password Credentials grant type. According to information in the The OAuth 2.0 Authorization Framework, minimize use of this grant type and utilize other grant types whenever possible.

Before you start, prepare AM, IG, and the sample application as described in Example installation for this guide.

  1. Set up AM:

    1. (From AM 6.5.3) Select Services > Add a Service, and add a Validation Service with the following Valid goto URL Resources:

      • http://ig.example.com:8080/*

      • http://ig.example.com:8080/*?*

    2. Select Applications > Agents > Identity Gateway, and add an agent with the following values:

      • Agent ID: ig_agent

      • Password: password

      • Token Introspection: Realm Only

    3. Select Services > Add a Service, and add an OAuth2 Provider service with the following values:

      • OAuth2 Access Token May Act Script : OAuth2 May Act Script

      • OAuth2 ID Token May Act Script : OAuth2 May Act Script

    4. Select Scripts > OAuth2 May Act Script, and replace the example script with the following script:

      import org.forgerock.json.JsonValue
      token.setMayAct(
          JsonValue.json(JsonValue.object(
              JsonValue.field("client_id", "serviceConfidentialClient"))))

      This script adds a may_act claim to the token, indicating that the OAuth 2.0 client, serviceConfidentialClient, may act to exchange the subject token in the impersonation use case.

    5. Add an OAuth 2.0 Client to request OAuth 2.0 access tokens:

      1. Select Applications > OAuth 2.0 > Clients, and add a client with the following values:

        • Client ID : client-application

        • Client secret : password

        • Scope(s) : mail, employeenumber

      2. On the Advanced tab, select Grant Types : Resource Owner Password Credentials.

    6. Add an OAuth 2.0 client to perform the token exchange:

      1. Select Applications > OAuth 2.0 > Clients, and add a client with the following values:

        • Client ID : serviceConfidentialClient

        • Client secret : password

        • Scope(s) : mail, employeenumber

      2. On the Advanced tab, select:

        • Grant Types : Token Exchange

        • Token Endpoint Authentication Methods : client_secret_post

  2. Set up IG:

    1. Set an environment variable for the serviceConfidentialClient password:

      $ export CLIENT_SECRET_ID='cGFzc3dvcmQ='
    2. Set an environment variable for the IG agent password, and then restart IG:

      $ export AGENT_SECRET_ID='cGFzc3dvcmQ='

      The password is retrieved by a SystemAndEnvSecretStore, and must be base64-encoded.

    3. Add the following route to IG to exchange the access token:

      • Linux

      • Windows

      $HOME/.openig/config/routes/token-exchange.json
      appdata\OpenIG\config\routes\token-exchange.json
      {
        "name": "token-exchange",
        "baseURI": "http://app.example.com:8081",
        "condition": "${find(request.uri.path, '^/token-exchange')}",
        "heap": [
          {
            "name": "SystemAndEnvSecretStore-1",
            "type": "SystemAndEnvSecretStore"
          },
          {
            "name": "AmService-1",
            "type": "AmService",
            "config": {
              "agent": {
                "username": "ig_agent",
                "passwordSecretId": "agent.secret.id"
              },
              "secretsProvider": "SystemAndEnvSecretStore-1",
              "url": "http://am.example.com:8088/openam/",
              "version": "7.2"
            }
          },
          {
            "name": "ExchangeHandler",
            "type": "Chain",
            "capture": "all",
            "config": {
              "handler": "ForgeRockClientHandler",
              "filters": [
                {
                  "type": "ClientSecretBasicAuthenticationFilter",
                  "config": {
                    "clientId": "serviceConfidentialClient",
                    "clientSecretId": "client.secret.id",
                    "secretsProvider" : "SystemAndEnvSecretStore-1"
                  }
                }
              ]
            }
          },
          {
            "name": "ExchangeFailureHandler",
            "type": "StaticResponseHandler",
            "capture": "all",
            "config": {
              "status": 400,
              "entity": "{\"${contexts.oauth2Failure.error}\": \"${contexts.oauth2Failure.description}\"}",
              "headers": {
                "Content-Type": [
                  "application/json"
                ]
              }
            }
          }
        ],
        "handler": {
          "type": "Chain",
          "config": {
            "filters": [
              {
                "name": "oauth2TokenExchangeFilter",
                "type": "OAuth2TokenExchangeFilter",
                "config": {
                  "amService": "AmService-1",
                  "endpointHandler": "ExchangeHandler",
                  "subjectToken": "#{request.entity.form['subject_token'][0]}",
                  "scopes": ["mail"],
                  "failureHandler": "ExchangeFailureHandler"
                }
              }
            ],
            "handler": {
              "type": "StaticResponseHandler",
              "config": {
                "status": 200,
                "headers": {
                  "content-type": [
                    "application/json"
                  ]
                },
                "entity": "{\"access_token\": \"${contexts.oauth2TokenExchange.issuedToken}\", \"issued_token_type\": \"${contexts.oauth2TokenExchange.issuedTokenType}\"}"
              }
            }
          }
        }
      }

      Notice the following features of the route:

      • The route matches requests to /token-exchange

      • IG reads the subjectToken from the request entity.

      • The StaticResponseHandler returns an issued token.

  3. Test the setup:

    1. In a terminal window, use a curl command similar to the following to retrieve an access token, which is the subject token:

      $ subjecttoken=$(curl -s \
      --user "client-application:password" \
      --data "grant_type=password&username=demo&password=Ch4ng31t&scope=mail%20employeenumber" \
      http://am.example.com:8088/openam/oauth2/access_token | jq -r ".access_token") \
      && echo $subjecttoken
      
      hc-...c6A
    2. Introspect the subject token at the AM introspection endpoint:

      $ curl --location \
      --request POST 'http://am.example.com:8088/openam/oauth2/realms/root/introspect' \
      --header 'Content-Type: application/x-www-form-urlencoded' \
      --data-urlencode "token=${subjecttoken}" \
      --data-urlencode 'client_id=client-application' \
      --data-urlencode 'client_secret=password'
      
      Decoded access_token: {
        "active": true,
        "scope": "employeenumber mail",
        "realm": "/",
        "client_id": "client-application",
        "user_id": "demo",
        "username": "demo",
        "token_type": "Bearer",
        "exp": 1626796888,
        "sub": "(usr!demo)",
        "subname": "demo",
        "iss": "http://am.example.com:8088/openam/oauth2",
        "auth_level": 0,
        "authGrantId": "W-j...E1E",
        "may_act": {
          "client_id": "serviceConfidentialClient"
        },
        "auditTrackingId": "4be...169"
      }

      Note that in the subject token, the client_id is client-application, and the scopes are employeenumber and mail. The may_act claim indicates that serviceConfidentialClient is authorized to exchange this token.

    3. Exchange the subject token for an issued token:

      $ issuedtoken=$(curl \
      --location \
      --request POST 'http://ig.example.com:8080/token-exchange' \
      --header 'Content-Type: application/x-www-form-urlencoded' \
      --data "subject_token=${subjecttoken}" | jq -r ".access_token") \
      && echo $issuedtoken
      
      F8e...Q3E
    4. Introspect the issued token at the AM introspection endpoint:

      $ curl --location \
      --request POST 'http://am.example.com:8088/openam/oauth2/realms/root/introspect' \
      --header 'Content-Type: application/x-www-form-urlencoded' \
      --data-urlencode "token=${issuedtoken}" \
      --data-urlencode 'client_id=serviceConfidentialClient' \
      --data-urlencode 'client_secret=password'
      
      {
        "active": true,
        "scope": "mail",
        "realm": "/",
        "client_id": "serviceConfidentialClient",
        "user_id": "demo",
        "username": "demo",
        "token_type": "Bearer",
        "exp": 1629200490,
        "sub": "(usr!demo)",
        "subname": "demo",
        "iss": "http://am.example.com:8088/openam/oauth2",
        "auth_level": 0,
        "authGrantId": "aYK...EPA",
        "may_act": {
          "client_id": "serviceConfidentialClient"
        },
        "auditTrackingId": "814...367"
      }

      Note that in the issued token, the client_id is serviceConfidentialClient, and the only the scope is mail.

Copyright © 2010-2023 ForgeRock, all rights reserved.