markdown Протокены,JSON Web Tokens(JWT),аутентификациюиавторизацию。基于令牌的身份验证

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了markdown Протокены,JSON Web Tokens(JWT),аутентификациюиавторизацию。基于令牌的身份验证相关的知识,希望对你有一定的参考价值。

# Про токены, JSON Web Tokens (JWT), аутентификацию и авторизацию. Token-Based Authentication

## Основы:

__Аутентификация(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
```
3. Клиент сохраняет токены и время смерти __access token'а__, используя __access token__ для последующей авторизации запросов
4. Перед каждым запросом клиент предварительно проверяет время жизни __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      | { refreshTokenTimestamp1: 'refreshTokenBody1', refreshTokenTimestamp2: 'refreshTokenBody2'}
-------------------------------------------------------------------------------------------------
```
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. На сервере создается новая пара токенов(__"от хакера"__)
5. Юзер пробует зайти на сервер >> обнаруживается что токены невалидны
6. Сервер перенаправляет юзера на форму аутентификации
7. Юзер вводит логин/пароль
8. Создается новая пара токенов >> пара токенов __"от хакера"__ становится не валидна

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

### В случае паранои:
- хранить список валидных IP/Subnet, 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://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://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


以上是关于markdown Протокены,JSON Web Tokens(JWT),аутентификациюиавторизацию。基于令牌的身份验证的主要内容,如果未能解决你的问题,请参考以下文章

markdown Протокены,JSON Web Tokens(JWT),аутентификациюиавторизацию

markdown Приемыпроектированияjavascript

markdown Протокены,JSON Web Tokens(JWT),аутентификациюиавторизацию。基于令牌的身份验证

markdown Определитькакиеещеможноустановитьпакетыphp

markdown 更多信息(занятых)портовнасервереилокалке(навсякий):

Erlangе демо