Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions apps/sim/lib/auth/sso/application/admit-sso-user.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 19 additions & 1 deletion apps/sim/lib/auth/sso/application/admit-sso-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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(and(eq(user.id, userId), eq(user.email, userRow.email)))
}

const attribution = { providerId, userName: userRow.name, userEmail: userRow.email }
if (!provider.organizationId) {
return {
Expand Down
Loading