Keystone — API Design
Status: Scoping only, for the spike defined in keystone-spike-scope.md. Same discipline as keystone-db-design.md — light and proportional to a spike, not an exhaustive permanent-system design.
Read first: keystone-db-design.md for the schema this API operates against, and keystone-external-login-proposal.md for the original endpoint logic this design carries forward and renames.
1. Two genuinely different kinds of endpoint here — worth separating clearly
OpenIddict-provided (framework surface, not designed here): GET /connect/authorize, POST /connect/token, GET /connect/userinfo. Same principle as the database doc's Section 3 — these are inherited from OpenIddict's own middleware, the same way EF Core's migration history table isn't hand-designed. Configuring them (grant types, scopes, token lifetimes) is real spike work; redesigning their shape is not.
Custom, Keystone-built (the actual scope of this document): the hosted login experience, registration, the optional-federation handshake, and account linking. This is what OpenIddict's authorize flow redirects to when a user isn't yet authenticated — Keystone's own UI and logic, not something OpenIddict provides.
2. The custom endpoints, mapped directly to the spike's proof points
| Endpoint | Proves (spike-scope §2) |
|---|---|
POST /account/register | Item 2 — primary email/password registration, creates a User row directly, never touches Entra |
GET /account/login (renders), POST /account/login | Item 2 — primary email/password authentication |
GET /account/login/external/{provider} | Item 3 — initiates the optional Microsoft challenge, only ever reached by an explicit click |
GET /account/login/external-callback | Items 3 & 4 — the federation handshake and linking logic |
POST /account/link-external | Item 4 — an already-authenticated user deliberately linking Microsoft from account settings |
DELETE /account/external-logins/{provider} | Item 4 — unlinking, admin-visible per the original proposal's Section 3.4 |
POST /account/forgot-password, POST /account/reset-password | The "ready to use" tier's minimum bar (keystone-framing.md §8) — some recovery path so nobody gets permanently locked out |
Renamed from the original proposal, worth noting explicitly: /account/tenant-login/external/{provider} → /account/login/external/{provider}. The original naming was scoped to RoundTrip's own TenantUser concept specifically — correct at the time, wrong now that Keystone serves every product centrally. Same real logic underneath, product-agnostic naming on top.
3. The real end-to-end flow — both paths, one diagram
Why this diagram matters more than a plain endpoint list: it's the direct, visual proof that Entra genuinely sits on an optional branch, not the default path — exactly the distinction that makes the TRA-389 fix real (keystone-framing.md §9). If a future implementation ever collapsed these into one path by default, this diagram is the reference that shows it drifted from the actual design.
4. Entitlement — stubbed, not a real endpoint here
Per the spike's own scope, a client checking product access calls out to Command Center, hardcoded to return "yes" for now. Not a Keystone endpoint at all — worth noting only so its absence here isn't mistaken for an oversight.
5. What's explicitly deferred, matching the database doc's own exclusions
Same list, same reasoning as keystone-db-design.md §6 — MFA, admin-tooling endpoints, production-tier session/token revocation, rate-limiting. Real work for later phases, not needed to prove the spike's core mechanics.