Skip to content

feat(web): support Google OAuth as alternative auth provider#431

Closed
duboff wants to merge 4 commits intoColeMurray:mainfrom
chattermill:feat/google-auth-provider
Closed

feat(web): support Google OAuth as alternative auth provider#431
duboff wants to merge 4 commits intoColeMurray:mainfrom
chattermill:feat/google-auth-provider

Conversation

@duboff
Copy link
Copy Markdown
Contributor

@duboff duboff commented Mar 28, 2026

Why

This is particularly useful for two cases:

  • Your company uses Gitlab or another SCM via the recently added support.
  • You want to extend usage to users who might not have a Github account (eg. Product Managers, Designers etc) like in the original Inspect design.

Summary

  • Adds AUTH_PROVIDER env var to select between github (default) and google OAuth
  • When set to google, uses Google OAuth with optional Workspace domain restriction via GOOGLE_WORKSPACE_DOMAIN
  • ALLOWED_USERS now accepts email addresses (for Google auth) in addition to GitHub usernames
  • No changes needed for PR creation — when the user has no SCM OAuth token (Google auth case), the existing fallback to the GitHub App token handles it

Changes

  • auth.tsbuildProvider() selects GitHub or Google based on AUTH_PROVIDER. Callbacks handle both provider profile shapes. JWT stores generic userId/userLogin instead of githubUserId/githubLogin.
  • access-control.tscheckAccessAllowed now also checks if email is in allowedUsers (OR logic with existing GitHub username check)
  • access-control.test.ts — added tests for email-in-allowedUsers case
  • .env.example — documented all auth-related env vars for both providers

Configuration

GitHub (default, no changes needed):

AUTH_PROVIDER=github
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...

Google:

AUTH_PROVIDER=google
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
GOOGLE_WORKSPACE_DOMAIN=example.com  # optional, restricts to Workspace domain

Test plan

  • Default config (no AUTH_PROVIDER set) — GitHub sign-in works as before
  • AUTH_PROVIDER=github — same behavior
  • AUTH_PROVIDER=google — Google sign-in page shown, login works
  • AUTH_PROVIDER=google + GOOGLE_WORKSPACE_DOMAIN — non-domain accounts rejected
  • ALLOWED_USERS with email addresses — only listed emails can sign in (Google)
  • ALLOWED_EMAIL_DOMAINS — domain restriction works for both providers
  • PR creation with Google auth — falls back to GitHub App token

Summary by CodeRabbit

  • New Features

    • Sign in now supports GitHub or Google OAuth; UI shows a generic "Sign in" CTA.
  • Configuration Updates

    • Added AUTH_PROVIDER (github|google), GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and optional GOOGLE_WORKSPACE_DOMAIN.
    • Access control interprets allowlists as GitHub usernames for GitHub or email addresses for Google.
  • Tests

    • Added tests for email-based allowlist behavior.
  • Documentation

    • Added Google OAuth setup and updated access-control guidance.

Add AUTH_PROVIDER env var to select between GitHub (default) and Google
OAuth. When set to "google", uses Google OAuth with optional Workspace
domain restriction. ALLOWED_USERS accepts email addresses for Google
auth (in addition to GitHub usernames). PR creation falls back to the
GitHub App token when the user has no SCM OAuth token.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 28, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 41f3a944-a445-46a8-8297-590d4e5b287c

📥 Commits

Reviewing files that changed from the base of the PR and between ac83a4c and 762096a.

📒 Files selected for processing (1)
  • docs/GETTING_STARTED.md
✅ Files skipped from review due to trivial changes (1)
  • docs/GETTING_STARTED.md

📝 Walkthrough

Walkthrough

Adds selectable auth provider via AUTH_PROVIDER (github|google), introduces Google OAuth env vars and optional workspace-domain check, generalizes NextAuth token/session fields, extends allowlist matching to emails, and makes the sign-in CTA provider-agnostic.

Changes

Cohort / File(s) Summary
Environment Configuration
packages/web/.env.example
Added AUTH_PROVIDER (github
Access Control
packages/web/src/lib/access-control.ts, packages/web/src/lib/access-control.test.ts
checkAccessAllowed now matches lowercased email entries in allowedUsers as well as GitHub usernames; tests added for exact match, case-insensitivity, and denial.
Auth Implementation
packages/web/src/lib/auth.ts
Introduced buildProvider() driven by AUTH_PROVIDER; NextAuth callbacks updated to enforce optional Google workspace domain, map provider IDs into generic userId/userLogin, and persist OAuth tokens only for GitHub. Module augmentation updated.
UI Sign-in CTA
packages/web/src/components/sidebar-layout.tsx
Removed GitHub-specific icon/text; changed button to “Sign in” and call to generic signIn() with callback URL.
Docs
docs/GETTING_STARTED.md
Added Google OAuth setup steps and documented provider-specific access-control semantics and Terraform vars.

Sequence Diagram

sequenceDiagram
    actor User
    participant Client as Web Client
    participant NextAuth as NextAuth
    participant Provider as OAuth Provider (GitHub/Google)
    participant ACL as Access Control
    participant Session as Session Store

    User->>Client: Click "Sign in"
    Client->>NextAuth: signIn(request)
    NextAuth->>NextAuth: buildProvider() -> GitHub or Google
    NextAuth->>Provider: Redirect for auth
    Provider->>NextAuth: OAuth callback (profile, tokens)

    alt Google
        NextAuth->>NextAuth: callbacks.signIn -> check GOOGLE_WORKSPACE_DOMAIN
        alt domain mismatch
            NextAuth->>Client: Deny sign-in
        else domain ok
            NextAuth->>ACL: checkAccessAllowed(email)
        end
    else GitHub
        NextAuth->>ACL: checkAccessAllowed(githubUsername)
    end

    ACL->>NextAuth: allow or deny

    alt allowed
        NextAuth->>NextAuth: callbacks.jwt -> map profile -> JWT.userId/userLogin
        NextAuth->>Session: callbacks.session -> populate session.user.id/login
        Session->>Client: Return session
    else denied
        NextAuth->>Client: Deny authentication
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped to swap a single key,
Now users pick how they sign in—whee!
Emails checked, domains watched tight,
One button now to start the flight,
A tiny hop, and auth feels right.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 title accurately describes the main change: adding Google OAuth as an alternative authentication provider to the web application.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Copy link
Copy Markdown

@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

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

Inline comments:
In `@packages/web/.env.example`:
- Around line 28-31: Sign-in for Google in auth.ts currently only checks
GOOGLE_WORKSPACE_DOMAIN and returns true, so ALLOWED_USERS and
ALLOWED_EMAIL_DOMAINS are not enforced; update the Google branch of the signIn
callback (the signIn function in auth.ts) to call the existing
checkAccessAllowed(email) helper (the same used for GitHub) after validating the
user email (and still allow bypass if GOOGLE_WORKSPACE_DOMAIN is set and
matches), ensuring ALLOWED_USERS and ALLOWED_EMAIL_DOMAINS are evaluated for
Google logins as well; ensure you handle missing email from the profile and
return false when checkAccessAllowed denies access.

In `@packages/web/src/lib/auth.ts`:
- Around line 69-80: In signIn (the async signIn handler) the Google path
returns true after enforcing GOOGLE_WORKSPACE_DOMAIN, which skips
ALLOWED_USERS/ALLOWED_EMAIL_DOMAINS checks; update signIn so that after the
workspaceDomain check (AUTH_PROVIDER === "google" and workspaceDomain validated)
it calls the existing checkAccessAllowed helper with the authenticated user's
email (and/or id) and returns its boolean result instead of unconditionally
returning true, ensuring ALLOWED_USERS and ALLOWED_EMAIL_DOMAINS are enforced
for Google users as well.
🪄 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

Run ID: 5b3a34e0-01c3-4715-98c7-7e595b1fbc01

📥 Commits

Reviewing files that changed from the base of the PR and between b4ac213 and 60e2925.

📒 Files selected for processing (4)
  • packages/web/.env.example
  • packages/web/src/lib/access-control.test.ts
  • packages/web/src/lib/access-control.ts
  • packages/web/src/lib/auth.ts

Comment thread packages/web/.env.example
Comment on lines +28 to 31
# Access Control (comma-separated, leave empty to allow all users)
# ALLOWED_USERS accepts GitHub usernames (AUTH_PROVIDER=github) or email addresses (AUTH_PROVIDER=google)
ALLOWED_EMAIL_DOMAINS=
ALLOWED_USERS=
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Documentation claims ALLOWED_USERS works with Google auth, but the implementation doesn't enforce it.

Looking at auth.ts, the signIn callback for Google auth (lines 71-79) only checks GOOGLE_WORKSPACE_DOMAIN and then returns true without calling checkAccessAllowed. This means ALLOWED_USERS and ALLOWED_EMAIL_DOMAINS are not enforced for Google users.

Either update the documentation to reflect the actual behavior, or update auth.ts to call checkAccessAllowed for Google auth as well.

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

In `@packages/web/.env.example` around lines 28 - 31, Sign-in for Google in
auth.ts currently only checks GOOGLE_WORKSPACE_DOMAIN and returns true, so
ALLOWED_USERS and ALLOWED_EMAIL_DOMAINS are not enforced; update the Google
branch of the signIn callback (the signIn function in auth.ts) to call the
existing checkAccessAllowed(email) helper (the same used for GitHub) after
validating the user email (and still allow bypass if GOOGLE_WORKSPACE_DOMAIN is
set and matches), ensuring ALLOWED_USERS and ALLOWED_EMAIL_DOMAINS are evaluated
for Google logins as well; ensure you handle missing email from the profile and
return false when checkAccessAllowed denies access.

Comment on lines 69 to +80
async signIn({ profile, user }) {
// Google Workspace domain check (server-side enforcement)
if (AUTH_PROVIDER === "google") {
const workspaceDomain = process.env.GOOGLE_WORKSPACE_DOMAIN;
if (workspaceDomain && user.email) {
const emailDomain = user.email.split("@")[1];
if (emailDomain !== workspaceDomain) {
return false;
}
}
return true;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Google auth bypasses ALLOWED_USERS and ALLOWED_EMAIL_DOMAINS access control.

The Google sign-in path only enforces GOOGLE_WORKSPACE_DOMAIN and then returns true, completely skipping the checkAccessAllowed call. This means:

  • ALLOWED_USERS with email addresses won't restrict Google users
  • ALLOWED_EMAIL_DOMAINS won't restrict Google users

The PR objectives and .env.example documentation state that ALLOWED_USERS should work with email addresses for Google auth, but this code doesn't implement that behavior.

🐛 Proposed fix to enforce access control for Google auth
       // Google Workspace domain check (server-side enforcement)
       if (AUTH_PROVIDER === "google") {
         const workspaceDomain = process.env.GOOGLE_WORKSPACE_DOMAIN;
         if (workspaceDomain && user.email) {
           const emailDomain = user.email.split("@")[1];
           if (emailDomain !== workspaceDomain) {
             return false;
           }
         }
-        return true;
+        // Apply access control (allowedUsers with emails, allowedDomains)
+        const config = {
+          allowedDomains: parseAllowlist(process.env.ALLOWED_EMAIL_DOMAINS),
+          allowedUsers: parseAllowlist(process.env.ALLOWED_USERS),
+        };
+        return checkAccessAllowed(config, {
+          email: user.email ?? undefined,
+        });
       }
📝 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
async signIn({ profile, user }) {
// Google Workspace domain check (server-side enforcement)
if (AUTH_PROVIDER === "google") {
const workspaceDomain = process.env.GOOGLE_WORKSPACE_DOMAIN;
if (workspaceDomain && user.email) {
const emailDomain = user.email.split("@")[1];
if (emailDomain !== workspaceDomain) {
return false;
}
}
return true;
}
async signIn({ profile, user }) {
// Google Workspace domain check (server-side enforcement)
if (AUTH_PROVIDER === "google") {
const workspaceDomain = process.env.GOOGLE_WORKSPACE_DOMAIN;
if (workspaceDomain && user.email) {
const emailDomain = user.email.split("@")[1];
if (emailDomain !== workspaceDomain) {
return false;
}
}
// Apply access control (allowedUsers with emails, allowedDomains)
const config = {
allowedDomains: parseAllowlist(process.env.ALLOWED_EMAIL_DOMAINS),
allowedUsers: parseAllowlist(process.env.ALLOWED_USERS),
};
return checkAccessAllowed(config, {
email: user.email ?? undefined,
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/lib/auth.ts` around lines 69 - 80, In signIn (the async
signIn handler) the Google path returns true after enforcing
GOOGLE_WORKSPACE_DOMAIN, which skips ALLOWED_USERS/ALLOWED_EMAIL_DOMAINS checks;
update signIn so that after the workspaceDomain check (AUTH_PROVIDER ===
"google" and workspaceDomain validated) it calls the existing checkAccessAllowed
helper with the authenticated user's email (and/or id) and returns its boolean
result instead of unconditionally returning true, ensuring ALLOWED_USERS and
ALLOWED_EMAIL_DOMAINS are enforced for Google users as well.

Google sign-in path was skipping checkAccessAllowed, so ALLOWED_USERS
and ALLOWED_EMAIL_DOMAINS were not enforced. Now both providers go
through the same access control check.

Also make sign-in button provider-agnostic — signIn() without args
redirects to whichever provider is configured.
Copy link
Copy Markdown

@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

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

Inline comments:
In `@packages/web/src/components/sidebar-layout.tsx`:
- Around line 95-98: The Sign in button currently calls signIn() which opens
NextAuth's provider selection page; change it to call signIn with the GitHub
provider id to preserve one-click OAuth. Locate the Sign in button onClick in
sidebar-layout.tsx and replace signIn() with signIn("github") (or derive the
active provider id from whatever providers prop/state you already fetch, e.g.,
providers.github.id) so the click immediately starts the GitHub OAuth flow.

In `@packages/web/src/lib/auth.ts`:
- Around line 93-120: The jwt/session handlers currently populate SCM-specific
fields (token.userId, token.userLogin and session.user.id/login) with Google
auth values; change this so SCM fields remain GitHub-only: in async jwt({ token,
account, profile }) only set token.userId/token.userLogin from the GitHub branch
(githubProfile.id/githubProfile.login) and do not map Google sub or access_token
into those fields; instead add provider-specific fields such as
token.authProvider, token.providerUserId, token.providerLogin and
token.providerAccessToken (set for any provider including Google) and propagate
those to session in async session({ session, token }) as
session.user.authProvider/session.user.providerId/session.user.providerLogin
while leaving session.user.id/login reserved for SCM (GitHub) usage, or
alternatively update the downstream contract in the same PR to accept the new
provider-specific fields used in
packages/web/src/app/api/sessions/[id]/ws-token/route.ts and
packages/web/src/app/api/sessions/route.ts.
🪄 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

Run ID: 40f7ff6e-7d8d-460e-92ba-f62de229599f

📥 Commits

Reviewing files that changed from the base of the PR and between 60e2925 and 5d67947.

📒 Files selected for processing (2)
  • packages/web/src/components/sidebar-layout.tsx
  • packages/web/src/lib/auth.ts

Comment on lines +95 to +98
onClick={() => signIn()}
className="flex items-center gap-2 bg-primary text-primary-foreground px-6 py-3 font-medium hover:opacity-90 transition"
>
<GitHubIcon className="w-5 h-5" />
Sign in with GitHub
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In the NextAuth.js v4 Client API, what happens when signIn() is called without a provider id, and how do you start the OAuth flow immediately for a specific provider?

💡 Result:

In NextAuth.js v4 Client API, calling signIn without a provider id redirects the user to the built-in NextAuth.js sign-in page, which lists available providers if multiple are configured. To start the OAuth flow immediately for a specific provider (skipping the sign-in page), call signIn with the provider's id as the first argument, e.g., signIn("google").

Citations:


signIn() changes flow from direct GitHub auth to provider selection page.

Calling signIn() without arguments redirects to the built-in NextAuth.js sign-in page listing available providers, whereas signIn("github") started the GitHub OAuth flow immediately. This changes the CTA from one-click auth to a two-step flow. If the direct provider redirect is needed, derive and pass the active provider id explicitly: signIn(providerId).

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

In `@packages/web/src/components/sidebar-layout.tsx` around lines 95 - 98, The
Sign in button currently calls signIn() which opens NextAuth's provider
selection page; change it to call signIn with the GitHub provider id to preserve
one-click OAuth. Locate the Sign in button onClick in sidebar-layout.tsx and
replace signIn() with signIn("github") (or derive the active provider id from
whatever providers prop/state you already fetch, e.g., providers.github.id) so
the click immediately starts the GitHub OAuth flow.

Comment thread packages/web/src/lib/auth.ts
duboff added 2 commits April 1, 2026 20:55
- Google access tokens are not SCM tokens — only store OAuth tokens in
  JWT for GitHub auth. Google auth users fall back to app tokens for PR
  creation (existing fallback path).
- Pass callbackUrl to signIn() so NextAuth redirects correctly with a
  single provider.
Copy link
Copy Markdown
Owner

@ColeMurray ColeMurray left a comment

Choose a reason for hiding this comment

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

[Automated Review]

Summary

Adds Google OAuth as an alternative to GitHub for web sign-in via AUTH_PROVIDER env var. Access control is unified — both providers go through checkAccessAllowed. For Google auth, SCM operations fall back to the GitHub App token. Clean, well-scoped change overall, but has one security gap and a few design concerns.


Critical Issues

  • [Security] auth.ts:71-78Workspace domain check silently bypassed when email is missing. If GOOGLE_WORKSPACE_DOMAIN is set but the Google profile returns no email (null/undefined), the condition if (workspaceDomain && user.email) is false, and the check is skipped entirely. The user then falls through to checkAccessAllowed, which — if both allowlists are empty — grants access. This defeats the workspace domain restriction.

    Fix:

    if (AUTH_PROVIDER === "google") {
      const workspaceDomain = process.env.GOOGLE_WORKSPACE_DOMAIN;
      if (workspaceDomain) {
        if (!user.email) return false;  // No email → deny
        const emailDomain = user.email.split("@")[1];
        if (emailDomain !== workspaceDomain) return false;
      }
    }

Suggestions

  • [Security] auth.ts:74Use Google's hd claim instead of parsing the email domain. The Google ID token includes an hd (hosted domain) claim which is the authoritative signal for Workspace membership. Email domain is a proxy. NextAuth's Google provider exposes this on the profile object. Consider:

    const googleProfile = profile as { hd?: string };
    if (workspaceDomain && googleProfile.hd !== workspaceDomain) {
      return false;
    }

    This is more robust and is the pattern recommended by Google's own docs.

  • [Correctness] auth.ts:105-109Google sub is forwarded as scmUserId to the control plane. token.userId is set to googleProfile.sub, which eventually flows into scmUserId in both ws-token/route.ts:43 and sessions/route.ts:73. The control plane uses scmUserId to look up and refresh GitHub OAuth tokens in D1 (see participant-service.ts:195-198). While functionally harmless today (no D1 tokens will exist for a Google user ID, and the App token fallback handles it), the naming is semantically misleading. Consider either:

    • Sending scmUserId: null for Google auth users explicitly, or
    • Adding an authProvider field to the session/ws-token payload so the control plane knows not to expect SCM identity.
  • [Robustness] auth.ts:11Validate AUTH_PROVIDER value. An invalid value like AUTH_PROVIDER=gitlab silently falls through to GitHub. Consider:

    const AUTH_PROVIDER = process.env.AUTH_PROVIDER || "github";
    if (AUTH_PROVIDER !== "github" && AUTH_PROVIDER !== "google") {
      throw new Error(`Invalid AUTH_PROVIDER: ${AUTH_PROVIDER}. Must be "github" or "google".`);
    }
  • [Testing] — The auth.ts changes (buildProvider, signIn callback with workspace domain enforcement, JWT population per provider) are the core of this PR but have no test coverage. The workspace domain bypass bug above would have been caught by a test like "denies Google user when email is missing and workspace domain is set".


Nitpicks

  • Nit: sidebar-layout.tsx:96 — The gap-2 class in the button's className is vestigial now that the GitHub icon was removed. The button only has a single text child, so the gap has no effect.

Positive Feedback

  • Clean buildProvider() factoring — Extracting provider construction into a function keeps authOptions readable and makes it easy to add providers in the future.
  • Unified access control — Both providers go through checkAccessAllowed with proper parameter mapping. The CodeRabbit concern about Google bypassing access control was addressed correctly.
  • Thoughtful SCM token handling — Only storing OAuth tokens for GitHub and relying on the App token fallback for Google users is pragmatic and avoids leaking Google tokens into an SCM context.

Verdict

Request Changes — The workspace domain bypass when user.email is missing is a security gap that should be fixed before merging. The other suggestions are improvements but not blocking.

* Auth provider selection.
* Set AUTH_PROVIDER=google to use Google OAuth, otherwise GitHub is used.
*/
const AUTH_PROVIDER = process.env.AUTH_PROVIDER || "github";
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[Automated Review] [Robustness] Validate AUTH_PROVIDER value. An invalid value like AUTH_PROVIDER=gitlab silently falls through to GitHub. Consider:

const AUTH_PROVIDER = process.env.AUTH_PROVIDER || "github";
if (AUTH_PROVIDER !== "github" && AUTH_PROVIDER !== "google") {
  throw new Error(`Invalid AUTH_PROVIDER: ${AUTH_PROVIDER}. Must be "github" or "google".`);
}

Comment on lines +71 to +78
if (AUTH_PROVIDER === "google") {
const workspaceDomain = process.env.GOOGLE_WORKSPACE_DOMAIN;
if (workspaceDomain && user.email) {
const emailDomain = user.email.split("@")[1];
if (emailDomain !== workspaceDomain) {
return false;
}
}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[Automated Review] [Security — Critical] Workspace domain check silently bypassed when email is missing. If GOOGLE_WORKSPACE_DOMAIN is set but the Google profile returns no email (null/undefined), the condition if (workspaceDomain && user.email) is false and the check is skipped entirely. The user then falls through to checkAccessAllowed, which — if both allowlists are empty — grants access. This defeats the workspace domain restriction.

Fix:

if (AUTH_PROVIDER === "google") {
  const workspaceDomain = process.env.GOOGLE_WORKSPACE_DOMAIN;
  if (workspaceDomain) {
    if (!user.email) return false;  // No email → deny
    const emailDomain = user.email.split("@")[1];
    if (emailDomain !== workspaceDomain) return false;
  }
}

if (AUTH_PROVIDER === "google") {
const workspaceDomain = process.env.GOOGLE_WORKSPACE_DOMAIN;
if (workspaceDomain && user.email) {
const emailDomain = user.email.split("@")[1];
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[Automated Review] [Security] Consider using Google's hd claim instead of parsing the email domain. The Google ID token includes an hd (hosted domain) claim which is the authoritative signal for Workspace membership. Email domain is a proxy. NextAuth's Google provider exposes this on the profile object:

const googleProfile = profile as { hd?: string };
if (workspaceDomain && googleProfile.hd !== workspaceDomain) {
  return false;
}

This is more robust and is the pattern recommended by Google's own docs.

Comment on lines +105 to +109
if (AUTH_PROVIDER === "google") {
const googleProfile = profile as { sub?: string };
if (googleProfile.sub) {
token.userId = googleProfile.sub;
}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[Automated Review] [Correctness] Google sub is forwarded as scmUserId to the control plane. token.userId is set to googleProfile.sub, which eventually flows into scmUserId in both ws-token/route.ts:43 and sessions/route.ts:73. The control plane uses scmUserId to look up and refresh GitHub OAuth tokens in D1 (see participant-service.ts:195-198). While functionally harmless today (no D1 tokens will exist for a Google user ID, and the App token fallback handles it), the naming is semantically misleading. Consider either:

  • Sending scmUserId: null for Google auth users explicitly, or
  • Adding an authProvider field to the session/ws-token payload so the control plane knows not to expect SCM identity.

Comment on lines 69 to 91
async signIn({ profile, user }) {
// Google Workspace domain check (server-side enforcement)
if (AUTH_PROVIDER === "google") {
const workspaceDomain = process.env.GOOGLE_WORKSPACE_DOMAIN;
if (workspaceDomain && user.email) {
const emailDomain = user.email.split("@")[1];
if (emailDomain !== workspaceDomain) {
return false;
}
}
}

// Access control (applies to both providers)
const config = {
allowedDomains: parseAllowlist(process.env.ALLOWED_EMAIL_DOMAINS),
allowedUsers: parseAllowlist(process.env.ALLOWED_USERS),
};

const githubProfile = profile as { login?: string };
const isAllowed = checkAccessAllowed(config, {
githubUsername: githubProfile.login,
return checkAccessAllowed(config, {
githubUsername: AUTH_PROVIDER === "github" ? githubProfile.login : undefined,
email: user.email ?? undefined,
});
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[Automated Review] [Testing] The auth.ts changes (buildProvider, signIn callback with workspace domain enforcement, JWT population per provider) are the core of this PR but have no test coverage. The workspace domain bypass bug flagged above would have been caught by a test like "denies Google user when email is missing and workspace domain is set".

<button
onClick={() => signIn("github")}
onClick={() => signIn(undefined, { callbackUrl: "/" })}
className="flex items-center gap-2 bg-primary text-primary-foreground px-6 py-3 font-medium hover:opacity-90 transition"
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[Automated Review] Nit: The gap-2 class is vestigial now that the GitHub icon was removed. The button only has a single text child, so the gap has no effect.

Comment on lines 94 to 99
<button
onClick={() => signIn("github")}
onClick={() => signIn(undefined, { callbackUrl: "/" })}
className="flex items-center gap-2 bg-primary text-primary-foreground px-6 py-3 font-medium hover:opacity-90 transition"
>
<GitHubIcon className="w-5 h-5" />
Sign in with GitHub
Sign in
</button>
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[Automated Review] [UX] The sign-in button is now fully generic — no icon, just "Sign in". When AUTH_PROVIDER=google, consider showing Google branding (icon + "Sign in with Google") to match Google's sign-in branding guidelines. Similarly, the GitHub icon should be preserved when AUTH_PROVIDER=github.

Since AUTH_PROVIDER is a server-side env var and this is a client component, one approach would be to expose it as a NEXT_PUBLIC_AUTH_PROVIDER var and conditionally render the appropriate icon and label.

@ColeMurray
Copy link
Copy Markdown
Owner

[Automated Review] Architecture Deep Dive: Separating Auth Identity from SCM Identity

The Current Coupling

Today, auth identity = SCM identity. The system assumes the user who logs in is also the GitHub user whose token is used for repo operations. This coupling runs deep:

NextAuth (GitHub OAuth)
  → session.user.id    = GitHub numeric ID     → scmUserId   → D1 user_scm_tokens PK
  → session.user.login = GitHub username       → scmLogin    → avatar URLs, display names
  → jwt.accessToken    = GitHub OAuth token     → scmToken    → PR creation, repo operations

Every layer — web client, control plane router, Durable Object, D1 store, sandbox runtime — assumes these three facts hold. Here is where:

Layer File Assumption
Web sessions/route.ts:70-77 user.id is a GitHub ID, jwt.accessToken is a GitHub token
Router router.ts:764-778 scmUserId is valid for keying user_scm_tokens in D1
DO Handler ws-token.handler.ts:48-55 Token expiry is comparable across refreshes (same OAuth provider)
Participant Svc participant-service.ts:218-225 refreshAccessToken() can use any stored refresh token against github.com/login/oauth/access_token
Participant Svc participant-service.ts:402-456 scm_access_token_encrypted is a valid GitHub token for PR creation
Participant Svc participant-service.ts:60-67 getAvatarUrl() builds github.com/{login}.png
Auth auth/github.ts:196 generateNoreplyEmail() uses @users.noreply.github.com
Child Sessions child-sessions.handler.ts:47-54 All SCM fields from parent are valid GitHub credentials
Sandbox bridge.py:611-620 scmName/scmEmail are suitable for git config user.*

What Breaks With This PR

When AUTH_PROVIDER=google, the auth identity becomes a Google account while the SCM is still GitHub. The current PR creates a split identity without handling the downstream consequences:

# What happens Where Impact
1 scmUserId = Google sub (wrong namespace) auth.ts:108sessions/route.ts:73router.ts:764 D1 token store keyed on a Google ID — never matches any GitHub user. Centralized refresh path skipped.
2 scmLogin = undefined auth.ts:105-118 (no userLogin set for Google) No avatars anywhere (returns undefined). Display name falls back to raw userId (an email or Google sub).
3 scmToken = undefined (correctly gated) auth.ts:94-102 Good — Google token is NOT stored as an SCM token. But scm_access_token_encrypted in the DO is null.
4 PR creation falls back to App token participant-service.ts:410-414pull-request-service.ts:174 Works, but PRs are authored by the GitHub App, not the user. No user identity on the PR.
5 Token refresh attempts GitHub OAuth endpoint participant-service.ts:218-225, 346-359 If any token were stored (currently prevented by #3), the refresh would POST to github.com/login/oauth/access_token with a Google refresh token → 401.
6 generateNoreplyEmail() produces undefined+undefined@users.noreply.github.com auth/github.ts:196 Called from getCommitEmail() fallback. Commits may get garbage attribution.
7 Child sessions inherit all null SCM fields child-sessions.handler.ts:47-54, router.ts:1434-1457 Agent-spawned child sessions also have no SCM identity — cascading the problem.

Net effect for a Google-authed user: Sessions work (prompts flow, code is generated), but PRs are created by the GitHub App bot with no user attribution, avatars are missing, and display names are degraded.


Long-Term Features This Impacts

1. Multi-user sessions / collaboration

The participants table supports multiple users per session. If some users auth with Google and others with GitHub, participant records will have inconsistent identity formats (scm_user_id being a Google sub vs a GitHub numeric ID). Authorization checks like "is this user the session owner?" use user_id (which falls back to email for Google users), creating a fragile identity chain.

2. User-scoped session listing

Today sessions are not user-scoped in D1 (no created_by column on sessions). If user-scoped views are added later, the system needs a stable, provider-agnostic user identity — not a GitHub ID that doesn't exist for Google users.

3. Audit trails and activity logs

Any future audit logging that records "who did what" needs a canonical user identity. Mixing GitHub numeric IDs and Google emails in the same userId field makes querying and attribution unreliable.

4. PR review assignments and commit attribution

The github-bot currently uses scm_login for @mention commands and review assignments. Google-authed users would have no scm_login, making them invisible to the bot. Commit attribution via git config user.email works (Google email is valid), but GitHub won't link the commit to a GitHub account unless the email is verified on that account.

5. GitLab / other SCM providers

The codebase already has a GitLab source control provider (source-control/providers/). Adding Google OAuth as auth while using GitLab as SCM would compound the identity split — three identity namespaces (auth, GitHub SCM, GitLab SCM) in fields designed for one.

6. Account linking

Enterprise users may want: "I log in with my Google Workspace account, but my GitHub identity is linked for PR creation." This is a common pattern (Slack, Linear, etc. all support GitHub account linking). The current PR's one-or-the-other toggle makes this impossible.

7. Centralized token management

The D1 user_scm_tokens table (keyed on provider_user_id) is designed for cross-session token sharing — when a user opens multiple sessions, they all share the same refreshed token via D1. With Google auth, provider_user_id would be a Google sub, making D1 lookup always miss. The system falls back to per-DO-SQLite token storage with no centralized refresh, defeating a key scalability feature.


Recommended Architecture

Rather than a toggle between auth providers, we recommend cleanly separating auth identity from SCM identity at the data model level:

Core principle: Auth tells us WHO, SCM tells us HOW

Auth Identity (who you are)          SCM Identity (how you interact with repos)
├── provider: "github" | "google"    ├── provider: "github" | "gitlab"
├── providerId: "12345" | "sub-xyz"  ├── userId: "12345" (GitHub numeric ID)
├── email: "user@company.com"        ├── login: "octocat"
├── name: "Jane Doe"                 ├── accessToken: "gho_..." (GitHub OAuth)
└── avatarUrl: "..."                 ├── refreshToken: "ghr_..."
                                     └── tokenExpiresAt: 1234567890

Proposed changes

1. Introduce a canonical authUser identity (web + control plane)

// NextAuth session — auth identity only
interface Session {
  user: {
    authProvider: "github" | "google";
    authId: string;        // provider-specific ID
    email?: string;
    name?: string;
    image?: string;
  };
}

// Separate SCM identity — only populated when the user has linked a GitHub account
interface ScmIdentity {
  provider: "github";
  userId: string;          // GitHub numeric ID
  login: string;           // GitHub username
  accessToken: string;
  refreshToken?: string;
  tokenExpiresAt?: number;
}

2. Support both providers simultaneously

// NEXT_PUBLIC_AUTH_PROVIDERS=github,google
function buildProviders(): Provider[] {
  const providers: Provider[] = [];
  if (enabledProviders.includes("github")) providers.push(GitHubProvider({...}));
  if (enabledProviders.includes("google")) providers.push(GoogleProvider({...}));
  return providers;
}

When GitHub is one of the providers, users who sign in with GitHub automatically have an SCM identity. When users sign in with Google only, SCM identity is null and the App token fallback handles repo operations.

3. Account linking (future, optional)

Add a /api/link-github endpoint that initiates a GitHub OAuth flow for Google-authed users, storing the resulting GitHub token as their SCM identity. This gives them user-attributed PRs without requiring GitHub as the primary auth provider.

4. Update the participant model

-- DO SQLite participants table
ALTER TABLE participants ADD COLUMN auth_provider TEXT;  -- "github" | "google"
ALTER TABLE participants ADD COLUMN auth_id TEXT;        -- canonical auth identity

-- scm_* fields remain as-is but are only populated when SCM identity exists
-- scm_user_id is NULL for Google-only users (no GitHub account linked)

5. Update D1 token store key

The user_scm_tokens table should be keyed on a composite of (scm_provider, scm_user_id) rather than just provider_user_id, to support future multi-SCM scenarios:

ALTER TABLE user_scm_tokens ADD COLUMN scm_provider TEXT NOT NULL DEFAULT 'github';
-- New PK: (scm_provider, provider_user_id)

6. Guard all SCM operations on SCM identity existence

// participant-service.ts — token refresh
if (!participant.scm_user_id) {
  this.log.info("No SCM identity, skipping token refresh");
  return null;  // Caller falls back to App token
}

// pull-request-service.ts — already has the fallback, just needs explicit guard
const prAuth = scmIdentity ? userOAuth : appAuth;

Summary

The current PR treats auth provider as a deployment-level toggle — GitHub or Google, never both. This works as a short-term solution but creates long-term architectural debt:

  1. Identity confusion — Google IDs in GitHub ID fields, undefined usernames, missing avatars
  2. No path to multi-provider — can't support both on the same instance
  3. No account linking — Google users can never get user-attributed PRs
  4. Semantic pollutionscm_* fields carry non-SCM data, confusing future maintainers

The recommended approach separates auth identity from SCM identity at the data model level, supports both providers simultaneously, and preserves a clean path to account linking and multi-SCM support.

@ColeMurray
Copy link
Copy Markdown
Owner

closing due to inactivity

@ColeMurray ColeMurray closed this Apr 18, 2026
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