@@ -58,8 +58,11 @@ const getSocialRefreshExchangeKey = (rtCode) => `project:social-auth:refresh-exc
5858 */
5959const getFrontendCallbackBaseUrl = ( project ) => {
6060 const configured = String ( project ?. siteUrl || '' ) . trim ( ) ;
61- const base = configured || process . env . FRONTEND_URL || 'http://localhost:5173' ;
62- return `${ base . replace ( / \/ $ / , '' ) } /auth/callback` ;
61+ const base = configured || process . env . FRONTEND_URL || '' ;
62+ if ( ! base ) {
63+ console . warn ( '[social-auth] getFrontendCallbackBaseUrl: siteUrl is not set on the project and FRONTEND_URL env is not configured. Falling back to http://localhost:5173. Set siteUrl in Project Settings or configure FRONTEND_URL.' ) ;
64+ }
65+ return `${ ( base || 'http://localhost:5173' ) . replace ( / \/ $ / , '' ) } /auth/callback` ;
6366} ;
6467
6568/**
@@ -69,6 +72,43 @@ const getFrontendCallbackBaseUrl = (project) => {
6972 */
7073const toBase64UrlBuffer = ( input ) => Buffer . from ( input . replace ( / - / g, '+' ) . replace ( / _ / g, '/' ) . padEnd ( Math . ceil ( input . length / 4 ) * 4 , '=' ) , 'base64' ) ;
7174
75+ /**
76+ * In-memory cache for Google's public JWK keys.
77+ * Keys are refreshed when the cache expires (based on Cache-Control max-age).
78+ */
79+ const googleJwkCache = { keys : null , expiresAt : 0 } ;
80+
81+ /**
82+ * Fetches Google's public JWK keys, using an in-memory cache keyed by Cache-Control max-age.
83+ * @returns {Promise<Array> } Array of JWK key objects
84+ */
85+ const getGooglePublicKeys = async ( ) => {
86+ const now = Date . now ( ) ;
87+ if ( googleJwkCache . keys && now < googleJwkCache . expiresAt ) {
88+ return googleJwkCache . keys ;
89+ }
90+
91+ const certsResponse = await fetch ( 'https://www.googleapis.com/oauth2/v3/certs' ) ;
92+ if ( ! certsResponse . ok ) {
93+ throw new Error ( 'Unable to fetch Google JWK keys' ) ;
94+ }
95+
96+ const certsPayload = await certsResponse . json ( ) ;
97+ const keys = certsPayload . keys || [ ] ;
98+
99+ // Parse Cache-Control max-age from response headers to determine TTL.
100+ const cacheControl = ( typeof certsResponse . headers ?. get === 'function' )
101+ ? ( certsResponse . headers . get ( 'cache-control' ) || '' )
102+ : '' ;
103+ const maxAgeMatch = cacheControl . match ( / m a x - a g e = ( \d + ) / ) ;
104+ const ttlMs = maxAgeMatch ? parseInt ( maxAgeMatch [ 1 ] , 10 ) * 1000 : 3600 * 1000 ;
105+
106+ googleJwkCache . keys = keys ;
107+ googleJwkCache . expiresAt = now + ttlMs ;
108+
109+ return keys ;
110+ } ;
111+
72112/**
73113 * Asserts that a project has auth enabled and a valid users collection schema.
74114 * Throws with an appropriate statusCode on failure.
@@ -315,10 +355,9 @@ const verifyGoogleIdToken = async ({ idToken, clientId }) => {
315355 throw new Error ( 'Unsupported Google id_token signature' ) ;
316356 }
317357
318- const certsResponse = await fetch ( 'https://www.googleapis.com/oauth2/v3/certs' ) ;
319- const certsPayload = await certsResponse . json ( ) ;
320- const signingKey = certsPayload . keys ?. find ( ( key ) => key . kid === header . kid ) ;
321- if ( ! certsResponse . ok || ! signingKey ) {
358+ const certsKeys = await getGooglePublicKeys ( ) ;
359+ const signingKey = certsKeys . find ( ( key ) => key . kid === header . kid ) ;
360+ if ( ! signingKey ) {
322361 throw new Error ( 'Unable to verify Google id_token signing key' ) ;
323362 }
324363
0 commit comments