JWT авторизация. Часть 1. Понятие и устройство JWT токенов

JWT авторизация. Часть 1. Понятие и устройство JWT токенов

Json Web Tokens (JWT) представляют собой разновидность токенов, которые хранят данные в виде json и используются в основном для авторизации пользователя. Такие токены также возможно использовать для передачи информации между микросервисами.

Как правило, при такой схеме JWT авторизации после ввода логина и пароля выдаются два токена – access и refresh. Браузер сохраняет оба токена.  При обращении к защищенному эндпоинту браузер подставляет access токен в заголовок Authorization, что позволяет пользователю получать информацию, недоступную незалогиненным пользователям. Со временем access токен становится невалидным. Тогда браузер достает refresh токен из localStorage и обращается на специальный эндпоинт по обновлению и access, и refresh токена. Получая новую пару, браузер вновь использует уже валидный access токен при обращении к API. Если пользователь долгое время не входил на сайт, то refresh токен становится невалидным, поэтому обновление токенов невозможно и пользователю нужно заново ввести данные для входа.  

Базовая схема получения и использования JWT токенов
Схема рефреша токенов

Устройство токенов

Сам по себе токен представляет просто строку, разделенную на три части точками. Информация представлена в виде закодированного джейсона в base64URL. Сама по себе закодированная информация не шифруется, а поэтому прочитать ее может кто угодно. По этой причине в токене нельзя хранить секретную информацию. Вся польза токена сводится к проверке того, были ли данные изменены третьими лицами и можно ли доверять токену. Это достигается за счет подписи. Прочитать токен сможет любой, но подписать только та сторона, которая имеет секретный ключ. Поэтому любой желающий может взять токен и проверить подпись. Если она невалидна – данные были подделаны. Что же касается бэкенда, то токен декодируется с помощью секретного слова, известного лишь на бэкенде, и, если возникают несостыковки, то пользователь отбрасывается.

Пример токена:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Состоит токен из трех частей - header, payload и signature. То есть header.payload.signature или

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Структура токена

Header содержит информацию для проверки подписи – например, алгоритм хэширования. Обязательным атрибутом объекта должен быть alg - тип шифрования. В данном случае это HS256. Таким образом в виде JSON это выглядит так:

{
  "alg": "HS256"
}

Каждая часть токена кодируется с помощью алгоритма Base64Url без знаков =.
Закодируем заголовок с помощью инструментов python:

import base64
import json

header = {
  "alg": "HS256"
}
header_json = json.dumps(header)
encoded_header = base64.urlsafe_b64encode(bytes(header_json, 'utf-8')) 
# b'eyJhbGciOiAiSFMyNTYifQ=='

encoded_header_as_string = encoded_header.decode('utf-8').replace('=', '') 
# 'eyJhbGciOiAiSFMyNTYifQ'

Таким образом, первая часть токена это eyJhbGciOiAiSFMyNTYifQ

Payload

Payload это полезная нагрузка, которую мы хотим передать. Например, это может быть информация об айди пользователя или его роль. Так же обычно в payload включают время жизни токена. Как правило, бэкенд декодирует токен, вытаскивает из полезной нагрузки айди пользователя и делает все действия на бэкенде от лица пользователя с этим айди. Пример закодированной информации в json:

{
  "user_id": 15,
  "exp": 15162390222
}

С помощью питона получить эту часть токена можно так:

import base64
import json

header = {
  "user_id": 15,
  "exp": 15162390222
}
payload_json = json.dumps(payload)
encoded_payload = base64.urlsafe_b64encode(bytes(payload_json, 'utf-8')) 
# b'eyJ1c2VyX2lkIjogMTUsICJleHAiOiAxNTE2MjM5MDIyMn0='

encoded_payload_as_string = encoded_header.decode('utf-8').replace('=', '') 
# 'eyJ1c2VyX2lkIjogMTUsICJleHAiOiAxNTE2MjM5MDIyMn0'

Данные о пользователи с id 15 помещены в строку eyJ1c2VyX2lkIjogMTUsICJleHAiOiAxNTE2MjM5MDIyMn0, то есть это вторая часть токена. Таким образом мы имеем неподписанный токен: eyJhbGciOiAiSFMyNTYifQ.eyJ1c2VyX2lkIjogMTUsICJleHAiOiAxNTE2MjM5MDIyMn0

Signature

Цифровая подпись, с помощью которой проверяют сохранность данных токена. Нужна для декодирования и принятия решения о том, можно ли данному токену доверять. Получается как результат хэширования строки header + '.' + payload с помощью какого-то секретного ключа и указанного алгоритма хэширования(обычно SHA256).

import hmac
import hashlib
import base64

header = 'eyJhbGciOiAiSFMyNTYifQ'
payload = 'eyJ1c2VyX2lkIjogMTUsICJleHAiOiAxNTE2MjM5MDIyMn0'

unsigned_token = f'{header}.{payload}'

signature_hash = hmac.new(key=b'SECRET_KEY', msg=bytes(unsigned_token, 'utf-8'), digestmod=hashlib.sha256)
#'ad9f3b5fb4a17bed27b0ae392401aa790eab513e3ee230431008b8224b54749c'

signature = base64.urlsafe_b64encode(hash.digest()).decode('utf-8').replace('=', '')
#'rZ87X7She-0nsK45JAGqeQ6rUT4-4jBDEAi4IktUdJw'

token = f'{header}.{payload}.{signature}'
#'eyJhbGciOiAiSFMyNTYifQ.eyJ1c2VyX2lkIjogMTUsICJleHAiOiAxNTE2MjM5MDIyMn0.rZ87X7She-0nsK45JAGqeQ6rUT4-4jBDEAi4IktUdJw'

В итоге мы получили токен eyJhbGciOiAiSFMyNTYifQ.eyJ1c2VyX2lkIjogMTUsICJleHAiOiAxNTE2MjM5MDIyMn0.rZ87X7She-0nsK45JAGqeQ6rUT4-4jBDEAi4IktUdJw. Расшифровать токен и проверить  его подпись можно на сайте https://jwt.io/

В правом нижнем углу отредактируем секретный ключ на SECRET_KEY  и вставим токен. Как видим, подпись верная, а наш payload  совпадает с тем, что мы закодировали.

PyJWT

Все вышеописанное уже реализовано в библиотеке PyJWT

>>> import jwt
>>> encoded = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256')
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg'

>>> jwt.decode(encoded, 'secret', algorithms=['HS256'])
{'some': 'payload'}

Итоги

  • JWT токены могут используются для системы авторизации пользователя. После входа пользователь получает два токена – access  и refresh. Каждый токен имеет время жизни и некоторые закодированные данные. Пользователь при каждом обращении к API использует access токен, а когда он истекает, то пользователь обновляет всю пару с помощью refresh токена.
  • В JWT токене нельзя хранить секретные данные, так-как они могут быть просмотрены кем угодно. Основное преимещуство JWT это цифровая подпись.
  • JWT представлен как строка header.paylaod.signature. Header содержит информацию об алгоритме хэширования, payload – полезную нагрузку, а signature подпись, по которой можно проверить валидность данных.
  • В случае кражи  access токена злоумышленник получает доступ к данным на короткий промежуток времени. После истечения срока жизни access токена злоумышленник не может пользоваться API из-за нехватки refresh токена.