Resource servers
Register your API in a realm, request tokens addressed to it with the resource parameter (RFC 8707), and validate them in the API.
A resource server is an API — a REST service, an MCP server — that accepts a realm’s access tokens. Registering it gives it an identifier that tokens carry as their audience (aud), a set of scopes of its own and the list of clients allowed to request tokens for it.
The audience matters because every client of a realm publishes its signing keys in the same key set: a valid signature only proves that the token came from the realm. The audience is what says it was meant for your API, so a token issued for another application of the same realm is refused.
Register an API
Open Resource Servers, choose New resource server and pick the realm whose tokens the API will accept.
| Field | Rules |
|---|---|
| Identifier | The API’s canonical http or https URL, such as https://api.acme.com, without a fragment and up to 512 characters. Scheme and host are lowercased, and a default port or a trailing slash removed. Unique in the realm and fixed once created, because every token issued for it carries it. |
| Name and description | Up to 128 and 512 characters. |
| Scopes | At least one, such as documents:read. Up to 128 characters each, without spaces, quotes or backslashes, case-sensitive and not repeated. |
| Allowed clients | The clients of the same realm that may request tokens for it. Being in the realm is not enough. |
| Accept CIMD clients | Lets MCP clients that registered themselves with a Client ID Metadata Document request tokens for it. |
| Introspection client | A confidential client of the realm that the API uses to ask whether a token is still active. |
- Deactivating keeps the identifier, scopes and clients, and stops new tokens for it at once.
- Deleting removes its scopes and client list and leaves the clients themselves untouched; tokens already issued stay valid until they expire.
- On edit, the scopes and clients you save replace the previous ones.
Request tokens for an API
Name the API with the resource parameter of RFC 8707 and ask for its scopes in scope. An authorization request takes at most one resource.
https://authio.api.hyperio.tech/realms/acme/protocol/openid-connect/auth
?client_id=acme-web
&response_type=code
&redirect_uri=https%3A%2F%2Fapp.acme.com%2Fapi%2Fauth%2Fcallback%2Fauthio
&scope=openid%20documents%3Aread
&resource=https%3A%2F%2Fapi.acme.com
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&state=af0ifjsldkj- When the code is exchanged,
resourcecan be left out; if sent, it must match the authorization request. - Refreshes keep the resource of the original authorization, so renewed tokens stay addressed to the same API.
- The resource server and the client’s access to it are checked again on every issuance, refreshes included: a deactivated server, or a client removed from its list, stops receiving tokens at once.
| Error | Why |
|---|---|
invalid_target | More than one resource, or one that names no active resource server this client may request tokens for. |
invalid_scope | A requested scope is neither enabled on the client nor one of the server’s scopes. |
access_denied | The plan does not include the MCP integration. |
What the token carries
The access token’s aud is the identifier, and its scope adds the server scopes the request asked for to the ones the client was granted. Without resource, aud is the audience configured on the client.
{
"iss": "https://authio.api.hyperio.tech/realms/acme",
"sub": "6f1c2e7a-4b8d-4f0e-9a51-3c2d7e8b9f10",
"aud": "https://api.acme.com",
"azp": "acme-web",
"typ": "Bearer",
"scope": "openid documents:read",
"iat": 1791370800,
"nbf": 1791370800,
"exp": 1791372600
}Validate access tokens in your API
Access tokens are signed JWTs, so your API validates them on its own with the realm’s public keys, without calling Authio on each request. On every request, check:
| Check | Expected |
|---|---|
| Signature | A key of the realm’s JWKS, https://authio.api.hyperio.tech/realms/acme/protocol/openid-connect/certs, picked by the kid header |
iss | https://authio.api.hyperio.tech/realms/acme |
aud | Your API’s identifier, https://api.acme.com |
exp and nbf | The current time falls inside the window |
typ | Bearer. ID tokens carry ID and refresh tokens Refresh. |
scope | Contains the scope the endpoint requires |
import { createRemoteJWKSet, jwtVerify } from 'jose'
const issuer = 'https://authio.api.hyperio.tech/realms/acme'
const jwks = createRemoteJWKSet(
new URL(`${issuer}/protocol/openid-connect/certs`),
)
export async function verifyAccessToken(token: string, requiredScope: string) {
const { payload } = await jwtVerify(token, jwks, {
issuer,
audience: 'https://api.acme.com',
})
if (payload.typ !== 'Bearer') {
throw new Error('Not an access token')
}
const scopes = typeof payload.scope === 'string' ? payload.scope.split(' ') : []
if (!scopes.includes(requiredScope)) {
throw new Error(`Missing scope ${requiredScope}`)
}
return payload
}The JWT bearer handler reads the issuer and the keys from the realm’s discovery document and checks the signature, issuer, audience and lifetime by itself. For ASP.NET Core there is also an Authio package; see .NET.
Check revocation with introspection
A token validated locally stays valid until it expires, even after its session ended or its user was suspended. When the API must know at once, ask the realm with the server’s introspection client (RFC 7662):
curl -X POST https://authio.api.hyperio.tech/realms/acme/protocol/openid-connect/introspect \
-u "acme-api:YOUR_CLIENT_SECRET" \
-d "token=$ACCESS_TOKEN" \
-d "token_type_hint=access_token"- The answer is always
200. A token that is expired, revoked, from an ended session or never valid gets only{"active": false}, with no reason. - The introspection client sees the tokens addressed to its resource servers, even when another client requested them.
- An active answer also returns the role claims,
acr,amr, the organization and, for delegated tokens,actanddelegation_id.
Activity
The resource server’s Activity tab counts, over 7, 30 or 90 days, the tokens issued and refused for it, the connected-account exchanges it made and the introspections it ran, with the most active clients and users and the most common reasons for refusal.