Skip to content

Add GitHub, Google, and X social sign-in - #677

Closed
kentcdodds wants to merge 8 commits into
mainfrom
cursor/social-auth-github-x-google-746b
Closed

kentcdodds wants to merge 8 commits into
mainfrom
cursor/social-auth-github-x-google-746b

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Jul 8, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds optional OAuth sign-in with GitHub, Google, and X on /login and /signup, following Epic Stack’s connection model and Remix 3’s built-in remix/auth providers.

  • New auth_connections table links provider_name + provider_id → users.id
  • Routes: GET /auth/{github,google,x} and GET /auth/{provider}/callback
  • OAuth state stored in signed kody_oauth_transaction cookie (WebAuthn-style)
  • Existing connections sign in; verified emails auto-link (GitHub profile email, Google email_verified); new users get OAuth-only accounts (sentinel password hash)
  • Production new-user OAuth signup still respects invite gating (inviteCode query param)
  • SOCIAL_AUTH_MOCK=1 (or test env) installs mock OAuth fetch at worker startup for local dev and unit tests

Review feedback addressed (5342a59b)

  • Removed redundant (provider_name, provider_id) index (UNIQUE already indexes it)
  • Trust GitHub profile emails for auto-link; keep Google email_verified gating; X stays synthetic
  • Clear kody_oauth_transaction cookie on OAuth callback failures
  • Fetch /auth/providers.json so social buttons persist across SPA login ↔ signup toggle
  • Refactored provider factory + handler validation; dropped redundant post-resolution user DB lookup
  • Threaded OAuth result types through finishSocialAuth; added regression tests

Provider app setup

Register OAuth apps on each provider using your deployment origin (production: https://heykody.dev, or your preview URL). Callback paths:

Provider Callback URL
GitHub https://<your-domain>/auth/github/callback
Google https://<your-domain>/auth/google/callback
X https://<your-domain>/auth/x/callback

GitHub

  1. GitHub → Settings → Developer settings → OAuth Apps → New OAuth App
  2. Authorization callback URL: https://<your-domain>/auth/github/callback
  3. Copy Client ID and generate a Client secret

Google

  1. Google Cloud Console → APIs & Services → Credentials
  2. Create OAuth client ID → type Web application
  3. Authorized redirect URIs: https://<your-domain>/auth/google/callback
  4. Configure OAuth consent screen (external or internal) with email, profile, openid scopes
  5. Copy Client ID and Client secret

X (Twitter)

  1. X Developer Portal → your app → User authentication settings
  2. Enable OAuth 2.0 with type Web App
  3. Callback URL: https://<your-domain>/auth/x/callback
  4. Scopes: at minimum users.read (default in code)
  5. Copy Client ID and Client secret

Note: X does not return an email by default. X-only new accounts use a synthetic internal email until the user adds a real address on /account.

GitHub Actions secrets

Add these repository secrets (Settings → Secrets and variables → Actions). All are optional — omit a pair to hide that provider’s button.

Secret Value
GITHUB_CLIENT_ID GitHub OAuth App client ID
GITHUB_CLIENT_SECRET GitHub OAuth App client secret
GOOGLE_CLIENT_ID Google OAuth client ID
GOOGLE_CLIENT_SECRET Google OAuth client secret
X_CLIENT_ID X OAuth 2.0 client ID
X_CLIENT_SECRET X OAuth 2.0 client secret

Production (.github/workflows/deploy.yml) and preview (.github/workflows/preview.yml) already sync these via tools/ci/sync-worker-secrets.ts when the secrets exist.

Local development

Add to packages/worker/.env (see .env.example):

GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...
# etc.

Or use mock mode without real apps:

SOCIAL_AUTH_MOCK=1

Testing

  • social-auth.node.test.ts — OAuth start redirect + transaction cookie (mock fetch)
  • resolve-social-auth.node.test.ts — connection lookup, GitHub/Google email auto-link, unverified Google skip, invite gate

Run: npx nx run worker:test -- social-auth

Migration

Apply packages/worker/migrations/0056-auth-connections.sql (included in normal D1 migrate on deploy).

System recap — extends existing primitives (medium risk)

Mode: recap · Base: main @ 8367cec · Head: 5342a59b

Classification: extends — adds OAuth social sign-in paths and auth_connections storage on top of existing browser session auth.

Primitives touched

Primitive Group Impact
app-sessions auth extends — GitHub/Google/X OAuth sign-in, connection linking, login UI
d1-app-db storage extends — auth_connections table + account deletion cascade

System map

flowchart LR
  Login["/login UI"] --> Start["GET /auth/:provider"]
  Start --> Cookie["kody_oauth_transaction cookie"]
  Start --> IdP["GitHub / Google / X"]
  IdP --> Callback["GET /auth/:provider/callback"]
  Callback --> Resolve["resolveSocialAuthUser"]
  Resolve --> D1["auth_connections + users"]
  Resolve --> Session["kody_session cookie"]
Loading

Invariants

Per-user isolation preserved: auth_connections rows are deleted via account-data-targets using db_user_id cascade on account deletion.

Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features
    • Added social sign-in for GitHub, Google, and X, including new login routes and a providers list for the UI.
    • Added OAuth flows with optional invite codes and support for 2FA-capable accounts.
    • Stores and links external authentication connections via a new database table.
  • Bug Fixes
    • Improved OAuth state/cookie handling and error redirects.
    • OAuth-only accounts now skip password requirements for email changes and account deletion.
  • Documentation
    • Updated contributor architecture and social sign-in environment-variable docs.
    • Updated worker .env example for the new social sign-in settings.
  • Chores
    • Updated deployment/preview secret syncing to include social provider credentials.

Implement Epic Stack-style auth_connections linking, remix/auth OAuth
providers, signed OAuth transaction cookies, login UI buttons, mock
provider HTTP for tests, and deploy workflow secret sync hooks.
@kentcdodds
kentcdodds marked this pull request as ready for review July 8, 2026 14:16
@coderabbitai

coderabbitai Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cursor[bot], you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bbf6f534-16ef-45f0-a510-1aae361a3001

📥 Commits

Reviewing files that changed from the base of the PR and between 6503647 and 6121aaa.

📒 Files selected for processing (3)
  • packages/worker/client/routes/login.tsx
  • packages/worker/src/app/resolve-social-auth.node.test.ts
  • packages/worker/src/app/resolve-social-auth.ts
📝 Walkthrough

Walkthrough

This PR adds social sign-in for GitHub, Google, and X, and updates account flows to distinguish OAuth-only accounts from accounts with usable passwords. It also adds the supporting routes, storage, docs, tests, and deploy/preview secret synchronization.

Changes

Authentication feature updates

Layer / File(s) Summary
Provider config and auth primitives
packages/worker/src/app/social-auth-provider-names.ts, packages/worker/src/app/social-auth-providers.ts, packages/worker/src/app/social-auth-provider-factory.ts, packages/worker/src/app/social-auth-mock.ts, packages/worker/src/app/oauth-transaction.ts, packages/worker/src/db.ts, packages/worker/migrations/0056-auth-connections.sql, packages/worker/src/env-schema.ts, packages/worker/src/app/account-data-targets.ts, packages/worker/tsconfig-client.json
Defines social provider names, configured-provider lookup, provider construction, OAuth transaction cookies, the auth_connections table, related env bindings, account-data coverage, and client tsconfig inclusion.
OAuth flow and user resolution
packages/worker/src/app/social-auth-flow.ts, packages/worker/src/app/resolve-social-auth.ts, packages/worker/src/app/resolve-social-auth.node.test.ts, packages/worker/src/app/handlers/social-auth.node.test.ts
Implements the OAuth start/finish flow, account linking and creation logic, and tests for provider resolution and start-handler behavior.
Routes, handlers, and SSR data
packages/worker/src/app/handlers/social-auth.ts, packages/worker/src/app/routes.ts, packages/worker/src/app/router.ts, packages/worker/src/app/handlers/auth-page.ts, packages/worker/src/app/loader-data.ts, packages/worker/src/app/handler.ts, packages/worker/src/app/handlers/social-auth.node.test.ts
Adds social-auth handlers and routes, passes provider data into SSR auth pages, and installs the mock fetch during worker initialization.
Account password usability
packages/shared/src/password-hash.ts, packages/shared/src/password-hash.node.test.ts, packages/worker/src/app/account-profile-data.ts, packages/worker/src/app/handlers/account-profile.ts, packages/worker/src/app/handlers/account.ts, packages/worker/src/app/handlers/account-delete.ts, packages/worker/src/app/handlers/account-email-change.ts, packages/worker/src/app/account-data-targets.ts, packages/worker/client/routes/account.tsx, packages/worker/client/navigation-data.node.test.ts, packages/worker/client/loader-data-context.node.test.ts, packages/worker/src/app/handlers/account-profile.node.test.ts, packages/worker/src/app/handlers/account-email-change.node.test.ts, packages/worker/src/app/ssr-render.node.test.ts
Adds usable-password detection, threads hasUsablePassword through account profile data, and updates account email-change/delete behavior and client rendering for OAuth-only accounts.
Login UI, docs, and workflow sync
packages/worker/client/routes/login.tsx, docs/contributing/architecture/authentication.md, docs/contributing/architecture/primitives.yaml, docs/contributing/environment-variables.md, packages/worker/.env.example, .github/workflows/deploy.yml, .github/workflows/preview.yml
Renders provider login links and query errors, documents social sign-in configuration, and syncs the new OAuth secrets in deploy and preview workflows.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant SocialAuthStartHandler
  participant SocialAuthCallbackHandler
  participant ResolveSocialAuthUser
  participant DB
  Browser->>SocialAuthStartHandler: GET /auth/:provider
  SocialAuthStartHandler-->>Browser: redirect + OAuth transaction cookie
  Browser->>SocialAuthCallbackHandler: GET /auth/:provider/callback
  SocialAuthCallbackHandler->>ResolveSocialAuthUser: resolveSocialAuthUser(profile)
  ResolveSocialAuthUser->>DB: lookup or create user and auth_connections
  DB-->>ResolveSocialAuthUser: resolution result
  ResolveSocialAuthUser-->>SocialAuthCallbackHandler: resolved user
  SocialAuthCallbackHandler-->>Browser: session redirect
Loading

Possibly related PRs

  • kentcdodds/kody#49: Both PRs modify .github/workflows/deploy.yml secret-sync wiring for tools/ci/sync-worker-secrets.ts.
  • kentcdodds/kody#608: Both PRs touch route-loader consumption plumbing used by packages/worker/client/routes/login.tsx.
  • kentcdodds/kody#645: Both PRs modify the account email-change flow in packages/worker/client/routes/account.tsx and related handlers.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding social sign-in for GitHub, Google, and X.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/social-auth-github-x-google-746b

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.

@github-actions

github-actions Bot commented Jul 8, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-677.kody-a99.workers.dev

Worker: kody-pr-677
D1: kody-pr-677-db
KV: kody-pr-677-oauth-kv

Mocks:

Comment thread packages/worker/src/app/resolve-social-auth.ts
Comment thread packages/worker/src/app/resolve-social-auth.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (5)
packages/worker/src/app/social-auth-provider-factory.ts (1)

38-97: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce duplication between real and mock provider creation paths.

createConfiguredSocialAuthProvider (lines 70–97) duplicates the provider-switching logic already in the main function (lines 38–67). Both branches construct the same create{Provider}AuthProvider({ clientId, clientSecret, redirectUri }) call per provider. Consolidating into a single helper that accepts credentials would eliminate ~30 lines of duplicated branching.

♻️ Proposed refactor: single helper for both paths
 export function createSocialAuthProvider(
 	env: Env,
 	provider: SocialAuthProviderName,
 	requestUrl: string | URL,
 ): AnySocialAuthProvider | null {
 	const origin = new URL(requestUrl).origin
 	const redirectUri = new URL(
 		`${getSocialAuthStartPath(provider)}/callback`,
 		origin,
 	)

+	let clientId: string | undefined
+	let clientSecret: string | undefined
+
 	if (isSocialAuthMockEnabled(env)) {
-		return createConfiguredSocialAuthProvider({
-			provider,
-			clientId: mockClientId,
-			clientSecret: mockClientSecret,
-			redirectUri,
-		})
+		clientId = mockClientId
+		clientSecret = mockClientSecret
+	} else {
+		clientId = readProviderCredentials(env, provider)?.clientId
+		clientSecret = readProviderCredentials(env, provider)?.clientSecret
 	}

-	if (provider === 'github') {
-		const clientId = env.GITHUB_CLIENT_ID?.trim()
-		const clientSecret = env.GITHUB_CLIENT_SECRET?.trim()
-		if (!clientId || !clientSecret) return null
-		return createGitHubAuthProvider({
-			clientId,
-			clientSecret,
-			redirectUri,
-		}) as AnySocialAuthProvider
-	}
-
-	if (provider === 'google') {
-		const clientId = env.GOOGLE_CLIENT_ID?.trim()
-		const clientSecret = env.GOOGLE_CLIENT_SECRET?.trim()
-		if (!clientId || !clientSecret) return null
-		return createGoogleAuthProvider({
-			clientId,
-			clientSecret,
-			redirectUri,
-		}) as AnySocialAuthProvider
-	}
-
-	const clientId = env.X_CLIENT_ID?.trim()
-	const clientSecret = env.X_CLIENT_SECRET?.trim()
 	if (!clientId || !clientSecret) return null
-	return createXAuthProvider({
-		clientId,
-		clientSecret,
-		redirectUri,
-	}) as AnySocialAuthProvider
+	return createConfiguredSocialAuthProvider({ provider, clientId, clientSecret, redirectUri })
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/social-auth-provider-factory.ts` around lines 38 -
97, The provider selection logic is duplicated between the main factory path and
createConfiguredSocialAuthProvider, with the same github/google/X branching and
create{Provider}AuthProvider calls repeated in both places. Refactor this by
extracting a single helper that takes provider, clientId, clientSecret, and
redirectUri, and have both the env-based path and
createConfiguredSocialAuthProvider delegate to it. Keep the existing
provider-specific constructors (createGitHubAuthProvider,
createGoogleAuthProvider, createXAuthProvider) but centralize the switch so
there is only one branching implementation.
packages/worker/src/app/resolve-social-auth.node.test.ts (1)

165-245: 🔒 Security & Privacy | 🔵 Trivial | 🏗️ Heavy lift

Add coverage for unverified-email and new-user signup paths.

The current tests only cover existing linked/email-match flows. Please add regression cases for unverified provider email not auto-linking, invite-required new OAuth signup, and successful new-user connection creation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/resolve-social-auth.node.test.ts` around lines 165 -
245, The current resolveSocialAuthUser tests only cover linked-account and
verified email-match paths, so add regression coverage for the missing signup
branches. In resolve-social-auth.node.test.ts, extend the resolveSocialAuthUser
suite with cases for an unverified provider email that must not auto-link to an
existing user, an invite-required OAuth signup that should be blocked when no
invite is present, and a successful new-user connection creation flow. Use the
existing helpers like createResolveSocialAuthTestEnv and assert on the returned
userId/isNewUser plus any new connection state to verify the behavior of
resolveSocialAuthUser.
packages/worker/src/app/loader-data.ts (1)

434-439: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use SocialAuthProviderName instead of duplicating the literal union.

The id field uses 'github' | 'google' | 'x' as a literal union, which duplicates SocialAuthProviderName from social-auth-providers.ts. If a new provider is added to SocialAuthProviderName, this type won't catch the mismatch.

♻️ Suggested fix: import and reference the shared type
+import { type SocialAuthProviderName } from '`#app/social-auth-providers.ts`'
+
 export type LoginAuthLoaderData = {
 	providers: Array<{
-		id: 'github' | 'google' | 'x'
+		id: SocialAuthProviderName
 		label: string
 		startPath: string
 	}>
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/loader-data.ts` around lines 434 - 439, The
LoginAuthLoaderData type duplicates the provider name union instead of reusing
the shared SocialAuthProviderName. Update the provider id field in
LoginAuthLoaderData to reference SocialAuthProviderName directly, and add the
needed import from social-auth-providers.ts so the loader data stays aligned
with the central provider सूची when new providers are added.
packages/worker/src/app/handlers/social-auth.ts (1)

127-178: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared provider validation to reduce duplication.

The start handler (lines 137-159) and callback handler (lines 191-213) contain identical logic: isSocialAuthProviderName check, isSocialAuthProviderConfigured check, createSocialAuthProvider call, and error redirect on failure. Extracting a helper would eliminate ~20 lines of copy-paste and prevent divergence when validation rules change.

♻️ Suggested extraction
+function resolveSocialAuthProvider(
+	env: Env,
+	providerName: string,
+	request: Request,
+) {
+	if (!isSocialAuthProviderName(providerName)) {
+		return { error: new Response('Not found', { status: 404 }) } as const
+	}
+	if (!isSocialAuthProviderConfigured(env, providerName)) {
+		return { error: buildLoginErrorRedirect(request, `${providerName} sign-in is not configured.`) } as const
+	}
+	const authProvider = createSocialAuthProvider(env, providerName, request.url)
+	if (!authProvider) {
+		return { error: buildLoginErrorRedirect(request, `${providerName} sign-in is not configured.`) } as const
+	}
+	return { authProvider, providerName } as const
+}

Then in both handlers:

-		const providerName = params.provider
-		if (!isSocialAuthProviderName(providerName)) {
-			return new Response('Not found', { status: 404 })
-		}
-		if (!isSocialAuthProviderConfigured(env, providerName)) {
-			return buildLoginErrorRedirect(request, `${providerName} sign-in is not configured.`)
-		}
-		const authProvider = createSocialAuthProvider(env, providerName, request.url)
-		if (!authProvider) {
-			return buildLoginErrorRedirect(request, `${providerName} sign-in is not configured.`)
-		}
+		const resolved = resolveSocialAuthProvider(env, params.provider, request)
+		if ('error' in resolved) return resolved.error
+		const { authProvider, providerName } = resolved

Also applies to: 181-268

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/handlers/social-auth.ts` around lines 127 - 178, The
start and callback handlers duplicate the same provider validation and provider
creation flow, so extract that shared logic into a helper used by
createSocialAuthStartHandler and the callback handler path. Move the repeated
isSocialAuthProviderName, isSocialAuthProviderConfigured,
createSocialAuthProvider, and failure redirect behavior into a single reusable
function, then have both handlers call it and only keep their handler-specific
start/callback behavior separate.
packages/worker/src/app/handlers/social-auth.node.test.ts (1)

93-99: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add assertions for state and codeVerifier in the OAuth transaction cookie.

The test verifies provider and returnTo but does not assert that state (CSRF protection) and codeVerifier (PKCE) are present and non-empty. These are security-critical fields — a regression that omits them could go undetected.

🛡️ Proposed additions
 	expect(transaction?.provider).toBe('github')
 	expect(transaction?.returnTo).toBe('/account')
+	expect(transaction?.state).toBeTruthy()
+	expect(transaction?.codeVerifier).toBeTruthy()
 })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/handlers/social-auth.node.test.ts` around lines 93 -
99, The social auth test reads the OAuth transaction via readOAuthTransaction
but only checks provider and returnTo; update this test to also assert that the
returned transaction includes non-empty state and codeVerifier fields. Use the
existing transaction variable in social-auth.node.test.ts alongside the current
provider and returnTo expectations so regressions that drop CSRF/PKCE data are
caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/worker/migrations/0056-auth-connections.sql`:
- Around line 13-14: Remove the redundant auth_connections provider index: the
UNIQUE constraint on (provider_name, provider_id) in the migration already
creates the needed SQLite index, so delete the explicit CREATE INDEX statement
in the migration and keep the UNIQUE constraint as the single source of indexing
for those columns.

In `@packages/worker/src/app/handlers/social-auth.ts`:
- Around line 54-56: The user-not-found path in issueSessionForSocialAuth
currently redirects without audit logging, creating an observability gap. Update
the userRecord check in social-auth.ts to call logAuditEvent before
buildLoginErrorRedirect, using the same audit patterns already used in the
callback handler for failed social auth flows. Keep the existing redirect
behavior, but ensure the “user missing after resolution” case is recorded with
enough context to identify the social auth failure.
- Around line 40-56: `issueSessionForSocialAuth` is doing an unnecessary
`createDb(...).findOne(usersTable, ...)` lookup even though `input.userId` and
`input.email` are already available from `resolveSocialAuthUser`. Update the
session issuance flow in `issueSessionForSocialAuth` to use the `input` fields
directly for cookie/session creation, and only keep a minimal guard if you still
want to handle a deleted-user edge case. Remove the stale `userRecord`
dependency so `userId` and `email` come from the function arguments instead of a
second database roundtrip.
- Around line 224-228: The social-auth handler is casting around a real type
mismatch between finishSocialAuth and resolveSocialAuthUser. Update the typing
in social-auth.ts so the concrete social-auth result type is threaded through
finishSocialAuth and the call site that builds the object passed into
resolveSocialAuthUser, instead of using a Parameters<typeof
resolveSocialAuthUser>[0]['result'] cast. Make the return and argument types
align with OAuthResult<SocialAuthProfile, SocialAuthProviderName> so the result
value is inferred correctly without assertions.

In `@packages/worker/src/app/resolve-social-auth.ts`:
- Around line 192-201: The OAuth email handling in resolve-social-auth should
only trust provider emails when isProviderEmailVerified reports true. Update the
email selection in the flow around readProfileEmail, syntheticEmailForProvider,
and getAvailableUsername so unverified provider emails do not drive account
linking, and use a synthetic email for new users when verification is absent.
Also guard the auto-linking path in the section that links by profileEmail so it
only matches existing accounts on verified email claims, leaving unverified
claims unlinked.

---

Nitpick comments:
In `@packages/worker/src/app/handlers/social-auth.node.test.ts`:
- Around line 93-99: The social auth test reads the OAuth transaction via
readOAuthTransaction but only checks provider and returnTo; update this test to
also assert that the returned transaction includes non-empty state and
codeVerifier fields. Use the existing transaction variable in
social-auth.node.test.ts alongside the current provider and returnTo
expectations so regressions that drop CSRF/PKCE data are caught.

In `@packages/worker/src/app/handlers/social-auth.ts`:
- Around line 127-178: The start and callback handlers duplicate the same
provider validation and provider creation flow, so extract that shared logic
into a helper used by createSocialAuthStartHandler and the callback handler
path. Move the repeated isSocialAuthProviderName,
isSocialAuthProviderConfigured, createSocialAuthProvider, and failure redirect
behavior into a single reusable function, then have both handlers call it and
only keep their handler-specific start/callback behavior separate.

In `@packages/worker/src/app/loader-data.ts`:
- Around line 434-439: The LoginAuthLoaderData type duplicates the provider name
union instead of reusing the shared SocialAuthProviderName. Update the provider
id field in LoginAuthLoaderData to reference SocialAuthProviderName directly,
and add the needed import from social-auth-providers.ts so the loader data stays
aligned with the central provider सूची when new providers are added.

In `@packages/worker/src/app/resolve-social-auth.node.test.ts`:
- Around line 165-245: The current resolveSocialAuthUser tests only cover
linked-account and verified email-match paths, so add regression coverage for
the missing signup branches. In resolve-social-auth.node.test.ts, extend the
resolveSocialAuthUser suite with cases for an unverified provider email that
must not auto-link to an existing user, an invite-required OAuth signup that
should be blocked when no invite is present, and a successful new-user
connection creation flow. Use the existing helpers like
createResolveSocialAuthTestEnv and assert on the returned userId/isNewUser plus
any new connection state to verify the behavior of resolveSocialAuthUser.

In `@packages/worker/src/app/social-auth-provider-factory.ts`:
- Around line 38-97: The provider selection logic is duplicated between the main
factory path and createConfiguredSocialAuthProvider, with the same
github/google/X branching and create{Provider}AuthProvider calls repeated in
both places. Refactor this by extracting a single helper that takes provider,
clientId, clientSecret, and redirectUri, and have both the env-based path and
createConfiguredSocialAuthProvider delegate to it. Keep the existing
provider-specific constructors (createGitHubAuthProvider,
createGoogleAuthProvider, createXAuthProvider) but centralize the switch so
there is only one branching implementation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4ae97613-ce60-4af8-9167-251c1d94f19d

📥 Commits

Reviewing files that changed from the base of the PR and between 8367cec and 3e6e95b.

📒 Files selected for processing (24)
  • .github/workflows/deploy.yml
  • .github/workflows/preview.yml
  • docs/contributing/architecture/authentication.md
  • docs/contributing/architecture/primitives.yaml
  • docs/contributing/environment-variables.md
  • packages/worker/.env.example
  • packages/worker/client/routes/login.tsx
  • packages/worker/migrations/0056-auth-connections.sql
  • packages/worker/src/app/account-data-targets.ts
  • packages/worker/src/app/handlers/auth-page.ts
  • packages/worker/src/app/handlers/social-auth.node.test.ts
  • packages/worker/src/app/handlers/social-auth.ts
  • packages/worker/src/app/loader-data.ts
  • packages/worker/src/app/oauth-transaction.ts
  • packages/worker/src/app/resolve-social-auth.node.test.ts
  • packages/worker/src/app/resolve-social-auth.ts
  • packages/worker/src/app/router.ts
  • packages/worker/src/app/routes.ts
  • packages/worker/src/app/social-auth-flow.ts
  • packages/worker/src/app/social-auth-mock.ts
  • packages/worker/src/app/social-auth-provider-factory.ts
  • packages/worker/src/app/social-auth-providers.ts
  • packages/worker/src/db.ts
  • packages/worker/src/env-schema.ts

Comment thread packages/worker/migrations/0056-auth-connections.sql Outdated
Comment thread packages/worker/src/app/handlers/social-auth.ts Outdated
Comment on lines +54 to +56
if (!userRecord) {
return buildLoginErrorRedirect(input.request, 'Unable to sign in.')
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Missing audit log for user-not-found edge case.

The callback handler logs failures via logAuditEvent (lines 242-249, 254-261), but issueSessionForSocialAuth silently redirects on user-not-found without logging. This creates an observability gap for a rare but security-relevant scenario (user deleted between resolution and session issuance).

🛡️ Suggested fix: add audit log before redirect
 	if (!userRecord) {
+		void logAuditEvent({
+			category: 'auth',
+			action: 'social_login',
+			result: 'failure',
+			email: input.email,
+			reason: 'user_not_found_after_resolution',
+		})
 		return buildLoginErrorRedirect(input.request, 'Unable to sign in.')
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!userRecord) {
return buildLoginErrorRedirect(input.request, 'Unable to sign in.')
}
if (!userRecord) {
void logAuditEvent({
category: 'auth',
action: 'social_login',
result: 'failure',
email: input.email,
reason: 'user_not_found_after_resolution',
})
return buildLoginErrorRedirect(input.request, 'Unable to sign in.')
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/handlers/social-auth.ts` around lines 54 - 56, The
user-not-found path in issueSessionForSocialAuth currently redirects without
audit logging, creating an observability gap. Update the userRecord check in
social-auth.ts to call logAuditEvent before buildLoginErrorRedirect, using the
same audit patterns already used in the callback handler for failed social auth
flows. Keep the existing redirect behavior, but ensure the “user missing after
resolution” case is recorded with enough context to identify the social auth
failure.

Comment thread packages/worker/src/app/handlers/social-auth.ts Outdated
Comment thread packages/worker/src/app/resolve-social-auth.ts
@cursor
cursor Bot temporarily deployed to preview-677 July 8, 2026 14:35 Inactive
Comment thread packages/worker/src/app/social-auth-mock.ts
Comment thread packages/worker/client/routes/login.tsx Outdated
Only Google OIDC email_verified claims can auto-link to an existing
account or mark a new signup verified. GitHub and X no longer trust
unverified IdP email for account linking.
@cursor
cursor Bot temporarily deployed to preview-677 July 8, 2026 14:47 Inactive
Comment thread packages/worker/src/app/resolve-social-auth.ts
Comment thread packages/worker/src/app/handlers/social-auth.ts Outdated
Comment thread packages/worker/src/app/social-auth-providers.ts
- Trust GitHub profile emails for auto-link while keeping Google
  email_verified gating and X synthetic addresses
- Remove redundant auth_connections provider index from migration
- Clear OAuth transaction cookie on callback failures
- Install mock OAuth fetch when SOCIAL_AUTH_MOCK is enabled
- Keep social provider buttons visible across SPA login/signup toggle
- Refactor provider factory and handler validation helpers
- Drop redundant user lookup in session issuance; fix OAuth result typing
- Add regression tests for GitHub link, unverified Google, and invite gate
@cursor
cursor Bot temporarily deployed to preview-677 July 8, 2026 15:16 Inactive
Comment thread packages/worker/src/app/resolve-social-auth.ts
- Allow email change and account deletion without a password when the
  account uses an unusable OAuth/admin sentinel hash
- Expose hasUsablePassword on account profile loader data and hide the
  password field for OAuth-only users
- Add hasUsablePasswordHash helper and regression coverage
- Fix social-auth handler formatting that failed CI format:check
@cursor
cursor Bot temporarily deployed to preview-677 July 8, 2026 15:37 Inactive
Comment thread packages/worker/src/app/resolve-social-auth.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/shared/src/password-hash.ts (1)

108-137: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant prefix branch and dead code after the early return.

Since hasUsablePasswordHash(normalizedHash) already asserts the pbkdf2_sha256$ prefix, the if (normalizedHash.startsWith(...)) guard on Line 111 is now always true and the trailing return false on Line 136 is unreachable. The body can be flattened for clarity.

♻️ Flatten the always-true branch
 	const normalizedHash = storedHash.trim()
 	if (!hasUsablePasswordHash(normalizedHash)) {
 		return false
 	}
-	if (normalizedHash.startsWith(`${passwordHashPrefix}$`)) {
-		const [prefix, iterationsRaw, saltHex, hashHex, ...extra] =
-			normalizedHash.split('$')
-		if (prefix !== passwordHashPrefix || extra.length > 0) {
-			return false
-		}
-		if (!iterationsRaw || !/^\d+$/.test(iterationsRaw)) return false
-		const iterations = Number(iterationsRaw)
-		const salt = saltHex ? fromHex(saltHex) : null
-		const hash = hashHex ? fromHex(hashHex) : null
-		if (!Number.isSafeInteger(iterations) || iterations < 1 || !salt || !hash) {
-			return false
-		}
-		if (iterations > maxPasswordHashIterations) {
-			return false
-		}
-		const derived = await derivePasswordKey(
-			password,
-			salt,
-			iterations,
-			hash.length,
-		)
-		return timingSafeEqual(derived, hash)
-	}
-
-	return false
+	const [prefix, iterationsRaw, saltHex, hashHex, ...extra] =
+		normalizedHash.split('$')
+	if (prefix !== passwordHashPrefix || extra.length > 0) {
+		return false
+	}
+	if (!iterationsRaw || !/^\d+$/.test(iterationsRaw)) return false
+	const iterations = Number(iterationsRaw)
+	const salt = saltHex ? fromHex(saltHex) : null
+	const hash = hashHex ? fromHex(hashHex) : null
+	if (!Number.isSafeInteger(iterations) || iterations < 1 || !salt || !hash) {
+		return false
+	}
+	if (iterations > maxPasswordHashIterations) {
+		return false
+	}
+	const derived = await derivePasswordKey(password, salt, iterations, hash.length)
+	return timingSafeEqual(derived, hash)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/src/password-hash.ts` around lines 108 - 137, Flatten the
password verification logic in the `verifyPasswordHash` path by removing the
redundant `normalizedHash.startsWith(`${passwordHashPrefix}$`)` branch, since
`hasUsablePasswordHash(normalizedHash)` already guarantees the prefix. Keep the
parsing and validation of `prefix`, `iterationsRaw`, `saltHex`, `hashHex`, and
the `derivePasswordKey`/`timingSafeEqual` checks directly in the main flow, and
remove the unreachable trailing `return false` after the branch to make the
control flow clearer.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/shared/src/password-hash.ts`:
- Around line 108-137: Flatten the password verification logic in the
`verifyPasswordHash` path by removing the redundant
`normalizedHash.startsWith(`${passwordHashPrefix}$`)` branch, since
`hasUsablePasswordHash(normalizedHash)` already guarantees the prefix. Keep the
parsing and validation of `prefix`, `iterationsRaw`, `saltHex`, `hashHex`, and
the `derivePasswordKey`/`timingSafeEqual` checks directly in the main flow, and
remove the unreachable trailing `return false` after the branch to make the
control flow clearer.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 65d3b3d9-0c43-4360-a3b4-1dadd32c7443

📥 Commits

Reviewing files that changed from the base of the PR and between a11b9ff and 6503647.

📒 Files selected for processing (27)
  • packages/shared/src/password-hash.node.test.ts
  • packages/shared/src/password-hash.ts
  • packages/worker/client/loader-data-context.node.test.ts
  • packages/worker/client/navigation-data.node.test.ts
  • packages/worker/client/routes/account.tsx
  • packages/worker/client/routes/login.tsx
  • packages/worker/migrations/0056-auth-connections.sql
  • packages/worker/src/app/account-profile-data.ts
  • packages/worker/src/app/handler.ts
  • packages/worker/src/app/handlers/account-delete.ts
  • packages/worker/src/app/handlers/account-email-change.node.test.ts
  • packages/worker/src/app/handlers/account-email-change.ts
  • packages/worker/src/app/handlers/account-profile.node.test.ts
  • packages/worker/src/app/handlers/account-profile.ts
  • packages/worker/src/app/handlers/account.ts
  • packages/worker/src/app/handlers/social-auth.node.test.ts
  • packages/worker/src/app/handlers/social-auth.ts
  • packages/worker/src/app/loader-data.ts
  • packages/worker/src/app/resolve-social-auth.node.test.ts
  • packages/worker/src/app/resolve-social-auth.ts
  • packages/worker/src/app/social-auth-flow.ts
  • packages/worker/src/app/social-auth-mock.ts
  • packages/worker/src/app/social-auth-provider-factory.ts
  • packages/worker/src/app/social-auth-provider-names.ts
  • packages/worker/src/app/social-auth-providers.ts
  • packages/worker/src/app/ssr-render.node.test.ts
  • packages/worker/tsconfig-client.json
💤 Files with no reviewable changes (1)
  • packages/worker/migrations/0056-auth-connections.sql
✅ Files skipped from review due to trivial changes (3)
  • packages/worker/src/app/social-auth-provider-names.ts
  • packages/worker/client/loader-data-context.node.test.ts
  • packages/worker/tsconfig-client.json
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/worker/src/app/handlers/social-auth.node.test.ts
  • packages/worker/client/routes/login.tsx
  • packages/worker/src/app/social-auth-flow.ts
  • packages/worker/src/app/social-auth-providers.ts
  • packages/worker/src/app/handlers/social-auth.ts
  • packages/worker/src/app/social-auth-provider-factory.ts
  • packages/worker/src/app/resolve-social-auth.ts

Refuse to attach a social identity to an existing password account when
email_verified_at is null, so an unverified signup cannot squat an
address and later capture a real owner's IdP sign-in.
@cursor
cursor Bot temporarily deployed to preview-677 July 8, 2026 15:45 Inactive

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit abce768. Configure here.

Comment thread packages/worker/client/routes/login.tsx
Comment thread packages/worker/client/routes/login.tsx
- Keep the typed invite field in component state so social buttons
  forward it even when it is not in the URL
- Carry inviteCode across login/signup toggle links alongside redirectTo
@cursor
cursor Bot deployed to preview-677 July 8, 2026 15:50 Active
@kentcdodds kentcdodds closed this Jul 8, 2026
@kentcdodds
kentcdodds deleted the cursor/social-auth-github-x-google-746b branch July 8, 2026 16:25

This branch was successfully deployed

1 active deployment
preview-677 — 6121aaaf Deployed Jul 8, 2026 by cursor[bot] via 🔎 Deploy Preview Resources #3147
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.

2 participants