JWT Token Lifecycle: Expiration, Refresh, Revocation, and Spring Boot Validation

Guilliano Molaire Guilliano Molaire 11 min read

Last updated: July 2026

A JWT lifecycle has three jobs: expire tokens quickly (Keycloak defaults access tokens to 5 minutes), refresh them safely (RFC 9700 requires rotation or sender-constrained refresh tokens for public clients), and revoke them on demand (Keycloak ships an RFC 7009 revocation endpoint, back-channel logout, and a not-before push). Get those three right on the identity provider, and validating tokens in Spring Boot shrinks to a few lines of configuration instead of a hand-rolled filter.

Most JWT tutorials get this backwards. They spend two thousand words minting tokens with a JJWT helper class and validating them in a custom OncePerRequestFilter wired to a shared secret, then mention expiration in a closing paragraph. That design quietly turns your API into its own identity provider, and a shared HMAC secret means every service that can validate a token can also forge one. Delegating issuance to an identity provider and validating against its published JWKS is less code and materially safer. This guide covers both halves: Keycloak on the issuing side, Spring Boot on the validating side, and the lifecycle rules that connect them.

How long should tokens live?

The exp claim is a Unix timestamp, and everything about it is a trade-off between blast radius and friction. A stolen access token is useful to an attacker for exactly as long as exp allows, because stateless validation never phones home to ask whether the token is still welcome. Expiration is the one revocation mechanism that always works.

Short-lived access tokens, in the 5 to 15 minute range, keep that window small. The cost is more refresh traffic and slightly more client-side plumbing, both of which are solved problems. Long-lived access tokens (hours or days) buy convenience at the price of a compromise window you cannot close without extra machinery. If you find yourself wanting a 24-hour access token, what you actually want is a short access token plus a refresh token.

There are two expiry models, and you usually want both at once:

  • Absolute expiration ends the session at a fixed point no matter how active the user is. Predictable, easy to audit, mildly annoying.
  • Sliding expiration extends the session while the user stays active and cuts it when they go idle. Friendlier, but it needs an upper bound or an active session never dies.

Regulated environments layer requirements on top of this. NIST SP 800-63B ties re-authentication and inactivity ceilings to assurance level, and PCI DSS mandates idle timeouts in cardholder data environments. If you operate under one of these frameworks, treat its ceilings as your maximums and tune downward from there.

Where do Keycloak token lifetimes come from?

Here is the part that trips people up: Keycloak has no standalone refresh token lifetime setting. If you are hunting for a “7 days” knob, you will not find one. A refresh token lives exactly as long as the session it belongs to, and session lifetimes are defined in the realm’s Sessions settings. The Tokens tab owns per-token lifespans; the Sessions tab owns the envelope around them.

Setting Location Default What it controls
Access Token Lifespan Realm settings > Tokens 5 minutes The exp on access tokens. A client’s Advanced tab can override it per client.
SSO Session Idle Realm settings > Sessions 30 minutes The sliding window. Refresh token expiry tracks it, with a 2-minute grace window added for clock drift.
SSO Session Max Realm settings > Sessions 10 hours The absolute ceiling. No amount of activity extends a session past it.
Client Session Idle / Client Session Max Realm settings > Sessions, overridable per client on the Advanced tab 0 (inherit SSO values) Per-client limits inside the SSO session. When set, Client Session Max caps refresh token lifetime for that client.
Offline Session Idle / Offline Session Max Realm settings > Sessions Separate schedule Lifetimes for offline tokens, the long-lived refresh variant for background access.

So “how long does a refresh token last in Keycloak” decomposes into: it can be exchanged until the idle timeout elapses without a refresh, it dies for good at the session max, and a per-client session max shortens both if configured. SSO Session Idle maps to sliding expiration, SSO Session Max to absolute expiration, and you get both simultaneously by default.

Notice which knobs are per client and which are not. The client Advanced tab can override Access Token Lifespan and the Client Session Idle/Max pair, but the SSO session envelope stays realm-wide. The full matrix is in the Keycloak timeout documentation.

Refresh tokens: rotation is the baseline now

The refresh flow itself is simple. The client POSTs grant_type=refresh_token to the token endpoint, Keycloak checks that the token is genuine and the session is still alive, and returns a fresh access token. The user sees nothing. The useful property is that refresh tokens, unlike access tokens, are tied to server-side session state, so they can be killed instantly.

The open question used to be whether refresh tokens should rotate (each one valid for a single use) or stay static (reusable until expiry). That question is now settled. RFC 9700, the OAuth 2.0 Security Best Current Practice published as BCP 240 in January 2025, states in Section 2.2.2 that refresh tokens issued to public clients MUST either be sender-constrained (bound to a key the client proves possession of, as with DPoP or mTLS) or be rotated on every use.

An earlier version of this post repeated a claim you will still find all over the internet: that static refresh tokens are fine for native mobile apps because the OS keychain is safe storage. RFC 9700 closed that door, and for good reason. A mobile app is a public client. It cannot hold a client secret, so a refresh token exfiltrated from it (malware, backups, device theft) is a bearer credential with nothing else to check. Rotation or sender-constraining is the floor, not a nice-to-have. Static reusable refresh tokens remain defensible only for confidential clients that authenticate to the token endpoint with a secret or private key.

Rotation also gives you theft detection for free. Section 4.14.2 of RFC 9700 describes the replay behavior: when the server sees an already-invalidated refresh token come back, it should treat that as evidence of compromise, revoke the currently active refresh token for that session, and SHOULD revoke the access tokens issued from it too. One of the two parties holding the token is an attacker; killing the lineage stops both.

Rotation (one-time use) Static (reusable)
Replay exposure Old token dies on first use; replay is detected and punished Stolen token works until expiry or manual revocation
Client complexity Must persist the new refresh token after every exchange Store once
Standards position Required for public clients (RFC 9700, Section 2.2.2) Acceptable only for confidential clients
Failure mode A lost response can strand the client mid-rotation Silent long-term compromise

Turning rotation on in Keycloak

This is a spot where a lot of guides, including the previous version of this one, point you at the wrong screen. Rotation is not configured in a client’s Advanced settings. It lives at the realm level, in Realm settings > Tokens, and it takes two controls that only work together:

  1. Enable Revoke Refresh Token. This is the master switch, and it is off by default. Without it, the next setting does nothing.
  2. Set Refresh Token Max Reuse to 0. Zero means every refresh token is single-use; each exchange invalidates the old token and returns a new one.

Because this is realm-level, it applies to every client in the realm. There is no per-client rotation toggle, which is worth knowing before you flip it on a realm that also serves legacy backend daemons.

One honest operational caveat: rotation punishes unreliable networks. If a mobile client times out mid-refresh and never receives the new token, its stored token is already burned and the user gets logged out. Setting Max Reuse to 1 gives flaky clients one retry at the cost of a slightly weaker replay guarantee. Measure your refresh failure rate before deciding.

Wherever tokens end up, store them properly: HttpOnly cookies for browsers, the platform keychain for mobile, and never localStorage or sessionStorage, which any injected script can read.

How do you revoke a JWT before it expires?

You cannot un-sign a JWT. A stateless access token stays cryptographically valid until exp, which is why every real revocation strategy works at the source rather than on the token. Keycloak gives you four levers, in escalating order of bluntness.

The revocation endpoint. Keycloak implements RFC 7009 at /realms/{realm}/protocol/openid-connect/revoke. It accepts both access tokens and refresh tokens. Revoking a refresh token has a side effect worth knowing: it also revokes the consent the user granted that client, so the client re-runs the consent step on next login where consent is required.

Back-channel logout. Configured per client via the Backchannel logout URL field: when a session ends, Keycloak POSTs a logout token directly to each registered client, server to server, so applications can tear down their own local sessions without waiting for a browser redirect that may never come.

Push not-before. Under the realm’s revocation policy, you can push a not-before timestamp to the realm. Every token issued before that instant is rejected, across all clients. It is a blunt instrument, and that is the point: it exists for the day a signing key or a batch of tokens leaks and you need everything gone at once.

Short lifetimes as the backstop. Revocation events propagate when clients come back to refresh. A 5-minute access token caps how stale any resource server’s view can be, which is the quiet reason the default is 5 minutes and not an hour.

If you are not fronting your APIs with an identity provider, the DIY equivalents are a blocklist keyed on the jti claim (checked on every request, so cache aggressively) or token versioning, where a password reset bumps a per-user version number and older tokens fail a claim check. Both reintroduce state, and in distributed systems that state propagates with lag; short lifetimes bound the lag either way. Managed Keycloak platforms surface this as session management UI, listing live sessions and killing them per user or per client. For the architectural trade-offs behind that, see our comparison of cookies, tokens, and server-side sessions in distributed systems.

Validate a JWT in Spring Boot (no custom filter)

The payoff for the lifecycle work above is that your API never has to be clever about tokens. Spring Security ships a full resource server, and the JJWT-plus-custom-filter pattern that dominates tutorials solves a problem you no longer have. Add one dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

And one block of configuration:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://id.example.com/realms/production
          audiences:
            - inventory-api

That is the whole integration. On first use, Spring fetches the issuer’s discovery document, finds the jwks_uri, and caches the signing keys. From then on, every request with a bearer token gets signature verification against the JWKS plus validation of iss, exp, and nbf, with a built-in 60-second clock skew allowance so a token minted one second ago is not rejected by a server whose clock runs early. Key rotation is handled for you: an unknown key ID triggers a JWKS re-fetch. The details are in the Spring Security resource server documentation.

The one thing the defaults skip is the audience check, which is what stops a token minted for service A being replayed against service B. The audiences property above is the easy fix. If you need logic beyond exact matching, wire a validator into the decoder:

@Bean
JwtDecoder jwtDecoder() {
    String issuer = "https://id.example.com/realms/production";
    NimbusJwtDecoder decoder = JwtDecoders.fromIssuerLocation(issuer);

    OAuth2TokenValidator<Jwt> audience = new JwtClaimValidator<List<String>>(
        "aud", aud -> aud != null && aud.contains("inventory-api"));

    decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
        JwtValidators.createDefaultWithIssuer(issuer), audience));
    return decoder;
}

JwtValidators.createDefaultWithIssuer keeps the stock timestamp and issuer checks, and DelegatingOAuth2TokenValidator bolts your audience rule on top. No filter, no SecurityContextHolder bookkeeping, no shared secret to leak.

A note on versions: these examples target Spring Boot 4.x and Spring Security 7.x. Every Spring Boot 3.x line reached open-source end of support in June 2026, so new work should start on 4.x. The properties and validator APIs shown here are unchanged from Security 6.x, so the same configuration runs on a 3.x app you have not migrated yet.

What this setup deliberately does not do is check revocation; a locally validated JWT is trusted until exp, which is the trade you accepted for statelessness. When an endpoint needs live revocation checks, use token introspection instead. Our guide to verifying Keycloak access tokens on the backend walks through both modes, with Node and Python equivalents. If JWKS is new territory, our JWKS explainer covers key IDs and rotation, and you can watch the process happen in the browser with the JWKS Verifier, which fetches a key set and checks a token’s signature client-side.

Pitfalls that undo everything else

A tight lifecycle still fails if validation is sloppy. The recurring offenders:

  • Algorithm confusion. If a validator accepts both RS256 and HS256, an attacker can sign a forged token with your public RSA key used as an HMAC secret. The resource server approach sidesteps this because algorithms come from JWKS key metadata, but any hand-rolled validator must pin its expected algorithm explicitly.
  • Skipping aud and iss. Signature validity proves who signed the token, not who it was for. Without audience and issuer checks, tokens hop between services that share an identity provider.
  • Hardcoded keys. A PEM file pasted into config works until the first key rotation, which will happen at the least convenient time. Fetch keys from the JWKS endpoint and honor the kid header. Paste any token into the JWT Token Analyzer and you can see the kid your validator needs to match.
  • Logging token values. Log issuance, refresh, and validation failures; never log the tokens themselves. On the issuing side, Keycloak’s event system records logins, refreshes, and revocations for you.
  • Inconsistent validation across services. In a microservice fleet, one service with laxer checks is the perimeter. Share the configuration source or a common library.
  • Chatty error responses. Return a generic 401 for every validation failure and rate-limit the token endpoint. “Token expired” versus “bad signature” is useful telemetry for you and free reconnaissance for an attacker.

Frequently asked questions

How do I validate a JWT in Spring Boot without a custom filter?

Add the spring-boot-starter-oauth2-resource-server dependency and set spring.security.oauth2.resourceserver.jwt.issuer-uri to your identity provider’s issuer URL. Spring Security fetches the JWKS automatically and validates the signature, issuer, expiry, and not-before claims on every request. Add the audiences property for audience checking. No filter code is involved.

What is the difference between issuer-uri and jwk-set-uri?

issuer-uri triggers discovery: Spring fetches the issuer’s /.well-known metadata, finds the JWKS location from it, and adds an issuer claim validator. jwk-set-uri points directly at the key set, skips discovery, and does not validate iss on its own. Prefer issuer-uri; reach for jwk-set-uri when discovery is blocked or the metadata endpoint is unreachable from your network.

How long should a JWT access token last?

Minutes, not hours. Keycloak’s default is 5 minutes, and 5 to 15 minutes suits most APIs. Short lifetimes cap the damage from a stolen token and bound how stale a resource server’s view of revocations can get. Session length is the refresh token’s job, governed in Keycloak by the SSO Session Idle and Max settings.

Does Keycloak support refresh token rotation?

Yes, at the realm level. In Realm settings > Tokens, enable Revoke Refresh Token, then set Refresh Token Max Reuse to 0 for strict one-time use. The toggle must be on for the reuse limit to apply, and the policy covers every client in the realm; there is no per-client rotation switch.

How do I revoke a JWT before it expires?

You cannot invalidate the signature itself, so revoke at the source. Keycloak’s /realms/{realm}/protocol/openid-connect/revoke endpoint (RFC 7009) accepts access and refresh tokens, back-channel logout notifies clients when sessions end, and a not-before push rejects every token issued before a chosen timestamp. Keep access tokens short so these events propagate within minutes.

Tired of running Keycloak yourself?

Skycloak runs real upstream Keycloak for you with a 99.99% SLA. No fork, no lock-in, just managed Keycloak that stays patched and on call so you don't have to.

Guilliano Molaire
Written by
Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Start Free Trial Talk to Sales
© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman