diff --git a/src/theme/Layout/GoogleOneTapInitializer.tsx b/src/theme/Layout/GoogleOneTapInitializer.tsx index 3a51efc4970..fd414e7be89 100644 --- a/src/theme/Layout/GoogleOneTapInitializer.tsx +++ b/src/theme/Layout/GoogleOneTapInitializer.tsx @@ -1,29 +1,70 @@ -import { type ReactNode, useEffect } from 'react'; +import { type ReactNode, useCallback, useEffect } from 'react'; import type { DebugLogger } from './debug-logger'; import type { GoogleOneTapConfig } from './google-one-tap'; - -type GoogleCredentialResponse = { - credential: string; -}; +import { useApiBaseUrl, useGoogleOneTapVerify } from './hooks'; +import type { + SiteConfig, + GoogleOneTapCredentialResponse, + GoogleOneTapVerifyResponse, +} from './types'; +import { appendPath } from '@silverhand/essentials'; type GoogleOneTapInitializerProps = { readonly config: GoogleOneTapConfig; readonly debugLogger: DebugLogger; + readonly siteConfig: SiteConfig; }; export default function GoogleOneTapInitializer({ config, debugLogger, + siteConfig, }: GoogleOneTapInitializerProps): ReactNode { - useEffect(() => { - // Define global handleCredentialResponse function - // eslint-disable-next-line @silverhand/fp/no-mutation - window.handleCredentialResponse = (response: GoogleCredentialResponse) => { - console.log('Encoded JWT ID token:', response.credential); - // TODO: Send to your backend for verification - }; - }, []); + const { logtoAdminConsoleUrl } = useApiBaseUrl(siteConfig); + + // Function to manually build Logto sign-in URL + const buildSignInUrl = useCallback( + ({ credential }: GoogleOneTapVerifyResponse) => { + try { + if (!logtoAdminConsoleUrl) { + throw new Error('Logto admin console URL is not set'); + } + + const signInUrl = new URL(appendPath(new URL(logtoAdminConsoleUrl), 'external-google-one-tap')); + + signInUrl.searchParams.set('credential', credential); + + return signInUrl.toString(); + } catch (error) { + debugLogger.error('Failed to build sign-in URL:', error); + return null; + } + }, + [logtoAdminConsoleUrl, debugLogger] + ); + + const handleCredentialResponse = useCallback( + async (response: GoogleOneTapCredentialResponse) => { + debugLogger.log('handleCredentialResponse received response:', response); + + try { + // Build Logto sign-in URL with one-time token + const signInUrl = buildSignInUrl(response); + + if (signInUrl) { + // Directly navigate to sign-in URL in current window + window.location.href = signInUrl; + debugLogger.log('Redirecting to Logto sign-in URL', signInUrl); + } else { + debugLogger.error('Failed to build sign-in URL'); + } + } catch (error) { + debugLogger.error('Failed to open sign-in URL:', error); + } + }, + [debugLogger, buildSignInUrl] + ); useEffect(() => { if (config.oneTap?.isEnabled && window.google?.accounts.id) { @@ -34,7 +75,7 @@ export default function GoogleOneTapInitializer({ window.google.accounts.id.initialize({ client_id: config.clientId, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - callback: window.handleCredentialResponse!, + callback: handleCredentialResponse, auto_select: config.oneTap.autoSelect, cancel_on_tap_outside: config.oneTap.closeOnTapOutside, itp_support: config.oneTap.itpSupport, diff --git a/src/theme/Layout/auth-status.ts b/src/theme/Layout/auth-status.ts index b78af06bec0..0aa6ecddd94 100644 --- a/src/theme/Layout/auth-status.ts +++ b/src/theme/Layout/auth-status.ts @@ -6,6 +6,7 @@ import { debugIframeTimeoutDelay, } from './constants'; import type { DebugLogger } from './debug-logger'; +import { createStorageAccessChecker } from './storage-access'; import { AuthMessageType, type AuthStatusRequest, type AuthStatusResponse } from './types'; export type AuthStatusCheckerOptions = { @@ -36,6 +37,12 @@ export function createAuthStatusChecker({ const authStatusCheckerHost = typeof logtoAdminConsoleUrl === 'string' ? new URL(logtoAdminConsoleUrl).origin : undefined; + const { checkStorageAccess, requestStorageAccess } = createStorageAccessChecker({ + logtoAdminConsoleUrl, + enableAuthStatusCheck, + debugLogger, + }); + /** * Function to check admin token status via cross-domain iframe communication * @@ -46,6 +53,27 @@ export function createAuthStatusChecker({ * @throws Error if auth status checker is not configured or request fails */ const checkAdminTokenStatus = async (): Promise => { + try { + debugLogger.log('Checking storage access before admin token check'); + + const hasStorageAccess = await checkStorageAccess(); + debugLogger.log('Storage access check result:', hasStorageAccess); + + if (!hasStorageAccess) { + debugLogger.log('Storage access not available, requesting access'); + const storageAccessGranted = await requestStorageAccess(); + debugLogger.log('Storage access request result:', storageAccessGranted); + + if (!storageAccessGranted) { + throw new Error('Storage access required but not granted'); + } + } + + debugLogger.log('Storage access confirmed, proceeding with admin token check'); + } catch (error) { + debugLogger.warn('Storage access check/request failed:', error); + } + return new Promise((resolve, reject) => { if (typeof document === 'undefined') { reject(new Error('Document not available (SSR environment)')); @@ -59,6 +87,7 @@ export function createAuthStatusChecker({ const iframe = document.createElement('iframe'); iframe.src = iframeSrc; + iframe.sandbox = 'allow-scripts allow-same-origin allow-storage-access-by-user-activation'; if (isIframeVisible) { // Temporarily show iframe for debugging @@ -232,6 +261,8 @@ export function createAuthStatusChecker({ return { checkAdminTokenStatus, + checkStorageAccess, + requestStorageAccess, authStatusCheckerHost, iframeSrc, }; diff --git a/src/theme/Layout/config-fetcher.ts b/src/theme/Layout/config-fetcher.ts index 418d582af38..3bad150a09b 100644 --- a/src/theme/Layout/config-fetcher.ts +++ b/src/theme/Layout/config-fetcher.ts @@ -24,11 +24,7 @@ export async function fetchGoogleOneTapConfig({ } } - const response = await fetch(`${apiBaseUrl}/api/google-one-tap/config`, { - headers: { - Origin: window.location.origin, - }, - }); + const response = await fetch(`${apiBaseUrl}/api/google-one-tap/config`); if (!response.ok) { throw new Error('Failed to fetch Google One Tap config'); diff --git a/src/theme/Layout/credential-verifier.ts b/src/theme/Layout/credential-verifier.ts new file mode 100644 index 00000000000..e7b5c69c6d2 --- /dev/null +++ b/src/theme/Layout/credential-verifier.ts @@ -0,0 +1,37 @@ +import type { DebugLogger } from './debug-logger'; +import type { GoogleOneTapCredentialResponse, GoogleOneTapVerifyResponse } from './types'; + +export type CredentialVerifierOptions = { + apiBaseUrl: string; + debugLogger: DebugLogger; +}; + +export async function verifyGoogleOneTapCredential( + { apiBaseUrl, debugLogger }: CredentialVerifierOptions, + response: GoogleOneTapCredentialResponse +): Promise { + debugLogger.log('Google One Tap credential response received:', response); + + try { + const verifyResponse = await fetch(`${apiBaseUrl}/api/google-one-tap/verify`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ idToken: response.credential }), + }); + + if (!verifyResponse.ok) { + throw new Error(`Verification failed: ${verifyResponse.status}`); + } + + const data = await verifyResponse.json(); + debugLogger.log('Google One Tap verification successful:', data); + + // eslint-disable-next-line no-restricted-syntax + return data as GoogleOneTapVerifyResponse; + } catch (error) { + debugLogger.error('Google One Tap verification failed:', error); + return undefined; + } +} diff --git a/src/theme/Layout/hooks.ts b/src/theme/Layout/hooks.ts index 60449b5dd32..89fc99f826c 100644 --- a/src/theme/Layout/hooks.ts +++ b/src/theme/Layout/hooks.ts @@ -1,5 +1,5 @@ /* eslint-disable @silverhand/fp/no-mutation */ -import { condString } from '@silverhand/essentials'; +import { condString, type Optional } from '@silverhand/essentials'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { createAuthStatusChecker } from './auth-status'; @@ -13,9 +13,14 @@ import { initialAuthCheckDelay, authCheckFallbackTimeout, } from './constants'; +import { verifyGoogleOneTapCredential } from './credential-verifier'; import { createDebugLogger, type DebugLogger } from './debug-logger'; import type { GoogleOneTapConfig } from './google-one-tap'; -import type { SiteConfig } from './types'; +import type { + SiteConfig, + GoogleOneTapCredentialResponse, + GoogleOneTapVerifyResponse, +} from './types'; export function useDebugLogger(siteConfig: SiteConfig): DebugLogger { const isDebugMode = Boolean(siteConfig.customFields?.isDebuggingEnabled); @@ -23,21 +28,40 @@ export function useDebugLogger(siteConfig: SiteConfig): DebugLogger { return useMemo(() => createDebugLogger(isDebugMode), [isDebugMode]); } -export function useApiBaseUrl(siteConfig: SiteConfig): string { +export function useApiBaseUrl(siteConfig: SiteConfig): { + baseUrl: string; + authUrl: string; + redirectUri: string; + logtoAdminConsoleUrl?: string; +} { return useMemo(() => { const logtoApiBaseUrl = siteConfig.customFields?.logtoApiBaseUrl; - return typeof logtoApiBaseUrl === 'string' - ? logtoApiBaseUrl - : siteConfig.customFields?.isDevFeatureEnabled - ? defaultApiBaseDevUrl - : defaultApiBaseProdUrl; - }, [siteConfig.customFields?.logtoApiBaseUrl, siteConfig.customFields?.isDevFeatureEnabled]); + const baseUrl = + typeof logtoApiBaseUrl === 'string' + ? logtoApiBaseUrl + : siteConfig.customFields?.isDevFeatureEnabled + ? defaultApiBaseDevUrl + : defaultApiBaseProdUrl; + const authUrl = `${baseUrl}/oidc/auth`; + const redirectUri = `${typeof logtoApiBaseUrl === 'string' ? `${logtoApiBaseUrl}/${new URL(logtoApiBaseUrl).hostname === 'localhost' ? 'demo-app' : 'callback'}` : `${defaultApiBaseProdUrl}/callback`}`; + const logtoAdminConsoleUrl = siteConfig.customFields?.logtoAdminConsoleUrl; + return { + baseUrl, + authUrl, + redirectUri, + logtoAdminConsoleUrl, + }; + }, [ + siteConfig.customFields?.logtoApiBaseUrl, + siteConfig.customFields?.isDevFeatureEnabled, + siteConfig.customFields?.logtoAdminConsoleUrl, + ]); } export function useGoogleOneTapConfig( apiBaseUrl: string, debugLogger: DebugLogger -): GoogleOneTapConfig | undefined { +): Optional { const [config, setConfig] = useState(); useEffect(() => { @@ -65,10 +89,24 @@ export function useGoogleOneTapConfig( return config; } +export function useGoogleOneTapVerify( + apiBaseUrl: string, + debugLogger: DebugLogger +): (response: GoogleOneTapCredentialResponse) => Promise> { + return useCallback( + async (response: GoogleOneTapCredentialResponse) => { + return verifyGoogleOneTapCredential({ apiBaseUrl, debugLogger }, response); + }, + [apiBaseUrl, debugLogger] + ); +} + export type AuthStatusResult = { authStatus?: boolean; authCheckError?: string; checkAdminTokenStatus: () => Promise; + checkStorageAccess: () => Promise; + requestStorageAccess: () => Promise; }; export function useAuthStatus(siteConfig: SiteConfig, debugLogger: DebugLogger): AuthStatusResult { @@ -81,18 +119,19 @@ export function useAuthStatus(siteConfig: SiteConfig, debugLogger: DebugLogger): const isDebugMode = Boolean(siteConfig.customFields?.isDebuggingEnabled); const isIframeVisible = Boolean(siteConfig.customFields?.isIframeVisible); - const { checkAdminTokenStatus, authStatusCheckerHost } = useMemo( - () => - createAuthStatusChecker({ - logtoAdminConsoleUrl: - typeof logtoAdminConsoleUrl === 'string' ? logtoAdminConsoleUrl : undefined, - enableAuthStatusCheck: Boolean(enableAuthStatusCheck), - isDebugMode, - isIframeVisible, - debugLogger, - }), - [logtoAdminConsoleUrl, enableAuthStatusCheck, isDebugMode, isIframeVisible, debugLogger] - ); + const { checkAdminTokenStatus, checkStorageAccess, requestStorageAccess, authStatusCheckerHost } = + useMemo( + () => + createAuthStatusChecker({ + logtoAdminConsoleUrl: + typeof logtoAdminConsoleUrl === 'string' ? logtoAdminConsoleUrl : undefined, + enableAuthStatusCheck: Boolean(enableAuthStatusCheck), + isDebugMode, + isIframeVisible, + debugLogger, + }), + [logtoAdminConsoleUrl, enableAuthStatusCheck, isDebugMode, isIframeVisible, debugLogger] + ); const performAuthCheckWithRetry = useCallback( async (retryCount = 0): Promise => { @@ -185,9 +224,11 @@ export function useAuthStatus(siteConfig: SiteConfig, debugLogger: DebugLogger): authStatus, authCheckError, checkAdminTokenStatus, + checkStorageAccess, + requestStorageAccess, }; } - }, [authStatus, authCheckError, checkAdminTokenStatus]); + }, [authStatus, authCheckError, checkAdminTokenStatus, checkStorageAccess, requestStorageAccess]); // Debug logging useEffect(() => { @@ -219,6 +260,8 @@ export function useAuthStatus(siteConfig: SiteConfig, debugLogger: DebugLogger): authStatus, authCheckError, checkAdminTokenStatus, + checkStorageAccess, + requestStorageAccess, }; } /* eslint-enable @silverhand/fp/no-mutation */ diff --git a/src/theme/Layout/index.tsx b/src/theme/Layout/index.tsx index 732b9c3d72e..69681d0b3b1 100644 --- a/src/theme/Layout/index.tsx +++ b/src/theme/Layout/index.tsx @@ -14,7 +14,7 @@ export default function LayoutWrapper(props: Props): ReactNode { // Hooks must be called at the top level, outside of try-catch const { siteConfig } = useDocusaurusContext(); const debugLogger = useDebugLogger(siteConfig); - const apiBaseUrl = useApiBaseUrl(siteConfig); + const { baseUrl: apiBaseUrl } = useApiBaseUrl(siteConfig); const config = useGoogleOneTapConfig(apiBaseUrl, debugLogger); const { authStatus } = useAuthStatus(siteConfig, debugLogger); @@ -28,7 +28,13 @@ export default function LayoutWrapper(props: Props): ReactNode { {authStatus === false && config?.oneTap?.isEnabled && ( Loading Google Sign-In...}> - {() => } + {() => ( + + )} )} diff --git a/src/theme/Layout/storage-access.ts b/src/theme/Layout/storage-access.ts new file mode 100644 index 00000000000..e5a682b9f84 --- /dev/null +++ b/src/theme/Layout/storage-access.ts @@ -0,0 +1,272 @@ +/* eslint-disable @silverhand/fp/no-mutation */ +import { iframeLoadDelay, requestTimeout } from './constants'; +import type { DebugLogger } from './debug-logger'; +import { AuthMessageType, type StorageAccessRequest, type StorageAccessResponse } from './types'; + +export type StorageAccessCheckerOptions = { + logtoAdminConsoleUrl?: string; + enableAuthStatusCheck?: boolean; + debugLogger: DebugLogger; +}; + +export function createStorageAccessChecker({ + logtoAdminConsoleUrl, + enableAuthStatusCheck, + debugLogger, +}: StorageAccessCheckerOptions) { + const iframeSrc = + typeof logtoAdminConsoleUrl === 'string' ? `${logtoAdminConsoleUrl}/auth-status` : undefined; + + const authStatusCheckerHost = + typeof logtoAdminConsoleUrl === 'string' ? new URL(logtoAdminConsoleUrl).origin : undefined; + + const checkStorageAccess = async (): Promise => { + return new Promise((resolve, reject) => { + if (typeof document === 'undefined') { + reject(new Error('Document not available (SSR environment)')); + return; + } + + if (!logtoAdminConsoleUrl || !enableAuthStatusCheck || !iframeSrc) { + reject(new Error('Auth status checker not configured')); + return; + } + + const iframe = document.createElement('iframe'); + iframe.src = iframeSrc; + iframe.sandbox = 'allow-scripts allow-same-origin allow-storage-access-by-user-activation'; + iframe.style.display = 'none'; + document.body.append(iframe); + + const requestId = Math.random().toString(36).slice(7); + // eslint-disable-next-line @silverhand/fp/no-let, prefer-const + let timeoutId: NodeJS.Timeout; + // eslint-disable-next-line @silverhand/fp/no-let + let messageHandlerAdded = false; + + const handleMessage = (event: MessageEvent) => { + if (event.origin !== authStatusCheckerHost) { + debugLogger.warn( + 'Storage access check: Origin mismatch:', + event.origin, + 'vs', + authStatusCheckerHost + ); + return; + } + + const { data } = event; + if (typeof data !== 'object' || data.requestId !== requestId) { + return; + } + + debugLogger.log('Storage access check response:', data); + + clearTimeout(timeoutId); + if (messageHandlerAdded) { + window.removeEventListener('message', handleMessage); + messageHandlerAdded = false; + } + + if (document.body.contains(iframe)) { + iframe.remove(); + } + + switch (data.type) { + case AuthMessageType.StorageAccessStatus: { + resolve(Boolean(data.hasStorageAccess)); + break; + } + case AuthMessageType.StorageAccessError: { + reject(new Error(String(data.error || 'Storage access check failed'))); + break; + } + } + }; + + window.addEventListener('message', handleMessage); + messageHandlerAdded = true; + + iframe.addEventListener('load', () => { + setTimeout(() => { + try { + const message: StorageAccessRequest = { + type: AuthMessageType.CheckStorageAccess, + requestId, + }; + + debugLogger.log('Sending storage access check message:', message); + iframe.contentWindow?.postMessage(message, authStatusCheckerHost ?? ''); + } catch (error) { + clearTimeout(timeoutId); + if (messageHandlerAdded) { + window.removeEventListener('message', handleMessage); + messageHandlerAdded = false; + } + if (document.body.contains(iframe)) { + iframe.remove(); + } + reject( + new Error( + `Failed to send storage access check message: ${ + error instanceof Error ? error.message : 'Unknown error' + }` + ) + ); + } + }, iframeLoadDelay); + }); + + // eslint-disable-next-line unicorn/prefer-add-event-listener + iframe.onerror = () => { + clearTimeout(timeoutId); + if (messageHandlerAdded) { + window.removeEventListener('message', handleMessage); + } + if (document.body.contains(iframe)) { + iframe.remove(); + } + reject(new Error('Storage access check: iframe failed to load')); + }; + + timeoutId = setTimeout(() => { + if (messageHandlerAdded) { + window.removeEventListener('message', handleMessage); + } + if (document.body.contains(iframe)) { + iframe.remove(); + } + reject(new Error('Storage access check: Request timeout')); + }, requestTimeout); + }); + }; + + const requestStorageAccess = async (): Promise => { + return new Promise((resolve, reject) => { + if (typeof document === 'undefined') { + reject(new Error('Document not available (SSR environment)')); + return; + } + + if (!logtoAdminConsoleUrl || !enableAuthStatusCheck || !iframeSrc) { + reject(new Error('Auth status checker not configured')); + return; + } + + const iframe = document.createElement('iframe'); + iframe.src = iframeSrc; + iframe.sandbox = 'allow-scripts allow-same-origin allow-storage-access-by-user-activation'; + iframe.style.display = 'none'; + document.body.append(iframe); + + const requestId = Math.random().toString(36).slice(7); + // eslint-disable-next-line @silverhand/fp/no-let, prefer-const + let timeoutId: NodeJS.Timeout; + // eslint-disable-next-line @silverhand/fp/no-let + let messageHandlerAdded = false; + + const handleMessage = (event: MessageEvent) => { + if (event.origin !== authStatusCheckerHost) { + debugLogger.warn( + 'Storage access request: Origin mismatch:', + event.origin, + 'vs', + authStatusCheckerHost + ); + return; + } + + const { data } = event; + if (typeof data !== 'object' || data.requestId !== requestId) { + return; + } + + debugLogger.log('Storage access request response:', data); + + clearTimeout(timeoutId); + if (messageHandlerAdded) { + window.removeEventListener('message', handleMessage); + messageHandlerAdded = false; + } + + if (document.body.contains(iframe)) { + iframe.remove(); + } + + switch (data.type) { + case AuthMessageType.StorageAccessStatus: { + resolve(Boolean(data.storageAccessGranted)); + break; + } + case AuthMessageType.StorageAccessError: { + reject(new Error(String(data.error || 'Storage access request failed'))); + break; + } + } + }; + + window.addEventListener('message', handleMessage); + messageHandlerAdded = true; + + iframe.addEventListener('load', () => { + setTimeout(() => { + try { + const message: StorageAccessRequest = { + type: AuthMessageType.RequestStorageAccess, + requestId, + }; + + debugLogger.log('Sending storage access request message:', message); + iframe.contentWindow?.postMessage(message, authStatusCheckerHost ?? ''); + } catch (error) { + clearTimeout(timeoutId); + if (messageHandlerAdded) { + window.removeEventListener('message', handleMessage); + messageHandlerAdded = false; + } + if (document.body.contains(iframe)) { + iframe.remove(); + } + reject( + new Error( + `Failed to send storage access request message: ${ + error instanceof Error ? error.message : 'Unknown error' + }` + ) + ); + } + }, iframeLoadDelay); + }); + + // eslint-disable-next-line unicorn/prefer-add-event-listener + iframe.onerror = () => { + clearTimeout(timeoutId); + if (messageHandlerAdded) { + window.removeEventListener('message', handleMessage); + } + if (document.body.contains(iframe)) { + iframe.remove(); + } + reject(new Error('Storage access request: iframe failed to load')); + }; + + timeoutId = setTimeout(() => { + if (messageHandlerAdded) { + window.removeEventListener('message', handleMessage); + } + if (document.body.contains(iframe)) { + iframe.remove(); + } + reject(new Error('Storage access request: Request timeout')); + }, requestTimeout); + }); + }; + + return { + checkStorageAccess, + requestStorageAccess, + authStatusCheckerHost, + iframeSrc, + }; +} +/* eslint-enable @silverhand/fp/no-mutation */ diff --git a/src/theme/Layout/types.ts b/src/theme/Layout/types.ts index dddafeb8eaa..6661b8b8fbc 100644 --- a/src/theme/Layout/types.ts +++ b/src/theme/Layout/types.ts @@ -2,6 +2,10 @@ export const enum AuthMessageType { CheckAdminToken = 'CheckAdminToken', AdminTokenStatus = 'AdminTokenStatus', AdminTokenError = 'AdminTokenError', + CheckStorageAccess = 'CheckStorageAccess', + RequestStorageAccess = 'RequestStorageAccess', + StorageAccessStatus = 'StorageAccessStatus', + StorageAccessError = 'StorageAccessError', } export type AuthStatusRequest = { @@ -9,6 +13,11 @@ export type AuthStatusRequest = { requestId: string; }; +export type StorageAccessRequest = { + type: AuthMessageType.CheckStorageAccess | AuthMessageType.RequestStorageAccess; + requestId: string; +}; + export type AuthStatusResponse = { type: AuthMessageType.AdminTokenStatus | AuthMessageType.AdminTokenError; requestId: string; @@ -16,6 +25,14 @@ export type AuthStatusResponse = { error?: string; }; +export type StorageAccessResponse = { + type: AuthMessageType.StorageAccessStatus | AuthMessageType.StorageAccessError; + requestId: string; + hasStorageAccess?: boolean; + storageAccessGranted?: boolean; + error?: string; +}; + export type SiteConfig = { customFields?: { isDebuggingEnabled?: boolean; @@ -27,10 +44,20 @@ export type SiteConfig = { }; }; +export type GoogleOneTapCredentialResponse = { + credential: string; +}; + +export type GoogleOneTapVerifyResponse = { + credential: string; +}; + export type AuthStatusGlobal = { authStatus: boolean | undefined; authCheckError: string | undefined; checkAdminTokenStatus: () => Promise; + checkStorageAccess: () => Promise; + requestStorageAccess: () => Promise; }; // Extend Window interface for TypeScript diff --git a/static/_headers b/static/_headers index 6d9ccd935ad..f6a5d5ee1a3 100644 --- a/static/_headers +++ b/static/_headers @@ -6,12 +6,3 @@ # Cross-Origin policies for Google One Tap Cross-Origin-Opener-Policy: same-origin-allow-popups Referrer-Policy: strict-origin-when-cross-origin - - # Content Security Policy for Google One Tap integration - # Essential CSP directives to prevent "Can't continue with google.com" errors: - # - script-src: Allows Google Identity Services scripts - # - img-src: Allows Google profile pictures and branding - # - connect-src: Allows network requests to Google's authentication endpoints - # - style-src: Allows Google's CSS and inline styles - # - font-src: Allows fonts from CDN, Google Fonts, and data URIs - Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://accounts.google.com https://apis.google.com https://www.gstatic.com https://akasha.logto.io https://cdn.jsdelivr.net; img-src 'self' data: https: https://accounts.google.com https://lh3.googleusercontent.com; connect-src 'self' https://accounts.google.com https://oauth2.googleapis.com https://akasha.logto.io https://cloud.logto.dev https://auth.logto.dev https://cloud.logto.io https://auth.logto.io; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com/css2; font-src 'self' data: https://cdn.jsdelivr.net https://fonts.googleapis.com/css2; object-src 'none'; base-uri 'self'; frame-src 'self' https://cloud.logto.dev https://auth.logto.dev https://cloud.logto.io https://auth.logto.io;