Skip to content

fix(auth): stop exposing GitHub OAuth token in client-accessible session#874

Open
advikdivekar wants to merge 2 commits into
Priyanshu-byte-coder:mainfrom
advikdivekar:fix/security-token-csp-injection
Open

fix(auth): stop exposing GitHub OAuth token in client-accessible session#874
advikdivekar wants to merge 2 commits into
Priyanshu-byte-coder:mainfrom
advikdivekar:fix/security-token-csp-injection

Conversation

@advikdivekar
Copy link
Copy Markdown
Contributor

What the problem was

The GitHub OAuth access token — scoped to repo (full private repository access) — was being copied into the NextAuth session object in src/lib/auth.ts and returned by the public /api/auth/session endpoint as plain JSON. Any client-side JavaScript on an authenticated page could call fetch('/api/auth/session') and read the raw token, making it trivially stealable via XSS.

The Session interface in src/types/next-auth.d.ts also declared accessToken?: string, which made the leak part of the typed contract.

What was changed and in which files

src/lib/auth.ts
Removed session.accessToken = token.accessToken from the session callback. The token now stays exclusively inside the httpOnly JWT cookie and is never sent to the browser.

src/types/next-auth.d.ts
Removed accessToken from the Session interface. Added a code comment explaining that the token lives only in the JWT and must be accessed server-side.

src/lib/server-github-token.ts (new file)
Added getGitHubAccessToken(req: NextRequest): Promise<string | null> — a thin wrapper around getToken({ req, secret }) from next-auth/jwt. All server-side API routes call this instead of reading session.accessToken.

src/lib/validate-github-username.ts (new file)
Added isValidGitHubUsername(username) to reject strings that do not match GitHub's 1–39 character alphanumeric-plus-hyphen format, preventing injection of extra search qualifiers into GitHub API calls.

Migrated API routes — all replaced session.accessToken with getGitHubAccessToken(req):

  • src/app/api/metrics/contributions/route.ts
  • src/app/api/metrics/prs/route.ts
  • src/app/api/metrics/pr-review-time/route.ts
  • src/app/api/metrics/streak/route.ts
  • src/app/api/metrics/repos/route.ts
  • src/app/api/metrics/repo-health/route.ts
  • src/app/api/metrics/ci/route.ts
  • src/app/api/metrics/compare/route.ts
  • src/app/api/metrics/issues/route.ts
  • src/app/api/metrics/languages/route.ts
  • src/app/api/metrics/pinned-repos/route.ts
  • src/app/api/metrics/pr-breakdown/route.ts
  • src/app/api/metrics/weekly-summary/route.ts

Why this approach fixes the root cause

The root cause was architectural: the session callback was treating the access token as session data rather than server-only credential. By removing it from the session callback entirely and reading it only from the encrypted, httpOnly JWT inside API route handlers, the token can never appear in the /api/auth/session JSON response — regardless of what client-side code does. The getGitHubAccessToken helper enforces this boundary for every call site.

Multi-account paths (accountId=combined, secondary accounts) were also updated — they pass the primary token retrieved from the JWT into getAllAccounts, which then retrieves linked account tokens from the encrypted Supabase store.

Steps to test

  1. Sign in with a GitHub account
  2. Open browser DevTools → Console
  3. Run: fetch('/api/auth/session').then(r => r.json()).then(console.log)
  4. Confirm the response object contains githubLogin and githubId but no accessToken field
  5. Verify all dashboard widgets (contribution graph, streak, PR metrics, repos, CI analytics) load correctly — confirming API routes still authenticate and call GitHub successfully
  6. Sign out and verify unauthenticated requests to all /api/metrics/* routes return 401

Edge cases covered

  • Primary account (no accountId param): token read from JWT, passed to GitHub API
  • Combined multi-account (accountId=combined): primary token retrieved from JWT, linked account tokens retrieved from Supabase
  • Per-account (accountId=<id>): primary account gets JWT token; secondary accounts get their stored tokens
  • Unauthenticated requests: getGitHubAccessToken returns null, route returns 401
  • Missing JWT secret: getToken returns null, guarded by the null check

Regressions

None. npm run type-check and npm run lint both pass. All routes behave identically from the caller's perspective — only the token retrieval path changed from session.accessToken to server-side JWT extraction.

Closes #856

Please review and merge this under GSSoC 2026.

Migrate all metrics API routes to retrieve the GitHub OAuth token
server-side via getGitHubAccessToken(req) instead of reading the
now-removed session.accessToken field. Covers contributions, prs,
pr-review-time, streak, repos, repo-health, ci, and compare routes.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 23, 2026

@advikdivekar is attempting to deploy a commit to the PRIYANSHU DOSHI's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added gssoc26 GSSoC 2026 contribution type:bug GSSoC type bonus: bug fix type:security GSSoC type bonus: security (+20 pts) labels May 23, 2026
@github-actions
Copy link
Copy Markdown

GSSoC Label Checklist 🏷️

@Priyanshu-byte-coder — please apply the appropriate labels before merging:

Difficulty (pick one):

  • level:beginner — 20 pts
  • level:intermediate — 35 pts
  • level:advanced — 55 pts
  • level:critical — 80 pts

Quality (optional):

  • quality:clean — ×1.2 multiplier
  • quality:exceptional — ×1.5 multiplier

Validation (required to score):

  • gssoc:approved — counts for points
  • gssoc:invalid / gssoc:spam / gssoc:ai-slop — does not score

Type labels (type:*) are auto-detected from files and title. Review and adjust if needed.
Points formula: (difficulty × quality_multiplier) + type_bonus

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gssoc26 GSSoC 2026 contribution type:bug GSSoC type bonus: bug fix type:security GSSoC type bonus: security (+20 pts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] GitHub OAuth access token (repo scope) exposed in client-accessible NextAuth session

1 participant