Authorization Code + PKCE
The most common OIDC flow. Used by every web app, mobile app, SPA, and desktop app that logs a human in. PKCE (Proof Key for Code Exchange, RFC 7636) is mandatory in this library — both because OAuth 2.0 BCP (RFC 9700) and FAPI 2.0 require it, and because there's no plausible deployment in 2026 that benefits from disabling it.
Specs referenced on this page
- RFC 6749 — OAuth 2.0 Authorization Framework (§5.2 error codes)
- RFC 7636 — Proof Key for Code Exchange (PKCE)
- RFC 9700 — OAuth 2.0 Security Best Current Practice
- RFC 9126 — Pushed Authorization Requests (PAR)
- OpenID Connect Core 1.0 — §3.1 (Authorization Code Flow)
New to the vocabulary?
- Authorization code — a one-time, opaque string the OP hands to the RP via a browser redirect. The RP swaps it at
/tokenfor the actual tokens. - PKCE ("pixie") — a small extra dance with
code_verifier/code_challengethat proves "the client redeeming this code is the same one that started the flow." Stops a malicious app from stealing a redirected code. Walked through in detail below. state— a random opaque value the RP sends with the authorize request and re-checks on the callback; CSRF defence for the redirect.nonce— a random opaque value bound into the ID Token; replay defence at the RP.
The full sequence
Parameter glossary
| Parameter | Sent at | Purpose |
|---|---|---|
response_type=code | /authorize | Asks for the authorization-code grant. |
client_id | /authorize, /token | Identifies the registered RP. |
redirect_uri | /authorize (and echoed at /token) | Where the OP sends the user back. Exact match against the registered list. |
scope | /authorize | Permissions requested. Must include openid for OIDC. |
state | /authorize | Random opaque value the RP echoes on callback. CSRF defense for the redirect. |
nonce | /authorize | Random value bound into the ID Token's nonce claim. Replay defense. |
code_challenge | /authorize | BASE64URL(SHA256(code_verifier)). |
code_challenge_method | /authorize | S256 (the only one this library accepts). |
code | /authorize response | Single-use; max-age 60 s by spec, this lib defaults to that. |
code_verifier | /token | The pre-image of code_challenge. The OP recomputes the SHA-256. |
grant_type=authorization_code | /token | Selects this grant. |
| Client auth | /token | One of client_secret_basic, client_secret_post, private_key_jwt, tls_client_auth, self_signed_tls_client_auth, or none (PKCE-only). |
What PKCE prevents
Walk-through: the attack PKCE blocks
Without PKCE, a malicious app on the same device that controls a URI-handler for myapp:// can intercept the authorization-code redirect:
- User logs in on the legit RP. OP issues
code=abctomyapp://callback. - Malicious app intercepts the redirect (race condition or universal-link spoof) and reads
code=abc. - Malicious app posts
code=abcto/tokenand gets tokens.
PKCE binds the code to a secret only the legitimate RP knows:
- The legit RP generates a random
code_verifierand sends onlySHA256(code_verifier)(thecode_challenge) to/authorize. - The OP stores
code_challengealongside the issued code. - At
/token, the OP requirescode_verifierand recomputes the SHA-256. - The malicious app saw the code but never saw the verifier — its
/tokencall fails.
This works even when the RP can't store a client secret (SPA / native).
How this library enforces it
| Behaviour | Where |
|---|---|
code_challenge_method=plain is rejected — only S256 accepted. | internal/pkce |
Authorization request without code_challenge is rejected when the client's RequiresPKCE is true (default for public clients, forced for FAPI 2.0). | internal/authorize |
code_verifier length and char-set are validated against RFC 7636 §4.1. | internal/pkce |
Mismatch returns RFC 6749 §5.2 invalid_grant at /token (not /authorize). | internal/tokenendpoint/authcode.go |
Common errors and what they mean
| Wire error | Cause | Where to look |
|---|---|---|
invalid_request code_challenge_method | Client sent plain | Send S256 |
invalid_request_uri | PAR request_uri expired or already consumed | New PAR request |
invalid_grant (at /token) | code_verifier doesn't match, or code already used / expired | Don't reuse codes, regenerate |
redirect_uri_mismatch | The redirect_uri at /token differs from /authorize | They must be byte-identical |
Run the flow yourself
examples/03-fapi2 runs a FAPI 2.0 Baseline OP that demands PAR + JAR + DPoP + PKCE in one wiring. The OFCS conformance suite drives this exact sequence through ~129 modules in two FAPI plans; OFCS status shows the breakdown.
Read next
- Sender constraint (DPoP / mTLS) — how PKCE upgrades to "the access token only works for the client that got it."
- Refresh tokens — what to do when the access token expires.