Protect an API (validate access tokens)
Validate OneiD access tokens in your API by checking the signature, issuer, expiry and scope, and answer with the right 401 or 403.
An API protected by OneiD accepts a bearer access token on each request and checks it locally. OneiD access tokens are signed JWTs, so your API needs no call to OneiD per request: it verifies the signature with OneiD’s published keys and then checks the claims.
What an access token looks like
OneiD access tokens are JWTs signed with RS256. They are not encrypted. The header carries a kid that names the signing key in OneiD’s JWKS.
| Claim | Meaning |
|---|---|
iss |
The OneiD issuer, https://YOUR_ONEID/, with the trailing slash. |
sub |
The user’s ID. For a client credentials token, the client ID. |
exp, iat |
Expiry and issue time, in seconds since 1970. |
scope |
The granted scopes, separated by spaces. |
client_id |
The client the token was issued to. |
role |
The user’s roles, when the roles scope was granted. |
Scope-released profile, email and phone claims can also be present. Ignore claims you do not know.
Warning OneiD access tokens have no
audclaim. Do not require an audience. A library that checks the audience by default rejects every OneiD token; turn that check off and check the scope instead.
Validate a token step by step
Your API does the following for every request. Use a maintained JWT library; it does most of these steps for you.
- Read the token from the
Authorization: Bearer <token>header. Reject the request if the header is missing or uses another scheme. - Check that the header
algisRS256. Rejectnoneand every other algorithm. - Find the key whose
kidmatches the token’skidin OneiD’s JWKS and verify the signature. - Check that
issequals theissuerfrom the discovery document exactly, including the trailing slash:https://YOUR_ONEID/. - Check
exp, andnbfif present, against the current time. Allow a small clock skew, such as a minute at most. - Check that the space-separated
scopeclaim contains the scope this endpoint requires. - If the endpoint needs a role, check the
roleclaim.
Get the keys from discovery
Read the discovery document once at start-up and take jwks_uri and issuer from it. Do not hard-code them.
GET /.well-known/openid-configuration HTTP/1.1
Host: YOUR_ONEID
The discovery document gives jwks_uri as https://YOUR_ONEID/.well-known/jwks.
Cache the JWKS and refresh on an unknown key
Cache the JWKS. When a token arrives with a kid that is not in your cache, fetch the JWKS again once and retry. Limit how often you re-fetch, so that tokens with made-up kid values cannot make your API call OneiD on every request.
OneiD publishes a new signing key in the JWKS before it starts signing with it, and keeps a retired key in the JWKS for 30 days. An API that caches the JWKS and re-fetches on an unknown kid keeps working through key rotation without changes.
Example in Node.js
This example uses the jose library. createRemoteJWKSet caches the keys and re-fetches them when it sees an unknown kid.
import { createRemoteJWKSet, jwtVerify } from 'jose';
const discovery = await fetch('https://YOUR_ONEID/.well-known/openid-configuration')
.then((r) => r.json());
const jwks = createRemoteJWKSet(new URL(discovery.jwks_uri));
export async function verifyAccessToken(token, requiredScope) {
const { payload } = await jwtVerify(token, jwks, {
issuer: discovery.issuer, // "https://YOUR_ONEID/" with the slash
algorithms: ['RS256'],
clockTolerance: 60, // seconds
// no audience option: OneiD access tokens have no aud claim
});
const scopes = typeof payload.scope === 'string' ? payload.scope.split(' ') : [];
if (!scopes.includes(requiredScope)) {
const err = new Error('insufficient_scope');
err.status = 403;
throw err;
}
return payload;
}
The API quickstarts show the same checks for each language: Node.js, .NET, Go, Python and Java.
Use one scope per API
Because access tokens have no audience, the scope is what ties a token to an API. A token that carries orders.read is accepted by every API that accepts orders.read.
- Ask your OneiD administrator to create distinct API scopes for each API, for example
orders.readandorders.writefor the orders API andinvoices.readfor the invoices API. - Require one of your own API’s scopes on every endpoint. Never accept a token only because its signature and issuer are valid.
- Do not reuse another API’s scope names, and do not accept identity scopes such as
openidorprofileas proof of access to your API.
Then a token issued for the invoices API cannot be used at the orders API, because it does not carry an orders scope.
Note API scopes exist only after an administrator creates them. They appear in the access token’s
scopeclaim and add no other claims. See Scopes, claims and roles.
Answer with 401 or 403
Follow RFC 6750 so that clients can tell what went wrong.
| Situation | Status | WWW-Authenticate header |
|---|---|---|
| No token | 401 | Bearer |
| Token malformed, expired, wrong issuer or bad signature | 401 | Bearer error="invalid_token" |
| Valid token without the required scope | 403 | Bearer error="insufficient_scope", scope="orders.read" |
| Valid token and scope, but the user lacks the role your endpoint needs | 403 | Not required |
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="The access token expired"
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", scope="orders.write"
Keep error_description short and do not echo the token.
Use roles for authorisation
When the client requests the roles scope and the user has roles, the access token carries a role claim. With one role it can be a single string; with several roles it is an array. Accept both forms.
{
"iss": "https://YOUR_ONEID/",
"sub": "8d0c6a3e-2f4b-4c1e-9a77-1b2c3d4e5f60",
"scope": "openid roles orders.read",
"role": ["OrderViewer", "Support"]
}
Roles are assigned to users in OneiD or mapped from directory or provider groups. See Scopes, claims and roles.
Client credentials tokens represent a service, not a user. Their sub is the client ID. If an endpoint must only serve users, or only serve one service, check sub or client_id as well.
Revocation and introspection
OneiD can revoke tokens: when a user signs out of an application, when the user’s password changes, when an administrator removes a role or locks the user, and in other cases listed in Tokens. An API that validates JWTs locally does not see a revocation until the token expires.
You have two options:
- Short-lived access tokens with local validation. This is the recommended pattern for most APIs. Access tokens live 1 hour by default, and an administrator can shorten the lifetime for a client.
- Introspection. An API that must see revocation immediately can call
POST /connect/introspect. Introspection requires a confidential client and only answers for tokens issued to that same client, so it does not fit an API that serves tokens issued to other clients.