ストーリー
佐藤CTOが認証フローの図を広げました。
OAuth 2.0 / OpenID Connect
OAuth 2.0の基本フロー
OAuth 2.0は認可(Authorization)のためのフレームワークです。
Authorization Code Flow(推奨フロー)
┌────────┐ ┌────────────┐ ┌──────────────┐
│ ユーザー │ │ クライアント │ │ 認可サーバー │
│(ブラウザ)│ │(Webアプリ) │ │(Auth0/Okta) │
└────┬───┘ └─────┬──────┘ └──────┬───────┘
│ 1. ログインクリック │ │
│──────────▶│ │
│ │ 2. 認可リクエスト │
│ │────────────────▶│
│ 3. ログインフォーム │ │
│◀─────────────────────────────│
│ 4. ログイン情報入力 │ │
│─────────────────────────────▶│
│ │ 5. Authorization Code │
│ │◀───────────────│
│ │ 6. Code → Token交換 │
│ │────────────────▶│
│ │ 7. Access Token + ID Token│
│ │◀───────────────│
│ 8. レスポンス │ │
│◀──────────│ │
└────────┘ └────────────┘ └──────────────┘
OpenID Connect (OIDC)
OIDCはOAuth 2.0の上に認証(Authentication)を追加したプロトコルです。
| 概念 | OAuth 2.0 | OIDC |
|---|---|---|
| 目的 | 認可(何ができるか) | 認証(誰であるか) |
| トークン | Access Token | Access Token + ID Token |
| ユーザー情報 | なし(標準化されていない) | UserInfo エンドポイント |
| スコープ | カスタム | openid, profile, email |
| 標準化 | フレームワーク | プロトコル |
// OIDC認証フローの実装例
import { Issuer, generators } from 'openid-client';
async function setupOIDC() {
const issuer = await Issuer.discover('https://auth.example.com/.well-known/openid-configuration');
const client = new issuer.Client({
client_id: process.env.OIDC_CLIENT_ID!,
client_secret: process.env.OIDC_CLIENT_SECRET!,
redirect_uris: ['https://app.example.com/callback'],
response_types: ['code'],
});
return client;
}
// ログイン開始
app.get('/auth/login', async (req, res) => {
const client = await setupOIDC();
const codeVerifier = generators.codeVerifier();
const codeChallenge = generators.codeChallenge(codeVerifier);
// PKCEを使用(Authorization Code Flow + PKCE)
const authUrl = client.authorizationUrl({
scope: 'openid profile email',
code_challenge: codeChallenge,
code_challenge_method: 'S256',
state: generators.state(),
nonce: generators.nonce(),
});
req.session.codeVerifier = codeVerifier;
res.redirect(authUrl);
});
// コールバック処理
app.get('/callback', async (req, res) => {
const client = await setupOIDC();
const params = client.callbackParams(req);
const tokenSet = await client.callback(
'https://app.example.com/callback',
params,
{
code_verifier: req.session.codeVerifier,
state: req.query.state as string,
nonce: req.session.nonce,
}
);
// ID Tokenからユーザー情報を取得
const claims = tokenSet.claims();
console.log('User ID:', claims.sub);
console.log('Email:', claims.email);
req.session.accessToken = tokenSet.access_token;
req.session.idToken = tokenSet.id_token;
res.redirect('/dashboard');
});
JWTのセキュリティ設計
JWT(JSON Web Token)の構造
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9. ← Header
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpv. ← Payload
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← Signature
JWTセキュリティのベストプラクティス
| 項目 | 推奨設定 | 理由 |
|---|---|---|
| アルゴリズム | RS256 / ES256 | HS256は鍵共有が必要でリスクが高い |
| 有効期限 | Access Token: 15分 | 窃取された場合の影響を最小化 |
| Refresh Token | 7日(+ローテーション) | 長期間のセッション維持 |
| 格納場所 | HttpOnly + Secure Cookie | XSSでの窃取を防止 |
| 検証 | alg, iss, aud, exp 全て検証 | alg=noneなどの攻撃を防止 |
// JWTの安全な実装
import jwt from 'jsonwebtoken';
import { createPrivateKey, createPublicKey } from 'crypto';
interface TokenPayload {
sub: string;
email: string;
roles: string[];
aud: string;
iss: string;
}
// トークン発行
function issueAccessToken(user: User): string {
const payload: Omit<TokenPayload, 'aud' | 'iss'> = {
sub: user.id,
email: user.email,
roles: user.roles,
};
return jwt.sign(payload, PRIVATE_KEY, {
algorithm: 'RS256', // 非対称暗号を使用
expiresIn: '15m', // 短い有効期限
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
jwtid: generateUUID(), // リプレイ攻撃防止
});
}
// トークン検証(厳格なバリデーション)
function verifyAccessToken(token: string): TokenPayload {
try {
const payload = jwt.verify(token, PUBLIC_KEY, {
algorithms: ['RS256'], // RS256のみ許可(alg=none攻撃を防止)
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
clockTolerance: 30, // 30秒のクロックスキュー許容
}) as TokenPayload;
return payload;
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new AuthError('TOKEN_EXPIRED', 'Token has expired');
}
throw new AuthError('INVALID_TOKEN', 'Token verification failed');
}
}
// Refresh Tokenのローテーション
async function refreshTokens(refreshToken: string): Promise<TokenPair> {
const stored = await tokenStore.findByRefreshToken(refreshToken);
if (!stored || stored.revoked) {
// リフレッシュトークンが再利用された場合 → 全トークンを無効化
if (stored?.revoked) {
await tokenStore.revokeAllByUserId(stored.userId);
securityLogger.alert('REFRESH_TOKEN_REUSE', { userId: stored.userId });
}
throw new AuthError('INVALID_REFRESH_TOKEN');
}
// 古いリフレッシュトークンを無効化
await tokenStore.revoke(refreshToken);
// 新しいトークンペアを発行
const user = await userRepository.findById(stored.userId);
const accessToken = issueAccessToken(user);
const newRefreshToken = generateSecureToken();
await tokenStore.save({
userId: user.id,
refreshToken: newRefreshToken,
expiresAt: addDays(new Date(), 7),
});
return { accessToken, refreshToken: newRefreshToken };
}
アクセス制御モデル:RBAC / ABAC / ReBAC
RBAC(Role-Based Access Control)
ロールに基づくアクセス制御。シンプルで最も広く使われています。
// RBAC実装例
interface Role {
name: string;
permissions: Permission[];
}
interface Permission {
resource: string;
actions: ('create' | 'read' | 'update' | 'delete')[];
}
const roles: Record<string, Role> = {
admin: {
name: 'admin',
permissions: [
{ resource: 'users', actions: ['create', 'read', 'update', 'delete'] },
{ resource: 'orders', actions: ['create', 'read', 'update', 'delete'] },
{ resource: 'products', actions: ['create', 'read', 'update', 'delete'] },
],
},
manager: {
name: 'manager',
permissions: [
{ resource: 'users', actions: ['read'] },
{ resource: 'orders', actions: ['read', 'update'] },
{ resource: 'products', actions: ['create', 'read', 'update'] },
],
},
viewer: {
name: 'viewer',
permissions: [
{ resource: 'orders', actions: ['read'] },
{ resource: 'products', actions: ['read'] },
],
},
};
function checkPermission(userRoles: string[], resource: string, action: string): boolean {
return userRoles.some(roleName => {
const role = roles[roleName];
return role?.permissions.some(
p => p.resource === resource && p.actions.includes(action as any)
);
});
}
ABAC(Attribute-Based Access Control)
属性に基づくきめ細かなアクセス制御です。
// ABAC実装例
interface AccessRequest {
subject: {
id: string;
department: string;
clearanceLevel: number;
location: string;
};
resource: {
type: string;
classification: 'public' | 'internal' | 'confidential' | 'restricted';
owner: string;
department: string;
};
action: string;
environment: {
time: Date;
ipAddress: string;
deviceTrustLevel: string;
};
}
interface Policy {
name: string;
effect: 'allow' | 'deny';
condition: (request: AccessRequest) => boolean;
}
const policies: Policy[] = [
{
name: '同部門の内部データアクセス',
effect: 'allow',
condition: (req) =>
req.subject.department === req.resource.department &&
req.resource.classification === 'internal' &&
['read'].includes(req.action),
},
{
name: '機密データは高クリアランスのみ',
effect: 'allow',
condition: (req) =>
req.resource.classification === 'confidential' &&
req.subject.clearanceLevel >= 3,
},
{
name: '業務時間外の制限データアクセス禁止',
effect: 'deny',
condition: (req) => {
const hour = req.environment.time.getHours();
return req.resource.classification === 'restricted' &&
(hour < 9 || hour > 18);
},
},
{
name: '信頼されていないデバイスからの機密アクセス禁止',
effect: 'deny',
condition: (req) =>
req.environment.deviceTrustLevel === 'untrusted' &&
['confidential', 'restricted'].includes(req.resource.classification),
},
];
function evaluateAccess(request: AccessRequest): boolean {
// Deny優先: 1つでもdenyがあれば拒否
const denyPolicies = policies.filter(p => p.effect === 'deny');
if (denyPolicies.some(p => p.condition(request))) {
return false;
}
// Allow: 1つでもallowがあれば許可
const allowPolicies = policies.filter(p => p.effect === 'allow');
return allowPolicies.some(p => p.condition(request));
}
ReBAC(Relationship-Based Access Control)
リソース間の関係性に基づくアクセス制御です。Google Zanzibar(SpiceDB)で有名です。
// ReBAC概念(SpiceDB / Zanzibar風)
// 定義:document の viewer は document の editor でもある
// 定義:folder の viewer は folder 内の document の viewer でもある
interface Relationship {
resource: { type: string; id: string };
relation: string;
subject: { type: string; id: string; relation?: string };
}
const relationships: Relationship[] = [
// Alice は folder:engineering の owner
{ resource: { type: 'folder', id: 'engineering' }, relation: 'owner', subject: { type: 'user', id: 'alice' } },
// doc:spec は folder:engineering に属する
{ resource: { type: 'document', id: 'spec' }, relation: 'parent', subject: { type: 'folder', id: 'engineering' } },
// Bob は document:spec の editor
{ resource: { type: 'document', id: 'spec' }, relation: 'editor', subject: { type: 'user', id: 'bob' } },
];
// folder の owner は folder 内の document を全て閲覧可能
// document の editor は document を閲覧可能
// → Alice は doc:spec を閲覧可能(folder owner 経由)
// → Bob は doc:spec を閲覧可能(editor 経由)
アクセス制御モデルの使い分け
| モデル | 適用場面 | メリット | デメリット |
|---|---|---|---|
| RBAC | 組織のロールが明確 | シンプル、理解しやすい | 権限爆発(Role Explosion) |
| ABAC | きめ細かな制御が必要 | 柔軟、動的な判定 | ポリシーが複雑になりやすい |
| ReBAC | ドキュメント共有、SNS | 直感的、スケーラブル | 実装が複雑 |
サービス間認証:mTLS
マイクロサービス間の通信には、mutual TLS(mTLS)が推奨されます。
# Istio ServiceMesh での mTLS 設定
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: strict-mtls
namespace: production
spec:
mtls:
mode: STRICT # 全通信でmTLSを強制
---
# AuthorizationPolicy: サービス間のアクセス制御
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: order-service-policy
namespace: production
spec:
selector:
matchLabels:
app: order-service
rules:
- from:
- source:
principals:
- "cluster.local/ns/production/sa/api-gateway"
- "cluster.local/ns/production/sa/payment-service"
to:
- operation:
methods: ["GET", "POST"]
paths: ["/api/orders/*"]
まとめ
| ポイント | 内容 |
|---|---|
| OAuth 2.0 / OIDC | 認可にはOAuth 2.0、認証にはOIDCを使用 |
| JWTセキュリティ | RS256、短い有効期限、HttpOnly Cookie、厳格な検証 |
| RBAC | ロールベースのアクセス制御。シンプルだが権限爆発のリスク |
| ABAC | 属性ベースの動的アクセス制御。きめ細かいがポリシーが複雑 |
| ReBAC | 関係性ベース。Zanzibar/SpiceDB。直感的だが実装が複雑 |
| mTLS | サービス間の相互認証。Istio等のサービスメッシュで実装 |
チェックリスト
- OAuth 2.0のAuthorization Code Flowを説明できる
- OIDCとOAuth 2.0の違いを理解した
- JWTのセキュリティベストプラクティスを実装できる
- RBAC / ABAC / ReBAC の使い分けを判断できる
- mTLSの仕組みとサービスメッシュでの設定を理解した
次のステップへ
次は「ネットワークセキュリティ」を学びます。マイクロセグメンテーション、Kubernetesネットワークポリシー、WAF、VPC設計など、ネットワーク層のゼロトラスト実装を身につけましょう。
推定読了時間: 40分