github 0auth fixes

This commit is contained in:
-Puter
2026-07-31 19:29:00 +05:30
parent fc8cfabacb
commit fc1d0b6916
5 changed files with 80 additions and 55 deletions

View File

@@ -1,17 +1,15 @@
# Auth Proxy — Production Ingress Requirement # Auth Proxy — Production Ingress Requirement
The application uses **same-origin authentication**: the browser and React Router SSR both hit 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.
`/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 ## Required production route
At the public application domain, route: At the public application domain, route:
| Prefix | Target | | Prefix | Target |
|---|---| | ------------- | --------------------- |
| `/api/auth/*` | Convex HTTP site | | `/api/auth/*` | Convex HTTP site |
| `/*` | React Router frontend | | `/*` | React Router frontend |
### Caddy ### Caddy
@@ -31,8 +29,7 @@ zopu.example.com {
### Dokploy / Traefik ### Dokploy / Traefik
Create a higher-priority path router for `/api/auth` that forwards to the external Convex Create a higher-priority path router for `/api/auth` that forwards to the external Convex site URL. Ensure it:
site URL. Ensure it:
- Preserves the original browser `Cookie` header - Preserves the original browser `Cookie` header
- Passes `Set-Cookie` responses back (rewrite domain if Convex emits an explicit one) - Passes `Set-Cookie` responses back (rewrite domain if Convex emits an explicit one)
@@ -44,8 +41,7 @@ site URL. Ensure it:
## Convex environment ## Convex environment
The Convex deployment `SITE_URL` must match the public origin users visit, not the Convex The Convex deployment `SITE_URL` must match the public origin users visit, not the Convex site URL:
site URL:
```bash ```bash
# Production # Production
@@ -58,15 +54,20 @@ npx convex env set SITE_URL 'http://100.101.157.28:5173'
npx convex env set SITE_URL 'https://zopu.cheaptricks.puter.wtf' npx convex env set SITE_URL 'https://zopu.cheaptricks.puter.wtf'
``` ```
This value populates Better Auth's `trustedOrigins`. The Convex deployment also uses This value is Better Auth's public `baseURL` and `trustedOrigins`. It must be the browser origin because Better Auth writes OAuth state cookies and receives provider callbacks through the same-origin proxy. `CONVEX_SITE_URL` remains the internal Convex HTTP site URL.
`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`). ## One origin per shared OAuth deployment
GitHub OAuth Apps expose one authorization callback URL. The value must exactly equal `<SITE_URL>/api/auth/callback/github`, including scheme, host, port, and path. A GitHub authorization request whose `redirect_uri` differs returns `Invalid Redirect URI` before the user can grant access.
`SITE_URL` is also single-valued in a Convex deployment. Therefore, two browser origins that share one deployment—such as Tailscale development and Puter staging—cannot use GitHub OAuth at the same time. Either switch both the Convex `SITE_URL` and the GitHub OAuth App callback together before testing the other origin, or give each public environment its own Convex deployment and OAuth App.
Never set Better Auth's `baseURL` to `CONVEX_SITE_URL` for the browser flow. The browser creates the OAuth state cookie on `SITE_URL`; a provider callback sent directly to `CONVEX_SITE_URL` cannot read that cookie and fails with `state_mismatch`.
## Why same-origin ## Why same-origin
1. **First-party cookies.** No `SameSite=None`, no third-party cookie restrictions. 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 2. **SSR consistency.** The React Router server uses the same auth surface the browser sees; no cross-host credential translation.
sees; no cross-host credential translation.
3. **No CORS complexity.** The browser talks only to its own origin. 3. **No CORS complexity.** The browser talks only to its own origin.
4. **The reverse proxy handles the cross-host hop server-side.** 4. **The reverse proxy handles the cross-host hop server-side.**

View File

@@ -1,11 +1,10 @@
# Deployment Notes # Deployment Notes
Two active environments share one Convex deployment (`dev:befitting-dalmatian-161`). Two active environments share one Convex deployment (`dev:befitting-dalmatian-161`). Each runs its own web server, Flue agents process, and `.env` file.
Each runs its own web server, Flue agents process, and `.env` file.
## Environments ## Environments
| | Local Mac Dev | Cheaptricks Staging | | | Local Mac Dev | Cheaptricks Staging |
| --- | --- | --- | | --- | --- | --- |
| SSH alias | (local) | `cheaptricks` | | SSH alias | (local) | `cheaptricks` |
| Repo path | `/Users/puter/Workspace/zopu/code` | `/workspace/code` | | Repo path | `/Users/puter/Workspace/zopu/code` | `/workspace/code` |
@@ -19,12 +18,9 @@ Each runs its own web server, Flue agents process, and `.env` file.
## Shared Convex deployment ## Shared Convex deployment
Deployment name: `dev:befitting-dalmatian-161` Deployment name: `dev:befitting-dalmatian-161` Convex URL: `https://befitting-dalmatian-161.convex.cloud` Convex Site URL: `https://befitting-dalmatian-161.convex.site`
Convex URL: `https://befitting-dalmatian-161.convex.cloud`
Convex Site URL: `https://befitting-dalmatian-161.convex.site`
Convex env vars are deployment-scoped, not per-machine. Authenticate from any Convex env vars are deployment-scoped, not per-machine. Authenticate from any machine with `npx convex dev` inside `packages/backend`.
machine with `npx convex dev` inside `packages/backend`.
Key Convex env var: Key Convex env var:
@@ -32,9 +28,7 @@ Key Convex env var:
SITE_URL = <the origin Better Auth should trust> SITE_URL = <the origin Better Auth should trust>
``` ```
This controls `trustedOrigins` in the Better Auth config This controls `trustedOrigins` in the Better Auth config (`packages/backend/convex/auth.ts`). It must match the URL the browser actually visits, or sign-in fails silently with a CORS rejection.
(`packages/backend/convex/auth.ts`). It must match the URL the browser
actually visits, or sign-in fails silently with a CORS rejection.
When switching between environments: When switching between environments:
@@ -48,6 +42,10 @@ npx convex env set SITE_URL 'http://100.101.157.28:5173'
npx convex env set SITE_URL 'https://zopu.cheaptricks.puter.wtf' npx convex env set SITE_URL 'https://zopu.cheaptricks.puter.wtf'
``` ```
### OAuth consequence
`SITE_URL` drives both Better Auth's public callback origin and the single GitHub OAuth App callback. When switching the shared deployment between the local and staging origins, also update the GitHub OAuth App's **Authorization callback URL** to `<SITE_URL>/api/auth/callback/github` before attempting a GitHub connection. GitHub OAuth Apps allow one callback URL; use distinct Convex deployments and OAuth Apps if both environments must operate concurrently.
## Local Mac Dev `.env` ## Local Mac Dev `.env`
```env ```env
@@ -143,16 +141,24 @@ zopu.cheaptricks.puter.wtf {
bind 135.181.82.179 2a01:4f9:c013:4a64::1 bind 135.181.82.179 2a01:4f9:c013:4a64::1
encode zstd gzip encode zstd gzip
# Must precede the generic /api route: preserves /api/auth/* and cookies.
handle /api/auth/* {
reverse_proxy https://befitting-dalmatian-161.convex.site {
header_up Host befitting-dalmatian-161.convex.site
}
}
handle_path /api/* { handle_path /api/* {
reverse_proxy 127.0.0.1:3585 reverse_proxy 127.0.0.1:3585
} }
reverse_proxy 127.0.0.1:5173 handle {
reverse_proxy 127.0.0.1:5173
}
} }
``` ```
`/api/*` routes to the Flue agents process (port 3585). `/api/auth/*` is the same-origin Better Auth proxy to Convex. The generic `/api/*` route serves Flue at port `3585`; it must be evaluated only after the authentication route.
Everything else routes to the web dev server (port 5173).
Reload after changes: Reload after changes:
@@ -162,8 +168,7 @@ sudo systemctl reload caddy
### Vite allowedHosts ### Vite allowedHosts
`apps/web/vite.config.ts` must include `server: { allowedHosts: true }` or `apps/web/vite.config.ts` must include `server: { allowedHosts: true }` or Vite rejects requests arriving through the Caddy domain.
Vite rejects requests arriving through the Caddy domain.
### Start staging dev ### Start staging dev
@@ -211,23 +216,18 @@ Both environments use the same model via the Cheaptricks AI gateway:
- Multimodal: text + image input - Multimodal: text + image input
The `AGENT_MODEL_BASE_URL` differs: The `AGENT_MODEL_BASE_URL` differs:
- Local Mac: uses the external gateway URL - Local Mac: uses the external gateway URL
- Cheaptricks: uses `https://ai.cheaptricks.puter.wtf/v1` (local to the box) - Cheaptricks: uses `https://ai.cheaptricks.puter.wtf/v1` (local to the box)
## Known gotchas ## Known gotchas
1. **Convex SITE_URL is single-valued.** Only one origin can be trusted at a 1. **Convex `SITE_URL` is single-valued and is the OAuth callback origin.** Switch it together with the GitHub OAuth App's single callback URL when moving between local and staging. Running both origins concurrently requires separate Convex deployments and OAuth Apps.
time. Switch it when moving between local and staging, or sign-in breaks
silently with a CORS error in the browser console.
2. **Vite blocks unknown hosts by default.** Caddy domain must be allowed 2. **Vite blocks unknown hosts by default.** Caddy domain must be allowed via `server.allowedHosts` in `apps/web/vite.config.ts`.
via `server.allowedHosts` in `apps/web/vite.config.ts`.
3. **Flue port changed from 3583 to 3585.** The old Caddy config pointed at 3. **Flue port changed from 3583 to 3585.** The old Caddy config pointed at 3583/13100. Current ports are 3585 (Flue) and 5173 (web).
3583/13100. Current ports are 3585 (Flue) and 5173 (web).
4. **`.env` is gitignored.** Each machine maintains its own copy. The repo 4. **`.env` is gitignored.** Each machine maintains its own copy. The repo ships `.env.example` as the template.
ships `.env.example` as the template.
5. **Convex CLI auth is per-machine.** Run `npx convex dev` once inside 5. **Convex CLI auth is per-machine.** Run `npx convex dev` once inside `packages/backend` on each new machine to authenticate the CLI.
`packages/backend` on each new machine to authenticate the CLI.

View File

@@ -6,20 +6,27 @@ This document covers manual setup for GitHub OAuth, Puter Git (Gitea), webhooks,
## GitHub OAuth Application ## GitHub OAuth Application
1. Go to GitHub Settings > Developer settings > OAuth Apps > New OAuth App. 1. Go to GitHub **Settings Developer settings OAuth Apps** and open the app whose client ID will be stored in Convex.
2. Set the application name (e.g., "Zopu"). 2. Set the application name and homepage URL. The homepage URL should be the current `SITE_URL`.
3. Set the homepage URL to your `SITE_URL` (e.g., `https://zopu.cheaptricks.puter.wtf`). 3. Set the single **Authorization callback URL** to exactly:
4. Set the callback URL to `<SITE_URL>/api/auth/callback/github`. ```text
5. Generate a client secret. <SITE_URL>/api/auth/callback/github
6. Set Convex environment variables:
``` ```
GitHub OAuth Apps support one callback URL. Scheme, host, port, and path must match the `redirect_uri` sent by Better Auth exactly.
4. Generate a client secret.
5. Set the credentials on the Convex deployment—not only in the local `.env` file:
```bash
cd packages/backend
npx convex env set GITHUB_CLIENT_ID <your-client-id> npx convex env set GITHUB_CLIENT_ID <your-client-id>
npx convex env set GITHUB_CLIENT_SECRET <your-client-secret> npx convex env set GITHUB_CLIENT_SECRET <your-client-secret>
npx convex env list
``` ```
The configured `SITE_URL`, GitHub callback URL, and browser URL are one unit. For a shared Convex deployment, switch all three before testing a different public origin. See [Auth Proxy](./auth-proxy.md#one-origin-per-shared-oauth-deployment) for the deployment boundary.
### Requested scopes ### Requested scopes
Zopu requests `repo` and `read:org`. The `repo` scope grants access to private repositories. `read:org` allows listing organization repositories. Zopu requests GitHub identity and private-repository access: `read:user`, `user:email`, `repo`, and `read:org`. `repo` grants access to private repositories; `read:org` permits organization repository discovery.
## GitHub Webhook (manual setup for OAuth-based version) ## GitHub Webhook (manual setup for OAuth-based version)
@@ -102,6 +109,18 @@ Better Auth requires the callback URL to be on the same origin as `SITE_URL`. If
2. Complete the GitHub OAuth flow. 2. Complete the GitHub OAuth flow.
3. Verify the connection state shows `active`. 3. Verify the connection state shows `active`.
### OAuth diagnostics
| Symptom | Cause | Fix |
| --- | --- | --- |
| `Provider not found` from `/api/auth/link-social` | `GITHUB_CLIENT_ID` or `GITHUB_CLIENT_SECRET` is absent from the Convex deployment env store. A local `.env` does not configure cloud functions. | Set both variables with `npx convex env set` and verify with `npx convex env list`. |
| GitHub `Invalid Redirect URI` | GitHub's saved callback does not exactly equal the app's `redirect_uri`. | Set the OAuth App's single callback to `<SITE_URL>/api/auth/callback/github`. |
| `state_mismatch` at a `convex.site` page | Better Auth started on the public origin but GitHub returned to the direct Convex host, so the public-origin state cookie is unavailable. | Set Better Auth `baseURL` to `SITE_URL`; proxy `/api/auth/*` through the public origin. |
| `email_doesn't_match` after selecting GitHub | A deployment predates the explicit-link policy or has not received the current auth configuration. | Deploy the current backend. Explicit `linkSocial` allows a different GitHub email without changing the Zopu account email. |
| `externalEmail` is `null` during `connectGithub` | The GitHub account hides its public email; GitHub returns `null` from `/user`. | Deploy the current backend, which normalizes nullable provider emails before Convex persistence. |
GitHub connections are explicit authenticated account links. The linked GitHub email may differ from the existing Zopu account email; Zopu retains the existing account email and only stores the GitHub identity and OAuth credential needed for repository access.
### Reconnection ### Reconnection
If a token is expired or revoked: If a token is expired or revoked:

View File

@@ -17,8 +17,13 @@ export const authComponent = createClient<DataModel>(components.betterAuth);
const createAuth = (ctx: GenericCtx<DataModel>) => const createAuth = (ctx: GenericCtx<DataModel>) =>
betterAuth({ betterAuth({
account: {
accountLinking: {
allowDifferentEmails: true,
},
},
advanced: { useSecureCookies: siteUrl.startsWith("https://") }, advanced: { useSecureCookies: siteUrl.startsWith("https://") },
baseURL: env.CONVEX_SITE_URL, baseURL: siteUrl,
database: authComponent.adapter(ctx), database: authComponent.adapter(ctx),
emailAndPassword: { emailAndPassword: {
enabled: true, enabled: true,

View File

@@ -80,13 +80,13 @@ const fetchGiteaUser = async (
); );
} }
const user = (await response.json()) as { const user = (await response.json()) as {
readonly email?: string; readonly email?: string | null;
readonly id: number; readonly id: number;
readonly login: string; readonly login: string;
}; };
return { return {
externalAccountId: String(user.id), externalAccountId: String(user.id),
externalEmail: user.email, externalEmail: user.email ?? undefined,
externalUsername: user.login, externalUsername: user.login,
}; };
}; };
@@ -105,13 +105,13 @@ const fetchGithubUser = async (token: string): Promise<ExternalUserInfo> => {
); );
} }
const user = (await response.json()) as { const user = (await response.json()) as {
readonly email?: string; readonly email?: string | null;
readonly id: number; readonly id: number;
readonly login: string; readonly login: string;
}; };
return { return {
externalAccountId: String(user.id), externalAccountId: String(user.id),
externalEmail: user.email, externalEmail: user.email ?? undefined,
externalUsername: user.login, externalUsername: user.login,
}; };
}; };