diff --git a/packages/backend/server/src/core/auth/config.ts b/packages/backend/server/src/core/auth/config.ts index 658e8e8b8b..780c0f97be 100644 --- a/packages/backend/server/src/core/auth/config.ts +++ b/packages/backend/server/src/core/auth/config.ts @@ -40,6 +40,11 @@ export interface AuthRuntimeConfigurations { */ allowSignup: boolean; + /** + * Whether require email domain record verification before access restricted resources + */ + requireEmailDomainVerification: boolean; + /** * Whether require email verification before access restricted resources */ @@ -76,6 +81,10 @@ defineRuntimeConfig('auth', { desc: 'Whether allow new registrations', default: true, }, + requireEmailDomainVerification: { + desc: 'Whether require email domain record verification before accessing restricted resources', + default: false, + }, requireEmailVerification: { desc: 'Whether require email verification before accessing restricted resources', default: true, diff --git a/packages/backend/server/src/core/auth/controller.ts b/packages/backend/server/src/core/auth/controller.ts index d24135e08f..1357da82ac 100644 --- a/packages/backend/server/src/core/auth/controller.ts +++ b/packages/backend/server/src/core/auth/controller.ts @@ -1,3 +1,5 @@ +import { resolveMx, resolveTxt, setServers } from 'node:dns/promises'; + import { Body, Controller, @@ -55,7 +57,16 @@ export class AuthController { private readonly user: UserService, private readonly token: TokenService, private readonly config: Config - ) {} + ) { + if (config.node.dev) { + // set DNS servers in dev mode + // NOTE: some network debugging software uses DNS hijacking + // to better debug traffic, but their DNS servers may not + // handle the non dns query(like txt, mx) correctly, so we + // set a public DNS server here to avoid this issue. + setServers(['1.1.1.1', '8.8.8.8']); + } + } @Public() @Post('/preflight') @@ -147,6 +158,33 @@ export class AuthController { if (!allowSignup) { throw new SignUpForbidden(); } + + const requireEmailDomainVerification = await this.config.runtime.fetch( + 'auth/requireEmailDomainVerification' + ); + if (requireEmailDomainVerification) { + // verify domain has MX, SPF, DMARC records + const [name, domain, ...rest] = email.split('@'); + if (rest.length || !domain) { + throw new InvalidEmail(); + } + const [mx, spf, dmarc] = await Promise.allSettled([ + resolveMx(domain).then(t => t.map(mx => mx.exchange).filter(Boolean)), + resolveTxt(domain).then(t => + t.map(([k]) => k).filter(txt => txt.includes('v=spf1')) + ), + resolveTxt('_dmarc.' + domain).then(t => + t.map(([k]) => k).filter(txt => txt.includes('v=DMARC1')) + ), + ]).then(t => t.filter(t => t.status === 'fulfilled').map(t => t.value)); + if (!mx?.length || !spf?.length || !dmarc?.length) { + throw new InvalidEmail(); + } + // filter out alias emails + if (name.includes('+') || name.includes('.')) { + throw new InvalidEmail(); + } + } } const token = await this.token.createToken(TokenType.SignIn, email);