OAuth 2.0 token exchange with PingAM
This page describes 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, refer to Token exchange in the AM documentation.
The following sequence diagram shows the flow of information during token exchange between PingGateway and AM:
| This procedure uses the Resource Owner Password Credentials grant type. As suggested in The OAuth 2.0 Authorization Framework, use other grant types whenever possible. |
Before you begin, prepare AM, PingGateway, and the sample application. Learn more in the example installation for this guide.
-
Set up AM:
-
Select Services > Add a Service and add a Validation Service with the following Valid goto URL Resources:
-
https://ig.example.com:8443/* -
https://ig.example.com:8443/*?*
-
-
Register a PingGateway agent with the following values, as described in Register a PingGateway agent in AM:
-
Agent ID:
ig_agent -
Password:
password -
Token Introspection:
Realm OnlyUse secure passwords in a production environment. Consider using a password manager to generate secure passwords.
-
-
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
-
-
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_actclaim to the token, indicating that the OAuth 2.0 client,serviceConfidentialClient, may act to exchange the subject token in the impersonation use case. -
Add an OAuth 2.0 Client to request OAuth 2.0 access tokens:
-
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
-
-
On the Advanced tab, select Grant Types :
Resource Owner Password Credentials.
-
-
Add an OAuth 2.0 client to perform the token exchange:
-
Select Applications > OAuth 2.0 > Clients, and add a client with the following values:
-
Client ID :
serviceConfidentialClient -
Client secret :
password -
Scope(s) :
mail,employeenumber
-
-
On the Advanced tab, select:
-
Grant Types :
Token Exchange -
Token Endpoint Authentication Methods :
client_secret_post
-
-
-
-
Set up PingGateway:
-
Set up PingGateway for HTTPS, as described in Configure PingGateway for TLS (server-side).
-
Set the following environment variables for the serviceConfidentialClient password:
$ export CLIENT_SECRET_ID='cGFzc3dvcmQ=' -
Set an environment variable for the PingGateway agent password, and then restart PingGateway:
$ export AGENT_SECRET_ID='cGFzc3dvcmQ='The password is retrieved by a SystemAndEnvSecretStore, and must be base64-encoded.
-
Add the following route to PingGateway to exchange the access token:
- Linux
-
$HOME/.openig/config/routes/token-exchange.json - Windows
-
%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/" } }, { "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}\"}" } } } } }Source: token-exchange.json
Notice the following features of the route:
-
The route matches requests to
/token-exchange -
PingGateway reads the
subjectTokenfrom the request entity. -
The StaticResponseHandler returns an issued token.
-
-
Test the setup:
-
In a terminal window, use a
curlcommand 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 $subjecttokenOutputhc-...c6A
-
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'OutputDecoded 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_idisclient-application, and the scopes areemployeenumberandmail. Themay_actclaim indicates thatserviceConfidentialClientis authorized to exchange this token. -
Exchange the subject token for an issued token:
$ issuedtoken=$(curl \ --cacert /path/to/secrets/ig.example.com-certificate.pem \ --location \ --request POST 'https://ig.example.com:8443/token-exchange' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data "subject_token=${subjecttoken}" | jq -r ".access_token") \ && echo $issuedtokenOutputF8e...Q3E
-
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'Output{ "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_idisserviceConfidentialClientand the only scope ismail.
-