Skip to content
~/sph.sh

Migrating from Serverless Framework to AWS CDK: Part 5 - Authentication, Authorization, and IAM

Implement robust authentication with Cognito, API Gateway authorizers, and fine-grained IAM policies when migrating from Serverless Framework to AWS CDK.

Migrating authentication and authorization from Serverless Framework to AWS CDK presents unique challenges that can impact both security posture and application performance. Organizations often discover their Serverless Framework implementations have accumulated security debt through organic growth and rapid iteration cycles.

Common patterns include functions with overly broad IAM permissions, scattered authorization logic across multiple custom authorizers, and insufficient audit trails for access control decisions. These issues become apparent during migration assessments and can significantly impact compliance requirements.

This guide covers rebuilding enterprise-grade authentication and authorization with AWS CDK while maintaining application availability throughout the migration process.

Series Navigation:

Understanding Authentication Migration Challenges

Before implementing solutions, it's essential to assess existing authentication patterns. Common issues discovered during migration assessments include:

Common Serverless Framework Authentication Patterns

User Management: Three different Cognito pools across environments, manually created, zero documentation of custom attributes.

Authorization: Multiple Lambda authorizers with different JWT validation logic, no caching, high authorization latency.

IAM Permissions: Numerous Lambda functions with wildcard permissions. Critical functions often have overly broad access to resources.

Secrets: API keys hardcoded in environment variables, shared across environments, infrequent rotation cycles.

Audit Trail: Limited logging of authorization decisions. Insufficient visibility into access patterns.

Migration Impact Considerations

  • Compliance risk: Potential regulatory fines for over-broad data access and insufficient access controls
  • Performance impact: High authorization latency contributing to overall request time
  • Operational overhead: Significant time spent resolving authentication issues and access problems
  • Security debt: Multiple functions with unnecessary permissions creating expanded attack surface

Production-Grade Cognito Implementation

Implementing enterprise-grade authentication requires careful consideration of security controls and operational requirements. Here's a comprehensive approach:

yaml
# serverless.ymlresources:  Resources:    UserPool:      Type: AWS::Cognito::UserPool      Properties:        UserPoolName: ${self:service}-${opt:stage}-users        Schema:          - Name: email            Required: true            Mutable: false          - Name: role            AttributeDataType: String            Mutable: true        AutoVerifiedAttributes:          - email        Policies:          PasswordPolicy:            MinimumLength: 8            RequireUppercase: true            RequireLowercase: true            RequireNumbers: true            RequireSymbols: true
    UserPoolClient:      Type: AWS::Cognito::UserPoolClient      Properties:        ClientName: ${self:service}-${opt:stage}-client        UserPoolId: !Ref UserPool        GenerateSecret: false        ExplicitAuthFlows:          - ALLOW_USER_PASSWORD_AUTH          - ALLOW_REFRESH_TOKEN_AUTH

CDK Implementation for Enterprise Authentication

This Cognito implementation follows security best practices and scales for enterprise requirements:

typescript
// lib/constructs/auth/production-cognito.tsimport {  UserPool,  UserPoolClient,  AccountRecovery,  Mfa,  UserPoolOperation,  StringAttribute,  ClientAttributes,  OAuthScope,  UserPoolDomain,  CognitoUserPoolsAuthorizer,  AdvancedSecurityMode,  UserInviteMessageAction} from 'aws-cdk-lib/aws-cognito';import { Duration, RemovalPolicy, Tags } from 'aws-cdk-lib';import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs';import { Alarm, Metric, TreatMissingData } from 'aws-cdk-lib/aws-cloudwatch';
export class ProductionCognitoAuth extends Construct {  public readonly userPool: UserPool;  public readonly userPoolClient: UserPoolClient;  public readonly authorizer: CognitoUserPoolsAuthorizer;
  constructor(scope: Construct, id: string, props: {    stage: string;    domainPrefix?: string;    callbackUrls?: string[];    api: RestApi;  }) {    super(scope, id);
    // Create user pool with audit-compliant settings    this.userPool = new UserPool(this, 'EnterpriseUserPool', {      userPoolName: `my-service-${props.stage}-users-v2`,      // Enhanced security: no self-signup in production      selfSignUpEnabled: props.stage !== 'prod',      signInAliases: {        email: true,        username: false,  // Email-only sign-in reduces attack surface      },      signInCaseSensitive: false,      autoVerify: { email: true },
      // Enterprise-compliant password policy      passwordPolicy: {        minLength: 14,  // Enterprise security requirement        requireLowercase: true,        requireUppercase: true,        requireDigits: true,        requireSymbols: true,        tempPasswordValidity: Duration.hours(24),  // Reduced from 3 days      },
      // Comprehensive user attributes for RBAC      standardAttributes: {        email: { required: true, mutable: false },        givenName: { required: true, mutable: true },        familyName: { required: true, mutable: true },      },      customAttributes: {        // Role-based access control        role: new StringAttribute({ mutable: true }),        department: new StringAttribute({ mutable: true }),        accessLevel: new StringAttribute({ mutable: true }),        // Audit trail attributes        lastLoginDate: new StringAttribute({ mutable: true }),        createdBy: new StringAttribute({ mutable: false }),        // Compliance attributes        dataAccessLevel: new StringAttribute({ mutable: true }),        complianceFlags: new StringAttribute({ mutable: true }),      },
      // Enterprise security settings      accountRecovery: AccountRecovery.EMAIL_ONLY,      mfa: props.stage === 'prod' ? Mfa.REQUIRED : Mfa.OPTIONAL,      mfaSecondFactor: {        sms: false,  // TOTP only for security        otp: true,      },
      // Advanced threat protection with current API      userPoolAddOns: {        advancedSecurityMode: props.stage === 'prod'          ? AdvancedSecurityMode.ENFORCED          : AdvancedSecurityMode.AUDIT,      },      // Additional threat protection settings      userInviteMessageAction: UserInviteMessageAction.SUPPRESS, // Prevent invitation abuse      enableSmsRole: false, // Disable SMS for enhanced security
      // Email configuration for branded communications      emailSettings: {        from: '[email protected]',        replyTo: '[email protected]',      },
      // Device tracking for security      deviceTracking: {        challengeRequiredOnNewDevice: true,        deviceOnlyRememberedOnUserPrompt: false,      },
      // Data protection      removalPolicy: props.stage === 'prod' ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY,      deletionProtection: props.stage === 'prod',    });
    // Add enterprise Lambda triggers    this.addSecurityTriggers(props.stage);
    // Create production app client    this.userPoolClient = new UserPoolClient(this, 'EnterpriseClient', {      userPool: this.userPool,      userPoolClientName: `my-service-${props.stage}-client-v2`,
      // Allowed authentication flows      authFlows: {        userPassword: false,  // Disable less secure flow        userSrp: true,       // Secure Remote Password protocol        custom: true,        // Custom auth challenges        adminUserPassword: props.stage !== 'prod',  // Admin flow only in non-prod      },
      // OAuth configuration for enterprise SSO      oAuth: {        flows: {          authorizationCodeGrant: true,          implicitCodeGrant: false,  // Disable implicit flow for security          clientCredentials: false,        },        scopes: [          OAuthScope.EMAIL,          OAuthScope.OPENID,          OAuthScope.PROFILE,          OAuthScope.custom('read:profile'),          OAuthScope.custom('write:profile'),        ],        callbackUrls: props.callbackUrls || [],        logoutUrls: [`https://${props.stage === 'prod' ? 'app' : props.stage}.yourcompany.com/logout`],      },
      generateSecret: false,  // Public client for SPA
      // Fine-grained attribute access      readAttributes: new ClientAttributes()        .withStandardAttributes({          email: true,          emailVerified: true,          givenName: true,          familyName: true,        })        .withCustomAttributes('role', 'department', 'accessLevel'),
      writeAttributes: new ClientAttributes()        .withCustomAttributes('lastLoginDate'),  // Limited write access
      // Security-focused token settings      idTokenValidity: Duration.minutes(30),     // Short-lived for security      accessTokenValidity: Duration.minutes(30), // Short-lived for security      refreshTokenValidity: Duration.days(1),    // Daily re-authentication
      // Enhanced security options      preventUserExistenceErrors: true,      enableTokenRevocation: true,
      // Custom token settings      authSessionValidity: Duration.minutes(3),  // Quick auth flow timeout    });
    // Create API Gateway authorizer    this.authorizer = new CognitoUserPoolsAuthorizer(this, 'CognitoAuthorizer', {      cognitoUserPools: [this.userPool],      authorizerName: `${props.api.restApiName}-cognito-auth`,      identitySource: 'method.request.header.Authorization',      resultsCacheTtl: Duration.minutes(5),  // Cache for performance    });
    // Add custom domain for branded experience    if (props.domainPrefix) {      new UserPoolDomain(this, 'UserPoolDomain', {        userPool: this.userPool,        cognitoDomainPrefix: `${props.domainPrefix}-${props.stage}`,      });    }
    // Production monitoring and alerting    this.addProductionMonitoring(props.stage);
    // Compliance tagging    Tags.of(this).add('DataClassification', 'PII');    Tags.of(this).add('Compliance', 'Enterprise-Security');    Tags.of(this).add('Service', 'authentication');    Tags.of(this).add('Stage', props.stage);  }
  private addSecurityTriggers(stage: string) {    // Pre-authentication security checks    const preAuthFn = new NodejsFunction(this, 'PreAuthSecurityFunction', {      entry: 'src/auth/triggers/pre-auth-security.ts',      handler: 'handler',      timeout: Duration.seconds(10),      logRetention: RetentionDays.ONE_MONTH,      environment: {        STAGE: stage,        SECURITY_LOG_LEVEL: stage === 'prod' ? 'WARN' : 'DEBUG',      },    });
    this.userPool.addTrigger(UserPoolOperation.PRE_AUTHENTICATION, preAuthFn);
    // Post-authentication audit logging    const postAuthFn = new NodejsFunction(this, 'PostAuthAuditFunction', {      entry: 'src/auth/triggers/post-auth-audit.ts',      handler: 'handler',      timeout: Duration.seconds(10),      logRetention: RetentionDays.ONE_YEAR,  // Long retention for audit      environment: {        STAGE: stage,        AUDIT_TABLE: `auth-audit-${stage}`,      },    });
    this.userPool.addTrigger(UserPoolOperation.POST_AUTHENTICATION, postAuthFn);
    // User creation with RBAC setup    const postConfirmFn = new NodejsFunction(this, 'PostConfirmationRBACFunction', {      entry: 'src/auth/triggers/post-confirmation-rbac.ts',      handler: 'handler',      timeout: Duration.seconds(30),      environment: {        STAGE: stage,        USERS_TABLE: `users-${stage}`,        ROLES_TABLE: `user-roles-${stage}`,        DEFAULT_ROLE: 'viewer',  // Least privilege by default      },    });
    this.userPool.addTrigger(UserPoolOperation.POST_CONFIRMATION, postConfirmFn);  }
  private addProductionMonitoring(stage: string) {    if (stage !== 'prod') return;
    // Failed authentication alarm    new Alarm(this, 'FailedAuthAlarm', {      metric: new Metric({        namespace: 'AWS/Cognito',        metricName: 'SignInFailures',        dimensionsMap: {          UserPool: this.userPool.userPoolId,        },        statistic: 'Sum',        period: Duration.minutes(5),      }),      threshold: 50,  // 50 failed attempts in 5 minutes      evaluationPeriods: 1,      treatMissingData: TreatMissingData.NOT_BREACHING,      alarmDescription: 'High number of authentication failures detected',    });
    // Compromised credentials alarm    new Alarm(this, 'CompromisedCredentialsAlarm', {      metric: new Metric({        namespace: 'AWS/Cognito',        metricName: 'CompromisedCredentialsRisk',        dimensionsMap: {          UserPool: this.userPool.userPoolId,        },        statistic: 'Sum',        period: Duration.minutes(15),      }),      threshold: 1,  // Any compromised credential is critical      evaluationPeriods: 1,      alarmDescription: 'Compromised credentials detected',    });  }}

Lambda Triggers for Custom Auth Flows

typescript
// src/auth/triggers/pre-signup.tsimport { PreSignUpTriggerEvent, PreSignUpTriggerHandler } from 'aws-lambda';
export const handler: PreSignUpTriggerHandler = async (event) => {  console.log('Pre-signup event:', JSON.stringify(event, null, 2));
  // Validate email domain for corporate accounts  const email = event.request.userAttributes.email;  const allowedDomains = ['company.com', 'partner.com'];  const domain = email.split('@')[1];
  if (!allowedDomains.includes(domain)) {    throw new Error('Registration is restricted to corporate email addresses');  }
  // Auto-confirm corporate emails  if (domain === 'company.com') {    event.response.autoConfirmUser = true;    event.response.autoVerifyEmail = true;  }
  return event;};
// src/auth/triggers/post-confirmation.tsimport { PostConfirmationTriggerEvent, PostConfirmationTriggerHandler } from 'aws-lambda';import { DynamoDBClient } from '@aws-sdk/client-dynamodb';import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export const handler: PostConfirmationTriggerHandler = async (event) => {  console.log('Post-confirmation event:', JSON.stringify(event, null, 2));
  // Create user record in DynamoDB  await client.send(new PutCommand({    TableName: process.env.USERS_TABLE,    Item: {      userId: event.request.userAttributes.sub,      email: event.request.userAttributes.email,      role: event.request.userAttributes['custom:role'] || 'user',      department: event.request.userAttributes['custom:department'],      createdAt: new Date().toISOString(),      status: 'active',    },  }));
  return event;};

Authorization Performance Optimization

Legacy authorization setups often create performance bottlenecks. Common issues include:

  1. JWT decode: Significant processing time without optimization
  2. Cognito JWK fetch: Network calls to Cognito for each request without caching
  3. Signature verification: Computational overhead for RS256 verification
  4. Database role lookup: Additional queries for role-based access control
  5. Cumulative latency: Authorization becomes substantial portion of total request time

Performance impact: Authorization latency contributes significantly to overall API response time, affecting user experience and mobile application performance.

High-Performance JWT Authorization

This caching-optimized authorizer significantly reduces authorization latency through strategic caching and optimization:

typescript
// lib/constructs/auth/high-performance-jwt-authorizer.tsimport {  TokenAuthorizer,  IdentitySource,  IRestApi} from 'aws-cdk-lib/aws-apigateway';import { Duration } from 'aws-cdk-lib';import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';import { RetentionDays } from 'aws-cdk-lib/aws-logs';
export class HighPerformanceJwtAuthorizer extends TokenAuthorizer {  constructor(scope: Construct, id: string, props: {    api: IRestApi;    userPoolId: string;    region: string;    stage: string;  }) {    // Optimized authorizer function for production    const authorizerFunction = new NodejsFunction(scope, 'OptimizedAuthorizerFunction', {      entry: 'src/auth/production-jwt-authorizer.ts',      handler: 'handler',      // Reserved concurrency for consistent performance (not provisioned to avoid costs)      reservedConcurrentExecutions: props.stage === 'prod' ? 10 : undefined,      timeout: Duration.seconds(5),  // Quick timeout for fast failures      memorySize: 512,  // Optimized for JWT processing      logRetention: RetentionDays.ONE_MONTH,      environment: {        USER_POOL_ID: props.userPoolId,        REGION: props.region,        STAGE: props.stage,        // Performance optimization flags        ENABLE_METRICS: props.stage === 'prod' ? 'true' : 'false',        CACHE_TIMEOUT_MS: '300000',  // 5 minutes      },      bundling: {        // Minimize bundle size for faster cold starts        minify: true,        target: 'node22', // Latest LTS for better performance        // Include only essential dependencies        nodeModules: ['jsonwebtoken', 'jwk-to-pem'],        externalModules: ['@aws-sdk/*'],      },    });
    super(scope, id, {      restApi: props.api,      handler: authorizerFunction,      identitySource: IdentitySource.header('Authorization'),      // API Gateway caching for performance (reduces Lambda invocations)      resultsCacheTtl: Duration.minutes(5),  // Balance between security and performance      authorizerName: `${props.api.restApiName}-jwt-authorizer-v2`,      // Strict token validation      validationRegex: '^Bearer [A-Za-z0-9\\-_=]+\\.[A-Za-z0-9\\-_=]+\\.[A-Za-z0-9\\-_.+/=]*$',    });  }}
// src/auth/production-jwt-authorizer.tsimport { APIGatewayTokenAuthorizerEvent, APIGatewayAuthorizerResult } from 'aws-lambda';import jwt from 'jsonwebtoken';import jwkToPem from 'jwk-to-pem';
// RECOMMENDED: Use AWS-provided 'aws-jwt-verify' library for new implementations// It provides built-in optimizations, better error handling, and official AWS support.// Example implementation with aws-jwt-verify://// import { CognitoJwtVerifier } from 'aws-jwt-verify';// const verifier = CognitoJwtVerifier.create({//   userPoolId: process.env.USER_POOL_ID!,//   tokenUse: 'access',//   clientId: process.env.CLIENT_ID,// });// const payload = await verifier.verify(token);//// This implementation uses jsonwebtoken for compatibility with existing setups.
// Multi-level caching for performancelet cachedKeys: Map<string, string> | null = null;let cacheTimestamp: number = 0;const CACHE_TIMEOUT = parseInt(process.env.CACHE_TIMEOUT_MS || '300000');
// Performance metrics (collected in production)const metrics = {  authCount: 0,  keyFetchCount: 0,  cacheHits: 0,  averageLatency: 0,};
async function getPublicKeys(): Promise<Map<string, string>> {  const now = Date.now();
  // Return cached keys if still valid  if (cachedKeys && (now - cacheTimestamp) < CACHE_TIMEOUT) {    metrics.cacheHits++;    return cachedKeys;  }
  const startTime = Date.now();  metrics.keyFetchCount++;
  try {    const jwksUrl = `https://cognito-idp.${process.env.REGION}.amazonaws.com/${process.env.USER_POOL_ID}/.well-known/jwks.json`;
    // Use fetch with timeout and retry logic    const controller = new AbortController();    const timeoutId = setTimeout(() => controller.abort(), 3000);
    const response = await fetch(jwksUrl, {      signal: controller.signal,      headers: {        'Cache-Control': 'max-age=300',  // Request 5-minute cache      },    });
    clearTimeout(timeoutId);
    if (!response.ok) {      throw new Error(`JWK fetch failed: ${response.status}`);    }
    const jwks = await response.json();
    // Convert and cache JWKs    cachedKeys = new Map();    jwks.keys.forEach((key: any) => {      try {        cachedKeys!.set(key.kid, jwkToPem(key));      } catch (error) {        console.warn(`Failed to convert JWK ${key.kid}:`, error);      }    });
    cacheTimestamp = now;
    const fetchTime = Date.now() - startTime;    console.log(`JWK fetch completed in ${fetchTime}ms, cached ${cachedKeys.size} keys`);
    return cachedKeys;  } catch (error) {    console.error('JWK fetch failed:', error);
    // Return stale cache if available as fallback    if (cachedKeys) {      console.warn('Using stale JWK cache due to fetch failure');      return cachedKeys;    }
    throw new Error('Unable to fetch signing keys');  }}
export const handler = async (  event: APIGatewayTokenAuthorizerEvent): Promise<APIGatewayAuthorizerResult> => {  const startTime = Date.now();  metrics.authCount++;
  // Enhanced request logging for audit trail  const requestId = Math.random().toString(36).substring(7);  console.log('Authorization request:', {    requestId,    methodArn: event.methodArn,    requestTime: new Date().toISOString(),    sourceIp: event.requestContext?.identity?.sourceIp,    userAgent: event.requestContext?.identity?.userAgent,  });
  try {    // Early token validation    if (!event.authorizationToken || !event.authorizationToken.startsWith('Bearer ')) {      throw new Error('Missing or invalid authorization header format');    }
    const token = event.authorizationToken.replace('Bearer ', '');
    // Basic token format validation    const tokenParts = token.split('.');    if (tokenParts.length !== 3) {      throw new Error('Invalid JWT format');    }
    // Decode token (doesn't verify signature yet)    const decodedToken = jwt.decode(token, { complete: true });    if (!decodedToken || typeof decodedToken === 'string') {      throw new Error('Invalid token structure');    }
    // Validate token expiration early    const payload = decodedToken.payload as any;    const now = Math.floor(Date.now() / 1000);
    if (payload.exp && payload.exp < now) {      throw new Error('Token has expired');    }
    if (payload.iat && payload.iat > now + 300) {      throw new Error('Token issued in the future');    }
    // Get signing keys (cached)    const keys = await getPublicKeys();    const signingKey = keys.get(decodedToken.header.kid!);
    if (!signingKey) {      throw new Error(`Signing key not found for kid: ${decodedToken.header.kid}`);    }
    // Verify JWT signature and claims    const verifiedPayload = jwt.verify(token, signingKey, {      algorithms: ['RS256'],      issuer: `https://cognito-idp.${process.env.REGION}.amazonaws.com/${process.env.USER_POOL_ID}`,      audience: payload.aud,      clockTolerance: 30,  // Allow 30 seconds clock skew    }) as any;
    // Extract user information    const userId = verifiedPayload.sub;    const email = verifiedPayload.email;    const role = verifiedPayload['custom:role'] || 'user';    const accessLevel = verifiedPayload['custom:accessLevel'] || 'basic';
    // Generate resource-specific policy    const policy = generateEnhancedPolicy(      userId,      'Allow',      event.methodArn,      {        userId,        email,        role,        accessLevel,        tokenUse: verifiedPayload.token_use,        authTime: verifiedPayload.auth_time?.toString(),        requestId,      }    );
    const totalTime = Date.now() - startTime;    metrics.averageLatency = (metrics.averageLatency + totalTime) / 2;
    // Log successful authorization    console.log('Authorization successful:', {      requestId,      userId,      email,      role,      accessLevel,      latency: totalTime,    });
    // Report metrics periodically    if (metrics.authCount % 100 === 0 && process.env.ENABLE_METRICS === 'true') {      console.log('Authorization metrics:', {        totalAuthorizations: metrics.authCount,        keyFetches: metrics.keyFetchCount,        cacheHitRate: (metrics.cacheHits / metrics.authCount * 100).toFixed(2) + '%',        averageLatency: metrics.averageLatency.toFixed(2) + 'ms',      });    }
    return policy;
  } catch (error) {    const totalTime = Date.now() - startTime;
    console.error('Authorization failed:', {      requestId,      error: error.message,      latency: totalTime,      stackTrace: error.stack,    });
    // For debugging in non-production    if (process.env.STAGE !== 'prod') {      console.debug('Token details:', {        token: event.authorizationToken,        methodArn: event.methodArn,      });    }
    throw new Error('Unauthorized');  // Always return generic error to client  }};
function generateEnhancedPolicy(  principalId: string,  effect: 'Allow' | 'Deny',  resource: string,  context: Record<string, any>): APIGatewayAuthorizerResult {  // Generate wildcard resource for better caching  const resourceParts = resource.split('/');  const wildcardResource = resourceParts.slice(0, -1).join('/') + '/*';
  return {    principalId,    policyDocument: {      Version: '2012-10-17',      Statement: [        {          Action: 'execute-api:Invoke',          Effect: effect,          Resource: wildcardResource,  // Enable broader caching        },      ],    },    context: {      // Convert all context values to strings (API Gateway requirement)      ...Object.entries(context).reduce((acc, [key, value]) => ({        ...acc,        [key]: String(value || ''),      }), {}),    },    // Enable longer TTL for stable users (API Gateway cache)    ttlOverride: context.role === 'admin' ? 300 : 120,  // Admin tokens cached longer (seconds)  };}

Request-Based Authorizer with Groups

typescript
// lib/constructs/auth/group-authorizer.tsexport class GroupAuthorizer extends RequestAuthorizer {  constructor(scope: Construct, id: string, props: {    api: IRestApi;    userPoolId: string;    requiredGroups?: string[];  }) {    const authorizerFunction = new NodejsFunction(scope, 'GroupAuthorizerFunction', {      entry: 'src/auth/group-authorizer.ts',      handler: 'handler',      environment: {        USER_POOL_ID: props.userPoolId,        REQUIRED_GROUPS: JSON.stringify(props.requiredGroups || []),      },    });
    super(scope, id, {      restApi: props.api,      handler: authorizerFunction,      identitySources: [IdentitySource.header('Authorization')],      resultsCacheTtl: Duration.minutes(5),      authorizerName: `${props.api.restApiName}-group-authorizer`,    });  }}

Common IAM Permission Issues

Security assessments often reveal functions with overly broad IAM policies. A typical problematic configuration:

json
{  "Version": "2012-10-17",  "Statement": [    {      "Effect": "Allow",      "Action": "*",      "Resource": "*"    }  ]}

Impact: Functions with wildcard permissions can access any AWS resource, creating significant security risks. Compromised functions with excessive permissions can lead to account-wide security breaches.

Business consequences: Regulatory compliance failures, security audit issues, and potential enterprise customer concerns.

Least Privilege IAM Architecture

This role-based system implements security best practices with minimal required permissions:

typescript
// lib/constructs/security/lambda-role.tsimport { Role, PolicyStatement, Effect, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
export class LeastPrivilegeLambdaRole extends Role {  constructor(scope: Construct, id: string, props: {    functionName: string;    stage: string;    additionalStatements?: PolicyStatement[];  }) {    super(scope, id, {      assumedBy: new ServicePrincipal('lambda.amazonaws.com'),      roleName: `${props.functionName}-${props.stage}-role`,      description: `Execution role for ${props.functionName}`,    });
    // Basic Lambda permissions    this.addToPolicy(new PolicyStatement({      effect: Effect.ALLOW,      actions: [        'logs:CreateLogGroup',        'logs:CreateLogStream',        'logs:PutLogEvents',      ],      resources: [        `arn:aws:logs:*:*:log-group:/aws/lambda/${props.functionName}-*`,      ],    }));
    // X-Ray tracing    this.addToPolicy(new PolicyStatement({      effect: Effect.ALLOW,      actions: [        'xray:PutTraceSegments',        'xray:PutTelemetryRecords',      ],      resources: ['*'],    }));
    // Add custom statements    props.additionalStatements?.forEach(statement => {      this.addToPolicy(statement);    });  }}

Resource-Based Policies

typescript
// lib/constructs/security/resource-policies.tsexport class SecureApiGateway extends RestApi {  constructor(scope: Construct, id: string, props: RestApiProps & {    allowedSourceIps?: string[];    allowedVpcs?: string[];  }) {    super(scope, id, props);
    if (props.allowedSourceIps || props.allowedVpcs) {      const conditions: any = {};
      if (props.allowedSourceIps) {        conditions['IpAddress'] = {          'aws:SourceIp': props.allowedSourceIps,        };      }
      if (props.allowedVpcs) {        conditions['StringEquals'] = {          'aws:SourceVpc': props.allowedVpcs,        };      }
      this.addGatewayResponse('UNAUTHORIZED', {        statusCode: '401',        responseHeaders: {          'Access-Control-Allow-Origin': "'*'",        },        templates: {          'application/json': '{"error": "Unauthorized access"}',        },      });
      // Resource policy      this.node.addDependency(        new PolicyDocument({          statements: [            new PolicyStatement({              effect: Effect.DENY,              principals: [new AnyPrincipal()],              actions: ['execute-api:Invoke'],              resources: ['execute-api:/*/*/*'],              conditions: {                ...conditions,              },            }),            new PolicyStatement({              effect: Effect.ALLOW,              principals: [new AnyPrincipal()],              actions: ['execute-api:Invoke'],              resources: ['execute-api:/*/*/*'],            }),          ],        })      );    }  }}

Cross-Service Authentication

Service-to-Service Auth with IAM

typescript
// lib/constructs/auth/service-auth.tsexport class ServiceAuthFunction extends ServerlessFunction {  constructor(scope: Construct, id: string, props: ServerlessFunctionProps & {    targetServiceUrl: string;  }) {    super(scope, id, {      ...props,      environment: {        ...props.environment,        TARGET_SERVICE_URL: props.targetServiceUrl,      },    });
    // Grant permission to invoke other services    this.addToRolePolicy(new PolicyStatement({      effect: Effect.ALLOW,      actions: ['execute-api:Invoke'],      resources: [        `arn:aws:execute-api:${Stack.of(this).region}:*:*/*/*/*`,      ],    }));  }}
// src/libs/service-client.tsimport { SignatureV4 } from '@aws-sdk/signature-v4';import { Sha256 } from '@aws-crypto/sha256-js';
export class ServiceClient {  private signer: SignatureV4;
  constructor(private baseUrl: string) {    this.signer = new SignatureV4({      service: 'execute-api',      region: process.env.AWS_REGION!,      credentials: {        accessKeyId: process.env.AWS_ACCESS_KEY_ID!,        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,        sessionToken: process.env.AWS_SESSION_TOKEN,      },      sha256: Sha256,    });  }
  async request(path: string, method: string, body?: any) {    const url = new URL(path, this.baseUrl);
    const signedRequest = await this.signer.sign({      method,      hostname: url.hostname,      path: url.pathname,      protocol: url.protocol,      headers: {        'Content-Type': 'application/json',        host: url.hostname,      },      body: body ? JSON.stringify(body) : undefined,    });
    const response = await fetch(url.toString(), {      method,      headers: signedRequest.headers,      body: signedRequest.body,    });
    return response.json();  }}

API Key Management

Secure API Key Distribution

typescript
// lib/constructs/auth/api-key-manager.tsexport class ApiKeyManager extends Construct {  private keys: Map<string, IApiKey> = new Map();
  constructor(scope: Construct, id: string, props: {    api: IRestApi;    stage: string;  }) {    super(scope, id);
    // Usage plan for rate limiting    const plan = new UsagePlan(this, 'UsagePlan', {      name: `${props.api.restApiName}-plan`,      throttle: {        rateLimit: 100,        burstLimit: 200,      },      quota: {        limit: 10000,        period: Period.DAY,      },    });
    plan.addApiStage({      stage: props.api.deploymentStage,    });  }
  createApiKey(name: string, customerId: string): IApiKey {    const key = new ApiKey(this, `ApiKey-${name}`, {      apiKeyName: `${name}-key`,      description: `API key for ${name}`,      customerId,      generateDistinctId: true,    });
    // Store in Secrets Manager    const secret = new Secret(this, `ApiKeySecret-${name}`, {      secretName: `/api-keys/${name}`,      generateSecretString: {        secretStringTemplate: JSON.stringify({ customerId }),        generateStringKey: 'apiKey',        includeSpace: false,      },    });
    // Associate key value with secret    new CustomResource(this, `StoreApiKey-${name}`, {      serviceToken: this.getKeyStorageFunction().functionArn,      properties: {        SecretId: secret.secretArn,        ApiKeyId: key.keyId,      },    });
    this.keys.set(name, key);    return key;  }}

Migration Security Checklist

Authentication Migration

  • Map Cognito user attributes to existing schema
  • Implement user migration Lambda trigger
  • Test password policy compatibility
  • Verify MFA settings match requirements
  • Set up proper account recovery flows

Authorization Migration

  • Convert custom authorizers to CDK
  • Implement proper caching strategies
  • Test token validation thoroughly
  • Verify CORS settings for auth endpoints
  • Map existing roles to new structure

IAM Migration

  • Audit existing Lambda roles
  • Implement least privilege principles
  • Remove wildcard permissions
  • Add resource-based policies where needed
  • Test cross-account access if required

Security Best Practices

typescript
// lib/constructs/security/security-headers.tsexport function addSecurityHeaders(api: IRestApi) {  const responseParameters = {    'method.response.header.X-Content-Type-Options': "'nosniff'",    'method.response.header.X-Frame-Options': "'DENY'",    'method.response.header.X-XSS-Protection': "'1; mode=block'",    'method.response.header.Strict-Transport-Security':      "'max-age=31536000; includeSubDomains'",    'method.response.header.Content-Security-Policy':      "'default-src 'self'",  };
  // Add to all methods  api.methods.forEach(method => {    method.addMethodResponse({      statusCode: '200',      responseParameters: Object.keys(responseParameters).reduce(        (acc, key) => ({ ...acc, [key]: true }),        {}      ),    });  });}

Security Migration Benefits

Implementing enterprise-grade authentication and authorization provides measurable improvements across multiple areas:

Performance Improvements

  • Authorization latency: Significant reduction through aggressive caching and optimized Lambda containers
  • Cache hit rate: High efficiency with JWK caching and appropriate TTL settings
  • API response time: Substantial improvement in overall request processing
  • Mobile app perceived performance: Enhanced user experience through reduced latency

Security Posture

  • Over-privileged functions: Complete elimination of excessive permissions
  • Wildcard IAM permissions: Removal of all wildcard access patterns
  • Audit trail coverage: Comprehensive logging of all authentication events
  • Failed auth detection: Automated alerting for security incidents
  • Compliance status: Achievement of enterprise compliance requirements

Operational Efficiency

  • Auth troubleshooting time: Significant reduction in time spent resolving authentication issues
  • Security incidents: Dramatic decrease in security-related incidents
  • Authorization cache hit rate: High efficiency with optimized TTL configuration
  • JWT validation errors: Substantial reduction through improved client-side token management

Business Impact

  • Enterprise deals: Improved ability to meet enterprise security requirements
  • Compliance audit: Achievement of enterprise security compliance requirements
  • Regulatory risk: Significant reduction in potential compliance violations
  • Customer trust: Enhanced security posture improving customer confidence

CDK Version Compatibility Note

This implementation is tested with AWS CDK v2.100+. Some Cognito properties and advanced security features may differ between CDK versions. Always verify current CDK documentation for the latest API changes, especially for Cognito advanced threat protection configuration.

Hard-Learned Security Lessons

1. Start with Least Privilege, Always

Before: "Action": "*" because "it's faster to ship" After: Explicit permissions for every function, every resource Impact: Substantial reduction in attack surface

2. Performance and Security Aren't Mutually Exclusive

Before: "Security adds latency" - uncached JWT verification on every request After: Proper caching (JWK keys, API Gateway results) made auth faster AND more secure Impact: Significant latency reduction with stronger security controls

3. Audit Trail is Non-Negotiable

Before: Zero visibility into who accessed what After: Every auth decision logged with full context Impact: Achieved compliance requirements, enabled enterprise adoption

4. Cache Everything (Securely)

Before: JWK fetch on every request with significant network overhead After: Multi-level caching with appropriate TTL and fallback to stale cache Impact: High cache hit rate with substantially improved authorization performance

5. Role-Based Access Control Scales

Before: Ad-hoc permissions per function After: Standardized roles with clear responsibilities Impact: Simplified management, better security

What's Next

Your serverless application now has enterprise-grade authentication and authorization with measurable performance improvements. User management is robust with proper controls, APIs are protected by optimized JWT verification with caching, and IAM policies follow strict least privilege principles.

In Part 6, we'll bring the entire migration together:

  • Complete migration strategies and timelines
  • Testing approaches proven in production environments
  • Safe rollback procedures for risk mitigation
  • Performance optimization across the entire stack
  • Monitoring and observability that prevents incidents

The security foundation is solid. Let's finish this migration properly.

Migrating from Serverless Framework to AWS CDK

A comprehensive 6-part guide covering the complete migration process from Serverless Framework to AWS CDK, including setup, implementation patterns, and best practices.

Progress5/6 posts completed

Related Posts