Skip to content

fix(plugin-auth): OIDC SSO registration works again — drop the retired oidcConfig.mapping.id - #8221

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-8193-oidc-mapping-id-strictobject
Aug 12, 2026
Merged

fix(plugin-auth): OIDC SSO registration works again — drop the retired oidcConfig.mapping.id#8221
os-zhuang merged 2 commits into
mainfrom
claude/issue-8193-oidc-mapping-id-strictobject

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Part of #8193

Part of, not Fixes: this lands the whole plugin-auth half — OIDC SSO
registration works again — but the card also asks for the now-dead mapId
action param to go or be re-pointed, and that param lives in
packages/platform-objects (domain:metadata's package), which this card's
dispatch put out of scope. That half is filed separately and linked below.

Re-measured first

Reproduced on the real path before changing anything, against the installed
@better-auth/sso@1.7.0-rc.2 (resolved from
packages/plugins/plugin-auth/node_modules), by driving the real
/sso/register endpoint of a real betterAuth() instance carrying the real
sso() plugin:

bodyresult
exactly what the bridge emits today400[body.oidcConfig.mapping] Unrecognized key: "id"
same body, id dropped401 Unauthorized — cleared the schema, stopped at the session gate
same body, id re-homed to extraFields401 Unauthorized — parses, but see below

The 400 matches the reported symptom verbatim. The second row is the load-bearing
one: @better-auth/sso validates the body before the endpoint's session
gate, so reaching 401 is positive evidence the emitted body parsed.

id was retired upstream, not moved

Read out of the two package versions rather than from docs or memory:

  • 1.6.20 declared oidcMappingSchema as a plain, non-strict z.object
    that did carry id, and honoured it when resolving the federated user:
    id: rawUserInfo[mapping.id || "sub"] / id: idToken[mapping.id || "sub"].
    So the bridge did not invent the key — it was correct against the version the
    stale attestation names.
  • 1.7.0-rc.2 deletes the member, closes the object with z.strictObject,
    and hard-wires the federated subject to the OIDC sub claim
    (id: readStringClaim(rawUserInfo, "sub") / id: idToken.sub), then
    cross-checks it (id_token_subject_missing,
    id_token_userinfo_subject_mismatch).

So there is no new home for the key, and dropping it is not merely what
makes the 400 go away. extraFields is the one open member and it does parse —
but it is spread beforeid in the profile literal, so an extraFields.id
is overwritten by sub before it is ever read. Re-homing there would have read
as configured while doing nothing, which is worse than dropping it.

What changed

  • register-sso-provider.ts emits mapping: { email, name } — the two members
    the strict schema requires. The email/name claim mappings the form collects
    still work exactly as before.
  • Because the subject claim is not configurable anywhere any more, a
    registration asking for a non-sub user-ID claim is now refused with a
    clear INVALID_REQUEST instead of being accepted and silently discarded.
    Empty, or sub (the value the form suggests), registers as normal. This is the
    in-scope half of the card's "the mapId param should go or be re-pointed":
    the param itself is not mine to touch, but honouring it silently is.
  • auth-manager.ts — the stale attestation. It claimed the plugin "accepts NO
    schema option (verified against 1.6.20 ... runtime never reads
    options.schema)". Re-checked against the pinned 1.7.0-rc.2: that claim is now
    false
    SSOOptions.schema.ssoProvider exists and the runtime honours
    modelName plus a per-field fieldName map. The comment now records what was
    actually measured, and says plainly that only the mapping surface was
    re-verified in depth. The adapter-level bridge itself is left alone (separate
    change, filed below) rather than rewired silently.

The coverage hole

The OIDC bridge had zero tests — the reason a total breakage shipped. Five
cases added; the two load-bearing ones drive the real/sso/register of a
real better-auth instance, so the emitted body is judged by the installed
package's own Zod schema rather than by a hand-copied restatement that would
drift silently on the next bump.

Predict-then-mutate ablation. Predicted, before running, that restoring only
the id member (keeping the new guard) turns 4 of 8 red: both real-path cases,
the emitted-shape case, and the explicit-sub case — with the mapId guard case
and all three SAML cases staying green. Measured: exactly that, 4 failed | 4 passed, all plain AssertionErrors, and the failure text is verbatim the 400
this card reports.

Verification

  • pnpm --filter @objectstack/plugin-auth test45 files, 1080 tests passed
    (post-merge with origin/main).
  • pnpm --filter @objectstack/plugin-auth typecheck — clean.
  • Gates: check:nul-bytes, check:test-source-alias,
    check:changeset-gate-self-tests, check:objectui-changeset,
    check:docs-audit-scope, check:error-code-casing,
    check:query-options-erasure, check:engine-double-contract,
    check-changeset-no-major — all green.
    check-objectui-pin-fresh is red, and is red on an untouched main too
    (pre-existing, unrelated to this diff).

origin/main was merged before opening; it touched auth-manager.ts too and
merged cleanly with no conflict.


Generated by Claude Code

…OIDC SSO registration (#8193)
@better-auth/sso declares oidcConfig.mapping as a strict object. `id` was a
real member in 1.6.20 and was honoured at login; the pinned 1.7.0-rc.2 retires
it and reads the federated subject from the OIDC `sub` claim directly, so the
key the bridge always sent made every OIDC registration answer 400.
Emit { email, name } (the strict schema's required members), refuse a non-`sub`
user-ID claim loudly rather than discarding it silently, and pin the path with
tests that drive the real /sso/register endpoint of a real better-auth
instance. Also corrects the stale "verified against 1.6.20" attestation in
auth-manager.ts to what was actually re-measured against 1.7.0-rc.2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEVB6w7D7uCszR9Mw1BL73
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 12, 2026 9:18pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth.

8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via @objectstack/plugin-auth)
  • content/docs/deployment/production-readiness.mdx(via @objectstack/plugin-auth)
  • content/docs/kernel/contracts/cache-service.mdx(via @objectstack/plugin-auth)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/plugin-auth)
  • content/docs/permissions/authentication.mdx(via @objectstack/plugin-auth)
  • content/docs/permissions/sso.mdx(via @objectstack/plugin-auth)
  • content/docs/plugins/index.mdx(via @objectstack/plugin-auth)
  • content/docs/plugins/packages.mdx(via @objectstack/plugin-auth)

2 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/plugin-auth)
  • content/docs/releases/v9.mdx(via @objectstack/plugin-auth)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 12, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 12, 2026 21:38
@os-zhuang
os-zhuang added this pull request to the merge queueAug 12, 2026
Merged via the queue into main with commit 759a53aAug 12, 2026
26 of 27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8193-oidc-mapping-id-strictobject branch August 12, 2026 21:55
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…ovider (objectstack-ai#8222) (objectstack-ai#8800)
The OIDC subject claim is not configurable in @better-auth/sso@1.7.0-rc.2:
oidcConfig.mapping is a z.strictObject with no `id` member, the subject is
hard-wired to the `sub` claim, and extraFields is spread before `id` so it is
not an escape hatch either. The form field's only accepted values were empty
and the `sub` it already defaulted to; anything else has answered
INVALID_REQUEST since objectstack-ai#8221. Removing it restores declared = enforced.
Translation bundles regenerated via scripts/check-i18n-bundles.mjs --write,
not hand-edited. The plugin-auth bridge guard is kept as belt-and-braces for
non-form callers; only its doc comment changed.
Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude