Skip to content

Commit eea2603

Browse files
test(plugin-auth): pin that a SAML provider with a URN IdP Entity ID registers and starts a sign-in (#30450)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgDsWCv46MXRaGzVjayCRqE9)_ domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF Fixes #30404 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:只加测试。在 Register SAML Provider 的真实路由测试里加两个用例:IdP Entity ID 填 URN(`urn:…`)时能注册成功,并且能从这个提供方发起登录。产品代码没改。 - 为什么改:卡片以为供应商会拒收 URN 形式的 Entity ID,实测不会(前提不成立,见 #30404 上的报告)。但供应商的 `issuer` 校验今天能放行 URN 只是因为 zod 4.6.7 的 url() 用的是通用 URL 解析,以后升级若收窄成只认 http(s),URN 的 IdP 会全部注册不了,这两个用例就是为了到时候变红。 - 风险与回滚:只改测试文件,不发布任何包;回滚就是 revert。 - 先看:`packages/plugins/plugin-auth/src/register-sso-provider.test.ts` 末尾新增的 `it.each`(两个 URN 用例)。 - 两分钟验证:`pnpm --filter @objectstack/plugin-auth exec vitest run src/register-sso-provider.test.ts`,应看到 12 个用例全部通过,其中两个「a URN IdP Entity ID registers and starts a sign-in」。 - 需要你做的:无,席位落地。 ## What changes Tests only. Two cases join the existing "Register SAML Provider through the REAL route" block in `register-sso-provider.test.ts`, one per URN shape (`urn:oasis:names:tc:SAML:2.0:idp:acme`, `urn:example:idp`): - `POST /api/v1/auth/admin/sso/register-saml` with a complete form whose IdP Entity ID is the URN answers 200, and the stored provider row carries `issuer` = the URN and `samlConfig.idpMetadata` = `{ entityID: the URN }`. - `POST /api/v1/auth/sign-in/sso` with that `providerId` answers 200 with `redirect: true` and a URL that starts with the form's `entryPoint` followed by `?SAMLRequest=`, so the provider is usable, not only stored. ## Why The card read `@better-auth/sso` 1.7.12's register body schema (`issuer: z.string().url()`) as refusing a URN Entity ID. Measured through the real route on `main` `09720e2c`, it does not: both URN forms registered (200) and both providers started a sign-in (200). In the installed zod 4.6.7, `z.string().url()` is the WHATWG URL parser, which accepts `urn:` values. The premise is false, so there is no product fix (report on #30404, comment 6656283296). What would break it is a narrower check in a later vendor or zod release (the vendor's own field description says "The issuer URL of the provider"). Nothing pinned the URN answer, so that break would land silently. The seat ruled for this regression pin. Why the top-level `issuer` is safe to carry a URN, read in the installed `dist/index.mjs`: sign-in finds a provider by `providerId`, organization or domain, never by `issuer`; the ACS takes the IdP identity from `idpMetadata.entityID` (`idp.entityMeta.getEntityID()`); the top-level `issuer` is read only as the SP EntityID fallback, behind `spMetadata.entityID`, which the bridge always sends, and inside the provider fingerprint, which is computed from the stored row on both sides. ## Tests - File: 12/12 passed (10 before, plus the two URN cases). - Can the pin fail? Mutation leg through `scripts/ablation-replace.mjs`: the `urn:example:idp` input replaced by a non-URI (anchor 1 to 0, blob `b7c345be` to `751fa12e`). Result: 1 failed / 11 passed, and the failing case shows the vendor's parse refusal, which is the answer a narrowed check would give: `400 SAML_REGISTER_FAILED "[body.issuer] Invalid URL"`. Restored: blob equals HEAD, `git diff HEAD` empty. The test imports `src`, so no build sat between the legs. - `pnpm --filter @objectstack/plugin-auth test` (after rebuilding the closure on the merged tree): 224 files passed, 3956 passed / 10 skipped; lock VERDICT command-exit 0. - `pnpm --filter @objectstack/plugin-auth typecheck`: lock VERDICT command-exit 0. ## Verification At head `1aaf6224` (`main` `8fa738fb` merged). - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 51 commands, each recorded as `command :: exit code` before any pipe (`gates-ran.txt` in the seat folder). 50 exit 0. `check-plugin-teardown-shape --self-test` exited 3 on the shallow clone and 0 after fetching its pinned commit `621a4876`. `pnpm check:dual-build-cjs-loads` exited 3, PREREQUISITE NOT MET (not every workspace package is built here): NOT MEASURED, left to CI. - `dispatch-gates --ran`: 51 derived, 50 run, 1 NOT-MEASURED, 0 UNRUN. - `pnpm lint` (repo-wide, `--no-inline-config`): exit 0. - Changed lines: 36 (+36 / -0), one file. No changeset: the diff is one test file, and `@objectstack/plugin-auth` publishes only `dist`, `README.md` and `CHANGELOG.md`; `skip-changeset` is sent with this PR. --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CbQwaXVXpbomZRKZq2)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent f273c9f commit eea2603

1 file changed

Lines changed: 36 additions & 0 deletions

File tree

‎packages/plugins/plugin-auth/src/register-sso-provider.test.ts‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,4 +286,40 @@ describe('Register SAML Provider through the REAL route — the INSTALLED @bette
286286
expect(body.error?.message).toContain('issuer');
287287
expect(db.ssoProvider).toHaveLength(storedBefore);
288288
});
289+
290+
// A SAML EntityID is a URI, and many IdPs publish a URN. The bridge sends the
291+
// form's Entity ID both as the top-level `issuer` and as
292+
// `idpMetadata.entityID`, and the vendor's register body types `issuer` as
293+
// `z.string().url()`. In the installed zod (4.6.7) that check is the WHATWG
294+
// URL parser, which accepts `urn:` values, so a URN form registers. A vendor
295+
// or zod release that narrows the check to http(s) would refuse every URN
296+
// IdP at the vendor's parse step; these cases turn red when that happens.
297+
//
298+
// Usable, not only stored: sign-in finds the provider by `providerId`
299+
// (or organization / domain), never by `issuer`, and the ACS reads the IdP
300+
// identity from `idpMetadata.entityID`.
301+
it.each([
302+
['urn-oasis', 'urn:oasis:names:tc:SAML:2.0:idp:acme'],
303+
['urn-short', 'urn:example:idp'],
304+
])('a URN IdP Entity ID registers and starts a sign-in (%s)', async (providerId, entityId) => {
305+
const res = await post({ ...COMPLETE_FORM, providerId, issuer: entityId });
306+
const body: any = await res.json();
307+
expect(res.status, JSON.stringify(body)).toBe(200);
308+
expect(body.success).toBe(true);
309+
expect(body.data.providerId).toBe(providerId);
310+
311+
const row = db.ssoProvider.find((p) => p.providerId === providerId);
312+
expect(row?.issuer).toBe(entityId);
313+
expect(JSON.parse(row.samlConfig).idpMetadata).toEqual({ entityID: entityId });
314+
315+
const signIn = await app.request(`${ORIGIN}${BASE}/sign-in/sso`, {
316+
method: 'POST',
317+
headers: { 'content-type': 'application/json', origin: ORIGIN },
318+
body: JSON.stringify({ providerId, callbackURL: '/' }),
319+
});
320+
const started: any = await signIn.json();
321+
expect(signIn.status, JSON.stringify(started)).toBe(200);
322+
expect(started.redirect).toBe(true);
323+
expect(started.url.startsWith(`${COMPLETE_FORM.entryPoint}?SAMLRequest=`)).toBe(true);
324+
});
289325
});

0 commit comments

Comments
 (0)