Use Authorization Code Flow with PKCE for Browser-Based OAuth Clients
PKCE lets public clients like SPAs use the OAuth Authorization Code flow without a client secret.
10 Mar 2026, 02:00 UTC

Your SPA cannot store a client secret
A browser-based application is a public client: anything shipped to the browser can be inspected by the user. That means the OAuth client secret cannot stay secret. If you embed a secret in JavaScript, it is not a secret.
Historically, teams worked around this with the Implicit flow, which returns an access token directly from the authorization endpoint. Implicit flow is now discouraged because the token is exposed in the browser redirect and cannot be bound to a request the way an authorization code can. The token also arrives without the client proving it originated the request.
The practical replacement is the Authorization Code flow with PKCE (RFC 7636). PKCE stands for Proof Key for Code Exchange. It lets a public client prove it originated the authorization request without using a client secret.
How PKCE binds the two requests
PKCE adds two pieces of data to the flow:
- Code verifier: a high-entropy string generated by the client, kept temporarily, and sent only in the token request.
- Code challenge: a transformed version of the verifier sent in the authorization request. With SHA-256, the challenge is
base64url(sha256(verifier)).
The authorization server stores the challenge when the user approves the request. When the client exchanges the authorization code for tokens, the server hashes the submitted verifier and compares it to the stored challenge. If they do not match, the token request fails.
Use S256 for code_challenge_method. The plain method offers no real protection and should be avoided.
PKCE does not replace the state parameter or strict redirect-URI matching. Use all three together.
Worked example: authorization and token requests
This example shows the two HTTP messages. Replace the placeholders with values from your authorization server. No command output is shown because this is a request-flow example, not a tested session.
1. Authorization request (browser)
Runs in the user's browser when the SPA starts the login flow. Generate the verifier first, then derive the challenge.
GET https://auth.example.com/authorize? response_type=code& client_id=spa-client-123& redirect_uri=https://app.example.com/callback& scope=openid profile& state=random-state-value& code_challenge=BASE64URL_ENCODED_CHALLENGE& code_challenge_method=S256
The code_verifier must be at least 43 characters of unreserved base64url entropy. Do not send it in this request.
2. Token request (terminal or service)
After the authorization server redirects back to your SPA with an authorization code, exchange it at the token endpoint. You can test this from a terminal with curl, but never paste a real verifier into shell history or logs.
curl -X POST https://auth.example.com/token \ -d grant_type=authorization_code \ -d code=AUTHORIZATION_CODE_FROM_REDIRECT \ -d redirect_uri=https://app.example.com/callback \ -d client_id=spa-client-123 \ -d code_verifier=ORIGINAL_CODE_VERIFIER
Where to run: a terminal or your SPA's HTTP client. Permissions: none special for the request itself, but the authorization server must allow public clients and enforce PKCE. Expected check: a valid verifier returns tokens; a wrong or missing verifier is rejected. Risk: the verifier appears in the request body and shell history if you use curl interactively.
Verify the behavior before you rely on it
- Open your browser's network inspector and start the login flow.
- Confirm the authorization request includes
code_challengeandcode_challenge_method=S256. - Confirm the token request includes the matching
code_verifier. - Repeat the token request with a deliberately wrong verifier. The server should reject it.
Limitation: PKCE protects the code, not the token
PKCE mitigates authorization-code interception. It does not make a public client confidential, and it does not protect an access token after issuance. If an attacker can run JavaScript in your SPA—such as through cross-site scripting (XSS)—they can still steal the token from memory or local storage. PKCE also does not relax redirect-URI allow-list requirements; exact matching remains mandatory.
Use short-lived tokens, strict redirect-URI validation, and consider refresh-token rotation where your authorization server supports it.
What to do next
If your browser-based app still uses Implicit flow, plan a migration to Authorization Code with PKCE. Generate a verifier per authentication attempt, send the S256 challenge in the authorization request, verify the verifier in the token request, and keep state and redirect-URI validation enabled. PKCE is not a complete security boundary by itself, but it is the correct default for public clients.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.