Repository navigation
Commit 26c2efd
fix(plugin-auth): Register SAML Provider registers a complete form (it answered 400 for every one) (#30402)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg8twJDyDkhCm8SFYtruR9Rb)_
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
## 维护者速读
- 改了什么:Setup → SSO Providers → Register SAML Provider 的服务端桥接,现在把表单里的「IdP
Entity ID」(`issuer` 字段)作为 `samlConfig.idpMetadata.entityID` 发给
`@better-auth/sso`。表单字段和必填项都没变。
- 为什么改:在这之前,管理员在 Setup 里填完整的 SAML 表单,每次都被 400 `SAML_REGISTER_FAILED`
拒绝("idpMetadata.entityID is required when IdP metadata XML is not
provided"),Setup 里根本注册不了任何 SAML 提供方。
- 风险与回滚:只在发给供应商的请求里多带一个 IdP
描述字段,值取自管理员已经在填的字段;漏填字段时仍然在桥接层被拒,并点名缺哪个字段。回滚就是 revert 这个 PR。
- 先看:① `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 behind `POST
/api/v1/auth/admin/sso/register-saml`) now sends
`samlConfig.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/sso` 1.7.12
requires `idpMetadata.entityID` there: its register body schema
(`samlIdentityProviderMetadataSchema`, a union of "metadata XML" and
"entityID") refused every form at the parse step, and
`assertSAMLIdentityProviderAuthority` (run at register and again in
`createIdP` at sign-in) refuses the same config at runtime.
- **Where the value comes from.** The form field set is read on the
server side, from the `register_saml_provider` action params in
`packages/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`, optional
`identifierFormat`. The IdP entity ID field exists, so this is a
server-only change.
- **Unchanged.** The required set, the refusal of an incomplete form
(`400 INVALID_REQUEST` naming the missing field, before the vendor is
called), the top-level `entryPoint` and `cert` (the vendor reads them
when `idpMetadata` names no `singleSignOnService` or `cert`),
`spMetadata.entityID` (the SP metadata URL, which keeps the top-level
`issuer` from 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
- **New block in `register-sso-provider.test.ts`: "Register SAML
Provider through the REAL route".** The plugin's own
`registerAuthRoutes` mounts the bridge on a real Hono app. The auth
manager's `handleRequest` is a real `betterAuth()` instance with the
real `sso()` 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 in
`admin-sso-bridge-gate.test.ts`).
- A complete form answers 200 with `acsUrl`, and exactly one provider is
stored, whose `samlConfig.idpMetadata` is `{ entityID:
'https://idp.acme.example/entity' }`.
- A form without the IdP Entity ID answers 400 `INVALID_REQUEST` naming
`issuer`, and nothing is stored.
- **Updated:** the existing shape case also asserts `idpMetadata: {
entityID }` in the re-dispatched body.
- Every earlier SAML case used a stand-in for `/sso/register`, which is
why this was never seen. The OIDC sibling already had a real-vendor
case.
## Verification
All at head `815fcdcd` (`main` `870ebce3` merged; nothing newer on
`main` at merge time).
- **Ablation.** `node scripts/ablation-replace.mjs --delete` on the one
`idpMetadata: { entityID: issuer },` line (anchor 1 → 0, blob `341a85ef`
→ `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 HEAD` empty). The test file imports
the bridge from `src`, so no build sits between the mutation and the
run.
- **Suites.** `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.
- **Lint.** eslint (`--no-inline-config`) on both changed source files:
exit 0. `pnpm lint` (repo-wide): exit 0.
- **Gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 61 commands: 60 exit 0, and `--ran`
reconciles 61 derived, 60 run, 1 NOT-MEASURED, 0 UNRUN.
`check-plugin-teardown-shape --self-test` first exited 3 on the shallow
clone and exits 0 once its pinned commit is fetched.
`check:dual-build-cjs-loads` exits 3 locally (PREREQUISITE NOT MET: not
every workspace package is built in this worktree), NOT MEASURED, left
to CI.
## Acceptance notes
- The vendor's register body schema types the top-level `issuer` as a
URL, so an IdP whose EntityID is a URN (`urn:...`) is still refused by
`@better-auth/sso` at the parse step. Not changed here; noted for the
seat.
- The form has no IdP metadata XML field, so the vendor's metadata-XML
arm is not reachable from Setup; the bridge does not invent one.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(not touched)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request. (the designated thread branch, as the Claim records)
- [x] 5. A change that publishes carries a changeset (`patch`,
`@objectstack/plugin-auth`), and both declaration lines are filled in
here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (not touched)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Utp6CboMj2jstF8f9snYQE
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 6fa842a commit 26c2efd
3 files changed
Lines changed: 131 additions & 2 deletions
File tree
- .changeset
- packages/plugins/plugin-auth/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
| 2 | + | |
| 3 | + | |
3 | 4 | | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| 8 | + | |
| 9 | + | |
7 | 10 | | |
8 | 11 | | |
9 | 12 | | |
| |||
152 | 155 | | |
153 | 156 | | |
154 | 157 | | |
| 158 | + | |
| 159 | + | |
155 | 160 | | |
156 | 161 | | |
157 | 162 | | |
| |||
177 | 182 | | |
178 | 183 | | |
179 | 184 | | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
246 | 246 | | |
247 | 247 | | |
248 | 248 | | |
249 | | - | |
| 249 | + | |
| 250 | + | |
250 | 251 | | |
251 | 252 | | |
252 | 253 | | |
| |||
298 | 299 | | |
299 | 300 | | |
300 | 301 | | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
301 | 309 | | |
302 | 310 | | |
303 | 311 | | |
| |||
0 commit comments