Skip to content
Published on

OAuth 2.0完全攻略 — 認証と認可のすべて

Authors
  • Name
    Twitter
OAuth Deep Dive

はじめに

「Googleでログイン」ボタンを押すと何が起こるでしょうか?たった1回のクリックの裏では、OAuth 2.0という精巧なプロトコルが動いています。

OAuthを理解すれば、ソーシャルログイン、API認証、マイクロサービス間通信、さらにはAnthropicのClaude API認証ポリシーまで明確になります。

核心概念:認証 vs 認可

認証(Authentication): 「あなたは誰?」
  → ユーザーが本人であることを証明
ID/パスワード、生体認証、OTP

認可(Authorization): 「何ができる?」
  → 認証されたユーザーに権限を付与
  → このアプリがあなたのGoogle Calendarを読んでもいい?

OAuth 2.0 = 認可(Authorization)フレームワーク
OpenID Connect = OAuth 2.0の上に認証を追加したもの

OAuth 2.0の4つの役割

┌──────────────┐
Resource Owner│  ← ユーザー
└──────┬───────┘
       │ 「このアプリに私のカレンダーへのアクセスを許可」
┌──────────────┐     ┌──────────────────┐
Client    │────▶│ Authorization│ (私たちのアプリ)│◀────│    Server└──────┬───────┘       (Google OAuth)       │             └──────────────────┘
Access Token
┌──────────────┐
ResourceServer     │  ← Google Calendar API
└──────────────┘

Grant Types(認可方式)

1. Authorization Code(最も重要!)

Webアプリで最も多く使われる方式です。

[1] ユーザー → アプリ: 「Googleでログイン」クリック
[2] アプリ → Google: リダイレクト(client_id, redirect_uri, scope)
[3] ユーザー → Google: ログイン + 「許可」クリック
[4] Google → アプリ: redirect_uri?code=AUTH_CODE
[5] アプリ → Google: AUTH_CODE + client_secret → Access Token
[6] アプリ → Google API: Access Tokenでデータ要求
# Step 2: Authorization要求URL生成
import urllib.parse

auth_url = "https://accounts.google.com/o/oauth2/v2/auth?" + urllib.parse.urlencode({
    "client_id": "YOUR_CLIENT_ID.apps.googleusercontent.com",
    "redirect_uri": "https://yourapp.com/callback",
    "response_type": "code",
    "scope": "openid email profile https://www.googleapis.com/auth/calendar.readonly",
    "state": "random_csrf_token_abc123",  # CSRF防止!
    "access_type": "offline",  # Refresh Tokenも受け取る
})
# → ユーザーをこのURLにリダイレクト

# Step 5: Authorization Code → Access Token交換
import requests

token_response = requests.post("https://oauth2.googleapis.com/token", data={
    "code": "AUTH_CODE_FROM_CALLBACK",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",  # サーバーサイドのみ!
    "redirect_uri": "https://yourapp.com/callback",
    "grant_type": "authorization_code",
})

tokens = token_response.json()
# {
#   "access_token": "ya29.xxx...",      ← API呼び出し用(1時間)
#   "refresh_token": "1//xxx...",        ← 更新用(永続)
#   "id_token": "eyJhbGci...",           ← ユーザー情報(OIDC)
#   "expires_in": 3600,
#   "token_type": "Bearer"
# }

# Step 6: API呼び出し
calendar = requests.get(
    "https://www.googleapis.com/calendar/v3/calendars/primary/events",
    headers={"Authorization": f"Bearer {tokens['access_token']}"}
)

なぜこんなに複雑なのか?

  • Authorization CodeはブラウザURLに露出しますが、1回限りで単独では使えません
  • Access Token交換はサーバー間通信(client_secretを保護)
  • フロントエンドにSecretが絶対に露出しません

2. Authorization Code + PKCE(モバイル/SPA必須!)

モバイルアプリやSPA(Reactなど)にはclient_secretを安全に保存する場所がありません。

import hashlib
import base64
import secrets

# Clientがcode_verifierを生成(秘密の値)
code_verifier = secrets.token_urlsafe(64)
# "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk..."

# code_challenge = SHA256(code_verifier) → Base64URL
code_challenge = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b'=').decode()

# Authorization要求にchallenge含める
auth_url = f"...&code_challenge={code_challenge}&code_challenge_method=S256"

# Token交換時にverifierを提出(Secretの代わり!)
token_response = requests.post("https://oauth2.googleapis.com/token", data={
    "code": auth_code,
    "client_id": "YOUR_CLIENT_ID",
    "code_verifier": code_verifier,  # Secretの代わりにこれを検証!
    "redirect_uri": "...",
    "grant_type": "authorization_code",
})

PKCEの核心: Auth Codeを傍受されてもcode_verifierがなければTokenを取得できません。

3. Client Credentials(サーバー間通信)

# ユーザーの介入なしにサーバー → サーバー直接認証
# 例: バックエンド → Google Cloud API
token = requests.post("https://oauth2.googleapis.com/token", data={
    "client_id": "SERVICE_CLIENT_ID",
    "client_secret": "SERVICE_CLIENT_SECRET",
    "grant_type": "client_credentials",
    "scope": "https://www.googleapis.com/auth/cloud-platform",
})

4. Refresh Token(更新)

# Access Token期限切れ(1時間)→ Refresh Tokenで更新
new_tokens = requests.post("https://oauth2.googleapis.com/token", data={
    "refresh_token": "1//xxx...",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "grant_type": "refresh_token",
})
# 新しいAccess Tokenが発行される(Refresh Tokenは維持)

Token種類の比較

Token有効期間用途保存場所
Authorization Code数秒Token交換用(1回限り)URLパラメータ(即時消滅)
Access Token1時間API呼び出しメモリ(ブラウザ)/ DB(サーバー)
Refresh Token永続(取消し可能)Access Token更新サーバーDB(安全に!)
ID Token (OIDC)1時間ユーザー情報確認メモリ

JWT(JSON Web Token)の構造

Access TokenとID Tokenは通常JWT形式です:

eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik...
├── Header ──────────┤├── Payload ─────────────────────────────...
import base64
import json

jwt = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature"

# Header
header = json.loads(base64.urlsafe_b64decode(jwt.split('.')[0] + '=='))
# {"alg": "RS256", "typ": "JWT"}

# Payload
payload = json.loads(base64.urlsafe_b64decode(jwt.split('.')[1] + '=='))
# {"sub": "1234567890", "name": "Youngju Kim", "iat": 1709420400}

# Signature = RS256(header + "." + payload, private_key)
# → サーバーのpublic keyで検証(改ざん不可)

OpenID Connect (OIDC) — OAuth + 認証

OAuth 2.0だけでは「このユーザーが誰か」は分からない
OIDCがid_tokenを追加してユーザー情報を提供

scopeに "openid" を追加 → id_tokenが発行される
scopeに "profile email" を追加 → 名前、メールが含まれる

セキュリティチェックリスト

すべき: stateパラメータでCSRFを防止
すべき: PKCEを使用(SPA/モバイルは必須)
すべき: redirect_uriを正確にマッチ(ワイルドカード不可)
すべき: Refresh Tokenはサーバーにのみ保存
すべき: Access TokenはAuthorizationヘッダーでのみ送信
すべき: HTTPS必須(Token盗取防止)
してはいけない: Access TokenをURLパラメータに入れない
してはいけない: client_secretをフロントエンドに露出しない
してはいけない: TokenをlocalStorageに保存しない(XSS脆弱)

実務例:Anthropic OAuthの問題

最近AnthropicがOAuthポリシーを変更したのもこの文脈です:

以前: Claude ProサブスクOAuth Token → サードパーティアプリ(OpenClawなど)
変更: OAuth TokenはClaude.ai / Claude Codeでのみ使用可能
理由: Tokenアービトラージ防止(サブスク料 より API Token価値が高い)
代替: Anthropic API Key(従量課金)を使用

クイズ — OAuth 2.0(クリックして確認!)

Q1. OAuth 2.0における認証(Authentication)と認可(Authorization)の違いは? ||認証: ユーザーが誰であるかを確認。認可: 認証されたユーザーに特定リソースへのアクセス権限を付与。OAuth 2.0は認可フレームワークであり、OIDCが認証を追加||

Q2. Authorization Code GrantでCodeが1回限りである理由は? ||CodeはブラウザURLに露出するため盗取リスクがある。1回限りでclient_secretなしではToken交換不可のため、単独では無価値||

Q3. PKCEにおけるcode_verifierとcode_challengeの関係は? ||code_challenge = SHA256(code_verifier)のBase64URLエンコーディング。認可リクエスト時にchallengeを送り、トークン交換時にverifierを送ってサーバーが検証||

Q4. Refresh TokenをlocalStorageに保存してはいけない理由は? ||XSS攻撃でJavaScriptがlocalStorageを読み取れる。Refresh Tokenは長期有効なため、盗取時に持続的なアクセスが可能。httpOnlyクッキーまたはサーバーDBに保存すべき||

Q5. Client Credentials Grantはどのような状況で使われるか? ||ユーザーの介入なしにサーバー間の直接認証。例: バックエンドサービスが別のAPIサーバーにアクセスする時。client_id + client_secretのみでToken発行||

Q6. stateパラメータがない場合、どのような攻撃が可能か? ||CSRF攻撃 — 攻撃者が自身のアカウントで発行したAuthorization Codeを被害者のコールバックURLに送り、被害者を攻撃者のアカウントにログインさせることができる||

Q7. JWTのSignatureは何を保証するか? ||トークンの内容(Header + Payload)が改ざんされていないことを保証。サーバーのPrivate Keyで署名し、Public Keyで検証。ただし暗号化ではない — PayloadはBase64デコードで誰でも読める||