Skip to content

feat(api): authenticate Claude Code to /v1/* with Entra ID SSO - #14665

Merged
diegosouzapw merged 14 commits into
cheaper-inference:release/v3.8.53from
BenjaminAronsson:feat/entra-sso
Oct 30, 2026
Merged

diegosouzapw merged 14 commits into
cheaper-inference:release/v3.8.53from
BenjaminAronsson:feat/entra-sso

Conversation

@BenjaminAronsson

@BenjaminAronsson BenjaminAronsson commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Developers authenticate to /v1/* with a short-lived Microsoft Entra ID token instead of a static OmniRoute API key. Their Entra security groups map onto existing key groups, and usage is attributed per person in usage_history.
  • Opt-in and additive — static API keys keep working. entraSsoEnabled defaults to false.
  • Server side: JWKS verification (jose, already a dependency) asserting tid as well as aud, group → key-group mapping with an opt-in Graph fallback for claim overage, and a just-in-time "shadow" api_keys row per Entra oid. That row is the design: key_group_members has a FK to api_keys(id), so group rules, call-log attribution and the instant is_active=0 kill-switch all work with no SSO branches downstream.
  • Client side: scripts/cli/omniroute-sso.mjs, dependency-free (Node 18+) so it can be handed to developers who never install the omniroute CLI. login (auth code + PKCE on a loopback port), token (the apiKeyHelper target — prints the credential alone), install, logout.
  • Scope of group mapping: key groups carry model allow/deny rules only. Budgets, rate limits, usage caps, schedules and combo allowlists are per-key columns on api_keys, so group membership does not confer them. An earlier revision of this description claimed otherwise.

Related Issues

Validation

  • Change type: DB (one migration) + auth/authz + CLI
  • Focused tests and category gates from the golden path
  • npm run lint — exit 0
  • Reconciled with the current active release base (release/v3.8.52); focused checks rerun afterward
  • Production-code changes include new automated tests in this PR
check result
tests/unit/entra-sso-auth.test.ts 36/36
tests/unit/entra-sso-client-script.test.ts 13/13
tests/integration/entra-sso-e2e.int.test.ts (opt-in) 8/8 under RUN_ENTRA_SSO_E2E=1; skipped otherwise
typecheck:core · check:cycles:ratchet · check:docs-all · check:changelog-integrity · prettier clean
test:vitest 51 files / 482 tests
semgrep pass

Inherited from the base tip, untouched by this PR: the unit/integration/package gate timeouts, the vitest MCP audit shutdown > checkpoints failure, and the check:ai-attribution git-range error. tests/unit/stream-readiness-fallback.test.ts is also red on the base — fixed separately in #15428.

Merge integrity (changelog + generated skills) is base-red, not this PR. The job fails on check:agent-skills-sync, which reports 20 drifted SKILL.md files. Checking out the clean release/v3.8.52 tip (3def9ae) and running npm run check:agent-skills-sync there reproduces the identical 20-skill list, byte for byte — this PR touches neither docs/openapi.yaml nor skills/, the generator's only two inputs. Per the base-red rule it is deliberately not fixed here; it needs its own fix/release-v3.8.52-* PR. (check:changelog-integrity, the job's other gate, passes.)

Tests Added Or Updated

  • tests/unit/entra-sso-auth.test.ts (new) — local RSA keypair + stubbed JWKS, so the real jose path including createRemoteJWKSet runs offline. Covers valid token, wrong aud, wrong tid, expired, signature not in the JWKS, missing oid, idtyp=app, missing delegated scp, RS256 pin, group mapping, default-group fallback, group drift, overage with and without Graph, Graph unreachable → 503, JIT race → exactly one key, deactivation kill-switch, static-key coexistence, unrelated JWT while SSO is off, and the auth→policy bridge including the JWT arriving in x-api-key.
  • tests/unit/entra-sso-client-script.test.ts (new) — PKCE derivation, token-freshness skew, settings merge.
  • tests/integration/entra-sso-e2e.int.test.ts (new, opt-in) — the real helper driven as a subprocess against a stub authority that verifies PKCE the way Entra does; loopback callback, code exchange, refresh rotation, authority pinning, 0600 cache.

Each of the fail-closed group-mapping tests and the RS256 pin test was confirmed RED against the preceding commit before the fix was restored.

Coverage Notes

The new unit suite covers every branch of src/server/authz/entra/*, the sso_user branch in clientApiPolicy, resolveSsoApiKeySecret in apiKeyPolicy.ts, and the source='sso' redaction in apiKeys.ts. The client script's pure functions are covered unconditionally; its network and filesystem paths are covered by the opt-in e2e.

Reviewer Notes

  • Migration is 206. It has been renumbered twice as the base moved. It was 197, which then collided head-on with the base's own 197_proxy_subscription_core_config.sql; release/v3.8.52 now tops out at 205. Docs migration count becomes 203 (the base's own line said 202 and was already stale against its tip).
  • Two ordering constraints are load-bearing. The SSO branch resolves before validateApiKey and before extractUngatedClientApiKey: Claude Code sends the apiKeyHelper credential in both Authorization: Bearer and x-api-key, so without that the policy layer receives the raw JWT and silently drops every limit — the GHSA-2phc-xp22-9f56 class. But the branch only claims a bearer while SSO is configured, so with SSO off a client's own JWT still degrades to anonymous under REQUIRE_API_KEY=false.
  • Group mapping fails closed. checkKeyModelAccess treats a key in zero groups as unrestricted, so a mapping pointing at a typo'd/deleted/inactive key group used to widen access. Ids are now narrowed to existing+active before granting, zero groups denies, provisioning deactivates a key that still lands in none, and PATCH /api/settings rejects unknown groups.
  • Shadow key secrets never leave the DB layer — getApiKeys/getApiKeyById blank them for source='sso', and /api/keys/[id]/reveal 403s rather than returning "".
  • AGENTS.md / llm.txt sign-off: the only change in those files is the migration count line, now 202 → 203, forced by the strict docs-counts gate. No instruction text is modified.
  • No live-tenant validation. No token in any suite was minted by Microsoft, so consent behavior, real claim shapes, Conditional Access and the Graph overage path against live responses are unverified. Hard Rule fix(ci): add environment for npm token access #18 names OAuth upstream flows as needing real-environment validation — tell me if you want that as a merge condition and I will run it.
  • The e2e is opt-in (RUN_ENTRA_SSO_E2E=1) because it spawns the helper fourteen times and 🔴 Release branch not green: release/v3.8.52 #15306 reports the always-on test:integration gate as already killed at its ceiling.
  • Not included: the dashboard settings form (adding UI strings trips check-new-key-coverage, which wants all 66 locales); folding /v1/models and /v1/embeddings onto the central auth decision — they re-validate the raw key in-handler and will 401 an SSO token.

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Thanks @BenjaminAronsson, this is a carefully built feature. The tid pinning, the non-forgeable auth stamp and the shadow-key bridge into the existing policy layer are exactly right, and the negative tests (wrong audience, wrong tenant, expired, unknown kid, missing oid) all pass locally. A few things before we can merge:

  1. Migration collision: release/v3.8.51 already has 186_conversation_turn_nodes_last_seen_index.sql (fix(db): stagger the cleanup/model-sync 6h schedulers and index conversation_turn_nodes.last_seen_at (#13973) #14567), so with both files present the runner throws "Migration version collision detected: version=186" on boot. Please rebase and renumber to 187; the docs counts become 184.
  2. Group mapping fails open: if entraGroupMappings points to a key group that doesn't exist (a typo, or a group deleted later) or is disabled, addKeyToGroup returns false silently, last_groups is still recorded, and the shadow key ends up in no group. checkKeyModelAccess treats that as unrestricted, and the shadow key is created with all models and combos allowed. Please fail closed: validate that the key group exists and is active (in PATCH /api/settings and at provisioning), and deny if the shadow key would end up in zero groups. A regression test for the typo case would be great.
  3. Token type: please require the delegated scp claim (and reject idtyp=app), so an id_token or a client-credentials token with a matching aud isn't accepted. Please also pin algorithms: ["RS256"] in jwtVerify.
  4. Helper script: authorityOf() trusts whatever authorityHost the server publishes, including on refresh. Please pin the authority in the local cache at login, require https (loopback excepted), and refuse http:// base URLs off loopback, so a compromised or intercepted server can't redirect refresh tokens.
  5. Docs: key groups only carry model allow/deny rules. Budgets and rate limits are per key, so SSO users get none through group mapping. Please adjust ENTRA_SSO.md and the description.

Also worth knowing: /v1/models, /v1/embeddings and a few other routes re-validate the raw key inside the handler and will 401 an SSO token. That's fine as a documented follow-up. Since this PR edits AGENTS.md and llm.txt (the migration-count line), the maintainer will also do an explicit sign-off before merging. Thanks again!

@diegosouzapw diegosouzapw added protected-surface Touches an agent-instruction surface (AGENTS/CLAUDE/llm.txt/SKILL.md) — per-PR operator OK to merge deferred-v3.8.52 Grande demais / suspeito para o lote atual; precisa de sessão dedicada no ciclo v3.8.52 labels Sep 24, 2026
@diegosouzapw diegosouzapw changed the title feat(api): authenticate Claude Code to /v1/* with Entra ID SSO [defer] feat(api): authenticate Claude Code to /v1/* with Entra ID SSO Sep 25, 2026
@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.51 to release/v3.8.52 September 29, 2026 11:21
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Re-homed to release/v3.8.52: v3.8.51 entered its release freeze, so the branch now belongs to the release captain and development continues on the next cycle. Nothing is wrong with this PR — it just needed a live base. No action needed from you; CI will re-run against the new base.

@diegosouzapw diegosouzapw removed the deferred-v3.8.52 Grande demais / suspeito para o lote atual; precisa de sessão dedicada no ciclo v3.8.52 label Oct 1, 2026
@diegosouzapw diegosouzapw changed the title [defer] feat(api): authenticate Claude Code to /v1/* with Entra ID SSO feat(api): authenticate Claude Code to /v1/* with Entra ID SSO Oct 1, 2026
Developers sign in with Microsoft Entra ID instead of holding a static
OmniRoute API key. Their Entra security groups map onto existing key
groups, so the rate limits, model allowlists, budgets and combo
permissions operators already configure apply per person, and usage is
attributed per person in the call log.

Static API keys keep working — this only adds a way to authenticate.

Server:
- verify access tokens against the tenant JWKS (jose), asserting tid as
  well as aud so a multi-tenant registration cannot mint a token another
  tenant's user can replay
- map Entra group objectIds to key_groups, with an opt-in Microsoft Graph
  fallback for users whose groups claim overflows past 150 entries
- provision a shadow api_keys row per Entra oid on first sight, so group
  policy and call-log attribution work with no SSO branches downstream
- cache the verdict per token so group reconciliation runs about once per
  user per helper refresh rather than once per request

The SSO branch resolves before validateApiKey and before the x-api-key
extractor: Claude Code sends an apiKeyHelper credential in both
Authorization and x-api-key, so without that ordering the policy layer
would receive the raw JWT, resolve it to no key, and silently drop every
limit the user is under — the GHSA-2phc-xp22-9f56 class. A bearer that
presents itself as an Entra token always gets a final verdict, so an
identity-provider outage cannot degrade to anonymous access.

getApiKeys/getApiKeyById blank the secret of source='sso' rows. Exposing
it would mint a permanent static credential that bypasses SSO and
survives offboarding.

Client: scripts/cli/omniroute-sso.mjs is dependency-free so it can be
handed to developers who never install the omniroute CLI. `login` runs
authorization code + PKCE on a loopback port; `token` prints the
credential alone, as apiKeyHelper requires; `install` merges the block
into ~/.claude/settings.json.

Docs also correct a stale migration count (178 -> 182) that the counts
gate blocks on; the llm.txt mirrors are regenerated by
scripts/i18n/sync-llm-mirrors.mjs.
getApiKeyById redacts the secret of source='sso' rows, so the reveal
endpoint answered 200 with an empty string and the dashboard silently
copied nothing. Return 403 with an explanation instead.

The accompanying test asserts a normal key still reveals at 200 in the
same conditions — reveal is feature-gated behind ALLOW_API_KEY_REVEAL and
returns 403 when disabled, so without that the SSO assertion would pass
for the wrong reason.
Rebase onto release/v3.8.51 surfaced three problems.

Migration 185 collided with 185_usage_history_cpa_auth_index from cheaper-inference#14544,
which the migration runner rejects outright. Renumbered to 186; no
retroactive isSchemaAlreadyApplied guard is needed because this migration
has never shipped under the old number.

The SSO branch claimed any RS256 JWT bearer even with SSO switched off,
so a client sending its own unrelated JWT — extractBearer already notes
VS Code Copilot does this — would hard-401 instead of degrading to
anonymous under REQUIRE_API_KEY=false. It is now gated on
isEntraSsoEnabled(), which answers false if the settings read throws so
an unreadable config falls through to the pre-SSO path rather than
500-ing auth. The new test is RED against the previous commit.

The migration count landed off by one: cheaper-inference#14559 had already moved the docs
to 181 and the base to 182, and the rebase merged both edits cleanly into
a number that no longer matched the 183 files on disk.
… SSO test

The authority host was hardcoded to login.microsoftonline.com, so the
feature could not work for a tenant on a sovereign cloud
(login.microsoftonline.us, login.partner.microsoftonline.cn). It is now
`entraAuthorityHost`, published through /api/auth/sso/config so the
sign-in helper follows the server with no per-machine configuration.
Plaintext http is refused unless the host is loopback: the authority
decides which keys sign the tokens we trust, so an http authority
reachable over the network would let anyone on the path mint accepted
identities.

The new end-to-end test runs the real helper as a subprocess against a
stub authority that verifies PKCE the way Entra does, then feeds the
resulting token to the real auth policy. It found two client bugs that
the pure-function tests could not reach:

- The login timeout was never cleared, so after printing "Signed in" the
  process kept the event loop alive for the full five minutes and looked
  hung. Login now takes 278ms instead of 300s.
- openBrowser passed a missing binary to spawn, which reports failure
  through an async 'error' event rather than a throw. Without a listener
  Node raised it as uncaught and killed the login — on exactly the
  headless machines that have no opener to begin with.
… groups, pin token type and authority

Review items 1-5 from cheaper-inference#14665.

Migration: renumbered to 197. The review asked for 187, but release/v3.8.52
already carries 185, 186 and 187 (and up to 196), so the next free number is
197 and the docs count becomes 194 rather than 184.

Group mapping no longer fails open. checkKeyModelAccess treats a key in zero
groups as unrestricted, and addKeyToGroup swallows a bad id, so a mapping
pointing at a typo'd, deleted or deactivated key group silently produced a
shadow key with every model allowed. Group ids are now narrowed to those that
exist AND are active (filterExistingActiveKeyGroupIds) before they can grant
anything; resolving to none denies with AUTH_SSO_NO_GROUP; provisioning
deactivates a key that still lands in zero groups; and PATCH /api/settings
rejects a config referencing unknown or inactive groups. Three tests, all RED
against the previous commit.

Token type: `aud` plus a valid signature did not prove a user token — an
id_token or an app-only client-credentials token from the same tenant carries
the same audience. Now rejects idtyp=app, requires the delegated scp claim, and
pins algorithms: ["RS256"]. The pin test signs PS256 with a key published in the
same JWKS, so it fails for the right reason rather than key-not-found.

Client helper: the authority, tenant, client id and scope are pinned into the
local cache at login and refresh no longer re-reads /api/auth/sso/config, so a
gateway compromised after enrolment cannot redirect the refresh grant. http is
refused for both the base URL and the authority unless the host is loopback.

Docs: corrected the claim that group mapping carries budgets and rate limits. It
carries model allow/deny rules only — budgets, rate limits, usage caps,
schedules and combo allowlists are per-key columns on api_keys, so SSO users get
the defaults for those. Added the table and noted that /v1/models and
/v1/embeddings re-validate the raw key in-handler and will 401 an SSO token.
@BenjaminAronsson

Copy link
Copy Markdown
Contributor Author

Thanks — all five are addressed in c9b7172. Two notes where I deviated from the letter of the request.

1. Migration collision. Renumbered, but to 197 rather than 187: release/v3.8.52 already carries 185_usage_history_cpa_auth_index, 186_conversation_turn_nodes_last_seen_index and 187_proxy_logs_attempts, and the highest is 196. So the docs count becomes 194, not 184. Your numbers were right for v3.8.51 at review time — the re-home moved them.

2. Group mapping failed open. Confirmed exactly as you described, and worse than a typo case: addKeyToGroup swallows the bad id, last_groups still records it, and checkKeyModelAccess returns {allowed: true} for a key in zero groups (apiKeyGroups.ts:247, "No groups = no restrictions"). So the misconfiguration widened access. Fixed at three layers:

  • filterExistingActiveKeyGroupIds() narrows ids to those that exist and are active, applied to both the mapping result and the default group, before anything is granted.
  • Resolving to zero groups denies with AUTH_SSO_NO_GROUP and provisions nothing.
  • Provisioning re-checks membership actually landed and deactivates the shadow key if not, rather than leaving an unrestricted one.
  • PATCH /api/settings rejects a config referencing unknown/inactive groups up front (ENTRA_SSO_UNKNOWN_KEY_GROUP).

Three tests cover the typo, the deactivated group, and a bad default. I reverted the fix and confirmed all three go RED before restoring it.

3. Token type. idtyp=app rejected (AUTH_SSO_APP_TOKEN), delegated scp required (AUTH_SSO_NOT_DELEGATED), and algorithms: ["RS256"] pinned. The pin test signs PS256 with a key published in the same JWKS — otherwise it would pass for the wrong reason (key-not-found rather than algorithm rejected). Also verified RED without the pin.

4. Helper authority. Authority, tenant, client id and scope are now pinned into the cache at login, and the refresh path no longer reads /api/auth/sso/config at all — so there is no point at which a later-compromised gateway can influence the refresh grant. A cache written without those fields refuses to refresh and asks for a fresh login rather than falling back to trusting the server. http is refused for both the base URL and the authority unless the host is loopback. The e2e test makes the stub gateway publish a hostile authority mid-session and asserts the pinned one is still used.

5. Docs. You're right, and the error was mine — I had it in the PR description too. Key groups carry model allow/deny rules only; budgets, rate limits, usage caps, schedules and combo allowlists are per-key columns on api_keys. ENTRA_SSO.md now has an explicit table of what group mapping does and does not carry, the description is corrected, and the changelog fragment no longer claims budgets or rate limits. Also documented the /v1/models and /v1/embeddings in-handler re-validation as a known limitation.

Also folded in while setting this up locally: the authority was hardcoded to login.microsoftonline.com (unusable for login.microsoftonline.us / login.partner.microsoftonline.cn), and the end-to-end test caught two client bugs — an uncleared login timeout that left the process hanging for five minutes after printing "Signed in", and openBrowser passing a missing binary to spawn, whose async error event went unhandled and killed the login on headless machines.

Still outstanding and understood: the dashboard settings form (the i18n new-key gate wants all 66 locales), and no live-tenant run yet — everything here is verified against a stub authority. Happy to do the live validation if you want it before merge. And noted on the AGENTS.md / llm.txt sign-off; the only change in those is the migration count line.

The suite sat in the always-on `test:integration` glob — the gate cheaper-inference#15306
reports as killed at its 2400s CI ceiling — and spawns the helper as a
subprocess fourteen times. Renamed to `.int.test.ts` and self-skipping
unless RUN_ENTRA_SSO_E2E=1, matching the RUN_SERVICES_INT and
RUN_CONTRACT_INT suites, so it cannot be blamed for that timeout.

Always-on coverage is unchanged: the unit suite already exercises the real
jose path including createRemoteJWKSet, plus every negative case. What the
env flag guards is the client round trip.

Also adds a "Validation status" section to ENTRA_SSO.md that says plainly
what is and is not proven. No token in any of these suites was minted by
Microsoft, so consent behavior, real claim shapes, Conditional Access and
the Graph overage path against live responses are unverified. Hard Rule cheaper-inference#18
names OAuth upstream flows as needing real-environment validation; this
records that gap rather than leaving a reviewer to find it.
@BenjaminAronsson

Copy link
Copy Markdown
Contributor Author

Rebased onto release/v3.8.52 and pushed (e89afc0). Two changes since the review reply, then this is ready from my side:

  • The e2e suite is now tests/integration/entra-sso-e2e.int.test.ts and self-skips unless RUN_ENTRA_SSO_E2E=1, matching the RUN_SERVICES_INT / RUN_CONTRACT_INT convention. It spawns the helper as a subprocess fourteen times and 🔴 Release branch not green: release/v3.8.52 #15306 reports the always-on test:integration gate as already killed at its 2400s ceiling — this PR should not add weight there. Always-on coverage is unchanged; the 36-test unit suite covers the whole server-side path including the real jose flow.
  • ENTRA_SSO.md and the description now carry an explicit Validation status: no token in any suite was minted by Microsoft, so consent behavior, real claim shapes, Conditional Access and the Graph overage path against live responses are unverified. Hard Rule fix(ci): add environment for npm token access #18 names OAuth upstream flows as needing real-environment validation — if you want a live-tenant run as a merge condition, say so and I will do it. I do not have tenant access today.

What I need from you: CI has never run on this PR. Only semgrep (passing) and a skipping Mergify have reported, and mergeStateStatus is UNSTABLE — fork PRs need a maintainer to approve workflow runs, so there is no green signal for anyone to review against yet.

The only AGENTS.md / llm.txt change is the migration count line, now 193 → 194 because this PR adds migration 197. Nothing else in those files is touched, if that helps the sign-off.

Also opened #15428 separately: stream-readiness-fallback.test.ts is red on the v3.8.52 tip because two tests cancel the served body instead of draining it, so the feeding pipeline settles after the test ends and trips an unhandledRejection — all ten assertions pass while the file fails, which is why it reads as a mystery. Same unreleased-handle class #15306 names as the suspected unit-gate timeout cause. While verifying it I confirmed the other #15306 candidates (specialty-catalog, flag count, qwen alias, the cliproxy TS2322, the env-doc drift, the ESLint suppressions) are already green on that tip and need nothing.

diegosouzapw and others added 4 commits October 8, 2026 10:51
# Conflicts:
#	AGENTS.md
#	README.md
#	docs/i18n/am/llm.txt
#	docs/i18n/ar/llm.txt
#	docs/i18n/az/llm.txt
#	docs/i18n/bg/llm.txt
#	docs/i18n/bn/llm.txt
#	docs/i18n/bs/llm.txt
#	docs/i18n/cs/llm.txt
#	docs/i18n/da/llm.txt
#	docs/i18n/de/llm.txt
#	docs/i18n/el/llm.txt
#	docs/i18n/es/llm.txt
#	docs/i18n/et/llm.txt
#	docs/i18n/fa/llm.txt
#	docs/i18n/fi/llm.txt
#	docs/i18n/fr/llm.txt
#	docs/i18n/ga/llm.txt
#	docs/i18n/gu/llm.txt
#	docs/i18n/ha/llm.txt
#	docs/i18n/he/llm.txt
#	docs/i18n/hi/llm.txt
#	docs/i18n/hr/llm.txt
#	docs/i18n/hu/llm.txt
#	docs/i18n/hy/llm.txt
#	docs/i18n/id/llm.txt
#	docs/i18n/ig/llm.txt
#	docs/i18n/it/llm.txt
#	docs/i18n/ja/llm.txt
#	docs/i18n/ka/llm.txt
#	docs/i18n/km/llm.txt
#	docs/i18n/kn/llm.txt
#	docs/i18n/ko/llm.txt
#	docs/i18n/lt/llm.txt
#	docs/i18n/lv/llm.txt
#	docs/i18n/ml/llm.txt
#	docs/i18n/mr/llm.txt
#	docs/i18n/ms/llm.txt
#	docs/i18n/mt/llm.txt
#	docs/i18n/my/llm.txt
#	docs/i18n/ne/llm.txt
#	docs/i18n/nl/llm.txt
#	docs/i18n/no/llm.txt
#	docs/i18n/or/llm.txt
#	docs/i18n/pa/llm.txt
#	docs/i18n/phi/llm.txt
#	docs/i18n/pl/llm.txt
#	docs/i18n/pt-BR/llm.txt
#	docs/i18n/pt/llm.txt
#	docs/i18n/ro/llm.txt
#	docs/i18n/ru/llm.txt
#	docs/i18n/si/llm.txt
#	docs/i18n/sk/llm.txt
#	docs/i18n/sl/llm.txt
#	docs/i18n/sr/llm.txt
#	docs/i18n/sv/llm.txt
#	docs/i18n/sw/llm.txt
#	docs/i18n/ta/llm.txt
#	docs/i18n/te/llm.txt
#	docs/i18n/th/llm.txt
#	docs/i18n/tr/llm.txt
#	docs/i18n/uk-UA/llm.txt
#	docs/i18n/ur/llm.txt
#	docs/i18n/uz/llm.txt
#	docs/i18n/vi/llm.txt
#	docs/i18n/yo/llm.txt
#	docs/i18n/zh-CN/llm.txt
#	docs/i18n/zh-TW/llm.txt
#	llm.txt
Reconcile with the moved base tip (23a1148 -> 3def9ae):

- Renumber the SSO migration 197 -> 206. The base added 197..205 since this
  branch was cut, so 197_entra_sso_identities.sql collided head-on with
  197_proxy_subscription_core_config.sql.
- Resolve the 69 docs-count conflicts (llm.txt, AGENTS.md, README.md and the
  66 docs/i18n/<locale>/llm.txt mirrors) to the real count: 203 migrations.
  Base said 202, this branch said 194; both are stale now.

Verified on the merged tree: typecheck:core, lint, check:docs-all,
check:changelog-integrity all exit 0; entra-sso-auth 36/36,
entra-sso-client-script 13/13, entra-sso-e2e 8/8 (RUN_ENTRA_SSO_E2E=1),
api-key policy/lifecycle/access 70/70.
The PR head had its own merge of an older release/v3.8.52 tip (pushed outside
this session). Merge it in rather than force-push, so no history is rewritten.

Both sides independently renumbered the SSO migration to 206 — no conflict
there. The 69 docs-count files conflicted (270 hunks, all count lines only);
resolved to this branch's 203, which is the real count against the current
base tip. The other side said 200 because it merged a base that predated
migrations 204 and 205.

Verified after resolution: typecheck:core, check:docs-all, check:docs-counts
exit 0 (203 confirmed strict); entra-sso unit suites 49/49.
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Hi @BenjaminAronsson — closing this pull request to clear the open queue. The branch feat/entra-sso is kept, so the commits stay. Comment here if it should be reopened.

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Hi @BenjaminAronsson — reopening this pull request. The close was a mistake, and it is back in the merge queue. The branch feat/entra-sso was kept.

@diegosouzapw diegosouzapw reopened this Oct 10, 2026
@diegosouzapw diegosouzapw added the deferred-v3.8.53 Feat PRs held for v3.8.53. Keep open. Do not merge into v3.8.52. label Oct 13, 2026
@diegosouzapw diegosouzapw changed the title feat(api): authenticate Claude Code to /v1/* with Entra ID SSO [defer] feat(api): authenticate Claude Code to /v1/* with Entra ID SSO Oct 13, 2026
@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.52 to release/v3.8.53 October 19, 2026 13:35
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Re-homed to release/v3.8.53: v3.8.52 entered its release freeze, so the branch now belongs to the release captain and development continues on the next cycle. Nothing is wrong with this PR — it just needed a live base. No action needed from you; CI will re-run against the new base.

@diegosouzapw diegosouzapw removed the deferred-v3.8.53 Feat PRs held for v3.8.53. Keep open. Do not merge into v3.8.52. label Oct 20, 2026
@diegosouzapw diegosouzapw changed the title [defer] feat(api): authenticate Claude Code to /v1/* with Entra ID SSO feat(api): authenticate Claude Code to /v1/* with Entra ID SSO Oct 20, 2026
BenjaminAronsson and others added 3 commits October 28, 2026 21:49
Resolve the conflicts with release/v3.8.53 and fix what the merge would break:

- Renumber the migration 206_entra_sso_identities.sql to 217: the release already ships 206_codex_wakeup_tasks.sql and runs up to 216, so boot threw a version collision.
- The release moved the settings re-auth list into src/shared/constants/securityImpactingSettings.ts; the PR's local SECURITY_IMPACTING_KEYS is gone, so every entra* settings key now lives in the shared list (re-auth gated, operator-only, dropped by the JSON import). Pinned by a new test.
- Move the Entra PATCH /api/settings checks into validateEntraSsoPatch(), next to the release's validateOidcPolicyPatch(), so PATCH does not grow; three new route tests cover the unknown-key-group, incomplete and no-group-policy rejections.
- clientApi policy: run the SSO check on the highest-precedence credential from the release's extractApiKeyCandidates(); apiKeyPolicy: resolvePolicyApiKey() consults the SSO shadow key first; the key reveal route keeps the SSO 403 and adds the release's management-scope check.
- Settings defaults and schema keep both the release's oidcAdmissionClaim and the entra* keys.
- AGENTS.md, llm.txt and the docs/i18n mirrors are taken from the release: the PR's migration-count hunks are stale and the counts are refreshed after merge. README.md carries the migration count of this tree (214).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…with release 217)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
@diegosouzapw
diegosouzapw merged commit 1e78e24 into cheaper-inference:release/v3.8.53 Oct 30, 2026
28 of 34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

protected-surface Touches an agent-instruction surface (AGENTS/CLAUDE/llm.txt/SKILL.md) — per-PR operator OK to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(auth): authenticate the client API with Microsoft Entra ID SSO instead of static API keys

2 participants