The session that should pass both ways in challenge-response calls to the service. If the caller must pass another challenge, they return a session with other challenge parameters. This session
- * should be passed as it is to the next RespondToAuthChallenge API call.
+ *
The session that should pass both ways in challenge-response calls to the service. If
+ * the caller must pass another challenge, they return a session with other challenge
+ * parameters. Include this session identifier in a RespondToAuthChallenge API
+ * request.
The result of the authentication response. This result is only returned if the caller doesn't need to pass another challenge. If the caller does need to pass another challenge before it gets
- * tokens, ChallengeName, ChallengeParameters, and Session are returned.
+ * tokens, ChallengeName, ChallengeParameters, AvailableChallenges, and Session are returned.
This response parameter prompts a user to select from multiple available challenges
+ * that they can complete authentication with. For example, they might be able to continue
+ * with passwordless authentication or with a one-time password from an SMS message.
+ */
+ AvailableChallenges?: ChallengeNameType[];
}
export type ListDevicesCommandInput = ListDevicesRequest;
export interface ListDevicesCommandOutput
diff --git a/packages/auth/src/providers/cognito/apis/signIn.ts b/packages/auth/src/providers/cognito/apis/signIn.ts
index 10a9be79214..c1677f971d5 100644
--- a/packages/auth/src/providers/cognito/apis/signIn.ts
+++ b/packages/auth/src/providers/cognito/apis/signIn.ts
@@ -13,6 +13,7 @@ import { signInWithCustomAuth } from './signInWithCustomAuth';
import { signInWithCustomSRPAuth } from './signInWithCustomSRPAuth';
import { signInWithSRP } from './signInWithSRP';
import { signInWithUserPassword } from './signInWithUserPassword';
+import { signInWithUserAuth } from './signInWithUserAuth';
/**
* Signs a user in
@@ -37,6 +38,8 @@ export async function signIn(input: SignInInput): Promise {
return signInWithCustomAuth(input);
case 'CUSTOM_WITH_SRP':
return signInWithCustomSRPAuth(input);
+ case 'USER_AUTH':
+ return signInWithUserAuth(input);
default:
return signInWithSRP(input);
}
diff --git a/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts b/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts
new file mode 100644
index 00000000000..3090c4289e3
--- /dev/null
+++ b/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts
@@ -0,0 +1,121 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { Amplify } from '@aws-amplify/core';
+import { assertTokenProviderConfig } from '@aws-amplify/core/internals/utils';
+
+import { AuthValidationErrorCode } from '../../../errors/types/validation';
+import { assertValidationError } from '../../../errors/utils/assertValidationError';
+import { assertServiceError } from '../../../errors/utils/assertServiceError';
+import {
+ ChallengeName,
+ ChallengeParameters,
+} from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types';
+import {
+ InitiateAuthException,
+ RespondToAuthChallengeException,
+} from '../types/errors';
+import {
+ getActiveSignInUsername,
+ getNewDeviceMetadata,
+ getSignInResult,
+ getSignInResultFromError,
+} from '../utils/signInHelpers';
+import {
+ CognitoAuthSignInDetails,
+ SignInWithUserAuthInput,
+ SignInWithUserAuthOutput,
+} from '../types';
+import {
+ cleanActiveSignInState,
+ setActiveSignInState,
+} from '../utils/signInStore';
+import { cacheCognitoTokens } from '../tokenProvider/cacheTokens';
+import { dispatchSignedInHubEvent } from '../utils/dispatchSignedInHubEvent';
+import { tokenOrchestrator } from '../tokenProvider';
+import { handleUserAuthFlow } from '../../../client/flows/userAuth/handleUserAuthFlow';
+
+/**
+ * Signs a user in through a registered email or phone number without a password by by receiving and entering an OTP.
+ *
+ * @param input - The SignInWithUserAuthInput object
+ * @returns SignInWithUserAuthOutput
+ * @throws service: {@link InitiateAuthException }, {@link RespondToAuthChallengeException } - Cognito service errors
+ * thrown during the sign-in process.
+ * @throws validation: {@link AuthValidationErrorCode } - Validation errors thrown when either username or password -- needs to change
+ * are not defined.
+ * @throws AuthTokenConfigException - Thrown when the token provider config is invalid.
+ */
+export async function signInWithUserAuth(
+ input: SignInWithUserAuthInput,
+): Promise {
+ const { username, password, options } = input;
+ const authConfig = Amplify.getConfig().Auth?.Cognito;
+ const signInDetails: CognitoAuthSignInDetails = {
+ loginId: username,
+ authFlowType: 'USER_AUTH',
+ };
+ assertTokenProviderConfig(authConfig);
+ const clientMetaData = options?.clientMetadata;
+ const preferredChallenge = options?.preferredChallenge;
+
+ assertValidationError(
+ !!username,
+ AuthValidationErrorCode.EmptySignInUsername,
+ );
+
+ try {
+ const response = await handleUserAuthFlow({
+ username,
+ config: authConfig,
+ tokenOrchestrator,
+ clientMetadata: clientMetaData,
+ preferredChallenge,
+ password,
+ });
+
+ const activeUsername = getActiveSignInUsername(username);
+ setActiveSignInState({
+ signInSession: response.Session,
+ username: activeUsername,
+ challengeName: response.ChallengeName as ChallengeName,
+ signInDetails,
+ });
+
+ if (response.AuthenticationResult) {
+ cleanActiveSignInState();
+ await cacheCognitoTokens({
+ username: activeUsername,
+ ...response.AuthenticationResult,
+ NewDeviceMetadata: await getNewDeviceMetadata({
+ userPoolId: authConfig.userPoolId,
+ userPoolEndpoint: authConfig.userPoolEndpoint,
+ newDeviceMetadata: response.AuthenticationResult.NewDeviceMetadata,
+ accessToken: response.AuthenticationResult.AccessToken,
+ }),
+ signInDetails,
+ });
+ await dispatchSignedInHubEvent();
+
+ return {
+ isSignedIn: true,
+ nextStep: { signInStep: 'DONE' },
+ };
+ }
+
+ return getSignInResult({
+ challengeName: response.ChallengeName as ChallengeName,
+ challengeParameters: response.ChallengeParameters as ChallengeParameters,
+ availableChallenges:
+ 'AvailableChallenges' in response
+ ? (response.AvailableChallenges as ChallengeName[])
+ : undefined,
+ });
+ } catch (error) {
+ cleanActiveSignInState();
+ assertServiceError(error);
+ const result = getSignInResultFromError(error.name);
+ if (result) return result;
+ throw error;
+ }
+}
diff --git a/packages/auth/src/providers/cognito/types/index.ts b/packages/auth/src/providers/cognito/types/index.ts
index 0b72451e925..eda7dfb1ee4 100644
--- a/packages/auth/src/providers/cognito/types/index.ts
+++ b/packages/auth/src/providers/cognito/types/index.ts
@@ -39,6 +39,7 @@ export {
SignInWithCustomAuthInput,
SignInWithCustomSRPAuthInput,
SignInWithSRPInput,
+ SignInWithUserAuthInput,
SignInWithUserPasswordInput,
SignInWithRedirectInput,
SignOutInput,
@@ -65,6 +66,7 @@ export {
SignInOutput,
SignInWithCustomAuthOutput,
SignInWithSRPOutput,
+ SignInWithUserAuthOutput,
SignInWithUserPasswordOutput,
SignInWithCustomSRPAuthOutput,
SignUpOutput,
diff --git a/packages/auth/src/providers/cognito/types/inputs.ts b/packages/auth/src/providers/cognito/types/inputs.ts
index 13952bf53e9..57aef3cb353 100644
--- a/packages/auth/src/providers/cognito/types/inputs.ts
+++ b/packages/auth/src/providers/cognito/types/inputs.ts
@@ -92,6 +92,11 @@ export type SignInWithCustomSRPAuthInput = AuthSignInInput;
*/
export type SignInWithSRPInput = AuthSignInInput;
+/**
+ * Input type for Cognito signInWithUserAuth API.
+ */
+export type SignInWithUserAuthInput = AuthSignInInput;
+
/**
* Input type for Cognito signInWithUserPasswordInput API.
*/
diff --git a/packages/auth/src/providers/cognito/types/models.ts b/packages/auth/src/providers/cognito/types/models.ts
index 3341d439918..a65d738127d 100644
--- a/packages/auth/src/providers/cognito/types/models.ts
+++ b/packages/auth/src/providers/cognito/types/models.ts
@@ -18,8 +18,11 @@ import { SignUpOutput } from './outputs';
/**
* Cognito supported AuthFlowTypes that may be passed as part of the Sign In request.
+ * USER_AUTH is a superset that can handle both USER_SRP_AUTH and USER_PASSWORD_AUTH,
+ * providing flexibility for future authentication methods.
*/
export type AuthFlowType =
+ | 'USER_AUTH'
| 'USER_SRP_AUTH'
| 'CUSTOM_WITH_SRP'
| 'CUSTOM_WITHOUT_SRP'
@@ -38,6 +41,16 @@ export const cognitoHostedUIIdentityProviderMap: Record =
*/
export type ClientMetadata = Record;
+/**
+ * Allowed values for preferredChallenge
+ */
+export type AuthFactorType =
+ | 'WEB_AUTHN'
+ | 'EMAIL_OTP'
+ | 'SMS_OTP'
+ | 'PASSWORD'
+ | 'PASSWORD_SRP';
+
/**
* The user attribute types available for Cognito.
*/
diff --git a/packages/auth/src/providers/cognito/types/options.ts b/packages/auth/src/providers/cognito/types/options.ts
index 52b4536297f..ae04219cccb 100644
--- a/packages/auth/src/providers/cognito/types/options.ts
+++ b/packages/auth/src/providers/cognito/types/options.ts
@@ -8,7 +8,12 @@ import {
AuthUserAttributes,
} from '../../../types';
-import { AuthFlowType, ClientMetadata, ValidationData } from './models';
+import {
+ AuthFactorType,
+ AuthFlowType,
+ ClientMetadata,
+ ValidationData,
+} from './models';
/**
* Options specific to Cognito Confirm Reset Password.
@@ -37,6 +42,7 @@ export type ResetPasswordOptions = AuthServiceOptions & {
export type SignInOptions = AuthServiceOptions & {
authFlowType?: AuthFlowType;
clientMetadata?: ClientMetadata;
+ preferredChallenge?: AuthFactorType;
};
/**
diff --git a/packages/auth/src/providers/cognito/types/outputs.ts b/packages/auth/src/providers/cognito/types/outputs.ts
index 595b9009998..381d4de167e 100644
--- a/packages/auth/src/providers/cognito/types/outputs.ts
+++ b/packages/auth/src/providers/cognito/types/outputs.ts
@@ -73,6 +73,11 @@ export type SignInWithCustomAuthOutput = AuthSignInOutput;
*/
export type SignInWithSRPOutput = AuthSignInOutput;
+/**
+ * Output type for Cognito signInWithUserAuth API.
+ */
+export type SignInWithUserAuthOutput = AuthSignInOutput;
+
/**
* Output type for Cognito signInWithUserPassword API.
*/
diff --git a/packages/auth/src/providers/cognito/utils/signInHelpers.ts b/packages/auth/src/providers/cognito/utils/signInHelpers.ts
index 6d71e517da8..8589edaee34 100644
--- a/packages/auth/src/providers/cognito/utils/signInHelpers.ts
+++ b/packages/auth/src/providers/cognito/utils/signInHelpers.ts
@@ -51,6 +51,10 @@ import {
} from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types';
import { getRegionFromUserPoolId } from '../../../foundation/parsers';
import { handleWebAuthnSignInResult } from '../../../client/flows/userAuth/handleWebAuthnSignInResult';
+import { handlePasswordSRP } from '../../../client/flows/shared/handlePasswordSRP';
+import { initiateSelectedChallenge } from '../../../client/flows/userAuth/handleSelectChallenge';
+import { handleSelectChallengeWithPassword } from '../../../client/flows/userAuth/handleSelectChallengeWithPassword';
+import { handleSelectChallengeWithPasswordSRP } from '../../../client/flows/userAuth/handleSelectChallengeWithPasswordSRP';
import { signInStore } from './signInStore';
import { assertDeviceMetadata } from './types';
@@ -434,60 +438,14 @@ export async function handleUserSRPAuthFlow(
config: CognitoUserPoolConfig,
tokenOrchestrator: AuthTokenOrchestrator,
): Promise {
- const { userPoolId, userPoolClientId, userPoolEndpoint } = config;
- const userPoolName = userPoolId?.split('_')[1] || '';
- const authenticationHelper = await getAuthenticationHelper(userPoolName);
-
- const authParameters: Record = {
- USERNAME: username,
- SRP_A: authenticationHelper.A.toString(16),
- };
-
- const UserContextData = getUserContextData({
+ return handlePasswordSRP({
username,
- userPoolId,
- userPoolClientId,
- });
-
- const jsonReq: InitiateAuthCommandInput = {
- AuthFlow: 'USER_SRP_AUTH',
- AuthParameters: authParameters,
- ClientMetadata: clientMetadata,
- ClientId: userPoolClientId,
- UserContextData,
- };
-
- const initiateAuth = createInitiateAuthClient({
- endpointResolver: createCognitoUserPoolEndpointResolver({
- endpointOverride: userPoolEndpoint,
- }),
- });
-
- const resp = await initiateAuth(
- {
- region: getRegionFromUserPoolId(userPoolId),
- userAgentValue: getAuthUserAgentValue(AuthAction.SignIn),
- },
- jsonReq,
- );
- const { ChallengeParameters: challengeParameters, Session: session } = resp;
- const activeUsername = challengeParameters?.USERNAME ?? username;
- setActiveSignInUsername(activeUsername);
-
- return retryOnResourceNotFoundException(
- handlePasswordVerifierChallenge,
- [
- password,
- challengeParameters as ChallengeParameters,
- clientMetadata,
- session,
- authenticationHelper,
- config,
- tokenOrchestrator,
- ],
- activeUsername,
+ password,
+ clientMetadata,
+ config,
tokenOrchestrator,
- );
+ authFlow: 'USER_SRP_AUTH',
+ });
}
export async function handleCustomAuthFlowWithoutSRP(
@@ -813,8 +771,9 @@ export async function handlePasswordVerifierChallenge(
export async function getSignInResult(params: {
challengeName: ChallengeName;
challengeParameters: ChallengeParameters;
+ availableChallenges?: ChallengeName[];
}): Promise {
- const { challengeName, challengeParameters } = params;
+ const { challengeName, challengeParameters, availableChallenges } = params;
const authConfig = Amplify.getConfig().Auth?.Cognito;
assertTokenProviderConfig(authConfig);
@@ -944,6 +903,22 @@ export async function getSignInResult(params: {
case 'WEB_AUTHN':
return handleWebAuthnSignInResult(challengeParameters);
+ case 'PASSWORD':
+ case 'PASSWORD_SRP':
+ return {
+ isSignedIn: false,
+ nextStep: {
+ signInStep: 'CONFIRM_SIGN_IN_WITH_PASSWORD',
+ },
+ };
+ case 'SELECT_CHALLENGE':
+ return {
+ isSignedIn: false,
+ nextStep: {
+ signInStep: 'CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION',
+ availableChallenges,
+ },
+ };
case 'ADMIN_NO_SRP_AUTH':
break;
case 'DEVICE_PASSWORD_VERIFIER':
@@ -1031,6 +1006,26 @@ export async function handleChallengeName(
const deviceName = options?.friendlyDeviceName;
switch (challengeName) {
+ case 'WEB_AUTHN':
+ case 'SELECT_CHALLENGE':
+ if (
+ challengeResponse === 'PASSWORD_SRP' ||
+ challengeResponse === 'PASSWORD'
+ ) {
+ return {
+ ChallengeName: challengeResponse,
+ Session: session,
+ $metadata: {},
+ };
+ }
+
+ return initiateSelectedChallenge({
+ username,
+ session,
+ selectedChallenge: challengeResponse,
+ config,
+ clientMetadata,
+ });
case 'SELECT_MFA_TYPE':
return handleSelectMFATypeChallenge({
challengeResponse,
@@ -1075,6 +1070,7 @@ export async function handleChallengeName(
);
case 'SMS_MFA':
case 'SOFTWARE_TOKEN_MFA':
+ case 'SMS_OTP':
case 'EMAIL_OTP':
return handleMFAChallenge({
challengeName,
@@ -1084,6 +1080,23 @@ export async function handleChallengeName(
username,
config,
});
+ case 'PASSWORD':
+ return handleSelectChallengeWithPassword(
+ username,
+ challengeResponse,
+ clientMetadata,
+ config,
+ session,
+ );
+ case 'PASSWORD_SRP':
+ return handleSelectChallengeWithPasswordSRP(
+ username,
+ challengeResponse, // This is the actual password
+ clientMetadata,
+ config,
+ session,
+ tokenOrchestrator,
+ );
}
// TODO: remove this error message for production apps
throw new AuthError({
@@ -1264,7 +1277,7 @@ export async function handleMFAChallenge({
}: HandleAuthChallengeRequest & {
challengeName: Extract<
ChallengeName,
- 'EMAIL_OTP' | 'SMS_MFA' | 'SOFTWARE_TOKEN_MFA'
+ 'EMAIL_OTP' | 'SMS_MFA' | 'SOFTWARE_TOKEN_MFA' | 'SMS_OTP'
>;
}) {
const { userPoolId, userPoolClientId, userPoolEndpoint } = config;
@@ -1281,6 +1294,10 @@ export async function handleMFAChallenge({
challengeResponses.SMS_MFA_CODE = challengeResponse;
}
+ if (challengeName === 'SMS_OTP') {
+ challengeResponses.SMS_OTP_CODE = challengeResponse;
+ }
+
if (challengeName === 'SOFTWARE_TOKEN_MFA') {
challengeResponses.SOFTWARE_TOKEN_MFA_CODE = challengeResponse;
}
diff --git a/packages/auth/src/types/models.ts b/packages/auth/src/types/models.ts
index e08b7bce5f9..1655de572e8 100644
--- a/packages/auth/src/types/models.ts
+++ b/packages/auth/src/types/models.ts
@@ -3,6 +3,7 @@
import { AuthStandardAttributeKey } from '@aws-amplify/core/internals/utils';
+import { ChallengeName } from '../foundation/factories/serviceClients/cognitoIdentityProvider/types';
import { SignInOutput } from '../providers/cognito';
/**
@@ -217,6 +218,16 @@ export interface DoneSignInStep {
signInStep: 'DONE';
}
+// New interfaces for USER_AUTH flow
+export interface ContinueSignInWithFirstFactorSelection {
+ signInStep: 'CONTINUE_SIGN_IN_WITH_FIRST_FACTOR_SELECTION';
+ availableChallenges?: ChallengeName[];
+}
+
+export interface ConfirmSignInWithPassword {
+ signInStep: 'CONFIRM_SIGN_IN_WITH_PASSWORD';
+}
+
export type AuthNextSignInStep<
UserAttributeKey extends AuthUserAttributeKey = AuthUserAttributeKey,
> =
@@ -229,6 +240,8 @@ export type AuthNextSignInStep<
| ContinueSignInWithTOTPSetup
| ContinueSignInWithEmailSetup
| ContinueSignInWithMFASetupSelection
+ | ContinueSignInWithFirstFactorSelection
+ | ConfirmSignInWithPassword
| ConfirmSignUpStep
| ResetPasswordStep
| DoneSignInStep;
diff --git a/packages/core/src/singleton/Auth/types.ts b/packages/core/src/singleton/Auth/types.ts
index fd7bc788472..8c662ec5620 100644
--- a/packages/core/src/singleton/Auth/types.ts
+++ b/packages/core/src/singleton/Auth/types.ts
@@ -255,6 +255,7 @@ interface AWSAuthSignInDetails {
* @deprecated
*/
type AuthFlowType =
+ | 'USER_AUTH'
| 'USER_SRP_AUTH'
| 'CUSTOM_WITH_SRP'
| 'CUSTOM_WITHOUT_SRP'
From 9a8ab675dada7d79dd3ce1fff6348f1ccfb3f9a9 Mon Sep 17 00:00:00 2001
From: James Jarvis
Date: Thu, 31 Oct 2024 13:15:57 -0700
Subject: [PATCH 07/25] feat(auth): Add USER_AUTH flow in Sign Up logic (#11)
---
.../createSignUpClient.test.ts | 53 +++++++
.../cognitoIdentityProvider/index.test.ts | 4 +-
.../cognitoIdentityProvider/testUtils/data.ts | 18 +++
.../providers/cognito/signUp.test.ts | 29 ++--
.../createSignUpClient.ts | 51 ++++++-
.../cognitoIdentityProvider/types/sdk.ts | 24 ++-
.../providers/cognito/apis/confirmSignUp.ts | 8 +-
.../auth/src/providers/cognito/apis/signUp.ts | 140 +++++++++---------
.../providers/cognito/utils/signUpHelpers.ts | 9 +-
packages/auth/src/types/inputs.ts | 2 +-
10 files changed, 241 insertions(+), 97 deletions(-)
create mode 100644 packages/auth/__tests__/foundation/factories/serviceClients/cognitoIdentityProvider/createSignUpClient.test.ts
diff --git a/packages/auth/__tests__/foundation/factories/serviceClients/cognitoIdentityProvider/createSignUpClient.test.ts b/packages/auth/__tests__/foundation/factories/serviceClients/cognitoIdentityProvider/createSignUpClient.test.ts
new file mode 100644
index 00000000000..4c949522c97
--- /dev/null
+++ b/packages/auth/__tests__/foundation/factories/serviceClients/cognitoIdentityProvider/createSignUpClient.test.ts
@@ -0,0 +1,53 @@
+import { composeServiceApi } from '@aws-amplify/core/internals/aws-client-utils/composers';
+
+import { DEFAULT_SERVICE_CLIENT_API_CONFIG } from '../../../../../src/foundation/factories/serviceClients/cognitoIdentityProvider/constants';
+import { createSignUpClient } from '../../../../../src/foundation/factories/serviceClients/cognitoIdentityProvider';
+import { createSignUpClientDeserializer } from '../../../../../src/foundation/factories/serviceClients/cognitoIdentityProvider/createSignUpClient';
+import { AuthError } from '../../../../../src/errors/AuthError';
+import { AuthValidationErrorCode } from '../../../../../src/errors/types/validation';
+import { validationErrorMap } from '../../../../../src/common/AuthErrorStrings';
+
+import {
+ mockServiceClientAPIConfig,
+ mockSignUpClientEmptySignUpPasswordResponse,
+} from './testUtils/data';
+
+jest.mock('@aws-amplify/core/internals/aws-client-utils/composers', () => ({
+ ...jest.requireActual(
+ '@aws-amplify/core/internals/aws-client-utils/composers',
+ ),
+ composeServiceApi: jest.fn(),
+}));
+
+describe('createSignUpClient', () => {
+ const mockComposeServiceApi = jest.mocked(composeServiceApi);
+
+ it('factory should invoke composeServiceApi with expected parameters', () => {
+ createSignUpClient(mockServiceClientAPIConfig);
+
+ expect(mockComposeServiceApi).toHaveBeenCalledWith(
+ expect.any(Function),
+ expect.any(Function),
+ expect.any(Function),
+ expect.objectContaining({
+ ...DEFAULT_SERVICE_CLIENT_API_CONFIG,
+ ...mockServiceClientAPIConfig,
+ }),
+ );
+ });
+
+ it('createSignUpDeserializer should throw expected error when', () => {
+ const deserializer = createSignUpClientDeserializer();
+
+ expect(
+ deserializer(mockSignUpClientEmptySignUpPasswordResponse),
+ ).rejects.toThrow(
+ new AuthError({
+ name: AuthValidationErrorCode.EmptySignUpPassword,
+ message:
+ validationErrorMap[AuthValidationErrorCode.EmptySignUpPassword]
+ .message,
+ }),
+ );
+ });
+});
diff --git a/packages/auth/__tests__/foundation/factories/serviceClients/cognitoIdentityProvider/index.test.ts b/packages/auth/__tests__/foundation/factories/serviceClients/cognitoIdentityProvider/index.test.ts
index f9105f3a43d..8cf31b2cbd3 100644
--- a/packages/auth/__tests__/foundation/factories/serviceClients/cognitoIdentityProvider/index.test.ts
+++ b/packages/auth/__tests__/foundation/factories/serviceClients/cognitoIdentityProvider/index.test.ts
@@ -24,7 +24,9 @@ describe('service clients', () => {
test.each(serviceClientFactories)(
'factory `%s` should invoke composeServiceApi with expected parameters',
serviceClientFactory => {
- serviceClients[serviceClientFactory](mockServiceClientAPIConfig);
+ serviceClients[serviceClientFactory as keyof typeof serviceClients](
+ mockServiceClientAPIConfig,
+ );
expect(mockComposeServiceApi).toHaveBeenCalledWith(
expect.any(Function),
diff --git a/packages/auth/__tests__/foundation/factories/serviceClients/cognitoIdentityProvider/testUtils/data.ts b/packages/auth/__tests__/foundation/factories/serviceClients/cognitoIdentityProvider/testUtils/data.ts
index 33a9a3d5534..0cea5ec340d 100644
--- a/packages/auth/__tests__/foundation/factories/serviceClients/cognitoIdentityProvider/testUtils/data.ts
+++ b/packages/auth/__tests__/foundation/factories/serviceClients/cognitoIdentityProvider/testUtils/data.ts
@@ -1,3 +1,5 @@
+import { HttpResponse } from '@aws-amplify/core/internals/aws-client-utils';
+
import { ServiceClientFactoryInput } from '../../../../../../src/foundation/factories/serviceClients/cognitoIdentityProvider/types';
export const mockServiceClientAPIConfig: ServiceClientFactoryInput = {
@@ -5,3 +7,19 @@ export const mockServiceClientAPIConfig: ServiceClientFactoryInput = {
ServiceClientFactoryInput['endpointResolver']
>,
};
+
+export const mockSignUpClientEmptySignUpPasswordResponse: HttpResponse = {
+ statusCode: 400,
+ body: {
+ json: () =>
+ Promise.resolve({
+ message:
+ "1 validation error detected: Value at 'password'failed to satisfy constraint: Member must not be null",
+ }),
+ blob: () => Promise.resolve(new Blob()),
+ text: () => Promise.resolve(''),
+ },
+ headers: {
+ 'x-amzn-errortype': 'InvalidParameterException',
+ },
+};
diff --git a/packages/auth/__tests__/providers/cognito/signUp.test.ts b/packages/auth/__tests__/providers/cognito/signUp.test.ts
index cb2b9b84d64..3b3f9bab4c5 100644
--- a/packages/auth/__tests__/providers/cognito/signUp.test.ts
+++ b/packages/auth/__tests__/providers/cognito/signUp.test.ts
@@ -244,6 +244,25 @@ describe('signUp', () => {
expect(mockSignUp).toHaveBeenCalledTimes(1);
(window as any).AmazonCognitoAdvancedSecurityData = undefined;
});
+
+ it('should not throw an error when password is empty', async () => {
+ await signUp({ username: user1.username, password: '' });
+ expect(mockSignUp).toHaveBeenCalledWith(
+ {
+ region: 'us-west-2',
+ userAgentValue: expect.any(String),
+ },
+ {
+ ClientMetadata: undefined,
+ Password: undefined,
+ UserAttributes: undefined,
+ Username: user1.username,
+ ValidationData: undefined,
+ ClientId: '111111-aaaaa-42d8-891d-ee81a1549398',
+ },
+ );
+ expect(mockSignUp).toHaveBeenCalledTimes(1);
+ });
});
describe('Error Path Cases:', () => {
@@ -265,16 +284,6 @@ describe('signUp', () => {
}
});
- it('should throw an error when password is empty', async () => {
- expect.assertions(2);
- try {
- await signUp({ username: user1.username, password: '' });
- } catch (error: any) {
- expect(error).toBeInstanceOf(AuthError);
- expect(error.name).toBe(AuthValidationErrorCode.EmptySignUpPassword);
- }
- });
-
it('should throw an error when service returns an error response', async () => {
expect.assertions(2);
mockSignUp.mockImplementation(() => {
diff --git a/packages/auth/src/foundation/factories/serviceClients/cognitoIdentityProvider/createSignUpClient.ts b/packages/auth/src/foundation/factories/serviceClients/cognitoIdentityProvider/createSignUpClient.ts
index e77676bab1d..ca9a2fda127 100644
--- a/packages/auth/src/foundation/factories/serviceClients/cognitoIdentityProvider/createSignUpClient.ts
+++ b/packages/auth/src/foundation/factories/serviceClients/cognitoIdentityProvider/createSignUpClient.ts
@@ -1,24 +1,61 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { composeServiceApi } from '@aws-amplify/core/internals/aws-client-utils/composers';
+import {
+ HttpResponse,
+ parseJsonBody,
+ parseJsonError,
+} from '@aws-amplify/core/internals/aws-client-utils';
+
+import { validationErrorMap } from '../../../../common/AuthErrorStrings';
+import { AuthError } from '../../../../errors/AuthError';
+import { AuthValidationErrorCode } from '../../../../errors/types/validation';
+import { assertServiceError } from '../../../../errors/utils/assertServiceError';
+import { SignUpException } from '../../../../providers/cognito/types/errors';
+import { createUserPoolSerializer } from './shared/serde';
+import { cognitoUserPoolTransferHandler } from './shared/handler';
+import { DEFAULT_SERVICE_CLIENT_API_CONFIG } from './constants';
import {
ServiceClientFactoryInput,
SignUpCommandInput,
SignUpCommandOutput,
} from './types';
-import { DEFAULT_SERVICE_CLIENT_API_CONFIG } from './constants';
-import { cognitoUserPoolTransferHandler } from './shared/handler';
-import {
- createUserPoolDeserializer,
- createUserPoolSerializer,
-} from './shared/serde';
+
+export const createSignUpClientDeserializer =
+ (): ((response: HttpResponse) => Promise) =>
+ async (response: HttpResponse): Promise => {
+ if (response.statusCode >= 300) {
+ const error = await parseJsonError(response);
+ assertServiceError(error);
+
+ if (
+ // Missing Password Error
+ // 1 validation error detected: Value at 'password'failed to satisfy constraint: Member must not be null
+ error.name === SignUpException.InvalidParameterException &&
+ /'password'/.test(error.message) &&
+ /Member must not be null/.test(error.message)
+ ) {
+ const name = AuthValidationErrorCode.EmptySignUpPassword;
+ const { message, recoverySuggestion } = validationErrorMap[name];
+ throw new AuthError({
+ name,
+ message,
+ recoverySuggestion,
+ });
+ }
+
+ throw new AuthError({ name: error.name, message: error.message });
+ }
+
+ return parseJsonBody(response);
+ };
export const createSignUpClient = (config: ServiceClientFactoryInput) =>
composeServiceApi(
cognitoUserPoolTransferHandler,
createUserPoolSerializer('SignUp'),
- createUserPoolDeserializer(),
+ createSignUpClientDeserializer(),
{
...DEFAULT_SERVICE_CLIENT_API_CONFIG,
...config,
diff --git a/packages/auth/src/foundation/factories/serviceClients/cognitoIdentityProvider/types/sdk.ts b/packages/auth/src/foundation/factories/serviceClients/cognitoIdentityProvider/types/sdk.ts
index 966e93f01f3..320bc6baab0 100644
--- a/packages/auth/src/foundation/factories/serviceClients/cognitoIdentityProvider/types/sdk.ts
+++ b/packages/auth/src/foundation/factories/serviceClients/cognitoIdentityProvider/types/sdk.ts
@@ -706,7 +706,15 @@ export interface ConfirmSignUpRequest {
/**
*
Represents the response from the server for the registration confirmation.
Your ConfirmSignUp request might produce a challenge that your user must
+ * respond to, for example a one-time code. The Session parameter tracks the
+ * session in the flow of challenge responses and requests. Include this parameter in
+ * RespondToAuthChallenge API requests.
Contextual data such as the user's device fingerprint, IP address, or location used for evaluating the risk of an unexpected event by Amazon Cognito advanced security.
The optional session ID from a ConfirmSignUp API
+ * request. You can sign in a user directly from the sign-up process with the
+ * USER_AUTH authentication flow.
The UUID of the authenticated user. This isn't the same as username.
*/
UserSub: string | undefined;
+
+ /**
+ *
A session Id that you can pass to ConfirmSignUp when you want to
+ * immediately sign in your user with the USER_AUTH flow after they complete
+ * sign-up.
The request to retrieve WebAuthN registration options.
- */
-export interface GetWebAuthnRegistrationOptionsInput {
+export interface StartWebAuthnRegistrationRequest {
+ /**
+ * A valid access token that Amazon Cognito issued to the user whose passkey metadata you want to
+ * generate.
+ */
AccessToken: string | undefined;
}
-/**
- *
The response containing WebAuthN registration options.
- */
-export interface GetWebAuthnRegistrationOptionsOutput {
- CredentialCreationOptions: string | undefined;
+
+export interface StartWebAuthnRegistrationResponse {
+ /**
+ * The information that a user can provide in their request to register with their
+ * passkey provider.
+ */
+ CredentialCreationOptions: Record | undefined;
}
-export type GetWebAuthnRegistrationOptionsCommandInput =
- GetWebAuthnRegistrationOptionsInput;
+export type StartWebAuthnRegistrationCommandInput =
+ StartWebAuthnRegistrationRequest;
-export interface GetWebAuthnRegistrationOptionsCommandOutput
- extends GetWebAuthnRegistrationOptionsOutput,
+export interface StartWebAuthnRegistrationCommandOutput
+ extends StartWebAuthnRegistrationResponse,
__MetadataBearer {}
-/**
- *
The request to verify a WebAuthN credential.
- */
-export interface VerifyWebAuthnRegistrationResultInput {
+export interface CompleteWebAuthnRegistrationRequest {
+ /**
+ * A valid access token that Amazon Cognito issued to the user whose passkey registration you want
+ * to verify. This information informs your user pool of the details of the user's
+ * successful registration with their passkey provider.
+ */
AccessToken: string | undefined;
- Credential: string | undefined;
+
+ /**
+ * A RegistrationResponseJSON public-key credential response from the
+ * user's passkey provider.
+ */
+ Credential: Record | undefined;
}
-export type VerifyWebAuthnRegistrationResultCommandInput =
- VerifyWebAuthnRegistrationResultInput;
+export type CompleteWebAuthnRegistrationResponse = Record;
-export type VerifyWebAuthnRegistrationResultCommandOutput = __MetadataBearer;
+export type CompleteWebAuthnRegistrationCommandInput =
+ CompleteWebAuthnRegistrationRequest;
+
+export interface CompleteWebAuthnRegistrationCommandOutput
+ extends CompleteWebAuthnRegistrationResponse,
+ __MetadataBearer {}
/**
*
The request to list WebAuthN credentials.
diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json
index 11957b39dc1..39f43d7b142 100644
--- a/packages/aws-amplify/package.json
+++ b/packages/aws-amplify/package.json
@@ -461,7 +461,7 @@
"name": "[Auth] Associate WebAuthN Credential (Cognito)",
"path": "./dist/esm/auth/index.mjs",
"import": "{ associateWebAuthnCredential }",
- "limit": "13.05 kB"
+ "limit": "13.07 kB"
},
{
"name": "[Auth] List WebAuthN Credentials (Cognito)",
diff --git a/packages/core/src/Platform/types.ts b/packages/core/src/Platform/types.ts
index 3a91b5d1191..351f04603c3 100644
--- a/packages/core/src/Platform/types.ts
+++ b/packages/core/src/Platform/types.ts
@@ -90,8 +90,8 @@ export enum AuthAction {
FetchDevices = '34',
SendUserAttributeVerificationCode = '35',
SignInWithRedirect = '36',
- GetWebAuthnRegistrationOptions = '37',
- VerifyWebAuthnRegistrationResult = '38',
+ StartWebAuthnRegistration = '37',
+ CompleteWebAuthnRegistration = '38',
ListWebAuthnCredentials = '39',
DeleteWebAuthnCredential = '40',
}
From c5b42e06e929078319f3fdd941c8886d1088ee60 Mon Sep 17 00:00:00 2001
From: James Jarvis
Date: Mon, 11 Nov 2024 20:14:00 -0800
Subject: [PATCH 15/25] update exception mapping (#15)
---
.../client/apis/associateWebAuthnCredential.ts | 8 ++++----
.../cognitoIdentityProvider/types/errors.ts | 16 ++++++++++------
2 files changed, 14 insertions(+), 10 deletions(-)
diff --git a/packages/auth/src/client/apis/associateWebAuthnCredential.ts b/packages/auth/src/client/apis/associateWebAuthnCredential.ts
index 09adf0b4ec8..d95dca05651 100644
--- a/packages/auth/src/client/apis/associateWebAuthnCredential.ts
+++ b/packages/auth/src/client/apis/associateWebAuthnCredential.ts
@@ -8,8 +8,8 @@ import {
} from '@aws-amplify/core/internals/utils';
import {
- GetWebAuthnRegistrationOptionsException,
- VerifyWebAuthnRegistrationResultException,
+ CompleteWebAuthnRegistrationException,
+ StartWebAuthnRegistrationException,
} from '../../foundation/factories/serviceClients/cognitoIdentityProvider/types';
import { assertAuthTokens } from '../../providers/cognito/utils/types';
import { createCognitoUserPoolEndpointResolver } from '../../providers/cognito/factories';
@@ -32,9 +32,9 @@ import { assertValidCredentialCreationOptions } from '../utils/passkey/types';
* - Thrown when intermediate state is invalid
* @throws - {@link AuthError}:
* - Thrown when user is unauthenticated
- * @throws - {@link GetWebAuthnRegistrationOptionsException}
+ * @throws - {@link StartWebAuthnRegistrationException}
* - Thrown due to a service error retrieving WebAuthn registration options
- * @throws - {@link VerifyWebAuthnRegistrationResultException}
+ * @throws - {@link CompleteWebAuthnRegistrationException}
* - Thrown due to a service error when verifying WebAuthn registration result
*/
export async function associateWebAuthnCredential(): Promise {
diff --git a/packages/auth/src/foundation/factories/serviceClients/cognitoIdentityProvider/types/errors.ts b/packages/auth/src/foundation/factories/serviceClients/cognitoIdentityProvider/types/errors.ts
index 944a71d1e01..3a45bd9abbf 100644
--- a/packages/auth/src/foundation/factories/serviceClients/cognitoIdentityProvider/types/errors.ts
+++ b/packages/auth/src/foundation/factories/serviceClients/cognitoIdentityProvider/types/errors.ts
@@ -1,27 +1,30 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-export enum GetWebAuthnRegistrationOptionsException {
+export enum StartWebAuthnRegistrationException {
ForbiddenException = 'ForbiddenException',
InternalErrorException = 'InternalErrorException',
InvalidParameterException = 'InvalidParameterException',
- InvalidWebAuthnConfigurationException = 'InvalidWebAuthnConfigurationException',
LimitExceededException = 'LimitExceededException',
NotAuthorizedException = 'NotAuthorizedException',
TooManyRequestsException = 'TooManyRequestsException',
WebAuthnNotEnabledException = 'WebAuthnNotEnabledException',
+ WebAuthnConfigurationMissingException = 'WebAuthnConfigurationMissingException',
}
-export enum VerifyWebAuthnRegistrationResultException {
- CredentialAlreadyExistsException = 'CredentialAlreadyExistsException',
+export enum CompleteWebAuthnRegistrationException {
ForbiddenException = 'ForbiddenException',
InternalErrorException = 'InternalErrorException',
InvalidParameterException = 'InvalidParameterException',
+ LimitExceededException = 'LimitExceededException',
NotAuthorizedException = 'NotAuthorizedException',
TooManyRequestsException = 'TooManyRequestsException',
- WebAuthnAuthenticatorSelectionMismatchException = 'WebAuthnAuthenticatorSelectionMismatchException',
- WebAuthnChallengeMismatchException = 'WebAuthnChallengeMismatchException',
+ WebAuthnNotEnabledException = 'WebAuthnNotEnabledException',
+ WebAuthnChallengeNotFoundException = 'WebAuthnChallengeNotFoundException',
WebAuthnRelyingPartyMismatchException = 'WebAuthnRelyingPartyMismatchException',
+ WebAuthnClientMismatchException = 'WebAuthnClientMismatchException',
+ WebAuthnOriginNotAllowedException = 'WebAuthnOriginNotAllowedException',
+ WebAuthnCredentialNotSupportedException = 'WebAuthnCredentialNotSupportedException',
}
export enum ListWebAuthnCredentialsException {
@@ -36,4 +39,5 @@ export enum DeleteWebAuthnCredentialException {
InternalErrorException = 'InternalErrorException',
InvalidParameterException = 'InvalidParameterException',
NotAuthorizedException = 'NotAuthorizedException',
+ ResourceNotFoundException = 'ResourceNotFoundException',
}
From b6b474c1bcdd8f3a0d37bcb1af7253c3cd86c207 Mon Sep 17 00:00:00 2001
From: James Jarvis
Date: Tue, 12 Nov 2024 10:49:29 -0800
Subject: [PATCH 16/25] feat(auth): passwordless webauthn ceremony errors (#16)
* update exception mapping
* update passkey error handling
* update tests
* bundle size tests
* simplify language
* refine error messages
---
.../apis/associateWebAuthnCredential.test.ts | 2 +-
.../userAuth/handleWebAuthnSignInResult.ts | 2 +-
.../auth/src/client/utils/passkey/errors.ts | 190 ++++++++++++++++--
.../src/client/utils/passkey/getPasskey.ts | 29 ++-
.../client/utils/passkey/registerPasskey.ts | 29 ++-
.../src/client/utils/passkey/types/shared.ts | 2 +-
packages/aws-amplify/package.json | 4 +-
7 files changed, 217 insertions(+), 41 deletions(-)
diff --git a/packages/auth/__tests__/client/apis/associateWebAuthnCredential.test.ts b/packages/auth/__tests__/client/apis/associateWebAuthnCredential.test.ts
index a9ca5d13585..bae6e6ec77f 100644
--- a/packages/auth/__tests__/client/apis/associateWebAuthnCredential.test.ts
+++ b/packages/auth/__tests__/client/apis/associateWebAuthnCredential.test.ts
@@ -173,7 +173,7 @@ describe('associateWebAuthnCredential', () => {
} catch (error: any) {
expect(error).toBeInstanceOf(PasskeyError);
expect(error.name).toBe(
- PasskeyErrorCode.InvalidCredentialCreationOptions,
+ PasskeyErrorCode.InvalidPasskeyRegistrationOptions,
);
}
});
diff --git a/packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.ts b/packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.ts
index 2364c22291a..2e67a52a5ab 100644
--- a/packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.ts
+++ b/packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.ts
@@ -55,7 +55,7 @@ export async function handleWebAuthnSignInResult(
assertPasskeyError(
!!credentialRequestOptions,
- PasskeyErrorCode.InvalidCredentialRequestOptions,
+ PasskeyErrorCode.InvalidPasskeyAuthenticationOptions,
);
const cred = await getPasskey(JSON.parse(credentialRequestOptions));
diff --git a/packages/auth/src/client/utils/passkey/errors.ts b/packages/auth/src/client/utils/passkey/errors.ts
index b746b2e8e1a..2b91c3fdb28 100644
--- a/packages/auth/src/client/utils/passkey/errors.ts
+++ b/packages/auth/src/client/utils/passkey/errors.ts
@@ -3,6 +3,7 @@
import {
AmplifyError,
+ AmplifyErrorCode,
AmplifyErrorMap,
AmplifyErrorParams,
AssertionFunction,
@@ -21,40 +22,193 @@ export class PasskeyError extends AmplifyError {
}
export enum PasskeyErrorCode {
+ // not supported
PasskeyNotSupported = 'PasskeyNotSupported',
- InvalidCredentialCreationOptions = 'InvalidCredentialCreationOptions',
- InvalidCredentialRequestOptions = 'InvalidCredentialRequestOptions',
+ // duplicate passkey
+ PasskeyAlreadyExists = 'PasskeyAlreadyExists',
+ // misconfigurations
+ InvalidPasskeyRegistrationOptions = 'InvalidPasskeyRegistrationOptions',
+ InvalidPasskeyAuthenticationOptions = 'InvalidPasskeyAuthenticationOptions',
+ RelyingPartyMismatch = 'RelyingPartyMismatch',
+ // failed credential creation / retrieval
PasskeyRegistrationFailed = 'PasskeyRegistrationFailed',
PasskeyRetrievalFailed = 'PasskeyRetrievalFailed',
+ // cancel / aborts
+ PasskeyRegistrationCanceled = 'PasskeyRegistrationCanceled',
+ PasskeyAuthenticationCanceled = 'PasskeyAuthenticationCanceled',
+ PasskeyOperationAborted = 'PasskeyOperationAborted',
}
+const notSupportedRecoverySuggestion =
+ 'Passkeys may not be supported on this device. Ensure your application is running in a secure context (HTTPS) and Web Authentication API is supported.';
+const abortOrCancelRecoverySuggestion =
+ 'User may have canceled the ceremony or another interruption has occurred. Check underlying error for details.';
+const misconfigurationRecoverySuggestion =
+ 'Ensure your user pool configured to support the WEB_AUTHN as an authentication factor.';
+
const passkeyErrorMap: AmplifyErrorMap = {
[PasskeyErrorCode.PasskeyNotSupported]: {
- message: 'Passkey not supported on this device.',
- recoverySuggestion:
- 'Ensure your application is running in a secure context (HTTPS).',
+ message: 'Passkeys may not be supported on this device.',
+ recoverySuggestion: notSupportedRecoverySuggestion,
},
- [PasskeyErrorCode.InvalidCredentialCreationOptions]: {
- message: 'Invalid credential creation options.',
- recoverySuggestion:
- 'Ensure your user pool is configured to support WebAuthN passkey registration',
+ [PasskeyErrorCode.InvalidPasskeyRegistrationOptions]: {
+ message: 'Invalid passkey registration options.',
+ recoverySuggestion: misconfigurationRecoverySuggestion,
},
- [PasskeyErrorCode.InvalidCredentialRequestOptions]: {
- message: 'Invalid credential request options.',
- recoverySuggestion:
- 'User pool may not be configured to support WEB_AUTHN authentication factor.',
+ [PasskeyErrorCode.InvalidPasskeyAuthenticationOptions]: {
+ message: 'Invalid passkey authentication options.',
+ recoverySuggestion: misconfigurationRecoverySuggestion,
},
[PasskeyErrorCode.PasskeyRegistrationFailed]: {
- message: 'Device failed to create credentials.',
- recoverySuggestion:
- 'Credentials may not be supported on this device. Ensure your browser is up to date and the Web Authentication API is supported.',
+ message: 'Device failed to create passkey.',
+ recoverySuggestion: notSupportedRecoverySuggestion,
},
[PasskeyErrorCode.PasskeyRetrievalFailed]: {
- message: 'Device failed to retrieve credentials.',
+ message: 'Device failed to retrieve passkey.',
+ recoverySuggestion:
+ 'Passkeys may not be available on this device. Try an alternative authentication factor like PASSWORD, EMAIL_OTP, or SMS_OTP.',
+ },
+ [PasskeyErrorCode.PasskeyAlreadyExists]: {
+ message: 'Passkey already exists in authenticator.',
+ recoverySuggestion:
+ 'Proceed with existing passkey or try again after deleting the credential.',
+ },
+ [PasskeyErrorCode.PasskeyRegistrationCanceled]: {
+ message: 'Passkey registration ceremony has been canceled.',
+ recoverySuggestion: abortOrCancelRecoverySuggestion,
+ },
+ [PasskeyErrorCode.PasskeyAuthenticationCanceled]: {
+ message: 'Passkey authentication ceremony has been canceled.',
+ recoverySuggestion: abortOrCancelRecoverySuggestion,
+ },
+ [PasskeyErrorCode.PasskeyOperationAborted]: {
+ message: 'Passkey operation has been aborted.',
+ recoverySuggestion: abortOrCancelRecoverySuggestion,
+ },
+ [PasskeyErrorCode.RelyingPartyMismatch]: {
+ message: 'Relying party does not match current domain.',
recoverySuggestion:
- 'Credentials may not be available on this device. Try an alternative authentication factor like PASSWORD, EMAIL_OTP, or SMS_OTP.',
+ 'Ensure relying party identifier matches current domain.',
},
};
export const assertPasskeyError: AssertionFunction =
createAssertionFunction(passkeyErrorMap, PasskeyError);
+
+/**
+ * Handle Passkey Authentication Errors
+ * https://w3c.github.io/webauthn/#sctn-get-request-exceptions
+ *
+ * @param err unknown
+ * @returns PasskeyError
+ */
+
+export const handlePasskeyAuthenticationError = (
+ err: unknown,
+): PasskeyError => {
+ if (err instanceof PasskeyError) {
+ return err;
+ }
+
+ if (err instanceof Error) {
+ if (err.name === 'NotAllowedError') {
+ const { message, recoverySuggestion } =
+ passkeyErrorMap[PasskeyErrorCode.PasskeyAuthenticationCanceled];
+
+ return new PasskeyError({
+ name: PasskeyErrorCode.PasskeyAuthenticationCanceled,
+ message,
+ recoverySuggestion,
+ underlyingError: err,
+ });
+ }
+ }
+
+ return handlePasskeyError(err);
+};
+
+/**
+ * Handle Passkey Registration Errors
+ * https://w3c.github.io/webauthn/#sctn-create-request-exceptions
+ *
+ * @param err unknown
+ * @returns PasskeyError
+ */
+export const handlePasskeyRegistrationError = (err: unknown): PasskeyError => {
+ if (err instanceof PasskeyError) {
+ return err;
+ }
+
+ if (err instanceof Error) {
+ // Duplicate Passkey
+ if (err.name === 'InvalidStateError') {
+ const { message, recoverySuggestion } =
+ passkeyErrorMap[PasskeyErrorCode.PasskeyAlreadyExists];
+
+ return new PasskeyError({
+ name: PasskeyErrorCode.PasskeyAlreadyExists,
+ message,
+ recoverySuggestion,
+ underlyingError: err,
+ });
+ }
+
+ // User Cancels Ceremony / Generic Catch All
+ if (err.name === 'NotAllowedError') {
+ const { message, recoverySuggestion } =
+ passkeyErrorMap[PasskeyErrorCode.PasskeyRegistrationCanceled];
+
+ return new PasskeyError({
+ name: PasskeyErrorCode.PasskeyRegistrationCanceled,
+ message,
+ recoverySuggestion,
+ underlyingError: err,
+ });
+ }
+ }
+
+ return handlePasskeyError(err);
+};
+
+/**
+ * Handles Overlapping Passkey Errors Between Registration & Authentication
+ * https://w3c.github.io/webauthn/#sctn-create-request-exceptions
+ * https://w3c.github.io/webauthn/#sctn-get-request-exceptions
+ *
+ * @param err unknown
+ * @returns PasskeyError
+ */
+const handlePasskeyError = (err: unknown): PasskeyError => {
+ if (err instanceof Error) {
+ // Passkey Operation Aborted
+ if (err.name === 'AbortError') {
+ const { message, recoverySuggestion } =
+ passkeyErrorMap[PasskeyErrorCode.PasskeyOperationAborted];
+
+ return new PasskeyError({
+ name: PasskeyErrorCode.PasskeyOperationAborted,
+ message,
+ recoverySuggestion,
+ underlyingError: err,
+ });
+ }
+ // Relying Party / Domain Mismatch
+ if (err.name === 'SecurityError') {
+ const { message, recoverySuggestion } =
+ passkeyErrorMap[PasskeyErrorCode.RelyingPartyMismatch];
+
+ return new PasskeyError({
+ name: PasskeyErrorCode.RelyingPartyMismatch,
+ message,
+ recoverySuggestion,
+ underlyingError: err,
+ });
+ }
+ }
+
+ return new PasskeyError({
+ name: AmplifyErrorCode.Unknown,
+ message: 'An unknown error has occurred.',
+ underlyingError: err,
+ });
+};
diff --git a/packages/auth/src/client/utils/passkey/getPasskey.ts b/packages/auth/src/client/utils/passkey/getPasskey.ts
index 5afdec24b77..8a3ee7f3d6e 100644
--- a/packages/auth/src/client/utils/passkey/getPasskey.ts
+++ b/packages/auth/src/client/utils/passkey/getPasskey.ts
@@ -1,7 +1,11 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import { PasskeyErrorCode, assertPasskeyError } from './errors';
+import {
+ PasskeyErrorCode,
+ assertPasskeyError,
+ handlePasskeyAuthenticationError,
+} from './errors';
import { getIsPasskeySupported } from './getIsPasskeySupported';
import {
deserializeJsonToPkcGetOptions,
@@ -13,17 +17,24 @@ import {
} from './types';
export const getPasskey = async (input: PasskeyGetOptionsJson) => {
- const isPasskeySupported = getIsPasskeySupported();
+ try {
+ const isPasskeySupported = getIsPasskeySupported();
- assertPasskeyError(isPasskeySupported, PasskeyErrorCode.PasskeyNotSupported);
+ assertPasskeyError(
+ isPasskeySupported,
+ PasskeyErrorCode.PasskeyNotSupported,
+ );
- const passkeyGetOptions = deserializeJsonToPkcGetOptions(input);
+ const passkeyGetOptions = deserializeJsonToPkcGetOptions(input);
- const credential = await navigator.credentials.get({
- publicKey: passkeyGetOptions,
- });
+ const credential = await navigator.credentials.get({
+ publicKey: passkeyGetOptions,
+ });
- assertCredentialIsPkcWithAuthenticatorAssertionResponse(credential);
+ assertCredentialIsPkcWithAuthenticatorAssertionResponse(credential);
- return serializePkcWithAssertionToJson(credential);
+ return serializePkcWithAssertionToJson(credential);
+ } catch (err: unknown) {
+ throw handlePasskeyAuthenticationError(err);
+ }
};
diff --git a/packages/auth/src/client/utils/passkey/registerPasskey.ts b/packages/auth/src/client/utils/passkey/registerPasskey.ts
index e1e522aacb5..88ae3eacf2e 100644
--- a/packages/auth/src/client/utils/passkey/registerPasskey.ts
+++ b/packages/auth/src/client/utils/passkey/registerPasskey.ts
@@ -10,7 +10,11 @@ import {
deserializeJsonToPkcCreationOptions,
serializePkcWithAttestationToJson,
} from './serde';
-import { PasskeyErrorCode, assertPasskeyError } from './errors';
+import {
+ PasskeyErrorCode,
+ assertPasskeyError,
+ handlePasskeyRegistrationError,
+} from './errors';
import { getIsPasskeySupported } from './getIsPasskeySupported';
/**
@@ -21,17 +25,24 @@ import { getIsPasskeySupported } from './getIsPasskeySupported';
export const registerPasskey = async (
input: PasskeyCreateOptionsJson,
): Promise => {
- const isPasskeySupported = getIsPasskeySupported();
+ try {
+ const isPasskeySupported = getIsPasskeySupported();
- assertPasskeyError(isPasskeySupported, PasskeyErrorCode.PasskeyNotSupported);
+ assertPasskeyError(
+ isPasskeySupported,
+ PasskeyErrorCode.PasskeyNotSupported,
+ );
- const passkeyCreationOptions = deserializeJsonToPkcCreationOptions(input);
+ const passkeyCreationOptions = deserializeJsonToPkcCreationOptions(input);
- const credential = await navigator.credentials.create({
- publicKey: passkeyCreationOptions,
- });
+ const credential = await navigator.credentials.create({
+ publicKey: passkeyCreationOptions,
+ });
- assertCredentialIsPkcWithAuthenticatorAttestationResponse(credential);
+ assertCredentialIsPkcWithAuthenticatorAttestationResponse(credential);
- return serializePkcWithAttestationToJson(credential);
+ return serializePkcWithAttestationToJson(credential);
+ } catch (err) {
+ throw handlePasskeyRegistrationError(err);
+ }
};
diff --git a/packages/auth/src/client/utils/passkey/types/shared.ts b/packages/auth/src/client/utils/passkey/types/shared.ts
index 648ae89f4bb..f684b08e9a6 100644
--- a/packages/auth/src/client/utils/passkey/types/shared.ts
+++ b/packages/auth/src/client/utils/passkey/types/shared.ts
@@ -88,7 +88,7 @@ export function assertValidCredentialCreationOptions(
!!credentialCreationOptions?.rp,
!!credentialCreationOptions?.pubKeyCredParams,
].every(Boolean),
- PasskeyErrorCode.InvalidCredentialCreationOptions,
+ PasskeyErrorCode.InvalidPasskeyRegistrationOptions,
);
}
diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json
index 39f43d7b142..8cf65e99f59 100644
--- a/packages/aws-amplify/package.json
+++ b/packages/aws-amplify/package.json
@@ -449,7 +449,7 @@
"name": "[Auth] Basic Auth Flow (Cognito)",
"path": "./dist/esm/auth/index.mjs",
"import": "{ signIn, signOut, fetchAuthSession, confirmSignIn }",
- "limit": "30.23 kB"
+ "limit": "30.34 kB"
},
{
"name": "[Auth] OAuth Auth Flow (Cognito)",
@@ -461,7 +461,7 @@
"name": "[Auth] Associate WebAuthN Credential (Cognito)",
"path": "./dist/esm/auth/index.mjs",
"import": "{ associateWebAuthnCredential }",
- "limit": "13.07 kB"
+ "limit": "13.50 kB"
},
{
"name": "[Auth] List WebAuthN Credentials (Cognito)",
From 19519f2ac75468dbc9b1662df42aafa5955bfded Mon Sep 17 00:00:00 2001
From: James Jarvis
Date: Tue, 19 Nov 2024 12:44:28 -0800
Subject: [PATCH 17/25] fix(auth): clear auto sign in store on sign in (#18)
* fix(auth): clear auto sign in store on sign in
* add unit test
---
.../autoSignInUserConfirmed.test.ts | 65 +++++++++++++++++++
.../auth/src/providers/cognito/apis/signIn.ts | 2 +
.../providers/cognito/utils/signUpHelpers.ts | 7 +-
3 files changed, 73 insertions(+), 1 deletion(-)
create mode 100644 packages/auth/__tests__/providers/cognito/utils/signUpHelpers/autoSignInUserConfirmed.test.ts
diff --git a/packages/auth/__tests__/providers/cognito/utils/signUpHelpers/autoSignInUserConfirmed.test.ts b/packages/auth/__tests__/providers/cognito/utils/signUpHelpers/autoSignInUserConfirmed.test.ts
new file mode 100644
index 00000000000..98c02e16e5f
--- /dev/null
+++ b/packages/auth/__tests__/providers/cognito/utils/signUpHelpers/autoSignInUserConfirmed.test.ts
@@ -0,0 +1,65 @@
+import { autoSignInUserConfirmed } from '../../../../../src/providers/cognito/utils/signUpHelpers';
+import { authAPITestParams } from '../../testUtils/authApiTestParams';
+import { signInWithUserAuth } from '../../../../../src/providers/cognito/apis/signInWithUserAuth';
+import { signIn } from '../../../../../src/providers/cognito/apis/signIn';
+import { SignInInput } from '../../../../../src/providers/cognito/types/inputs';
+
+jest.mock('@aws-amplify/core/internals/utils', () => ({
+ ...jest.requireActual('@aws-amplify/core/internals/utils'),
+ isBrowser: jest.fn(() => false),
+}));
+
+const { user1 } = authAPITestParams;
+
+jest.mock('../../../../../src/providers/cognito/apis/signInWithUserAuth');
+jest.mock('../../../../../src/providers/cognito/apis/signIn');
+
+describe('autoSignInUserConfirmed()', () => {
+ const mockSignInWithUserAuth = jest.mocked(signInWithUserAuth);
+ const mockSignIn = jest.mocked(signIn);
+
+ jest.useFakeTimers();
+
+ afterEach(() => {
+ jest.runAllTimers();
+ });
+
+ beforeEach(() => {
+ mockSignInWithUserAuth.mockReset();
+ mockSignIn.mockReset();
+ });
+
+ beforeAll(() => {
+ mockSignInWithUserAuth.mockImplementation(jest.fn());
+ mockSignIn.mockImplementation(jest.fn());
+ });
+
+ it('should call the correct API with authFlowType USER_AUTH', () => {
+ const signInInput: SignInInput = {
+ username: user1.username,
+ options: {
+ authFlowType: 'USER_AUTH',
+ },
+ };
+
+ autoSignInUserConfirmed(signInInput)();
+
+ expect(mockSignInWithUserAuth).toHaveBeenCalledTimes(1);
+ expect(mockSignInWithUserAuth).toHaveBeenCalledWith(signInInput);
+
+ expect(mockSignIn).not.toHaveBeenCalled();
+ });
+
+ it('should call the correct API with default authFlowType', () => {
+ const signInInput: SignInInput = {
+ username: user1.username,
+ };
+
+ autoSignInUserConfirmed(signInInput)();
+
+ expect(mockSignInWithUserAuth).not.toHaveBeenCalled();
+
+ expect(mockSignIn).toHaveBeenCalledTimes(1);
+ expect(mockSignIn).toHaveBeenCalledWith(signInInput);
+ });
+});
diff --git a/packages/auth/src/providers/cognito/apis/signIn.ts b/packages/auth/src/providers/cognito/apis/signIn.ts
index c1677f971d5..0dd23cec92d 100644
--- a/packages/auth/src/providers/cognito/apis/signIn.ts
+++ b/packages/auth/src/providers/cognito/apis/signIn.ts
@@ -8,6 +8,7 @@ import {
import { assertUserNotAuthenticated } from '../utils/signInHelpers';
import { SignInInput, SignInOutput } from '../types';
import { AuthValidationErrorCode } from '../../../errors/types/validation';
+import { autoSignInStore } from '../../../client/utils/store';
import { signInWithCustomAuth } from './signInWithCustomAuth';
import { signInWithCustomSRPAuth } from './signInWithCustomSRPAuth';
@@ -27,6 +28,7 @@ import { signInWithUserAuth } from './signInWithUserAuth';
* @throws AuthTokenConfigException - Thrown when the token provider config is invalid.
*/
export async function signIn(input: SignInInput): Promise {
+ autoSignInStore.dispatch({ type: 'RESET' });
const authFlowType = input.options?.authFlowType;
await assertUserNotAuthenticated();
switch (authFlowType) {
diff --git a/packages/auth/src/providers/cognito/utils/signUpHelpers.ts b/packages/auth/src/providers/cognito/utils/signUpHelpers.ts
index 47c57c135c0..0725f9046f1 100644
--- a/packages/auth/src/providers/cognito/utils/signUpHelpers.ts
+++ b/packages/auth/src/providers/cognito/utils/signUpHelpers.ts
@@ -11,6 +11,7 @@ import { AuthError } from '../../../errors/AuthError';
import { resetAutoSignIn, setAutoSignIn } from '../apis/autoSignIn';
import { AUTO_SIGN_IN_EXCEPTION } from '../../../errors/constants';
import { autoSignInStore } from '../../../client/utils/store';
+import { signInWithUserAuth } from '../apis/signInWithUserAuth';
const MAX_AUTOSIGNIN_POLLING_MS = 3 * 60 * 1000;
@@ -123,7 +124,11 @@ async function handleAutoSignInWithCodeOrUserConfirmed(
reject: (reason?: any) => void,
) {
try {
- const output = await signIn(signInInput);
+ const output =
+ signInInput?.options?.authFlowType === 'USER_AUTH'
+ ? await signInWithUserAuth(signInInput)
+ : await signIn(signInInput);
+
resolve(output);
resetAutoSignIn();
} catch (error) {
From 58a86502ca2172e349d8e054cc1a9e8981a10ba0 Mon Sep 17 00:00:00 2001
From: James Jarvis
Date: Tue, 19 Nov 2024 13:56:45 -0800
Subject: [PATCH 18/25] feat(auth): refactor foundational APIs to not access
singleton (#17)
* enable ssr list and delete web authn credentials
* update unit tests
* add foundation tests
* revert: expose server APIs
---
.../apis/deleteWebAuthnCredential.test.ts | 29 +++++++-------
.../apis/listWebAuthnCredentials.test.ts | 39 ++++++++++---------
.../client/apis/deleteWebAuthnCredential.ts | 24 ++++++++++++
packages/auth/src/client/apis/index.ts | 2 +
.../client/apis/listWebAuthnCredentials.ts | 28 +++++++++++++
.../apis/deleteWebAuthnCredential.ts | 18 ++-------
.../apis/listWebAuthnCredentials.ts | 19 +++------
packages/auth/src/index.ts | 2 +-
8 files changed, 98 insertions(+), 63 deletions(-)
create mode 100644 packages/auth/src/client/apis/deleteWebAuthnCredential.ts
create mode 100644 packages/auth/src/client/apis/listWebAuthnCredentials.ts
diff --git a/packages/auth/__tests__/foundation/apis/deleteWebAuthnCredential.test.ts b/packages/auth/__tests__/foundation/apis/deleteWebAuthnCredential.test.ts
index a1eaaae117d..c4726e93692 100644
--- a/packages/auth/__tests__/foundation/apis/deleteWebAuthnCredential.test.ts
+++ b/packages/auth/__tests__/foundation/apis/deleteWebAuthnCredential.test.ts
@@ -1,17 +1,22 @@
-import { Amplify, fetchAuthSession } from '@aws-amplify/core';
+import { Amplify } from '@aws-amplify/core';
import { decodeJWT } from '@aws-amplify/core/internals/utils';
import { createDeleteWebAuthnCredentialClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider';
-import {
- DeleteWebAuthnCredentialInput,
- deleteWebAuthnCredential,
-} from '../../../src/';
+import { DeleteWebAuthnCredentialInput } from '../../../src';
import { setUpGetConfig } from '../../providers/cognito/testUtils/setUpGetConfig';
import { mockAccessToken } from '../../providers/cognito/testUtils/data';
+import { deleteWebAuthnCredential } from '../../../src/foundation/apis';
jest.mock('@aws-amplify/core', () => ({
...(jest.createMockFromModule('@aws-amplify/core') as object),
- Amplify: { getConfig: jest.fn(() => ({})) },
+ Amplify: {
+ getConfig: jest.fn(),
+ Auth: {
+ fetchAuthSession: jest.fn(() => ({
+ tokens: { accessToken: decodeJWT(mockAccessToken) },
+ })),
+ },
+ },
}));
jest.mock('@aws-amplify/core/internals/utils', () => ({
...jest.requireActual('@aws-amplify/core/internals/utils'),
@@ -23,8 +28,6 @@ jest.mock(
jest.mock('../../../src/providers/cognito/factories');
describe('deleteWebAuthnCredential', () => {
- const mockFetchAuthSession = jest.mocked(fetchAuthSession);
-
const mockDeleteWebAuthnCredential = jest.fn();
const mockCreateDeleteWebAuthnCredentialClient = jest.mocked(
createDeleteWebAuthnCredentialClient,
@@ -32,24 +35,18 @@ describe('deleteWebAuthnCredential', () => {
beforeAll(() => {
setUpGetConfig(Amplify);
- mockFetchAuthSession.mockResolvedValue({
- tokens: { accessToken: decodeJWT(mockAccessToken) },
- });
+
mockCreateDeleteWebAuthnCredentialClient.mockReturnValue(
mockDeleteWebAuthnCredential,
);
});
- afterEach(() => {
- mockFetchAuthSession.mockClear();
- });
-
it('should pass correct service options when deleting a credential', async () => {
const input: DeleteWebAuthnCredentialInput = {
credentialId: 'dummyId',
};
- await deleteWebAuthnCredential(input);
+ await deleteWebAuthnCredential(Amplify, input);
expect(mockDeleteWebAuthnCredential).toHaveBeenCalledWith(
{
diff --git a/packages/auth/__tests__/foundation/apis/listWebAuthnCredentials.test.ts b/packages/auth/__tests__/foundation/apis/listWebAuthnCredentials.test.ts
index 1d562904440..f0708aa06e2 100644
--- a/packages/auth/__tests__/foundation/apis/listWebAuthnCredentials.test.ts
+++ b/packages/auth/__tests__/foundation/apis/listWebAuthnCredentials.test.ts
@@ -1,18 +1,23 @@
-import { Amplify, fetchAuthSession } from '@aws-amplify/core';
+import { Amplify } from '@aws-amplify/core';
import { decodeJWT } from '@aws-amplify/core/internals/utils';
import { createListWebAuthnCredentialsClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider';
-import {
- ListWebAuthnCredentialsInput,
- listWebAuthnCredentials,
-} from '../../../src/';
+import { ListWebAuthnCredentialsInput } from '../../../src';
import { mockUserCredentials } from '../../mockData';
import { setUpGetConfig } from '../../providers/cognito/testUtils/setUpGetConfig';
import { mockAccessToken } from '../../providers/cognito/testUtils/data';
+import { listWebAuthnCredentials } from '../../../src/foundation/apis';
jest.mock('@aws-amplify/core', () => ({
...(jest.createMockFromModule('@aws-amplify/core') as object),
- Amplify: { getConfig: jest.fn(() => ({})) },
+ Amplify: {
+ getConfig: jest.fn(),
+ Auth: {
+ fetchAuthSession: jest.fn(() => ({
+ tokens: { accessToken: decodeJWT(mockAccessToken) },
+ })),
+ },
+ },
}));
jest.mock('@aws-amplify/core/internals/utils', () => ({
...jest.requireActual('@aws-amplify/core/internals/utils'),
@@ -24,8 +29,6 @@ jest.mock(
jest.mock('../../../src/providers/cognito/factories');
describe('listWebAuthnCredentials', () => {
- const mockFetchAuthSession = jest.mocked(fetchAuthSession);
-
const mockListWebAuthnCredentials = jest.fn();
const mockCreateListWebAuthnCredentialsClient = jest.mocked(
createListWebAuthnCredentialsClient,
@@ -33,9 +36,7 @@ describe('listWebAuthnCredentials', () => {
beforeAll(() => {
setUpGetConfig(Amplify);
- mockFetchAuthSession.mockResolvedValue({
- tokens: { accessToken: decodeJWT(mockAccessToken) },
- });
+
mockCreateListWebAuthnCredentialsClient.mockReturnValue(
mockListWebAuthnCredentials,
);
@@ -51,12 +52,8 @@ describe('listWebAuthnCredentials', () => {
});
});
- afterEach(() => {
- mockFetchAuthSession.mockClear();
- });
-
it('should pass correct service options when listing credentials', async () => {
- await listWebAuthnCredentials();
+ await listWebAuthnCredentials(Amplify);
expect(mockListWebAuthnCredentials).toHaveBeenCalledWith(
{
@@ -74,7 +71,10 @@ describe('listWebAuthnCredentials', () => {
pageSize: 3,
};
- const { credentials, nextToken } = await listWebAuthnCredentials(input);
+ const { credentials, nextToken } = await listWebAuthnCredentials(
+ Amplify,
+ input,
+ );
expect(mockListWebAuthnCredentials).toHaveBeenCalledWith(
{
@@ -116,7 +116,10 @@ describe('listWebAuthnCredentials', () => {
nextToken: 'exampleToken',
};
- const { credentials, nextToken } = await listWebAuthnCredentials(input);
+ const { credentials, nextToken } = await listWebAuthnCredentials(
+ Amplify,
+ input,
+ );
expect(mockListWebAuthnCredentials).toHaveBeenCalledWith(
{
diff --git a/packages/auth/src/client/apis/deleteWebAuthnCredential.ts b/packages/auth/src/client/apis/deleteWebAuthnCredential.ts
new file mode 100644
index 00000000000..5e17d71fe38
--- /dev/null
+++ b/packages/auth/src/client/apis/deleteWebAuthnCredential.ts
@@ -0,0 +1,24 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { Amplify } from '@aws-amplify/core';
+
+import { DeleteWebAuthnCredentialException } from '../../foundation/factories/serviceClients/cognitoIdentityProvider/types';
+import { DeleteWebAuthnCredentialInput } from '../../foundation/types';
+import { AuthError } from '../../errors/AuthError';
+import { deleteWebAuthnCredential as deleteWebAuthnCredentialFoundation } from '../../foundation/apis';
+
+/**
+ * Delete a registered credential for an authenticated user by credentialId
+ * @param {DeleteWebAuthnCredentialInput} input The delete input parameters including the credentialId
+ * @returns Promise
+ * @throws - {@link AuthError}:
+ * - Thrown when user is unauthenticated
+ * @throws - {@link DeleteWebAuthnCredentialException}
+ * - Thrown due to a service error when deleting a WebAuthn credential
+ */
+export async function deleteWebAuthnCredential(
+ input: DeleteWebAuthnCredentialInput,
+): Promise {
+ return deleteWebAuthnCredentialFoundation(Amplify, input);
+}
diff --git a/packages/auth/src/client/apis/index.ts b/packages/auth/src/client/apis/index.ts
index 0bc604f0beb..dd3d1acb548 100644
--- a/packages/auth/src/client/apis/index.ts
+++ b/packages/auth/src/client/apis/index.ts
@@ -2,3 +2,5 @@
// SPDX-License-Identifier: Apache-2.0
export { associateWebAuthnCredential } from './associateWebAuthnCredential';
+export { listWebAuthnCredentials } from './listWebAuthnCredentials';
+export { deleteWebAuthnCredential } from './deleteWebAuthnCredential';
diff --git a/packages/auth/src/client/apis/listWebAuthnCredentials.ts b/packages/auth/src/client/apis/listWebAuthnCredentials.ts
new file mode 100644
index 00000000000..91ee2b2310f
--- /dev/null
+++ b/packages/auth/src/client/apis/listWebAuthnCredentials.ts
@@ -0,0 +1,28 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { Amplify } from '@aws-amplify/core';
+
+import { ListWebAuthnCredentialsException } from '../../foundation/factories/serviceClients/cognitoIdentityProvider/types';
+import {
+ ListWebAuthnCredentialsInput,
+ ListWebAuthnCredentialsOutput,
+} from '../../foundation/types';
+import { AuthError } from '../../errors/AuthError';
+import { listWebAuthnCredentials as listWebAuthnCredentialsFoundation } from '../../foundation/apis';
+
+/**
+ * Lists registered credentials for an authenticated user
+ *
+ * @param {ListWebAuthnCredentialsInput} input The list input parameters including page size and next token.
+ * @returns Promise
+ * @throws - {@link AuthError}:
+ * - Thrown when user is unauthenticated
+ * @throws - {@link ListWebAuthnCredentialsException}
+ * - Thrown due to a service error when listing WebAuthn credentials
+ */
+export async function listWebAuthnCredentials(
+ input?: ListWebAuthnCredentialsInput,
+): Promise {
+ return listWebAuthnCredentialsFoundation(Amplify, input);
+}
diff --git a/packages/auth/src/foundation/apis/deleteWebAuthnCredential.ts b/packages/auth/src/foundation/apis/deleteWebAuthnCredential.ts
index 872422813eb..c47b13ea303 100644
--- a/packages/auth/src/foundation/apis/deleteWebAuthnCredential.ts
+++ b/packages/auth/src/foundation/apis/deleteWebAuthnCredential.ts
@@ -1,37 +1,27 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import { Amplify, fetchAuthSession } from '@aws-amplify/core';
+import { AmplifyClassV6 } from '@aws-amplify/core';
import {
AuthAction,
assertTokenProviderConfig,
} from '@aws-amplify/core/internals/utils';
-import { DeleteWebAuthnCredentialException } from '../factories/serviceClients/cognitoIdentityProvider/types';
import { assertAuthTokens } from '../../providers/cognito/utils/types';
import { createCognitoUserPoolEndpointResolver } from '../../providers/cognito/factories';
import { getRegionFromUserPoolId } from '../parsers';
import { getAuthUserAgentValue } from '../../utils';
import { createDeleteWebAuthnCredentialClient } from '../factories/serviceClients/cognitoIdentityProvider';
import { DeleteWebAuthnCredentialInput } from '../types';
-import { AuthError } from '../../errors/AuthError';
-/**
- * Delete a registered credential for an authenticated user by credentialId
- *
- * @returns Promise
- * @throws - {@link AuthError}:
- * - Thrown when user is unauthenticated
- * @throws - {@link DeleteWebAuthnCredentialException}
- * - Thrown due to a service error when deleting a WebAuthn credential
- */
export async function deleteWebAuthnCredential(
+ amplify: AmplifyClassV6,
input: DeleteWebAuthnCredentialInput,
): Promise {
- const authConfig = Amplify.getConfig().Auth?.Cognito;
+ const authConfig = amplify.getConfig().Auth?.Cognito;
assertTokenProviderConfig(authConfig);
const { userPoolEndpoint, userPoolId } = authConfig;
- const { tokens } = await fetchAuthSession();
+ const { tokens } = await amplify.Auth.fetchAuthSession();
assertAuthTokens(tokens);
const deleteWebAuthnCredentialResult = createDeleteWebAuthnCredentialClient({
diff --git a/packages/auth/src/foundation/apis/listWebAuthnCredentials.ts b/packages/auth/src/foundation/apis/listWebAuthnCredentials.ts
index 16ffed9c152..5016833bdc6 100644
--- a/packages/auth/src/foundation/apis/listWebAuthnCredentials.ts
+++ b/packages/auth/src/foundation/apis/listWebAuthnCredentials.ts
@@ -1,13 +1,12 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import { Amplify, fetchAuthSession } from '@aws-amplify/core';
+import { AmplifyClassV6 } from '@aws-amplify/core';
import {
AuthAction,
assertTokenProviderConfig,
} from '@aws-amplify/core/internals/utils';
-import { ListWebAuthnCredentialsException } from '../factories/serviceClients/cognitoIdentityProvider/types';
import { assertAuthTokens } from '../../providers/cognito/utils/types';
import { createCognitoUserPoolEndpointResolver } from '../../providers/cognito/factories';
import { getRegionFromUserPoolId } from '../parsers';
@@ -18,24 +17,16 @@ import {
ListWebAuthnCredentialsInput,
ListWebAuthnCredentialsOutput,
} from '../types';
-import { AuthError } from '../../errors/AuthError';
-/**
- * Lists registered credentials for an authenticated user
- *
- * @returns Promise
- * @throws - {@link AuthError}:
- * - Thrown when user is unauthenticated
- * @throws - {@link ListWebAuthnCredentialsException}
- * - Thrown due to a service error when listing WebAuthn credentials
- */
export async function listWebAuthnCredentials(
+ amplify: AmplifyClassV6,
input?: ListWebAuthnCredentialsInput,
): Promise {
- const authConfig = Amplify.getConfig().Auth?.Cognito;
+ const authConfig = amplify.getConfig().Auth?.Cognito;
assertTokenProviderConfig(authConfig);
const { userPoolEndpoint, userPoolId } = authConfig;
- const { tokens } = await fetchAuthSession();
+
+ const { tokens } = await amplify.Auth.fetchAuthSession();
assertAuthTokens(tokens);
const listWebAuthnCredentialsResult = createListWebAuthnCredentialsClient({
diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts
index 886bd2ff2d7..0ca9948aa9b 100644
--- a/packages/auth/src/index.ts
+++ b/packages/auth/src/index.ts
@@ -93,7 +93,7 @@ export { associateWebAuthnCredential } from './client/apis';
export {
listWebAuthnCredentials,
deleteWebAuthnCredential,
-} from './foundation/apis';
+} from './client/apis';
export {
AuthWebAuthnCredential,
From 34c57433659914c1cd84bf58835bbbac43020ab5 Mon Sep 17 00:00:00 2001
From: James Jarvis
Date: Wed, 20 Nov 2024 15:39:04 -0800
Subject: [PATCH 19/25] feat(auth): passwordless - enable test specs / push
trigger (#19)
* enable test specs / push trigger
* check for PublicKeyCredential
* bundle size tests
* fix recovery suggestion language
* align assertion with expected type
* fix tsdocs
---
.github/integ-config/integ-all.yml | 56 +++++++++++++++++++
.github/workflows/push-integ-test.yml | 2 +-
.../apis/associateWebAuthnCredential.ts | 2 +-
.../auth/src/client/utils/passkey/errors.ts | 2 +-
.../utils/passkey/getIsPasskeySupported.ts | 7 ++-
.../src/client/utils/passkey/types/shared.ts | 1 +
packages/aws-amplify/package.json | 2 +-
7 files changed, 67 insertions(+), 5 deletions(-)
diff --git a/.github/integ-config/integ-all.yml b/.github/integ-config/integ-all.yml
index 94a2d85a157..36ad0a20235 100644
--- a/.github/integ-config/integ-all.yml
+++ b/.github/integ-config/integ-all.yml
@@ -915,3 +915,59 @@ tests:
browser: [chrome]
env:
NEXT_PUBLIC_BACKEND_CONFIG: mfa-setup
+ - test_name: integ_next_passwordless_auto_sign_in
+ desc: 'passwordless auto sign in with session'
+ framework: next
+ category: auth
+ sample_name: [mfa]
+ spec: passwordless/auto-sign-in
+ browser: *minimal_browser_list
+ env:
+ NEXT_PUBLIC_BACKEND_CONFIG: pwl-autosignin
+ - test_name: integ_next_passwordless_first_factor_selection
+ desc: 'passwordless sign in with first factor selection'
+ framework: next
+ category: auth
+ sample_name: [mfa]
+ spec: passwordless/first-factor-selection
+ browser: *minimal_browser_list
+ env:
+ NEXT_PUBLIC_BACKEND_CONFIG: pwl-ffselect
+ - test_name: integ_next_passwordless_preferred_challenge
+ desc: 'passwordless sign in with preferred challenge'
+ framework: next
+ category: auth
+ sample_name: [mfa]
+ spec: passwordless/preferred-challenge
+ browser: *minimal_browser_list
+ env:
+ NEXT_PUBLIC_BACKEND_CONFIG: pwl-prefchal
+ - test_name: integ_next_passwordless_sign_up
+ desc: 'passwordless sign up'
+ framework: next
+ category: auth
+ sample_name: [mfa]
+ spec: passwordless/sign-up
+ browser: *minimal_browser_list
+ env:
+ NEXT_PUBLIC_BACKEND_CONFIG: pwl-signup
+ - test_name: integ_next_passwordless_misc
+ desc: 'passwordless miscellaneous flows'
+ framework: next
+ category: auth
+ sample_name: [mfa]
+ spec: passwordless/miscellaneous
+ browser: *minimal_browser_list
+ env:
+ NEXT_PUBLIC_BACKEND_CONFIG: pwl-misc
+ - test_name: integ_next_passwordless_webauthn
+ desc: 'passwordless webauthn sign in and lifecycle management'
+ framework: next
+ category: auth
+ sample_name: [mfa]
+ spec: passwordless/webauthn
+ # chrome only
+ # https://chromedevtools.github.io/devtools-protocol/tot/WebAuthn/
+ browser: [chrome]
+ env:
+ NEXT_PUBLIC_BACKEND_CONFIG: pwl-webauthn
diff --git a/.github/workflows/push-integ-test.yml b/.github/workflows/push-integ-test.yml
index 03e43dd2865..680aa6b7d06 100644
--- a/.github/workflows/push-integ-test.yml
+++ b/.github/workflows/push-integ-test.yml
@@ -8,7 +8,7 @@ concurrency:
on:
push:
branches:
- - replace-with-your-branch
+ - feat/passwordless
jobs:
e2e:
diff --git a/packages/auth/src/client/apis/associateWebAuthnCredential.ts b/packages/auth/src/client/apis/associateWebAuthnCredential.ts
index d95dca05651..caf8307f447 100644
--- a/packages/auth/src/client/apis/associateWebAuthnCredential.ts
+++ b/packages/auth/src/client/apis/associateWebAuthnCredential.ts
@@ -27,7 +27,7 @@ import { assertValidCredentialCreationOptions } from '../utils/passkey/types';
/**
* Registers a new passkey for an authenticated user
*
- * @returns Promise
+ * @returns Promise
* @throws - {@link PasskeyError}:
* - Thrown when intermediate state is invalid
* @throws - {@link AuthError}:
diff --git a/packages/auth/src/client/utils/passkey/errors.ts b/packages/auth/src/client/utils/passkey/errors.ts
index 2b91c3fdb28..288cb14e810 100644
--- a/packages/auth/src/client/utils/passkey/errors.ts
+++ b/packages/auth/src/client/utils/passkey/errors.ts
@@ -44,7 +44,7 @@ const notSupportedRecoverySuggestion =
const abortOrCancelRecoverySuggestion =
'User may have canceled the ceremony or another interruption has occurred. Check underlying error for details.';
const misconfigurationRecoverySuggestion =
- 'Ensure your user pool configured to support the WEB_AUTHN as an authentication factor.';
+ 'Ensure your user pool is configured to support the WEB_AUTHN as an authentication factor.';
const passkeyErrorMap: AmplifyErrorMap = {
[PasskeyErrorCode.PasskeyNotSupported]: {
diff --git a/packages/auth/src/client/utils/passkey/getIsPasskeySupported.ts b/packages/auth/src/client/utils/passkey/getIsPasskeySupported.ts
index 1934d7f86f2..c0d3674a8a4 100644
--- a/packages/auth/src/client/utils/passkey/getIsPasskeySupported.ts
+++ b/packages/auth/src/client/utils/passkey/getIsPasskeySupported.ts
@@ -9,5 +9,10 @@ import { isBrowser } from '@aws-amplify/core/internals/utils';
* @returns boolean
*/
export const getIsPasskeySupported = (): boolean => {
- return isBrowser() && window.isSecureContext && 'credentials' in navigator;
+ return (
+ isBrowser() &&
+ window.isSecureContext &&
+ 'credentials' in navigator &&
+ typeof window.PublicKeyCredential === 'function'
+ );
};
diff --git a/packages/auth/src/client/utils/passkey/types/shared.ts b/packages/auth/src/client/utils/passkey/types/shared.ts
index f684b08e9a6..847118d7e25 100644
--- a/packages/auth/src/client/utils/passkey/types/shared.ts
+++ b/packages/auth/src/client/utils/passkey/types/shared.ts
@@ -84,6 +84,7 @@ export function assertValidCredentialCreationOptions(
assertPasskeyError(
[
!!credentialCreationOptions,
+ !!credentialCreationOptions?.challenge,
!!credentialCreationOptions?.user,
!!credentialCreationOptions?.rp,
!!credentialCreationOptions?.pubKeyCredParams,
diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json
index 8cf65e99f59..5e06af1eecb 100644
--- a/packages/aws-amplify/package.json
+++ b/packages/aws-amplify/package.json
@@ -449,7 +449,7 @@
"name": "[Auth] Basic Auth Flow (Cognito)",
"path": "./dist/esm/auth/index.mjs",
"import": "{ signIn, signOut, fetchAuthSession, confirmSignIn }",
- "limit": "30.34 kB"
+ "limit": "30.35 kB"
},
{
"name": "[Auth] OAuth Auth Flow (Cognito)",
From 6fadcdfcfc765af168d9c7aef13085fd71f26558 Mon Sep 17 00:00:00 2001
From: James Jarvis
Date: Wed, 20 Nov 2024 16:02:44 -0800
Subject: [PATCH 20/25] bundle size updates
---
packages/aws-amplify/package.json | 36 +++++++++++++++----------------
1 file changed, 18 insertions(+), 18 deletions(-)
diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json
index 8b45df6c12e..44303b6c0e8 100644
--- a/packages/aws-amplify/package.json
+++ b/packages/aws-amplify/package.json
@@ -293,7 +293,7 @@
"name": "[Analytics] record (Pinpoint)",
"path": "./dist/esm/analytics/index.mjs",
"import": "{ record }",
- "limit": "17.59 kB"
+ "limit": "17.60 kB"
},
{
"name": "[Analytics] record (Kinesis)",
@@ -317,7 +317,7 @@
"name": "[Analytics] identifyUser (Pinpoint)",
"path": "./dist/esm/analytics/index.mjs",
"import": "{ identifyUser }",
- "limit": "16.09 kB"
+ "limit": "16.10 kB"
},
{
"name": "[Analytics] enable",
@@ -335,7 +335,7 @@
"name": "[API] generateClient (AppSync)",
"path": "./dist/esm/api/index.mjs",
"import": "{ generateClient }",
- "limit": "44.21 kB"
+ "limit": "44.23 kB"
},
{
"name": "[API] REST API handlers",
@@ -353,13 +353,13 @@
"name": "[Auth] resetPassword (Cognito)",
"path": "./dist/esm/auth/index.mjs",
"import": "{ resetPassword }",
- "limit": "12.66 kB"
+ "limit": "12.68 kB"
},
{
"name": "[Auth] confirmResetPassword (Cognito)",
"path": "./dist/esm/auth/index.mjs",
"import": "{ confirmResetPassword }",
- "limit": "12.60 kB"
+ "limit": "12.63 kB"
},
{
"name": "[Auth] signIn (Cognito)",
@@ -371,7 +371,7 @@
"name": "[Auth] resendSignUpCode (Cognito)",
"path": "./dist/esm/auth/index.mjs",
"import": "{ resendSignUpCode }",
- "limit": "12.61 kB"
+ "limit": "12.64 kB"
},
{
"name": "[Auth] confirmSignUp (Cognito)",
@@ -389,25 +389,25 @@
"name": "[Auth] updateMFAPreference (Cognito)",
"path": "./dist/esm/auth/index.mjs",
"import": "{ updateMFAPreference }",
- "limit": "12.07 kB"
+ "limit": "12.11 kB"
},
{
"name": "[Auth] fetchMFAPreference (Cognito)",
"path": "./dist/esm/auth/index.mjs",
"import": "{ fetchMFAPreference }",
- "limit": "12.1 kB"
+ "limit": "12.14 kB"
},
{
"name": "[Auth] verifyTOTPSetup (Cognito)",
"path": "./dist/esm/auth/index.mjs",
"import": "{ verifyTOTPSetup }",
- "limit": "12.94 kB"
+ "limit": "12.99 kB"
},
{
"name": "[Auth] updatePassword (Cognito)",
"path": "./dist/esm/auth/index.mjs",
"import": "{ updatePassword }",
- "limit": "12.96 kB"
+ "limit": "12.99 kB"
},
{
"name": "[Auth] setUpTOTP (Cognito)",
@@ -419,7 +419,7 @@
"name": "[Auth] updateUserAttributes (Cognito)",
"path": "./dist/esm/auth/index.mjs",
"import": "{ updateUserAttributes }",
- "limit": "12.19 kB"
+ "limit": "12.21 kB"
},
{
"name": "[Auth] getCurrentUser (Cognito)",
@@ -431,7 +431,7 @@
"name": "[Auth] confirmUserAttribute (Cognito)",
"path": "./dist/esm/auth/index.mjs",
"import": "{ confirmUserAttribute }",
- "limit": "12.93 kB"
+ "limit": "12.98 kB"
},
{
"name": "[Auth] signInWithRedirect (Cognito)",
@@ -443,13 +443,13 @@
"name": "[Auth] fetchUserAttributes (Cognito)",
"path": "./dist/esm/auth/index.mjs",
"import": "{ fetchUserAttributes }",
- "limit": "12.01 kB"
+ "limit": "12.03 kB"
},
{
"name": "[Auth] Basic Auth Flow (Cognito)",
"path": "./dist/esm/auth/index.mjs",
"import": "{ signIn, signOut, fetchAuthSession, confirmSignIn }",
- "limit": "30.35 kB"
+ "limit": "30.56 kB"
},
{
"name": "[Auth] OAuth Auth Flow (Cognito)",
@@ -461,19 +461,19 @@
"name": "[Auth] Associate WebAuthN Credential (Cognito)",
"path": "./dist/esm/auth/index.mjs",
"import": "{ associateWebAuthnCredential }",
- "limit": "13.50 kB"
+ "limit": "13.55 kB"
},
{
"name": "[Auth] List WebAuthN Credentials (Cognito)",
"path": "./dist/esm/auth/index.mjs",
"import": "{ listWebAuthnCredentials }",
- "limit": "12.10 kB"
+ "limit": "12.14 kB"
},
{
"name": "[Auth] Delete WebAuthN Credential (Cognito)",
"path": "./dist/esm/auth/index.mjs",
"import": "{ deleteWebAuthnCredential }",
- "limit": "11.95 kB"
+ "limit": "12.01 kB"
},
{
"name": "[Storage] copy (S3)",
@@ -503,7 +503,7 @@
"name": "[Storage] list (S3)",
"path": "./dist/esm/storage/index.mjs",
"import": "{ list }",
- "limit": "16.69 kB"
+ "limit": "16.74 kB"
},
{
"name": "[Storage] remove (S3)",
From b25316677f4c6a6bb6f0b0eade4c75c5709be006 Mon Sep 17 00:00:00 2001
From: James Jarvis
Date: Fri, 22 Nov 2024 10:16:14 -0800
Subject: [PATCH 21/25] fix(auth): passwordless pr feedback (#22)
* callout in ts docs for password requirement
* unify callback and store reset for autosignin
* comment for clarity
---
.../auth/__tests__/providers/cognito/autoSignIn.test.ts | 2 --
packages/auth/src/providers/cognito/apis/autoSignIn.ts | 8 ++++++--
.../auth/src/providers/cognito/apis/confirmSignUp.ts | 1 -
packages/auth/src/providers/cognito/apis/signIn.ts | 9 +++++++--
.../auth/src/providers/cognito/apis/signInWithSRP.ts | 6 ++----
.../src/providers/cognito/apis/signInWithUserAuth.ts | 5 ++---
.../src/providers/cognito/apis/signInWithUserPassword.ts | 8 +++-----
.../auth/src/providers/cognito/utils/signUpHelpers.ts | 5 -----
packages/auth/src/types/inputs.ts | 2 +-
9 files changed, 21 insertions(+), 25 deletions(-)
diff --git a/packages/auth/__tests__/providers/cognito/autoSignIn.test.ts b/packages/auth/__tests__/providers/cognito/autoSignIn.test.ts
index 19a163dff5f..05389b40773 100644
--- a/packages/auth/__tests__/providers/cognito/autoSignIn.test.ts
+++ b/packages/auth/__tests__/providers/cognito/autoSignIn.test.ts
@@ -84,7 +84,6 @@ describe('autoSignIn()', () => {
mockCreateSignUpClient.mockClear();
handleUserSRPAuthFlowSpy.mockClear();
- autoSignInStore.dispatch({ type: 'RESET' });
resetAutoSignIn();
});
@@ -164,7 +163,6 @@ describe('autoSignIn()', () => {
mockHandleUserAuthFlow.mockClear();
mockCreateConfirmSignUpClient.mockClear();
- autoSignInStore.dispatch({ type: 'RESET' });
resetAutoSignIn();
});
diff --git a/packages/auth/src/providers/cognito/apis/autoSignIn.ts b/packages/auth/src/providers/cognito/apis/autoSignIn.ts
index d10b4a8c820..6186ac159c9 100644
--- a/packages/auth/src/providers/cognito/apis/autoSignIn.ts
+++ b/packages/auth/src/providers/cognito/apis/autoSignIn.ts
@@ -1,6 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
+import { autoSignInStore } from '../../../client/utils/store';
import { AuthError } from '../../../errors/AuthError';
import { AUTO_SIGN_IN_EXCEPTION } from '../../../errors/constants';
import { AutoSignInCallback } from '../../../types/models';
@@ -114,6 +115,9 @@ export function setAutoSignIn(callback: AutoSignInCallback) {
*
* @internal
*/
-export function resetAutoSignIn() {
- autoSignIn = initialAutoSignIn;
+export function resetAutoSignIn(resetCallback = true) {
+ if (resetCallback) {
+ autoSignIn = initialAutoSignIn;
+ }
+ autoSignInStore.dispatch({ type: 'RESET' });
}
diff --git a/packages/auth/src/providers/cognito/apis/confirmSignUp.ts b/packages/auth/src/providers/cognito/apis/confirmSignUp.ts
index 41cb1f7a141..c9633531908 100644
--- a/packages/auth/src/providers/cognito/apis/confirmSignUp.ts
+++ b/packages/auth/src/providers/cognito/apis/confirmSignUp.ts
@@ -93,7 +93,6 @@ export async function confirmSignUp(
autoSignInStoreState.username !== username
) {
resolve(signUpOut);
- autoSignInStore.dispatch({ type: 'RESET' });
resetAutoSignIn();
return;
diff --git a/packages/auth/src/providers/cognito/apis/signIn.ts b/packages/auth/src/providers/cognito/apis/signIn.ts
index 0dd23cec92d..7fc23cfcc67 100644
--- a/packages/auth/src/providers/cognito/apis/signIn.ts
+++ b/packages/auth/src/providers/cognito/apis/signIn.ts
@@ -8,13 +8,13 @@ import {
import { assertUserNotAuthenticated } from '../utils/signInHelpers';
import { SignInInput, SignInOutput } from '../types';
import { AuthValidationErrorCode } from '../../../errors/types/validation';
-import { autoSignInStore } from '../../../client/utils/store';
import { signInWithCustomAuth } from './signInWithCustomAuth';
import { signInWithCustomSRPAuth } from './signInWithCustomSRPAuth';
import { signInWithSRP } from './signInWithSRP';
import { signInWithUserPassword } from './signInWithUserPassword';
import { signInWithUserAuth } from './signInWithUserAuth';
+import { resetAutoSignIn } from './autoSignIn';
/**
* Signs a user in
@@ -28,7 +28,12 @@ import { signInWithUserAuth } from './signInWithUserAuth';
* @throws AuthTokenConfigException - Thrown when the token provider config is invalid.
*/
export async function signIn(input: SignInInput): Promise {
- autoSignInStore.dispatch({ type: 'RESET' });
+ // Here we want to reset the store but not reassign the callback.
+ // The callback is reset when the underlying promise resolves or rejects.
+ // With the advent of session based sign in, this guarantees that the signIn API initiates a new auth flow,
+ // regardless of whether it is called for a user currently engaged in an active auto sign in session.
+ resetAutoSignIn(false);
+
const authFlowType = input.options?.authFlowType;
await assertUserNotAuthenticated();
switch (authFlowType) {
diff --git a/packages/auth/src/providers/cognito/apis/signInWithSRP.ts b/packages/auth/src/providers/cognito/apis/signInWithSRP.ts
index 43ecc94ed6a..4cff40e7cd7 100644
--- a/packages/auth/src/providers/cognito/apis/signInWithSRP.ts
+++ b/packages/auth/src/providers/cognito/apis/signInWithSRP.ts
@@ -28,7 +28,6 @@ import {
SignInWithSRPOutput,
} from '../types';
import {
- autoSignInStore,
cleanActiveSignInState,
setActiveSignInState,
} from '../../../client/utils/store';
@@ -93,8 +92,6 @@ export async function signInWithSRP(
});
if (AuthenticationResult) {
cleanActiveSignInState();
- autoSignInStore.dispatch({ type: 'RESET' });
- resetAutoSignIn();
await cacheCognitoTokens({
username: activeUsername,
...AuthenticationResult,
@@ -109,6 +106,8 @@ export async function signInWithSRP(
await dispatchSignedInHubEvent();
+ resetAutoSignIn();
+
return {
isSignedIn: true,
nextStep: { signInStep: 'DONE' },
@@ -121,7 +120,6 @@ export async function signInWithSRP(
});
} catch (error) {
cleanActiveSignInState();
- autoSignInStore.dispatch({ type: 'RESET' });
resetAutoSignIn();
assertServiceError(error);
const result = getSignInResultFromError(error.name);
diff --git a/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts b/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts
index 9eb731fc593..9ac1223a105 100644
--- a/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts
+++ b/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts
@@ -101,8 +101,6 @@ export async function signInWithUserAuth(
if (response.AuthenticationResult) {
cleanActiveSignInState();
- autoSignInStore.dispatch({ type: 'RESET' });
- resetAutoSignIn();
await cacheCognitoTokens({
username: activeUsername,
...response.AuthenticationResult,
@@ -116,6 +114,8 @@ export async function signInWithUserAuth(
});
await dispatchSignedInHubEvent();
+ resetAutoSignIn();
+
return {
isSignedIn: true,
nextStep: { signInStep: 'DONE' },
@@ -132,7 +132,6 @@ export async function signInWithUserAuth(
});
} catch (error) {
cleanActiveSignInState();
- autoSignInStore.dispatch({ type: 'RESET' });
resetAutoSignIn();
assertServiceError(error);
const result = getSignInResultFromError(error.name);
diff --git a/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts b/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts
index 488829179c8..0cd3acd88d3 100644
--- a/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts
+++ b/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts
@@ -26,7 +26,6 @@ import {
SignInWithUserPasswordOutput,
} from '../types';
import {
- autoSignInStore,
cleanActiveSignInState,
setActiveSignInState,
} from '../../../client/utils/store';
@@ -87,6 +86,7 @@ export async function signInWithUserPassword(
signInDetails,
});
if (AuthenticationResult) {
+ cleanActiveSignInState();
await cacheCognitoTokens({
...AuthenticationResult,
username: activeUsername,
@@ -98,12 +98,11 @@ export async function signInWithUserPassword(
}),
signInDetails,
});
- cleanActiveSignInState();
- autoSignInStore.dispatch({ type: 'RESET' });
- resetAutoSignIn();
await dispatchSignedInHubEvent();
+ resetAutoSignIn();
+
return {
isSignedIn: true,
nextStep: { signInStep: 'DONE' },
@@ -116,7 +115,6 @@ export async function signInWithUserPassword(
});
} catch (error) {
cleanActiveSignInState();
- autoSignInStore.dispatch({ type: 'RESET' });
resetAutoSignIn();
assertServiceError(error);
const result = getSignInResultFromError(error.name);
diff --git a/packages/auth/src/providers/cognito/utils/signUpHelpers.ts b/packages/auth/src/providers/cognito/utils/signUpHelpers.ts
index 0725f9046f1..9bebcf4be82 100644
--- a/packages/auth/src/providers/cognito/utils/signUpHelpers.ts
+++ b/packages/auth/src/providers/cognito/utils/signUpHelpers.ts
@@ -10,7 +10,6 @@ import { AutoSignInCallback } from '../../../types/models';
import { AuthError } from '../../../errors/AuthError';
import { resetAutoSignIn, setAutoSignIn } from '../apis/autoSignIn';
import { AUTO_SIGN_IN_EXCEPTION } from '../../../errors/constants';
-import { autoSignInStore } from '../../../client/utils/store';
import { signInWithUserAuth } from '../apis/signInWithUserAuth';
const MAX_AUTOSIGNIN_POLLING_MS = 3 * 60 * 1000;
@@ -37,7 +36,6 @@ export function handleCodeAutoSignIn(signInInput: SignInInput) {
// This will stop the listener if confirmSignUp is not resolved.
const timeOutId = setTimeout(() => {
stopHubListener();
- autoSignInStore.dispatch({ type: 'RESET' });
clearTimeout(timeOutId);
resetAutoSignIn();
}, MAX_AUTOSIGNIN_POLLING_MS);
@@ -84,20 +82,17 @@ function handleAutoSignInWithLink(
}),
);
resetAutoSignIn();
- autoSignInStore.dispatch({ type: 'RESET' });
} else {
try {
const signInOutput = await signIn(signInInput);
if (signInOutput.nextStep.signInStep !== 'CONFIRM_SIGN_UP') {
resolve(signInOutput);
clearInterval(autoSignInPollingIntervalId);
- autoSignInStore.dispatch({ type: 'RESET' });
resetAutoSignIn();
}
} catch (error) {
clearInterval(autoSignInPollingIntervalId);
reject(error);
- autoSignInStore.dispatch({ type: 'RESET' });
resetAutoSignIn();
}
}
diff --git a/packages/auth/src/types/inputs.ts b/packages/auth/src/types/inputs.ts
index a7189912cd0..c2947b4650a 100644
--- a/packages/auth/src/types/inputs.ts
+++ b/packages/auth/src/types/inputs.ts
@@ -75,7 +75,7 @@ export interface AuthSignInWithRedirectInput {
* The parameters for constructing a Sign Up input.
*
* @param username - a standard username, potentially an email/phone number
- * @param password - the user's password
+ * @param password - the user's password, may be required depending on your Cognito User Pool configuration
* @param options - optional parameters for the Sign Up process, including user attributes
*/
export interface AuthSignUpInput<
From b66249dc1be6a3dc4ee323655fd4266ca10bbb6e Mon Sep 17 00:00:00 2001
From: James Jarvis
Date: Fri, 22 Nov 2024 10:17:07 -0800
Subject: [PATCH 22/25] enable integ tests
---
.github/workflows/codeql.yml | 2 +-
.github/workflows/pr.yml | 8 ++++----
.github/workflows/push-integ-test.yml | 2 +-
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index ce532d0c795..5b5b8865166 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -5,7 +5,7 @@ on:
push:
branches: ['*']
pull_request:
- branches: ['release', 'next/main', 'next/release']
+ branches: ['main', 'release', 'next/main', 'next/release']
schedule:
# Run every Tuesday at midnight GMT
- cron: '0 0 * * 2'
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 72e0b51af1d..f7e0982cc9c 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -36,9 +36,9 @@ jobs:
tsc-compliance-test:
needs: prebuild
uses: ./.github/workflows/callable-test-tsc-compliance.yml
- # dependency-review:
- # needs: prebuild
- # uses: ./.github/workflows/callable-dependency-review.yml
+ dependency-review:
+ needs: prebuild
+ uses: ./.github/workflows/callable-dependency-review.yml
all-unit-tests-pass:
name: Unit and Bundle tests have passed
needs:
@@ -47,7 +47,7 @@ jobs:
- license-test
- github-actions-test
- tsc-compliance-test
- # - dependency-review
+ - dependency-review
runs-on: ubuntu-latest
if: success() # only run when all checks have passed
# store success output flag for ci job
diff --git a/.github/workflows/push-integ-test.yml b/.github/workflows/push-integ-test.yml
index 03e43dd2865..680aa6b7d06 100644
--- a/.github/workflows/push-integ-test.yml
+++ b/.github/workflows/push-integ-test.yml
@@ -8,7 +8,7 @@ concurrency:
on:
push:
branches:
- - replace-with-your-branch
+ - feat/passwordless
jobs:
e2e:
From 8b5320d6ccb0dcb05cd87635eaa20c5cd0ff8ad7 Mon Sep 17 00:00:00 2001
From: yuhengshs
Date: Fri, 22 Nov 2024 13:44:17 -0800
Subject: [PATCH 23/25] fix: set active username after auth attempt to maintain
consistent user context
---
.../src/client/flows/userAuth/handleUserAuthFlow.ts | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/packages/auth/src/client/flows/userAuth/handleUserAuthFlow.ts b/packages/auth/src/client/flows/userAuth/handleUserAuthFlow.ts
index 24ba20500a6..753ac66db04 100644
--- a/packages/auth/src/client/flows/userAuth/handleUserAuthFlow.ts
+++ b/packages/auth/src/client/flows/userAuth/handleUserAuthFlow.ts
@@ -18,6 +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';
export interface HandleUserAuthFlowInput {
username: string;
@@ -107,11 +108,18 @@ export async function handleUserAuthFlow({
}),
});
- return initiateAuth(
+ const response = await initiateAuth(
{
region: getRegionFromUserPoolId(userPoolId),
userAgentValue: getAuthUserAgentValue(AuthAction.SignIn),
},
jsonReq,
);
+
+ // Set the active username immediately after successful authentication attempt
+ // If a user starts a new sign-in while another sign-in is incomplete,
+ // this ensures we're tracking the correct user for subsequent auth challenges.
+ setActiveSignInUsername(username);
+
+ return response;
}
From 385168661430c27d1ac6cfc06518f995ec8907eb Mon Sep 17 00:00:00 2001
From: James Jarvis
Date: Fri, 22 Nov 2024 13:47:24 -0800
Subject: [PATCH 24/25] temporarily run single test spec per environment
---
.github/integ-config/integ-all.yml | 15 ++++++++++-----
1 file changed, 10 insertions(+), 5 deletions(-)
diff --git a/.github/integ-config/integ-all.yml b/.github/integ-config/integ-all.yml
index 00334c347e5..3c37683608b 100644
--- a/.github/integ-config/integ-all.yml
+++ b/.github/integ-config/integ-all.yml
@@ -935,7 +935,8 @@ tests:
category: auth
sample_name: [mfa]
spec: passwordless/auto-sign-in
- browser: *minimal_browser_list
+ # browser: *minimal_browser_list
+ browser: [chrome]
env:
NEXT_PUBLIC_BACKEND_CONFIG: pwl-autosignin
- test_name: integ_next_passwordless_first_factor_selection
@@ -944,7 +945,8 @@ tests:
category: auth
sample_name: [mfa]
spec: passwordless/first-factor-selection
- browser: *minimal_browser_list
+ # browser: *minimal_browser_list
+ browser: [chrome]
env:
NEXT_PUBLIC_BACKEND_CONFIG: pwl-ffselect
- test_name: integ_next_passwordless_preferred_challenge
@@ -953,7 +955,8 @@ tests:
category: auth
sample_name: [mfa]
spec: passwordless/preferred-challenge
- browser: *minimal_browser_list
+ # browser: *minimal_browser_list
+ browser: [chrome]
env:
NEXT_PUBLIC_BACKEND_CONFIG: pwl-prefchal
- test_name: integ_next_passwordless_sign_up
@@ -962,7 +965,8 @@ tests:
category: auth
sample_name: [mfa]
spec: passwordless/sign-up
- browser: *minimal_browser_list
+ # browser: *minimal_browser_list
+ browser: [chrome]
env:
NEXT_PUBLIC_BACKEND_CONFIG: pwl-signup
- test_name: integ_next_passwordless_misc
@@ -971,7 +975,8 @@ tests:
category: auth
sample_name: [mfa]
spec: passwordless/miscellaneous
- browser: *minimal_browser_list
+ # browser: *minimal_browser_list
+ browser: [chrome]
env:
NEXT_PUBLIC_BACKEND_CONFIG: pwl-misc
- test_name: integ_next_passwordless_webauthn
From d2d3fc56619d61bc987cfb2d62c4ab3f9521450d Mon Sep 17 00:00:00 2001
From: James Jarvis
Date: Fri, 22 Nov 2024 16:45:00 -0800
Subject: [PATCH 25/25] reset push integ yml
---
.github/workflows/push-integ-test.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/push-integ-test.yml b/.github/workflows/push-integ-test.yml
index 680aa6b7d06..03e43dd2865 100644
--- a/.github/workflows/push-integ-test.yml
+++ b/.github/workflows/push-integ-test.yml
@@ -8,7 +8,7 @@ concurrency:
on:
push:
branches:
- - feat/passwordless
+ - replace-with-your-branch
jobs:
e2e: