Skip to content

OAuth 2.0 Security: Single-Page Apps & PKCE

CoreConceptAugust 3, 20269 min read

Single-Page Applications (SPAs) executing inside client-side web browsers are classified by OAuth 2.0 standards as Public Clients. Unlike confidential backend servers, browser JavaScript cannot securely store client secrets without exposing them to reverse-engineering, Cross-Site Scripting (XSS), or browser developer tools.

Historically, SPAs used the deprecated Implicit Flow, which returned access tokens directly in URL hash fragments (#access_token=...), exposing tokens to browser history logs and referrer headers. The modern gold standard is OAuth 2.0 with PKCE (Proof Key for Code Exchange). This guide details PKCE cryptographic verifiers, authorization code exchanges, and secure token storage strategies using HTTP-only cookies.

OAuth 2.0 PKCE authentication flow and HTTP-only cookie token security
OAuth 2.0 PKCE authentication flow and HTTP-only cookie token security

Mental Model: Proof Key for Code Exchange (PKCE) Flow

PKCE dynamically binds an authorization code request to the specific browser session that initiated the login flow, neutralizing authorization code interception attacks.

Before redirecting the user to the Identity Provider (IdP), the SPA generates a high-entropy random string called the Code Verifier. It creates a cryptographic SHA-256 hash of this string called the Code Challenge.

The SPA sends the code_challenge to the authorization server during login. When the IdP redirects back with an authorization_code, the SPA sends both the authorization_code AND the unhashed code_verifier back to token endpoint. The IdP hashes code_verifier and verifies it matches original code_challenge before returning access tokens. For API-level OAuth rules, read securing rest apis oauth2 jwt best practices and zero trust architecture cloud native microservices.

OAuth 2.0 PKCE authorization code request and token redemption step flow
OAuth 2.0 PKCE authorization code request and token redemption step flow

Quick reference

  • PKCE replaces client secrets with dynamic per-request cryptographic verifier pairs.
  • Code Verifier is a high-entropy unguessable random string (43 to 128 characters).
  • Code Challenge is generated via base64url(sha256(code_verifier)).
  • Prevents malicious browser extensions from intercepting and redeeming authorization codes.
  • Required by IETF OAuth 2.1 specifications for all browser-based public clients.

Remember this

Implement PKCE by sending code_challenge during login and code_verifier during token redemption.

Generating Cryptographic Code Verifier & Code Challenge Pairs

Generating secure PKCE verifiers requires using browser Web Crypto APIs (window.crypto.getRandomValues) rather than predictable Math.random() functions.

First, generate a 32-byte cryptographically secure random array and encode it as URL-safe base64 string (code_verifier). Second, pass code_verifier through crypto.subtle.digest('SHA-256', buffer) to compute its SHA-256 hash digest (code_challenge).

Store code_verifier temporarily in sessionStorage or an in-memory variable during authorization redirect, and clear it immediately after exchanging the code for tokens.

Quick reference

  • Use Web Crypto API (window.crypto.getRandomValues) to generate 32 random bytes.
  • Apply base64url encoding without padding characters (=, +, /) to satisfy RFC 7636.
  • Compute SHA-256 hash using crypto.subtle.digest('SHA-256', verifierBytes).
  • Store code_verifier temporarily in sessionStorage during login redirect.
  • Clear code_verifier from client storage immediately after token exchange completes.

Remember this

Use Web Crypto API window.crypto.getRandomValues and SHA-256 to create URL-safe PKCE verifier pairs.

Token Storage: In-Memory vs HTTP-Only SameSite Cookies

Storing access tokens in localStorage or sessionStorage makes them vulnerable to XSS (Cross-Site Scripting) attacks. Any malicious third-party script injected via an npm dependency or CDN script tag can read localStorage.getItem('access_token') and exfiltrate user credentials.

To achieve maximum security, store tokens inside HTTP-Only, Secure, SameSite=Strict cookies managed by a Backend-For-Frontend (BFF) proxy or serverless API route.

JavaScript running in the browser DOM cannot access HTTP-Only cookies. When the SPA calls API routes, the browser includes the cookie automatically, protecting tokens from XSS exfiltration.

OAuth 2.0 PKCE authorization code request and token redemption step flow
OAuth 2.0 PKCE authorization code request and token redemption step flow

Quick reference

  • Never store sensitive JWT access or refresh tokens in localStorage or sessionStorage.
  • Malicious XSS scripts can read localStorage variables and steal authentication tokens.
  • Use HTTP-Only cookies to make tokens completely invisible to JavaScript DOM APIs.
  • Set Secure flag to enforce HTTPS-only cookie transmission across network adapters.
  • Apply SameSite=Strict or SameSite=Lax flags to neutralize Cross-Site Request Forgery (CSRF).

Remember this

Store authentication tokens inside HTTP-Only SameSite=Strict cookies to protect tokens from XSS exfiltration.

Handling Silent Refresh Tokens & Token Rotation

Because SPA access tokens should be short-lived (e.g., 15 minutes expiration), application sessions require automated background token renewal.

Implement Refresh Token Rotation. Every time the client presents a refresh token to obtain a new access token, the IdP invalidates the old refresh token and issues a brand-new refresh token.

If an attacker captures a leaked refresh token and attempts to use it after legitimate rotation, the IdP detects reuse, invalidates all tokens associated with that user session immediately, and forces a fresh re-authentication.

Quick reference

  • Configure short-lived access tokens (15 minutes) paired with refresh token rotation.
  • Identity Provider issues a new refresh token upon every refresh request execution.
  • Refresh token reuse detection automatically invalidates compromised user sessions.
  • Execute silent token refresh using background fetch calls before access tokens expire.
  • Handle token expiration gracefully by redirecting users to login without losing active form state.

Remember this

Deploy Refresh Token Rotation with reuse detection to protect short-lived access token sessions.

Key takeaway

To test OAuth 2.0 PKCE flow, trigger login in Chrome DevTools. Inspect network requests to confirm code_challenge is sent to authorization endpoint and code_verifier is passed to /oauth/token.

Share:

Related Articles

HTTPS and a valid JWT only prove the front door locked. Many real API incidents happen after authentication succeeds: a

Read

You log out and the admin panel still accepts the old token. Or you build "Sign in with Google" and accidentally treat a

Read

Jul 1, 2026 · 12 min read

A partner script scrapes your API with a leaked key. A mobile build ships a password in every header. A "Sign in with Gi

Read

Keep learning

Follow a structured path or browse all courses to go deeper.