fix(auth): only remember a GET as the post-login destination - #80
Merged
ralyodio merged 1 commit intoJul 30, 2026
Merged
Conversation
requireAuth stores the blocked URL in mc_next so the user can be returned there after signing in. That value is always consumed by a redirect, which the browser follows as a GET, but requireAuth stored the URL of any method. Several guarded routes are POST-only: /credits/buy, /settings/channels, /settings/apikeys, /settings/apikeys/:id/delete, /push/subscribe and /push/unsubscribe. When one of those was blocked, signing in redirected to the same path as a GET, which no route serves, so the user landed on the 404 page. /credits/buy is the worst case: it is the purchase flow. Only remember the destination when the request is a GET.
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.
The bug
requireAuth(apps/pwa/src/lib/session.mjs) remembers where a blocked request was headed so the user can be returned there after signing in:Every consumer of
mc_nexthands it to a redirect (auth.mjs:49,64,75,passkey.mjs:66,109,coinpay.mjs:63), which the browser follows as a GET. ButrequireAuthstored the URL regardless of method, and six guarded routes are POST-only:/credits/buy,/settings/channels,/settings/apikeys,/settings/apikeys/:id/delete,/push/subscribe,/push/unsubscribeSo a blocked POST was remembered as a destination that no route serves, and signing in dropped the user on the 404 page.
How it happens
The session cookie and the CSRF cookie are separate, so
mc_csrfroutinely outlivesmc_sess(session expiry, or signing out in another tab). The stale form still submits a tokencsrfGuardaccepts,requireAuththen rejects it, andmc_nextis set to the POST-only path.The worst case is
/credits/buy: a customer clicks buy, is asked to sign in, signs in correctly, and lands on "404 — no such page in the pit" instead of the checkout.Reproduction
Run against unmodified
main(df9d1e0) with the real routers and a real libsql database, no fault injection:The fix
Only remember the destination when the request is a GET.
requireAuthis the only caller ofsetNext, so this is the one place the method is known.Behaviour for guarded GETs is unchanged — a blocked
/settingsstill returns you to/settingsafter sign-in. A blocked POST now falls back to/, the dashboard.Tests
New
apps/pwa/test/require-auth-next.test.mjs, 3 tests, same harness style aslogout-csrf.test.mjs(skip-guard plus a throwaway libsql file).Verified by reverting only the source change and keeping the tests: 2 fail / 1 pass unpatched, 3/3 patched. The third test (a guarded GET is still remembered) passes both ways by design — it is the control that proves the fix does not simply disable the feature.
Suites are green before and after:
apps/pwa30 → 33, rootnpm test204 → 207, 0 failures.Related, not fixed here
public/push.jsposts to/push/subscribewithfetch, which follows the 302 to/and receives a 200 HTML page, sor.okis true and the button flips to "Disable notifications on this device" even though nothing was stored. That needsrequireAuthto answer JSON 401 for fetch callers rather than redirecting, which is a larger behaviour change — happy to open it separately if you want it.