Exam Guide: Developer - Associate
๐๏ธ Domain 2: Security
๐ Task 1: Implement Authentication And/Or Authorisation For Applications And AWS
Services
Authentication (who are you?) and Authorisation (what can you do?) are central to the DVA-C02, and just Cloud Development in general.
You need to know Cognito, IAM Roles and Policies, STS, Lambda Authorizers, and how to secure microservice-to-microservice communication or communication within a Microservices Architectural Environment.
๐ Concepts
Authentication vs Authorisation
| Term | What It Answers | AWS Services |
|---|---|---|
| Authentication | Who are you? | Cognito User Pools, IAM |
| Authorisation | What can you do? | IAM Policies, Cognito Identity Pools, Lambda Authorizers |
Amazon Cognito
User Pools vs Identity Pools
| Feature | User Pool | Identity Pool |
|---|---|---|
| Purpose | Authentication (sign up, sign in) | Authorisation (temporary AWS credentials) |
| Returns | JWT tokens (ID, access, refresh) | AWS credentials (access key, secret, session token) |
| Use When | You need a user directory | Users need to call AWS services directly |
| Federation | Google, Facebook, SAML, OIDC | Google, Facebook, SAML, User Pools |
User Pools = authentication (get tokens).
Identity Pools = authorisation (get AWS credentials).
They're often used together but serve different purposes.
The Three Cognito Tokens
| Token | Contains | Use For |
|---|---|---|
| ID Token | User identity claims (name, email, groups) | Your application to identify the user |
| Access Token | Scopes and permissions | API authorisation |
| Refresh Token | Long-lived credential | Getting new ID/access tokens without re-authentication |
API Gateway Authorisation Options
| Option | What It Does | When to Use |
|---|---|---|
| None | No auth | Public APIs |
| IAM | SigV4 signing | Internal/service-to-service calls |
| Cognito Authorizer | Validates JWTs from User Pool | Simple JWT validation |
| Lambda Authorizer (Token) | Custom logic on the token | Complex auth, external IDPs |
| Lambda Authorizer (Request) | Custom logic on full request | Auth based on headers, query strings, source IP |
IAM Roles vs Users
| Type | Credentials | Use Case |
|---|---|---|
| IAM User | Long-lived (access key + secret) | Humans, legacy CLI access |
| IAM Role | Temporary (via STS AssumeRole) | Applications, EC2, Lambda, cross-account |
Roles are always preferred for applications. Never hardcode access keys.
Lambda uses an execution role, EC2 uses an instance profile, ECS uses a task role.
All of these give your code temporary credentials automatically.
STS (Security Token Service) Operations
Operation |
Use Case |
|---|---|
AssumeRole |
Same or cross-account role assumption |
AssumeRoleWithWebIdentity |
Exchange OAuth/OIDC token for AWS credentials |
AssumeRoleWithSAML |
Exchange SAML assertion for AWS credentials |
GetSessionToken |
Temporary credentials (for MFA-protected API calls) |
Credential Resolution Order (SDK Default Chain)
The AWS SDK looks for credentials in this order:
1. Environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
2. Shared credentials file (~/.aws/credentials)
3. AWS config file (~/.aws/config)
4. Container credentials (ECS task role)
5. Instance profile (EC2 role)
6. SSO credentials (IAM Identity Center)
๐๏ธ Build A Secure Notes API
Now lets put these concepts into practice, by building a Secure Notes API with Cognito authentication:
- A Cognito User Pool with Managed Login for sign-up/sign-in
- An App Client configured with the authorization code grant flow
- A Lambda function that stores and retrieves notes
- An API Gateway REST API protected by a Cognito authorizer
- Test users signing in and calling the API with JWT tokens
Prerequisites
Part I
Create the Cognito User Pool
The Cognito console now uses a streamlined, application-focused setup wizard with Managed Login (the successor to the classic Hosted UI). The wizard creates your user pool and app client in one flow.
Step 01: Open the Cognito console
Step 02: Click Create user pool
Step 03: Set up resources for your application
Application type: Traditional web application
Name your application: My Notes App
Options for sign-in identifiers: Email
Self-registration: โ
Required attributes for sign-up: email name
Click Create user directory
โ Green banner: Your application "My Notes App" and user pool "User pool - gcvhqy" have been created successfully! Follow the instruction to continue the setup.
๐กThe new console creates the user pool, app client, and managed login domain all in one step. "Traditional web application" configures the authorization code grant flow with a client secret by default. This is the recommended pattern for server-side apps.
Step 04: Get Your Pool Details
After creation, click on the user pool and note:
-
User pool ID (looks like
us-east-1_ABC123XYZ) - App client ID (from the โผ Applications tab โ App clients)
- Cognito domain (from the โผ Branding tab โ Domain )
Part II
Create a Test User
Step 01: In the User Pool, click the โผ Users management tab
Click Users
Step 02: Users
Click Create user
User Information
-
Invitation message:
Send an email invitation -
Email address:
Use a real email you can access -
Temporary password:
Generate a password
Click Create user
โ Green banner: User username@example.com has been created successfully.
You'll receive an email with a temporary password.
โ ๏ธ Don't log in yet. We'll do that through the Managed Login in a moment.
Part III
Test with Managed Login (Authorization Code Flow)
Step 01: View the hosted UI URL
From the โผ Applications tab โ App clients
Click View login page
https://YOUR_COGNITO_DOMAIN.auth.us-east-1.amazoncognito.com/login?
client_id=YOUR_APP_CLIENT_ID&
response_type=code&
scope=email+openid+profile&
redirect_uri=YOUR_CALLBACK_URL
Step 02: Sign in
Sign in with the user credentials from the email
Cognito will force you to set a permanent password
Step 03: Change password
โ : Success page
Successfully signed in
This is the default redirect page for Amazon Cognito user pools.
You're seeing this page because your Amazon Cognito app client doesn't have a return URL set.
Step 04: After login, you'll be redirected to https://d84l1y8p4kdic.cloudfront.net/?code=2f927666-cd13-4e94-9af9-58887a9d9cd3...
๐กCopy the code value from the URL fragment.
You'll need it to call the API.
โ ๏ธ This code is NOT a token. It's a one-time authorization code that expires in a few minutes. You need to exchange it for actual JWT tokens in the next step.
Step 05: Exchange the Code for Tokens
Use the Cognito token endpoint to exchange the authorization code for JWT tokens:
curl -X POST "https://YOUR_COGNITO_DOMAIN.auth.us-east-1.amazoncognito.com/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code&code=YOUR_AUTH_CODE&client_id=YOUR_APP_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&redirect_uri=YOUR_CALLBACK_URL"
โ ๏ธ If your app client has a client secret (default for "Traditional web application"), you also need to include a
client_secretparameter, or pass the credentials as a Basic auth header:-H "Authorization: Basic BASE64(client_id:client_secret)"
Expected response:
{
"id_token": "eyJraWQiOiJ...",
"access_token": "eyJraWQiOiJ...",
"refresh_token": "eyJjdHkiOiJ...",
"expires_in": 3600,
"token_type": "Bearer"
}
๐ก Copy the id_token value. You'll need it to call the API.
response_type=tokenuses the implicit grant flow (returns tokens directly).response_type=codeuses the authorization code flow (returns a code you exchange for tokens via the token endpoint).
Auth code is more secure for production apps.
Part IV
Create the DynamoDB Table
Step 01: Open the DynamoDB console โ Create table
Step 02: Create table
-
Table name:
Notes -
Partition key:
userId(String) -
Sort key:
noteId(String) -
Table settings:
Default settings
Click Create table
โ Green banner: The Notes table was created successfully.
Part V
Create the Notes Lambda Function
Step 01: Open Lambda โ Create function
Step 02: Create function
-
Function name:
NotesAPI -
Runtime:
Python 3.12
Click Create function
โ Green banner: Successfully created the function "NotesAPI"
Step 03: Configuration โ General configuration โ click Edit
-
Memory:
256 MB -
Timeout:
0 min 10 sec
Click Save
โ Green banner: Successfully updated the function "NotesAPI".
Step 04: Add DynamoDB Permissions
Configuration โ Permissions โ click the Role name
Step 05: Add permissions โผ โ Attach policies โ search AmazonDynamoDBFullAccess โ attach
โ Green banner: Policies have been successfully attached to role.
Step 06: Function Code
import json
import boto3
import uuid
from datetime import datetime
from boto3.dynamodb.conditions import Key
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Notes')
def lambda_handler(event, context):
"""
Notes API with user isolation.
Each user can only access their own notes โ enforced by extracting
the userId from the JWT claims and using it as the partition key.
"""
http_method = event.get('httpMethod', 'GET')
path = event.get('path', '/')
# Extract the authenticated user from Cognito claims
# API Gateway puts the JWT claims here after the authorizer validates the token
try:
claims = event['requestContext']['authorizer']['claims']
user_id = claims['sub'] # Cognito's unique user ID
user_email = claims.get('email', 'unknown')
except KeyError:
return response(401, {'error': 'Unauthorized'})
print(f"Request from user {user_email} (ID: {user_id})")
try:
if path == '/notes' and http_method == 'GET':
return list_notes(user_id)
elif path == '/notes' and http_method == 'POST':
body = json.loads(event.get('body') or '{}')
return create_note(user_id, body)
elif path.startswith('/notes/') and http_method == 'GET':
note_id = path.split('/')[-1]
return get_note(user_id, note_id)
elif path.startswith('/notes/') and http_method == 'DELETE':
note_id = path.split('/')[-1]
return delete_note(user_id, note_id)
else:
return response(404, {'error': 'Not found'})
except Exception as e:
print(f"Error: {str(e)}")
return response(500, {'error': 'Internal server error'})
def list_notes(user_id):
"""List all notes for the authenticated user."""
result = table.query(
KeyConditionExpression=Key('userId').eq(user_id)
)
return response(200, {
'notes': result['Items'],
'count': result['Count']
})
def create_note(user_id, body):
"""Create a new note owned by the authenticated user."""
note_id = str(uuid.uuid4())
note = {
'userId': user_id, # โ partition key ensures data isolation
'noteId': note_id,
'title': body.get('title', 'Untitled'),
'content': body.get('content', ''),
'createdAt': datetime.utcnow().isoformat()
}
table.put_item(Item=note)
return response(201, note)
def get_note(user_id, note_id):
"""Get a specific note. User can only access their own notes."""
result = table.get_item(Key={'userId': user_id, 'noteId': note_id})
item = result.get('Item')
if not item:
return response(404, {'error': 'Note not found'})
return response(200, item)
def delete_note(user_id, note_id):
"""Delete a note. User can only delete their own notes."""
table.delete_item(Key={'userId': user_id, 'noteId': note_id})
return response(204, {})
def response(status_code, body):
return {
'statusCode': status_code,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps(body, default=str)
}
Step 07: Click Deploy.
Notice the user isolation pattern. The
userIdcomes from the JWT claims (set by Cognito, validated by API Gateway), not from the request body. Users can't forge this value because API Gateway won't invoke Lambda without a valid token. This is application-level authorization.
Part VI
Create the API Gateway API
Create the API
Step 01: Open API Gateway โ Create API โ REST API โ Build
Step 02: Create REST API
New API-
API name:
NotesAPI - API endpoint type: Regional
-
Security policy - new:
TLS_1_0
Click Create API
โ Green banner: Successfully created REST API 'NotesAPI (xoq5e4tu3j)'.
Create the Cognito Authorizer
Step 03: In the left sidebar, click Authorizers
Click Create authorizer
Step 04: Create authorizer
-
Authorizer name:
CognitoAuth -
Authorizer type:
Cognito -
Cognito user pool: Select
NotesUserPool -
Token source:
Authorization(this is the header name)
Click Create authorizer
โ Green banner: Successfully created authorizer 'CognitoAuth'.
Create the Resources and Methods
Step 05: Click Resources โ Create resource
Step 06: Create resource
-
Resource path:
/ -
Resource name:
notes โ CORS (Cross Origin Resource Sharing)
Click Create resource
โ Green banner: Successfully created resource '/notes'
Step 07: Now create a child resource for /notes/{noteId}:
Select the /notes resource โ Create resource
-
Resource path:
/notes/ -
Resource name:
{noteID}(proxy resource)
Click Create resource
โ Green banner: Successfully created resource '/notes/{noteID}'
Add the Methods
Step 08: For each method below, click the resource, then Create method:
Step 09: POST /notes (create a note)
-
โผ Method type:
POST -
Integration type:
Lambda Function -
Lambda proxy integration:
โ -
Lambda function:
NotesAPI
โ Green banner: Successfully created method 'POST' in 'notes'.
Step 10: GET /notes (list notes)
-
โผ Method type:
GET -
Integration type:
Lambda Function -
Lambda proxy integration:
โ -
Lambda function:
NotesAPI
โ Green banner: Successfully created method 'GET' in 'notes'.
Step 11: GET /notes/{noteId} (get one note)
-
โผ Method type:
GET -
Integration type:
Lambda Function -
Lambda proxy integration:
โ -
Lambda function:
NotesAPI
โ Green banner: Successfully created method 'GET' in 'noteID'.
Step 12: DELETE /notes/{noteId} (delete a note)
-
โผ Method type:
DELETE -
Integration type:
Lambda Function -
Lambda proxy integration:
โ -
Lambda function:
NotesAPI
โ Green banner: Successfully created method 'DELETE' in 'noteID'.
Attach the Authorizer to Each Method
Step 13: For each of the four methods:
13.1: Click the method (e.g., GET under /notes)
13.2: Click Method request โ Edit
13.3: Set Authorization to CognitoAuth
13.4: Click Save
โ Green banners: Successfully edited method request for โxโ
Deploy the API
Step 14: Click Deploy API
Stage: Create a new stage called dev
Click Deploy
โ Green banner: Successfully created deployment for NotesAPI.
Step 15: Copy the Invoke URL
Part VII
Test the Authenticated API
Test One: Unauthenticated Request (Should Fail)
curl -X POST https://YOUR_API_URL/dev/notes \
-H "Content-Type: application/json" \
-d '{"title": "My first note", "content": "Hello world"}'
Expected response:
{"message": "Unauthorized"}
โ ๏ธ API Gateway rejected the request because there's no JWT token. Lambda was never invoked.
Test Two: Authenticated Request (Should Succeed)
Use the id_token you got from the token exchange in Part III:
curl -X POST https://YOUR_API_URL/dev/notes \
-H "Authorization: eyJraWQiOiJ..." \
-d '{"title": "My first note", "content": "Hello world"}'
Expected response:
{
"userId": "abc-123-def-456",
"noteId": "7f8e9d0c-...",
"title": "My first note",
"content": "Hello world",
"createdAt": "2026-04-24T10:00:00"
}
๐ก Notice the
userIdwas pulled from the JWT claims. The user didn't have to send it.
Test Three: List Your Notes
curl https://YOUR_API_URL/dev/notes \
-H "Authorization: YOUR_ID_TOKEN"
You'll only see notes belonging to the authenticated user.
Test Four: Invalid Token (Should Fail)
curl https://YOUR_API_URL/dev/notes \
-H "Authorization: invalid.token.here"
Expected response:
{"message": "Unauthorized"}
Part VIII
Build a Lambda Authorizer (Advanced)
Cognito authorizers are simple.
They just validate JWT signatures.
Lambda authorizers let you implement custom logic.
Let's see how.
Create the Authorizer Function
Step 01: Open Lambda โ Create function
Step 02: Create function
-
Function name:
NotesLambdaAuthorizer -
Runtime:
Python 3.12
Click Create function
โ Green banner: Successfully created the function "NotesLambdaAuthorizer".
Step 03: Paste this code
import json
def lambda_handler(event, context):
"""
Custom Lambda authorizer.
Returns an IAM policy that allows or denies the API call.
Lambda authorizer results can be cached (default 5 min, max 1 hour)
to reduce the number of Lambda invocations.
"""
token = event.get('authorizationToken', '').replace('Bearer ', '')
method_arn = event['methodArn']
print(f"Authorizing request with token: {token[:20]}...")
# In production, validate the JWT signature using the Cognito JWKS
# For this demo, we just check if the token starts with a specific prefix
if not token or token == 'invalid':
# Deny access
raise Exception('Unauthorized')
# Simulate extracting user info from the token
user_id = 'demo-user-001'
user_role = 'admin' if 'admin' in token else 'user'
# Build the policy based on the user's role
if user_role == 'admin':
effect = 'Allow'
# Admin can access everything in the API
resource = method_arn.rsplit('/', 2)[0] + '/*/*'
else:
effect = 'Allow'
# Regular user can only access the specific method
resource = method_arn
return {
'principalId': user_id,
'policyDocument': {
'Version': '2012-10-17',
'Statement': [{
'Action': 'execute-api:Invoke',
'Effect': effect,
'Resource': resource
}]
},
'context': {
'userId': user_id,
'userRole': user_role
}
}
Step 04: Click Deploy
โ Green banner: Successfully updated the function "NotesLambdaAuthorizer"
Step 05: Attach It to the API (Walkthrough Only)
In API Gateway:
1. Authorizers โ Create authorizer
2. Configure:
-
Name:
LambdaAuth - Authorizer type: Lambda
-
Lambda function:
NotesLambdaAuthorizer - Lambda event payload: Token
-
Token source:
Authorization - Authorization caching: 300 seconds 3. Create authorizer
You could switch methods to use this instead of the Cognito authorizer. We won't switch for this tutorial. The Cognito authorizer is already doing its job.
๐กLambda authorizer results are cached (up to 1 hour). This reduces Lambda invocations but means permission changes aren't immediate. Two types:
- Token-based: receives the token, caches by token value
- Request-based: receives the full request, caches by specified identity sources.
๐๏ธ What You Built | ๐Exam Concepts Recap
| What You Built | Exam Concept |
|---|---|
| Created a Cognito User Pool | Authentication via managed user directory |
| Configured the Cognito Hosted UI | OAuth 2.0 authorization code / implicit grant flows |
| Signed in and received ID, access, and refresh tokens | The three Cognito token types and their purposes |
| Attached a Cognito Authorizer to API Gateway methods | JWT validation at the API layer |
Extracted sub and email from JWT claims in Lambda |
Passing identity from API Gateway to Lambda |
Used userId as the DynamoDB partition key |
Application-level authorization (data isolation) |
| Built a Lambda authorizer with custom logic | Fine-grained authorization |
| Returned an IAM policy from the Lambda authorizer | How Lambda authorizers control access |
| Set the Lambda authorizer cache TTL | Performance vs permission freshness trade-off |
| Tested with and without a valid token | API Gateway rejects unauthenticated requests before Lambda |
โ ๏ธ Clean Up Protocol
-
API Gateway โ Delete
NotesAPI -
Lambda โ Delete
NotesAPI,NotesLambdaAuthorizer -
DynamoDB โ Delete the
Notestable - Cognito โ Delete the User Pool (also delete the domain first)
- IAM โ Delete the Lambda execution roles
- CloudWatch โ Delete the log groups
Key Takeaways
- Cognito User Pools = authentication (tokens). Identity Pools = authorization (AWS credentials).
- Three tokens: ID (user identity), Access (API auth), Refresh (getting new tokens).
- Cognito Authorizer = simple JWT validation. Lambda Authorizer = custom logic.
- Lambda authorizer caching: (up to 1 hour) reduces invocations but delays permission changes.
- Never hardcode credentials: use IAM roles (execution role for Lambda, task role for ECS, instance profile for EC2).
- STS AssumeRole for cross-account access and temporary credentials.
- SigV4 signing is used for IAM authorization on API Gateway (service-to-service).
- Credential resolution order: env vars โ credentials file โ config โ container โ instance profile.
-
JWT claims are passed to Lambda via
event.requestContext.authorizer.claims. - Application-level authorization: use the authenticated user ID as the partition key to enforce data isolation.
Additional Resources
- Authentication
- Authorisation
- Authentication vs Authorisation
- Amazon Cognito User Pools
- Control access to a REST API using Amazon Cognito user pools as authorizer
- Use API Gateway Lambda authorizers
- Temporary security credentials in IAM
- Identity and access management for API Gateway
- Amazon Cognito Identity Pools
๐๏ธ
Top comments (0)