Social Login for Signup API — Feasibility & Plan

publicv9
1d ago
6 views5 comments1 reviews19 min read

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

comments (0)

reviews (0)