OAuth 2.0 と JWT - 認証・認可の完全ガイド
📚 概要
OAuth 2.0 は、サードパーティアプリケーションに対してユーザーの代理でリソースへのアクセスを許可する認可フレームワークです。JWT(JSON Web Token)は、情報を安全に送信するためのコンパクトなトークン形式です。
この記事では、OAuth 2.0 の仕組みと JWT の構造を詳しく解説し、セキュリティベストプラクティスまでカバーします。
🕰️ 歴史的背景
認証・認可の進化
2000年代初頭: パスワード共有の時代
- ユーザーがサードパーティアプリに自分のパスワードを渡していた
- セキュリティリスクが極めて高い
- パスワード変更時にすべてのアプリで更新が必要
2006年: OAuth 1.0の誕生
- Twitter と Google が協力して開発
- パスワードを共有せずにアクセス許可を実現
- しかし、実装が複雑で署名処理が必要
2012年: OAuth 2.0の登場
- RFC 6749 として標準化
- HTTPS前提でシンプル化
- モバイルアプリやSPAに対応
- 現在の事実上の標準
2015年: JWTの標準化
- RFC 7519 として標準化
- OAuth 2.0のアクセストークンとして広く採用
- ステートレスな認証を実現
🔧 技術解説
OAuth 2.0 の基本概念
4つの役割:
graph LR
A[Resource Owner] -->|Authorizes| B[Client]
B -->|Requests Token| C[Authorization Server]
C -->|Issues Token| B
B -->|Access with Token| D[Resource Server]
D -->|Returns Data| B
style A fill:#51cf66
style C fill:#4dabf7
style D fill:#ff6b6b- Resource Owner: ユーザー(リソースの所有者)
- Client: サードパーティアプリケーション
- Authorization Server: 認可サーバー(トークン発行)
- Resource Server: リソースサーバー(APIサーバー)
OAuth 2.0 のフロー
1. Authorization Code Flow(最も安全)
sequenceDiagram
participant User
participant Client
participant AuthServer
participant API
User->>Client: Login Request
Client->>AuthServer: Authorization Request
AuthServer->>User: Login Page
User->>AuthServer: Credentials
AuthServer->>Client: Authorization Code
Client->>AuthServer: Code + Client Secret
AuthServer->>Client: Access Token
Client->>API: API Request + Token
API->>Client: Protected Resourceステップ:
- クライアントが認可サーバーにリダイレクト
- ユーザーがログインして許可
- 認可サーバーがauthorization codeを返す
- クライアントがcodeをaccess tokenと交換
- access tokenでAPIにアクセス
実装例:
// 1. 認可リクエスト
const authUrl = new URL('https://auth.example.com/oauth/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', 'your_client_id');
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/callback');
authUrl.searchParams.set('scope', 'read:profile write:posts');
authUrl.searchParams.set('state', generateRandomState()); // CSRF対策
window.location.href = authUrl.toString();
// 2. コールバック処理
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
// stateを検証(CSRF対策)
if (state !== req.session.oauthState) {
return res.status(400).send('Invalid state');
}
// 3. トークン取得
const response = await fetch('https://auth.example.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code,
client_id: 'your_client_id',
client_secret: 'your_client_secret',
redirect_uri: 'https://yourapp.com/callback'
})
});
const { access_token, refresh_token, expires_in } = await response.json();
// トークンを保存
req.session.accessToken = access_token;
req.session.refreshToken = refresh_token;
res.redirect('/dashboard');
});
// 4. APIアクセス
app.get('/api/user', async (req, res) => {
const response = await fetch('https://api.example.com/user', {
headers: {
'Authorization': `Bearer ${req.session.accessToken}`
}
});
const user = await response.json();
res.json(user);
});
2. Implicit Flow(非推奨)
SPAで使われていましたが、セキュリティ上の理由で非推奨になりました。
3. Client Credentials Flow(サーバー間通信)
const response = await fetch('https://auth.example.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: 'your_client_id',
client_secret: 'your_client_secret',
scope: 'api:read'
})
});
const { access_token } = await response.json();
4. Refresh Token Flow
async function refreshAccessToken(refreshToken: string) {
const response = await fetch('https://auth.example.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: 'your_client_id',
client_secret: 'your_client_secret'
})
});
const { access_token, refresh_token, expires_in } = await response.json();
return { access_token, refresh_token, expires_in };
}
JWT - JSON Web Token
JWTは、3つの部分で構成されます:
header.payload.signature
構造:
graph LR
A[JWT Token] --> B[Header]
A --> C[Payload]
A --> D[Signature]
B --> E[Algorithm + Type]
C --> F[Claims]
D --> G[HMAC or RSA]
style B fill:#4dabf7
style C fill:#51cf66
style D fill:#ff6b6b1. Header(ヘッダー)
{
"alg": "HS256",
"typ": "JWT"
}
- alg: 署名アルゴリズム(HS256, RS256など)
- typ: トークンタイプ(JWT)
2. Payload(ペイロード)
{
"sub": "1234567890",
"name": "John Doe",
"email": "john@example.com",
"iat": 1516239022,
"exp": 1516242622,
"scope": "read:profile write:posts"
}
標準クレーム:
- sub: Subject(ユーザーID)
- iat: Issued At(発行時刻)
- exp: Expiration Time(有効期限)
- nbf: Not Before(この時刻より前は無効)
- iss: Issuer(発行者)
- aud: Audience(対象)
3. Signature(署名)
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
実装例(Node.js):
import jwt from 'jsonwebtoken';
// トークン生成
function generateToken(userId: string) {
const payload = {
sub: userId,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600 // 1時間後
};
const secret = process.env.JWT_SECRET!;
return jwt.sign(payload, secret, { algorithm: 'HS256' });
}
// トークン検証
function verifyToken(token: string) {
try {
const secret = process.env.JWT_SECRET!;
const decoded = jwt.verify(token, secret) as { sub: string };
return decoded;
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new Error('Token expired');
}
if (error instanceof jwt.JsonWebTokenError) {
throw new Error('Invalid token');
}
throw error;
}
}
// ミドルウェア
app.use((req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
try {
const decoded = verifyToken(token);
req.userId = decoded.sub;
next();
} catch (error) {
const message = error instanceof Error ? error.message : 'Authentication failed';
res.status(401).json({ error: message });
}
});
💡 実践例
完全なOAuth 2.0 + JWT実装
import express from 'express';
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
const app = express();
const JWT_SECRET = process.env.JWT_SECRET!;
const sessions = new Map(); // 本番ではRedis使用
// 認可エンドポイント
app.get('/oauth/authorize', (req, res) => {
const { client_id, redirect_uri, scope, state } = req.query;
// クライアント検証(省略)
// ログイン画面を表示(省略)
// 認可コード生成
const code = crypto.randomBytes(32).toString('hex');
sessions.set(code, {
client_id,
redirect_uri,
scope,
userId: req.user.id, // ログイン済みユーザー
expires: Date.now() + 600000 // 10分
});
res.redirect(`${redirect_uri}?code=${code}&state=${state}`);
});
// トークンエンドポイント
app.post('/oauth/token', express.json(), (req, res) => {
const { grant_type, code, client_id, client_secret, redirect_uri } = req.body;
if (grant_type === 'authorization_code') {
const session = sessions.get(code);
if (!session || session.expires < Date.now()) {
return res.status(400).json({ error: 'invalid_grant' });
}
// クライアント認証(省略)
sessions.delete(code);
// アクセストークン生成
const accessToken = jwt.sign({
sub: session.userId,
scope: session.scope,
exp: Math.floor(Date.now() / 1000) + 3600
}, JWT_SECRET);
// リフレッシュトークン生成
const refreshToken = crypto.randomBytes(32).toString('hex');
sessions.set(refreshToken, {
userId: session.userId,
scope: session.scope,
type: 'refresh'
});
res.json({
access_token: accessToken,
token_type: 'Bearer',
expires_in: 3600,
refresh_token: refreshToken,
scope: session.scope
});
}
});
// 保護されたAPIエンドポイント
app.get('/api/user', (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Unauthorized' });
}
const token = authHeader.substring(7);
try {
const decoded = jwt.verify(token, JWT_SECRET) as { sub: string; scope: string };
// スコープチェック
if (!decoded.scope.includes('read:profile')) {
return res.status(403).json({ error: 'Insufficient scope' });
}
// ユーザー情報を返す
res.json({
id: decoded.sub,
name: 'John Doe',
email: 'john@example.com'
});
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
});
🔒 セキュリティベストプラクティス
1. HTTPS必須
すべての通信はHTTPSで行う。HTTPでの通信は絶対にしない。
2. State パラメータでCSRF対策
// 認可リクエスト時
const state = crypto.randomBytes(32).toString('hex');
req.session.oauthState = state;
// コールバック時
if (req.query.state !== req.session.oauthState) {
throw new Error('CSRF detected');
}
3. Refresh Tokenのローテーション
// リフレッシュ時に新しいRefresh Tokenを発行
const newRefreshToken = crypto.randomBytes(32).toString('hex');
sessions.delete(oldRefreshToken); // 古いトークンを無効化
sessions.set(newRefreshToken, { userId, scope });
4. トークンの有効期限を短く
- Access Token: 15分〜1時間
- Refresh Token: 1週間〜30日
- Authorization Code: 10分
5. JWTの署名検証
// ❌ 悪い例: 署名を検証しない
const decoded = jwt.decode(token); // 危険!
// ✅ 良い例: 必ず検証する
const decoded = jwt.verify(token, secret);
6. alg: none攻撃への対策
// アルゴリズムを明示的に指定
jwt.verify(token, secret, { algorithms: ['HS256'] });
7. Scopeの適切な設定
// 必要最小限のスコープのみ要求
const scope = 'read:profile'; // 書き込み権限は不要なら含めない
🎯 OAuth 2.0 vs JWT
| OAuth 2.0 | JWT | |
|---|---|---|
| 目的 | 認可フレームワーク | トークン形式 |
| 役割 | アクセス許可の仕組み | 情報を安全に送信 |
| 使い方 | プロトコル | データフォーマット |
| 関係 | JWTをトークンとして使える | OAuth 2.0で使われることが多い |
🔍 関連する問題
この記事に関連するクイズ問題:
- Q4: OAuth 2.0のフロー
- Q5: JWTの構造と検証
📝 まとめ
- OAuth 2.0: 認可フレームワーク、パスワードを共有せずにアクセス許可
- Authorization Code Flow: 最も安全なフロー
- JWT: 署名付きトークン、ステートレスな認証
- セキュリティ: HTTPS必須、State/CSRF対策、短い有効期限
- ベストプラクティス: 署名検証、スコープ最小化、トークンローテーション
次のステップ: 実際にOAuth 2.0サーバーを構築し、JWTベースの認証を実装してみましょう!
推奨リソース:
- OAuth 2.0 RFC 6749
- JWT RFC 7519
- jwt.io - JWTデバッガ