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:
Waleed
2026-05-28 13:32:25 -07:00
committed by GitHub
parent e78ac0f6b6
commit e62c3ad2d3
2 changed files with 72 additions and 14 deletions
+47
View File
@@ -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
View File
@@ -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') {