Chuyển đến nội dung chính

レッスン 24: 認証と認可 — OAuth2、JWT、OIDC

OAuth2 フロー、JWT 構造と検証、OpenID Connect、Keycloak/Auth0 による集中認証、マイクロサービスでのトークン伝播、API ゲートウェイ認証統合、RBAC と ABAC。

🏗️ アーキテクチャ — レッスン 24 レッスン 24: 認証と認可 — OAuth2、JWT、OIDC

クラウドネイティブのマイクロサービスアーキテクチャ

パート 8: セキュリティと本番環境の準備

xdev.asia

レッスン 24: 認証と認可 — OAuth2、JWT、OIDC

はじめに

マイクロサービスにおける認証と認可はモノリスよりも複雑です。各サービスは「誰が電話をかけてきたのか?」を知る必要があります。 「彼らにはこれを行う権利があるのか​​?」 — リクエストごとにデータベースをチェックする必要がありません。

解決策: 一元化されたアイデンティティ プロバイダー (IdP) + JWT トークン — API ゲートウェイで 1 回認証すると、トークンはすべてのサービスに渡されます。


1. 認証と認可

Authentication (AuthN): "Bạn là ai?"
  → Verify identity (username/password, API key, certificate)
  → Kết quả: "Đây là user John, email [email protected]"

Authorization (AuthZ): "Bạn có thể làm gì?"
  → Verify permissions (roles, scopes)
  → Kết quả: "John có role 'customer', có thể tạo và xem orders của mình"

2. OAuth2 — 認可フレームワーク

2.1 OAuth2 の役割

Resource Owner: User (người dùng cuối)
Client:         Application yêu cầu access (web app, mobile app)
Authorization Server: Keycloak/Auth0 — cấp tokens
Resource Server: API/Service chứa protected resources

2.2 認可コード フロー (Web アプリ)

Browser                    Backend App             Auth Server (Keycloak)
   │                           │                           │
   │── Click Login ───────────▶│                           │
   │                           │── Redirect ──────────────▶│
   │◀─── Redirect to Login ────────────────────────────────│
   │                           │                           │
   │── Enter credentials ─────────────────────────────────▶│
   │                           │                           │
   │◀─── Redirect with code ───────────────────────────────│
   │                           │                           │
   │── Forward code ──────────▶│                           │
   │                           │── Exchange code for token▶│
   │                           │◀─ access_token + id_token─│
   │                           │                           │
   │◀─── Session created ──────│                           │

2.3 クライアント資格情報フロー (サービス間)

Service A                         Auth Server
   │                                   │
   │── POST /token                     │
   │   client_id + client_secret ─────▶│
   │◀─ access_token (JWT) ─────────────│
   │                                   │
   │── API Call + Bearer token ───────▶ Service B
                                           │
                                           │── Validate JWT (signature + expiry)
                                           │── Check scopes
                                           │
                                           ▼
                                      Process request

2.4 PKCE フロー (モバイル/SPA)

クライアント シークレットなし (ユーザーのデバイスで実行されているコードを秘密にすることはできません):

# 1. Tạo code_verifier (random string)
code_verifier = base64url(random_bytes(32))

# 2. Tạo code_challenge
code_challenge = base64url(sha256(code_verifier))

# 3. Authorization request kèm code_challenge
GET /authorize?
  response_type=code&
  client_id=xxx&
  code_challenge=abc&
  code_challenge_method=S256

# 4. Token exchange kèm code_verifier
POST /token
  code=xxx&code_verifier=original_verifier

3. JWT — JSON Web トークン

3.1 構造

eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyLTAwMSIsInJvbGVzIjpbImN1c3RvbWVyIl19.signature
└─────────────────┘ └──────────────────────────────────────────┘ └───────────┘
      Header                         Payload                       Signature

Header (Base64URL decoded):
{
  "alg": "RS256",    ← Thuật toán ký: RS256 (asymmetric) hoặc HS256 (symmetric)
  "typ": "JWT",
  "kid": "key-2024"  ← Key ID để lookup public key
}

Payload (Base64URL decoded):
{
  "sub": "user-001",              ← Subject (user ID)
  "iss": "https://auth.example.com", ← Issuer
  "aud": "order-service",         ← Audience (intended recipient)
  "exp": 1711900800,              ← Expiration time (Unix timestamp)
  "iat": 1711897200,              ← Issued At
  "jti": "abc-123-xyz",           ← JWT ID (prevent replay)
  "email": "[email protected]",
  "roles": ["customer"],
  "tenant_id": "tenant-001"      ← Custom claims
}

Signature: RS256(base64url(header) + "." + base64url(payload), private_key)

3.2 RS256 と HS256

HS256 (Symmetric):
  - Ký và verify bằng cùng một secret key
  - Tất cả services cần biết secret → nguy hiểm
  - Dùng cho: monolith, internal tools

RS256 (Asymmetric):
  - Auth Server ký bằng private key (giữ bí mật)
  - Services verify bằng public key (có thể public)
  - Services KHÔNG thể forge tokens
  - Dùng cho: microservices, distributed systems

JWK Set (JWKS): Auth Server expose public keys tại /.well-known/jwks.json
Services cache và dùng để verify JWT offline

3.3 JWT の検証

@Component
public class JwtValidator {
    // Cache JWKS (public keys) từ Auth Server
    @Autowired
    private JwkProvider jwkProvider;

    public Claims validate(String token) {
        // Parse header để lấy kid
        DecodedJWT jwt = JWT.decode(token);
        JwkKey key = jwkProvider.get(jwt.getKeyId());

        try {
            // Verify signature + claims
            return JWT.require(Algorithm.RSA256(key.getPublicKey(), null))
                .withIssuer("https://auth.example.com")
                .withAudience("order-service")
                .build()
                .verify(token)
                .getClaims();

        } catch (TokenExpiredException e) {
            throw new UnauthorizedException("Token expired");
        } catch (JWTVerificationException e) {
            throw new UnauthorizedException("Invalid token");
        }
    }
}

4. OpenID Connect (OIDC)

OAuth2 は承認フレームワークです。 OIDC は OAuth2 の ID レイヤーです。

OAuth2 trả về: access_token (opaque, for authorization)
OIDC thêm:     id_token (JWT, contains user identity info)
               /userinfo endpoint

Ví dụ id_token:
{
  "sub": "user-001",
  "name": "John Doe",
  "email": "[email protected]",
  "email_verified": true,
  "picture": "https://...",
  "iss": "https://auth.example.com",
  "aud": "my-app",
  "exp": 1711900800
}

OIDC を使用する場合: ユーザー プロファイル情報が必要な場合 (ログイン、プロファイル ページ)。アクセストークンはAPIにアクセスするために使用されます。


5. Keycloak — セルフホスト型アイデンティティプロバイダー

5.1 Kubernetes へのインストール

helm repo add bitnami https://charts.bitnami.com/bitnami

helm upgrade --install keycloak bitnami/keycloak \
  --namespace platform \
  --set auth.adminPassword=<password> \
  --set replicaCount=2 \
  --set postgresql.enabled=true \
  --set ingress.enabled=true \
  --set ingress.hostname=auth.example.com

5.2 レルム構成

// Realm: myapp
// Client: order-service-api (Resource Server)
{
  "clientId": "order-service-api",
  "protocol": "openid-connect",
  "bearerOnly": true,  // Chỉ accept tokens, không issue
  "defaultClientScopes": ["openid", "profile", "email"],
  "protocolMappers": [
    {
      "name": "tenant-id-mapper",
      "protocol": "openid-connect",
      "protocolMapper": "oidc-usermodel-attribute-mapper",
      "config": {
        "user.attribute": "tenant_id",
        "claim.name": "tenant_id",
        "access.token.claim": "true"
      }
    }
  ]
}

6. マイクロサービスでのトークンの伝播

6.1 API ゲートウェイ — 集中認証

Client ──Bearer token──▶ API Gateway
                              │
                              │ 1. Validate JWT signature
                              │ 2. Check expiry
                              │ 3. Check audience
                              │
                              │ 4. Forward token (or user info headers) → downstream services
                              │
                              ▼
                         Order Service
                              │
                              │ 5. Extract claims từ token/headers
                              │ 6. Apply RBAC logic
                              │
                              ▼
                         Process request

6.2 Kong JWT プラグイン

apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: jwt-auth
plugin: jwt
config:
  key_claim_name: kid
  claims_to_verify:
    - exp
    - nbf
  anonymous: null
  run_on_preflight: true
# Hoặc dùng OIDC plugin
plugin: oidc
config:
  issuer: https://auth.example.com/realms/myapp
  client_id: kong-gateway
  client_secret: xxxx
  scope: openid
  bearer_only: yes

6.3 トークン転送とヘッダー挿入

オプション 1: 元の JWT を転送 (推奨)

# Kong: forward original Authorization header
# Downstream service tự validate

利点: ダウンストリーム サービスには完全なクレームがあり、署名を個別に検証できます。

オプション 2: ヘッダー インジェクション (シンプルですが安全性は低くなります)

# Kong inject parsed claims làm headers
X-Consumer-Username: john
X-Consumer-Groups: customer,admin
X-Tenant-ID: tenant-001

利点: ダウンストリーム サービスには JWT ライブラリが必要ありません 欠点: これらのヘッダーは、誰かが API ゲートウェイをバイパスすると簡単に偽造されてしまいます。


7. 認可パターン

7.1 RBAC — ロールベースのアクセス制御

// Spring Security
@PreAuthorize("hasRole('ADMIN') or (hasRole('CUSTOMER') and #customerId == authentication.name)")
public Order getOrder(String orderId, String customerId) { ... }

// Hoặc Method Security
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
public void deleteOrder(String orderId) { ... }
# Kubernetes RBAC cho service-to-service (service accounts)
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: order-service-role
rules:
  - apiGroups: [""]
    resources: ["configmaps", "secrets"]
    verbs: ["get", "list"]

7.2 ABAC — 属性ベースのアクセス制御

// Sử dụng Spring Security SpEL expression
@PreAuthorize("""
    hasRole('CUSTOMER') and
    @orderService.isOwner(#orderId, authentication.name) and
    T(java.time.LocalDate).now().isBefore(
        @orderService.getOrder(#orderId).createdAt.plusDays(30)
    )
""")
public void cancelOrder(String orderId) { ... }

7.3 OPA (オープン ポリシー エージェント) — コードとしてのポリシー

# order-policy.rego
package order

default allow = false

# Customer có thể xem order của mình
allow {
    input.action == "GET"
    input.user.roles[_] == "customer"
    input.resource.owner_id == input.user.id
}

# Admin có thể xem tất cả
allow {
    input.user.roles[_] == "admin"
    input.action == "GET"
}

# Refund chỉ trong 30 ngày
allow {
    input.action == "REFUND"
    input.user.roles[_] == "customer"
    input.resource.owner_id == input.user.id
    time.diff(input.resource.created_at, time.now_ns()) < 2592000  # 30 ngày
}
// Gọi OPA để evaluate policy
@Service
public class AuthorizationService {
    public boolean isAllowed(String action, User user, Resource resource) {
        OpaRequest request = OpaRequest.builder()
            .input(Map.of(
                "action", action,
                "user", Map.of("id", user.getId(), "roles", user.getRoles()),
                "resource", Map.of("owner_id", resource.getOwnerId(), "created_at", resource.getCreatedAt())
            ))
            .build();

        OpaResponse response = opaClient.evaluate("order/allow", request);
        return response.getResult();
    }
}

8. トークンの更新とセキュリティに関する考慮事項

8.1 アクセストークンとリフレッシュトークン

Access Token:
  - Short-lived: 5-15 phút
  - Dùng để access APIs
  - Stateless (validate offline với public key)
  - Khi bị stolen: expire sớm → damage limited

Refresh Token:
  - Long-lived: 7-30 ngày
  - Dùng để lấy access token mới
  - Stored lên Auth Server (có thể revoke)
  - Khi bị stolen: phải revoke ngay

8.2 セキュリティのベストプラクティス

□ Access token TTL: 5-15 phút (không phải 1 ngày)
□ RS256 không phải HS256 cho distributed systems
□ Validate audience (aud) claim — token không dùng được cho service khác
□ Validate issuer (iss)
□ Store tokens trong httpOnly cookies (không localStorage)
□ Refresh token rotation: mỗi lần refresh → issue token mới + revoke cũ
□ Token không chứa sensitive data (credit card, password)
□ Rate limit authentication endpoints
□ Brute force protection (lockout sau N failed attempts)

概要

コンセプト目的
OAuth2認可フレームワーク (代理アクセス)
OIDCOAuth2 の ID 層 (ユーザーが誰であるか)
JWT (RS256)ステートレストークン、DBなしのオフライン検証
JWKSトークンを検証するためのサービスの公開キー エンドポイント
APIゲートウェイ認証集中認証、ダウンストリームは検証済みのクレームを受け取ります
RBACロールベースのアクセス - シンプル、予測可能
アバック属性ベース — 柔軟、コンテキスト認識
オーパコードとしてのポリシー — 一元化された監査可能な認可

次の記事: シークレット管理とコンテナーのセキュリティ