Sessions and Tokens — From the Browser to the Mesh
You logged out, but the token still works
In one line
A signed JWT access token is verified by the resource server alone. So even if the issuer says "this token is now invalid", the API does not hear it, and the token lives until exp. There are only three ways to reflect a revocation immediately — ask the issuer every time (introspection), have the resource server hold a denylist, or shorten the token lifetime to reduce the exposure window itself. None of the three is free.
Why this was needed
The user pressed logout, lost their laptop, or changed their password because it leaked. What they expect then is "from that moment, nothing works with that person's token". With the server session of the earlier module, deleting one store key was the end of it. But a JWT is self-contained. The API server lets it through looking only at the signature and exp through the JWKS, so even if the issuer deletes the session, it has no way of learning that.
RFC 7009 standardized a revocation endpoint through which a client tells the issuer "I'm no longer using this token". But section 3 of this RFC (the Implementation Note) states its own limit. If the token is self-contained, immediate revocation needs an issuer-to-resource-server integration that is "currently non-standardized", and the other alternative is to keep renewing a short-lived access token with a refresh token. That means calling the revocation endpoint does not make the API find out by itself.
How it works
Put the three strategies side by side and it looks like this.
전략 폐기가 반영되는 시점 대가
짧은 TTL 최대 토큰 수명만큼 늦게 refresh 가 잦아져 발급자 부하
introspection 즉시 요청마다 발급자 왕복, 가용성이 묶임
로컬 차단목록(jti) 즉시(목록을 가진 서버만) 상태 저장소, 서버 간 전파
Revocation (RFC 7009). The client sends token and an optional token_type_hint (access_token or refresh_token) to POST /revoke. A confidential client sends its own authentication and a public client sends client_id along, and the server checks that the token was issued to the requesting client. The response is 200 whether the revocation succeeded or the token was already invalid — because there is nothing the client can do with that error. If you revoke a refresh token, then when the server supports access token revocation it must also invalidate the access tokens of the same grant (SHOULD), and revoking the refresh when you revoke an access token is optional (MAY).
This difference shows up directly in practice. When we checked again with the Keycloak 26.0.7 in this lab, revoking a refresh token ends the session, the access token of that session becomes active=false in introspection, and the refresh grant is 400. On the other hand, if you revoke only the access token, new access tokens keep coming out with the refresh. That is why logout has to revoke the refresh. The endpoint path is in the Keycloak docs.
Introspection (RFC 7662). When the resource server POSTs the token it received to the RFC 7662 endpoint as a form, {"active": true, ...} comes back. If it is expired, revoked, or forged, it is active=false, and it is recommended not to give the reason (SHOULD NOT). This endpoint MUST require caller authentication to prevent token scanning — so the resource server calls with the credentials of a confidential client (api-svc). The security considerations of RFC 7662 point out both sides of caching. A short cache is fresh but increases the issuer load, and a long one creates "a window in which a revoked token can be used".
Denylist. You put the revoked token's jti in Redis and check on every request. The key is to set the TTL to the token's remaining lifetime (exp - now). An expired token is rejected at the signature verification stage anyway, so there is no need to remember it after that, and that is why the size of the list is bounded by "the number of revoked tokens alive right now".
What it looks like in the field
The report "I logged out but the API keeps working" is usually a case where an API that only does local verification is combined with a long access token lifetime. The access token lifetime of this realm is 900 seconds, so with local verification alone a revoked token lives up to 15 more minutes. If you shorten the lifetime the window shrinks, but the refresh traffic piles onto the issuer. Section 4.14 of RFC 9700 (the OAuth 2.0 Security BCP) sums up that, because refresh tokens exist, you can issue short access tokens to reduce the impact of a leak, and it notes that the refresh token may be revoked automatically (MAY) on a security event such as a password change or an authorization server logout.
So a common combination lays "a short access token + revoking the refresh at logout" as the base and puts introspection or a denylist only on the APIs where even one misuse is expensive, such as money transfers. If you put introspection on every API, the moment the issuer slows down, every service slows down with it — that is the point where verification cost turns into availability coupling.
What you will do in the next lab
You turn on Keycloak, get a token, confirm active=true with introspection, and then revoke the refresh token with RFC 7009 and see it change to false. Next you build a local verification path and an introspection path side by side on the resource server, confirm that a revoked token is 200 on one and 401 on the other, and measure the latency of the two paths. Finally you attach a jti denylist and logout and prove, in one flow, a way to block immediately without a round trip.