經過 Day 24 的 SES 郵件服務建置,我們已經有了完整的用戶溝通管道。今天我們要整合 AWS Cognito,這是 AWS 提供的託管式用戶認證服務。雖然我們在 Day 25 後端篇已經實作了自建的 JWT 認證,但 Cognito 提供了許多額外的企業級功能,包括 MFA、社交登入、用戶池管理等。我們將探討何時該用 Cognito,以及如何整合。
/**
* AWS Cognito vs 自建認證系統
*
* ┌──────────────────┬─────────────────┬──────────────────┐
* │ 特性 │ AWS Cognito │ 自建 (JWT+Redis) │
* ├──────────────────┼─────────────────┼──────────────────┤
* │ 開發時間 │ 🟢 快速 (數小時)│ 🔴 慢 (數天) │
* │ 維護成本 │ 🟢 低 (AWS管理) │ 🔴 高 (自行維護) │
* │ 安全性 │ 🟢 企業級 │ 🟡 取決於實作 │
* │ 功能完整性 │ 🟢 豐富 │ 🟡 需自行開發 │
* │ 客製化彈性 │ 🔴 受限 │ 🟢 完全控制 │
* │ 成本 (小規模) │ 🟢 低/免費 │ 🟡 中等 │
* │ 成本 (大規模) │ 🔴 高 │ 🟢 低 │
* │ 供應商鎖定 │ 🔴 是 │ 🟢 否 │
* │ 用戶體驗控制 │ 🟡 有限 │ 🟢 完全控制 │
* │ MFA 支援 │ 🟢 內建 │ 🟡 需自行開發 │
* │ 社交登入 │ 🟢 內建 │ 🟡 需自行整合 │
* └──────────────────┴─────────────────┴──────────────────┘
*
* 建議使用場景:
*
* ✅ 使用 Cognito:
* - 快速 MVP
* - 企業合規需求(SOC2, HIPAA)
* - 需要 MFA、社交登入等完整功能
* - 團隊資源有限
* - 用戶規模 < 50萬 MAU
*
* ✅ 自建認證:
* - 需要完全客製化
* - 特殊業務邏輯
* - 用戶規模大(> 100萬 MAU)
* - 成本敏感
* - 避免供應商鎖定
*
* 💡 我們的策略:混合式
* - B2C 用戶: Cognito (快速、功能完整)
* - B2B 企業: 自建 (客製化、SSO 整合)
*/
/**
* AWS Cognito 架構
*
* ┌─────────────────────────────────────────────┐
* │ AWS Cognito 服務架構 │
* └─────────────────────────────────────────────┘
*
* 1. User Pool (用戶池)
* - 用戶目錄服務
* - 處理註冊、登入、密碼重設
* - 內建 JWT Token 發放
* - 支援 MFA、自訂屬性
*
* 2. Identity Pool (身份池)
* - 聯合身份管理
* - 授予 AWS 資源存取權限
* - 支援匿名存取
* - 整合多個 IdP (Identity Provider)
*
* 3. App Client
* - 應用程式介面
* - 定義 OAuth 流程
* - 設定 Token 有效期
*
* 4. Hosted UI
* - AWS 託管的登入頁面
* - 可自訂樣式
* - 支援社交登入
*
* 定價 (ap-northeast-1):
* - 前 50,000 MAU: 免費
* - 50,001 - 100,000: $0.00550 per MAU
* - 100,001 - 1,000,000: $0.00460 per MAU
* - MFA SMS: $0.05 per message
* - 進階安全功能: $0.05 per MAU
*/
// infrastructure/lib/cognito-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as sns from 'aws-cdk-lib/aws-sns';
import { Construct } from 'constructs';
export class CognitoStack extends cdk.Stack {
public readonly userPool: cognito.UserPool;
public readonly userPoolClient: cognito.UserPoolClient;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// 建立 User Pool
this.userPool = new cognito.UserPool(this, 'KyoUserPool', {
userPoolName: 'kyo-users',
// 自助註冊
selfSignUpEnabled: true,
// 登入方式
signInAliases: {
email: true,
username: false,
phone: false,
},
// 自動驗證
autoVerify: {
email: true,
},
// 標準屬性
standardAttributes: {
email: {
required: true,
mutable: false, // 不可變更
},
fullname: {
required: true,
mutable: true,
},
phoneNumber: {
required: false,
mutable: true,
},
},
// 自訂屬性
customAttributes: {
tenantId: new cognito.StringAttribute({
minLen: 1,
maxLen: 64,
mutable: false,
}),
role: new cognito.StringAttribute({
minLen: 1,
maxLen: 20,
mutable: true,
}),
plan: new cognito.StringAttribute({
minLen: 1,
maxLen: 20,
mutable: true,
}),
},
// 密碼政策
passwordPolicy: {
minLength: 8,
requireLowercase: true,
requireUppercase: true,
requireDigits: true,
requireSymbols: true,
tempPasswordValidity: cdk.Duration.days(3),
},
// 帳號復原
accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
// MFA 設定
mfa: cognito.Mfa.OPTIONAL,
mfaSecondFactor: {
sms: true,
otp: true, // TOTP
},
// Email 設定
email: cognito.UserPoolEmail.withSES({
fromEmail: 'noreply@kyong.com',
fromName: 'Kyo Team',
sesRegion: 'ap-northeast-1',
sesVerifiedDomain: 'kyong.com',
}),
// SMS 設定 (用於 MFA)
smsRole: this.createSMSRole(),
// 進階安全功能
advancedSecurityMode: cognito.AdvancedSecurityMode.ENFORCED,
// Lambda Triggers
lambdaTriggers: {
preSignUp: this.createPreSignUpTrigger(),
postConfirmation: this.createPostConfirmationTrigger(),
preAuthentication: this.createPreAuthenticationTrigger(),
},
// 裝置追蹤
deviceTracking: {
challengeRequiredOnNewDevice: true,
deviceOnlyRememberedOnUserPrompt: true,
},
// 用戶刪除保護
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
// 建立 App Client
this.userPoolClient = this.userPool.addClient('WebAppClient', {
userPoolClientName: 'web-app',
// OAuth 流程
oAuth: {
flows: {
authorizationCodeGrant: true,
implicitCodeGrant: false,
},
scopes: [
cognito.OAuthScope.EMAIL,
cognito.OAuthScope.OPENID,
cognito.OAuthScope.PROFILE,
],
callbackUrls: [
'http://localhost:5173/auth/callback',
'https://app.kyong.com/auth/callback',
],
logoutUrls: [
'http://localhost:5173',
'https://app.kyong.com',
],
},
// Token 有效期
accessTokenValidity: cdk.Duration.minutes(60),
idTokenValidity: cdk.Duration.minutes(60),
refreshTokenValidity: cdk.Duration.days(30),
// 啟用 Token 撤銷
enableTokenRevocation: true,
// 防止用戶存在枚舉
preventUserExistenceErrors: true,
// 支援的身份提供商
supportedIdentityProviders: [
cognito.UserPoolClientIdentityProvider.COGNITO,
cognito.UserPoolClientIdentityProvider.GOOGLE,
cognito.UserPoolClientIdentityProvider.FACEBOOK,
],
// 讀寫屬性
readAttributes: new cognito.ClientAttributes()
.withStandardAttributes({
email: true,
emailVerified: true,
fullname: true,
phoneNumber: true,
})
.withCustomAttributes('tenantId', 'role', 'plan'),
writeAttributes: new cognito.ClientAttributes()
.withStandardAttributes({
fullname: true,
phoneNumber: true,
}),
});
// 建立 User Pool Domain (for Hosted UI)
const domain = this.userPool.addDomain('CognitoDomain', {
cognitoDomain: {
domainPrefix: 'kyo-auth',
},
});
// 設定社交登入:Google
const googleProvider = new cognito.UserPoolIdentityProviderGoogle(this, 'Google', {
userPool: this.userPool,
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
scopes: ['profile', 'email', 'openid'],
attributeMapping: {
email: cognito.ProviderAttribute.GOOGLE_EMAIL,
fullname: cognito.ProviderAttribute.GOOGLE_NAME,
profilePicture: cognito.ProviderAttribute.GOOGLE_PICTURE,
},
});
// App Client 需依賴 IdP
this.userPoolClient.node.addDependency(googleProvider);
// 輸出
new cdk.CfnOutput(this, 'UserPoolId', {
value: this.userPool.userPoolId,
description: 'Cognito User Pool ID',
});
new cdk.CfnOutput(this, 'UserPoolClientId', {
value: this.userPoolClient.userPoolClientId,
description: 'Cognito User Pool Client ID',
});
new cdk.CfnOutput(this, 'UserPoolDomain', {
value: `https://${domain.domainName}.auth.ap-northeast-1.amazoncognito.com`,
description: 'Cognito Hosted UI Domain',
});
}
/**
* 建立 SMS 角色(用於 MFA)
*/
private createSMSRole(): iam.Role {
return new iam.Role(this, 'CognitoSMSRole', {
assumedBy: new iam.ServicePrincipal('cognito-idp.amazonaws.com'),
inlinePolicies: {
sns: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['sns:Publish'],
resources: ['*'],
}),
],
}),
},
});
}
/**
* Pre Sign Up Trigger
* 在用戶註冊前執行,可用於驗證、自動確認等
*/
private createPreSignUpTrigger(): lambda.Function {
return new lambda.Function(this, 'PreSignUpTrigger', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromInline(`
exports.handler = async (event) => {
console.log('Pre Sign Up Event:', JSON.stringify(event, null, 2));
// 自動確認用戶(跳過 email 驗證)
// 實際應該發送驗證郵件
if (event.request.userAttributes.email) {
event.response.autoConfirmUser = true;
event.response.autoVerifyEmail = true;
}
// 設定自訂屬性
event.response.claimsOverrideDetails = {
claimsToAddOrOverride: {
'custom:tenantId': 'default-tenant',
'custom:role': 'user',
'custom:plan': 'free',
},
};
return event;
};
`),
timeout: cdk.Duration.seconds(10),
});
}
/**
* Post Confirmation Trigger
* 用戶確認後執行,可用於發送歡迎郵件、建立租戶等
*/
private createPostConfirmationTrigger(): lambda.Function {
return new lambda.Function(this, 'PostConfirmationTrigger', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromInline(`
exports.handler = async (event) => {
console.log('Post Confirmation Event:', JSON.stringify(event, null, 2));
const { email, name } = event.request.userAttributes;
// TODO: 呼叫後端 API 建立租戶
// TODO: 發送歡迎郵件
return event;
};
`),
timeout: cdk.Duration.seconds(10),
});
}
/**
* Pre Authentication Trigger
* 登入前執行,可用於 IP 檢查、異常登入偵測等
*/
private createPreAuthenticationTrigger(): lambda.Function {
return new lambda.Function(this, 'PreAuthenticationTrigger', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromInline(`
exports.handler = async (event) => {
console.log('Pre Authentication Event:', JSON.stringify(event, null, 2));
// 檢查 IP 白名單(範例)
// const ipAddress = event.request.userContextData?.sourceIp;
// 檢查登入嘗試次數
// const failedAttempts = await checkFailedAttempts(event.userName);
// if (failedAttempts > 5) {
// throw new Error('Account locked due to multiple failed attempts');
// }
return event;
};
`),
timeout: cdk.Duration.seconds(10),
});
}
}
// apps/kyo-otp-service/src/lib/cognito-client.ts
import {
CognitoIdentityProviderClient,
SignUpCommand,
InitiateAuthCommand,
RespondToAuthChallengeCommand,
GetUserCommand,
UpdateUserAttributesCommand,
ChangePasswordCommand,
ForgotPasswordCommand,
ConfirmForgotPasswordCommand,
AdminGetUserCommand,
AdminUpdateUserAttributesCommand,
} from '@aws-sdk/client-cognito-identity-provider';
export class CognitoService {
private client: CognitoIdentityProviderClient;
private userPoolId: string;
private clientId: string;
constructor(config: {
region: string;
userPoolId: string;
clientId: string;
}) {
this.client = new CognitoIdentityProviderClient({
region: config.region,
});
this.userPoolId = config.userPoolId;
this.clientId = config.clientId;
}
/**
* 註冊用戶
*/
async signUp(data: {
email: string;
password: string;
name: string;
}): Promise<{
userSub: string;
confirmed: boolean;
}> {
const command = new SignUpCommand({
ClientId: this.clientId,
Username: data.email,
Password: data.password,
UserAttributes: [
{ Name: 'email', Value: data.email },
{ Name: 'name', Value: data.name },
],
});
const response = await this.client.send(command);
return {
userSub: response.UserSub!,
confirmed: response.UserConfirmed || false,
};
}
/**
* 登入 (USER_PASSWORD_AUTH flow)
*/
async login(data: {
email: string;
password: string;
}): Promise<{
accessToken: string;
idToken: string;
refreshToken: string;
expiresIn: number;
}> {
const command = new InitiateAuthCommand({
ClientId: this.clientId,
AuthFlow: 'USER_PASSWORD_AUTH',
AuthParameters: {
USERNAME: data.email,
PASSWORD: data.password,
},
});
const response = await this.client.send(command);
if (response.ChallengeName) {
// 需要處理挑戰(如 MFA)
throw new Error(`Challenge required: ${response.ChallengeName}`);
}
if (!response.AuthenticationResult) {
throw new Error('Authentication failed');
}
return {
accessToken: response.AuthenticationResult.AccessToken!,
idToken: response.AuthenticationResult.IdToken!,
refreshToken: response.AuthenticationResult.RefreshToken!,
expiresIn: response.AuthenticationResult.ExpiresIn!,
};
}
/**
* 刷新 Token
*/
async refreshToken(refreshToken: string): Promise<{
accessToken: string;
idToken: string;
expiresIn: number;
}> {
const command = new InitiateAuthCommand({
ClientId: this.clientId,
AuthFlow: 'REFRESH_TOKEN_AUTH',
AuthParameters: {
REFRESH_TOKEN: refreshToken,
},
});
const response = await this.client.send(command);
if (!response.AuthenticationResult) {
throw new Error('Token refresh failed');
}
return {
accessToken: response.AuthenticationResult.AccessToken!,
idToken: response.AuthenticationResult.IdToken!,
expiresIn: response.AuthenticationResult.ExpiresIn!,
};
}
/**
* 取得用戶資訊
*/
async getUser(accessToken: string): Promise<{
username: string;
attributes: Record<string, string>;
}> {
const command = new GetUserCommand({
AccessToken: accessToken,
});
const response = await this.client.send(command);
const attributes: Record<string, string> = {};
response.UserAttributes?.forEach((attr) => {
attributes[attr.Name] = attr.Value!;
});
return {
username: response.Username!,
attributes,
};
}
/**
* 更新用戶屬性
*/
async updateUserAttributes(
accessToken: string,
attributes: Record<string, string>
): Promise<void> {
const command = new UpdateUserAttributesCommand({
AccessToken: accessToken,
UserAttributes: Object.entries(attributes).map(([name, value]) => ({
Name: name,
Value: value,
})),
});
await this.client.send(command);
}
/**
* 變更密碼
*/
async changePassword(data: {
accessToken: string;
previousPassword: string;
proposedPassword: string;
}): Promise<void> {
const command = new ChangePasswordCommand({
AccessToken: data.accessToken,
PreviousPassword: data.previousPassword,
ProposedPassword: data.proposedPassword,
});
await this.client.send(command);
}
/**
* 忘記密碼(發送驗證碼)
*/
async forgotPassword(email: string): Promise<void> {
const command = new ForgotPasswordCommand({
ClientId: this.clientId,
Username: email,
});
await this.client.send(command);
}
/**
* 確認忘記密碼(使用驗證碼重設)
*/
async confirmForgotPassword(data: {
email: string;
code: string;
newPassword: string;
}): Promise<void> {
const command = new ConfirmForgotPasswordCommand({
ClientId: this.clientId,
Username: data.email,
ConfirmationCode: data.code,
Password: data.newPassword,
});
await this.client.send(command);
}
/**
* 管理員取得用戶資訊
*/
async adminGetUser(username: string): Promise<{
username: string;
status: string;
attributes: Record<string, string>;
}> {
const command = new AdminGetUserCommand({
UserPoolId: this.userPoolId,
Username: username,
});
const response = await this.client.send(command);
const attributes: Record<string, string> = {};
response.UserAttributes?.forEach((attr) => {
attributes[attr.Name] = attr.Value!;
});
return {
username: response.Username!,
status: response.UserStatus!,
attributes,
};
}
/**
* 管理員更新用戶屬性
*/
async adminUpdateUserAttributes(
username: string,
attributes: Record<string, string>
): Promise<void> {
const command = new AdminUpdateUserAttributesCommand({
UserPoolId: this.userPoolId,
Username: username,
UserAttributes: Object.entries(attributes).map(([name, value]) => ({
Name: name,
Value: value,
})),
});
await this.client.send(command);
}
}
// apps/kyo-otp-service/src/plugins/cognito-auth.ts
import { FastifyPluginAsync } from 'fastify';
import fp from 'fastify-plugin';
import { CognitoJwtVerifier } from 'aws-jwt-verify';
declare module 'fastify' {
interface FastifyRequest {
cognitoUser?: {
sub: string;
email: string;
name: string;
tenantId: string;
role: string;
};
}
}
const cognitoAuthPlugin: FastifyPluginAsync = async (server) => {
// 建立 JWT Verifier
const accessTokenVerifier = CognitoJwtVerifier.create({
userPoolId: process.env.COGNITO_USER_POOL_ID!,
tokenUse: 'access',
clientId: process.env.COGNITO_CLIENT_ID!,
});
const idTokenVerifier = CognitoJwtVerifier.create({
userPoolId: process.env.COGNITO_USER_POOL_ID!,
tokenUse: 'id',
clientId: process.env.COGNITO_CLIENT_ID!,
});
/**
* onRequest Hook: 自動驗證 Cognito Token
*/
server.addHook('onRequest', async (request) => {
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return; // 不強制,讓路由自行決定
}
const token = authHeader.substring(7);
try {
// 先嘗試驗證 Access Token
let payload = await accessTokenVerifier.verify(token);
// 如果失敗,嘗試 ID Token
if (!payload) {
payload = await idTokenVerifier.verify(token);
}
// 解析用戶資訊
request.cognitoUser = {
sub: payload.sub,
email: payload.email || '',
name: payload.name || '',
tenantId: payload['custom:tenantId'] || '',
role: payload['custom:role'] || 'user',
};
} catch (err) {
// Token 無效,但不拋出錯誤
server.log.debug('Invalid Cognito token:', err);
}
});
/**
* 裝飾器:要求 Cognito 認證
*/
server.decorate('requireCognitoAuth', () => {
return async (request: any, reply: any) => {
if (!request.cognitoUser) {
throw server.httpErrors.unauthorized('Cognito authentication required');
}
};
});
};
export default fp(cognitoAuthPlugin, {
name: 'cognito-auth',
});
// apps/kyo-otp-service/src/routes/hybrid-auth.ts
import { FastifyPluginAsync } from 'fastify';
/**
* 混合式認證路由
*
* 支援:
* 1. 自建 JWT (適合 B2B 企業客戶)
* 2. Cognito (適合 B2C 一般用戶)
*/
export const hybridAuthRoutes: FastifyPluginAsync = async (server) => {
/**
* 統一的認證檢查
*/
const requireAuth = async (request: any) => {
// 優先檢查 Cognito
if (request.cognitoUser) {
return {
userId: request.cognitoUser.sub,
email: request.cognitoUser.email,
provider: 'cognito',
};
}
// 檢查自建 JWT
if (request.user) {
return {
userId: request.user.userId,
email: request.user.email,
provider: 'jwt',
};
}
throw server.httpErrors.unauthorized('Authentication required');
};
/**
* 範例:取得用戶資訊(支援兩種認證)
*/
server.get('/profile', async (request, reply) => {
const auth = await requireAuth(request);
let user;
if (auth.provider === 'cognito') {
// 從 Cognito 取得用戶資訊
user = await server.cognitoService.getUser(
request.headers.authorization!.substring(7)
);
} else {
// 從自建資料庫取得用戶資訊
user = await server.db.user.findUnique({
where: { id: auth.userId },
});
}
return reply.send({
success: true,
user,
provider: auth.provider,
});
});
};
/**
* AWS Cognito 成本分析 (ap-northeast-1)
*
* 基礎定價:
* - 前 50,000 MAU: 免費
* - 50,001 - 100,000: $0.0055 per MAU
* - 100,001 - 1,000,000: $0.0046 per MAU
* - 1,000,000+: $0.0025 per MAU
*
* 額外費用:
* - SMS MFA: $0.05 per message
* - 進階安全功能: $0.05 per MAU
* - SAML/OIDC IdP: 無額外費用
*
* MAU (Monthly Active User) 定義:
* - 在該月執行過身份操作的唯一用戶
* - 操作包括:登入、註冊、Token 刷新、密碼重設
*/
interface CognitoCostEstimate {
monthlyActiveUsers: number;
smsMessagesPerMonth?: number;
advancedSecurityEnabled?: boolean;
}
function calculateCognitoCost(config: CognitoCostEstimate): {
baseCost: number;
smsCost: number;
advancedSecurityCost: number;
totalMonthlyCost: number;
} {
const { monthlyActiveUsers, smsMessagesPerMonth = 0, advancedSecurityEnabled = false } = config;
let baseCost = 0;
// 計算基礎費用
if (monthlyActiveUsers <= 50000) {
baseCost = 0; // 免費
} else if (monthlyActiveUsers <= 100000) {
const billableUsers = monthlyActiveUsers - 50000;
baseCost = billableUsers * 0.0055;
} else if (monthlyActiveUsers <= 1000000) {
baseCost = 50000 * 0.0055 + (monthlyActiveUsers - 100000) * 0.0046;
} else {
baseCost =
50000 * 0.0055 +
900000 * 0.0046 +
(monthlyActiveUsers - 1000000) * 0.0025;
}
// SMS 費用
const smsCost = smsMessagesPerMonth * 0.05;
// 進階安全功能費用
const advancedSecurityCost = advancedSecurityEnabled
? monthlyActiveUsers * 0.05
: 0;
const totalMonthlyCost = baseCost + smsCost + advancedSecurityCost;
return {
baseCost,
smsCost,
advancedSecurityCost,
totalMonthlyCost,
};
}
// 範例 1: 免費額度內
const smallScale = calculateCognitoCost({
monthlyActiveUsers: 30000,
smsMessagesPerMonth: 500,
advancedSecurityEnabled: false,
});
console.log('=== Small Scale (30K MAU) ===');
console.log('Base Cost: $' + smallScale.baseCost.toFixed(2));
console.log('SMS Cost: $' + smallScale.smsCost.toFixed(2));
console.log('Total: $' + smallScale.totalMonthlyCost.toFixed(2));
// 範例 2: 中型規模
const mediumScale = calculateCognitoCost({
monthlyActiveUsers: 200000,
smsMessagesPerMonth: 5000,
advancedSecurityEnabled: true,
});
console.log('\n=== Medium Scale (200K MAU) ===');
console.log('Base Cost: $' + mediumScale.baseCost.toFixed(2));
console.log('SMS Cost: $' + mediumScale.smsCost.toFixed(2));
console.log('Advanced Security: $' + mediumScale.advancedSecurityCost.toFixed(2));
console.log('Total: $' + mediumScale.totalMonthlyCost.toFixed(2));
// 範例 3: 大型規模(與自建比較)
const largeScale = calculateCognitoCost({
monthlyActiveUsers: 1000000,
smsMessagesPerMonth: 20000,
advancedSecurityEnabled: true,
});
console.log('\n=== Large Scale (1M MAU) ===');
console.log('Cognito Total: $' + largeScale.totalMonthlyCost.toFixed(2));
// 自建成本估算
const selfHostedCost = {
ec2: 100, // t3.medium x 3
rds: 200, // db.t3.medium
elasticache: 50, // cache.t3.micro
dataTransfer: 100,
engineeringTime: 2000, // 1 個工程師維護
};
const selfHostedTotal = Object.values(selfHostedCost).reduce((a, b) => a + b, 0);
console.log('Self-hosted Total: $' + selfHostedTotal.toFixed(2));
console.log('\n💡 Break-even point: ~100K MAU (without engineering cost)');
console.log('💡 With engineering cost: Cognito is cheaper up to 1M MAU');
/**
* 成本優化策略:
*
* 1. 減少 MAU
* ✅ 只在必要時觸發身份操作
* ✅ 延長 Token 有效期(減少刷新)
* ✅ 使用本地快取
*
* 2. 減少 SMS 費用
* ✅ 優先使用 TOTP (免費)
* ✅ Email MFA 代替 SMS
* ✅ 只對高風險操作要求 MFA
*
* 3. 進階安全功能
* ✅ 評估是否真的需要
* ✅ 只對關鍵用戶群啟用
* ✅ 使用自建的風險評估
*
* 4. 混合策略
* ✅ B2C 用戶: Cognito (< 50K 免費)
* ✅ B2B 企業: 自建 (客製化需求)
* ✅ 內部用戶: SSO (SAML/OIDC)
*/
我們今天完成了 AWS Cognito 的完整整合:
Cognito vs 自建何時選擇?:
Lambda Triggers 用途:
Token 管理策略:
進階安全功能值得嗎?: