В данной заметке рассматривается работа JWT с симметичным алгоритмом шифрования (HS256/HS384/HS512)
Аутентификация(authentication, от греч. αὐθεντικός [authentikos] – реальный, подлинный; от αὐθέντης [authentes] – автор) - это процесс проверки учётных данных пользователя (логин/пароль). Проверка подлинности пользователя путём сравнения введённого им логина/пароля с данными сохранёнными в базе данных.
Авторизация(authorization — разрешение, уполномочивание) - это проверка прав пользователя на доступ к определенным ресурсам.
Например после аутентификации юзер sasha получает право обращатся и получать от ресурса "super.com/vip" некие данные. Во время обращения юзера sasha к ресурсу vip система авторизации проверит имеет ли право юзер обращатся к этому ресурсу (проще говоря переходить по неким разрешенным ссылкам)
- Юзер c емайлом sasha_gmail.com успешно прошел аутентификацию
- Сервер посмотрел в БД какая роль у юзера
- Сервер сгенерил юзеру токен с указанной ролью
- Юзер заходит на некий ресурс используя полученный токен
- Сервер смотрит на права(роль) юзера в токене и соотвественно пропускает или отсекает запрос
Собственно п.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)
refresh token - выдается сервером по результам успешной аутентификации и используется для получения нового access token'a и обновления refresh token'a
Каждый токен имеет свой срок жизни, например access: 30мин, refresh: 60дней
Поскольку токены это не зашифрованная информация крайне не рекомендуется хранить в них такую информацию как пароли.
Роль рефреш токенов и зачем их хранить в БД. Рефреш на сервере хранится для учета доступа и инвалидации краденых токенов. Таким образом сервер наверняка знает о клиентах которым стоит доверять(кому позволено авторизоваться). Если не хранить рефреш токен в БД то велика вероятность того что токены будут бесконтрольно гулять по рукам злоумышленников. Для отслеживания которых нам прийдется заводить черный список и периодически чистить его от просроченных. В место этого мы храним лимитированный список белых токенов для каждого юзера отдельно и в случае кражи у нас уже есть механизм противодействия(описано ниже).
- Пользователь логинится в приложении, передавая логин/пароль на сервер
- Сервер проверят подлинность логина/пароля, в случае удачи генерирует и отправляет клиенту два токена(access, refresh) и время смерти access token'а (
expires_in
поле, в unix timestamp). Также в payload refresh token'a добавляется user_id
"accessToken": "...",
"refreshToken": "...",
"expires_in": 1502305985425
- Клиент сохраняет токены и время смерти access token'а, используя access token для последующей авторизации запросов
- Перед каждым запросом клиент предварительно проверяет время жизни access token'а (из
expires_in
)и если оно истекло использует refresh token чтобы обновить ОБА токена и продолжает использовать новый access token
- Клиент проверяет перед запросом не истекло ли время жизни access token'на
- Если истекло клиент отправляет на
auth/refresh-token
URL refresh token - Сервер берет user_id из payload'a refresh token'a по нему ищет в БД запись данного юзера и достает из него refresh token
- Сравнивает refresh token клиента с refresh token'ом найденным в БД
- Проверяет валидность и срок действия refresh token'а
- В случае успеха сервер:
- Создает и перезаписывает refresh token в БД
- Создает новый access token
- Отправляет оба токена и новый
expires_in
access token'а клиенту
- Клиент повторяет запрос к API c новым access token'ом
С такой схемой юзер сможет быть залогинен только на одном устройстве. Тоесть в любом случае при смене устройства ему придется логинится заново.
Если рассматривать возможность аутентификации на более чем одном девайсе/браузере: необходимо хранить весь список валидных рефреш токенов юзера. Если юзер авторизовался более чем на ±10ти устройствах(что есть весьма подозрительно), автоматически инвалидоровать все рефреш токены кроме текущего и отправлять email с security уведомлением. Как вариант список токенов можно хранить в jsonb(если используется PostgreSQL).
Для использования возможности аутентификации на более чем одном девайсе необходимо хранить все рефреш токены по каждому юзеру. Я этот список храню в записи юзера в виде JSONB.
-------------------------------------------------------------------------------------------------
| id | username | refreshTokensMap
-------------------------------------------------------------------------------------------------
| 1 | alex | { refreshTokenTimestamp1: 'refreshTokenBody1', refreshTokenTimestamp2: 'refreshTokenBody2'}
-------------------------------------------------------------------------------------------------
- Клиент проверяет перед запросом не истекло ли время жизни access token'на
- Если истекло клиент отправляет на
auth/refresh-token
URL refresh token - Сервер берет user_id из payload'a refresh token'a по нему ищет в БД запись данного юзера и достает из него refresh token
- Сравнивает refresh token клиента с refresh token'ом найденным в БД
- Проверяет валидность и срок действия refresh token'а (но если токен не валиден удаляет его сразу)
- В случае успеха сервер:
- Удаляет старый рефреш токен
- Проверяет количество уже существующих решфреш токенов.
- Если их больше 10, удаляет все токены, создает новый и запиывает его в БД.
- Если их меньше 10 просто создает и записывает новый в БД.
- Создает новый access token
- Отправляет оба токена и новый
expires_in
access token'а клиенту
- Клиент повторяет запрос к API c новым access token'ом
Таким образом если юзер залогинился на пяти устройствах, рефреш токены будут постоянно обновлятся и все счастливы. Но если с аккаунтом юзера начнут производить подозрительные действия(попытаются залогинится более чем на 10ти устройствах) система сбросит все сессии(рефреш токены) кроме последней.
Как дополнительная мера можно вообще заблокировать данного юзера при попытке залогинится более чем на 10ти устройствах. С возможностью разблокировки только через email. Но в этом случае нам необходимо будет во время каждого рефреша проверять список токенов на наличие мертвых(не валидных).
В момент рефреша то есть обновления access token'a обновляются ОБА токена. Но как же refresh token может сам себя обновить, он ведь создается только после успешной аунтефикации ? refresh token в момент рефреша сравнивает себя с тем refresh token'ом который лежит в БД и вслучае успеха, а также если у него не истек срок, система рефрешит токены. Внимание при обновлении refresh token'a продливается также и его срок жизни.
Возникает вопрос зачем refresh token'y срок жизни, если он обновляется каждый раз при обновлении access token'a ? Это сделано на случай если юзер будет в офлайне более 60 дней, тогда прийдется заново вбить логин/пароль.
- Хакер воспользовался access token'ом
- Закончилось время жизни access token'на
- Клиент хакера отправляет refresh token
- Хакер получает новую пару токенов
- На сервере создается новая пара токенов("от хакера")
- Юзер пробует зайти на сервер >> обнаруживается что токены невалидны
- Сервер перенаправляет юзера на форму аутентификации
- Юзер вводит логин/пароль
- Создается новая пара токенов >> пара токенов "от хакера" становится не валидна
Проблема: Поскольку refresh token продлевает срок своей жизни каждый раз при рефреше токенов >> хакер пользуется токенами до тех пор пока юзер не залогинится.
- хранить список валидных IP, deviceID, fingerprint браузера, генерить рандомный randomUserID
- дополнительно шифровать токены (в nodejs например crypt >> aes-256)
- зашивать в payload также IP/подсеть владельца токена. В этом случае при каждой попытке зайти с новой точки доступа к интерету придется перелогиниватся.
Front-end: https://github.com/zmts/beauty-vuejs-boilerplate/blob/master/src/services/http.init.js
Back-end: https://github.com/zmts/supra-api-nodejs/tree/master/actions/auth
- Заметка базируется на: https://habrahabr.ru/company/Voximplant/blog/323160/
- https://tools.ietf.org/html/rfc6749
- 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://www.youtube.com/watch?v=Ngh3KZcGNaU
- https://www.youtube.com/playlist?list=PLvTBThJr861y60LQrUGpJNPu3Nt2EeQsP
- https://egghead.io/courses/json-web-token-jwt-authentication-with-node-js
- https://www.digitalocean.com/community/tutorials/oauth-2-ru