はじめに

複数サーバーや異なるサービス間でユーザーの認証状態を共有するのは簡単ではありません。従来のセッション方式では、サーバー同士で状態を同期する必要があり、スケールや保守の負担が増します。
JWT(JSON Web Token)は、認証に必要な情報を署名付きトークンとしてクライアントに渡し、受け取ったサーバーが自分だけで検証できる仕組みです。これにより、状態共有の仕組みを省きながら、安全に認証を行えます。

この記事を読むことで、JWTの基本構造と、アクセストークン・リフレッシュトークンを組み合わせた安全な認証フローの設計・実装ポイントを理解できます。

JWTの基本構造

JWTは「Header」「Payload」「Signature」の3つの部分で構成されます。それぞれをBase64URL形式でエンコードし、ドット(.)で連結して1つの文字列にします。例えば次のようになります。

xxxxx.yyyyy.zzzzz
  • xxxxx → HeaderをBase64URLエンコードした部分
  • yyyyy → PayloadをBase64URLエンコードした部分
  • zzzzz → 上記2つを連結して秘密鍵で署名した部分

Base64URL形式は暗号化ではありません。URLに安全に載せるための文字コード変換にすぎないため、中身は誰でも読めます。JWTの安全性は、内容の秘匿ではなく「改ざんできないこと」を署名で保証する点にあります。

Headerはトークンのメタ情報を示し、署名アルゴリズムやトークンの種類を指定します。
例:

1
2
3
4
{
  "alg": "HS256",
  "typ": "JWT"
}

この場合、「署名はHS256(HMAC-SHA256)方式で行われ、この文字列はJWTである」という意味になります。これは手紙の封筒に「どの方法で封印したか」を明記するようなものです。

Payload

Payloadは「クレーム(claim)」と呼ばれる属性情報を含みます。これは手紙の中身にあたり、ユーザーを識別する情報や有効期限、発行時刻などを記載します。

クレームには以下の2種類があります。

  • 登録済みクレーム(Registered Claims): JWT仕様で予約されているキー
    • sub(Subject): ユーザーやエンティティの一意識別子
    • iat(Issued At): 発行時刻(UNIX時間、1970/1/1からの経過秒)
    • exp(Expiration Time): 有効期限(UNIX時間)
    • iss(Issuer): トークンの発行者
    • aud(Audience): トークンの対象者
  • カスタムクレーム(Custom Claims): アプリケーション固有の情報
    例:scope, roles, department など

例:

1
2
3
4
5
6
7
8
{
  "sub": "123456",
  "name": "Taro Yamada",
  "roles": ["user", "editor"],
  "scope": "read write",
  "iat": 1691730000,
  "exp": 1691730900
}

Payloadは暗号化されないため、パスワードや個人情報などの機密データは絶対に含めません。さらに、JWTを検証する際は署名だけでなく、exp(期限)、nbf(Not Before)、iss(発行者)、aud(対象者)などの値を必ずチェックします。これらのバリデーションを行わないと、不正や期限切れのトークンを受け入れてしまう危険があります。

Signature

Signatureは、base64url(Header) + "." + base64url(Payload) を秘密鍵(または非対称鍵暗号方式の秘密鍵)で署名した値です。受け取った側は同じ手順で署名を再計算し、一致すれば改ざんされていないと判断できます。これは、封筒の封印が破られていないかを確かめるのと同じ役割です。

署名には以下の方式があります。

  • 対称鍵方式(例: HS256): 秘密鍵を共有する両者間でのみ検証可能
  • 非対称鍵方式(例: RS256, ES256): 発行者が秘密鍵で署名し、受信者は公開鍵で検証可能。ES256は鍵サイズが小さく高速なため近年推奨されるケースもある

例(非対称鍵方式: RS256 の署名処理):

RS256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  privateKey
)

署名アルゴリズムは強度と運用要件に応じて選びますが、algnone を使うことは避け、サーバー実装側で検証アルゴリズムを固定することが重要です。これは「algインジェクション脆弱性」と呼ばれる攻撃を防ぐためです。

署名アルゴリズム選択の注意

  • 強度と運用要件に応じてHS256 / RS256 / ES256などを選択する
  • algnone を使わない
  • サーバー実装で、トークン内の alg をそのまま信用せず、検証側で使用するアルゴリズムを固定する(algインジェクション脆弱性対策)

署名を検証できるのは、秘密鍵を共有している相手(対称鍵の場合)か、公開鍵を知っている相手(非対称鍵の場合)に限られます。これにより、トークンの改ざんを確実に検出できます。

この構造を理解した上で、次にどのようにJWTを使って認証を行うのかを見ていきます。

認証フロー

JWT認証は、ユーザーがログインしてサーバーからアクセストークンを受け取り、そのトークンを使ってAPIへアクセスする仕組みです。アクセストークンは短命で、期限切れになると利用できなくなります。この短命性により、万一漏洩しても悪用されるリスクを最小限にできます。

ログインとアクセストークン発行

ユーザーがログイン情報(例: ユーザー名とパスワード)を送信すると、サーバーは認証処理を行い、アクセストークンを生成します。

1
2
3
4
5
6
7
{
  "sub": "123456",
  "iat": 1691730000,
  "exp": 1691730900,
  "scope": "read write",
  "roles": ["user"]
}

個人情報や機密情報は含めないことが推奨されます。

アクセストークンの管理

アクセストークンは、サーバーから受け取った後、適切に保存し、安全に送信する必要があります。代表的な3つの管理方法を以下にまとめます。


メモリで管理

  • 特徴: ページリロードで消える。一時的なセッション維持向き(SPAやネイティブアプリ)
  • 受け取り方: HTTPレスポンスボディで取得
  • 保存方法: アプリの変数や状態管理に保持
  • 送信方法: 必ずHTTPS経由でBearerトークンを設定
    1
    
    Authorization: Bearer <access_token>
    
  • 注意点: リロードや終了後は再ログイン、またはリフレッシュトークンで再取得が必要

セッションストレージ / ローカルストレージで管理

  • 特徴: セッションストレージは終了時に消える、ローカルストレージは持続。ページ間で共有可能
  • 受け取り方: HTTPレスポンスボディで取得
  • 保存方法: ブラウザのストレージに保存
  • 送信方法: メモリ管理と同様にBearerトークンを設定
    1
    
    Authorization: Bearer <access_token>
    
  • 注意点: JavaScriptからアクセス可能なためXSS攻撃で盗まれるリスクがある

Cookie(HttpOnly + Secure)で管理

  • 特徴: JavaScriptから直接参照できない。HTTPS通信時のみ送信
  • 受け取り方: Set-Cookieヘッダーで取得
    Set-Cookie: access_token=<JWT文字列>; HttpOnly; Secure; SameSite=Strict
    
  • 保存方法: ブラウザが自動保存
  • 送信方法: ブラウザが自動送信(Authorizationヘッダー不要)
  • 注意点: CSRF対策としてSameSite属性やCSRFトークンを利用

有効期限の扱い

アクセストークンは数分〜十数分程度の有効期限を設定します。期限切れ後は再ログインするか、リフレッシュトークンを使って新しいアクセストークンを取得します。

次は、期限切れ時に再ログインを不要にするリフレッシュトークンの仕組みについて解説します。

リフレッシュトークン

リフレッシュトークンは、期限切れになったアクセストークンを再発行するためだけに使用される長期間有効なトークンです。アクセストークンは短命にすることで漏洩時の被害を小さくできますが、その分、有効期限が切れるたびにユーザーが再ログインする必要があります。この不便を解消し、利便性とセキュリティを両立するためにリフレッシュトークンが利用されます。

動作の流れ

  • ユーザーがログインすると、サーバーは短命なアクセストークンと長命なリフレッシュトークンを同時に発行する
  • 発行したリフレッシュトークンは、サーバー側の永続ストレージやインメモリストア(例:データベース、Redisなど)に記録して管理する
  • クライアントはアクセストークンを使ってAPIを呼び出す
  • アクセストークンが期限切れになると(多くの場合HTTP 401エラー)、クライアントはリフレッシュトークンをサーバーに送信する
  • サーバーは保存しているトークン情報と照合し、有効であれば新しいアクセストークンを返す(必要に応じて新しいリフレッシュトークンも発行し、古いものを失効)

安全な保管と送信

リフレッシュトークンは、HttpOnly + Secure 属性付きのCookieに保存するのが推奨されます。この設定によりJavaScriptから直接アクセスできず、XSS攻撃による盗難リスクを軽減できます。CookieはHTTPS通信時に自動送信されるため、リフレッシュ用エンドポイントにアクセスするだけで送信されます。

Set-Cookie: refresh_token=<JWT文字列>; HttpOnly; Secure; SameSite=Strict

セキュリティ対策

トークンローテーションを行う

新しいリフレッシュトークンを発行した時点で古いものを無効化し、使い回しを防ぎます。この方式により、盗まれたトークンが再利用されるリスクを減らせます。

失効条件を設定する

一定期間使用されなかった場合や、ユーザーがパスワードを変更した場合など、特定の条件を満たしたときにリフレッシュトークンを失効させます。これにより、長期間放置されたトークンの悪用を防ぎます。

即時失効させたい場合は以下のような仕組みを導入します。

  • jti(JWT ID)を付与し、サーバー側でブラックリストとして管理する
  • 発行済みのリフレッシュトークンをサーバー側ストアから削除して再利用を防ぐ

長期有効の危険性を考慮する

リフレッシュトークンは有効期限が長いため、盗難されると長期間にわたり悪用される可能性があります。保存場所の安全性確保やHTTPS通信の徹底が重要です。

実装イメージ

アクセストークンは短命にして漏洩時の被害を最小化し、リフレッシュトークンは長めに設定して利便性を確保します。エンドポイントは以下のように分けることで、認証と更新、失効を明確に区別できます。

  • アクセストークン有効期限:5〜15分
  • リフレッシュトークン有効期限:7〜30日
  • エンドポイント例:
    /auth/login → 両トークン発行
    /auth/refresh → リフレッシュトークンでアクセストークン再発行
    /auth/logout → リフレッシュトークンを失効

まとめ

JWTは署名によって改ざんを防ぎ、サーバーが状態を持たずに認証情報をやり取りできる仕組みです。アクセストークンとリフレッシュトークンの併用は、セキュリティと利便性を両立させます。安全な実装には、クレームのバリデーション、署名アルゴリズムの選定、alg の固定化、失効管理の仕組みが不可欠です。