Table of Contents

Resource indicators

A client names the API it wants a token for with resource, and gets a token addressed to that API alone. It matters when a scope belongs to more than one API: the demo host's ledger.read is a scope of both https://ledger.demo.example.com and https://orders.demo.example.com, and the token's audience, not its scope, is what decides which of them will accept it. The client is the demo host's resource-machine.

Follows: RFC 8707.

Recorded from the demo host (src/Fdyay.IdentityServer.Host) by the ResourceIndicatorFlow 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 Client
    participant AuthorizationServer as Authorization Server
    Client->>AuthorizationServer: POST /connect/token
    AuthorizationServer-->>Client: 200 OK
    Client->>AuthorizationServer: POST /connect/token
    AuthorizationServer-->>Client: 200 OK
    Client->>AuthorizationServer: POST /connect/token
    AuthorizationServer-->>Client: 400 Bad Request, invalid_target

1. A token for the ledger

The ledger API is registered with RequireResourceIndicator, so it is never one audience among several: a token reaches it only when a client names it.

Client → Authorization Server

POST /connect/token HTTP/1.1
Authorization: Basic cmVzb3VyY2UtbWFjaGluZTpzZWNyZXQ=
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&scope=ledger.read
&resource=https%3A%2F%2Fledger.demo.example.com

Authorization Server → Client

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_1}",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "ledger.read"
}

{access_token_1} decoded — the header, then the payload; the signature is not shown:

{
  "alg": "RS256",
  "kid": "{key_id_1}",
  "typ": "at+jwt"
}
{
  "aud": [
    "https://ledger.demo.example.com"
  ],
  "iss": "http://localhost",
  "iat": {iat},
  "nbf": {iat},
  "exp": {iat + 3600},
  "jti": "{jti_1}",
  "client_id": "resource-machine",
  "scope": "ledger.read"
}

2. A token for the orders API, with the same scope

The scope is narrowed to what the named API exposes, and the audience is that API.

Client → Authorization Server

POST /connect/token HTTP/1.1
Authorization: Basic cmVzb3VyY2UtbWFjaGluZTpzZWNyZXQ=
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&scope=ledger.read+orders.write
&resource=https%3A%2F%2Forders.demo.example.com

Authorization Server → Client

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": "ledger.read orders.write"
}

{access_token_2} decoded — the header, then the payload; the signature is not shown:

{
  "alg": "RS256",
  "kid": "{key_id_1}",
  "typ": "at+jwt"
}
{
  "aud": [
    "https://orders.demo.example.com"
  ],
  "iss": "http://localhost",
  "iat": {iat},
  "nbf": {iat},
  "exp": {iat + 3600},
  "jti": "{jti_2}",
  "client_id": "resource-machine",
  "scope": "ledger.read orders.write"
}

3. A resource the client may not have

An API none of the requested scopes belong to is refused.

Client → Authorization Server

POST /connect/token HTTP/1.1
Authorization: Basic cmVzb3VyY2UtbWFjaGluZTpzZWNyZXQ=
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&scope=orders.write
&resource=https%3A%2F%2Fledger.demo.example.com

Authorization Server → Client

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=UTF-8
Cache-Control: no-store, no-cache, max-age=0
Pragma: no-cache

{
  "error": "invalid_target",
  "error_description": "The resource indicator names no API this grant reaches."
}