Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SM] - KOGO-232/validation required on verify-email and reset-password #5

Merged
merged 1 commit into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions cdk/lib/lambdaStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,28 @@ export class LambdaStack extends cdk.Stack {
bundling,
});
emailVerificationLambda.addToRolePolicy(policies.ses.sendEmail);
emailVerificationLambda.addToRolePolicy(policies.cognito.userManagement);

// path: /student/verify-email
const emailVerificationIntegration = new apigateway.LambdaIntegration(emailVerificationLambda);
const verifyEmailResource = studentResource.addResource('verify-email');
verifyEmailResource.addMethod('POST', emailVerificationIntegration);

// =================================================================
// Verify Email For Password Lambda
// =================================================================
const verifyEmailForPasswordLambda = new NodejsFunction(this, 'VerifyEmailForPasswordHandler', {
...nodeJsFunctionProps,
entry: path.join(__dirname, '../src/lambda/handlers/emailVerificationForPassword.ts'),
bundling,
});
verifyEmailForPasswordLambda.addToRolePolicy(policies.ses.sendEmail);
verifyEmailForPasswordLambda.addToRolePolicy(policies.cognito.userManagement);

// path: /student/verify-email/password
const verifyEmailForPasswordIntegration = new apigateway.LambdaIntegration(verifyEmailForPasswordLambda);
verifyEmailResource.addResource('password').addMethod('POST', verifyEmailForPasswordIntegration);

// =================================================================
// Verify Email Complete Lambda
// =================================================================
Expand Down
2 changes: 2 additions & 0 deletions cdk/src/docs/emailVerification.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ paths:
description: Verification email sent
'400':
description: Bad request - missing or invalid parameters
'409':
description: Conflict - User already exists with the provided email
'500':
description: Internal server error
8 changes: 4 additions & 4 deletions cdk/src/docs/emailVerificationComplete.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ paths:
/student/verify-email/complete:
post:
summary: Complete email verification
description: Validates the verification code sent to the student's email and returns an authorization token if successful.
description: Validates the verification code sent to the student's email and returns an email verified token if successful.
parameters:
- name: email
in: query
Expand All @@ -18,7 +18,7 @@ paths:
type: string
responses:
'200':
description: Email verified successfully and an authorization token returned
description: Email verified successfully and an email verified returned
content:
application/json:
schema:
Expand All @@ -27,9 +27,9 @@ paths:
message:
type: string
example: "Email verified successfully."
authToken:
emailVerifiedToken:
type: string
description: Authorization token for further authentication
description: Email verified token for further authentication
example: "eyJhbGciOiJIUzI1NiIsInR5..."
'400':
description: Bad request - missing or invalid parameters
Expand Down
21 changes: 21 additions & 0 deletions cdk/src/docs/emailVerificationForPassword.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
paths:
/student/verify-email/password:
post:
summary: Verify a student's email while resetting passwords
description: Generates a verification code and sends it to the email provided.
parameters:
- name: email
in: query
required: true
description: The email address of the student to verify.
schema:
type: string
responses:
'200':
description: Verification email sent
'400':
description: Bad request - missing or invalid parameters
'404':
description: Not found - No user exists with the provided email
'500':
description: Internal server error
6 changes: 3 additions & 3 deletions cdk/src/docs/passwordReset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ paths:
/student/password-reset:
post:
summary: Reset password for a student
description: Resets the password for a student after the authorization token validation.
description: Resets the password for a student after the email verified token validation.
requestBody:
description: The new password for the student
required: true
Expand All @@ -21,7 +21,7 @@ paths:
description: The email address of the student.
schema:
type: string
- name: authToken
- name: emailVerifiedToken
in: query
required: true
description: The authorization token provided after email verification.
Expand All @@ -41,7 +41,7 @@ paths:
'400':
description: Bad request - missing or invalid parameters
'401':
description: Unauthorized - Invalid or expired authorization token
description: Unauthorized - Invalid or expired email verified token
'404':
description: Not found - user does not exist
'500':
Expand Down
18 changes: 4 additions & 14 deletions cdk/src/docs/userRegistration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ paths:
description: The email address of the student.
schema:
type: string
- name: authToken
- name: emailVerifiedToken
in: query
required: true
description: The authorization token received after email verification.
Expand Down Expand Up @@ -58,26 +58,16 @@ paths:
properties:
message:
type: string
example: "Email and authorization token are required"
example: "Email and email verified token are required"
'401':
description: Unauthorized - Invalid or expired authorization token.
description: Unauthorized - Invalid or expired email verified token.
content:
application/json:
schema:
type: object
properties:
message:
type: string
example: "Invalid or expired authorization token"
'409':
description: Conflict - User already exists.
content:
application/json:
schema:
type: object
properties:
message:
type: string
example: "User already exists with the provided email"
example: "Invalid or expired email verified token"
'500':
description: Internal server error.
2 changes: 0 additions & 2 deletions cdk/src/lambda/handlers/authenticateUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ const authenticateUser: APIGatewayProxyHandler = async event => {

return successResponse({
userdata: {
username: userDetails.username,
email: userDetails.email,
schoolKey,
schoolData,
Expand All @@ -41,7 +40,6 @@ const authenticateUser: APIGatewayProxyHandler = async event => {

return successResponse({
userdata: {
username: userDetails.username,
email: userDetails.email,
schoolKey,
schoolData,
Expand Down
7 changes: 7 additions & 0 deletions cdk/src/lambda/handlers/emailVerification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { buildEmailParams } from '../../service/email';
import { RedisClient } from '../../service/redis';
import { isDesignatedSchoolEmail } from '../../service/school';
import { settings } from '../../settings';
import { doesUserExistByEmail } from '../../service/cognito';

// Constants
const EXPIRATION_TIME = 900; // Set expiration to 15 minutes (900 seconds)
Expand All @@ -23,6 +24,12 @@ const emailVerification: APIGatewayProxyHandler = async event => {
return errorResponse('Email is not from a designated school domain', 400);
}

// Check if a user with the same email already exists
const doesUserExist = await doesUserExistByEmail(email);
if (doesUserExist) {
return errorResponse('User already exists with the provided email', 409);
}

try {
const verificationCode = Math.floor(100000 + Math.random() * 900000).toString();
const redis = RedisClient.getInstance();
Expand Down
10 changes: 5 additions & 5 deletions cdk/src/lambda/handlers/emailVerificationComplete.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { APIGatewayProxyHandler } from 'aws-lambda';
import { successResponse, errorResponse, wrapHandler } from '../handlerUtil';
import { RedisClient } from '../../service/redis';
import { generateAuthToken, storeAuthToken } from '../../service/email/authToken';
import { generateEmailVerifiedToken, storeEmailVerifiedToken } from '../../service/email/emailVerifiedToken';

const emailVerificationComplete: APIGatewayProxyHandler = async event => {
const email = event.queryStringParameters?.email;
Expand All @@ -25,13 +25,13 @@ const emailVerificationComplete: APIGatewayProxyHandler = async event => {
// Verification successful, delete the code from Redis
await redis.delete(email);

// Generate a new auth token and store it in Redis
const authToken = generateAuthToken();
await storeAuthToken(email, authToken);
// Generate a new email verified token and store it in Redis
const emailVerifiedToken = generateEmailVerifiedToken();
await storeEmailVerifiedToken(email, emailVerifiedToken);

return successResponse({
message: 'Email verified successfully.',
authToken,
emailVerifiedToken,
});
} catch (error) {
return errorResponse(error instanceof Error ? error.message : 'Verification failed', 500);
Expand Down
43 changes: 43 additions & 0 deletions cdk/src/lambda/handlers/emailVerificationForPassword.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { APIGatewayProxyHandler } from 'aws-lambda';
import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';
import { successResponse, errorResponse, wrapHandler } from '../handlerUtil';
import { buildEmailParams } from '../../service/email';
import { RedisClient } from '../../service/redis';
import { settings } from '../../settings';
import { doesUserExistByEmail } from '../../service/cognito';

// Constants
const EXPIRATION_TIME = 900; // Set expiration to 15 minutes (900 seconds)

// SES Client
const SES = new SESClient({ region: settings.ses.sesIdentityRegion });

const emailVerificationForPassword: APIGatewayProxyHandler = async event => {
const email = event.queryStringParameters?.email;

if (!email) {
return errorResponse('Email query parameter is required', 400);
}

// Check if a user with the same email already exists
const doesUserExist = await doesUserExistByEmail(email);
if (!doesUserExist) {
return errorResponse('No user exists with the provided email', 404);
}

try {
const verificationCode = Math.floor(100000 + Math.random() * 900000).toString();
const redis = RedisClient.getInstance();
await redis.setWithExpiry(email, verificationCode, EXPIRATION_TIME);

const emailParams = buildEmailParams(email, 'verification', { verificationCode }, '[email protected]');
const command = new SendEmailCommand(emailParams);
await SES.send(command);

return successResponse({ message: 'Verification email sent' });
} catch (error) {
return errorResponse(error instanceof Error ? error.message : 'Failed to send verification email', 500);
}
};

export const handler = wrapHandler(emailVerificationForPassword);
24 changes: 12 additions & 12 deletions cdk/src/lambda/handlers/passwordReset.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { APIGatewayProxyHandler } from 'aws-lambda';
import { doesUserExistByEmail, resetUserPassword } from '../../service/cognito';
import { successResponse, errorResponse, wrapHandler } from '../handlerUtil';
import { getAuthToken, deleteAuthToken } from '../../service/email/authToken';
import { getEmailVerifiedToken, deleteEmailVerifiedToken } from '../../service/email/emailVerifiedToken';

const passwordReset: APIGatewayProxyHandler = async event => {
const email = event.queryStringParameters?.email;
const authToken = event.queryStringParameters?.authToken;
const emailVerifiedToken = event.queryStringParameters?.emailVerifiedToken;

if (!email || !authToken) {
return errorResponse('Email and authorization token are required', 400);
if (!email || !emailVerifiedToken) {
return errorResponse('Email and email verified token are required', 400);
}

try {
// Verify the auth token
const storedAuthToken = await getAuthToken(email);
if (!storedAuthToken) {
return errorResponse('Authorization token has expired or does not exist', 401);
// Verify the email verified token
const storedEmailVerifiedToken = await getEmailVerifiedToken(email);
if (!storedEmailVerifiedToken) {
return errorResponse('Email verified token has expired or does not exist', 401);
}
if (storedAuthToken !== authToken) {
return errorResponse('Invalid authorization token', 401);
if (storedEmailVerifiedToken !== emailVerifiedToken) {
return errorResponse('Invalid email verified token', 401);
}

const userExists = await doesUserExistByEmail(email);
Expand All @@ -32,9 +32,9 @@ const passwordReset: APIGatewayProxyHandler = async event => {
return errorResponse('New password is required', 400);
}

// Reset the user password and remove the auth token after successful password reset
// Reset the user password and remove the email verified token after successful password reset
await resetUserPassword(email, newPassword);
await deleteAuthToken(email);
await deleteEmailVerifiedToken(email);

return successResponse({ message: 'Password reset successfully' });
} catch (error) {
Expand Down
34 changes: 14 additions & 20 deletions cdk/src/lambda/handlers/userRegistration.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,38 @@
import { APIGatewayProxyHandler } from 'aws-lambda';
import { successResponse, errorResponse, wrapHandler } from '../handlerUtil';
import { getAuthToken, deleteAuthToken } from '../../service/email/authToken';
import { createUserInCognito, doesUserExistByEmail } from '../../service/cognito';
import { getEmailVerifiedToken, deleteEmailVerifiedToken } from '../../service/email/emailVerifiedToken';
import { createUserInCognito } from '../../service/cognito';

export const handler: APIGatewayProxyHandler = wrapHandler(async event => {
const email = event.queryStringParameters?.email;
const authToken = event.queryStringParameters?.authToken;
const emailVerifiedToken = event.queryStringParameters?.emailVerifiedToken;

if (!email || !authToken) {
return errorResponse('Email and authorization token are required', 400);
if (!email || !emailVerifiedToken) {
return errorResponse('Email and email verified token are required', 400);
}

// Get username and password from the request body
// Get password from the request body
const requestBody = JSON.parse(event.body || '{}');
const { password } = requestBody;
// Check if password is provided
if (!password) {
return errorResponse('Password is required in the request body', 400);
}

// Verify the auth token after checking username and password
const storedAuthToken = await getAuthToken(email);
if (!storedAuthToken) {
return errorResponse('No authorization token found or it has expired', 401);
// Verify the email verified token after checking email and password
const storedEmailVerifiedToken = await getEmailVerifiedToken(email);
if (!storedEmailVerifiedToken) {
return errorResponse('No email verified token found or it has expired', 401);
}
if (authToken !== storedAuthToken) {
return errorResponse('Invalid authorization token', 401);
}

// Check if a user with the same email already exists
const doesUserExist = await doesUserExistByEmail(email);
if (doesUserExist) {
return errorResponse('User already exists with the provided email', 409);
if (emailVerifiedToken !== storedEmailVerifiedToken) {
return errorResponse('Invalid email verified token', 401);
}

// Proceed with user registration in Cognito
const { AccessToken, IdToken, RefreshToken } = await createUserInCognito(email, password);

// Upon successful registration, delete the auth token from Redis
await deleteAuthToken(email);
// Upon successful registration, delete the email verified token from Redis
await deleteEmailVerifiedToken(email);

return successResponse({
message: 'User successfully created',
Expand Down
Loading
Loading