fix(auth): link SCIM-provisioned users through SSO and surface sign-in errors

SCIM provisioning created users with emailVerified: false, so better-auth
refused to link the matching SSO account (account_not_linked), and the
resulting redirect landed on better-auth's bare /error page with no
actionable feedback.

- Mark SCIM-provisioned users as email-verified at creation time, same
  trust rationale already applied to admin-invited credential users.
- Sync a SCIM user's membership role to the organization's configured
  default role instead of the hardcoded "member" the SCIM plugin creates.
- Set errorCallbackURL/onAPIError.errorURL to the sign-in page so failed
  SSO/OAuth callbacks land back on Dokploy's UI instead of better-auth's
  generic error page.
- Read the error query param on the sign-in page and show it through the
  existing AlertBlock instead of failing silently.

Fixes #4973
This commit is contained in:
Mauricio Siu
2026-08-13 02:11:15 -06:00
parent 92a1f0d0de
commit a1a672b672
5 changed files with 48 additions and 3 deletions
+21 -2
View File
@@ -77,6 +77,9 @@ const createBetterAuth = () =>
...(!IS_CLOUD ? ["/verify-email"] : []),
],
secret: betterAuthSecret,
onAPIError: {
errorURL: "/",
},
...(!IS_CLOUD
? {
advanced: {
@@ -165,6 +168,9 @@ const createBetterAuth = () =>
user: {
create: {
before: async (_user, context) => {
if (context?.path.includes("/scim")) {
return { data: { emailVerified: true } };
}
if (!IS_CLOUD) {
const xDokployToken =
context?.request?.headers?.get("x-dokploy-token");
@@ -197,8 +203,7 @@ const createBetterAuth = () =>
}
} else {
const isSSORequest = context?.path.includes("/sso");
const isSCIMRequest = context?.path.includes("/scim");
if (isSSORequest || isSCIMRequest) {
if (isSSORequest) {
return;
}
const isAdminPresent = await db.query.member.findFirst({
@@ -251,6 +256,20 @@ const createBetterAuth = () =>
}
if (isSCIMRequest) {
const membership = await db.query.member.findFirst({
where: eq(schema.member.userId, user.id),
});
if (membership) {
const defaultRole = await resolveOrganizationDefaultRole(
membership.organizationId,
);
if (defaultRole !== membership.role) {
await db
.update(schema.member)
.set({ role: defaultRole })
.where(eq(schema.member.id, membership.id));
}
}
return;
}