ndovuoauth2

SPEC PRIMER · RFC 9068 · OCTOBER 2021

JWT access tokens

The interoperable profile for JWT access tokens: at+jwt typing, the required claims, and the six checks a resource server runs offline.

October 28, 2026readable is not inspectableWatch the six checks run
01 · What it standardizes

RFC 6749 is deliberately silent on what an access token looks like: it is a string the client presents and the resource server must understand. But many servers wanted to validate tokens themselves, so authorization servers began issuing them as JWTs - "many commercial OAuth 2.0 implementations elected to issue access tokens using a format that can be parsed and validated by resource servers directly, without further authorization server involvement" (section 1). The layouts worked, but only per vendor: same ideas, different claim names.

RFC 9068 (Bertocci; October 2021) is the interoperable answer: one claim layout, one typing convention, and one validation story for access tokens issued as JWTs. Do not confuse it with RFC 7523, which is about JWTsas a grant - a client authenticating with an assertion; 9068 is about JWTs as the token (section 1). The payoff is offline validation: a resource server checks the signature against the AS's published keys and enforces the claims locally - no introspection round trip per request. The two coexist; an RS that needs immediate revocation can still introspect.

02 · The token on the wire

To the client it is still one opaque-looking string. The profile's first conventions live in the header: tokens MUST be signed, alg: noneMUST NOT be used, asymmetric signing is RECOMMENDED, and RS256 MUST be supported by conforming ASes and RSes (section 2.1). And every JWT access token MUST declare the at+jwt media type in its typheader - since the "application/" prefix is RECOMMENDED to be omitted, the value used SHOULD be at+jwt - which is what makes an access token distinguishable from every other JWT in circulation (section 2.1).

What the resource server receives
Authorization: Bearer eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjcxIn0.<payload>.<signature>

And what it does with it: split the three dot-separated segments, decode, and check. The payload is the whole contract:

// header (the segment above, decoded)
{
  "typ": "at+jwt",
  "alg": "RS256",
  "kid": "71"
}

// payload
{
  "iss": "https://as.example.com/",
  "sub": "<resource-owner-or-client>",
  "aud": "https://api.example.com/",
  "client_id": "<client-id>",
  "jti": "<token-id>",
  "iat": 1634571490,
  "exp": 1634575090,
  "scope": "invoices:issue"
}

// signature: the AS's key, verifiable from its published JWKS
ClaimStatusMeans
typ header, 2.1SHOULD be at+jwtExplicit typing - the primary defense against ID-token/access-token confusion (with RFC 8725's practices behind it)
issREQUIREDThe issuer; a resource server MUST match it exactly (section 4)
subREQUIREDThe resource owner's subject - or, for client-credentials tokens, an identifier of the client itself (2.2)
audREQUIREDWho the token is for; the resource server MUST find itself in it (section 4)
client_idREQUIRED registered in RFC 8693 §4.3The client the AS authenticated when minting the token
iat / expREQUIREDIssued at, and expiry - the resource server compares the current time against exp (section 4)
jtiREQUIREDA unique identity for this token - the anchor for revocation and audit
scopeSHOULD 2.2.3Present whenever the request carried a scope, so the RS sees what was granted

Authentication flourishes like auth_time, acr, andamr are optional (2.2.1), and the profile borrowsroles / groups / entitlements from SCIM (2.2.3.1). The audience rule underneath the table is section 3: when the request carried a resource parameter (RFC 8707), theaud SHOULD equal it; otherwise the AS MUST apply a default resource indicator. And the spec is blunt about ambiguity: "The authorization server MUST NOT issue a JWT access token if the authorization granted by the token would be ambiguous" (section 3).

03 · The validation checklist

Section 4 is the practical payload of the spec: the checks that make a JWT access token locally decidable. "Resource servers receiving a JWT access token MUST validate it in the following manner" - and this is the sequence a resource server runs on every protected request:

#Check (section 4)On failure
1The typ header is at+jwt or application/at+jwt - "reject tokens carrying any other value"Reject: not this profile's token
2The issuer identifier MUST exactly match the iss claimReject: wrong authorization server
3aud contains a resource indicator the resource server expects for itselfReject: minted for someone else
4The signature verifies with the alg-specified algorithm and the keys provided by the AS - and alg: none MUST be rejectedReject: forged or tampered
5The current time MUST be before exp - "some small leeway, usually no more than a few minutes" MAY cover clock skewReject: expired
6Authorization claims, if present, SHOULD inform the accept-or-reject decision alongside the request's contextReject: does not meet this API's contract

Any failure answers 401 invalid_token: the resource server "MUST handle errors as described in Section 3.1 of [RFC6750]", the error the bearer page tabulates. The JWT inspectorruns this same sequence live in your browser - six steps, with tamper buttons for each defense.

04 · The one thing clients must not do

The token decodes; the client can read it; and the spec closes that door explicitly: "The client MUST NOT inspect the content of the access token" (section 6). The client's job is to present the token, not to parse authorization decisions out of it - because the profile is an AS-to-RS contract, and the format can change at any time without notice to the client. A dashboard that reads scope from the token to hide buttons has coupled itself to an implementation detail.

Section 5 adds a quieter corollary for authorization servers: using different signing keys for ID tokens and access tokens buys nothing, because resource servers accept any key the AS publishes - the typing and the audience checks are the separation, not the keyring.

05 · How it goes wrong
FailureWhat happensThe fix (and its clause)
Token confusionAn ID token, a refresh assertion, or a crafted JWT is presented at an API and acceptedExplicit typing: typ MUST be at+jwt and other values are rejected (§4); ASes MUST give each resource a distinct aud to prevent cross-JWT confusion (§5, with RFC 8725)
alg: none acceptedAn unsigned "token" sails through a validator that trusts the headernone MUST NOT be used, and the RS MUST reject it (§2.1, §4)
No audience disciplineA token minted for service A is accepted by service B - one leak becomes lateral movementaud MUST include the accepting RS (§4); ASes MUST NOT issue ambiguous tokens (§3)
Machine tokens read as usersAuthorization keyed on sub breaks: a client-credentials token's sub is the client, not a person (2.2)Model machine identities as first-class principals - living with the token
Expired-but-usableA generous clock-skew window keeps dead tokens alive past their windowThe RFC's own allowance is small: leeway "usually no more than a few minutes" (§4); real margins belong on the client's refresh side
06 · Primary sources