AtAuth
1/**
2 * PKCE (Proof Key for Code Exchange) Utilities
3 *
4 * Handles PKCE code challenge verification for OAuth 2.0
5 */
6
7import crypto from 'crypto';
8
9/**
10 * Verify a PKCE code verifier against a code challenge
11 */
12export function verifyCodeChallenge(
13 codeVerifier: string,
14 codeChallenge: string,
15 method: 'S256' | 'plain' = 'S256'
16): boolean {
17 // Reject plain method -- only S256 is secure
18 if (method === 'plain') {
19 return false;
20 }
21
22 // S256: SHA256(code_verifier) base64url encoded
23 const hash = crypto.createHash('sha256').update(codeVerifier).digest();
24 const computed = hash.toString('base64url');
25
26 // Constant-time comparison to prevent timing attacks
27 const a = Buffer.from(computed);
28 const b = Buffer.from(codeChallenge);
29 if (a.length !== b.length) return false;
30 return crypto.timingSafeEqual(a, b);
31}
32
33/**
34 * Generate a code verifier for testing
35 */
36export function generateCodeVerifier(): string {
37 return crypto.randomBytes(32).toString('base64url');
38}
39
40/**
41 * Generate a code challenge from a code verifier
42 */
43export function generateCodeChallenge(codeVerifier: string, method: 'S256' | 'plain' = 'S256'): string {
44 if (method === 'plain') {
45 return codeVerifier;
46 }
47
48 const hash = crypto.createHash('sha256').update(codeVerifier).digest();
49 return hash.toString('base64url');
50}
51
52/**
53 * Validate code verifier format
54 * Must be 43-128 characters, containing only [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
55 */
56export function isValidCodeVerifier(codeVerifier: string): boolean {
57 if (codeVerifier.length < 43 || codeVerifier.length > 128) {
58 return false;
59 }
60
61 return /^[A-Za-z0-9\-._~]+$/.test(codeVerifier);
62}
63
64/**
65 * Validate code challenge method
66 */
67export function isValidCodeChallengeMethod(method: string): method is 'S256' {
68 return method === 'S256';
69}