Get CV

Security

The token never belongs in JavaScript

Sanctum writes auth_token as httpOnly. The login JSON has no token field. If the page can read the session, XSS can send it away.

The first login JSON on this API had a token field. Vue stored it. The body also landed in the nginx access log and in the browser network panel. localStorage is readable by any script on the origin. One injected package or one unescaped field is enough to copy the token, and the token stays valid until it expires.

An httpOnly cookie is not reachable from JavaScript. The same injection cannot read auth_token. The login route now sets the cookie with httpOnly, secure, and SameSite lax. The API and the Vue SSR app share the same site, so the browser sends the cookie without CORS credential tricks. The JSON body of a successful login has no token key.

Users never needed the string. XSS did. The host needed one session store, not a copy in JS and a copy in the cookie.

Do not put the token in the JSON body. Logs and client state would keep a copy.
Do not put the token in the JSON body. Logs and client state would keep a copy.

CSRF still matters

Cookies ride on any request the browser makes to this site. XSS cannot read the cookie. A form on another site can still trigger a write, unless CSRF is in the way. Sanctum reads the CSRF cookie and expects the matching header on unsafe methods.

Logout deletes the token on the server. Clearing the cookie in the browser is not enough. A stolen cookie that still exists in Redis or in the personal access table would keep working. After logout, /api/user must return 401.

I found a helper that put the user object into sessionStorage after login, including a token key copied from an old response shape. The cookie was already httpOnly. The helper recreated the leak. The object that remains in storage is name and locale, nothing that can call /api.

Pair the cookie with CSRF. Sanctum expects the header on unsafe methods.
Pair the cookie with CSRF. Sanctum expects the header on unsafe methods.

Tests that keep this

Four assertions catch most of the regressions I have seen here. Login returns no token field. The cookie has the httpOnly flag. A write without the CSRF header is rejected. After logout, the user endpoint is 401.

The frontend never writes auth_token into localStorage, sessionStorage, or a Pinia persist plugin. If a future helper puts the user object in storage, the object must not contain the token. The cookie is the session. Everything else is a display name.

SSR must not print the cookie into the page state. A serialized user in the HTML is fine. A serialized secret is the same leak as localStorage, with a longer cache lifetime if nginx ever stores that HTML.

What I took from this

I grep login JSON, Pinia persist, and the SSR payload for a token field. Logs and HTML are copies of the response. If the string is there, XSS does not need localStorage.

I do not trust logout that only clears the browser cookie. The server row has to die. I do not trust SameSite alone. CSRF still sits on unsafe methods.

The rule I keep: Sanctum cookie only, httpOnly, never in the JSON body. The frontend stores a display name. /api/user after logout is 401.

Back to notes