AWS Cognito: User Identity and Access Management
Implementing authentication and authorisation with AWS Cognito. User pools, identity pools, OAuth flows, MFA, and integration with API Gateway.
AWS Cognito: User Identity and Access Management
AWS Cognito provides authentication, authorisation, and user management for web and mobile applications. It handles user sign-up, sign-in, and access control, integrating seamlessly with other AWS services and external identity providers.
Cognito Components
User Pools vs Identity Pools
Cognito Architecture:
┌──────────────────────────────────────────────────────────────┐
│ User Pool │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ User Directory │ │
│ │ - Sign-up / Sign-in │ │
│ │ - MFA │ │
│ │ - Password policies │ │
│ │ - Email/SMS verification │ │
│ │ - Social identity providers (Google, Facebook) │ │
│ │ - SAML/OIDC enterprise providers │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ [JWT Tokens] │
│ (ID, Access, Refresh) │
└───────────────────────────┬──────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Identity Pool │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ AWS Credentials │ │
│ │ - Exchange tokens for temporary AWS credentials │ │
│ │ - IAM role mapping │ │
│ │ - Fine-grained access control │ │
│ │ - Guest access (unauthenticated) │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘When to Use Each
| Scenario | User Pool | Identity Pool |
|---|---|---|
| User authentication | Yes | No |
| API authorisation (JWT) | Yes | No |
| Access AWS services | No | Yes |
| Social login | Yes | Yes |
| Guest access | No | Yes |
| Custom attributes | Yes | No |
User Pool Configuration
Terraform Setup
# cognito.tf
resource "aws_cognito_user_pool" "main" {
name = "${var.project}-user-pool"
# Sign-in options
username_attributes = ["email"]
auto_verified_attributes = ["email"]
# Password policy
password_policy {
minimum_length = 12
require_lowercase = true
require_uppercase = true
require_numbers = true
require_symbols = true
temporary_password_validity_days = 7
}
# MFA configuration
mfa_configuration = "OPTIONAL"
software_token_mfa_configuration {
enabled = true
}
# Account recovery
account_recovery_setting {
recovery_mechanism {
name = "verified_email"
priority = 1
}
}
# Email configuration
email_configuration {
email_sending_account = "DEVELOPER"
from_email_address = "noreply@${var.domain_name}"
source_arn = aws_ses_email_identity.noreply.arn
}
# Custom attributes
schema {
name = "tenant_id"
attribute_data_type = "String"
mutable = false
string_attribute_constraints {
min_length = 1
max_length = 256
}
}
schema {
name = "role"
attribute_data_type = "String"
mutable = true
string_attribute_constraints {
min_length = 1
max_length = 50
}
}
# User verification
verification_message_template {
default_email_option = "CONFIRM_WITH_CODE"
email_subject = "Your verification code"
email_message = "Your verification code is {####}"
}
# Lambda triggers
lambda_config {
pre_sign_up = aws_lambda_function.pre_signup.arn
post_confirmation = aws_lambda_function.post_confirmation.arn
pre_token_generation = aws_lambda_function.pre_token.arn
custom_message = aws_lambda_function.custom_message.arn
pre_authentication = aws_lambda_function.pre_auth.arn
post_authentication = aws_lambda_function.post_auth.arn
}
# Deletion protection
deletion_protection = var.environment == "production" ? "ACTIVE" : "INACTIVE"
tags = var.tags
}
# App Client
resource "aws_cognito_user_pool_client" "web" {
name = "${var.project}-web-client"
user_pool_id = aws_cognito_user_pool.main.id
# OAuth configuration
allowed_oauth_flows = ["code"]
allowed_oauth_flows_user_pool_client = true
allowed_oauth_scopes = ["email", "openid", "profile"]
callback_urls = var.callback_urls
logout_urls = var.logout_urls
supported_identity_providers = ["COGNITO", "Google"]
# Token configuration
access_token_validity = 1 # hours
id_token_validity = 1 # hours
refresh_token_validity = 30 # days
token_validity_units {
access_token = "hours"
id_token = "hours"
refresh_token = "days"
}
# Security
generate_secret = false
prevent_user_existence_errors = "ENABLED"
enable_token_revocation = true
enable_propagate_additional_user_context_data = true
# Read/write attributes
read_attributes = ["email", "email_verified", "custom:tenant_id", "custom:role"]
write_attributes = ["email", "custom:role"]
explicit_auth_flows = [
"ALLOW_REFRESH_TOKEN_AUTH",
"ALLOW_USER_SRP_AUTH"
]
}
# Domain
resource "aws_cognito_user_pool_domain" "main" {
domain = var.cognito_domain
certificate_arn = aws_acm_certificate.auth.arn
user_pool_id = aws_cognito_user_pool.main.id
}Lambda Triggers
Pre-Sign-Up Validation
// pre-signup.ts
import { PreSignUpTriggerEvent } from 'aws-lambda';
export const handler = async (
event: PreSignUpTriggerEvent
): Promise<PreSignUpTriggerEvent> => {
const email = event.request.userAttributes.email;
// Domain validation
const allowedDomains = process.env.ALLOWED_DOMAINS?.split(',') || [];
if (allowedDomains.length > 0) {
const domain = email.split('@')[1];
if (!allowedDomains.includes(domain)) {
throw new Error('Registration not allowed for this email domain');
}
}
// Auto-confirm for specific domains
const autoConfirmDomains = process.env.AUTO_CONFIRM_DOMAINS?.split(',') || [];
if (autoConfirmDomains.includes(email.split('@')[1])) {
event.response.autoConfirmUser = true;
event.response.autoVerifyEmail = true;
}
return event;
};Pre-Token Generation
// pre-token.ts
import { PreTokenGenerationTriggerEvent } from 'aws-lambda';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';
const docClient = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export const handler = async (
event: PreTokenGenerationTriggerEvent
): Promise<PreTokenGenerationTriggerEvent> => {
const userId = event.request.userAttributes.sub;
// Fetch additional user data from DynamoDB
const userData = await docClient.send(new GetCommand({
TableName: process.env.USERS_TABLE,
Key: { userId }
}));
// Add custom claims to the token
event.response.claimsOverrideDetails = {
claimsToAddOrOverride: {
'custom:permissions': JSON.stringify(userData.Item?.permissions || []),
'custom:organization': userData.Item?.organizationId || '',
'custom:subscription_tier': userData.Item?.subscriptionTier || 'free'
},
claimsToSuppress: ['email_verified'] // Remove unnecessary claims
};
// Add groups (appear in cognito:groups claim)
if (userData.Item?.groups) {
event.response.claimsOverrideDetails.groupOverrideDetails = {
groupsToOverride: userData.Item.groups
};
}
return event;
};Post-Confirmation
// post-confirmation.ts
import { PostConfirmationTriggerEvent } from 'aws-lambda';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
const docClient = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export const handler = async (
event: PostConfirmationTriggerEvent
): Promise<PostConfirmationTriggerEvent> => {
// Only process sign-up confirmation
if (event.triggerSource !== 'PostConfirmation_ConfirmSignUp') {
return event;
}
const { sub, email, 'custom:tenant_id': tenantId } = event.request.userAttributes;
// Create user record in DynamoDB
await docClient.send(new PutCommand({
TableName: process.env.USERS_TABLE,
Item: {
userId: sub,
email,
tenantId: tenantId || 'default',
createdAt: new Date().toISOString(),
status: 'active',
subscriptionTier: 'free',
permissions: ['read:own'],
groups: ['users']
}
}));
// Send welcome email
await sendWelcomeEmail(email);
// Notify admin
await notifyNewUser({ userId: sub, email });
return event;
};Integration with API Gateway
JWT Authoriser
# api-gateway.tf
resource "aws_apigatewayv2_authorizer" "cognito" {
api_id = aws_apigatewayv2_api.main.id
authorizer_type = "JWT"
identity_sources = ["$request.header.Authorization"]
name = "cognito-authorizer"
jwt_configuration {
issuer = "https://cognito-idp.${var.region}.amazonaws.com/${aws_cognito_user_pool.main.id}"
audience = [aws_cognito_user_pool_client.web.id]
}
}
resource "aws_apigatewayv2_route" "protected" {
api_id = aws_apigatewayv2_api.main.id
route_key = "GET /api/protected"
target = "integrations/${aws_apigatewayv2_integration.lambda.id}"
authorization_type = "JWT"
authorizer_id = aws_apigatewayv2_authorizer.cognito.id
# Require specific scope
authorization_scopes = ["profile"]
}Extracting Claims in Lambda
// handler.ts
import { APIGatewayProxyEventV2WithJWTAuthorizer } from 'aws-lambda';
export const handler = async (
event: APIGatewayProxyEventV2WithJWTAuthorizer
) => {
// JWT claims are automatically extracted
const claims = event.requestContext.authorizer.jwt.claims;
const userId = claims.sub as string;
const email = claims.email as string;
const groups = claims['cognito:groups'] as string[] || [];
const tenantId = claims['custom:tenant_id'] as string;
const permissions = JSON.parse(claims['custom:permissions'] as string || '[]');
// Check authorization
if (!groups.includes('admin') && !permissions.includes('write:all')) {
return {
statusCode: 403,
body: JSON.stringify({ error: 'Insufficient permissions' })
};
}
// Process request with user context
const result = await processRequest({
userId,
tenantId,
permissions
});
return {
statusCode: 200,
body: JSON.stringify(result)
};
};Frontend Integration
React Authentication Hook
// useAuth.ts
import {
CognitoUserPool,
CognitoUser,
AuthenticationDetails,
CognitoUserSession
} from 'amazon-cognito-identity-js';
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
const poolData = {
UserPoolId: process.env.NEXT_PUBLIC_COGNITO_USER_POOL_ID!,
ClientId: process.env.NEXT_PUBLIC_COGNITO_CLIENT_ID!
};
const userPool = new CognitoUserPool(poolData);
interface AuthState {
isAuthenticated: boolean;
isLoading: boolean;
user: CognitoUser | null;
session: CognitoUserSession | null;
}
interface AuthContextType extends AuthState {
signIn: (email: string, password: string) => Promise<void>;
signUp: (email: string, password: string, attributes?: Record<string, string>) => Promise<void>;
confirmSignUp: (email: string, code: string) => Promise<void>;
signOut: () => void;
getAccessToken: () => Promise<string | null>;
}
const AuthContext = createContext<AuthContextType | null>(null);
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState<AuthState>({
isAuthenticated: false,
isLoading: true,
user: null,
session: null
});
useEffect(() => {
const currentUser = userPool.getCurrentUser();
if (currentUser) {
currentUser.getSession((err: Error | null, session: CognitoUserSession | null) => {
if (err || !session?.isValid()) {
setState({ isAuthenticated: false, isLoading: false, user: null, session: null });
} else {
setState({ isAuthenticated: true, isLoading: false, user: currentUser, session });
}
});
} else {
setState(prev => ({ ...prev, isLoading: false }));
}
}, []);
const signIn = async (email: string, password: string): Promise<void> => {
const cognitoUser = new CognitoUser({
Username: email,
Pool: userPool
});
const authDetails = new AuthenticationDetails({
Username: email,
Password: password
});
return new Promise((resolve, reject) => {
cognitoUser.authenticateUser(authDetails, {
onSuccess: (session) => {
setState({
isAuthenticated: true,
isLoading: false,
user: cognitoUser,
session
});
resolve();
},
onFailure: (err) => {
reject(err);
},
newPasswordRequired: (userAttributes) => {
// Handle new password required
reject(new Error('New password required'));
},
mfaRequired: (challengeName) => {
// Handle MFA
reject(new Error('MFA required'));
}
});
});
};
const signUp = async (
email: string,
password: string,
attributes: Record<string, string> = {}
): Promise<void> => {
const attributeList = Object.entries(attributes).map(([key, value]) => ({
Name: key,
Value: value
}));
return new Promise((resolve, reject) => {
userPool.signUp(email, password, attributeList, [], (err, result) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
};
const confirmSignUp = async (email: string, code: string): Promise<void> => {
const cognitoUser = new CognitoUser({
Username: email,
Pool: userPool
});
return new Promise((resolve, reject) => {
cognitoUser.confirmRegistration(code, true, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
};
const signOut = () => {
const currentUser = userPool.getCurrentUser();
if (currentUser) {
currentUser.signOut();
}
setState({
isAuthenticated: false,
isLoading: false,
user: null,
session: null
});
};
const getAccessToken = async (): Promise<string | null> => {
return new Promise((resolve) => {
const currentUser = userPool.getCurrentUser();
if (!currentUser) {
resolve(null);
return;
}
currentUser.getSession((err: Error | null, session: CognitoUserSession | null) => {
if (err || !session?.isValid()) {
resolve(null);
} else {
resolve(session.getAccessToken().getJwtToken());
}
});
});
};
return (
<AuthContext.Provider value={{
...state,
signIn,
signUp,
confirmSignUp,
signOut,
getAccessToken
}}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};Identity Federation
SAML Integration
# saml-provider.tf
resource "aws_cognito_identity_provider" "okta" {
user_pool_id = aws_cognito_user_pool.main.id
provider_name = "Okta"
provider_type = "SAML"
provider_details = {
MetadataURL = var.okta_metadata_url
SLORedirectBindingURI = var.okta_slo_url
SSORedirectBindingURI = var.okta_sso_url
}
attribute_mapping = {
email = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
given_name = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
family_name = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
"custom:role" = "role"
}
}Key Takeaways
-
Use User Pools for auth: User Pools handle authentication; Identity Pools provide AWS credentials
-
Customise with Lambda triggers: Add business logic at key points in the auth flow
-
Token claims matter: Use pre-token generation to add custom claims for authorisation
-
Secure your clients: Use PKCE for public clients, rotate secrets for confidential clients
-
Enable MFA: At minimum offer TOTP-based MFA for sensitive applications
-
Federation options: Support social login and enterprise SAML/OIDC providers
-
Monitor authentication: Enable advanced security features and CloudWatch metrics
-
Token management: Use short access tokens with refresh token rotation
Cognito simplifies identity management while providing enterprise-grade security features. Proper configuration of triggers and token claims enables fine-grained authorisation.