Table of Contents

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 web client, 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"
  }
}