SJ blog
security
S

信頼度ランク

S 公式ソース確認済み
A 成功実績多数・失敗例少数
B 賛否両論
C 動作未確認・セキュリティリスク高
Z 個人所感

OAuth 2.0とOpenID Connectを正しく理解する

「ログインしてください」の裏で何が起きているか。OAuth 2.0の認可フロー・スコープ・トークンの種類と、認証のためのOpenID Connectの仕組みをシーケンス図と実装例で解説します。

一言結論

OAuth 2.0は「認可」、OpenID Connectはその上に載せた「認証」であり、この区別を理解してPKCEとstateパラメータを正しく実装することが、ソーシャルログインを安全に構築する唯一の道だ。

OAuth 2.0 と OpenID Connect の違い

OAuth 2.0      → 認可(Authorization)
                 「このアプリにXXXへのアクセスを許可しますか?」

OpenID Connect → 認証(Authentication)をOAuth 2.0の上に構築
                 「誰がログインしているか」を標準的に伝える

OpenID Connect = OAuth 2.0 + Identity Layer(ID トークン)

OAuth 2.0 の登場人物

Resource Owner(ユーザー)   → アクセスを許可する人
Client(アプリ)             → アクセスしたいアプリ
Authorization Server         → 認可を行うサーバー(Google, GitHub等)
Resource Server              → 保護されたリソースを持つサーバー(Gmail API等)

認可コードフロー(最も安全)

1. ユーザーが「GitHubでログイン」クリック

2. アプリ → GitHub の認可エンドポイントにリダイレクト
   https://github.com/login/oauth/authorize
   ?client_id=xxx&redirect_uri=https://myapp.com/callback
   &scope=user:email&state=random_string&response_type=code

3. ユーザーが GitHub でログイン・許可

4. GitHub → アプリのコールバックにリダイレクト
   https://myapp.com/callback?code=AUTH_CODE&state=random_string

5. アプリ → Authorization Server にコードを送る(サーバー側で)
   POST https://github.com/login/oauth/access_token
   { client_id, client_secret, code }

6. Authorization Server → Access Token を返す
   { access_token, token_type, scope }

7. アプリ → Access Token で API を呼ぶ
   GET https://api.github.com/user
   Authorization: Bearer ACCESS_TOKEN

PKCE — パブリッククライアントのセキュリティ強化

SPAやモバイルアプリは client_secret を安全に保管できないため、PKCE を使います。

// code_verifier を生成(ランダムな文字列)
const codeVerifier = crypto.randomBytes(32).toString("base64url");

// code_challenge = SHA256(code_verifier)
const codeChallenge = crypto
  .createHash("sha256")
  .update(codeVerifier)
  .digest("base64url");

// 認可リクエストに追加
const params = new URLSearchParams({
  response_type: "code",
  client_id: CLIENT_ID,
  redirect_uri: REDIRECT_URI,
  code_challenge: codeChallenge,
  code_challenge_method: "S256",
  state: generateState(),
});

window.location.href = `https://auth.example.com/authorize?${params}`;

// トークン交換時に code_verifier を送る
const response = await fetch("https://auth.example.com/token", {
  method: "POST",
  body: new URLSearchParams({
    grant_type: "authorization_code",
    code: authCode,
    redirect_uri: REDIRECT_URI,
    code_verifier: codeVerifier, // ← サーバーが SHA256 して一致確認
  }),
});

OpenID Connect の ID トークン

// ID トークン(JWT)のペイロード
{
  "iss": "https://accounts.google.com",  // 発行者
  "sub": "1234567890",                   // ユーザーの一意ID
  "aud": "my-client-id",                 // 対象のクライアント
  "exp": 1712534400,                     // 有効期限
  "iat": 1712530800,                     // 発行時刻
  "email": "user@example.com",           // OpenID Connect クレーム
  "email_verified": true,
  "name": "Alice Smith"
}
// ID トークンの検証
import * as jose from "jose";

const JWKS = jose.createRemoteJWKSet(
  new URL("https://accounts.google.com/.well-known/openid-configuration")
);

const { payload } = await jose.jwtVerify(idToken, JWKS, {
  issuer: "https://accounts.google.com",
  audience: CLIENT_ID,
});

const userId = payload.sub; // ユーザーの一意ID
const email = payload.email;

よくある実装ミス

// ❌ state パラメータを検証しない(CSRF 攻撃に脆弱)
app.get("/callback", async (req, res) => {
  const { code } = req.query;
  const token = await exchangeCode(code);
});

// ✅ state を検証する
app.get("/callback", async (req, res) => {
  const { code, state } = req.query;

  // セッションに保存した state と一致するか確認
  if (state !== req.session.oauthState) {
    return res.status(400).json({ error: "Invalid state" });
  }

  delete req.session.oauthState;
  const token = await exchangeCode(code);
});

スコープの設計

最小権限の原則を適用する

❌ scope=*
✅ scope=openid email profile  # 必要最小限のみ

よくあるスコープ:
  openid    → OpenID Connect を使うための必須スコープ
  email     → メールアドレス
  profile   → 名前・アバター
  offline_access → Refresh Token を発行する

まとめ

用語意味
OAuth 2.0リソースへのアクセスの認可フレームワーク
OpenID ConnectOAuth 2.0 上の認証レイヤー
Authorization Code短命のコード(一度しか使えない)
Access TokenAPI アクセスに使うトークン(短命)
Refresh TokenAccess Token を更新するトークン(長命)
PKCESPAとモバイルの認可コードフローのセキュリティ強化

参考: OAuth 2.0 RFC 6749 / OpenID Connect 公式