PKCE and Verification Redirects
Learn how PKCE secures email verification, password reset, email change, and OAuth flows by preventing authorization code interception.
PKCE code challenge code verifier token exchange verification redirect authorization code securityPKCE (Proof Key for Code Exchange, RFC 7636) prevents authorization code interception attacks. Instead of including a refresh token directly in a redirect URL, the server returns an authorization code that can only be exchanged by presenting the original code verifier.
PKCE is used across multiple authentication flows:
- Email/password sign-up (when email verification is enabled)
- Magic links
- Password reset
- Email change
- WebAuthn
- Anonymous deanonymization
- OAuth providers
How It Works
Section titled “How It Works”- The client generates a random
codeVerifierand derives acodeChallengeusing SHA-256 - The
codeChallengeis sent with the initial authentication request - When the user completes verification (clicks an email link, completes OAuth), the server returns an authorization
code - The client exchanges the
code+ originalcodeVerifierviaPOST /token/exchange - The server validates that
SHA256(codeVerifier) == storedCodeChallengeand returns a session
Authorization codes are single-use and expire after 5 minutes.
Generating a PKCE Pair
Section titled “Generating a PKCE Pair”The JavaScript SDK provides a helper for generating PKCE pairs:
import { generatePKCEPair } from '@nhost/nhost-js/auth';
const { verifier, challenge } = await generatePKCEPair();Handling the Verification Redirect
Section titled “Handling the Verification Redirect”Email verification links and OAuth callbacks redirect to your app with a code query parameter. Your verification page must exchange this code for a session using the stored PKCE verifier. Below you can find an example page for React:
import { useEffect, useState } from 'react';import { useNavigate, useLocation } from 'react-router-dom';import { nhost } from './lib/nhost';
export default function Verify() { const navigate = useNavigate(); const location = useLocation(); const [status, setStatus] = useState('verifying'); const [error, setError] = useState('');
useEffect(() => { const params = new URLSearchParams(location.search); const code = params.get('code');
if (!code) { setStatus('error'); setError('No authorization code found in URL'); return; }
async function exchangeCode() { // Retrieve and consume the stored verifier const codeVerifier = localStorage.getItem('nhost_pkce_verifier'); localStorage.removeItem('nhost_pkce_verifier');
if (!codeVerifier) { setStatus('error'); setError( 'No PKCE verifier found. The flow must be initiated from the same browser.', ); return; }
try { await nhost.auth.tokenExchange({ code, codeVerifier }); setStatus('success'); navigate('/profile'); } catch (err) { setStatus('error'); setError(`Verification failed: ${err.message}`); } }
exchangeCode(); }, [location.search, navigate]);
// Render based on status...}Check our tutorials for more detailed examples.