Software EngineeringAPI Management

Caching Service Tokens with a Gravitee Loopback API

How I worked with Gravitee's response-cache constraint by putting the token endpoint behind a small internal API.

Black-line diagram of a business API calling an internal loopback API that caches the authentication server's token response.
Lead image Black-line diagram of a business API calling an internal loopback API that caches the authentication server's token response.
On this page

The gateway would not act as the token store I had in mind.

Each request through our business API made an HTTP callout for a service token, even though that token remained valid for hours. Caching it seemed straightforward. The Cache policy available to us, however, cached an upstream HTTP response and returned that response to the caller. It did not load an arbitrary value into a context attribute and continue the same request flow.

That constraint changed the useful question. Instead of asking how to make the response cache behave like a key-value store, I asked:

What if the cached HTTP response were the token response the caller already expected?

The result was a small internal API between the business API and the authentication server. The internal API returned the authentication response, while Gravitee’s Cache policy stored that full response. I call this the loopback pattern here because one API managed by the gateway calls another API on the same gateway.

Loopback cache flow showing a cache miss going to the authentication server and a cache hit returning the stored token response.
The internal API gives the response cache a complete token response to store and return.

The constraint that shaped the design

My first design assumed this flow:

read token from cache
  -> put token into a context attribute
  -> continue the business request

The Cache policy behaved differently:

read response from cache
  -> return the cached response to its caller
  -> stop processing that API request

That behavior is appropriate for a response cache. It was only awkward because I was trying to use it as something else.

The authentication server already returned JSON that the business API’s HTTP Callout policy knew how to parse:

{
  "access_token": "<service-token>",
  "token_type": "Bearer",
  "expires_in": 38399
}

So I kept that contract and changed the callout target:

Business API -> HTTP Callout -> Internal token API -> Authentication server

The internal API has one job: return a valid service-token response, from the response cache when possible.

The two-API shape

The Business API handles the client request. It calls for a service token, extracts access_token, adds the Authorization header, and sends the request to the backend.

The Internal token API is a proxy in front of the authentication server. Its Cache policy can satisfy a request immediately on a hit or forward a miss to the authentication server and cache the successful response.

Configuration map showing the Business API HTTP Callout target changed from the authentication server to the local internal token API.
The Business API keeps the same callout and extraction flow; only its target changes.

The full request path is:

Client
  -> Business API
  -> HTTP Callout to http://localhost:8082/loopback
  -> Internal token API
  -> Cache policy
      -> hit: return cached token response
      -> miss: call authentication server, cache success, return response
  -> Business API extracts access_token
  -> Business API adds the Authorization header
  -> Backend

This preserved the business API’s existing callout contract. It did not, by itself, make the design safe: the cache scope, token lifetime, internal route, and failure behavior still needed deliberate configuration.

Keep the internal API narrow

Our internal API was a transparent proxy with a Cache policy in front of it:

SettingValue in this setup
API nameAuth Loopback
Entrypoint/loopback
Local callout URLhttp://localhost:8082/loopback
Backend targetthe existing service-token endpoint
Public exposurenone
Main policyCache

It did not need Groovy, response rewriting, or a custom plugin. The authentication server’s response was already the response the caller needed.

localhost is specific to this deployment shape. In a container or multi-node deployment, the hostname and route must resolve back to the intended gateway instance. More importantly, the internal API must not become a public token endpoint merely because it is managed alongside public APIs.

Configure the cache around the credential

Our tokens lasted roughly ten hours, so I used an eight-hour cache TTL. That left a margin before token expiry instead of assuming the cached response and the token would expire at exactly the same instant.

The relevant settings were:

Cache settingValue in this setupReason
Time to live28800 secondsExpire the cached response before the roughly ten-hour token.
Cache keyservice-token-generalAll intended callers used the same service credential and permissions.
ScopeAPIShare one cached response across the intended callers.
Methodsinclude POSTThe token endpoint uses POST; it is not in the policy’s default method set.
Response conditionsuccessful responses onlyDo not retain authentication or server errors.

These settings are not universal defaults. Gravitee’s Cache policy documentation warns that API scope shares cached data across consumers. That was acceptable only because this API represented one service credential with one permission set.

A static key is wrong if the returned token varies by environment, backend, tenant, credential, audience, or scope. Those inputs need to be separated in the key, for example:

service-token-{environment}-{backend}-{scope}

The same caution applies to TTL. It must be shorter than the minimum token lifetime the authentication server can issue under this flow, not merely shorter than the lifetime seen in one example response.

Point the existing callout at the internal API

Before the change, the Business API called the authentication server directly:

HTTP Callout URL:
http://example.com/authorization-server/token

After the change, it called the internal API:

HTTP Callout URL:
http://localhost:8082/loopback

The request method, body, Content-Type, extraction rule, and header transformation stayed the same. The extraction remained:

access_token = {#jsonPath(#calloutResponse.content, '$.access_token')}

And the backend header remained:

Authorization: Bearer {#context.attributes['access_token']}

This matches Gravitee’s documented HTTP Callout model: the callout response is available as calloutResponse, and selected values can be placed in request context variables.

What two requests showed

I sent one request through the Business API with a cold cache and then repeated the same request a few seconds later:

Cold-cache TTFB: 1.416641 seconds
Warm-cache TTFB: 0.635187 seconds

The backend logs showed the same access token on both requests, including identical jti and issued-at claims. That result was consistent with the second call using the cached token response.

Two horizontal latency bars showing a 1.416-second cold-cache request and a 0.635-second warm-cache request.
One cold request and one warm request showed the expected latency drop; this was a functional check, not a benchmark.

Two requests do not establish a latency distribution, and matching token claims alone do not prove which network calls occurred. For stronger verification, I would also inspect authentication-server request logs or gateway traces and repeat the test across a controlled sample. Here, the pair was enough to check that the configuration behaved as expected before moving on to operational safeguards.

Operational boundaries

The loopback API is small, but it handles credentials and sits on the request path. These are the failure modes I would check before relying on it.

Cache only successful responses

The Cache policy defaults to caching successful 2xx responses. I would still verify the deployed policy version and configuration so 401, 403, 429, and 5xx responses pass through without being retained. Caching an authentication failure extends a short upstream problem to the cache lifetime.

Keep the route private

The internal API returns a usable token response. Restrict it to the gateway process or trusted internal network paths, and check the actual listener and ingress rules rather than treating the word localhost as the control.

Observe both sides of the cache

Logs or traces should let an operator distinguish a cache hit from a call to the authentication server. That makes it possible to spot repeated misses after restarts, unexpected expiry, or authentication failures that never reach the cache.

Account for every gateway node

With a node-local cache, each gateway node warms independently. That may be acceptable, but it means another authentication request after expiry or a node restart. It should be an explicit capacity and failover assumption. A distributed cache changes that behavior and adds its own dependency.

Work with the response boundary

I began by trying to force a response cache to provide hidden state inside another request. The simpler design was to make the cached response a legitimate API response and consume it through the HTTP Callout mechanism already in place.

That did not remove the need to reason about credentials, scope, expiry, routing, and failure. It did give each piece one clear job: the internal API owns the cacheable token response, and the business API consumes that response without knowing how it was obtained.

Series

Gravitee API Caching

  1. 01 Finding an 800ms Token Callout in a Gravitee API Gateway
  2. 02 Caching Service Tokens with a Gravitee Loopback API Current note
  3. 03 Caching Service Tokens in Gravitee with a Loopback API

Continue reading

Complete index →