Authentication Security
Configure secure cookies, rate limiting, and cross-domain authentication. Learn best practices for production-ready auth configuration.
Quickback enforces secure authentication defaults out of the box. This page explains how cookie security, rate limiting, and cross-domain authentication work, plus how to configure them for your use case.
Cookie Security
Quickback sets secure cookie defaults to protect against CSRF, XSS, and man-in-the-middle attacks.
Default Attributes
auth: defineAuth("better-auth", {
advanced: {
defaultCookieAttributes: {
sameSite: 'lax', // CSRF protection
secure: true, // HTTPS only
httpOnly: true, // No JavaScript access
}
}
})What each attribute does:
| Attribute | Value | Purpose |
|---|---|---|
sameSite | lax | Blocks cookies on cross-site POST requests (CSRF protection) |
secure | true | Cookies only sent over HTTPS (prevents interception) |
httpOnly | true | JavaScript cannot access cookies via document.cookie (XSS mitigation) |
SameSite Options
lax(recommended): Blocks cross-site POST requests but allows top-level navigation. Best balance of security and usability.strict: Blocks cookies on all cross-site requests. More secure but may break legitimate flows (e.g., returning from payment provider).none: Allows all cross-site requests. Required for cross-domain authentication (requiressecure: true).
Development Override
For local development over HTTP, you can relax the secure flag:
auth: defineAuth("better-auth", {
advanced: {
defaultCookieAttributes: {
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
}
}
})Warning: Never use secure: false in production.
Rate Limiting
Quickback includes per-IP rate limiting by default to protect against brute-force attacks and abuse.
How It Works
Rate limits are applied per IP address, not globally:
┌─────────────────────────────────────────────────┐
│ IP: 192.168.1.1 │
│ Endpoint: /sign-in/email │
│ Limit: 10 requests per 60 seconds │
│ │
│ Request 1-10: ✅ Allowed │
│ Request 11: ❌ 429 Too Many Requests │
│ After 60s: Counter resets to 0 │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ IP: 192.168.1.2 (different IP) │
│ Endpoint: /sign-in/email │
│ Independent counter: Not affected by 1.1 │
│ Starts at 0 │
└─────────────────────────────────────────────────┘Default Configuration
auth: defineAuth("better-auth", {
rateLimit: {
enabled: true,
window: 60, // 60 second window
max: 100, // 100 requests per IP per window
customRules: {
"/sign-in/email": { window: 60, max: 10 },
"/sign-in/social": { window: 60, max: 10 },
"/device/init": { window: 60, max: 5 },
"/device/poll": { window: 60, max: 30 },
"/device/token": { window: 60, max: 10 },
}
}
})Custom Rules by Endpoint
Set different limits based on endpoint sensitivity:
| Endpoint Type | Recommended Limit | Reasoning |
|---|---|---|
| Sign-in | 10/min per IP | Prevent credential stuffing |
| Device codes | 5/min per IP | Prevent device code enumeration |
| Token exchange | 10/min per IP | Prevent token brute-force |
| Polling | 30/min per IP | Allow reasonable polling (every 2s) |
| General API | 100/min per IP | Balance abuse prevention and usability |
Why Per-IP?
✅ Per-IP (what Quickback uses):
- Prevents single attacker from brute-forcing
- Doesn't penalize legitimate users when one IP is malicious
- Standard for auth endpoints
❌ Global total (not used):
- Would allow distributed attacks to succeed
- One attacker with many IPs could exhaust the limit for everyone
- Not effective for security
Disabling Rate Limiting
Not recommended, but you can disable for testing:
rateLimit: {
enabled: process.env.NODE_ENV === 'production',
window: 60,
max: 1000, // Higher limit for development
}Cross-Domain Authentication
For applications spanning multiple subdomains (e.g., account.example.com, api.example.com, dashboard.example.com), Quickback provides dual-layer security.
The Problem
Browsers restrict cookies to exact domains by default. If your auth API is at api.example.com, cookies won't be sent to dashboard.example.com.
The Solution: Dual-Layer Protection
Quickback uses two complementary security layers:
1. Cookie Domain (Browser-Level)
auth: defineAuth("better-auth", {
advanced: {
crossSubDomainCookies: {
enabled: true,
domain: 'example.com' // All *.example.com can access cookies
}
}
})Behavior: Allows ALL subdomains to access the cookie (browser limitation per RFC 6265)
- Cannot selectively allow only specific subdomains
- All-or-nothing: either exact domain only, or all subdomains
2. Trusted Origins (Application-Level)
auth: defineAuth("better-auth", {
trustedOrigins: [
'https://account.example.com',
'https://api.example.com',
'https://dashboard.example.com'
]
})Behavior: Better Auth validates request origins before processing auth operations
- Granular whitelist of allowed origins
- Blocks requests from untrusted subdomains even if they have the cookie
- Prevents CSRF and unauthorized auth operations
Security Model
┌─────────────────────────────────────────────────┐
│ Subdomain: evil.example.com │
│ Has Cookie: ✓ (via crossSubDomainCookies) │
│ In trustedOrigins: ✗ │
│ Result: REQUEST BLOCKED by Better Auth │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Subdomain: account.example.com │
│ Has Cookie: ✓ (via crossSubDomainCookies) │
│ In trustedOrigins: ✓ │
│ Result: REQUEST ALLOWED │
└─────────────────────────────────────────────────┘Full Cross-Domain Configuration
import { defineAuth, defineConfig, defineDatabase, defineRuntime } from "@quickback/compiler";
export default defineConfig({
name: "my-saas",
providers: {
runtime: defineRuntime("cloudflare"),
database: defineDatabase("cloudflare-d1", {
vars: {
ACCOUNT_URL: "https://account.example.com",
BETTER_AUTH_URL: "https://api.example.com",
}
}),
auth: defineAuth("better-auth", {
emailAndPassword: { enabled: true },
plugins: ["organization"],
// Trusted origins (granular whitelist)
trustedOrigins: [
"https://account.example.com",
"https://api.example.com",
"https://dashboard.example.com",
"http://localhost:3000", // Development
],
// Cookie configuration
advanced: {
crossSubDomainCookies: {
enabled: true,
domain: "example.com",
},
defaultCookieAttributes: {
secure: true,
sameSite: "none", // Required for cross-domain
httpOnly: true,
}
}
})
}
})Important: When using sameSite: "none", you must also set secure: true. This is required by browsers.
Environment Variables
Set these in your Cloudflare Workers environment or .env file:
Required
# Better Auth secret (minimum 32 characters)
BETTER_AUTH_SECRET=your-secret-key-min-32-chars
# Base URL for Better Auth endpoints
BETTER_AUTH_URL=https://api.example.comOptional (Cross-Domain)
# Enable cross-subdomain cookies
CROSS_SUBDOMAIN_COOKIES=true
COOKIE_DOMAIN=example.com
# Trusted origins (comma-separated)
TRUSTED_ORIGINS=https://account.example.com,https://api.example.com
# CORS allowed origins (typically matches trustedOrigins)
ALLOWED_ORIGINS=https://account.example.com,https://api.example.comSetting Secrets with Wrangler
For sensitive values like BETTER_AUTH_SECRET:
wrangler secret put BETTER_AUTH_SECRET
# Paste your secret when promptedFor non-secret vars, add to wrangler.toml:
[vars]
BETTER_AUTH_URL = "https://api.example.com"
CROSS_SUBDOMAIN_COOKIES = "false"Security Best Practices
Single Domain (Recommended)
For most applications, use exact domain cookies:
auth: defineAuth("better-auth", {
trustedOrigins: ["https://app.example.com"],
advanced: {
crossSubDomainCookies: {
enabled: false // Default: exact domain only
},
defaultCookieAttributes: {
sameSite: 'lax',
secure: true,
httpOnly: true,
}
}
})Benefits:
- Maximum security (smallest cookie scope)
- No cross-subdomain attack surface
- Simpler configuration
Multi-Subdomain (When Needed)
Only enable cross-subdomain cookies if you need SSO across multiple subdomains:
auth: defineAuth("better-auth", {
trustedOrigins: [
"https://account.example.com",
"https://dashboard.example.com",
],
advanced: {
crossSubDomainCookies: {
enabled: true,
domain: "example.com",
},
defaultCookieAttributes: {
sameSite: 'none', // Required for cross-domain
secure: true,
httpOnly: true,
}
}
})Security requirements:
- Trust all subdomains equally (any compromised subdomain can steal cookies)
- Use separate root domains for untrusted services
- Always set
trustedOriginsfor granular control
CORS vs Trusted Origins
These serve different purposes and should typically match:
| Setting | Purpose |
|---|---|
ALLOWED_ORIGINS (CORS) | Controls which origins can make credentialed requests |
trustedOrigins (Better Auth) | Validates origins for auth operations + prevents open redirects |
Example:
database: defineDatabase("cloudflare-d1", {
vars: {
ALLOWED_ORIGINS: "https://account.example.com,https://api.example.com"
}
}),
auth: defineAuth("better-auth", {
trustedOrigins: [
"https://account.example.com",
"https://api.example.com"
]
})Checklist
Before deploying to production:
-
BETTER_AUTH_SECRETis set (minimum 32 random characters) -
secure: trueis enabled (HTTPS only) -
httpOnly: trueis enabled (XSS protection) -
sameSite: 'lax'or'strict'unless cross-domain required -
trustedOriginsincludes only your domains -
ALLOWED_ORIGINSmatchestrustedOrigins - Rate limiting is enabled (
rateLimit.enabled: true) - Custom rate limits set for sensitive endpoints
Deployment Considerations
Behind Proxy/Load Balancer
If your app is behind a proxy, ensure IP address headers are trusted:
auth: defineAuth("better-auth", {
autoDetectIpAddress: true, // Uses X-Forwarded-For or CF-Connecting-IP
})Cloudflare Workers: IP automatically available via CF-Connecting-IP header
Other proxies: Configure to trust X-Forwarded-For or X-Real-IP
Multiple Environments
Use environment variables to configure different settings per environment:
auth: defineAuth("better-auth", {
trustedOrigins: process.env.TRUSTED_ORIGINS?.split(',') || [],
advanced: {
crossSubDomainCookies: {
enabled: process.env.CROSS_SUBDOMAIN_COOKIES === 'true',
domain: process.env.COOKIE_DOMAIN,
},
defaultCookieAttributes: {
sameSite: process.env.COOKIE_SAMESITE || 'lax',
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
}
}
})Development (.dev.vars):
TRUSTED_ORIGINS=http://localhost:3000,http://localhost:5173
CROSS_SUBDOMAIN_COOKIES=false
COOKIE_SAMESITE=laxProduction (Wrangler secrets):
TRUSTED_ORIGINS=https://account.example.com,https://api.example.com
CROSS_SUBDOMAIN_COOKIES=true
COOKIE_DOMAIN=example.com
COOKIE_SAMESITE=none