You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
90 lines
2.5 KiB
90 lines
2.5 KiB
// server/utils/pkce.ts
|
|
|
|
/**
|
|
* PKCE (Proof Key for Code Exchange) utilities for OAuth2 security.
|
|
*
|
|
* PKCE prevents authorization code interception attacks by requiring
|
|
* the client to prove possession of the original code verifier.
|
|
*
|
|
* Flow:
|
|
* 1. Generate random code_verifier (43-128 chars)
|
|
* 2. Hash verifier with SHA-256 → code_challenge
|
|
* 3. Send challenge to authorization server
|
|
* 4. Server returns authorization code
|
|
* 5. Exchange code + verifier for tokens
|
|
* 6. Server validates: SHA256(verifier) === stored_challenge
|
|
*/
|
|
|
|
/**
|
|
* Generate a random code verifier (43-128 URL-safe characters)
|
|
*
|
|
* @param length - Length of verifier (default: 64)
|
|
* @returns Base64URL-encoded random string
|
|
*/
|
|
export function generateCodeVerifier(length: number = 64): string {
|
|
// Generate random bytes
|
|
const randomBytes = crypto.getRandomValues(new Uint8Array(length))
|
|
|
|
// Convert to base64url (URL-safe base64 without padding)
|
|
return base64UrlEncode(randomBytes)
|
|
}
|
|
|
|
/**
|
|
* Generate SHA-256 hash of code verifier → code challenge
|
|
*
|
|
* @param verifier - The code verifier
|
|
* @returns Base64URL-encoded SHA-256 hash
|
|
*/
|
|
export async function generateCodeChallenge(verifier: string): Promise<string> {
|
|
// Convert verifier string to Uint8Array
|
|
const encoder = new TextEncoder()
|
|
const data = encoder.encode(verifier)
|
|
|
|
// Hash with SHA-256
|
|
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
|
|
|
|
// Convert to base64url
|
|
return base64UrlEncode(new Uint8Array(hashBuffer))
|
|
}
|
|
|
|
/**
|
|
* Generate PKCE verifier + challenge pair
|
|
*
|
|
* @returns Object with verifier and challenge
|
|
*/
|
|
export async function generatePKCE(): Promise<{
|
|
verifier: string
|
|
challenge: string
|
|
}> {
|
|
const verifier = generateCodeVerifier()
|
|
const challenge = await generateCodeChallenge(verifier)
|
|
|
|
return { verifier, challenge }
|
|
}
|
|
|
|
/**
|
|
* Convert Uint8Array to Base64URL string
|
|
*
|
|
* Base64URL is URL-safe variant of Base64:
|
|
* - Replace '+' with '-'
|
|
* - Replace '/' with '_'
|
|
* - Remove padding '='
|
|
*/
|
|
function base64UrlEncode(buffer: Uint8Array): string {
|
|
// Convert to base64
|
|
const base64 = btoa(String.fromCharCode(...buffer))
|
|
|
|
// Convert to base64url
|
|
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
|
|
}
|
|
|
|
/**
|
|
* Generate random state parameter for CSRF protection
|
|
*
|
|
* @param length - Length of state string (default: 32)
|
|
* @returns Random URL-safe string
|
|
*/
|
|
export function generateState(length: number = 32): string {
|
|
const randomBytes = crypto.getRandomValues(new Uint8Array(length))
|
|
return base64UrlEncode(randomBytes)
|
|
}
|
|
|