AWS Cloud Practitioner — Lab 08 of 08

Lab 08 — Email Verification & User Registration

Build a two-step email verification pipeline: form → verify email → DynamoDB confirmed record.

Advanced~$0 Free Tier2–4 HoursRequires Labs 03 + 04

Lab Overview

PREREQ: Complete Lab 03 and Lab 04 before this lab.

Build a two-step email verification and user registration pipeline. A user submits a form, receives a verification email, clicks the link, and only then is their record permanently saved to DynamoDB and the site owner notified. Unverified submissions automatically expire after 24 hours via DynamoDB TTL.

ServicePurposeFree Tier
Amazon SESSends the verification email to the user and notification to owner62,000 emails/mo free
Amazon DynamoDBStores PENDING registrations with TTL and CONFIRMED records permanently25 GB free forever
AWS Lambda (x2)RegistrationHandler starts the flow; ConfirmationHandler completes it1M requests/mo free
Amazon API GatewayPOST /register starts flow; GET /confirm completes it from email link1M calls/mo free
Amazon S3Hosts register2.html and confirmed2.htmlFree Tier
AWS IAMRole granting Lambda permission to use SES and DynamoDBAlways free

Architecture:

#What HappensAWS Service
1User submits name + email on register2.htmlS3 / Browser
2JavaScript POSTs to API Gateway POST /registerAPI Gateway
3Lambda generates a UUID verification token and saves PENDING recordDynamoDB
4Lambda sends a verification email to the userSES
5User clicks Verify My Email in the emailEmail Client
6Browser GETs API Gateway GET /confirm?token=UUIDAPI Gateway
7Lambda marks record CONFIRMED, removes TTL, emails ownerDynamoDB + SES
8Lambda redirects browser to confirmed2.htmlS3 / Browser

Step-by-Step Instructions

1
Amazon DynamoDB
Create the UserRegistrations Table
  1. DynamoDB → Tables → Create table
  2. Table name: UserRegistrations
  3. Partition key: token (String)
  4. Table settings: Customize → Capacity mode: On-demand → Create table
  5. Click the table → Additional settings tab → TTL → Enable
  6. TTL attribute name: ttl → Enable TTL
NOTE: The attribute name ttl must match exactly what the Lambda writes. DynamoDB auto-deletes PENDING records after 24 hours.
2
AWS IAM
Create the Lambda Execution Role
  1. IAM → Roles → Create role → AWS service → Lambda → Next
  2. Attach: AmazonSESFullAccess
  3. Attach: AmazonDynamoDBFullAccess
  4. Attach: AWSLambdaBasicExecutionRole
  5. Role name: LambdaRegistrationRole → Create role
3
AWS Lambda
Create RegistrationHandler
  1. Lambda → Create function → Author from scratch
  2. Name: RegistrationHandler → Runtime: Python 3.12
  3. Execution role: LambdaRegistrationRole → Create function
  4. Delete all code and paste the following. Replace SENDER_EMAIL and update API_BASE_URL after Step 4.
import json, boto3, uuid
from datetime import datetime, timezone, timedelta

ses      = boto3.client('ses', region_name='us-east-1')
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
table    = dynamodb.Table('UserRegistrations')

SENDER_EMAIL = 'no-reply@cloudpracticelabs.org'  # replace
API_BASE_URL = 'https://YOUR-API-ID.execute-api.us-east-1.amazonaws.com/prod'  # replace after Step 4

def lambda_handler(event, context):
    headers = {'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type'}
    try:
        body  = json.loads(event.get('body', '{}'))
        name  = body.get('name', '').strip()
        email = body.get('email', '').strip()
        if not name or not email:
            return {'statusCode': 400, 'headers': headers,
                    'body': json.dumps({'error': 'Name and email required.'})}
        token    = str(uuid.uuid4())
        ts       = datetime.now(timezone.utc).isoformat()
        ttl_time = int((datetime.now(timezone.utc) + timedelta(hours=24)).timestamp())
        confirm_url = f"{API_BASE_URL}/confirm?token={token}"
        table.put_item(Item={'token': token, 'name': name, 'email': email,
                             'status': 'PENDING', 'timestamp': ts, 'ttl': ttl_time})
        ses.send_email(
            Source=SENDER_EMAIL, Destination={'ToAddresses': [email]},
            Message={'Subject': {'Data': 'Please verify your email address'},
                     'Body': {'Html': {'Data':
                         f'<h2>Hi {name},</h2>'
                         f'<p>Click to verify your email:</p>'
                         f'<a href="{confirm_url}" style="background:#1e3a8a;color:white;padding:12px 24px;text-decoration:none;font-weight:bold;">Verify My Email</a>'
                         f'<p style="color:#888;">Link expires in 24 hours.</p>'
                     }}}
        )
        return {'statusCode': 200, 'headers': headers,
                'body': json.dumps({'message': f'Verification email sent to {email}.'})}
    except Exception as e:
        print(f'Error: {str(e)}')
        return {'statusCode': 500, 'headers': headers, 'body': json.dumps({'error': str(e)})}
  1. Click Deploy
4
AWS Lambda
Create ConfirmationHandler
  1. Lambda → Create function → ConfirmationHandler → Python 3.12 → LambdaRegistrationRole
  2. Delete all code and paste the following. Replace SENDER_EMAIL, OWNER_EMAIL, and SUCCESS_URL after Step 5.
import json, boto3, urllib.parse
from datetime import datetime, timezone
from boto3.dynamodb.conditions import Attr

ses      = boto3.client('ses', region_name='us-east-1')
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
table    = dynamodb.Table('UserRegistrations')

SENDER_EMAIL = 'no-reply@cloudpracticelabs.org'  # replace
OWNER_EMAIL  = 'fred.loomis@gmail.com'            # replace
SUCCESS_URL  = 'http://YOUR-BUCKET-URL/confirmed2.html'  # replace after Step 5

def lambda_handler(event, context):
    params = event.get('queryStringParameters') or {}
    token  = params.get('token', '').strip()
    if not token: return redirect('Missing token.')
    try:
        item = table.get_item(Key={'token': token}).get('Item')
        if not item: return redirect('Link is invalid or has expired (24 hours).')
        if item.get('status') == 'CONFIRMED':
            return redirect(f"Already verified: {item.get('email')}")
        name  = item.get('name', 'User')
        email = item.get('email', '')
        ts    = datetime.now(timezone.utc).isoformat()
        table.update_item(Key={'token': token},
            UpdateExpression='SET #s = :c, confirmedAt = :ts REMOVE #ttl',
            ExpressionAttributeNames={'#s': 'status', '#ttl': 'ttl'},
            ExpressionAttributeValues={':c': 'CONFIRMED', ':ts': ts},
            ConditionExpression=Attr('status').eq('PENDING'))
        ses.send_email(Source=SENDER_EMAIL, Destination={'ToAddresses': [OWNER_EMAIL]},
            Message={'Subject': {'Data': f'New registration: {name}'},
                     'Body': {'Text': {'Data': f'Name: {name}
Email: {email}
Time: {ts}'}}})
        return {'statusCode': 302, 'headers': {'Location': SUCCESS_URL}, 'body': ''}
    except Exception as e:
        print(f'Error: {str(e)}')
        return redirect(str(e))

def redirect(msg):
    return {'statusCode': 302,
            'headers': {'Location': f'{SUCCESS_URL}?msg={urllib.parse.quote(msg)}'},
            'body': ''}
  1. Click Deploy
5
Amazon API Gateway
Create the API with Two Routes
  1. API Gateway → Create API → HTTP API → Build
  2. Integration: Lambda → RegistrationHandler → API name: RegistrationAPI → Next
  3. Method: POST → path: /register → Next → Stage: prod → Create
  4. Copy the Invoke URL from the summary page

Add GET /confirm

  1. Routes → Create → Method: GET → path: /confirm → Create
  2. Click GET /confirm → Attach integration → Create and attach → Lambda → ConfirmationHandler → Create

Enable CORS

  1. CORS → Allow-Origin: * → Allow-Methods: POST, GET, OPTIONS → Allow-Headers: content-type → Save

Update RegistrationHandler

  1. Lambda → RegistrationHandler → Code tab
  2. Replace YOUR-API-ID in API_BASE_URL with your actual Invoke URL (keep only /prod at end, NOT /register)
  3. Click Deploy
6
Amazon S3
Upload register2.html and confirmed2.html
  1. Upload both files to your S3 bucket
  2. Lambda → ConfirmationHandler → Code tab
  3. Update SUCCESS_URL to your S3 website URL + /confirmed2.html
  4. Click Deploy
7
End-to-End Testing
Test the Complete Registration Flow

Happy path

  1. Open your S3 URL + /register2.html
  2. Enter name and email → click Send Verification Email
  3. DynamoDB → UserRegistrations → Explore items — PENDING record appears
  4. Check inbox — verification email arrives
  5. Click Verify My Email in the email
  6. Browser redirects to confirmed2.html showing success
  7. Owner inbox receives notification email
  8. DynamoDB record shows status: CONFIRMED, ttl attribute removed

Error path

  1. Click the verification link a second time
  2. You should see the already-verified notice — idempotency confirmed
TIP: If the form hangs: press F12 → Console tab and look for errors. The most common issue is CORS on API Gateway.

Verification Checklist

What You Learned

Lab Cleanup

IMPORTANT: Delete all resources when finished.
#ResourceHow to Delete
1DynamoDB TableDynamoDB → Tables → UserRegistrations → Delete
2RegistrationHandlerLambda → RegistrationHandler → Actions → Delete
3ConfirmationHandlerLambda → ConfirmationHandler → Actions → Delete
4API GatewayAPI Gateway → RegistrationAPI → Actions → Delete
5IAM RoleIAM → Roles → LambdaRegistrationRole → Delete
6S3 FilesS3 → delete register2.html and confirmed2.html
7CloudWatch LogsCloudWatch → Log groups → delete RegistrationHandler and ConfirmationHandler groups