用 Auth.js / NextAuth Email 提供方对接 Rovela(2026)
自定义 Auth.js sendVerificationRequest,经 Rovela POST /emails 与 Idempotency-Key 发送魔法链接登录信——Auth.js 管令牌,Rovela 只投递;与 Next.js 魔法链接、Clerk、Supabase 指南区分。
Auth.js 管魔法链接,Rovela 只负责投递
Auth.js(前身为 NextAuth.js)可通过 Email 提供方做邮箱登录:用户输入地址后,Auth.js 生成验证令牌,借助数据库 adapter 存哈希,并发送一次性登录 URL。官方已有若干厂商的 HTTP 邮件集成;对接 Rovela 时,按 Auth.js「自建 HTTP 邮件」文档实现自定义 sendVerificationRequest 即可。
本文演示该回调如何调用 Rovela 的扁平接口 POST https://api.rovela.dev/emails,并带上 Idempotency-Key:Auth.js / NextAuth 继续拥有会话与令牌,Rovela 负责入队、DKIM 签名与投递(发送邮件 API、快速开始、鉴权)。
Rovela 不替代 Auth.js。 它不创建用户、不签发会话、不算验证令牌哈希、也不判断链接是否有效。令牌生命周期由 Auth.js(加你的 adapter)负责;sendVerificationRequest 只是把已经生成好的 url 送到收件箱。
与下列文章区分:
- Next.js 魔法链接邮件 API — 应用自行发链,不走 Auth.js Email 提供方
- Clerk + Rovela / Supabase + Rovela — 托管认证平台 + webhook / Edge Function
- 魔法链接安全实践 — 威胁模型,而非提供方接线指南
Auth.js 做什么,Rovela 发什么
| 职责 | 归属 |
|---|---|
Email 提供方配置、maxAge、adapter 表 | Auth.js + 你的数据库 |
| 生成 / 哈希验证令牌 | Auth.js |
构造用户点击的回调 url | Auth.js |
| 登录邮件的 HTML / 纯文本 | 你的 sendVerificationRequest |
POST /emails、DKIM、队列、退信 | Rovela |
| 核销链接并建立会话 | Auth.js |
Rovela 返回 200 表示已接受并入队,不等于「已进收件箱」(快速开始)。
前置条件
- 在 Rovela 控制台完成发信域名验证。未验证的
from域名会返回400(快速开始)。认证类邮件建议用mail.example.com一类子域(OTP / 魔法链接进垃圾箱)。 - 形如
re_…的 API 密钥。生产发信优先sending_access(鉴权)。 - Auth.js(或 NextAuth.js v4)并配置数据库 adapter。Email 提供方必须有 adapter(Auth.js Email、自建 HTTP 邮件)。
ROVELA_API_KEY、AUTH_SECRET/NEXTAUTH_SECRET与数据库凭证只放在服务端,不要打进客户端包。
环境变量
export ROVELA_API_KEY=re_your_sending_access_key
export ROVELA_FROM='Acme <signin@mail.example.com>'
export AUTH_SECRET="$(openssl rand -hex 32)" # Auth.js / NextAuth
# adapter 所需的数据库 URL(Postgres 等)— Email 提供方必需
轮换 Rovela 密钥:先创建新密钥并部署,再吊销旧密钥(鉴权)。
注册自定义 Email 提供方
Auth.js 接受 type: "email" 且带自定义 sendVerificationRequest 的提供方对象。官方 HTTP 邮件指南常用自定义 id(如 http-email),随后用 signIn("http-email", { email })(或你所用框架的等价 API)触发(自建 HTTP 邮件)。
精简的 Auth.js / Next.js 示意(auth.ts)——此处省略 adapter 细节,但必须配置:
import NextAuth from "next-auth";
import { sendVerificationRequest } from "./lib/rovelaSendVerificationRequest";
// import 你的数据库 adapter,并作为 `adapter` 传入
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter, // Email 提供方必需
providers: [
{
id: "http-email",
name: "Email",
type: "email",
maxAge: 60 * 60 * 24, // 链接有效期 — Auth.js 常见默认可为 24h
sendVerificationRequest,
},
],
// 按需配置 pages / callbacks
});
NextAuth.js v4 通常用 next-auth/providers/email 的 EmailProvider({ sendVerificationRequest, ... }) 包一层——回调参数(identifier、url、expires、provider、token、theme)同属这一族输入(NextAuth Email 提供方)。
sendVerificationRequest → Rovela POST /emails
Auth.js 会传入收件人(identifier)和可点击的 url。不要让 Rovela 发明令牌。自定义 HTML 时注意转义主机名;日志中不要出现原始 API 密钥。
// lib/rovelaSendVerificationRequest.ts
const ROVELA_URL = "https://api.rovela.dev/emails";
export async function sendVerificationRequest(params: {
identifier: string;
url: string;
expires: Date;
token: string;
// 另有 provider、theme、request — 见 Auth.js Email 类型
}) {
const { identifier: to, url, expires, token } = params;
const host = new URL(url).host;
const apiKey = process.env.ROVELA_API_KEY!;
const from = process.env.ROVELA_FROM!;
// 同一 Auth.js token 在传输重试时保持稳定
const idempotencyKey = `authjs-email-${token}`.slice(0, 256);
const escapedHost = host.replace(/\./g, "​.");
const html =
`<p>登录 <strong>${escapedHost}</strong></p>` +
`<p><a href="${url}">点击登录</a></p>` +
`<p>此链接将于 ${expires.toISOString()} 过期。</p>` +
`<p>若非本人操作,可忽略本邮件。</p>`;
const text =
`登录 ${host}\n${url}\n\n` +
`过期时间: ${expires.toISOString()}\n`;
const res = await fetch(ROVELA_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
Accept: "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({
from,
to,
subject: `Sign in to ${host}`,
html,
text,
}),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
// 不要记录 API 密钥 — 只记状态与提供方消息
console.error("rovela_send_failed", res.status, data.message);
throw new Error(`Rovela send failed: ${res.status}`);
}
}
与 Rovela 文档对齐的要点:
- 尽量同时发送
html与text(发送邮件 API)。 Idempotency-Key(≤ 256 字符):24 小时内重试会返回原先的接受结果,而不会再发一封——宿主在超时后重试 Auth.js 请求时很有用(发送邮件 API — 幂等)。优先用 Auth.js 的token(或其他按挑战稳定的 id)派生,不要每次请求随机 UUID。from必须属于已验证域名,否则返回400。
在 sendVerificationRequest 中抛错会让 Auth.js 感知发信失败;这是刻意的——静默成功却没有投递更糟。
触发邮箱登录
提供方 id 为 http-email 时:
import { signIn } from "@/auth"; // 或 next-auth/react 客户端辅助
await signIn("http-email", { email: "user@company.com" });
Auth.js 生成令牌、经 adapter 持久化哈希、构造 url,再调用你的发信函数。用户点击后,由 Auth.js 对照 adapter 校验令牌并建立会话——这条路径不经过 Rovela。
投递与运维清单
- 为
ROVELA_FROM所用子域配置 SPF / DKIM / DMARC(DKIM 配置、SaaS 的 DMARC)。 - 认证邮件使用独立子域,避免营销信拖累同一声誉(垃圾箱问题)。
- 生产发信优先
sending_access;只有需要读取消息事件时才用full_access(鉴权)。 - 在应用层限制同一地址申请新链接的频率——Auth.js 的
maxAge只限制链接寿命,不限制刷请求。 - 不要把验证
token查询串写进可读面很广的 Rovelatags或日志;邮件正文里已经带有一次性 URL。
排障
| 现象 | 可能原因 |
|---|---|
Rovela 发信 400 | from 域名未验证,或缺少 html / text / template |
Rovela 401 | Bearer 密钥缺失或无效(鉴权) |
| Auth.js 启动拒绝 Email 提供方 | 未配置数据库 adapter |
| 发信返回 200 但用户未收到 | 已入队但延迟/被过滤——检查域名认证与垃圾箱;必要时用 full_access 查看事件 |
| 重试出现重复邮件 | 缺少或不稳定的 Idempotency-Key |
小结
为 Auth.js / NextAuth 的 Email 提供方实现自定义 sendVerificationRequest,用 fetch 调用 Rovela POST /emails,设置稳定的 Idempotency-Key,并把密钥留在服务端。Auth.js 仍是令牌与会话的唯一真相源;Rovela 只是事务邮件投递层。
下一步:快速开始 · 发送邮件 API · Auth.js HTTP 邮件指南