Skip to content

Instantly share code, notes, and snippets.

@akleandrov
Forked from zmts/tokens.md
Created May 7, 2018 19:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save akleandrov/f289d44e808bc13b6c949f9ed45a6b70 to your computer and use it in GitHub Desktop.
Save akleandrov/f289d44e808bc13b6c949f9ed45a6b70 to your computer and use it in GitHub Desktop.
Про токены, JSON Web Tokens (JWT), аутентификацию и авторизацию

Token-Based Authentication(JWT)

Preconditions:

В данной заметке рассматривается работа JWT с симметичным алгоритмом шифрования (HS256/HS384/HS512)

Основы:

Аутентификация(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)

refresh token - выдается сервером по результам успешной аутентификации и используется для получения нового access token'a и обновления refresh token'a

Каждый токен имеет свой срок жизни, например access: 30мин, refresh: 60дней

Поскольку токены это не зашифрованная информация крайне не рекомендуется хранить в них такую информацию как пароли

Схема создания/использования токенов (api/auth/login):

  1. Пользователь логинится в приложении, передавая логин/пароль на сервер
  2. Сервер проверят подлинность логина/пароля, в случае удачи генерирует и отправляет клиенту два токена(access, refresh) и время смерти access token'а (expires_in поле, в unix timestamp). Также в payload refresh token'a добавляется user_id
"accessToken": "...",
"refreshToken": "...",
"expires_in": 1502305985425
  1. Клиент сохраняет токены и время смерти access token'а, используя access token для последующей авторизации запросов
  2. Перед каждым запросом клиент предварительно проверяет время жизни access token'а (из expires_in)и если оно истекло использует refresh token чтобы обновить ОБА токена и продолжает использовать новый access token

Схема рефреша токенов (api/auth/refresh-tokens):

  1. Клиент проверяет перед запросом не истекло ли время жизни access token'на
  2. Если истекло клиент отправляет на auth/refresh-token URL refresh token
  3. Сервер берет user_id из payload'a refresh token'a по нему ищет в БД запись данного юзера и достает из него refresh token
  4. Сравнивает refresh token клиента с refresh token'ом найденным в БД
  5. Проверяет валидность и срок действия refresh token'а
  6. В случае успеха сервер:
    1. Создает и записывает refresh token в БД
    2. Создает новый access token
    3. Отправляет оба токена и новый expires_in access token'а клиенту
  7. Клиент повторяет запрос к API c новым access token'ом

С такой схемой юзер сможет быть залогинен только на одном устройстве. Тоесть в любом случае при смене устройства ему придется логинится заново.

Если рассматривать возможность аутентификации на более чем одном девайсе/браузере: необходимо хранить весь список валидных рефреш токенов юзера. Если юзер авторизовался более чем на ±10ти устройствах(что есть весьма подозрительно), автоматически инвалидоровать все рефреш токены кроме текущего и отправлять email с security уведомлением. Как вариант список токенов можно хранить в jsonb(если используется PostgreSQL).

Схема рефреша токенов (мульти сессии, api/auth/refresh-tokens):

Для использования возможности аутентификации на более чем одном девайсе необходимо хранить все рефреш токены по каждому юзеру. Я этот список храню в записи юзера в виде JSONB.

-------------------------------------------------------------------------------------------------
| id | username | refreshTokensMap
-------------------------------------------------------------------------------------------------
| 1 | alex      | { refreshTokenId1: 'refreshTokenBody1', refreshTokenId2: 'refreshTokenBody2'}
-------------------------------------------------------------------------------------------------

При создании рефрештокена я дополнительно его шифрую. П - паранойя. https://github.com/zmts/supra-api-nodejs/blob/master/services/auth/cryptoEncryptService.js И использую время создания (timestamp) в качестве refreshTokenId. Но можно прекрасно обойтись и без дополнительного шифрования и без JSONB, просто создав для рефреш токенов отдельную таблицу one-to-many. Возможно стоит так сделать и мне.

Схема(без учета доп. шифрования):

  1. Клиент проверяет перед запросом не истекло ли время жизни access token'на
  2. Если истекло клиент отправляет на auth/refresh-token URL refresh token
  3. Сервер берет user_id из payload'a refresh token'a по нему ищет в БД запись данного юзера и достает из него refresh token
  4. Сравнивает refresh token клиента с refresh token'ом найденным в БД
  5. Проверяет валидность и срок действия refresh token'а (но если токен не валиден удаляет его сразу)
  6. В случае успеха сервер:
    1. Удаляет старый рефреш токен
    2. Проверяет количество уже существующих решфреш токенов.
    3. Если их больше 10, удаляет все токены, создает новый и запиывает его в БД.
    4. Если их меньше 10 просто создает и записывает новый в БД.
    5. Создает новый access token
    6. Отправляет оба токена и новый expires_in access token'а клиенту
  7. Клиент повторяет запрос к 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 дней, тогда прийдется заново вбить логин/пароль.

В случае кражи(обоих токенов):

  1. Хакер воспользовался access token'ом
  2. Закончилось время жизни access token'на
  3. Клиент хакера отправляет refresh token
  4. Хакер получает новую пару токенов
  5. На сервере создается новая пара токенов("от хакера")
  6. Юзер пробует зайти на сервер >> обнаруживается что токены невалидны
  7. Сервер перенаправляет юзера на форму аутентификации
  8. Юзер вводит логин/пароль
  9. Создается новая пара токенов >> пара токенов "от хакера" становится не валидна

Проблема: Поскольку refresh token продлевает срок своей жизни каждый раз при рефреше токенов >> хакер пользуется токенами до тех пор пока юзер не залогинится.

В случае паранои:

  • хранить список валидных IP, deviceID, fingerprint браузера, генерить рандомный randomUserID
  • дополнительно шифровать токены (в nodejs например crypt >> aes-256)

Пример имплементации:

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

Чтиво:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment