AUTH4D-208 Flows token patches at issuance
The Flows patch_claims (console: Add token claims) and patch_roles (Add token roles) steps now
change the access tokens of the sign-in their flow lets through. Nothing is stored: the patch lives
with the sign-in and its session. No route or wire shape is added. The contract changes are in
packages/contracts (FlowClaimNameSchema, FlowTokenPatchSchema, flowTokenPatch,
mergeFlowTokenPatch, flowDefinitionProblems). The engine is apps/api/src/modules/flows/engine.ts
and issuance is packages/sessions.
Where patches apply
| Trigger | Sign-in | Tokens patched |
|---|---|---|
post_login |
Hosted browser sign-in (/oauth/authorize/continue) |
authorization_code exchange |
post_login |
prompt=none session restore |
authorization_code exchange |
post_login |
POST /t/{tenantId}/guest and GET /t/{tenantId}/guest/authorize |
the guest response, or the code exchange |
device_approval |
POST /device/approval |
the device’s device_code poll |
pre_registration and pre_guest_upgrade flows still only allow or deny; any patch they carry is
ignored. The authorization code and the approved device record hold the patch until the code is
redeemed or the device polls.
Token shape
{
"sub": "usr_…",
"client_id": "cli_…",
"auth4_roles": ["player", "veteran"],
"auth4_meta": { "level": 3, "tier": "gold" }
}
- Claims go into
auth4_metaand roles intoauth4_roles, the AUTH4D-154 claims. Every SDK already reads these, and every SDK refuses unknown top-level claims, so a patch never adds a top-level claim and can’t change one auth4.dev sets. - The stored values (AUTH4D-154, including
set_roleandset_claimchanges from the same flow) are read first. Patched roles join the stored roles, sorted and unique. Patched claims are merged over stored metadata; on the same key the patch wins. patch_rolesremovetakes back roles an earlier step of the same flow added. It never hides a stored role.- ID tokens and UserInfo never carry patches.
Claim names and values
Claim names follow the AUTH4D-154 metadata key rule: ^[a-z][a-z0-9_]{0,39}$, not one of
RESERVED_CLAIM_NAMES (sub, email, scope and the rest) and not starting with auth4. Values
are strings up to 256 characters, finite numbers or booleans; null removes a claim an earlier
step added. A draft with a list value under such a name is refused with 400 validation_failed.
Before AUTH4D-208, names were namespace:name (game:tier). Such names, and their list values,
still parse so stored definitions keep loading and bound flows never start failing closed. They
never reach a token, and publishing a draft that has one answers 400 flow_invalid with a detail
for definition.steps.{n}.claims.{name}.
Limits
- The flow’s own patch must fit on its own: at most 20 roles, and at most 32 keys in 2,048 bytes
of UTF-8 JSON. The step that goes over fails like any failed step, so
onErrordecides (failed_openskips it,failed_closeddenies withflow_step_failed). Namespaced claims don’t count. - At issuance, if the stored roles plus the patched roles exceed 20, the patched roles are left out
and the stored roles are used alone. The same goes for metadata over 32 keys or 2,048 bytes. Each
half is decided separately. A change to stored data therefore never makes a sign-in fail, and a
missing claim can only take privileges away. The Worker logs
flows.token_patch_droppedwith the tenant id only.
Refresh
The patch is stored with the session in the session Durable Object (StoredSession.flowPatch) and
applied again on every refresh_token grant of that session’s family. Stored roles and metadata
are still re-read on every refresh, so management changes show up at once and the patch is merged
over the current values. The patch ends with the session: the next sign-in runs the flow again and
gets a fresh patch, and a guest upgrade starts a new session without one. Flows never run at
refresh, so a newly published flow reaches existing sessions only at their next sign-in.
A session stored by an earlier Worker version has no patch and refreshes as before. A stored patch that no longer parses is dropped rather than failing the refresh.
Rollout notes
An authorization code or device record that carries a patch can’t be read by a Worker version
older than AUTH4D-208 (their schemas are strict). Codes live 60 seconds and device codes at most
their configured lifetime, so a rollback during sign-ins can make those few fail with
invalid_grant or expired_token; the player signs in again.
Source: docs/api-spec/auth4d-208.md