mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(auth): suffix-match BLOCKED_SIGNUP_DOMAINS to catch subdomain rotation (#4773)
* improvement(auth): suffix-match BLOCKED_SIGNUP_DOMAINS to catch subdomain rotation * improvement(auth): dedupe denylist entries, extract isEmailInDenylist with tests
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isEmailInDenylist } from '@/lib/auth/auth'
|
||||
|
||||
describe('isEmailInDenylist', () => {
|
||||
it('returns false when denylist is null, empty, or email is missing', () => {
|
||||
expect(isEmailInDenylist('a@example.com', null)).toBe(false)
|
||||
expect(isEmailInDenylist('a@example.com', [])).toBe(false)
|
||||
expect(isEmailInDenylist(null, ['example.com'])).toBe(false)
|
||||
expect(isEmailInDenylist(undefined, ['example.com'])).toBe(false)
|
||||
expect(isEmailInDenylist('', ['example.com'])).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false when email has no @', () => {
|
||||
expect(isEmailInDenylist('not-an-email', ['example.com'])).toBe(false)
|
||||
})
|
||||
|
||||
it('matches exact domain', () => {
|
||||
expect(isEmailInDenylist('user@dpdns.org', ['dpdns.org'])).toBe(true)
|
||||
expect(isEmailInDenylist('user@DPDNS.ORG', ['dpdns.org'])).toBe(true)
|
||||
})
|
||||
|
||||
it('matches arbitrary-depth subdomains of a listed parent zone', () => {
|
||||
expect(isEmailInDenylist('user@xx.lucky04.dpdns.org', ['dpdns.org'])).toBe(true)
|
||||
expect(isEmailInDenylist('user@a.b.c.qzz.io', ['qzz.io'])).toBe(true)
|
||||
})
|
||||
|
||||
it('does not match look-alike domains', () => {
|
||||
expect(isEmailInDenylist('user@xdpdns.org', ['dpdns.org'])).toBe(false)
|
||||
expect(isEmailInDenylist('user@notdpdns.org', ['dpdns.org'])).toBe(false)
|
||||
})
|
||||
|
||||
it('does not match disallowed domains', () => {
|
||||
expect(isEmailInDenylist('user@gmail.com', ['dpdns.org', 'qzz.io'])).toBe(false)
|
||||
expect(isEmailInDenylist('user@example.com', ['dpdns.org'])).toBe(false)
|
||||
})
|
||||
|
||||
it('handles multiple denylist entries', () => {
|
||||
const denylist = ['dpdns.org', 'qzz.io', 'cc.cd']
|
||||
expect(isEmailInDenylist('user@foo.dpdns.org', denylist)).toBe(true)
|
||||
expect(isEmailInDenylist('user@bar.qzz.io', denylist)).toBe(true)
|
||||
expect(isEmailInDenylist('user@baz.cc.cd', denylist)).toBe(true)
|
||||
expect(isEmailInDenylist('user@example.com', denylist)).toBe(false)
|
||||
})
|
||||
})
|
||||
+25
-14
@@ -143,9 +143,29 @@ function getMicrosoftUserInfoFromIdToken(tokens: { accessToken?: string }, provi
|
||||
}
|
||||
|
||||
const blockedSignupDomains = env.BLOCKED_SIGNUP_DOMAINS
|
||||
? new Set(env.BLOCKED_SIGNUP_DOMAINS.split(',').map((d) => d.trim().toLowerCase()))
|
||||
? Array.from(
|
||||
new Set(
|
||||
env.BLOCKED_SIGNUP_DOMAINS.split(',')
|
||||
.map((d) => d.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
)
|
||||
)
|
||||
: null
|
||||
|
||||
export function isEmailInDenylist(
|
||||
email: string | undefined | null,
|
||||
denylist: readonly string[] | null
|
||||
): boolean {
|
||||
if (!denylist || denylist.length === 0 || !email) return false
|
||||
const domain = email.split('@')[1]?.toLowerCase()
|
||||
if (!domain) return false
|
||||
return denylist.some((entry) => domain === entry || domain.endsWith(`.${entry}`))
|
||||
}
|
||||
|
||||
function isSignupEmailBlocked(email: string | undefined | null): boolean {
|
||||
return isEmailInDenylist(email, blockedSignupDomains)
|
||||
}
|
||||
|
||||
const additionalTrustedOrigins = parseOriginList(env.TRUSTED_ORIGINS, (value) =>
|
||||
logger.warn('Ignoring invalid entry in TRUSTED_ORIGINS', { value })
|
||||
)
|
||||
@@ -219,11 +239,8 @@ export const auth = betterAuth({
|
||||
user: {
|
||||
create: {
|
||||
before: async (user) => {
|
||||
if (blockedSignupDomains) {
|
||||
const emailDomain = user.email?.split('@')[1]?.toLowerCase()
|
||||
if (emailDomain && blockedSignupDomains.has(emailDomain)) {
|
||||
throw new Error('Sign-ups from this email domain are not allowed.')
|
||||
}
|
||||
if (isSignupEmailBlocked(user.email)) {
|
||||
throw new Error('Sign-ups from this email domain are not allowed.')
|
||||
}
|
||||
return { data: user }
|
||||
},
|
||||
@@ -814,14 +831,8 @@ export const auth = betterAuth({
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.path.startsWith('/sign-up') && blockedSignupDomains) {
|
||||
const requestEmail = ctx.body?.email?.toLowerCase()
|
||||
if (requestEmail) {
|
||||
const emailDomain = requestEmail.split('@')[1]
|
||||
if (emailDomain && blockedSignupDomains.has(emailDomain)) {
|
||||
throw new Error('Sign-ups from this email domain are not allowed.')
|
||||
}
|
||||
}
|
||||
if (ctx.path.startsWith('/sign-up') && isSignupEmailBlocked(ctx.body?.email)) {
|
||||
throw new Error('Sign-ups from this email domain are not allowed.')
|
||||
}
|
||||
|
||||
if (ctx.path === '/oauth2/authorize' || ctx.path === '/oauth2/token') {
|
||||
|
||||
Reference in New Issue
Block a user