From 694583df69a9844347e8bdd8e0e624123d05ca59 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 2 Nov 2026 15:43:32 -0800 Subject: [PATCH 1/2] fix(sso): mark email verified on sign-in through a verified-domain provider --- .../application/admit-sso-user.integration.ts | 42 +++++++++++++++++++ .../auth/sso/application/admit-sso-user.ts | 20 ++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/apps/sim/lib/auth/sso/application/admit-sso-user.integration.ts b/apps/sim/lib/auth/sso/application/admit-sso-user.integration.ts index 072ba900cc0..43523149b99 100644 --- a/apps/sim/lib/auth/sso/application/admit-sso-user.integration.ts +++ b/apps/sim/lib/auth/sso/application/admit-sso-user.integration.ts @@ -207,6 +207,48 @@ describe('SSO admission with a hosted SCIM directory in PostgreSQL', () => { expect(await membership()).toEqual([{ id: memberId, role: 'admin' }]) }) + async function emailVerified() { + const { db, schema, eq } = runtime + const [row] = await db + .select({ emailVerified: schema.user.emailVerified }) + .from(schema.user) + .where(eq(schema.user.id, userId)) + return row?.emailVerified + } + + it('verifies a directory-provisioned member who signs in through the verified-domain provider', async () => { + const { db, schema, eq } = runtime + const memberId = generateId() + await db.insert(schema.member).values({ id: memberId, userId, organizationId, role: 'member' }) + await db.update(schema.user).set({ emailVerified: false }).where(eq(schema.user.id, userId)) + + await expect(admit()).resolves.toEqual({ kind: 'already-member', organizationId, memberId }) + expect(await emailVerified()).toBe(true) + }) + + it('leaves an address outside the provider domain unverified', async () => { + const { db, schema, eq } = runtime + await db + .update(schema.user) + .set({ emailVerified: false, email: `${userId}@elsewhere.test` }) + .where(eq(schema.user.id, userId)) + + await expect(admit()).resolves.toEqual({ kind: 'denied', reason: 'domain-mismatch' }) + expect(await emailVerified()).toBe(false) + }) + + it('leaves the address unverified when the provider domain is not proven', async () => { + const { db, schema, eq } = runtime + await db.update(schema.user).set({ emailVerified: false }).where(eq(schema.user.id, userId)) + await db + .update(schema.ssoProvider) + .set({ domainVerified: false }) + .where(eq(schema.ssoProvider.providerId, providerId)) + + await expect(admit()).resolves.toEqual({ kind: 'denied', reason: 'provider-not-trusted' }) + expect(await emailVerified()).toBe(false) + }) + it('resumes normal JIT admission when the directory permits automatic membership', async () => { const { db, schema, eq } = runtime await db diff --git a/apps/sim/lib/auth/sso/application/admit-sso-user.ts b/apps/sim/lib/auth/sso/application/admit-sso-user.ts index 8e97f7fda49..00be6c327e1 100644 --- a/apps/sim/lib/auth/sso/application/admit-sso-user.ts +++ b/apps/sim/lib/auth/sso/application/admit-sso-user.ts @@ -110,7 +110,12 @@ async function runAdmissionTransaction( const [[userRow], [linkedAccount]] = await Promise.all([ tx - .select({ id: user.id, email: user.email, name: user.name }) + .select({ + id: user.id, + email: user.email, + name: user.name, + emailVerified: user.emailVerified, + }) .from(user) .where(eq(user.id, userId)) .limit(1), @@ -133,6 +138,19 @@ async function runAdmissionTransaction( return { providerId, result: { kind: 'denied', reason: 'domain-mismatch' } } } + /** + * `trustEmailVerified` stays off, so Better Auth always stores an SSO user as + * unverified. Signing in through a linked provider on a DNS-verified domain + * that matches the address is the ownership proof, and without it SSO- and + * SCIM-created users can never connect accounts. + */ + if (!userRow.emailVerified) { + await tx + .update(user) + .set({ emailVerified: true, updatedAt: new Date() }) + .where(eq(user.id, userId)) + } + const attribution = { providerId, userName: userRow.name, userEmail: userRow.email } if (!provider.organizationId) { return { From 48f6aabd1f6d205fa37e41ae490dd451728c1717 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 2 Nov 2026 15:50:28 -0800 Subject: [PATCH 2/2] fix(sso): only verify the email the domain check approved --- apps/sim/lib/auth/sso/application/admit-sso-user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/lib/auth/sso/application/admit-sso-user.ts b/apps/sim/lib/auth/sso/application/admit-sso-user.ts index 00be6c327e1..011dbb51f67 100644 --- a/apps/sim/lib/auth/sso/application/admit-sso-user.ts +++ b/apps/sim/lib/auth/sso/application/admit-sso-user.ts @@ -148,7 +148,7 @@ async function runAdmissionTransaction( await tx .update(user) .set({ emailVerified: true, updatedAt: new Date() }) - .where(eq(user.id, userId)) + .where(and(eq(user.id, userId), eq(user.email, userRow.email))) } const attribution = { providerId, userName: userRow.name, userEmail: userRow.email }