Skip to content

Add GitHub as a second OAuth sign-in provider - #21

Open
kolaCZek wants to merge 2 commits into
masterfrom
DE-000-github-oauth-provider
Open

Add GitHub as a second OAuth sign-in provider#21
kolaCZek wants to merge 2 commits into
masterfrom
DE-000-github-oauth-provider

Conversation

@kolaCZek

@kolaCZek kolaCZek commented Aug 3, 2026

Copy link
Copy Markdown

Description: Add GitHub as a second OAuth sign-in provider next to Google
Possible impact: Authentication, sign-in UI, session cookies, privacy policy, deployment env vars


Summary

  • Users can now sign in with GitHub in addition to Google.
  • src/services/auth.ts is refactored from Google-only helpers into a small provider registry (PROVIDERS, OAuthProvider), so the auth routes are provider-agnostic.
  • Both providers share the single /api/auth/callback route. The provider is picked with /api/auth?provider=github and carried through the flow in the OAuth state parameter plus a short-lived httpOnly oauth_state cookie. Plain /api/auth still goes to Google, and the existing GOOGLE_REDIRECT_URI does not have to change.
  • As a side effect of needing state, the flow now validates it - the callback previously accepted any code with no CSRF check.
  • GitHub sign-in is restricted to active members of the GITHUB_ORG organization (BrandEmbassy by default). Non-members are redirected back to /?error=not_authorized and no session cookie is set. The Google flow is unchanged and still unrestricted.

Changes

  • src/services/auth.ts - provider registry with a google and a github provider; getUserInfo now returns null when the account is not allowed to use the app.
  • src/services/oauth-state.ts (new) - createState / parseState, nonce compared with timingSafeEqual.
  • src/routes/api/auth/index.ts - resolves ?provider=, sets the state cookie, redirects to the provider.
  • src/routes/api/auth/callback/index.ts - resolves the provider from the state, then fails closed with invalid_state / auth_failed / not_authorized.
  • src/routes/api/auth/logout/index.ts - also clears oauth_state.
  • src/routes/layout.tsx, src/routes/index.tsx, src/global.css - both sign-in buttons plus an error banner for the failure cases.
  • .env.example, README.md - GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_REDIRECT_URI, GITHUB_ORG (all optional; the GitHub button just will not work when unset).
  • src/routes/privacy/index.tsx - GitHub user data section, the new oauth_state cookie, and the GitHub revoke link.

Note on identity

Reservations are keyed by display name only (occupant: t.string()), so a person whose GitHub profile has no name set gets @login as their occupant and would not match their Google-derived name. No cross-provider identity merging is in scope here.

Test plan

  • npm test - 76 passing, incl. new src/services/auth.test.ts and src/services/oauth-state.test.ts (auth URLs and scopes per provider, GitHub token error handling, name -> login fallback, non-member / pending-membership / bad-token rejection, state round-trip and tamper rejection)
  • npm run lint (no new warnings), npm run build.types, npm run fmt.check, npm run build
  • Manually against npm run dev: /api/auth?provider=github redirects to github.com/login/oauth/authorize with read:user read:org and sets oauth_state; plain /api/auth still goes to Google; /api/auth/callback?code=xyz with no cookie is rejected with /?error=invalid_state; /?error=not_authorized renders the banner and both sign-in buttons
  • Full GitHub round trip against a real OAuth app (needs client ID/secret) - as an org member and as a non-member

🤖 Generated with Claude Code

https://claude.ai/code/session_01WLRrfRm7MNWzFgpNczackB

Refactor the Google-only OAuth helpers into a provider registry and add a
GitHub provider next to Google. Both providers share the single
/api/auth/callback route; the provider is selected with /api/auth?provider=github
and carried through the flow in the OAuth state parameter plus a short-lived
httpOnly cookie, which also adds the CSRF check the flow was missing.

GitHub sign-in is restricted to active members of the GITHUB_ORG organization
(BrandEmbassy by default); everyone else is redirected back with an error.
The occupant name comes from the GitHub profile name, falling back to the
login when no name is set.

Claude-Session: https://claude.ai/code/session_01WLRrfRm7MNWzFgpNczackB

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds GitHub as a second OAuth sign-in provider alongside Google, refactoring the auth layer to be provider-agnostic and introducing a CSRF-protecting state mechanism shared by both providers.

Changes:

  • Refactors src/services/auth.ts into a provider registry (PROVIDERS) and adds a GitHub OAuth provider with org-membership gating.
  • Adds a new oauth_state CSRF state helper + cookie and wires it through /api/auth and /api/auth/callback.
  • Updates UI, privacy policy, and docs/env examples to reflect the GitHub sign-in option and related data/cookie usage.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/services/oauth-state.ts New CSRF state creation/parsing for OAuth flow (cookie + state param validation).
src/services/oauth-state.test.ts Tests for state round-trip and rejection cases.
src/services/auth.ts Refactor to provider registry; adds GitHub provider and org-membership check.
src/services/auth.test.ts Provider URL/token/userinfo behavior tests, including GitHub org membership gating.
src/routes/privacy/index.tsx Privacy policy updates for GitHub data scopes and oauth_state cookie disclosure.
src/routes/layout.tsx Adds GitHub sign-in button and renders an auth error banner from ?error=.
src/routes/index.tsx Updates landing page copy and adds GitHub sign-in button.
src/routes/api/auth/logout/index.ts Clears the new oauth_state cookie on logout.
src/routes/api/auth/index.ts Chooses provider, sets oauth_state cookie, redirects to provider auth URL.
src/routes/api/auth/callback/index.ts Validates state, resolves provider, exchanges code, enforces not_authorized behavior.
src/global.css Styles for sign-in action grouping and auth error banner.
README.md Documents GitHub OAuth env vars and the shared callback/state behavior.
.env.example Adds example GITHUB_* env vars.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/routes/api/auth/index.ts
Comment thread src/routes/layout.tsx Outdated
Comment thread src/services/auth.ts
- fail fast with /?error=provider_not_configured when a provider's client id
  or secret is not set, instead of redirecting with client_id=undefined
- only treat a 404 membership response (and a non-active membership) as
  not_authorized; any other GitHub failure now throws and the callback maps
  it to auth_failed, so an API outage is not reported as "not a member"
- make the not_authorized banner org-agnostic, since GITHUB_ORG is configurable

Claude-Session: https://claude.ai/code/session_01WLRrfRm7MNWzFgpNczackB
@kolaCZek
kolaCZek marked this pull request as ready for review August 3, 2026 09:07
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.

2 participants