Skip to main content

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

EndpointProves (spike-scope §2)
POST /account/registerItem 2 — primary email/password registration, creates a User row directly, never touches Entra
GET /account/login (renders), POST /account/loginItem 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-callbackItems 3 & 4 — the federation handshake and linking logic
POST /account/link-externalItem 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-passwordThe "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.