前言
在現代雲端架構中,應用程式的認證與授權是不可或缺的安全機制。AWS Application Load Balancer (ALB) 提供了內建的認證功能,可以在流量到達後端應用程式之前就完成使用者身份驗證,大幅簡化了開發團隊的工作負擔。
本文將詳細介紹如何利用 AWS ALB 的認證功能,整合 Amazon Cognito 或第三方 OIDC Provider,建立安全且可擴展的認證機制。
1. ALB 內建認證功能概述
什麼是 ALB Authentication?
AWS ALB 自 2018 年起支援內建認證功能,允許您在負載平衡器層級執行使用者認證,而不需要修改後端應用程式的程式碼。這項功能支援:
- Amazon Cognito User Pools:AWS 原生的使用者管理服務
- 任何符合 OIDC 標準的 Identity Provider (IdP):如 Okta、Auth0、Google、Azure AD 等
運作原理
1
2
| 使用者 → ALB → 認證檢查 → [未認證] → 重導向至 IdP
→ [已認證] → 轉發至後端 (附帶 JWT Token)
|
當使用者嘗試存取受保護的資源時,ALB 會執行以下流程:
- 檢查請求中是否包含有效的認證 Session Cookie
- 若無有效 Session,將使用者重導向至 IdP 登入頁面
- 使用者完成登入後,IdP 回傳 Authorization Code
- ALB 使用 Authorization Code 交換 Access Token 和 ID Token
- ALB 驗證 Token 並建立 Session
- 將請求轉發至後端,並在 HTTP Header 中加入使用者資訊
優勢
| 優勢 | 說明 |
|---|
| 簡化開發 | 無需在應用程式中實作認證邏輯 |
| 集中管理 | 在負載平衡器層級統一處理認證 |
| 安全性提升 | 未認證流量不會到達後端 |
| 支援多種 IdP | 可整合 Cognito 或任何 OIDC Provider |
2. Amazon Cognito 整合設定
建立 Cognito User Pool
首先,我們需要建立一個 Cognito User Pool 來管理使用者。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| # 使用 AWS CLI 建立 User Pool
aws cognito-idp create-user-pool \
--pool-name my-app-user-pool \
--auto-verified-attributes email \
--username-attributes email \
--policies '{
"PasswordPolicy": {
"MinimumLength": 12,
"RequireUppercase": true,
"RequireLowercase": true,
"RequireNumbers": true,
"RequireSymbols": true
}
}' \
--schema '[
{
"Name": "email",
"Required": true,
"Mutable": true
}
]'
|
建立 App Client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # 取得 User Pool ID(假設為 us-east-1_xxxxxxxxx)
USER_POOL_ID="us-east-1_xxxxxxxxx"
# 建立 App Client
aws cognito-idp create-user-pool-client \
--user-pool-id $USER_POOL_ID \
--client-name my-app-client \
--generate-secret \
--supported-identity-providers COGNITO \
--callback-urls '["https://your-domain.com/oauth2/idpresponse"]' \
--logout-urls '["https://your-domain.com/logout"]' \
--allowed-o-auth-flows code \
--allowed-o-auth-scopes openid email profile \
--allowed-o-auth-flows-user-pool-client
|
設定 Cognito Domain
1
2
3
4
| # 設定 Cognito 託管的網域
aws cognito-idp create-user-pool-domain \
--domain my-app-auth \
--user-pool-id $USER_POOL_ID
|
在 ALB 設定 Cognito 認證
透過 AWS Console 或 CLI 設定 Listener Rule:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| # 取得必要資訊
ALB_ARN="arn:aws:elasticloadbalancing:region:account-id:loadbalancer/app/my-alb/xxxxxxxxxx"
TARGET_GROUP_ARN="arn:aws:elasticloadbalancing:region:account-id:targetgroup/my-tg/xxxxxxxxxx"
# 建立帶有 Cognito 認證的 Listener Rule
aws elbv2 create-rule \
--listener-arn $LISTENER_ARN \
--priority 10 \
--conditions '[{
"Field": "path-pattern",
"Values": ["/protected/*"]
}]' \
--actions '[
{
"Type": "authenticate-cognito",
"Order": 1,
"AuthenticateCognitoConfig": {
"UserPoolArn": "arn:aws:cognito-idp:region:account-id:userpool/us-east-1_xxxxxxxxx",
"UserPoolClientId": "xxxxxxxxxxxxxxxxxxxxxxxxxx",
"UserPoolDomain": "my-app-auth",
"SessionCookieName": "AWSELBAuthSessionCookie",
"Scope": "openid email profile",
"SessionTimeout": 604800,
"OnUnauthenticatedRequest": "authenticate"
}
},
{
"Type": "forward",
"Order": 2,
"TargetGroupArn": "'$TARGET_GROUP_ARN'"
}
]'
|
3. 第三方 OIDC Provider 整合
支援的 OIDC Provider
ALB 可以整合任何符合 OIDC 標準的 Identity Provider,常見的包括:
- Okta
- Auth0
- Google Workspace
- Azure Active Directory
- Keycloak
- PingIdentity
以 Okta 為例的整合設定
在 Okta 建立應用程式
- 登入 Okta Admin Console
- 前往 Applications → Create App Integration
- 選擇 OIDC - OpenID Connect 和 Web Application
- 設定以下資訊:
- Sign-in redirect URIs:
https://your-alb-domain.com/oauth2/idpresponse - Sign-out redirect URIs:
https://your-alb-domain.com
- 記錄 Client ID 和 Client Secret
取得 OIDC Endpoints
1
2
| # Okta 的 OIDC Discovery Endpoint
curl https://your-okta-domain.okta.com/.well-known/openid-configuration
|
主要需要的 Endpoints:
- Authorization Endpoint:
https://your-okta-domain.okta.com/oauth2/v1/authorize - Token Endpoint:
https://your-okta-domain.okta.com/oauth2/v1/token - User Info Endpoint:
https://your-okta-domain.okta.com/oauth2/v1/userinfo - Issuer:
https://your-okta-domain.okta.com
在 ALB 設定 OIDC 認證
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| aws elbv2 create-rule \
--listener-arn $LISTENER_ARN \
--priority 20 \
--conditions '[{
"Field": "path-pattern",
"Values": ["/api/*"]
}]' \
--actions '[
{
"Type": "authenticate-oidc",
"Order": 1,
"AuthenticateOidcConfig": {
"Issuer": "https://your-okta-domain.okta.com",
"AuthorizationEndpoint": "https://your-okta-domain.okta.com/oauth2/v1/authorize",
"TokenEndpoint": "https://your-okta-domain.okta.com/oauth2/v1/token",
"UserInfoEndpoint": "https://your-okta-domain.okta.com/oauth2/v1/userinfo",
"ClientId": "your-client-id",
"ClientSecret": "your-client-secret",
"SessionCookieName": "AWSELBAuthSessionCookie",
"Scope": "openid email profile",
"SessionTimeout": 3600,
"OnUnauthenticatedRequest": "authenticate"
}
},
{
"Type": "forward",
"Order": 2,
"TargetGroupArn": "'$TARGET_GROUP_ARN'"
}
]'
|
以 Azure AD 為例
1
2
3
4
5
| # Azure AD OIDC 設定
# Issuer: https://login.microsoftonline.com/{tenant-id}/v2.0
# Authorization Endpoint: https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize
# Token Endpoint: https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token
# User Info Endpoint: https://graph.microsoft.com/oidc/userinfo
|
4. JWT Token 驗證與傳遞
當認證成功後,ALB 會在轉發給後端的請求中加入以下 Headers:
| Header | 說明 |
|---|
x-amzn-oidc-accesstoken | Access Token(Base64 編碼) |
x-amzn-oidc-identity | 使用者識別(通常是 sub claim) |
x-amzn-oidc-data | JWT 格式的使用者資訊(已簽名) |
驗證 x-amzn-oidc-data Token
x-amzn-oidc-data 是一個由 ALB 簽署的 JWT Token,後端應用程式應該驗證其簽名以確保資料的完整性。
Python 範例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| import jwt
import requests
import base64
from cryptography.hazmat.primitives import serialization
def get_alb_public_key(region, key_id):
"""從 AWS 取得 ALB 的公鑰"""
url = f"https://public-keys.auth.elb.{region}.amazonaws.com/{key_id}"
response = requests.get(url)
return response.text
def verify_alb_token(token, region):
"""驗證 ALB 簽發的 JWT Token"""
# 解析 JWT Header 取得 Key ID
header = jwt.get_unverified_header(token)
key_id = header['kid']
# 取得公鑰
public_key_pem = get_alb_public_key(region, key_id)
# 驗證並解碼 Token
try:
payload = jwt.decode(
token,
public_key_pem,
algorithms=['ES256'],
options={
'verify_exp': True,
'verify_iss': False # ALB Token 沒有標準 issuer
}
)
return payload
except jwt.ExpiredSignatureError:
raise Exception("Token has expired")
except jwt.InvalidTokenError as e:
raise Exception(f"Invalid token: {e}")
# 使用範例
from flask import Flask, request
app = Flask(__name__)
@app.route('/api/user')
def get_user():
oidc_data = request.headers.get('x-amzn-oidc-data')
if oidc_data:
user_info = verify_alb_token(oidc_data, 'us-east-1')
return {
'email': user_info.get('email'),
'sub': user_info.get('sub'),
'name': user_info.get('name')
}
return {'error': 'No authentication data'}, 401
|
Node.js 範例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| const jwt = require('jsonwebtoken');
const axios = require('axios');
async function getALBPublicKey(region, keyId) {
const url = `https://public-keys.auth.elb.${region}.amazonaws.com/${keyId}`;
const response = await axios.get(url);
return response.data;
}
async function verifyALBToken(token, region) {
// 解析 JWT Header 取得 Key ID
const decoded = jwt.decode(token, { complete: true });
const keyId = decoded.header.kid;
// 取得公鑰
const publicKey = await getALBPublicKey(region, keyId);
// 驗證並解碼 Token
return new Promise((resolve, reject) => {
jwt.verify(token, publicKey, { algorithms: ['ES256'] }, (err, payload) => {
if (err) {
reject(err);
} else {
resolve(payload);
}
});
});
}
// Express.js 中介軟體範例
async function authMiddleware(req, res, next) {
const oidcData = req.headers['x-amzn-oidc-data'];
if (!oidcData) {
return res.status(401).json({ error: 'No authentication data' });
}
try {
req.user = await verifyALBToken(oidcData, 'us-east-1');
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
}
module.exports = { authMiddleware };
|
Go 範例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
| package auth
import (
"crypto/ecdsa"
"encoding/pem"
"fmt"
"io/ioutil"
"net/http"
"github.com/golang-jwt/jwt/v5"
)
func GetALBPublicKey(region, keyID string) (*ecdsa.PublicKey, error) {
url := fmt.Sprintf("https://public-keys.auth.elb.%s.amazonaws.com/%s", region, keyID)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
keyPEM, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
block, _ := pem.Decode(keyPEM)
if block == nil {
return nil, fmt.Errorf("failed to parse PEM block")
}
return jwt.ParseECPublicKeyFromPEM(keyPEM)
}
func VerifyALBToken(tokenString, region string) (jwt.MapClaims, error) {
// 解析未驗證的 Token 以取得 Key ID
token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
if err != nil {
return nil, err
}
keyID := token.Header["kid"].(string)
// 取得公鑰
publicKey, err := GetALBPublicKey(region, keyID)
if err != nil {
return nil, err
}
// 驗證 Token
token, err = jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodECDSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return publicKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}
|
5. Listener Rules 設定
認證行為選項
ALB 提供三種未認證請求的處理行為:
| 選項 | 說明 | 適用場景 |
|---|
authenticate | 重導向至 IdP 進行認證 | 需要強制登入的頁面 |
allow | 允許請求通過(不認證) | 公開 API 或資源 |
deny | 回傳 401 Unauthorized | API 端點(不需重導向) |
混合式認證設定
在實際應用中,您可能需要同時支援已認證和未認證的請求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
| # 規則 1:公開路徑(不需認證)
aws elbv2 create-rule \
--listener-arn $LISTENER_ARN \
--priority 5 \
--conditions '[{
"Field": "path-pattern",
"Values": ["/public/*", "/health", "/api/v1/status"]
}]' \
--actions '[{
"Type": "forward",
"TargetGroupArn": "'$TARGET_GROUP_ARN'"
}]'
# 規則 2:Web 頁面(需認證,重導向登入)
aws elbv2 create-rule \
--listener-arn $LISTENER_ARN \
--priority 10 \
--conditions '[{
"Field": "path-pattern",
"Values": ["/app/*", "/dashboard/*"]
}]' \
--actions '[
{
"Type": "authenticate-cognito",
"Order": 1,
"AuthenticateCognitoConfig": {
"UserPoolArn": "'$USER_POOL_ARN'",
"UserPoolClientId": "'$CLIENT_ID'",
"UserPoolDomain": "'$COGNITO_DOMAIN'",
"OnUnauthenticatedRequest": "authenticate"
}
},
{
"Type": "forward",
"Order": 2,
"TargetGroupArn": "'$TARGET_GROUP_ARN'"
}
]'
# 規則 3:API 端點(需認證,回傳 401)
aws elbv2 create-rule \
--listener-arn $LISTENER_ARN \
--priority 15 \
--conditions '[{
"Field": "path-pattern",
"Values": ["/api/v1/protected/*"]
}]' \
--actions '[
{
"Type": "authenticate-cognito",
"Order": 1,
"AuthenticateCognitoConfig": {
"UserPoolArn": "'$USER_POOL_ARN'",
"UserPoolClientId": "'$CLIENT_ID'",
"UserPoolDomain": "'$COGNITO_DOMAIN'",
"OnUnauthenticatedRequest": "deny"
}
},
{
"Type": "forward",
"Order": 2,
"TargetGroupArn": "'$TARGET_GROUP_ARN'"
}
]'
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| # 對於帶有特定 Header 的請求跳過認證(例如:內部服務呼叫)
aws elbv2 create-rule \
--listener-arn $LISTENER_ARN \
--priority 1 \
--conditions '[
{
"Field": "path-pattern",
"Values": ["/api/*"]
},
{
"Field": "http-header",
"HttpHeaderConfig": {
"HttpHeaderName": "X-Internal-Request",
"Values": ["true"]
}
}
]' \
--actions '[{
"Type": "forward",
"TargetGroupArn": "'$INTERNAL_TARGET_GROUP_ARN'"
}]'
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
| # variables.tf
variable "environment" {
description = "Environment name"
type = string
default = "production"
}
variable "domain_name" {
description = "Domain name for the application"
type = string
}
variable "vpc_id" {
description = "VPC ID"
type = string
}
variable "public_subnet_ids" {
description = "List of public subnet IDs"
type = list(string)
}
variable "certificate_arn" {
description = "ACM Certificate ARN"
type = string
}
# cognito.tf
resource "aws_cognito_user_pool" "main" {
name = "${var.environment}-user-pool"
# 密碼策略
password_policy {
minimum_length = 12
require_lowercase = true
require_numbers = true
require_symbols = true
require_uppercase = true
temporary_password_validity_days = 7
}
# MFA 設定
mfa_configuration = "OPTIONAL"
software_token_mfa_configuration {
enabled = true
}
# 使用者屬性
schema {
name = "email"
attribute_data_type = "String"
mutable = true
required = true
developer_only_attribute = false
string_attribute_constraints {
min_length = 5
max_length = 256
}
}
# 自動驗證設定
auto_verified_attributes = ["email"]
username_attributes = ["email"]
# 帳號復原設定
account_recovery_setting {
recovery_mechanism {
name = "verified_email"
priority = 1
}
}
# Email 設定
email_configuration {
email_sending_account = "COGNITO_DEFAULT"
}
tags = {
Environment = var.environment
}
}
resource "aws_cognito_user_pool_domain" "main" {
domain = "${var.environment}-auth-domain"
user_pool_id = aws_cognito_user_pool.main.id
}
resource "aws_cognito_user_pool_client" "alb_client" {
name = "${var.environment}-alb-client"
user_pool_id = aws_cognito_user_pool.main.id
generate_secret = true
# OAuth 設定
allowed_oauth_flows = ["code"]
allowed_oauth_flows_user_pool_client = true
allowed_oauth_scopes = ["openid", "email", "profile"]
supported_identity_providers = ["COGNITO"]
# Callback URLs
callback_urls = [
"https://${var.domain_name}/oauth2/idpresponse"
]
logout_urls = [
"https://${var.domain_name}/logout"
]
# Token 有效期設定
access_token_validity = 1 # 小時
id_token_validity = 1 # 小時
refresh_token_validity = 30 # 天
token_validity_units {
access_token = "hours"
id_token = "hours"
refresh_token = "days"
}
# 防止 Token 撤銷
enable_token_revocation = true
# 防止使用者存在錯誤
prevent_user_existence_errors = "ENABLED"
}
# alb.tf
resource "aws_lb" "main" {
name = "${var.environment}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = var.public_subnet_ids
enable_deletion_protection = true
enable_http2 = true
access_logs {
bucket = aws_s3_bucket.alb_logs.id
prefix = "alb-logs"
enabled = true
}
tags = {
Environment = var.environment
}
}
resource "aws_security_group" "alb" {
name = "${var.environment}-alb-sg"
description = "Security group for ALB"
vpc_id = var.vpc_id
ingress {
description = "HTTPS from anywhere"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTP from anywhere (redirect to HTTPS)"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.environment}-alb-sg"
}
}
resource "aws_lb_target_group" "app" {
name = "${var.environment}-app-tg"
port = 8080
protocol = "HTTP"
vpc_id = var.vpc_id
target_type = "ip"
health_check {
enabled = true
healthy_threshold = 2
interval = 30
matcher = "200"
path = "/health"
port = "traffic-port"
protocol = "HTTP"
timeout = 5
unhealthy_threshold = 3
}
tags = {
Environment = var.environment
}
}
# HTTPS Listener
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.main.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = var.certificate_arn
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "Not Found"
status_code = "404"
}
}
}
# HTTP to HTTPS Redirect
resource "aws_lb_listener" "http_redirect" {
load_balancer_arn = aws_lb.main.arn
port = "80"
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
# listener_rules.tf
# 公開路徑規則
resource "aws_lb_listener_rule" "public" {
listener_arn = aws_lb_listener.https.arn
priority = 10
action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
condition {
path_pattern {
values = ["/public/*", "/health", "/api/v1/status"]
}
}
}
# 需要認證的 Web 頁面規則
resource "aws_lb_listener_rule" "authenticated_web" {
listener_arn = aws_lb_listener.https.arn
priority = 20
action {
type = "authenticate-cognito"
authenticate_cognito {
user_pool_arn = aws_cognito_user_pool.main.arn
user_pool_client_id = aws_cognito_user_pool_client.alb_client.id
user_pool_domain = aws_cognito_user_pool_domain.main.domain
session_cookie_name = "AWSELBAuthSessionCookie"
session_timeout = 604800 # 7 天
scope = "openid email profile"
on_unauthenticated_request = "authenticate"
}
}
action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
condition {
path_pattern {
values = ["/app/*", "/dashboard/*", "/settings/*"]
}
}
}
# 需要認證的 API 規則(回傳 401)
resource "aws_lb_listener_rule" "authenticated_api" {
listener_arn = aws_lb_listener.https.arn
priority = 30
action {
type = "authenticate-cognito"
authenticate_cognito {
user_pool_arn = aws_cognito_user_pool.main.arn
user_pool_client_id = aws_cognito_user_pool_client.alb_client.id
user_pool_domain = aws_cognito_user_pool_domain.main.domain
session_cookie_name = "AWSELBAuthSessionCookie"
session_timeout = 3600 # 1 小時
scope = "openid email profile"
on_unauthenticated_request = "deny"
}
}
action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
condition {
path_pattern {
values = ["/api/v1/*"]
}
}
}
# outputs.tf
output "alb_dns_name" {
description = "ALB DNS name"
value = aws_lb.main.dns_name
}
output "cognito_user_pool_id" {
description = "Cognito User Pool ID"
value = aws_cognito_user_pool.main.id
}
output "cognito_app_client_id" {
description = "Cognito App Client ID"
value = aws_cognito_user_pool_client.alb_client.id
}
output "cognito_domain" {
description = "Cognito Domain"
value = "https://${aws_cognito_user_pool_domain.main.domain}.auth.${data.aws_region.current.name}.amazoncognito.com"
}
data "aws_region" "current" {}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| # 使用 OIDC Provider(如 Okta)的認證規則
resource "aws_lb_listener_rule" "oidc_authenticated" {
listener_arn = aws_lb_listener.https.arn
priority = 25
action {
type = "authenticate-oidc"
authenticate_oidc {
authorization_endpoint = "https://your-okta-domain.okta.com/oauth2/v1/authorize"
client_id = var.okta_client_id
client_secret = var.okta_client_secret
issuer = "https://your-okta-domain.okta.com"
token_endpoint = "https://your-okta-domain.okta.com/oauth2/v1/token"
user_info_endpoint = "https://your-okta-domain.okta.com/oauth2/v1/userinfo"
session_cookie_name = "AWSELBAuthSessionCookie"
session_timeout = 3600
scope = "openid email profile"
on_unauthenticated_request = "authenticate"
}
}
action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
condition {
path_pattern {
values = ["/oidc/*"]
}
}
}
# 使用 AWS Secrets Manager 儲存 OIDC 憑證
resource "aws_secretsmanager_secret" "oidc_credentials" {
name = "${var.environment}/oidc/credentials"
tags = {
Environment = var.environment
}
}
resource "aws_secretsmanager_secret_version" "oidc_credentials" {
secret_id = aws_secretsmanager_secret.oidc_credentials.id
secret_string = jsonencode({
client_id = var.okta_client_id
client_secret = var.okta_client_secret
})
}
|
7. 安全性最佳實務
1. 使用 HTTPS
務必使用 HTTPS,避免認證 Token 在傳輸過程中被攔截。
1
2
3
4
5
6
| # 強制使用 TLS 1.2 或更高版本
resource "aws_lb_listener" "https" {
# ...
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
# ...
}
|
2. Cookie 安全設定
ALB 的認證 Cookie 預設使用 Secure 和 HttpOnly 屬性,但您應該確保:
- 使用足夠隨機的 Session Cookie 名稱
- 設定適當的 Session Timeout
1
2
| # 設定較短的 Session Timeout(例如:1 小時)
"SessionTimeout": 3600
|
3. 限制 Callback URL
在 Cognito 或 OIDC Provider 中,僅允許您控制的 Callback URL:
1
2
3
4
5
6
7
8
| resource "aws_cognito_user_pool_client" "alb_client" {
# ...
callback_urls = [
"https://app.example.com/oauth2/idpresponse"
# 不要使用萬用字元如 https://*.example.com
]
# ...
}
|
4. 驗證 JWT Token
永遠在後端驗證 x-amzn-oidc-data Token 的簽名:
1
2
3
4
5
6
7
8
| # 不要直接信任 Header 內容
# 錯誤做法
user_email = request.headers.get('x-amzn-oidc-identity') # ❌ 不安全
# 正確做法
oidc_data = request.headers.get('x-amzn-oidc-data')
verified_payload = verify_alb_token(oidc_data, region) # ✅ 驗證後使用
user_email = verified_payload.get('email')
|
5. 實施最小權限原則
為 ALB 和 Cognito 設定最小必要的 IAM 權限:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| # ALB 的 IAM Policy(如需存取 Secrets Manager)
resource "aws_iam_role_policy" "alb_secrets_access" {
name = "alb-secrets-access"
role = aws_iam_role.alb_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"secretsmanager:GetSecretValue"
]
Resource = [
aws_secretsmanager_secret.oidc_credentials.arn
]
}
]
})
}
|
6. 啟用 Access Logs
記錄所有 ALB 流量以供審計:
1
2
3
4
5
6
7
8
9
| resource "aws_lb" "main" {
# ...
access_logs {
bucket = aws_s3_bucket.alb_logs.id
prefix = "alb-logs"
enabled = true
}
# ...
}
|
7. 設定 WAF 規則
使用 AWS WAF 保護 ALB:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| resource "aws_wafv2_web_acl_association" "alb" {
resource_arn = aws_lb.main.arn
web_acl_arn = aws_wafv2_web_acl.main.arn
}
resource "aws_wafv2_web_acl" "main" {
name = "${var.environment}-waf"
description = "WAF for ALB"
scope = "REGIONAL"
default_action {
allow {}
}
# 防止 SQL Injection
rule {
name = "AWSManagedRulesSQLiRuleSet"
priority = 1
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesSQLiRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "SQLiRuleSet"
sampled_requests_enabled = true
}
}
# Rate Limiting
rule {
name = "RateLimitRule"
priority = 2
action {
block {}
}
statement {
rate_based_statement {
limit = 2000
aggregate_key_type = "IP"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "RateLimitRule"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "WAFWebACL"
sampled_requests_enabled = true
}
}
|
8. 定期輪換 Client Secret
1
2
3
4
5
6
7
8
| # 建立新的 Client Secret
aws cognito-idp update-user-pool-client \
--user-pool-id $USER_POOL_ID \
--client-id $CLIENT_ID \
--generate-secret
# 更新 ALB Listener Rule 使用新的 Secret
# (需要更新 Terraform 或 CLI 設定)
|
8. 故障排除指南
常見錯誤與解決方案
1. 認證重導向迴圈
症狀:使用者持續被重導向至 IdP,無法完成登入。
可能原因與解決方案:
1
2
3
4
5
6
7
| # 檢查 Callback URL 是否正確
# Cognito App Client 的 Callback URL 必須包含:
# https://your-alb-domain.com/oauth2/idpresponse
# 確認 ALB 的 DNS 名稱與 Callback URL 相符
aws elbv2 describe-load-balancers --names my-alb \
--query 'LoadBalancers[0].DNSName'
|
2. 401 Unauthorized 錯誤
症狀:API 請求收到 401 錯誤。
檢查項目:
1
2
3
4
5
6
7
8
| # 1. 確認 Session Cookie 是否存在
curl -v https://your-domain.com/api/v1/resource
# 2. 檢查 Cookie 過期時間
# SessionTimeout 設定是否合理
# 3. 確認 OIDC Scope 是否正確
# 必須包含 "openid"
|
3. Token 驗證失敗
症狀:後端無法驗證 x-amzn-oidc-data Token。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| # 除錯腳本
import jwt
import base64
import json
def debug_alb_token(token):
# 解碼 JWT(不驗證)
parts = token.split('.')
# Header
header = json.loads(base64.urlsafe_b64decode(parts[0] + '=='))
print(f"Header: {json.dumps(header, indent=2)}")
# Payload
payload = json.loads(base64.urlsafe_b64decode(parts[1] + '=='))
print(f"Payload: {json.dumps(payload, indent=2)}")
# 檢查 Key ID
print(f"Key ID: {header.get('kid')}")
# 檢查過期時間
import datetime
exp = payload.get('exp')
if exp:
exp_time = datetime.datetime.fromtimestamp(exp)
print(f"Expires: {exp_time}")
print(f"Expired: {datetime.datetime.now() > exp_time}")
# 使用
debug_alb_token(request.headers.get('x-amzn-oidc-data'))
|
4. CORS 問題
症狀:前端 JavaScript 無法存取認證後的 API。
解決方案:在後端設定適當的 CORS Headers:
1
2
3
4
5
6
7
| # Flask 範例
from flask_cors import CORS
app = Flask(__name__)
CORS(app, supports_credentials=True, origins=[
"https://your-frontend-domain.com"
])
|
5. IdP 連線問題
症狀:ALB 無法與 OIDC Provider 通訊。
1
2
3
4
5
6
| # 檢查 ALB 的 Security Group 是否允許 outbound 443
aws ec2 describe-security-groups --group-ids $ALB_SG_ID \
--query 'SecurityGroups[0].IpPermissionsEgress'
# 確認 VPC 有 NAT Gateway 或 Internet Gateway
# ALB 需要能夠存取 OIDC Provider 的端點
|
CloudWatch Logs 除錯
啟用 ALB Access Logs 來追蹤認證流程:
1
2
3
4
5
6
7
8
| # 分析認證相關的請求
aws s3 cp s3://your-log-bucket/alb-logs/ ./logs --recursive
# 搜尋認證錯誤
zcat logs/*.gz | grep -E "(401|302)" | head -20
# 檢查認證重導向
zcat logs/*.gz | grep "oauth2/idpresponse"
|
使用 AWS CLI 檢查設定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # 列出所有 Listener Rules
aws elbv2 describe-rules \
--listener-arn $LISTENER_ARN \
--query 'Rules[*].{Priority:Priority,Conditions:Conditions,Actions:Actions}' \
--output table
# 檢查 Cognito User Pool 設定
aws cognito-idp describe-user-pool \
--user-pool-id $USER_POOL_ID
# 檢查 App Client 設定
aws cognito-idp describe-user-pool-client \
--user-pool-id $USER_POOL_ID \
--client-id $CLIENT_ID
|
測試認證流程
1
2
3
4
5
6
7
8
9
| # 使用 curl 測試認證流程
# 1. 未認證請求(應回傳 302 重導向)
curl -v -c cookies.txt https://your-domain.com/protected/resource
# 2. 手動完成認證後,使用 Cookie 再次請求
curl -v -b cookies.txt https://your-domain.com/protected/resource
# 3. 檢查 Headers
curl -v -b cookies.txt https://your-domain.com/api/debug-headers
|
總結
AWS ALB 的內建認證功能提供了一個強大且靈活的方式來保護您的應用程式。透過整合 Amazon Cognito 或第三方 OIDC Provider,您可以:
- 簡化開發:將認證邏輯從應用程式中抽離
- 提升安全性:在網路邊緣攔截未認證流量
- 支援多種認證方式:輕鬆整合企業 SSO 解決方案
- 靈活配置:根據路徑和條件設定不同的認證策略
在實作時,請務必遵循安全性最佳實務,包括使用 HTTPS、驗證 JWT Token、限制 Callback URL、以及定期審計存取日誌。
如有任何問題,可以參考 AWS 官方文件 或在 AWS 論壇尋求協助。
參考資源