fix(api): /config/validate honours published JWKS (#13) + accurate /org/* contract (#7)#14
Open
rafiki270 wants to merge 1 commit into
Open
fix(api): /config/validate honours published JWKS (#13) + accurate /org/* contract (#7)#14rafiki270 wants to merge 1 commit into
rafiki270 wants to merge 1 commit into
Conversation
…ntract Addresses three open issues found during real integrations. #13 — /config/validate false-negatived a 'signature' the live /auth runtime accepts. The validator only resolved the key via (DB-by-kid) → CONFIG_JWKS_URL, while the runtime additionally falls back to the JWKS the partner publishes at the jwks_url in the config payload (via auto-onboarding). Any kid not yet in the DB therefore failed in the validator but passed at /auth. Extracted that partner- JWKS verification into a shared verifyConfigJwtViaPublishedJwks() reused by both tryAutoOnboard and the validator's signature stage, so the validator now mirrors the runtime. Also documents config-JWT exp/iss semantics in /llm (exp enforced if present, iss not checked). #7 — the /api machine schema misrepresented the /org/* contract (omitted the required `domain` query and the X-UOA-Access-Token header, wrongly listed owner_id in the create body, and never mentioned the org_features gates), so integrators were blocked. /api now advertises domain+config_url on every /org/* endpoint plus a contract note covering the access-token requirement, owner derivation, and the allow_user_create_org gate; /llm's create-org row is fixed to match. org-features gate now carries an ORG_FEATURES_DISABLED code and ORG_CREATION_NOT_ALLOWED / ORG_FEATURES_DISABLED / DOMAIN_MISMATCH gained debug-mode explanations (production responses stay generic). No behaviour change to auth/security beyond the validator now accepting what the runtime already accepts. Adds unit tests for the shared verifier. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This was referenced Jun 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes two integration-blocking issues found in production onboarding, plus closes a third as already-resolved.
#13 —
/config/validatefalse-negativessignaturefor a JWT the runtime acceptsRoot cause (verified in code): the validator's
signaturestage only resolves the key via per-domain DB key (findJwkByKidDb) → deploymentCONFIG_JWKS_URL. The live/authruntime has an extra fallback: on signature failure it callstryAutoOnboard, which fetches the JWKS the partner publishes at thejwks_urlin the config payload and verifies against that (auto-onboarding.service.ts). So anykidnot already registered in the DB false-negatives in the validator while/authsucceeds — exactly the reporter's "rotated the kid, still fails" symptom.Fix: extracted the partner-JWKS verification core into a shared
verifyConfigJwtViaPublishedJwks()reused by bothtryAutoOnboardand the validator'ssignaturestage. The validator now mirrors the runtime's full key resolution (DB → deployment JWKS → publishedjwks_url), so it no longer rejects a signature/authaccepts. Honours the/llmpromise that the validator "runs the same pipeline as the auth runtime."Also (#13 doc gaps): documented config-JWT
exp/isssemantics in/llm—expis enforced if present (recommended, ~15m) but not required;issis not checked (thedomainclaim is the trust anchor). Note: the access-tokenname/pictureclaims the issue mentions do not exist intoken.service.ts(onlyemail/domain/client_id/role/tv/org+ standard claims), so the §4.2 table is correct as-is — not adding non-existent claims.#7 —
/apischema didn't match the real/org/*contractVerified every claim.
/apinow advertises, for all/org/*endpoints:domainandconfig_urlquery (all routes useDomainQuerySchema.strict()viaparseDomainContextHook)org_features.enabledrequired (404 otherwise); reads & mutations require theX-UOA-Access-Tokenheader (acting user = itsuserId, new orgs owned by it); non-superuser create needsallow_user_create_org(403ORG_CREATION_NOT_ALLOWED)POST /org/organisationsbody corrected to{ name }only (was wrongly{ name, owner_id })/llmcreate-org row fixed to match. The org-features gate now carries anORG_FEATURES_DISABLEDcode, andORG_FEATURES_DISABLED/ORG_CREATION_NOT_ALLOWED/DOMAIN_MISMATCHgained debug-mode explanations. Production error responses stay generic ({ error: "Request failed" }) — the codes only surface whenDEBUG_ENABLED, preserving the no-enumeration posture.#1 — already fixed (will close separately)
The UI (
SocialButtons.tsx) and backend (social/index.ts) now both gate on the sameenabled_auth_methodsfield (theallowed_social_providersdivergence the issue describes no longer exists), andSOCIAL_PROVIDER_DISABLEDalready has a sanitized debug-page enrichment. No code change needed.Tests
pnpm typecheck✅ ·lint✅verifyConfigJwtViaPublishedJwks(happy path, kid-not-in-JWKS, non-opt-in) ✅auto-onboarding.service.test.ts(12) +config-verify.test.ts(7) +root.test.ts✅auth-entrypoint*,internal-admin-auth) are environmental (missingAuth/dist/Admin/dist) — identical on cleanmain, unaffected by this change.🤖 Generated with Claude Code
Closes #7
Closes #13