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) * 1000View on GitHub (pinned to a1a16a5a77)
Solutions
- Make the signIn callback return true for allowed email sign-ins — ensure every code path returns an explicit boolean
- 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
- Log the inputs (user.email, verificationRequest) inside the callback to see which rule rejects the request
- 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
- Always `return true` at the end of the signIn callback — never rely on implicit undefined
- Return a redirect URL string instead of false when you want a friendly denial page
- Log decisions inside the callback to trace which rule rejects sign-ins
- Test email sign-in with addresses inside and outside your allow-list
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
- AccessDenied
- TODO: Authorization server did not provide a token endpoint.
- unsupported client authentication method
- No userinfo endpoint configured
- State data was provided but the provider is not configured t
AI-assisted analysis of nextauthjs/next-auth@a1a16a5a77 (2026-08-28).
Data as JSON: /api/errors/229903b0e41d9859.
Report an issue: GitHub.