Keycloak and Enterprise Identity
Invalidation, the Hard Problem
Summary
A JWT is fast because the server is not involved after issuance. And for the same reason it is hard to revoke. Practical work is understanding this trade and managing it through lifetime.
Why this was needed
"I logged out but the token is still valid" is a problem every team that uses JWTs runs into. With session-based authentication, it was over once the server deleted the session from the session store. A JWT has no state on the server, so there is nothing to delete.
As a solution, a blacklist comes to mind. You put revoked token IDs in a store and check them on every request. But then every request does a store lookup, and the advantage of statelessness disappears. It is not much different from going back to the session approach.
Manage it by lifetime
The standard practical answer is to give the two tokens different lifetimes.
| Access token | Refresh token | |
|---|---|---|
| Lifetime | 5–15 minutes | Days to weeks |
| Verification | Only check the signature (stateless) | The authentication server holds state |
| Immediate revocation | Not possible | Possible |
| Where kept | Memory | httpOnly cookie or a safe store |
If you keep access tokens short, even if they are stolen they are useless after that time, and even if you revoke permissions, it takes effect within that time. You keep refresh tokens long but rotate them. Each time you refresh, you give a new one and invalidate the old one.
The effect of this combination is important. When the user logs out, you revoke the refresh token. The access token remains but expires after at most 15 minutes, and after that it cannot be refreshed, so the session effectively ends. You get invalidation within 15 minutes without a blacklist.
Rotation has one more side effect. If an old refresh token is used again, that is a sign of theft — because a legitimate client holds the new one. Invalidating that user's entire token lineage at this point is reuse detection.
정상: RT1 → (갱신) → RT2 → (갱신) → RT3
탈취: RT1 → (갱신) → RT2
└── 공격자가 RT1 을 다시 사용 → 서버가 계열 전체를 끊는다
Only in cases where you truly must cut off immediately — an account-theft report, an administrator's forced logout — do you use a blacklist as an exception. The target is not all tokens but only that user's tokens, so the storage burden is small. A lighter method is to keep one token_not_before timestamp per user and reject all tokens issued before it. One value per user in the store is enough.
Keycloak's four lifetime settings
Keycloak has several lifetime settings, which is confusing. Each controls something different.
| Setting | What it controls | Common value |
|---|---|---|
| Access Token Lifespan | How long an access token is valid | 5–15 minutes |
| SSO Session Idle | How long until the session ends without activity | 30 minutes |
| SSO Session Max | How long until it is forcibly cut even with activity | 10 hours |
| Offline Session Idle | The idle limit of an offline token | 30 days |
The difference between idle time and max time is the key. Idle is "it is cut if you take your hands off", and max is "even if you keep using it, it will be cut someday". In regulated places such as banks, you keep the max time short. Conversely, for internal tools you keep the max time long so that people log in only once a day.
If you get the relationship between the two values wrong, strange phenomena occur. If Access Token Lifespan is longer than SSO Session Idle, there is an interval where the session has already ended but the access token is still valid. Always keep the access token lifetime shorter than the idle time.
A concurrent login limit is also often required. Keycloak provides a feature for limiting the number of sessions, but to handle it at the application level you must manage a per-user session list separately.
Back-channel logout and BFF
If several applications use the same authentication server, when you log out in one place you must cut off the others as well. OIDC's back-channel logout is a method in which the authentication server sends a logout token directly to each application's registered address. Because it does not go through the browser, it works even when the tab is closed. The receiving side checks the token's signature and sid and deletes the corresponding session.
The BFF (Backend for Frontend) pattern is also advantageous from the viewpoint of session management. If the backend holds the tokens and gives the browser only a session cookie, deleting that session at logout invalidates it immediately. The path by which tokens get stolen through XSS also disappears. In exchange, the backend now holds state, so horizontal scaling needs a session store.
Where to keep the token
As important as lifetime design is the storage location. If you place it wrongly, it does no good however short you make the lifetime.
| Location | Safe against XSS | Safe against CSRF | Persists after refresh | Assessment |
|---|---|---|---|---|
localStorage |
✗ scripts can read it | ✓ | ✓ | The most common and the most dangerous |
sessionStorage |
✗ | ✓ | Per tab | No better |
| JavaScript variable | △ readable in the same context | ✓ | ✗ | Practical when used with refresh |
httpOnly cookie |
✓ scripts cannot read it | ✗ needs SameSite | ✓ | The place for the refresh token |
The practical combination is this. The access token in memory, the refresh token in an httpOnly + Secure + SameSite=Lax cookie. When you refresh the page, the access token in memory disappears, but you quietly get it again with the refresh token in the cookie.
SameSite=Strict makes the cookie not attach when you arrive through an external link, so it looks as if you were logged out. Usually Lax is right, and you narrow to Strict only for sensitive flows such as payment.
What happens when clocks drift
JWT verification looks at exp and nbf with the server's clock. If the clocks of the authentication server and the resource server drift by even a few seconds, the error "the token I just received is not yet valid" (an nbf violation) occurs intermittently. It is the kind of bug that is hard to reproduce and keeps you wandering for a long time.
Verification libraries usually allow a margin of 30–60 seconds (clock skew). Do not reduce this value to 0. Instead, sync NTP on all nodes.
What you meet in the field
Token lifetime settings are just a few numbers on a screen, but the symptoms those numbers create show up in forms that seem unrelated to authentication.
"I get logged out sometimes" is usually the session idle time. The access token is refreshed, but if the SSO session's idle time is short, when a user who stepped away briefly comes back, the refresh is rejected. The user says "it was released within a few minutes", and the log records it as a normal expiry.
"I logged out but another tab is still alive" means back-channel logout is not attached. With the front channel alone, the other open clients do not know about it. If there is no place that stores the session ID and receives the logout notification to delete it, logout happens only in that browser tab.
"Everyone gets logged out right after a deployment" means the signing key changed. When rotating keys, you must leave the old key for verification for a while. You also look at the refresh interval of the side that caches the public key list (JWKS).
"Only certain users have trouble" is usually a token that has grown large. An account with many groups or roles has a larger token, and if you put it in a cookie, it hits the header size limit and the proxy returns 431 or 400. The symptom appears not as an authentication problem but as no screen showing up at all.
"The times drift slightly" really is a clock problem. If the clocks of the issuing server and the verifying server differ by even a few seconds, the nbf/exp decisions split. Sync NTP and leave a margin of a few seconds (clock skew) on the verifying side.
When investigating, do not leave tokens in the logs as they are. A token is itself a credential. What you need is about sub, sid, and exp, so extract and keep only those.
What to look at in the next check
This module checks the decision criteria of lifetime design with a quiz and finishes the course.