Skip to content

fix(auth): only remember a GET as the post-login destination - #80

Merged
ralyodio merged 1 commit into
moshcoder:mainfrom
clawedassistant26:fix/next-remembers-post-only-path
Jul 30, 2026
Merged

fix(auth): only remember a GET as the post-login destination#80
ralyodio merged 1 commit into
moshcoder:mainfrom
clawedassistant26:fix/next-remembers-post-only-path

Conversation

@clawedassistant26

Copy link
Copy Markdown
Contributor

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:

if (!req.user) { setNext(res, req.originalUrl); return res.redirect("/"); }

Every consumer of mc_next hands it to a redirect (auth.mjs:49,64,75, passkey.mjs:66,109, coinpay.mjs:63), which the browser follows as a GET. But requireAuth stored 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/unsubscribe

So 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_csrf routinely outlives mc_sess (session expiry, or signing out in another tab). The stale form still submits a token csrfGuard accepts, requireAuth then rejects it, and mc_next is 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:

1. POST /credits/buy (no session) -> 302 /
   mc_next stored: "/credits/buy"
2. POST /auth/login -> 302 redirect to /credits/buy
3. GET /credits/buy -> 404

The fix

Only remember the destination when the request is a GET. requireAuth is the only caller of setNext, so this is the one place the method is known.

Behaviour for guarded GETs is unchanged — a blocked /settings still returns you to /settings after 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 as logout-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/pwa 30 → 33, root npm test 204 → 207, 0 failures.

Related, not fixed here

public/push.js posts to /push/subscribe with fetch, which follows the 302 to / and receives a 200 HTML page, so r.ok is true and the button flips to "Disable notifications on this device" even though nothing was stored. That needs requireAuth to answer JSON 401 for fetch callers rather than redirecting, which is a larger behaviour change — happy to open it separately if you want it.

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.
@ralyodio
ralyodio merged commit 660a1b6 into moshcoder:main Jul 30, 2026
3 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.

2 participants