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 {