Skip to Content
TutorialsProtect a backend API

Protect a backend API

This tutorial accepts an Identity access token at a product API and turns it into a product authorization context. Token validation proves the caller’s Identity subject; the product backend still decides which business objects and operations that subject may access.

Before you start

Register the API as an Identity resource with:

  • a stable resource URI, for example https://api.example.com;
  • the audience expected in access tokens, for example example-api;
  • finite scopes such as orders.read and orders.refund.

Authorize the calling OAuth client for that resource and its required scopes. Issue a resource-server credential only if this API will call token introspection.

1. Read discovery and signing keys

ISSUER='https://auth.example.com'
curl --fail --silent --show-error \
  "$ISSUER/.well-known/openid-configuration"

Cache the discovery document for the configured issuer. Fetch keys from its jwks_uri and cache them according to HTTP response semantics. Refresh the key set when a token carries an unknown kid.

Never derive a tenant from an unverified token and then trust that tenant’s issuer. Select the configured issuer from the API’s own routing or tenant configuration first.

2. Parse the bearer credential

Accept one authorization header in this form:

Authorization: Bearer eyJraWQiOiJrZXktMjAyNi0xMCIsImFsZyI6IlJTMjU2In0...

Return 401 when the header is absent, duplicated, malformed or uses another scheme. Do not log the credential.

3. Validate the JWT completely

A successful signature check is only one condition. Require all of these invariants:

CheckRequired value
signature algorithmRS256 from the API’s allowlist
signing keypublic key matching the header kid
issuerexact configured tenant issuer
audiencethe resource’s registered audience
expiryexp is in the future within bounded clock skew
issued timeiat is plausible within bounded clock skew
subjectnon-empty sub
token identitynon-empty jti
scopesevery scope required by the operation

Reject an ID token at the API even if its signature is valid. The API accepts access tokens issued for its own audience.

A useful authorization result is a closed value owned by the API:

ValidatedIdentity { issuer subject clientId scopes tokenId expiresAt }

Create it only after every token invariant succeeds.

4. Add live introspection when the operation needs it

Local JWT validation remains valid until token expiry. For high-risk operations that must observe revocation or current account status immediately, call the tenant’s discovered introspection endpoint.

ISSUER='https://auth.example.com'
curl --fail --silent --show-error \
  --user 'resource-server-id:RESOURCE_SERVER_SECRET' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'token=ACCESS_TOKEN' \
  "$ISSUER/oauth2/introspect"

Require active: true, then repeat the issuer, audience and scope checks against the response. Treat 200 {"active":false} as an unauthenticated request.

Security boundary

An introspection credential belongs to one registered resource. Keep it in the API’s secret manager and never ship it to a browser or native application.

Do not introspect every request by accident. Choose the policy explicitly:

  • local validation for normal bounded-lifetime API access;
  • introspection for sensitive actions or when immediate revocation is a product requirement;
  • a short, bounded introspection cache only if the accepted revocation delay is documented.

5. Resolve the product identity

Store an explicit association such as:

external_identities issuer = "https://auth.example.com" subject = "usr_01JY7N8SV4B30TQGB8Y3YH5J9M" user_id = 42

Resolve by exact issuer and subject. Then load the user’s product organizations, memberships, roles and permissions.

A first-time subject can trigger explicit just-in-time provisioning when the product allows it. Linking an existing product user requires a migration or account-linking proof. Email matching alone is insufficient.

6. Enforce scope and business permission separately

For an order refund, the request might require:

  1. token scope orders.refund;
  2. an active product membership in the order’s organization;
  3. the product permission to refund that order;
  4. any business constraint such as refund window or order state.

Return 401 when authentication failed. Return 403 when the authenticated subject lacks scope or product permission. Avoid returning 404 as a generic substitute unless hiding resource existence is an explicit product policy.

Verify the finished use case

Call one protected endpoint with:

  • no token: expect 401;
  • an ID token: expect 401;
  • an access token for another audience: expect 401;
  • a valid token without the operation scope: expect 403;
  • a valid token and scope but no product membership: expect 403;
  • the correct token and product permission: expect the business response;
  • a revoked token on an introspected action: expect 401.

Also test signing-key overlap during rotation: both the retiring key and new active key must validate until old tokens can no longer be valid.

See token response and JWKS contracts, resource-server invariants and token handling.