Start GitHub's OAuth flow server-side, and let the wizard report its own failures - #636
Conversation
report its own failures (#632) #631 — every theme assembled GitHub's authorize URL in an inline script, concatenating window.location straight into redirect_uri. A next containing & ended the redirect_uri value early and the rest reached GitHub as further authorize parameters. /login/github now does it in Go, and the themes point a plain anchor at it: no client_id in the page, no inline script, and the escaping is tested rather than duplicated four times. The issue proposed stripping the query from redirect_uri, which would have been a regression: next round-trips *through* redirect_uri, since GitHub returns the visitor to that exact URL with &code= appended and Login reads next from the query. So the fix escapes rather than strips, and both the redirect and the next inside it are escaped. SafeNext guards the new entry point too — next reaches redirect_uri, so an open redirect here would be handed to GitHub. The origin comes from the request rather than the site_url setting, on purpose: GitHub matches redirect_uri against the callback registered for the app, so it has to be the host the visitor is on — which is what window.location used to give. #632 — both wizard database handlers returned nothing when ParseForm failed, which gin sends as a bare 200 with an empty body. For /test_db that is worse than silence: the wizard's jQuery call has no dataType, so an empty 200 takes the success path and the Test Database button turns green on a request the server never read. /wizard_db rendered a blank page despite having a fail() helper for exactly this. On the client, the error handler read data.responseJSON.error, which is undefined whenever the body was not JSON — a proxy error, an HTML error page, a dropped connection. Reading .error threw, so the button went red and the reason never appeared. It now falls back through statusText, and renders with .text() rather than .html(). Verified against a local instance: the anchor carries an escaped next, /login/github escapes both layers, an offsite next is dropped, X-Forwarded-Proto is honoured, and replaying GitHub's return leg resolves next back to /search?q=a&b=c — the case that was broken. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The new server-side GitHub OAuth authorize URL construction still omits an OAuth state parameter, leaving the login flow vulnerable to login CSRF/session-swapping unless state is generated and validated end-to-end.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (2)
What changed in this PR
This PR hardens the GitHub OAuth entry-point by moving authorize URL construction from theme inline JavaScript into server-side Go code (fixing redirect URI escaping issues), and improves the install wizard’s ability to reliably report database form/connection failures to the user.
Changes:
- Add a new
/login/githubroute that redirects to GitHub with a correctly-escapedredirect_uri(including a safely-carriednext). - Fix wizard DB handlers to return actionable responses on
ParseFormerrors, and improve the wizard UI error fallback handling. - Add targeted tests covering OAuth URL escaping/origin detection and wizard malformed-form behavior.
| File | Description |
|---|---|
goblog.go |
Adds /login/github route; fixes wizard DB handlers to respond on malformed forms. |
blog/blog.go |
Implements RequestOrigin, GithubAuthorizeURL, and GithubLogin redirect logic. |
themes/default/templates/login.html |
Replaces inline JS authorize URL construction with a plain link to /login/github. |
themes/default/templates/wizard_db.html |
Makes the wizard client robust to non-JSON failures and avoids HTML injection by using .text(). |
blog/github_login_test.go |
Adds tests for redirect escaping, next round-trip behavior, origin detection, and offsite-next rejection. |
wizard_errors_test.go |
Adds regression tests ensuring malformed forms don’t appear as successful wizard DB actions. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Review caught it on the login button. Fixed everywhere it appears as user-facing text, leaving identifiers (github_url, fa-github, /login/github) alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>


Fixes #631. Fixes #632.
#631 — GitHub's OAuth flow starts server-side now
Every theme assembled the authorize URL in an inline script:
window.locationwent in raw. Anextcontaining&ended theredirect_urivalue early and everything after it reached GitHub as further authorize parameters. Reproduced as a test against the old behaviour:/login/githubnow does it in Go and the themes point a plain anchor at it. Noclient_idin the page, no inline script, and the escaping is tested once instead of duplicated across four repos.The issue's proposed fix was wrong
#631 suggested stripping the query from
redirect_uri— "nextalready survives separately". It does not.nextround-trips throughredirect_uri: GitHub returns the visitor to that exact URL with&code=appended, andLoginreadsnextfrom that query. Stripping it would have quietly sent everyone to/after signing in. So this escapes rather than strips, at both layers — the redirect, and thenextinside it. I've corrected the issue.Two deliberate choices
SafeNextguards the new entry point.nextreachesredirect_uri, so an open redirect here would be handed to GitHub. Covered byTestGithubLogin_RejectsOffsiteNext.The origin comes from the request, not the
site_urlsetting. GitHub matchesredirect_uriagainst the callback registered for the app, so it has to be the host the visitor is actually on — which is whatwindow.locationgave. Usingsite_urlwould have been a behaviour change with real breakage risk if it does not match what is registered.#632 — the wizard can report its own failures
Server. Both database handlers returned nothing when
ParseFormfailed, which gin sends as a bare 200 with an empty body. For/test_dbthat is worse than silence: the wizard's jQuery call sets nodataType, so an empty 200 takes the success path and the Test Database button turns green on a request the server never read./wizard_dbrendered a blank page despite already having afail()helper for exactly this.Client. The error handler read
data.responseJSON.error.responseJSONis undefined whenever the body was not JSON — a proxy error, an HTML error page, a dropped connection — so reading.errorthrew: the button went red and the reason never appeared, which is the one thing someone stuck on that step needs. It falls back throughstatusTextnow, and renders with.text()rather than.html().This goblog PR must merge and release before the theme PRs. The themes link to
/login/github, which does not exist until this ships; on an older goblog that link 404s and GitHub login is dead.(#624 was the reverse — themes first — because there the themes had to tolerate a goblog change. Here they depend on a new route.)
Note for the deploy:
goblog-site-themeis cloned atmainby iac, so merging its PR before goblog is released would break goblog.live's GitHub login on the next playbook run, even one for an unrelated reason.Tests
All verified to fail without the fix:
TestGithubAuthorizeURL_EscapesAmpersandInNext— the GitHub OAuth redirect_uri is built from window.location without encoding #631 bug, asserting the authorize URL has exactly two parametersTestGithubAuthorizeURL_KeepsNextThroughTheRoundTrip—redirect_urikeeps?next=, since that is how it survivesTestGithubLogin_RejectsOffsiteNext—//evil.exampleand friends never reachredirect_uriTestRequestOrigin— scheme detection includingX-Forwarded-ProtochainsTestTestDB_MalformedFormIsNotReportedAsSuccess— a form the server could not read is not a passing database testTestUpdateDB_MalformedFormRendersTheWizardAgain— no more blank pageVerified against a local instance
href="/login/github?next=%2Fadmin%2Fsettings"/login/github302s with both layers escapednextis droppedX-Forwarded-Proto: httpsis honourednextback to/search?q=a&b=c— the exact case that was broken🤖 Generated with Claude Code