# Authentication Security

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

```ts
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 (requires `secure: true`).

### Development Override

For local development over HTTP, you can relax the `secure` flag:

```ts
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

```ts
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:

```ts
rateLimit: {
  enabled: process.env.NODE_ENV === 'production',
  window: 60,
  max: 1000,  // Higher limit for development
}
```

---

## Bearer Tokens and Cookie Precedence

Quickback supports three bearer-token flavors alongside the standard browser session cookie:

| Credential | Used by | Resolved by |
|------------|---------|-------------|
| Session cookie | Browser SPAs (CMS, Account, your app) | Better Auth session lookup |
| Session-token bearer | API clients that grabbed `set-auth-token` | Better Auth `bearer` plugin |
| JWT bearer | API clients that hit `/api/v1/token` | In-worker JWT fast-path (no DB hit) |
| API-key bearer | Long-lived programmatic access | `apiKey` plugin / `x-api-key` / `?api_key=` |

These match Better Auth's own framing of bearer tokens as **an alternative to cookies, not a supplement** — a request uses one or the other.

### Precedence: bearer always wins

If a request carries both `Authorization` and `Cookie` headers, the bearer wins universally and the cookie is dropped before session resolution:

```
Request                                  Resolved as
──────────────────────────────────────   ─────────────
Cookie only                              cookie session
Authorization: Bearer <jwt>              JWT claims
Authorization: Bearer <api-key>          API-key owner
Authorization: Bearer <session-token>    bearer session
Cookie + Authorization: Bearer <token>   bearer wins, cookie ignored
```

This closes a class of silent-impersonation bugs where an ambient session cookie (multi-account browser tabs, a malicious extension, a misconfigured proxy injecting `Set-Cookie`) would otherwise override an explicit bearer credential. The decision is made before `getSession()` runs, so it applies to every code path in the auth middleware.

The api-key and JWT bearers also short-circuit before cookie resolution via dedicated fast-paths — they never touch the database for session lookup. Only session-token bearers reach `getSession()`, where Better Auth's `bearer` plugin resolves the `Authorization` header.

### `requireSignature: true` is the secure default

Quickback configures the Better Auth `bearer` plugin with `requireSignature: true` by default. This means:

- The session token Better Auth emits via the `set-auth-token` response header is **signed server-side** with `BETTER_AUTH_SECRET`.
- Unsigned bearer tokens are **rejected** at the plugin layer before any session lookup happens.
- A session token leaked via a proxy access log, a browser dev-tools panel, or a misrouted `Referer` header **cannot** be replayed as a bearer token by an attacker — they would need the signing secret to forge a valid signature.

Better Auth's own default for this option is `false`. Quickback overrides it on the secure side. JWT bearers and agent-auth bearers are independently signed and unaffected by this setting.

### Overriding the default

If a project genuinely needs unsigned bearers (e.g., a closed loopback service where signing is redundant), pass an explicit object:

```ts
auth: defineAuth("better-auth", {
  plugins: {
    bearer: { requireSignature: false },
  },
})
```

User-supplied options merge on top of the secure default, so any other [`bearer` plugin option](https://better-auth.com/docs/plugins/bearer) can be passed the same way.

### Threat model and trade-offs

| Threat | Mitigation |
|--------|-----------|
| Stolen session-token bearer replayed against the API | `requireSignature: true` blocks unsigned values |
| Browser extension injects a victim's session cookie into an attacker's bearer-authed API call | Cookie stripped when `Authorization` is present |
| Multi-account user has cookies for two identities and uses bearer for the second | Bearer identity wins; no ambiguity in audit log |
| MCP / agent flow exposed to ambient browser cookies | `/mcp` and agent-auth routes are bearer-only (cookies dropped entirely, see [Agent Auth](/stack/auth/plugins/agent-auth)) |
| Alg-confusion / forged JWT (alg=none, HS384, RS256) | Verifier pins HS256; signature math rejects all non-matching algs uniformly with 401 |

For a deeper walkthrough of the bearer flow itself, see the upstream [Better Auth bearer plugin docs](https://better-auth.com/docs/plugins/bearer).

---

## 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)

```ts
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)

```ts
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

```ts
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

```bash
# 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.com
```

### Optional (Cross-Domain)

```bash
# 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.com
```

### Setting Secrets with Wrangler

For sensitive values like `BETTER_AUTH_SECRET`:

```bash
wrangler secret put BETTER_AUTH_SECRET
# Paste your secret when prompted
```

For non-secret vars, add to `wrangler.toml`:

```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:

```ts
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:

```ts
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 `trustedOrigins` for 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**:

```ts
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_SECRET` is set (minimum 32 random characters)
- [ ] `secure: true` is enabled (HTTPS only)
- [ ] `httpOnly: true` is enabled (XSS protection)
- [ ] `sameSite: 'lax'` or `'strict'` unless cross-domain required
- [ ] `trustedOrigins` includes only your domains
- [ ] `ALLOWED_ORIGINS` matches `trustedOrigins`
- [ ] 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:

```ts
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:

```ts
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`):
```bash
TRUSTED_ORIGINS=http://localhost:3000,http://localhost:5173
CROSS_SUBDOMAIN_COOKIES=false
COOKIE_SAMESITE=lax
```

**Production** (Wrangler secrets):
```bash
TRUSTED_ORIGINS=https://account.example.com,https://api.example.com
CROSS_SUBDOMAIN_COOKIES=true
COOKIE_DOMAIN=example.com
COOKIE_SAMESITE=none
```

---

## Further Reading

- [Better Auth: Cookies Documentation](https://www.better-auth.com/docs/concepts/cookies)
- [Better Auth: Security Guide](https://www.better-auth.com/docs/reference/security)
- [OWASP: Cross-Site Request Forgery Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html)
- [MDN: SameSite Cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value)
