From c63d1e5495fede1d71f9ba2fb5c88ee8203352b1 Mon Sep 17 00:00:00 2001 From: scott0929 Date: Mon, 23 Dec 2024 15:06:07 -0800 Subject: [PATCH 1/2] feat: password policy --- cdk/src/lambda/handlers/passwordReset.ts | 9 +++++-- cdk/src/lambda/handlers/userRegistration.ts | 10 +++++++- cdk/src/service/cognito.ts | 26 +++++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/cdk/src/lambda/handlers/passwordReset.ts b/cdk/src/lambda/handlers/passwordReset.ts index 39b0680..1b03720 100644 --- a/cdk/src/lambda/handlers/passwordReset.ts +++ b/cdk/src/lambda/handlers/passwordReset.ts @@ -1,5 +1,5 @@ import { APIGatewayProxyHandler } from 'aws-lambda'; -import { doesUserExistByEmail, resetUserPassword } from '../../service/cognito'; +import { checkPasswordPolicy, doesUserExistByEmail, resetUserPassword } from '../../service/cognito'; import { successResponse, errorResponse, wrapHandler } from '../handlerUtil'; import { getEmailVerifiedToken, deleteEmailVerifiedToken } from '../../service/email/emailVerifiedToken'; @@ -32,13 +32,18 @@ const passwordReset: APIGatewayProxyHandler = async event => { return errorResponse('New password is required', 400); } + await checkPasswordPolicy(newPassword); + // Reset the user password and remove the email verified token after successful password reset await resetUserPassword(email, newPassword); await deleteEmailVerifiedToken(email); return successResponse({ message: 'Password reset successfully' }); } catch (error) { - return errorResponse(error instanceof Error ? error.message : 'Password reset failed', 500); + if (error instanceof Error) { + return errorResponse(error.message, 400); + } + return errorResponse('Password reset failed', 500); } }; diff --git a/cdk/src/lambda/handlers/userRegistration.ts b/cdk/src/lambda/handlers/userRegistration.ts index 86df2f6..fe0ed86 100644 --- a/cdk/src/lambda/handlers/userRegistration.ts +++ b/cdk/src/lambda/handlers/userRegistration.ts @@ -1,7 +1,7 @@ import { APIGatewayProxyHandler } from 'aws-lambda'; import { successResponse, errorResponse, wrapHandler } from '../handlerUtil'; import { getEmailVerifiedToken, deleteEmailVerifiedToken } from '../../service/email/emailVerifiedToken'; -import { createUserInCognito } from '../../service/cognito'; +import { checkPasswordPolicy, createUserInCognito } from '../../service/cognito'; export const handler: APIGatewayProxyHandler = wrapHandler(async event => { const emailVerifiedToken = event.queryStringParameters?.emailVerifiedToken; @@ -28,6 +28,14 @@ export const handler: APIGatewayProxyHandler = wrapHandler(async event => { return errorResponse('Invalid email verified token', 401); } + try { + await checkPasswordPolicy(password); + } catch (error) { + if (error instanceof Error) { + return errorResponse(error.message, 400); + } + } + // Proceed with user registration in Cognito const { AccessToken, IdToken, RefreshToken } = await createUserInCognito(email, password); diff --git a/cdk/src/service/cognito.ts b/cdk/src/service/cognito.ts index 5e415d2..00722da 100644 --- a/cdk/src/service/cognito.ts +++ b/cdk/src/service/cognito.ts @@ -154,3 +154,29 @@ export async function resetUserPassword(email: string, newPassword: string): Pro await cognito.send(command); } + +export async function checkPasswordPolicy(password: string): Promise { + if (password.length < 8) { + throw new Error('Password must be at least 8 characters'); + } + + if (password.length > 20) { + throw new Error('Password cannot exceed 20 characters'); + } + + if (!/[A-Z]/.test(password)) { + throw new Error('Password must contain at least one uppercase letter'); + } + + if (!/[a-z]/.test(password)) { + throw new Error('Password must contain at least one lowercase letter'); + } + + if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + throw new Error('Password must contain at least one special character'); + } + + if (!/[0-9]/.test(password)) { + throw new Error('Password must contain at least one number'); + } +} From 38a03cd261ec8a20ca982cdc35f506c7b58c1311 Mon Sep 17 00:00:00 2001 From: scott0929 Date: Mon, 23 Dec 2024 15:58:52 -0800 Subject: [PATCH 2/2] foo: creating passwordpolicy.ts --- cdk/src/lambda/handlers/passwordReset.ts | 5 +-- cdk/src/lambda/handlers/userRegistration.ts | 5 +-- cdk/src/service/cognito.ts | 26 --------------- cdk/src/service/passwordPolicy.ts | 32 +++++++++++++++++++ .../lambda/handlers/passwordReset.test.ts | 13 +++++++- 5 files changed, 50 insertions(+), 31 deletions(-) create mode 100644 cdk/src/service/passwordPolicy.ts diff --git a/cdk/src/lambda/handlers/passwordReset.ts b/cdk/src/lambda/handlers/passwordReset.ts index 1b03720..fe8728a 100644 --- a/cdk/src/lambda/handlers/passwordReset.ts +++ b/cdk/src/lambda/handlers/passwordReset.ts @@ -1,7 +1,8 @@ import { APIGatewayProxyHandler } from 'aws-lambda'; -import { checkPasswordPolicy, doesUserExistByEmail, resetUserPassword } from '../../service/cognito'; +import { doesUserExistByEmail, resetUserPassword } from '../../service/cognito'; import { successResponse, errorResponse, wrapHandler } from '../handlerUtil'; import { getEmailVerifiedToken, deleteEmailVerifiedToken } from '../../service/email/emailVerifiedToken'; +import { CustomPasswordError, checkPasswordPolicy } from '../../service/passwordPolicy'; const passwordReset: APIGatewayProxyHandler = async event => { const email = event.queryStringParameters?.email; @@ -40,7 +41,7 @@ const passwordReset: APIGatewayProxyHandler = async event => { return successResponse({ message: 'Password reset successfully' }); } catch (error) { - if (error instanceof Error) { + if (error instanceof CustomPasswordError) { return errorResponse(error.message, 400); } return errorResponse('Password reset failed', 500); diff --git a/cdk/src/lambda/handlers/userRegistration.ts b/cdk/src/lambda/handlers/userRegistration.ts index fe0ed86..939a92c 100644 --- a/cdk/src/lambda/handlers/userRegistration.ts +++ b/cdk/src/lambda/handlers/userRegistration.ts @@ -1,7 +1,8 @@ import { APIGatewayProxyHandler } from 'aws-lambda'; import { successResponse, errorResponse, wrapHandler } from '../handlerUtil'; import { getEmailVerifiedToken, deleteEmailVerifiedToken } from '../../service/email/emailVerifiedToken'; -import { checkPasswordPolicy, createUserInCognito } from '../../service/cognito'; +import { createUserInCognito } from '../../service/cognito'; +import { CustomPasswordError, checkPasswordPolicy } from '../../service/passwordPolicy'; export const handler: APIGatewayProxyHandler = wrapHandler(async event => { const emailVerifiedToken = event.queryStringParameters?.emailVerifiedToken; @@ -31,7 +32,7 @@ export const handler: APIGatewayProxyHandler = wrapHandler(async event => { try { await checkPasswordPolicy(password); } catch (error) { - if (error instanceof Error) { + if (error instanceof CustomPasswordError) { return errorResponse(error.message, 400); } } diff --git a/cdk/src/service/cognito.ts b/cdk/src/service/cognito.ts index 00722da..5e415d2 100644 --- a/cdk/src/service/cognito.ts +++ b/cdk/src/service/cognito.ts @@ -154,29 +154,3 @@ export async function resetUserPassword(email: string, newPassword: string): Pro await cognito.send(command); } - -export async function checkPasswordPolicy(password: string): Promise { - if (password.length < 8) { - throw new Error('Password must be at least 8 characters'); - } - - if (password.length > 20) { - throw new Error('Password cannot exceed 20 characters'); - } - - if (!/[A-Z]/.test(password)) { - throw new Error('Password must contain at least one uppercase letter'); - } - - if (!/[a-z]/.test(password)) { - throw new Error('Password must contain at least one lowercase letter'); - } - - if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { - throw new Error('Password must contain at least one special character'); - } - - if (!/[0-9]/.test(password)) { - throw new Error('Password must contain at least one number'); - } -} diff --git a/cdk/src/service/passwordPolicy.ts b/cdk/src/service/passwordPolicy.ts new file mode 100644 index 0000000..c0273f4 --- /dev/null +++ b/cdk/src/service/passwordPolicy.ts @@ -0,0 +1,32 @@ +export class CustomPasswordError extends Error { + constructor(message: string) { + super(message); + this.name = 'CustomPasswordError'; + } +} + +export async function checkPasswordPolicy(password: string): Promise { + if (password.length < 8) { + throw new CustomPasswordError('Password must be at least 8 characters'); + } + + if (password.length > 20) { + throw new CustomPasswordError('Password cannot exceed 20 characters'); + } + + if (!/[A-Z]/.test(password)) { + throw new CustomPasswordError('Password must contain at least one uppercase letter'); + } + + if (!/[a-z]/.test(password)) { + throw new CustomPasswordError('Password must contain at least one lowercase letter'); + } + + if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + throw new CustomPasswordError('Password must contain at least one special character'); + } + + if (!/[0-9]/.test(password)) { + throw new CustomPasswordError('Password must contain at least one number'); + } +} diff --git a/cdk/test/lambda/handlers/passwordReset.test.ts b/cdk/test/lambda/handlers/passwordReset.test.ts index c994cae..b93b3cc 100644 --- a/cdk/test/lambda/handlers/passwordReset.test.ts +++ b/cdk/test/lambda/handlers/passwordReset.test.ts @@ -10,7 +10,8 @@ jest.mock('../../../src/service/email/emailVerifiedToken'); describe('passwordReset', () => { const mockStoredEmailVerifiedToken = 'stored-email-verified-token'; const mockEmail = 'test@sfu.ca'; - const mockNewPassword = 'newPassword123'; + const mockNewPassword = 'newPassword@!#123'; + const mockInvalidPassword = 'newPassword'; const invokeHandler = async (event: Partial) => { const context = {} as Context; @@ -67,6 +68,16 @@ describe('passwordReset', () => { expect(handlerUtil.errorResponse).toHaveBeenCalledWith('New password is required', 400); }); + it('should call errorResponse when password is shorter than 8 characters', async () => { + await invokeHandler({ + queryStringParameters: { email: mockEmail, emailVerifiedToken: mockStoredEmailVerifiedToken }, + body: JSON.stringify({ newPassword: mockInvalidPassword }), + }); + expect(handlerUtil.errorResponse).toHaveBeenCalledWith(expect.stringMatching(/Password/), 400); + expect(resetUserPassword).not.toHaveBeenCalled(); + expect(deleteEmailVerifiedToken).not.toHaveBeenCalled(); + }); + it('should reset password successfully with valid inputs', async () => { await invokeHandler({ queryStringParameters: { email: mockEmail, emailVerifiedToken: mockStoredEmailVerifiedToken },