Skip to content

External JWT Token Revocation #2794

Description

@thenav56

1. Problem

go-api access tokens are long-lived (validity of up to one year — a hard client
requirement). stac-auth-proxy currently validates them locally only: JWT signature
against the go-api OIDC JWKS, plus expiry (exp). Local validation cannot detect a token
that go-api has revoked before its natural expiry, so a leaked or withdrawn token
stays valid for up to a year. stac-auth-proxy needs to honor revocation.

2. Approach

Every token carries a unique jti (JWT ID) claim. go-api tracks revocation state
centrally and exposes a lightweight verification (introspection) endpoint: given a
jti, it reports whether the token is still active. stac-auth-proxy keeps all its
existing local checks and adds one call to this endpoint.

  • Local validation (unchanged): signature + exp, via the go-api JWKS.
  • New step: confirm the token's jti is still active.

3. What go-api Provides

Already available:

  • Every token is tracked centrally by its jti, so a token can be identified and acted
    on individually after it has been issued.
  • Tokens carry jti and exp claims.

Not yet available:

  • A revocation flag per token. Administrators can revoke (and un-revoke) a token from
    the go-api admin; revocation takes effect immediately on go-api's side.
  • Self-service token management from go-web-app. Users can revoke
    their own individual tokens directly from go-web-app.
  • The verification endpoint (Section 4) that reports a token as inactive when it is
    revoked, expired, superseded, or unrecognized — a single active: true/false answer.
  • An iat (issued-at) claim added to newly issued tokens. iat is informational and
    not required for validation.

4. Verification Endpoint Contract

Item Value
Method POST
URL https://goadmin.ifrc.org/api/v2/external-token/verify/
Request body {"jti": "<jti-from-token>"}
Response HTTP 200 with {"active": true} or {"active": false}
Malformed jti HTTP 400 (must be a valid UUID)
Auth None required — consistent with go-api's other public endpoints
POST https://goadmin.ifrc.org/api/v2/external-token/verify/
Content-Type: application/json

{ "jti": "<jti-from-token>" }
{ "active": true }

Semantics:

  • active: true — not revoked; allow (subject to stac-auth-proxy's existing checks).
  • active: false — reject. This covers revoked tokens and any jti go-api does not
    recognize (unknown jti{"active": false}, definitively).
  • Malformed jti (not a UUID) → HTTP 400.

Auth: the endpoint requires no credential, consistent with go-api's other public
endpoints. It returns only a boolean and is keyed by an unguessable UUID (jti).

5. What stac-auth-proxy Implements

Per incoming request (framework-agnostic):

  1. Local validation — signature + exp via JWKS. Reject on failure, as today.
  2. Extract jti from the validated token. A token with no jti cannot be checked for
    revocation — coordinate handling with go-api (see fail policy).
  3. Check the cache for this jti; on a usable "active" hit, proceed.
  4. On a cache miss, call the endpoint with the jti.
  5. Enforce: active: true → allow (and cache); active: false → reject with 401 / 403.

Caching (required)

  • Cache positive results only, so a revoked token is never served stale.
  • Short TTL, 1-5 m. Revocation then takes effect within at most one TTL window.
  • Do not cache positive results with a long TTL — it defeats revocation.

Failure policy (endpoint unreachable)

  • Fail-closed (recommended): reject on outage. An outage should not let a
    possibly-revoked token through; the tradeoff is reduced STAC availability.
  • Fail-open: allow on outage. Better availability, but a revoked token can slip through.

go-api recommends fail-closed, but leaves the final call to stac-auth-proxy.
Fail-closed plus the short positive cache above is a reasonable middle ground:
recently-verified tokens keep working briefly through a transient outage, while unverified
tokens are held.

6. Request Lifecycle

Client ──(request + Bearer JWT)──▶ stac-auth-proxy
                                        │
                    1. Verify JWT signature (JWKS) + expiry (exp)
                                        │  fail ─▶ 401
                                        ▼
                    2. Extract `jti` claim
                                        │
                    3. Look up `jti` in local revocation cache
                                        │
                 ┌──────────hit "active"─┴─miss/expired──────────┐
                 ▼                                                ▼
            proceed                            4. POST { jti } ──▶ go-api
                 │                                verify endpoint
                 │                                                │
                 │                          {"active": true|false} ◀┘
                 │                                                │
                 │                    5a. active:true  ─▶ cache (short TTL), proceed
                 │                    5b. active:false ─▶ 401/403
                 ▼                                                │
        forward to STAC backend ◀───────────────────────────────┘

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions