Skip to main content
Back to Blog
6 September 202514 min read

AWS Cognito: User Identity and Access Management

AWSCognitoAuthenticationSecurityIdentity

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

ScenarioUser PoolIdentity Pool
User authenticationYesNo
API authorisation (JWT)YesNo
Access AWS servicesNoYes
Social loginYesYes
Guest accessNoYes
Custom attributesYesNo

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

  1. Use User Pools for auth: User Pools handle authentication; Identity Pools provide AWS credentials

  2. Customise with Lambda triggers: Add business logic at key points in the auth flow

  3. Token claims matter: Use pre-token generation to add custom claims for authorisation

  4. Secure your clients: Use PKCE for public clients, rotate secrets for confidential clients

  5. Enable MFA: At minimum offer TOTP-based MFA for sensitive applications

  6. Federation options: Support social login and enterprise SAML/OIDC providers

  7. Monitor authentication: Enable advanced security features and CloudWatch metrics

  8. 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.

Share this article