π― 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
| Aspect | Single User Pool | Multiple User Pools |
|---|---|---|
| Use Case | Single tenant, unified user base | Multi-tenant, isolated environments |
| Complexity | Low | High |
| Cost | Lower (one pool) | Higher (multiple pools) |
| Isolation | Group-based | Pool-based (stronger) |
| Management | Centralized | Distributed |
| Scalability | Very high (millions of users) | High per pool |
| Customization | Shared settings | Per-tenant settings |
| Migration | Easier | More complex |
β Recommended: Single User Pool with Groups
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:
- Regulatory Compliance: Data residency requirements (different regions)
- Strong Tenant Isolation: Complete separation of user data
- Different Security Policies: Varying password policies per tenant
- Custom Branding: Unique hosted UI per tenant
- 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
| Strategy | Use Case | Isolation | Complexity | Cost |
|---|---|---|---|---|
| Single Pool + Groups | SaaS with shared infrastructure | Group-level | Low | Low |
| Single Pool + Custom Attributes | Multi-tenant with shared users | Attribute-level | Low | Low |
| Multiple Pools per Tenant | Enterprise B2B | Pool-level | High | High |
| Multiple Pools per Region | Global apps with data residency | Region-level | Medium | Medium |
| Hybrid (Pool + Groups) | Mix of enterprise and SMB | Multi-level | High | Medium |
β Recommended: Single Pool with Groups + Custom Attributes
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
- Start with Single Pool: Use single user pool with groups for most use cases
- Custom Attributes: Leverage custom attributes for tenant isolation
- Group-based RBAC: Implement role-based access control with groups
- Lambda Triggers: Use triggers for custom business logic
- Monitoring: Set up comprehensive monitoring and alerting
- Security: Enable MFA, advanced security, and audit logging
- 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
- AWS Cognito: Amazon Cognito Documentation
- OAuth 2.0: OAuth 2.0 Simplified
- JWT: JWT.io
- Multi-tenancy: AWS Multi-Tenant SaaS Guidance
π― 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:
- Deploying Hugging Face Models to AWS: A Complete Guide with CDK, SageMaker, and Lambda
- Building Production Kubernetes Platform with AWS EKS and CDK
- Building Serverless URL Shortener with AWS CDK
- TypeScript Best Practices: A Comprehensive Guide to Type-Safe Development
Tags: #AWS #Cognito #Authentication #Authorization #CDK #TypeScript #Security #MultiTenant #RBAC #IAM #IdentityManagement #Serverless