diff --git a/cdk/src/lambda/handlers/passwordReset.ts b/cdk/src/lambda/handlers/passwordReset.ts index 39b0680..fe8728a 100644 --- a/cdk/src/lambda/handlers/passwordReset.ts +++ b/cdk/src/lambda/handlers/passwordReset.ts @@ -2,6 +2,7 @@ import { APIGatewayProxyHandler } from 'aws-lambda'; 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; @@ -32,13 +33,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 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 86df2f6..939a92c 100644 --- a/cdk/src/lambda/handlers/userRegistration.ts +++ b/cdk/src/lambda/handlers/userRegistration.ts @@ -2,6 +2,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 { CustomPasswordError, checkPasswordPolicy } from '../../service/passwordPolicy'; export const handler: APIGatewayProxyHandler = wrapHandler(async event => { const emailVerifiedToken = event.queryStringParameters?.emailVerifiedToken; @@ -28,6 +29,14 @@ export const handler: APIGatewayProxyHandler = wrapHandler(async event => { return errorResponse('Invalid email verified token', 401); } + try { + await checkPasswordPolicy(password); + } catch (error) { + if (error instanceof CustomPasswordError) { + 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/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 },