Skip to content

security(oauth): replace corsproxy.io with first-party Cloudflare Worker for token exchange#31

Merged
iamvirul merged 10 commits into
OpenDevFlow:mainfrom
iamvirul:fix/github-oauth-token-proxy
Jun 1, 2026
Merged

security(oauth): replace corsproxy.io with first-party Cloudflare Worker for token exchange#31
iamvirul merged 10 commits into
OpenDevFlow:mainfrom
iamvirul:fix/github-oauth-token-proxy

Conversation

@iamvirul
Copy link
Copy Markdown
Contributor

@iamvirul iamvirul commented Jun 1, 2026

Summary

  • Introduces apps/github-oauth-proxy/ - a minimal Cloudflare Worker that performs the GitHub device flow token exchange server-side, so the access_token never transits corsproxy.io or any other public third-party infrastructure
  • CORS is locked to https://opendevflow.github.io via the ALLOWED_ORIGIN env var; the Worker rejects requests from any other origin
  • The Worker only forwards the exact required fields (client_id, device_code, grant_type) to GitHub, no blind request passthrough
  • requestDeviceCode continues using corsproxy.io (only a device_code/user_code are returned here, no tokens, low risk)
  • Removes the cache-busting _t timestamp and Cache-Control headers from pollForToken, which were only needed to defeat proxy CDN caching
  • Updates deploy.yml to inject NEXT_PUBLIC_GITHUB_CLIENT_ID and NEXT_PUBLIC_GITHUB_OAUTH_PROXY_URL from repository secrets at build time (required so NEXT_PUBLIC_* vars are baked into the static export)

Repository secrets to add

Secret Value
NEXT_PUBLIC_GITHUB_CLIENT_ID existing GitHub OAuth App Client ID
NEXT_PUBLIC_GITHUB_OAUTH_PROXY_URL Cloudflare Worker URL after deploying apps/github-oauth-proxy

Test plan

  • Add the two repository secrets above
  • Trigger a deploy and confirm the build succeeds with env vars injected
  • Open the deployed app, click "Connect GitHub", complete device flow - confirm the token is obtained via the Worker (network tab: POST to the Worker URL, not corsproxy.io)
  • Confirm requests from origins other than opendevflow.github.io are blocked (403) by the Worker

Closes #29

Summary by CodeRabbit

  • New Features

    • Added a first-party GitHub OAuth proxy worker to handle token exchanges and a corresponding package for deployment.
  • Security

    • Token exchanges no longer transit an untrusted third-party proxy; CORS is restricted to the site origin.
  • Chores

    • Deployment workflow now validates and injects required OAuth build configuration.
  • Documentation

    • Changelog updated with the OAuth proxy migration and security note.

iamvirul added 3 commits June 1, 2026 12:31
…ker for token exchange

The GitHub device flow token exchange in pollForToken was routing through
the public third-party proxy corsproxy.io. Because the access_token (full
repo scope) is present in the response body, a compromised or logging proxy
could harvest it.

This change introduces apps/github-oauth-proxy — a minimal Cloudflare Worker
that receives the device_code + client_id from the browser, performs the POST
to GitHub server-side, and returns only the result. CORS is locked to the
GitHub Pages origin (ALLOWED_ORIGIN env var), so no other domain can invoke
the worker. The cache-busting _t timestamp and no-cache headers are also
removed since they were only needed to defeat proxy CDN caching.

requestDeviceCode continues using corsproxy.io; it only receives a
device_code and user_code with no access tokens in the response, so
third-party transit risk there is low.

Closes OpenDevFlow#29
NEXT_PUBLIC_* vars are baked into the static export at build time.
Without explicit env injection in the build step, the deployed app
would see empty strings for CLIENT_ID and OAUTH_PROXY_URL, breaking
the GitHub authentication flow entirely.

Both values must be added as repository secrets:
  NEXT_PUBLIC_GITHUB_CLIENT_ID
  NEXT_PUBLIC_GITHUB_OAUTH_PROXY_URL
@iamvirul iamvirul requested a review from a team June 1, 2026 07:10
@iamvirul iamvirul requested a review from hiranyasemindi as a code owner June 1, 2026 07:10
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c1d835a4-bd42-4281-986e-a54629945b4d

📥 Commits

Reviewing files that changed from the base of the PR and between d5215fb and 74c63ef.

📒 Files selected for processing (1)
  • CHANGELOG.md
💤 Files with no reviewable changes (1)
  • CHANGELOG.md

📝 Walkthrough

Walkthrough

Replaces the third‑party CORS proxy with a first‑party Cloudflare Worker that validates origin, proxies GitHub device‑flow token exchanges, updates the client to use the worker URL, and wires build-time secrets and CI validation.

Changes

First-party OAuth proxy via Cloudflare Worker

Layer / File(s) Summary
Cloudflare Worker infrastructure and configuration
apps/github-oauth-proxy/wrangler.toml, apps/github-oauth-proxy/package.json, apps/github-oauth-proxy/tsconfig.json, apps/github-oauth-proxy/.gitignore
Worker project metadata and scripts, TypeScript strict config, Wrangler config with ALLOWED_ORIGIN var, and local ignore rules.
OAuth proxy fetch handler and helpers
apps/github-oauth-proxy/src/index.ts
Fetch handler enforces CORS (Origin == ALLOWED_ORIGIN), handles OPTIONS preflight, accepts only POST, validates client_id, device_code, grant_type (device-code URN), forwards to https://github.com/login/oauth/access_token, and returns upstream JSON; includes corsHeaders and jsonResponse helpers.
Web app integration and CI wiring
apps/web/lib/githubAuth.ts, .github/workflows/deploy.yml, CHANGELOG.md
Adds NEXT_PUBLIC_GITHUB_OAUTH_PROXY_URL (OAUTH_PROXY_URL) and updates pollForToken to POST to the proxy while preserving polling/cancellation/error handling. CI workflow validates required secrets and injects NEXT_PUBLIC_GITHUB_CLIENT_ID and NEXT_PUBLIC_GITHUB_OAUTH_PROXY_URL into the Next.js build. Changelog entry added.

Sequence Diagram(s)

sequenceDiagram
  participant Browser as Browser
  participant Worker as OAuthProxyWorker
  participant GitHub as GitHubOAuthAPI
  Browser->>Worker: OPTIONS (preflight)
  Worker-->>Browser: 204 + Access-Control-Allow-* (if ALLOWED_ORIGIN)
  Browser->>Worker: POST { client_id, device_code, grant_type }
  Worker->>GitHub: POST /login/oauth/access_token
  GitHub-->>Worker: 200 { access_token | error }
  Worker-->>Browser: Upstream JSON response (+ CORS headers)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • hiranyasemindi

Poem

🐰 I hopped a little worker near the gate,
To guard the tokens that we share and make,
Device codes bounce, the proxy keeps them tight,
Origins checked by moon and morning light,
Safe hops and happy devs — a joyful crate.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main change: replacing the public corsproxy.io proxy with a first-party Cloudflare Worker for OAuth token exchange security.
Linked Issues check ✅ Passed The PR fully addresses the objectives from issue #29: deploys a Cloudflare Worker for token exchange, restricts CORS to opendevflow.github.io, validates required fields, and keeps requestDeviceCode on the public proxy.
Out of Scope Changes check ✅ Passed All changes are within scope: github-oauth-proxy Worker implementation, deploy.yml secret injection, githubAuth.ts routing updates, and documentation in CHANGELOG.md align with the linked issue requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 1, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/lib/githubAuth.ts (1)

76-89: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard JSON parsing for non-JSON proxy failures.

If the proxy returns a non-JSON error page, Line 88 throws a parse error and breaks with a confusing message.

Suggested fix
-    const response = await fetch(OAUTH_PROXY_URL, {
+    const response = await fetch(OAUTH_PROXY_URL, {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',
       },
       body: JSON.stringify({
         client_id: CLIENT_ID,
         device_code: deviceCode,
         grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
       }),
     });
-
-    const data = await response.json();
+    let data: any;
+    try {
+      data = await response.json();
+    } catch {
+      throw new Error('OAuth proxy returned an invalid response.');
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/githubAuth.ts` around lines 76 - 89, The code calls
response.json() unguarded which throws if the proxy returns non-JSON; wrap the
JSON parse in a try/catch (or check response.headers.get('content-type') and
response.ok) before using data so non-JSON responses are handled gracefully:
attempt response.json() inside try, and on failure fall back to response.text()
(or include response.status and text) and surface a clear error; update the
block around the fetch/response handling (referencing fetch, OAUTH_PROXY_URL,
response, data, CLIENT_ID, deviceCode) to avoid unhandled parse errors and
produce a helpful error message when the proxy returns HTML or plain text.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/deploy.yml:
- Around line 47-53: Add a fail-fast validation step before the "Build Next.js
app" run that checks the required secrets NEXT_PUBLIC_GITHUB_CLIENT_ID and
NEXT_PUBLIC_GITHUB_OAUTH_PROXY_URL are present and non-empty; implement it as a
separate step (e.g., "Validate required secrets") that inspects the environment
variables (the same ${{ secrets.NEXT_PUBLIC_GITHUB_CLIENT_ID }} and ${{
secrets.NEXT_PUBLIC_GITHUB_OAUTH_PROXY_URL }}) and exits non‑zero with a clear
error message if either is missing or empty so the workflow stops before
executing the "Build Next.js app" step.

In `@apps/github-oauth-proxy/src/index.ts`:
- Around line 79-84: The token response returned in
apps/github-oauth-proxy/src/index.ts (the Response constructed with
JSON.stringify(body)) must be marked non-cacheable: add strict cache-control
headers (e.g., "Cache-Control": "no-store, no-cache, must-revalidate,
max-age=0", "Pragma": "no-cache", "Expires": "0") to the headers object
alongside 'Content-Type' and the conditional corsHeaders(allowedOrigin) so
access_token values are never persisted by browsers or intermediaries.
- Around line 53-61: The fetch to GITHUB_TOKEN_URL (the call that assigns
githubRes) can throw on network/runtime failures and currently bubbles as an
opaque Worker error; wrap the fetch and subsequent githubRes.json() in a
try/catch, catch network or JSON errors and return a jsonResponse with a clear
error object and an appropriate status (e.g., 502 or 504) and allowedOrigin
instead of letting the exception escape; also handle non-2xx responses from
githubRes by returning a jsonResponse with the parsed error body (or a fallback
message) and githubRes.status so callers always receive a JSON error contract
from this handler.
- Around line 45-51: The current check only verifies presence of client_id,
device_code, and grant_type; update the validation in index.ts to also require
grant_type strictly equals "urn:ietf:params:oauth:grant-type:device_code" (the
device-flow URN) and return a 400 jsonResponse (using jsonResponse and
allowedOrigin) with an appropriate error (e.g., invalid_request or
unsupported_grant_type) if it does not match; keep client_id and device_code
presence checks as-is and ensure the error message mentions the expected
grant_type value.

In `@apps/web/lib/githubAuth.ts`:
- Around line 63-67: The code currently only guards that OAUTH_PROXY_URL is set;
add a similar fail-fast check for NEXT_PUBLIC_GITHUB_CLIENT_ID so pollForToken
and other callers don't issue bad requests—either validate
NEXT_PUBLIC_GITHUB_CLIENT_ID alongside OAUTH_PROXY_URL where the module
initializes, or add the same guard at the top of pollForToken; reference the
NEXT_PUBLIC_GITHUB_CLIENT_ID environment variable and the pollForToken function
(and the existing OAUTH_PROXY_URL check) and throw a clear Error when the client
ID is missing.

---

Outside diff comments:
In `@apps/web/lib/githubAuth.ts`:
- Around line 76-89: The code calls response.json() unguarded which throws if
the proxy returns non-JSON; wrap the JSON parse in a try/catch (or check
response.headers.get('content-type') and response.ok) before using data so
non-JSON responses are handled gracefully: attempt response.json() inside try,
and on failure fall back to response.text() (or include response.status and
text) and surface a clear error; update the block around the fetch/response
handling (referencing fetch, OAUTH_PROXY_URL, response, data, CLIENT_ID,
deviceCode) to avoid unhandled parse errors and produce a helpful error message
when the proxy returns HTML or plain text.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3bf4f279-2dc3-4ab0-bc19-147fa2daf945

📥 Commits

Reviewing files that changed from the base of the PR and between 9c233d0 and 90fc332.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • .github/workflows/deploy.yml
  • apps/github-oauth-proxy/package.json
  • apps/github-oauth-proxy/src/index.ts
  • apps/github-oauth-proxy/tsconfig.json
  • apps/github-oauth-proxy/wrangler.toml
  • apps/web/lib/githubAuth.ts

Comment thread .github/workflows/deploy.yml
Comment thread apps/github-oauth-proxy/src/index.ts
Comment thread apps/github-oauth-proxy/src/index.ts Outdated
Comment thread apps/github-oauth-proxy/src/index.ts
Comment thread apps/web/lib/githubAuth.ts
iamvirul and others added 5 commits June 1, 2026 12:48
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Without this, a missing secret silently bakes an empty string into
the static export and the GitHub auth feature fails at runtime with
no indication in CI. The new step runs before the build and exits 1
with a clear per-secret error message if either var is absent.
@iamvirul iamvirul merged commit badbb61 into OpenDevFlow:main Jun 1, 2026
5 checks passed
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.

security: Replace corsproxy.io with a first-party endpoint for GitHub OAuth token exchange

2 participants