nextauthjs/next-auth · error · AccessDenied

AccessDenied

Error message

AccessDenied

What it means

In the email/magic-link sign-in flow, after verifying the token Auth.js calls the signIn callback with the user, account, and verificationRequest flag. If that callback returns a falsy value, sign-in is refused and an AccessDenied error with message 'AccessDenied' is thrown.

Source

Thrown at packages/core/src/lib/actions/signin/send-token.ts:39

  const defaultUser = { id: crypto.randomUUID(), email, emailVerified: null }
  const user = (await adapter!.getUserByEmail(email)) ?? defaultUser

  const account = {
    providerAccountId: email,
    userId: user.id,
    type: "email",
    provider: provider.id,
  } satisfies Account

  let authorized
  try {
    authorized = await callbacks.signIn({
      user,
      account,
      email: { verificationRequest: true },
    })
  } catch (e) {
    throw new AccessDenied(e as Error)
  }
  if (!authorized) throw new AccessDenied("AccessDenied")
  if (typeof authorized === "string") {
    return {
      redirect: await callbacks.redirect({
        url: authorized,
        baseUrl: options.url.origin,
      }),
    }
  }

  const { callbackUrl, theme } = options
  const token =
    (await provider.generateVerificationToken?.()) ?? randomString(32)

  const ONE_DAY_IN_SECONDS = 86400
  const expires = new Date(
    Date.now() + (provider.maxAge ?? ONE_DAY_IN_SECONDS) * 1000

View on GitHub (pinned to a1a16a5a77)

Solutions

  1. Make the signIn callback return true for allowed email sign-ins — ensure every code path returns an explicit boolean
  2. If you intend to deny specific addresses, return a redirect URL string instead of false to send the user to a friendly 'access denied' page
  3. Log the inputs (user.email, verificationRequest) inside the callback to see which rule rejects the request
  4. Review domain allow-list logic for case sensitivity or typos that unintentionally fail the check

Example fix

// before
callbacks: {
  async signIn({ user, email }) {
    if (email?.verificationRequest && !user.email?.endsWith('@company.com')) return false;
    // falls through returning undefined -> AccessDenied
  },
}
// after
callbacks: {
  async signIn({ user, email }) {
    if (email?.verificationRequest && !user.email?.endsWith('@company.com')) return false;
    return true;
  },
}
Defensive patterns

Strategy: validation

Validate before calling

// Audit your signIn callback: every branch must return an explicit boolean
function auditSignInCallback(cb: (...a: any[]) => unknown) {
  // ensure no path falls through returning undefined
  return cb;
}

Try / catch

try {
  await signIn('email', { email });
} catch (e) {
  if (e instanceof AccessDenied) {
    // show 'check your email' or an access-denied page instead of a raw error
  }
}

Prevention

When it happens

Trigger: The developer-supplied signIn callback explicitly returns false or returns undefined/void (implicit undefined) during an email verification request, so authorized is falsy and the error is thrown at send-token.ts:39.

Common situations: Implementing a custom signIn callback with an early `return false` for unverified domains or blocked users; forgetting to return true at the end of the callback (all branches must return); allow-list logic accidentally rejecting the requesting email; copy-pasted callback that only handles credentials provider cases.

Related errors


AI-assisted analysis of nextauthjs/next-auth@a1a16a5a77 (2026-08-28). Data as JSON: /api/errors/229903b0e41d9859. Report an issue: GitHub.