İçeriğe atla

Serverless Framework'ten AWS CDK'ya Geçiş: Bölüm 5 - Authentication, Authorization ve IAM

Serverless Framework'ten AWS CDK'ya geçerken Cognito ile güçlü kimlik doğrulama, API Gateway authorizer'lar ve ince taneli IAM politikaları uygulama.

Ayhan Sipahi Ayhan Sipahi

Serverless Framework’ten AWS CDK’ya authentication ve authorization migrasyonu, genellikle birikmiş bir güvenlik borcunu ortaya çıkarır: organik büyüyen ve hiç gözden geçirilmemiş izinler ve authorization logic’i.

Yaygın pattern’lar arasında aşırı geniş IAM izinleri olan fonksiyonlar, multiple custom authorizer’lara dağılmış authorization logic’i ve erişim kontrol kararları için yetersiz audit trail’ler bulunur. Bu sorunlar migration değerlendirmeleri sırasında belirgin hale gelir ve compliance gereksinimlerini önemli ölçüde etkileyebilir.

İşe yarayan varsayılan şu: stage başına tek bir Cognito user pool, API Gateway’in önünde cache’lenen tek bir token authorizer ve her Lambda fonksiyonu için dar kapsamlı tek bir IAM rolü. Her parça mevcut endpoint’lerin arkasında devreye alınabildiği için migration sürerken uygulama ayakta kalır.

Seri Navigasyonu:

Authentication Migration Zorluklarını Anlama#

Çözümler implement etmeden önce mevcut authentication pattern’larını değerlendirmek gerekir. Migration değerlendirmeleri genellikle aynı sorun kümesini ortaya çıkarır. Bunları önce risk, sonra performans sırasına koymak yeniden inşanın düşük değerli işlerde takılmasını önler.

Yaygın Serverless Framework Authentication Pattern’ları#

User Management: Environment’lar boyunca üç farklı Cognito pool, manuel oluşturulmuş, custom attribute’ların sıfır dokümantasyonu.

Authorization: Farklı JWT validation logic’lerine sahip birden fazla Lambda authorizer, caching yok, yüksek authorization latency’si.

Wildcard izinler de sık görülür: çok sayıda Lambda fonksiyonu, gerçekte ihtiyaç duyduğundan çok daha geniş kaynak erişimine sahiptir.

Secret’lar ve audit trail: Environment variable’larda hardcode edilmiş API key’ler, environment’lar arası paylaşılmış ve seyrek rotasyona tabi. Authorization kararları neredeyse hiç log bırakmıyor, bu da erişim pattern’larını sonradan yeniden kurmayı zorlaştırıyor.

Migration Etki Değerlendirmeleri#

  • Compliance riski: Aşırı geniş veri erişimi ve yetersiz erişim kontrolleri nedeniyle potansiyel regülasyon cezaları
  • Performans etkisi: Toplam request süresine eklenen yüksek authorization latency’si
  • Operasyonel yük: Authentication ve erişim sorunlarını çözmeye giden ciddi zaman
  • Güvenlik borcu: Gereksiz izinlere sahip çok sayıda fonksiyonun genişlettiği attack surface

Production-Grade Cognito Implementation#

Aşağıdaki serverless.yml versiyonu user pool’u ve şifre politikasını kapsıyor; MFA yok, device tracking yok, sign-in denemeleri için audit trail yok:

# serverless.yml
resources:
  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

Enterprise Authentication için CDK Implementasyonu#

Aşağıdaki CDK versiyonu, YAML’ın atladığı parçaları ekliyor: production’da MFA, device tracking, advanced security mode ve audit trail yazan Lambda trigger’ları.

// lib/constructs/auth/production-cognito.ts
import {
  UserPool,
  UserPoolClient,
  AccountRecovery,
  Mfa,
  UserPoolOperation,
  StringAttribute,
  ClientAttributes,
  OAuthScope,
  UserPoolDomain,
  CognitoUserPoolsAuthorizer,
  AdvancedSecurityMode
} 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);

    // Audit-uyumlu ayarlarla user pool oluştur
    this.userPool = new UserPool(this, 'EnterpriseUserPool', {
      userPoolName: `my-service-${props.stage}-users-v2`,
      // Gelişmiş güvenlik: production'da self-signup yok
      selfSignUpEnabled: props.stage !== 'prod',
      signInAliases: {
        email: true,
        username: false,  // Email-only sign-in attack surface'i azaltır
      },
      signInCaseSensitive: false,
      autoVerify: { email: true },

      // Enterprise uyumlu şifre politikası
      passwordPolicy: {
        minLength: 14,  // Enterprise güvenlik gereksinimi
        requireLowercase: true,
        requireUppercase: true,
        requireDigits: true,
        requireSymbols: true,
        tempPasswordValidity: Duration.hours(24),  // 3 günden azaltıldı
      },

      // RBAC için kapsamlı user attribute'ları
      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 attribute'ları
        lastLoginDate: new StringAttribute({ mutable: true }),
        createdBy: new StringAttribute({ mutable: false }),
        // Compliance attribute'ları
        dataAccessLevel: new StringAttribute({ mutable: true }),
        complianceFlags: new StringAttribute({ mutable: true }),
      },

      // Enterprise güvenlik ayarları
      accountRecovery: AccountRecovery.EMAIL_ONLY,
      mfa: props.stage === 'prod' ? Mfa.REQUIRED : Mfa.OPTIONAL,
      mfaSecondFactor: {
        sms: false,  // Güvenlik için sadece TOTP
        otp: true,
      },

      // Gelişmiş tehdit koruması
      advancedSecurityMode: props.stage === 'prod'
        ? AdvancedSecurityMode.ENFORCED
        : AdvancedSecurityMode.AUDIT,
      enableSmsRole: false, // Güvenlik için SMS'i devre dışı bırak

      // Markalı iletişimler için email konfigürasyonu
      emailSettings: {
        from: 'noreply@yourcompany.com',
        replyTo: 'support@yourcompany.com',
      },

      // Güvenlik için device tracking
      deviceTracking: {
        challengeRequiredOnNewDevice: true,
        deviceOnlyRememberedOnUserPrompt: false,
      },

      // Data protection
      removalPolicy: props.stage === 'prod' ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY,
      deletionProtection: props.stage === 'prod',
    });

    // Enterprise Lambda trigger'ları ekle
    this.addSecurityTriggers(props.stage);

    // Production app client oluştur
    this.userPoolClient = new UserPoolClient(this, 'EnterpriseClient', {
      userPool: this.userPool,
      userPoolClientName: `my-service-${props.stage}-client-v2`,

      // İzin verilen authentication flow'ları
      authFlows: {
        userPassword: false,  // Daha az güvenli flow'u devre dışı bırak
        userSrp: true,  // Secure Remote Password protocol
        custom: true,  // Custom auth challenge'lar
        adminUserPassword: props.stage !== 'prod',  // Admin flow sadece non-prod'da
      },

      // Enterprise SSO için OAuth konfigürasyonu
      oAuth: {
        flows: {
          authorizationCodeGrant: true,
          implicitCodeGrant: false,  // Güvenlik için implicit flow'u devre dışı bırak
          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,  // SPA için public client

      // Fine-grained attribute erişimi
      readAttributes: new ClientAttributes()
        .withStandardAttributes({
          email: true,
          emailVerified: true,
          givenName: true,
          familyName: true,
        })
        .withCustomAttributes('role', 'department', 'accessLevel'),

      writeAttributes: new ClientAttributes()
        .withCustomAttributes('lastLoginDate'),  // Sınırlı write erişimi

      // Güvenlik odaklı token ayarları
      idTokenValidity: Duration.minutes(30),  // Güvenlik için kısa ömürlü
      accessTokenValidity: Duration.minutes(30), // Güvenlik için kısa ömürlü
      refreshTokenValidity: Duration.days(1),  // Günlük re-authentication

      // Gelişmiş güvenlik seçenekleri
      preventUserExistenceErrors: true,
      enableTokenRevocation: true,

      // Custom token ayarları
      authSessionValidity: Duration.minutes(3),  // Hızlı auth flow timeout
    });

    // API Gateway authorizer oluştur
    this.authorizer = new CognitoUserPoolsAuthorizer(this, 'CognitoAuthorizer', {
      cognitoUserPools: [this.userPool],
      authorizerName: `${props.api.restApiName}-cognito-auth`,
      identitySource: 'method.request.header.Authorization',
      resultsCacheTtl: Duration.minutes(5),  // Performans için cache
    });

    // Markalı deneyim için custom domain ekle
    if (props.domainPrefix) {
      new UserPoolDomain(this, 'UserPoolDomain', {
        userPool: this.userPool,
        cognitoDomainPrefix: `${props.domainPrefix}-${props.stage}`,
      });
    }

    // Production monitoring ve 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 güvenlik kontrolleri
    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,  // Audit için uzun retention
      environment: {
        STAGE: stage,
        AUDIT_TABLE: `auth-audit-${stage}`,
      },
    });

    this.userPool.addTrigger(UserPoolOperation.POST_AUTHENTICATION, postAuthFn);

    // RBAC kurulumu ile user oluşturma
    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',  // Default olarak least privilege
      },
    });

    this.userPool.addTrigger(UserPoolOperation.POST_CONFIRMATION, postConfirmFn);
  }

  private addProductionMonitoring(stage: string) {
    if (stage !== 'prod') return;

    // Başarısız 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,  // 5 dakikada 50 başarısız deneme
      evaluationPeriods: 1,
      treatMissingData: TreatMissingData.NOT_BREACHING,
      alarmDescription: 'High number of authentication failures detected',
    });

    // Compromised credential'lar 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,  // Herhangi bir compromised credential kritik
      evaluationPeriods: 1,
      alarmDescription: 'Compromised credentials detected',
    });
  }
}

Custom Auth Flow’ları için Lambda Trigger’ları#

// src/auth/triggers/pre-signup.ts
import { PreSignUpTriggerEvent, PreSignUpTriggerHandler } from 'aws-lambda';

export const handler: PreSignUpTriggerHandler = async (event) => {
  console.log('Pre-signup event:', JSON.stringify(event, null, 2));

  // Kurumsal hesaplar için email domain'i doğrula
  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');
  }

  // Kurumsal email'leri otomatik onayla
  if (domain === 'company.com') {
    event.response.autoConfirmUser = true;
    event.response.autoVerifyEmail = true;
  }

  return event;
};

// src/auth/triggers/post-confirmation.ts
import { 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));

  // DynamoDB'de user kaydı oluştur
  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 Performans Optimizasyonu#

Legacy authorization kurulumları her request’te aynı işi baştan yapar:

  1. JWT decode: Token header’ının ve payload’ının parse edilmesi
  2. Cognito JWK fetch: Cache yoksa user pool’un JWKS endpoint’ine network çağrısı
  3. Signature verification: Eşleşen public key ile RS256 doğrulaması
  4. Database role lookup: Roller token dışında tutuluyorsa ek bir sorgu
  5. Sonuç cache’lenmiyor: Aynı token’la gelen bir sonraki request’te tüm dizi yeniden çalışır

Request path’inden çıkarılmaya değer iki adım JWKS fetch ve role lookup’tır. İkisi de cache’lenebilir. Signature kontrolü ise her seferinde çalışmak zorundadır.

Yüksek Performanslı JWT Authorization#

Aşağıdaki authorizer JWKS’i module scope’ta tutar, fetch başarısız olduğunda eski kopyaya düşer ve döndürdüğü policy’yi API Gateway’in cache’lemesine izin verir. Doğrulanmış imza yalnızca kimliği çözer. Policy’nin hangi route’ları kapsadığına role claim’i karar verir ve API’nin eşlemediği bir rol Deny alır:

// lib/constructs/auth/high-performance-jwt-authorizer.ts
import {
  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;
    // Role claim -> o rolün invoke edebileceği METHOD/path pattern'ları,
    // örnek: { viewer: ['GET/orders/*'], admin: ['*/*'] }
    roleRoutes: Record<string, string[]>;
  }) {
    // Production için optimize edilmiş authorizer fonksiyonu
    const authorizerFunction = new NodejsFunction(scope, 'OptimizedAuthorizerFunction', {
      entry: 'src/auth/production-jwt-authorizer.ts',
      handler: 'handler',
      // Tutarlı performans için reserved concurrency (maliyet nedeniyle provisioned değil)
      reservedConcurrentExecutions: props.stage === 'prod' ? 10 : undefined,
      timeout: Duration.seconds(5),  // Hızlı başarısızlıklar için kısa timeout
      memorySize: 512,  // JWT processing için optimize edilmiş
      logRetention: RetentionDays.ONE_MONTH,
      environment: {
        USER_POOL_ID: props.userPoolId,
        REGION: props.region,
        STAGE: props.stage,
        // Authorization girdisi: bu map'te olmayan rol hiçbir yetki almaz
        ROLE_ROUTES: JSON.stringify(props.roleRoutes),
        // Performans optimizasyon flag'leri
        ENABLE_METRICS: props.stage === 'prod' ? 'true' : 'false',
        CACHE_TIMEOUT_MS: '300000',  // 5 dakika
      },
      bundling: {
        // Daha hızlı cold start'lar için bundle boyutunu minimize et
        minify: true,
        target: 'node22', // Daha iyi performans için güncel LTS
        // Sadece gerekli dependency'leri dahil et
        nodeModules: ['jsonwebtoken', 'jwk-to-pem'],
        externalModules: ['@aws-sdk/*'],
      },
    });

    super(scope, id, {
      restApi: props.api,
      handler: authorizerFunction,
      identitySource: IdentitySource.header('Authorization'),
      // Performans için API Gateway caching (Lambda invocation'larını azaltır)
      resultsCacheTtl: Duration.minutes(5),  // Güvenlik ile performans arasında denge
      authorizerName: `${props.api.restApiName}-jwt-authorizer-v2`,
      // Sıkı token validation
      validationRegex: '^Bearer [A-Za-z0-9\\-_=]+\\.[A-Za-z0-9\\-_=]+\\.[A-Za-z0-9\\-_.+/=]*$',
    });
  }
}

// src/auth/production-jwt-authorizer.ts
import { APIGatewayTokenAuthorizerEvent, APIGatewayAuthorizerResult } from 'aws-lambda';
import jwt from 'jsonwebtoken';
import jwkToPem from 'jwk-to-pem';

// ÖNERİLEN: Yeni implementasyonlarda AWS'in 'aws-jwt-verify' kütüphanesini kullanın.
// Hazır optimizasyonlar, daha iyi hata yönetimi ve resmi AWS desteği sunar.
// aws-jwt-verify ile örnek:
//
// 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);
//
// Aşağıdaki kod, mevcut kurulumlarla uyum için jsonwebtoken kullanıyor.

// Performans için multi-level caching
let cachedKeys: Map<string, string> | null = null;
let cacheTimestamp: number = 0;
const CACHE_TIMEOUT = parseInt(process.env.CACHE_TIMEOUT_MS || '300000');

// Role -> izinli METHOD/path pattern'ları, CDK construct'ı tarafından enjekte edilir
const ROLE_ROUTES: Record<string, string[]> = JSON.parse(process.env.ROLE_ROUTES || '{}');

// Performans metrikleri (production'da toplanır)
const metrics = {
  authCount: 0,
  keyFetchCount: 0,
  cacheHits: 0,
  averageLatency: 0,
};

async function getPublicKeys(): Promise<Map<string, string>> {
  const now = Date.now();

  // Hala geçerliyse cached key'leri döndür
  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`;

    // Timeout ve retry logic ile fetch kullan
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 3000);

    const response = await fetch(jwksUrl, {
      signal: controller.signal,
      headers: {
        'Cache-Control': 'max-age=300',  // 5 dakikalık cache iste
      },
    });

    clearTimeout(timeoutId);

    if (!response.ok) {
      throw new Error(`JWK fetch failed: ${response.status}`);
    }

    const jwks = await response.json();

    // JWK'ları dönüştür ve cache'le
    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);

    // Fallback olarak stale cache varsa döndür
    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++;

  // Audit trail için gelişmiş request logging
  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 {
    // Erken token validation
    if (!event.authorizationToken || !event.authorizationToken.startsWith('Bearer ')) {
      throw new Error('Missing or invalid authorization header format');
    }

    const token = event.authorizationToken.replace('Bearer ', '');

    // Temel token format validation
    const tokenParts = token.split('.');
    if (tokenParts.length !== 3) {
      throw new Error('Invalid JWT format');
    }

    // Token'ı decode et (henüz signature verify etmiyor)
    const decodedToken = jwt.decode(token, { complete: true });
    if (!decodedToken || typeof decodedToken === 'string') {
      throw new Error('Invalid token structure');
    }

    // Token expiration'ını erken validate et
    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');
    }

    // Signing key'leri al (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}`);
    }

    // JWT signature ve claim'leri verify et
    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,  // 30 saniye clock skew'a izin ver
    }) as any;

    // User bilgilerini çıkar
    const userId = verifiedPayload.sub;
    const email = verifiedPayload.email;
    // cognito:groups hem ID hem access token'da bulunur, custom attribute'lar yalnızca ID token'da
    const groups: string[] = verifiedPayload['cognito:groups'] || [];
    const role = verifiedPayload['custom:role'] || groups[0];
    const accessLevel = verifiedPayload['custom:accessLevel'] || 'basic';

    const authContext = {
      userId,
      email,
      role: role || '',
      accessLevel,
      tokenUse: verifiedPayload.token_use,
      authTime: verifiedPayload.auth_time?.toString(),
      requestId,
    };

    // İmza doğrulaması yalnızca kimliği kanıtlar. ROLE_ROUTES'ta olmayan bir rol
    // explicit Deny alır; API Gateway bunu 403 olarak döndürür.
    const allowedRoutes = role ? ROLE_ROUTES[role] : undefined;
    if (!allowedRoutes || allowedRoutes.length === 0) {
      console.warn('Authorization denied:', { requestId, userId, role: role || 'none' });
      return generateEnhancedPolicy(userId, 'Deny', event.methodArn, [], authContext);
    }

    const policy = generateEnhancedPolicy(
      userId,
      'Allow',
      event.methodArn,
      allowedRoutes,
      authContext
    );

    const totalTime = Date.now() - startTime;
    metrics.averageLatency = (metrics.averageLatency + totalTime) / 2;

    // Başarılı authorization'ı logla
    console.log('Authorization successful:', {
      requestId,
      userId,
      email,
      role,
      accessLevel,
      latency: totalTime,
    });

    // Metrikleri periyodik olarak raporla
    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,
    });

    // Non-production'da debugging için
    if (process.env.STAGE !== 'prod') {
      console.debug('Token details:', {
        token: event.authorizationToken,
        methodArn: event.methodArn,
      });
    }

    throw new Error('Unauthorized');  // Client'a her zaman generic error döndür
  }
};

function generateEnhancedPolicy(
  principalId: string,
  effect: 'Allow' | 'Deny',
  methodArn: string,
  allowedRoutes: string[],
  context: Record<string, any>
): APIGatewayAuthorizerResult {
  // arn:aws:execute-api:region:account:apiId/stage/METHOD/resource/path
  const [apiArn, stage] = methodArn.split('/');

  // Cache'lenen sonuç bu token'ın dokunduğu her path için yeniden kullanılır;
  // bu yüzden statement istenen route'u değil, rolün kendi route'larını listeler.
  const resources = effect === 'Allow'
    ? allowedRoutes.map(route => `${apiArn}/${stage}/${route}`)
    : [`${apiArn}/${stage}/*`];

  return {
    principalId,
    policyDocument: {
      Version: '2012-10-17',
      Statement: [
        {
          Action: 'execute-api:Invoke',
          Effect: effect,
          Resource: resources,
        },
      ],
    },
    context: {
      // Tüm context değerlerini string'e dönüştür (API Gateway requirement)
      ...Object.entries(context).reduce((acc, [key, value]) => ({
        ...acc,
        [key]: String(value || ''),
      }), {}),
    },
  };
}

Group’larla Request-Based Authorizer#

// lib/constructs/auth/group-authorizer.ts
export 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`,
    });
  }
}

Wildcard IAM İzinleri#

Güvenlik değerlendirmeleri sıklıkla aşırı geniş IAM policy’lere sahip fonksiyonları ortaya çıkarır. Tipik sorunlu bir konfigürasyon:

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

Etki: "Action": "*" taşıyan bir fonksiyon hesaptaki S3 bucket’larını silebilir, EC2 instance’larını terminate edebilir veya herhangi bir DynamoDB tablosunu okuyabilir. Ele geçirilen tek bir fonksiyon, hesabın tamamının ele geçirilmesine dönüşür.

Wildcard policy’ler ayrıca bir güvenlik incelemesinin genellikle ilk işaretlediği şeydir; tek başına compliance onayını tıkayabilir.

Least Privilege IAM Mimarisi#

Aşağıdaki role-based sistem, her fonksiyona yalnızca ihtiyaç duyduğu izinleri verir:

// lib/constructs/security/lambda-role.ts
import { 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}`,
    });

    // Temel Lambda izinleri
    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: ['*'],
    }));

    // Custom statement'ları ekle
    props.additionalStatements?.forEach(statement => {
      this.addToPolicy(statement);
    });
  }
}

Resource-Based Policy’ler#

// lib/constructs/security/resource-policies.ts
export 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#

IAM ile Service-to-Service Auth#

// lib/constructs/auth/service-auth.ts
export 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,
      },
    });

    // Diğer servisleri invoke etme izni ver
    this.addToRolePolicy(new PolicyStatement({
      effect: Effect.ALLOW,
      actions: ['execute-api:Invoke'],
      resources: [
        `arn:aws:execute-api:${Stack.of(this).region}:*:*/*/*/*`,
      ],
    }));
  }
}

// src/libs/service-client.ts
import { 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#

Güvenli API Key Dağıtımı#

// lib/constructs/auth/api-key-manager.ts
export class ApiKeyManager extends Construct {
  private keys: Map<string, IApiKey> = new Map();

  constructor(scope: Construct, id: string, props: {
    api: IRestApi;
    stage: string;
  }) {
    super(scope, id);

    // Rate limiting için usage plan
    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,
    });

    // Secrets Manager'da sakla
    const secret = new Secret(this, `ApiKeySecret-${name}`, {
      secretName: `/api-keys/${name}`,
      generateSecretString: {
        secretStringTemplate: JSON.stringify({ customerId }),
        generateStringKey: 'apiKey',
        includeSpace: false,
      },
    });

    // Key value'yu secret ile ilişkilendir
    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#

  • Cognito user attribute’larını mevcut schema’ya map et
  • User migration Lambda trigger implement et
  • Password policy uyumluluğunu test et
  • MFA ayarlarının gereksinimleri karşıladığını doğrula

Authorization Migration#

  • Custom authorizer’ları CDK’ya dönüştür
  • Düzgün caching stratejileri implement et
  • Mevcut rolleri yeni yapıya map et

IAM Migration#

  • Mevcut Lambda rollerini audit et
  • Least privilege ilkelerini implement et
  • Wildcard izinleri kaldır
  • Gerektiği yerde resource-based policy’ler ekle
  • Cross-account erişim gerekliyse test et

Güvenlik Yanıt Başlıkları#

// lib/constructs/security/security-headers.ts
export 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'",
  };

  // Tüm method'lara ekle
  api.methods.forEach(method => {
    method.addMethodResponse({
      statusCode: '200',
      responseParameters: Object.keys(responseParameters).reduce(
        (acc, key) => ({ ...acc, [key]: true }),
        {}
      ),
    });
  });
}

CDK Versiyon Uyumluluğu#

Bu implementasyon AWS CDK v2.100+ hedefler. Cognito property’leri CDK sürümleri arasında yer değiştirir ve gelişmiş tehdit koruması yapılandırması birden fazla kez biçim değiştirdi. User pool ayarlarını kopyalamadan önce sabitlediğiniz sürümün CDK API referansını kontrol edin.

Anti-Pattern’lar ve Yerine Konacaklar#

1. Hız uğruna wildcard izinler#

Anti-pattern: Policy’yi daraltmak ship etmekten uzun sürdüğü için "Action": "*". Yerine: Fonksiyon başına explicit action’lar ve resource ARN’leri; böylece ele geçirilen bir fonksiyon yalnızca rolünün adını verdiği kaynaklara ulaşabilir.

2. Planı olmayan caching#

Her request’te cache’siz JWT doğrulaması genellikle yavaş endpoint’ler için “güvenlik overhead’i” suçlanır; tam tersi hata da sık görülür: TTL’i ve fallback’i olmayan bir JWKS cache’i. Çözüm, JWKS’i module scope’ta TTL ile cache’lemek, authorizer sonucunu API Gateway’e cache’letmek ve fetch başarısız olduğunda stale kopyaya düşmektir. Bu maliyetin büyük kısmı JWKS’i çekmek için yapılan network çağrısındadır, kriptografik kontrolün çok üzerinde; asıl gecikmeyi kazandıran bu çağrıyı cache’lemektir. Key rotation’ın er geç devreye girmesi gerekir ve bir JWKS kesintisi authorization’ı beraberinde düşürmemelidir.

3. Audit trail’in olmaması#

Arkasında kayıt bırakmayan authorization kararları hâlâ yaygın. Çözüm API Gateway stage’inde access logging: log formatında request ID ve resource path’in yanına $context.authorizer.userId ve $context.authorizer.role alanlarını eklemek. Post-authentication trigger’ı yalnızca sign-in’i görür, cache’lenen authorizer sonucu da sonraki çağrılarda Lambda’yı atlar; bu yüzden her request’in düştüğü tek yer access log’dur.

4. Fonksiyon başına ad-hoc izinler#

Anti-pattern: Her fonksiyonun kendi elle yazılmış policy’si. Yerine: Fonksiyona özel eklemeler alabilen ortak bir least-privilege rol construct’ı. Neden: Tek bir paylaşılan construct’ı gözden geçirmek, ad-hoc yaklaşımın ürettiği onlarca özel policy’yi gözden geçirmekten daha kolaydır; üstelik bu policy’lerin her biri her audit’te baştan okunur.

Sonraki Adımlar#

Bu varsayılan, kullanıcılar stage başına tek bir Cognito pool’da toplandığında ve her authorization kararı token claim’lerinden verilebildiğinde geçerlidir: cache’lenen bir token authorizer ile fonksiyon başına roller, hem request path’ini hem de blast radius’u küçük tutar. Karar, token’ın taşıyamayacağı bir veriye (örneğin kayıt bazlı sahiplik) bağlıysa varsayılanı bırakın. O durumda kontrol handler’a taşınır ve authorizer’a yalnızca kimliği kanıtlama işi kalır.

6. Bölüm migration stratejilerini ve zaman çizelgelerini, test yaklaşımlarını, rollback prosedürlerini, stack genelinde performans çalışmasını ve regresyonları erken yakalayan monitoring’i ele alıyor.

Kaynaklar#

Serverless Framework'ten AWS CDK'ya Geçiş Rehberi

Serverless Framework'ten AWS CDK'ya tam geçiş sürecini kapsayan 6 bölümlük kapsamlı rehber. Kurulum, uygulama pattern'leri ve best practice'ler dahil.

İlerleme 5/6 yazı tamamlandı

İlgili yazılar