fix(core): answer 401, not 400, when nobody is signed in - #149
Conversation
ensureCookies gates every access-required route. When the required cookie was absent and there was no refresh cookie to fall back on, which is exactly the signed-out case, it answered 400. The request was perfectly well formed. There was simply no session, and that is what 401 means. Two things went wrong with the old status. Every signed-out page view produced 400s, so 400 became ordinary background traffic in adopter logs and would hide a real malformed request from anyone watching. And consumers translate status codes into words for readers: 400 asks a product to say "that request did not come through in a form we could use" when the true sentence is "you have been signed out, sign in again", and there was no way to tell the two apart from outside. The neighbouring branches in the same function already answered 401 for a refresh that failed and for a cookie that was invalid or expired, so this branch was the outlier rather than the convention. All three now agree, and the status no longer depends on which way the session happened to be absent. The response body is unchanged. Two tests pinned the old status, both with a comment explaining the 400 as intended behaviour. Both now pin 401, and the express one keeps its real assertion that the request is never forwarded upstream. Closes #148
…xpress The parity suite already drove a session-less request through both adapters, but it only asserted that the two agree, not what they agree on, so it passed just as happily when both answered 400. The status itself is what a consumer reads to tell "sign in again" from "that request was not understood", so it is now pinned by value on an access-gated route and a pre-auth gated one, along with the fact that neither adapter asks upstream anything. Checked against the previous behaviour: both new cases fail with 400 received where 401 was expected, so this catches the regression rather than merely restating the fix.
|
Asked whether this reaches Fastify as well as Express. It does, and I have now Both adapters read the status straight off the core result: // packages/express/src/middleware/ensureCookies.ts:80
res.status(result.status ?? 401).json({ error: result.errorCode });
// packages/fastify/src/hooks/ensureCookies.ts
return reply.status(result.status ?? 401).send({ error: result.errorCode });So the one change in What was missing was coverage. The parity suite already drove a session-less Added a parity case that pins 401 by value on an access-gated route Fastify is now 46 tests, and |
Closes #148, transferred here from
seamless-auth-api#153because the handlerlives in this package.
Where it actually was
The report came in against the auth API, observed through the
/authmount inseamless-idea-api. It is not that repository: the API answers401for amissing or invalid bearer token on every protected route, and the string in the
report appears nowhere in it. The
400isensureCookies, here.The change
ensureCookiesgates every access-required route. When the required cookie isabsent and there is no refresh cookie to fall back on,
refreshRequiredCookiereturns null. That is exactly the signed-out case, and it answered
400. Therequest is perfectly well formed. There is simply no session, which is what
401means.
The two branches either side of it already answered
401, for a refresh thatfailed and for a cookie that is invalid or expired. This one was the outlier
rather than the convention, so the status a signed-out visitor got depended on
which way their session happened to be absent. All three now agree. The response
body is unchanged, since it was already accurate.
Why it is worth the behaviour change
Every signed-out page view produced a
400, so400became ordinary backgroundtraffic in adopter logs. The consuming product has just added telemetry, and this
is the shape of thing that hides a real malformed request once somebody is
watching.
Consumers also turn status codes into sentences.
400asks a product to say"that request did not come through in a form we could use" when the true sentence
is "you have been signed out, sign in again", and from outside there was no way
to tell which had happened.
Blast radius
Adopter-visible, which is why the changeset is a
minorunder the pre-1.0 policyin RELEASES.md. Anything branching on
400from an/authroute to detect amalformed request will now see
401for a signed-out visitor.I checked the dependents rather than assuming:
seamless-auth-reactdoes not branch on response status for this.seamless-auth-admin-dashboardkeeps a set of statuses whose upstream messageit surfaces verbatim,
{400, 404, 409, 422}, and uses friendly wording for authstatuses. A signed-out request there stops surfacing
Missing required cookie "access"to an operator and starts getting thesigned-out wording, which is the improvement the issue asked for.
seamless-idea-apiandseamless-idea-webhave their own400s, none of themreading this one.
The change is in
core, and both adapters pass the status straight through, soExpress and Fastify pick it up without adapter changes.
Not changed
ensureCookieshas a second400, for when the adapter supplied no cookie namefor a route's requirement. That is a misconfiguration rather than a bad request,
so
500would arguably be the honest status, but it is a setup-time error ratherthan the runtime noise this issue is about and I left it alone rather than widen
the change. Worth a separate issue if you want it.
Checks
pnpm buildandpnpm testpass at the workspace root: core 227 tests, express150, fastify 44, all suites green.
Two tests pinned the old status, each with a comment presenting the
400asintended. Both now pin
401, and the express one keeps its real assertion thatthe request is never forwarded upstream. Added coverage for a pre-auth route
reached with no cookies, and for the refresh-failed branch, so all three
signed-out paths are pinned to agree.