Skip to content

Revoking access

Revocation is the ordinary division of responsibility between an origin and a cache, and it applies to every cached authenticated response: the shared one, the partitioned one, and the Vary-keyed one alike.

An entry never crosses callers, and it stops being served when the freshness you set runs out. If you configure an auth provider for the header, authentication runs on every request, cache hit included: a credential whose signature does not check out, whose exp has passed or is missing (unless the provider sets allow_missing_exp), or that fails a required claim is refused and gets nothing back. Without a provider, GraphPilot does not verify the credential at all, and an entry keyed on its raw value keeps being served to whoever sends that exact value until it expires or is purged.

A change that leaves the credential itself valid: a session revoked server-side, a role taken away, an item shared and then unshared. Your origin knows, and during the freshness window it is not being asked. Nothing at the edge can infer it, and a cache that tried would have to consult your origin on every hit, which is no longer a cache.

Revoking access is two steps rather than one, exactly as it is behind any CDN: revoke at your origin, then purge. A purge by surrogate key reaches every cached response that touched a given key across all its variants, so if your responses carry a per-user key such as User:id:482910, purging that key is the second step. See surrogate keys for attaching one.

Two things bound how much that matters. The window is exactly what you set, the lifetime plus any stale window, so a short lifetime on permission-sensitive data is the control you have without acting at all. And a response whose correctness depends on a permission check at the moment of reading should not be cached: Cache-Control: no-store, or leave it out of the annotated part of your schema.

Ben is an admin in the organization org-acme and becomes a viewer. The service caches three kinds of answer for him, partitioned as in three rungs in one service: products (PUBLIC), the organization’s report (a named scope keyed on organization and role) and his own profile, Me (PRIVATE).

  1. Change the role and stop new tokens carrying the old one. Update the role at your identity provider and revoke Ben’s sessions or refresh tokens there, so the next token he is issued says viewer. From that token on, his requests derive x-gp-role: viewer and land in the viewer entry of the report.

  2. Know the window that is left. The token Ben already holds still says admin and still verifies, so until it expires he keeps landing in the admin entry, served from the cache without your origin being asked. The edge checks exp on every request, cache hits included, so the window ends with that token. Shorter-lived access tokens shorten it, and max_validity on the provider refuses any token issued longer ago than the limit, whatever its exp says.

  3. Leave the shared entries alone. The admin entry of the report is still correct for the other admins in org-acme, and purging it would not keep Ben out: his next request with the old token misses, reaches your origin and is stored in the admin entry again.

  4. Purge what was computed for him alone. His Me entry was answered while he was an admin. A response containing a Me object carries its entity key, here Me:id:user-ben, so one purge removes it wherever it was stored:

    Terminal window
    gpilot purge --service shop-api 'Me:id:user-ben'

    Read the key off the response’s gp-surrogate-key header, if your service sends it, rather than constructing it; see verifying a purge.

A per-user partition built with pseudonymize, such as Me under a named scope keyed on the organization and a pseudonym of the user, is revoked exactly like the example above:

gpilot.toml
[[cache.policy.vary]]
name = "x-gp-organization-user"
value = "auth:demo.org_id + ':' + pseudonymize(auth:demo.sub)"
scope_only = true
  • You purge by the same key. A purge never addresses a partition: it addresses surrogate keys, and those come from the response (its entity keys, such as Me:id:user-ben, and its types) and from the request’s cache key, never from a vary rule’s value or the private value. So gpilot purge --service shop-api 'Me:id:user-ben' removes Ben’s entry whether the rule reads auth:demo.sub or its pseudonym, and you never need to know the pseudonym.
  • A purge by cache key reaches every partition. ck: and purgeByRequest remove the entry for that request in every caller’s partition, not only Ben’s. Use them when the answer is wrong for everybody, not to take one person’s access away.
  • A demotion does not change the pseudonym. It is derived from sub alone, so Ben keeps the same partition after the role change, and his old token keeps landing in it until it expires. If what Me contains depends on the role, purge it as in step 4. Moving Ben to another organization changes the organization part of the value, so tokens that carry the new organization land in a new entry, while the old token still reads the old one until it expires.