xtablo-source/.planning/phases/08-social-sign-in/08-RESEARCH.md
2026-05-15 20:41:58 +02:00

12 KiB

Phase 8: Social Sign-in - Research

Researched: 2026-05-15 Domain: Google OAuth/OIDC, Sign in with Apple, local account linking, Go server-managed sessions, sqlc/Postgres migration impact Confidence: HIGH

Summary

Phase 8 adds Google and Apple sign-in without changing the core auth authority: Xtablo still owns users, sessions, CSRF, and cookies. Provider tokens are only proof used during callback processing. After verification, the Go server creates or links a local user, then issues the existing Xtablo session cookie through auth.Store.

The safest implementation shape is:

  1. Add user_identities and make users.password_hash nullable.
  2. Add provider config parsing and disabled-button state.
  3. Add Google start/callback.
  4. Add Apple start/callback, including ES256 client-secret generation and Apple ID token verification.
  5. Add linked-provider status view.
  6. Verify existing email/password behavior has not regressed.

Use golang.org/x/oauth2 for authorization-code exchange. Use an OIDC/JWT verification package rather than hand-parsing ID tokens. Keep provider subject as the durable external identity key; email can change and is not the identity key after first link.

User Decisions From CONTEXT.md

  • Auto-link provider login to an existing Xtablo user when provider email is verified and matches.
  • Reject provider callbacks with missing or unverified email.
  • Provider subject wins after it is linked, even if email later changes.
  • Add a simple linked-provider status view; unlinking is deferred.
  • Make users.password_hash nullable and allow social-only users.
  • Defer add-password/password management.
  • Block email/password signup if the email already belongs to a social-only account.
  • Existing password users who link Google/Apple keep password login.
  • Show Google and Apple buttons on both login and signup.
  • Show disabled provider buttons when config is missing.
  • Give Google and Apple equal prominence.
  • Redirect successful social sign-in to /.
  • Store provider display name and avatar URL when available.
  • Accept Apple private relay emails as verified when Apple verifies them.
  • Update both user_identities.email and local users.email when provider email changes, with explicit conflict handling.

Phase Requirements

ID Description Research Support
AUTH-08 User can start Google sign-in from login/signup Provider buttons + /auth/google/start route; disabled config state
AUTH-09 Google callback validates state, exchanges code, verifies ID token, creates/links local user OAuth2 auth code flow + ID token verification + account linking transaction
AUTH-10 User can start Apple sign-in from login/signup Provider buttons + /auth/apple/start route; Apple Services ID config
AUTH-11 Apple callback validates state/nonce, exchanges code, verifies ID token, creates/links local user Apple token endpoint + ES256 client secret + nonce/state validation
AUTH-12 Social sign-in issues existing server-managed session cookie Reuse auth.Store.Create / auth.SetSessionCookie
AUTH-13 Existing email/password auth still works Regression tests around signup/login/logout/CSRF/rate limiting

Standard Stack

Library / API Purpose Recommendation
golang.org/x/oauth2 OAuth2 authorization URL and code exchange Add as a direct dependency
github.com/coreos/go-oidc/v3/oidc OIDC discovery, JWKS, ID token verification Good fit for Google; can also verify ID tokens when issuer/JWKS are available
github.com/go-jose/go-jose/v4 JOSE/JWT primitives, ES256 client secret signing Good fit for Apple client-secret JWT if go-oidc does not cover all Apple needs cleanly
Google OpenID Connect Provider docs and ID token semantics Use official docs for issuer/audience/email_verified behavior
Apple Sign in REST API Authorization/token/client-secret rules Use official docs for client_secret, token exchange, and ID token validation

Avoid:

  • Managed auth platforms such as Clerk/Auth0/Lucia.
  • Treating provider access tokens as Xtablo sessions.
  • Linking on unverified email.
  • Relinking a provider subject based on changed email.

Architecture Responsibility Map

Capability Primary Tier Secondary Tier Rationale
Provider start URL Go web handler Browser redirect Server owns state/nonce generation and config
OAuth state/nonce storage Go server + short-lived cookie or DB table Browser returns state Callback must prove it belongs to an initiated flow
Code exchange Go server Provider token endpoint Client secret stays server-side
ID token verification Go server Provider JWKS Must validate issuer, audience, expiry, nonce where applicable, subject, verified email
Account linking Postgres transaction Go handler Race-safe lookup/create/link logic
Local session creation Existing auth.Store Browser cookie Xtablo session remains authoritative
Provider status view Go templates Authenticated browser Minimal account visibility; no unlink actions

Data Model Research

users changes

Current users.password_hash text NOT NULL blocks social-only accounts. Phase 8 should migrate to nullable:

  • password_hash text NULL
  • Email/password login must treat NULL as "no password set" and return the same generic login error used for invalid credentials.
  • Signup must check existing users by email before insert. If an existing user has password_hash IS NULL, block email/password signup and tell the user to sign in with the provider.

New user_identities

Recommended columns:

  • id uuid primary key default gen_random_uuid()
  • user_id uuid not null references users(id) on delete cascade
  • provider text not null or enum constrained to google, apple
  • provider_subject text not null
  • email citext not null
  • email_verified boolean not null
  • display_name text
  • avatar_url text
  • created_at timestamptz not null default now()
  • updated_at timestamptz not null default now()
  • last_login_at timestamptz

Indexes/constraints:

  • unique(provider, provider_subject)
  • index(user_id)
  • optional check(email_verified = true) if unverified identities are never stored

Linking should happen in a DB transaction:

  1. Look up identity by (provider, provider_subject).
  2. If found: update identity profile/email metadata, update local users.email if needed, create session for identity's user_id.
  3. If not found: require verified email.
  4. Look up local users.email.
  5. If found: insert identity for that user.
  6. If not found: create social-only user with nullable password_hash, insert identity.

Conflict case: if provider subject is linked to user A and provider email changes to an email owned by user B, D-03 says provider identity wins. The planner must handle the users.email uniqueness collision from D-17 explicitly. Recommended behavior: keep user A signed in, update user_identities.email, do not update users.email, and surface/log a conflict for future account cleanup. Silent relinking is forbidden.

Flow Details

Start routes

  • GET /auth/google/start
  • GET /auth/apple/start

Each route:

  • Verifies provider config exists.
  • Generates cryptographically random state.
  • Generates nonce for ID token validation where applicable.
  • Stores state/nonce short-term, preferably in a signed HTTP-only cookie or a DB-backed OAuth state table.
  • Redirects to the provider authorization endpoint.

Callback routes

  • GET /auth/google/callback
  • POST or GET /auth/apple/callback depending on Apple response mode selected during implementation.

Each callback:

  • Validates state.
  • Validates nonce if included in ID token.
  • Exchanges authorization code server-side.
  • Verifies ID token:
    • issuer
    • audience/client ID
    • expiry
    • subject
    • email present
    • email verified
  • Links/creates local user through a transaction.
  • Calls existing session creation and cookie helper.
  • Redirects to /.

Provider buttons

Login and signup pages should both show Google and Apple buttons:

  • Equal prominence.
  • Disabled if required env vars are missing.
  • No provider button should submit the password form; use links/buttons to provider start routes.

Testing Strategy

Use real DB integration tests for account linking and schema behavior. Mock providers locally with httptest.Server for token endpoints and JWKS responses; do not call real Google/Apple in tests.

Required test classes:

  • Unit tests for provider config detection and disabled button state.
  • Unit tests for OAuth state/nonce generation and validation.
  • Unit tests for Apple client-secret JWT claims if generated in-process.
  • Integration tests for nullable password_hash and email/password login behavior.
  • Integration tests for provider identity linking:
    • verified email matches existing password user -> identity inserted, session created
    • missing/unverified email -> rejected
    • new verified email -> social-only user created with NULL password hash
    • social-only email/password signup blocked
    • provider subject already linked -> linked user wins
    • provider email update conflict with another user does not silently relink
  • Handler tests for /auth/{provider}/start redirects and /auth/{provider}/callback session issuance.
  • Regression tests for existing signup/login/logout/CSRF/rate-limit behavior.

Validation Architecture

Automated validation should run through go test ./... from backend/. Phase-specific fast feedback can target:

  • go test ./internal/auth ./internal/web
  • go test ./internal/db/... ./internal/web -run 'Test.*(Social|OAuth|Provider|Login|Signup|Session)'

Validation dimensions:

  1. Schema: migrations apply cleanly; password_hash nullable; user_identities constraints exist.
  2. Provider Verification: invalid state, invalid nonce, invalid audience, unverified email, and missing email are rejected.
  3. Linking Semantics: every D-01..D-17 decision has an integration test or explicit acceptance assertion.
  4. Session Continuity: social callback uses existing session cookie path and protected routes recognize the session.
  5. Regression: existing auth tests continue to pass.
  6. Config: .env.example documents provider secrets without real values.
  7. UI: login/signup render equal provider buttons and disabled states.

Manual validation:

  • Configure local test OAuth credentials or mocked callback path.
  • Verify disabled provider buttons with missing env vars.
  • Verify login/signup buttons route to provider start routes when configured.

Threat Model Notes

Plans must include a <threat_model> block because this phase handles authentication.

Threats to cover:

  • CSRF/login CSRF via OAuth callback without state validation.
  • Replay/substitution via missing nonce validation.
  • Token confusion by accepting ID tokens with wrong issuer or audience.
  • Account takeover by linking on unverified email.
  • Account takeover by relinking a provider subject after email change.
  • Session fixation if callback reuses an old session without rotation.
  • Open redirect if callback supports return URLs in the future.
  • Secret leakage through logs (client_secret, auth code, ID token).
  • Email uniqueness conflict when updating users.email.

Open Implementation Choices for Planner

  • State/nonce storage: signed short-lived cookie vs DB-backed OAuth state table.
  • Exact OAuth/OIDC library combination for Google and Apple.
  • Exact callback method for Apple (form_post vs query response mode).
  • Whether provider status view lives at /account or /account/providers.
  • Whether provider is a text column with CHECK constraint or a Postgres enum.

Sources


RESEARCH COMPLETE