@@ -287,13 +287,28 @@ export class DirectCognitoAuthService {
287287 return null ;
288288 }
289289
290+ // Try to extract expiration time from the ID token
291+ let expiresIn = 0 ;
292+ let expiresAt = '' ;
293+ try {
294+ const payload = JSON . parse ( atob ( idToken . split ( '.' ) [ 1 ] ) ) ;
295+ if ( payload . exp ) {
296+ // exp is in seconds since epoch
297+ const expirationTime = payload . exp * 1000 ; // Convert to milliseconds
298+ expiresIn = Math . max ( 0 , Math . floor ( ( expirationTime - Date . now ( ) ) / 1000 ) ) ;
299+ expiresAt = new Date ( expirationTime ) . toISOString ( ) ;
300+ }
301+ } catch ( error ) {
302+ console . warn ( 'Error parsing ID token expiration:' , error ) ;
303+ }
304+
290305 return {
291306 id_token : idToken ,
292307 access_token : accessToken ,
293308 refresh_token : refreshToken || '' ,
294309 token_type : 'bearer' ,
295- expires_in : 0 , // We don't store this in localStorage
296- expires_at : '' , // We don't store this in localStorage
310+ expires_in : expiresIn ,
311+ expires_at : expiresAt ,
297312 } ;
298313 }
299314
@@ -419,59 +434,132 @@ export class DirectCognitoAuthService {
419434 }
420435
421436 /**
422- * Refreshes the token using the refresh token if available
437+ * Check if tokens need refresh
423438 * @param tokens Current tokens
424- * @returns Updated tokens if refresh succeeded, original tokens otherwise
439+ * @returns Boolean indicating if refresh is needed
425440 */
426- private static async refreshTokensIfNeeded ( tokens : UserTokens ) : Promise < UserTokens > {
427- // Check if token is about to expire (within 5 minutes)
428- const needsRefresh = tokens . expires_at
429- ? new Date ( tokens . expires_at ) . getTime ( ) - Date . now ( ) < 5 * 60 * 1000
430- : false ;
431-
432- // If token doesn't need refresh or we don't have a refresh token, return original tokens
433- if ( ! needsRefresh || ! tokens . refresh_token ) {
434- return tokens ;
441+ private static isTokenRefreshNeeded ( tokens : UserTokens ) : boolean {
442+ // If we don't have a refresh token, we can't refresh
443+ if ( ! tokens . refresh_token ) {
444+ return false ;
435445 }
436446
447+ // Check if token is expired or about to expire (within 10 minutes)
448+ const expTime = tokens . expires_at ? new Date ( tokens . expires_at ) . getTime ( ) : 0 ;
449+ const isExpired = expTime > 0 && expTime <= Date . now ( ) ;
450+ const isExpiringSoon = expTime > 0 && expTime - Date . now ( ) < 10 * 60 * 1000 ;
451+
452+ return isExpired || isExpiringSoon ;
453+ }
454+
455+ /**
456+ * Perform the token refresh API call
457+ * @param refreshToken The refresh token to use
458+ * @returns The new tokens or null if refresh failed
459+ */
460+ private static async performTokenRefresh ( refreshToken : string ) : Promise < {
461+ IdToken : string ;
462+ AccessToken : string ;
463+ ExpiresIn : number ;
464+ } | null > {
437465 try {
438466 const refreshParams = {
439467 AuthFlow : 'REFRESH_TOKEN_AUTH' as AuthFlowType ,
440468 ClientId : this . clientId ,
441469 AuthParameters : {
442- REFRESH_TOKEN : tokens . refresh_token ,
470+ REFRESH_TOKEN : refreshToken ,
443471 } ,
444472 } ;
445473
446474 const refreshCommand = new InitiateAuthCommand ( refreshParams ) ;
447475 const refreshResponse = await this . client . send ( refreshCommand ) ;
448476
449477 if ( ! refreshResponse . AuthenticationResult ) {
450- return tokens ; // No auth result, return original tokens
478+ console . warn ( 'Token refresh did not return authentication result' ) ;
479+ return null ;
451480 }
452481
453482 const { IdToken, AccessToken, ExpiresIn } = refreshResponse . AuthenticationResult ;
454483
455484 if ( ! IdToken || ! AccessToken ) {
456- return tokens ; // Missing tokens, return original tokens
485+ console . warn ( 'Token refresh missing ID or Access token' ) ;
486+ return null ;
457487 }
458488
459- // Update tokens in local storage
460- localStorage . setItem ( 'cognito_id_token' , IdToken ) ;
461- localStorage . setItem ( 'cognito_access_token' , AccessToken ) ;
462-
463- // Return updated tokens
464489 return {
465- ...tokens ,
466- id_token : IdToken ,
467- access_token : AccessToken ,
468- expires_in : ExpiresIn || 3600 ,
469- expires_at : new Date ( Date . now ( ) + ( ExpiresIn || 3600 ) * 1000 ) . toISOString ( ) ,
490+ IdToken,
491+ AccessToken,
492+ ExpiresIn : ExpiresIn || 3600 ,
470493 } ;
471494 } catch ( error ) {
472- console . warn ( 'Token refresh failed, proceeding with existing token:' , error ) ;
473- return tokens ; // Return original tokens on error
495+ console . warn ( 'Token refresh failed:' , error ) ;
496+
497+ // Check for invalid refresh token
498+ if (
499+ error instanceof Error &&
500+ ( error . name === 'NotAuthorizedException' || error . message . includes ( 'Invalid refresh token' ) )
501+ ) {
502+ console . warn ( 'Clearing invalid refresh token' ) ;
503+ localStorage . removeItem ( 'cognito_refresh_token' ) ;
504+ }
505+
506+ return null ;
507+ }
508+ }
509+
510+ /**
511+ * Update local tokens with refreshed values
512+ * @param tokens Original tokens
513+ * @param newTokens New token values
514+ * @returns Updated UserTokens object
515+ */
516+ private static updateTokensWithRefreshed (
517+ tokens : UserTokens ,
518+ newTokens : { IdToken : string ; AccessToken : string ; ExpiresIn : number } ,
519+ ) : UserTokens {
520+ const { IdToken, AccessToken, ExpiresIn } = newTokens ;
521+
522+ // Calculate new expiration time
523+ const expiresAt = new Date ( Date . now ( ) + ExpiresIn * 1000 ) . toISOString ( ) ;
524+ console . log ( 'Tokens refreshed successfully, new expiration:' , expiresAt ) ;
525+
526+ // Update tokens in local storage
527+ localStorage . setItem ( 'cognito_id_token' , IdToken ) ;
528+ localStorage . setItem ( 'cognito_access_token' , AccessToken ) ;
529+
530+ // Return updated tokens
531+ return {
532+ ...tokens ,
533+ id_token : IdToken ,
534+ access_token : AccessToken ,
535+ expires_in : ExpiresIn ,
536+ expires_at : expiresAt ,
537+ } ;
538+ }
539+
540+ /**
541+ * Refreshes the token using the refresh token if available
542+ * @param tokens Current tokens
543+ * @returns Updated tokens if refresh succeeded, original tokens otherwise
544+ */
545+ private static async refreshTokensIfNeeded ( tokens : UserTokens ) : Promise < UserTokens > {
546+ // Check if token needs refresh
547+ if ( ! this . isTokenRefreshNeeded ( tokens ) ) {
548+ return tokens ;
549+ }
550+
551+ console . log ( 'Refreshing tokens - current token expires at:' , tokens . expires_at ) ;
552+
553+ // Perform token refresh
554+ const refreshedTokens = await this . performTokenRefresh ( tokens . refresh_token ) ;
555+
556+ // If refresh failed, return original tokens
557+ if ( ! refreshedTokens ) {
558+ return tokens ;
474559 }
560+
561+ // Update and return the refreshed tokens
562+ return this . updateTokensWithRefreshed ( tokens , refreshedTokens ) ;
475563 }
476564
477565 /**
@@ -553,18 +641,73 @@ export class DirectCognitoAuthService {
553641 throw new Error ( 'No active session found' ) ;
554642 }
555643
556- // Refresh tokens if needed
644+ // Check for expired token
645+ if ( tokens . expires_at ) {
646+ const expTime = new Date ( tokens . expires_at ) . getTime ( ) ;
647+ if ( expTime <= Date . now ( ) ) {
648+ console . warn ( 'Token is expired, attempting to refresh' ) ;
649+ }
650+ }
651+
652+ // Always try to refresh tokens
557653 const refreshedTokens = await this . refreshTokensIfNeeded ( tokens ) ;
558654
559- // Get identity ID
560- const identityId = await this . getIdentityId ( refreshedTokens ) ;
655+ try {
656+ // Get identity ID
657+ const identityId = await this . getIdentityId ( refreshedTokens ) ;
658+
659+ // Get AWS credentials
660+ const credentials = await this . getAWSCredentials ( identityId , refreshedTokens . id_token ) ;
661+
662+ return { credentials } ;
663+ } catch ( credentialError ) {
664+ console . error ( 'Error getting credentials:' , credentialError ) ;
665+
666+ // If we get authentication errors, force token refresh one more time
667+ if (
668+ credentialError instanceof Error &&
669+ ( credentialError . name === 'NotAuthorizedException' ||
670+ credentialError . message . includes ( 'Invalid login token' ) ||
671+ credentialError . message . includes ( 'Token expired' ) )
672+ ) {
673+ console . warn ( 'Auth error detected, forcing token refresh' ) ;
674+
675+ // Force a refresh by simulating an expired token
676+ const forcedRefreshTokens = {
677+ ...refreshedTokens ,
678+ expires_at : new Date ( Date . now ( ) - 1000 ) . toISOString ( ) ,
679+ } ;
680+
681+ // Try one more refresh
682+ const finalTokens = await this . refreshTokensIfNeeded ( forcedRefreshTokens ) ;
561683
562- // Get AWS credentials
563- const credentials = await this . getAWSCredentials ( identityId , refreshedTokens . id_token ) ;
684+ // Try again with refreshed tokens
685+ const identityId = await this . getIdentityId ( finalTokens ) ;
686+ const credentials = await this . getAWSCredentials ( identityId , finalTokens . id_token ) ;
687+
688+ return { credentials } ;
689+ }
564690
565- return { credentials } ;
691+ // Re-throw other errors
692+ throw credentialError ;
693+ }
566694 } catch ( error ) {
567695 console . error ( 'Error fetching auth session:' , error ) ;
696+
697+ // If we still have auth errors after all attempts, try to sign the user out
698+ // to force a fresh login on next attempt
699+ if (
700+ error instanceof Error &&
701+ ( error . name === 'NotAuthorizedException' ||
702+ error . message . includes ( 'Invalid login token' ) ||
703+ error . message . includes ( 'Token expired' ) )
704+ ) {
705+ console . warn ( 'Authentication failed completely, clearing local session' ) ;
706+ localStorage . removeItem ( 'cognito_id_token' ) ;
707+ localStorage . removeItem ( 'cognito_access_token' ) ;
708+ localStorage . removeItem ( 'cognito_refresh_token' ) ;
709+ }
710+
568711 throw new Error (
569712 'Failed to get authentication session: ' +
570713 ( error instanceof Error ? error . message : String ( error ) ) ,
0 commit comments