The Auth Stack is the Phase 3 authentication layer of the EveryoneCook project. It manages user authentication, registration, and account security using AWS Cognito with custom Lambda triggers for enhanced user experience.
Deployment Order: This stack MUST be deployed after Core Stack and before Backend Stack.
Cognito User Pool:
Cognito User Pool Client:
Lambda Triggers (5 triggers):
┌─────────────────────────────────────────────────────────────────┐
│ Auth Stack (Phase 3) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Cognito User Pool │ │
│ │ ├─ Sign-in: Username or Email │ │
│ │ ├─ Password: Min 12 chars, strong policy │ │
│ │ ├─ Email Verification: Required │ │
│ │ ├─ MFA: Disabled (email + password only) │ │
│ │ ├─ Device Tracking: Enabled (no MFA) │ │
│ │ └─ Custom Attributes: account_status, country │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Lambda Triggers (Custom Workflows) │ │
│ │ │ │
│ │ 1️⃣ PreSignUp │ │
│ │ ├─ Check existing username/email │ │
│ │ ├─ Delete expired unverified users (>24h) │ │
│ │ └─ Allow new registration │ │
│ │ │ │
│ │ 2️⃣ CustomMessage │ │
│ │ ├─ Customize email verification template │ │
│ │ ├─ Customize password reset template │ │
│ │ └─ Add styling and branding │ │
│ │ │ │
│ │ 3️⃣ PostConfirmation │ │
│ │ ├─ Create DynamoDB entities: │ │
│ │ │ ├─ Core Profile (PK=USER#{userId}, SK=PROFILE) │ │
│ │ │ ├─ Privacy Settings (SK=PRIVACY_SETTINGS) │ │
│ │ │ └─ AI Preferences (SK=AI_PREFERENCES) │ │
│ │ └─ Initialize user data │ │
│ │ │ │
│ │ 4️⃣ PreAuthentication │ │
│ │ ├─ Check user account status │ │
│ │ ├─ Reject if banned/suspended │ │
│ │ └─ Allow login if active │ │
│ │ │ │
│ │ 5️⃣ PostAuthentication │ │
│ │ ├─ Update lastLoginAt timestamp │ │
│ │ └─ Track user activity │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Cognito User Pool Client │ │
│ │ ├─ Client Type: Web (no secret) │ │
│ │ ├─ Auth Flows: Password, SRP │ │
│ │ ├─ OAuth: Authorization code grant │ │
│ │ ├─ Tokens: 1h access, 1h ID, 30d refresh │ │
│ │ ├─ Callback: https://{env}.everyonecook.cloud │ │
│ │ └─ Security: Token revocation, user enum protection │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
│ Exports
▼
Backend Stack
(API Gateway Cognito Authorizer)
infrastructure/lib/stacks/
└── auth-stack.ts # Auth Stack (865 lines)
services/auth-module/triggers/
├── pre-signup.ts # PreSignUp trigger
├── custom-message.ts # CustomMessage trigger
├── post-confirmation.ts # PostConfirmation trigger
├── pre-authentication.ts # PreAuthentication trigger
└── post-authentication.ts # PostAuthentication trigger
File: infrastructure/lib/stacks/auth-stack.ts
/**
* Create Cognito User Pool with production-grade security
*/
private createUserPool(): cdk.aws_cognito.UserPool {
const cognitoConfig = this.config.cognito;
const userPool = new cdk.aws_cognito.UserPool(this, 'UserPool', {
userPoolName: `EveryoneCook-${this.config.environment}`,
// Sign-in configuration
signInAliases: {
username: true,
email: true,
},
// Self sign-up enabled
selfSignUpEnabled: true,
// Standard attributes
standardAttributes: {
email: {
required: true,
mutable: false, // Email cannot be changed
},
givenName: {
required: true, // fullName stored in given_name
mutable: true,
},
birthdate: { required: false, mutable: true },
gender: { required: false, mutable: true },
},
// Custom attributes
customAttributes: {
account_status: new cdk.aws_cognito.StringAttribute({
mutable: true,
minLen: 1,
maxLen: 20,
}),
country: new cdk.aws_cognito.StringAttribute({
mutable: true,
minLen: 2,
maxLen: 2, // ISO 3166-1 alpha-2
}),
},
// Password policy
passwordPolicy: {
minLength: 12, // 8 for dev
requireLowercase: true,
requireUppercase: true,
requireDigits: true,
requireSymbols: true,
tempPasswordValidity: cdk.Duration.days(7),
},
// Account recovery
accountRecovery: cdk.aws_cognito.AccountRecovery.EMAIL_ONLY,
// Email configuration (Cognito default)
email: cdk.aws_cognito.UserPoolEmail.withCognito(),
// Auto-verify email
autoVerify: { email: true },
// MFA: Disabled
mfa: cdk.aws_cognito.Mfa.OFF,
// Device tracking (no MFA challenge)
deviceTracking: {
challengeRequiredOnNewDevice: false,
deviceOnlyRememberedOnUserPrompt: true,
},
// Email templates
userVerification: {
emailSubject: '🍳 Verify your Everyone Cook account',
emailBody: 'Hello {username}, your verification code is: {####}',
emailStyle: cdk.aws_cognito.VerificationEmailStyle.CODE,
},
// Deletion protection for production
deletionProtection: this.config.environment === 'prod',
});
return userPool;
}
/**
* Create PostConfirmation Lambda Trigger
*
* Creates 3 DynamoDB entities after email verification:
* 1. Core Profile (PK=USER#{userId}, SK=PROFILE)
* 2. Privacy Settings (SK=PRIVACY_SETTINGS)
* 3. AI Preferences (SK=AI_PREFERENCES)
*/
private createPostConfirmationTrigger(
dynamoTable: cdk.aws_dynamodb.ITable
): cdk.aws_lambda.Function {
const trigger = new cdk.aws_lambda.Function(this, 'PostConfirmationTrigger', {
functionName: `EveryoneCook-${this.config.environment}-PostConfirmation`,
runtime: cdk.aws_lambda.Runtime.NODEJS_20_X,
handler: 'post-confirmation.handler',
code: cdk.aws_lambda.Code.fromAsset(
path.join(__dirname, '../../../services/auth-module/triggers/dist')
),
memorySize: 512,
timeout: cdk.Duration.seconds(30),
environment: {
DYNAMODB_TABLE_NAME: dynamoTable.tableName,
ENVIRONMENT: this.config.environment,
},
});
// Grant DynamoDB write permissions
dynamoTable.grantReadWriteData(trigger);
return trigger;
}
/**
* Create PreSignUp Lambda Trigger
*
* Handles cleanup of unverified users:
* - If user exists and UNCONFIRMED >24h → delete and allow new signup
* - If user exists and UNCONFIRMED <24h → reject with "wait 24h" message
* - If user doesn't exist → allow signup
*/
private createPreSignUpTrigger(): cdk.aws_lambda.Function {
const trigger = new cdk.aws_lambda.Function(this, 'PreSignUpTrigger', {
functionName: `EveryoneCook-${this.config.environment}-PreSignUp`,
runtime: cdk.aws_lambda.Runtime.NODEJS_20_X,
handler: 'pre-signup.handler',
code: cdk.aws_lambda.Code.fromAsset(
path.join(__dirname, '../../../services/auth-module/triggers/dist')
),
memorySize: 256,
timeout: cdk.Duration.seconds(10),
});
// Grant Cognito permissions
trigger.addToRolePolicy(
new cdk.aws_iam.PolicyStatement({
effect: cdk.aws_iam.Effect.ALLOW,
actions: ['cognito-idp:ListUsers', 'cognito-idp:AdminDeleteUser'],
resources: [`arn:aws:cognito-idp:${this.region}:${this.account}:userpool/*`],
})
);
return trigger;
}
/**
* Create Cognito User Pool Client for web application
*/
private createUserPoolClient(): cdk.aws_cognito.UserPoolClient {
const callbackUrls = this.getCallbackUrls();
const logoutUrls = this.getLogoutUrls();
const userPoolClient = new cdk.aws_cognito.UserPoolClient(
this, 'UserPoolClient', {
userPoolClientName: `EveryoneCook-Web-Client-${this.config.environment}`,
userPool: this.userPool,
// Auth flows
authFlows: {
userPassword: true, // USER_PASSWORD_AUTH
userSrp: true, // USER_SRP_AUTH (Secure Remote Password)
custom: false,
adminUserPassword: false,
},
// OAuth configuration (future social login)
oAuth: {
flows: {
authorizationCodeGrant: true,
implicitCodeGrant: false,
clientCredentials: false,
},
scopes: [
cdk.aws_cognito.OAuthScope.EMAIL,
cdk.aws_cognito.OAuthScope.OPENID,
cdk.aws_cognito.OAuthScope.PROFILE,
],
callbackUrls: callbackUrls,
logoutUrls: logoutUrls,
},
// Token validity
accessTokenValidity: cdk.Duration.hours(1),
idTokenValidity: cdk.Duration.hours(1),
refreshTokenValidity: cdk.Duration.days(30),
// Read attributes
readAttributes: new cdk.aws_cognito.ClientAttributes()
.withStandardAttributes({
email: true,
emailVerified: true,
givenName: true,
})
.withCustomAttributes('account_status', 'country'),
// Security settings
preventUserExistenceErrors: true, // Prevent enumeration attacks
enableTokenRevocation: true, // Allow token revocation
generateSecret: false, // No secret for web apps
}
);
return userPoolClient;
}
Registration Process:
1. User signs up → PreSignUp trigger
├─ Check if username/email exists
├─ If UNCONFIRMED >24h: Delete old user
├─ If UNCONFIRMED <24h: Reject with "wait 24h"
└─ Allow registration
2. User receives verification email → CustomMessage trigger
├─ Customize email template
└─ Send verification code
3. User verifies email → PostConfirmation trigger
├─ Create DynamoDB entities:
│ ├─ Core Profile (username, email, fullName, etc.)
│ ├─ Privacy Settings (default: private)
│ └─ AI Preferences (default settings)
└─ User account ready
4. User logs in → PreAuthentication trigger
├─ Check account_status
├─ If banned/suspended: Reject login
└─ Allow login
5. Login successful → PostAuthentication trigger
└─ Update lastLoginAt timestamp
Environments:
| Environment | Min Length | Requirements |
|---|---|---|
| Dev | 8 chars | Uppercase, lowercase, digits, symbols |
| Staging | 12 chars | Uppercase, lowercase, digits, symbols |
| Prod | 12 chars | Uppercase, lowercase, digits, symbols |
Example Valid Passwords:
MyP@ssw0rd123 (12 chars)Str0ng!Pass (11 chars, invalid for prod/staging)| Token Type | Validity | Purpose |
|---|---|---|
| Access Token | 1 hour | API authorization |
| ID Token | 1 hour | User identity claims |
| Refresh Token | 30 days | Renew access/ID tokens |
Token Refresh Flow:
Access token expires (1h) → Use refresh token → Get new access/ID tokens
Refresh token expires (30d) → User must login again
Purpose: Prevent “username already taken” errors for unverified users
Logic:
if (userExists && userStatus === 'UNCONFIRMED') {
const hoursSinceCreation = (now - userCreationDate) / (1000 * 60 * 60);
if (hoursSinceCreation > 24) {
// Delete expired unverified user
await deleteUser(username);
return allowSignUp();
} else {
// User still has time to verify
return rejectSignUp(`Please wait ${24 - hoursSinceCreation}h to register again`);
}
} else {
return allowSignUp();
}
DynamoDB Entities Created:
// 1. Core Profile
{
PK: "USER#{userId}",
SK: "PROFILE",
username: "john_doe",
email: "john@example.com",
fullName: "John Doe",
account_status: "active",
createdAt: "2025-01-01T00:00:00Z",
// ... other fields
}
// 2. Privacy Settings
{
PK: "USER#{userId}",
SK: "PRIVACY_SETTINGS",
profileVisibility: "private",
showEmail: false,
allowMessages: "friends",
// ... other settings
}
// 3. AI Preferences
{
PK: "USER#{userId}",
SK: "AI_PREFERENCES",
aiEnabled: true,
preferredLanguage: "en",
dietaryRestrictions: [],
// ... other preferences
}
After deployment, the stack exports the following values:
| Output Name | Value | Used By |
|---|---|---|
UserPoolId | ap-southeast-1_XXXXXXXXX | Backend Stack (Authorizer) |
UserPoolArn | arn:aws:cognito-idp:... | Lambda IAM policies |
UserPoolClientId | 1234567890abcdef | Frontend (Amplify config) |
CustomMessageFunctionArn | arn:aws:lambda:... | Monitoring |
PostConfirmationFunctionArn | arn:aws:lambda:... | Monitoring |
PreAuthenticationFunctionArn | arn:aws:lambda:... | Monitoring |
PostAuthenticationFunctionArn | arn:aws:lambda:... | Monitoring |
Before deploying, compile Lambda triggers to JavaScript:
cd D:\Project_AWS\everyonecook\services\auth-module\triggers
# Install dependencies
npm install
# Build TypeScript to JavaScript
npm run build
Expected output:
> auth-module-triggers@1.0.0 build
> tsc
Compiled successfully to dist/
dist/ folderNavigate to infrastructure directory:
cd D:\Project_AWS\everyonecook\infrastructure
Deploy Auth Stack to ap-southeast-1:
# Deploy Auth Stack
npx cdk deploy EveryoneCook-dev-Auth --context environment=dev
Expected output:
✨ Synthesis time: 7.23s
EveryoneCook-dev-Auth: deploying...
[████████████████████████████████████████] (9/9)
EveryoneCook-dev-Auth: creating CloudFormation changeset...
EveryoneCook-dev-Auth
✨ Deployment time: 180.45s
Outputs:
EveryoneCook-dev-Auth.UserPoolId = ap-southeast-1_a1B2c3D4e
EveryoneCook-dev-Auth.UserPoolClientId = 1a2b3c4d5e6f7g8h9i0j
EveryoneCook-dev-Auth.UserPoolArn = arn:aws:cognito-idp:ap-southeast-1:616580903213:userpool/...
EveryoneCook-dev-Auth.PostConfirmationFunctionArn = arn:aws:lambda:ap-southeast-1:...
EveryoneCook-dev-Auth.PreAuthenticationFunctionArn = arn:aws:lambda:ap-southeast-1:...
Stack ARN:
arn:aws:cloudformation:ap-southeast-1:616580903213:stack/EveryoneCook-dev-Auth/...
EveryoneCook-dev
Cognito User Pool showing sign-in options (username/email), MFA disabled, password policy, and deletion protection
Verify:
Click on the User Pool to view details:
User Pool configuration showing authentication settings, attributes, password policy, and security features
Check:
Go to User pool properties > Lambda triggers:
Lambda triggers configured for Pre sign-up, Custom message, Post confirmation, Pre authentication, and Post authentication
Verify 5 triggers:
EveryoneCook-dev-PreSignUpEveryoneCook-dev-CustomMessageEveryoneCook-dev-PostConfirmationEveryoneCook-dev-PreAuthenticationEveryoneCook-dev-PostAuthenticationGo to App integration > App clients:
User Pool Client showing auth flows, OAuth settings, token validity, callback URLs, and security settings
Verify:
Go to Lambda > Functions, find Auth triggers:
Lambda functions showing all 5 Cognito triggers with runtime Node.js 20.x, memory 256-512 MB, and timeout 10-30s
Verify:
Click on a Lambda function → Configuration > Permissions:
Lambda execution role showing permissions for DynamoDB (PostConfirmation), Cognito (PreSignUp), and CloudWatch Logs
Expected permissions:
| Resource | Configuration | Monthly Cost | Notes |
|---|---|---|---|
| Cognito User Pool | <50 MAU | $0 | First 50K MAU free |
| Lambda Triggers | 5 functions, low invocations | $0-1 | Free tier covers most |
| CloudWatch Logs | 7-day retention, 5 log groups | $0.50 | ~1GB logs |
| Total (Estimated) | ~$0.50-1.50/month | Very low for dev |
Production Estimate (1000 MAU):
From Core Stack:
dynamoTable: cdk.Fn.importValue('EveryoneCook-dev-DynamoDBTableName')
Backend Stack imports:
Core Stack → DynamoDB Table
│
▼
Auth Stack (creates Cognito + Lambda triggers)
│
├─► User Pool ID → Backend Stack (API Gateway Authorizer)
├─► User Pool Client ID → Frontend (Amplify config)
└─► Lambda triggers → User management workflows
Before proceeding to Backend Stack deployment:
dist/ folderTest sign-up with Cognito console:
Go to Cognito > User pools > EveryoneCook-dev > Users > Create user
Create a test user:
Username: testuser01
Email: your-email@example.com
Full Name: Test User
Temporary Password: TempP@ss123
Verify email sent:
Check your email for verification code.
Check Lambda logs:
# View PostConfirmation logs
aws logs tail /aws/lambda/EveryoneCook-dev-PostConfirmation --follow --region ap-southeast-1
Verify DynamoDB entries:
# Query user profile
aws dynamodb query \
--table-name EveryoneCook-dev-v2 \
--key-condition-expression "PK = :pk" \
--expression-attribute-values '{":pk":{"S":"USER#testuser01"}}' \
--region ap-southeast-1
Expected: 3 items (PROFILE, PRIVACY_SETTINGS, AI_PREFERENCES)
Login with test user:
Use AWS CLI to authenticate:
aws cognito-idp initiate-auth \
--auth-flow USER_PASSWORD_AUTH \
--client-id <USER_POOL_CLIENT_ID> \
--auth-parameters USERNAME=testuser01,PASSWORD=<password> \
--region ap-southeast-1
Check PreAuthentication logs:
aws logs tail /aws/lambda/EveryoneCook-dev-PreAuthentication --follow --region ap-southeast-1
Check PostAuthentication logs:
aws logs tail /aws/lambda/EveryoneCook-dev-PostAuthentication --follow --region ap-southeast-1
Create unverified user:
Sign up a user but don’t verify email.
Wait 24 hours (or modify trigger code to 1 minute for testing)
Try to sign up again with same username:
PreSignUp trigger should delete old user and allow new signup.
After successfully deploying the Auth Stack:
➡️ 5.4.5 Backend Stack - Create API Gateway, Lambda functions, and SQS queues
The Backend Stack will:
infrastructure/lib/stacks/auth-stack.tsservices/auth-module/triggers/infrastructure/lib/base-stack.tsinfrastructure/config/environment.ts