Что обычно возвращает backend при успешной авторизации
Что проверяют
Представляете ли вы контракт логина целиком: что приходит в ответе, куда фронтенд это кладёт и чем один вариант хранения отличается от другого. Это вопрос не про формат JSON, а про безопасность.
Как отвечать
Есть два распространённых варианта, и стоит назвать оба.
Токены в теле ответа, схема в духе OAuth 2:
{ "access_token": "...", "token_type": "Bearer",
"expires_in": 3600, "refresh_token": "..." }expires_in задаётся в секундах, дальше клиент шлёт заголовок Authorization: Bearer <токен>.
Сессия в куке: сервер отвечает заголовком Set-Cookie с флагами HttpOnly, Secure, SameSite, а в теле только профиль. До такой куки скрипт на странице не дотянется, и хранить на клиенте ничего не нужно.
Дополнительно обычно приходит минимальный профиль: идентификатор, имя, аватар, роли или права. По нему рисуют шапку и прячут недоступные разделы. Факт успеха несёт код ответа, отдельное поле "status": "success" в теле избыточно.
Хорошо добавить, что делает фронтенд дальше: хранит токен, обновляет его по refresh-токену, чистит состояние и уводит на страницу входа при 401.
Где спотыкаются
Кладут токен в localStorage и называют это нормой. Любой сторонний скрипт на странице читает его свободно, а срок жизни токена не поможет. Если токен всё же приходит в теле, безопаснее держать его в памяти вкладки и обновлять по куке.
Второе: считают полезную нагрузку JWT секретом. Это обычный base64, его видно в инструментах разработчика. Подделать токен без ключа нельзя, но и прятать в нём личные данные бессмысленно, а решение о правах всегда принимает сервер.
Третье: не продумывают протухание. Вкладка открыта сутки, токен жил час, и приложение сыплет ошибками. Нужен один перехватчик запросов, который обновляет токен и повторяет отложенные вызовы, иначе десяток параллельных запросов запускает десяток обновлений сразу.