Access and Refresh Tokens

This section documents OAuth 2.0 (RFC 6749) as amended by the OAuth 2.0 Security Best Current Practice (RFC 9700 / BCP 240), the OAuth 1.0 Protocol (RFC 5849) for historical context, and OpenID Connect Core 1.0, as published at the IETF Datatracker and the OpenID Foundation specifications — and, on the Spring pages, Spring Boot 4.1.x and Spring Security 7.1.x as published at the Spring Security reference documentation — which are the references these pages are written and verified against.

OAuth 2.1 is still an Internet-Draft (draft-ietf-oauth-v2-1-16, 3 September 2026) and is flagged as such everywhere it appears on these pages. It is a working-group consolidation in progress, not a published standard; nothing here should be read as saying otherwise.

This content was generated with the assistance of AI and should be verified against those official specifications before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

An access token is the credential a client presents to a resource server (an API) to prove it has been granted delegated access to some scope of that API, on behalf of a resource owner or on its own behalf. This page covers what that credential is (and is not) to the client that holds it, the wire format of the token response and its error cases, and the refresh-token mechanism that lets a client obtain new access tokens without repeating the whole authorization dance.

What an access token is to the client

RFC 6749 §1.4 is explicit: "the client MUST NOT rely on the value or structure of the access token, or make any assumptions about the token’s contents." Whatever format the authorization server chooses to mint — an opaque random string, a JSON Web Token, anything else — the client’s contract is the same: it obtained the token via a grant, it sends it to the resource server exactly as issued, and it never inspects it, decodes it, or tries to extract claims from it. Even when the access token happens to be a JWT that the client could technically decode, doing so is a client-side anti-pattern: the resource server’s claim set and the client’s needs are not the same contract, and the authorization server is free to change the token’s internal format at any time without breaking a client that respects the opacity rule. A client that wants to know who is signed in reads the ID token instead — see ID Tokens vs. Access Tokens.

"Opaque to the client" and "opaque token" (the token type discussed below) are different things. Every access token is opaque to the client by contract, whether or not its wire format is a structured JWT or a random by-reference string.

Bearer vs. sender-constrained

RFC 6750 defines the bearer token model: whoever holds the token can use it, full stop. There is no proof the presenter is the party the token was issued to — if it leaks (a logged header, a compromised browser storage area, a malicious redirect), the thief has everything the legitimate client had, for as long as the token remains valid. This is convenient (any HTTP client can send Authorization: Bearer <token>) and is the default almost everywhere.

A sender-constrained token binds the token to a cryptographic key the legitimate client holds, so a stolen token is useless without also stealing that key:

  • DPoP (RFC 9449) — the client proves possession of a private key with a signed JWT sent alongside the request; the access token carries a cnf.jkt thumbprint of the corresponding public key, and the resource server checks the two match.

  • Mutual-TLS (RFC 8705) — the access token is bound to the client’s TLS certificate (cnf.x5t#S256); the resource server checks the certificate presented on the current TLS connection against that thumbprint.

Both mechanisms are covered in full on Sender-Constrained Tokens: DPoP and mTLS. RFC 9700 §4.14 recommends sender-constraining wherever the deployment can support it, and the in-progress OAuth 2.1 consolidation (draft-ietf-oauth-v2-1-16) keeps bearer tokens as the default but documents DPoP as the recommended upgrade path for higher-risk clients.

By-value vs. by-reference

An access token’s format is independent of the bearer/sender-constrained distinction above. Two families are in production use:

By-value (structured, typically a JWT) By-reference (opaque)

What it looks like

A JWT: base64url header, payload and signature, self-contained

A random string with no extractable structure (e.g. a UUID or a long opaque handle)

How the resource server validates it

Locally: verify the signature against a cached public key (JWKS), then check the claims — no network call needed

Remotely: call the authorization server’s introspection endpoint (RFC 7662) on every request, or on a cache-refresh interval

Latency and availability

No dependency on the authorization server being reachable at request time

Adds a network hop (mitigated by short-TTL caching); resource server degrades if the AS is down

Revocation

Cannot be revoked before exp — the resource server has no way to know it was revoked without also calling back to the AS

Revocation is immediate once the AS marks it revoked, since every validation is a live lookup

Size and content exposure

Carries its own claims — easy to leak PII or internal identifiers into logs, browser storage or a URL if handled carelessly

Carries no claims at all; the AS decides what to return from introspection and to whom

Multi-resource-server fan-out

Any RS with the JWKS can validate independently; no coordination needed

Every RS needs network access (and, usually, its own client credentials) to the AS’s introspection endpoint

Typical profile

RFC 9068 at+jwt access tokens

Random handle stored server-side by the AS, resolved via RFC 7662

Neither option is universally "better" — it is a deployment trade-off between latency/availability and revocation immediacy/privacy. The full decision table, including when to mix the two per resource server, is on Opaque Tokens, Introspection and Revocation. The JWT format itself — claims, algorithms, validation — is covered on JWT and the JOSE Family.

Lifetime, audience and scope

Three properties travel with (or alongside) every access token regardless of format:

  • Lifetime (exp, or the introspection response’s own exp) — access tokens are deliberately short-lived (minutes to roughly an hour is typical) precisely because a bearer token cannot usually be revoked before it expires. A short lifetime bounds the blast radius of a leak; the refresh token (below) is what lets the client keep working without re-running the authorization flow every few minutes.

  • Audience (aud, or resource per RFC 8707) — the resource server(s) the token is valid for. A resource server that skips the audience check will accept a token that was minted for a different API, which is exactly the confused-deputy scenario RFC 9700 §4.9 warns about. See Scopes, Claims and Permissions for resource indicators and multi-audience tokens.

  • Scope (scope) — the delegated capability the token carries, echoed back in the token response (it may be narrower than what was requested, per RFC 6749 §5.1). Scope is a request for capability, not proof of identity or role — see the same page above.

Where a token may be stored, per client type

Storage guidance follows directly from the client’s threat model:

Client type Where the access (and refresh) token may live

Confidential server-side web app

Server-side session store or an encrypted, HttpOnly, Secure session cookie — never sent to the browser as a readable value

Single-page application (SPA)

Not in localStorage or sessionStorage (readable by any script on the page, including an XSS payload); the recommended pattern is a Backend-for-Frontend that keeps tokens server-side — see Browser-Based Apps

Native / mobile app

Platform-provided secure storage (Android Keystore-backed EncryptedSharedPreferences, iOS Keychain) — see Native and Mobile Apps

CLI / daemon / service (client credentials)

Process memory only, re-requested on expiry; never written to a dotfile or shell history in plain text

Public client generally

Refresh tokens for public clients should be sender-constrained and rotated on every use (see below) — a stolen refresh token is more damaging than a stolen access token because it is longer-lived

The token response

A successful token endpoint response (RFC 6749 §5.1) is a JSON object with Cache-Control: no-store and Pragma: no-cache (both mandatory, so intermediary caches never retain a token):

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "8xLOxBtZp8",
  "scope": "orders:read orders:write"
}
  • access_token (required) — the credential itself, opaque to the client regardless of its internal format.

  • token_type (required) — Bearer (RFC 6750) or DPoP (RFC 9449) when sender-constrained; the client uses this to pick the right Authorization header form.

  • expires_in (recommended) — seconds until the access token expires, measured from the moment the response was generated, not from when the client receives or parses it.

  • refresh_token (optional) — present only when the grant and client are configured to receive one; absent for e.g. a plain client_credentials grant, since there is no "session" to refresh.

  • scope (required only if different from what was requested) — best practice is to always echo it, so the client never has to assume it got everything it asked for.

Error responses

A failed token request (RFC 6749 §5.2) returns 400 Bad Request with a machine-readable error code:

HTTP/1.1 400 Bad Request
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store

{
  "error": "invalid_grant",
  "error_description": "Refresh token expired or already used"
}
error Meaning

invalid_request

A required parameter is missing, duplicated, malformed, or otherwise does not conform to the grant

invalid_client

Client authentication failed (unknown client, missing or wrong credential, unsupported authentication method)

invalid_grant

The authorization grant, refresh token, or PKCE code_verifier is invalid, expired, revoked, does not match the redirection URI, or was issued to a different client — also the code returned on refresh-token reuse (see below)

unauthorized_client

The authenticated client is not authorized to use this grant type

unsupported_grant_type

The authorization server does not support the requested grant_type

invalid_scope

The requested scope is invalid, unknown, malformed, or exceeds what the resource owner or client is allowed

Refresh tokens

Why they exist

Access tokens are kept short-lived to bound the damage of a leak, but re-running an entire user-facing authorization flow every few minutes would be unusable. A refresh token is a long-lived credential, issued alongside the access token, that the client exchanges at the token endpoint for a fresh access token (and, usually, a fresh refresh token) without any resource-owner interaction. It shifts the "how do we bound risk" problem from the short-lived access token onto a token that is issued more selectively, stored more carefully, and can be revoked as a unit.

Rotation and reuse detection

Refresh token rotation means every use of a refresh token issues a brand-new refresh token and invalidates the one just used, rather than letting the same refresh token be exchanged repeatedly. RFC 9700 §4.14.2 recommends rotation for public clients (and the in-progress OAuth 2.1 consolidation, draft-ietf-oauth-v2-1-16, makes rotation the expected behaviour for refresh tokens issued to public clients that cannot otherwise be sender-constrained).

Rotation only earns its keep when paired with reuse detection: if a refresh token that has already been rotated away is presented again, the authorization server has strong evidence the token was copied by an attacker (the legitimate client would have moved on to the new one). The correct response is not just to reject that single request — it is to revoke the entire token family descending from the original grant (every refresh token, and typically every access token, issued from that authorization). This limits an attacker who raced the legitimate client to a single stolen use, and it also warns the legitimate client (via the next invalid_grant) that something is wrong.

Absolute vs. idle expiry

Two expiry clocks are commonly combined:

  • Absolute expiry — a hard ceiling on the token family’s lifetime from the moment of the original grant (e.g. 30 days), forcing a full re-authorization no matter how often the token is refreshed.

  • Idle expiry — a shorter window that resets on every successful refresh (e.g. 14 days of inactivity); if the client never comes back, the refresh token dies quietly instead of remaining valid indefinitely.

A session that is refreshed regularly stays alive up to the absolute ceiling; one that goes quiet dies at the idle ceiling, whichever comes first.

Sender-constraining for public clients

A public client (one that cannot hold a confidential secret — a native app or a browser-based app) cannot prove it is the same party the refresh token was issued to using a client secret. RFC 9700 §4.14 and the in-progress OAuth 2.1 draft both push public clients towards sender-constraining the refresh token — typically DPoP, so the authorization server binds the refresh token to the client’s proof-of-possession key and rejects a refresh request from anyone who does not hold that key, even if they have the refresh token’s string value. Where sender-constraining is not available, PKCE (mandatory for public clients under RFC 7636 and OAuth 2.1) plus rotation and reuse detection is the practical minimum. See Authorization Code and PKCE and Sender-Constrained Tokens: DPoP and mTLS.

The refresh_token grant wire format

POST /oauth2/token HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW

grant_type=refresh_token&
refresh_token=8xLOxBtZp8&
scope=orders%3Aread
  • grant_type (required) — literal refresh_token.

  • refresh_token (required) — the token obtained from a previous token or refresh response.

  • scope (optional) — may only narrow the originally granted scope (RFC 6749 §6), never widen it.

  • Client authentication (Authorization: Basic, client_secret_post, private_key_jwt, mTLS, …​) follows whatever method the client registered with — see Client Credentials and Client Authentication.

A successful response looks exactly like the token response above, generally with a new refresh_token value when rotation is enabled — the client must discard the old one and persist the new one atomically, since the old value becomes invalid (and reuse-triggering) the moment the new one is issued.

Token lifecycle: expiry, refresh and rotation

sequenceDiagram participant C as Client participant AS as Authorization server C->>AS: API call with access token Note over C,AS: Access token still valid AS-->>C: 200 OK Note over C: Access token expires C->>AS: POST /oauth2/token\ngrant_type=refresh_token\nrefresh_token=RT1 alt RT1 is current (not yet used) AS-->>C: 200 OK\nnew access_token + refresh_token=RT2 Note over AS: RT1 invalidated, RT2 becomes current else RT1 already used previously (reuse detected) AS-->>C: 400 invalid_grant Note over AS: Entire token family revoked\n(all refresh + access tokens from this grant) C->>AS: Full re-authorization required end