{"record":{"id":"2aeb16bfc036aa17","repo":"nextauthjs/next-auth","slug":"verification","errorCode":null,"errorMessage":"Verification","messagePattern":"Verification","errorType":"exception","errorClass":"Verification","httpStatus":null,"severity":"warning","filePath":"packages/core/src/lib/actions/callback/index.ts","lineNumber":234,"sourceCode":"      }\n\n      const secret = provider.secret ?? options.secret\n      // @ts-expect-error -- Verified in `assertConfig`.\n      const invite = await adapter.useVerificationToken({\n        // @ts-expect-error User-land adapters might decide to omit the identifier during lookup\n        identifier: paramIdentifier, // TODO: Drop this requirement for lookup in official adapters too\n        token: await createHash(`${paramToken}${secret}`),\n      })\n\n      const hasInvite = !!invite\n      const expired = hasInvite && invite.expires.valueOf() < Date.now()\n      const invalidInvite =\n        !hasInvite ||\n        expired ||\n        // The user might have configured the link to not contain the identifier\n        // so we only compare if it exists\n        (paramIdentifier && invite.identifier !== paramIdentifier)\n      if (invalidInvite) throw new Verification({ hasInvite, expired })\n\n      const { identifier } = invite\n      const user = (await adapter!.getUserByEmail(identifier)) ?? {\n        id: crypto.randomUUID(),\n        email: identifier,\n        emailVerified: null,\n      }\n\n      const account: Account = {\n        providerAccountId: user.email,\n        userId: user.id,\n        type: \"email\" as const,\n        provider: provider.id,\n      }\n\n      const redirect = await handleAuthorized({ user, account }, options)\n      if (redirect) return { redirect, cookies }\n","sourceCodeStart":216,"sourceCodeEnd":252,"githubUrl":"https://github.com/nextauthjs/next-auth/blob/a1a16a5a7780488c7449feece410033f445d0b31/packages/core/src/lib/actions/callback/index.ts#L216-L252","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nEmailProvider({ server, from })\n// after\nEmailProvider({ server, from, maxAge: 60 * 60 }) // 1 hour instead of default 24h","handlingStrategy":"try-catch","validationCode":"// Before trusting a token client-side, you can only check URL presence:\nconst hasToken = new URLSearchParams(window.location.search).has('token')\nif (!hasToken) show('This link is incomplete — request a new sign-in email')","typeGuard":null,"tryCatchPattern":"// Client-side: handle the error page Auth.js renders; server-side:\ntry {\n  await callback(request)\n} catch (e) {\n  if (e instanceof Verification) {\n    const { hasInvite, expired } = e\n    // redirect to /auth/error?error=Verification with hasInvite/expired info\n  }\n}","preventionTips":["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."],"tags":["email","magic-link","verification","token-expired","authjs"],"backgroundTag":"verification-link-expired","analyzedSha":"a1a16a5a7780488c7449feece410033f445d0b31","analyzedAt":"2026-08-28T21:52:38.200Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}