{"id":24873872,"url":"https://github.com/andreiextr/tokens","last_synced_at":"2026-01-06T10:41:46.787Z","repository":{"id":274094290,"uuid":"921892880","full_name":"AndreiExtr/Tokens","owner":"AndreiExtr","description":null,"archived":false,"fork":false,"pushed_at":"2025-01-24T20:22:47.000Z","size":9,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-02-01T06:18:22.231Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AndreiExtr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-01-24T20:19:57.000Z","updated_at":"2025-01-24T20:22:50.000Z","dependencies_parsed_at":"2025-01-24T21:33:50.858Z","dependency_job_id":null,"html_url":"https://github.com/AndreiExtr/Tokens","commit_stats":null,"previous_names":["andreiextr/tokens"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndreiExtr%2FTokens","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndreiExtr%2FTokens/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndreiExtr%2FTokens/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndreiExtr%2FTokens/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AndreiExtr","download_url":"https://codeload.github.com/AndreiExtr/Tokens/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245760901,"owners_count":20667893,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-02-01T06:18:23.912Z","updated_at":"2026-01-06T10:41:46.719Z","avatar_url":"https://github.com/AndreiExtr.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# Про токены, JSON Web Tokens (JWT), аутентификацию и авторизацию. Token-Based Authentication\n\n`Last major update: 25.08.2020`\n\n- Что такое авторизация/аутентификация\n- Где хранить токены\n- Как ставить куки ?\n- Процесс логина\n- Процесс рефреш токенов\n- Кража токенов/Механизм контроля токенов\n- Зачем все это ? JWT vs Cookie sessions\n\n## Основа:\n__Аутентификация(authentication, от греч. αὐθεντικός [authentikos] – реальный, подлинный; от αὐθέντης [authentes] – автор)__ - это процесс проверки учётных данных пользователя (логин/пароль). Проверка подлинности пользователя путём сравнения введённого им логина/пароля с данными сохранёнными в базе данных.\n\n__Авторизация(authorization — разрешение, уполномочивание)__ - это проверка прав пользователя на доступ к определенным ресурсам.\n\nНапример, после аутентификации юзер _**sasha**_ получает право обращаться и получать от ресурса __\"super.com/vip\"__ некие данные. Во время обращения юзера _**sasha**_ к ресурсу __vip__ система авторизации проверит имеет ли право юзер обращаться к этому ресурсу (проще говоря переходить по неким разрешенным ссылкам)\n\n1. Юзер c емайлом _**sasha_gmail.com**_ успешно прошел аутентификацию\n2. Сервер посмотрел в БД какая роль у юзера\n3. Сервер сгенерил юзеру токен с указанной ролью\n4. Юзер заходит на некий ресурс используя полученный токен\n5. Сервер смотрит на права(роль) юзера в токене и соответственно пропускает или отсекает запрос\n\nСобственно п.5 и есть процесс __авторизации__.\n\n*Дабы не путаться с понятиями __Authentication/Authorization__ можно использовать псевдонимы __checkPassword/checkAccess__(я так сделал в своей API)*\n\n__JSON Web Token (JWT)__ — содержит три блока, разделенных точками: заголовок(__header__), набор полей (__payload__) и __сигнатуру__. Первые два блока представлены в JSON-формате и дополнительно закодированы в формат base64. Набор полей содержит произвольные пары имя/значения, притом стандарт JWT определяет несколько зарезервированных имен (iss, aud, exp и другие). Сигнатура может генерироваться при помощи и симметричных алгоритмов шифрования, и асимметричных. Кроме того, существует отдельный стандарт, отписывающий формат зашифрованного JWT-токена.\n\nПример подписанного JWT токена (после декодирования 1 и 2 блоков):\n```\n{ alg: \"HS256\", typ: \"JWT\" }.{ iss: \"auth.myservice.com\", aud: \"myservice.com\", exp: 1435937883, userName: \"John Smith\", userRole: \"Admin\" }.S9Zs/8/uEGGTVVtLggFTizCsMtwOJnRhjaQ2BMUQhcY\n```\n\n__Токены__ предоставляют собой средство __авторизации__ для каждого запроса от клиента к серверу. Токены(и соответственно сигнатура токена) генерируются на сервере основываясь на секретном ключе(который хранится на сервере) и __payload'e__. Токен в итоге хранится на клиенте и используется при необходимости __авторизации__ какого-либо запроса. Такое решение отлично подходит при разработке SPA.\n\nПри попытке хакером подменить данные в __header'ре__ или __payload'е__, токен станет не валидным, поскольку сигнатура не будет соответствовать изначальным значениям. А возможность сгенерировать новую сигнатуру у хакера отсутствует, поскольку секретный ключ для зашифровки лежит на сервере.\n\n__access token__ - используется для __авторизации запросов__ и хранения дополнительной информации о пользователе (аля __user_id__, __user_role__ или еще что либо, эту информацию также называет __payload__). Все поля в __payload__ это свободный набор полей необходимый для реализации вашей частной бизнес логики. То бишь __user_id__ и __user_role__ не являются требованием и представляют собой исключительно частный случай. __Сам токен храним не в localStorage как это обычно делают, а в памяти клиентского приложения__ (что приводит к необходимости при каждом обновлении странички браузера запрашивать новую пару токенов).\n\n__refresh token__ - выдается сервером по результам успешной аутентификации и используется для получения новой пары __access/refresh__ токенов. __Храним исключительно в httpOnly куке__.\n\nКаждый токен имеет свой срок жизни, например __access__: 30 мин, __refresh__: 60 дней\n\n__Поскольку токены(а данном случае access) это не зашифрованная информация крайне не рекомендуется хранить в них какую либо `sensitive data` (passwords, payment credentials, etc...)__\n\n### Роль рефреш токенов и зачем их хранить в БД ?\nРефреш на сервере хранится для учета доступа и инвалидации краденых токенов. Таким образом сервер наверняка знает о клиентах которым стоит доверять(кому позволено авторизоваться). Если не хранить рефреш токен в БД то велика вероятность того что токены будут бесконтрольно гулять по рукам злоумышленников. Для отслеживания которых нам придется заводить черный список и периодически чистить его от просроченных. В место этого мы храним лимитированный список белых токенов для каждого юзера отдельно и в случае кражи у нас уже есть механизм противодействия(описано ниже).\n\n## Как ставить куки ?\nДля того что бы `refreshToken` кука была успешно уставленна и отправлена браузером, адреса эндпоинтов аутентификации(`/api/auth/login`, `/api/auth/refresh-tokens`, `/api/auth/logout`) должны располагася в доменном пространстве сайта. Тоесть для домена `super.com` на сервере ставим куку с такими опциями:\n```\n{\n    domain: '.super.com',\n    path: '/api/auth'\n}\n```\n\nТаким образом кука установится в браузер и прийдет на все эндпоинты по адресу `super.com/api/auth/\u003cany-path\u003e`\n\nЕсли у нас монолит и за аутентификацию отвечает один и тот-же API, тут проблем не должно быть. Но если за аутентификацию отвечает отдельный микросервис, прячем его средствами `nginx` по выше указанному пути (`super.com/api/auth`).\n```\n# пример настройки nginx конфига(только основые настройки)\nserver {\n    listen 80;\n    server_name super.com;\n    # SPA/Front-end\n    location / {\n        try_files $uri /index.html;\n        root /var/www/frontend/dist;\n        index index.html;\n    }\n    # Main API\n    location /api {\n        proxy_pass http://111.111.111.111:7000;\n    }\n    # Auth API\n    location /api/auth {\n        proxy_redirect http://222.222.222.222:7000   /auth/;\n        proxy_pass http://222.222.222.222:7000;\n    }\n}\n```\n\n## Логин, создание сессии/токенов (api/auth/login):\n1. Пользователь логинится в приложении, передавая логин/пароль и __fingerprint__ браузера (ну или некий иной уникальный идентификатор устройства если это не браузер)\n2. Сервер проверят подлинность логина/пароля \n3. В случае удачи создает и записывает сессию в БД `{ userId: uuid, refreshToken: uuid, expiresIn: int, fingerprint: string, ... }` (схема таблицы ниже)\n4. Создает __access token__\n5. Отправляет клиенту __access и refresh token uuid__ (взятый из выше созданной сессии)\n```\nSet-Cookie: refreshToken='c84f18a2-c6c7-4850-be15-93f9cbaef3b3'; HttpOnly // для браузера\n{\n  body: { \n    accessToken: 'eyJhbGciOiJIUzUxMiIsI...',\n    refreshToken: 'c84f18a2-c6c7-4850-be15-93f9cbaef3b3' // для мобильных приложений\n  }\n}\n```\n6. Клиент сохраняет токены(__access__ в памяти приложения, __refresh__  сетится как кука автоматом)\n\nНа что нужно обратить внимание при установке __refresh__ куки:\n- `maxAge` куки ставим равную `expiresIn` из выше созданной сессии\n- В `path` ставим корневой роут `auth` контроллера  (`/api/auth`) это важно, таким образом токен получат только те хендлеры которым он нужен(`/api/auth/logout` и `/api/auth/rerfesh-tokens`), остальные обойдутся(нечего зря почём отправлять __sensitive data__).\n\n__Стоит заметить, что процесс добавления сессии в таблицу должен имеет свои меры безопасности.__ При добавлении стоит проверять сколько рефреш-сессий всего есть у юзера и, если их слишком много или юзер конектится одновременно из нескольких подсетей, стоит предпринять меры. Имплементируя данную проверку, я проверяю только что бы юзер имел максимум до 5 одновременных рефреш-сессий максимум, и при попытке установить следующую удаляю предыдущие. Все остальные проверки на ваше усмотрение в зависимости от задачи.\n\nТаким образом если юзер залогинился на пяти устройствах, рефреш токены будут постоянно обновляться и все счастливы. Но если с аккаунтом юзера начнут производить подозрительные действия(попытаются залогинится более чем на 5'ти устройствах) система сбросит все сессии(рефреш токены) кроме последней.\n\nПеред каждым запросом клиент предварительно проверяет время жизни __access token'а__ (да берем `expiresIn` прямо из JWT в клиентском приложении) и если оно истекло  шлет запрос на обновление токенов. Для большей уверенности можем обновлять токены на несколько секунд раньше. То есть кейс когда API получит истекший __access__ токен практически исключен.\n\nЧто такое __fingerprint__ ? Это инструмент отслеживания браузера вне зависимости от желания пользователя быть идентифицированным. Это хеш сгенерированный js'ом на базе неких уникальных параметров/компонентов браузера. Преимущество __fingerprint'a__ в том что он нигде персистентно не хранится и генерируется только в момент логина и рефреша.\n- Библиотека для хеширования:  https://github.com/Valve/fingerprintjs2\n- Более подробно:  https://player.vimeo.com/video/151208427\n- Пример ф-ции получения такого хеша: https://gist.github.com/zmts/b26ba9a61aa0b93126fc6979e7338ca3\n\nВ случае если клиент не браузер, а мобильное приложение, в качестве __fingerprint__ используем любую уникальную строку(тот же `uuid`) персистентно хранящуюся на устройстве.\n\n## Рефреш токенов (api/auth/refresh-tokens):\nДля использования возможности аутентификации на более чем одном девайсе необходимо хранить все рефреш токены по каждому юзеру. Я храню это список в PostgreSQL таблице(а надо бы в Redis'е). В процессе каждого логина создается запись с IP/Fingerprint и другой мета информацией, так званая __рефреш-сессия__.\n```\nCREATE TABLE refreshSessions (\n    \"id\" SERIAL PRIMARY KEY,\n    \"userId\" uuid REFERENCES users(id) ON DELETE CASCADE,\n    \"refreshToken\" uuid NOT NULL,\n    \"ua\" character varying(200) NOT NULL, /* user-agent */\n    \"fingerprint\" character varying(200) NOT NULL,\n    \"ip\" character varying(15) NOT NULL,\n    \"expiresIn\" bigint NOT NULL,\n    \"createdAt\" timestamp with time zone NOT NULL DEFAULT now()\n);\n```\n\n1. Клиент(фронтенд) проверяет перед запросом не истекло ли время жизни __access token'на__\n2. Если истекло клиент делает запрос на `POST auth/refresh-tokens` `{ fingerprint: string }` в `body` и соответственно `refreshToken` куку.\n3. Сервер получает запись рефреш-сессии по UUID'у рефреш токена\n4. Сохраняет текущую рефреш-сессию в переменную и удаляет ее из таблицы\n5. Проверяет текущую рефреш-сессию:\n    1. Не истекло ли время жизни\n    2. На соответствие старого  __fingerprint'a__ полученного из текущей рефреш-сессии с новым полученным из тела запроса\n6. В случае негативного результата бросает ошибку `TOKEN_EXPIRED`/`INVALID_REFRESH_SESSION`\n7. В случае успеха создает новую рефреш-сессию и записывает ее в БД\n8. Создает __access token__\n8. Отправляет клиенту __access и refresh token uuid__ (взятый из выше созданной рефреш-сессии)\n```\nSet-Cookie: refreshToken='c84f18a2-c6c7-4850-be15-93f9cbaef3b3'; HttpOnly // для браузера\n{\n  body: { \n    accessToken: 'eyJhbGciOiJIUzUxMiIsI...',\n    refreshToken: 'c84f18a2-c6c7-4850-be15-93f9cbaef3b3' // для мобильных приложений\n  }\n}\n```\n\n_Tip:_ Для отправки запроса с куками для `axios` есть опция `{ withCredentials: true }`\n\n## Ключевой момент:\nВ момент рефреша то есть обновления __access token'a__ обновляются __ОБА__ токена. Но как же __refresh token__ может сам себя обновить, он ведь создается только после успешной аутентификации ? __refresh token__ в момент рефреша сравнивает себя с тем __refresh token'ом__ который лежит в БД и вслучае успеха, а также если у него не истек срок, система рефрешит токены. \n\nВопрос зачем __refresh token'y__ срок жизни, если он обновляется каждый раз при обновлении __access token'a__ ? Это сделано на случай, если юзер будет в офлайне более 60 дней, тогда придется заново вбить логин/пароль.\n\n## Logout (api/auth/logout)\n1. Front-end делает кол `POST: api/auth/logout` c __refreshToken__ в куке или бади (лучше в куки)\n2. Front-end удаляет локально сохраненный в памяти __accessToken__\n3. Back-end удаляет запись из таблицы `refreshSessions` по __refreshToken__\n\n__accessToken__ умирает по истечению строка его жизни. Руками банить, удалять, хранить __accessToken__ не нужно, это нарушает всю суть эксесс токена.\n\n## В случае кражи access токена и refresh куки:\n1. Хакер воспользовался __access token'ом__\n2. Закончилось время жизни __access token'на__\n3. __Клиент хакера__ отправляет __refresh token__ и __fingerprint__\n4. Сервер смотрит __fingerprint__ хакера\n5. Сервер не находит __fingerprint__ хакера в рефреш-сессии и удаляет ее из БД\n6. Сервер логирует попытку несанкционированного обновления токенов\n7. Сервер перенаправляет хакера на станицу логина. Хакер идет лесом\n8. Юзер пробует зайти на сервер \u003e\u003e обнаруживается что __refresh token__ отсутствует\n9. Сервер перенаправляет юзера на форму аутентификации\n10. Юзер вводит логин/пароль\n\n## В случае кражи access токена, refresh куки и fingerprint'а:\nСтащить все авторизационные данные это не из легких задач, но все же допустим этот кейс как крайний и наиболее неудобный с точки зрения UX (без примера в кодовой базе `supra-api-nodejs`).\n\nПредложу несколько вариантов решения данной проблемы:\n\n- Хранить IP или Subnet залогиненного клиента\n1. Хакер воспользовался __access token'ом__\n2. Закончилось время жизни __access token'на__\n3. __Хакер__ отправляет __refresh__ куку и __fingerprint__\n4. Сервер проверяет IP __хакера__, __хакер__ идет лесом\n\n_UX минус: нужно логинится с каждого нового IP._\n\n- Удалять все сессии в случае если __refresh__ токен не найден\n1. Хакер воспользовался __access token'ом__\n2. Закончилось время жизни __access token'на__\n3. __Хакер__ отправляет __refresh__ куку и __fingerprint__\n4. На сервере создается новый __refresh__ токен (\"от хакера\")\n5. Хакер получает новую пару токенов\n6. Юзер пробует отправить запрос на сервер \u003e\u003e обнаруживается что __refresh__ токен не валиден\n7. Сервер удаляет все сессии юзера, в последствии чего хакер больше не сможет обновлять __access__ токен\n8. Сервер создает новую сессию для пользователя\n\n_UX минус: в каждом случае когда сервер не будет находить рефреш токен - будут сбрасиватся все сессии юзера на всех устройствах._\n\n## Зачем все это ? JWT vs Cookie sessions\nЗачем этот весь геморой ? Почему не юзать старые добрые cookie sessions ? Чем не угодили куки ?\n- Куки подвержены CSRF: https://habr.com/ru/company/oleg-bunin/blog/412855 https://www.youtube.com/watch?v=x5AuK_IbJlg\n- Нативыным приложениям для сматфонов удобнее работать с токенами. Да есть хаки для работы с куки, но это не нативная поддержка\n- Куки в микросерисной архитектуре использовать не вариант. Напомню зачастую микросервисы раскиданы на разных доменах, а куки не поддерживают кросc-доменные запросы\n- В микросерисной архитектуре JWT позволяет каждому сервису независимо от сервера авторизации верифицировать `access` токен (через публичный ключ)\n- При использовании cookie sessions программист зачастую надеется на то, что предоставил фреймворк и оставляет как есть\n- При использовании jwt мы видим проблему с безопасностью и стараемся предусмотреть механизмы контроля в случае каржи авторизационных данных. При использовании cookie сессий программист зачастую даже не задумывается что сессия может быть скомпрометирована\n- __На каждом запросе__ использование JWT избавляет бекенд от одного запроса в БД(или кеш) за данными пользователя(`userId`, `email`, etc.)\n\n## В итоге:\n- __access__ токены храним исключительно в памяти клиентского приложения. Не в глобально доступной переменной аля `window.accessToken` а в __замыкании__\n- __refresh__ токен храним исключительно в __httpOnly__ куке\n- Механизмы контроля при угоне __sensitive data__ в наличии\n- Взяли лучшее из обеих технологий, максимально обезопасились от CSRF/XSS\n- Добавьте в компанию ко всему CSP заголовки и SameSite=Strict флаг для кук и ждите прихода злодеев\n\np.s. Каждой задаче свой подход. Юзайте в небольших/средних монолитах `cookie sessions` и не парьтесь. Ну или на ваш вкус :) \n\n___\n### Имплементация:\n__Front-end:__\n- https://github.com/zmts/beauty-vuejs-boilerplate/blob/master/src/services/http.init.js\n- https://github.com/zmts/beauty-vuejs-boilerplate/blob/master/src/services/auth.service.js\n\n__Back-end:__\n- https://github.com/zmts/supra-api-nodejs/tree/master/modules/auth\n\n### Info:\n- https://www.youtube.com/playlist?list=PLvTBThJr861y60LQrUGpJNPu3Nt2EeQsP\n- https://habrahabr.ru/company/Voximplant/blog/323160/\n- https://tools.ietf.org/html/rfc6749\n- https://www.digitalocean.com/community/tutorials/oauth-2-ru\n- https://jwt.io/introduction/\n- https://auth0.com/blog/using-json-web-tokens-as-api-keys/\n- https://auth0.com/blog/cookies-vs-tokens-definitive-guide/\n- https://auth0.com/blog/ten-things-you-should-know-about-tokens-and-cookies/\n- https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/\n- https://habr.com/company/dataart/blog/262817/\n- https://habr.com/post/340146/\n- https://habr.com/company/mailru/blog/115163/\n- https://scotch.io/tutorials/authenticate-a-node-js-api-with-json-web-tokens\n- https://egghead.io/courses/json-web-token-jwt-authentication-with-node-js\n- https://www.digitalocean.com/community/tutorials/oauth-2-ru\n- https://github.com/shieldfy/API-Security-Checklist/blob/master/README-ru.md\n- https://www.youtube.com/watch?v=Ngh3KZcGNaU\n- https://www.youtube.com/watch?v=R0-eoLp871s\n- https://www.youtube.com/watch?v=u9hn3s2kUrg\n- https://ain.ua/2020/02/29/adtech-bez-cookies/\n- https://habr.com/ru/post/492830 (cookies SameSite)\n\n### And why JWT is bad\n- http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/\n- http://cryto.net/~joepie91/blog/2016/06/19/stop-using-jwt-for-sessions-part-2-why-your-solution-doesnt-work/\n- https://medium.com/@cjainn/anatomy-of-a-jwt-token-part-1-8f7616113c14\n- https://medium.com/@cjainn/anatomy-of-a-jwt-token-part-2-c12888abc1a2\n- https://scotch.io/bar-talk/why-jwts-suck-as-session-tokens\n- https://t.me/why_jwt_is_bad\n\n---\n_Комментарии периодически подчищаются_","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandreiextr%2Ftokens","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandreiextr%2Ftokens","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandreiextr%2Ftokens/lists"}