🔐

OAuth 2.0とOpenID Connectの仕組み - 認証・認可の設計原則

18分 で読める | 2025.12.02

現代のWebアプリケーションやモバイルアプリでは、「Googleでログイン」「GitHubでログイン」といったソーシャルログインが当たり前になっています。この背後で動いているのが OAuth 2.0OpenID Connect(OIDC) という2つの標準仕様です。本記事では、これらの仕組みを基礎から応用まで詳しく解説します。

OAuth 2.0とOIDCの違い

まず、よく混同されるOAuth 2.0とOIDCの違いを明確にしましょう。

項目OAuth 2.0OpenID Connect
目的認可(Authorization)認証(Authentication)
答える質問「このアプリにリソースへのアクセスを許可しますか?」「あなたは誰ですか?」
取得するものアクセストークンIDトークン + アクセストークン
策定年2012年(RFC 6749)2014年

認証と認可の違い:

  • 認証(Authentication): ユーザーが「誰であるか」を確認するプロセス
  • 認可(Authorization): ユーザーが「何にアクセスできるか」を決定するプロセス

OAuth 2.0の登場人物

OAuth 2.0には4つの主要な役割(ロール)が存在します。

flowchart TB
    RO["Resource Owner<br/>(リソースオーナー/ユーザー)<br/>データの所有者。通常はエンドユーザー。"]
    AS["Authorization Server<br/>(認可サーバー)<br/>トークンを発行。Google, GitHub等が該当。"]
    Client["Client<br/>(クライアント)<br/>リソースにアクセスしたいアプリケーション。"]
    RS["Resource Server<br/>(リソースサーバー)<br/>保護されたリソースをホスト。API等。"]

    RO -->|認可を与える| AS
    AS -->|トークン発行| Client
    Client -->|APIリクエスト| RS

OAuth 2.0のグラントタイプ

OAuth 2.0には、異なるユースケースに対応する複数の「グラントタイプ」(フロー)が定義されています。

1. Authorization Code Grant(認可コードグラント)

最も安全で推奨されるフローです。サーバーサイドアプリケーションで使用します。

sequenceDiagram
    participant U as User
    participant C as Client
    participant AS as Auth Server
    participant RS as Resource Server

    U->>C: 1. ログイン開始
    C->>AS: 2. 認可リクエスト
    AS->>U: 3. ログイン画面・同意画面
    U->>AS: 4. 認証・同意
    AS->>C: 5. 認可コード
    C->>AS: 6. トークン交換
    AS->>C: 7. アクセストークン
    C->>RS: 8. API呼び出し
    RS->>C: 9. リソース
// Step 2: 認可リクエストURL生成
const authorizationUrl = new URL('https://auth.example.com/authorize');
authorizationUrl.searchParams.set('response_type', 'code');
authorizationUrl.searchParams.set('client_id', CLIENT_ID);
authorizationUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authorizationUrl.searchParams.set('scope', 'openid profile email');
authorizationUrl.searchParams.set('state', generateRandomState());

// Step 6: トークン交換
const tokenResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: authorizationCode,
    redirect_uri: REDIRECT_URI,
    client_id: CLIENT_ID,
    client_secret: CLIENT_SECRET,
  }),
});

2. Authorization Code Grant with PKCE

SPAやモバイルアプリなど、クライアントシークレットを安全に保管できない環境向けの拡張です。

// PKCE: Proof Key for Code Exchange
import crypto from 'crypto';

// Step 1: Code Verifierの生成(43-128文字のランダム文字列)
const codeVerifier = crypto.randomBytes(32)
  .toString('base64url');

// Step 2: Code Challengeの生成
const codeChallenge = crypto
  .createHash('sha256')
  .update(codeVerifier)
  .digest('base64url');

// Step 3: 認可リクエストにcode_challengeを追加
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('scope', 'openid profile');
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('state', generateState());

// Step 4: トークン交換時にcode_verifierを送信
const tokenResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: authorizationCode,
    redirect_uri: REDIRECT_URI,
    client_id: CLIENT_ID,
    code_verifier: codeVerifier,  // シークレットの代わりにverifierを送信
  }),
});

3. Client Credentials Grant

サーバー間通信(M2M: Machine to Machine)で使用します。ユーザーが介在しません。

// バックエンドサービス間の認証
const tokenResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'Authorization': `Basic ${Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')}`,
  },
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    scope: 'api:read api:write',
  }),
});

const { access_token } = await tokenResponse.json();

4. Refresh Token Grant

アクセストークンを更新するためのフローです。

// リフレッシュトークンによるアクセストークン更新
const refreshResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'refresh_token',
    refresh_token: storedRefreshToken,
    client_id: CLIENT_ID,
  }),
});

const { access_token, refresh_token } = await refreshResponse.json();
// 新しいリフレッシュトークンが返される場合は保存を更新

OpenID Connect(OIDC)

OIDCはOAuth 2.0の上に構築された認証レイヤーです。

IDトークンの構造

OIDCでは、アクセストークンに加えて IDトークン(JWT形式) が発行されます。

flowchart TB
    subgraph JWT["IDトークン(JWT)の構造"]
        subgraph Header["Header(ヘッダー)"]
            H1["alg: RS256, typ: JWT, kid: key-id-123"]
        end

        subgraph Payload["Payload(ペイロード)"]
            P1["iss: 発行者"]
            P2["sub: サブジェクト"]
            P3["aud: オーディエンス"]
            P4["exp: 有効期限"]
            P5["iat: 発行日時"]
            P6["nonce: リプレイ攻撃対策"]
            P7["email, name: ユーザー情報"]
        end

        subgraph Signature["Signature(署名)"]
            S1["認可サーバーの秘密鍵で署名"]
        end

        Header --> Payload --> Signature
    end

IDトークンの検証

import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';

// JWKS(JSON Web Key Set)クライアントの設定
const client = jwksClient({
  jwksUri: 'https://auth.example.com/.well-known/jwks.json',
  cache: true,
  rateLimit: true,
});

// 公開鍵を取得する関数
const getKey = (header: jwt.JwtHeader, callback: jwt.SigningKeyCallback) => {
  client.getSigningKey(header.kid, (err, key) => {
    callback(err, key?.getPublicKey());
  });
};

// IDトークンの検証
const verifyIdToken = (idToken: string): Promise<jwt.JwtPayload> => {
  return new Promise((resolve, reject) => {
    jwt.verify(
      idToken,
      getKey,
      {
        algorithms: ['RS256'],
        issuer: 'https://auth.example.com',
        audience: CLIENT_ID,
      },
      (err, decoded) => {
        if (err) reject(err);
        else resolve(decoded as jwt.JwtPayload);
      }
    );
  });
};

UserInfoエンドポイント

アクセストークンを使用して、追加のユーザー情報を取得できます。

const userInfoResponse = await fetch('https://auth.example.com/userinfo', {
  headers: {
    'Authorization': `Bearer ${accessToken}`,
  },
});

const userInfo = await userInfoResponse.json();
// {
//   "sub": "user-123",
//   "name": "田中太郎",
//   "email": "user@example.com",
//   "email_verified": true,
//   "picture": "https://example.com/avatar.jpg"
// }

スコープの設計

OIDCの標準スコープ

スコープ返されるクレーム
openidsub(必須)
profilename, family_name, given_name, picture, etc.
emailemail, email_verified
addressaddress
phonephone_number, phone_number_verified

カスタムスコープの例

// APIアクセス用のカスタムスコープ
const scopes = [
  'openid',           // OIDC必須
  'profile',          // 基本プロフィール
  'email',            // メールアドレス
  'api:read',         // API読み取り権限
  'api:write',        // API書き込み権限
  'admin:users',      // ユーザー管理権限
];

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

1. State パラメータによるCSRF対策

// セッションごとにユニークなstateを生成
const generateState = (): string => {
  const state = crypto.randomBytes(32).toString('hex');
  // セッションに保存
  session.oauthState = state;
  return state;
};

// コールバック時にstateを検証
const validateState = (receivedState: string): boolean => {
  const isValid = receivedState === session.oauthState;
  delete session.oauthState;  // 使用後は削除
  return isValid;
};

2. Nonceによるリプレイ攻撃対策

// IDトークンリクエスト時にnonceを含める
const nonce = crypto.randomBytes(16).toString('hex');
session.oidcNonce = nonce;

authUrl.searchParams.set('nonce', nonce);

// IDトークン検証時にnonceを確認
const decoded = await verifyIdToken(idToken);
if (decoded.nonce !== session.oidcNonce) {
  throw new Error('Invalid nonce');
}

3. トークンの安全な保管

// バックエンド: HTTPOnly Cookieでセッション管理
res.cookie('session_id', sessionId, {
  httpOnly: true,      // JavaScriptからアクセス不可
  secure: true,        // HTTPS必須
  sameSite: 'lax',     // CSRF対策
  maxAge: 3600000,     // 1時間
});

// フロントエンド(SPA): メモリに保持
// LocalStorageは XSS に脆弱なため避ける
class TokenStore {
  private accessToken: string | null = null;

  setToken(token: string) {
    this.accessToken = token;
  }

  getToken() {
    return this.accessToken;
  }
}

4. トークンの有効期限設計

推奨される有効期限:

トークン有効期限
アクセストークン15分〜1時間
リフレッシュトークン7日〜30日(絶対有効期限)
IDトークン5分〜1時間
認可コード10分以内

Discovery Document

OIDCプロバイダーは、設定情報を自動取得できるDiscoveryエンドポイントを提供します。

// Well-known エンドポイントから設定を取得
const discoveryUrl = 'https://auth.example.com/.well-known/openid-configuration';

const config = await fetch(discoveryUrl).then(r => r.json());
// {
//   "issuer": "https://auth.example.com",
//   "authorization_endpoint": "https://auth.example.com/authorize",
//   "token_endpoint": "https://auth.example.com/token",
//   "userinfo_endpoint": "https://auth.example.com/userinfo",
//   "jwks_uri": "https://auth.example.com/.well-known/jwks.json",
//   "scopes_supported": ["openid", "profile", "email"],
//   "response_types_supported": ["code", "token", "id_token"],
//   ...
// }

まとめ

OAuth 2.0とOIDCは、現代のWeb認証・認可の基盤技術です。

OAuth 2.0のポイント

  • 目的: 認可(リソースへのアクセス権限の委譲)
  • 推奨フロー: Authorization Code Grant + PKCE
  • トークン: アクセストークン、リフレッシュトークン

OIDCのポイント

  • 目的: 認証(ユーザーの身元確認)
  • 追加要素: IDトークン(JWT形式)
  • 標準スコープ: openid, profile, email など

セキュリティ必須事項

  1. PKCEの使用(特にSPA/モバイル)
  2. Stateパラメータによるcsrf対策
  3. Nonceによるリプレイ攻撃対策
  4. HTTPOnly Cookieによるトークン保管
  5. 短いアクセストークン有効期限

これらの概念を正しく理解し実装することで、安全で使いやすい認証システムを構築できます。

参考リンク

← 一覧に戻る