Skip to main content
Version: Next

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 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 reservedapplications 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

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, 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.