Skip to content

Commit 4b2c458

Browse files
jjarvispisrax
andcommitted
[Email MFA] Add support for EMAIL_OTP during sign in flows (#13745)
* Confirm Sign In With Email OTP * Confirm Sign In Tests With Email OTP * Update packages/auth/src/types/models.ts Co-authored-by: israx <[email protected]> * Fix Errant Pascal Casing --------- Co-authored-by: israx <[email protected]>
1 parent 164e9cc commit 4b2c458

File tree

6 files changed

+266
-87
lines changed

6 files changed

+266
-87
lines changed

packages/auth/__tests__/providers/cognito/confirmSignInErrorCases.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ describe('confirmSignIn API error path cases:', () => {
5151
}
5252
});
5353

54-
it('should throw an error when sign-in step is CONTINUE_SIGN_IN_WITH_MFA_SELECTION and challengeResponse is not "SMS" or "TOTP"', async () => {
54+
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 () => {
5555
expect.assertions(2);
5656
try {
5757
await confirmSignIn({ challengeResponse: 'NO_SMS' });

packages/auth/__tests__/providers/cognito/confirmSignInHappyCases.test.ts

Lines changed: 167 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,55 @@ describe('confirmSignIn API happy path cases', () => {
122122
mockedGetCurrentUser.mockClear();
123123
});
124124

125+
test(`confirmSignIn with EMAIL_OTP ChallengeName`, async () => {
126+
Amplify.configure({
127+
Auth: authConfig,
128+
});
129+
130+
const handleUserSRPAuthflowSpy = jest
131+
.spyOn(signInHelpers, 'handleUserSRPAuthFlow')
132+
.mockImplementationOnce(
133+
async (): Promise<RespondToAuthChallengeCommandOutput> => ({
134+
ChallengeName: 'EMAIL_OTP',
135+
Session: '1234234232',
136+
$metadata: {},
137+
ChallengeParameters: {
138+
CODE_DELIVERY_DELIVERY_MEDIUM: 'EMAIL',
139+
CODE_DELIVERY_DESTINATION: 'j***@a***',
140+
},
141+
}),
142+
);
143+
144+
const signInResult = await signIn({ username, password });
145+
146+
expect(signInResult).toEqual({
147+
isSignedIn: false,
148+
nextStep: {
149+
signInStep: 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE',
150+
codeDeliveryDetails: {
151+
deliveryMedium: 'EMAIL',
152+
destination: 'j***@a***',
153+
},
154+
},
155+
});
156+
157+
const confirmSignInResult = await confirmSignIn({
158+
challengeResponse: '123456',
159+
});
160+
161+
expect(confirmSignInResult).toEqual({
162+
isSignedIn: true,
163+
nextStep: {
164+
signInStep: 'DONE',
165+
},
166+
});
167+
168+
expect(handleChallengeNameSpy).toHaveBeenCalledTimes(1);
169+
expect(handleUserSRPAuthflowSpy).toHaveBeenCalledTimes(1);
170+
171+
handleUserSRPAuthflowSpy.mockClear();
172+
});
173+
125174
test(`confirmSignIn tests MFA_SETUP challengeName`, async () => {
126175
Amplify.configure({
127176
Auth: authConfig,
@@ -162,7 +211,7 @@ describe('confirmSignIn API happy path cases', () => {
162211
handleUserSRPAuthflowSpy.mockClear();
163212
});
164213

165-
test(`confirmSignIn tests SELECT_MFA_TYPE challengeName `, async () => {
214+
test(`confirmSignIn with SELECT_MFA_TYPE challengeName and SMS response`, async () => {
166215
Amplify.configure({
167216
Auth: authConfig,
168217
});
@@ -175,7 +224,7 @@ describe('confirmSignIn API happy path cases', () => {
175224
Session: '1234234232',
176225
$metadata: {},
177226
ChallengeParameters: {
178-
MFAS_CAN_CHOOSE: '["SMS_MFA","SOFTWARE_TOKEN_MFA"]',
227+
MFAS_CAN_CHOOSE: '["SMS_MFA","SOFTWARE_TOKEN_MFA", "EMAIL_OTP"]',
179228
},
180229
}),
181230
);
@@ -204,7 +253,7 @@ describe('confirmSignIn API happy path cases', () => {
204253
isSignedIn: false,
205254
nextStep: {
206255
signInStep: 'CONTINUE_SIGN_IN_WITH_MFA_SELECTION',
207-
allowedMFATypes: ['SMS', 'TOTP'],
256+
allowedMFATypes: ['SMS', 'TOTP', 'EMAIL'],
208257
},
209258
});
210259

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

278+
test(`confirmSignIn with SELECT_MFA_TYPE challengeName and TOTP response`, async () => {
279+
Amplify.configure({
280+
Auth: authConfig,
281+
});
282+
283+
const handleUserSRPAuthflowSpy = jest
284+
.spyOn(signInHelpers, 'handleUserSRPAuthFlow')
285+
.mockImplementationOnce(
286+
async (): Promise<RespondToAuthChallengeCommandOutput> => ({
287+
ChallengeName: 'SELECT_MFA_TYPE',
288+
Session: '1234234232',
289+
$metadata: {},
290+
ChallengeParameters: {
291+
MFAS_CAN_CHOOSE: '["SMS_MFA","SOFTWARE_TOKEN_MFA", "EMAIL_OTP"]',
292+
},
293+
}),
294+
);
295+
296+
handleChallengeNameSpy.mockImplementationOnce(
297+
async (): Promise<RespondToAuthChallengeCommandOutput> => ({
298+
ChallengeName: 'SOFTWARE_TOKEN_MFA',
299+
$metadata: {},
300+
Session: '123456789',
301+
ChallengeParameters: {},
302+
}),
303+
);
304+
305+
const signInResult = await signIn({ username, password });
306+
307+
expect(signInResult).toEqual({
308+
isSignedIn: false,
309+
nextStep: {
310+
signInStep: 'CONTINUE_SIGN_IN_WITH_MFA_SELECTION',
311+
allowedMFATypes: ['SMS', 'TOTP', 'EMAIL'],
312+
},
313+
});
314+
315+
const confirmSignInResult = await confirmSignIn({
316+
challengeResponse: 'TOTP',
317+
});
318+
319+
expect(confirmSignInResult).toEqual({
320+
isSignedIn: false,
321+
nextStep: {
322+
signInStep: 'CONFIRM_SIGN_IN_WITH_TOTP_CODE',
323+
},
324+
});
325+
326+
expect(handleChallengeNameSpy).toHaveBeenCalledTimes(1);
327+
expect(handleUserSRPAuthflowSpy).toHaveBeenCalledTimes(1);
328+
329+
handleUserSRPAuthflowSpy.mockClear();
330+
});
331+
332+
test(`confirmSignIn with SELECT_MFA_TYPE challengeName and EMAIL response`, async () => {
333+
Amplify.configure({
334+
Auth: authConfig,
335+
});
336+
337+
const handleUserSRPAuthflowSpy = jest
338+
.spyOn(signInHelpers, 'handleUserSRPAuthFlow')
339+
.mockImplementationOnce(
340+
async (): Promise<RespondToAuthChallengeCommandOutput> => ({
341+
ChallengeName: 'SELECT_MFA_TYPE',
342+
Session: '1234234232',
343+
$metadata: {},
344+
ChallengeParameters: {
345+
MFAS_CAN_CHOOSE: '["SMS_MFA","SOFTWARE_TOKEN_MFA", "EMAIL_OTP"]',
346+
},
347+
}),
348+
);
349+
350+
handleChallengeNameSpy.mockImplementationOnce(
351+
async (): Promise<RespondToAuthChallengeCommandOutput> => ({
352+
ChallengeName: 'EMAIL_OTP',
353+
$metadata: {},
354+
Session: '1234234232',
355+
ChallengeParameters: {
356+
CODE_DELIVERY_DELIVERY_MEDIUM: 'EMAIL',
357+
CODE_DELIVERY_DESTINATION: 'j***@a***',
358+
},
359+
}),
360+
);
361+
362+
const signInResult = await signIn({ username, password });
363+
364+
expect(signInResult).toEqual({
365+
isSignedIn: false,
366+
nextStep: {
367+
signInStep: 'CONTINUE_SIGN_IN_WITH_MFA_SELECTION',
368+
allowedMFATypes: ['SMS', 'TOTP', 'EMAIL'],
369+
},
370+
});
371+
372+
const confirmSignInResult = await confirmSignIn({
373+
challengeResponse: 'EMAIL',
374+
});
375+
376+
expect(confirmSignInResult).toEqual({
377+
isSignedIn: false,
378+
nextStep: {
379+
signInStep: 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE',
380+
codeDeliveryDetails: {
381+
deliveryMedium: 'EMAIL',
382+
destination: 'j***@a***',
383+
},
384+
},
385+
});
386+
387+
expect(handleChallengeNameSpy).toHaveBeenCalledTimes(1);
388+
expect(handleUserSRPAuthflowSpy).toHaveBeenCalledTimes(1);
389+
390+
handleUserSRPAuthflowSpy.mockClear();
391+
});
392+
229393
test('handleChallengeName should be called with clientMetadata and usersub', async () => {
230394
Amplify.configure({
231395
Auth: authConfig,

packages/auth/src/common/AuthErrorStrings.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@ export const validationErrorMap: AmplifyErrorMap<AuthValidationErrorCode> = {
4747
recoverySuggestion: 'Do not include a password in your signIn call.',
4848
},
4949
[AuthValidationErrorCode.IncorrectMFAMethod]: {
50-
message: 'Incorrect MFA method was chosen. It should be either SMS or TOTP',
51-
recoverySuggestion: 'Try to pass TOTP or SMS as the challengeResponse',
50+
message:
51+
'Incorrect MFA method was chosen. It should be either SMS, TOTP, or EMAIL',
52+
recoverySuggestion:
53+
'Try to pass SMS, TOTP, or EMAIL as the challengeResponse',
5254
},
5355
[AuthValidationErrorCode.EmptyVerifyTOTPSetupCode]: {
5456
message: 'code is required to verifyTotpSetup',

packages/auth/src/providers/cognito/utils/clients/CognitoIdentityProvider/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { MetadataBearer as __MetadataBearer } from '@aws-sdk/types';
88
export type ChallengeName =
99
| 'SMS_MFA'
1010
| 'SOFTWARE_TOKEN_MFA'
11+
| 'EMAIL_OTP'
1112
| 'SELECT_MFA_TYPE'
1213
| 'MFA_SETUP'
1314
| 'PASSWORD_VERIFIER'
@@ -28,7 +29,7 @@ export type ChallengeParameters = {
2829
MFAS_CAN_SETUP?: string;
2930
} & Record<string, unknown>;
3031

31-
export type CognitoMFAType = 'SMS_MFA' | 'SOFTWARE_TOKEN_MFA';
32+
export type CognitoMFAType = 'SMS_MFA' | 'SOFTWARE_TOKEN_MFA' | 'EMAIL_OTP';
3233

3334
export interface CognitoMFASettings {
3435
Enabled?: boolean;
@@ -55,6 +56,7 @@ declare enum ChallengeNameType {
5556
SELECT_MFA_TYPE = 'SELECT_MFA_TYPE',
5657
SMS_MFA = 'SMS_MFA',
5758
SOFTWARE_TOKEN_MFA = 'SOFTWARE_TOKEN_MFA',
59+
EMAIL_OTP = 'EMAIL_OTP',
5860
}
5961
declare enum DeliveryMediumType {
6062
EMAIL = 'EMAIL',

0 commit comments

Comments
 (0)