@@ -287,13 +287,28 @@ export class DirectCognitoAuthService {
287
287
return null ;
288
288
}
289
289
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
+
290
305
return {
291
306
id_token : idToken ,
292
307
access_token : accessToken ,
293
308
refresh_token : refreshToken || '' ,
294
309
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 ,
297
312
} ;
298
313
}
299
314
@@ -419,59 +434,132 @@ export class DirectCognitoAuthService {
419
434
}
420
435
421
436
/**
422
- * Refreshes the token using the refresh token if available
437
+ * Check if tokens need refresh
423
438
* @param tokens Current tokens
424
- * @returns Updated tokens if refresh succeeded, original tokens otherwise
439
+ * @returns Boolean indicating if refresh is needed
425
440
*/
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 ;
435
445
}
436
446
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 > {
437
465
try {
438
466
const refreshParams = {
439
467
AuthFlow : 'REFRESH_TOKEN_AUTH' as AuthFlowType ,
440
468
ClientId : this . clientId ,
441
469
AuthParameters : {
442
- REFRESH_TOKEN : tokens . refresh_token ,
470
+ REFRESH_TOKEN : refreshToken ,
443
471
} ,
444
472
} ;
445
473
446
474
const refreshCommand = new InitiateAuthCommand ( refreshParams ) ;
447
475
const refreshResponse = await this . client . send ( refreshCommand ) ;
448
476
449
477
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 ;
451
480
}
452
481
453
482
const { IdToken, AccessToken, ExpiresIn } = refreshResponse . AuthenticationResult ;
454
483
455
484
if ( ! IdToken || ! AccessToken ) {
456
- return tokens ; // Missing tokens, return original tokens
485
+ console . warn ( 'Token refresh missing ID or Access token' ) ;
486
+ return null ;
457
487
}
458
488
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
464
489
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 ,
470
493
} ;
471
494
} 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 ;
474
559
}
560
+
561
+ // Update and return the refreshed tokens
562
+ return this . updateTokensWithRefreshed ( tokens , refreshedTokens ) ;
475
563
}
476
564
477
565
/**
@@ -553,18 +641,73 @@ export class DirectCognitoAuthService {
553
641
throw new Error ( 'No active session found' ) ;
554
642
}
555
643
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
557
653
const refreshedTokens = await this . refreshTokensIfNeeded ( tokens ) ;
558
654
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 ) ;
561
683
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
+ }
564
690
565
- return { credentials } ;
691
+ // Re-throw other errors
692
+ throw credentialError ;
693
+ }
566
694
} catch ( error ) {
567
695
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
+
568
711
throw new Error (
569
712
'Failed to get authentication session: ' +
570
713
( error instanceof Error ? error . message : String ( error ) ) ,
0 commit comments