Skip to main content

Resource Indicators

Resource Indicators for OAuth 2.0 (RFC 8707) lets a client tell ThunderID which resource server the access token is for. The client passes a single resource parameter. ThunderID looks it up against the registered resource servers, narrows the granted permission scopes to those owned by that resource server, and writes its identifier into the token's aud claim. When permission scopes are requested without resource, ThunderID uses the configured defaultResourceServer. If neither target is available, token issuance fails with invalid_target. OIDC-only and scopeless requests without resource are not bound to a resource server, so their audience is the application's configured default audience (token.accessToken.defaultAudience), or the client_id when it is unset.

The effect: a token issued for the payments API cannot be replayed against the bookings API. Each token is bound to its intended audience.

How It Works

The resource parameter is accepted on authorization and PAR requests, on authorization code, client credentials, JWT bearer, refresh token, and token exchange requests, and on CIBA backchannel authentication requests (POST /oauth2/bc-authorize). A CIBA request binds to its resource server at initiation, before the user authorizes it. A resource supplied when polling the token endpoint must match that binding.

GET /oauth2/authorize
?response_type=code
&client_id=$CLIENT_ID
&redirect_uri=https://app.example.com/callback
&scope=payments:read%20payments:write
&resource=https://api.example.com/payments
&state=xyz
POST /oauth2/token
grant_type=authorization_code
&code=$CODE
&resource=https://api.example.com/payments

The issued access token carries the resource identifier as its aud:

{
"sub": "user-123",
"iss": "https://thunderid.example.com",
"aud": "https://api.example.com/payments",
"scope": "payments:read payments:write",
"exp": 1717000000
}
How ThunderID Implements It
AspectBehavior
Accepted on/oauth2/authorize, /oauth2/par, /oauth2/bc-authorize, and /oauth2/token for authorization code, client credentials, JWT bearer, refresh token, and token exchange grants
Value rulesThe resource must be an absolute URI with no fragment. A request with more than one resource parameter is rejected with invalid_target.
ResolutionThe URI is matched to a registered resource server's identifier
Unknown identifierRequest rejected with invalid_target
No resourcePermission-bearing requests use defaultResourceServer and return invalid_target if it is unset. OIDC-only and scopeless requests use the application's token.accessToken.defaultAudience, or client_id when it is unset
CIBAThe request binds to a resource server at /oauth2/bc-authorize, before user authorization. A resource supplied at token-endpoint polling must equal that binding, or the poll is rejected with invalid_target
Scope filteringRequested scopes are filtered to those defined on the targeted resource server. Scopes not owned by that resource server are silently dropped
OIDC standard scopesopenid, profile, email, phone, address are not filtered by resource indicators
Custom scope_claimsScopes covered by an application's scopeClaims mapping also pass through unchanged

aud Claim Behavior

Caller passes resourceaud value
One resourceJSON string: "aud": "https://api.example.com/payments"
Multiple resource parametersRejected with invalid_target
No resource, permission scopes requestedThe configured defaultResourceServer identifier. If no default is configured, the request is rejected with invalid_target.
No resource, no permission scopesThe application's configured default audience (token.accessToken.defaultAudience), or the requesting client's client_id when it is unset.

The aud claim is always a single string, per RFC 7519 §4.1.3.

Scope Filtering Rules

Scope categoryFiltered by resource?
Permissions owned by a targeted resource server✅ Kept
Permissions owned by a different resource server❌ Dropped
OIDC standard scopes (openid, profile, email, phone, address)Always kept
Custom scopes mapped via scopeClaimsAlways kept

Resource on Refresh

On refresh, resource may only name the resource server the refresh token is already bound to. A mismatch is rejected with invalid_target:

curl -X POST https://thunderid.example.com/oauth2/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d "grant_type=refresh_token" \
-d "refresh_token=$REFRESH_TOKEN" \
-d "resource=https://api.example.com/payments"

Without resource, the new access token keeps the refresh token's existing resource-server binding.

Try It in ThunderID

Resource indicators activate as soon as you register a resource server with an absolute-URI identifier.

Register a Resource Server

See Resource Servers for full setup. At minimum:

FieldValue
namePayments API
handlepayments-api
identifierhttps://api.example.com/payments
delimiter:
permissionspayments:read, payments:write, …

Request a Token for It

curl -X POST https://thunderid.example.com/oauth2/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d "grant_type=client_credentials" \
-d "scope=payments:read" \
-d "resource=https://api.example.com/payments"

Explore with AI

ThunderID LogoThunderID Logo

Product

DocsAPIsSDKs
© Copyright Linux Foundation Europe.For web site terms of use, trademark policy and other project policies please see https://linuxfoundation.eu/en/policies.