Loading...
Loading...
Compare original and translation side by side
raccoon-auditturtle-hardenheartwood-authspider-weaveturtle-hardenraccoon-auditturtle-hardenheartwood-authspider-weaveturtle-harden| Component | Detail |
|---|---|
| Frontend | |
| API | |
| Database | Cloudflare D1 (SQLite) |
| Session cache | Cloudflare KV ( |
| Providers | Google OAuth, Magic Links, Passkeys |
| Cookie domain | |
heartwood-auth| 组件 | 详情 |
|---|---|
| 前端 | |
| API | |
| 数据库 | Cloudflare D1(SQLite) |
| 会话缓存 | Cloudflare KV( |
| 登录提供商 | Google OAuth、魔法链接、密钥登录 |
| Cookie域名 | |
heartwood-auth| Variable | Purpose |
|---|---|
| Encryption secret (min 32 chars). Generate: |
| Base URL (e.g., |
| Comma-separated trusted origins |
baseURLsecret| 变量 | 用途 |
|---|---|
| 加密密钥(至少32字符)。生成方式: |
| 基础URL(例如: |
| 逗号分隔的可信源列表 |
baseURLsecretauth.ts././lib./utils./src--configauth.ts././lib./utils./src--confignpx @better-auth/cli@latest migrate # Apply schema (built-in adapter)
npx @better-auth/cli@latest generate # Generate schema for Prisma/Drizzlenpx @better-auth/cli@latest migrate # 应用数据库 schema(内置适配器)
npx @better-auth/cli@latest generate # 为Prisma/Drizzle生成schema| Option | Notes |
|---|---|
| Display name (used in 2FA issuer, emails) |
| Only if |
| Default |
| Only if |
| Required. Connection or adapter instance |
| Redis/KV for sessions & rate limits |
| |
| |
| Array of plugins |
| CSRF whitelist (baseURL auto-trusted) |
| 选项 | 说明 |
|---|---|
| 显示名称(用于双因素认证发行方、邮件内容) |
| 仅当未设置 |
| 默认值 |
| 仅当未设置 |
| 必填项。数据库连接或适配器实例 |
| 用于会话和速率限制的Redis/KV存储 |
| 设置 |
| 配置示例: |
| 插件数组 |
| CSRF白名单(baseURL会自动加入可信列表) |
pg.Poolmysql2better-sqlite3bun:sqlitebetter-auth/adapters/drizzlebetter-auth/adapters/prismabetter-auth/adapters/mongodbUserusersmodelName: "user""users"pg.Poolmysql2better-sqlite3bun:sqlitebetter-auth/adapters/drizzlebetter-auth/adapters/prismabetter-auth/adapters/mongodbUserusersmodelName: "user""users"import { betterAuth } from "better-auth";
export const auth = betterAuth({
rateLimit: {
enabled: true, // Default: true in production
window: 10, // Time window in seconds (default: 10)
max: 100, // Max requests per window (default: 100)
},
});import { betterAuth } from "better-auth";
export const auth = betterAuth({
rateLimit: {
enabled: true, // 默认:生产环境启用
window: 10, // 时间窗口(秒),默认值:10
max: 100, // 窗口内最大请求数,默认值:100
},
});rateLimit: {
storage: "secondary-storage", // Best for production
}| Storage | Behavior |
|---|---|
| Fast, resets on restart. Not recommended for serverless. |
| Persistent, adds DB load |
| Uses configured KV/Redis. Default when available. |
"secondary-storage"rateLimit: {
storage: "secondary-storage", // 生产环境最佳选择
}| 存储方式 | 特性 |
|---|---|
| 速度快,但重启后重置。不推荐用于无服务器环境。 |
| 持久化存储,但会增加数据库负载 |
| 使用已配置的KV/Redis存储。当可用时为默认选项。 |
"secondary-storage"/sign-in/sign-up/change-password/change-emailrateLimit: {
customRules: {
"/api/auth/sign-in/email": {
window: 60, // 1 minute
max: 5, // 5 attempts
},
"/api/auth/sign-up/email": {
window: 60,
max: 3, // Very strict for registration
},
"/api/auth/some-safe-endpoint": false, // Disable rate limiting
},
}/sign-in/sign-up/change-password/change-emailrateLimit: {
customRules: {
"/api/auth/sign-in/email": {
window: 60, // 1分钟
max: 5, // 最多5次尝试
},
"/api/auth/sign-up/email": {
window: 60,
max: 3, // 注册接口限制更严格
},
"/api/auth/some-safe-endpoint": false, // 禁用该端点的速率限制
},
}rateLimit: {
customStorage: {
get: async (key) => {
// Return { count: number, expiresAt: number } or null
},
set: async (key, data) => {
// Store the rate limit data
},
},
}rateLimit: {
customStorage: {
get: async (key) => {
// 返回 { count: number, expiresAt: number } 或 null
},
set: async (key, data) => {
// 存储速率限制数据
},
},
}secondaryStoragesession.storeSessionInDatabase: truecookieCachesecondaryStoragesession.storeSessionInDatabase: truecookieCachesession: {
expiresIn: 60 * 60 * 24 * 7, // 7 days (default)
updateAge: 60 * 60 * 24, // Refresh every 24 hours (default)
freshAge: 60 * 60 * 24, // 24 hours for sensitive actions (default)
}freshAgesession: {
expiresIn: 60 * 60 * 24 * 7, // 7天(默认值)
updateAge: 60 * 60 * 24, // 每24小时刷新一次(默认值)
freshAge: 60 * 60 * 24, // 敏感操作要求会话有效期不超过24小时(默认值)
}freshAgesession: {
cookieCache: {
enabled: true,
maxAge: 60 * 5, // 5 minutes
strategy: "compact", // Options: "compact", "jwt", "jwe"
version: 1, // Change to invalidate all sessions
},
}| Strategy | Description |
|---|---|
| Base64url + HMAC. Smallest size. Default. |
| Standard HS256 JWT. Readable but signed. |
| A256CBC-HS512 encrypted. Maximum security. |
session: {
cookieCache: {
enabled: true,
maxAge: 60 * 5, // 5分钟
strategy: "compact", // 可选值:"compact"、"jwt"、"jwe"
version: 1, // 修改版本号可使所有会话失效
},
}| 策略 | 描述 |
|---|---|
| Base64url + HMAC加密。体积最小,为默认选项。 |
| 标准HS256 JWT格式。内容可读但已签名。 |
| A256CBC-HS512加密。安全性最高。 |
options.secretBETTER_AUTH_SECRETAUTH_SECREToptions.secretBETTER_AUTH_SECRETAUTH_SECRETOriginRefererSec-Fetch-SiteSec-Fetch-ModeSec-Fetch-Destadvanced: {
disableCSRFCheck: false, // KEEP THIS FALSE
}OriginRefererSec-Fetch-SiteSec-Fetch-ModeSec-Fetch-Destadvanced: {
disableCSRFCheck: false, // 保持为false
}trustedOrigins: [
"https://app.grove.place",
"https://*.grove.place", // Wildcard subdomain
"exp://192.168.*.*:*/*", // Custom schemes (Expo)
]trustedOrigins: async (request) => {
const tenant = getTenantFromRequest(request);
return [`https://${tenant}.grove.place`];
}callbackURLredirectToerrorCallbackURLnewUserCallbackURLorigintrustedOrigins: [
"https://app.grove.place",
"https://*.grove.place", // 通配符子域名
"exp://192.168.*.*:*/*", // 自定义协议(如Expo)
]trustedOrigins: async (request) => {
const tenant = getTenantFromRequest(request);
return [`https://${tenant}.grove.place`];
}callbackURLredirectToerrorCallbackURLnewUserCallbackURLoriginsecure: truesameSite: "lax"httpOnly: true__Secure-advanced: {
useSecureCookies: true,
cookiePrefix: "better-auth",
defaultCookieAttributes: {
sameSite: "lax",
},
crossSubDomainCookies: {
enabled: true,
domain: ".grove.place", // Note the leading dot
additionalCookies: ["session_token", "session_data"],
},
}secure: truesameSite: "lax"httpOnly: true__Secure-advanced: {
useSecureCookies: true,
cookiePrefix: "better-auth",
defaultCookieAttributes: {
sameSite: "lax",
},
crossSubDomainCookies: {
enabled: true,
domain: ".grove.place", // 注意开头的点
additionalCookies: ["session_token", "session_data"],
},
}advanced: {
ipAddress: {
ipAddressHeaders: ["x-forwarded-for", "x-real-ip"],
ipv6Subnet: 64, // Group IPv6 by /64
disableIpTracking: false, // Keep enabled for rate limiting
},
trustedProxyHeaders: true, // Only if behind a trusted proxy
}advanced: {
ipAddress: {
ipAddressHeaders: ["x-forwarded-for", "x-real-ip"],
ipv6Subnet: 64, // 按/64子网分组IPv6地址
disableIpTracking: false, // 保持启用以支持速率限制
},
trustedProxyHeaders: true, // 仅当部署在可信代理后时启用
}handlerwaitUntilExecutionContext// In your Worker fetch handler:
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
// Create auth with ctx in scope
const auth = createAuth(env, ctx);
return auth.handler(request);
},
};
// In your auth config factory:
function createAuth(env: Env, ctx: ExecutionContext) {
return betterAuth({
// ...
advanced: {
backgroundTasks: {
handler: (promise) => ctx.waitUntil(promise),
},
},
});
}waitUntil@vercel/functionsimport { waitUntil } from "@vercel/functions";
advanced: {
backgroundTasks: {
handler: (promise) => waitUntil(promise),
},
}handlerwaitUntilExecutionContext// 在Worker的fetch处理函数中:
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
// 创建auth实例并传入ctx
const auth = createAuth(env, ctx);
return auth.handler(request);
},
};
// 在auth配置工厂函数中:
function createAuth(env: Env, ctx: ExecutionContext) {
return betterAuth({
// ...
advanced: {
backgroundTasks: {
handler: (promise) => ctx.waitUntil(promise),
},
},
});
}@vercel/functionswaitUntilimport { waitUntil } from "@vercel/functions";
advanced: {
backgroundTasks: {
handler: (promise) => waitUntil(promise),
},
}code_verifiercode_challengecode_verifiercode_challengeaccount: {
storeStateStrategy: "cookie", // "cookie" (default) or "database"
}account: {
storeStateStrategy: "cookie", // "cookie"(默认)或 "database"
}account: {
encryptOAuthTokens: true, // AES-256-GCM
}account: {
encryptOAuthTokens: true, // AES-256-GCM加密
}emailVerification: {
sendVerificationEmail: async ({ user, url }) => {
await sendEmail({ to: user.email, subject: "Verify your email", url });
},
sendOnSignUp: true,
requireEmailVerification: true, // Blocks sign-in until verified
}emailVerification: {
sendVerificationEmail: async ({ user, url }) => {
await sendEmail({ to: user.email, subject: "验证你的邮箱", url });
},
sendOnSignUp: true,
requireEmailVerification: true, // 验证完成前阻止登录
}emailAndPassword: {
sendResetPassword: async ({ user, url }) => {
await sendEmail({ to: user.email, subject: "Reset your password", url });
},
password: {
minLength: 8, // Default
maxLength: 128, // Default
},
revokeSessionsOnPasswordReset: true, // Log out all sessions
}emailAndPassword: {
sendResetPassword: async ({ user, url }) => {
await sendEmail({ to: user.email, subject: "重置你的密码", url });
},
password: {
minLength: 8, // 默认值
maxLength: 128, // 默认值
},
revokeSessionsOnPasswordReset: true, // 密码重置后登出所有会话
}hashverifyhashverifyimport { twoFactor } from "better-auth/plugins";
// Server
plugins: [
twoFactor({
issuer: "Grove", // Shown in authenticator apps
totpOptions: { digits: 6, period: 30 },
backupCodeOptions: { amount: 10, length: 10, storeBackupCodes: "encrypted" },
}),
]
// Client
plugins: [
twoFactorClient({
onTwoFactorRedirect() {
window.location.href = "/2fa";
},
}),
]twoFactorEnabledimport { twoFactor } from "better-auth/plugins";
// 服务端
plugins: [
twoFactor({
issuer: "Grove", // 在认证器应用中显示
totpOptions: { digits: 6, period: 30 },
backupCodeOptions: { amount: 10, length: 10, storeBackupCodes: "encrypted" },
}),
]
// 客户端
plugins: [
twoFactorClient({
onTwoFactorRedirect() {
window.location.href = "/2fa";
},
}),
]twoFactorEnabledtwoFactorRedirect: truetwoFactorRedirect: truetwoFactor({
trustDeviceMaxAge: 30 * 24 * 60 * 60, // 30 days
})
// During verification:
await authClient.twoFactor.verifyTotp({ code, trustDevice: true });twoFactor({
trustDeviceMaxAge: 30 * 24 * 60 * 60, // 30天
})
// 验证时:
await authClient.twoFactor.verifyTotp({ code, trustDevice: true });twoFactor({
otpOptions: {
sendOTP: async ({ user, otp }) => {
await sendEmail({ to: user.email, subject: "Your code", text: `Code: ${otp}` });
},
period: 5, // Minutes
digits: 6,
allowedAttempts: 5,
storeOTP: "encrypted",
},
})twoFactor({
otpOptions: {
sendOTP: async ({ user, otp }) => {
await sendEmail({ to: user.email, subject: "你的验证码", text: `验证码:${otp}` });
},
period: 5, // 有效期(分钟)
digits: 6,
allowedAttempts: 5,
storeOTP: "encrypted",
},
})import { organization } from "better-auth/plugins";
plugins: [
organization({
// Limit who can create orgs
allowUserToCreateOrganization: async (user) => {
return user.emailVerified;
},
}),
]import { organization } from "better-auth/plugins";
plugins: [
organization({
// 限制可创建组织的用户
allowUserToCreateOrganization: async (user) => {
return user.emailVerified;
},
}),
]setActive()setActive()import { twoFactor, organization } from "better-auth/plugins";| Plugin | Purpose | Scoped Package? |
|---|---|---|
| TOTP/OTP/backup codes | No |
| Teams & multi-tenant | No |
| WebAuthn | |
| Passwordless email | No |
| Email-based OTP | No |
| Username auth | No |
| Phone auth | No |
| User management | No |
| API key auth | No |
| Bearer token auth | No |
| JWT tokens | No |
| Multiple sessions | No |
| SAML/OIDC enterprise | |
| Be an OAuth provider | No |
| Be an OIDC provider | No |
| API documentation | No |
| Custom OAuth provider | No |
createAuthClient({ plugins: [...] })import { twoFactor, organization } from "better-auth/plugins";| 插件 | 用途 | 是否为独立包? |
|---|---|---|
| TOTP/OTP/备用码 | 否 |
| 团队与多租户管理 | 否 |
| WebAuthn密钥登录 | |
| 无密码邮箱登录 | 否 |
| 邮箱验证码登录 | 否 |
| 用户名登录 | 否 |
| 手机号登录 | 否 |
| 用户管理 | 否 |
| API密钥认证 | 否 |
| Bearer令牌认证 | 否 |
| JWT令牌 | 否 |
| 多会话管理 | 否 |
| SAML/OIDC企业单点登录 | |
| 作为OAuth提供商 | 否 |
| 作为OIDC提供商 | 否 |
| API文档生成 | 否 |
| 自定义OAuth提供商 | 否 |
createAuthClient({ plugins: [...] })| Framework | Import |
|---|---|
| React/Next.js | |
| Svelte/SvelteKit | |
| Vue/Nuxt | |
| Solid | |
| Vanilla JS | |
signUp.email()signIn.email()signIn.social()signOut()useSession()getSession()revokeSession()revokeSessions()| 框架 | 导入路径 |
|---|---|
| React/Next.js | |
| Svelte/SvelteKit | |
| Vue/Nuxt | |
| Solid | |
| 原生JS | |
signUp.email()signIn.email()signIn.social()signOut()useSession()getSession()revokeSession()revokeSessions()// Infer types from server config
type Session = typeof auth.$Infer.Session;
type User = typeof auth.$Infer.Session.user;
// For separate client/server projects
createAuthClient<typeof auth>();// 从服务端配置推断类型
type Session = typeof auth.$Infer.Session;
type User = typeof auth.$Infer.Session.user;
// 适用于客户端与服务端分离的项目
createAuthClient<typeof auth>();hooks: {
before: [
{
matcher: (ctx) => ctx.path === "/sign-in/email",
handler: createAuthMiddleware(async (ctx) => {
// Access: ctx.path, ctx.context.session, ctx.context.secret
// Return modified context or void
}),
},
],
after: [
{
matcher: (ctx) => true,
handler: createAuthMiddleware(async (ctx) => {
// Access: ctx.context.returned (response data)
}),
},
],
}hooks: {
before: [
{
matcher: (ctx) => ctx.path === "/sign-in/email",
handler: createAuthMiddleware(async (ctx) => {
// 可访问:ctx.path、ctx.context.session、ctx.context.secret
// 返回修改后的上下文或void
}),
},
],
after: [
{
matcher: (ctx) => true,
handler: createAuthMiddleware(async (ctx) => {
// 可访问:ctx.context.returned(响应数据)
}),
},
],
}databaseHooks: {
user: {
create: {
before: async ({ data }) => { /* add defaults, return false to block */ },
after: async ({ data }) => { /* audit log, send welcome email */ },
},
},
session: {
create: {
after: async ({ data, ctx }) => {
await auditLog("session.created", {
userId: data.userId,
ip: ctx?.request?.headers.get("x-forwarded-for"),
});
},
},
},
}ctx.contextsessionsecretauthCookiespassword.hash()verify()adapterinternalAdaptergenerateId()tablesbaseURLdatabaseHooks: {
user: {
create: {
before: async ({ data }) => { /* 添加默认值,返回false可阻止创建 */ },
after: async ({ data }) => { /* 审计日志、发送欢迎邮件 */ },
},
},
session: {
create: {
after: async ({ data, ctx }) => {
await auditLog("session.created", {
userId: data.userId,
ip: ctx?.request?.headers.get("x-forwarded-for"),
});
},
},
},
}ctx.contextsessionsecretauthCookiespassword.hash()verify()adapterinternalAdaptergenerateId()tablesbaseURLBETTER_AUTH_SECRETBETTER_AUTH_URLtrustedOrigins"secondary-storage""database"disableCSRFCheck: falseaccount.encryptOAuthTokens: trueExecutionContextdatabaseHookshooksjweaccount.accountLinkingBETTER_AUTH_SECRETBETTER_AUTH_URLtrustedOrigins"secondary-storage""database"disableCSRFCheck: falseaccount.encryptOAuthTokens: trueExecutionContextdatabaseHookshooksjweaccount.accountLinkingimport { betterAuth } from "better-auth";
import { twoFactor, organization } from "better-auth/plugins";
// Factory pattern — ctx comes from the Worker fetch handler
export function createAuth(env: Env, ctx: ExecutionContext) {
return betterAuth({
appName: "Grove",
secret: env.BETTER_AUTH_SECRET,
baseURL: "https://auth-api.grove.place",
trustedOrigins: [
"https://heartwood.grove.place",
"https://*.grove.place",
],
database: d1Adapter(env),
secondaryStorage: kvAdapter(env),
// Rate limiting (replaces custom threshold SDK for auth)
rateLimit: {
enabled: true,
storage: "secondary-storage",
customRules: {
"/api/auth/sign-in/email": { window: 60, max: 5 },
"/api/auth/sign-up/email": { window: 60, max: 3 },
"/api/auth/change-password": { window: 60, max: 3 },
},
},
// Sessions
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // 24 hours
freshAge: 60 * 60, // 1 hour for sensitive actions
cookieCache: {
enabled: true,
maxAge: 300,
strategy: "jwe",
},
},
// OAuth
account: {
encryptOAuthTokens: true,
storeStateStrategy: "cookie",
},
// Security
advanced: {
useSecureCookies: true,
crossSubDomainCookies: {
enabled: true,
domain: ".grove.place",
},
ipAddress: {
ipAddressHeaders: ["x-forwarded-for"],
ipv6Subnet: 64,
},
backgroundTasks: {
handler: (promise) => ctx.waitUntil(promise), // ctx captured from fetch handler
},
},
// Plugins
plugins: [
twoFactor({
issuer: "Grove",
backupCodeOptions: { storeBackupCodes: "encrypted" },
}),
organization(),
],
// Audit hooks
databaseHooks: {
session: {
create: {
after: async ({ data, ctx: hookCtx }) => {
console.log(`[audit] session created: user=${data.userId}`);
},
},
},
},
});
}import { betterAuth } from "better-auth";
import { twoFactor, organization } from "better-auth/plugins";
// 工厂模式 —— ctx来自Worker的fetch处理函数
export function createAuth(env: Env, ctx: ExecutionContext) {
return betterAuth({
appName: "Grove",
secret: env.BETTER_AUTH_SECRET,
baseURL: "https://auth-api.grove.place",
trustedOrigins: [
"https://heartwood.grove.place",
"https://*.grove.place",
],
database: d1Adapter(env),
secondaryStorage: kvAdapter(env),
// 速率限制(替代认证端点的自定义阈值SDK)
rateLimit: {
enabled: true,
storage: "secondary-storage",
customRules: {
"/api/auth/sign-in/email": { window: 60, max: 5 },
"/api/auth/sign-up/email": { window: 60, max: 3 },
"/api/auth/change-password": { window: 60, max: 3 },
},
},
// 会话配置
session: {
expiresIn: 60 * 60 * 24 * 7, // 7天
updateAge: 60 * 60 * 24, // 24小时
freshAge: 60 * 60, // 敏感操作要求会话有效期不超过1小时
cookieCache: {
enabled: true,
maxAge: 300,
strategy: "jwe",
},
},
// OAuth配置
account: {
encryptOAuthTokens: true,
storeStateStrategy: "cookie",
},
// 安全配置
advanced: {
useSecureCookies: true,
crossSubDomainCookies: {
enabled: true,
domain: ".grove.place",
},
ipAddress: {
ipAddressHeaders: ["x-forwarded-for"],
ipv6Subnet: 64,
},
backgroundTasks: {
handler: (promise) => ctx.waitUntil(promise), // ctx从fetch处理函数中捕获
},
},
// 插件
plugins: [
twoFactor({
issuer: "Grove",
backupCodeOptions: { storeBackupCodes: "encrypted" },
}),
organization(),
],
// 审计钩子
databaseHooks: {
session: {
create: {
after: async ({ data, ctx: hookCtx }) => {
console.log(`[audit] session created: user=${data.userId}`);
},
},
},
},
});
}