Токену в JavaScript не место
Sanctum пишет auth_token как httpOnly. В JSON логина нет поля token. Если страница читает сессию, XSS её унесёт.
В первом JSON логина на этом API было поле token. Vue его сохранил. Тело ещё попало в access-лог nginx и в network-панель браузера. localStorage читает любой скрипт на origin. Одной внедрённой зависимости или одного неэкранированного поля хватает, чтобы токен ушёл, и он живёт до истечения срока.
Cookie с httpOnly из JavaScript не достать. Та же инъекция не прочитает auth_token. Маршрут логина теперь ставит cookie с httpOnly, secure и SameSite lax. API и Vue SSR на одном сайте, браузер шлёт cookie без игр с CORS credentials. В JSON успешного логина нет ключа token.
Пользователям строка не была нужна. XSS была нужна. Хосту нужно одно хранилище сессии, не копия в JS и копия в cookie.
CSRF всё ещё нужен
Cookie едет на любом запросе браузера к этому сайту. XSS cookie не прочитает. Форма на другом сайте всё ещё может вызвать запись, если CSRF не стоит на пути. Sanctum читает CSRF-cookie и ждёт заголовок на небезопасных методах.
Logout удаляет токен на сервере. Стереть cookie в браузере мало. Украденная cookie, которая ещё лежит в Redis или в таблице токенов, продолжит работать. После logout /api/user должен отдать 401.
Я нашёл хелпер, который после логина клал объект пользователя в sessionStorage, включая ключ token со старой формы ответа. Cookie уже была httpOnly. Хелпер вернул утечку. В storage остаётся имя и локаль, ничего, чем вызвать /api.
Тесты, которые это держат
Четыре проверки ловят большую часть регрессий, которые я тут видел. Логин не возвращает поле token. У cookie флаг httpOnly. Запись без CSRF-заголовка отклоняется. После logout эндпоинт пользователя это 401.
Фронтенд никогда не пишет auth_token в localStorage, sessionStorage или persist у Pinia. Если следующий хелпер положит объект пользователя в storage, в объекте не должно быть токена. Сессия это cookie. Всё остальное это имя на экране.
SSR не должен печатать cookie в состояние страницы. Сериализованный пользователь в HTML нормален. Сериализованный секрет это та же утечка, что localStorage, с более длинной жизнью в кэше, если nginx когда-нибудь сохранит этот HTML.
Какой опыт из этого
Грепаю JSON логина, persist Pinia и payload SSR на поле token. Логи и HTML это копии ответа. Если строка там, XSS не нужен localStorage.
Logout, который только чистит cookie в браузере, я не доверяю. Строка на сервере должна умереть. Одному SameSite не верю. CSRF всё ещё стоит на небезопасных методах.
Правило, которое оставляю: только cookie Sanctum, httpOnly, никогда в JSON. Фронтенд хранит имя на экране. /api/user после logout это 401.
