# 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)

---

## Objective

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.)

---

## Architecture Overview

DeployHQ uses a two-tier auth system:

| Component | Current 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 Diagrams

### Flow 1: Browser Login (Existing Account)

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

```mermaid
sequenceDiagram
    participant User
    participant DeployHQ as DeployHQ (Main App)
    participant Identity as Identity Service
    participant GitHub

    User->>DeployHQ: Visits deployhq.com/login
    Note over DeployHQ: Login form with email/password,<br/>SSO button, and social login buttons
    User->>DeployHQ: Clicks "Sign in with GitHub"
    DeployHQ->>Identity: Redirect to /authentication/social/login/github?account=acme-corp
    Identity->>Identity: Store application + account + state in session
    Identity->>GitHub: Redirect to github.com/login/oauth/authorize
    GitHub->>User: Show authorization prompt
    User->>GitHub: Authorize
    GitHub->>Identity: Callback with authorization code + state
    Identity->>Identity: Validate state against session
    Identity->>GitHub: Exchange code for access token (server-side)
    GitHub-->>Identity: Access token
    Identity->>GitHub: GET /user + GET /user/emails
    GitHub-->>Identity: Profile + verified emails
    Identity->>Identity: OauthIdentity.find_or_create_from_provider()
    Identity->>Identity: Look up user's Assignment for application
    Identity->>Identity: Create LoginToken (30-sec TTL)
    Identity->>DeployHQ: Redirect to /identity/accept/{token}
    DeployHQ->>DeployHQ: Consume token, create session
    DeployHQ->>User: Logged in
```

**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):

```mermaid
sequenceDiagram
    participant Client as Client (CLI/MCP/Browser)
    participant GitHub
    participant API as DeployHQ Signup API
    participant Identity as Identity Service
    participant Billy as Billy (Billing)

    Client->>Client: Generate PKCE code_verifier + code_challenge
    Client->>GitHub: Open browser to OAuth consent page (with code_challenge)
    GitHub->>Client: Authorization code via localhost callback
    Client->>API: POST /api/v1/signup {provider, provider_code, redirect_uri, code_verifier}
    API->>Identity: POST /api/register_with_social {provider, code, redirect_uri, code_verifier}
    Identity->>GitHub: Exchange code + code_verifier for access token
    GitHub-->>Identity: Access token
    Identity->>GitHub: GET /user + GET /user/emails
    GitHub-->>Identity: Profile + verified emails
    Identity->>Identity: OauthIdentity.find_or_create_from_provider()
    Identity-->>API: User hash (same format as email/password registration)
    API->>Billy: Create billing account
    Billy-->>API: Account created
    API->>API: Create Account, API key, SSH key pair
    API->>Identity: Assign user to account
    API-->>Client: 201 {account, api_key, ssh_public_key, oauth_urls, mcp_config}
```

**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:

```mermaid
sequenceDiagram
    participant User
    participant DeployHQ as DeployHQ (Main App)
    participant Identity as Identity Service
    participant GitHub
    participant Billy as Billy (Billing)

    User->>DeployHQ: Visits deployhq.com/signup
    Note over DeployHQ: Signup form with email/password<br/>and "Sign up with GitHub" button
    User->>DeployHQ: Clicks "Sign up with GitHub"
    DeployHQ->>Identity: Redirect to /authentication/social/signup/github
    Identity->>Identity: Store state + return_to in session
    Identity->>GitHub: Redirect to github.com/login/oauth/authorize
    GitHub->>User: Show authorization prompt
    User->>GitHub: Authorize
    GitHub->>Identity: Callback with authorization code + state
    Identity->>Identity: Validate state against session
    Identity->>GitHub: Exchange code for access token (server-side)
    GitHub-->>Identity: Access token
    Identity->>GitHub: GET /user + GET /user/emails
    GitHub-->>Identity: Profile + verified emails
    Identity->>Identity: OauthIdentity.find_or_create_from_provider()
    Identity-->>DeployHQ: Redirect back with user token/session
    DeployHQ->>Billy: Create billing account
    Billy-->>DeployHQ: Account created
    DeployHQ->>DeployHQ: Create Account, API key, SSH key pair
    DeployHQ->>Identity: Assign user to account
    DeployHQ->>User: Welcome! Your account is ready.
```

**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:

```mermaid
flowchart TD
    A[Provider returns verified email + UID] --> B{OauthIdentity exists for provider+UID?}
    B -->|Yes| C[Return existing user - update last_authenticated_at]
    B -->|No| D{User.locate finds user by verified email?}
    D -->|Yes| E[Link new OauthIdentity to existing user]
    D -->|No| F[Create new user with random password]
    F --> G[Auto-verify email]
    G --> H[Create OauthIdentity link]
    E --> I{User has 2FA enabled?}
    H --> I
    C --> I
    I -->|Yes, Browser| J[Temp auth-only session -> 2FA prompt]
    I -->|Yes, API| K[Return two_factor_required + token]
    I -->|No, Browser| L[Create LoginToken -> redirect to app]
    I -->|No, API| M[Return user hash]

    style A fill:#e1f5fe
    style C fill:#c8e6c9
    style E fill:#fff9c4
    style F fill:#fff9c4
    style J fill:#ffecb3
    style K fill:#ffecb3
    style L fill:#c8e6c9
    style M fill:#c8e6c9
```

---

## Concrete Scenarios

### 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: `dev@example.com`, 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.

### Scenario 2: Existing User Links GitHub to Their Account

**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: `user@company.com`, verified)
4. Identity calls `User.locate("user@company.com")` — 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:
   ```json
   {
     "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:
   ```json
   {
     "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 `dev@example.com`. 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:
   ```json
   {
     "email": "user@example.com",
     "password": "secret123",
     "provider": "github",
     "provider_code": "abc123",
     "redirect_uri": "...",
     "code_verifier": "..."
   }
   ```
2. API returns HTTP 422:
   ```json
   {
     "error": "invalid_params",
     "message": "Cannot use both email/password and social login in the same request"
   }
   ```

---

## API Contract

### Social Signup

```
POST /api/v1/signup
```

**Request (social login variant):**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `provider` | String | Yes | `github`, `gitlab`, or `google` |
| `provider_code` | String | Yes | OAuth authorization code |
| `redirect_uri` | String | Yes | Must match the OAuth authorization request |
| `code_verifier` | String | Yes | PKCE code verifier (RFC 7636) |

**Success response (201):**
```json
{
  "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 Code | HTTP | When |
|------------|------|------|
| `invalid_params` | 422 | Both email/password and provider params sent |
| `missing_params` | 422 | Provider present but code/redirect_uri/code_verifier missing |
| `unsupported_provider` | 422 | Provider not `github`, `gitlab`, or `google` |
| `provider_email_unverified` | 422 | No verified email from provider |
| `provider_code_invalid` | 422 | Code exchange failed (expired, used, redirect mismatch) |
| `provider_code_verifier_invalid` | 422 | PKCE verification failed |
| `two_factor_required` | 202 | User has 2FA enabled (see 2FA flow) |
| `provider_unavailable` | 502 | Provider API unreachable |
| `provider_email_not_deliverable` | 422 | Only noreply/non-deliverable email from provider |
| `account_link_confirmation_required` | 409 | Email matches existing account — confirmation email sent |
| `invalid_redirect_uri` | 422 | redirect_uri not in server-side allowlist |

### 2FA Completion

```
POST /api/v1/signup/verify_2fa
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `two_factor_token` | String | Yes | Token from the 202 response |
| `two_factor_code` | String | Yes | 6-digit code from authenticator app |

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

---

## Provider Contracts

### GitHub

| Detail | Value |
|--------|-------|
| OAuth URL | `https://github.com/login/oauth/authorize` |
| Scopes | `user:email` |
| Token exchange | `POST https://github.com/login/oauth/access_token` |
| Profile | `GET /user` + `GET /user/emails` |
| Email verification | Select first email where `verified: true` |
| UID | `id` field (integer, stable across username changes) |
| Token expiry | Never (no refresh needed) |

### GitLab

| Detail | Value |
|--------|-------|
| OAuth URL | `https://gitlab.com/oauth/authorize` |
| Scopes | `read_user` |
| Token exchange | `POST https://gitlab.com/oauth/token` |
| Profile | `GET /api/v4/user` |
| Email verification | Check `confirmed_at` is not nil |
| UID | `id` field (integer, stable) |
| Token expiry | 2 hours (not stored — auth-only use) |

### Google

| Detail | Value |
|--------|-------|
| OAuth URL | `https://accounts.google.com/o/oauth2/v2/auth` |
| Scopes | `openid email profile` |
| Token exchange | `POST https://oauth2.googleapis.com/token` |
| Profile | ID token (JWT) with `sub`, `email`, `email_verified`, `name` |
| Email verification | Reject if `email_verified: false` |
| UID | `sub` claim (stable, opaque string) |
| Token expiry | Expires (not stored — auth-only use) |
| Note | Google SAML SSO (enterprise) and Google social login (individual) are separate features |

---

## Configuration

**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).

---

## Security Design

### 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

---

## Policy Decisions (Defaults for Team Review)

These are chosen for consistency with existing SAML behavior:

| Decision | Default | Rationale |
|----------|---------|-----------|
| Account linking | Auto-create new users; **confirm before linking existing** | Diverges from SAML — consumer OAuth is self-service, auto-link is a takeover vector |
| 2FA | Required if enabled | Same as SAML — bypassing would weaken accounts |
| PKCE | Required for all API callers | Public endpoint, one code path |
| Enforcement | **Not 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).

---

## Implementation Phases

### Phase 1: Identity Service (~8.5 days)

| Task | Est | Description |
|------|-----|-------------|
| 1.1 Spike | 0.5d | Team validation, provider registration |
| 1.2 Migration | 0.5d | `oauth_identities` table (mirrors `saml_identities`) |
| 1.3 Model | 1d | `OauthIdentity` with `find_or_create_from_provider` |
| 1.4 Adapters | 1.5d | `OauthProviders::Github` + `OauthProviders::Gitlab` + `OauthProviders::Google` |
| 1.5 Controller | 1.5d | `Authentication::SocialController` (browser OAuth flow) |
| 1.6 Routes | 0.5d | Social login routes in Identity (buttons live on DeployHQ) |
| 1.7 API | 1d | `api/register_with_social` endpoint |
| 1.8 Gems | 0.5d | `octokit` + GitLab HTTP client |
| 1.9 Log filtering | 0.5d | Release blocker — filter_params + Sentry before_send + verification test |
| 1.10 redirect_uri allowlist | 0.5d | Server-side validation, reject non-allowlisted URIs |
| 1.11 Rate limiting | 0.5d | Rack::Attack, circuit breakers, per-provider kill switch |
| 1.12 Account link confirmation | 1d | Confirmation email for existing accounts (no auto-link) |
| 1.13 Security review gate | 0.5d | Review before proceeding to Phase 2 |
| 1.14 Tests | 1.5d | 19+ test cases (see acceptance criteria) |

### Phase 2: Main App (~5 days)

| Task | Est | Description |
|------|-----|-------------|
| 2.1 Client lib | 0.5d | `AtechIdentity.register_with_social` |
| 2.2 SignupService | 1d | `resolve_social_user!` branch |
| 2.3 Login page | 0.5d | Social buttons on `deployhq.com/login` + redirect to Identity |
| 2.4 Signup controller | 0.5d | Accept new params, filtering |
| 2.5 2FA endpoint | 1d | `POST /api/v1/signup/verify_2fa` |
| 2.6 OAS docs | 0.5d | Annotations for both endpoints |
| 2.7 Tests | 1.5d | 18 test cases (see acceptance criteria) |

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

| Task | Est | Description |
|------|-----|-------------|
| 2.8 Invite claim | 1d | Social buttons on `/users/claim/{code}` page (follows existing SSO claim pattern) |

### Phase 3: Edge Cases (~2 days)

| Task | Est | Description |
|------|-----|-------------|
| 3.1 Email collision | 0.5d | Auto-link with verified emails |
| 3.2 Unverified emails | 0.5d | Reject with clear error |
| 3.3 Set password | 0.5d | UI hint for social-only users |
| 3.4 Semantics guard | 0.5d | OauthIdentity vs ExternalAuthentication |

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

| Task | Est | Description |
|------|-----|-------------|
| 4.1 API docs | 0.5d | Support articles with PKCE examples |
| 4.2 CLI flow | 2d | Browser OAuth + localhost callback + PKCE |
| 4.3 MCP update | 0.5d | Social 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.

---

## Acceptance Criteria

### Identity Service (19 tests)

| # | Test Case | Expected |
|---|-----------|----------|
| 1 | New user, GitHub, verified email | Creates User + OauthIdentity |
| 2 | New user, GitLab, confirmed email | Creates User + OauthIdentity |
| 2b | New user, Google, verified email | Creates User + OauthIdentity |
| 3 | Existing user, email matches | Links OauthIdentity to existing user |
| 4 | Returning user, OauthIdentity exists | Updates `last_authenticated_at` |
| 5 | GitHub, no verified email | `provider_email_unverified` error |
| 6 | GitLab, `confirmed_at: nil` | `provider_email_unverified` error |
| 7 | Replayed authorization code | `provider_code_invalid` error |
| 8 | Expired authorization code | `provider_code_invalid` error |
| 9 | `redirect_uri` mismatch | `provider_code_invalid` error |
| 10 | `code_verifier` mismatch | `provider_code_verifier_invalid` error |
| 11 | `code_verifier` missing | `missing_params` error |
| 12 | Browser: successful login | LoginToken created, redirect to app |
| 13 | Browser: `state` mismatch | Redirect to login with error |
| 14 | Browser: provider error | Redirect to login with error |
| 15 | Browser: user has 2FA | Temp session + 2FA prompt |
| 16 | API: user has 2FA | `two_factor_required` response |
| 17 | Browser: no Assignment | "No access" error |
| 18 | Provider API down | `provider_unavailable` error |
| 19 | Sensitive params not in logs/Sentry | Verified by grep |

### Main App (18 tests)

| # | Test Case | Expected |
|---|-----------|----------|
| 1 | Valid GitHub signup | 201 with full response |
| 2 | Valid GitLab signup | 201 with full response |
| 2b | Valid Google signup | 201 with full response |
| 3 | Mixed email/password + provider | 422 `invalid_params` |
| 4 | Missing `provider_code` | 422 `missing_params` |
| 5 | Missing `code_verifier` | 422 `missing_params` |
| 6 | Missing `redirect_uri` | 422 `missing_params` |
| 7 | Unsupported provider | 422 `unsupported_provider` |
| 8 | Identity: email unverified | 422, no account |
| 9 | Identity: code invalid | 422, no account |
| 10 | Identity: provider down | 502, no account |
| 11 | User has 2FA | 202 with token + next_step |
| 12 | 2FA: valid completion | 201 with full response |
| 13 | 2FA: invalid code | 422, no account |
| 14 | 2FA: expired token | 422, no account |
| 15 | 2FA: replay (token reused) | 422, no account |
| 16 | Email/password regression | 201 unchanged |
| 17 | Rate limiting (3/IP/hour) | 429 |
| 18 | Params filtered from logs | Verified |

---

## Risks

| Risk | Severity | Mitigation |
|------|----------|------------|
| Token leakage in logs/Sentry | **High** | Log filtering deploys **before** any social login code |
| GitHub private emails | Medium | `/user/emails` endpoint + reject if no verified email |
| Account takeover via email | Medium | Auto-link only with verified emails (same as SAML) |
| Social-only lockout | Low | Password reset via verified email |
| 2FA complexity | Medium | Follow existing SAML/password patterns |

---

## Rollout Strategy

- **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)

---

## Questions for Team

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

---

## Key Reusable Infrastructure

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

| What We Need | What Already Exists |
|--------------|-------------------|
| External identity model | `SamlIdentity` (copy pattern for `OauthIdentity`) |
| Passwordless user creation | `SamlIdentity` creates users with random passwords |
| Cross-service session bridge | `LoginToken` (30-sec, single-use, provider-agnostic) |
| JIT user provisioning | `find_or_create_from_assertion` |
| Account context in session | `application_sessions_controller` |
| Token consumption | `sessions_controller` + `LoginToken.authenticate` |
| 2FA flow | `ApiController#authenticate` + `Authie::Session` |
| Provider config fetching | `SamlConfigClient` pattern |
