Add GitHub as a second OAuth sign-in provider - #21
Open
kolaCZek wants to merge 2 commits into
Open
Conversation
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
There was a problem hiding this comment.
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.tsinto a provider registry (PROVIDERS) and adds a GitHub OAuth provider with org-membership gating. - Adds a new
oauth_stateCSRF state helper + cookie and wires it through/api/authand/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.
- 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
marked this pull request as ready for review
August 3, 2026 09:07
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
src/services/auth.tsis refactored from Google-only helpers into a small provider registry (PROVIDERS,OAuthProvider), so the auth routes are provider-agnostic./api/auth/callbackroute. The provider is picked with/api/auth?provider=githuband carried through the flow in the OAuthstateparameter plus a short-lived httpOnlyoauth_statecookie. Plain/api/authstill goes to Google, and the existingGOOGLE_REDIRECT_URIdoes not have to change.state, the flow now validates it - the callback previously accepted anycodewith no CSRF check.GITHUB_ORGorganization (BrandEmbassyby default). Non-members are redirected back to/?error=not_authorizedand no session cookie is set. The Google flow is unchanged and still unrestricted.Changes
src/services/auth.ts- provider registry with agoogleand agithubprovider;getUserInfonow returnsnullwhen the account is not allowed to use the app.src/services/oauth-state.ts(new) -createState/parseState, nonce compared withtimingSafeEqual.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 withinvalid_state/auth_failed/not_authorized.src/routes/api/auth/logout/index.ts- also clearsoauth_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 newoauth_statecookie, 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 nonameset gets@loginas 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. newsrc/services/auth.test.tsandsrc/services/oauth-state.test.ts(auth URLs and scopes per provider, GitHub token error handling,name->loginfallback, 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 buildnpm run dev:/api/auth?provider=githubredirects togithub.com/login/oauth/authorizewithread:user read:organd setsoauth_state; plain/api/authstill goes to Google;/api/auth/callback?code=xyzwith no cookie is rejected with/?error=invalid_state;/?error=not_authorizedrenders the banner and both sign-in buttons🤖 Generated with Claude Code
https://claude.ai/code/session_01WLRrfRm7MNWzFgpNczackB