nextauthjs/next-auth · warning · Verification
Verification
Error message
Verification
What it means
A Verification error thrown when processing an email sign-in (magic link) callback: the verification token has no matching invite (hasInvite false), has expired, or the identifier in the callback URL does not match the token's identifier. The error carries { hasInvite, expired } so the client can distinguish the causes.
Source
Thrown at packages/core/src/lib/actions/callback/index.ts:234
}
const secret = provider.secret ?? options.secret
// @ts-expect-error -- Verified in `assertConfig`.
const invite = await adapter.useVerificationToken({
// @ts-expect-error User-land adapters might decide to omit the identifier during lookup
identifier: paramIdentifier, // TODO: Drop this requirement for lookup in official adapters too
token: await createHash(`${paramToken}${secret}`),
})
const hasInvite = !!invite
const expired = hasInvite && invite.expires.valueOf() < Date.now()
const invalidInvite =
!hasInvite ||
expired ||
// The user might have configured the link to not contain the identifier
// so we only compare if it exists
(paramIdentifier && invite.identifier !== paramIdentifier)
if (invalidInvite) throw new Verification({ hasInvite, expired })
const { identifier } = invite
const user = (await adapter!.getUserByEmail(identifier)) ?? {
id: crypto.randomUUID(),
email: identifier,
emailVerified: null,
}
const account: Account = {
providerAccountId: user.email,
userId: user.id,
type: "email" as const,
provider: provider.id,
}
const redirect = await handleAuthorized({ user, account }, options)
if (redirect) return { redirect, cookies }
View on GitHub (pinned to a1a16a5a77)
Solutions
- Request a new sign-in email and use the fresh link promptly (links are single-use).
- Increase the verification token maxAge: emailVerification: { sendVerificationRequest, maxAge: ... } or via adapter token settings.
- Verify both the app sending and consuming the link use the same AUTH_SECRET and database/adapter.
- Exclude /api/auth from link-scanning email security tools, or send links from a domain scanners ignore less aggressively.
- Show a friendly 'link expired, sign in again' page by handling the Verification error's hasInvite/expired properties.
Example fix
// before
EmailProvider({ server, from })
// after
EmailProvider({ server, from, maxAge: 60 * 60 }) // 1 hour instead of default 24h Defensive patterns
Strategy: try-catch
Validate before calling
// Before trusting a token client-side, you can only check URL presence:
const hasToken = new URLSearchParams(window.location.search).has('token')
if (!hasToken) show('This link is incomplete — request a new sign-in email') Try / catch
// Client-side: handle the error page Auth.js renders; server-side:
try {
await callback(request)
} catch (e) {
if (e instanceof Verification) {
const { hasInvite, expired } = e
// redirect to /auth/error?error=Verification with hasInvite/expired info
}
} Prevention
- Educate users that magic links are single-use and short-lived.
- Set a sensible maxAge for verification tokens.
- Use the same AUTH_SECRET and adapter across all deployed instances (no split-brain).
- Mitigate email-scanner link prefetching (e.g. per-user one-time link endpoints, or use a different sign-in method for corporate mail).
- Render a clear 'link expired, resend' page using the error's hasInvite/expired fields.
When it happens
Trigger: User clicks a magic link after the token expired (default 24h maxAge, one-time use); clicking the same link twice (token consumed on first use); an email/URL mangler (mail scanner, link preview bot) pre-consuming the link; the link was edited or the email param altered so it no longer matches the token's identifier.
Common situations: Corporate email security bots prefetching links; users forwarding magic-link emails; long delays between requesting and clicking the link; multiple Auth.js instances with different secrets/database so the token cannot be found.
Related errors
- Another account already exists with the same e-mail address
- Must pass `secret` if not set to JWT getToken()
- The account is already associated with another user
- Callback route called without provider
- Missing email from request body.
AI-assisted analysis of nextauthjs/next-auth@a1a16a5a77 (2026-08-28).
Data as JSON: /api/errors/2aeb16bfc036aa17.
Report an issue: GitHub.