switching clears the token & responses
No token

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

  1. Make sure Kong (:8080) and IAM (:3100) are up and the routes are loaded.
  2. 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.
  3. Open a scenario with a login step — 2 / 3 / 9 / 10 / 14 (tenant) or 33 (operator) — and run it; the response's accessToken is captured automatically into the top Authorize bar.
  4. Protected/admin endpoints then send Authorization: Bearer automatically; 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.
  5. Fill path/query params and JSON bodies Swagger-style, then Send.
  6. 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/login can answer with a challenge instead of tokens, and each hop is cleared at /v1/auth/login/challenge/verify until the plan is exhausted (scenarios 1113). The whole chain sits behind DEVICE_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_register is 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 refused 409 …_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-targets and /v1/me/app-joins behind no flag; only the drop-down check at its last step is behind MULTI_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-tenant has its own card in 50 rather than a footnote, and why the join response returns a tenantId even 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_ENABLED defaults false). 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 fetch call. GET /v1/oauth/authorize answers 302 and 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 undifferentiated 400 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}/refresh route — and /v1/auth/refresh answers 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) and OPTIONS /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-rolled fetch of 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

4447 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/invitations reuses 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/describe answers which, without consuming the invitation, so password is sent only when the account has no credential yet. Both mismatches are 422. 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 a disabled identity back to life.
  • Resend, don't re-invite (47). POST /v1/invitations answers 409 while an open invitation exists — even a lapsed one, because the open-invite index is partial on used_at IS NULL and cannot consider expiry. POST /v1/invitations/{userId}/resend is 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.

Signing up

1Rendering the right sign-up formBefore showing any sign-up UI, the client asks which registration methods are live so it renders an email form, a phone form, or both.1 endpoint(s)2Signing up with emailA new user signs up with email + password, which also creates their organization (tenant) and makes them its founding admin. Email verification is required before the first login — and verification itself now ends in a session.4 endpoint(s)3Signing up with a phone numberA new user signs up with a phone number; this also creates their tenant + founding admin (full parity with email sign-up). Since Enhancement 4 it is a three-step flow that ends in a real password — the account is no longer password-less.4 endpoint(s)4Signing up with email + phone (link mode)A user wants both an email/password login and a phone login. Under combinationMode=link (the default) this is a two-step journey: sign up one way, then link the other credential. The linking step is independent of the sign-up policy.6 endpoint(s)5Signing up with email + phone (atomic mode)A single sign-up that supplies and verifies BOTH credentials at once: the phone proven inline by its OTP, the email proven out-of-band afterwards. Routed here when the operator has set combinationMode=atomic and allowedMethods holds both methods.4 endpoint(s)6Resuming an interrupted sign-upA user closed the tab, lost the SMS, or came back days later. They re-enter only the email or phone they started with, and the API says which step to render — for every sign-up path above and for a pending invitation.2 endpoint(s)

Signing in

7Rendering the right sign-in formThe mirror of scenario 1, for the other half of the front door: before showing any sign-in UI the client asks which LOGIN methods are live, so it offers a password form, a one-time-code form, or both. A different question from scenario 1 — that policy governs how an account may be CREATED, this one how an account that already EXISTS may authenticate.1 endpoint(s)8Recognising a returning browserBefore any credential is typed, the login screen asks whether THIS browser has signed in here before, so it can render “Welcome back, Budi” instead of a blank form. Behind DEVICE_TRUST_ENABLED; always { known: false } when off.2 endpoint(s)9Logging in with email & passwordThe base login: email + password, on a browser this IAM already recognises, with no second factor enrolled (the LE2 path). Whether MFA or a device challenge applies is not something the client can check beforehand — the login response itself reveals the branch.1 endpoint(s)10Logging in with a phone & passwordA phone-first account (from scenario 3) signs in with its NUMBER and password — the LE1 path. New in Enhancement 4: before it, the phone path never checked a password and the password path never accepted a phone.1 endpoint(s)11Logging in on a new deviceThe password was right, but this browser has never signed in here before, so IAM adds exactly ONE challenge — delivered on the account's primary channel (LN1 phone mode / LN2 email mode). Routed here when DEVICE_TRUST_ENABLED is on and no valid device_token cookie was presented.3 endpoint(s)12Logging in with a second factorThe account has an MFA method enrolled, so login returns a challenge instead of tokens — even on a recognised device (LE3). A recognised device never skips the MFA hop: device trust is a convenience, an enrolled second factor is a commitment.4 endpoint(s)13Logging in on a new device with a second factorThe hardest case (LN3): a new browser AND an enrolled MFA method, so the user clears TWO challenges in sequence. This is the reason the single-hop mfaToken was replaced by a chain.4 endpoint(s)14Logging in with a phone (one-time code)An existing phone user signs in with an OTP and NO password at all — primary phone authentication, distinct from the phone-OTP second factor (scenario 12) and from phone + password (scenario 10). Deliberately untouched by Enhancement 4 AS AN AUTHENTICATION FLOW: no device challenge, and clearing it earns no device trust; amr stays [sms]. The one exception is about ADDRESSING, not authentication — a device_token cookie may supply the destination so the number can be omitted.2 endpoint(s)15Entering a partner app (cross-app login)A relying app (e.g. web-it-cms) sends the user to IAM to sign in and wants them bounced back afterwards. IAM validates the return URL against the registered client BEFORE rendering anything, which is where the open-redirect hole is closed. Behind OAUTH_ENABLED — the route 404s when off. THIS IS MODEL A, AND SCENARIO 16 SUPERSEDES IT: here the app asks IAM to validate a URL and IAM renders the login; there, the app sends a standards OAuth 2.1 authorization request and receives its OWN app-scoped tokens rather than riding the shared session. The two are gated by different flags (OAUTH_ENABLED here, MULTI_APP_ENABLED there) and can be rolled out independently, but an app that has moved to Model B has no use for this probe.2 endpoint(s)16Signing in through an app's authorization request (OAuth 2.1)The full Model B front door, and the successor to scenario 15: the app does not ask IAM to validate a URL and then render its own login — it sends a standards OAuth 2.1 authorization request and receives a code it redeems for its OWN app-scoped tokens. The app must already be registered (scenario 41) and granted to the member (scenario 49).4 endpoint(s)MULTI_APP_ENABLED17Switching apps from the drop-down (hand-off)The in-product counterpart of scenario 16: an already-signed-in user picks another app from the drop-down and lands in it signed in, without a round trip through a login screen.3 endpoint(s)MULTI_APP_ENABLED

Sessions & devices

Profile & credentials

33Signing in (operator authentication)Operators authenticate via /v1/admin/auth/* over the internal-only ingress, with MFA mandatory (TOTP — the tenant-facing phone-OTP method does not apply). The operator access token is minted here (aud: iam-platform); the gateway then verifies it to mint the x-gateway-context every other /v1/admin/* route consumes. Device trust does NOT apply to operators — by decision, not omission: a mandatory second factor on every login is the control, so sign-in is identical on a familiar and an unfamiliar machine whatever DEVICE_TRUST_ENABLED is set to.6 endpoint(s)34Onboard an operator and manage rolesList existing operators, create one (setup token out-of-band), grant a time-bounded role with step-up, confirm, review grants, and later revoke. Requires iam-platform-admin throughout — so the VERY FIRST operator cannot be created here: it is seeded once, out of band (scenario 52).6 endpoint(s)35Provision a tenantReview existing tenants, create a tenant + founding-admin membership in one audited transaction (single-use 7-day invite out-of-band), then inspect it. Requires tenant-lifecycle-operator.4 endpoint(s)36Decommission a tenant (suspend → soft-delete → hard-delete)Suspend → soft-delete → hard-delete. Hard delete is two-phase and four-eyes. Requires tenant-lifecycle-operator. Restore is the counterpart of suspend — and the only lifecycle action that needs no step-up.4 endpoint(s)37Support acts inside a tenant (act-as)Spans both profiles. Mint a short-lived, tenant-scoped token on the control plane — the cross-tenant decision is made once, here: audited, time-bounded, reason-bound — then use it against the public surface. Requires support-acting-in-tenant.4 endpoint(s)38Investigate with the audit log (cross-tenant)The canonical cross-tenant forensic read. Allow-listed filters only; cursor-paginated. Requires security-investigator. ip / userAgent are deliberately not exposed.1 endpoint(s)39Configure the self-registration policySpans both profiles. Read and set the single global policy that governs which sign-up methods the public profile offers for every tenant. The write lives here on the control plane; the public profile only reads it. Requires iam-platform-admin.3 endpoint(s)40Configure the login policySpans both profiles. The sign-in counterpart of scenario 39: a single GLOBAL setting governing which methods an EXISTING account may authenticate with, for every tenant. The write lives here on the control plane; the public profile only reads it. Requires iam-platform-admin. Restricting sign-up and restricting sign-in are separate levers on purpose — one must not quietly move the other.3 endpoint(s)41Register an app and enable it for a tenantStanding up a new product app is TWO separate acts, deliberately: registering the app (platform configuration, one row serving every tenant) and enabling a tenant for it (access control, per tenant). Neither grants any individual anything — that is scenario 49.6 endpoint(s)MULTI_APP_ENABLED42Diagnose an app that is missing a memberA tenant admin reports that someone who plainly exists in IAM is absent from the product app — no row, or a stale one. The app holds its OWN copy of identity, maintained by the asynchronous projection, so “missing downstream” and “missing in IAM” are different faults with different answers. This is how an operator tells them apart without guessing.3 endpoint(s)MULTI_APP_ENABLED43Internal discovery & health (ops)Not user flows — these serve the operator-profile infrastructure: internal liveness, the machine-generated API contract, and interactive exploration. Raw bodies, internal-only ingress.3 endpoint(s)
44Inviting a teammate by emailAn admin adds someone to their tenant by email address; the invitee finishes sign-up themselves and lands ALREADY SIGNED IN. This is R4 (a tenant admin inviting) and R5 (the founding admin of an operator-provisioned tenant, scenario 35).4 endpoint(s)45Inviting a teammate by phone (WhatsApp)The same flow as scenario 44, addressed by PHONE NUMBER and delivered over WhatsApp — for teammates who have no work email. This is R3. Routed by which identifier the admin supplies; the two are mutually exclusive.3 endpoint(s)46Accepting an invitation when you already have an accountOne identifier is one identity across every tenant, so POST /v1/invitations REUSES an existing account rather than duplicating it. The invitee therefore arrives at the accept screen in one of two entirely different situations, and the screen has to know which BEFORE it renders — which is what POST /v1/auth/invitations/describe is for.3 endpoint(s)47Resending an invitation that never arrivedThe invitee reports “I never got it” — or got only the 15-minute code and let it lapse. The admin re-delivers THE SAME pending invitation with fresh secrets; nothing about the membership changes. Step 1½ of scenario 44 / 45, not a new invite: POST /v1/invitations answers 409 while an open invitation exists — even a lapsed one — so this is the only path that helps during the link's 7 days.2 endpoint(s)48Administering members and rolesThe admin-only surface for managing the caller's tenant. The tenant is always the admin's own (principal.tenantId).9 endpoint(s)49Managing which apps a member may useThe tenant-admin half of the app dimension, and the sibling of scenario 48: roles say what a member may do INSIDE an app, grants say which apps they may enter at all. An operator reaches this the same way — through act-as (scenario 37). The tenant must first be enabled for the app (scenario 41). THIS IS THE ADMIN-DRIVEN HALF: a member can also acquire a FREE-TO-ACQUIRE app for an organization they administer themselves, without an admin or an operator, at scenario 50 — the same write, reached from the other side.4 endpoint(s)MULTI_APP_ENABLED50Joining an app you already have an account forThe mirror of scenario 1 for someone who is ALREADY REGISTERED. An app the operator has marked is_auto_enabled_on_register (scenario 41) is 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. A user who already has an account, opening that same page, is refused 409 …_already_registered, and had no other route: every enablement write was an operator endpoint or that one-shot inside registration. The rule this closes: A CALLER MAY ACQUIRE THE APP EXACTLY WHEN AN ANONYMOUS VISITOR COULD, so both calls gate on the same registry predicates the registration hand-off uses, refusals included. This is NOT the paid subscription path — an app the operator has not opted in is refused here, and reaching it means subscribing, not joining.5 endpoint(s)51Reading the tenant audit logAn admin reviews tenant security events; filterable + cursor-paginated.1 endpoint(s)