Skip to content

Google and GitHub OAuth sign-in share a single identity field, permanently blocking cross-provider login on the same email #28

Description

@Kingvic300

Summary

Google and GitHub OAuth sign-in share a single identity field (google_sub), with no provider discriminator. Once a user authenticates with one provider, signing in with the other on the same email permanently fails with a misleading error and no self-service recovery.

Background

AuthService::oauth_login_or_register is the shared resolution function for both Google and GitHub sign-in. It takes a parameter literally named google_sub and persists it to the user document's google_sub field. GitHub's callback passes its own numeric GitHub user ID into this same parameter (api/src/api/handlers/auth_handler.rs, github callback, ~line 795: service.oauth_login_or_register(github_id.to_string(), email)).

Evidence

  • api/src/services/auth_service.rs:417-421oauth_login_or_register(&self, google_sub: String, email: String, ...), called identically from both the Google and GitHub callback paths.
  • api/src/model/user.rs:22 — single pub google_sub: Option<String> field, no provider tag anywhere in the schema.
  • api/src/repositories/user_repository.rs:68-76find_by_google_sub queries doc! { "google_sub": google_sub } with no way to know which provider actually wrote that value. This field also has no index — every OAuth login does a full collection scan on users.
  • api/src/services/auth_service.rs:484-507resolve_oauth_account: if a user is found by email but their stored google_sub doesn't match the value just presented, it hard-rejects with Err(Unauthorized("This Google account is not linked to the existing user")), regardless of which provider actually wrote the stored value.
  • No linking endpoint exists for Google (grep -rn "link_google" api/src returns nothing), unlike GitHub, which has an unlink route and auto-links post-hoc without ever touching google_sub.

Deterministic repro, traced end-to-end against current api/src:

  1. User registers via "Sign in with GitHub" (user@example.com) → new account created, google_sub is set to the GitHub numeric ID (User::new_oauth).
  2. Same user later clicks "Sign in with Google" using the same email.
  3. find_by_google_sub(<real_google_sub>)None (doesn't match the stored GitHub ID).
  4. Falls back to find_by_email → finds the existing account.
  5. user.google_sub is Some("<github_id>"), which doesn't equal the real Google sub → hits the reject branch: "This Google account is not linked to the existing user."
  6. There is no in-app recovery — only DB intervention. The failure is symmetric for a Google-first user later trying GitHub.

User impact: any user who tries both supported OAuth providers on the same email — a normal thing to attempt on an app that advertises both — is permanently and confusingly locked out of the second provider.

Security note: because the field is shared with no re-verification against the presented email, the pattern is also a weak point for future provider additions — a bare field match with no domain separation between providers.

Proposed Solution

Give each provider its own identity anchor — either separate google_sub: Option<String> / github_sub: Option<String> fields (simplest, matches the existing one-field-per-provider shape already used for github_account), or a Vec<OAuthIdentity { provider, sub }> if more providers are planned. Update oauth_login_or_register (or split it per provider) and the corresponding lookup functions. Add an explicit "link this provider to my current account" flow mirroring the auto-link GitHub already does, so a user who already has a password or a Google account can deliberately attach GitHub, and vice versa, instead of hard-erroring. Add an index on whichever field(s) replace the current unindexed lookup.

Alternatives considered: leaving the single field but adding a provider sibling field was considered, but a per-provider field (or typed list) avoids ambiguity when a migration touches historical rows.

Technical Scope

  • api/src/services/auth_service.rsoauth_login_or_register, resolve_oauth_account
  • api/src/model/user.rs — user schema (identity fields)
  • api/src/repositories/user_repository.rsfind_by_google_sub and any new lookup functions, plus index creation
  • api/src/api/handlers/auth_handler.rs — Google and GitHub callback handlers
  • Data migration for existing google_sub rows
  • New "link provider" endpoint (optional but recommended)
  • Unit/integration tests for both OAuth flows

Acceptance Criteria

  • A user who registers via GitHub can subsequently sign in with Google using the same email without a manual DB fix, and vice versa
  • A GitHub numeric ID can never satisfy a Google-sub lookup, or vice versa
  • Existing users' google_sub data migrates without breaking their existing login
  • New/renamed lookup field(s) are indexed
  • Existing single-provider login flows (Google-only, GitHub-only) continue to work unchanged

Edge Cases

  • A genuinely already-linked different account on the same provider must still correctly reject (don't weaken this check while fixing the cross-provider case)
  • User has no password and only one OAuth provider linked, then tries the second provider for the first time
  • Migration must handle rows where google_sub currently holds a GitHub ID due to this bug

Risks

  • Migration touches every existing OAuth user row — needs a dry-run/backup before applying in production
  • Auto-linking a second provider by email match has account-takeover implications if email verification isn't enforced consistently across both providers — mirror whatever verification precedent GitHub's existing auto-link already sets

Deliverables

  • Code changes to identity storage/lookup and both OAuth callback paths
  • Migration script for existing google_sub data
  • Unit tests covering: GitHub-first → Google link succeeds; Google-first → GitHub link succeeds; genuine already-linked-to-a-different-account still rejects; migration correctness

Priority

High — breaks an advertised core feature (multi-provider login) for every user who naturally tries both, with no recovery path short of support intervention.


GrantFox Evaluation

Impact Score: 68/100
Difficulty Score: 45/100
Priority Score: 61
Confidence: 90%

Category: Bug / Architecture

Estimated Reward Tier: B

AI Rationale: A concrete, evidence-traced, deterministically-reproducible identity bug affecting a core advertised feature (dual OAuth login) with no user-facing recovery path today. Difficulty is moderate — touches auth service, repository, and handler layers plus a data migration, but the fix pattern mirrors an existing precedent (GitHub's own auto-link) already in the codebase.


Estimated Completion: 96 hours

Telegram: https://t.me/txioCommunity

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26Third CampaignCampaign: Third CampaignbugSomething isn't workingpriority:mediumNormal priority

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions