TT Lab
はじめる
学ぶ 学習パス コース

セッションとトークン — ブラウザからメッシュまで

ログアウトしたのにトークンがまだ通るなら

TT Labで続きを見る

一言でいうと

署名されたJWTのアクセストークンは、リソースサーバーが単独で検証します。そのため、発行者が「このトークンはもう無効」と言ってもAPIには聞こえず、トークンはexpまで生きます。取り消しを即座に反映する方法は3つだけです。毎回発行者に尋ねる(introspection)、リソースサーバーが拒否リストを持つ、トークンの寿命を短くして露出期間そのものを縮める、のどれかです。どれもタダではありません。

なぜ必要なのか

ユーザーがログアウトを押した、ノートPCをなくした、パスワードが漏れて変更した。このとき期待するのは「その瞬間から、その人のトークンでは何もできない」ことです。前のモジュールのサーバーセッションなら、ストアのキーを1つ消せば終わりでした。ところがJWTは自己完結型です。APIサーバーはJWKSで署名とexpだけを見て通すので、発行者側でセッションを消しても、その事実を知る手段がありません。

RFC 7009は、クライアントが「このトークンはもう使わない」と発行者に知らせる取り消しエンドポイントを標準化しました。ところがこのRFCの3節(Implementation Note)が、自ら限界を書いています。トークンが自己完結型だと、即時の取り消しには「currently non-standardized」な発行者とリソースサーバー間の連携が必要で、別の代替案は、短い寿命のアクセストークンをrefreshトークンで更新し続けることだ、と。取り消しエンドポイントを呼んだだけでAPIが自然に知るわけではない、という意味です。

どう動くのか

3つの戦略を並べるとこうなります。

전략                 폐기가 반영되는 시점        대가
짧은 TTL             최대 토큰 수명만큼 늦게     refresh 가 잦아져 발급자 부하
introspection        즉시                        요청마다 발급자 왕복, 가용성이 묶임
로컬 차단목록(jti)   즉시(목록을 가진 서버만)    상태 저장소, 서버 간 전파

取り消し(RFC 7009)。クライアントがPOST /revokeにtokenと、省略可能なtoken_type_hint(access_token・refresh_token)を送ります。機密クライアントは自分の認証を、パブリッククライアントはclient_idを一緒に送り、サーバーはそのトークンが要求したクライアントに発行されたものかを確認します。レスポンスは、取り消しに成功しても、もともと無効なトークンでも200です。クライアントがそのエラーでできることがないからです。refreshトークンを取り消すと、サーバーがアクセストークンの取り消しをサポートする場合は、同じグラントのアクセストークンも無効にする必要があり(SHOULD)、アクセストークンを取り消すときにrefreshまで取り消すのは任意(MAY)です。

この違いは実務でそのまま表れます。このラボのKeycloak 26.0.7で試し直すと、refreshトークンを取り消すとセッションが終わり、そのセッションのアクセストークンがintrospectionでactive=falseになり、refreshグラントは400になります。一方、アクセストークンだけを取り消すと、refreshで新しいアクセストークンが出続けます。ログアウトがrefreshを取り消す必要がある理由です。エンドポイントのパスはKeycloakのドキュメントにあります。

Introspection(RFC 7662)。リソースサーバーが受け取ったトークンを、RFC 7662のエンドポイントにformでPOSTすると、{"active": true, ...}が返ってきます。期限切れ・取り消し済み・偽造ならactive=falseで、理由は知らせないのが推奨(SHOULD NOT)です。このエンドポイントは、トークンのスキャンを防ぐため、呼び出し元の認証をMUSTで要求します。そのためリソースサーバーは、機密クライアント(api-svc)の資格で呼びます。RFC 7662のセキュリティ上の考慮事項は、キャッシュの両面を指摘しています。キャッシュを短くすれば最新ですが発行者の負荷が増え、長くすれば「取り消されたトークンが使えてしまう期間」が生じます。

拒否リスト。取り消されたトークンのjtiをRedisに載せ、リクエストのたびに確認します。核心は、TTLをトークンの残りの寿命(exp - now)にすることです。期限切れのトークンは、署名検証の段階でどのみち拒否されるので、その後は覚えておく必要がなく、そのためリストの大きさが「いま生きている取り消し済みトークンの数」で抑えられます。

現場での姿

「ログアウトしたのにAPIが使え続ける」という報告は、たいていローカル検証だけを行うAPIに、長いアクセストークンの寿命が重なった場合です。このレルムのアクセストークンの寿命は900秒なので、ローカル検証だけだと、取り消されたトークンが最大15分余計に生きます。寿命を短くすれば期間は縮みますが、refreshのトラフィックが発行者に集中します。RFC 9700(OAuth 2.0 Security BCP)4.14節は、refreshトークンがあるからこそアクセストークンを短く発行して漏洩の影響を減らせるとまとめ、パスワード変更や認可サーバーのログアウトのようなセキュリティイベントのときに、refreshトークンを自動で取り消してもよい(MAY)と書いています。

そのため、よくある組み合わせは、「短いアクセストークンと、ログアウト時のrefreshの取り消し」を基本に置き、送金のように1回の悪用でも高くつくAPIにだけ、introspectionや拒否リストを重ねる形です。introspectionをすべてのAPIにかけると、発行者が遅くなった瞬間にすべてのサービスが一緒に遅くなります。検証のコストが、可用性の結合に変わる地点です。

次のラボですること

Keycloakを起動してトークンを受け取り、introspectionでactive=trueを確認した後、RFC 7009でrefreshトークンを取り消し、falseに変わるのを見ます。続いて、リソースサーバーにローカル検証の経路とintrospectionの経路を並べて作り、取り消されたトークンが片方では200、もう片方では401になることを確認しながら、2つの経路のレイテンシを測ります。最後に、jtiの拒否リストとログアウトを加えて、往復なしで即座に防ぐ方法まで、1つの流れで証明します。