Authorization Code and PKCE

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.

The authorization code grant (RFC 6749 §4.1) is the flow every other flow on this site is measured against: a redirect-capable user agent (almost always a browser) carries an opaque, single-use authorization code from the authorization server back to the client, and the client exchanges that code for tokens on a back channel the user agent never touches. This page works through every parameter at the wire level — the authorization request, the redirect response, the token request, the token response, and every error code — then covers PKCE (RFC 7636), which the in-progress OAuth 2.1 consolidation (draft-ietf-oauth-v2-1-16, 3 September 2026) makes mandatory for every client, confidential or not.

The round trip in one picture

sequenceDiagram participant RO as Resource owner (browser) participant C as Client participant AS as Authorization server C->>C: generate code_verifier (random, 43-128 chars)\nderive code_challenge = BASE64URL(SHA256(code_verifier)) C->>RO: 302 redirect to /authorize\nresponse_type=code, client_id, redirect_uri,\nscope, state, code_challenge, code_challenge_method=S256 RO->>AS: GET /authorize?... (full request line below) AS->>RO: authenticate resource owner + show consent AS->>RO: 302 redirect to redirect_uri\ncode, state, iss (RFC 9207) RO->>C: GET redirect_uri?code=...&state=...&iss=... C->>C: verify state matches the one it issued C->>AS: POST /token\ngrant_type=authorization_code, code,\nredirect_uri, client_id, code_verifier AS->>AS: recompute BASE64URL(SHA256(code_verifier))\nand compare to the stored code_challenge AS->>C: 200 OK\naccess_token, token_type, expires_in,\nrefresh_token, scope

The authorization request

The client redirects the resource owner’s user agent to the authorization server’s /authorize endpoint as a plain GET with query parameters. Nothing in this request is confidential — it travels through the browser’s address bar, history, and any proxy in front of it — so it must never carry a secret.

GET /authorize?response_type=code
    &client_id=s6BhdRkqt3
    &redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback
    &scope=openid%20profile%20orders.read
    &state=af0ifjsldkj
    &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
    &code_challenge_method=S256
    &nonce=n-0S6_WzA2Mj
    &prompt=login
    &login_hint=alice%40example.com
    &max_age=3600
    &acr_values=urn%3Amace%3Aincommon%3Aiap%3Asilver
    &response_mode=query HTTP/1.1
Host: as.example.com
Parameter Meaning

response_type

code for this flow — asks the authorization server for an authorization code rather than a token directly. Any other value (token, or the removed hybrid combinations) selects a different, mostly legacy, flow.

client_id

The client’s identifier, issued at registration. Not secret; it identifies which registered client configuration (redirect URIs, allowed scopes) applies.

redirect_uri

Where the authorization response is sent. Must match, byte-for-byte, one of the URIs registered for this client_id — see Exact redirect-URI matching below.

scope

Space-delimited list of the permissions requested. openid triggers the OIDC extensions (nonce, ID token). See Scopes, Claims and Permissions.

state

An opaque value the client generates and later verifies unchanged on return — the CSRF defence for the authorization response. See state vs. nonce vs. PKCE.

code_challenge

The PKCE challenge derived from a client-generated secret; see PKCE.

code_challenge_method

S256 (SHA-256, the only method OAuth 2.1 permits) or the deprecated plain.

nonce

An OIDC-only value echoed inside the ID token to bind it to this specific authorization request, defending against ID-token replay. See state vs. nonce vs. PKCE.

prompt

Controls the authorization server’s UI: none (fail silently if interaction would be needed — used for silent re-authentication checks), login (force re-authentication even if a session exists), consent (force the consent screen), select_account.

login_hint

A hint (username, e-mail) to pre-fill the login form; never a substitute for actual authentication.

max_age

Maximum acceptable time, in seconds, since the resource owner last actively authenticated; forces re-authentication if exceeded. Returned to the client as auth_time in the ID token.

acr_values

Space-delimited, preference-ordered list of requested Authentication Context Class Reference values — how the client asks for a particular authentication strength (e.g. MFA) without knowing which specific method the authorization server will use. See Authentication Methods, 2FA and Passwordless.

response_mode

How the authorization response parameters are returned: query (default for this flow, appended to the redirect URI’s query string), fragment, or form_post (POSTed to the redirect URI as form fields — used by Apple’s Sign in with Apple, see Social Login and Federation).

nonce, prompt, max_age, and acr_values are OpenID Connect parameters layered on top of the OAuth 2.0 authorization request, not part of RFC 6749 itself. They are listed here because the authorization code flow is overwhelmingly used together with OIDC in practice; see OpenID Connect for the full picture.

The authorization response

On success, the authorization server redirects back to redirect_uri with the code and the client’s own state echoed unchanged, plus iss identifying which authorization server issued the response (RFC 9207) — the defence against mix-up attacks, covered in full on Security Best Practices.

HTTP/1.1 302 Found
Location: https://client.example.com/callback?code=SplxlOBeZQQYbYS6WxSbIA
    &state=af0ifjsldkj
    &iss=https%3A%2F%2Fas.example.com

The client must verify, before doing anything else with the response:

  1. state equals the value it generated for this request (a mismatch means the response does not belong to this browser session — reject it).

  2. iss, when present, equals the issuer this client expected to talk to (defeats mix-up attacks where two authorization servers share a client and the wrong one’s response is delivered).

  3. The redirect landed on exactly the redirect_uri this client registered — enforced by the authorization server, but worth defence-in-depth logging on the client side too.

Error responses

If the authorization request itself is malformed or refused, the authorization server redirects back the same way with error (and, if it can, state) instead of code:

HTTP/1.1 302 Found
Location: https://client.example.com/callback?error=access_denied
    &error_description=The%20resource%20owner%20denied%20the%20request
    &state=af0ifjsldkj
error Meaning

invalid_request

Missing a required parameter, a parameter appears twice, or the request is otherwise malformed.

unauthorized_client

This client is not authorized to use the authorization code grant.

access_denied

The resource owner or the authorization server denied the request (the user clicked "Deny").

unsupported_response_type

The authorization server does not support response_type=code for this client.

invalid_scope

The requested scope is invalid, unknown, or exceeds what the client may request.

server_error

The authorization server hit an unexpected condition.

temporarily_unavailable

The authorization server is overloaded or under maintenance — retry later.

A malformed redirect_uri or unknown client_id is a special case: the authorization server must not redirect at all (there is nowhere safe to send the error), and instead renders an error page directly, to avoid becoming an open redirector.

The token request

The user agent hands the code to the client at redirect_uri; the client then exchanges it for tokens with a direct, server-to-server POST that never touches the browser. This is where the client authenticates itself (see Client Credentials and Client Authentication for the full comparison of methods) and where the PKCE code_verifier is presented.

POST /token HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW  # client_secret_basic, confidential clients only

grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback
&client_id=s6BhdRkqt3
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

redirect_uri must be repeated here identically to the value sent in the authorization request — it is not used for another redirect, it is compared by the authorization server as proof the code redemption request comes from the same client that started the flow. client_id is required even for confidential clients that also authenticate via Authorization: Basic, since some authentication methods (e.g. private_key_jwt) do not carry it implicitly.

The token response

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
  "access_token": "2YotnFZFEjr1zCsicMWpAA",
  "token_type": "Bearer",
  "expires_in": 600,
  "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
  "scope": "openid profile orders.read",
  "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Cache-Control: no-store and Pragma: no-cache are mandatory on this response — tokens must never end up in a shared cache. id_token is present only when scope included openid. See Access and Refresh Tokens and ID Tokens vs. Access Tokens for what each field means and how each token is validated.

Token endpoint error responses

HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store

{
  "error": "invalid_grant",
  "error_description": "The authorization code has already been used"
}
error Meaning

invalid_request

Missing or duplicated parameter, or the request is otherwise malformed.

invalid_client

Client authentication failed (bad secret, unknown client_id, unsupported authentication method for this client).

invalid_grant

The code is invalid, expired, already redeemed, issued to a different client, or the redirect_uri / code_verifier does not match — see PKCE and the reuse-detection note below.

unauthorized_client

This client is not authorized to use the authorization code grant.

unsupported_grant_type

The authorization server does not support grant_type=authorization_code for this client.

invalid_scope

The requested scope exceeds what was granted at authorization time.

An authorization code is single-use and short-lived (RFC 6749 recommends 10 minutes maximum; most deployments use far less). A code redeemed twice is a strong signal of interception or replay — RFC 9700 recommends treating a second redemption attempt as evidence of compromise and revoking every token already issued from that code, exactly like the refresh-token reuse-detection rule on Access and Refresh Tokens.

PKCE

PKCE — Proof Key for Code Exchange, RFC 7636, pronounced "pixy" — closes a gap the base authorization code flow leaves open: without it, anything that can observe or intercept the authorization response (a malicious app registered for the same custom URI scheme on a mobile device, a network intermediary, a misconfigured redirect) can steal the authorization code and redeem it itself, because redeeming a code required nothing more than knowing it and, for a public client, no secret was ever in play to prevent that.

PKCE fixes this by binding the code to a secret only the client that started the flow knows:

  1. The client generates a code_verifier: a cryptographically random string, 43-128 characters, from the unreserved URI character set.

  2. The client derives a code_challenge from it — code_challenge = BASE64URL(SHA256(code_verifier)) when code_challenge_method=S256 — and sends only the challenge in the authorization request. The verifier itself never leaves the client at this stage.

  3. The authorization server stores the challenge alongside the issued code.

  4. At the token request, the client sends the raw code_verifier. The authorization server recomputes BASE64URL(SHA256(code_verifier)) and compares it to the stored challenge; a mismatch fails the exchange with invalid_grant.

Whoever intercepts the authorization code in transit does not have the code_verifier — it was never sent over that channel — so the stolen code is worthless to them. This is exactly the authorization-code interception attack PKCE was designed to stop.

code_challenge_method=plain (sending the verifier itself as the challenge, unhashed) exists only for clients that cannot compute SHA-256, and provides no protection against an attacker who can read the authorization request — it is effectively PKCE in name only. S256 is the only method OAuth 2.1 permits.

Why OAuth 2.1 requires PKCE even for confidential clients

RFC 6749 originally scoped PKCE as protection for public clients, since a confidential client’s own client authentication at the token endpoint already stops a third party from redeeming a stolen code — the attacker would still need the client secret. In practice this distinction has proven fragile: confidential-client secrets leak (misconfigured repositories, logging, server-side request forgery reading environment variables), multiple server instances sometimes share a secret in ways that widen its blast radius, and there is no operational cost to requiring PKCE everywhere — it is one extra pair of values the client already has the machinery to generate. The in-progress OAuth 2.1 consolidation (draft-ietf-oauth-v2-1-16) removes the distinction and mandates PKCE for every authorization code request, confidential or public, precisely because defence-in-depth against a leaked secret costs nothing and closes an entire class of code-interception incidents outright, rather than leaving them contingent on a second control staying intact.

Spring Authorization Server does not wait for the draft to become a standard: it already requires PKCE by default for public clients and supports it transparently for confidential ones, so a Spring-issued authorization server is already aligned with where OAuth 2.1 is heading. See that page for the RegisteredClient configuration; it is not repeated here.

state vs. nonce vs. PKCE

These three values look similar — all three are client-generated, opaque-looking strings carried through the authorization request — but they protect three different things, and none of them substitutes for another.

Value Protects against Verified by

state

CSRF against the authorization response — an attacker tricking a victim’s browser into completing an authorization flow the attacker initiated, so the victim’s session gets linked to the attacker’s account.

The client, comparing the returned state to the one it generated and bound to the browser session before the redirect.

nonce

Replay of a stolen or reused ID token — an attacker who obtains a previously issued ID token presenting it again to claim a fresh authentication event happened.

The client, comparing the nonce claim inside the ID token to the value it generated for this specific request. OIDC-only; irrelevant when no ID token is requested.

PKCE (code_verifier / code_challenge)

Theft or interception of the authorization code itself between the authorization response and the token request.

The authorization server, recomputing the challenge from the verifier presented at the token endpoint.

All three are typically in play together on any OIDC authorization code request, and none is optional in a hardened deployment: skipping state reopens CSRF, skipping nonce on an OIDC request reopens ID-token replay, and skipping PKCE reopens code interception — three independent attacks with three independent, cheap defences.

Exact redirect-URI matching

RFC 9700 and OAuth 2.1 both mandate exact string matching of redirect_uri against the registered value — scheme, host, port, and path all compared byte-for-byte, with no wildcard subdomains and no pattern matching on the path. Query strings are the one place implementations vary: RFC 6749 allows a registered URI to be a prefix with the client appending additional query parameters, but this has been the source of real vulnerabilities (an attacker appending their own query parameters to redirect a code to a different endpoint on the same host), so current guidance — and OAuth 2.1 — treats the full registered URI, query string included, as the only acceptable match. Loosely matched redirect URIs are consistently the root cause behind open redirector and code-theft incidents; see Security Best Practices for the exact attack shapes this closes.

The OIDC hybrid flow, briefly

OpenID Connect defines hybrid response_type values (code id_token, code token, code id_token token) that return some tokens directly in the authorization response fragment alongside the authorization code — originally useful when a client wanted an ID token immediately, before completing the code exchange, to render a signed-in UI state without waiting on a round trip. The hybrid flow inherits the front-channel token exposure that motivates removing the pure implicit grant (see Legacy Implicit and Password Grants) for whichever tokens it returns directly, so current guidance treats it as a narrow, mostly legacy option: prefer the plain authorization code flow and read the ID token only after the code exchange completes, at which point the ID token was delivered over the back channel like everything else in this page. Full hybrid-flow mechanics are covered on OpenID Connect.