Skip to content

fix(api): /config/validate honours published JWKS (#13) + accurate /org/* contract (#7)#14

Open
rafiki270 wants to merge 1 commit into
mainfrom
fix/issues-config-validate-and-org-schema
Open

fix(api): /config/validate honours published JWKS (#13) + accurate /org/* contract (#7)#14
rafiki270 wants to merge 1 commit into
mainfrom
fix/issues-config-validate-and-org-schema

Conversation

@rafiki270

@rafiki270 rafiki270 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Fixes two integration-blocking issues found in production onboarding, plus closes a third as already-resolved.

#13/config/validate false-negatives signature for a JWT the runtime accepts

Root cause (verified in code): the validator's signature stage only resolves the key via per-domain DB key (findJwkByKidDb) → deployment CONFIG_JWKS_URL. The live /auth runtime has an extra fallback: on signature failure it calls tryAutoOnboard, which fetches the JWKS the partner publishes at the jwks_url in the config payload and verifies against that (auto-onboarding.service.ts). So any kid not already registered in the DB false-negatives in the validator while /auth succeeds — exactly the reporter's "rotated the kid, still fails" symptom.

Fix: extracted the partner-JWKS verification core into a shared verifyConfigJwtViaPublishedJwks() reused by both tryAutoOnboard and the validator's signature stage. The validator now mirrors the runtime's full key resolution (DB → deployment JWKS → published jwks_url), so it no longer rejects a signature /auth accepts. Honours the /llm promise that the validator "runs the same pipeline as the auth runtime."

Also (#13 doc gaps): documented config-JWT exp/iss semantics in /llmexp is enforced if present (recommended, ~15m) but not required; iss is not checked (the domain claim is the trust anchor). Note: the access-token name/picture claims the issue mentions do not exist in token.service.ts (only email/domain/client_id/role/tv/org + standard claims), so the §4.2 table is correct as-is — not adding non-existent claims.

#7/api schema didn't match the real /org/* contract

Verified every claim. /api now advertises, for all /org/* endpoints:

  • required domain and config_url query (all routes use DomainQuerySchema.strict() via parseDomainContextHook)
  • a contract note: org_features.enabled required (404 otherwise); reads & mutations require the X-UOA-Access-Token header (acting user = its userId, new orgs owned by it); non-superuser create needs allow_user_create_org (403 ORG_CREATION_NOT_ALLOWED)
  • POST /org/organisations body corrected to { name } only (was wrongly { name, owner_id })

/llm create-org row fixed to match. The org-features gate now carries an ORG_FEATURES_DISABLED code, and ORG_FEATURES_DISABLED / ORG_CREATION_NOT_ALLOWED / DOMAIN_MISMATCH gained debug-mode explanations. Production error responses stay generic ({ error: "Request failed" }) — the codes only surface when DEBUG_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 same enabled_auth_methods field (the allowed_social_providers divergence the issue describes no longer exists), and SOCIAL_PROVIDER_DISABLED already has a sanitized debug-page enrichment. No code change needed.

Tests

  • pnpm typecheck ✅ · lint
  • New unit tests for verifyConfigJwtViaPublishedJwks (happy path, kid-not-in-JWKS, non-opt-in) ✅
  • auto-onboarding.service.test.ts (12) + config-verify.test.ts (7) + root.test.ts
  • 11 pre-existing suite failures (auth-entrypoint*, internal-admin-auth) are environmental (missing Auth/dist/Admin/dist) — identical on clean main, unaffected by this change.

🤖 Generated with Claude Code

Closes #7
Closes #13

…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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant