目次

「ログインに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 ServerAPIを提供するサーバー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に保存するのがセキュリティ上の推奨パターンだ。

免責事項 — 掲載情報は執筆時点のものです。料金・機能は変更される場合があります。最新情報は各公式サイトをご確認ください。