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.
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
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | Must be client_credentials |
client_id | Yes | The application's client ID |
client_secret | Yes | The application's client secret |
audience | No | A registered resource server's identifier |
scope | No | Space-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:
- Have the application request its own token with the plain
client_credentialsgrant — noaudiencethis 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" - 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.