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.

Operational status (2026-10-01, superseding the 2026-09-28 note this replaces): the external-login endpoints described below (GET /account/login/external/{provider}, GET /account/login/external-callback, plus linking/unlinking) were built, deployed, then deliberately disabled the same evening (2026-09-28) after a startup-crash incident and a credential-exposure incident. Pete has since made the final call that external login is not coming back — as of 2026-10-01 the endpoints, the ExternalLogin domain entity, and its EF migration have been fully removed from the codebase, verified directly against the solution source. They no longer return a 404 because the feature is switched off; there's no longer a route there to hit at all. The design below is preserved as a historical record of what was built, not a current or returning feature. See the shared repo's keystone-production-secrets.md for the incident and platform-action-items.md (Keystone section) for the removal decision. keystone-api-reference.md in that repo is the live, regenerated contract and has no trace of this feature.


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. Removed from the codebase 2026-10-01 — see Operational status above.
GET /account/login/external-callbackItems 3 & 4 — the federation handshake and linking logic. Removed from the codebase 2026-10-01 — see Operational status above.
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. Removed from the codebase 2026-10-01 — see Operational status above.
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​

Note: the "optional path" branch below was real and tested, then fully removed from the codebase on 2026-10-01 (see Operational status at the top of this document) — read the diagram as a historical record of the design that was built, not a live or returning path.

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.