İçeriğe atla

Amazon Cognito Derinlemesine: Temel Authentication'ın Ötesinde

Gelişmiş Amazon Cognito teknik rehberi: özel auth akışları, federation, multi-tenancy, migration stratejileri ve CDK ile production-grade güvenlik.

Ayhan Sipahi Ayhan Sipahi

Amazon Cognito, sign-up ve sign-in tarafını çok az konfigürasyonla karşılıyor. Production sistemleri ise daha fazlasını istiyor: token’ın içinde taşınan tenant context, kurumsal SSO, mevcut bir sağlayıcıdan çıkış yolu ve AWS’nin region’lar arası kopyalamadığı bir dizin için yedekleme planı.

AWS-native bir üründe ve standart authentication gereksinimlerinde varsayılan tercih şu: tenant izolasyonu için custom attribute’lı tek bir shared user pool, Cognito’nun yerel olarak modellemediği davranış için Lambda trigger’ları ve kısa cache TTL’li bir API Gateway Cognito authorizer’ı. Tenant başına siloed pool, SAML federation ve ayrı bir identity servisi ise karmaşıklığını ancak belirli koşullarda hak ediyor: yüzlerle ifade edilen tenant sayısı, kendi identity provider’ıyla gelen bir B2B müşterisi veya yönetilen bir dizinin rahat taşıyamayacağı ölçek.

Mimari#

User Pools vs Identity Pools#

User Pools ve Identity Pools arasındaki fark başlangıçta birçok developer’ı karıştırıyor. Temelde farklı amaçlara hizmet ediyorlar:

User Pools authentication’ı yönetiyor: kullanıcıların kim olduğunu doğruluyor. User directory’leri, credential’ları, MFA’yı, password policy’lerini ve OAuth flow’larını yönetiyorlar. Kullanıcılar sign-in olduğunda JWT token’ları (ID token, access token, refresh token) alıyorlar.

Identity Pools authorization’ı yönetiyor: client uygulamalarından S3, DynamoDB veya SQS gibi servislere doğrudan erişim için geçici AWS credential’ları sağlıyor. Authentication token’larını (User Pools veya external provider’lardan) AWS credential’larına exchange ediyorlar.

AWS Services

Application Layer

Authentication Layer

JWT Tokens

JWT Token

Temporary AWS Credentials

JWT Token

AWS Credentials

AWS Credentials

AWS Credentials

Kullanici

Cognito User Pool Authentication

Cognito Identity Pool Authorization

Web Application

API Gateway Cognito Authorizer

Lambda Functions

S3 Bucket

DynamoDB

SQS Queue

Sadece User Pool, API Gateway veya backend servislerini çağıran bir frontend için yeterli. Sadece Identity Pool, AWS kaynaklarına guest erişim (analytics, public data gibi) gerektiren durumlara uyuyor. İkisini birlikte kullanmak ise frontend’den doğrudan S3 veya DynamoDB’ye erişen authenticated kullanıcılar için gerekiyor.

CDK ile Production Setup#

User Pool ve Identity Pool’u uygun güvenlik konfigürasyonuyla gösteren complete bir setup:

import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as iam from 'aws-cdk-lib/aws-iam';

// Authentication için User Pool
const userPool = new cognito.UserPool(this, 'UserPool', {
  selfSignUpEnabled: false, // Production: User creation'ı kontrol et
  signInAliases: { email: true, username: true },
  autoVerify: { email: true },
  passwordPolicy: {
    minLength: 12,
    requireLowercase: true,
    requireUppercase: true,
    requireDigits: true,
    requireSymbols: true,
  },
  accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
  advancedSecurityMode: cognito.AdvancedSecurityMode.ENFORCED,
  mfa: cognito.Mfa.OPTIONAL,
  mfaSecondFactor: {
    sms: true,
    otp: true, // Time-based one-time password (TOTP)
  },
});

// Web application için app client
const appClient = userPool.addClient('WebAppClient', {
  authFlows: {
    userPassword: false, // Daha az güvenli flow'u devre dışı bırak
    userSrp: true, // Secure Remote Password
    custom: true, // Custom auth flow'ları aktifleştir
  },
  oAuth: {
    flows: {
      authorizationCodeGrant: true,
      implicitCodeGrant: false, // Production'da implicit flow'dan kaçın
    },
    scopes: [
      cognito.OAuthScope.OPENID,
      cognito.OAuthScope.EMAIL,
      cognito.OAuthScope.PROFILE,
      cognito.OAuthScope.custom('billing-api/read'),
    ],
    callbackUrls: ['https://app.example.com/callback'],
    logoutUrls: ['https://app.example.com/logout'],
  },
  generateSecret: true, // Server-side app'ler için gerekli
});

// AWS kaynaklarına erişim için Identity Pool
const identityPool = new cognito.CfnIdentityPool(this, 'IdentityPool', {
  allowUnauthenticatedIdentities: false,
  cognitoIdentityProviders: [{
    clientId: appClient.userPoolClientId,
    providerName: userPool.userPoolProviderName,
  }],
});

// Scoped permission'larla authenticated role
const authenticatedRole = new iam.Role(this, 'CognitoAuthenticatedRole', {
  assumedBy: new iam.FederatedPrincipal(
    'cognito-identity.amazonaws.com',
    {
      StringEquals: {
        'cognito-identity.amazonaws.com:aud': identityPool.ref,
      },
      'ForAnyValue:StringLike': {
        'cognito-identity.amazonaws.com:amr': 'authenticated',
      },
    },
    'sts:AssumeRoleWithWebIdentity'
  ),
});

// User-scoped path'lerle spesifik S3 erişimi ver
authenticatedRole.addToPolicy(new iam.PolicyStatement({
  effect: iam.Effect.ALLOW,
  actions: ['s3:GetObject', 's3:PutObject'],
  resources: ['arn:aws:s3:::my-bucket/${cognito-identity.amazonaws.com:sub}/*'],
}));

Bu ayarların birkaçı önemli. selfSignUpEnabled: false yetkisiz user creation’ı önlerken, advancedSecurityMode: ENFORCED compromised credential detection’ı aktifleştiriyor. mfa: OPTIONAL esneklik sağlıyor (REQUIRED’dan kaçın; geri çevirmek desteklenmiyor), generateSecret: true ise secret’ı güvenli şekilde saklayabilen backend client’lar için geçerli.

Custom Authentication Flow’ları#

CAPTCHA doğrulama, bir güvenlik sorusu veya tamamen passwordless bir akış, hepsi aynı üç Lambda trigger üzerinden geçiyor; bu üçü challenge sequence’ını birlikte orkestra ediyor.

Custom Auth Nasıl Çalışır#

Evet

Hayir

Evet

Hayir

Kullanici Auth Baslatir

Define Auth Challenge Lambda

Create Auth Challenge Lambda

Kullanici Yanit Verir

Verify Auth Challenge Response Lambda

Token Ver?

Authentication Basarili

Daha Challenge?

Authentication Basarisiz

Multi-Factor Challenge Implementasyonu#

Bu örnek complete bir flow implement ediyor: password → CAPTCHA → security question.

// Define Auth Challenge - Challenge sequence'ını orkestra eder
export const defineAuthChallenge = async (event: DefineAuthChallengeTrigger) => {
  const session = event.request.session;

  // İlk challenge: SRP password verification (Cognito tarafından handle edilir)
  if (session.length === 0) {
    event.response.issueTokens = false;
    event.response.failAuthentication = false;
    event.response.challengeName = 'SRP_A';
  }
  // İkinci challenge: SRP password verifier
  else if (session.length === 1 && session[0].challengeName === 'SRP_A') {
    event.response.issueTokens = false;
    event.response.failAuthentication = false;
    event.response.challengeName = 'PASSWORD_VERIFIER';
  }
  // Üçüncü challenge: CAPTCHA
  else if (session.length === 2 && session[1].challengeName === 'PASSWORD_VERIFIER'
           && session[1].challengeResult === true) {
    event.response.issueTokens = false;
    event.response.failAuthentication = false;
    event.response.challengeName = 'CUSTOM_CHALLENGE';
    event.response.challengeMetadata = 'CAPTCHA_CHALLENGE';
  }
  // Dördüncü challenge: Security question
  else if (session.length === 3 && session[2].challengeName === 'CUSTOM_CHALLENGE'
           && session[2].challengeResult === true) {
    event.response.issueTokens = false;
    event.response.failAuthentication = false;
    event.response.challengeName = 'CUSTOM_CHALLENGE';
    event.response.challengeMetadata = 'SECURITY_QUESTION';
  }
  // Tüm challenge'lar başarılı
  else if (session.length === 4 && session[3].challengeName === 'CUSTOM_CHALLENGE'
           && session[3].challengeResult === true) {
    event.response.issueTokens = true;
    event.response.failAuthentication = false;
  }
  // Challenge başarısız
  else {
    event.response.issueTokens = false;
    event.response.failAuthentication = true;
  }

  return event;
};

// Create Auth Challenge - Challenge data'sını oluşturur
export const createAuthChallenge = async (event: CreateAuthChallengeTrigger) => {
  const metadata = event.request.challengeMetadata;

  if (metadata === 'CAPTCHA_CHALLENGE') {
    // External servis veya internal logic kullanarak CAPTCHA oluştur
    const captchaToken = await generateCaptcha();

    event.response.publicChallengeParameters = {
      captchaUrl: `https://captcha.example.com/${captchaToken}`,
      challengeType: 'CAPTCHA',
    };

    event.response.privateChallengeParameters = {
      captchaAnswer: await getCaptchaAnswer(captchaToken),
    };
  }
  else if (metadata === 'SECURITY_QUESTION') {
    // DynamoDB'den kullanıcının güvenlik sorusunu al
    const question = await getSecurityQuestion(event.userName);

    event.response.publicChallengeParameters = {
      question: question.text,
      challengeType: 'SECURITY_QUESTION',
    };

    event.response.privateChallengeParameters = {
      answer: question.answer,
    };
  }

  return event;
};

// Verify Auth Challenge Response
export const verifyAuthChallenge = async (event: VerifyAuthChallengeTrigger) => {
  const privateParams = event.request.privateChallengeParameters;
  const challengeAnswer = event.request.challengeAnswer;

  if (privateParams.captchaAnswer) {
    event.response.answerCorrect =
      challengeAnswer.toLowerCase() === privateParams.captchaAnswer.toLowerCase();
  }
  else if (privateParams.answer) {
    event.response.answerCorrect =
      challengeAnswer.toLowerCase() === privateParams.answer.toLowerCase();
  }

  return event;
};

Challenge sequence session array’ine göre deterministic olmalı ve challengeMetadata custom challenge’ları ayırt etmek için kullanılmalı. privateChallengeParameters asla client’a gönderilmiyor; sadece server tarafında verification için var. Her trigger’ın da 5 saniyelik bir timeout’u olduğu için logic’in hızlı kalması gerekiyor.

Multi-Tenancy için Token Customization#

Pre Token Generation Lambda, JWT token’larına custom claim eklemeyi sağlıyor. Tenant context’in her request ile birlikte taşınması gereken multi-tenant SaaS uygulamalarında bu kritik bir yetenek.

Pre Token Generation V2#

// Pre Token Generation V2 - Hem ID hem de Access token'ları özelleştir
export const preTokenGeneration = async (event: PreTokenGenerationTriggerEvent) => {
  // DynamoDB'den tenant ve role bilgisini al
  const userMetadata = await getUserMetadata(event.userName);

  // V2 event'lerinde claim ve scope override'ları claimsAndScopeOverrideDetails altında
  event.response.claimsAndScopeOverrideDetails = {
    idTokenGeneration: { claimsToAddOrOverride: {} },
  };
  const idTokenClaims =
    event.response.claimsAndScopeOverrideDetails.idTokenGeneration.claimsToAddOrOverride;

  if (event.request.userAttributes['custom:tenantId']) {
    const tenantId = event.request.userAttributes['custom:tenantId'];

    // Tenant'ın active olduğunu doğrula
    const tenant = await getTenantById(tenantId);
    if (!tenant || tenant.status !== 'ACTIVE') {
      throw new Error('Tenant is not active');
    }

    // ID token'a custom claim'ler ekle (user info için)
    Object.assign(idTokenClaims, {
      'custom:tenantId': tenantId,
      'custom:tenantName': tenant.name,
      'custom:organizationId': tenant.organizationId,
      'custom:role': userMetadata.role,
      'custom:permissions': JSON.stringify(userMetadata.permissions),
    });

    // Access Token'ı özelleştir (sadece Cognito Essentials/Plus tier)
    if (event.triggerSource === 'TokenGeneration_Authentication') {
      event.response.claimsAndScopeOverrideDetails.accessTokenGeneration = {
        claimsToAddOrOverride: {
          'tenant_id': tenantId,
          'role': userMetadata.role,
        },
        claimsToSuppress: [],
        scopesToAdd: [`tenant:${tenantId}:read`, `tenant:${tenantId}:write`],
      };
    }
  }

  // Feature flag'ler için subscription tier ekle
  if (userMetadata.subscriptionTier) {
    idTokenClaims['custom:tier'] = userMetadata.subscriptionTier;
  }

  return event;
};

// DynamoDB helper fonksiyonları
async function getUserMetadata(username: string) {
  const result = await dynamoDB.get({
    TableName: 'UserMetadata',
    Key: { username },
  }).promise();

  return result.Item || { role: 'user', permissions: [] };
}

async function getTenantById(tenantId: string) {
  const result = await dynamoDB.get({
    TableName: 'Tenants',
    Key: { tenantId },
  }).promise();

  return result.Item;
}

Token’lara asla password veya API key gibi sensitive data eklenmemeli, boyut da HTTP header limitleri yüzünden 8KB’ın altında kalmalı. Büyük permission set’leri doğrudan gömmek yerine opaque reference’larla temsil etmek, tenant context’i de token forgery’yi önlemek için doğrulamak gerekiyor.

Warning

Token Size Pitfall: Çok fazla custom claim eklemek, token’ları 8KB üzerine çıkarıp HTTP 431 hatalarına neden olabilir. Production’da token boyutunu izle ve büyük veri yapıları yerleştirmek yerine reference ID’leri kullan.

Multi-Tenancy Pattern’leri#

Bunu büyük ölçüde tenant sayısı belirliyor.

< 100 Kucuk Olcek

100-1000 Orta Olcek

> 1000 Enterprise

Custom Config Per Tenant

Compliance Isolation

Standard Config

Multi-Tenancy Pattern Sec

Tenant Sayisi

Shared User Pool Custom Attributes

Shared Pool Groups-Based

Enterprise Gereksinimler?

Siloed User Pools Tenant Basina Bir

Multi-Region Shared Pools

Basit setup Duesuk operasyonel maliyet Tek config Sinirli izolasyon

Daha iyi izolasyon Group-based policy'ler 1000'lere olceklenebilir 10,000 groups limiti

Tam izolasyon Tenant basina custom config Compliance-ready Yuksek operasyonel maliyet Karmasik otomasyon

Cografi dagilim Quota izolasyonu Region'a gore compliance Karmasik orkestrasyon

Custom Attribute’larla Shared Pool#

Bu pattern, 100’den az tenant’lı çoğu SaaS uygulaması için iyi çalışıyor:

// Tenant izolasyonuyla Shared User Pool
const userPool = new cognito.UserPool(this, 'MultiTenantUserPool', {
  selfSignUpEnabled: false,
  standardAttributes: {
    email: { required: true, mutable: true },
  },
  customAttributes: {
    tenantId: new cognito.StringAttribute({
      minLen: 1,
      maxLen: 128,
      mutable: false, // Oluşturulduktan sonra tenant değiştirilemez
    }),
    organizationId: new cognito.StringAttribute({
      minLen: 1,
      maxLen: 128,
      mutable: false,
    }),
    role: new cognito.StringAttribute({
      minLen: 1,
      maxLen: 64,
      mutable: true, // Role güncellenebilir
    }),
  },
});

// Pre Sign Up - Invitation token'dan tenant ata
export const preSignUp = async (event: PreSignUpTriggerEvent) => {
  const invitationToken = event.request.validationData?.invitationToken;

  if (!invitationToken) {
    throw new Error('Invitation token required');
  }

  // Invitation'ı doğrula ve tenant bilgisini al
  const invitation = await validateInvitation(invitationToken);

  if (!invitation || invitation.expired) {
    throw new Error('Invalid or expired invitation');
  }

  // Auto-confirm ve tenant attribute'larını set et
  event.response.autoConfirmUser = true;
  event.response.autoVerifyEmail = true;

  // Bunlar custom attribute olarak set edilecek
  event.request.userAttributes['custom:tenantId'] = invitation.tenantId;
  event.request.userAttributes['custom:organizationId'] = invitation.organizationId;
  event.request.userAttributes['custom:role'] = invitation.role;

  // Invitation'ı kullanıldı olarak işaretle
  await markInvitationUsed(invitationToken, event.userName);

  return event;
};

Enterprise Identity Provider’ları ile SAML Federation#

Federation, kullanıcıların Azure AD, Okta veya OneLogin gibi kurumsal identity provider’lar üzerinden authenticate olmasını sağlıyor.

Azure AD SAML Konfigürasyonu#

// SAML provider için CDK setup
const samlProvider = new cognito.UserPoolIdentityProviderSaml(this, 'AzureADProvider', {
  userPool,
  name: 'AzureAD',
  metadata: cognito.UserPoolIdentityProviderSamlMetadata.url(
    'https://login.microsoftonline.com/TENANT_ID/federationmetadata/2007-06/federationmetadata.xml'
  ),
  attributeMapping: {
    email: cognito.ProviderAttribute.other('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'),
    givenName: cognito.ProviderAttribute.other('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'),
    familyName: cognito.ProviderAttribute.other('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname'),
    custom: {
      'tenantId': cognito.ProviderAttribute.other('http://schemas.microsoft.com/identity/claims/tenantid'),
    },
  },
  idpSignout: true,
});

// Federated user'ı mevcut profile link et (duplicate'leri önle)
export const postAuthentication = async (event: PostAuthenticationTriggerEvent) => {
  // Bu federated bir identity mi kontrol et
  if (event.request.userAttributes.identities) {
    const identities = JSON.parse(event.request.userAttributes.identities);
    const federatedIdentity = identities[0];

    if (federatedIdentity.providerName === 'AzureAD') {
      const email = event.request.userAttributes.email;

      // Bu email ile zaten bir kullanıcı var mı kontrol et
      const existingUser = await findUserByEmail(email);

      if (existingUser && existingUser.username !== event.userName) {
        // Federated identity'yi mevcut kullanıcıya link et
        await idp.adminLinkProviderForUser({
          UserPoolId: event.userPoolId,
          DestinationUser: {
            ProviderName: 'Cognito',
            ProviderAttributeValue: existingUser.username,
          },
          SourceUser: {
            ProviderName: federatedIdentity.providerName,
            ProviderAttributeName: 'Cognito_Subject',
            ProviderAttributeValue: federatedIdentity.userId,
          },
        }).promise();

        // Audit için linking'i logla
        await auditLog({
          action: 'FEDERATED_IDENTITY_LINKED',
          email,
          provider: federatedIdentity.providerName,
        });
      }
    }
  }

  return event;
};

Bunu güvenilir tutan birkaç pratik var.

  • Otomatik certificate rotation için metadata URL kullan
  • NameId’yi immutable attribute’a (user_id) map et, email’e değil
  • Duplicate kullanıcıları önlemek için account linking implement et
  • Hem SP-initiated hem de IdP-initiated logout flow’larını test et

Tip

Federation Testing: Logout flow’larını detaylıca test et. Federated logout, Cognito, IdP ve uygulama arasında koordinasyon gerektiriyor. Kullanıcıların app’te logout görünmesi ama IdP seviyesinde hala authenticated olması yaygın bir sorun.

API Gateway Entegrasyonu#

Uçtan Uca Entegrasyon Kurulumu#

// CDK: Cognito authorizer ile API Gateway
const api = new apigateway.RestApi(this, 'MyApi', {
  restApiName: 'Secure API',
  deployOptions: {
    stageName: 'prod',
    tracingEnabled: true,
  },
});

const authorizer = new apigateway.CognitoUserPoolsAuthorizer(this, 'CognitoAuthorizer', {
  cognitoUserPools: [userPool],
  authorizerName: 'CognitoAuthorizer',
  identitySource: 'method.request.header.Authorization',
  resultsCacheTtl: Duration.minutes(5), // Authorization kararlarını cache'le
});

// Spesifik OAuth scope gerektiren protected endpoint
const protectedResource = api.root.addResource('billing');
protectedResource.addMethod('GET', new apigateway.LambdaIntegration(billingFunction), {
  authorizer,
  authorizationType: apigateway.AuthorizationType.COGNITO,
  authorizationScopes: ['billing-api/read'], // OAuth scope validation
  requestValidator: new apigateway.RequestValidator(this, 'RequestValidator', {
    restApi: api,
    validateRequestBody: true,
    validateRequestParameters: true,
  }),
});

// JWT validation ve tenant izolasyonu ile Lambda fonksiyonu
export const handler = async (event: APIGatewayProxyEvent) => {
  // API Gateway JWT'yi zaten doğruladı, claim'leri çıkar
  const claims = event.requestContext.authorizer?.claims;

  if (!claims) {
    return { statusCode: 401, body: 'Unauthorized' };
  }

  const tenantId = claims['custom:tenantId'];
  const role = claims['custom:role'];

  // Tenant context'i doğrula
  if (!tenantId) {
    return { statusCode: 403, body: 'Missing tenant context' };
  }

  // Tenant izolasyonuyla query
  const result = await dynamoDB.query({
    TableName: 'BillingRecords',
    IndexName: 'TenantIndex',
    KeyConditionExpression: 'tenantId = :tenantId',
    ExpressionAttributeValues: {
      ':tenantId': tenantId,
    },
  }).promise();

  // Role-based filtering uygula
  const filteredRecords = filterByRole(result.Items, role);

  return {
    statusCode: 200,
    body: JSON.stringify(filteredRecords),
  };
};

Cache TTL, performance ve güvenlik arasında net bir trade-off.

Cache TTLPerformanceGüvenlikKullanım Alanı
YokEn yüksek latencyGerçek zamanlı permission’larYüksek güvenlikli operasyonlar
5 dakikaİyi denge~5 dakika gecikmeStandard API endpoint’leri
30-60 dakikaEn iyi performanceBayat permission’larRead-only public data

Cache’lenen kararlar, permission’lar değişse bile TTL boyunca geçerli kalıyor. Kritik permission değişiklikleri için daha kısa bir TTL kullan veya cache-busting uygula.

External Auth Provider’lardan Migration#

Lazy Migration Stratejisi#

// User Migration Lambda - Lazy migration yaklaşımı
export const userMigration = async (event: UserMigrationTriggerEvent) => {
  if (event.triggerSource === 'UserMigration_Authentication') {
    // Kullanıcı sign-in olmaya çalışıyor ama Cognito'da yok
    const { userName, password } = event.request;

    try {
      // Credential'ları Auth0'a karşı doğrula
      const auth0User = await validateWithAuth0(userName, password);

      if (auth0User) {
        // Kullanıcı geçerli, Cognito'ya migrate et
        event.response.userAttributes = {
          email: auth0User.email,
          email_verified: 'true',
          given_name: auth0User.given_name,
          family_name: auth0User.family_name,
          'custom:auth0Id': auth0User.user_id,
          'custom:migratedAt': new Date().toISOString(),
        };

        event.response.finalUserStatus = 'CONFIRMED';
        event.response.messageAction = 'SUPPRESS'; // Welcome email gönderme

        // Tracking için migration'ı logla
        await logMigration(userName, 'success');

        return event;
      }
    } catch (error) {
      await logMigration(userName, 'failed', error);
      throw error;
    }
  }

  if (event.triggerSource === 'UserMigration_ForgotPassword') {
    // Kullanıcı password reset istiyor ama Cognito'da yok
    const { userName } = event.request;

    // Kullanıcı Auth0'da var mı kontrol et
    const auth0User = await getUserFromAuth0(userName);

    if (auth0User) {
      event.response.userAttributes = {
        email: auth0User.email,
        email_verified: 'true',
        'custom:auth0Id': auth0User.user_id,
      };

      event.response.messageAction = 'SUPPRESS';

      return event;
    }
  }

  throw new Error('User not found in legacy system');
};

async function validateWithAuth0(username: string, password: string) {
  const response = await axios.post('https://YOUR_DOMAIN.auth0.com/oauth/token', {
    grant_type: 'password',
    username,
    password,
    client_id: process.env.AUTH0_CLIENT_ID,
    client_secret: process.env.AUTH0_CLIENT_SECRET,
    audience: process.env.AUTH0_AUDIENCE,
    scope: 'openid profile email',
  });

  if (response.data.access_token) {
    // User info al
    const userInfo = await axios.get('https://YOUR_DOMAIN.auth0.com/userinfo', {
      headers: { Authorization: `Bearer ${response.data.access_token}` },
    });

    return userInfo.data;
  }

  return null;
}

User Migration Lambda önce implement edilip staging kullanıcılarıyla test ediliyor. Production’da aktifleştirilen lazy migration’la active kullanıcılar kendi ritimlerinde geçiş yaparken, migrate olan hesap sayısı izleniyor. Active nüfus migrate olduktan sonra kalan inactive hesaplar CSV veya admin API ile bulk import ediliyor, legacy sistem ise tüm kullanıcıların migrate olduğu doğrulandıktan sonra kapatılıyor.

Lazy fazın ne kadar süreceği kullanıcıların ne sıklıkta sign-in olduğuna bağlı; hiç authenticate olmayan atıl hesaplardan oluşan kuyruğu da bulk import adımı karşılıyor.

Gelişmiş Güvenlik Özellikleri#

Cognito’nun advanced security özellikleri Plus tier pricing gerektiriyor ancak enterprise-grade koruma sağlıyor.

Güvenlik Konfigürasyonu#

// Advanced Security'yi aktifleştir (Plus tier gerekli)
const userPool = new cognito.UserPool(this, 'SecureUserPool', {
  advancedSecurityMode: cognito.AdvancedSecurityMode.ENFORCED,
  signInAliases: { email: true },
  signInCaseSensitive: false,
});

// Post Authentication - Risk seviyelerini handle et
export const postAuthentication = async (event: PostAuthenticationTriggerEvent) => {
  const riskLevel = event.request.userContextData?.encodedData
    ? parseRiskData(event.request.userContextData.encodedData)
    : 'LOW';

  // Risk seviyesiyle authentication'ı logla
  await logAuthentication({
    username: event.userName,
    riskLevel,
    ipAddress: event.request.userContextData?.ipAddress,
    deviceKey: event.request.userContextData?.deviceKey,
    timestamp: new Date().toISOString(),
  });

  // Yüksek riskli authentication'lar için ek güvenlik tetikle
  if (riskLevel === 'HIGH' || riskLevel === 'MEDIUM') {
    await sendSecurityAlert(event.userName, riskLevel);

    if (riskLevel === 'HIGH') {
      await setUserMFARequired(event.userPoolId, event.userName);
    }
  }

  return event;
};

Bu üç katmana ayrılıyor.

  1. Compromised Credentials Protection: AWS, ihlal edilmiş credential database’lerini izliyor ve bilinen compromised password’lerle sign-in’leri blokluyor
  2. Adaptive Authentication: IP, device, location’a göre risk skorları ve risk seviyesi başına otomatik yanıtlar
  3. MFA Seçenekleri: SMS (en yüksek sürtünme), TOTP (dengeli), WebAuthn/FIDO2 (en düşük sürtünme)

Warning

MFA Configuration Lock-in: MFA bir kez “REQUIRED” olarak ayarlandığında (herhangi bir metod için: SMS, TOTP veya WebAuthn), pool’u yeniden oluşturmadan devre dışı bırakamaz veya “OPTIONAL“‘a çeviremezsin. Her zaman “OPTIONAL” kullan ve MFA’yı uygulama logic’i veya adaptive authentication ile seçici olarak enforce et.

SDK Karşılaştırması: Amplify vs AWS SDK#

Bundle boyutu, özellik desteği ve bakım yükü üçü arasında farklılaşıyor.

KriterAWS Amplifyamazon-cognito-identity-jsAWS SDK v3
Bundle Boyutu~500KB (tree-shakeable)~100KB~50KB (modular)
Kullanım AlanıFrontend app’ler (React, React Native)Custom UI ile frontendBackend/server-side
Secret DesteğiHayırHayırEvet
SRP AuthEvet, Built-inEvet, Built-inHayır, Manuel implementasyon
Token YönetimiEvet, OtomatikEvet, ManuelHayır, Manuel
OAuth Flow’larıEvet, Full destekSınırlıEvet, Full destek
SSR DesteğiSınırlı (Next.js/Nuxt)HayırEvet
BakımEvet, AktifSınırlı, DeprecatingEvet, Aktif

Amplify Frontend Implementasyonu#

import { Amplify } from 'aws-amplify';
import { signIn, signOut, getCurrentUser } from 'aws-amplify/auth';

Amplify.configure({
  Auth: {
    Cognito: {
      userPoolId: 'us-east-1_ABC123',
      userPoolClientId: 'abc123def456',
      identityPoolId: 'us-east-1:abc123-def456',
      loginWith: {
        oauth: {
          domain: 'auth.example.com',
          scopes: ['openid', 'email', 'profile', 'billing-api/read'],
          redirectSignIn: ['https://app.example.com/callback'],
          redirectSignOut: ['https://app.example.com/logout'],
          responseType: 'code',
        },
      },
    },
  },
});

async function handleSignIn(email: string, password: string) {
  try {
    const { isSignedIn, nextStep } = await signIn({
      username: email,
      password,
    });

    if (nextStep.signInStep === 'CONFIRM_SIGN_IN_WITH_TOTP_CODE') {
      const code = await promptForMFACode();
      await confirmSignIn({ challengeResponse: code });
    }

    // Token'lar otomatik olarak saklanıyor ve refresh ediliyor
    const user = await getCurrentUser();
    return user;
  } catch (error) {
    console.error('Sign in error:', error);
    throw error;
  }
}

AWS SDK Backend Implementasyonu#

import {
  CognitoIdentityProviderClient,
  AdminInitiateAuthCommand,
} from '@aws-sdk/client-cognito-identity-provider';
import { createHmac } from 'crypto';

const client = new CognitoIdentityProviderClient({ region: 'us-east-1' });

function calculateSecretHash(username: string): string {
  const message = username + process.env.COGNITO_CLIENT_ID;
  const hash = createHmac('sha256', process.env.COGNITO_CLIENT_SECRET!)
    .update(message)
    .digest('base64');
  return hash;
}

async function authenticateUser(username: string, password: string) {
  const command = new AdminInitiateAuthCommand({
    UserPoolId: process.env.USER_POOL_ID,
    ClientId: process.env.COGNITO_CLIENT_ID,
    AuthFlow: 'ADMIN_USER_PASSWORD_AUTH',
    AuthParameters: {
      USERNAME: username,
      PASSWORD: password,
      SECRET_HASH: calculateSecretHash(username),
    },
  });

  const response = await client.send(command);

  return {
    accessToken: response.AuthenticationResult?.AccessToken,
    idToken: response.AuthenticationResult?.IdToken,
    refreshToken: response.AuthenticationResult?.RefreshToken,
    expiresIn: response.AuthenticationResult?.ExpiresIn,
  };
}

Seçim kılavuzu: Otomatik token yönetimi olan React/React Native frontend uygulamaları için Amplify kullan. Client secret’ları ve custom authentication flow’ları gerektiren backend servisleri için AWS SDK kullan.

Production Pattern’leri ve Monitoring#

Token Refresh Stratejisi#

const TOKEN_REFRESH_THRESHOLD = 5 * 60 * 1000; // 5 dakika

async function getValidToken(): Promise<string> {
  const session = await Auth.currentSession();
  const expiresAt = session.getAccessToken().getExpiration() * 1000;

  if (Date.now() + TOKEN_REFRESH_THRESHOLD > expiresAt) {
    const newSession = await Auth.currentSession();
    return newSession.getAccessToken().getJwtToken();
  }

  return session.getAccessToken().getJwtToken();
}

Temel CloudWatch Metrikleri#

SignInSuccesses ve SignInThrottles authentication health’ini gösteriyor, TokenRefreshSuccesses token refresh failure’larını takip ediyor; authentication süresi ve MFA completion rate gibi custom metrikler de tabloyu tamamlıyor. Alarm’lar yüksek failure rate, throttling ve advanced security block’ları üzerine kurulmalı.

Güvenlik tarafında ise compromised credential tespitleri, yüksek riskli authentication girişimleri, adaptive authentication tetiklemeleri ve account takeover prevention rate izlenmeye değer.

Production’da Görülen Hata Modları#

Hata moduNeden olurÇözüm
Backup stratejisi yokCognito User Pool’ları backup alınamıyor veya region’lar arası replicate edilemiyor; yanlışlıkla silme veya region failure total user data kaybı anlamına geliyorListUsers API ile günlük user data’yı S3’e export et, kritik metadata’yı DynamoDB’de yedekle, export’u scheduled Lambda ile otomatikleştir ve pool recreation prosedürünü dokümante et
Token boyutu limitleriÇok fazla custom claim, token’ları 8KB header limitinin üzerine çıkarıp HTTP 431 hatalarına yol açıyorBüyük dataset’leri DynamoDB’de sakla ve full object embed etmek yerine reference ID kullan (permissionSetId: "ps-123"); büyük permission set’leri için pagination uygula
Authorizer cache invalidationAPI Gateway authorization kararlarını cache’liyor, bu yüzden revoke edilen bir permission cache expire olana kadar çalışmaya devam ediyorHassas operasyonlar için daha kısa TTL (5-15 dakika) kullan, veya gerçek zamanlı permission kontrolü gereken yerlerde Lambda authorizer’a geç
SMS region kısıtlamalarıSMS gönderimi AWS End User Messaging SMS (eski adı SNS) üzerinden tüm Cognito region’larında desteklenmiyorSMS’e güvenmeden önce region desteğini kontrol et, SMS kullanılamadığında email verification’a fallback yap
Lambda trigger timeout’larıSync trigger’lar 5 saniyelik timeout’a sahip, bu da external API yavaş yanıt verdiğinde authentication’ı başarısız kılıyorTrigger logic’ini 3 saniyenin altında tut, kritik olmayan görevleri async operasyonlara kaydır ve external API response’larını cache’le

Maliyet Analizi#

Fiyatlandırma Tier’ları (Aralık 2024)#

Lite tier (10,000 MAU ücretsiz, sonra kademeli fiyatlandırma):

  • Temel authentication, MFA, social provider’lar
  • Advanced security yok
  • Free tier sonrası kademeli fiyatlandırma: $0.0025/MAU (10K-50K), $0.00375/MAU (50K-100K), vb.

Essentials tier ($0.015/MAU):

  • Advanced security (audit mode)
  • Access token customization

Plus tier ($0.02/MAU):

  • Advanced security (enforced mode)
  • SAML/OIDC federation
  • Essentials’a göre 1.33x maliyet

Gözden kaçması kolay birkaç maliyet var.

  • SMS MFA: US’de $0.00645/mesaj (AWS End User Messaging SMS ile, eski adı SNS)
  • Lambda trigger invocation’ları: 1M request başına $0.20
  • API Gateway authorizer call’ları (caching devre dışıysa)

Optimizasyon tarafında inactive kullanıcıları otomatik archive etmek ve direct user sayısını azaltmak için federation kullanmak işe yarıyor; buna MAU büyüme trendlerini izlemek ve düşük trafikli API’ler için Lambda authorizer’ı değerlendirmek de eklenebilir.

Cognito ile Alternatifler Arasında Seçim Yapmak#

Cognito’nun Avantajlı Olduğu Durumlar#

  • AWS-native mimari
  • Standard authentication gereksinimleri
  • Budget-conscious projeler
  • Hızlı MVP geliştirme
  • Küçük-orta ölçek (< 10M kullanıcı)

Alternatiflerin Öne Çıktığı Durumlar#

Auth0: Karmaşık authentication flow’ları, kapsamlı özelleştirme, enterprise SLA gereksinimleri, global compliance ihtiyaçları

Okta: Workforce identity (çalışanlar), enterprise SSO, gelişmiş lifecycle yönetimi

Custom Çözüm: Benzersiz authentication gereksinimleri, tam data kontrolü, mevcut identity altyapısı, çok yüksek ölçek (> 100M kullanıcı)

Kabul Edilmesi Gereken Kısıtlamalar#

  • Sınırlı user management API’leri
  • 3KB CSS özelleştirme limiti
  • Doğrudan database erişimi yok

Kendi identity provider’ı ve sözleşmeye bağlı izolasyon şartı olan bir müşteri geldiğinde, tenant sayısı birkaç yüzü aştığında veya eksik cross-region replication taahhüt edilmiş kurtarma hedefiyle çeliştiğinde shared pool varsayılanını değiştir. Günlük user export’unu ilk production sign-up’tan önce kur, karar hangi yöne giderse gitsin.

Kaynaklar#

İlgili yazılar