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 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.
The built-in O2ID Management API resource servers
Every tenant is seeded with its own built-in resource server, identifier
o2id-management-api, whose scopes mirror O2ID's own RBAC scopes (see
Managing Roles) — plus one further built-in resource
server per scope domain (o2id-management-api/connections,
o2id-management-api/users, o2id-management-api/applications, and so
on). They show up in resource-servers list like any other resource
server.
The umbrella o2id-management-api identifier is not special-cased: an
application's RBAC scope ceiling (Application.AllowedScopes, see
Managing Applications) is an
ordinary authorization against it, the same mechanism as any other
resource server. applications authorize-resource-server <app-id> o2id-management-api --scope users:read and applications create --scope users:read (before any --api) produce the identical result.
The per-domain resource servers (o2id-management-api/connections, etc.)
are different: they're reserved — applications authorize-resource-server
and deauthorize-resource-server both refuse to target them (400 error) —
and functional rather than an ordinary authorization target. Requesting a
client credentials grant with
audience=o2id-management-api/<domain> (e.g.
o2id-management-api/connections) mints a token scoped to just that
domain — computed directly from the application's own AllowedScopes
intersected with that domain's known scopes, not from any authorization
against the resource server itself. If the application's AllowedScopes
has nothing in that domain, the request fails the same way an
unauthorized real resource server audience would (invalid_target). A
domain-scoped token is confined to that domain's own routes — it cannot
be used against a different domain's management-API endpoints, even
though its Scope is just as real as any other token's.
This is client_credentials-only for now: requesting
resource=o2id-management-api/<domain> on the authorization code flow
isn't supported (these per-domain resource servers deliberately never
get a real authorization, which that path requires).
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-identifier> \
--scope read:messages
<resource-server-identifier> is the resource server's identifier (the
same value used as resource=/audience=), not its internal ID. 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-identifier>
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, 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.