Skip to content

Commit

Permalink
[Email MFA] Add support for EMAIL_OTP during sign in flows (#13745)
Browse files Browse the repository at this point in the history
* Confirm Sign In With Email OTP

* Confirm Sign In Tests With Email OTP

* Update packages/auth/src/types/models.ts

Co-authored-by: israx <70438514+israx@users.noreply.github.com>

* Fix Errant Pascal Casing

---------

Co-authored-by: israx <70438514+israx@users.noreply.github.com>
  • Loading branch information
jjarvisp and israx authored Aug 27, 2024
1 parent 4ed4918 commit df6cba9
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('confirmSignIn API error path cases:', () => {
}
});

it('should throw an error when sign-in step is CONTINUE_SIGN_IN_WITH_MFA_SELECTION and challengeResponse is not "SMS" or "TOTP"', async () => {
it('should throw an error when sign-in step is CONTINUE_SIGN_IN_WITH_MFA_SELECTION and challengeResponse is not "SMS", "TOTP", or "EMAIL"', async () => {
expect.assertions(2);
try {
await confirmSignIn({ challengeResponse: 'NO_SMS' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,55 @@ describe('confirmSignIn API happy path cases', () => {
mockedGetCurrentUser.mockClear();
});

test(`confirmSignIn with EMAIL_OTP ChallengeName`, async () => {
Amplify.configure({
Auth: authConfig,
});

const handleUserSRPAuthflowSpy = jest
.spyOn(signInHelpers, 'handleUserSRPAuthFlow')
.mockImplementationOnce(
async (): Promise<RespondToAuthChallengeCommandOutput> => ({
ChallengeName: 'EMAIL_OTP',
Session: '1234234232',
$metadata: {},
ChallengeParameters: {
CODE_DELIVERY_DELIVERY_MEDIUM: 'EMAIL',
CODE_DELIVERY_DESTINATION: 'j***@a***',
},
}),
);

const signInResult = await signIn({ username, password });

expect(signInResult).toEqual({
isSignedIn: false,
nextStep: {
signInStep: 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE',
codeDeliveryDetails: {
deliveryMedium: 'EMAIL',
destination: 'j***@a***',
},
},
});

const confirmSignInResult = await confirmSignIn({
challengeResponse: '123456',
});

expect(confirmSignInResult).toEqual({
isSignedIn: true,
nextStep: {
signInStep: 'DONE',
},
});

expect(handleChallengeNameSpy).toHaveBeenCalledTimes(1);
expect(handleUserSRPAuthflowSpy).toHaveBeenCalledTimes(1);

handleUserSRPAuthflowSpy.mockClear();
});

test(`confirmSignIn tests MFA_SETUP challengeName`, async () => {
Amplify.configure({
Auth: authConfig,
Expand Down Expand Up @@ -162,7 +211,7 @@ describe('confirmSignIn API happy path cases', () => {
handleUserSRPAuthflowSpy.mockClear();
});

test(`confirmSignIn tests SELECT_MFA_TYPE challengeName `, async () => {
test(`confirmSignIn with SELECT_MFA_TYPE challengeName and SMS response`, async () => {
Amplify.configure({
Auth: authConfig,
});
Expand All @@ -175,7 +224,7 @@ describe('confirmSignIn API happy path cases', () => {
Session: '1234234232',
$metadata: {},
ChallengeParameters: {
MFAS_CAN_CHOOSE: '["SMS_MFA","SOFTWARE_TOKEN_MFA"]',
MFAS_CAN_CHOOSE: '["SMS_MFA","SOFTWARE_TOKEN_MFA", "EMAIL_OTP"]',
},
}),
);
Expand Down Expand Up @@ -204,7 +253,7 @@ describe('confirmSignIn API happy path cases', () => {
isSignedIn: false,
nextStep: {
signInStep: 'CONTINUE_SIGN_IN_WITH_MFA_SELECTION',
allowedMFATypes: ['SMS', 'TOTP'],
allowedMFATypes: ['SMS', 'TOTP', 'EMAIL'],
},
});

Expand All @@ -226,6 +275,121 @@ describe('confirmSignIn API happy path cases', () => {
handleUserSRPAuthflowSpy.mockClear();
});

test(`confirmSignIn with SELECT_MFA_TYPE challengeName and TOTP response`, async () => {
Amplify.configure({
Auth: authConfig,
});

const handleUserSRPAuthflowSpy = jest
.spyOn(signInHelpers, 'handleUserSRPAuthFlow')
.mockImplementationOnce(
async (): Promise<RespondToAuthChallengeCommandOutput> => ({
ChallengeName: 'SELECT_MFA_TYPE',
Session: '1234234232',
$metadata: {},
ChallengeParameters: {
MFAS_CAN_CHOOSE: '["SMS_MFA","SOFTWARE_TOKEN_MFA", "EMAIL_OTP"]',
},
}),
);

handleChallengeNameSpy.mockImplementationOnce(
async (): Promise<RespondToAuthChallengeCommandOutput> => ({
ChallengeName: 'SOFTWARE_TOKEN_MFA',
$metadata: {},
Session: '123456789',
ChallengeParameters: {},
}),
);

const signInResult = await signIn({ username, password });

expect(signInResult).toEqual({
isSignedIn: false,
nextStep: {
signInStep: 'CONTINUE_SIGN_IN_WITH_MFA_SELECTION',
allowedMFATypes: ['SMS', 'TOTP', 'EMAIL'],
},
});

const confirmSignInResult = await confirmSignIn({
challengeResponse: 'TOTP',
});

expect(confirmSignInResult).toEqual({
isSignedIn: false,
nextStep: {
signInStep: 'CONFIRM_SIGN_IN_WITH_TOTP_CODE',
},
});

expect(handleChallengeNameSpy).toHaveBeenCalledTimes(1);
expect(handleUserSRPAuthflowSpy).toHaveBeenCalledTimes(1);

handleUserSRPAuthflowSpy.mockClear();
});

test(`confirmSignIn with SELECT_MFA_TYPE challengeName and EMAIL response`, async () => {
Amplify.configure({
Auth: authConfig,
});

const handleUserSRPAuthflowSpy = jest
.spyOn(signInHelpers, 'handleUserSRPAuthFlow')
.mockImplementationOnce(
async (): Promise<RespondToAuthChallengeCommandOutput> => ({
ChallengeName: 'SELECT_MFA_TYPE',
Session: '1234234232',
$metadata: {},
ChallengeParameters: {
MFAS_CAN_CHOOSE: '["SMS_MFA","SOFTWARE_TOKEN_MFA", "EMAIL_OTP"]',
},
}),
);

handleChallengeNameSpy.mockImplementationOnce(
async (): Promise<RespondToAuthChallengeCommandOutput> => ({
ChallengeName: 'EMAIL_OTP',
$metadata: {},
Session: '1234234232',
ChallengeParameters: {
CODE_DELIVERY_DELIVERY_MEDIUM: 'EMAIL',
CODE_DELIVERY_DESTINATION: 'j***@a***',
},
}),
);

const signInResult = await signIn({ username, password });

expect(signInResult).toEqual({
isSignedIn: false,
nextStep: {
signInStep: 'CONTINUE_SIGN_IN_WITH_MFA_SELECTION',
allowedMFATypes: ['SMS', 'TOTP', 'EMAIL'],
},
});

const confirmSignInResult = await confirmSignIn({
challengeResponse: 'EMAIL',
});

expect(confirmSignInResult).toEqual({
isSignedIn: false,
nextStep: {
signInStep: 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE',
codeDeliveryDetails: {
deliveryMedium: 'EMAIL',
destination: 'j***@a***',
},
},
});

expect(handleChallengeNameSpy).toHaveBeenCalledTimes(1);
expect(handleUserSRPAuthflowSpy).toHaveBeenCalledTimes(1);

handleUserSRPAuthflowSpy.mockClear();
});

test('handleChallengeName should be called with clientMetadata and usersub', async () => {
Amplify.configure({
Auth: authConfig,
Expand Down
6 changes: 4 additions & 2 deletions packages/auth/src/common/AuthErrorStrings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ export const validationErrorMap: AmplifyErrorMap<AuthValidationErrorCode> = {
recoverySuggestion: 'Do not include a password in your signIn call.',
},
[AuthValidationErrorCode.IncorrectMFAMethod]: {
message: 'Incorrect MFA method was chosen. It should be either SMS or TOTP',
recoverySuggestion: 'Try to pass TOTP or SMS as the challengeResponse',
message:
'Incorrect MFA method was chosen. It should be either SMS, TOTP, or EMAIL',
recoverySuggestion:
'Try to pass SMS, TOTP, or EMAIL as the challengeResponse',
},
[AuthValidationErrorCode.EmptyVerifyTOTPSetupCode]: {
message: 'code is required to verifyTotpSetup',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { MetadataBearer as __MetadataBearer } from '@aws-sdk/types';
export type ChallengeName =
| 'SMS_MFA'
| 'SOFTWARE_TOKEN_MFA'
| 'EMAIL_OTP'
| 'SELECT_MFA_TYPE'
| 'MFA_SETUP'
| 'PASSWORD_VERIFIER'
Expand All @@ -28,7 +29,7 @@ export type ChallengeParameters = {
MFAS_CAN_SETUP?: string;
} & Record<string, unknown>;

export type CognitoMFAType = 'SMS_MFA' | 'SOFTWARE_TOKEN_MFA';
export type CognitoMFAType = 'SMS_MFA' | 'SOFTWARE_TOKEN_MFA' | 'EMAIL_OTP';

export interface CognitoMFASettings {
Enabled?: boolean;
Expand All @@ -55,6 +56,7 @@ declare enum ChallengeNameType {
SELECT_MFA_TYPE = 'SELECT_MFA_TYPE',
SMS_MFA = 'SMS_MFA',
SOFTWARE_TOKEN_MFA = 'SOFTWARE_TOKEN_MFA',
EMAIL_OTP = 'EMAIL_OTP',
}
declare enum DeliveryMediumType {
EMAIL = 'EMAIL',
Expand Down
Loading

0 comments on commit df6cba9

Please sign in to comment.