Skip to main content

Managing Resource Servers

A resource server is a downstream API that accepts O2ID-issued access tokens. Registering one turns its identifier into a value O2ID actually recognizes: a caller can then name it as resource/audience (see Token Exchange and the Client Credentials Grant), and O2ID checks it against real, registered resource servers instead of accepting any string handed to it. A resource server has no credentials of its own — it's a catalog entry, not a principal that authenticates itself.

note

Managing resource servers requires a role with the resourceservers:read/ resourceservers:create/resourceservers:update/resourceservers:delete scopes; authorizing an application for one requires applications:authorize-resource-servers — see Using o2idctl for how to log in, and Managing Roles for how to create and assign roles.

Registering a resource server

./o2idctl resource-servers create --name "Messages API" --identifier https://api.example.com

--identifier is the exact value a caller later passes as resource or audience.

Defining scopes

A resource server's own scopes/permissions — a namespace entirely separate from O2ID's own RBAC scopes (see Managing Roles) — are managed as their own resource, independent from creating or updating the resource server itself:

./o2idctl resource-servers scopes create --resource-id <resource-id> \
--value read:messages --display-name "Read Messages" --description "Allows reading messages"

--value is what actually gets granted and checked (e.g. read:messages) and must be unique among that resource server's other scopes; --display-name/--description are optional, purely descriptive metadata for whoever is later choosing which scopes to authorize. List, fetch, update, or delete scopes with:

./o2idctl resource-servers scopes list --resource-id <resource-id>
./o2idctl resource-servers scopes get <scope-id>
./o2idctl resource-servers scopes update <scope-id> --display-name "Read Message Data"
./o2idctl resource-servers scopes delete <scope-id>

get, update, and delete take only the scope's own ID — no --resource-id needed, since that ID is already globally unique. Renaming a scope's --value doesn't retroactively fix up any application authorization already granted under the old value — the same way renaming any OAuth scope string is a breaking change in any system.

Authorizing an application

Registering a resource server on its own does nothing until at least one Application is authorized to mint or introspect tokens for it. Authorizing grants a subset of the resource server's own scopes to that application:

./o2idctl applications authorize-resource-server <app-id> <resource-server-id> \
--scope read:messages

Only scopes the resource server itself defines can be granted — attempting to authorize a scope it doesn't declare is rejected. Re-running authorize-resource-server for the same pair replaces the previously granted scopes, rather than merging with them. List what's currently authorized, or remove an authorization entirely, with:

./o2idctl applications resource-servers <app-id>
./o2idctl applications deauthorize-resource-server <app-id> <resource-server-id>

Getting a token for a resource server

Once authorized, the application requests a token naming the resource server's identifier as audience on the client credentials grant — the primary way client_credentials gets used against a real downstream API, rather than O2ID's own management API.

Token endpoint: POST /oauth2/token

ParameterRequiredDescription
grant_typeYesMust be client_credentials
client_idYesThe application's client ID
client_secretYesThe application's client secret
audienceNoA registered resource server's identifier
scopeNoSpace-delimited list of requested scopes
curl -s -X POST http://localhost:8080/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_id=${CLIENT_ID}" \
--data-urlencode "client_secret=my-secret" \
--data-urlencode "audience=https://api.example.com" \
--data-urlencode "scope=read:messages"

When audience is present, the granted scope comes entirely from that resource server's own defined scopes and the subset the application has been authorized for — never from the application's allowedScopes, which govern O2ID's own management API instead. A requested scope the application isn't authorized for on that resource server is silently dropped rather than granted; requesting no scope at all grants the full authorized subset.

audience must name a resource server that's both registered and one the application has some standing authorization for — anything else is rejected with invalid_target, the same error Token Exchange uses for an unrecognized resource/audience.

Introspecting a resource server's own tokens

O2ID's access tokens are opaque, not JWTs — an API can't just decode one itself to see if it's valid. So the same application can also check a token by calling introspection, the only way to see one. No separate grant is needed for this — being authorized for a resource server already covers it:

  1. Have the application request its own token with the plain client_credentials grant — no audience this time:
    curl -s -X POST http://localhost:8080/oauth2/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    --data-urlencode "grant_type=client_credentials" \
    --data-urlencode "client_id=${CLIENT_ID}" \
    --data-urlencode "client_secret=my-secret"
  2. Use that token to introspect whichever token it needs to check:
    curl -s -X POST http://localhost:8080/oauth2/introspect \
    -H "Authorization: Bearer ${SELF_SERVICE_TOKEN}" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    --data-urlencode "token=${TOKEN_TO_CHECK}"

If TOKEN_TO_CHECK's aud is a resource server this application is authorized for, the response looks exactly like an admin's — active, with scope, subject, and expiry. If it names any other resource server (or none), the response is just {"active": false}, identical to a token that was never issued at all — the application never learns that the other token exists.

API Reference

See the API Reference for the full request/response schemas.