Skip to content

feat: cache the frontier authn token call in redis - #23

Open
rohilsurana wants to merge 6 commits into
mainfrom
feat/cache-authn-token
Open

feat: cache the frontier authn token call in redis#23
rohilsurana wants to merge 6 commits into
mainfrom
feat/cache-authn-token

Conversation

@rohilsurana

@rohilsurana rohilsurana commented Sep 2, 2026

Copy link
Copy Markdown
Member

The problem

Every request through this plugin makes an HTTP call to Frontier to exchange the
cookie or bearer for a user token. Two hundred requests from one browser tab
means two hundred identical calls.

What this does

It caches that exchange in Redis. The lookup is Redis first, then Frontier on a
miss. Redis is shared by every pod, so a token is fetched once for the fleet
rather than once per pod.

The default TTL is 5 seconds, and it is deliberately short. A cached token means
a change to someone's access is not noticed until the entry expires.

Caching needs Redis. Set redis_host and it works. Leave it unset and
cache_ttl does nothing on its own, so every request goes to Frontier exactly
as it does today. There is no deployment step beyond pointing it at a Redis.

The cache is three files: cache.lua is the whole chain at 131 lines,
redis.lua is the driver and its breaker, and jwt_decoder.lua gains a guard
against a malformed token. access.lua changes only in where it asks for a
token and how it ends a failed request.

Config

Field Default What it does
redis_host unset Setting it turns caching on
cache_ttl 5 Seconds a token is reused for. 0 turns caching off
cache_cookie_names ["sid"] Only these cookies go into the cache key
cache_exp_skew 2 Clock skew allowed when clamping the TTL to the token's expiry
redis_timeout 100 Milliseconds. A healthy Redis answers in well under one
redis_key_prefix frontier:authn: Prefix on every key
redis_breaker_seconds 10 How long a worker stops trying Redis after a command fails

Plus redis_port, redis_username, redis_password, redis_database,
redis_ssl, redis_ssl_verify, redis_server_name, redis_keepalive_ms and
redis_pool_size. Full table in the README.

An entry costs about 1KB in Redis, so size it as users active within the TTL
window, times 1KB.

What is cached, and what is not

  • The key is a sha256 of the named cookies, the authorization header, and
    the config that decides what an entry means. Sessions are never stored as
    plaintext keys, and two routes that would resolve a credential differently
    cannot share an entry.
  • Only the named cookies go into the key. Browsers send analytics and
    consent cookies that change constantly, so keying on the whole cookie header
    would miss on nearly every request.
  • A request with no credential is never cached, so anonymous requests cannot
    share an entry.
  • A failed exchange is never cached. Someone who just got access is not
    locked out for the length of the TTL.
  • The entry expiry is clamped to the token's own exp, minus
    cache_exp_skew. Since that is the only thing that sets the expiry, a token
    still in Redis always has at least the skew left on it, so the read side never
    has to check again.
  • Only the authn call is cached. The authz_url permission check still runs
    on every request.
  • There is no lock. Several requests arriving together with the same new
    credential will each fetch a token. They all write an equivalent entry, and
    everything after that is served from Redis.
  • A token is read for its exp when it is stored, and for the claims that
    become headers when it is used. Nothing else about it is assumed, so one that
    is valid JSON but not shaped like a JWT is refused rather than half applied.

How Redis behaves

It never fails a request. Redis is a cache, not an authority. A connect
error, a timeout, a bad reply, even a raise, gets logged and the plugin carries
on to Frontier. When a command fails, that worker stops trying that instance for
redis_breaker_seconds, so an outage cannot make every request pay the timeout.
The pause is per instance, so one bad Redis does not stop the worker using
another. A wrong password or database index is a config mistake rather than a
broken instance, so those are logged without starting the pause.

Guard write access to this Redis. The plugin does not check the token
signature, with or without Redis. It trusts what Frontier hands back. So
anything that can write these keys can put a token of its choosing in front of
your upstream. The entries also hold live user tokens, which is more sensitive
than something like rate limit counters. Turn on auth and SSL if the instance is
shared or reachable from outside the cluster.

One behaviour change

If Frontier answers 200 but the plugin cannot find a token in the response, it
now returns 401. It used to pass nil into set_header and fail with a 500:

invalid header value for "x-user-token": got nil, expected array of string

This had to change, because nil cannot pass through a cache cleanly. 401 is also
the right answer. Frontier returns a proper error status for every real auth
failure, so this state only means the plugin is pointed at the wrong endpoint,
or token_response_field does not match what the server returns. The 401 cannot
hide a successful auth.

Performance

Measured against a real Frontier, a real Redis and Kong in DB-less mode.
Absolute numbers come from Docker on macOS, so read the gaps rather than the
values.

One session making requests as fast as it can for 20 seconds, cache_ttl at 5:

Requests served Frontier calls
Caching on 455 4
Caching off 262 262

Four calls in a 20 second window is what a 5 second TTL should give. The same
client also got through 1.7 times as many requests, because it was not waiting
on an auth call every time.

Kong's own CPU per request, from the container's cgroup accounting, and latency
with the plain and cached routes interleaved:

Kong CPU per request Median latency
No plugin 0.224 ms 6.8 ms
Plugin, Redis hit 0.330 ms 7.4 ms
Plugin, caching off 1.731 ms 39.2 ms

So the Redis hop adds about 0.6ms and saves about 32ms.

Testing

Suite Result
Unit, Kong 3.4 43 passed
Unit, Kong 3.9 43 passed
End to end, 3 pods and a real Redis 29 passed

The unit tests cover the key builder, the TTL clamp, hostile tokens and the
cache behaviour, with Redis stood up as a table so both paths can be driven
directly. The end to end suite runs against a real Frontier, with postgres and
SpiceDB, using a real session cookie from the mailotp flow, and counts actual
AuthToken calls from Frontier's own metrics.

Two things only a real gateway shows:

  • No shared memory zone is configured on any pod, and the suite asserts none
    of them logs a warning about a missing one.
  • A Kong 3.4 pod and two 3.9 pods share one Redis correctly, both
    directions, which matters if the fleet is ever mid-upgrade.

Works on Kong 3.4 and later, using only modules that ship with Kong and
OpenResty.

Why not a node cache as well

An earlier version of this kept a shared memory cache on each pod in front of
Redis, so the common path would pay no network at all. It is not here because
it did not earn its keep. It needed a lua_shared_dict added to kong.conf on
every gateway, it brought mlcache and a single-flight lock, and it needed
extra machinery to stop a token going stale twice over on its way through two
caches. Three adversarial reviews found sixteen defects on this branch, and by
the end every open one lived in that layer and none lived in Redis.

The measurements say it was not buying much. The Redis round trip it would have
saved costs 0.6ms of latency and no measurable CPU: 0.330ms per request against
0.34ms for the node memory hit it replaced.

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: d69ddd23-4050-4cf2-8c80-3d933ce9252e


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@rohilsurana rohilsurana changed the title feat: cache the frontier authn token call feat: cache the frontier authn token call, with an optional shared redis layer Sep 3, 2026
@rohilsurana rohilsurana changed the title feat: cache the frontier authn token call, with an optional shared redis layer feat: cache the frontier authn token call in redis Sep 8, 2026
@rohilsurana
rohilsurana force-pushed the feat/cache-authn-token branch from 1cf7c34 to c037ab6 Compare September 8, 2026 18:38
@rohilsurana
rohilsurana marked this pull request as ready for review September 8, 2026 19:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant