Аутентификация с помощью JSON Web Token

5 min read

Во время разработки RESTful API для одного проекта возникла необходимость реализовать аутентификацию пользователя. Одним из принципов REST является независимость от состояния (stateless). Это значит, что клиент должен сам позаботиться о своей аутентификации при каждом запросе. Поискав в интернете статьи на эту тему, я обнаружил интересную технологию — JSON Web Token или просто JWT.

Привычные подходы

Самый простой подход для аутентификации в REST это отправка логина и пароля пользователя при каждом запросе. Понятно, что такой способ не безопасен, особенно если клиент использует незащищенный протокол.

Более привычное решение — сопоставление пользователя некому уникальному идентификатору — токену. При первом логине клиенту от сервера выдается токен, образованный хеш-функцией от каких-нибудь уникальных данных пользователя (id, логин, пароль). В базу заносится пара токен — id. При следующих запросах клиент передает этот токен, а сервер ищет в базе запись. Если запись найдена, пользователя авторизуют. Часто, для большей безопасности, токену дают определенное время жизни, после которого он становится недействителен.

JWT

JSON Web Token работает схоже с привычной реализацией. Но JWT имеет некоторые преимущества — он самодостаточен, все необходимые для аутентификации данные можно хранить в самом токене. Последовательно рассмотрим устройство токена.

Структура

JWT состоит из трех основных частей: заголовка (header), нагрузки (payload) и подписи (signature). Заголовок и нагрузка формируются отдельно, а затем на их основе вычисляется подпись.

Header

Обычно заголовок состоит из двух полей: типа токена (в данном случае JWT) и алгоритма хэширования подписи:

{ "typ": "JWT", "alg": "HS256" }

Официальный сайт jwt.io предлагает два алгоритма хэширования: HS256 и RS256. Но на деле можно использовать любой алгоритм с приватным ключом.

Payload

Payload — это любые данные, которые вы хотите передать в токене. Но стандарт предусматривает несколько зарезервированных полей:

Все эти поля не являются обязательными, но их использование не по назначению может привести к коллизиям.

Любые другие данные можно передавать по договоренности между сторонами, использующими токен. Например, payload может выглядеть так:

{ "iss": "Codex Team", "sub": "auth", "exp": 1505467756869, "iat": 1505467152069, "user": 1 }

Payload не шифруется при использовании токена, поэтому не стоит передавать в нем данные, которые не должны попасть в открытый доступ.

Signature

Подпись вычисляется на основе заголовка и нагрузки. Таким образом, если кто-то попытается изменить данные в токене, он не сможет изменить подпись, не зная приватного ключа. При аутентификации приватным ключом может выступать пароль пользователя (или хеш от пароля).

Сначала header и payload приводятся к формату JSON, а затем переводятся в base64:

Header: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 Payload: eyJpc3MiOiJDb2RleCBUZWFtIiwic3ViIjoiYXV0aCIsImV4cCI6MTUwNTQ2Nzc1Njg2OSwiaWF0IjoxNTA1NDY3MTUyMDY5LCJ1c2VyIjoxfQ

Затем, две эти строки соединяются через точку и хэшируются указанным в header алгоритмом. Допустим, пользователь использует пароль password:

HS256('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9' + '.' + 'eyJpc3MiOiJDb2RleCBUZWFtIiwic3ViIjoiYXV0aCIsImV4cCI6MTUwNTQ2Nzc1Njg2OSwiaWF0IjoxNTA1NDY3MTUyMDY5LCJ1c2VyIjoxfQ', 'password') = '0ynjTRZT9Uk77TnGy_g9Mxi1decLBjKxQK6e2dVzDJo'

Результат работы алгоритма и есть подпись. Теперь осталось только сформировать сам токен, для этого нужно через точку соединить header и payload в base64 и подпись:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJDb2RleCBUZWFtIiwic3ViIjoiYXV0aCIsImV4cCI6MTUwNTQ2Nzc1Njg2OSwiaWF0IjoxNTA1NDY3MTUyMDY5LCJ1c2VyIjoxfQ.0ynjTRZT9Uk77TnGy_g9Mxi1decLBjKxQK6e2dVzDJo

Токен готов. Проверить его можно на jwt.io.

Аутентификация

После первого логина, клиенту возвращается сгенерированный сервером JWT. При каждом следующем запросе, клиент должен передавать JWT установленным API способом (например, через заголовок или как параметр запроса). Сервер декодирует header и payload и проверяет зарезервированные поля. Если все в порядке, по указанному в header алгоритму составляется подпись. Если полученная подпись совпадает с переданной, пользователя авторизуют. Можно реализовать всю эту схему вручную, а можно использовать одну из библиотек указанных на jwt.io.