Authentication Email Handling#
Overview#
Email input is processed through two consistent layers across all Langfuse auth flows: Zod schema validation (format check only, no transformation) on the server boundary, and .toLowerCase() normalization applied server-side before every database read or write. No flow relies on the client to normalize case.
Zod Schemas by Flow#
| Flow | Schema | Location |
|---|---|---|
| Sign-up | signupSchema (z.string().email()) | signupSchema.ts |
| Sign-in (credentials) | credentialAuthForm (z.string().email()) | sign-in.tsx |
| Password reset | resetPasswordSchema (z.string().email()) | ResetPasswordPage.tsx |
| Enterprise SSO lookup | enterpriseSsoFormSchema (z.string().email()) | enterprise-sso-required.tsx |
All four schemas use z.string().email() with no .transform() — meaning schemas validate format but do not lowercase. Normalization is deferred to server-side code.
Server-Side Normalization Points#
Sign-up (signupApiHandler)#
signupApiHandler receives the validated form body and:
- Calls
signupSchema.safeParse(req.body)— rejects malformed input with 422 . - Lowercases the domain portion for SSO domain blocking checks:
body.email.split("@")[1]?.toLowerCase(). - Passes the raw (non-lowercased) email to
createUserEmailPassword, which lowercases it for both the duplicate check and theprisma.user.createcall .
Sign-in (Credentials Provider)#
Inside the NextAuth CredentialsProvider.authorize:
- Domain is extracted and lowercased for SSO enforcement:
credentials.email.split("@")[1]?.toLowerCase(). - The Prisma lookup always uses
email: credentials.email.toLowerCase().
signIn Callback (all providers)#
The signIn callback runs on every authentication attempt:
- Lowercases
user.emailimmediately:const email = user.email?.toLowerCase(). - Validates the lowercased result with
z.string().email().safeParse(email)— throws if invalid . - Extracts the domain from the already-lowercased email for multi-tenant SSO enforcement .
- For the
emailprovider (OTP / password reset), queries Prisma with the lowercased email to gate the flow to existing users only .
Session Callback#
The session callback fetches the user record using token.email!.toLowerCase() , ensuring case-insensitive session hydration.
SSO Domain Enforcement and Email#
Domain-blocking logic in multiple places all derive the domain the same way — email.split("@")[1]?.toLowerCase():
- Static SSO block list (
AUTH_DOMAINS_WITH_SSO_ENFORCEMENTenv var): parsed ingetSSOBlockedDomains(), applied in both sign-in and sign-up . - Multi-tenant SSO (EE):
getSsoAuthProviderIdForDomain(domain)is called with the lowercased domain in sign-in, sign-up, and the enterprise SSO page. When a domain match is found the sign-in callback redirects to/auth/enterprise-sso-requiredwith the lowercased email in the query string . - Enterprise SSO page: re-validates the email with
z.string().email(), then lowercases the domain before calling/api/auth/check-sso.
Key Files#
| File | Role |
|---|---|
web/src/features/auth/lib/signupSchema.ts | signupSchema, passwordSchema, nameSchema |
web/src/features/auth-credentials/server/signupApiHandler.ts | Sign-up API route; Zod parse → SSO block → createUserEmailPassword |
web/src/features/auth-credentials/lib/credentialsServerUtils.ts | createUserEmailPassword — canonical email-lowercase-on-write |
web/src/server/auth.ts | NextAuth config: credentials provider, signIn callback, session callback |
web/src/pages/auth/sign-in.tsx | Sign-in form; client-side Zod schema for UI validation |
web/src/features/auth-credentials/components/ResetPasswordPage.tsx | Password reset form; resetPasswordSchema |
web/src/pages/auth/enterprise-sso-required.tsx | Enterprise SSO fallback page; enterpriseSsoFormSchema |