diff --git a/packages/auth/__tests__/client/flows/shared/handlePasswordSRP.test.ts b/packages/auth/__tests__/client/flows/shared/handlePasswordSRP.test.ts index de71d7a071b..6c66929065a 100644 --- a/packages/auth/__tests__/client/flows/shared/handlePasswordSRP.test.ts +++ b/packages/auth/__tests__/client/flows/shared/handlePasswordSRP.test.ts @@ -6,7 +6,9 @@ import { createCognitoUserPoolEndpointResolver } from '../../../../src/providers import { getAuthenticationHelper } from '../../../../src/providers/cognito/utils/srp'; import { getUserContextData } from '../../../../src/providers/cognito/utils/userContextData'; import { handlePasswordSRP } from '../../../../src/client/flows/shared/handlePasswordSRP'; -import * as signInHelpers from '../../../../src/providers/cognito/utils/signInHelpers'; +import { handlePasswordVerifierChallenge } from '../../../../src/providers/cognito/utils/handlePasswordVerifierChallenge'; +import { retryOnResourceNotFoundException } from '../../../../src/providers/cognito/utils/retryOnResourceNotFoundException'; +import { setActiveSignInUsername } from '../../../../src/providers/cognito/utils/setActiveSignInUsername'; // Mock dependencies jest.mock( @@ -15,14 +17,13 @@ jest.mock( jest.mock('../../../../src/providers/cognito/factories'); jest.mock('../../../../src/providers/cognito/utils/srp'); jest.mock('../../../../src/providers/cognito/utils/userContextData'); -jest.mock('../../../../src/providers/cognito/utils/signInHelpers', () => ({ - ...jest.requireActual( - '../../../../src/providers/cognito/utils/signInHelpers', - ), - setActiveSignInUsername: jest.fn(), - handlePasswordVerifierChallenge: jest.fn(), - retryOnResourceNotFoundException: jest.fn(), -})); +jest.mock( + '../../../../src/providers/cognito/utils/handlePasswordVerifierChallenge', +); +jest.mock( + '../../../../src/providers/cognito/utils/retryOnResourceNotFoundException', +); +jest.mock('../../../../src/providers/cognito/utils/setActiveSignInUsername'); describe('handlePasswordSRP', () => { const mockConfig = { @@ -31,6 +32,13 @@ describe('handlePasswordSRP', () => { userPoolEndpoint: 'test-endpoint', }; + const mockHandlePasswordVerifierChallenge = jest.mocked( + handlePasswordVerifierChallenge, + ); + const mockRetryOnResourceNotFoundException = jest.mocked( + retryOnResourceNotFoundException, + ); + const mockSetActiveSignInUsername = jest.mocked(setActiveSignInUsername); const mockInitiateAuth = jest.fn(); const mockCreateEndpointResolver = jest.fn(); const mockAuthenticationHelper = { @@ -53,9 +61,9 @@ describe('handlePasswordSRP', () => { (getUserContextData as jest.Mock).mockReturnValue({ UserContextData: 'test', }); - ( - signInHelpers.retryOnResourceNotFoundException as jest.Mock - ).mockImplementation((fn, args) => fn(...args)); + mockRetryOnResourceNotFoundException.mockImplementation((fn, args) => + fn(...args), + ); mockInitiateAuth.mockResolvedValue({ ChallengeParameters: { USERNAME: 'testuser' }, Session: 'test-session', @@ -173,8 +181,8 @@ describe('handlePasswordSRP', () => { authFlow: 'USER_AUTH', }); - expect(signInHelpers.retryOnResourceNotFoundException).toHaveBeenCalledWith( - signInHelpers.handlePasswordVerifierChallenge, + expect(mockRetryOnResourceNotFoundException).toHaveBeenCalledWith( + mockHandlePasswordVerifierChallenge, [ password, challengeParameters, @@ -208,9 +216,7 @@ describe('handlePasswordSRP', () => { }); expect(result).toEqual(mockResponse); - expect( - signInHelpers.retryOnResourceNotFoundException, - ).not.toHaveBeenCalled(); + expect(mockRetryOnResourceNotFoundException).not.toHaveBeenCalled(); }); test('should handle client metadata when provided', async () => { @@ -254,9 +260,7 @@ describe('handlePasswordSRP', () => { authFlow: 'USER_SRP_AUTH', }); - expect(signInHelpers.setActiveSignInUsername).toHaveBeenCalledWith( - challengeUsername, - ); + expect(mockSetActiveSignInUsername).toHaveBeenCalledWith(challengeUsername); }); test('should call handlePasswordVerifierChallenge with correct parameters', async () => { @@ -285,8 +289,8 @@ describe('handlePasswordSRP', () => { authFlow: 'USER_SRP_AUTH', }); - expect(signInHelpers.retryOnResourceNotFoundException).toHaveBeenCalledWith( - signInHelpers.handlePasswordVerifierChallenge, + expect(mockRetryOnResourceNotFoundException).toHaveBeenCalledWith( + mockHandlePasswordVerifierChallenge, [ password, challengeParameters, @@ -341,9 +345,7 @@ describe('handlePasswordSRP', () => { authFlow: 'USER_AUTH', }); - expect(signInHelpers.setActiveSignInUsername).toHaveBeenCalledWith( - username, - ); + expect(mockSetActiveSignInUsername).toHaveBeenCalledWith(username); }); test('should not add PREFERRED_CHALLENGE for USER_AUTH when preferredChallenge is undefined', async () => { diff --git a/packages/auth/__tests__/client/flows/userAuth/handleSelectChallengeWithPassword.test.ts b/packages/auth/__tests__/client/flows/userAuth/handleSelectChallengeWithPassword.test.ts index 78322b59536..9d304e03871 100644 --- a/packages/auth/__tests__/client/flows/userAuth/handleSelectChallengeWithPassword.test.ts +++ b/packages/auth/__tests__/client/flows/userAuth/handleSelectChallengeWithPassword.test.ts @@ -5,7 +5,7 @@ import { createRespondToAuthChallengeClient } from '../../../../src/foundation/f import { createCognitoUserPoolEndpointResolver } from '../../../../src/providers/cognito/factories'; import { getUserContextData } from '../../../../src/providers/cognito/utils/userContextData'; import { handleSelectChallengeWithPassword } from '../../../../src/client/flows/userAuth/handleSelectChallengeWithPassword'; -import * as signInHelpers from '../../../../src/providers/cognito/utils/signInHelpers'; +import { setActiveSignInUsername } from '../../../../src/providers/cognito/utils/setActiveSignInUsername'; // Mock dependencies jest.mock( @@ -13,12 +13,7 @@ jest.mock( ); jest.mock('../../../../src/providers/cognito/factories'); jest.mock('../../../../src/providers/cognito/utils/userContextData'); -jest.mock('../../../../src/providers/cognito/utils/signInHelpers', () => ({ - ...jest.requireActual( - '../../../../src/providers/cognito/utils/signInHelpers', - ), - setActiveSignInUsername: jest.fn(), -})); +jest.mock('../../../../src/providers/cognito/utils/setActiveSignInUsername'); describe('handlePasswordChallenge', () => { const mockConfig = { @@ -27,6 +22,7 @@ describe('handlePasswordChallenge', () => { userPoolEndpoint: 'test-endpoint', }; + const mockSetActiveSignInUsername = jest.mocked(setActiveSignInUsername); const mockRespondToAuthChallenge = jest.fn(); const mockCreateEndpointResolver = jest.fn(); @@ -124,9 +120,7 @@ describe('handlePasswordChallenge', () => { session, ); - expect(signInHelpers.setActiveSignInUsername).toHaveBeenCalledWith( - challengeUsername, - ); + expect(mockSetActiveSignInUsername).toHaveBeenCalledWith(challengeUsername); }); test('should set active username as original username when challenge parameters are missing', async () => { @@ -148,9 +142,7 @@ describe('handlePasswordChallenge', () => { session, ); - expect(signInHelpers.setActiveSignInUsername).toHaveBeenCalledWith( - username, - ); + expect(mockSetActiveSignInUsername).toHaveBeenCalledWith(username); }); test('should throw error when respondToAuthChallenge fails', async () => { diff --git a/packages/auth/__tests__/client/flows/userAuth/handleSelectChallengeWithPasswordSRP.test.ts b/packages/auth/__tests__/client/flows/userAuth/handleSelectChallengeWithPasswordSRP.test.ts index b89414c3ae1..ba44e6e293e 100644 --- a/packages/auth/__tests__/client/flows/userAuth/handleSelectChallengeWithPasswordSRP.test.ts +++ b/packages/auth/__tests__/client/flows/userAuth/handleSelectChallengeWithPasswordSRP.test.ts @@ -5,7 +5,9 @@ import { createRespondToAuthChallengeClient } from '../../../../src/foundation/f import { getAuthenticationHelper } from '../../../../src/providers/cognito/utils/srp'; import { getUserContextData } from '../../../../src/providers/cognito/utils/userContextData'; import { handleSelectChallengeWithPasswordSRP } from '../../../../src/client/flows/userAuth/handleSelectChallengeWithPasswordSRP'; -import * as signInHelpers from '../../../../src/providers/cognito/utils/signInHelpers'; +import { handlePasswordVerifierChallenge } from '../../../../src/providers/cognito/utils/handlePasswordVerifierChallenge'; +import { retryOnResourceNotFoundException } from '../../../../src/providers/cognito/utils/retryOnResourceNotFoundException'; +import { setActiveSignInUsername } from '../../../../src/providers/cognito/utils/setActiveSignInUsername'; // Mock dependencies jest.mock( @@ -14,14 +16,13 @@ jest.mock( jest.mock('../../../../src/providers/cognito/factories'); jest.mock('../../../../src/providers/cognito/utils/srp'); jest.mock('../../../../src/providers/cognito/utils/userContextData'); -jest.mock('../../../../src/providers/cognito/utils/signInHelpers', () => ({ - ...jest.requireActual( - '../../../../src/providers/cognito/utils/signInHelpers', - ), - setActiveSignInUsername: jest.fn(), - handlePasswordVerifierChallenge: jest.fn(), - retryOnResourceNotFoundException: jest.fn(), -})); +jest.mock( + '../../../../src/providers/cognito/utils/handlePasswordVerifierChallenge', +); +jest.mock( + '../../../../src/providers/cognito/utils/retryOnResourceNotFoundException', +); +jest.mock('../../../../src/providers/cognito/utils/setActiveSignInUsername'); describe('handleSelectChallengeWithPasswordSRP', () => { const mockConfig = { @@ -35,6 +36,13 @@ describe('handleSelectChallengeWithPasswordSRP', () => { clearDeviceMetadata: jest.fn(), } as any; + const mockHandlePasswordVerifierChallenge = jest.mocked( + handlePasswordVerifierChallenge, + ); + const mockRetryOnResourceNotFoundException = jest.mocked( + retryOnResourceNotFoundException, + ); + const mockSetActiveSignInUsername = jest.mocked(setActiveSignInUsername); const mockRespondToAuthChallenge = jest.fn(); const mockAuthenticationHelper = { A: { toString: () => '123456' }, @@ -108,13 +116,12 @@ describe('handleSelectChallengeWithPasswordSRP', () => { }; mockRespondToAuthChallenge.mockResolvedValueOnce(verifierResponse); - ( - signInHelpers.retryOnResourceNotFoundException as jest.Mock - ).mockImplementation((fn, args) => fn(...args)); - ( - signInHelpers.handlePasswordVerifierChallenge as jest.Mock - ).mockResolvedValue({ + mockRetryOnResourceNotFoundException.mockImplementation((fn, args) => + fn(...args), + ); + mockHandlePasswordVerifierChallenge.mockResolvedValue({ AuthenticationResult: { AccessToken: 'token' }, + $metadata: {}, }); await handleSelectChallengeWithPasswordSRP( @@ -126,8 +133,8 @@ describe('handleSelectChallengeWithPasswordSRP', () => { mockTokenOrchestrator, ); - expect(signInHelpers.retryOnResourceNotFoundException).toHaveBeenCalledWith( - signInHelpers.handlePasswordVerifierChallenge, + expect(mockRetryOnResourceNotFoundException).toHaveBeenCalledWith( + mockHandlePasswordVerifierChallenge, [ password, verifierResponse.ChallengeParameters, @@ -188,9 +195,7 @@ describe('handleSelectChallengeWithPasswordSRP', () => { mockTokenOrchestrator, ); - expect(signInHelpers.setActiveSignInUsername).toHaveBeenCalledWith( - challengeUsername, - ); + expect(mockSetActiveSignInUsername).toHaveBeenCalledWith(challengeUsername); }); test('should use original username when ChallengeParameters is undefined', async () => { @@ -215,9 +220,7 @@ describe('handleSelectChallengeWithPasswordSRP', () => { ); // Verify it falls back to the original username - expect(signInHelpers.setActiveSignInUsername).toHaveBeenCalledWith( - username, - ); + expect(mockSetActiveSignInUsername).toHaveBeenCalledWith(username); }); test('should handle userPoolId without second part after underscore', async () => { diff --git a/packages/auth/__tests__/providers/cognito/getNewDeviceMetadata.test.ts b/packages/auth/__tests__/providers/cognito/getNewDeviceMetadata.test.ts index 6058fc363b7..f05c4f8c603 100644 --- a/packages/auth/__tests__/providers/cognito/getNewDeviceMetadata.test.ts +++ b/packages/auth/__tests__/providers/cognito/getNewDeviceMetadata.test.ts @@ -5,7 +5,7 @@ import { Amplify } from '@aws-amplify/core'; import { AuthError } from '../../../src/errors/AuthError'; import { ConfirmDeviceException } from '../../../src/providers/cognito/types/errors'; -import { getNewDeviceMetadata } from '../../../src/providers/cognito/utils/signInHelpers'; +import { getNewDeviceMetadata } from '../../../src/providers/cognito/utils/getNewDeviceMetadata'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; import { createConfirmDeviceClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; diff --git a/packages/auth/__tests__/providers/cognito/utils/signInHelpers/handleWebAuthnSignInResult.test.ts b/packages/auth/__tests__/providers/cognito/utils/signInHelpers/handleWebAuthnSignInResult.test.ts index dc9a7c2296a..bf5773ac0bd 100644 --- a/packages/auth/__tests__/providers/cognito/utils/signInHelpers/handleWebAuthnSignInResult.test.ts +++ b/packages/auth/__tests__/providers/cognito/utils/signInHelpers/handleWebAuthnSignInResult.test.ts @@ -19,6 +19,11 @@ import { assertCredentialIsPkcWithAuthenticatorAssertionResponse, assertCredentialIsPkcWithAuthenticatorAttestationResponse, } from '../../../../../src/client/utils/passkey/types'; +import { AuthSignInOutput } from '../../../../../src/types'; +import { + ChallengeName, + ChallengeParameters, +} from '../../../../../src/foundation/factories/serviceClients/cognitoIdentityProvider/types'; jest.mock('@aws-amplify/core', () => ({ ...(jest.createMockFromModule('@aws-amplify/core') as object), @@ -101,6 +106,7 @@ describe('handleWebAuthnSignInResult', () => { expect(error.name).toBe(AuthErrorCodes.SignInException); } }); + it('should throw an error when CREDENTIAL_REQUEST_OPTIONS is empty', async () => { expect.assertions(2); try { @@ -166,9 +172,51 @@ describe('handleWebAuthnSignInResult', () => { mockCacheCognitoTokens.mockResolvedValue(undefined); mockDispatchSignedInHubEvent.mockResolvedValue(undefined); - const result = await handleWebAuthnSignInResult(challengeParameters); + const result = (await handleWebAuthnSignInResult( + challengeParameters, + )) as AuthSignInOutput; expect(result.isSignedIn).toBe(true); expect(result.nextStep.signInStep).toBe('DONE'); }); + + it('should return the next challenge', async () => { + mockStoreGetState.mockReturnValue({ + username, + challengeName, + signInSession, + }); + mockRespondToAuthChallenge.mockResolvedValue( + authAPITestParams.CustomChallengeResponse, + ); + mockCacheCognitoTokens.mockResolvedValue(undefined); + mockDispatchSignedInHubEvent.mockResolvedValue(undefined); + + const result = (await handleWebAuthnSignInResult(challengeParameters)) as { + challengeName: ChallengeName; + challengeParameters: ChallengeParameters; + }; + + expect(result.challengeName).toBe( + authAPITestParams.CustomChallengeResponse.ChallengeName, + ); + }); + + it('should throw an error if next challenge is WEB_AUTHN', async () => { + mockStoreGetState.mockReturnValue({ + username, + challengeName, + signInSession, + }); + mockRespondToAuthChallenge.mockResolvedValue({ + ChallengeName: 'WEB_AUTHN', + Session: 'Session', + }); + mockCacheCognitoTokens.mockResolvedValue(undefined); + mockDispatchSignedInHubEvent.mockResolvedValue(undefined); + + await expect( + handleWebAuthnSignInResult(challengeParameters), + ).rejects.toThrow('Sequential WEB_AUTHN challenges returned'); + }); }); diff --git a/packages/auth/src/client/flows/shared/handlePasswordSRP.ts b/packages/auth/src/client/flows/shared/handlePasswordSRP.ts index 77e298867df..6afbb873229 100644 --- a/packages/auth/src/client/flows/shared/handlePasswordSRP.ts +++ b/packages/auth/src/client/flows/shared/handlePasswordSRP.ts @@ -13,16 +13,14 @@ import { RespondToAuthChallengeCommandOutput, } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; import { getAuthenticationHelper } from '../../../providers/cognito/utils/srp'; -import { - handlePasswordVerifierChallenge, - retryOnResourceNotFoundException, - setActiveSignInUsername, -} from '../../../providers/cognito/utils/signInHelpers'; import { createInitiateAuthClient } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../providers/cognito/factories'; import { getRegionFromUserPoolId } from '../../../foundation/parsers'; import { getAuthUserAgentValue } from '../../../utils'; import { AuthFactorType } from '../../../providers/cognito/types/models'; +import { handlePasswordVerifierChallenge } from '../../../providers/cognito/utils/handlePasswordVerifierChallenge'; +import { retryOnResourceNotFoundException } from '../../../providers/cognito/utils/retryOnResourceNotFoundException'; +import { setActiveSignInUsername } from '../../../providers/cognito/utils/setActiveSignInUsername'; interface HandlePasswordSRPInput { username: string; diff --git a/packages/auth/src/client/flows/userAuth/handleSelectChallengeWithPassword.ts b/packages/auth/src/client/flows/userAuth/handleSelectChallengeWithPassword.ts index 50858764c79..3369f755148 100644 --- a/packages/auth/src/client/flows/userAuth/handleSelectChallengeWithPassword.ts +++ b/packages/auth/src/client/flows/userAuth/handleSelectChallengeWithPassword.ts @@ -11,7 +11,7 @@ import { getRegionFromUserPoolId } from '../../../foundation/parsers'; import { getAuthUserAgentValue } from '../../../utils'; import { getUserContextData } from '../../../providers/cognito/utils/userContextData'; import { RespondToAuthChallengeCommandOutput } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; -import { setActiveSignInUsername } from '../../../providers/cognito/utils/signInHelpers'; +import { setActiveSignInUsername } from '../../../providers/cognito/utils/setActiveSignInUsername'; /** * Handles the SELECT_CHALLENGE response specifically for Password authentication. diff --git a/packages/auth/src/client/flows/userAuth/handleSelectChallengeWithPasswordSRP.ts b/packages/auth/src/client/flows/userAuth/handleSelectChallengeWithPasswordSRP.ts index 1a463e60a68..2613b0abbbe 100644 --- a/packages/auth/src/client/flows/userAuth/handleSelectChallengeWithPasswordSRP.ts +++ b/packages/auth/src/client/flows/userAuth/handleSelectChallengeWithPasswordSRP.ts @@ -16,11 +16,9 @@ import { getRegionFromUserPoolId } from '../../../foundation/parsers'; import { getAuthUserAgentValue } from '../../../utils'; import { getAuthenticationHelper } from '../../../providers/cognito/utils/srp'; import { getUserContextData } from '../../../providers/cognito/utils/userContextData'; -import { - handlePasswordVerifierChallenge, - retryOnResourceNotFoundException, - setActiveSignInUsername, -} from '../../../providers/cognito/utils/signInHelpers'; +import { setActiveSignInUsername } from '../../../providers/cognito/utils/setActiveSignInUsername'; +import { retryOnResourceNotFoundException } from '../../../providers/cognito/utils/retryOnResourceNotFoundException'; +import { handlePasswordVerifierChallenge } from '../../../providers/cognito/utils/handlePasswordVerifierChallenge'; /** * Handles the SELECT_CHALLENGE response specifically for Password SRP authentication. diff --git a/packages/auth/src/client/flows/userAuth/handleUserAuthFlow.ts b/packages/auth/src/client/flows/userAuth/handleUserAuthFlow.ts index 753ac66db04..f45530b7aa8 100644 --- a/packages/auth/src/client/flows/userAuth/handleUserAuthFlow.ts +++ b/packages/auth/src/client/flows/userAuth/handleUserAuthFlow.ts @@ -18,7 +18,7 @@ import { getAuthUserAgentValue } from '../../../utils'; import { handlePasswordSRP } from '../shared/handlePasswordSRP'; import { assertValidationError } from '../../../errors/utils/assertValidationError'; import { AuthValidationErrorCode } from '../../../errors/types/validation'; -import { setActiveSignInUsername } from '../../../providers/cognito/utils/signInHelpers'; +import { setActiveSignInUsername } from '../../../providers/cognito/utils/setActiveSignInUsername'; export interface HandleUserAuthFlowInput { username: string; diff --git a/packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.ts b/packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.ts index ee3bc6e6b6c..b0105694047 100644 --- a/packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.ts +++ b/packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.ts @@ -17,12 +17,7 @@ import { getRegionFromUserPoolId } from '../../../foundation/parsers'; import { createCognitoUserPoolEndpointResolver } from '../../../providers/cognito/factories'; import { cacheCognitoTokens } from '../../../providers/cognito/tokenProvider/cacheTokens'; import { dispatchSignedInHubEvent } from '../../../providers/cognito/utils/dispatchSignedInHubEvent'; -import { - getNewDeviceMetadata, - getSignInResult, -} from '../../../providers/cognito/utils/signInHelpers'; import { setActiveSignInState, signInStore } from '../../../client/utils/store'; -import { AuthSignInOutput } from '../../../types'; import { getAuthUserAgentValue } from '../../../utils'; import { getPasskey } from '../../utils/passkey'; import { @@ -30,10 +25,13 @@ import { assertPasskeyError, } from '../../utils/passkey/errors'; import { AuthError } from '../../../errors/AuthError'; +import { getNewDeviceMetadata } from '../../../providers/cognito/utils/getNewDeviceMetadata'; + +import { WebAuthnSignInResult } from './types'; export async function handleWebAuthnSignInResult( challengeParameters: ChallengeParameters, -): Promise { +): Promise { const authConfig = Amplify.getConfig().Auth?.Cognito; assertTokenProviderConfig(authConfig); const { username, signInSession, signInDetails, challengeName } = @@ -119,8 +117,8 @@ export async function handleWebAuthnSignInResult( }); } - return getSignInResult({ + return { challengeName: nextChallengeName as ChallengeName, challengeParameters: nextChallengeParameters as ChallengeParameters, - }); + }; } diff --git a/packages/auth/src/client/flows/userAuth/types.ts b/packages/auth/src/client/flows/userAuth/types.ts new file mode 100644 index 00000000000..d5e644f489b --- /dev/null +++ b/packages/auth/src/client/flows/userAuth/types.ts @@ -0,0 +1,15 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + ChallengeName, + ChallengeParameters, +} from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; +import { AuthSignInOutput } from '../../../types'; + +export type WebAuthnSignInResult = + | AuthSignInOutput + | { + challengeName: ChallengeName; + challengeParameters: ChallengeParameters; + }; diff --git a/packages/auth/src/providers/cognito/apis/confirmSignIn.ts b/packages/auth/src/providers/cognito/apis/confirmSignIn.ts index 3edb0e9eab0..48c904d5897 100644 --- a/packages/auth/src/providers/cognito/apis/confirmSignIn.ts +++ b/packages/auth/src/providers/cognito/apis/confirmSignIn.ts @@ -17,7 +17,6 @@ import { } from '../../../client/utils/store'; import { AuthError } from '../../../errors/AuthError'; import { - getNewDeviceMetadata, getSignInResult, getSignInResultFromError, handleChallengeName, @@ -33,6 +32,7 @@ import { ChallengeName, ChallengeParameters, } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; +import { getNewDeviceMetadata } from '../utils/getNewDeviceMetadata'; /** * Continues or completes the sign in process when required by the initial call to `signIn`. diff --git a/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts b/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts index 3ee2de1302c..a1260538d17 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts @@ -9,11 +9,9 @@ import { assertValidationError } from '../../../errors/utils/assertValidationErr import { assertServiceError } from '../../../errors/utils/assertServiceError'; import { getActiveSignInUsername, - getNewDeviceMetadata, getSignInResult, getSignInResultFromError, handleCustomAuthFlowWithoutSRP, - retryOnResourceNotFoundException, } from '../utils/signInHelpers'; import { InitiateAuthException } from '../types/errors'; import { @@ -32,6 +30,8 @@ import { } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; import { tokenOrchestrator } from '../tokenProvider'; import { dispatchSignedInHubEvent } from '../utils/dispatchSignedInHubEvent'; +import { retryOnResourceNotFoundException } from '../utils/retryOnResourceNotFoundException'; +import { getNewDeviceMetadata } from '../utils/getNewDeviceMetadata'; /** * Signs a user in using a custom authentication flow without password diff --git a/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts b/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts index 35eb7f29419..3827699f476 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts @@ -9,7 +9,6 @@ import { assertValidationError } from '../../../errors/utils/assertValidationErr import { assertServiceError } from '../../../errors/utils/assertServiceError'; import { getActiveSignInUsername, - getNewDeviceMetadata, getSignInResult, getSignInResultFromError, handleCustomSRPAuthFlow, @@ -34,6 +33,7 @@ import { } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; import { tokenOrchestrator } from '../tokenProvider'; import { dispatchSignedInHubEvent } from '../utils/dispatchSignedInHubEvent'; +import { getNewDeviceMetadata } from '../utils/getNewDeviceMetadata'; /** * Signs a user in using a custom authentication flow with SRP diff --git a/packages/auth/src/providers/cognito/apis/signInWithSRP.ts b/packages/auth/src/providers/cognito/apis/signInWithSRP.ts index 05c79cf35a0..d2d9588f6bc 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithSRP.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithSRP.ts @@ -17,7 +17,6 @@ import { } from '../types/errors'; import { getActiveSignInUsername, - getNewDeviceMetadata, getSignInResult, getSignInResultFromError, handleUserSRPAuthFlow, @@ -34,6 +33,7 @@ import { import { cacheCognitoTokens } from '../tokenProvider/cacheTokens'; import { tokenOrchestrator } from '../tokenProvider'; import { dispatchSignedInHubEvent } from '../utils/dispatchSignedInHubEvent'; +import { getNewDeviceMetadata } from '../utils/getNewDeviceMetadata'; import { resetAutoSignIn } from './autoSignIn'; diff --git a/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts b/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts index 4f653f46c94..1b2957c9480 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts @@ -17,7 +17,6 @@ import { } from '../types/errors'; import { getActiveSignInUsername, - getNewDeviceMetadata, getSignInResult, getSignInResultFromError, } from '../utils/signInHelpers'; @@ -38,6 +37,7 @@ import { HandleUserAuthFlowInput, handleUserAuthFlow, } from '../../../client/flows/userAuth/handleUserAuthFlow'; +import { getNewDeviceMetadata } from '../utils/getNewDeviceMetadata'; import { resetAutoSignIn } from './autoSignIn'; diff --git a/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts b/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts index 56e1c1af9f5..e9280227a37 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts @@ -13,11 +13,9 @@ import { } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; import { getActiveSignInUsername, - getNewDeviceMetadata, getSignInResult, getSignInResultFromError, handleUserPasswordAuthFlow, - retryOnResourceNotFoundException, } from '../utils/signInHelpers'; import { InitiateAuthException } from '../types/errors'; import { @@ -32,6 +30,8 @@ import { import { cacheCognitoTokens } from '../tokenProvider/cacheTokens'; import { tokenOrchestrator } from '../tokenProvider'; import { dispatchSignedInHubEvent } from '../utils/dispatchSignedInHubEvent'; +import { retryOnResourceNotFoundException } from '../utils/retryOnResourceNotFoundException'; +import { getNewDeviceMetadata } from '../utils/getNewDeviceMetadata'; import { resetAutoSignIn } from './autoSignIn'; diff --git a/packages/auth/src/providers/cognito/utils/getNewDeviceMetadata.ts b/packages/auth/src/providers/cognito/utils/getNewDeviceMetadata.ts new file mode 100644 index 00000000000..ea687b4b3d1 --- /dev/null +++ b/packages/auth/src/providers/cognito/utils/getNewDeviceMetadata.ts @@ -0,0 +1,88 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + base64Encoder, + getDeviceName, +} from '@aws-amplify/core/internals/utils'; + +import { DeviceMetadata } from '../tokenProvider/types'; +import { createConfirmDeviceClient } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider'; +import { createCognitoUserPoolEndpointResolver } from '../factories'; +import { NewDeviceMetadataType } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; +import { getRegionFromUserPoolId } from '../../../foundation/parsers'; + +import { getAuthenticationHelper, getBytesFromHex } from './srp'; + +/** + * This function is used to kick off the device management flow. + * + * If an error is thrown while generating a hash device or calling the `ConfirmDevice` + * client, then this API will ignore the error and return undefined. Otherwise the authentication + * flow will not complete and the user won't be able to be signed in. + * + * @returns DeviceMetadata | undefined + */ +export async function getNewDeviceMetadata({ + userPoolId, + userPoolEndpoint, + newDeviceMetadata, + accessToken, +}: { + userPoolId: string; + userPoolEndpoint: string | undefined; + newDeviceMetadata?: NewDeviceMetadataType; + accessToken?: string; +}): Promise { + if (!newDeviceMetadata) return undefined; + const userPoolName = userPoolId.split('_')[1] || ''; + const authenticationHelper = await getAuthenticationHelper(userPoolName); + const deviceKey = newDeviceMetadata?.DeviceKey; + const deviceGroupKey = newDeviceMetadata?.DeviceGroupKey; + + try { + await authenticationHelper.generateHashDevice( + deviceGroupKey ?? '', + deviceKey ?? '', + ); + } catch (errGenHash) { + // TODO: log error here + return undefined; + } + + const deviceSecretVerifierConfig = { + Salt: base64Encoder.convert( + getBytesFromHex(authenticationHelper.getSaltToHashDevices()), + ), + PasswordVerifier: base64Encoder.convert( + getBytesFromHex(authenticationHelper.getVerifierDevices()), + ), + }; + const randomPassword = authenticationHelper.getRandomPassword(); + + try { + const confirmDevice = createConfirmDeviceClient({ + endpointResolver: createCognitoUserPoolEndpointResolver({ + endpointOverride: userPoolEndpoint, + }), + }); + await confirmDevice( + { region: getRegionFromUserPoolId(userPoolId) }, + { + AccessToken: accessToken, + DeviceName: await getDeviceName(), + DeviceKey: newDeviceMetadata?.DeviceKey, + DeviceSecretVerifierConfig: deviceSecretVerifierConfig, + }, + ); + + return { + deviceKey, + deviceGroupKey, + randomPassword, + }; + } catch (error) { + // TODO: log error here + return undefined; + } +} diff --git a/packages/auth/src/providers/cognito/utils/handleDeviceSRPAuth.ts b/packages/auth/src/providers/cognito/utils/handleDeviceSRPAuth.ts new file mode 100644 index 00000000000..27f5b1c1e5c --- /dev/null +++ b/packages/auth/src/providers/cognito/utils/handleDeviceSRPAuth.ts @@ -0,0 +1,146 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { CognitoUserPoolConfig } from '@aws-amplify/core'; + +import { ClientMetadata } from '../types'; +import { AuthTokenOrchestrator } from '../tokenProvider/types'; +import { createRespondToAuthChallengeClient } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider'; +import { createCognitoUserPoolEndpointResolver } from '../factories'; +import { + ChallengeParameters, + RespondToAuthChallengeCommandInput, + RespondToAuthChallengeCommandOutput, +} from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; +import { getRegionFromUserPoolId } from '../../../foundation/parsers'; + +import { assertDeviceMetadata } from './types'; +import { + getAuthenticationHelper, + getNowString, + getSignatureString, +} from './srp'; +import { BigInteger } from './srp/BigInteger'; +import { AuthenticationHelper } from './srp/AuthenticationHelper'; +import { getUserContextData } from './userContextData'; + +interface HandleDeviceSRPInput { + username: string; + config: CognitoUserPoolConfig; + clientMetadata: ClientMetadata | undefined; + session: string | undefined; + tokenOrchestrator?: AuthTokenOrchestrator; +} + +export async function handleDeviceSRPAuth({ + username, + config, + clientMetadata, + session, + tokenOrchestrator, +}: HandleDeviceSRPInput): Promise { + const { userPoolId, userPoolEndpoint } = config; + const clientId = config.userPoolClientId; + const deviceMetadata = await tokenOrchestrator?.getDeviceMetadata(username); + assertDeviceMetadata(deviceMetadata); + const authenticationHelper = await getAuthenticationHelper( + deviceMetadata.deviceGroupKey, + ); + const challengeResponses: Record = { + USERNAME: username, + SRP_A: authenticationHelper.A.toString(16), + DEVICE_KEY: deviceMetadata.deviceKey, + }; + + const jsonReqResponseChallenge: RespondToAuthChallengeCommandInput = { + ChallengeName: 'DEVICE_SRP_AUTH', + ClientId: clientId, + ChallengeResponses: challengeResponses, + ClientMetadata: clientMetadata, + Session: session, + }; + const respondToAuthChallenge = createRespondToAuthChallengeClient({ + endpointResolver: createCognitoUserPoolEndpointResolver({ + endpointOverride: userPoolEndpoint, + }), + }); + const { ChallengeParameters: respondedChallengeParameters, Session } = + await respondToAuthChallenge( + { region: getRegionFromUserPoolId(userPoolId) }, + jsonReqResponseChallenge, + ); + + return handleDevicePasswordVerifier( + username, + respondedChallengeParameters as ChallengeParameters, + clientMetadata, + Session, + authenticationHelper, + config, + tokenOrchestrator, + ); +} + +async function handleDevicePasswordVerifier( + username: string, + challengeParameters: ChallengeParameters, + clientMetadata: ClientMetadata | undefined, + session: string | undefined, + authenticationHelper: AuthenticationHelper, + { userPoolId, userPoolClientId, userPoolEndpoint }: CognitoUserPoolConfig, + tokenOrchestrator?: AuthTokenOrchestrator, +): Promise { + const deviceMetadata = await tokenOrchestrator?.getDeviceMetadata(username); + assertDeviceMetadata(deviceMetadata); + + const serverBValue = new BigInteger(challengeParameters?.SRP_B, 16); + const salt = new BigInteger(challengeParameters?.SALT, 16); + const { deviceKey } = deviceMetadata; + const { deviceGroupKey } = deviceMetadata; + const hkdf = await authenticationHelper.getPasswordAuthenticationKey({ + username: deviceMetadata.deviceKey, + password: deviceMetadata.randomPassword, + serverBValue, + salt, + }); + + const dateNow = getNowString(); + const challengeResponses = { + USERNAME: (challengeParameters?.USERNAME as string) ?? username, + PASSWORD_CLAIM_SECRET_BLOCK: challengeParameters?.SECRET_BLOCK, + TIMESTAMP: dateNow, + PASSWORD_CLAIM_SIGNATURE: getSignatureString({ + username: deviceKey, + userPoolName: deviceGroupKey, + challengeParameters, + dateNow, + hkdf, + }), + DEVICE_KEY: deviceKey, + } as Record; + + const UserContextData = getUserContextData({ + username, + userPoolId, + userPoolClientId, + }); + + const jsonReqResponseChallenge: RespondToAuthChallengeCommandInput = { + ChallengeName: 'DEVICE_PASSWORD_VERIFIER', + ClientId: userPoolClientId, + ChallengeResponses: challengeResponses, + Session: session, + ClientMetadata: clientMetadata, + UserContextData, + }; + const respondToAuthChallenge = createRespondToAuthChallengeClient({ + endpointResolver: createCognitoUserPoolEndpointResolver({ + endpointOverride: userPoolEndpoint, + }), + }); + + return respondToAuthChallenge( + { region: getRegionFromUserPoolId(userPoolId) }, + jsonReqResponseChallenge, + ); +} diff --git a/packages/auth/src/providers/cognito/utils/handlePasswordVerifierChallenge.ts b/packages/auth/src/providers/cognito/utils/handlePasswordVerifierChallenge.ts new file mode 100644 index 00000000000..aa35a918d99 --- /dev/null +++ b/packages/auth/src/providers/cognito/utils/handlePasswordVerifierChallenge.ts @@ -0,0 +1,106 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { CognitoUserPoolConfig } from '@aws-amplify/core'; + +import { ClientMetadata } from '../types'; +import { AuthError } from '../../../errors/AuthError'; +import { AuthTokenOrchestrator } from '../tokenProvider/types'; +import { createRespondToAuthChallengeClient } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider'; +import { createCognitoUserPoolEndpointResolver } from '../factories'; +import { + ChallengeParameters, + RespondToAuthChallengeCommandInput, + RespondToAuthChallengeCommandOutput, +} from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; +import { getRegionFromUserPoolId } from '../../../foundation/parsers'; + +import { getNowString, getSignatureString } from './srp'; +import { BigInteger } from './srp/BigInteger'; +import { AuthenticationHelper } from './srp/AuthenticationHelper'; +import { getUserContextData } from './userContextData'; +import { handleDeviceSRPAuth } from './handleDeviceSRPAuth'; + +export async function handlePasswordVerifierChallenge( + password: string, + challengeParameters: ChallengeParameters, + clientMetadata: ClientMetadata | undefined, + session: string | undefined, + authenticationHelper: AuthenticationHelper, + config: CognitoUserPoolConfig, + tokenOrchestrator: AuthTokenOrchestrator, +): Promise { + const { userPoolId, userPoolClientId, userPoolEndpoint } = config; + const userPoolName = userPoolId?.split('_')[1] || ''; + const serverBValue = new (BigInteger as any)(challengeParameters?.SRP_B, 16); + const salt = new (BigInteger as any)(challengeParameters?.SALT, 16); + const username = challengeParameters?.USER_ID_FOR_SRP; + if (!username) + throw new AuthError({ + name: 'EmptyUserIdForSRPException', + message: 'USER_ID_FOR_SRP was not found in challengeParameters', + }); + const hkdf = await authenticationHelper.getPasswordAuthenticationKey({ + username, + password, + serverBValue, + salt, + }); + + const dateNow = getNowString(); + + const challengeResponses = { + USERNAME: username, + PASSWORD_CLAIM_SECRET_BLOCK: challengeParameters?.SECRET_BLOCK, + TIMESTAMP: dateNow, + PASSWORD_CLAIM_SIGNATURE: getSignatureString({ + username, + userPoolName, + challengeParameters, + dateNow, + hkdf, + }), + } as Record; + + const deviceMetadata = await tokenOrchestrator.getDeviceMetadata(username); + if (deviceMetadata && deviceMetadata.deviceKey) { + challengeResponses.DEVICE_KEY = deviceMetadata.deviceKey; + } + + const UserContextData = getUserContextData({ + username, + userPoolId, + userPoolClientId, + }); + + const jsonReqResponseChallenge: RespondToAuthChallengeCommandInput = { + ChallengeName: 'PASSWORD_VERIFIER', + ChallengeResponses: challengeResponses, + ClientMetadata: clientMetadata, + Session: session, + ClientId: userPoolClientId, + UserContextData, + }; + + const respondToAuthChallenge = createRespondToAuthChallengeClient({ + endpointResolver: createCognitoUserPoolEndpointResolver({ + endpointOverride: userPoolEndpoint, + }), + }); + + const response = await respondToAuthChallenge( + { region: getRegionFromUserPoolId(userPoolId) }, + jsonReqResponseChallenge, + ); + + if (response.ChallengeName === 'DEVICE_SRP_AUTH') + return handleDeviceSRPAuth({ + username, + config, + clientMetadata, + session: response.Session, + tokenOrchestrator, + }); + + return response; +} diff --git a/packages/auth/src/providers/cognito/utils/retryOnResourceNotFoundException.ts b/packages/auth/src/providers/cognito/utils/retryOnResourceNotFoundException.ts new file mode 100644 index 00000000000..a8b8095b226 --- /dev/null +++ b/packages/auth/src/providers/cognito/utils/retryOnResourceNotFoundException.ts @@ -0,0 +1,34 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AuthError } from '../../../errors/AuthError'; +import { AuthTokenOrchestrator } from '../tokenProvider/types'; + +/** + * It will retry the function if the error is a `ResourceNotFoundException` and + * will clean the device keys stored in the storage mechanism. + * + */ +export async function retryOnResourceNotFoundException< + F extends (...args: any[]) => any, +>( + func: F, + args: Parameters, + username: string, + tokenOrchestrator: AuthTokenOrchestrator, +): Promise> { + try { + return await func(...args); + } catch (error) { + if ( + error instanceof AuthError && + error.name === 'ResourceNotFoundException' && + error.message.includes('Device does not exist.') + ) { + await tokenOrchestrator.clearDeviceMetadata(username); + + return func(...args); + } + throw error; + } +} diff --git a/packages/auth/src/providers/cognito/utils/setActiveSignInUsername.ts b/packages/auth/src/providers/cognito/utils/setActiveSignInUsername.ts new file mode 100644 index 00000000000..8134fd47ff1 --- /dev/null +++ b/packages/auth/src/providers/cognito/utils/setActiveSignInUsername.ts @@ -0,0 +1,10 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { signInStore } from '../../../client/utils/store'; + +export function setActiveSignInUsername(username: string) { + const { dispatch } = signInStore; + + dispatch({ type: 'SET_USERNAME', value: username }); +} diff --git a/packages/auth/src/providers/cognito/utils/signInHelpers.ts b/packages/auth/src/providers/cognito/utils/signInHelpers.ts index d4123c475c1..85bb060aa9d 100644 --- a/packages/auth/src/providers/cognito/utils/signInHelpers.ts +++ b/packages/auth/src/providers/cognito/utils/signInHelpers.ts @@ -6,8 +6,6 @@ import { AmplifyUrl, AuthAction, assertTokenProviderConfig, - base64Encoder, - getDeviceName, } from '@aws-amplify/core/internals/utils'; import { ClientMetadata, ConfirmSignInOptions } from '../types'; @@ -29,11 +27,10 @@ import { AuthValidationErrorCode } from '../../../errors/types/validation'; import { assertValidationError } from '../../../errors/utils/assertValidationError'; import { USER_ALREADY_AUTHENTICATED_EXCEPTION } from '../../../errors/constants'; import { getCurrentUser } from '../apis/getCurrentUser'; -import { AuthTokenOrchestrator, DeviceMetadata } from '../tokenProvider/types'; +import { AuthTokenOrchestrator } from '../tokenProvider/types'; import { getAuthUserAgentValue } from '../../../utils'; import { createAssociateSoftwareTokenClient, - createConfirmDeviceClient, createInitiateAuthClient, createRespondToAuthChallengeClient, createVerifySoftwareTokenClient, @@ -45,7 +42,6 @@ import { CognitoMFAType, InitiateAuthCommandInput, InitiateAuthCommandOutput, - NewDeviceMetadataType, RespondToAuthChallengeCommandInput, RespondToAuthChallengeCommandOutput, } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; @@ -56,17 +52,14 @@ import { initiateSelectedChallenge } from '../../../client/flows/userAuth/handle import { handleSelectChallengeWithPassword } from '../../../client/flows/userAuth/handleSelectChallengeWithPassword'; import { handleSelectChallengeWithPasswordSRP } from '../../../client/flows/userAuth/handleSelectChallengeWithPasswordSRP'; import { signInStore } from '../../../client/utils/store'; +import { WebAuthnSignInResult } from '../../../client/flows/userAuth/types'; -import { assertDeviceMetadata } from './types'; -import { - getAuthenticationHelper, - getBytesFromHex, - getNowString, - getSignatureString, -} from './srp'; -import { BigInteger } from './srp/BigInteger'; -import { AuthenticationHelper } from './srp/AuthenticationHelper'; +import { getAuthenticationHelper } from './srp'; import { getUserContextData } from './userContextData'; +import { handlePasswordVerifierChallenge } from './handlePasswordVerifierChallenge'; +import { handleDeviceSRPAuth } from './handleDeviceSRPAuth'; +import { retryOnResourceNotFoundException } from './retryOnResourceNotFoundException'; +import { setActiveSignInUsername } from './setActiveSignInUsername'; const USER_ATTRIBUTES = 'userAttributes.'; @@ -80,12 +73,10 @@ interface HandleAuthChallengeRequest { config: CognitoUserPoolConfig; } -interface HandleDeviceSRPInput { - username: string; - config: CognitoUserPoolConfig; - clientMetadata: ClientMetadata | undefined; - session: string | undefined; - tokenOrchestrator?: AuthTokenOrchestrator; +function isWebAuthnResultAuthSignInOutput( + result: WebAuthnSignInResult, +): result is AuthSignInOutput { + return 'isSignedIn' in result && 'nextStep' in result; } export async function handleCustomChallenge({ @@ -571,203 +562,6 @@ export async function handleCustomSRPAuthFlow( ); } -async function handleDeviceSRPAuth({ - username, - config, - clientMetadata, - session, - tokenOrchestrator, -}: HandleDeviceSRPInput): Promise { - const { userPoolId, userPoolEndpoint } = config; - const clientId = config.userPoolClientId; - const deviceMetadata = await tokenOrchestrator?.getDeviceMetadata(username); - assertDeviceMetadata(deviceMetadata); - const authenticationHelper = await getAuthenticationHelper( - deviceMetadata.deviceGroupKey, - ); - const challengeResponses: Record = { - USERNAME: username, - SRP_A: authenticationHelper.A.toString(16), - DEVICE_KEY: deviceMetadata.deviceKey, - }; - - const jsonReqResponseChallenge: RespondToAuthChallengeCommandInput = { - ChallengeName: 'DEVICE_SRP_AUTH', - ClientId: clientId, - ChallengeResponses: challengeResponses, - ClientMetadata: clientMetadata, - Session: session, - }; - const respondToAuthChallenge = createRespondToAuthChallengeClient({ - endpointResolver: createCognitoUserPoolEndpointResolver({ - endpointOverride: userPoolEndpoint, - }), - }); - const { ChallengeParameters: respondedChallengeParameters, Session } = - await respondToAuthChallenge( - { region: getRegionFromUserPoolId(userPoolId) }, - jsonReqResponseChallenge, - ); - - return handleDevicePasswordVerifier( - username, - respondedChallengeParameters as ChallengeParameters, - clientMetadata, - Session, - authenticationHelper, - config, - tokenOrchestrator, - ); -} - -async function handleDevicePasswordVerifier( - username: string, - challengeParameters: ChallengeParameters, - clientMetadata: ClientMetadata | undefined, - session: string | undefined, - authenticationHelper: AuthenticationHelper, - { userPoolId, userPoolClientId, userPoolEndpoint }: CognitoUserPoolConfig, - tokenOrchestrator?: AuthTokenOrchestrator, -): Promise { - const deviceMetadata = await tokenOrchestrator?.getDeviceMetadata(username); - assertDeviceMetadata(deviceMetadata); - - const serverBValue = new BigInteger(challengeParameters?.SRP_B, 16); - const salt = new BigInteger(challengeParameters?.SALT, 16); - const { deviceKey } = deviceMetadata; - const { deviceGroupKey } = deviceMetadata; - const hkdf = await authenticationHelper.getPasswordAuthenticationKey({ - username: deviceMetadata.deviceKey, - password: deviceMetadata.randomPassword, - serverBValue, - salt, - }); - - const dateNow = getNowString(); - const challengeResponses = { - USERNAME: (challengeParameters?.USERNAME as string) ?? username, - PASSWORD_CLAIM_SECRET_BLOCK: challengeParameters?.SECRET_BLOCK, - TIMESTAMP: dateNow, - PASSWORD_CLAIM_SIGNATURE: getSignatureString({ - username: deviceKey, - userPoolName: deviceGroupKey, - challengeParameters, - dateNow, - hkdf, - }), - DEVICE_KEY: deviceKey, - } as Record; - - const UserContextData = getUserContextData({ - username, - userPoolId, - userPoolClientId, - }); - - const jsonReqResponseChallenge: RespondToAuthChallengeCommandInput = { - ChallengeName: 'DEVICE_PASSWORD_VERIFIER', - ClientId: userPoolClientId, - ChallengeResponses: challengeResponses, - Session: session, - ClientMetadata: clientMetadata, - UserContextData, - }; - const respondToAuthChallenge = createRespondToAuthChallengeClient({ - endpointResolver: createCognitoUserPoolEndpointResolver({ - endpointOverride: userPoolEndpoint, - }), - }); - - return respondToAuthChallenge( - { region: getRegionFromUserPoolId(userPoolId) }, - jsonReqResponseChallenge, - ); -} - -export async function handlePasswordVerifierChallenge( - password: string, - challengeParameters: ChallengeParameters, - clientMetadata: ClientMetadata | undefined, - session: string | undefined, - authenticationHelper: AuthenticationHelper, - config: CognitoUserPoolConfig, - tokenOrchestrator: AuthTokenOrchestrator, -): Promise { - const { userPoolId, userPoolClientId, userPoolEndpoint } = config; - const userPoolName = userPoolId?.split('_')[1] || ''; - const serverBValue = new (BigInteger as any)(challengeParameters?.SRP_B, 16); - const salt = new (BigInteger as any)(challengeParameters?.SALT, 16); - const username = challengeParameters?.USER_ID_FOR_SRP; - if (!username) - throw new AuthError({ - name: 'EmptyUserIdForSRPException', - message: 'USER_ID_FOR_SRP was not found in challengeParameters', - }); - const hkdf = await authenticationHelper.getPasswordAuthenticationKey({ - username, - password, - serverBValue, - salt, - }); - - const dateNow = getNowString(); - - const challengeResponses = { - USERNAME: username, - PASSWORD_CLAIM_SECRET_BLOCK: challengeParameters?.SECRET_BLOCK, - TIMESTAMP: dateNow, - PASSWORD_CLAIM_SIGNATURE: getSignatureString({ - username, - userPoolName, - challengeParameters, - dateNow, - hkdf, - }), - } as Record; - - const deviceMetadata = await tokenOrchestrator.getDeviceMetadata(username); - if (deviceMetadata && deviceMetadata.deviceKey) { - challengeResponses.DEVICE_KEY = deviceMetadata.deviceKey; - } - - const UserContextData = getUserContextData({ - username, - userPoolId, - userPoolClientId, - }); - - const jsonReqResponseChallenge: RespondToAuthChallengeCommandInput = { - ChallengeName: 'PASSWORD_VERIFIER', - ChallengeResponses: challengeResponses, - ClientMetadata: clientMetadata, - Session: session, - ClientId: userPoolClientId, - UserContextData, - }; - - const respondToAuthChallenge = createRespondToAuthChallengeClient({ - endpointResolver: createCognitoUserPoolEndpointResolver({ - endpointOverride: userPoolEndpoint, - }), - }); - - const response = await respondToAuthChallenge( - { region: getRegionFromUserPoolId(userPoolId) }, - jsonReqResponseChallenge, - ); - - if (response.ChallengeName === 'DEVICE_SRP_AUTH') - return handleDeviceSRPAuth({ - username, - config, - clientMetadata, - session: response.Session, - tokenOrchestrator, - }); - - return response; -} - export async function getSignInResult(params: { challengeName: ChallengeName; challengeParameters: ChallengeParameters; @@ -902,8 +696,14 @@ export async function getSignInResult(params: { }, }; - case 'WEB_AUTHN': - return handleWebAuthnSignInResult(challengeParameters); + case 'WEB_AUTHN': { + const result = await handleWebAuthnSignInResult(challengeParameters); + if (isWebAuthnResultAuthSignInOutput(result)) { + return result; + } + + return getSignInResult(result); + } case 'PASSWORD': case 'PASSWORD_SRP': return { @@ -1154,114 +954,6 @@ export async function assertUserNotAuthenticated() { } } -/** - * This function is used to kick off the device management flow. - * - * If an error is thrown while generating a hash device or calling the `ConfirmDevice` - * client, then this API will ignore the error and return undefined. Otherwise the authentication - * flow will not complete and the user won't be able to be signed in. - * - * @returns DeviceMetadata | undefined - */ -export async function getNewDeviceMetadata({ - userPoolId, - userPoolEndpoint, - newDeviceMetadata, - accessToken, -}: { - userPoolId: string; - userPoolEndpoint: string | undefined; - newDeviceMetadata?: NewDeviceMetadataType; - accessToken?: string; -}): Promise { - if (!newDeviceMetadata) return undefined; - const userPoolName = userPoolId.split('_')[1] || ''; - const authenticationHelper = await getAuthenticationHelper(userPoolName); - const deviceKey = newDeviceMetadata?.DeviceKey; - const deviceGroupKey = newDeviceMetadata?.DeviceGroupKey; - - try { - await authenticationHelper.generateHashDevice( - deviceGroupKey ?? '', - deviceKey ?? '', - ); - } catch (errGenHash) { - // TODO: log error here - return undefined; - } - - const deviceSecretVerifierConfig = { - Salt: base64Encoder.convert( - getBytesFromHex(authenticationHelper.getSaltToHashDevices()), - ), - PasswordVerifier: base64Encoder.convert( - getBytesFromHex(authenticationHelper.getVerifierDevices()), - ), - }; - const randomPassword = authenticationHelper.getRandomPassword(); - - try { - const confirmDevice = createConfirmDeviceClient({ - endpointResolver: createCognitoUserPoolEndpointResolver({ - endpointOverride: userPoolEndpoint, - }), - }); - await confirmDevice( - { region: getRegionFromUserPoolId(userPoolId) }, - { - AccessToken: accessToken, - DeviceName: await getDeviceName(), - DeviceKey: newDeviceMetadata?.DeviceKey, - DeviceSecretVerifierConfig: deviceSecretVerifierConfig, - }, - ); - - return { - deviceKey, - deviceGroupKey, - randomPassword, - }; - } catch (error) { - // TODO: log error here - return undefined; - } -} - -/** - * It will retry the function if the error is a `ResourceNotFoundException` and - * will clean the device keys stored in the storage mechanism. - * - */ -export async function retryOnResourceNotFoundException< - F extends (...args: any[]) => any, ->( - func: F, - args: Parameters, - username: string, - tokenOrchestrator: AuthTokenOrchestrator, -): Promise> { - try { - return await func(...args); - } catch (error) { - if ( - error instanceof AuthError && - error.name === 'ResourceNotFoundException' && - error.message.includes('Device does not exist.') - ) { - await tokenOrchestrator.clearDeviceMetadata(username); - - return func(...args); - } - throw error; - } -} - -export function setActiveSignInUsername(username: string) { - const { dispatch } = signInStore; - - dispatch({ type: 'SET_USERNAME', value: username }); -} - export function getActiveSignInUsername(username: string): string { const state = signInStore.getState(); diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index c399a5309e6..dc081bf4ec5 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -383,7 +383,7 @@ "name": "[Auth] confirmSignIn (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ confirmSignIn }", - "limit": "28.66 kB" + "limit": "28.70 kB" }, { "name": "[Auth] updateMFAPreference (Cognito)", @@ -449,7 +449,7 @@ "name": "[Auth] Basic Auth Flow (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ signIn, signOut, fetchAuthSession, confirmSignIn }", - "limit": "30.89 kB" + "limit": "31.00 kB" }, { "name": "[Auth] OAuth Auth Flow (Cognito)",