Skip to content

feat(auth): support multiple Google OAuth callback URLs for web + mobile#134

Merged
gaboesquivel merged 7 commits intomainfrom
google-auth
Mar 10, 2026
Merged

feat(auth): support multiple Google OAuth callback URLs for web + mobile#134
gaboesquivel merged 7 commits intomainfrom
google-auth

Conversation

@gaboesquivel
Copy link
Member

@gaboesquivel gaboesquivel commented Mar 10, 2026

Summary by CodeRabbit

  • New Features

    • Added Google redirect flow (fallback + linking) with authorize/exchange/link endpoints and provider enablement flags; providers list now reflects redirect capability.
  • Documentation

    • Updated authentication and deployment docs with dual-flow guidance, redirect_uri allowlisting, and setup notes.
  • UI / Client

    • Login and linked-accounts UI/hooks updated to support Google redirect flow and One Tap fallback.
  • Tests

    • Added tests covering Google authorize and exchange error scenarios.

- 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
- 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
@vercel
Copy link

vercel bot commented Mar 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
basilic-docs Ready Ready Preview, Comment Mar 10, 2026 8:49pm
basilic-fastify Ready Ready Preview, Comment Mar 10, 2026 8:49pm
basilic-next Ready Ready Preview, Comment Mar 10, 2026 8:49pm

@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a Google OAuth redirect-based PKCE flow (authorize, exchange, link-authorize), persists redirectUri in verification.meta, exposes googleRedirect in the providers API, updates env/config, adds Fastify routes/tests and Next.js callback, and updates client hooks/UI to support redirect fallback alongside One Tap.

Changes

Cohort / File(s) Summary
Docs
apps/docu/content/docs/architecture/account-linking.mdx, apps/docu/content/docs/architecture/authentication.mdx, apps/docu/content/docs/deployment/vercel.mdx
Document dual Google flows (One Tap + redirect), redirect_uri allowlist, new env vars and config notes, and OAuth_NOT_CONFIGURED behavior.
Fastify — Google OAuth routes & tests
apps/fastify/src/routes/auth/oauth/google/authorize-url.ts, .../authorize-url.test.ts, .../exchange.ts, .../exchange.test.ts, .../link-authorize-url.ts
New routes for Google: GET authorize-url, POST exchange, GET link-authorize-url. Implement PKCE/state, persist oauth_state with meta (redirectUri, codeVerifier), redirect_uri validation, linking rate limits, token exchange, user linking/creation, encrypted tokens, session creation, and tests for unconfigured cases.
API Schema & Providers
apps/fastify/openapi/openapi.json, apps/fastify/src/routes/auth/oauth/providers.ts, .../providers.test.ts
OpenAPI additions for Google authorize/exchange/link endpoints; added googleRedirect boolean to providers response; tests updated.
Env & DB schema
apps/fastify/src/lib/env.ts, apps/fastify/src/db/schema/tables/verification.ts
Add env vars: GOOGLE_CLIENT_SECRET, OAUTH_GOOGLE_CALLBACK_URL(S) with parsing; extend verification.meta type to include optional redirectUri.
Fastify tooling & spec imports
apps/fastify/eslint.config.mjs, apps/fastify/src/routes/auth/oauth.spec.ts
Add new exchange file to ESLint config; remove standalone eslint block for a template file; import new Google oauth tests into oauth.spec.
Fastify lib — Google helpers
apps/fastify/src/lib/oauth-google.ts
New module to exchange codes and fetch Google userinfo with timeout, typed responses, and structured error throws.
Next.js callback & login UX
apps/next/app/auth/callback/oauth/google/route.ts, apps/next/app/auth/login/login-actions.tsx, apps/next/app/auth/login/use-google-one-tap.ts
New Next.js Google callback route to call exchange API and set auth cookies; unify Google click handling (One Tap vs redirect fallback), add onSkipped hook and load/error handling to One Tap hook.
React hooks & UI linking
packages/react/src/hooks/use-oauth-login.ts, packages/react/src/hooks/use-oauth-link.ts, packages/react/src/hooks/use-oauth-providers.ts, apps/next/app/(dashboard)/settings/(profile)/linked-accounts-section.tsx, apps/next/lib/auth/auth-error-messages.ts
Extend hooks to accept redirectUri and Google provider, expose googleRedirect, add Google-specific error mappings, and update linked-accounts UI to respect provider enablement.
Reference template & assets
apps/fastify/src/routes/reference/template-scripts.ts, apps/fastify/src/routes/reference/template-styles.ts, apps/fastify/src/routes/reference/template.ts
Refactor reference page: move inline script/styles to modular files, add getInitScript and scalarStyles exports, update template to import them.
Misc tests & helpers
apps/fastify/src/routes/auth/oauth/providers.test.ts, apps/fastify/src/routes/auth/oauth.spec.ts
Tests updated/added to assert new googleRedirect flag and Google endpoint unconfigured responses.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client/Browser
    participant NextJS as Next.js (callback)
    participant Fastify as Fastify API
    participant Google as Google OAuth
    participant DB as Database

    rect rgba(100,150,240,0.5)
    Note over Client,NextJS: Start authorization
    Client->>NextJS: Click "Sign in with Google" (or link)
    NextJS->>Fastify: GET /auth/oauth/google/authorize-url?redirect_uri=...
    Fastify->>Fastify: validate redirect_uri, generate state & PKCE
    Fastify->>DB: store oauth_state (meta: redirectUri, codeVerifier)
    Fastify-->>NextJS: { redirectUrl }
    NextJS->>Google: Redirect user to Google (state, code_challenge)
    Google-->>Client: Consent and redirect with code & state
    Client->>NextJS: GET /auth/callback/oauth/google?code=...&state=...
    end

    rect rgba(150,220,130,0.5)
    Note over NextJS,Fastify: Exchange & session
    NextJS->>Fastify: POST /auth/oauth/google/exchange { code, state }
    Fastify->>DB: validate & consume oauth_state, read redirectUri & codeVerifier
    Fastify->>Google: POST token exchange (code, code_verifier, redirect_uri)
    Google-->>Fastify: tokens (access_token, refresh_token, id_token)
    Fastify->>Google: GET userinfo (Bearer access_token)
    Google-->>Fastify: userinfo (email, verified)
    Fastify->>DB: link/create user, store encrypted account tokens, create refresh session
    Fastify-->>NextJS: { token, refreshToken, redirectTo }
    NextJS-->>Client: Set auth cookies and redirect to redirectTo
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • feat: oauth and passkey login #127: Overlaps Google OAuth provider additions and endpoint changes (authorize/exchange/link) that intersect with this PR's redirect/PKCE and provider surface updates.
  • feat: account linking  #130: Related account-linking foundation and verification state handling extended here for Google redirect flow and oauth_state meta usage.
  • feat(auth): oauth #97: Touches verification table/schema changes relevant to the added verification.meta.redirectUri field.

Poem

🐰 I hopped a state, I hid a code,

I stored redirectUri along the road,
PKCE danced and tokens flew,
One Tap winked — redirect too,
A little rabbit cheers — auth cookies in view.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: adding support for multiple Google OAuth callback URLs to handle both web and mobile scenarios.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch google-auth

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/next/app/(dashboard)/settings/(profile)/linked-accounts-section.tsx (1)

141-147: ⚠️ Potential issue | 🟠 Major

Gate Google linking on backend availability, not the static allowlist.

Line 144 only checks whether providerId is in linkableProviders. With this PR, Google redirect linking has its own availability bit (googleRedirect) separate from google, so adding 'google' here makes the button clickable even when the backend can only answer OAUTH_NOT_CONFIGURED. Please drive Google’s enabled state from the providers payload instead of the hard-coded list.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/next/app/`(dashboard)/settings/(profile)/linked-accounts-section.tsx
around lines 141 - 147, The LinkProviderButton currently determines canLink
using the hard-coded linkableProviders array; instead, derive whether a provider
can be linked from the backend providers payload and pass that to useOAuthLink.
Update LinkProviderButton to consume the providers availability (e.g., a prop or
context containing the providers payload) and compute canLink using that payload
for providerId (use something like providers[providerId]?.enabled or
providers.includes(providerId)), then call useOAuthLink only when the backend
says the provider is available (and fall back safely for others). Refer to
linkableProviders, LinkProviderButton, providerId, and useOAuthLink when making
the change.
🧹 Nitpick comments (1)
packages/react/src/hooks/use-oauth-link.ts (1)

14-30: Consider simplifying the conditional logic for better readability.

The current implementation has Google appearing in both the special-case branch (with redirectUri) and the provider map (without redirectUri). While functionally correct, this could be refactored to reduce cognitive load.

♻️ Optional refactor for clarity
     mutationFn: async () => {
-      const data =
-        provider === 'google' && redirectUri
-          ? await client.auth.oauth.google.linkAuthorizeUrl({
-              query: {
-                // biome-ignore lint/style/useNamingConvention: OAuth API expects redirect_uri
-                redirect_uri: redirectUri,
-              },
-              throwOnError: true,
-            })
-          : await (
-              {
-                github: client.auth.oauth.github.linkAuthorizeUrl,
-                google: client.auth.oauth.google.linkAuthorizeUrl,
-                facebook: client.auth.oauth.facebook.linkAuthorizeUrl,
-                twitter: client.auth.oauth.twitter.linkAuthorizeUrl,
-              } as const
-            )[provider]({ throwOnError: true })
+      const providerMethods = {
+        github: client.auth.oauth.github.linkAuthorizeUrl,
+        google: client.auth.oauth.google.linkAuthorizeUrl,
+        facebook: client.auth.oauth.facebook.linkAuthorizeUrl,
+        twitter: client.auth.oauth.twitter.linkAuthorizeUrl,
+      } as const
+
+      const options =
+        provider === 'google' && redirectUri
+          ? {
+              query: {
+                // biome-ignore lint/style/useNamingConvention: OAuth API expects redirect_uri
+                redirect_uri: redirectUri,
+              },
+              throwOnError: true as const,
+            }
+          : { throwOnError: true as const }
+
+      const data = await providerMethods[provider](options)
       const url = data?.redirectUrl
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react/src/hooks/use-oauth-link.ts` around lines 14 - 30, The
conditional is confusing because google is handled twice; refactor
use-oauth-link by building a single args object (include redirect_uri only when
provider === 'google' and redirectUri is defined) and then call the appropriate
linkAuthorizeUrl from client.auth.oauth via a provider map; e.g., prepare const
args = { throwOnError: true, ...(provider === 'google' && redirectUri ? { query:
{ redirect_uri: redirectUri } } : {}) } and invoke ( { github:
client.auth.oauth.github.linkAuthorizeUrl, google:
client.auth.oauth.google.linkAuthorizeUrl, facebook:
client.auth.oauth.facebook.linkAuthorizeUrl, twitter:
client.auth.oauth.twitter.linkAuthorizeUrl } as const )[provider](args) so
linkAuthorizeUrl is called once for all providers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/fastify/src/routes/auth/oauth/google/authorize-url.ts`:
- Around line 62-81: Generate a per-request PKCE code_verifier, compute its S256
code_challenge, persist the code_verifier (or its hash) alongside
state/stateHash in the same verification record inserted via getDb().insert(...
) (use the existing verification/meta object to store code_challenge and/or
code_verifier hash and expiresAt), add
redirectUrl.searchParams.set('code_challenge', <challenge>) and
.set('code_challenge_method','S256') when building the Google authorize URL (and
mirror this change in link-authorize-url), and update
apps/fastify/src/routes/auth/oauth/google/exchange.ts to require and validate
the submitted code_verifier by computing/verifying the S256 match against the
stored code_challenge before minting a session.

In `@apps/fastify/src/routes/auth/oauth/google/exchange.ts`:
- Around line 213-224: When reusing an existing account row in exchange.ts
(inside the existingAccount branch), also persist the token expiry and any other
token metadata returned by encryptAccountTokens so the DB stays in sync with the
newly stored credentials; update the db.update(account).set(...) call (the same
block that sets accessToken and updatedAt) to also set accessTokenExpiresAt (and
any other fields returned by encryptAccountTokens, e.g., refreshToken or scope
if present) using values from the encrypted result or accountData.

In `@apps/fastify/src/routes/auth/oauth/google/link-authorize-url.ts`:
- Around line 49-58: The /link-authorize-url endpoint currently only checks for
GOOGLE_CLIENT_ID and callback URLs (googleClientId, allowedUrls, defaultUrl) but
not GOOGLE_CLIENT_SECRET, allowing a successful redirect that later fails in the
exchange route; update the gate in link-authorize-url.ts to use the same
configuration check as the exchange route by requiring env.GOOGLE_CLIENT_SECRET
(in addition to GOOGLE_CLIENT_ID and callback URL(s)) and return the same 503
OAUTH_NOT_CONFIGURED response when the secret is missing so users cannot
initiate an OAuth flow that will inevitably fail in the exchange handler.

In `@apps/next/app/auth/callback/oauth/google/route.ts`:
- Around line 50-56: translateOAuthError currently doesn't handle the new
TOKEN_EXCHANGE_FAILED code emitted by the Google exchange route, so
token-exchange failures fall back to the generic oauth_failed; update the
translateOAuthError function to explicitly detect the TOKEN_EXCHANGE_FAILED
condition (compare against the raw message and/or ApiError body) and return a
distinct error key such as "token_exchange_failed" (or your project's canonical
key) so the redirect in route.ts uses that specific message instead of
oauth_failed.

In `@apps/next/app/auth/login/login-actions.tsx`:
- Around line 194-205: The handleGoogleClick callback currently shows
oauth_not_configured when Google init is slow; change its guards so it does not
report "not configured" while GIS is still booting: first check if
!isGoogleReady and handle that case by returning early (e.g., disable the button
or show a loading/info toast) instead of falling through to the
configured-check, then only show the oauth_not_configured toast when
isGoogleReady is true and isGoogleRedirectConfigured is false; update the logic
in handleGoogleClick (which references oneTapSkipped, isGoogleReady,
isGoogleRedirectConfigured, startOAuthLogin, promptGoogle) accordingly.

In `@apps/next/app/auth/login/use-google-one-tap.ts`:
- Around line 199-206: The prompt useCallback currently only handles the case
where window.google.accounts.id exists or clientId is missing, so when clientId
is present but the Google script failed to load (loadError === true) it silently
no-ops; update the prompt function (the useCallback named prompt that references
handledRef and handleMoment) to check loadError first and, if true, call the
fallback (invoke onSkipped if provided) and/or show an error toast via
toast.error(getAuthErrorMessage('oauth_not_configured') or a more specific
message) so users are guided to the redirect fallback instead of nothing
happening.

In `@apps/next/lib/auth/auth-error-messages.ts`:
- Line 29: Remove the orphaned mapping entry for the exact key "Could not
retrieve email from Google" from the auth error mappings (the error map
constant/object that contains "'Could not retrieve verified email from Google':
'email_required'"); it is dead code because the backend only emits the
"verified" variant—delete the redundant line so only the correct "Could not
retrieve verified email from Google" -> "email_required" mapping remains.

---

Outside diff comments:
In `@apps/next/app/`(dashboard)/settings/(profile)/linked-accounts-section.tsx:
- Around line 141-147: The LinkProviderButton currently determines canLink using
the hard-coded linkableProviders array; instead, derive whether a provider can
be linked from the backend providers payload and pass that to useOAuthLink.
Update LinkProviderButton to consume the providers availability (e.g., a prop or
context containing the providers payload) and compute canLink using that payload
for providerId (use something like providers[providerId]?.enabled or
providers.includes(providerId)), then call useOAuthLink only when the backend
says the provider is available (and fall back safely for others). Refer to
linkableProviders, LinkProviderButton, providerId, and useOAuthLink when making
the change.

---

Nitpick comments:
In `@packages/react/src/hooks/use-oauth-link.ts`:
- Around line 14-30: The conditional is confusing because google is handled
twice; refactor use-oauth-link by building a single args object (include
redirect_uri only when provider === 'google' and redirectUri is defined) and
then call the appropriate linkAuthorizeUrl from client.auth.oauth via a provider
map; e.g., prepare const args = { throwOnError: true, ...(provider === 'google'
&& redirectUri ? { query: { redirect_uri: redirectUri } } : {}) } and invoke ( {
github: client.auth.oauth.github.linkAuthorizeUrl, google:
client.auth.oauth.google.linkAuthorizeUrl, facebook:
client.auth.oauth.facebook.linkAuthorizeUrl, twitter:
client.auth.oauth.twitter.linkAuthorizeUrl } as const )[provider](args) so
linkAuthorizeUrl is called once for all providers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bffdfdd9-b2e4-44be-aa0b-77989807e417

📥 Commits

Reviewing files that changed from the base of the PR and between f96b173 and 1ef006c.

⛔ Files ignored due to path filters (10)
  • apps/fastify/.env-sample is excluded by !**/.env*
  • apps/next/.env.development is excluded by !**/.env*
  • apps/next/.env.local.example is excluded by !**/.env*
  • apps/next/.env.production is excluded by !**/.env*
  • apps/next/.env.staging is excluded by !**/.env*
  • packages/core/src/api-client.gen.ts is excluded by !**/*.gen.ts
  • packages/core/src/api-wrapper.gen.ts is excluded by !**/*.gen.ts
  • packages/core/src/gen/index.ts is excluded by !**/gen/**, !**/gen/**
  • packages/core/src/gen/sdk.gen.ts is excluded by !**/gen/**, !**/gen/**, !**/*.gen.ts
  • packages/core/src/gen/types.gen.ts is excluded by !**/gen/**, !**/gen/**, !**/*.gen.ts
📒 Files selected for processing (23)
  • apps/docu/content/docs/architecture/account-linking.mdx
  • apps/docu/content/docs/architecture/authentication.mdx
  • apps/docu/content/docs/deployment/vercel.mdx
  • apps/fastify/eslint.config.mjs
  • apps/fastify/openapi/openapi.json
  • apps/fastify/src/db/schema/tables/verification.ts
  • apps/fastify/src/lib/env.ts
  • apps/fastify/src/routes/auth/oauth.spec.ts
  • apps/fastify/src/routes/auth/oauth/google/authorize-url.test.ts
  • apps/fastify/src/routes/auth/oauth/google/authorize-url.ts
  • apps/fastify/src/routes/auth/oauth/google/exchange.test.ts
  • apps/fastify/src/routes/auth/oauth/google/exchange.ts
  • apps/fastify/src/routes/auth/oauth/google/link-authorize-url.ts
  • apps/fastify/src/routes/auth/oauth/providers.test.ts
  • apps/fastify/src/routes/auth/oauth/providers.ts
  • apps/next/app/(dashboard)/settings/(profile)/linked-accounts-section.tsx
  • apps/next/app/auth/callback/oauth/google/route.ts
  • apps/next/app/auth/login/login-actions.tsx
  • apps/next/app/auth/login/use-google-one-tap.ts
  • apps/next/lib/auth/auth-error-messages.ts
  • packages/react/src/hooks/use-oauth-link.ts
  • packages/react/src/hooks/use-oauth-login.ts
  • packages/react/src/hooks/use-oauth-providers.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/fastify/eslint.config.mjs`:
- Around line 12-15: The ESLint rule 'max-lines' in the rules object should not
be loosened to 350; revert or change the 'max-lines' setting back to the repo
cap of 300 and do not create a blanket exemption for route files. Instead,
refactor the large Fastify route handler (see
src/routes/auth/oauth/google/exchange.ts) by extracting logical pieces of the
exchange flow into small helper functions/modules (e.g., token parsing, provider
calls, error handling, response building) so the route handler stays under 300
lines; keep the 'rules' entry as 'max-lines': ['error', { max: 300,
skipBlankLines: true, skipComments: true }] and move implementation changes into
helpers referenced from the route.

In `@apps/next/app/auth/login/login-actions.tsx`:
- Around line 194-205: Current logic blocks running Google One Tap when
isGoogleReady is true but isGoogleRedirectConfigured is false by showing
oauth_not_configured; update handleGoogleClick so that the redirect fallback
(useRedirect) only triggers startOAuthLogin('google') and returns, but do not
show the oauth_not_configured error or block promptGoogle() based on
isGoogleRedirectConfigured — i.e., remove the branch that calls
toast.error(getAuthErrorMessage('oauth_not_configured')) and instead: if
useRedirect then startOAuthLogin('google') and return; else if (!isGoogleReady)
return; else call promptGoogle(); keep references to oneTapSkipped,
isGoogleReady, isGoogleRedirectConfigured, startOAuthLogin, promptGoogle,
toast.error/getAuthErrorMessage for locating the code to change.

In `@apps/next/lib/auth/auth-error-messages.ts`:
- Around line 29-39: The Google-specific error strings are mapped to generic
codes that render GitHub-branded text; update the mappings in
auth-error-messages.ts so Google errors map to provider-specific codes (e.g.,
add keys like 'google_fetch_user_failed' and 'google_oauth_failed' instead of
reusing 'fetch_user_failed'/'oauth_failed'), and add corresponding entries in
authErrorMessages (or ensure authErrorMessages has provider-specific keys) so
Google callback errors render "Google ..." not "GitHub ..."; look for the
keys/values around 'Failed to fetch Google user', 'Google sign-in failed',
'fetch_user_failed', 'oauth_failed' and authErrorMessages.oauth_failed to apply
the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 260d03bc-9b84-43d1-a1a7-fc3a20bc6b1d

📥 Commits

Reviewing files that changed from the base of the PR and between 1ef006c and 183c177.

📒 Files selected for processing (9)
  • apps/fastify/eslint.config.mjs
  • apps/fastify/src/routes/auth/oauth/google/authorize-url.ts
  • apps/fastify/src/routes/auth/oauth/google/exchange.ts
  • apps/fastify/src/routes/auth/oauth/google/link-authorize-url.ts
  • apps/next/app/(dashboard)/settings/(profile)/linked-accounts-section.tsx
  • apps/next/app/auth/login/login-actions.tsx
  • apps/next/app/auth/login/use-google-one-tap.ts
  • apps/next/lib/auth/auth-error-messages.ts
  • packages/react/src/hooks/use-oauth-link.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/next/app/(dashboard)/settings/(profile)/linked-accounts-section.tsx

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
apps/fastify/src/lib/oauth-google.ts (1)

47-55: Throw Error instances here, not plain objects.

These object literals lose stack context and already trip the lint rule in this file. Wrap the provider payload in a typed Error subclass instead, then narrow on instanceof in the route handler.

Proposed refactor
+class GoogleOAuthError<T> extends Error {
+  constructor(
+    message: string,
+    readonly status: number,
+    readonly details: T,
+  ) {
+    super(message)
+    this.name = 'GoogleOAuthError'
+  }
+}
+
 export async function fetchGoogleTokens(input: {
   code: string
   codeVerifier: string
@@
   })
   const tokenData = (await tokenRes.json()) as GoogleTokenResponse
-  if (!tokenRes.ok) throw { status: tokenRes.status, tokenData }
-  if (tokenData.error || !tokenData.access_token) throw { status: 400, tokenData }
+  if (!tokenRes.ok)
+    throw new GoogleOAuthError('Google token exchange failed', tokenRes.status, tokenData)
+  if (tokenData.error || !tokenData.access_token)
+    throw new GoogleOAuthError('Google token exchange failed', 400, tokenData)
   return tokenData
 }
@@
   })
   const gUser = (await userRes.json()) as GoogleUser
-  if (!userRes.ok) throw { status: userRes.status, gUser }
-  if (!gUser?.id) throw { status: 400, gUser }
+  if (!userRes.ok)
+    throw new GoogleOAuthError('Google user info fetch failed', userRes.status, gUser)
+  if (!gUser?.id)
+    throw new GoogleOAuthError('Google user info payload missing id', 400, gUser)
   return gUser
 }

Also applies to: 60-67

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/fastify/src/lib/oauth-google.ts` around lines 47 - 55, Replace the plain
object throws in the token exchange logic with actual Error instances (or a
small typed Error subclass) so stack traces are preserved and can be narrowed
with instanceof; specifically change the throws after parsing tokenRes/tokenData
in oauth-google.ts (the token exchange surrounding tokenRes, tokenData and the
subsequent error checks) to throw a new Error subclass that wraps the provider
payload (status and tokenData) and use that class in the route handler to detect
provider errors; apply the same pattern to the second similar block (lines
~60-67) that currently throws plain objects.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/fastify/src/routes/auth/oauth/google/exchange.ts`:
- Around line 61-65: When consuming the OAuth state (use the value at
stateRecord.meta.redirectUri), re-validate that redirectUri against the current
allowlist (allowedUrls derived from env.OAUTH_GOOGLE_CALLBACK_URLS /
env.OAUTH_GOOGLE_CALLBACK_URL) instead of trusting the stored value; in the
exchange handler (where stateRecord is read) check that
stateRecord.meta.redirectUri is included in allowedUrls (or equals defaultUrl)
and abort the exchange with an error if not, applying the same guard at the
other consumption site noted (around the code near line 87) so stale or tampered
state cannot bypass the current callback whitelist.
- Around line 110-115: The code currently returns a hardcoded 400 for
token-exchange failures; instead read and use the upstream status from the error
object (the local variable e cast from the helper error) so transient/5xx and
rate-limit/429 responses are preserved; specifically set reply.code to e.status
when e.status is a 4xx/429, and normalize server errors (e.status >= 500) to 503
to allow retry behavior, and apply the same change to the other identical branch
that handles token exchange failures (the other branch around lines 129-133).

In `@apps/next/app/auth/login/login-actions.tsx`:
- Around line 29-33: The Google button is currently gated only by
isGoogleConfigured so deployments with googleRedirect=true but google=false
can’t start sign-in; update the OAuthButtons props and usage to accept a
googleRedirect (or googleRedirectEnabled) boolean alongside isGoogleConfigured,
pass the parent-level googleRedirect into OAuthButtons, and change the disable
condition for the Google button (in the UI and any prop checks around
handleGoogleClick / startOAuthLogin) to disable only when both
isGoogleConfigured === false AND googleRedirect === false; update all places
referencing OAuthButtons and the Google button logic (e.g., the
startOAuthLogin/onGoogleClick wiring and any isGoogleConfigured checks) to use
the new combined condition so redirect-only setups enable the button and still
call handleGoogleClick which performs the redirect.

In `@apps/next/lib/auth/auth-error-messages.ts`:
- Around line 55-58: translateOAuthError currently only partly rewrites
body.code for Google and can leave GitHub-branded keys in getAuthErrorMessage;
change the function so it first resolves the base error key (using
messageToKey[raw] or body.code) and then, if the provider is 'google', apply a
second mapping that overrides specific keys to Google-specific variants (e.g.,
map 'token_exchange_failed' -> 'google_token_exchange_failed', and ensure you
add 'google_email_required' to auth-error-messages.ts alongside
'email_required'); update translateOAuthError to reference messageToKey,
body.code, and provider so all Google overrides are applied in one place after
key resolution.

---

Nitpick comments:
In `@apps/fastify/src/lib/oauth-google.ts`:
- Around line 47-55: Replace the plain object throws in the token exchange logic
with actual Error instances (or a small typed Error subclass) so stack traces
are preserved and can be narrowed with instanceof; specifically change the
throws after parsing tokenRes/tokenData in oauth-google.ts (the token exchange
surrounding tokenRes, tokenData and the subsequent error checks) to throw a new
Error subclass that wraps the provider payload (status and tokenData) and use
that class in the route handler to detect provider errors; apply the same
pattern to the second similar block (lines ~60-67) that currently throws plain
objects.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ff8639c0-487c-4269-865d-e9e7d76c2321

📥 Commits

Reviewing files that changed from the base of the PR and between 183c177 and 5dade43.

📒 Files selected for processing (6)
  • apps/fastify/eslint.config.mjs
  • apps/fastify/src/lib/oauth-google.ts
  • apps/fastify/src/routes/auth/oauth/google/exchange.ts
  • apps/next/app/auth/callback/oauth/google/route.ts
  • apps/next/app/auth/login/login-actions.tsx
  • apps/next/lib/auth/auth-error-messages.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/next/app/auth/callback/oauth/google/route.ts

- 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
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
apps/fastify/src/routes/auth/oauth/google/exchange.ts (2)

88-88: ⚠️ Potential issue | 🟠 Major

Re-validate the consumed redirectUri against the current allowlist.

Line 88 still trusts stateRecord.meta?.redirectUri once the state is loaded. That means a stale or malformed state record can bypass the current OAUTH_GOOGLE_CALLBACK_URLS policy unless you re-check the final redirectUri against allowedUrls before exchanging the code.

As per coding guidelines, SECURITY: Ensure server-side validation (never trust client-side validation alone).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/fastify/src/routes/auth/oauth/google/exchange.ts` at line 88, The code
currently trusts stateRecord.meta?.redirectUri (assigned to redirectUri) without
re-checking it against the current allowlist (OAUTH_GOOGLE_CALLBACK_URLS /
allowedUrls); update the OAuth exchange flow to validate the resolved
redirectUri against allowedUrls before proceeding to exchange the code: after
computing redirectUri (from stateRecord.meta?.redirectUri ?? defaultUrl) call
the existing allowlist check (or implement a small helper like
isAllowedRedirect(redirectUri, allowedUrls)), and if it fails log and return an
error (or fall back to defaultUrl) and abort the token exchange; ensure you
reference redirectUri, stateRecord, defaultUrl and allowedUrls in the check so
stale/malformed state cannot bypass policy.

105-108: ⚠️ Potential issue | 🟠 Major

Don’t infer HTTP status from the helper message text.

Line 108 checks includes('timeout'), but buildTokenExchangeError() returns "Token exchange timed out", so token-exchange timeouts currently fall through as 400 instead of 504. These branches also collapse the structured upstream status into 400 for non-timeout failures. Carry an explicit status or timeout flag from the helper and map that here.

Also applies to: 116-118

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/fastify/src/routes/auth/oauth/google/exchange.ts` around lines 105 -
108, The code currently infers HTTP status from tokenErr.message text; change
the flow so buildTokenExchangeError(...) returns an explicit numeric status
(e.g., status) or an explicit timeout flag (e.g., isTimeout) on the tokenErr
object, and then use that field when setting the reply code in this handler
(replace the message.includes('timeout') logic with reply.code(tokenErr.status
?? (tokenErr.isTimeout ? 504 : 400))). Update both places that handle tokenErr
(the block around buildTokenExchangeError at lines ~105–108 and the similar
block around ~116–118) so structured upstream statuses and timeouts are
preserved instead of being inferred from the message. Ensure tokenErr is still
sent in the response body.
🧹 Nitpick comments (3)
apps/fastify/src/routes/reference/template-scripts.ts (2)

175-178: Add a brief comment explaining the intentional empty catch.

The empty catch block is a valid pattern for "try to parse, ignore if fails" but can be confusing to readers. A brief comment improves clarity.

💡 Suggested improvement
          try {
            const body = await res.json();
            msg = body.message || msg;
-          } catch {}
+          } catch { /* ignore parse errors, use default message */ }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/fastify/src/routes/reference/template-scripts.ts` around lines 175 -
178, The empty catch after attempting to parse the response JSON (the try around
await res.json() that sets msg = body.message || msg) is intentional to ignore
JSON parse errors; add a brief inline comment above the catch explaining this
intent (e.g., "ignore parse errors — fallback to default msg") so readers
understand why the catch is empty and no error handling is performed.

156-157: Consider validating that the code contains only digits.

The HTML pattern attribute (\\d*) provides some validation, but the JavaScript only checks length. Adding a digits-only check would improve UX by catching invalid input before the API call.

💡 Suggested improvement
      const code = input?.value?.trim();
-      if (!code || code.length !== 6) return;
+      if (!code || !/^\\d{6}$/.test(code)) return;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/fastify/src/routes/reference/template-scripts.ts` around lines 156 -
157, The current validation only checks length after trimming (const code =
input?.value?.trim(); if (!code || code.length !== 6) return;) — update this to
ensure the code is digits-only as well (e.g. test with a regex like /^\d{6}$/)
and return early if the test fails; locate the validation around the const code
= input?.value?.trim() and replace the length-only check with a
digits-plus-length check so non-numeric input is rejected before the API call.
apps/fastify/src/routes/reference/template.ts (1)

18-18: Consider pinning the Scalar CDN version for production stability.

Using @latest means the script can change without notice, potentially breaking functionality or introducing security issues. Consider pinning to a specific version (e.g., @1.x.x) for predictable behavior.

💡 Suggested change
-  <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference@latest/dist/browser/standalone.js"></script>
+  <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.25.0/dist/browser/standalone.js"></script>

Replace 1.25.0 with the actual version currently in use or the latest stable version.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/fastify/src/routes/reference/template.ts` at line 18, Replace the
dynamic CDN specifier in the script tag string
"@scalar/api-reference@latest/dist/browser/standalone.js" with a pinned version
(for example "@scalar/api-reference@1.25.0/dist/browser/standalone.js" or a
semver-stable spec like "@scalar/api-reference@1.x.x/...") so the template
served by the reference route uses a fixed package version; update the script
src in the template that contains that string and optionally add a comment or
TODO to document the chosen pinned version and process for updating it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/fastify/src/routes/reference/template-scripts.ts`:
- Around line 24-28: The current use of window.getComputedStyle(configureButton,
':hover') is invalid for pseudo-classes; change the approach so the Configure
button actively captures its hover color into a variable and the link uses that
value: add mouseenter/mouseleave listeners on configureButton that save its
computed backgroundColor (e.g., hoverBg =
getComputedStyle(configureButton).backgroundColor on
configureButton.onmouseenter and clear onmouseleave) and then update
link.onmouseenter to use that saved hoverBg (fallback to style.backgroundColor
if empty); keep link.onmouseleave setting link.style.backgroundColor back to
style.backgroundColor and reference the existing configureButton, link,
link.onmouseenter/leave handlers, and getComputedStyle call to locate where to
change.

---

Duplicate comments:
In `@apps/fastify/src/routes/auth/oauth/google/exchange.ts`:
- Line 88: The code currently trusts stateRecord.meta?.redirectUri (assigned to
redirectUri) without re-checking it against the current allowlist
(OAUTH_GOOGLE_CALLBACK_URLS / allowedUrls); update the OAuth exchange flow to
validate the resolved redirectUri against allowedUrls before proceeding to
exchange the code: after computing redirectUri (from
stateRecord.meta?.redirectUri ?? defaultUrl) call the existing allowlist check
(or implement a small helper like isAllowedRedirect(redirectUri, allowedUrls)),
and if it fails log and return an error (or fall back to defaultUrl) and abort
the token exchange; ensure you reference redirectUri, stateRecord, defaultUrl
and allowedUrls in the check so stale/malformed state cannot bypass policy.
- Around line 105-108: The code currently infers HTTP status from
tokenErr.message text; change the flow so buildTokenExchangeError(...) returns
an explicit numeric status (e.g., status) or an explicit timeout flag (e.g.,
isTimeout) on the tokenErr object, and then use that field when setting the
reply code in this handler (replace the message.includes('timeout') logic with
reply.code(tokenErr.status ?? (tokenErr.isTimeout ? 504 : 400))). Update both
places that handle tokenErr (the block around buildTokenExchangeError at lines
~105–108 and the similar block around ~116–118) so structured upstream statuses
and timeouts are preserved instead of being inferred from the message. Ensure
tokenErr is still sent in the response body.

---

Nitpick comments:
In `@apps/fastify/src/routes/reference/template-scripts.ts`:
- Around line 175-178: The empty catch after attempting to parse the response
JSON (the try around await res.json() that sets msg = body.message || msg) is
intentional to ignore JSON parse errors; add a brief inline comment above the
catch explaining this intent (e.g., "ignore parse errors — fallback to default
msg") so readers understand why the catch is empty and no error handling is
performed.
- Around line 156-157: The current validation only checks length after trimming
(const code = input?.value?.trim(); if (!code || code.length !== 6) return;) —
update this to ensure the code is digits-only as well (e.g. test with a regex
like /^\d{6}$/) and return early if the test fails; locate the validation around
the const code = input?.value?.trim() and replace the length-only check with a
digits-plus-length check so non-numeric input is rejected before the API call.

In `@apps/fastify/src/routes/reference/template.ts`:
- Line 18: Replace the dynamic CDN specifier in the script tag string
"@scalar/api-reference@latest/dist/browser/standalone.js" with a pinned version
(for example "@scalar/api-reference@1.25.0/dist/browser/standalone.js" or a
semver-stable spec like "@scalar/api-reference@1.x.x/...") so the template
served by the reference route uses a fixed package version; update the script
src in the template that contains that string and optionally add a comment or
TODO to document the chosen pinned version and process for updating it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9329cf90-ec57-48e4-b8cb-c8437d6e13ec

📥 Commits

Reviewing files that changed from the base of the PR and between 5dade43 and dfca768.

📒 Files selected for processing (9)
  • apps/fastify/eslint.config.mjs
  • apps/fastify/src/lib/oauth-google.ts
  • apps/fastify/src/routes/auth/oauth/google/exchange-helpers.ts
  • apps/fastify/src/routes/auth/oauth/google/exchange.ts
  • apps/fastify/src/routes/reference/template-scripts.ts
  • apps/fastify/src/routes/reference/template-styles.ts
  • apps/fastify/src/routes/reference/template.ts
  • apps/next/app/auth/callback/oauth/google/route.ts
  • apps/next/app/auth/login/login-actions.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/next/app/auth/callback/oauth/google/route.ts
  • apps/next/app/auth/login/login-actions.tsx

- 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
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
apps/next/lib/auth/auth-error-messages.ts (1)

82-87: ⚠️ Potential issue | 🟡 Minor

Add Google overrides for the remaining known base codes.

knownOAuthCodes still accepts fetch_user_failed and oauth_failed, but googleOverrides does not rewrite either one. Because body.code takes precedence over messageToKey[raw] at Line 102, those valid Google inputs still resolve to the GitHub-branded keys.

🔧 Suggested fix
 const googleOverrides: Record<string, string> = {
   token_exchange_failed: 'google_token_exchange_failed',
+  fetch_user_failed: 'google_fetch_user_failed',
   user_info_failed: 'google_fetch_user_failed',
   email_required: 'google_email_required',
+  oauth_failed: 'oauth_failed_google',
 }

Also applies to: 90-104

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/next/lib/auth/auth-error-messages.ts` around lines 82 - 87,
googleOverrides is missing mappings for the remaining base OAuth error keys
(e.g., fetch_user_failed and oauth_failed), so Google-sent body.code values fall
back to GitHub-branded keys via messageToKey/body.code precedence; update the
googleOverrides Record to include entries mapping "fetch_user_failed" ->
"google_fetch_user_failed" and "oauth_failed" -> "google_oauth_failed" (using
the exact base key strings used in knownOAuthCodes/messageToKey) so
Google-specific inputs resolve to the correct google_* keys.
apps/fastify/src/routes/auth/oauth/google/exchange.ts (1)

110-115: ⚠️ Potential issue | 🟠 Major

Preserve the upstream Google status in both catch blocks.

Line 113 and Line 123 still collapse every non-timeout upstream failure into 400. That makes Google 401/429/5xx responses look like permanent client errors and leaves the declared 503 path effectively unreachable.

Also applies to: 120-124

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/fastify/src/routes/auth/oauth/google/exchange.ts` around lines 110 -
115, The catch blocks currently map all non-timeout token exchange failures to
400; instead, when buildTokenExchangeError(err) returns an error carrying the
upstream Google HTTP status (e.g., err.status or err.upstreamStatus), pass that
status through to reply.code so 401/429/5xx are preserved; otherwise fall back
to the existing timeout->504 / default->400 logic. Update both catch blocks that
call buildTokenExchangeError and reply.code to compute status = tokenErr.status
|| tokenErr.upstreamStatus || (tokenErr.message.includes('timeout') ? 504 : 400)
and use reply.code(status).send(tokenErr).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/fastify/src/routes/auth/oauth/google/exchange.ts`:
- Around line 87-95: When stateRecord.meta indicates an OAuth link flow
(isLinkMode === true) but linkUserId is missing, explicitly fail closed instead
of falling through: in the handler that reads const { isLinkMode, linkUserId,
stateRecord } add a guard that when isLinkMode is truthy and linkUserId is falsy
you immediately reply with a 401/INVALID_STATE error (same structure used for
other invalid-state responses) to prevent the request from continuing to the
normal sign-in/create branches; apply the same guard logic to the related branch
handling around the sign-in/create flow so oauth_link_state without meta.userId
is always rejected.
- Around line 182-205: When updating an existingAccount, don’t overwrite the
stored refresh token with null when Google omits refresh_token; instead preserve
the DB value. Keep building accountData from tokenData but change the update
logic (the db.update(account).set({...}) block) so refreshToken is only set to
the newly encrypted refresh token when tokenData.refresh_token exists; otherwise
leave the column as account.refreshToken (the existingAccount DB value). Use the
result of encryptAccountTokens for the new value when present: set refreshToken:
tokenData.refresh_token ? encrypted.refreshToken ?? null : account.refreshToken.

---

Duplicate comments:
In `@apps/fastify/src/routes/auth/oauth/google/exchange.ts`:
- Around line 110-115: The catch blocks currently map all non-timeout token
exchange failures to 400; instead, when buildTokenExchangeError(err) returns an
error carrying the upstream Google HTTP status (e.g., err.status or
err.upstreamStatus), pass that status through to reply.code so 401/429/5xx are
preserved; otherwise fall back to the existing timeout->504 / default->400
logic. Update both catch blocks that call buildTokenExchangeError and reply.code
to compute status = tokenErr.status || tokenErr.upstreamStatus ||
(tokenErr.message.includes('timeout') ? 504 : 400) and use
reply.code(status).send(tokenErr).

In `@apps/next/lib/auth/auth-error-messages.ts`:
- Around line 82-87: googleOverrides is missing mappings for the remaining base
OAuth error keys (e.g., fetch_user_failed and oauth_failed), so Google-sent
body.code values fall back to GitHub-branded keys via messageToKey/body.code
precedence; update the googleOverrides Record to include entries mapping
"fetch_user_failed" -> "google_fetch_user_failed" and "oauth_failed" ->
"google_oauth_failed" (using the exact base key strings used in
knownOAuthCodes/messageToKey) so Google-specific inputs resolve to the correct
google_* keys.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 720776ca-9412-4999-ace5-c3f40e18b45a

📥 Commits

Reviewing files that changed from the base of the PR and between dfca768 and 386c13b.

📒 Files selected for processing (2)
  • apps/fastify/src/routes/auth/oauth/google/exchange.ts
  • apps/next/lib/auth/auth-error-messages.ts

- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant