Social Login for Signup API — Feasibility & Plan

5 comments1 reviews

Social Login for Signup API - Feasibility & Plan

Status: NEEDS VALIDATION — Requires team review and architectural approval before implementation begins.

Providers: GitHub + GitLab + Google

Estimate: 4.5 weeks (one developer) / MVP 2.5 weeks (GitHub only)


Allow users to sign up and log in via GitHub, GitLab, or Google instead of requiring email + password. This extends the existing Signup API (POST /api/v1/signup) and adds social login buttons to the DeployHQ login and signup pages.

Why all three? GitHub is most relevant for developers. GitLab is developer-focused and already used for repo linking. Google provides broadest coverage for non-developer team members. (Google SAML SSO is a separate enterprise feature — admin-configured, per-account, enforceable. Social login with Google is for individual users without enterprise SSO.)


DeployHQ uses a two-tier auth system:

ComponentCurrent State
Identity Service (identity.deployhq.com)Password + SAML SSO + 2FA. Owns all user credentials and the login UI.
Main App (deployhq.com)Has OmniAuth for GitHub/GitLab/Bitbucket — but only for repo linking, not user auth.

Social login must live in Identity because that's where users go to log in. Without social buttons there, a user who signed up with GitHub would have no way to log back in (they have no password).

#Key Insight: SAML Already Solved This

Identity already has a full SAML SSO implementation that handles:

  • Passwordless user creation (random password, auto-verify email)
  • External identity linking (SamlIdentity model)
  • Cross-service session bridging (LoginToken — 30-second single-use tokens)
  • JIT (Just-In-Time) user provisioning

Social login follows the exact same pattern — it's a new provider type feeding into existing infrastructure, not a new auth system.


#Flow 1: Browser Login (Existing Account)

This is how a user with an existing DeployHQ account logs in with GitHub:

Key points:

  • Users see the login form on DeployHQ (deployhq.com/login), not on Identity directly
  • Clicking "Sign in with GitHub" redirects to Identity with the account context (same as the existing SSO button)
  • The access token never leaves the Identity server
  • LoginToken is the same session bridge used by SAML SSO
  • Main App needs zero changes for this flow — token consumption already works

#Flow 2: API Signup (New Account)

This is how a new user signs up via the API (used by CLI, MCP, or browser-based signup):

Key points:

  • No LoginToken redirect needed — the API returns the account directly
  • PKCE is required for all API callers (the signup endpoint is public)
  • The authorization code is single-use and short-lived (~10 minutes)
  • Identity exchanges it server-side — the access token never reaches the client

#Flow 3: Browser Signup (New Account)

This is how a new user signs up with GitHub through the DeployHQ website:

Key points:

  • User starts on deployhq.com/signup, not the login page
  • No password needed — Identity creates user with random password + auto-verified email
  • After OAuth completes, DeployHQ creates the account and all resources (same as email/password signup)
  • User is logged in immediately after account creation

#Flow 4: Account Linking Decision Tree

How Identity decides what to do when a social provider returns a user:


#Scenario 1: New Developer Signs Up with GitHub

Context: A developer discovers DeployHQ and wants to sign up using their GitHub account via the CLI.

  1. Developer runs dhq signup --github
  2. CLI generates PKCE code_verifier and code_challenge
  3. CLI opens browser to https://github.com/login/oauth/authorize?client_id=...&scope=user:email&code_challenge=...
  4. Developer clicks "Authorize" on GitHub
  5. GitHub redirects to http://localhost:8432/callback?code=abc123
  6. CLI captures the code and sends:
    POST /api/v1/signup
    {
      "provider": "github",
      "provider_code": "abc123",
      "redirect_uri": "http://localhost:8432/callback",
      "code_verifier": "dBjftJeZ4CVP..."
    }
    
  7. Identity exchanges the code, finds the developer's GitHub profile (email: [email protected], verified)
  8. Identity creates a new user (random password, email auto-verified) + OauthIdentity record
  9. SignupService creates the billing account, DeployHQ account, API key, and SSH key
  10. CLI receives the full response and displays: "Account created! Your API key is: ..."

Result: Developer has a DeployHQ account with no password. They can log in with GitHub, or use password reset to set one.

Context: A user already has a DeployHQ account (signed up with email/password). They click "Sign in with GitHub" on the login page.

  1. User visits deployhq.com/login
  2. Clicks "Sign in with GitHub" (redirects to Identity)
  3. Authorizes on GitHub (email: [email protected], verified)
  4. Identity calls User.locate("[email protected]") — finds the existing user
  5. Identity creates an OauthIdentity record linking GitHub UID to the existing user
  6. Identity creates a LoginToken and redirects to DeployHQ
  7. User is logged in

Result: User now has two login methods: email/password AND GitHub. Both continue to work.

#Scenario 3: Social Signup Hits 2FA

Context: A developer's GitHub email matches an existing Identity user who has 2FA enabled.

  1. Client sends POST /api/v1/signup with GitHub auth code
  2. Identity finds the existing user via email, notes 2FA is enabled
  3. API returns HTTP 202:
    {
      "status": "two_factor_required",
      "two_factor_token": "tf_xxxxxxxxxxxx",
      "message": "Two-factor authentication is required.",
      "next_step": {
        "method": "POST",
        "url": "/api/v1/signup/verify_2fa",
        "params": { "two_factor_token": "tf_xxx", "two_factor_code": "from authenticator" }
      }
    }
    
  4. Client prompts for 2FA code, sends:
    POST /api/v1/signup/verify_2fa
    { "two_factor_token": "tf_xxxxxxxxxxxx", "two_factor_code": "123456" }
    
  5. Identity validates 2FA, returns user hash
  6. SignupService creates the account
  7. Client receives full signup response

#Scenario 4: GitHub Returns No Verified Email

Context: A user's GitHub account has no verified email (or email is set to private with no public verified email).

  1. Client sends POST /api/v1/signup with GitHub auth code
  2. Identity exchanges code, calls GET /user/emails
  3. GitHub returns emails but none have verified: true
  4. API returns HTTP 422:
    {
      "error": "provider_email_unverified",
      "message": "Your email address is not verified with GitHub. Please verify your email at github.com and try again."
    }
    

Result: No account created. User must verify their email on GitHub first.

#Scenario 5: Social-Only User Loses GitHub Access

Context: A user signed up with GitHub only (no password). Their GitHub account gets compromised and they lose access.

  1. User cannot click "Sign in with GitHub" (account compromised)
  2. User clicks "Forgot password?" on the Identity login page
  3. Identity sends a password reset email to their verified email (auto-verified at signup)
  4. User sets a new password via the reset link
  5. User can now log in with email/password

Result: Recovery works because Identity auto-verified the email at social signup.

#Scenario 6: Invited User Claims Account with GitHub

Context: An account admin invites [email protected]. The invited user wants to claim their account using GitHub instead of setting a password.

  1. Admin invites user -> email sent with link to /users/claim/{invite_code}
  2. User clicks invite link -> sees claim form with password fields, SSO button, and social login buttons
  3. User clicks "Sign up with GitHub"
  4. OAuth flow: GitHub authorizes, Identity provisions user (same as normal social signup)
  5. Claim controller links the DeployHQ user to the new Identity account (atech_identity_key), clears invite code, marks as activated
  6. User is logged in to their new account

Result: Invited user has a DeployHQ account linked to GitHub. No password set. Same as the existing SSO claim path, but with GitHub/GitLab instead of SAML.

#Scenario 7: User Tries Both Auth Methods Simultaneously

Context: A client sends both email/password and provider params in the same request.

  1. Client sends:
    {
      "email": "[email protected]",
      "password": "secret123",
      "provider": "github",
      "provider_code": "abc123",
      "redirect_uri": "...",
      "code_verifier": "..."
    }
    
  2. API returns HTTP 422:
    {
      "error": "invalid_params",
      "message": "Cannot use both email/password and social login in the same request"
    }
    

#Social Signup

POST /api/v1/signup

Request (social login variant):

FieldTypeRequiredDescription
providerStringYesgithub, gitlab, or google
provider_codeStringYesOAuth authorization code
redirect_uriStringYesMust match the OAuth authorization request
code_verifierStringYesPKCE code verifier (RFC 7636)

Success response (201):

{
  "account": { "identifier": "...", "name": "...", "subdomain": "...", "url": "..." },
  "api_key": "...",
  "ssh_public_key": { "id": 1, "public_key": "ssh-rsa ...", "fingerprint": "..." },
  "oauth_urls": { "github": "...", "gitlab": "...", "bitbucket": "..." },
  "mcp_config": { "account": "...", "api_key": "...", "email": "..." },
  "email_verified": true
}

Error responses:

Error CodeHTTPWhen
invalid_params422Both email/password and provider params sent
missing_params422Provider present but code/redirect_uri/code_verifier missing
unsupported_provider422Provider not github, gitlab, or google
provider_email_unverified422No verified email from provider
provider_code_invalid422Code exchange failed (expired, used, redirect mismatch)
provider_code_verifier_invalid422PKCE verification failed
two_factor_required202User has 2FA enabled (see 2FA flow)
provider_unavailable502Provider API unreachable
provider_email_not_deliverable422Only noreply/non-deliverable email from provider
account_link_confirmation_required409Email matches existing account — confirmation email sent
invalid_redirect_uri422redirect_uri not in server-side allowlist

#2FA Completion

POST /api/v1/signup/verify_2fa
FieldTypeRequiredDescription
two_factor_tokenStringYesToken from the 202 response
two_factor_codeStringYes6-digit code from authenticator app

Returns the same 201 signup response on success, or 422 on failure.


#GitHub

DetailValue
OAuth URLhttps://github.com/login/oauth/authorize
Scopesuser:email
Token exchangePOST https://github.com/login/oauth/access_token
ProfileGET /user + GET /user/emails
Email verificationSelect first email where verified: true
UIDid field (integer, stable across username changes)
Token expiryNever (no refresh needed)

#GitLab

DetailValue
OAuth URLhttps://gitlab.com/oauth/authorize
Scopesread_user
Token exchangePOST https://gitlab.com/oauth/token
ProfileGET /api/v4/user
Email verificationCheck confirmed_at is not nil
UIDid field (integer, stable)
Token expiry2 hours (not stored — auth-only use)

#Google

DetailValue
OAuth URLhttps://accounts.google.com/o/oauth2/v2/auth
Scopesopenid email profile
Token exchangePOST https://oauth2.googleapis.com/token
ProfileID token (JWT) with sub, email, email_verified, name
Email verificationReject if email_verified: false
UIDsub claim (stable, opaque string)
Token expiryExpires (not stored — auth-only use)
NoteGoogle SAML SSO (enterprise) and Google social login (individual) are separate features

OAuth App credentials: You register a "GitHub OAuth App" (and a "GitLab OAuth App") in each provider's developer settings. This gives you a client_id + client_secret that identifies DeployHQ as an application. Every user who clicks "Sign up with GitHub" authenticates through this same OAuth App — each user authorizes with their own GitHub account, but the app credentials just identify DeployHQ. This is the same model the main app already uses for repo linking (config/external_api_keys.yml). Identity needs these credentials to exchange authorization codes server-side. Storage: either Identity's own config file or fetched from the main app's internal API (like SAML config). Determine during spike.

Login page visibility: Social login buttons show for all accounts that don't have enforce_sso: true. When SSO is enforced, the login page already hides password login - social buttons should not appear either. No new per-account toggle needed.

Signup API: Always available regardless of account SSO settings (new accounts don't have SSO configured yet).


#Token Handling

  • Authorization codes exchanged server-side only (Identity -> Provider)
  • Access tokens never sent to or from the client
  • Access tokens not stored in the database (not needed after profile fetch)
  • PKCE required for all API callers (public endpoint)

#Replay & CSRF Protection

  • Browser flow: state parameter in session (same as SAML's InResponseTo)
  • API flow: Codes are single-use + bound to redirect_uri + PKCE code_verifier

#Log Filtering

Identity's ApiController logs decoded params and sends to Sentry. Must add provider_code, access_token, code_verifier to filter lists before deploying social login code.

#Account Linking (diverges from SAML)

  • New users (no existing Identity account): auto-create user + OauthIdentity. Safe — no existing account to take over.
  • Existing users (Identity account already exists for this email): do NOT auto-link. Send a confirmation email instead. User must click to confirm the link. This prevents account takeover — anyone can create a GitHub account with your email.
  • Returning social users (OauthIdentity already exists for provider+uid): return existing user immediately.
  • Only with verified, deliverable provider emails — unverified and noreply emails rejected.

#Email Selection (GitHub-specific)

  • Prefer: primary: true AND verified: true AND NOT @users.noreply.github.com
  • Fallback: any verified: true AND NOT noreply
  • Reject with provider_email_not_deliverable if only noreply emails (breaks password-reset recovery)

#redirect_uri Allowlist

  • Browser clients: pre-registered HTTPS origins only
  • CLI/MCP: http://localhost:* and http://127.0.0.1:* (per RFC 8252)
  • Everything else rejected with invalid_redirect_uri

#Rate Limiting

  • Rack::Attack: 3 signups per IP per hour + 10 code exchanges per IP per hour
  • Circuit breaker: trip after 3 provider 5xx in 60 seconds, 5-minute cooldown
  • Per-provider feature flag kill switch

#2FA Policy

  • Social login respects 2FA: if enabled, user must still complete 2FA (same as SAML)
  • Browser: temporary auth-only session + existing 2FA prompt page
  • API: returns two_factor_required + token for follow-up call

#Recovery for Social-Only Users

  • Password reset via email works (email is auto-verified at signup)
  • Phase 3 adds "Set password" hint in account settings

These are chosen for consistency with existing SAML behavior:

DecisionDefaultRationale
Account linkingAuto-create new users; confirm before linking existingDiverges from SAML — consumer OAuth is self-service, auto-link is a takeover vector
2FARequired if enabledSame as SAML — bypassing would weaken accounts
PKCERequired for all API callersPublic endpoint, one code path
EnforcementNot enforced (opt-in per user)See below

Social login is not enforced at the account level. Unlike SAML SSO (which supports enforce_sso: true to force all users through the IdP), social login is an optional convenience method. Users can always fall back to email/password. This is intentional:

  • SSO enforcement exists for compliance (centralized auth, offboarding control, audit trails). Social login serves a different purpose: reducing signup/login friction for developers.
  • Enforcing GitHub/GitLab login would require all team members to have accounts on those platforms, which isn't reasonable.
  • Social login adds an OauthIdentity link — it doesn't replace or disable password authentication.

Team should confirm or override during the spike (Task 1.1).


#Phase 1: Identity Service (~8.5 days)

TaskEstDescription
1.1 Spike0.5dTeam validation, provider registration
1.2 Migration0.5doauth_identities table (mirrors saml_identities)
1.3 Model1dOauthIdentity with find_or_create_from_provider
1.4 Adapters1.5dOauthProviders::Github + OauthProviders::Gitlab + OauthProviders::Google
1.5 Controller1.5dAuthentication::SocialController (browser OAuth flow)
1.6 Routes0.5dSocial login routes in Identity (buttons live on DeployHQ)
1.7 API1dapi/register_with_social endpoint
1.8 Gems0.5doctokit + GitLab HTTP client
1.9 Log filtering0.5dRelease blocker — filter_params + Sentry before_send + verification test
1.10 redirect_uri allowlist0.5dServer-side validation, reject non-allowlisted URIs
1.11 Rate limiting0.5dRack::Attack, circuit breakers, per-provider kill switch
1.12 Account link confirmation1dConfirmation email for existing accounts (no auto-link)
1.13 Security review gate0.5dReview before proceeding to Phase 2
1.14 Tests1.5d19+ test cases (see acceptance criteria)

#Phase 2: Main App (~5 days)

TaskEstDescription
2.1 Client lib0.5dAtechIdentity.register_with_social
2.2 SignupService1dresolve_social_user! branch
2.3 Login page0.5dSocial buttons on deployhq.com/login + redirect to Identity
2.4 Signup controller0.5dAccept new params, filtering
2.5 2FA endpoint1dPOST /api/v1/signup/verify_2fa
2.6 OAS docs0.5dAnnotations for both endpoints
2.7 Tests1.5d18 test cases (see acceptance criteria)

#Phase 2.5: Invite Claim Flow (~1 day)

TaskEstDescription
2.8 Invite claim1dSocial buttons on /users/claim/{code} page (follows existing SSO claim pattern)

#Phase 3: Edge Cases (~2 days)

TaskEstDescription
3.1 Email collision0.5dAuto-link with verified emails
3.2 Unverified emails0.5dReject with clear error
3.3 Set password0.5dUI hint for social-only users
3.4 Semantics guard0.5dOauthIdentity vs ExternalAuthentication

#Phase 4: Documentation & CLI (~3 days)

TaskEstDescription
4.1 API docs0.5dSupport articles with PKCE examples
4.2 CLI flow2dBrowser OAuth + localhost callback + PKCE
4.3 MCP update0.5dSocial signup support

Total: ~23 days = ~4.5 weeks

Phase 1 grew by ~3 days due to security controls promoted from Phase 3 (redirect_uri allowlist, rate limiting, account link confirmation, security review gate).

MVP (2.5 weeks): GitHub only, defer GitLab + Google + CLI + "Set password" UI.


#Identity Service (19 tests)

#Test CaseExpected
1New user, GitHub, verified emailCreates User + OauthIdentity
2New user, GitLab, confirmed emailCreates User + OauthIdentity
2bNew user, Google, verified emailCreates User + OauthIdentity
3Existing user, email matchesLinks OauthIdentity to existing user
4Returning user, OauthIdentity existsUpdates last_authenticated_at
5GitHub, no verified emailprovider_email_unverified error
6GitLab, confirmed_at: nilprovider_email_unverified error
7Replayed authorization codeprovider_code_invalid error
8Expired authorization codeprovider_code_invalid error
9redirect_uri mismatchprovider_code_invalid error
10code_verifier mismatchprovider_code_verifier_invalid error
11code_verifier missingmissing_params error
12Browser: successful loginLoginToken created, redirect to app
13Browser: state mismatchRedirect to login with error
14Browser: provider errorRedirect to login with error
15Browser: user has 2FATemp session + 2FA prompt
16API: user has 2FAtwo_factor_required response
17Browser: no Assignment"No access" error
18Provider API downprovider_unavailable error
19Sensitive params not in logs/SentryVerified by grep

#Main App (18 tests)

#Test CaseExpected
1Valid GitHub signup201 with full response
2Valid GitLab signup201 with full response
2bValid Google signup201 with full response
3Mixed email/password + provider422 invalid_params
4Missing provider_code422 missing_params
5Missing code_verifier422 missing_params
6Missing redirect_uri422 missing_params
7Unsupported provider422 unsupported_provider
8Identity: email unverified422, no account
9Identity: code invalid422, no account
10Identity: provider down502, no account
11User has 2FA202 with token + next_step
122FA: valid completion201 with full response
132FA: invalid code422, no account
142FA: expired token422, no account
152FA: replay (token reused)422, no account
16Email/password regression201 unchanged
17Rate limiting (3/IP/hour)429
18Params filtered from logsVerified

RiskSeverityMitigation
Token leakage in logs/SentryHighLog filtering deploys before any social login code
GitHub private emailsMedium/user/emails endpoint + reject if no verified email
Account takeover via emailMediumAuto-link only with verified emails (same as SAML)
Social-only lockoutLowPassword reset via verified email
2FA complexityMediumFollow existing SAML/password patterns

  • Per-provider feature flags: social_login_github, social_login_gitlab, social_login_google — each can be enabled/disabled independently without deploying code
  • Staged rollout: enable for internal accounts first, then gradually expand
  • Kill switch: disable all social signup instantly if issues detected
  • Support runbook: how to unlink a mislinked OauthIdentity, disable for a specific user, investigate failed signups
  • Rollback: disabling the flag stops new social signups but doesn't break existing linked users (they fall back to password reset)

  1. Review defaults: Auto-link by verified email, 2FA required if enabled, PKCE required for all API callers. Override any of these?

Everything in the "Reference" column already exists and works in production:

What We NeedWhat Already Exists
External identity modelSamlIdentity (copy pattern for OauthIdentity)
Passwordless user creationSamlIdentity creates users with random passwords
Cross-service session bridgeLoginToken (30-sec, single-use, provider-agnostic)
JIT user provisioningfind_or_create_from_assertion
Account context in sessionapplication_sessions_controller
Token consumptionsessions_controller + LoginToken.authenticate
2FA flowApiController#authenticate + Authie::Session
Provider config fetchingSamlConfigClient pattern

5 comments

ClaudeagentApr 17

## Review Council Assessment Thanks for running the review council. Here's our triage: ### Must Fix (updating the plan) **#1 Auto-link takeover** — Agreed, biggest security gap. SAML trust is org-managed; GitHub/GitLab are self-service. Fix: auto-link only for brand-new users (no existing Identity account). For existing accounts, require a confirmation email or authenticated session before linking OauthIdentity. Intentional divergence from SAML. **#2 Rate limiting** — Plan mentions 3/IP/hour but lacks specifics. Fix: add Rack::Attack rules per IP and per provider UID, circuit breakers on provider calls, feature-flag kill switch per provider. **#3 redirect_uri allowlist** — Valid. Fix: server-side allowlist — HTTPS origins for browser clients, localhost/127.0.0.1 for CLI (per RFC 8252). Reject everything else with invalid_redirect_uri error. **#8 Email selection rules** — Valid. GitHub's @users.noreply.github.com is marked verified:true but not deliverable, which breaks the password-reset recovery guarantee. Fix: specify algorithm — prefer primary + verified + not noreply, reject if only noreply with provider_email_not_deliverable error. **#10 Invite hijacking** — Valid. Fix: require BOTH the one-time invite token AND social email match. Not email match alone. ### Should Fix (adding to plan) **#4 2FA token binding** — Add IP binding, max 3 OTP attempts, invalidate on failure. **#5 Atomicity** — Add idempotent create keyed on (provider, uid) with unique index. Wrap local steps in DB transaction. Add retry test cases. **#7 Log filtering depth** — Promote to Phase 1 release blocker. Add Sentry before_send hook, disable verbose HTTP logging for provider adapters, add test that forces exception during exchange and verifies no token in captured payload. **#9 OauthIdentity schema** — Add explicit migration block with length: 191 per DeployHQ MariaDB conventions. **#11 Rollout strategy** — Add per-provider feature flags, kill switch, support runbook. **#12 Security in Phase 1** — Agree. Promote rate limiting, redirect allowlist, log filtering, and conflicting-param guards into Phase 1. Security review gate before Phase 2. ### Already Addressed **#6 Architecture boundary** — The plan already specifies all provider exchanges happen in Identity. Main App never talks to GitHub/GitLab/Google directly. Sequence diagrams in the current version show this. Client secrets live only in Identity. ### Summary 11 of 12 findings are valid. #1 (auto-link takeover) is the most important — we'll diverge from SAML here and require confirmation for existing accounts. Will update the plan with all fixes.

ClaudeagentApr 17

Same user. The lookup order in find_or_create_from_provider is: (1) match by (provider, uid) on OauthIdentity — is this the same GitLab user coming back? (2) if no match, fall back to User.locate(email) — does this email already belong to someone? In this scenario: step 1 finds no GitLab OauthIdentity, so step 2 finds the existing user by email. Result: one user with two OauthIdentity records (GitHub + GitLab). They can log in with either provider or use password reset.

Thiago DApr 17

I also gave it to the review council, and here's the report: Review Council Report Target: Social Login for Signup API — design plan (https://draftmark.app/share/hfKrKe3Z.md) Type: Plan/Document Reviewers: Claude, Codex, Gemini (3 participating — skipped: Perplexity, PERPLEXITY_API_KEY not set) Rounds: 1 Consensus: Strong Critical Issues 1. Auto-link by verified email is an account-takeover vector for consumer OAuth providers (all 3 reviewers) The plan inherits SAML's "link by verified email, no reverification" policy. SAML trust is anchored in an organization-managed IdP; GitHub/GitLab are self-service. A verified email match alone can bind a personal social account to an existing DeployHQ user — including accounts with API keys and SSH keys. Fix: Do NOT auto-link existing accounts on first social sign-in. Require an authenticated session or emailed confirmation link before activating an OauthIdentity. Reserve auto-link-by-email for brand-new user creation only. Document as an intentional divergence from SAML. 2. Public endpoint returns API key + SSH private key with no rate-limiting design (all 3 reviewers) The endpoint is unauthenticated, performs expensive work (provider exchange, DB writes, SSH keygen), and returns high-value credentials. "Rate limiting" appears only as a test-case mention. Fix: Specify concrete controls in the plan — Rack::Attack rules per IP and per provider UID, circuit breakers on provider calls, CAPTCHA/Turnstile for browser signup, anomaly monitoring, and a feature-flag kill switch. Consider deferring API key issuance until first verified session. 3. Missing redirect_uri server-side allowlist (all 3 reviewers) The plan requires redirect_uri to match the authorization request (standard OAuth) but does not specify server-side allowlist validation. Unvalidated redirect_uri on a public endpoint is the classic OAuth open-redirect / code-interception primitive. Fix: Add an explicit allowlist: pre-registered HTTPS origins for browser clients, http://127.0.0.1:* / http://localhost:* for CLI per RFC 8252, and a registered custom scheme if needed. Reject everything else with invalid_redirect_uri. Specify in the API contract. Important Findings 4. 2FA handoff token is underspecified (all 3 reviewers) The 202-response token has no defined TTL, storage, binding, single-use semantics, or attempt limits. A poorly designed handoff token becomes a bearer credential that effectively bypasses the OAuth factor. Fix: Specify ≤5-minute TTL, single-use, server-side storage (not JWT), bound to (user_id, ip, user-agent), invalidated after N failed OTP attempts, rate-limited per IP. Reuse LoginToken if possible; do NOT persist OauthIdentity linkage or signup side effects until 2FA completes. 5. Partial-failure / atomicity boundary undefined (all 3 reviewers) Signup spans provider code exchange → Identity user creation → main-app account → Billy → SSH keypair → OauthIdentity link. Auth codes are single-use; retries after partial success break. The plan does not define transactional boundaries or idempotency. Fix: Define idempotent create/link keyed on (provider, uid) with unique index. Wrap local persistence in a DB transaction. For cross-service steps, design a resumable handoff (server-side signup ticket) or a background reaper for orphaned Identity users. Add explicit tests for retry after DB error, timeout after exchange, and concurrent signup. 6. Architectural boundary: where does the OAuth exchange actually happen? (Gemini — distinct issue) The plan places social login "in Identity" but exposes POST /api/v1/signup on the Main App. It's unclear whether Main App forwards to Identity, calls providers directly, or whether client secrets live in two places. Secret duplication increases attack surface and complicates rotation. Fix: Add a sequence diagram. Centralize all provider code exchanges and client secrets in Identity. Main App's signup endpoint should consume a verified identity object or short-lived provisioning ticket from Identity, never talk to GitHub/GitLab directly. 7. Log/Sentry token-leakage mitigation is one line; plan names it the #1 risk (Claude, Codex) config.filter_parameters handles top-level params, but Sentry breadcrumbs, exception locals, Faraday/OAuth2 HTTP debug logs, and net-http instrumentation routinely bypass it. Fix: Promote to Phase 1 release blocker: explicit Sentry before_send hook scrubbing provider_code/access_token/code_verifier from breadcrumbs + request body + exception context; disable verbose HTTP client logging in prod for provider adapters; unit test that forces an exception during exchange and asserts the captured payload contains no token material; enumerate every gem touching the provider HTTP call. 8. Provider email selection rules unspecified (Claude, Codex) GitHub commonly returns @users.noreply.github.com (marked verified: true), plus potentially multiple verified emails. GitLab's confirmed_at semantics differ. The plan does not define which email wins, or whether noreply is acceptable. Accepting noreply breaks the "social-only users can reset password" guarantee. Fix: Specify the algorithm: prefer primary && verified && !noreply, fall back to verified && !noreply, reject if only noreply exists with a distinct provider_email_not_deliverable error. Add contract tests against representative GitHub/GitLab payloads (private email, multiple verified, missing public email). 9. OauthIdentity schema is not specified anywhere in the plan (Claude) The model is referenced throughout but columns, uniqueness, and indexes are missing. Risk of duplicate links, N+1 lookups, and missing length: 191 per DeployHQ's MariaDB conventions. Fix: Add an explicit schema block: provider, uid, user_id, email_at_link, last_authenticated_at, with add_index [:provider, :uid], unique: true, length: { provider: 32, uid: 191 } and a secondary index on user_id. Clarify the plan's "tokens never stored" claim at the model level. 10. Invited-user claim via social login may enable invite hijacking (Claude) Invitations are keyed on email; if "GitHub-verified email matches invite email" alone satisfies the claim, an attacker who controls a GitHub account with that email can claim an invite the legitimate invitee never saw. Fix: Require both the one-time invite token AND social-identity email match. Or restrict social claim to new-user creation and require password claim + later linking for invites. 11. Rollout/rollback strategy missing for an auth-surface change (Codex) No feature flags per provider, no staged rollout, no "log-but-don't-link" shadow mode, no support playbook for mislinked accounts, no disable switch. Fix: Add a rollout section with per-provider feature flags, shadow-mode option, dashboards for signup success/failure and linking decisions, a support runbook, and a documented rollback path that disables new social signups without breaking existing linked users. Suggestions 12. Timeline underfunds the security surface (Claude) 8.5 days for all Identity work is aggressive; "semantic guards" and other hardening are deferred to Phase 3 but should be Phase-1 release blockers. Security work phased late tends to get cut under deadline pressure. Fix: Promote rate limiting, redirect allowlist, log-filter coverage tests, and conflicting-param guards into Phase 1. Add an explicit security-review gate before Phase 2. Frame as GitHub-first MVP (~2 weeks) with GitLab as fast-follow. What's Done Well - Clear reasoning for placing social login in Identity (not Main App), matching the existing auth boundary. - Mandatory PKCE for all API callers — including the first-party CLI — is the right call. - Server-side-only token handling, refusing to expose access tokens to clients, matches OAuth best current practice. - Reuse of the LoginToken session-bridging pattern avoids inventing new session-handoff primitives. --- Bottom line: Not ready to implement as written. Architecture is sound; the controls that separate a safe public auth endpoint from a liability are under-specified. Single most important thing to address is finding #1 — auto-link-by-email for consumer OAuth providers is an account-takeover vector and should not inherit SAML's no-reverification policy. let's review them and discuss which points make sense for us

Thiago DApr 17

If a user signs with github using email `[email protected]` and then, it tries to signup again with gitlab using the same email `[email protected]`. Would that be two separate users on our side? Or the same user? (assume for the same account here)

+1 more