現代のWebアプリケーションやモバイルアプリでは、「Googleでログイン」「GitHubでログイン」といったソーシャルログインが当たり前になっています。この背後で動いているのが OAuth 2.0 と OpenID Connect(OIDC) という2つの標準仕様です。本記事では、これらの仕組みを基礎から応用まで詳しく解説します。
OAuth 2.0とOIDCの違い
まず、よく混同されるOAuth 2.0とOIDCの違いを明確にしましょう。
| 項目 | OAuth 2.0 | OpenID 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の標準スコープ
| スコープ | 返されるクレーム |
|---|---|
openid | sub(必須) |
profile | name, family_name, given_name, picture, etc. |
email | email, email_verified |
address | address |
phone | phone_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 など
セキュリティ必須事項
- PKCEの使用(特にSPA/モバイル)
- Stateパラメータによるcsrf対策
- Nonceによるリプレイ攻撃対策
- HTTPOnly Cookieによるトークン保管
- 短いアクセストークン有効期限
これらの概念を正しく理解し実装することで、安全で使いやすい認証システムを構築できます。
参考リンク
- RFC 6749 - OAuth 2.0
- RFC 7636 - PKCE
- OpenID Connect Core 1.0
- OAuth 2.0 Security Best Current Practice