# Про токены, JSON Web Tokens (JWT), аутентификацию и авторизацию. Token-Based Authentication `Last major update: 21.10.2019` ## Основы: __Аутентификация(authentication, от греч. αὐθεντικός [authentikos] – реальный, подлинный; от αὐθέντης [authentes] – автор)__ - это процесс проверки учётных данных пользователя (логин/пароль). Проверка подлинности пользователя путём сравнения введённого им логина/пароля с данными сохранёнными в базе данных. __Авторизация(authorization — разрешение, уполномочивание)__ - это проверка прав пользователя на доступ к определенным ресурсам. Например после аутентификации юзер _**sasha**_ получает право обращатся и получать от ресурса __"super.com/vip"__ некие данные. Во время обращения юзера _**sasha**_ к ресурсу __vip__ система авторизации проверит имеет ли право юзер обращатся к этому ресурсу (проще говоря переходить по неким разрешенным ссылкам) 1. Юзер c емайлом _**sasha_gmail.com**_ успешно прошел аутентификацию 2. Сервер посмотрел в БД какая роль у юзера 3. Сервер сгенерил юзеру токен с указанной ролью 4. Юзер заходит на некий ресурс используя полученный токен 5. Сервер смотрит на права(роль) юзера в токене и соотвественно пропускает или отсекает запрос Собственно п.5 и есть процесс __авторизации__. *Дабы не путатся с понятиями __Authentication/Authorization__ можно использовать псевдонимы __checkPassword/checkAccess__(я так сделал в своей API)* __JSON Web Token (JWT)__ — содержит три блока, разделенных точками: заголовок(__header__), набор полей (__payload__) и __сигнатуру__. Первые два блока представлены в JSON-формате и дополнительно закодированы в формат base64. Набор полей содержит произвольные пары имя/значения, притом стандарт JWT определяет несколько зарезервированных имен (iss, aud, exp и другие). Сигнатура может генерироваться при помощи и симметричных алгоритмов шифрования, и асимметричных. Кроме того, существует отдельный стандарт, отписывающий формат зашифрованного JWT-токена. Пример подписанного JWT токена (после декодирования 1 и 2 блоков): ``` { alg: "HS256", typ: "JWT" }.{ iss: "auth.myservice.com", aud: "myservice.com", exp: 1435937883, userName: "John Smith", userRole: "Admin" }.S9Zs/8/uEGGTVVtLggFTizCsMtwOJnRhjaQ2BMUQhcY ``` __Токены__ предоставляют собой средство __авторизации__ для каждого запроса от клиента к серверу. Токены(и соотвественно сигнатура токена) генерируются на сервере основываясь на секретном ключе(который хранится на сервере) и __payload'e__. Токен в итоге хранится на клиенте и используется при необходимости __авторизации__ како-го либо запроса. Такое решение отлично подходит при разработке SPA. При попытке хакером подменить данные в __header'ре__ или __payload'е__, токен cтанет не валидным, поскольку сигнатура не будет соответствовать изначальным значениям. А возможность сгенерировать новую сигнатуру у хакера отсутствует, поскольку секретный ключ для зашифровки лежит на сервере. __access token__ - используется для __авторизации запросов__ и хранения дополнительной информации о пользователе (аля __user_id__, __user_role__ или еще что либо, эту информацию также называет __payload__). Сам токен храним не в localStorage как это обычно делают, а __в памяти клиентского приложения.__ __refresh token__ - выдается сервером по результам успешной аутентификации и используется для получения новой пары __access/refresh__ токенов. Храним в любом персистентном хранилище. Каждый токен имеет свой срок жизни, например __access__: 30 мин, __refresh__: 60 дней __Поскольку токены это не зашифрованная информация крайне не рекомендуется хранить в них какую либо `sensitive data` (passwords, payment credentials, etc...)__ __Роль рефреш токенов и зачем их хранить в БД.__ Рефреш на сервере хранится для учета доступа и инвалидации краденых токенов. Таким образом сервер наверняка знает о клиентах которым стоит доверять(кому позволено авторизоваться). Если не хранить рефреш токен в БД то велика вероятность того что токены будут бесконтрольно гулять по рукам злоумышленников. Для отслеживания которых нам прийдется заводить черный список и периодически чистить его от просроченных. В место этого мы храним лимитированный список белых токенов для каждого юзера отдельно и в случае кражи у нас уже есть механизм противодействия(описано ниже). ## Логин, создание сессии/токенов (api/auth/login): 1. Пользователь логинится в приложении, передавая логин/пароль и __fingerprint__ браузера (ну или некий иной уникальный индентификатор устройства если это не браузер) 2. Сервер проверят подлинность логина/пароля, 3. В случае удачи создает и записывает сессию в БД `{ userId: uuid, refreshToken: uuid, expiresIn: int, fingerprint: string, ... }` (схема таблицы ниже) 4. Отправляет клиенту два токена __access и refresh token uuid__ (взятый из выше созданной сессии) ``` "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "9f34dd3a-ff8d-43aa-b286-9f22555319f6" ``` 5. Клиент сохраняет токены(__access__ в памяти приложения, __refresh__ персистентно), используя __access token__ для последующей авторизации запросов. __Стоит заметить что процесс добавления сессии в таблицу должен имеет свои меры безопасности.__ При добавлении стоит проверять сколько сессий всего есть у юзера и если их слишком много или юзер конектится одновременно из нескольких подсетей, стоит предпринять меры. Имплементируя данную проверку я проверяю только что бы юзер имел максимум до 5 одновременных сессий максимум, и на 6'ой удаляю все остальные сессии кроме текущей(6'ой). Все остальные проверки на ваше усмотрение в зависимости от задачи. Таким образом если юзер залогинился на пяти устройствах, рефреш токены будут постоянно обновляться и все счастливы. Но если с аккаунтом юзера начнут производить подозрительные действия(попытаются залогинится более чем на 5'ти устройствах) система сбросит все сессии(рефреш токены) кроме последней. Перед каждым запросом клиент предварительно проверяет время жизни __access token'а__ (да берем `expires_in` прямо из JWT в клиентском приложении) и если оно истекло использует __refresh token__ чтобы обновить __ОБА__ токена и продолжает использовать новый __access token__. Для большей уверенности можем обновлять токены на несколько секунд раньше. Что такое __fingerprint__ ? Это инструмент отслеживания браузера вне зависимости от желания пользователя быть идентифицированным. Это хеш сгенерированный js'ом на базе неких уникальных параметров/компонентов браузера. Преимущество __fingerprint'a__ в том что он нигде персистентно не хранится и генерируется только в момент логина и рефреша. - Библиотека для хеширования: https://github.com/Valve/fingerprintjs2 - Более подробно: https://player.vimeo.com/video/151208427 - Пример ф-ции получения такого хеша: https://gist.github.com/zmts/b26ba9a61aa0b93126fc6979e7338ca3 ## Рефреш токенов (api/auth/refresh-tokens): Для использования возможности аутентификации на более чем одном девайсе необходимо хранить все рефреш токены по каждому юзеру. Я храню это список в PostgreSQL таблице. В процессе каждого логина создается запись с IP/Fingerprint и другой мета информацией то есть сессия. ``` CREATE TABLE sessions ( "id" SERIAL PRIMARY KEY, "userId" uuid REFERENCES users(id) ON DELETE CASCADE, "refreshToken" uuid NOT NULL, "ua" character varying(200) NOT NULL, /* user-agent */ "fingerprint" character varying(200) NOT NULL, "ip" character varying(15) NOT NULL, "expiresIn" bigint NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now() ); ``` 1. Клиент(фронтенд) проверяет перед запросом не истекло ли время жизни __access token'на__ 2. Если истекло клиент отправляет на `auth/refresh-token` `{ refreshToken: uuid, fingerprint: string }` 3. Сервер получает запись сессии по UUID'у рефреш токена 4. Cохраняет текущую сессию в переменную и удаляет ее из таблицы 5. Проверяет текущую сессию: 1. Не истекло ли время жизни 2. На соответствие старого __fingerprint'a__ полученного из текущей сессии с новым полученным из тела запроса 6. В случае негативного результата бросает ошибку `TOKEN_EXPIRED`/`INVALID_SESSION` 7. В случае успеха создает новую сессию и записывает ее в БД 8. Создает новый __access token__ 9. Отправляет клиенту `{ accessToken, refreshToken }` ## Ключевой момент: В момент рефреша то есть обновления __access token'a__ обновляются __ОБА__ токена. Но как же __refresh token__ может сам себя обновить, он ведь создается только после успешной аунтефикации ? __refresh token__ в момент рефреша сравнивает себя с тем __refresh token'ом__ который лежит в БД и вслучае успеха, а также если у него не истек срок, система рефрешит токены. Вопрос зачем __refresh token'y__ срок жизни, если он обновляется каждый раз при обновлении __access token'a__ ? Это сделано на случай если юзер будет в офлайне более 60 дней, тогда прийдется заново вбить логин/пароль. ## В случае кражи токенов: 1. Хакер воспользовался __access token'ом__ 2. Закончилось время жизни __access token'на__ 3. __Клиент хакера__ отправляет __refresh token__ и __fingerprint__ (если знает что он нужен) 4. Сервер смотрит __fingerprint__ хакера (если он есть в запросе) 5. Сервер не находит __fingerprint__ хакера в сессии и удаляет ее из БД 6. Сервер логирует попытку несанкционированного обновления токенов 7. Сервер перенаправляет хакера на станицу логина. Хакер идет лесом 8. Юзер пробует зайти на сервер >> обнаруживается что __refresh token__ отсутствует 9. Сервер перенаправляет юзера на форму аутентификации 10. Юзер вводит логин/пароль ### Пример имплементации: __Front-end:__ - https://github.com/zmts/beauty-vuejs-boilerplate/blob/master/src/services/http.init.js - https://github.com/zmts/beauty-vuejs-boilerplate/blob/master/src/services/auth.service.js __Back-end:__ - https://github.com/zmts/supra-api-nodejs/tree/master/actions/auth ### Info: - https://habrahabr.ru/company/Voximplant/blog/323160/ - https://tools.ietf.org/html/rfc6749 - https://www.digitalocean.com/community/tutorials/oauth-2-ru - https://jwt.io/introduction/ - https://auth0.com/blog/using-json-web-tokens-as-api-keys/ - https://auth0.com/blog/cookies-vs-tokens-definitive-guide/ - https://auth0.com/blog/ten-things-you-should-know-about-tokens-and-cookies/ - https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/ - https://habr.com/company/dataart/blog/262817/ - https://habr.com/post/340146/ - https://habr.com/company/mailru/blog/115163/ - https://scotch.io/tutorials/authenticate-a-node-js-api-with-json-web-tokens - https://egghead.io/courses/json-web-token-jwt-authentication-with-node-js - https://www.digitalocean.com/community/tutorials/oauth-2-ru - https://github.com/shieldfy/API-Security-Checklist/blob/master/README-ru.md - https://www.youtube.com/watch?v=Ngh3KZcGNaU - https://www.youtube.com/playlist?list=PLvTBThJr861y60LQrUGpJNPu3Nt2EeQsP - https://www.youtube.com/watch?v=R0-eoLp871s - https://www.youtube.com/watch?v=u9hn3s2kUrg - https://ain.ua/2020/02/29/adtech-bez-cookies/ ### And why JWT is bad - http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/ - http://cryto.net/~joepie91/blog/2016/06/19/stop-using-jwt-for-sessions-part-2-why-your-solution-doesnt-work/ - https://medium.com/@cjainn/anatomy-of-a-jwt-token-part-1-8f7616113c14 - https://medium.com/@cjainn/anatomy-of-a-jwt-token-part-2-c12888abc1a2 - https://scotch.io/bar-talk/why-jwts-suck-as-session-tokens - https://t.me/why_jwt_is_bad