-
Notifications
You must be signed in to change notification settings - Fork 6
session and token storage
The admin described here is barakoBrew, which now lives in its own repository (BaryoDev/barakoBrew). The API half, the refresh cookie and the endpoints that issue and read it, is still here.
Short version: the access token lives in memory and disappears on reload; the refresh token lives in a cookie the page cannot read. If you sign in on a second tab and it pauses for a moment before loading, that is this working as intended.
Before 4.0 the admin kept both tokens in localStorage.
localStorage is readable by any JavaScript running on the origin. That is not a flaw in
localStorage, it is what it is for. It becomes a problem when what you keep there is a credential.
Two credentials were kept there, and they are not equally dangerous:
| token | life | what it can do |
|---|---|---|
| access token | 15 minutes | authorise requests until it expires |
| refresh token | 7 days, renewable | mint fresh access tokens, indefinitely |
The refresh token is the one that mattered. Stealing it is not stealing a session, it is stealing the ability to keep making sessions. Rotation does not help: an attacker who holds it and keeps refreshing rotates it along with you, and the honest owner is the one who gets logged out.
So a single cross-site scripting bug, or a single compromised npm dependency inside the admin's own build, was a week of account takeover rather than fifteen minutes of nuisance.
The refresh token is an httpOnly cookie. The server sets it; the browser sends it back; the page has no way to read it. The durable credential is never in reach of script, so an XSS cannot copy it out and keep refreshing from somewhere else next week.
Be clear about what that does not buy you. Script running on the page can still call
POST /api/auth/refresh, and the browser will attach the cookie for it, exactly as it does for the
admin. So an active XSS can use your session for as long as it is running. What it cannot do is take
the session with it. That is the difference between a bug you fix and a credential you have to
revoke, and it is the whole reason for the change, but it is not immunity.
The cookie is scoped to /api/auth/refresh, so it is not attached to every API call, only to the one
route that consumes it.
The access token is a variable in memory. Not localStorage, not sessionStorage. It is gone
when the tab reloads, and the admin quietly asks for a new one using the cookie.
Two things, both small, both a direct consequence of the above:
- A reload or a new tab pauses briefly. There is no token in memory yet, so the admin does one silent refresh before the first request. That round trip is the cost of not persisting anything.
- Closing every tab ends the in-memory half. The cookie still carries your session, so you are not signed out; the next visit refreshes and continues.
If you were signed in before upgrading, the old values are cleared from localStorage the first time
you sign out or your session ends, so an upgrade does not leave a week-long refresh token sitting in
storage for a later bug to find.
The API still returns the refresh token in the response body. A cookie is a browser mechanism. The generated clients, anything you build with the module packages, a mobile app, a script in CI: none of those have a cookie jar you would want to rely on, and all of them read the token from the response today.
Making the cookie a replacement rather than an addition would have broken every non-browser caller
in order to fix a browser-only problem. So POST /api/auth/refresh accepts the token in the body
or in the cookie, and prefers the body when both are present.
The security gain is not that the token stopped existing in the response. It is that the admin stops persisting it. A page that holds a credential for a few milliseconds during sign-in is a much smaller target than one that keeps it in storage for a week.
The cookie is SameSite=Lax, which means the browser sends it when the admin and the API are the
same site.
-
Same origin, or same site with different paths. Works with no configuration. One host
serving the console at
/adminand the API at/apineeds nothing set: same origin, so the cookie is sent. -
Different port, same host, for example the admin on
:3000and the API on:5005in local development. These are different origins but the same site, andSameSite=Laxis about the site, so the cookie is sent. It needs credentialed CORS to be useful, which the API already configures forhttp://localhost:3000. If you move the admin to another port, add that origin. - Genuinely different sites, the admin and the API on separate domains. The cookie is not sent, and there is no fallback: the admin posts an empty refresh, gets nothing back, clears its in-memory token and sends you to the sign-in page. It does not read the refresh token out of the response body.
So if the two halves are on separate domains, put them behind one reverse proxy so they share an origin. That is the same thing you would do for cookie auth on any other application.
Secure is set on every host except Development, rather than following the scheme of the request.
Request.IsHttps describes the hop that reached the process, not the one the browser made, so
behind a proxy that terminates TLS the request arrives as plain http and the cookie would have gone
out without Secure on exactly the deployment that needs it. Development is exempt because a
Secure cookie is not sent over http and every local stack would break with a symptom that looks
like "refresh is broken" rather than like a cookie policy.
Cross-site scripting is still bad. This limits the blast radius of one; it does not prevent one.
An XSS present during sign-in can still read the access token from memory and the refresh token out of the login response as it arrives. What it can no longer do is arrive a week later, read storage, and find a valid credential waiting.
The Content-Security-Policy, the dependency audit in CI, and the accessibility and lint gates all exist to make that first XSS less likely. This assumes one happens anyway and asks what it costs.
Related: docs/compliance-posture.md for the wider picture, and SECURITY.md for reporting.
Generated from docs/session-and-token-storage.md by scripts/wiki-sync.sh. Edit the doc in the repository, not this page.
Releases
Start here
- Approval by configuration
- Configuring email
- Delivering a client project on barakoCMS
- Deploying barakoCMS on a VM
- Deploying barakoCMS on a managed platform
- Upgrading from 3.x to 4.0
- Your first module
Content
- Content type blueprints
- Choice fields
- Pushing entries to a collection
- Collections filled from outside
- Public delivery API
- Event-sourced content types
- Image variants
- Scheduling publish, unpublish and sensitivity
- SEO fields
- Site settings
- URL redirects
Security and access
- Security and compliance posture
- Scanning uploads for malware
- Where the admin keeps your session, and why
Tenancy
Operations