[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6P7F8GTH0SE6HTV8H6BTEFC":3,"skill-same:01M6P7F8GTH0SE6HTV8H6BTEFC":65},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":39,"linkOnly":40,"markdownContent":41,"markdownBody":42,"frontMatter":43,"refName":44,"commitSha":45,"skillDirectory":46,"forksCount":14,"repositoryHtmlUrl":47,"repositoryHomepageUrl":6,"ownerHtmlUrl":48,"ownerBlogUrl":6,"ownerDescription":49,"updatedAt":50,"install":51,"resources":64},"01M6P7F8GTH0SE6HTV8H6BTEFC","better-auth-security-best-practices",null,"Secure and audit cross-cutting Better Auth features, including rate limiting, CSRF protection, session security, trusted origins, secret management, OAuth security, IP tracking, and security auditing.\nUSE FOR: harden a Better Auth deployment; audit a Better Auth setup for OAuth risks and IP tracking; configure secure Better Auth secrets and trusted origins; reject insecure Better Auth security configuration.\nDO NOT USE FOR: standalone service throttling or security work outside Better Auth.","sarthchawla\u002FSetupMyAI","sarthchawla","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F41484686?v=4","01M6P7D22NFMY9MA11Q2YGPKV2","Sarthak Chawla","01M6P7D26JJ9KW98SD1G8BB83R",0,"MIT","https:\u002F\u002Fgithub.com\u002Fsarthchawla\u002FSetupMyAI\u002Fblob\u002Fmain\u002Fpackages\u002Fauth-security\u002Fskills\u002Fbetter-auth-security-best-practices\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],"auth","security","best practices","security best practices","better auth security best practices","better auth","rate","features","for","standalone","csrf protection","ip","do","work","configure","secrets","csrf","throttling","audit","secure","2026-11-10 10:12:30",false,"---\nname: better-auth-security-best-practices\ndescription: |\n  Secure and audit cross-cutting Better Auth features, including rate limiting, CSRF protection, session security, trusted origins, secret management, OAuth security, IP tracking, and security auditing.\n  USE FOR: harden a Better Auth deployment; audit a Better Auth setup for OAuth risks and IP tracking; configure secure Better Auth secrets and trusted origins; reject insecure Better Auth security configuration.\n  DO NOT USE FOR: standalone service throttling or security work outside Better Auth.\n---\n\n## Secret Management\n\nThe auth secret is the foundation of Better Auth's security. It's used for signing session tokens, encrypting sensitive data, and generating secure cookies.\n\n### Configuring the Secret\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  secret: process.env.BETTER_AUTH_SECRET, \u002F\u002F or via `BETTER_AUTH_SECRET` env\n});\n```\n\nBetter Auth looks for secrets in this order:\n1. `options.secret` in your config\n2. `BETTER_AUTH_SECRET` environment variable\n3. `AUTH_SECRET` environment variable\n\n### Secret Requirements\n\nBetter Auth validates your secret and will:\n- **Reject** default\u002Fplaceholder secrets in production\n- **Warn** if the secret is shorter than 32 characters\n- **Warn** if entropy is below 120 bits\n\nGenerate a secure secret:\n\n```bash\nopenssl rand -base64 32\n```\n\n**Important**: Never commit secrets to version control. Use environment variables or a secrets manager.\n\n## Rate Limiting\n\nRate limiting protects your authentication endpoints from brute-force attacks and abuse.\nBy default, rate limiting is enabled in production but disabled in development. To explicitly enable it, set `rateLimit.enabled` to `true` in your auth config.\nBetter Auth applies rate limiting to all endpoints by default.\n\nEach plugin can optionally have it's own configuration to adjust rate-limit rules for a given endpoint.\n\n### Default Configuration\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  rateLimit: {\n    enabled: true, \u002F\u002F Default: true in production\n    window: 10, \u002F\u002F Time window in seconds (default: 10)\n    max: 100, \u002F\u002F Max requests per window (default: 100)\n  },\n});\n```\n\n### Storage Options\n\nConfigure where rate limit counters are stored:\n\n```ts\nrateLimit: {\n  storage: \"database\", \u002F\u002F Options: \"memory\", \"database\", \"secondary-storage\"\n}\n```\n\n- **`memory`**: Fast, but resets on server restart (default when no secondary storage)\n- **`database`**: Persistent, but adds database load\n- **`secondary-storage`**: Uses configured secondary storage like Redis (default when available)\n\n**Note**: It is not recommended to use `memory` especially on serverless platforms.\n\n### Custom Storage\n\nImplement your own rate limit storage:\n\n```ts\nrateLimit: {\n  customStorage: {\n    get: async (key) => {\n      \u002F\u002F Return { count: number, expiresAt: number } or null\n    },\n    set: async (key, data) => {\n      \u002F\u002F Store the rate limit data\n    },\n  },\n}\n```\n\n### Per-Endpoint Rules\n\nBetter Auth applies stricter limits to sensitive endpoints by default:\n- `\u002Fsign-in`, `\u002Fsign-up`, `\u002Fchange-password`, `\u002Fchange-email`: 3 requests per 10 seconds\n\nOverride or customize rules for specific paths:\n\n```ts\nrateLimit: {\n  customRules: {\n    \"\u002Fapi\u002Fauth\u002Fsign-in\u002Femail\": {\n      window: 60, \u002F\u002F 1 minute window\n      max: 5, \u002F\u002F 5 attempts\n    },\n    \"\u002Fapi\u002Fauth\u002Fsome-safe-endpoint\": false, \u002F\u002F Disable rate limiting\n  },\n}\n```\n\n## CSRF Protection\n\nBetter Auth implements multiple layers of CSRF protection to prevent cross-site request forgery attacks.\n\n### How CSRF Protection Works\n\n1. **Origin Header Validation**: When cookies are present, the `Origin` or `Referer` header must match a trusted origin\n2. **Fetch Metadata**: Uses `Sec-Fetch-Site`, `Sec-Fetch-Mode`, and `Sec-Fetch-Dest` headers to detect cross-site requests\n3. **First-Login Protection**: Even without cookies, validates origin when Fetch Metadata indicates a cross-site navigation\n\n### Configuration\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  advanced: {\n    disableCSRFCheck: false, \u002F\u002F Default: false (keep enabled)\n  },\n});\n```\n\n**Warning**: Only disable CSRF protection for testing or if you have an alternative CSRF mechanism in place.\n\n### Fetch Metadata Blocking\n\nBetter Auth automatically blocks requests where:\n- `Sec-Fetch-Site: cross-site` AND\n- `Sec-Fetch-Mode: navigate` AND\n- `Sec-Fetch-Dest: document`\n\nThis prevents form-based CSRF attacks even on first login when no session cookie exists.\n\n## Trusted Origins\n\nTrusted origins control which domains can make authenticated requests to your Better Auth instance. This protects against open redirect attacks and cross-origin abuse.\n\n### Configuring Trusted Origins\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  baseURL: \"https:\u002F\u002Fapi.example.com\",\n  trustedOrigins: [\n    \"https:\u002F\u002Fapp.example.com\",\n    \"https:\u002F\u002Fadmin.example.com\",\n  ],\n});\n```\n\n**Note**: The `baseURL` origin is automatically trusted.\n\n### Environment Variable\n\nSet trusted origins via environment variable (comma-separated):\n\n```bash\nBETTER_AUTH_TRUSTED_ORIGINS=https:\u002F\u002Fapp.example.com,https:\u002F\u002Fadmin.example.com\n```\n\n### Wildcard Patterns\n\nSupport for subdomain wildcards:\n\n```ts\ntrustedOrigins: [\n  \"*.example.com\", \u002F\u002F Matches any subdomain\n  \"https:\u002F\u002F*.example.com\", \u002F\u002F Protocol-specific wildcard\n  \"exp:\u002F\u002F192.168.*.*:*\u002F*\", \u002F\u002F Custom schemes (e.g., Expo)\n]\n```\n\n### Dynamic Trusted Origins\n\nCompute trusted origins based on the request:\n\n```ts\ntrustedOrigins: async (request) => {\n  \u002F\u002F Validate against database, header, etc.\n  const tenant = getTenantFromRequest(request);\n  return [`https:\u002F\u002F${tenant}.myapp.com`];\n}\n```\n\n### What Gets Validated\n\nBetter Auth validates these URL parameters against trusted origins:\n- `callbackURL` - Where to redirect after authentication\n- `redirectTo` - General redirect parameter\n- `errorCallbackURL` - Where to redirect on errors\n- `newUserCallbackURL` - Where to redirect new users\n- `origin` - Request origin header\n- and more... \n\nInvalid URLs receive a 403 Forbidden response.\n\n## Session Security\n\nSessions control how long users stay authenticated and how session data is secured.\n\n### Session Expiration\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  session: {\n    expiresIn: 60 * 60 * 24 * 7, \u002F\u002F 7 days (default)\n    updateAge: 60 * 60 * 24, \u002F\u002F Refresh session every 24 hours (default)\n  },\n});\n```\n\n### Fresh Sessions for Sensitive Actions\n\nThe `freshAge` setting defines how recently a user must have authenticated to perform sensitive operations:\n\n```ts\nsession: {\n  freshAge: 60 * 60 * 24, \u002F\u002F 24 hours (default)\n}\n```\n\nUse this to require re-authentication for actions like changing passwords or viewing sensitive data.\n\n### Session Caching Strategies\n\nCache session data in cookies to reduce database queries:\n\n```ts\nsession: {\n  cookieCache: {\n    enabled: true,\n    maxAge: 60 * 5, \u002F\u002F 5 minutes\n    strategy: \"compact\", \u002F\u002F Options: \"compact\", \"jwt\", \"jwe\"\n  },\n}\n```\n\n- **`compact`**: Base64url + HMAC-SHA256 (smallest, signed)\n- **`jwt`**: HS256 JWT (standard, signed)\n- **`jwe`**: A256CBC-HS512 encrypted (largest, encrypted)\n\n**Note**: Use `jwe` strategy when session data contains sensitive information that shouldn't be readable client-side.\n\n\n## Cookie Security\n\nBetter Auth uses secure cookie defaults but allows customization for specific deployment scenarios.\n\n### Default Cookie Settings\n\n- **`secure`**: `true` when baseURL uses HTTPS or in production\n- **`sameSite`**: `\"lax\"` (prevents CSRF while allowing normal navigation)\n- **`httpOnly`**: `true` (prevents JavaScript access)\n- **`path`**: `\"\u002F\"` (available site-wide)\n- **Prefix**: `__Secure-` when secure is enabled\n\n### Custom Cookie Configuration\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  advanced: {\n    useSecureCookies: true, \u002F\u002F Force secure cookies\n    cookiePrefix: \"myapp\", \u002F\u002F Custom prefix (default: \"better-auth\")\n    defaultCookieAttributes: {\n      sameSite: \"strict\", \u002F\u002F Stricter CSRF protection\n      path: \"\u002Fauth\", \u002F\u002F Limit cookie scope\n    },\n  },\n});\n```\n\n### Per-Cookie Configuration\n\nCustomize specific cookies:\n\n```ts\nadvanced: {\n  cookies: {\n    session_token: {\n      name: \"auth-session\",\n      attributes: {\n        sameSite: \"strict\",\n      },\n    },\n  },\n}\n```\n\n### Cross-Subdomain Cookies\n\nShare authentication across subdomains:\n\n```ts\nadvanced: {\n  crossSubDomainCookies: {\n    enabled: true,\n    domain: \".example.com\", \u002F\u002F Note the leading dot\n    additionalCookies: [\"session_token\", \"session_data\"],\n  },\n}\n```\n\n**Security Note**: Cross-subdomain cookies expand the attack surface. Only enable if you need authentication sharing and trust all subdomains.\n\n## OAuth \u002F Social Provider Security\n\nWhen using social login providers, Better Auth implements industry-standard security measures.\n\n### PKCE (Proof Key for Code Exchange)\n\nBetter Auth automatically uses PKCE for all OAuth flows:\n\n1. Generates a 128-character random `code_verifier`\n2. Creates a `code_challenge` using S256 (SHA-256)\n3. Sends `code_challenge_method: \"S256\"` in the authorization URL\n4. Validates the code exchange with the original verifier\n\nThis prevents authorization code interception attacks.\n\n### State Parameter Security\n\nThe state parameter prevents CSRF attacks on OAuth callbacks:\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  account: {\n    storeStateStrategy: \"cookie\", \u002F\u002F Options: \"cookie\" (default), \"database\"\n  },\n});\n```\n\nState tokens:\n- Are 32-character random strings\n- Expire after 10 minutes\n- Contain callback URLs and PKCE verifier (encrypted)\n\n### Encrypting OAuth Tokens\n\nEncrypt stored access and refresh tokens in the database:\n\n```ts\naccount: {\n  encryptOAuthTokens: true, \u002F\u002F Uses AES-256-GCM\n}\n```\n\n**Recommendation**: Enable this if you store OAuth tokens for API access on behalf of users.\n\n### Skipping State Cookie Check\n\nFor mobile apps or specific OAuth flows where cookies aren't available:\n\n```ts\naccount: {\n  skipStateCookieCheck: true, \u002F\u002F Not recommended for web apps\n}\n```\n\n**Warning**: Only use this for mobile apps that cannot maintain cookies across redirects.\n\n## IP-Based Security\n\nBetter Auth tracks IP addresses for rate limiting and session security.\n\n### IP Address Configuration\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  advanced: {\n    ipAddress: {\n      ipAddressHeaders: [\"x-forwarded-for\", \"x-real-ip\"], \u002F\u002F Headers to check\n      disableIpTracking: false, \u002F\u002F Keep enabled for rate limiting\n    },\n  },\n});\n```\n\n### IPv6 Subnet Configuration\n\nFor rate limiting, IPv6 addresses can be grouped by subnet:\n\n```ts\nadvanced: {\n  ipAddress: {\n    ipv6Subnet: 64, \u002F\u002F Options: 128, 64, 48, 32 (default: 64)\n  },\n}\n```\n\nSmaller values group more addresses together, which is useful when users share IPv6 prefixes.\n\n### Trusted Proxy Headers\n\nWhen behind a reverse proxy, enable trusted headers:\n\n```ts\nadvanced: {\n  trustedProxyHeaders: true, \u002F\u002F Trust x-forwarded-host, x-forwarded-proto\n}\n```\n\n**Security Note**: Only enable this if you trust your proxy. Malicious clients could spoof these headers otherwise.\n\n## Database Hooks for Security Auditing\n\nUse database hooks to implement security auditing and monitoring.\n\n### Setting Up Audit Logging\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  databaseHooks: {\n    session: {\n      create: {\n        after: async ({ data, ctx }) => {\n          await auditLog(\"session.created\", {\n            userId: data.userId,\n            ip: ctx?.request?.headers.get(\"x-forwarded-for\"),\n            userAgent: ctx?.request?.headers.get(\"user-agent\"),\n          });\n        },\n      },\n      delete: {\n        before: async ({ data }) => {\n          await auditLog(\"session.revoked\", { sessionId: data.id });\n        },\n      },\n    },\n    user: {\n      update: {\n        after: async ({ data, oldData }) => {\n          if (oldData?.email !== data.email) {\n            await auditLog(\"user.email_changed\", {\n              userId: data.id,\n              oldEmail: oldData?.email,\n              newEmail: data.email,\n            });\n          }\n        },\n      },\n    },\n    account: {\n      create: {\n        after: async ({ data }) => {\n          await auditLog(\"account.linked\", {\n            userId: data.userId,\n            provider: data.providerId,\n          });\n        },\n      },\n    },\n  },\n});\n```\n\n### Blocking Operations\n\nReturn `false` from a `before` hook to prevent an operation:\n\n```ts\ndatabaseHooks: {\n  user: {\n    delete: {\n      before: async ({ data }) => {\n        \u002F\u002F Prevent deletion of protected users\n        if (protectedUserIds.includes(data.id)) {\n          return false;\n        }\n      },\n    },\n  },\n}\n```\n\n## Background Tasks for Timing Attack Prevention\n\nSensitive operations should complete in constant time to prevent timing attacks.\n\n### Configuring Background Tasks\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  advanced: {\n    backgroundTasks: {\n      handler: (promise) => {\n        \u002F\u002F Platform-specific handler\n        \u002F\u002F Vercel: waitUntil(promise)\n        \u002F\u002F Cloudflare: ctx.waitUntil(promise)\n        waitUntil(promise);\n      },\n    },\n  },\n});\n```\n\nThis ensures operations like sending emails don't affect response timing, which could leak information about whether a user exists.\n\n## Account Enumeration Prevention\n\nBetter Auth implements several measures to prevent attackers from discovering valid accounts.\n\n### Built-in Protections\n\n1. **Consistent Response Messages**: Password reset always returns \"If this email exists in our system, check your email for the reset link\"\n2. **Dummy Operations**: When a user isn't found, Better Auth still performs token generation and database lookups with dummy values\n3. **Background Email Sending**: Emails are sent asynchronously to prevent timing differences\n\n### Additional Recommendations\n\nFor sign-up and sign-in endpoints, consider:\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  emailAndPassword: {\n    enabled: true,\n    \u002F\u002F Non-specific error messages (implement in your error handling)\n  },\n});\n```\n\nReturn non-specific error messages like \"Invalid credentials\" rather than \"User not found\" or \"Incorrect password\".\n\n## Complete Security Configuration Example\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  secret: process.env.BETTER_AUTH_SECRET,\n  baseURL: \"https:\u002F\u002Fapi.example.com\",\n  trustedOrigins: [\n    \"https:\u002F\u002Fapp.example.com\",\n    \"https:\u002F\u002F*.preview.example.com\",\n  ],\n  \n  \u002F\u002F Rate limiting\n  rateLimit: {\n    enabled: true,\n    storage: \"secondary-storage\",\n    customRules: {\n      \"\u002Fapi\u002Fauth\u002Fsign-in\u002Femail\": { window: 60, max: 5 },\n      \"\u002Fapi\u002Fauth\u002Fsign-up\u002Femail\": { window: 60, max: 3 },\n    },\n  },\n  \n  \u002F\u002F Session security\n  session: {\n    expiresIn: 60 * 60 * 24 * 7, \u002F\u002F 7 days\n    updateAge: 60 * 60 * 24, \u002F\u002F 24 hours\n    freshAge: 60 * 60, \u002F\u002F 1 hour for sensitive actions\n    cookieCache: {\n      enabled: true,\n      maxAge: 300,\n      strategy: \"jwe\", \u002F\u002F Encrypted session data\n    },\n  },\n  \n  \u002F\u002F OAuth security\n  account: {\n    encryptOAuthTokens: true,\n    storeStateStrategy: \"cookie\",\n  },\n  \n  \n  \u002F\u002F Advanced settings\n  advanced: {\n    useSecureCookies: true,\n    cookiePrefix: \"myapp\",\n    defaultCookieAttributes: {\n      sameSite: \"lax\",\n    },\n    ipAddress: {\n      ipAddressHeaders: [\"x-forwarded-for\"],\n      ipv6Subnet: 64,\n    },\n    backgroundTasks: {\n      handler: (promise) => waitUntil(promise),\n    },\n  },\n  \n  \u002F\u002F Security auditing\n  databaseHooks: {\n    session: {\n      create: {\n        after: async ({ data, ctx }) => {\n          console.log(`New session for user ${data.userId}`);\n        },\n      },\n    },\n    user: {\n      update: {\n        after: async ({ data, oldData }) => {\n          if (oldData?.email !== data.email) {\n            console.log(`Email changed for user ${data.id}`);\n          }\n        },\n      },\n    },\n  },\n});\n```\n\n## Security Checklist\n\nBefore deploying to production:\n\n- [ ] **Secret**: Use a strong, unique secret (32+ characters, high entropy)\n- [ ] **HTTPS**: Ensure `baseURL` uses HTTPS\n- [ ] **Trusted Origins**: Configure all valid origins (frontend, mobile apps)\n- [ ] **Rate Limiting**: Keep enabled with appropriate limits\n- [ ] **CSRF Protection**: Keep enabled (`disableCSRFCheck: false`)\n- [ ] **Secure Cookies**: Enabled automatically with HTTPS\n- [ ] **OAuth Tokens**: Consider `encryptOAuthTokens: true` if storing tokens\n- [ ] **Background Tasks**: Configure for serverless platforms\n- [ ] **Audit Logging**: Implement via `databaseHooks` or `hooks`\n- [ ] **IP Tracking**: Configure headers if behind a proxy\n","## Secret Management\n\nThe auth secret is the foundation of Better Auth's security. It's used for signing session tokens, encrypting sensitive data, and generating secure cookies.\n\n### Configuring the Secret\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  secret: process.env.BETTER_AUTH_SECRET, \u002F\u002F or via `BETTER_AUTH_SECRET` env\n});\n```\n\nBetter Auth looks for secrets in this order:\n1. `options.secret` in your config\n2. `BETTER_AUTH_SECRET` environment variable\n3. `AUTH_SECRET` environment variable\n\n### Secret Requirements\n\nBetter Auth validates your secret and will:\n- **Reject** default\u002Fplaceholder secrets in production\n- **Warn** if the secret is shorter than 32 characters\n- **Warn** if entropy is below 120 bits\n\nGenerate a secure secret:\n\n```bash\nopenssl rand -base64 32\n```\n\n**Important**: Never commit secrets to version control. Use environment variables or a secrets manager.\n\n## Rate Limiting\n\nRate limiting protects your authentication endpoints from brute-force attacks and abuse.\nBy default, rate limiting is enabled in production but disabled in development. To explicitly enable it, set `rateLimit.enabled` to `true` in your auth config.\nBetter Auth applies rate limiting to all endpoints by default.\n\nEach plugin can optionally have it's own configuration to adjust rate-limit rules for a given endpoint.\n\n### Default Configuration\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  rateLimit: {\n    enabled: true, \u002F\u002F Default: true in production\n    window: 10, \u002F\u002F Time window in seconds (default: 10)\n    max: 100, \u002F\u002F Max requests per window (default: 100)\n  },\n});\n```\n\n### Storage Options\n\nConfigure where rate limit counters are stored:\n\n```ts\nrateLimit: {\n  storage: \"database\", \u002F\u002F Options: \"memory\", \"database\", \"secondary-storage\"\n}\n```\n\n- **`memory`**: Fast, but resets on server restart (default when no secondary storage)\n- **`database`**: Persistent, but adds database load\n- **`secondary-storage`**: Uses configured secondary storage like Redis (default when available)\n\n**Note**: It is not recommended to use `memory` especially on serverless platforms.\n\n### Custom Storage\n\nImplement your own rate limit storage:\n\n```ts\nrateLimit: {\n  customStorage: {\n    get: async (key) => {\n      \u002F\u002F Return { count: number, expiresAt: number } or null\n    },\n    set: async (key, data) => {\n      \u002F\u002F Store the rate limit data\n    },\n  },\n}\n```\n\n### Per-Endpoint Rules\n\nBetter Auth applies stricter limits to sensitive endpoints by default:\n- `\u002Fsign-in`, `\u002Fsign-up`, `\u002Fchange-password`, `\u002Fchange-email`: 3 requests per 10 seconds\n\nOverride or customize rules for specific paths:\n\n```ts\nrateLimit: {\n  customRules: {\n    \"\u002Fapi\u002Fauth\u002Fsign-in\u002Femail\": {\n      window: 60, \u002F\u002F 1 minute window\n      max: 5, \u002F\u002F 5 attempts\n    },\n    \"\u002Fapi\u002Fauth\u002Fsome-safe-endpoint\": false, \u002F\u002F Disable rate limiting\n  },\n}\n```\n\n## CSRF Protection\n\nBetter Auth implements multiple layers of CSRF protection to prevent cross-site request forgery attacks.\n\n### How CSRF Protection Works\n\n1. **Origin Header Validation**: When cookies are present, the `Origin` or `Referer` header must match a trusted origin\n2. **Fetch Metadata**: Uses `Sec-Fetch-Site`, `Sec-Fetch-Mode`, and `Sec-Fetch-Dest` headers to detect cross-site requests\n3. **First-Login Protection**: Even without cookies, validates origin when Fetch Metadata indicates a cross-site navigation\n\n### Configuration\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  advanced: {\n    disableCSRFCheck: false, \u002F\u002F Default: false (keep enabled)\n  },\n});\n```\n\n**Warning**: Only disable CSRF protection for testing or if you have an alternative CSRF mechanism in place.\n\n### Fetch Metadata Blocking\n\nBetter Auth automatically blocks requests where:\n- `Sec-Fetch-Site: cross-site` AND\n- `Sec-Fetch-Mode: navigate` AND\n- `Sec-Fetch-Dest: document`\n\nThis prevents form-based CSRF attacks even on first login when no session cookie exists.\n\n## Trusted Origins\n\nTrusted origins control which domains can make authenticated requests to your Better Auth instance. This protects against open redirect attacks and cross-origin abuse.\n\n### Configuring Trusted Origins\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  baseURL: \"https:\u002F\u002Fapi.example.com\",\n  trustedOrigins: [\n    \"https:\u002F\u002Fapp.example.com\",\n    \"https:\u002F\u002Fadmin.example.com\",\n  ],\n});\n```\n\n**Note**: The `baseURL` origin is automatically trusted.\n\n### Environment Variable\n\nSet trusted origins via environment variable (comma-separated):\n\n```bash\nBETTER_AUTH_TRUSTED_ORIGINS=https:\u002F\u002Fapp.example.com,https:\u002F\u002Fadmin.example.com\n```\n\n### Wildcard Patterns\n\nSupport for subdomain wildcards:\n\n```ts\ntrustedOrigins: [\n  \"*.example.com\", \u002F\u002F Matches any subdomain\n  \"https:\u002F\u002F*.example.com\", \u002F\u002F Protocol-specific wildcard\n  \"exp:\u002F\u002F192.168.*.*:*\u002F*\", \u002F\u002F Custom schemes (e.g., Expo)\n]\n```\n\n### Dynamic Trusted Origins\n\nCompute trusted origins based on the request:\n\n```ts\ntrustedOrigins: async (request) => {\n  \u002F\u002F Validate against database, header, etc.\n  const tenant = getTenantFromRequest(request);\n  return [`https:\u002F\u002F${tenant}.myapp.com`];\n}\n```\n\n### What Gets Validated\n\nBetter Auth validates these URL parameters against trusted origins:\n- `callbackURL` - Where to redirect after authentication\n- `redirectTo` - General redirect parameter\n- `errorCallbackURL` - Where to redirect on errors\n- `newUserCallbackURL` - Where to redirect new users\n- `origin` - Request origin header\n- and more... \n\nInvalid URLs receive a 403 Forbidden response.\n\n## Session Security\n\nSessions control how long users stay authenticated and how session data is secured.\n\n### Session Expiration\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  session: {\n    expiresIn: 60 * 60 * 24 * 7, \u002F\u002F 7 days (default)\n    updateAge: 60 * 60 * 24, \u002F\u002F Refresh session every 24 hours (default)\n  },\n});\n```\n\n### Fresh Sessions for Sensitive Actions\n\nThe `freshAge` setting defines how recently a user must have authenticated to perform sensitive operations:\n\n```ts\nsession: {\n  freshAge: 60 * 60 * 24, \u002F\u002F 24 hours (default)\n}\n```\n\nUse this to require re-authentication for actions like changing passwords or viewing sensitive data.\n\n### Session Caching Strategies\n\nCache session data in cookies to reduce database queries:\n\n```ts\nsession: {\n  cookieCache: {\n    enabled: true,\n    maxAge: 60 * 5, \u002F\u002F 5 minutes\n    strategy: \"compact\", \u002F\u002F Options: \"compact\", \"jwt\", \"jwe\"\n  },\n}\n```\n\n- **`compact`**: Base64url + HMAC-SHA256 (smallest, signed)\n- **`jwt`**: HS256 JWT (standard, signed)\n- **`jwe`**: A256CBC-HS512 encrypted (largest, encrypted)\n\n**Note**: Use `jwe` strategy when session data contains sensitive information that shouldn't be readable client-side.\n\n\n## Cookie Security\n\nBetter Auth uses secure cookie defaults but allows customization for specific deployment scenarios.\n\n### Default Cookie Settings\n\n- **`secure`**: `true` when baseURL uses HTTPS or in production\n- **`sameSite`**: `\"lax\"` (prevents CSRF while allowing normal navigation)\n- **`httpOnly`**: `true` (prevents JavaScript access)\n- **`path`**: `\"\u002F\"` (available site-wide)\n- **Prefix**: `__Secure-` when secure is enabled\n\n### Custom Cookie Configuration\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  advanced: {\n    useSecureCookies: true, \u002F\u002F Force secure cookies\n    cookiePrefix: \"myapp\", \u002F\u002F Custom prefix (default: \"better-auth\")\n    defaultCookieAttributes: {\n      sameSite: \"strict\", \u002F\u002F Stricter CSRF protection\n      path: \"\u002Fauth\", \u002F\u002F Limit cookie scope\n    },\n  },\n});\n```\n\n### Per-Cookie Configuration\n\nCustomize specific cookies:\n\n```ts\nadvanced: {\n  cookies: {\n    session_token: {\n      name: \"auth-session\",\n      attributes: {\n        sameSite: \"strict\",\n      },\n    },\n  },\n}\n```\n\n### Cross-Subdomain Cookies\n\nShare authentication across subdomains:\n\n```ts\nadvanced: {\n  crossSubDomainCookies: {\n    enabled: true,\n    domain: \".example.com\", \u002F\u002F Note the leading dot\n    additionalCookies: [\"session_token\", \"session_data\"],\n  },\n}\n```\n\n**Security Note**: Cross-subdomain cookies expand the attack surface. Only enable if you need authentication sharing and trust all subdomains.\n\n## OAuth \u002F Social Provider Security\n\nWhen using social login providers, Better Auth implements industry-standard security measures.\n\n### PKCE (Proof Key for Code Exchange)\n\nBetter Auth automatically uses PKCE for all OAuth flows:\n\n1. Generates a 128-character random `code_verifier`\n2. Creates a `code_challenge` using S256 (SHA-256)\n3. Sends `code_challenge_method: \"S256\"` in the authorization URL\n4. Validates the code exchange with the original verifier\n\nThis prevents authorization code interception attacks.\n\n### State Parameter Security\n\nThe state parameter prevents CSRF attacks on OAuth callbacks:\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  account: {\n    storeStateStrategy: \"cookie\", \u002F\u002F Options: \"cookie\" (default), \"database\"\n  },\n});\n```\n\nState tokens:\n- Are 32-character random strings\n- Expire after 10 minutes\n- Contain callback URLs and PKCE verifier (encrypted)\n\n### Encrypting OAuth Tokens\n\nEncrypt stored access and refresh tokens in the database:\n\n```ts\naccount: {\n  encryptOAuthTokens: true, \u002F\u002F Uses AES-256-GCM\n}\n```\n\n**Recommendation**: Enable this if you store OAuth tokens for API access on behalf of users.\n\n### Skipping State Cookie Check\n\nFor mobile apps or specific OAuth flows where cookies aren't available:\n\n```ts\naccount: {\n  skipStateCookieCheck: true, \u002F\u002F Not recommended for web apps\n}\n```\n\n**Warning**: Only use this for mobile apps that cannot maintain cookies across redirects.\n\n## IP-Based Security\n\nBetter Auth tracks IP addresses for rate limiting and session security.\n\n### IP Address Configuration\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  advanced: {\n    ipAddress: {\n      ipAddressHeaders: [\"x-forwarded-for\", \"x-real-ip\"], \u002F\u002F Headers to check\n      disableIpTracking: false, \u002F\u002F Keep enabled for rate limiting\n    },\n  },\n});\n```\n\n### IPv6 Subnet Configuration\n\nFor rate limiting, IPv6 addresses can be grouped by subnet:\n\n```ts\nadvanced: {\n  ipAddress: {\n    ipv6Subnet: 64, \u002F\u002F Options: 128, 64, 48, 32 (default: 64)\n  },\n}\n```\n\nSmaller values group more addresses together, which is useful when users share IPv6 prefixes.\n\n### Trusted Proxy Headers\n\nWhen behind a reverse proxy, enable trusted headers:\n\n```ts\nadvanced: {\n  trustedProxyHeaders: true, \u002F\u002F Trust x-forwarded-host, x-forwarded-proto\n}\n```\n\n**Security Note**: Only enable this if you trust your proxy. Malicious clients could spoof these headers otherwise.\n\n## Database Hooks for Security Auditing\n\nUse database hooks to implement security auditing and monitoring.\n\n### Setting Up Audit Logging\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  databaseHooks: {\n    session: {\n      create: {\n        after: async ({ data, ctx }) => {\n          await auditLog(\"session.created\", {\n            userId: data.userId,\n            ip: ctx?.request?.headers.get(\"x-forwarded-for\"),\n            userAgent: ctx?.request?.headers.get(\"user-agent\"),\n          });\n        },\n      },\n      delete: {\n        before: async ({ data }) => {\n          await auditLog(\"session.revoked\", { sessionId: data.id });\n        },\n      },\n    },\n    user: {\n      update: {\n        after: async ({ data, oldData }) => {\n          if (oldData?.email !== data.email) {\n            await auditLog(\"user.email_changed\", {\n              userId: data.id,\n              oldEmail: oldData?.email,\n              newEmail: data.email,\n            });\n          }\n        },\n      },\n    },\n    account: {\n      create: {\n        after: async ({ data }) => {\n          await auditLog(\"account.linked\", {\n            userId: data.userId,\n            provider: data.providerId,\n          });\n        },\n      },\n    },\n  },\n});\n```\n\n### Blocking Operations\n\nReturn `false` from a `before` hook to prevent an operation:\n\n```ts\ndatabaseHooks: {\n  user: {\n    delete: {\n      before: async ({ data }) => {\n        \u002F\u002F Prevent deletion of protected users\n        if (protectedUserIds.includes(data.id)) {\n          return false;\n        }\n      },\n    },\n  },\n}\n```\n\n## Background Tasks for Timing Attack Prevention\n\nSensitive operations should complete in constant time to prevent timing attacks.\n\n### Configuring Background Tasks\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  advanced: {\n    backgroundTasks: {\n      handler: (promise) => {\n        \u002F\u002F Platform-specific handler\n        \u002F\u002F Vercel: waitUntil(promise)\n        \u002F\u002F Cloudflare: ctx.waitUntil(promise)\n        waitUntil(promise);\n      },\n    },\n  },\n});\n```\n\nThis ensures operations like sending emails don't affect response timing, which could leak information about whether a user exists.\n\n## Account Enumeration Prevention\n\nBetter Auth implements several measures to prevent attackers from discovering valid accounts.\n\n### Built-in Protections\n\n1. **Consistent Response Messages**: Password reset always returns \"If this email exists in our system, check your email for the reset link\"\n2. **Dummy Operations**: When a user isn't found, Better Auth still performs token generation and database lookups with dummy values\n3. **Background Email Sending**: Emails are sent asynchronously to prevent timing differences\n\n### Additional Recommendations\n\nFor sign-up and sign-in endpoints, consider:\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  emailAndPassword: {\n    enabled: true,\n    \u002F\u002F Non-specific error messages (implement in your error handling)\n  },\n});\n```\n\nReturn non-specific error messages like \"Invalid credentials\" rather than \"User not found\" or \"Incorrect password\".\n\n## Complete Security Configuration Example\n\n```ts\nimport { betterAuth } from \"better-auth\";\n\nexport const auth = betterAuth({\n  secret: process.env.BETTER_AUTH_SECRET,\n  baseURL: \"https:\u002F\u002Fapi.example.com\",\n  trustedOrigins: [\n    \"https:\u002F\u002Fapp.example.com\",\n    \"https:\u002F\u002F*.preview.example.com\",\n  ],\n  \n  \u002F\u002F Rate limiting\n  rateLimit: {\n    enabled: true,\n    storage: \"secondary-storage\",\n    customRules: {\n      \"\u002Fapi\u002Fauth\u002Fsign-in\u002Femail\": { window: 60, max: 5 },\n      \"\u002Fapi\u002Fauth\u002Fsign-up\u002Femail\": { window: 60, max: 3 },\n    },\n  },\n  \n  \u002F\u002F Session security\n  session: {\n    expiresIn: 60 * 60 * 24 * 7, \u002F\u002F 7 days\n    updateAge: 60 * 60 * 24, \u002F\u002F 24 hours\n    freshAge: 60 * 60, \u002F\u002F 1 hour for sensitive actions\n    cookieCache: {\n      enabled: true,\n      maxAge: 300,\n      strategy: \"jwe\", \u002F\u002F Encrypted session data\n    },\n  },\n  \n  \u002F\u002F OAuth security\n  account: {\n    encryptOAuthTokens: true,\n    storeStateStrategy: \"cookie\",\n  },\n  \n  \n  \u002F\u002F Advanced settings\n  advanced: {\n    useSecureCookies: true,\n    cookiePrefix: \"myapp\",\n    defaultCookieAttributes: {\n      sameSite: \"lax\",\n    },\n    ipAddress: {\n      ipAddressHeaders: [\"x-forwarded-for\"],\n      ipv6Subnet: 64,\n    },\n    backgroundTasks: {\n      handler: (promise) => waitUntil(promise),\n    },\n  },\n  \n  \u002F\u002F Security auditing\n  databaseHooks: {\n    session: {\n      create: {\n        after: async ({ data, ctx }) => {\n          console.log(`New session for user ${data.userId}`);\n        },\n      },\n    },\n    user: {\n      update: {\n        after: async ({ data, oldData }) => {\n          if (oldData?.email !== data.email) {\n            console.log(`Email changed for user ${data.id}`);\n          }\n        },\n      },\n    },\n  },\n});\n```\n\n## Security Checklist\n\nBefore deploying to production:\n\n- [ ] **Secret**: Use a strong, unique secret (32+ characters, high entropy)\n- [ ] **HTTPS**: Ensure `baseURL` uses HTTPS\n- [ ] **Trusted Origins**: Configure all valid origins (frontend, mobile apps)\n- [ ] **Rate Limiting**: Keep enabled with appropriate limits\n- [ ] **CSRF Protection**: Keep enabled (`disableCSRFCheck: false`)\n- [ ] **Secure Cookies**: Enabled automatically with HTTPS\n- [ ] **OAuth Tokens**: Consider `encryptOAuthTokens: true` if storing tokens\n- [ ] **Background Tasks**: Configure for serverless platforms\n- [ ] **Audit Logging**: Implement via `databaseHooks` or `hooks`\n- [ ] **IP Tracking**: Configure headers if behind a proxy\n",{"name":5,"description":7},"main","3934845b800ff514cb89eb81d43f3dc4cbf51ff2","packages\u002Fauth-security\u002Fskills\u002Fbetter-auth-security-best-practices","https:\u002F\u002Fgithub.com\u002Fsarthchawla\u002FSetupMyAI","https:\u002F\u002Fgithub.com\u002Fsarthchawla","there ain't no problem i can't solve","2026-10-23 01:26:57",{"source":8,"skillName":5,"commands":52,"prompt":62,"zipAvailable":63},[53,56,59],{"tool":54,"command":55},"npx","npx skills add sarthchawla\u002FSetupMyAI --skill better-auth-security-best-practices",{"tool":57,"command":58},"bunx","bunx skills add sarthchawla\u002FSetupMyAI --skill better-auth-security-best-practices",{"tool":60,"command":61},"pnpm","pnpm dlx skills add sarthchawla\u002FSetupMyAI --skill better-auth-security-best-practices","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fsarthchawla\u002FSetupMyAI\u002Fblob\u002Fmain\u002Fpackages\u002Fauth-security\u002Fskills\u002Fbetter-auth-security-best-practices\u002FSKILL.md",true,[],[66,79,95,104,114,135],{"skillNo":67,"name":68,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":69,"categories":70,"keywords":76,"publishedAt":39},"01M6P8953HNC422WFQSPP2FMQE","sample-skill","https:\u002F\u002Fgithub.com\u002Fsarthchawla\u002FSetupMyAI\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fmock-repo\u002F.claude\u002Fskills\u002Fsample-skill\u002FSKILL.md",[71],{"categoryNo":72,"categoryName":73,"taxonomyType":74,"taxonomyTypeDesc":75},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[77,78],"sample","sample skill",{"skillNo":80,"name":81,"nameZh":6,"description":82,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":83,"categories":84,"keywords":85,"publishedAt":39},"01M6P8924ZSDAZ88Y5Q9P3KAQV","synthetic-review-candidate","Candidate for smoke testing an evaluation runner only.","https:\u002F\u002Fgithub.com\u002Fsarthchawla\u002FSetupMyAI\u002Fblob\u002Fmain\u002Fscripts\u002Fskill-eval\u002Ffixtures\u002Fcandidate\u002FSKILL.md",[],[86,87,88,89,90,91,92,93,94,27],"synthetic review candidate","candidate","synthetic","review","only","testing","Evaluation","smoke","runner",{"skillNo":96,"name":97,"nameZh":6,"description":98,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":99,"categories":100,"keywords":101,"publishedAt":39},"01M6P88FEGGNH8EZY0PDHD9KQA","synthetic-review-baseline","Baseline for smoke testing an evaluation runner only.","https:\u002F\u002Fgithub.com\u002Fsarthchawla\u002FSetupMyAI\u002Fblob\u002Fmain\u002Fscripts\u002Fskill-eval\u002Ffixtures\u002Fbaseline\u002FSKILL.md",[],[102,88,103,89,93,94,27,90,91,92],"synthetic review baseline","baseline",{"skillNo":105,"name":89,"nameZh":6,"description":106,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":107,"categories":108,"keywords":109,"publishedAt":39},"01M6P87ZXVEHRM860960DB5FEQ","Run the existing user-invoked review bundle and independently adjudicate its findings.","https:\u002F\u002Fgithub.com\u002Fsarthchawla\u002FSetupMyAI\u002Fblob\u002Fmain\u002Fpackages\u002Funiversal\u002Fskills\u002Freview\u002FSKILL.md",[],[89,110,111,112,113],"run","user","bundle","Existing",{"skillNo":115,"name":116,"nameZh":6,"description":117,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":118,"categories":119,"keywords":120,"publishedAt":39},"01M6P87FC73MZTRB6ZBP4GR1VQ","review-adjudication","Verify generated or human review findings before fixing, defending, replying, resolving, or reusing them as learned guidance. Use within a review, review-comment fix, monitor, or readiness workflow.","https:\u002F\u002Fgithub.com\u002Fsarthchawla\u002FSetupMyAI\u002Fblob\u002Fmain\u002Fpackages\u002Funiversal\u002Fskills\u002Freview-adjudication\u002FSKILL.md",[],[121,89,122,123,124,125,126,127,128,129,130,131,132,133,134],"review adjudication","workflow","generated","human review","guidance","human","as","readiness","learned","review findings","fix","use","verify","monitor",{"skillNo":136,"name":137,"nameZh":6,"description":138,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":139,"categories":140,"keywords":141,"publishedAt":39},"01M6P871Q44KQES293T60JQETA","monitor-mr","Monitor review findings and CI using the shared adjudication procedure.","https:\u002F\u002Fgithub.com\u002Fsarthchawla\u002FSetupMyAI\u002Fblob\u002Fmain\u002Fpackages\u002Funiversal\u002Fskills\u002Fmonitor-mr\u002FSKILL.md",[],[134,142,143,130,144,145,89],"mr","monitor mr","shared","ci"]