Building a Centralized User Access Control System with AWS Cognito and CDK

🎯 Introduction

Building a centralized user access control system is one of the most critical architectural decisions for modern applications. Whether you’re managing a single application or a microservices ecosystem, having a robust, scalable authentication and authorization system is essential for:

  • Single Source of Truth: One system managing all user identities and permissions
  • Consistency: Uniform authentication experience across all services
  • Security: Centralized security policies and compliance controls
  • Scalability: Support for millions of users across multiple applications
  • Developer Experience: Simple integration for new services
  • Cost Efficiency: Managed service without operational overhead

This comprehensive guide demonstrates how to design and implement a production-ready centralized access control system using AWS Cognito and CDK (TypeScript), with strategies for multi-tenancy, role-based access control (RBAC), and integration patterns for various services.

πŸ’‘ Core Philosophy: “A well-designed identity system should be invisible to users, straightforward for developers, and bulletproof for security teams”

🎬 What We’ll Build

A complete centralized authentication and authorization system featuring:

  • AWS Cognito User Pools for user authentication and management
  • Custom Domain with hosted UI for seamless user experience
  • Multiple App Clients for different applications and services
  • Group-based RBAC with custom scopes and permissions
  • Custom Attributes for flexible user metadata
  • Pre/Post Authentication Triggers for custom business logic
  • API Integration patterns for microservices
  • Multi-tenant Support with different isolation strategies
  • Admin Portal for user management
  • Audit Logging for compliance

πŸ—οΈ Architecture Overview

πŸ“Š High-Level Architecture

graph TB
    subgraph "Client Applications"
        WebApp[Web Application]
        MobileApp[Mobile App]
        AdminPortal[Admin Portal]
        ThirdParty[Third-party App]
    end

    subgraph "Centralized Auth System"
        Cognito[Cognito User Pool]
        CustomDomain[Custom Domain]
        HostedUI[Hosted UI]

        subgraph "App Clients"
            WebClient[Web Client]
            MobileClient[Mobile Client]
            AdminClient[Admin Client]
            APIClient[API Client]
        end

        subgraph "User Groups"
            AdminGroup[Admin Group]
            UserGroup[User Group]
            PremiumGroup[Premium Group]
            TenantGroups[Tenant Groups]
        end

        subgraph "Triggers"
            PreAuth[Pre-Authentication]
            PostAuth[Post-Authentication]
            PreSignUp[Pre-Sign Up]
            PostConfirm[Post-Confirmation]
        end
    end

    subgraph "Backend Services"
        APIGW[API Gateway]
        Lambda1[Service Lambda]
        Lambda2[Service Lambda]
        AppSync[AppSync GraphQL]
    end

    subgraph "Monitoring & Security"
        CloudWatch[CloudWatch Logs]
        CloudTrail[CloudTrail]
        WAF[AWS WAF]
    end

    WebApp --> CustomDomain
    MobileApp --> Cognito
    AdminPortal --> CustomDomain
    ThirdParty --> APIClient

    CustomDomain --> HostedUI
    HostedUI --> Cognito

    Cognito --> WebClient
    Cognito --> MobileClient
    Cognito --> AdminClient
    Cognito --> APIClient

    Cognito --> AdminGroup
    Cognito --> UserGroup
    Cognito --> PremiumGroup
    Cognito --> TenantGroups

    Cognito --> PreAuth
    Cognito --> PostAuth
    Cognito --> PreSignUp
    Cognito --> PostConfirm

    WebClient --> APIGW
    MobileClient --> APIGW
    AdminClient --> Lambda1
    APIClient --> AppSync

    APIGW --> Lambda1
    APIGW --> Lambda2
    AppSync --> Lambda2

    Cognito -.-> CloudWatch
    Cognito -.-> CloudTrail
    APIGW -.-> WAF

    style Cognito fill:#ff6b6b
    style APIGW fill:#4ecdc4
    style AdminGroup fill:#feca57
    style CloudWatch fill:#95e1d3

πŸ”„ Authentication Flow

sequenceDiagram
    participant User
    participant App
    participant Cognito
    participant Lambda Trigger
    participant API Gateway
    participant Backend Service

    User->>App: Access Application
    App->>Cognito: Initiate Auth (OAuth2/OIDC)
    Cognito->>Lambda Trigger: Pre-Authentication
    Lambda Trigger->>Lambda Trigger: Custom validation
    Lambda Trigger-->>Cognito: Continue/Deny
    Cognito->>User: Show Login UI
    User->>Cognito: Provide Credentials
    Cognito->>Cognito: Validate Credentials
    Cognito->>Lambda Trigger: Post-Authentication
    Lambda Trigger->>Lambda Trigger: Add custom claims
    Lambda Trigger-->>Cognito: Enhanced token
    Cognito-->>App: Return Tokens (ID, Access, Refresh)
    App->>API Gateway: Request + Access Token
    API Gateway->>API Gateway: Validate Token
    API Gateway->>Backend Service: Forward Request
    Backend Service->>Backend Service: Check Permissions
    Backend Service-->>API Gateway: Response
    API Gateway-->>App: Response
    App-->>User: Show Data

🎨 Design Patterns: Single vs Multiple User Pools

πŸ“‹ Pattern Comparison

AspectSingle User PoolMultiple User Pools
Use CaseSingle tenant, unified user baseMulti-tenant, isolated environments
ComplexityLowHigh
CostLower (one pool)Higher (multiple pools)
IsolationGroup-basedPool-based (stronger)
ManagementCentralizedDistributed
ScalabilityVery high (millions of users)High per pool
CustomizationShared settingsPer-tenant settings
MigrationEasierMore complex

For most use cases, a single user pool with group-based access control provides the best balance of simplicity, cost, and functionality.

 1// Single User Pool Strategy
 2// βœ… GOOD: One pool, multiple groups
 3UserPool
 4β”œβ”€β”€ Groups
 5β”‚   β”œβ”€β”€ tenant-acme-admin
 6β”‚   β”œβ”€β”€ tenant-acme-user
 7β”‚   β”œβ”€β”€ tenant-widget-admin
 8β”‚   β”œβ”€β”€ tenant-widget-user
 9β”‚   β”œβ”€β”€ global-admin
10β”‚   └── support
11β”œβ”€β”€ Custom Attributes
12β”‚   β”œβ”€β”€ tenantId
13β”‚   β”œβ”€β”€ role
14β”‚   └── permissions
15└── App Clients
16    β”œβ”€β”€ web-app-client
17    β”œβ”€β”€ mobile-app-client
18    β”œβ”€β”€ admin-portal-client
19    └── api-client

πŸ”„ When to Use Multiple User Pools

Use multiple user pools when you need:

  1. Regulatory Compliance: Data residency requirements (different regions)
  2. Strong Tenant Isolation: Complete separation of user data
  3. Different Security Policies: Varying password policies per tenant
  4. Custom Branding: Unique hosted UI per tenant
  5. B2B SaaS: Enterprise customers requiring dedicated pools
1// Multiple User Pools Strategy
2// Use for strong isolation
3β”œβ”€β”€ UserPool-TenantA (Region: us-east-1)
4β”œβ”€β”€ UserPool-TenantB (Region: eu-west-1)
5└── UserPool-TenantC (Region: ap-southeast-1)

πŸ“¦ CDK Project Structure

auth-system-cdk/
β”œβ”€β”€ bin/
β”‚   └── auth-system.ts              # CDK app entry point
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ stacks/
β”‚   β”‚   β”œβ”€β”€ cognito-stack.ts        # Main Cognito configuration
β”‚   β”‚   β”œβ”€β”€ triggers-stack.ts       # Lambda triggers
β”‚   β”‚   β”œβ”€β”€ api-integration-stack.ts # API Gateway authorizers
β”‚   β”‚   └── monitoring-stack.ts     # CloudWatch dashboards
β”‚   β”œβ”€β”€ constructs/
β”‚   β”‚   β”œβ”€β”€ user-pool.ts            # Reusable User Pool construct
β”‚   β”‚   β”œβ”€β”€ app-client.ts           # App Client construct
β”‚   β”‚   └── user-groups.ts          # Groups construct
β”‚   β”œβ”€β”€ config/
β”‚   β”‚   β”œβ”€β”€ auth-config.ts          # Authentication configuration
β”‚   β”‚   β”œβ”€β”€ groups-config.ts        # Groups and permissions
β”‚   β”‚   └── app-clients-config.ts   # App client settings
β”‚   └── utils/
β”‚       β”œβ”€β”€ custom-attributes.ts    # Custom attribute helpers
β”‚       └── permissions.ts          # Permission definitions
β”œβ”€β”€ lambda/
β”‚   β”œβ”€β”€ triggers/
β”‚   β”‚   β”œβ”€β”€ pre-authentication.ts
β”‚   β”‚   β”œβ”€β”€ post-authentication.ts
β”‚   β”‚   β”œβ”€β”€ pre-sign-up.ts
β”‚   β”‚   └── post-confirmation.ts
β”‚   β”œβ”€β”€ admin/
β”‚   β”‚   β”œβ”€β”€ create-user.ts
β”‚   β”‚   β”œβ”€β”€ manage-groups.ts
β”‚   β”‚   └── custom-attributes.ts
β”‚   └── authorizers/
β”‚       └── cognito-authorizer.ts
β”œβ”€β”€ test/
β”‚   β”œβ”€β”€ unit/
β”‚   └── integration/
β”œβ”€β”€ docs/
β”‚   β”œβ”€β”€ integration-guide.md
β”‚   └── api-reference.md
β”œβ”€β”€ cdk.json
β”œβ”€β”€ tsconfig.json
└── package.json

βš™οΈ Configuration Design

🎯 Authentication Configuration

  1// lib/config/auth-config.ts
  2export interface AuthConfig {
  3  userPoolName: string;
  4  passwordPolicy: PasswordPolicyConfig;
  5  mfaConfig: MfaConfig;
  6  emailConfig: EmailConfig;
  7  customDomain?: string;
  8  supportedIdentityProviders: IdentityProvider[];
  9  customAttributes: CustomAttribute[];
 10}
 11
 12export interface PasswordPolicyConfig {
 13  minLength: number;
 14  requireLowercase: boolean;
 15  requireUppercase: boolean;
 16  requireDigits: boolean;
 17  requireSymbols: boolean;
 18  tempPasswordValidity: number;
 19}
 20
 21export interface MfaConfig {
 22  enabled: boolean;
 23  optional: boolean;
 24  methods: ('SMS' | 'TOTP' | 'SOFTWARE_TOKEN')[];
 25}
 26
 27export interface CustomAttribute {
 28  name: string;
 29  type: 'String' | 'Number' | 'DateTime' | 'Boolean';
 30  mutable: boolean;
 31  required?: boolean;
 32  min?: number;
 33  max?: number;
 34}
 35
 36export const authConfigs = {
 37  development: {
 38    userPoolName: 'dev-central-auth-pool',
 39    passwordPolicy: {
 40      minLength: 8,
 41      requireLowercase: true,
 42      requireUppercase: true,
 43      requireDigits: true,
 44      requireSymbols: false,
 45      tempPasswordValidity: 7,
 46    },
 47    mfaConfig: {
 48      enabled: false,
 49      optional: true,
 50      methods: ['SOFTWARE_TOKEN'],
 51    },
 52    emailConfig: {
 53      emailSendingAccount: 'COGNITO_DEFAULT',
 54    },
 55    supportedIdentityProviders: ['COGNITO'],
 56    customAttributes: [
 57      {
 58        name: 'tenantId',
 59        type: 'String',
 60        mutable: false,
 61        required: true,
 62        min: 1,
 63        max: 256,
 64      },
 65      {
 66        name: 'role',
 67        type: 'String',
 68        mutable: true,
 69        required: true,
 70      },
 71      {
 72        name: 'permissions',
 73        type: 'String',
 74        mutable: true,
 75      },
 76      {
 77        name: 'organizationId',
 78        type: 'String',
 79        mutable: false,
 80      },
 81    ],
 82  },
 83  production: {
 84    userPoolName: 'prod-central-auth-pool',
 85    passwordPolicy: {
 86      minLength: 12,
 87      requireLowercase: true,
 88      requireUppercase: true,
 89      requireDigits: true,
 90      requireSymbols: true,
 91      tempPasswordValidity: 3,
 92    },
 93    mfaConfig: {
 94      enabled: true,
 95      optional: false,
 96      methods: ['SOFTWARE_TOKEN', 'SMS'],
 97    },
 98    emailConfig: {
 99      emailSendingAccount: 'DEVELOPER',
100      from: 'auth@yourdomain.com',
101      replyTo: 'support@yourdomain.com',
102    },
103    customDomain: 'auth.yourdomain.com',
104    supportedIdentityProviders: ['COGNITO', 'Google', 'SAML'],
105    customAttributes: [
106      {
107        name: 'tenantId',
108        type: 'String',
109        mutable: false,
110        required: true,
111      },
112      {
113        name: 'role',
114        type: 'String',
115        mutable: true,
116        required: true,
117      },
118      {
119        name: 'permissions',
120        type: 'String',
121        mutable: true,
122      },
123      {
124        name: 'organizationId',
125        type: 'String',
126        mutable: false,
127      },
128      {
129        name: 'department',
130        type: 'String',
131        mutable: true,
132      },
133    ],
134  },
135} as const;

πŸ‘₯ Groups and Permissions Configuration

  1// lib/config/groups-config.ts
  2export interface GroupConfig {
  3  name: string;
  4  description: string;
  5  precedence: number;
  6  permissions: Permission[];
  7}
  8
  9export interface Permission {
 10  resource: string;
 11  actions: string[];
 12  effect: 'Allow' | 'Deny';
 13  conditions?: Record<string, any>;
 14}
 15
 16export const groupConfigs: GroupConfig[] = [
 17  {
 18    name: 'global-admin',
 19    description: 'Global administrators with full access',
 20    precedence: 1,
 21    permissions: [
 22      {
 23        resource: '*',
 24        actions: ['*'],
 25        effect: 'Allow',
 26      },
 27    ],
 28  },
 29  {
 30    name: 'tenant-admin',
 31    description: 'Tenant administrators',
 32    precedence: 10,
 33    permissions: [
 34      {
 35        resource: 'tenant:${tenantId}:*',
 36        actions: ['read', 'write', 'delete'],
 37        effect: 'Allow',
 38      },
 39      {
 40        resource: 'user:${tenantId}:*',
 41        actions: ['read', 'write', 'invite'],
 42        effect: 'Allow',
 43      },
 44    ],
 45  },
 46  {
 47    name: 'tenant-user',
 48    description: 'Standard tenant users',
 49    precedence: 20,
 50    permissions: [
 51      {
 52        resource: 'tenant:${tenantId}:*',
 53        actions: ['read'],
 54        effect: 'Allow',
 55      },
 56      {
 57        resource: 'user:${tenantId}:${userId}',
 58        actions: ['read', 'write'],
 59        effect: 'Allow',
 60      },
 61    ],
 62  },
 63  {
 64    name: 'support',
 65    description: 'Customer support team',
 66    precedence: 30,
 67    permissions: [
 68      {
 69        resource: 'user:*',
 70        actions: ['read'],
 71        effect: 'Allow',
 72      },
 73      {
 74        resource: 'ticket:*',
 75        actions: ['read', 'write'],
 76        effect: 'Allow',
 77      },
 78    ],
 79  },
 80];
 81
 82// Dynamic group creation for tenants
 83export function createTenantGroups(tenantId: string): GroupConfig[] {
 84  return [
 85    {
 86      name: `tenant-${tenantId}-admin`,
 87      description: `Admin group for tenant ${tenantId}`,
 88      precedence: 100,
 89      permissions: [
 90        {
 91          resource: `tenant:${tenantId}:*`,
 92          actions: ['*'],
 93          effect: 'Allow',
 94        },
 95      ],
 96    },
 97    {
 98      name: `tenant-${tenantId}-user`,
 99      description: `User group for tenant ${tenantId}`,
100      precedence: 200,
101      permissions: [
102        {
103          resource: `tenant:${tenantId}:*`,
104          actions: ['read'],
105          effect: 'Allow',
106        },
107      ],
108    },
109  ];
110}

πŸ“± App Clients Configuration

  1// lib/config/app-clients-config.ts
  2export interface AppClientConfig {
  3  name: string;
  4  description: string;
  5  authFlows: AuthFlow[];
  6  oAuth: OAuthConfig;
  7  tokenValidity: TokenValidityConfig;
  8  scopes: string[];
  9  callbackUrls?: string[];
 10  logoutUrls?: string[];
 11}
 12
 13export type AuthFlow =
 14  | 'ALLOW_USER_PASSWORD_AUTH'
 15  | 'ALLOW_USER_SRP_AUTH'
 16  | 'ALLOW_REFRESH_TOKEN_AUTH'
 17  | 'ALLOW_CUSTOM_AUTH';
 18
 19export interface OAuthConfig {
 20  enabled: boolean;
 21  flows: ('code' | 'implicit' | 'client_credentials')[];
 22  scopes: string[];
 23}
 24
 25export interface TokenValidityConfig {
 26  accessToken: number; // minutes
 27  idToken: number; // minutes
 28  refreshToken: number; // days
 29}
 30
 31export const appClientConfigs: Record<string, AppClientConfig> = {
 32  webApp: {
 33    name: 'web-app-client',
 34    description: 'Client for web application',
 35    authFlows: ['ALLOW_USER_SRP_AUTH', 'ALLOW_REFRESH_TOKEN_AUTH'],
 36    oAuth: {
 37      enabled: true,
 38      flows: ['code'],
 39      scopes: ['openid', 'email', 'profile', 'aws.cognito.signin.user.admin'],
 40    },
 41    tokenValidity: {
 42      accessToken: 60, // 1 hour
 43      idToken: 60,
 44      refreshToken: 30, // 30 days
 45    },
 46    scopes: ['read', 'write'],
 47    callbackUrls: ['http://localhost:3000/callback', 'https://app.yourdomain.com/callback'],
 48    logoutUrls: ['http://localhost:3000', 'https://app.yourdomain.com'],
 49  },
 50  mobileApp: {
 51    name: 'mobile-app-client',
 52    description: 'Client for mobile applications',
 53    authFlows: ['ALLOW_USER_SRP_AUTH', 'ALLOW_REFRESH_TOKEN_AUTH'],
 54    oAuth: {
 55      enabled: true,
 56      flows: ['code'],
 57      scopes: ['openid', 'email', 'profile'],
 58    },
 59    tokenValidity: {
 60      accessToken: 60,
 61      idToken: 60,
 62      refreshToken: 90, // 90 days for mobile
 63    },
 64    scopes: ['read', 'write'],
 65    callbackUrls: ['myapp://callback'],
 66    logoutUrls: ['myapp://logout'],
 67  },
 68  adminPortal: {
 69    name: 'admin-portal-client',
 70    description: 'Client for admin portal',
 71    authFlows: ['ALLOW_USER_SRP_AUTH', 'ALLOW_REFRESH_TOKEN_AUTH'],
 72    oAuth: {
 73      enabled: true,
 74      flows: ['code'],
 75      scopes: ['openid', 'email', 'profile', 'aws.cognito.signin.user.admin'],
 76    },
 77    tokenValidity: {
 78      accessToken: 30, // 30 minutes for admin
 79      idToken: 30,
 80      refreshToken: 1, // 1 day
 81    },
 82    scopes: ['admin', 'read', 'write', 'delete'],
 83    callbackUrls: ['http://localhost:3001/callback', 'https://admin.yourdomain.com/callback'],
 84    logoutUrls: ['http://localhost:3001', 'https://admin.yourdomain.com'],
 85  },
 86  apiClient: {
 87    name: 'api-client',
 88    description: 'Client for server-to-server API calls',
 89    authFlows: ['ALLOW_CUSTOM_AUTH', 'ALLOW_REFRESH_TOKEN_AUTH'],
 90    oAuth: {
 91      enabled: true,
 92      flows: ['client_credentials'],
 93      scopes: ['api/read', 'api/write'],
 94    },
 95    tokenValidity: {
 96      accessToken: 60,
 97      idToken: 60,
 98      refreshToken: 30,
 99    },
100    scopes: ['api.read', 'api.write'],
101  },
102};

πŸ—οΈ CDK Stack Implementation

🎯 Main Cognito Stack

  1// lib/stacks/cognito-stack.ts
  2import * as cdk from 'aws-cdk-lib';
  3import * as cognito from 'aws-cdk-lib/aws-cognito';
  4import * as lambda from 'aws-cdk-lib/aws-lambda';
  5import * as iam from 'aws-cdk-lib/aws-iam';
  6import { Construct } from 'constructs';
  7import { authConfigs, AuthConfig } from '../config/auth-config';
  8import { groupConfigs } from '../config/groups-config';
  9import { appClientConfigs } from '../config/app-clients-config';
 10
 11export interface CognitoStackProps extends cdk.StackProps {
 12  environment: 'development' | 'production';
 13}
 14
 15export class CognitoStack extends cdk.Stack {
 16  public readonly userPool: cognito.UserPool;
 17  public readonly userPoolId: string;
 18  public readonly userPoolArn: string;
 19  public readonly appClients: Map<string, cognito.UserPoolClient>;
 20  public readonly groups: Map<string, cognito.CfnUserPoolGroup>;
 21
 22  constructor(scope: Construct, id: string, props: CognitoStackProps) {
 23    super(scope, id, props);
 24
 25    const config = authConfigs[props.environment];
 26
 27    // Create User Pool
 28    this.userPool = this.createUserPool(config);
 29    this.userPoolId = this.userPool.userPoolId;
 30    this.userPoolArn = this.userPool.userPoolArn;
 31
 32    // Add custom domain
 33    if (config.customDomain) {
 34      this.addCustomDomain(config.customDomain);
 35    }
 36
 37    // Create groups
 38    this.groups = this.createGroups();
 39
 40    // Create app clients
 41    this.appClients = this.createAppClients();
 42
 43    // Add identity providers (optional)
 44    this.addIdentityProviders(config);
 45
 46    // Output important values
 47    this.createOutputs();
 48  }
 49
 50  private createUserPool(config: AuthConfig): cognito.UserPool {
 51    // Create custom attributes
 52    const customAttributes: Record<string, cognito.ICustomAttribute> = {};
 53
 54    config.customAttributes.forEach((attr) => {
 55      if (attr.type === 'String') {
 56        customAttributes[attr.name] = new cognito.StringAttribute({
 57          mutable: attr.mutable,
 58          minLen: attr.min,
 59          maxLen: attr.max,
 60        });
 61      } else if (attr.type === 'Number') {
 62        customAttributes[attr.name] = new cognito.NumberAttribute({
 63          mutable: attr.mutable,
 64          min: attr.min,
 65          max: attr.max,
 66        });
 67      }
 68    });
 69
 70    const userPool = new cognito.UserPool(this, 'UserPool', {
 71      userPoolName: config.userPoolName,
 72
 73      // Sign-in configuration
 74      signInAliases: {
 75        email: true,
 76        username: true,
 77        phone: false,
 78      },
 79
 80      // Auto-verify
 81      autoVerify: {
 82        email: true,
 83      },
 84
 85      // Self sign-up
 86      selfSignUpEnabled: true,
 87
 88      // User attributes
 89      standardAttributes: {
 90        email: {
 91          required: true,
 92          mutable: true,
 93        },
 94        givenName: {
 95          required: true,
 96          mutable: true,
 97        },
 98        familyName: {
 99          required: true,
100          mutable: true,
101        },
102      },
103
104      customAttributes,
105
106      // Password policy
107      passwordPolicy: {
108        minLength: config.passwordPolicy.minLength,
109        requireLowercase: config.passwordPolicy.requireLowercase,
110        requireUppercase: config.passwordPolicy.requireUppercase,
111        requireDigits: config.passwordPolicy.requireDigits,
112        requireSymbols: config.passwordPolicy.requireSymbols,
113        tempPasswordValidity: cdk.Duration.days(
114          config.passwordPolicy.tempPasswordValidity
115        ),
116      },
117
118      // MFA
119      mfa: config.mfaConfig.enabled
120        ? config.mfaConfig.optional
121          ? cognito.Mfa.OPTIONAL
122          : cognito.Mfa.REQUIRED
123        : cognito.Mfa.OFF,
124
125      mfaSecondFactor: {
126        sms: config.mfaConfig.methods.includes('SMS'),
127        otp: config.mfaConfig.methods.includes('SOFTWARE_TOKEN'),
128      },
129
130      // Account recovery
131      accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
132
133      // Email configuration
134      email: config.emailConfig.emailSendingAccount === 'DEVELOPER'
135        ? cognito.UserPoolEmail.withSES({
136            fromEmail: config.emailConfig.from!,
137            fromName: 'Your App',
138            replyTo: config.emailConfig.replyTo,
139          })
140        : cognito.UserPoolEmail.withCognito(),
141
142      // Advanced security
143      advancedSecurityMode: cognito.AdvancedSecurityMode.ENFORCED,
144
145      // Deletion protection
146      deletionProtection: props.environment === 'production',
147
148      // Device tracking
149      deviceTracking: {
150        challengeRequiredOnNewDevice: true,
151        deviceOnlyRememberedOnUserPrompt: true,
152      },
153
154      // Lambda triggers (will be added later)
155      lambdaTriggers: {},
156
157      // Removal policy
158      removalPolicy:
159        props.environment === 'production'
160          ? cdk.RemovalPolicy.RETAIN
161          : cdk.RemovalPolicy.DESTROY,
162    });
163
164    // Enable user pool analytics
165    new cognito.CfnUserPoolUserToGroupAttachment(this, 'UserPoolAnalytics', {
166      userPoolId: userPool.userPoolId,
167      groupName: 'Analytics',
168      username: 'analytics-user',
169    });
170
171    return userPool;
172  }
173
174  private addCustomDomain(domainName: string): void {
175    const domain = this.userPool.addDomain('CustomDomain', {
176      customDomain: {
177        domainName,
178        certificate: undefined, // Add ACM certificate here
179      },
180    });
181
182    new cdk.CfnOutput(this, 'UserPoolDomain', {
183      value: domain.domainName,
184      description: 'Cognito User Pool Domain',
185    });
186  }
187
188  private createGroups(): Map<string, cognito.CfnUserPoolGroup> {
189    const groups = new Map<string, cognito.CfnUserPoolGroup>();
190
191    groupConfigs.forEach((groupConfig) => {
192      const group = new cognito.CfnUserPoolGroup(this, `Group-${groupConfig.name}`, {
193        userPoolId: this.userPool.userPoolId,
194        groupName: groupConfig.name,
195        description: groupConfig.description,
196        precedence: groupConfig.precedence,
197      });
198
199      groups.set(groupConfig.name, group);
200    });
201
202    return groups;
203  }
204
205  private createAppClients(): Map<string, cognito.UserPoolClient> {
206    const clients = new Map<string, cognito.UserPoolClient>();
207
208    Object.entries(appClientConfigs).forEach(([key, config]) => {
209      const client = this.userPool.addClient(`AppClient-${key}`, {
210        userPoolClientName: config.name,
211        generateSecret: key === 'apiClient', // Only API client needs secret
212
213        // Auth flows
214        authFlows: {
215          userPassword: config.authFlows.includes('ALLOW_USER_PASSWORD_AUTH'),
216          userSrp: config.authFlows.includes('ALLOW_USER_SRP_AUTH'),
217          custom: config.authFlows.includes('ALLOW_CUSTOM_AUTH'),
218          adminUserPassword: false,
219        },
220
221        // OAuth configuration
222        oAuth: config.oAuth.enabled
223          ? {
224              flows: {
225                authorizationCodeGrant: config.oAuth.flows.includes('code'),
226                implicitCodeGrant: config.oAuth.flows.includes('implicit'),
227                clientCredentials: config.oAuth.flows.includes('client_credentials'),
228              },
229              scopes: config.oAuth.scopes.map((scope) =>
230                scope === 'openid'
231                  ? cognito.OAuthScope.OPENID
232                  : scope === 'email'
233                  ? cognito.OAuthScope.EMAIL
234                  : scope === 'profile'
235                  ? cognito.OAuthScope.PROFILE
236                  : cognito.OAuthScope.custom(scope)
237              ),
238              callbackUrls: config.callbackUrls,
239              logoutUrls: config.logoutUrls,
240            }
241          : undefined,
242
243        // Token validity
244        accessTokenValidity: cdk.Duration.minutes(config.tokenValidity.accessToken),
245        idTokenValidity: cdk.Duration.minutes(config.tokenValidity.idToken),
246        refreshTokenValidity: cdk.Duration.days(config.tokenValidity.refreshToken),
247
248        // Prevent user existence errors
249        preventUserExistenceErrors: true,
250
251        // Read/write attributes
252        readAttributes: new cognito.ClientAttributes()
253          .withStandardAttributes({
254            email: true,
255            givenName: true,
256            familyName: true,
257            emailVerified: true,
258          })
259          .withCustomAttributes(...config.customAttributes || []),
260
261        writeAttributes: new cognito.ClientAttributes()
262          .withStandardAttributes({
263            givenName: true,
264            familyName: true,
265          }),
266      });
267
268      clients.set(key, client);
269
270      // Output client ID
271      new cdk.CfnOutput(this, `AppClientId-${key}`, {
272        value: client.userPoolClientId,
273        description: `${config.name} Client ID`,
274      });
275    });
276
277    return clients;
278  }
279
280  private addIdentityProviders(config: AuthConfig): void {
281    // Add Google Identity Provider
282    if (config.supportedIdentityProviders.includes('Google')) {
283      new cognito.UserPoolIdentityProviderGoogle(this, 'GoogleProvider', {
284        userPool: this.userPool,
285        clientId: process.env.GOOGLE_CLIENT_ID || '',
286        clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
287        scopes: ['openid', 'email', 'profile'],
288        attributeMapping: {
289          email: cognito.ProviderAttribute.GOOGLE_EMAIL,
290          givenName: cognito.ProviderAttribute.GOOGLE_GIVEN_NAME,
291          familyName: cognito.ProviderAttribute.GOOGLE_FAMILY_NAME,
292        },
293      });
294    }
295
296    // Add SAML Identity Provider
297    if (config.supportedIdentityProviders.includes('SAML')) {
298      new cognito.UserPoolIdentityProviderSaml(this, 'SamlProvider', {
299        userPool: this.userPool,
300        name: 'SAML',
301        metadata: cognito.UserPoolIdentityProviderSamlMetadata.url(
302          'https://your-idp.com/metadata.xml'
303        ),
304        attributeMapping: {
305          email: cognito.ProviderAttribute.other('email'),
306          givenName: cognito.ProviderAttribute.other('firstName'),
307          familyName: cognito.ProviderAttribute.other('lastName'),
308        },
309      });
310    }
311  }
312
313  private createOutputs(): void {
314    new cdk.CfnOutput(this, 'UserPoolId', {
315      value: this.userPoolId,
316      description: 'Cognito User Pool ID',
317      exportName: 'CentralAuthUserPoolId',
318    });
319
320    new cdk.CfnOutput(this, 'UserPoolArn', {
321      value: this.userPoolArn,
322      description: 'Cognito User Pool ARN',
323      exportName: 'CentralAuthUserPoolArn',
324    });
325  }
326}

⚑ Lambda Triggers Stack

 1// lib/stacks/triggers-stack.ts
 2import * as cdk from 'aws-cdk-lib';
 3import * as cognito from 'aws-cdk-lib/aws-cognito';
 4import * as lambda from 'aws-cdk-lib/aws-lambda';
 5import * as logs from 'aws-cdk-lib/aws-logs';
 6import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
 7import { Construct } from 'constructs';
 8import * as path from 'path';
 9
10export interface TriggersStackProps extends cdk.StackProps {
11  userPool: cognito.UserPool;
12}
13
14export class TriggersStack extends cdk.Stack {
15  constructor(scope: Construct, id: string, props: TriggersStackProps) {
16    super(scope, id, props);
17
18    const { userPool } = props;
19
20    // Pre-Authentication Trigger
21    const preAuthFunction = new NodejsFunction(this, 'PreAuthFunction', {
22      runtime: lambda.Runtime.NODEJS_20_X,
23      handler: 'handler',
24      entry: path.join(__dirname, '../../lambda/triggers/pre-authentication.ts'),
25      timeout: cdk.Duration.seconds(5),
26      memorySize: 256,
27      logRetention: logs.RetentionDays.ONE_WEEK,
28      environment: {
29        USER_POOL_ID: userPool.userPoolId,
30      },
31    });
32
33    // Post-Authentication Trigger
34    const postAuthFunction = new NodejsFunction(this, 'PostAuthFunction', {
35      runtime: lambda.Runtime.NODEJS_20_X,
36      handler: 'handler',
37      entry: path.join(__dirname, '../../lambda/triggers/post-authentication.ts'),
38      timeout: cdk.Duration.seconds(5),
39      memorySize: 256,
40      logRetention: logs.RetentionDays.ONE_WEEK,
41    });
42
43    // Pre-Sign-Up Trigger
44    const preSignUpFunction = new NodejsFunction(this, 'PreSignUpFunction', {
45      runtime: lambda.Runtime.NODEJS_20_X,
46      handler: 'handler',
47      entry: path.join(__dirname, '../../lambda/triggers/pre-sign-up.ts'),
48      timeout: cdk.Duration.seconds(5),
49      memorySize: 256,
50      logRetention: logs.RetentionDays.ONE_WEEK,
51    });
52
53    // Post-Confirmation Trigger
54    const postConfirmFunction = new NodejsFunction(this, 'PostConfirmFunction', {
55      runtime: lambda.Runtime.NODEJS_20_X,
56      handler: 'handler',
57      entry: path.join(__dirname, '../../lambda/triggers/post-confirmation.ts'),
58      timeout: cdk.Duration.seconds(5),
59      memorySize: 256,
60      logRetention: logs.RetentionDays.ONE_WEEK,
61    });
62
63    // Grant Cognito permission to invoke Lambda functions
64    preAuthFunction.grantInvoke(
65      new cdk.aws_iam.ServicePrincipal('cognito-idp.amazonaws.com')
66    );
67    postAuthFunction.grantInvoke(
68      new cdk.aws_iam.ServicePrincipal('cognito-idp.amazonaws.com')
69    );
70    preSignUpFunction.grantInvoke(
71      new cdk.aws_iam.ServicePrincipal('cognito-idp.amazonaws.com')
72    );
73    postConfirmFunction.grantInvoke(
74      new cdk.aws_iam.ServicePrincipal('cognito-idp.amazonaws.com')
75    );
76
77    // Add triggers to User Pool
78    const cfnUserPool = userPool.node.defaultChild as cognito.CfnUserPool;
79    cfnUserPool.lambdaConfig = {
80      preAuthentication: preAuthFunction.functionArn,
81      postAuthentication: postAuthFunction.functionArn,
82      preSignUp: preSignUpFunction.functionArn,
83      postConfirmation: postConfirmFunction.functionArn,
84    };
85  }
86}

πŸ” Lambda Trigger Implementations

 1// lambda/triggers/pre-authentication.ts
 2import {
 3  PreAuthenticationTriggerEvent,
 4  PreAuthenticationTriggerHandler,
 5} from 'aws-lambda';
 6
 7export const handler: PreAuthenticationTriggerHandler = async (event) => {
 8  console.log('Pre-Authentication Trigger:', JSON.stringify(event, null, 2));
 9
10  // Example: Block users from specific domains
11  const email = event.request.userAttributes.email;
12  const blockedDomains = ['blocked-domain.com', 'spam.com'];
13
14  if (blockedDomains.some((domain) => email.endsWith(`@${domain}`))) {
15    throw new Error('Email domain is not allowed');
16  }
17
18  // Example: Check if user is active in external system
19  const isActive = await checkUserActiveStatus(event.request.userAttributes.sub);
20
21  if (!isActive) {
22    throw new Error('User account is not active');
23  }
24
25  // Example: Add custom validation based on user attributes
26  const tenantId = event.request.userAttributes['custom:tenantId'];
27  if (tenantId) {
28    const isTenantActive = await checkTenantStatus(tenantId);
29    if (!isTenantActive) {
30      throw new Error('Tenant account is suspended');
31    }
32  }
33
34  return event;
35};
36
37async function checkUserActiveStatus(userId: string): Promise<boolean> {
38  // Implement your logic to check user status
39  // Example: Query DynamoDB, call external API, etc.
40  return true;
41}
42
43async function checkTenantStatus(tenantId: string): Promise<boolean> {
44  // Implement your logic to check tenant status
45  return true;
46}
 1// lambda/triggers/post-authentication.ts
 2import {
 3  PostAuthenticationTriggerEvent,
 4  PostAuthenticationTriggerHandler,
 5} from 'aws-lambda';
 6
 7export const handler: PostAuthenticationTriggerHandler = async (event) => {
 8  console.log('Post-Authentication Trigger:', JSON.stringify(event, null, 2));
 9
10  // Example: Log successful authentication
11  await logAuthentication({
12    userId: event.request.userAttributes.sub,
13    email: event.request.userAttributes.email,
14    timestamp: new Date().toISOString(),
15    ipAddress: event.request.userContextData?.sourceIp[0],
16  });
17
18  // Example: Add custom claims to the token
19  event.response.claimsOverrideDetails = {
20    claimsToAddOrOverride: {
21      tenantId: event.request.userAttributes['custom:tenantId'] || '',
22      role: event.request.userAttributes['custom:role'] || 'user',
23      permissions: event.request.userAttributes['custom:permissions'] || '',
24    },
25  };
26
27  // Example: Update last login timestamp
28  await updateLastLogin(event.request.userAttributes.sub);
29
30  return event;
31};
32
33async function logAuthentication(data: any): Promise<void> {
34  // Log to CloudWatch, DynamoDB, or external system
35  console.log('Authentication logged:', data);
36}
37
38async function updateLastLogin(userId: string): Promise<void> {
39  // Update user's last login timestamp in database
40  console.log('Last login updated for user:', userId);
41}
 1// lambda/triggers/pre-sign-up.ts
 2import {
 3  PreSignUpTriggerEvent,
 4  PreSignUpTriggerHandler,
 5} from 'aws-lambda';
 6
 7export const handler: PreSignUpTriggerHandler = async (event) => {
 8  console.log('Pre-Sign-Up Trigger:', JSON.stringify(event, null, 2));
 9
10  // Auto-confirm and auto-verify user
11  event.response.autoConfirmUser = true;
12  event.response.autoVerifyEmail = true;
13
14  // Example: Validate invitation code
15  const invitationCode = event.request.userAttributes['custom:invitationCode'];
16  if (invitationCode) {
17    const isValidInvitation = await validateInvitation(invitationCode);
18    if (!isValidInvitation) {
19      throw new Error('Invalid invitation code');
20    }
21  }
22
23  // Example: Assign default tenant based on email domain
24  const email = event.request.userAttributes.email;
25  const domain = email.split('@')[1];
26  const tenantId = await getTenantIdByDomain(domain);
27
28  if (tenantId) {
29    event.response.claimsOverrideDetails = {
30      claimsToAddOrOverride: {
31        'custom:tenantId': tenantId,
32      },
33    };
34  }
35
36  return event;
37};
38
39async function validateInvitation(code: string): Promise<boolean> {
40  // Validate invitation code against database
41  return true;
42}
43
44async function getTenantIdByDomain(domain: string): Promise<string | null> {
45  // Map email domain to tenant ID
46  const domainToTenant: Record<string, string> = {
47    'company.com': 'tenant-company',
48    'startup.io': 'tenant-startup',
49  };
50
51  return domainToTenant[domain] || null;
52}
 1// lambda/triggers/post-confirmation.ts
 2import {
 3  PostConfirmationTriggerEvent,
 4  PostConfirmationTriggerHandler,
 5} from 'aws-lambda';
 6import {
 7  CognitoIdentityProviderClient,
 8  AdminAddUserToGroupCommand,
 9} from '@aws-sdk/client-cognito-identity-provider';
10
11const cognitoClient = new CognitoIdentityProviderClient({});
12
13export const handler: PostConfirmationTriggerHandler = async (event) => {
14  console.log('Post-Confirmation Trigger:', JSON.stringify(event, null, 2));
15
16  const userId = event.request.userAttributes.sub;
17  const tenantId = event.request.userAttributes['custom:tenantId'];
18
19  // Example: Add user to default group
20  await addUserToGroup(event.userPoolId, event.userName, 'tenant-user');
21
22  // Example: Add user to tenant-specific group
23  if (tenantId) {
24    await addUserToGroup(event.userPoolId, event.userName, `tenant-${tenantId}-user`);
25  }
26
27  // Example: Send welcome email
28  await sendWelcomeEmail(event.request.userAttributes.email);
29
30  // Example: Create user profile in database
31  await createUserProfile({
32    userId,
33    email: event.request.userAttributes.email,
34    name: `${event.request.userAttributes.given_name} ${event.request.userAttributes.family_name}`,
35    tenantId,
36  });
37
38  return event;
39};
40
41async function addUserToGroup(
42  userPoolId: string,
43  username: string,
44  groupName: string
45): Promise<void> {
46  try {
47    await cognitoClient.send(
48      new AdminAddUserToGroupCommand({
49        UserPoolId: userPoolId,
50        Username: username,
51        GroupName: groupName,
52      })
53    );
54    console.log(`Added user ${username} to group ${groupName}`);
55  } catch (error) {
56    console.error('Error adding user to group:', error);
57  }
58}
59
60async function sendWelcomeEmail(email: string): Promise<void> {
61  // Send welcome email via SES, SendGrid, etc.
62  console.log('Welcome email sent to:', email);
63}
64
65async function createUserProfile(profile: any): Promise<void> {
66  // Create user profile in DynamoDB or other database
67  console.log('User profile created:', profile);
68}

πŸ”— Integration Patterns

🌐 API Gateway Integration

  1// lib/stacks/api-integration-stack.ts
  2import * as cdk from 'aws-cdk-lib';
  3import * as apigateway from 'aws-cdk-lib/aws-apigateway';
  4import * as cognito from 'aws-cdk-lib/aws-cognito';
  5import { Construct } from 'constructs';
  6
  7export interface ApiIntegrationStackProps extends cdk.StackProps {
  8  userPool: cognito.UserPool;
  9  userPoolClient: cognito.UserPoolClient;
 10}
 11
 12export class ApiIntegrationStack extends cdk.Stack {
 13  public readonly api: apigateway.RestApi;
 14
 15  constructor(scope: Construct, id: string, props: ApiIntegrationStackProps) {
 16    super(scope, id, props);
 17
 18    const { userPool, userPoolClient } = props;
 19
 20    // Create Cognito Authorizer
 21    const authorizer = new apigateway.CognitoUserPoolsAuthorizer(
 22      this,
 23      'CognitoAuthorizer',
 24      {
 25        cognitoUserPools: [userPool],
 26        identitySource: 'method.request.header.Authorization',
 27        authorizerName: 'CognitoAuthorizer',
 28      }
 29    );
 30
 31    // Create API Gateway
 32    this.api = new apigateway.RestApi(this, 'ProtectedApi', {
 33      restApiName: 'Protected API',
 34      description: 'API protected by Cognito',
 35      defaultCorsPreflightOptions: {
 36        allowOrigins: apigateway.Cors.ALL_ORIGINS,
 37        allowMethods: apigateway.Cors.ALL_METHODS,
 38        allowHeaders: ['Content-Type', 'Authorization'],
 39      },
 40    });
 41
 42    // Example: Protected resource
 43    const protectedResource = this.api.root.addResource('protected');
 44
 45    protectedResource.addMethod(
 46      'GET',
 47      new apigateway.MockIntegration({
 48        integrationResponses: [
 49          {
 50            statusCode: '200',
 51            responseTemplates: {
 52              'application/json': JSON.stringify({
 53                message: 'Hello from protected endpoint',
 54                user: '$context.authorizer.claims',
 55              }),
 56            },
 57          },
 58        ],
 59        requestTemplates: {
 60          'application/json': '{"statusCode": 200}',
 61        },
 62      }),
 63      {
 64        authorizer,
 65        authorizationType: apigateway.AuthorizationType.COGNITO,
 66        authorizationScopes: ['openid', 'email'],
 67        methodResponses: [{ statusCode: '200' }],
 68      }
 69    );
 70
 71    // Example: Admin-only resource
 72    const adminResource = this.api.root.addResource('admin');
 73
 74    adminResource.addMethod(
 75      'POST',
 76      new apigateway.MockIntegration({
 77        integrationResponses: [
 78          {
 79            statusCode: '200',
 80            responseTemplates: {
 81              'application/json': JSON.stringify({
 82                message: 'Admin action completed',
 83              }),
 84            },
 85          },
 86        ],
 87        requestTemplates: {
 88          'application/json': '{"statusCode": 200}',
 89        },
 90      }),
 91      {
 92        authorizer,
 93        authorizationType: apigateway.AuthorizationType.COGNITO,
 94        // Note: Group-based authorization needs Lambda authorizer
 95        methodResponses: [{ statusCode: '200' }],
 96      }
 97    );
 98
 99    // Output API URL
100    new cdk.CfnOutput(this, 'ApiUrl', {
101      value: this.api.url,
102      description: 'Protected API URL',
103    });
104  }
105}

πŸ” Custom Lambda Authorizer for Fine-Grained Access

 1// lambda/authorizers/cognito-authorizer.ts
 2import {
 3  APIGatewayAuthorizerResult,
 4  APIGatewayTokenAuthorizerEvent,
 5  APIGatewayTokenAuthorizerHandler,
 6} from 'aws-lambda';
 7import { CognitoJwtVerifier } from 'aws-jwt-verify';
 8
 9const verifier = CognitoJwtVerifier.create({
10  userPoolId: process.env.USER_POOL_ID!,
11  tokenUse: 'access',
12  clientId: process.env.CLIENT_ID!,
13});
14
15export const handler: APIGatewayTokenAuthorizerHandler = async (
16  event: APIGatewayTokenAuthorizerEvent
17): Promise<APIGatewayAuthorizerResult> => {
18  console.log('Authorizer event:', JSON.stringify(event, null, 2));
19
20  try {
21    // Extract token from Authorization header
22    const token = event.authorizationToken.replace('Bearer ', '');
23
24    // Verify JWT token
25    const payload = await verifier.verify(token);
26
27    console.log('Token payload:', payload);
28
29    // Extract user information
30    const userId = payload.sub;
31    const username = payload.username;
32    const groups = payload['cognito:groups'] || [];
33    const tenantId = payload['custom:tenantId'] || '';
34    const role = payload['custom:role'] || 'user';
35
36    // Check permissions based on resource and method
37    const resource = event.methodArn;
38    const hasPermission = checkPermissions(resource, groups, role);
39
40    if (!hasPermission) {
41      throw new Error('Insufficient permissions');
42    }
43
44    // Generate policy
45    const policy = generatePolicy(userId, 'Allow', event.methodArn, {
46      userId,
47      username,
48      groups: JSON.stringify(groups),
49      tenantId,
50      role,
51    });
52
53    return policy;
54  } catch (error) {
55    console.error('Authorization error:', error);
56    throw new Error('Unauthorized');
57  }
58};
59
60function checkPermissions(
61  resource: string,
62  groups: string[],
63  role: string
64): boolean {
65  // Example permission checks
66  if (groups.includes('global-admin')) {
67    return true;
68  }
69
70  if (resource.includes('/admin/') && !groups.includes('admin')) {
71    return false;
72  }
73
74  // Add more permission logic here
75  return true;
76}
77
78function generatePolicy(
79  principalId: string,
80  effect: 'Allow' | 'Deny',
81  resource: string,
82  context?: Record<string, any>
83): APIGatewayAuthorizerResult {
84  return {
85    principalId,
86    policyDocument: {
87      Version: '2012-10-17',
88      Statement: [
89        {
90          Action: 'execute-api:Invoke',
91          Effect: effect,
92          Resource: resource,
93        },
94      ],
95    },
96    context,
97  };
98}

🎯 User Management Strategies

πŸ“Š Strategy Comparison

StrategyUse CaseIsolationComplexityCost
Single Pool + GroupsSaaS with shared infrastructureGroup-levelLowLow
Single Pool + Custom AttributesMulti-tenant with shared usersAttribute-levelLowLow
Multiple Pools per TenantEnterprise B2BPool-levelHighHigh
Multiple Pools per RegionGlobal apps with data residencyRegion-levelMediumMedium
Hybrid (Pool + Groups)Mix of enterprise and SMBMulti-levelHighMedium
 1// User structure
 2{
 3  "sub": "uuid",
 4  "email": "user@company.com",
 5  "custom:tenantId": "tenant-acme",
 6  "custom:role": "admin",
 7  "custom:permissions": "read,write,delete",
 8  "custom:organizationId": "org-123",
 9  "cognito:groups": [
10    "tenant-acme-admin",
11    "global-user"
12  ]
13}

πŸ”„ Multi-Tenant User Lifecycle

graph TD
    A[New User Signup] --> B{Has Invitation?}
    B -->|Yes| C[Extract Tenant Info]
    B -->|No| D[Create New Tenant]

    C --> E[Set Custom Attributes]
    D --> E

    E --> F[Create User in Cognito]
    F --> G[Assign to Tenant Group]
    G --> H[Set Default Permissions]

    H --> I{User Role?}
    I -->|Admin| J[Add to Tenant Admin Group]
    I -->|User| K[Add to Tenant User Group]

    J --> L[Send Welcome Email]
    K --> L

    L --> M[Create User Profile in DB]
    M --> N[User Active]

    style A fill:#ff6b6b
    style N fill:#4ecdc4
    style F fill:#feca57

πŸ“ˆ Monitoring and Audit

πŸ“Š CloudWatch Dashboard

 1// lib/stacks/monitoring-stack.ts
 2import * as cdk from 'aws-cdk-lib';
 3import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
 4import * as cognito from 'aws-cdk-lib/aws-cognito';
 5import { Construct } from 'constructs';
 6
 7export interface MonitoringStackProps extends cdk.StackProps {
 8  userPool: cognito.UserPool;
 9}
10
11export class MonitoringStack extends cdk.Stack {
12  constructor(scope: Construct, id: string, props: MonitoringStackProps) {
13    super(scope, id, props);
14
15    const { userPool } = props;
16
17    const dashboard = new cloudwatch.Dashboard(this, 'AuthDashboard', {
18      dashboardName: 'Centralized-Auth-Metrics',
19    });
20
21    // Authentication metrics
22    dashboard.addWidgets(
23      new cloudwatch.GraphWidget({
24        title: 'Sign-In Success Rate',
25        width: 12,
26        left: [
27          new cloudwatch.Metric({
28            namespace: 'AWS/Cognito',
29            metricName: 'SignInSuccesses',
30            dimensionsMap: {
31              UserPool: userPool.userPoolId,
32            },
33            statistic: 'Sum',
34            period: cdk.Duration.minutes(5),
35          }),
36        ],
37        right: [
38          new cloudwatch.Metric({
39            namespace: 'AWS/Cognito',
40            metricName: 'SignInThrottles',
41            dimensionsMap: {
42              UserPool: userPool.userPoolId,
43            },
44            statistic: 'Sum',
45            period: cdk.Duration.minutes(5),
46            color: cloudwatch.Color.RED,
47          }),
48        ],
49      })
50    );
51
52    // User registration metrics
53    dashboard.addWidgets(
54      new cloudwatch.GraphWidget({
55        title: 'User Registrations',
56        width: 12,
57        left: [
58          new cloudwatch.Metric({
59            namespace: 'AWS/Cognito',
60            metricName: 'UserAuthentication',
61            dimensionsMap: {
62              UserPool: userPool.userPoolId,
63            },
64            statistic: 'Sum',
65          }),
66        ],
67      })
68    );
69
70    // Alarms
71    new cloudwatch.Alarm(this, 'HighFailedSignIns', {
72      metric: new cloudwatch.Metric({
73        namespace: 'AWS/Cognito',
74        metricName: 'SignInSuccesses',
75        dimensionsMap: {
76          UserPool: userPool.userPoolId,
77        },
78        statistic: 'Sum',
79      }),
80      threshold: 100,
81      evaluationPeriods: 2,
82      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
83      alarmDescription: 'Alert when sign-in failures are high',
84    });
85  }
86}

βœ… Pros and Cons Analysis

🎯 Single User Pool Approach

Pros:

  • βœ… Lower Cost: Single pool pricing, no duplication
  • βœ… Simpler Management: One place to manage all users
  • βœ… Easier Migration: Move users between tenants without pool migration
  • βœ… Unified Analytics: Single dashboard for all users
  • βœ… Shared Features: SSO, MFA policies apply consistently
  • βœ… Better for SaaS: Most SaaS applications use this pattern

Cons:

  • ❌ Limited Isolation: Users share same pool
  • ❌ Shared Limits: Rate limits apply to entire pool
  • ❌ Complex Permissions: Need careful group/attribute management
  • ❌ Single Region: Can’t have regional isolation easily

πŸ”„ Multiple User Pools Approach

Pros:

  • βœ… Strong Isolation: Complete separation between tenants
  • βœ… Custom Policies: Different password/MFA policies per tenant
  • βœ… Regional Deployment: Can deploy pools in different regions
  • βœ… Independent Scaling: Each pool scales independently
  • βœ… Compliance: Easier to meet strict isolation requirements

Cons:

  • ❌ Higher Cost: Pay for each pool
  • ❌ Complex Management: Multiple pools to maintain
  • ❌ Difficult Migration: Moving users between pools is complex
  • ❌ Operational Overhead: More infrastructure to monitor
  • ❌ Code Complexity: Application needs to handle multiple pools

πŸ“š Integration Guide for Services

πŸ”— React Web Application

 1// Example: React app integration
 2import { Amplify } from 'aws-amplify';
 3import { signIn, signOut, getCurrentUser } from 'aws-amplify/auth';
 4
 5// Configure Amplify
 6Amplify.configure({
 7  Auth: {
 8    Cognito: {
 9      userPoolId: 'us-east-1_xxxxx',
10      userPoolClientId: 'xxxxx',
11      loginWith: {
12        oauth: {
13          domain: 'auth.yourdomain.com',
14          scopes: ['openid', 'email', 'profile'],
15          redirectSignIn: ['http://localhost:3000/callback'],
16          redirectSignOut: ['http://localhost:3000'],
17          responseType: 'code',
18        },
19      },
20    },
21  },
22});
23
24// Sign in
25async function handleSignIn(username: string, password: string) {
26  try {
27    const user = await signIn({ username, password });
28    console.log('Sign in success:', user);
29  } catch (error) {
30    console.error('Sign in error:', error);
31  }
32}
33
34// Get current user
35async function getCurrentUserInfo() {
36  try {
37    const user = await getCurrentUser();
38    console.log('Current user:', user);
39  } catch (error) {
40    console.error('Not authenticated');
41  }
42}

πŸ“± API Gateway Integration

 1// Example: Calling protected API
 2import axios from 'axios';
 3import { fetchAuthSession } from 'aws-amplify/auth';
 4
 5async function callProtectedApi() {
 6  try {
 7    // Get access token
 8    const session = await fetchAuthSession();
 9    const accessToken = session.tokens?.accessToken.toString();
10
11    // Call API
12    const response = await axios.get(
13      'https://api.yourdomain.com/protected',
14      {
15        headers: {
16          Authorization: `Bearer ${accessToken}`,
17        },
18      }
19    );
20
21    console.log('API response:', response.data);
22  } catch (error) {
23    console.error('API error:', error);
24  }
25}

πŸš€ Deployment

 1# Install dependencies
 2npm install
 3
 4# Bootstrap CDK (first time only)
 5cdk bootstrap
 6
 7# Synthesize CloudFormation templates
 8cdk synth
 9
10# Deploy Cognito stack
11cdk deploy CognitoStack --context environment=development
12
13# Deploy triggers
14cdk deploy TriggersStack --context environment=development
15
16# Deploy API integration
17cdk deploy ApiIntegrationStack --context environment=development
18
19# Deploy monitoring
20cdk deploy MonitoringStack --context environment=development
21
22# Deploy all stacks
23cdk deploy --all --context environment=production --require-approval never

πŸ“‹ Summary and Best Practices

🎯 Key Takeaways

  1. Start with Single Pool: Use single user pool with groups for most use cases
  2. Custom Attributes: Leverage custom attributes for tenant isolation
  3. Group-based RBAC: Implement role-based access control with groups
  4. Lambda Triggers: Use triggers for custom business logic
  5. Monitoring: Set up comprehensive monitoring and alerting
  6. Security: Enable MFA, advanced security, and audit logging
  7. Documentation: Provide clear integration guides for developers

βœ… Design Checklist

  • Define authentication requirements
  • Choose single vs multiple pool strategy
  • Design group hierarchy
  • Define custom attributes
  • Configure password policies
  • Enable MFA (production)
  • Set up custom domain
  • Implement Lambda triggers
  • Configure app clients
  • Set up API Gateway authorizer
  • Implement monitoring and alarms
  • Create integration documentation
  • Test authentication flows
  • Set up backup and disaster recovery
  • Implement audit logging

πŸŽ“ Further Learning

🎯 Conclusion

Building a centralized user access control system with AWS Cognito provides a scalable, secure, and cost-effective solution for managing authentication and authorization across multiple services and applications.

By using AWS CDK with TypeScript, you can create infrastructure as code that’s maintainable, testable, and easily adaptable to changing requirements. The single user pool with groups and custom attributes approach offers the best balance of simplicity, isolation, and functionality for most use cases.

Key Benefits:

  • Centralized identity management
  • Consistent authentication across services
  • Fine-grained access control with RBAC
  • Scalable to millions of users
  • Built-in security features (MFA, advanced security)
  • Easy integration with AWS services and third-party applications

Related Posts:

Tags: #AWS #Cognito #Authentication #Authorization #CDK #TypeScript #Security #MultiTenant #RBAC #IAM #IdentityManagement #Serverless