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 Connect | OAuth 2.0 上の認証レイヤー |
| Authorization Code | 短命のコード(一度しか使えない) |
| Access Token | API アクセスに使うトークン(短命) |
| Refresh Token | Access Token を更新するトークン(長命) |
| PKCE | SPAとモバイルの認可コードフローのセキュリティ強化 |