Skip to content

Commit 5012094

Browse files
authored
Merge pull request #155 from ModusCreateOrg/NO-TICKET-FIX-AUTH
feat: enhance token management by extracting expiration from ID token and improving refresh logic
2 parents 1b25781 + b1be482 commit 5012094

File tree

2 files changed

+178
-34
lines changed

2 files changed

+178
-34
lines changed

frontend/src/common/services/auth/direct-cognito-auth-service.ts

Lines changed: 177 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -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)),

frontend/src/common/utils/i18n/resources/en/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"uploadSuccessful": "Upload Successful",
5454
"fileReadyForProcessing": "Your file is ready for processing",
5555
"uploadFailed": "Upload Failed",
56+
"secondsLeft": "seconds left",
5657
"error": {
5758
"noFile": "No file selected",
5859
"permissionDenied": "Permission denied",

0 commit comments

Comments
 (0)