Auth Scenario Tester
Scenarios 1–51 below are the usage flows of iam-api-list.md §2 — numbered and titled to match it, so §2.14 in the doc is 14 here — and 52 is the platform bootstrap from iam-operational.md §1. Requests are sent directly from your browser to the Kong gateway (http://localhost:8080) — no app-side proxy — so you are exercising the real gateway↔IAM orchestration: klp-auth-guard verifies the access token, forward-auths to IAM /internal/validate, mints the x-gateway-context, and proxies to the IAM service. Because this is a cross-origin call, Kong must allow CORS from http://localhost:4300 (incl. credentials and the OPTIONS preflight) or the browser will block the response.
How to use
- Make sure Kong (:8080) and IAM (:3100) are up and the routes are loaded.
- Empty database? There is no operator yet and none can be created through the API — start at 52 Creating the genesis operator (Genesis, the last menu item) to seed the first operator out of band and bring it online. Everything under Operator Auth needs that account to exist first.
- Open a scenario with a login step — 2 / 3 / 9 / 10 / 14 (tenant) or 33 (operator) — and run it; the response's
accessTokenis captured automatically into the top Authorize bar. - Protected/admin endpoints then send
Authorization: Bearerautomatically; the gateway turns that token into the verified context the IAM service consumes. Public endpoints (🌐) send no Bearer token — there is no token input on them. - Fill path/query params and JSON bodies Swagger-style, then Send.
- Every card carries a Field guide (what each input means and where its value comes from) and a Response guide (every property that comes back and what each value implies) — open them instead of guessing.
Seeded smoke user: smoke.user@example.com / Password123! (admin in tenant A). Out-of-band values (email verify links and short codes, password resets, invitations, OTP/TOTP codes, login-challenge codes, device and registration tokens) are never returned by the API — use the ⛓ out-of-band helper shown on the card that consumes them to mint one locally (dev only), then copy the value into the form.
Four things to know before the sign-up and sign-in groups
- Each front door opens with the scenario that renders it. 1 asks which sign-up methods are live, 7 which sign-in methods are. They are separate policies — one governs how an account may be created, the other how an account that already exists may authenticate — written from two separate operator scenarios (39 / 40), and they may disagree. A sign-in screen rendered from the registration policy will offer a method login refuses, or hide one it accepts.
- Login takes an
identifier— an email or an E.164 phone, discriminated by shape (scenarios 9 / 10).{ email }still works as a deprecated alias, so nothing breaks. On a browser IAM already recognises the body needs only{ password }: the device cookie names the account, which is what lets the “Welcome back” screen (8) avoid asking for an identifier it can only ever show masked. - A login may be a chain.
POST /v1/auth/logincan answer with achallengeinstead of tokens, and each hop is cleared at/v1/auth/login/challenge/verifyuntil the plan is exhausted (scenarios 11–13). The whole chain sits behindDEVICE_TRUST_ENABLED; with it off (the default) login behaves exactly as it always did and only 9 / 12 apply. - Link and short code are two halves of one challenge. Every out-of-band message (email verification, invitation) carries a long link and a 6-digit code on the same row — redeem either, and the other dies with it.
The single-hop /v1/auth/mfa/verify and /v1/auth/mfa/otp/request routes, and the one-shot /v1/auth/phone/register, are deprecated but fully working — rewired onto the same engine. They are shown where they still apply so existing clients can read their own flow.
One session, many apps
Seven scenarios — 16, 17, 19, 41, 42, 49 and 50 — cover the multi-app surface. In the first six, every route sits behind MULTI_APP_ENABLED and answers 404 when it is off; they carry a ⚑ badge in the menu so a 404 there reads as the flag rather than a wrong path. 50 carries none, because the doc gates its two join routes behind no flag.
- Three separate acts, deliberately. Registering an app (platform configuration, one row serving every tenant) is not enabling a tenant for it (access control, per tenant) — both live in 41 — and neither grants any individual anything, which is 49. Roles say what a member may do inside an app; grants say which apps they may enter at all.
- …and one act the member performs themselves (50). An app the operator marked
is_auto_enabled_on_registeris free to acquire at sign-up — a visitor with no account gets a tenant, an enablement and a grant in one transaction just by filling in the registration page. Someone who already has an account, opening that same page, is refused409 …_already_registered, and had no other route: 50 is that route, and gates on the same registry predicates the registration hand-off uses, refusals included. It is the only app-surface scenario with no ⚑ badge — the doc puts/v1/me/app-join-targetsand/v1/me/app-joinsbehind no flag; only the drop-down check at its last step is behindMULTI_APP_ENABLED. It is not the paid subscription path: an app the operator has not opted in is refused, and reaching it means subscribing. - A join is not finished until you switch (50). A session carries one tenant. Join into organization B while signed in as A, redirect without switching, and the app receives a token scoped to A — which has no enablement for it — so the caller meets the very refusal they came to escape, having just successfully joined. That is why
switch-tenanthas its own card in 50 rather than a footnote, and why the join response returns atenantIdeven for an organization it has just founded. - A grant is live for authorization before it is visible in the app. Granting (49) emits no projection event, and for a first grant nothing else does either — the reconciler walks only ledger rows that already exist, and the sweep that would enrol a new pair ships report-only (
PROJECTION_SCOPE_SWEEP_REPAIR_ENABLEDdefaultsfalse). So a member granted an app after their tenant's backfill finished can act through the gateway while being absent from the app's own copy, and waiting does not fix it. The repair is an operator re-sync; the diagnosis is 42. - Model A and Model B sit side by side. 15 is the older shared-login probe (
OAUTH_ENABLED): the app asks IAM to validate a return URL and IAM renders the login. 16 supersedes it — a standards OAuth 2.1 authorization request returning the app's own app-scoped tokens. Different flags, rolled out independently. - Authorize is not a
fetchcall.GET /v1/oauth/authorizeanswers302and carries no CORS headers under any circumstances, because OAuth 2.1 reaches it by navigation. Its card opens the URL in a new tab and takes the redirect back by paste. The 🔑 PKCE control mints the verifier/challenge pair and fills the verifier into the token exchange, because a mismatch there answers a deliberately undifferentiated400 invalid_grant. - N chains, one session. 18 renews the session; 19 is what lets two products stay open in two tabs without either signing the other out. Each app rotates on its own
/v1/auth/apps/{clientId}/refreshroute — and/v1/auth/refreshanswers 404 wherever the flag is on. The chains are isolated; the session is not. Switching tenant (20) repoints every live chain at once, and revoking the session (21) kills all of them — there is no per-app logout. - The two CORS preflights have no cards, on purpose.
OPTIONS /v1/oauth/token(§3.90a) andOPTIONS /v1/auth/apps/{clientId}/refresh(§3.92a) are issued by the browser, never by application code, so every Send on the token and app-refresh cards already exercises them — and a hand-rolledfetchof one could only ever be blocked. They differ in granularity because of what a preflight can identify: the refresh route names the client in the path, so its allow-list is genuinely per-client and refuses another registered client's origin; the token route carries the client id in the body, so its allow-list is registry-wide and the per-client binding is enforced by the code binding and PKCE instead.
One invitation, four scenarios
44–47 follow one invitation in the order it actually travels, and each of the last two exists because a plausible-looking shortcut is wrong.
- Ask before you render (46). One identifier is one identity across every tenant, so
POST /v1/invitationsreuses an existing account rather than duplicating it — and the invitee therefore arrives at the accept screen in one of two entirely different situations.POST /v1/auth/invitations/describeanswers which, without consuming the invitation, sopasswordis sent only when the account has no credential yet. Both mismatches are422. That matters beyond ergonomics: accept used to overwrite the password unconditionally, which let a tenant-scoped invitation act as a global password reset for any address an admin could name — and bring adisabledidentity back to life. - Resend, don't re-invite (47).
POST /v1/invitationsanswers409while an open invitation exists — even a lapsed one, because the open-invite index is partial onused_at IS NULLand cannot consider expiry.POST /v1/invitations/{userId}/resendis the only path that helps during the link's 7 days: it consumes the open row (both old secrets die) and mints a fresh link + code on the invitation's original channel. Mint the ⛓ helper value after the resend — anything copied before it is already dead.
How the scenarios are grouped
- Tenant Auth (1–32) — the public, tenant-facing profile: signing up, signing in, sessions & devices, passwords, MFA, profile & credentials, and the platform discovery surface. Sub-headings follow the doc's own purpose clusters, so counterparts sit adjacent: enrol-MFA next to disable-MFA, add-phone next to remove-phone, sessions next to devices, one app (18) next to many (19).
- Operator Auth (33–43) — the internal control plane: operator sign-in (MFA mandatory, no device trust), operator & role administration, tenant lifecycle, act-as, the cross-tenant audit read, the two global policy writes — registration (39) and login (40), separate levers on purpose, since retiring a channel usually means editing both — the app registry (41) next to diagnosing an app that is missing a member (42), and internal ops.
- Both (44–51) — tenant-admin flows an operator also reaches via act-as. The invitation half is four scenarios in the order an invite actually travels: sending it by email (44) or by phone (45), the accept screen discovering the invitee already has an account (46), and re-delivering one that never arrived (47). Then member/role administration (48). The app half splits the same dimension two ways — an admin deciding which apps a member may enter (49) and a member acquiring a free-to-acquire app for an organization they administer (50) — and the tenant audit log (51) records both.
- Genesis (52) — platform bootstrap. Outside §2 (it is
iam-operational.md §1) and deliberately last, because you only ever run it once per platform.