Отзыв доступа к токенам JWT с помощью черного или запрещенного списка

Отзыв доступа к токенам JWT с помощью черного или запрещенного списка

5 мая 2022 г.

В зависимости от того, кого вы слушаете, JWT либо являются панацеей от всех ваших проблем с аутентификацией, либо их следует избегать как чумы. Мы в SuperTokens сомневаемся.


Что такое токен JWT?


JWT или JSON Web Token — это строка/токен, выдаваемый сервером, который утверждает свойства, содержащиеся в его «полезной нагрузке». Чаще всего его используют для аутентификации (OAuth 2.0 + Open ID Connect) и управления сеансами.


Как следует из названия, JWT может содержать внутри себя любую информацию в формате JSON. Это также известно как «претензии JWT». Например, для управления сеансом JSON должен, по крайней мере, содержать вошедшего в систему пользователя userId:


```json


"Логин пользователя": "...",


"срок действия": 1646472008501,


Если срок действия JWT, содержащего эту информацию, должен истечь, JSON также может содержать время истечения срока действия токена (как показано выше). Существует также условие для имен ключей к этим полям. В нашем примере выше это:


  • userId -> sub

  • срок действия -> exp

В качестве примера процесса входа в систему клиенту будет отправлен JSON, содержащий указанную выше информацию, а затем при каждом запросе клиент может отправить этот JSON обратно на сервер. Таким образом, сервер знает, какой идентификатор пользователя запрашивает его API. Если срок действия JSON истек, то сервер может отклонить запрос, и пользователь должен снова войти в систему.


Однако с точки зрения безопасности клиенту очень легко изменить значение userId в JSON и подделать другого пользователя. Чтобы предотвратить это, сервер отправляет исходную «подпись» JSON вместе с JSON. Эта «подпись» создается с использованием секрета, который известен только серверу, поэтому клиент не может сам создать подпись JSON. Поэтому, если клиент изменит JSON, сервер не сможет сопоставить исходную подпись JSON с входящей/измененной подписью JSON (это называется проверкой подписи) и затем может отклонить запрос.


Существует несколько алгоритмов, которые сервер может использовать для создания подписи для JSON:


  • HMAC + SHA256

  • RSASSA-PKCS1-v1_5 + SHA256

  • ECDSA+P-256+SHA256

Метод подписи, выбранный для создания подписи, должен быть каким-то образом закодирован в JWT, чтобы тот же метод использовался при проверке подписи.


В целом, JWT состоит из трех частей:


  • Строка заголовка содержит информацию об используемом алгоритме подписи.

  • Строка тела содержит фактический JSON.

  • Строка подписи может использоваться для проверки того, что JWT не был изменен клиентом.

Эти три раздела объединяются разделителем ., чтобы сформировать полный токен JWT. Пример JWT можно позже увидеть в этом сообщении в блоге.


Преимущества токенов JWT


Подход JWT, безусловно, имеет свои преимущества перед непрозрачными токенами. JWT:


  • Автономный: JWT может содержать сведения о пользователе (не только идентификатор сеанса, такой как файл cookie, но и другие пользовательские данные, такие как имя пользователя и даже разрешения), а также время истечения срока действия токена, чтобы вы не необходимо запросить базу данных для получения этой информации. Это совершенно не похоже на непрозрачный токен, который по своей природе представляет собой просто бессмысленную мешанину буквенно-цифровых символов.

  • Безопасность: JWT имеют цифровую подпись с использованием либо секретного (HMAC), либо пары открытого/закрытого ключа (RSA или ECDSA), что защищает их от изменения клиентом или злоумышленником.

  • Хранится только на клиенте: вы создаете JWT на сервере и отправляете их клиенту. Затем клиент отправляет JWT с каждым запросом. Это экономит место в базе данных.

  • Эффективно: проверка JWT выполняется быстро, поскольку не требует поиска в базе данных.

Недостатки токенов JWT


Тот факт, что JWT хранятся только на стороне клиента, приводит к фундаментальному недостатку JWT. А именно: как отозвать доступ пользователя?


Конечно, у JWT есть срок действия, и он может быть таким коротким, как вам нравится. Однако, как только срок действия маркера доступа истекает, JWT становится недействительным, и клиент должен повторно аутентифицироваться на вашем сервере. Это, конечно, негативно сказывается на пользовательском опыте.


Но предположим, что пользователь намеренно выходит из вашей системы? Или вы хотите выгнать их, потому что опасаетесь, что безопасность была скомпрометирована? Вы не можете[1]: если у них все еще есть токен с неистекшим сроком действия, у них все еще есть доступ.


Есть решение этой проблемы, но оно требует дополнительной работы с вашей стороны. Это метод, который требует ведения черного списка/запрещенного списка JWT. В этой статье мы покажем вам, что такое черный список/запретный список JWT, как его реализовать и обсудим, является ли он хорошим решением этой проблемы или нет.


Что такое черный список/черный список JWT?


Черный список/список запрещенных JWT — это список токенов, которые больше не должны предоставлять доступ к вашей системе.


Где вы храните этот список, зависит от вас. Вы можете использовать традиционную базу данных, но гораздо лучше использовать кеш данных в памяти, такой как Redis. Кэш данных в памяти обеспечивает гораздо более быстрое и предсказуемое время поиска, чем данные, хранящиеся на диске.


Это то, что мы будем использовать в этом примере, и мы напишем наше решение, используя Node.js и Express. Если это не выбранный вами стек технологий, не бойтесь: фундаментальный подход один и тот же, независимо от того, как вы решите его построить.


Итак, как мне это сделать?


Во-первых, вам нужно создать экземпляр приложения сервера Express и настроить Redis, чтобы вы могли поддерживать список активных JWT:


```javascript


импортировать экспресс из «экспресс»;


импортировать bodyparser из "body-parser";


импортировать jwt из «jsonwebtoken»;


импортировать Redis из «redis»;


const JWT_SECRET = "Сверхбезопасный секрет";


константное приложение = экспресс();


app.use(bodyparser.urlencoded({extended: false}));


app.use(bodyparser.json());


пусть redisClient = ноль;


(асинхронный () => {


redisClient = redis.createClient();


redisClient.on("ошибка", (ошибка) => {


console.log(ошибка);


redisClient.on("connect", () => {


console.log("Redis подключен!");


ожидайте redisClient.connect();


// прослушивание запросов


const listener = app.listen(3000, () => {


console.log("Сервер работает");


Чтобы протестировать хранилище и последующий отзыв JWT, вам понадобится способ создать пользователя и сгенерировать JWT для этого пользователя. Давайте добавим для этого конечную точку createUser. Вы можете сделать POST-запрос к этой конечной точке с именем нового пользователя:


```js


импортировать jwt из «jsonwebtoken»;


app.post("/createUser", (запрос, ответ) => {


константный токен = generateAccessToken({имя пользователя: request.body.username});


ответ.json (токен);


const generateAccessToken = (имя пользователя) => {


return jwt.sign(имя пользователя, JWT_SECRET, {expiresIn: "3600s"});


Нажмите на эту конечную точку, и вы увидите, что для пользователя выдается JWT:


``` ударить


curl --location --request POST 'http://localhost:3000/createUser' \


--header 'Тип содержимого: приложение/json' \


--данные-сырые '{


"имя пользователя": "Дерек"


"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkRlcmVrIiwiaWF0IjoxNjQxMzA3MTgxLCJleHAiOjE2NDEzMTA3ODF9.3yrIJpWMS952QVakifjviiTs0ANJOmxZDovQO0N5"


Если вы вставите этот токен в [онлайн-отладчик] (https://jwt.io), вы увидите, что в JWT закодировано имя пользователя, а также другие сведения, такие как время его выпуска (iat) и время его истечения (exp):


```json


"имя пользователя": "Дерек",


"иат": 1641307181,


"эксп": 1641310781


Следующее, что вам нужно сделать, это аутентифицировать пользователя с помощью этого JWT всякий раз, когда этот пользователь пытается что-либо сделать с вашим приложением. Это трехэтапный процесс. Во-первых, вы должны убедиться, что JWT предоставлен. Затем вы захотите проверить, находится ли предоставленный JWT в черном списке/списке запрещенных, хранящемся в Redis. Если это не так, то вы, наконец, захотите убедиться, что JWT действителен.


Мы можем закодировать эти шаги как промежуточное ПО Express:


```js


// промежуточное ПО JWT


const authenticationToken = async (запрос, ответ, следующий) => {


const authHeader = request.headers["авторизация"];


константный токен = authHeader && authHeader.split(" ")[1];


// токен предоставлен?


если (токен == ноль) {


вернуть ответ.статус(401).отправить({


сообщение: "Токен не предоставлен",


// токен в запрещенном списке?


const inDenyList = await redisClient.get(bl_${токен});


если (в списке запрещенных) {


вернуть ответ.статус(401).отправить({


сообщение: "JWT отклонено",


// токен действителен?


jwt.verify (токен, JWT_SECRET, (ошибка, пользователь) => {


если (ошибка) {


вернуть ответ.статус(401).отправить({


статус: "ошибка",


сообщение: error.message,


request.userId = имя_пользователя;


request.tokenExp = user.exp;


запрос.токен = токен;


следующий();


Теперь произойдет то, что каждый раз, когда пользователь, которого вы создали ранее, попытается посетить ваш домашний маршрут (/), предоставленный им JWT будет сначала проверен по черному списку/списку запрещенных, а затем проверен перед предоставлением доступа.


Это все хорошо, но на данный момент у вас нет возможности отозвать JWT, поэтому в черном списке/запрещенном списке ничего нет.


Давайте создадим еще один маршрут, который будет имитировать выход пользователя из системы. При попадании в эту конечную точку JWT пользователя сохраняется в Redis с использованием формата bl_<токен> для ключа, а значением является фактический токен. Мы установим срок действия ключа, когда истечет срок действия самого токена, чтобы не заполнять Redis большим количеством токенов с истекшим сроком действия.


```js


app.post("/logout", authenticationToken, async (запрос, ответ) => {


const {userId, token, tokenExp} = запрос;


const token_key = bl_${токен};


ожидание redisClient.set(token_key, token);


redisClient.expireAt(token_key, tokenExp);


return response.status(200).send("Токен недействителен");


Выполните запрос POST к конечной точке /logout:


``` ударить


curl --location --request ПОЛУЧИТЬ 'http://localhost:3000/' \


--header 'Авторизация: носитель eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkRlcmVrIiwiaWF0IjoxNjQxMzA4NTQwLCJleHAiOjE2NDEzMTIxNDB9.nlJJe7hK5jMDJVgvqjevyHa


Теперь JWT пользователя технически все еще действителен (до истечения срока его действия), но теперь он находится в черном списке/списке запрещенных и поэтому будет перехватываться при последующих запросах. Повторите запрос POST к указанному выше домашнему маршруту, и вы получите ошибку HTTP 403 Forbidden с сообщением "JWT Rejected".


Однако это не поможет, если вы хотите запретить доступ пользователю, который не вышел из системы и чей токен все еще действителен. Проблема в том, что вы не будете знать, какой у пользователя токен доступа в этот момент, а значит, не будете знать, что класть в кеш.


Чтобы решить эту проблему, вы должны добавить возможность внесения в черный список пользователя userID через Redis, а также проверять этот черный список во время проверки JWT. В отличие от черного списка JWT, эта запись не будет иметь срока действия, связанного с ней.


Вы можете найти рабочую версию этого примера на Github.


Это хорошая идея?


Итак, теперь вы видите, что эту концепцию черного списка/запрещенного списка JWT относительно легко реализовать. Но лучший ли это выход из сложившейся ситуации?


На наш взгляд, нет.


Самым большим преимуществом JWT является то, что они ускоряют проверку сеанса. Если вы ведете черный список/список запрещенных и должны запрашивать его при каждом вызове API, вы теряете это преимущество.


Вместо этого мы выступаем за решение для управления сеансами, которое сочетает в себе соответствующие сильные стороны как JWT, так и непрозрачных токенов.


В этом рабочем процессе, когда пользователь входит в систему, сервер выдает как недолговечный JWT (токен доступа), так и долгоживущий непрозрачный токен (токен обновления). Когда пользователю предоставляется доступ, оба этих маркера отправляются клиенту.


Когда пользователь пытается получить доступ к ресурсу, он отправляет токен доступа JWT вместе с каждым запросом. По истечении срока действия JWT клиент использует непрозрачный токен обновления, чтобы запросить новый JWT и новый непрозрачный токен обновления. Этот процесс известен как ротация токена обновления.


Затем клиент использует этот новый JWT для выполнения последующих запросов, и процесс продолжается.


Преимущества и недостатки выпуска двух токенов и ротации токенов обновления


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


Обратите внимание, что это не решает проблему все еще действительных JWT, существующих на клиенте. Но, поскольку вы можете сделать эти JWT гораздо более недолговечными (даже всего на несколько минут), вы обнаружите, что это не слишком большая проблема.


Таким образом, вы сохраняете одно из ключевых преимуществ использования JWT, а именно тот факт, что вам не нужно постоянно обращаться к базе данных при каждом вызове API для проверки JWT — вам нужно только выполнять поиск в базе данных при обновлении сеанса. - что случается относительно редко.


Если это звучит идеально для вашего варианта использования, ознакомьтесь с реализацией этого потока сеанса в SuperTokens. SuperTokens использует этот метод, чтобы использовать все преимущества использования JWT, одновременно смягчая многие из их недостатков, комбинируя их с непрозрачными токенами.


Кроме того, изменение токена обновления при каждом использовании добавляет дополнительные преимущества безопасности, такие как возможность [обнаруживать перехват сеанса] (https://supertokens.com/blog/the-best-way-to-securely-manage-user-sessions).


Написано пользователями SuperTokens — надеюсь, вам понравилось! Мы всегда доступны на нашем сервере Discord. Присоединяйтесь к нам, если у вас есть вопросы или вам нужна помощь.



Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE