auth

Require work emails with Auth.js (NextAuth)

Use the Auth.js / NextAuth signIn callback to reject personal and disposable emails with isBusinessEmail, for OAuth and email magic links.

isBusinessEmail for Auth.js (NextAuth)

Auth.js (formerly NextAuth.js) calls the signIn callback before a sign-in completes. Returning false denies it; returning a URL string redirects there. That’s a clean place to require a work email for every provider.

The callback

// auth.js (Auth.js v5) — the same callback works in NextAuth v4's authOptions
import NextAuth from 'next-auth';
import Google from 'next-auth/providers/google';
import { checkWorkEmail } from './lib/work-email';

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [Google],
  callbacks: {
    async signIn({ user }) {
      if (!user?.email) return false;

      const verdict = await checkWorkEmail(user.email);
      if (verdict.recommendation === 'block') {
        const reason = encodeURIComponent(verdict.category ?? 'personal');
        return `/signup?error=work-email&reason=${reason}`;
      }
      return true;
    },
  },
});
// lib/work-email.js
export async function checkWorkEmail(email) {
  try {
    const res = await fetch('https://api.isbusinessemail.com/v1/check', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.IBE_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ email }),
      signal: AbortSignal.timeout(2500),
    });
    return res.ok ? await res.json() : { recommendation: 'allow', failOpen: true };
  } catch {
    return { recommendation: 'allow', failOpen: true };
  }
}

On /signup, read the error query parameter and show: “Please sign in with your work account. Personal Google accounts can’t connect your company workspace.”

With an email provider, signIn is also called when the user requests the link (email.verificationRequest is true). Rejecting there means no link is sent to a disposable inbox:

async signIn({ user, email }) {
  // email?.verificationRequest is true while the magic link is being requested
  const verdict = await checkWorkEmail(user.email);
  return verdict.recommendation !== 'block';
}

Don’t check on every sign-in

signIn runs on every sign-in, not just the first. To save quota and latency:

  • Store the verdict on the user (with a database adapter) and skip the check when it’s already there.
  • Or cache by domain for a day; verdicts per domain change rarely.

Tips

  • “Sign in with Google” works for both Workspace and personal Gmail accounts. The check is what tells them apart. Google’s hd claim is only present for Workspace accounts, but the API also gives you disposable, relay and Microsoft 365 detection.
  • Use policy: 'strict' in the request body to block unknown domains and role accounts too.

Official docs: Auth.js (see Callbacks). See also: Categories and policies.