Skip to content

Commit d7704b7

Browse files
authored
feat(auth): support multiple Google OAuth callback URLs for web + mobile (#134)
* feat(auth): support multiple Google OAuth callback URLs for web + mobile - Add OAUTH_GOOGLE_CALLBACK_URLS (comma-separated) with backward compat for OAUTH_GOOGLE_CALLBACK_URL - Client sends redirect_uri in authorize-url request; server validates against allowlist - Store redirectUri in verification meta for exchange step - useOAuthLogin/useOAuthLink accept optional redirectUri for Google (mobile custom schemes) - Update providers and auth docs * docs: update auth and deployment for Google OAuth multi-URL callback - authentication.mdx: add Google redirect endpoints, link-authorize-url with redirect_uri, callback pages - account-linking.mdx: document meta.redirectUri and Google multi-URL flow - vercel.mdx: add OAuth env vars pointer to Authentication doc * fix(auth): add Google OAuth PKCE, token sync, and UI fixes * fix(auth): refactor Google exchange, handleGoogleClick, Google error mappings * fix(fastify,next): address inline review findings - Revert max-lines to 300, extract exchange/template helpers - Remove oauth_not_configured toast, use oauth_failed_google for Google callback - Fix no-throw-literal in oauth-google * fix(auth): validate OAuth redirect URI and improve Google error mapping - Re-validate redirectUri from state against allowedUrls in Google exchange - Add googleOverrides map and google_email_required message - Refactor translateOAuthError: resolve base key then apply provider overrides * fix(fastify): harden Google OAuth exchange and error handling - add isLinkMode/linkUserId guard to fail closed on invalid link state - preserve refresh token when Google omits it on existing accounts - pass through upstream HTTP status (401/429/5xx) in token/user catch blocks - add 429 to exchange response schema for rate limits
1 parent f96b173 commit d7704b7

38 files changed

+1942
-378
lines changed

apps/docu/content/docs/architecture/account-linking.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@ Logged-in users can link additional OAuth providers from Profile (Settings). The
2323

2424
- Uses `oauth_link_state` (distinct from `oauth_state` for sign-in)
2525
- Binds the link to `meta.userId` so the exchange attaches to the correct account
26+
- Stores `meta.redirectUri` for Google (from `redirect_uri` query) when multiple callback URLs are configured (web + mobile)
2627
- Returns 409 `PROVIDER_ALREADY_LINKED` when the provider is linked to another user
2728
- Returns `redirectTo` so the client can return the user to the settings page after success
2829

30+
For Google, when using multiple callback URLs (e.g. web + mobile app), the client passes `redirect_uri` in the link-authorize-url request. It must be in the allowlist (`OAUTH_GOOGLE_CALLBACK_URLS`). See [Authentication](/docs/architecture/authentication) for full Google OAuth setup.
31+
2932
## Provider trust
3033

3134
OAuth providers are trusted for identity on first link. Subsequent links require re-authorization; existing tokens are replaced on each successful link.

apps/docu/content/docs/architecture/authentication.mdx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ OAuth sign-in uses provider authorization + callback flows and issues access + r
123123

124124
### OAuth (Google, Facebook, Twitter)
125125

126-
**Google One Tap:** Uses [Google Identity Services](https://developers.google.com/identity/gsi/web) (GIS). Load GIS script, call `google.accounts.id.prompt()` — popup appears in top-right when user is logged into Google. Client POSTs `{ credential }` to `POST /auth/oauth/google/verify-id-token`. Backend verifies JWT with `google-auth-library`, creates session. **Setup:** `GOOGLE_CLIENT_ID` (Fastify), `NEXT_PUBLIC_GOOGLE_CLIENT_ID` (Next.js).
126+
**Google:** Two flows. **(1) One Tap (primary):** Uses [Google Identity Services](https://developers.google.com/identity/gsi/web) (GIS). Popup in top-right when user is logged into Google. Client POSTs `{ credential }` to `POST /auth/oauth/google/verify-id-token`. **(2) Redirect (fallback + linking):** When One Tap is blocked/dismissed or for account linking. `GET /auth/oauth/google/authorize-url?redirect_uri=` (optional) → callback → `POST /auth/oauth/google/exchange`. Client can send `redirect_uri` to choose which callback URL to use (must be in allowlist). **Setup:** One Tap: `GOOGLE_CLIENT_ID`, `NEXT_PUBLIC_GOOGLE_CLIENT_ID`. Redirect/linking: add `GOOGLE_CLIENT_SECRET`, and either `OAUTH_GOOGLE_CALLBACK_URL` (single) or `OAUTH_GOOGLE_CALLBACK_URLS` (comma-separated for web + mobile).
127127

128128
**Facebook:** Redirect flow like GitHub. `GET /auth/oauth/facebook/authorize-url``POST /auth/oauth/facebook/exchange`. Callback: `/auth/callback/oauth/facebook`. **Setup:** `FACEBOOK_CLIENT_ID`, `FACEBOOK_CLIENT_SECRET`, `OAUTH_FACEBOOK_CALLBACK_URL`.
129129

@@ -135,7 +135,7 @@ When a provider is not configured (503 `OAUTH_NOT_CONFIGURED`), the client shows
135135

136136
Logged-in users can link additional OAuth providers from Profile (Settings). Flow: `GET /auth/oauth/{provider}/link-authorize-url` (session/JWT required, no API key) → redirect to provider → callback → exchange with `oauth_link_state` → link account, return `{ token, refreshToken, redirectTo: "/settings?linked=ok" }`.
137137

138-
- **Link-authorize-url:** Session/JWT required, no API key; rate limit 10/hour per user. Stores `oauth_link_state` with `meta.userId`.
138+
- **Link-authorize-url:** Session/JWT required, no API key; rate limit 10/hour per user. Stores `oauth_link_state` with `meta.userId`. Google accepts optional query `redirect_uri` for mobile (must be in `OAUTH_GOOGLE_CALLBACK_URLS`).
139139
- **Exchange:** State lookup branches on `oauth_link_state` vs `oauth_state`. Link mode uses `meta.userId`; if provider already linked to another user → 409 `PROVIDER_ALREADY_LINKED`.
140140
- **Unlink:** `DELETE /account/link/oauth/:providerId` (Bearer). Guardrail: cannot unlink if it would leave no sign-in method (400 `LAST_SIGN_IN_METHOD`).
141141
- **Session user:** `GET /auth/session/user` returns `linkedAccounts: [{ providerId }]`.
@@ -306,6 +306,9 @@ All endpoints below are served by `apps/fastify`. The API does not set cookies;
306306
- `POST /auth/oauth/github/exchange``{ token, refreshToken }` (body: `{ code, state }`)
307307
- **OAuth (Google One Tap)**
308308
- `POST /auth/oauth/google/verify-id-token``{ token, refreshToken }` (body: `{ credential }`)
309+
- **OAuth (Google redirect)** — fallback + linking
310+
- `GET /auth/oauth/google/authorize-url``{ redirectUrl }` (optional query: `redirect_uri` — must be in allowlist)
311+
- `POST /auth/oauth/google/exchange``{ token, refreshToken, redirectTo? }` (body: `{ code, state }`)
309312
- **OAuth (Facebook)**
310313
- `GET /auth/oauth/facebook/authorize-url``{ redirectUrl }`
311314
- `POST /auth/oauth/facebook/exchange``{ token, refreshToken }` (body: `{ code, state }`)
@@ -324,7 +327,7 @@ All endpoints below are served by `apps/fastify`. The API does not set cookies;
324327
- `DELETE /account/link/wallet/:id``204` (unlink wallet by id from `linkedWallets`)
325328
- `POST /account/link/email/request``{ ok }` (body: `email`, `callbackUrl`)
326329
- `POST /account/link/email/verify``{ token, refreshToken }` (body: `token`)
327-
- `GET /auth/oauth/{github,facebook,twitter}/link-authorize-url``{ redirectUrl }` (Session required — cookie/JWT session, no API key; rate limit 10/hour)
330+
- `GET /auth/oauth/{github,google,facebook,twitter}/link-authorize-url``{ redirectUrl }` (Session required; Google accepts optional query `redirect_uri` for mobile; rate limit 10/hour)
328331
- `DELETE /account/link/oauth/:providerId``204` (unlink OAuth provider)
329332

330333
**Account link error codes**: `WALLET_ALREADY_LINKED`, `EMAIL_ALREADY_IN_USE`, `PROVIDER_ALREADY_LINKED`, `LAST_SIGN_IN_METHOD`
@@ -358,7 +361,7 @@ All endpoints below are served by `apps/fastify`. The API does not set cookies;
358361

359362
## Next.js Auth (web)
360363

361-
**Callback pages** (`/auth/callback/magiclink`, `/auth/callback/change-email`, `/auth/callback/oauth/github`, `/auth/callback/oauth/facebook`, `/auth/callback/oauth/twitter`, `/auth/callback/web3`, `/auth/callback/passkey`) exchange credentials with Fastify, set cookies, and redirect. **Logout** (`/auth/logout`) calls Fastify to revoke the session and clears cookies.
364+
**Callback pages** (`/auth/callback/magiclink`, `/auth/callback/change-email`, `/auth/callback/oauth/github`, `/auth/callback/oauth/google`, `/auth/callback/oauth/facebook`, `/auth/callback/oauth/twitter`, `/auth/callback/web3`, `/auth/callback/passkey`) exchange credentials with Fastify, set cookies, and redirect. **Logout** (`/auth/logout`) calls Fastify to revoke the session and clears cookies.
362365

363366
**API routes** (cookie updates only):
364367
- `POST /api/auth/update-tokens`: accepts `{ token, refreshToken }`, sets single cookie. Used after 401 refresh (core's `onTokensRefreshed`) and after link email or profile edit.

apps/docu/content/docs/deployment/vercel.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ RESEND_API_KEY=re_...
4242
PORT=3001
4343
NODE_ENV=production
4444
SENTRY_DSN=https://...
45+
# Optional: OAuth — see [Authentication](/docs/architecture/authentication) for GITHUB_*, GOOGLE_*, OAUTH_*_CALLBACK_URL(S).
4546
```
4647

4748
**For Web (`apps/next`):**

apps/fastify/.env-sample

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,13 @@ DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
2626
# GITHUB_CLIENT_SECRET=
2727
# OAUTH_GITHUB_CALLBACK_URL=http://localhost:3000/auth/callback/oauth/github
2828

29-
# Optional: Google OAuth One Tap (GIS popup, top-right)
29+
# Optional: Google OAuth (One Tap popup + redirect fallback)
3030
# GOOGLE_CLIENT_ID=xxx.apps.googleusercontent.com
31-
# NEXT_PUBLIC_GOOGLE_CLIENT_ID=xxx.apps.googleusercontent.com (same value, for Next.js)
31+
# GOOGLE_CLIENT_SECRET=xxx
32+
# Single callback URL (backward compat):
33+
# OAUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/callback/oauth/google
34+
# Multiple callbacks for web + mobile (comma-separated; client sends redirect_uri to choose):
35+
# OAUTH_GOOGLE_CALLBACK_URLS=http://localhost:3000/auth/callback/oauth/google,yourapp://auth/callback
3236

3337
# Optional: Facebook OAuth
3438
# FACEBOOK_CLIENT_ID=

apps/fastify/eslint.config.mjs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,8 @@ export default [
77
'src/routes/auth/oauth/twitter/exchange.ts',
88
'src/routes/auth/oauth/github/exchange.ts',
99
'src/routes/auth/oauth/facebook/exchange.ts',
10+
'src/routes/auth/oauth/google/exchange.ts',
1011
],
1112
rules: { complexity: 'off' },
1213
},
13-
{
14-
files: ['src/routes/reference/template.ts'],
15-
rules: {
16-
'max-lines': ['error', { max: 350, skipBlankLines: true, skipComments: true }],
17-
'max-params': 'off',
18-
},
19-
},
2014
]

0 commit comments

Comments
 (0)