Continuous Access Evaluation for Immediate Session Revocation
This guide is for developers designing authentication and authorization flows that must react to risk changes before access tokens naturally expire. You’ll learn where continuous access evaluation fits in a Zero Trust architecture, how the event-driven revocation path works end to end, and what to actually configure in apps, gateways, and token validation code.
TL;DR — Continuous access evaluation (CAE) replaces the old "wait for the token to expire" model with "re-check or revoke when risk changes". The practical takeaway: keep short-lived access tokens, add an event-driven revocation signal plus a policy check at request time, and teach your API or gateway to reject or challenge sessions immediately when the user, device, network, or account state changes. Reading time: ~7 min
What it is and where it sits
Continuous access evaluation is the control loop that closes the gap between authentication time and request time. In a classic bearer-token setup, the identity provider issues an access token for 15-60 minutes, your API validates the signature and expiry, and the user keeps access until exp even if their laptop is stolen 30 seconds later. CAE changes that: a risk or policy change triggers revocation or re-evaluation before token expiry.
In Zero Trust terms, CAE sits between:
- the identity plane: IdP, MFA, device posture, directory, risk engine
- the policy plane: authorization rules, session state, revocation lists, conditional access
- the enforcement plane: API gateway, reverse proxy, service mesh, application middleware
It does not replace authentication, OAuth, OIDC, or token validation. It replaces the assumption that exp is the only meaningful end of a session.
Typical things that talk to it:
- IdP or risk engine emits events: user disabled, password reset, impossible travel, device non-compliant, IP reputation changed, MFA strength downgraded
- session store or revocation service records those events against a subject, session ID, token family, or device ID
- API gateway or app middleware checks revocation state on each request or on a short cache interval
- client gets
401,403, or a claims/auth challenge and must re-authenticate or step up
What it replaces in a typical flow:
- Old model: validate JWT locally, trust it until
exp - CAE model: validate JWT locally and consult current session/risk state, or accept a push/event that invalidates local trust
View as text
[User Browser] ---- 1. GET /api/orders ----> [API Gateway] [API Gateway] ---- 2. validate JWT ----> [JWKS Cache] [API Gateway] ---- 3. check session/risk ----> [Revocation Service] [Revocation Service] ---- 4. active? no ----> [API Gateway] [API Gateway] ---- 5. 401 reauth required ----> [User Browser] [Risk Engine] | 0. device marked non-compliant v [Revocation Service]
In a typical request/data flow, CAE usually lives in one of three places:
At the gateway
Best if you already terminate auth at nginx, Envoy, Kong, an ingress controller, or a cloud API gateway. You get one enforcement point for many services.
In shared auth middleware
Best if each service validates tokens itself and you have a common library. The library checks token claims plus revocation/session state.
In a sidecar or authz service
Best if you already use an external authorization callout pattern. The app asks a policy service, "Is this request still allowed right now?"
How it actually works
Here is one realistic end-to-end example: an employee is using your internal admin API from a managed laptop. Mid-session, the device falls out of compliance because endpoint protection is disabled.
Step 1: user gets a normal access token
The browser or CLI authenticates with your IdP and receives:
- access token: short TTL, say 10 minutes
- refresh token or session cookie: longer-lived
- claims:
sub,sid,jti,amr, maybe device/session identifiers
Your API gateway validates the JWT locally against cached JWKS and allows requests.
Step 2: risk changes outside the request path
Your device management or risk engine detects non-compliance and emits an event. The important part is not vendor-specific UI; it is the data shape. You need enough identifiers to map the event to active sessions.
A realistic event payload looks like this:
{
"type": "device.non_compliant",
"occurred_at": "2026-10-30T12:04:19Z",
"user_id": "00u123...",
"device_id": "dev-9f3a",
"session_ids": ["sid-7b2c"],
"reason": "endpoint_protection_disabled"
}
The revocation service writes a record like:
- subject
user_id=00u123... - session
sid-7b2c - status
revoked - reason
device.non_compliant - effective time
2026-10-30T12:04:19Z
Step 3: enforcement point checks current state
On the next request, the gateway does two checks, in this order:
- cryptographic token validation: signature,
iss,aud,nbf,exp - live session/risk check: is
sidorjtirevoked? is the user still allowed under current policy?
If the second check fails, the gateway returns 401 or a challenge response. If your client can handle step-up auth, include a machine-readable reason header or body.
Example response shape:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
WWW-Authenticate: Bearer error="invalid_token", error_description="session revoked due to risk change"
Cache-Control: no-store
{"error":"session_revoked","reason":"device.non_compliant"}
With curl -i, that looks like:
curl -i https://api.example.com/admin/users \
-H 'Authorization: Bearer eyJhbGciOi...'
HTTP/2 401
content-type: application/json
www-authenticate: Bearer error="invalid_token", error_description="session revoked due to risk change"
cache-control: no-store
content-length: 62
{"error":"session_revoked","reason":"device.non_compliant"}
Step 4: client reacts
The client should not blindly retry the same token. Correct behavior is:
- browser app: redirect to login or trigger silent reauth if policy allows
- CLI: stop and print a specific message
- service-to-service client: fetch a fresh token, and if the same challenge repeats, fail closed
A useful CLI error is:
ERROR: access token rejected: session_revoked (device.non_compliant)
Run: mycli auth login
exit status 1
Step 5: caches expire or are bypassed carefully
The hard part is not the 401; it is cache behavior. If your gateway caches revocation lookups for 60 seconds, your "continuous" access evaluation is really "revocation within 60 seconds". That may be acceptable, but write it down as an SLO.
The practical target is usually:
- token TTL: 5-15 minutes
- revocation propagation: seconds
- enforcement cache TTL: 5-30 seconds for low-risk apps, near-zero for admin apps
When to use it (and when not to)
Use CAE when the business impact of stale access is larger than the extra complexity of live policy checks.
| Scenario | Recommendation |
|---|---|
| Admin consoles, finance actions, customer data export, production control planes | Use CAE. Risk changes should cut access before token expiry. |
| Workforce SSO with device posture, geo/risk signals, and step-up MFA | Use CAE. This is where Zero Trust value shows up. |
| Public consumer app with low-risk read-only APIs | Probably skip CAE; short token TTLs are usually enough. |
| Service-to-service APIs using mTLS and very short-lived workload identities | Usually skip full CAE; rotate credentials aggressively instead. |
| Offline-capable mobile app that must function without network | CAE is a bad fit for hard real-time revocation; use short TTLs and scoped local capability. |
| Single internal app with server-side sessions in Redis | You may not need CAE terminology at all; deleting the server session already gives immediate revocation. |
You probably don't need this if...
- all sensitive access already uses server-side sessions you can delete instantly
- your access tokens live for 1-5 minutes and your risk model is simple
- your app must work offline for long periods
- your only requirement is "log out everywhere" and you can implement that by rotating a per-user session version
Trade-offs
Every CAE benefit comes with a bill.
| Benefit | What it costs |
|---|---|
| Immediate revocation after password reset, account disablement, device risk, or IP change | You now operate a revocation/event pipeline, not just JWT validation. |
| Better Zero Trust posture: access depends on current state, not stale login state | More moving parts: risk engine, event delivery, policy service, caches, retries. |
| Shorter exposure window for stolen tokens | More request-path latency unless you cache aggressively. |
| Centralized enforcement at gateway | Potential gateway lock-in and pressure to standardize auth patterns across teams. |
| Better auditability: "why was access cut?" | You need structured reason codes and correlation IDs, or support becomes guesswork. |
| Fine-grained step-up auth | Client complexity increases; browsers, mobile apps, CLIs, and service clients all need different challenge handling. |
A few edge cases experienced teams ask about:
What if the revocation service is down?
Pick one of these explicitly:
- fail closed for admin/high-risk APIs
- fail open for N seconds using last-known-good cache for user-facing low-risk APIs
Do not let this be an accidental behavior hidden in library defaults.
What about clock skew?
If you revoke based on iat or compare event time to token issuance time, allow a small skew window. Otherwise you'll get hard-to-reproduce false revocations around time drift.
What about long-lived streaming connections?
WebSockets and gRPC streams need periodic revalidation or server-initiated disconnect on revocation events. A one-time check at connect is not CAE.
In practice
Example 1: nginx with auth_request to a revocation check endpoint
server {
listen 443 ssl http2;
server_name api.example.com;
location /api/ {
auth_request /_authz;
auth_request_set $auth_status $upstream_status;
error_page 401 = @reauth;
proxy_pass http://app_backend;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header Authorization $http_authorization;
}
location = /_authz {
internal;
proxy_pass http://authz_service/check;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Authorization $http_authorization;
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Original-URI $request_uri;
}
location @reauth {
add_header WWW-Authenticate 'Bearer error="invalid_token", error_description="session revoked or reauth required"' always;
return 401 '{"error":"session_revoked"}';
}
}
This makes nginx call an internal authorization endpoint before proxying /api/ traffic. The gotcha: auth_request treats only 2xx as allow; if your authz service returns 302 to a login page, nginx will not do what you expect.
Diagnose that with:
curl -i https://api.example.com/api/orders \
-H 'Authorization: Bearer bad-or-revoked-token'
Expected shape when misconfigured:
HTTP/2 500
server: nginx
content-type: text/html
<html>
<head><title>500 Internal Server Error</title></head>
If you see 500 here, check nginx error logs for lines like:
auth request unexpected status: 302 while sending to client
The fix is to make /check return 200, 401, or 403, never redirects.
Example 2: Express middleware with JWT validation plus revocation lookup
import express from "express";
import jwt from "jsonwebtoken";
import jwksClient from "jwks-rsa";
import Redis from "ioredis";
const app = express();
const redis = new Redis(process.env.REDIS_URL);
const client = jwksClient({ jwksUri: process.env.JWKS_URI, cache: true, cacheMaxAge: 600000 });
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) return callback(err);
callback(null, key.getPublicKey());
});
}
async function auth(req, res, next) {
const hdr = req.headers.authorization || "";
const token = hdr.startsWith("Bearer ") ? hdr.slice(7) : null;
if (!token) return res.status(401).json({ error: "missing_token" });
jwt.verify(token, getKey, {
algorithms: ["RS256"],
issuer: process.env.TOKEN_ISSUER,
audience: process.env.TOKEN_AUDIENCE,
clockTolerance: 5
}, async (err, payload) => {
if (err) {
return res.status(401).json({ error: "invalid_token", detail: err.message });
}
const sid = payload.sid;
const jti = payload.jti;
const revoked = await redis.get(`revoked:sid:${sid}`) || await redis.get(`revoked:jti:${jti}`);
if (revoked) {
res.set("WWW-Authenticate", 'Bearer error="invalid_token", error_description="session revoked due to risk change"');
return res.status(401).json({ error: "session_revoked", reason: revoked });
}
req.user = payload;
next();
});
}
app.get("/admin/users", auth, (req, res) => {
res.json([{ id: 1, email: "a@example.com" }]);
});
app.listen(3000);
This validates the JWT and then checks Redis for a revoked session or token ID. The gotcha: if you only revoke by jti, a refreshed token with a new jti may stay valid; revoking by sid or a per-user session version is usually the safer unit.
To revoke a session from the terminal:
redis-cli SETEX revoked:sid:sid-7b2c 3600 device.non_compliant
And to verify behavior:
curl -i http://localhost:3000/admin/users \
-H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...'
Expected response shape:
HTTP/1.1 401 Unauthorized
X-Powered-By: Express
WWW-Authenticate: Bearer error="invalid_token", error_description="session revoked due to risk change"
Content-Type: application/json; charset=utf-8
{"error":"session_revoked","reason":"device.non_compliant"}
Further reading
- OAuth 2.0 Token Revocation
- JSON Web Token (JWT) RFC 7519
- OpenID Connect Core 1.0
- NIST SP 800-207 Zero Trust Architecture
- The "Caching" chapter of the MDN HTTP docs
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI