diff --git a/packages/auth/__tests__/providers/cognito/tokenProvider/tokenOrchestrator.test.ts b/packages/auth/__tests__/providers/cognito/tokenProvider/tokenOrchestrator.test.ts index d514772695a..db8fad4d0aa 100644 --- a/packages/auth/__tests__/providers/cognito/tokenProvider/tokenOrchestrator.test.ts +++ b/packages/auth/__tests__/providers/cognito/tokenProvider/tokenOrchestrator.test.ts @@ -96,6 +96,10 @@ describe('tokenOrchestrator', () => { }); describe('handleErrors method', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('does not call clearTokens() if the error is a network error thrown from fetch handler', () => { const clearTokensSpy = jest.spyOn(tokenOrchestrator, 'clearTokens'); const error = new AmplifyError({ @@ -109,5 +113,102 @@ describe('tokenOrchestrator', () => { expect(clearTokensSpy).not.toHaveBeenCalled(); }); + + it('calls clearTokens() for NotAuthorizedException', () => { + const clearTokensSpy = jest.spyOn(tokenOrchestrator, 'clearTokens'); + const error = new AmplifyError({ + name: 'NotAuthorizedException', + message: 'Not authorized', + }); + + const result = (tokenOrchestrator as any).handleErrors(error); + + expect(clearTokensSpy).toHaveBeenCalled(); + expect(result).toBeNull(); + }); + + it('calls clearTokens() for TokenRevokedException', () => { + const clearTokensSpy = jest.spyOn(tokenOrchestrator, 'clearTokens'); + const error = new AmplifyError({ + name: 'TokenRevokedException', + message: 'Token revoked', + }); + + expect(() => { + (tokenOrchestrator as any).handleErrors(error); + }).toThrow(error); + + expect(clearTokensSpy).toHaveBeenCalled(); + }); + + it('calls clearTokens() for UserNotFoundException', () => { + const clearTokensSpy = jest.spyOn(tokenOrchestrator, 'clearTokens'); + const error = new AmplifyError({ + name: 'UserNotFoundException', + message: 'User not found', + }); + + expect(() => { + (tokenOrchestrator as any).handleErrors(error); + }).toThrow(error); + + expect(clearTokensSpy).toHaveBeenCalled(); + }); + + it('does not call clearTokens() for service errors (500)', () => { + const clearTokensSpy = jest.spyOn(tokenOrchestrator, 'clearTokens'); + const error = new AmplifyError({ + name: 'InternalServerError', + message: 'Internal server error', + }); + + expect(() => { + (tokenOrchestrator as any).handleErrors(error); + }).toThrow(error); + + expect(clearTokensSpy).not.toHaveBeenCalled(); + }); + + it('does not call clearTokens() for rate limit errors', () => { + const clearTokensSpy = jest.spyOn(tokenOrchestrator, 'clearTokens'); + const error = new AmplifyError({ + name: 'TooManyRequestsException', + message: 'Too many requests', + }); + + expect(() => { + (tokenOrchestrator as any).handleErrors(error); + }).toThrow(error); + + expect(clearTokensSpy).not.toHaveBeenCalled(); + }); + + it('does not call clearTokens() for throttling errors', () => { + const clearTokensSpy = jest.spyOn(tokenOrchestrator, 'clearTokens'); + const error = new AmplifyError({ + name: 'ThrottlingException', + message: 'Request throttled', + }); + + expect(() => { + (tokenOrchestrator as any).handleErrors(error); + }).toThrow(error); + + expect(clearTokensSpy).not.toHaveBeenCalled(); + }); + + it('does not call clearTokens() for temporary service issues', () => { + const clearTokensSpy = jest.spyOn(tokenOrchestrator, 'clearTokens'); + const error = new AmplifyError({ + name: 'ServiceUnavailable', + message: 'Service temporarily unavailable', + }); + + expect(() => { + (tokenOrchestrator as any).handleErrors(error); + }).toThrow(error); + + expect(clearTokensSpy).not.toHaveBeenCalled(); + }); }); }); diff --git a/packages/auth/src/providers/cognito/tokenProvider/TokenOrchestrator.ts b/packages/auth/src/providers/cognito/tokenProvider/TokenOrchestrator.ts index 3f8027d2596..b6b0cafdf57 100644 --- a/packages/auth/src/providers/cognito/tokenProvider/TokenOrchestrator.ts +++ b/packages/auth/src/providers/cognito/tokenProvider/TokenOrchestrator.ts @@ -9,7 +9,6 @@ import { } from '@aws-amplify/core'; import { AMPLIFY_SYMBOL, - AmplifyErrorCode, assertTokenProviderConfig, isBrowser, isTokenExpired, @@ -170,10 +169,15 @@ export class TokenOrchestrator implements AuthTokenOrchestrator { private handleErrors(err: unknown) { assertServiceError(err); - if (err.name !== AmplifyErrorCode.NetworkError) { - // TODO(v6): Check errors on client + + // Only clear tokens for definitive authentication failures + // Do NOT clear tokens for transient errors like service issues, rate limits, etc. + const shouldClearTokens = this.isAuthenticationError(err); + + if (shouldClearTokens) { this.clearTokens(); } + Hub.dispatch( 'auth', { @@ -190,6 +194,22 @@ export class TokenOrchestrator implements AuthTokenOrchestrator { throw err; } + private isAuthenticationError(err: any): boolean { + // Only clear tokens for errors that definitively indicate the tokens are invalid + // and re-authentication is required. All other errors (service errors, rate limits, etc.) + // should preserve the tokens to allow for retry. + // See: https://github.com/aws-amplify/amplify-js/issues/14534 + const authErrorNames = [ + 'NotAuthorizedException', // Refresh token is expired or invalid + 'TokenRevokedException', // Token was revoked by admin + 'UserNotFoundException', // User no longer exists + 'PasswordResetRequiredException', // User must reset password + 'UserNotConfirmedException', // User account is not confirmed + ]; + + return authErrorNames.some(errorName => err.name?.startsWith(errorName)); + } + async setTokens({ tokens }: { tokens: CognitoAuthTokens }) { return this.getTokenStore().storeTokens(tokens); }