Repository navigation
feat(sso)!: move to better-auth 1.7 with managed scim connections - #71
Merged
Merged
Conversation
hono moves to 4.13.11 in both the api and the app, and the S3 client and presigner move to 3.1142.0. Patch releases only, no code change needed.
The Stripe plugin writes its `subscription` rows through BetterAuth's Drizzle adapter, which resolves every model from the schema object `db` is built with. The billing tables were exported by `@packages/drizzle` but never added to that object, so any subscription write coming from the plugin (checkout completion, webhook updates) would have failed on a missing model. better-auth 1.7 checks this at boot and reported it. The billing schema now joins the adapter schema, with a comment saying why a plugin-owned table has to be listed there.
Moves the whole better-auth family from 1.6.30 to 1.7.6. The pin existed because `@better-auth/scim` 1.7 is a rewrite: per-provider bearer tokens, `scim_provider` and the generate-token/delete-connection endpoints are gone, replaced by managed connections the application drives through server APIs, with identity and membership left to application callbacks. Each organization now has at most one connection, its provisioning domain being the organization id. `ScimConnectionService` and `GET/POST/DELETE /settings/organization/scim-connection` read its status, issue or rotate its token (rotation revokes every other credential) and decommission it, gated by a new owner-only `scim:["manage"]` statement and the `sso` entitlement. The settings card shows the expiry and offers rotate and disconnect. Membership is a projection the plugin runs inside its own transaction: an active user gets a member row if a seat is free (otherwise a SCIM 402 that rolls the provisioning back), an inactive one loses it, an owner is never removed. An existing account is linked only when its email is verified on a verified SSO domain of the organization. The after-hook reports every SCIM user change and the membership it caused, with the connection creator as actor, and `scim.connection.token_rotated` joins the catalog. The plugin requires native transactions, so the Drizzle adapter runs with `transaction: true`, and it stores tokens under a new `SCIM_CREDENTIAL_HASH_SECRET`, required in production. Migrations 0022 and 0023 drop the legacy table and create the ten plugin tables with short explicit foreign key names. Connections issued before this change stop working: directories have to be reconnected with a new token. The rest of 1.7 is adapted too: SAML registration sends the SP entity ID as `issuer` and the IdP's as `idpMetadata.entityID` (the form field is renamed), two-factor enable narrows its new result union, the removed `scimClient` is dropped from the front, and the vitest peer exception that only 1.6 needed is gone. Verified live against Postgres: sign-up, sign-in, TOTP, passkey options, the full SCIM user lifecycle with its events, the seat cap, rotation, disconnect and SAML registration.
The SCIM debt recorded at G.1d is closed in both the history and the roadmap, and a new history entry keeps the non-obvious parts of the 1.7 migration (managed connections, transactional adapter, the new secret, the SAML field split, the latent Stripe schema gap it surfaced) with what was verified live and what was not. It also states why three packages stay behind `latest`, each measured: `@better-auth/utils` 0.4.2 is the exact version better-auth 1.7.6 ships, `@simplewebauthn/server` 13 is the major passkey 1.7.6 depends on (14 brings TS2883 back), and `conventional-changelog-conventionalcommits` 10 cannot render notes with the writer 8 that semantic-release 25's stable plugins still bundle.
Issuing a directory token lists the organization's connection, then creates or rotates it, then revokes every other credential, across several plugin transactions. Four concurrent first issues created four connections, and concurrent rotations could each revoke the credential the other had just issued. Writes for one organization now run behind an advisory lock held for their whole duration. The events are emitted after the plugin committed, on a savepoint. A failed emit is reported to telemetry and the token is still returned: it is the only copy of the credential that now works, and failing the request would leave the owner with a revoked token and no replacement. Only issuing a token requires the `sso` entitlement. Reading the status and disconnecting stay available after a downgrade, so an owner can always cut off a directory. Proven by `check:scim-connection` against Postgres (four connections before, one after) and by route and service tests.
With native transactions on, the SSO plugin creates the user and its session in one transaction, and `session.create.before` bootstraps the user's Personal organization. That bootstrap ran through `db`, on another connection, where the user does not exist yet: the `member` insert failed its foreign key and the whole sign-in with it. The adapter now receives `db` through `withPublishedTransactions`, which publishes every transaction it opens, and the in-hook writes (`ensurePersonalOrgFor`, the event bridge, the pending-email and empty-org writes) join it, committing or rolling back with the change that caused them. The same mechanism closes the SCIM seat race: five concurrent provisionings on a 3-seat plan ended with 6 members, because the count ran on another connection without a lock. The count now runs on the plugin transaction behind a per-organization advisory lock. SCIM provisioning also stops trusting the directory with addresses the organization does not control: an email outside its verified SSO domains is refused with a SCIM 403 `SCIM_DOMAIN_NOT_VERIFIED`, instead of creating an account that reserves someone else's address. Once the plan loses `sso`, new users and members are refused with `SCIM_PLAN_REQUIRED`, while deactivations and removals still apply. Proven against Postgres by `check:auth-transaction` and `check:scim-seats`, both failing before this change and wired into CI next to the existing checks together with `check:scim-connection`. `@better-auth/core` joins the dev dependencies, pinned to the version better-auth ships, for the transaction helper these checks drive.
Marks the move to `@better-auth/scim` managed connections as the breaking change it is for any deployment that provisions through SCIM, and records the review fixes in the feature inventory and the history. BREAKING CHANGE: SCIM tokens issued before the move to managed connections no longer authenticate, since the legacy `scim_provider` table is dropped. Every organization that provisions through SCIM must issue a new token from the SSO settings page and paste it into its identity provider. The API also needs a new `SCIM_CREDENTIAL_HASH_SECRET` (at least 32 characters, required in production), the HMAC key directory tokens are stored under; rotating it invalidates every issued token.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings every dependency that can move to its latest release, including the better-auth 1.6.30 to 1.7.6 jump that had been held back by the SCIM config change.
Breaking. SCIM moves to one managed connection per organization, created and rotated by the owner from the SSO settings. Tokens are stored as an HMAC under the new
SCIM_CREDENTIAL_HASH_SECRET(32+ chars, required in production), so every directory provisioned before must be reconnected with a newly issued token. The commit carries aBREAKING CHANGE:footer;semantic-release --dry-runcomputes a major.What changed with 1.7. Owner-only
scim:managepermission and three routes under/settings/organization/scim-connection(issuing requires thessoplan, reading and disconnecting stay open after a downgrade). Directories of an org withoutssocan no longer create users or members. SCIM creates accounts only inside the org's verified SSO domains (SCIM_DOMAIN_NOT_VERIFIEDotherwise) and links an existing account only when its email and domain are verified. Seat caps and connection writes are serialized per org with an advisory lock. The Drizzle adapter runs in transactions, and the auth hooks (personal org self-heal, event emits) now join the plugin's transaction, which fixes a first SSO sign-in that would have failed on thememberforeign key. Migrations 0022 and 0023 only add tables. The Stripe plugin also gets thesubscriptiontable it was missing, so its writes no longer fail.Still pinned, verified.
@simplewebauthn/server13 (@better-auth/passkey1.7.6 requires^13, 14 breaks the type-check),@better-auth/utils0.4.2 (the exact version better-auth 1.7.6 ships),conventional-changelog-conventionalcommits9 (10 still breaks note generation with semantic-release 25's writer).Checks. Pre-push green (920 api tests, also in random order). Three new real-database checks, each failing before its fix:
check:auth-transaction,check:scim-seats,check:scim-connection. Exercised live against a local API: sign-up, sign-in, TOTP, passkey options, the full SCIM user cycle, the seat cap, domain refusal, rotation, downgrade. Not covered: a browser round trip against a real IdP.Merge commit, not squash.