JWTは本当に万能か? – 認証・認可・セキュリティを見直す
JWTは本当に万能か? – 認証・認可・セキュリティを見直す
JWT(Json Web Token)はWebアプリケーションにおける認証手段として広く使われていますが、誤った使い方をすると、深刻なセキュリティリスクを招く可能性があります。
JWTは「認証」用途に限定するべき
多くの開発者が、JWTに「ユーザーの権限情報」や「アクセス可能なリソース」など、動的に変化しうる情報まで詰め込んでしまうミスを犯しています。
✅ JWTに含めて良い情報:user_id、email、org_id、固定ロールなど
❌ JWTに含めてはいけない情報(修正済みの日本語訳)
- ユーザーの実際のロール(チーム異動・昇格・降格などにより変更される可能性がある)
→ 例:ログイン時は「admin」だったが、後に「user」に変更された場合など - ログイン状態やセッション状態(例:isLoggedIn、sessionValid など)
→ 状態が動的に変わるため、トークン発行時の値がすぐに古くなるリスクがある - 現在アクセス可能なリソース一覧(例:accessibleDocs、accessibleProjects など)
→ 時間や状況によりアクセス権が変わるため、トークンに含めると整合性が崩れる
{
"user_id": "123",
"email": "test@example.com",
"role": "admin",
"accessibleProjects": ["project-a", "project-b"],
"sessionValid": true
}
これらは時間経過とともに変わる可能性があるため、トークンに含めてしまうと、すでに古い状態で動作し続けてしまうリスクがあります。
権限(Authorization)は別で管理しよう
セッションに権限情報を保存するのはなぜ問題なのか?
Next.jsでは、MiddlewareやNextAuth.jsのような認証ツールを使うことで、セッションベースの認証を簡単に実装できます。
しかし、ログイン時にroleなどの権限情報をセッションに一緒に保存する方法には、リアルタイムのセキュリティ要件を満たせないという欠点があります。
短期的には便利で素早く動作しますが、次のような問題が発生する可能性があります。
- ユーザーの権限が変更されても、既存のセッションにはその変更が反映されない
- センシティブな権限(例:管理者 → 一般ユーザーなど)が即時に制限されず、セキュリティ上のリスクが発生する可能性がある
- セッションはあくまで「スナップショット」のようなものであるため、動的な判断が難しく、変更の追跡もできない
- そのため、セッションにはユーザーIDなど最低限の識別情報のみを保存し、API呼び出し時に毎回別のロジックやポリシーエンジンを用いてリアルタイムで権限を判定する構成の方がセキュリティ上安全である
多くのシステムでは、ログイン時にユーザーの権限情報をセッションに一緒に保存し、その後のリクエストごとにセッションからその情報を参照する方法が広く使われています。
例えば、ユーザーがログインすると req.session.user = { id, role } のようにセッションに保存し、以降の権限確認ではその値をそのまま利用する、といった具合です。
しかし、この方法には以下のような問題が生じる可能性があります。
- ユーザーの権限が変更されても、すでにログイン済みのセッションには古い権限情報がそのまま残り続けます。
- 権限の変更が即座に反映されないため、セキュリティ要件の高いシステムでは、不適切な権限でリソースにアクセスされるリスクがあります。
- 特に「管理者 → 一般ユーザー」のように権限が下がった場合、セッションを更新しない限り、依然として管理者権限でシステムを利用できてしまうという重大なセキュリティ上の欠陥になります。
こうした理由から、セッションに権限情報を固定的に保存するのではなく、APIリクエストのたびにリアルタイムで権限を確認する仕組みが、近年ますます支持されるようになっています。
したがって、認可(Authorization)はユーザー認証(JWTの発行)とは分離し、リクエストごとに権限をリアルタイムで判断する構成が推奨されます。
JWTにはユーザー識別に必要な最低限の情報のみを含め、権限の判定は別途ポリシーエンジンで処理するのが最も安全な方法です。
権限の判定は、以下のようなサービスによって行われます。
- OPA(Open Policy Agent)
- Cedar
- Permit.io
権限ロジックは、一般的にAPIサーバーのミドルウェアや権限管理用のサービス層に組み込まれます。
つまり、ユーザーが特定のAPIを呼び出す際に、サーバー側では user_id を基にポリシーエンジンへ現在のユーザー権限を問い合わせます。
checkPermission(userId, "edit", "project", "project-a")
このように、サーバーでリアルタイムに権限を判定することで、ポリシーの柔軟な変更にも対応しやすく、監査ログ(Audit Log)の記録にも適しています。
SPAのセキュリティを強化するためのBFFパターン
Single Page Application(SPA)では、JWTをクライアント側(localStorageなど)に保存し、そのままAPIに送信する方式が一般的ですが、
この方法はXSS(クロスサイトスクリプティング)攻撃に非常に弱いという重大な欠点があります。
この問題を解決するためのアーキテクチャが、**BFF(Backend for Frontend)**です。
BFFとは?
BFFとは、文字通り「フロントエンドのためのバックエンド」を指します。
SPAのようにブラウザ上で動作するアプリケーションが、認証サーバーやAPIサーバーに直接アクセスするのではなく、
すべての認証やリクエスト処理をBFFサーバー経由で行う構成です。
BFFの処理フロー
- ユーザーが /login にアクセス
- BFFサーバーが認証サーバー(OIDC/OAuth)と連携してJWTを取得
- JWTはサーバー側のセッション、または HttpOnly + Secure 属性付きのクッキーに保存(クライアント側からはアクセス不可)
- クライアントはクッキーのみを保持し、そのままAPIをリクエスト → BFFがJWTを付与してバックエンドに転送
[ユーザー] → [BFF:ログイン処理] → [クライアント:クッキーのみ保持] → [BFF:APIリクエストを代理送信]
この構成のメリットは次の通りです
- クライアント側にトークンが露出しないため、XSS攻撃に対して安全
- クッキーに HttpOnly と Secure フラグを設定することで、ブラウザが自動的に保護してくれる
- トークンの更新や有効期限の管理も、すべてBFFサーバー側で一元的に処理できる
つまり、SPAであってもセッションベースの認証のように動作させることができ、
セキュリティ性とユーザー体験の両立が可能なアーキテクチャです。
まとめ
JWTは非常に強力なツールですが、使い方を誤るとセキュリティリスクが大きくなります。
トークンには軽量かつ固定された認証情報のみを含めるべきです。
権限はリアルタイムのポリシーベースで別途処理するのが安全です。
SPA構成ではトークンが露出しないよう、BFFパターンの導入が推奨されます。
JWTを安全に活用するには、「認証」と「認可」を明確に分離し、
アーキテクチャ全体としてセキュリティを強化することが重要です。
Discussion