目次
「ログインにOAuth2を使っています」「認証にJWTを採用しています」——こうした説明は技術現場でよく聞くが、実際には混同や誤解が多い。OAuth2は「認可(Authorization)」のフレームワークであり「認証(Authentication)」ではない。JWTはトークンの形式であり、それ自体は認証でも認可でもない。本稿ではOAuth2とJWTの仕組みを正確に整理し、実務での正しい理解を提供する。
認証と認可の違い
この2つの概念の混同が多くの誤解を生む。
認証(Authentication): 「あなたは誰か」を確認すること。ユーザー名とパスワードを照合する行為が典型例。「本人確認」に相当する。
認可(Authorization): 「あなたは何をしてよいか」を確認すること。Aさんには請求書の閲覧権限があるが、削除権限はない——という判断が認可だ。「権限確認」に相当する。
OAuth2は認可のフレームワークだ。「GoogleのカレンダーAPIを、ユーザーに代わって読み取るアプリに許可する」という権限委譲の仕組みを提供する。ログイン機能(認証)を直接提供するものではない。認証にOAuth2を使いたい場合は、OAuth2の上に構築されたOpenID Connect(OIDC)が正しい選択だ。
OAuth2の登場人物
OAuth2フローには4つの役割が登場する。
| 役割 | 説明 | 例 |
|---|---|---|
| Resource Owner | データの所有者(エンドユーザー) | GoogleアカウントのユーザーA |
| Client | リソースへのアクセスを求めるアプリ | カレンダー連携スマホアプリ |
| Authorization Server | トークンを発行するサーバー | Google OAuth Server |
| Resource Server | APIを提供するサーバー | Google Calendar API |
Authorization Code フロー
最も安全で広く使われるOAuth2フローを図示する。
ユーザー(ブラウザ) アプリ(Client) Google(AuthZ Server) Google API
│ │ │ │
│─── 「Googleでログイン」をクリック ──→│ │ │
│ │ │ │
│ │─── リダイレクト ──────→│ │
│ │ (client_id, scope, │ │
│ │ redirect_uri, │ │
│ │ state, code_challenge) │ │
│ │ │ │
│←────────────────────────────────────────────────│ │
│ Googleのログイン・同意画面を表示 │ │
│ │ │ │
│─── ユーザーが同意 ─────────────────────────────→│ │
│ │ │ │
│←── リダイレクト ───────────────────────────────│ │
│ (redirect_uri?code=AUTH_CODE&state=xxx) │ │
│ │ │ │
│ code を渡す │ │ │
│──────────────────────→│ │ │
│ │─── トークン交換 ────────→│ │
│ │ (code, client_secret, │ │
│ │ code_verifier) │ │
│ │ │ │
│ │←── アクセストークン ────│ │
│ │ + リフレッシュトークン │ │
│ │ │ │
│ │─── APIリクエスト ─────────────────────────→│
│ │ Authorization: Bearer {access_token} │
│ │ │
│ │←── カレンダーデータ ─────────────────────────│
PKCEとは
Authorization Codeフローには**PKCE(Proof Key for Code Exchange)**と呼ばれる拡張がある。モバイルアプリやSPAはクライアントシークレットを安全に保管できないため、code_verifier(ランダム文字列)とcode_challenge(そのハッシュ)を使って認可コード横取り攻撃を防ぐ。
import secrets
import hashlib
import base64
# PKCE: code_verifier を生成
code_verifier = secrets.token_urlsafe(64)
# code_challenge = BASE64URL(SHA256(code_verifier))
digest = hashlib.sha256(code_verifier.encode()).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b'=').decode()
その他のOAuth2フロー
| フロー | 用途 | 適した場面 |
|---|---|---|
| Authorization Code + PKCE | ユーザーが関与する認可 | SPAアプリ、モバイルアプリ |
| Client Credentials | サーバー間のAPI認証 | バックエンド同士の通信 |
| Device Code | 入力が困難なデバイス | スマートTV、IoTデバイス |
| Implicit(非推奨) | 旧式SPAでの使用 | 現在はPKCEに置き換え推奨 |
Client Credentials フローはユーザーが介在しないM2M(Machine-to-Machine)通信でよく使われる。
[バックエンドサービスA]
│
│ POST /token
│ client_id=xxx&client_secret=yyy&grant_type=client_credentials
▼
[認可サーバー]
│
│ { "access_token": "...", "expires_in": 3600 }
▼
[バックエンドサービスA]
│
│ Authorization: Bearer {access_token}
▼
[バックエンドサービスB(API)]
JWTの構造
JWT(JSON Web Token)はトークンを表現するフォーマットだ。3つのBase64URLエンコードされた部分をドットで結合した文字列で表される。
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsIm5hbWUiOiLnlLDnlJ8iLCJleHAiOjE3MDAwMDAwMDB9.signature
[ヘッダー] [ペイロード] [署名]
ヘッダー(Header)
{
"alg": "RS256", // 署名アルゴリズム(RS256: RSA + SHA-256)
"typ": "JWT" // トークンタイプ
}
ペイロード(Payload)
{
"sub": "user_123", // Subject(ユーザーID)
"name": "山田太郎",
"email": "yamada@example.com",
"roles": ["user", "editor"],
"iat": 1699999200, // Issued At(発行時刻)
"exp": 1700002800, // Expiration(有効期限: iatの1時間後)
"iss": "https://auth.example.com", // Issuer(発行者)
"aud": "https://api.example.com" // Audience(受け取り対象)
}
ペイロードはBase64URLでエンコードされているだけで暗号化されていない。誰でもデコードして中身を読める。機密情報(パスワード、クレジットカード番号など)をJWTに含めてはいけない。
署名(Signature)
署名はヘッダーとペイロードの改ざんを検証するために使う。
HMAC-SHA256(対称鍵の場合):
signature = HMAC_SHA256(
base64url(header) + "." + base64url(payload),
secret_key
)
RSA-SHA256(非対称鍵の場合):
signature = RSA_SIGN_SHA256(
base64url(header) + "." + base64url(payload),
private_key
)
RSA(非対称鍵)を使う場合、認可サーバーが秘密鍵で署名し、リソースサーバーは公開鍵で検証できる。認可サーバーとリソースサーバーを分離した構成に適している。
JWTの検証フロー
APIサーバーがJWTを受け取ったときの検証手順。
import jwt
from datetime import datetime
def verify_jwt(token: str, public_key: str, expected_audience: str) -> dict:
try:
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"],
audience=expected_audience, # audクレームを検証
options={
"require": ["exp", "iat", "sub"], # 必須クレーム
"verify_exp": True, # 有効期限を検証
}
)
return payload
except jwt.ExpiredSignatureError:
raise AuthError("トークンの有効期限が切れています")
except jwt.InvalidAudienceError:
raise AuthError("このトークンはこのAPIを対象としていません")
except jwt.InvalidSignatureError:
raise AuthError("トークンの署名が不正です")
アクセストークンとリフレッシュトークン
OAuth2では通常2種類のトークンを使い分ける。
| アクセストークン | リフレッシュトークン | |
|---|---|---|
| 有効期限 | 短い(15分〜1時間) | 長い(数日〜数週間) |
| 用途 | APIリクエストの認可 | アクセストークンの更新 |
| 送信先 | 毎回APIサーバーへ | 認可サーバーのみ |
| 保存場所 | メモリ推奨(SPAの場合) | HTTPOnly Cookie推奨 |
アクセストークンが期限切れになった場合:
クライアント → 認可サーバー: POST /token
grant_type=refresh_token
refresh_token=xxx
認可サーバー → クライアント: 新しいアクセストークン(+ 新しいリフレッシュトークン)
OpenID Connect(OIDC)と認証
OAuth2はあくまで認可のフレームワーク。「このユーザーが誰か」という認証情報を標準化したのがOpenID Connectだ。OIDCはOAuth2の上位プロトコルで、IDトークンという新しいトークンを追加する。
// IDトークン(JWTフォーマット)のペイロード
{
"iss": "https://accounts.google.com",
"sub": "110169484474386276334", // Google内でのユーザーID
"aud": "123456789-xxx.apps.googleusercontent.com",
"email": "user@gmail.com",
"email_verified": true,
"name": "Taro Yamada",
"picture": "https://lh3.googleusercontent.com/...",
"iat": 1699999200,
"exp": 1700002800
}
Googleでログインするとき、アプリはOIDCプロバイダー(Google)からIDトークンを受け取り、そのトークンを検証することでユーザーの身元を確認する。これが「Googleアカウントでログイン」の実態だ。
よくある間違いと対策
間違い1: JWTをセッションの完全な代替とする
JWTは発行後に無効化するのが難しい(ブロックリストが必要になる)。強制ログアウトや権限変更を即座に反映したい場合、短いTTLと組み合わせるか、サーバーサイドのセッションIDを維持する設計を検討すること。
間違い2: JWTを機密情報の保管場所にする
ペイロードは暗号化されていない。ユーザーIDや権限情報は入れてよいが、パスワードや個人情報をペイロードに含めてはいけない。
間違い3: 署名アルゴリズムをHS256(対称鍵)で固定する
HS256では署名鍵の漏洩が全トークンの偽造につながる。認可サーバーとリソースサーバーを分離する設計では、RS256(非対称鍵)を使うことで秘密鍵の共有を回避できる。
まとめ
OAuth2は「ユーザーに代わってリソースへアクセスする権限を委譲する」フレームワークであり、認証そのものではない。ユーザー認証が必要な場合はOAuth2を拡張したOpenID Connectを使う。
JWTはヘッダー・ペイロード・署名の3部構成のトークンフォーマットで、改ざん検証のための署名を持つ。ペイロードは暗号化されていないため機密情報を含めてはいけない。アクセストークン(短期)とリフレッシュトークン(長期)を使い分け、アクセストークンはメモリに、リフレッシュトークンはHTTPOnly Cookieに保存するのがセキュリティ上の推奨パターンだ。
免責事項 — 掲載情報は執筆時点のものです。料金・機能は変更される場合があります。最新情報は各公式サイトをご確認ください。