The owner asked for "an auth blueprint + recipe" because every app has a
different auth model and modules will sit on several clouds at once. This is
the recipe: what to do on the module side, what to do on the app side, in
what order, for a new module and for an existing service. canon/02 is the
why; adr/002 is the decision; this is the checklist.
Audience: anyone adding identity to a module or wiring an app to call one.
Vocabulary, once#
- caller: the thing calling (an app per environment, a service, a tool, an agent); verified
- tenant: the org or workspace the work is for; on every request
- scopes: what the caller may do at this module
- attribution: who the app says it acts for; claimed, stored, searchable
- verifier: the module's port that turns a bearer credential into a
caller context;
jwks,keys, orlocal
Users never reach a module.
For a new module#
- Add the verifier port with all four implementations from the template (
jwks,keys,remote,local). Config selects one;localrefuses to start on a public URL. - Require
Authorization: BearerandX-Tenant-Idon every job route;/health,/build-info,/manifeststay open;/health/deep, stats needstats:read. - Check tenant against the caller context's
tenants, scope against the route,envclaim against the module's own env. Reject with 401 (no or bad credential), 403 (credential fine, not allowed), never 404 for an auth failure. - Stamp
caller_id,env,tenant,attributionon the job; everything downstream inherits. - Internal routes (queue callbacks) verify platform identity only: Cloud Tasks OIDC with the module URL as audience and the queue's service account as subject; arq trusts its Redis network. Never a static shared secret on a public URL.
- Register the module as an audience at the issuer (or, before the issuer,
create its
keystable) per environment. Register each expected caller with its tenants and scopes. - Conformance rows: missing tenant rejected; wrong-env credential
rejected; caller credential on
/internal/*rejected;localverifier refuses on a public URL.
For an existing service (the services-api shape)#
- Keep the current credential working: the shared password becomes one row
in a
keystable withtenants: "*"and full scopes. Nothing breaks. - Add
X-Tenant-Idwith a logging-only grace period, then require it. - Issue a per-caller key to each known caller (each app per env, each tool), each with its real tenants and scopes; hand the shared password row a short expiry and watch its use drop to zero; delete it.
- When the issuer exists, switch the verifier to
jwks; keepkeysfor any caller that has not moved; delete rows as callers move. - Separate the environments' identities if they are shared (dev running as prod's service account is the known case).
For an app that calls modules#
- Get one client credential per environment at the issuer (or one key per module before it), scoped to the tenants the app serves and the modules it calls. Store it in the app's secret store, never in the frontend.
- Exchange it server-side for a short-lived token; cache until near expiry; never forward a user's session, cookie, or JWT to a module.
- Put the tenant on every request from the app's own verified org or
workspace context (walmart's
X-Org-Idafter membership check; speedway'srequireWorkspaceAccess); put the user and the workflow run inattribution. - Translate the app's roles into scopes at call time (a member may submit and read; only an admin may cancel, if that is the app's policy). Roles stay in the app.
- Callback receiver: verify the HMAC signature with the per-caller callback secret the issuer hands out; dedup on event id.
- Local development: point at a local module with the
localverifier's static token; nothing else about the integration changes.
For a developer or an agent calling directly#
The debugging path. A personal or per-tool credential at the issuer (or a
key), scoped to dev tenants only, short expiry, revocable on its own. The
/guide prose and the manifest are what an agent reads; the per-item log
route is what it uses when something is wrong. Never the shared app
credential, never a prod tenant from a laptop.
Do-nots#
- Do not forward user credentials to a module.
- Do not put tenant only in a self-declared tag.
- Do not share a credential across callers or across environments.
- Do not verify users in a module.
- Do not use a static shared secret on an internal callback reachable from a public URL.
- Do not create service-account keys to let a non-GCP caller in.
- Do not answer 404 to an authorization failure.