Repository navigation
fix(plugin-auth): Register SAML Provider registers a complete form (it answered 400 for every one) - #30402
Conversation
…endor requires Claude-Session: https://claude.ai/code/session_01Utp6CboMj2jstF8f9snYQE Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CboMj2jstF8f9snYQE Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CboMj2jstF8f9snYQE Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CboMj2jstF8f9snYQE Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 442b4519672d2ad896492b3c985196b23afed49d && git checkout 442b4519672d2ad896492b3c985196b23afed49d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0e4dfbe27a2a312864cd8d14e4954b6af14e2a22 815fcdcdada86647a59b69548c0e534921bf92d3 && git checkout -B drift-repro 0e4dfbe27a2a312864cd8d14e4954b6af14e2a22 && git merge --no-ff 815fcdcdada86647a59b69548c0e534921bf92d3
node scripts/docs-audit/affected-docs.mjs --json 0e4dfbe27a2a312864cd8d14e4954b6af14e2a22 |
|
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF 席位意见:ACCEPT(#30352)。Setup → SSO 提供方 → 注册 SAML 时,桥接层现在把表单里的 IdP 发行方作为 席位核对Head-sha:
|
Requested by Bill · project thread
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF
Fixes #30352
Clause-②: no — a form the bridge always refused registers; nothing a caller sends today is refused
Metadata-protocol: none
维护者速读
issuer字段)作为samlConfig.idpMetadata.entityID发给@better-auth/sso。表单字段和必填项都没变。SAML_REGISTER_FAILED拒绝("idpMetadata.entityID is required when IdP metadata XML is not provided"),Setup 里根本注册不了任何 SAML 提供方。packages/plugins/plugin-auth/src/register-sso-provider.ts的samlConfig那一段(新增idpMetadata);②register-sso-provider.test.ts末尾新增的真实路由测试块。pnpm --filter @objectstack/plugin-auth exec vitest run src/register-sso-provider.test.ts,应看到 10 个用例全部通过,其中「a complete form registers the provider」返回 200,并存下一行提供方记录。What changes
runRegisterSamlProviderFromForm(the bridge behindPOST /api/v1/auth/admin/sso/register-saml) now sendssamlConfig.idpMetadata: { entityID: issuer }. The Register SAML Provider form has no IdP metadata XML field, so the bridge always lands in the vendor's manual-configuration arm, and@better-auth/sso1.7.12 requiresidpMetadata.entityIDthere: its register body schema (samlIdentityProviderMetadataSchema, a union of "metadata XML" and "entityID") refused every form at the parse step, andassertSAMLIdentityProviderAuthority(run at register and again increateIdPat sign-in) refuses the same config at runtime.register_saml_provideraction params inpackages/platform-objects/src/identity/sys-sso-provider.object.ts:providerId,issuer(label "IdP Entity ID", help "The IdP's SAML EntityID (issuer)"),domain,entryPoint,cert, optionalidentifierFormat. The IdP entity ID field exists, so this is a server-only change.400 INVALID_REQUESTnaming the missing field, before the vendor is called), the top-levelentryPointandcert(the vendor reads them whenidpMetadatanames nosingleSignOnServiceorcert),spMetadata.entityID(the SP metadata URL, which keeps the top-levelissuerfrom being read as the SP entity ID) and the answer (providerId,acsUrl,spMetadataUrl).Why
An administrator who sets up enterprise sign-in from Setup → SSO Providers → Register SAML Provider could not register any SAML provider there: every complete form answered
400 SAML_REGISTER_FAILED. The only working path was the vendor's raw/sso/register, which Setup does not expose (measured by the #30300 dev, report 6652003986).Tests
register-sso-provider.test.ts: "Register SAML Provider through the REAL route". The plugin's ownregisterAuthRoutesmounts the bridge on a real Hono app. The auth manager'shandleRequestis a realbetterAuth()instance with the realsso()plugin and a memory adapter, and the vendor sees the real session cookie of a signed-up user. The platform-admin gate is judged on a platform-admin session shape (its refusals are pinned inadmin-sso-bridge-gate.test.ts).acsUrl, and exactly one provider is stored, whosesamlConfig.idpMetadatais{ entityID: 'https://idp.acme.example/entity' }.INVALID_REQUESTnamingissuer, and nothing is stored.idpMetadata: { entityID }in the re-dispatched body./sso/register, which is why this was never seen. The OIDC sibling already had a real-vendor case.Verification
All at head
815fcdcd(main870ebce3merged; nothing newer onmainat merge time).node scripts/ablation-replace.mjs --deleteon the oneidpMetadata: { entityID: issuer },line (anchor 1 → 0, blob341a85ef→967366eb), then the file's suite: 3 failed / 7 passed. The real-route case fails with exactly the card's answer:{"success":false,"error":{"code":"SAML_REGISTER_FAILED","message":"[body.samlConfig.idpMetadata] idpMetadata.entityID is required when IdP metadata XML is not provided"}}. Restored (git diff HEADempty). The test file imports the bridge fromsrc, so no build sits between the mutation and the run.pnpm --filter @objectstack/plugin-auth test: 223 files passed, 3,937 tests passed, 10 skipped.pnpm --filter @objectstack/plugin-auth typecheck(tsc, examples,check:test-typecheck): exit 0.--no-inline-config) on both changed source files: exit 0.pnpm lint(repo-wide): exit 0.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 61 commands: 60 exit 0, and--ranreconciles 61 derived, 60 run, 1 NOT-MEASURED, 0 UNRUN.check-plugin-teardown-shape --self-testfirst exited 3 on the shallow clone and exits 0 once its pinned commit is fetched.check:dual-build-cjs-loadsexits 3 locally (PREREQUISITE NOT MET: not every workspace package is built in this worktree), NOT MEASURED, left to CI.Acceptance notes
issueras a URL, so an IdP whose EntityID is a URN (urn:...) is still refused by@better-auth/ssoat the parse step. Not changed here; noted for the seat.Human Minimum
packages/specholds no runtime logic. (not touched)pnpm lintpasses.main, one pull request. (the designated thread branch, as the Claim records)patch,@objectstack/plugin-auth), and both declaration lines are filled in here and in the changeset.packages/spectouched: its generated artifacts are regenerated and committed. (not touched)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01Utp6CboMj2jstF8f9snYQE