feat: same-origin auth via reverse proxy and secure cookies
Route /api/auth through the same origin in both dev and prod so cookies stay first-party and the browser and React Router SSR share one auth surface. - Vite dev server proxies /api/auth to the Convex HTTP site. - Better Auth uses secure cookies when the site URL is https. - Add docs/auth-proxy.md documenting the required production ingress (Caddy/Traefik) and Convex SITE_URL / CONVEX_SITE_URL setup.
This commit is contained in:
@@ -7,14 +7,17 @@ import { defineConfig } from "vite-plus";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
envDir: path.resolve(import.meta.dirname, "../.."),
|
envDir: path.resolve(import.meta.dirname, "../.."),
|
||||||
plugins: [tailwindcss(), reactRouter()],
|
plugins: [tailwindcss(), reactRouter()],
|
||||||
ssr: {
|
|
||||||
noExternal: true,
|
|
||||||
},
|
|
||||||
resolve: {
|
resolve: {
|
||||||
dedupe: ["convex", "react", "react-dom"],
|
dedupe: ["convex", "react", "react-dom"],
|
||||||
tsconfigPaths: true,
|
tsconfigPaths: true,
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
|
proxy: {
|
||||||
|
"/api/auth": {
|
||||||
|
changeOrigin: true,
|
||||||
|
target: "https://befitting-dalmatian-161.convex.site",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
78
docs/auth-proxy.md
Normal file
78
docs/auth-proxy.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Auth Proxy — Production Ingress Requirement
|
||||||
|
|
||||||
|
The application uses **same-origin authentication**: the browser and React Router SSR both hit
|
||||||
|
`/api/auth/*` on the public application domain. This keeps cookies first-party, avoids
|
||||||
|
cross-origin credentials, and gives development and production the same API surface.
|
||||||
|
|
||||||
|
## Required production route
|
||||||
|
|
||||||
|
At the public application domain, route:
|
||||||
|
|
||||||
|
| Prefix | Target |
|
||||||
|
|---|---|
|
||||||
|
| `/api/auth/*` | Convex HTTP site |
|
||||||
|
| `/*` | React Router frontend |
|
||||||
|
|
||||||
|
### Caddy
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
zopu.example.com {
|
||||||
|
handle /api/auth/* {
|
||||||
|
reverse_proxy https://befitting-dalmatian-161.convex.site {
|
||||||
|
header_up Host befitting-dalmatian-161.convex.site
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle {
|
||||||
|
reverse_proxy frontend:3000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dokploy / Traefik
|
||||||
|
|
||||||
|
Create a higher-priority path router for `/api/auth` that forwards to the external Convex
|
||||||
|
site URL. Ensure it:
|
||||||
|
|
||||||
|
- Preserves the original browser `Cookie` header
|
||||||
|
- Passes `Set-Cookie` responses back (rewrite domain if Convex emits an explicit one)
|
||||||
|
- Preserves the original method and body
|
||||||
|
- Forwards `X-Forwarded-Host` and `X-Forwarded-Proto`
|
||||||
|
- Passes the full path unchanged (e.g. `/api/auth/convex/token`)
|
||||||
|
- Does not cache auth responses
|
||||||
|
- Allows OAuth callback routes under the same prefix
|
||||||
|
|
||||||
|
## Convex environment
|
||||||
|
|
||||||
|
The Convex deployment `SITE_URL` must match the public origin users visit, not the Convex
|
||||||
|
site URL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Production
|
||||||
|
npx convex env set SITE_URL 'https://zopu.example.com'
|
||||||
|
|
||||||
|
# Local
|
||||||
|
npx convex env set SITE_URL 'http://100.101.157.28:5173'
|
||||||
|
|
||||||
|
# Staging
|
||||||
|
npx convex env set SITE_URL 'https://zopu.cheaptricks.puter.wtf'
|
||||||
|
```
|
||||||
|
|
||||||
|
This value populates Better Auth's `trustedOrigins`. The Convex deployment also uses
|
||||||
|
`CONVEX_SITE_URL` as the internal `baseURL` for route registration; that value is the HTTP
|
||||||
|
site URL (e.g. `https://befitting-dalmatian-161.convex.site`).
|
||||||
|
|
||||||
|
## Why same-origin
|
||||||
|
|
||||||
|
1. **First-party cookies.** No `SameSite=None`, no third-party cookie restrictions.
|
||||||
|
2. **SSR consistency.** The React Router server uses the same auth surface the browser
|
||||||
|
sees; no cross-host credential translation.
|
||||||
|
3. **No CORS complexity.** The browser talks only to its own origin.
|
||||||
|
4. **The reverse proxy handles the cross-host hop server-side.**
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Better Auth: Trusted Origins](https://github.com/better-auth/better-auth/blob/main/docs/content/docs/reference/security.mdx)
|
||||||
|
- `apps/web/vite.config.ts` — Vite dev proxy (`/api/auth` → Convex site)
|
||||||
|
- `packages/auth/src/web/auth-client.ts` — `window.location.origin`
|
||||||
|
- `apps/web/src/lib/auth.server.ts` — SSR token loader via request origin
|
||||||
@@ -17,6 +17,7 @@ export const authComponent = createClient<DataModel>(components.betterAuth);
|
|||||||
|
|
||||||
const createAuth = (ctx: GenericCtx<DataModel>) =>
|
const createAuth = (ctx: GenericCtx<DataModel>) =>
|
||||||
betterAuth({
|
betterAuth({
|
||||||
|
advanced: { useSecureCookies: siteUrl.startsWith("https://") },
|
||||||
baseURL: env.CONVEX_SITE_URL,
|
baseURL: env.CONVEX_SITE_URL,
|
||||||
database: authComponent.adapter(ctx),
|
database: authComponent.adapter(ctx),
|
||||||
emailAndPassword: {
|
emailAndPassword: {
|
||||||
|
|||||||
Reference in New Issue
Block a user