ログアウトしたのにトークンがまだ通るなら
一言でいうと
署名された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つの流れで証明します。