Secret Management
The auth secret is the foundation of Better Auth's security. It's used for signing session tokens, encrypting sensitive data, and generating secure cookies.
Configuring the Secret
ts1import { betterAuth } from "better-auth"; 2 3export const auth = betterAuth({ 4 secret: process.env.BETTER_AUTH_SECRET, // or via `BETTER_AUTH_SECRET` env 5});
Better Auth looks for secrets in this order:
options.secretin your configBETTER_AUTH_SECRETenvironment variableAUTH_SECRETenvironment variable
Secret Requirements
Better Auth validates your secret and will:
- Reject default/placeholder secrets in production
- Warn if the secret is shorter than 32 characters
- Warn if entropy is below 120 bits
Generate a secure secret:
bash1openssl rand -base64 32
Important: Never commit secrets to version control. Use environment variables or a secrets manager.
Rate Limiting
Rate limiting protects your authentication endpoints from brute-force attacks and abuse.
By default, rate limiting is enabled in production but disabled in development. To explicitly enable it, set rateLimit.enabled to true in your auth config.
Better Auth applies rate limiting to all endpoints by default.
Each plugin can optionally have it's own configuration to adjust rate-limit rules for a given endpoint.
Default Configuration
ts1import { betterAuth } from "better-auth"; 2 3export const auth = betterAuth({ 4 rateLimit: { 5 enabled: true, // Default: true in production 6 window: 10, // Time window in seconds (default: 10) 7 max: 100, // Max requests per window (default: 100) 8 }, 9});
Storage Options
Configure where rate limit counters are stored:
ts1rateLimit: { 2 storage: "database", // Options: "memory", "database", "secondary-storage" 3}
memory: Fast, but resets on server restart (default when no secondary storage)database: Persistent, but adds database loadsecondary-storage: Uses configured secondary storage like Redis (default when available)
Note: It is not recommended to use memory especially on serverless platforms.
Custom Storage
Implement your own rate limit storage:
ts1rateLimit: { 2 customStorage: { 3 get: async (key) => { 4 // Return { count: number, expiresAt: number } or null 5 }, 6 set: async (key, data) => { 7 // Store the rate limit data 8 }, 9 }, 10}
Per-Endpoint Rules
Better Auth applies stricter limits to sensitive endpoints by default:
/sign-in,/sign-up,/change-password,/change-email: 3 requests per 10 seconds
Override or customize rules for specific paths:
ts1rateLimit: { 2 customRules: { 3 "/api/auth/sign-in/email": { 4 window: 60, // 1 minute window 5 max: 5, // 5 attempts 6 }, 7 "/api/auth/some-safe-endpoint": false, // Disable rate limiting 8 }, 9}
CSRF Protection
Better Auth implements multiple layers of CSRF protection to prevent cross-site request forgery attacks.
How CSRF Protection Works
- Origin Header Validation: When cookies are present, the
OriginorRefererheader must match a trusted origin - Fetch Metadata: Uses
Sec-Fetch-Site,Sec-Fetch-Mode, andSec-Fetch-Destheaders to detect cross-site requests - First-Login Protection: Even without cookies, validates origin when Fetch Metadata indicates a cross-site navigation
Configuration
ts1import { betterAuth } from "better-auth"; 2 3export const auth = betterAuth({ 4 advanced: { 5 disableCSRFCheck: false, // Default: false (keep enabled) 6 }, 7});
Warning: Only disable CSRF protection for testing or if you have an alternative CSRF mechanism in place.
Fetch Metadata Blocking
Better Auth automatically blocks requests where:
Sec-Fetch-Site: cross-siteANDSec-Fetch-Mode: navigateANDSec-Fetch-Dest: document
This prevents form-based CSRF attacks even on first login when no session cookie exists.
Trusted Origins
Trusted origins control which domains can make authenticated requests to your Better Auth instance. This protects against open redirect attacks and cross-origin abuse.
Configuring Trusted Origins
ts1import { betterAuth } from "better-auth"; 2 3export const auth = betterAuth({ 4 baseURL: "https://api.example.com", 5 trustedOrigins: [ 6 "https://app.example.com", 7 "https://admin.example.com", 8 ], 9});
Note: The baseURL origin is automatically trusted.
Environment Variable
Set trusted origins via environment variable (comma-separated):
bash1BETTER_AUTH_TRUSTED_ORIGINS=https://app.example.com,https://admin.example.com
Wildcard Patterns
Support for subdomain wildcards:
ts1trustedOrigins: [ 2 "*.example.com", // Matches any subdomain 3 "https://*.example.com", // Protocol-specific wildcard 4 "exp://192.168.*.*:*/*", // Custom schemes (e.g., Expo) 5]
Dynamic Trusted Origins
Compute trusted origins based on the request:
ts1trustedOrigins: async (request) => { 2 // Validate against database, header, etc. 3 const tenant = getTenantFromRequest(request); 4 return [`https://${tenant}.myapp.com`]; 5}
What Gets Validated
Better Auth validates these URL parameters against trusted origins:
callbackURL- Where to redirect after authenticationredirectTo- General redirect parametererrorCallbackURL- Where to redirect on errorsnewUserCallbackURL- Where to redirect new usersorigin- Request origin header- and more...
Invalid URLs receive a 403 Forbidden response.
Session Security
Sessions control how long users stay authenticated and how session data is secured.
Session Expiration
ts1import { betterAuth } from "better-auth"; 2 3export const auth = betterAuth({ 4 session: { 5 expiresIn: 60 * 60 * 24 * 7, // 7 days (default) 6 updateAge: 60 * 60 * 24, // Refresh session every 24 hours (default) 7 }, 8});
Fresh Sessions for Sensitive Actions
The freshAge setting defines how recently a user must have authenticated to perform sensitive operations:
ts1session: { 2 freshAge: 60 * 60 * 24, // 24 hours (default) 3}
Use this to require re-authentication for actions like changing passwords or viewing sensitive data.
Session Caching Strategies
Cache session data in cookies to reduce database queries:
ts1session: { 2 cookieCache: { 3 enabled: true, 4 maxAge: 60 * 5, // 5 minutes 5 strategy: "compact", // Options: "compact", "jwt", "jwe" 6 }, 7}
compact: Base64url + HMAC-SHA256 (smallest, signed)jwt: HS256 JWT (standard, signed)jwe: A256CBC-HS512 encrypted (largest, encrypted)
Note: Use jwe strategy when session data contains sensitive information that shouldn't be readable client-side.
Cookie Security
Better Auth uses secure cookie defaults but allows customization for specific deployment scenarios.
Default Cookie Settings
secure:truewhen baseURL uses HTTPS or in productionsameSite:"lax"(prevents CSRF while allowing normal navigation)httpOnly:true(prevents JavaScript access)path:"/"(available site-wide)- Prefix:
__Secure-when secure is enabled
Custom Cookie Configuration
ts1import { betterAuth } from "better-auth"; 2 3export const auth = betterAuth({ 4 advanced: { 5 useSecureCookies: true, // Force secure cookies 6 cookiePrefix: "myapp", // Custom prefix (default: "better-auth") 7 defaultCookieAttributes: { 8 sameSite: "strict", // Stricter CSRF protection 9 path: "/auth", // Limit cookie scope 10 }, 11 }, 12});
Per-Cookie Configuration
Customize specific cookies:
ts1advanced: { 2 cookies: { 3 session_token: { 4 name: "auth-session", 5 attributes: { 6 sameSite: "strict", 7 }, 8 }, 9 }, 10}
Cross-Subdomain Cookies
Share authentication across subdomains:
ts1advanced: { 2 crossSubDomainCookies: { 3 enabled: true, 4 domain: ".example.com", // Note the leading dot 5 additionalCookies: ["session_token", "session_data"], 6 }, 7}
Security Note: Cross-subdomain cookies expand the attack surface. Only enable if you need authentication sharing and trust all subdomains.
OAuth / Social Provider Security
When using social login providers, Better Auth implements industry-standard security measures.
PKCE (Proof Key for Code Exchange)
Better Auth automatically uses PKCE for all OAuth flows:
- Generates a 128-character random
code_verifier - Creates a
code_challengeusing S256 (SHA-256) - Sends
code_challenge_method: "S256"in the authorization URL - Validates the code exchange with the original verifier
This prevents authorization code interception attacks.
State Parameter Security
The state parameter prevents CSRF attacks on OAuth callbacks:
ts1import { betterAuth } from "better-auth"; 2 3export const auth = betterAuth({ 4 account: { 5 storeStateStrategy: "cookie", // Options: "cookie" (default), "database" 6 }, 7});
State tokens:
- Are 32-character random strings
- Expire after 10 minutes
- Contain callback URLs and PKCE verifier (encrypted)
Encrypting OAuth Tokens
Encrypt stored access and refresh tokens in the database:
ts1account: { 2 encryptOAuthTokens: true, // Uses AES-256-GCM 3}
Recommendation: Enable this if you store OAuth tokens for API access on behalf of users.
Skipping State Cookie Check
For mobile apps or specific OAuth flows where cookies aren't available:
ts1account: { 2 skipStateCookieCheck: true, // Not recommended for web apps 3}
Warning: Only use this for mobile apps that cannot maintain cookies across redirects.
IP-Based Security
Better Auth tracks IP addresses for rate limiting and session security.
IP Address Configuration
ts1import { betterAuth } from "better-auth"; 2 3export const auth = betterAuth({ 4 advanced: { 5 ipAddress: { 6 ipAddressHeaders: ["x-forwarded-for", "x-real-ip"], // Headers to check 7 disableIpTracking: false, // Keep enabled for rate limiting 8 }, 9 }, 10});
IPv6 Subnet Configuration
For rate limiting, IPv6 addresses can be grouped by subnet:
ts1advanced: { 2 ipAddress: { 3 ipv6Subnet: 64, // Options: 128, 64, 48, 32 (default: 64) 4 }, 5}
Smaller values group more addresses together, which is useful when users share IPv6 prefixes.
Trusted Proxy Headers
When behind a reverse proxy, enable trusted headers:
ts1advanced: { 2 trustedProxyHeaders: true, // Trust x-forwarded-host, x-forwarded-proto 3}
Security Note: Only enable this if you trust your proxy. Malicious clients could spoof these headers otherwise.
Database Hooks for Security Auditing
Use database hooks to implement security auditing and monitoring.
Setting Up Audit Logging
ts1import { betterAuth } from "better-auth"; 2 3export const auth = betterAuth({ 4 databaseHooks: { 5 session: { 6 create: { 7 after: async ({ data, ctx }) => { 8 await auditLog("session.created", { 9 userId: data.userId, 10 ip: ctx?.request?.headers.get("x-forwarded-for"), 11 userAgent: ctx?.request?.headers.get("user-agent"), 12 }); 13 }, 14 }, 15 delete: { 16 before: async ({ data }) => { 17 await auditLog("session.revoked", { sessionId: data.id }); 18 }, 19 }, 20 }, 21 user: { 22 update: { 23 after: async ({ data, oldData }) => { 24 if (oldData?.email !== data.email) { 25 await auditLog("user.email_changed", { 26 userId: data.id, 27 oldEmail: oldData?.email, 28 newEmail: data.email, 29 }); 30 } 31 }, 32 }, 33 }, 34 account: { 35 create: { 36 after: async ({ data }) => { 37 await auditLog("account.linked", { 38 userId: data.userId, 39 provider: data.providerId, 40 }); 41 }, 42 }, 43 }, 44 }, 45});
Blocking Operations
Return false from a before hook to prevent an operation:
ts1databaseHooks: { 2 user: { 3 delete: { 4 before: async ({ data }) => { 5 // Prevent deletion of protected users 6 if (protectedUserIds.includes(data.id)) { 7 return false; 8 } 9 }, 10 }, 11 }, 12}
Background Tasks for Timing Attack Prevention
Sensitive operations should complete in constant time to prevent timing attacks.
Configuring Background Tasks
ts1import { betterAuth } from "better-auth"; 2 3export const auth = betterAuth({ 4 advanced: { 5 backgroundTasks: { 6 handler: (promise) => { 7 // Platform-specific handler 8 // Vercel: waitUntil(promise) 9 // Cloudflare: ctx.waitUntil(promise) 10 waitUntil(promise); 11 }, 12 }, 13 }, 14});
This ensures operations like sending emails don't affect response timing, which could leak information about whether a user exists.
Account Enumeration Prevention
Better Auth implements several measures to prevent attackers from discovering valid accounts.
Built-in Protections
- Consistent Response Messages: Password reset always returns "If this email exists in our system, check your email for the reset link"
- Dummy Operations: When a user isn't found, Better Auth still performs token generation and database lookups with dummy values
- Background Email Sending: Emails are sent asynchronously to prevent timing differences
Additional Recommendations
For sign-up and sign-in endpoints, consider:
ts1import { betterAuth } from "better-auth"; 2 3export const auth = betterAuth({ 4 emailAndPassword: { 5 enabled: true, 6 // Generic error messages (implement in your error handling) 7 }, 8});
Return generic error messages like "Invalid credentials" rather than "User not found" or "Incorrect password".
Complete Security Configuration Example
ts1import { betterAuth } from "better-auth"; 2 3export const auth = betterAuth({ 4 secret: process.env.BETTER_AUTH_SECRET, 5 baseURL: "https://api.example.com", 6 trustedOrigins: [ 7 "https://app.example.com", 8 "https://*.preview.example.com", 9 ], 10 11 // Rate limiting 12 rateLimit: { 13 enabled: true, 14 storage: "secondary-storage", 15 customRules: { 16 "/api/auth/sign-in/email": { window: 60, max: 5 }, 17 "/api/auth/sign-up/email": { window: 60, max: 3 }, 18 }, 19 }, 20 21 // Session security 22 session: { 23 expiresIn: 60 * 60 * 24 * 7, // 7 days 24 updateAge: 60 * 60 * 24, // 24 hours 25 freshAge: 60 * 60, // 1 hour for sensitive actions 26 cookieCache: { 27 enabled: true, 28 maxAge: 300, 29 strategy: "jwe", // Encrypted session data 30 }, 31 }, 32 33 // OAuth security 34 account: { 35 encryptOAuthTokens: true, 36 storeStateStrategy: "cookie", 37 }, 38 39 40 // Advanced settings 41 advanced: { 42 useSecureCookies: true, 43 cookiePrefix: "myapp", 44 defaultCookieAttributes: { 45 sameSite: "lax", 46 }, 47 ipAddress: { 48 ipAddressHeaders: ["x-forwarded-for"], 49 ipv6Subnet: 64, 50 }, 51 backgroundTasks: { 52 handler: (promise) => waitUntil(promise), 53 }, 54 }, 55 56 // Security auditing 57 databaseHooks: { 58 session: { 59 create: { 60 after: async ({ data, ctx }) => { 61 console.log(`New session for user ${data.userId}`); 62 }, 63 }, 64 }, 65 user: { 66 update: { 67 after: async ({ data, oldData }) => { 68 if (oldData?.email !== data.email) { 69 console.log(`Email changed for user ${data.id}`); 70 } 71 }, 72 }, 73 }, 74 }, 75});
Security Checklist
Before deploying to production:
- Secret: Use a strong, unique secret (32+ characters, high entropy)
- HTTPS: Ensure
baseURLuses HTTPS - Trusted Origins: Configure all valid origins (frontend, mobile apps)
- Rate Limiting: Keep enabled with appropriate limits
- CSRF Protection: Keep enabled (
disableCSRFCheck: false) - Secure Cookies: Enabled automatically with HTTPS
- OAuth Tokens: Consider
encryptOAuthTokens: trueif storing tokens - Background Tasks: Configure for serverless platforms
- Audit Logging: Implement via
databaseHooksorhooks - IP Tracking: Configure headers if behind a proxy