Skip to content

feat(github-oauth): real GitHub OAuth flow + identity resolution + claim-pending handoff#41

Merged
themightychris merged 5 commits into
mainfrom
feat/github-oauth
May 17, 2026
Merged

feat(github-oauth): real GitHub OAuth flow + identity resolution + claim-pending handoff#41
themightychris merged 5 commits into
mainfrom
feat/github-oauth

Conversation

@themightychris
Copy link
Copy Markdown
Member

Summary

Replaces the auth-jwt-substrate 501 stubs at /api/auth/github/{start,callback} with the real OAuth flow per specs/api/auth.md and specs/behaviors/account-migration.md.

  • Hand-rolled PKCE (S256) + 32-byte CSPRNG cfp_oauth_state cookie; 10-min signed cfp_oauth_session JWT carries { state, codeVerifier, return }
  • Token exchange + /user + /user/emails via Node's built-in fetch; verified-only email filter
  • Account-matching algorithm: byGithubUserId → email match → username weak match
  • Three callback outcomes routed through mintSessionFor / issueClaimPending:
    • existing-linked → refresh githubLogin + PrivateProfile.email, mint session, redirect to return
    • create-fresh → write Person + PrivateProfile via store.transact (private-first), mint session
    • candidates → mint claim-pending JWT (scope:claim, 5m), redirect /account-claim?return=…
  • All documented error modes redirect to /login?error=<code>
  • Temporary /account-claim placeholder page in apps/web

Test plan

  • OAuth happy path: never-seen-this-user → fresh Person + PrivateProfile → session issued → redirected to /
  • OAuth returning user: existing githubUserId → callback refreshes githubLogin + email → session, no shadow account created
  • OAuth with candidates (email match): claim-pending JWT issued, cfp_claim cookie set, redirect to /account-claim
  • OAuth with candidates (username weak match): same outcome even with no email overlap
  • CSRF: tampered state query param → /login?error=oauth_state_mismatch
  • PKCE token-exchange error → /login?error=github_unreachable
  • User denies on GitHub (error=access_denied) → /login?error=access_denied
  • No verified emails → /login?error=email_unverified
  • Expired cfp_oauth_session cookie → /login?error=oauth_session_invalid
  • npm run type-check, npm run lint, npm run build all green
  • Full API test suite green (158 passed) — the parallel-pool flakiness reported on first run reproduces on main; passes cleanly with --no-file-parallelism

…ng handoff

Replaces the 501 stubs at /api/auth/github/{start,callback} with the full
spec-compliant flow per specs/api/auth.md and specs/behaviors/account-migration.md.

- Hand-rolled PKCE (S256) + 32-byte CSPRNG state cookie (HttpOnly, SameSite=Lax)
- 10-min signed cfp_oauth_session JWT carries { state, codeVerifier, return }
- GitHub client (token exchange + /user + /user/emails) using built-in fetch
- Account-matching algorithm: byGithubUserId → email match → username weak match
- Three callback outcomes wired into auth-jwt-substrate's mintSessionFor /
  issueClaimPending:
  * existing-linked → refresh login+email, mint session, redirect to return
  * create-fresh → write Person + PrivateProfile via store.transact, mint session
  * candidates → mint claim-pending JWT (scope:claim, 5m), redirect /account-claim
- Error redirects to /login?error=… for access_denied, oauth_state_mismatch,
  oauth_session_invalid, github_unreachable, email_unverified
15 new tests across the three callback outcomes + every documented error
mode (CSRF mismatch, expired oauth session, GitHub denial, email_unverified,
token-exchange error). Each test uses a unique remoteAddress to dodge the
10-req/min/IP cap on /api/auth/*.

Trims the OAuth 501-stub tests from auth.test.ts now that the real handlers
redirect instead of returning 501.

Extends createGitHubMock with the github.com/login/oauth/access_token endpoint
plus a setTokenResponse override for the error-path tests.
Temporary landing page for the candidate-match callback path. The full
account-claim screens land in the next plan; this placeholder gives a
recognizable end-of-flow for the github-oauth tests + manual QA so the
redirect target isn't a 404.
@themightychris themightychris merged commit 610ed04 into main May 17, 2026
1 check passed
@themightychris themightychris deleted the feat/github-oauth branch May 17, 2026 00:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant