Skip to content

Commit a698f87

Browse files
Merge branch 'main' into fix/lex-rate-limiting-v2
2 parents 736382e + b6d4c7a commit a698f87

File tree

13 files changed

+396
-33
lines changed

13 files changed

+396
-33
lines changed

.github/canary-config/canary-all.yml

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,15 @@ tests:
153153
# sample_name: [chatbot-component]
154154
# spec: chatbot-component
155155
# browser: *minimal_browser_list
156-
- test_name: integ_react_interactions_chatbot_v1
157-
desc: 'Chatbot V1'
158-
framework: react
159-
category: interactions
160-
sample_name: [lex-test-component]
161-
spec: chatbot-v1
162-
browser: [chrome, firefox]
156+
# Skipping chatbot_v1 test due to persistent AWS Lex V1 rate limiting issues
157+
# TODO: Re-enable after investigating bot configuration
158+
# - test_name: integ_react_interactions_chatbot_v1
159+
# desc: 'Chatbot V1'
160+
# framework: react
161+
# category: interactions
162+
# sample_name: [lex-test-component]
163+
# spec: chatbot-v1
164+
# browser: [chrome, firefox]
163165
- test_name: integ_react_interactions_chatbot_v2
164166
desc: 'Chatbot V2'
165167
framework: react

.github/integ-config/integ-all.yml

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -820,13 +820,15 @@ tests:
820820
# amplifyjs_dir: true
821821

822822
# INTERACTIONS
823-
- test_name: integ_react_interactions_chatbot_v1
824-
desc: 'Chatbot V1'
825-
framework: react
826-
category: interactions
827-
sample_name: [lex-test-component]
828-
spec: chatbot-v1
829-
browser: *minimal_browser_list
823+
# Skipping chatbot_v1 test due to persistent AWS Lex V1 rate limiting issues
824+
# TODO: Re-enable after investigating bot configuration
825+
# - test_name: integ_react_interactions_chatbot_v1
826+
# desc: 'Chatbot V1'
827+
# framework: react
828+
# category: interactions
829+
# sample_name: [lex-test-component]
830+
# spec: chatbot-v1
831+
# browser: *minimal_browser_list
830832
- test_name: integ_react_interactions_chatbot_v2
831833
desc: 'Chatbot V2'
832834
framework: react

packages/auth/__tests__/client/flows/userAuth/handleUserAuthFlow.test.ts

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,16 @@ describe('handleUserAuthFlow', () => {
113113

114114
test('should handle EMAIL_OTP preferred challenge', async () => {
115115
const username = 'testuser';
116+
const configWithPasswordless = {
117+
...mockConfig,
118+
passwordless: {
119+
emailOtpEnabled: true,
120+
},
121+
};
116122

117123
await handleUserAuthFlow({
118124
username,
119-
config: mockConfig,
125+
config: configWithPasswordless,
120126
tokenOrchestrator: expect.anything(),
121127
preferredChallenge: 'EMAIL_OTP',
122128
});
@@ -197,6 +203,146 @@ describe('handleUserAuthFlow', () => {
197203
).rejects.toThrow('password is required to signIn');
198204
});
199205

206+
test('should throw validation error for EMAIL_OTP when not enabled', async () => {
207+
const configWithPasswordless = {
208+
...mockConfig,
209+
passwordless: {
210+
emailOtpEnabled: false,
211+
},
212+
};
213+
214+
await expect(
215+
handleUserAuthFlow({
216+
username: 'testuser',
217+
config: configWithPasswordless,
218+
tokenOrchestrator: expect.anything(),
219+
preferredChallenge: 'EMAIL_OTP',
220+
}),
221+
).rejects.toThrow(
222+
'The preferred challenge is not enabled in your backend configuration',
223+
);
224+
});
225+
226+
test('should throw validation error for SMS_OTP when not enabled', async () => {
227+
const configWithPasswordless = {
228+
...mockConfig,
229+
passwordless: {
230+
smsOtpEnabled: false,
231+
},
232+
};
233+
234+
await expect(
235+
handleUserAuthFlow({
236+
username: 'testuser',
237+
config: configWithPasswordless,
238+
tokenOrchestrator: expect.anything(),
239+
preferredChallenge: 'SMS_OTP',
240+
}),
241+
).rejects.toThrow(
242+
'The preferred challenge is not enabled in your backend configuration',
243+
);
244+
});
245+
246+
test('should throw validation error for WEB_AUTHN when not enabled', async () => {
247+
const configWithPasswordless = {
248+
...mockConfig,
249+
passwordless: {
250+
webAuthn: undefined,
251+
},
252+
};
253+
254+
await expect(
255+
handleUserAuthFlow({
256+
username: 'testuser',
257+
config: configWithPasswordless,
258+
tokenOrchestrator: expect.anything(),
259+
preferredChallenge: 'WEB_AUTHN',
260+
}),
261+
).rejects.toThrow(
262+
'The preferred challenge is not enabled in your backend configuration',
263+
);
264+
});
265+
266+
test('should allow EMAIL_OTP when enabled in config', async () => {
267+
const configWithPasswordless = {
268+
...mockConfig,
269+
passwordless: {
270+
emailOtpEnabled: true,
271+
},
272+
};
273+
274+
await handleUserAuthFlow({
275+
username: 'testuser',
276+
config: configWithPasswordless,
277+
tokenOrchestrator: expect.anything(),
278+
preferredChallenge: 'EMAIL_OTP',
279+
});
280+
281+
expect(mockInitiateAuth).toHaveBeenCalledWith(
282+
expect.anything(),
283+
expect.objectContaining({
284+
AuthParameters: {
285+
USERNAME: 'testuser',
286+
PREFERRED_CHALLENGE: 'EMAIL_OTP',
287+
},
288+
}),
289+
);
290+
});
291+
292+
test('should allow SMS_OTP when enabled in config', async () => {
293+
const configWithPasswordless = {
294+
...mockConfig,
295+
passwordless: {
296+
smsOtpEnabled: true,
297+
},
298+
};
299+
300+
await handleUserAuthFlow({
301+
username: 'testuser',
302+
config: configWithPasswordless,
303+
tokenOrchestrator: expect.anything(),
304+
preferredChallenge: 'SMS_OTP',
305+
});
306+
307+
expect(mockInitiateAuth).toHaveBeenCalledWith(
308+
expect.anything(),
309+
expect.objectContaining({
310+
AuthParameters: {
311+
USERNAME: 'testuser',
312+
PREFERRED_CHALLENGE: 'SMS_OTP',
313+
},
314+
}),
315+
);
316+
});
317+
318+
test('should allow WEB_AUTHN when enabled in config', async () => {
319+
const configWithPasswordless = {
320+
...mockConfig,
321+
passwordless: {
322+
webAuthn: {
323+
relyingPartyId: 'example.com',
324+
},
325+
},
326+
};
327+
328+
await handleUserAuthFlow({
329+
username: 'testuser',
330+
config: configWithPasswordless,
331+
tokenOrchestrator: expect.anything(),
332+
preferredChallenge: 'WEB_AUTHN',
333+
});
334+
335+
expect(mockInitiateAuth).toHaveBeenCalledWith(
336+
expect.anything(),
337+
expect.objectContaining({
338+
AuthParameters: {
339+
USERNAME: 'testuser',
340+
PREFERRED_CHALLENGE: 'WEB_AUTHN',
341+
},
342+
}),
343+
);
344+
});
345+
200346
test('should throw error when initiateAuth fails', async () => {
201347
const error = new Error('Auth failed');
202348
mockInitiateAuth.mockRejectedValueOnce(error);

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

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,92 @@ describe('signInWithUserAuth API tests', () => {
175175
});
176176
});
177177

178+
test('should use config preferredChallenge when not provided by user', async () => {
179+
const authConfigWithPasswordless = {
180+
Cognito: {
181+
...authConfig.Cognito,
182+
passwordless: {
183+
emailOtpEnabled: true,
184+
preferredChallenge: 'EMAIL_OTP' as const,
185+
},
186+
},
187+
};
188+
189+
Amplify.configure({
190+
Auth: authConfigWithPasswordless,
191+
});
192+
193+
const mockResponse: InitiateAuthCommandOutput = {
194+
ChallengeName: 'EMAIL_OTP',
195+
Session: 'mockSession',
196+
ChallengeParameters: {},
197+
$metadata: {},
198+
};
199+
handleUserAuthFlow.mockResolvedValue(mockResponse);
200+
201+
await signInWithUserAuth({
202+
username: 'testuser',
203+
});
204+
205+
expect(handleUserAuthFlow).toHaveBeenCalledWith({
206+
username: 'testuser',
207+
clientMetadata: undefined,
208+
config: authConfigWithPasswordless.Cognito,
209+
tokenOrchestrator: expect.anything(),
210+
preferredChallenge: 'EMAIL_OTP',
211+
password: undefined,
212+
});
213+
214+
// Reset config
215+
Amplify.configure({
216+
Auth: authConfig,
217+
});
218+
});
219+
220+
test('should prioritize user-provided preferredChallenge over config', async () => {
221+
const authConfigWithPasswordless = {
222+
Cognito: {
223+
...authConfig.Cognito,
224+
passwordless: {
225+
emailOtpEnabled: true,
226+
smsOtpEnabled: true,
227+
preferredChallenge: 'EMAIL_OTP' as const,
228+
},
229+
},
230+
};
231+
232+
Amplify.configure({
233+
Auth: authConfigWithPasswordless,
234+
});
235+
236+
const mockResponse: InitiateAuthCommandOutput = {
237+
ChallengeName: 'SMS_OTP',
238+
Session: 'mockSession',
239+
ChallengeParameters: {},
240+
$metadata: {},
241+
};
242+
handleUserAuthFlow.mockResolvedValue(mockResponse);
243+
244+
await signInWithUserAuth({
245+
username: 'testuser',
246+
options: { preferredChallenge: 'SMS_OTP' },
247+
});
248+
249+
expect(handleUserAuthFlow).toHaveBeenCalledWith({
250+
username: 'testuser',
251+
clientMetadata: undefined,
252+
config: authConfigWithPasswordless.Cognito,
253+
tokenOrchestrator: expect.anything(),
254+
preferredChallenge: 'SMS_OTP',
255+
password: undefined,
256+
});
257+
258+
// Reset config
259+
Amplify.configure({
260+
Auth: authConfig,
261+
});
262+
});
263+
178264
test('should throw error when service error has no sign in result', async () => {
179265
const error = new Error('Unknown error');
180266
error.name = 'UnknownError';

packages/auth/src/client/flows/userAuth/handleUserAuthFlow.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,24 @@ export async function handleUserAuthFlow({
6262
const authParameters: Record<string, string> = { USERNAME: username };
6363

6464
if (preferredChallenge) {
65+
// Validate that the preferred challenge is enabled in the backend config
66+
// Only validate if passwordless config exists (for backward compatibility)
67+
if (config.passwordless) {
68+
const isInvalidChallenge =
69+
(preferredChallenge === 'EMAIL_OTP' &&
70+
!config.passwordless.emailOtpEnabled) ||
71+
(preferredChallenge === 'SMS_OTP' &&
72+
!config.passwordless.smsOtpEnabled) ||
73+
(preferredChallenge === 'WEB_AUTHN' && !config.passwordless.webAuthn);
74+
75+
if (isInvalidChallenge) {
76+
assertValidationError(
77+
false,
78+
AuthValidationErrorCode.InvalidPreferredChallenge,
79+
);
80+
}
81+
}
82+
6583
if (preferredChallenge === 'PASSWORD_SRP') {
6684
assertValidationError(
6785
!!password,

packages/auth/src/common/AuthErrorStrings.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ export const validationErrorMap: AmplifyErrorMap<AuthValidationErrorCode> = {
6161
[AuthValidationErrorCode.EmptyConfirmUserAttributeCode]: {
6262
message: 'confirmation code is required to confirmUserAttribute',
6363
},
64+
[AuthValidationErrorCode.InvalidPreferredChallenge]: {
65+
message:
66+
'The preferred challenge is not enabled in your backend configuration',
67+
recoverySuggestion:
68+
'Ensure the authentication method is enabled in your Amplify backend configuration',
69+
},
6470
};
6571

6672
// TODO: delete this code when the Auth class is removed.

packages/auth/src/errors/types/validation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ export enum AuthValidationErrorCode {
1919
EmptyConfirmUserAttributeCode = 'EmptyConfirmUserAttributeCode',
2020
IncorrectMFAMethod = 'IncorrectMFAMethod',
2121
EmptyUpdatePassword = 'EmptyUpdatePassword',
22+
InvalidPreferredChallenge = 'InvalidPreferredChallenge',
2223
}

packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ export async function signInWithUserAuth(
6363
};
6464
assertTokenProviderConfig(authConfig);
6565
const clientMetaData = options?.clientMetadata;
66-
const preferredChallenge = options?.preferredChallenge;
66+
const preferredChallenge =
67+
options?.preferredChallenge ?? authConfig?.passwordless?.preferredChallenge;
6768

6869
assertValidationError(
6970
!!username,

0 commit comments

Comments
 (0)