LESSON 40分

ストーリー

佐藤CTO
ゼロトラストの核心はアイデンティティ

佐藤CTOが認証フローの図を広げました。

佐藤CTO
誰が、何を、どの条件で、何にアクセスできるか。これを正確に制御できなければ、ゼロトラストは絵に描いた餅だ
あなた
OAuth 2.0やJWTは使っていますが、それだけでは不十分ですか?
佐藤CTO
OAuth 2.0は認可のフレームワークだ。認証にはOpenID Connect。サービス間にはmTLS。そしてアクセス制御にはRBAC、ABAC、ReBAC…。それぞれの特性を理解して、正しく組み合わせよう

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.0OIDC
目的認可(何ができるか)認証(誰であるか)
トークンAccess TokenAccess 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 / ES256HS256は鍵共有が必要でリスクが高い
有効期限Access Token: 15分窃取された場合の影響を最小化
Refresh Token7日(+ローテーション)長期間のセッション維持
格納場所HttpOnly + Secure CookieXSSでの窃取を防止
検証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分