@@ -4,6 +4,7 @@ import axios from 'axios';
44import { eq } from 'drizzle-orm' ;
55import { PostgresJsDatabase } from 'drizzle-orm/postgres-js' ;
66import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb' ;
7+ import { addSeconds } from 'date-fns' ;
78import { PKCECodeChallenge , UserInfoEndpointResponse , UserSession } from '../types/index.js' ;
89import * as schema from '../db/schema.js' ;
910import { sessions } from '../db/schema.js' ;
@@ -38,7 +39,7 @@ export type AuthUtilsOptions = {
3839 } ;
3940} ;
4041
41- const tokenExpirationWindowSkew = 60 * 5 ;
42+ const tokenExpirationWindowSkew = 60 * 5 ; // 5 minutes
4243const pkceMaxAgeSec = 60 * 15 ; // 15 minutes
4344const pkceCodeAlgorithm = 'S256' ;
4445const scope = 'openid profile email' ;
@@ -298,6 +299,18 @@ export default class AuthUtils {
298299 } ;
299300 }
300301
302+ public static isSessionExpired ( session : { createdAt : Date ; updatedAt : Date | null ; expiresAt : Date } ) : boolean {
303+ const now = new Date ( ) ;
304+ if ( session . expiresAt <= now ) {
305+ // Session reached end-of-life
306+ return true ;
307+ }
308+
309+ const sessionLastUpdatedOrCreation = session . updatedAt ?? session . createdAt ;
310+ const sessionExpiresAt = addSeconds ( sessionLastUpdatedOrCreation , DEFAULT_SESSION_MAX_AGE_SEC ) ;
311+ return sessionExpiresAt <= now ;
312+ }
313+
301314 /**
302315 * renewSession renews the user session if the access token is expired.
303316 * If the refresh token is expired, an error is thrown.
@@ -327,30 +340,27 @@ export default class AuthUtils {
327340 throw new AuthenticationError ( EnumStatusCode . ERROR_NOT_AUTHENTICATED , 'Refresh token expired' ) ;
328341 }
329342
330- // The session expiration is relative to the creation time
331- const baseMs = userSession . createdAt . getTime ( ) ;
332- const expiresAtMs = baseMs + DEFAULT_SESSION_MAX_AGE_SEC * 1000 ;
333- const sessionExpiresDate = new Date ( expiresAtMs ) ;
334- const remainingSeconds = Math . max ( 0 , Math . floor ( ( expiresAtMs - Date . now ( ) ) / 1000 ) ) ;
335-
336- if ( remainingSeconds <= 0 ) {
343+ // The session expiration is relative
344+ if ( AuthUtils . isSessionExpired ( userSession ) ) {
337345 // Absolute session lifetime has elapsed; do not renew.
338346 throw new AuthenticationError ( EnumStatusCode . ERROR_NOT_AUTHENTICATED , 'Session expired' ) ;
339347 }
340348
341349 // Refresh the access token with the refresh token
342350 // The method will throw an error if the request fails
351+ const now = new Date ( ) ;
343352 const { accessToken, refreshToken, idToken } = await this . refreshToken ( userSession . refreshToken ) ;
344353
345354 // Update active session
355+ const expiresAt = addSeconds ( now , DEFAULT_SESSION_MAX_AGE_SEC ) ;
346356 const updatedSessions = await this . db
347357 . update ( sessions )
348358 . set ( {
349359 accessToken,
350360 refreshToken,
351- expiresAt : sessionExpiresDate ,
361+ expiresAt,
352362 idToken,
353- updatedAt : new Date ( ) ,
363+ updatedAt : now ,
354364 } )
355365 . where ( eq ( sessions . id , sessionId ) )
356366 . returning ( )
@@ -363,7 +373,7 @@ export default class AuthUtils {
363373 const newUserSession = updatedSessions [ 0 ] ;
364374
365375 const jwt = await encrypt < UserSession > ( {
366- maxAgeInSeconds : remainingSeconds ,
376+ maxAgeInSeconds : DEFAULT_SESSION_MAX_AGE_SEC ,
367377 token : {
368378 iss : userSession . userId ,
369379 sessionId : newUserSession . id ,
@@ -372,7 +382,7 @@ export default class AuthUtils {
372382 } ) ;
373383
374384 // Update the session cookie
375- this . createSessionCookie ( res , jwt , sessionExpiresDate ) ;
385+ this . createSessionCookie ( res , jwt , expiresAt ) ;
376386
377387 return newUserSession ;
378388 }
0 commit comments