security/SKILL.md
Configure rate limiting, manage auth secrets, set up CSRF protection, define trusted origins, secure sessions and cookies, encrypt OAuth tokens, track IP addresses, and implement audit logging for Better Auth. Use when users need to secure their auth setup, prevent brute force attacks, or harden a Better Auth deployment.
npx skillsauth add better-auth/skills better-auth-security-best-practicesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
secret: process.env.BETTER_AUTH_SECRET, // or via `BETTER_AUTH_SECRET` env
});
Better Auth looks for secrets in this order:
options.secret in your configBETTER_AUTH_SECRET environment variableAUTH_SECRET environment variableopenssl rand -base64 32Enabled in production by default. Applies to all endpoints. Plugins can override per-endpoint.
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)
},
});
Options: "memory" (resets on restart, avoid on serverless), "database" (persistent), "secondary-storage" (Redis, default when available).
rateLimit: {
storage: "database",
}
Implement your own rate limit storage:
rateLimit: {
customStorage: {
get: async (key) => {
// Return { count: number, expiresAt: number } or null
},
set: async (key, data) => {
// Store the rate limit data
},
},
}
Sensitive endpoints default to 3 requests per 10 seconds (/sign-in, /sign-up, /change-password, /change-email). Override:
rateLimit: {
customRules: {
"/api/auth/sign-in/email": {
window: 60, // 1 minute window
max: 5, // 5 attempts
},
"/api/auth/some-safe-endpoint": false, // Disable rate limiting
},
}
Multi-layer protection: origin header validation, Fetch Metadata checks, and first-login protection.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
advanced: {
disableCSRFCheck: false, // Default: false (keep enabled)
},
});
Only disable for testing or with an alternative CSRF mechanism.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
baseURL: "https://api.example.com",
trustedOrigins: [
"https://app.example.com",
"https://admin.example.com",
],
});
The baseURL origin is automatically trusted. Also configurable via env: BETTER_AUTH_TRUSTED_ORIGINS=https://app.example.com,https://admin.example.com
trustedOrigins: [
"*.example.com", // Matches any subdomain
"https://*.example.com", // Protocol-specific wildcard
"exp://192.168.*.*:*/*", // Custom schemes (e.g., Expo)
]
Compute trusted origins based on the request:
trustedOrigins: async (request) => {
// Validate against database, header, etc.
const tenant = getTenantFromRequest(request);
return [`https://${tenant}.myapp.com`];
}
Validates callbackURL, redirectTo, errorCallbackURL, newUserCallbackURL, and origin against trusted origins. Invalid URLs receive 403.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days (default)
updateAge: 60 * 60 * 24, // Refresh session every 24 hours (default)
},
});
Cache session data in cookies to reduce database queries:
session: {
cookieCache: {
enabled: true,
maxAge: 60 * 5, // 5 minutes
strategy: "compact", // Options: "compact", "jwt", "jwe"
},
}
Strategies: "compact" (Base64url + HMAC, smallest), "jwt" (HS256, standard), "jwe" (encrypted, use when session has sensitive data).
Defaults: secure: true (HTTPS/production), sameSite: "lax", httpOnly: true, path: "/", prefix __Secure-.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
advanced: {
useSecureCookies: true, // Force secure cookies
cookiePrefix: "myapp", // Custom prefix (default: "better-auth")
defaultCookieAttributes: {
sameSite: "strict", // Stricter CSRF protection
path: "/auth", // Limit cookie scope
},
},
});
advanced: {
crossSubDomainCookies: {
enabled: true,
domain: ".example.com", // Note the leading dot
additionalCookies: ["session_token", "session_data"],
},
}
Only enable if you need authentication sharing and trust all subdomains.
PKCE is automatic for all OAuth flows. State tokens are 32-char random strings expiring after 10 minutes.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
account: {
storeStateStrategy: "cookie", // Options: "cookie" (default), "database"
},
});
account: {
encryptOAuthTokens: true, // Uses AES-256-GCM
}
Enable if storing OAuth tokens for API access on behalf of users. Use skipStateCookieCheck: true only for mobile apps that cannot maintain cookies.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
advanced: {
ipAddress: {
ipAddressHeaders: ["x-forwarded-for", "x-real-ip"], // Headers to check
disableIpTracking: false, // Keep enabled for rate limiting
},
},
});
Set ipv6Subnet (128, 64, 48, 32; default 64) to group IPv6 addresses. Enable trustedProxyHeaders: true only if behind a trusted reverse proxy.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
databaseHooks: {
session: {
create: {
after: async ({ data, ctx }) => {
await auditLog("session.created", {
userId: data.userId,
ip: ctx?.request?.headers.get("x-forwarded-for"),
userAgent: ctx?.request?.headers.get("user-agent"),
});
},
},
delete: {
before: async ({ data }) => {
await auditLog("session.revoked", { sessionId: data.id });
},
},
},
user: {
update: {
after: async ({ data, oldData }) => {
if (oldData?.email !== data.email) {
await auditLog("user.email_changed", {
userId: data.id,
oldEmail: oldData?.email,
newEmail: data.email,
});
}
},
},
},
account: {
create: {
after: async ({ data }) => {
await auditLog("account.linked", {
userId: data.userId,
provider: data.providerId,
});
},
},
},
},
});
Return false from a before hook to prevent an operation.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
advanced: {
backgroundTasks: {
handler: (promise) => {
// Platform-specific handler
// Vercel: waitUntil(promise)
// Cloudflare: ctx.waitUntil(promise)
waitUntil(promise);
},
},
},
});
Ensures operations like sending emails don't affect response timing.
Built-in: consistent response messages, dummy operations on invalid requests, background email sending. Return generic error messages ("Invalid credentials") rather than specific ones ("User not found").
import { betterAuth } from "better-auth";
export const auth = betterAuth({
secret: process.env.BETTER_AUTH_SECRET,
baseURL: "https://api.example.com",
trustedOrigins: [
"https://app.example.com",
"https://*.preview.example.com",
],
// Rate limiting
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 },
},
},
// Session security
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", // Encrypted session data
},
},
// OAuth security
account: {
encryptOAuthTokens: true,
storeStateStrategy: "cookie",
},
// Advanced settings
advanced: {
useSecureCookies: true,
cookiePrefix: "myapp",
defaultCookieAttributes: {
sameSite: "lax",
},
ipAddress: {
ipAddressHeaders: ["x-forwarded-for"],
ipv6Subnet: 64,
},
backgroundTasks: {
handler: (promise) => waitUntil(promise),
},
},
// Security auditing
databaseHooks: {
session: {
create: {
after: async ({ data, ctx }) => {
console.log(`New session for user ${data.userId}`);
},
},
},
user: {
update: {
after: async ({ data, oldData }) => {
if (oldData?.email !== data.email) {
console.log(`Email changed for user ${data.id}`);
}
},
},
},
},
});
Before deploying to production:
baseURL uses HTTPSdisableCSRFCheck: false)encryptOAuthTokens: true if storing tokensdatabaseHooks or hookstools
Configure TOTP authenticator apps, send OTP codes via email/SMS, manage backup codes, handle trusted devices, and implement 2FA sign-in flows using Better Auth's twoFactor plugin. Use when users need MFA, multi-factor authentication, authenticator setup, or login security with Better Auth.
tools
Configure multi-tenant organizations, manage members and invitations, define custom roles and permissions, set up teams, and implement RBAC using Better Auth's organization plugin. Use when users need org setup, team management, member roles, access control, or the Better Auth organization plugin.
development
Configure email verification, implement password reset flows, set password policies, and customise hashing algorithms for Better Auth email/password authentication. Use when users need to set up login, sign-in, sign-up, credential authentication, or password security with Better Auth.
development
Scaffold and implement authentication in TypeScript/JavaScript apps using Better Auth. Detect frameworks, configure database adapters, set up route handlers, add OAuth providers, and create auth UI pages. Use when users want to add login, sign-up, or authentication to a new or existing project with Better Auth.