Token exchange
A back-end service holds a user's access token and needs a token of its own to call a further API for that user. It exchanges the user's token at the token endpoint: the subject stays the user, the client becomes the service. With an actor_token as well, the new token also records who is acting (act), which the demo host's delegation policy (DemoDelegationPolicy) allows for the machine client only. The exchanging client is the demo host's exchange-client.
Follows: RFC 8693.
Recorded from the demo host (src/Fdyay.IdentityServer.Host) by the TokenExchangeFlow test on every CI run. A value in braces, such as {access_token_1}, is one that differs on every run; the same placeholder is the same value wherever it appears. See how these pages are made.
sequenceDiagram
participant Service
participant AuthorizationServer as Authorization Server
participant Machine
Note over Service: The service holds an access token issued to alice<br/>through the web client, from the authorization code<br/>flow.
Service->>AuthorizationServer: POST /connect/token
AuthorizationServer-->>Service: 200 OK
Machine->>AuthorizationServer: POST /connect/token
AuthorizationServer-->>Machine: 200 OK
Service->>AuthorizationServer: POST /connect/token
AuthorizationServer-->>Service: 200 OK
The service holds an access token issued to alice through the
webclient, from the authorization code flow.
1. Impersonation: the service exchanges the user's token for its own
subject_token_type says what is being presented. The response's issued_token_type says what came back.
Service → Authorization Server
POST /connect/token HTTP/1.1
Authorization: Basic ZXhjaGFuZ2UtY2xpZW50OnNlY3JldA==
Content-Type: application/x-www-form-urlencoded
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange
&subject_token={access_token_1}
&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token
&scope=api
{access_token_1} decoded — the header, then the payload; the signature is not shown:
{
"alg": "RS256",
"kid": "{key_id_1}",
"typ": "at+jwt"
}
{
"aud": [
"api-resource"
],
"iss": "http://localhost",
"iat": {iat},
"nbf": {iat},
"exp": {iat + 3600},
"jti": "{jti_1}",
"client_id": "web",
"scope": "api openid profile",
"sub": "alice",
"auth_time": {auth_time},
"idp": "local",
"amr": [
"pwd"
],
"sid": "{session_id_1}"
}
Authorization Server → Service
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Cache-Control: no-store, no-cache, max-age=0
Pragma: no-cache
{
"access_token": "{access_token_2}",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "api",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token"
}
{access_token_2} decoded — the header, then the payload; the signature is not shown:
{
"alg": "RS256",
"kid": "{key_id_1}",
"typ": "at+jwt"
}
{
"aud": [
"api-resource"
],
"iss": "http://localhost",
"iat": {iat},
"nbf": {iat},
"exp": {iat + 3600},
"jti": "{jti_2}",
"client_id": "exchange-client",
"scope": "api",
"sub": "alice"
}
2. The acting party gets a token of its own
The machine client, by client credentials: it is the party that will act for alice.
Machine → Authorization Server
POST /connect/token HTTP/1.1
Authorization: Basic bWFjaGluZTpzZWNyZXQ=
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&scope=api
Authorization Server → Machine
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Cache-Control: no-store, no-cache, max-age=0
Pragma: no-cache
{
"access_token": "{access_token_3}",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "api"
}
{access_token_3} decoded — the header, then the payload; the signature is not shown:
{
"alg": "RS256",
"kid": "{key_id_1}",
"typ": "at+jwt"
}
{
"aud": [
"api-resource"
],
"iss": "http://localhost",
"iat": {iat},
"nbf": {iat},
"exp": {iat + 3600},
"jti": "{jti_3}",
"client_id": "machine",
"scope": "api"
}
3. Delegation: the exchange names the actor
The new token is still about alice, and its act claim says the machine client is acting for her.
Service → Authorization Server
POST /connect/token HTTP/1.1
Authorization: Basic ZXhjaGFuZ2UtY2xpZW50OnNlY3JldA==
Content-Type: application/x-www-form-urlencoded
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange
&subject_token={access_token_1}
&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token
&actor_token={access_token_3}
&actor_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token
&scope=api
Authorization Server → Service
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Cache-Control: no-store, no-cache, max-age=0
Pragma: no-cache
{
"access_token": "{access_token_4}",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "api",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token"
}
{access_token_4} decoded — the header, then the payload; the signature is not shown:
{
"alg": "RS256",
"kid": "{key_id_1}",
"typ": "at+jwt"
}
{
"aud": [
"api-resource"
],
"iss": "http://localhost",
"iat": {iat},
"nbf": {iat},
"exp": {iat + 3600},
"jti": "{jti_4}",
"client_id": "exchange-client",
"scope": "api",
"sub": "alice",
"act": {
"client_id": "machine"
}
}