From 43b5a51ec73214d3c56aa48c82783ccfeec1a127 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 2 Feb 2026 23:27:40 +0800 Subject: [PATCH] fix(auth): make PKCE optional for OAuth sign-in (#5570) Fixes issue where OAuth sign-in fails with 'Cannot read properties of undefined (reading 'digest')' when accessing Memos over HTTP. The crypto.subtle API is only available in secure contexts (HTTPS or localhost), but PKCE (RFC 7636) is optional per OAuth 2.0 standards. Changes: - Make PKCE generation optional with graceful fallback - Use PKCE when crypto.subtle available (HTTPS/localhost) - Fall back to standard OAuth flow when unavailable (HTTP) - Log warning to console when PKCE unavailable - Only include code_challenge in auth URL when PKCE enabled The backend already supports optional PKCE (empty codeVerifier), so no backend changes needed. This fix aligns frontend behavior with backend. Benefits: - OAuth sign-in works on HTTP deployments (reverse proxy scenarios) - Enhanced security (PKCE) still used when HTTPS available - Backward compatible with OAuth providers that don't support PKCE Fixes #5570 --- web/src/pages/SignIn.tsx | 14 ++++++++++---- web/src/utils/oauth.ts | 32 +++++++++++++++++++++++++++----- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/web/src/pages/SignIn.tsx b/web/src/pages/SignIn.tsx index 6502c3853..2b39cc163 100644 --- a/web/src/pages/SignIn.tsx +++ b/web/src/pages/SignIn.tsx @@ -49,17 +49,23 @@ const SignIn = () => { try { // Generate and store secure state parameter with CSRF protection - // Also generate PKCE parameters (code_challenge) for enhanced security + // Also generate PKCE parameters (code_challenge) for enhanced security if available const identityProviderId = extractIdentityProviderIdFromName(identityProvider.name); const { state, codeChallenge } = await storeOAuthState(identityProviderId); - // Build OAuth authorization URL with secure state and PKCE + // Build OAuth authorization URL with secure state + // Include PKCE if available (requires HTTPS/localhost for crypto.subtle) // Using S256 (SHA-256) as the code_challenge_method per RFC 7636 - const authUrl = `${oauth2Config.authUrl}?client_id=${ + let authUrl = `${oauth2Config.authUrl}?client_id=${ oauth2Config.clientId }&redirect_uri=${encodeURIComponent(redirectUri)}&state=${state}&response_type=code&scope=${encodeURIComponent( oauth2Config.scopes.join(" "), - )}&code_challenge=${codeChallenge}&code_challenge_method=S256`; + )}`; + + // Add PKCE parameters if available + if (codeChallenge) { + authUrl += `&code_challenge=${codeChallenge}&code_challenge_method=S256`; + } window.location.href = authUrl; } catch (error) { diff --git a/web/src/utils/oauth.ts b/web/src/utils/oauth.ts index a6dc0e98d..a3cb5c9a4 100644 --- a/web/src/utils/oauth.ts +++ b/web/src/utils/oauth.ts @@ -40,18 +40,40 @@ function base64UrlEncode(buffer: Uint8Array): string { } // Store OAuth state and PKCE parameters in sessionStorage -// Returns both state and codeChallenge for use in authorization URL -export async function storeOAuthState(identityProviderId: number, returnUrl?: string): Promise<{ state: string; codeChallenge: string }> { +// Returns state and optional codeChallenge for use in authorization URL +// PKCE is optional - if crypto APIs are unavailable (HTTP context), falls back to standard OAuth +export async function storeOAuthState(identityProviderId: number, returnUrl?: string): Promise<{ state: string; codeChallenge?: string }> { const state = generateSecureState(); - const codeVerifier = generateCodeVerifier(); - const codeChallenge = await generateCodeChallenge(codeVerifier); + + // Try to generate PKCE parameters if crypto.subtle is available (HTTPS/localhost) + // Falls back to standard OAuth flow if unavailable (HTTP context) + let codeVerifier: string | undefined; + let codeChallenge: string | undefined; + + try { + // Check if crypto.subtle is available (requires secure context: HTTPS or localhost) + if (typeof crypto !== "undefined" && crypto.subtle) { + codeVerifier = generateCodeVerifier(); + codeChallenge = await generateCodeChallenge(codeVerifier); + } else { + console.warn( + "PKCE not available: crypto.subtle requires HTTPS. Falling back to standard OAuth flow without PKCE. " + + "For enhanced security, please access Memos over HTTPS.", + ); + } + } catch (error) { + // If PKCE generation fails for any reason, fall back to standard OAuth + console.warn("Failed to generate PKCE parameters, falling back to standard OAuth:", error); + codeVerifier = undefined; + codeChallenge = undefined; + } const stateData: OAuthState = { state, identityProviderId, timestamp: Date.now(), returnUrl, - codeVerifier, // Store for later retrieval in callback + codeVerifier, // Store for later retrieval in callback (undefined if PKCE not available) }; try {