Skip to content

fix(plugin-auth): isolate optional better-auth plugin init failures — one broken federation feature must not 500 all of auth#3087

Merged
os-zhuang merged 1 commit into
mainfrom
fix/plugin-auth-optional-plugin-isolation
Jul 17, 2026
Merged

fix(plugin-auth): isolate optional better-auth plugin init failures — one broken federation feature must not 500 all of auth#3087
os-zhuang merged 1 commit into
mainfrom
fix/plugin-auth-optional-plugin-isolation

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Blast-radius follow-up to #3085

In the 15.1.0 incident, @better-auth/oauth-provider threw Cannot set properties of undefined (setting 'modelName') during plugin construction. Because the better-auth instance is lazily (re)built per request, that one optional IdP feature failed the whole instance and turned every auth endpoint — sign-up, sign-in, get-session — into a 500. #3085 fixed that specific version mix; this PR fixes the disproportionate failure mode itself.

Two explicit tiers in AuthManager.buildPluginList

OPTIONAL — degrade gracefully via a new addOptionalPlugin() wrapper: organization, admin, phoneNumber, magicLink, genericOAuth, jwt+oauthProvider (one atomic unit — on failure neither lands), sso, scim, deviceAuthorization. On construction failure the feature is skipped with one loud, actionable console.error, recorded in a new getDegradedAuthFeatures(), and its endpoints 404 — while core email/password + session auth keeps serving. The degraded set resets on every rebuild (applyConfigPatch → healthy restart clears it).

CORE — still fail hard (better a hard 500 than fail-open security):

  • bearer — token auth for every non-cookie API client;
  • twoFactor — skipping it would let 2FA-enrolled accounts sign in on password alone;
  • haveIBeenPwned — operator explicitly opted into breached-password rejection; dropping it silently relaxes policy;
  • customSession — carries positions[]/isPlatformAdmin and the ADR-0069 authGate (password expiry, enforced MFA).

The rationale table lives in the buildPluginList docstring so the tier of any future plugin is a deliberate decision.

OIDC discovery mount is degrade-aware

registerOidcDiscoveryRoutes (the "Failed to register OIDC discovery routes" path from the incident log) now checks the degraded set after getAuthInstance(): if oidcProvider is degraded it logs why and skips mounting /.well-known/{oauth-authorization-server,openid-configuration}, instead of advertising an IdP whose endpoints would all 404.

Regression tests (real pipeline, not mocks)

New auth-manager.optional-plugin-isolation.test.ts — unlike the existing suite it does not mock better-auth: the real handler runs against a minimal in-memory IDataEngine behind the genuine ObjectQL adapter, and only the two plugin factories get controllable failure switches (via importOriginal passthrough):

  • byte-for-byte incident TypeError injected into oauthProvidersign-up 200, get-session 200, degraded set [{feature: 'oidcProvider', error: …modelName…}], loud console.error asserted;
  • healthy control → 200 with the real oauthProvider constructed, degraded set empty (proves the harness doesn't bypass the plugin);
  • throwing core plugin (bearer) → request rejects, nothing recorded as degraded (fail-hard semantics pinned);
  • degraded state clears when the instance rebuilds after the fault is gone.

Full plugin-auth suite: 457/457 pass (20 files). Repo-wide pnpm lint (CI-identical --no-inline-config) clean.

Refs: #3085 · GHSA-j8v8-g9cx-5qf4 (why the family pin exists at all)

🤖 Generated with Claude Code

…rom core auth

Follow-up to #3085. In the 15.1.0 incident, @better-auth/oauth-provider
threw "Cannot set properties of undefined (setting 'modelName')" during
plugin construction, and because the better-auth instance is built
lazily per request, that single optional federation feature turned EVERY
auth endpoint (sign-up / sign-in / get-session) into a 500 — a blast
radius wildly out of proportion to the feature.

AuthManager.buildPluginList now constructs plugins in two explicit tiers:

- OPTIONAL (organization, admin, phoneNumber, magicLink, genericOAuth,
  jwt+oauthProvider as one atomic unit, sso, scim, deviceAuthorization):
  built through addOptionalPlugin(), which on failure skips the feature
  with one loud actionable console.error, records it in
  getDegradedAuthFeatures(), and leaves core email/password + session
  auth up. The feature's endpoints 404 until the operator fixes the
  underlying error and restarts.

- CORE (bearer, twoFactor, haveIBeenPwned, customSession): still fail
  hard. Skipping these would silently weaken auth — 2FA-enrolled
  accounts signing in on password alone, opted-in breached-password
  rejection vanishing, the ADR-0069 authGate (password expiry, enforced
  MFA) and positions[] disappearing from sessions. Better a hard 500
  than fail-open security.

The OIDC discovery mount in auth-plugin.ts consults the degraded set and
skips advertising /.well-known documents for an IdP whose endpoints did
not come up (previously the "Failed to register OIDC discovery routes"
path), logging why instead of sending external clients into 404s.

Regression tests run the REAL better-auth pipeline (no betterAuth mock,
a minimal in-memory IDataEngine behind the genuine ObjectQL adapter)
with the exact incident TypeError injected into oauthProvider: sign-up
and get-session return 200, the degraded feature is reported, the
console.error fires; a throwing CORE plugin (bearer) still rejects; the
degraded set resets on a healthy rebuild. Full plugin-auth suite: 457
tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 17, 2026 4:58am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m and removed documentation Improvements or additions to documentation tests tooling labels Jul 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/deployment/production-readiness.mdx (via @objectstack/plugin-auth)
  • content/docs/getting-started/cli.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)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-auth)
  • content/docs/releases/v9.mdx (via @objectstack/plugin-auth)

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.

@os-zhuang
os-zhuang merged commit 9dbb883 into main Jul 17, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the fix/plugin-auth-optional-plugin-isolation branch July 17, 2026 05:01
os-zhuang added a commit that referenced this pull request Jul 17, 2026
…r would (#3100)

The 15.1.0 incident (#3091): workspace pnpm overrides kept every in-repo CI
job on better-auth 1.7.0-rc.1, but overrides do not ship with published
packages, so every fresh `npx create-objectstack` project resolved an
untested dependency mix that 500'd all auth endpoints. #3085 added the static
gate (override ↔ declaration consistency); this adds the dynamic gate —
install the exact bits a user would get and drive the first-run flow.

scripts/publish-smoke.sh, two modes sharing one probe set:
- pack (default): pnpm-pack all 67 publishable packages (pack applies the
  same manifest rewrites as publish), scaffold a fresh project outside the
  workspace with the repo-built create-objectstack, pin @objectstack/* to
  the tarballs via the project's OWN pnpm-workspace.yaml overrides (nothing
  inherited from the repo), install, and smoke. A lockfile assertion proves
  no @objectstack/* leaked to the registry.
- registry: npx create-objectstack@latest + npm install against the real
  registry — catches ^-range ecosystem drift breaking published versions.

Probes: anonymous get-session 200 → sign-up 200 → sign-in 200 → session
carries the user → REST CRUD round (201/200/200/200) on the scaffolded
object as the seeded dev admin → zero error/fatal log lines (ANSI-stripped;
catches the #3091 'Failed to register OIDC discovery routes' signature).

.github/workflows/publish-smoke.yml:
- workflow_run after each Release run — the changesets release PR is pushed
  with GITHUB_TOKEN, whose events never trigger on:push/pull_request (the
  release PR has no Actions checks today), so the smoke runs after Release
  completes, checks out the release-branch head SHA iff an open release PR
  exists, and reports the verdict back as a commit status on that SHA so it
  is visible on the release PR. Deliberately not in normal PR CI (too slow).
- weekly registry canary (Mon 04:47 UTC) that opens/refreshes an issue on
  failure.

Verified locally: pack mode green on the current tree; a negative run with
plugin-auth's declarations reverted to the 15.1.0 mix (^1.6.23 + scim rc.1)
goes red via the log scan — core sign-in stays up thanks to the #3087
isolation, and the smoke still catches the broken optional-plugin init.
Registry mode green against published 15.1.1. better-sqlite3 stays an
optionalDependency, so a failed native build falls back to WASM sqlite
(#2229) instead of blocking the smoke.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant