# Harden the OAuth Callback in a Single-Page Application

> PKCE is one control; a safe SPA callback also binds state, issuer, redirect, storage, and one-time processing.

Canonical URL: https://www.devobs.io/articles/spa-pkce-callback-hardening/
By: Lena Fischer
Published: 2023-06-30T01:01:37.791Z
Updated: 2026-09-06T10:18:15.722Z
Section: Identity

## Bind callbacks to stored login transactions

Accept a callback only when it matches a locally initiated transaction: state, expected issuer, exact redirect URI, unexpired PKCE verifier, and one-time processing must all agree. Remove authorization parameters from history after completion. [RFC 7636: PKCE](https://www.rfc-editor.org/rfc/rfc7636) supplies the primary specification or operating model for this decision.

## Test concurrent tabs and retry behavior

Generate state, nonce, and verifier per attempt; store issuer and creation time with them. Reject duplicate parameters and consumed transactions. Handle refresh as a safe completed state, and log error classes without codes or tokens. [RFC 9700: OAuth Security BCP](https://www.rfc-editor.org/rfc/rfc9700) provides the complementary protocol or operational detail.

## Treat callback state as a transaction

Before redirecting, generate state and a PKCE verifier, store them with issuer, client ID, exact redirect URI, and expiry, then send the S256 challenge. On return, atomically consume the transaction by state. Validate issuer where applicable and exchange the code using only metadata bound when the transaction began.

Browser storage choice changes the threat model. Keep transaction material short-lived and scoped to the tab or flow where practical. Never put the verifier in a URL, log, analytics event, or error report.

## Handle tabs, retries, and hostile callbacks

Two tabs may start login concurrently. Key transactions by state rather than one global verifier, and make consumption atomic so a duplicate callback cannot race the first exchange. Clear only the matching transaction after success or terminal failure.

Test wrong state, expired state, wrong issuer, mismatched redirect URI, missing verifier, reused code, duplicate callback, and token-endpoint timeout. A timeout may leave code status uncertain; do not loop exchanges without a bounded policy. Instrument rejection reason without recording codes. Review one deployed callback and verify that every token-endpoint input came from the stored transaction or validated authorization response, never from arbitrary query parameters. Keep callback error pages free of authorization codes and state values, including analytics URLs and client-side exception payloads.

## Source references

- <https://www.rfc-editor.org/rfc/rfc7636>
- <https://www.rfc-editor.org/rfc/rfc9700>
