security(oauth): replace corsproxy.io with first-party Cloudflare Worker for token exchange#31
Conversation
…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
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
💤 Files with no reviewable changes (1)
📝 WalkthroughWalkthroughReplaces 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. ChangesFirst-party OAuth proxy via Cloudflare Worker
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)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
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 winGuard 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (6)
.github/workflows/deploy.ymlapps/github-oauth-proxy/package.jsonapps/github-oauth-proxy/src/index.tsapps/github-oauth-proxy/tsconfig.jsonapps/github-oauth-proxy/wrangler.tomlapps/web/lib/githubAuth.ts
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.
Summary
apps/github-oauth-proxy/- a minimal Cloudflare Worker that performs the GitHub device flow token exchange server-side, so theaccess_tokennever transitscorsproxy.ioor any other public third-party infrastructurehttps://opendevflow.github.iovia theALLOWED_ORIGINenv var; the Worker rejects requests from any other originclient_id,device_code,grant_type) to GitHub, no blind request passthroughrequestDeviceCodecontinues usingcorsproxy.io(only adevice_code/user_codeare returned here, no tokens, low risk)_ttimestamp andCache-Controlheaders frompollForToken, which were only needed to defeat proxy CDN cachingdeploy.ymlto injectNEXT_PUBLIC_GITHUB_CLIENT_IDandNEXT_PUBLIC_GITHUB_OAUTH_PROXY_URLfrom repository secrets at build time (required soNEXT_PUBLIC_*vars are baked into the static export)Repository secrets to add
NEXT_PUBLIC_GITHUB_CLIENT_IDNEXT_PUBLIC_GITHUB_OAUTH_PROXY_URLapps/github-oauth-proxyTest plan
corsproxy.io)opendevflow.github.ioare blocked (403) by the WorkerCloses #29
Summary by CodeRabbit
New Features
Security
Chores
Documentation