Skip to content

feat(montandon-eoapi): enforce external-token revocation in stac-auth--proxy - #207

Merged
thenav56 merged 3 commits into
developfrom
feat/stac-auth-proxy-token-revocation
Jul 27, 2026
Merged

feat(montandon-eoapi): enforce external-token revocation in stac-auth--proxy#207
thenav56 merged 3 commits into
developfrom
feat/stac-auth-proxy-token-revocation

Conversation

@thenav56

@thenav56 thenav56 commented Jul 23, 2026

Copy link
Copy Markdown
Member

Related to

stac-auth-proxy validates go-api JWTs locally (signature + exp) only, so a token revoked before its natural expiry stays valid for up to a year. Add a revocation check via go-api's verify endpoint (go-api issue #2794).

stac-auth-proxy v1.1.1 has no config hook for token validation, so inject a montandon_revocation.py module (mounted through the configmap) that wraps EnforceAuthMiddleware.validate_token at app-construction time:

  • Reject tokens with no jti or a jti go-api reports inactive (401).
  • Static jti blacklist (env) checked first as a permanent kill-switch.
  • Positive-only in-memory cache, 300s TTL.
  • Verify URL derived from OIDC_DISCOVERY_URL; pod fails to start if unset.
  • Fail policy configurable (deployed fail-open until the verify endpoint is live in prod goadmin, then flip to fail-closed).
  • Patch guard raises on upstream signature drift so revocation can never be silently disabled.

Rename the filters configmap/volume to stac-auth-proxy-modules / custom-modules since it now carries the auth patch alongside the CQL2 filter.

…-proxy

stac-auth-proxy validates go-api JWTs locally (signature + exp) only, so a
token revoked before its natural expiry stays valid for up to a year. Add a
revocation check via go-api's verify endpoint (go-api issue #2794).

stac-auth-proxy v1.1.1 has no config hook for token validation, so inject a
montandon_revocation.py module (mounted through the configmap) that wraps
EnforceAuthMiddleware.validate_token at app-construction time:

- Reject tokens with no jti or a jti go-api reports inactive (401).
- Static jti blacklist (env) checked first as a permanent kill-switch.
- Positive-only in-memory cache, 300s TTL.
- Verify URL derived from OIDC_DISCOVERY_URL; pod fails to start if unset.
- Fail policy configurable (deployed fail-open until the verify endpoint is
  live in prod goadmin, then flip to fail-closed).
- Patch guard raises on upstream signature drift so revocation can never be
  silently disabled.

Rename the filters configmap/volume to stac-auth-proxy-modules / custom-modules
since it now carries the auth patch alongside the CQL2 filter.
Comment on lines +159 to +163
if not _is_active(jti):
logger.info("Rejecting revoked/unknown jti %s", jti)
raise _reject("Token has been revoked")

_cache_put(jti)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If go-api is down and we let the token through, do we then cache that as “active” for the full TTL? If so, could a revoked token keep working for a bit even after go-api comes back? Would it make sense to only cache when we actually got active: true from go-api?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to skip the full TTL during an outage.

Maybe we use a very short TTL, say M seconds. This way we do not add high latency to every request (we have a timeout of 3 seconds for go-api requests). But we still check it every M seconds.

For now, start with 30 seconds for M.

thenav56 added 2 commits July 24, 2026 09:29
Cache each verify outcome with its own TTL instead of one positive TTL:

- verified active:true  -> 300s (GOAPI_TOKEN_VERIFY_TTL)
- fail-open allow during a go-api outage -> 30s (GOAPI_TOKEN_VERIFY_OUTAGE_TTL),
  so revocation resumes within seconds of recovery instead of being masked
  for a full positive-cache window
- definitive deny (go-api reports the jti inactive/unknown) -> 24h
  (GOAPI_TOKEN_VERIFY_DENY_TTL), so a known-revoked token is not re-checked on
  every request

Only definitive 200 active:false denials are negative-cached; a fail-closed
outage rejects but is not cached, so recovery is never masked. The cache now
stores the (allowed, expiry) decision and check_revocation honors a cached
allow or deny before calling go-api.
Address code-review findings in montandon_revocation.py:

- Decode the verify response strictly: only `active is True` allows and only
  `active is False` is a definitive (cacheable) deny. A 200 without a boolean
  `active` (field drift / interstitial served as 200) now rejects WITHOUT
  caching, so a valid token is never locked out for the 24h deny TTL; a stringy
  `"active":"false"` no longer coerces to allow.
- Bound the decision cache with GOAPI_TOKEN_VERIFY_CACHE_MAX (default 50000);
  on overflow drop expired entries then the soonest-to-expire, so a long-lived
  pod seeing many distinct jtis can't grow unbounded.
- Never let a token check become a 500: _verify wraps the call in a catch-all
  mapping to the fail policy, and a non-string jti is rejected before it can be
  used as an unhashable cache key.
- Validate numeric env vars (_num_env): clear RuntimeError on non-numeric or
  negative values instead of an opaque float() crash.

Blocking sync httpx on the event loop is retained by design (bounded by
VERIFY_TIMEOUT, absorbed by the cache).
@thenav56
thenav56 marked this pull request as ready for review July 24, 2026 11:06
@thenav56
thenav56 requested review from batpad and pantierra and removed request for pantierra July 24, 2026 11:52
@thenav56
thenav56 merged commit acbe365 into develop Jul 27, 2026
3 checks passed
@thenav56
thenav56 deleted the feat/stac-auth-proxy-token-revocation branch July 27, 2026 08:36
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.

3 participants