Аннулирование веб-токенов JSON
Для нового проекта node.js, над которым я работаю, я думаю о переключении с подхода, основанного на использовании файлов cookie (я имею в виду сохранение идентификатора в хранилище значений ключей, содержащем сеансы пользователя в браузере пользователя). к подходу сеанса на основе токенов (без хранения значения ключа) с использованием веб-токенов JSON (jwt).
Проект представляет собой игру, в которой используется socket.io - полезно иметь сеанс на основе токенов в таком сценарии, когда в одном сеансе будет несколько каналов связи (web и socket.io).
Как обеспечить токен / недействительность сеанса с сервера, используя подход jwt?
Я также хотел понять, на какие распространенные (или необычные) подводные камни / атаки я должен обращать внимание с такой парадигмой. Например, если эта парадигма уязвима к тем же / различным видам атак, что и подход на основе хранилища сеансов / файлов cookie.
Итак, скажем, у меня есть следующее (адаптировано из этого и этого):
Вход в магазин сессий:
app.get('/login', function(request, response) {
var user = {username: request.body.username, password: request.body.password };
// Validate somehow
validate(user, function(isValid, profile) {
// Create session token
var token= createSessionToken();
// Add to a key-value database
KeyValueStore.add({token: {userid: profile.id, expiresInMinutes: 60}});
// The client should save this session token in a cookie
response.json({sessionToken: token});
});
}
Логин на основе токена:
var jwt = require('jsonwebtoken');
app.get('/login', function(request, response) {
var user = {username: request.body.username, password: request.body.password };
// Validate somehow
validate(user, function(isValid, profile) {
var token = jwt.sign(profile, 'My Super Secret', {expiresInMinutes: 60});
response.json({token: token});
});
}
-
Выход (или аннулирование) для подхода хранилища сеансов потребует обновления базы данных KeyValueStore с указанным токеном.
Похоже, такого механизма не было бы в подходе на основе токенов, поскольку сам токен содержал бы информацию, которая обычно существовала бы в хранилище значений ключей.
35 ответов
Я тоже исследовал этот вопрос, и хотя ни одна из представленных ниже идей не является полным решением, они могут помочь другим исключить идеи или предложить дальнейшие.
1) Просто удалите токен из клиента
Очевидно, что это ничего не делает для безопасности на стороне сервера, но останавливает злоумышленника, удаляя токен из существования (т. Е. Им пришлось бы украсть токен до выхода из системы).
2) Создать черный список токенов
Вы можете хранить недействительные токены до истечения срока их действия и сравнивать их с входящими запросами. По-видимому, это сводит на нет причину, по которой, прежде всего, нужно использовать полностью токен, поскольку вам нужно будет обращаться к базе данных для каждого запроса. Размер хранилища, вероятно, будет меньше, так как вам нужно будет хранить только токены, которые были между выходом из системы и временем истечения срока действия (это внутреннее чувство, и оно определенно зависит от контекста).
3) Просто держите время истечения токена коротким и часто меняйте его
Если вы сохраняете время истечения токена с достаточно короткими интервалами, а работающий клиент отслеживает и запрашивает обновления в случае необходимости, номер 1 будет эффективно работать в качестве полной системы выхода из системы. Проблема этого метода в том, что он не позволяет пользователю войти в систему между закрытиями клиентского кода (в зависимости от того, как долго вы делаете интервал истечения).
Планы действий в чрезвычайных ситуациях
Если когда-либо возникла чрезвычайная ситуация или токен пользователя был скомпрометирован, вы могли бы разрешить пользователю изменить базовый идентификатор поиска пользователя с помощью своих учетных данных для входа. Это сделает все связанные токены недействительными, поскольку связанный пользователь больше не сможет быть найден.
Я также хотел отметить, что это хорошая идея, чтобы включить в маркер дату последнего входа в систему, чтобы вы могли провести повторный вход через некоторое время.
С точки зрения сходства / различий в отношении атак с использованием токенов, этот пост посвящен вопросу: http://blog.auth0.com/2014/01/07/angularjs-authentication-with-cookies-vs-token/
Приведенные выше идеи хороши, но очень простой и легкий способ аннулировать все существующие JWT - просто изменить секрет.
Если ваш сервер создает JWT, подписывает его секретом (JWS), а затем отправляет его клиенту, простое изменение секрета приведет к аннулированию всех существующих токенов и потребует от всех пользователей получения нового токена для аутентификации, поскольку их старый токен внезапно становится недействительным согласно на сервер.
Он не требует каких-либо модификаций для реального содержимого токена (или идентификатора поиска).
Понятно, что это работает только в экстренном случае, когда вы хотите, чтобы истек срок действия всех существующих токенов, для истечения срока действия токена требуется одно из указанных выше решений (например, короткое время истечения токена или аннулирование сохраненного ключа внутри токена).
Это в первую очередь длинный комментарий, поддерживающий и основывающийся на ответе @mattway
Дано:
Некоторые из других предлагаемых решений на этой странице рекомендуют использовать хранилище данных при каждом запросе. Если вы нажмете на главное хранилище данных для проверки каждого запроса на аутентификацию, то я вижу меньше причин использовать JWT вместо других установленных механизмов аутентификации токена. По сути, вы сделали JWT с состоянием, а не с состоянием, если каждый раз заходите в хранилище данных.
(Если ваш сайт получает большое количество неавторизованных запросов, то JWT будет отклонять их, не обращаясь к хранилищу данных, что полезно. Возможно, есть и другие варианты использования, подобные этому.)
Дано:
Истинная аутентификация JWT без сохранения состояния не может быть достигнута для типичного реального веб-приложения, потому что JWT без сохранения состояния не может обеспечить немедленную и безопасную поддержку для следующих важных случаев использования:
Учетная запись пользователя удалена / заблокирована / приостановлена.
Пароль пользователя изменен.
Роли или разрешения пользователя изменены.
Пользователь вышел из системы администратором.
Любые другие критически важные данные приложения в токене JWT изменяются администратором сайта.
В этих случаях вы не можете дождаться истечения срока действия токена. Аннулирование токена должно произойти немедленно. Кроме того, вы не можете доверять клиенту, что он не хранит и не использует копию старого токена, со злым умыслом или нет.
Поэтому: я думаю, что ответ @matt-way, #2 TokenBlackList, был бы наиболее эффективным способом добавить требуемое состояние к аутентификации на основе JWT.
У вас есть черный список, в котором хранятся эти токены до истечения срока их действия. Список токенов будет довольно небольшим по сравнению с общим числом пользователей, так как он должен хранить токены в черном списке только до истечения срока их действия. Я бы реализовал это, поместив недействительные токены в redis, memcached или другое хранилище данных в памяти, которое поддерживает установку времени истечения для ключа.
Вы по-прежнему должны вызывать свою базу данных в памяти для каждого запроса аутентификации, который проходит начальную аутентификацию JWT, но вам не нужно хранить ключи для всего вашего набора пользователей. (Что может или не может иметь большого значения для данного сайта.)
Еще не пробовал, и он использует много информации, основанной на некоторых других ответах. Сложность здесь заключается в том, чтобы избежать обращения к хранилищу данных на стороне сервера при каждом запросе информации о пользователе. Большинство других решений требуют поиска в БД на запрос к хранилищу пользовательских сеансов. Это хорошо в определенных сценариях, но это было сделано в попытке избежать таких вызовов и сделать все необходимое состояние на стороне сервера очень маленьким. В конечном итоге вы воссоздадите сеанс на стороне сервера, хотя и небольшой, чтобы обеспечить все функции принудительного аннулирования. Но если вы хотите сделать это вот суть:
Цели:
- Смягчение использования хранилища данных (без учета состояния).
- Возможность принудительного выхода из системы всех пользователей.
- Возможность принудительного выхода любого человека в любое время.
- Возможность требовать повторного ввода пароля через определенное время.
- Возможность работы с несколькими клиентами.
- Возможность принудительного повторного входа в систему, когда пользователь нажимает кнопку выхода из определенного клиента. (Чтобы предотвратить "удаление" клиентского токена после ухода пользователя - см. Комментарии для дополнительной информации)
Решение:
- Используйте токены с кратковременным (<5м) доступом в сочетании с сохраненным токеном обновления с более долгим (несколько часов) клиентом.
- Каждый запрос проверяет срок действия токена авторизации или обновления.
- Когда срок действия маркера доступа истекает, клиент использует токен обновления для обновления токена доступа.
- Во время проверки маркера обновления сервер проверяет небольшой черный список идентификаторов пользователей - если он найден, отклонить запрос на обновление.
- Если у клиента нет действительного (не просроченного) обновления или токена авторизации, пользователь должен снова войти в систему, поскольку все остальные запросы будут отклонены.
- При входе в систему проверьте хранилище пользовательских данных на бан.
- При выходе из системы - добавьте этого пользователя в черный список сеансов, чтобы он мог снова войти в систему. Вам потребуется хранить дополнительную информацию, чтобы не выходить из системы со всех устройств в среде с несколькими устройствами, но это можно сделать, добавив поле устройства в черный список пользователей.
- Для принудительного повторного входа через x времени - сохраните дату последнего входа в систему в токене авторизации и проверяйте ее по запросу.
- Для принудительного выхода из системы всех пользователей - сбросить ключ хеша токена.
Это требует от вас поддерживать черный список (состояние) на сервере, предполагая, что таблица пользователя содержит запрещенную информацию о пользователе. Недопустимый черный список сеансов - это список идентификаторов пользователей. Этот черный список проверяется только во время запроса токена обновления. Записи должны жить на нем до тех пор, пока токен обновления TTL. По истечении срока действия маркера обновления пользователь должен будет снова войти в систему.
Минусы:
- По-прежнему требуется выполнить поиск в хранилище данных по запросу обновления токена.
- Недействительные токены могут продолжать работать для TTL токена доступа.
Плюсы:
- Обеспечивает желаемую функциональность.
- Действие обновления токена скрыто от пользователя при нормальной работе.
- Требуется только выполнить поиск в хранилище данных по запросам на обновление вместо каждого запроса. т.е. 1 раз в 15 минут вместо 1 в секунду.
- Минимизирует состояние на стороне сервера до очень маленького черного списка.
При таком решении хранилище данных в памяти, такое как reddis, не требуется, по крайней мере, не для информации о пользователях, как вы, поскольку сервер выполняет вызов БД только каждые 15 минут или около того. При использовании reddis сохранение действительного / недействительного списка сеансов будет очень быстрым и простым решением. Нет необходимости в обновлении токена. Каждый токен аутентификации будет иметь идентификатор сеанса и идентификатор устройства, они могут быть сохранены в таблице reddis при создании и аннулированы в случае необходимости. Затем они будут проверяться при каждом запросе и отклоняться при недействительности.
Я бы вел учет номера версии jwt в пользовательской модели. Новые токены jwt установят свою версию на это.
Когда вы проверяете jwt, просто убедитесь, что он имеет номер версии, равный текущей версии jwt пользователей.
В любое время, когда вы хотите сделать недействительными старые jwts, просто увеличьте номер версии jwt пользователей.
Подход, который я рассматривал, состоит в том, чтобы всегда иметь iat
(выпущено в) значение в JWT. Затем, когда пользователь выходит из системы, сохраните эту метку времени в записи пользователя. При проверке JWT просто сравните iat
до последнего времени выхода из системы. Если iat
старше, то не действует Да, вы должны пойти в БД, но я всегда буду извлекать пользовательскую запись, если JWT в противном случае является действительным.
Основной недостаток, который я вижу в этом, заключается в том, что он будет выходить из всех сессий, если они находятся в нескольких браузерах или имеют мобильный клиент.
Это также может быть хорошим механизмом для аннулирования всех JWT в системе. Часть проверки может соответствовать глобальной отметке времени последнего действительного iat
время.
------------------------ Немного поздно для этого ответа, но может быть это кому-то поможет ------------- -----------
На стороне клиента проще всего удалить токен из хранилища браузера.
Но что, если вы хотите уничтожить токен на сервере Node -
Проблема с пакетом JWT в том, что он не предоставляет никакого метода или способа уничтожить токен. Вы можете использовать различные методы в отношении JWT, упомянутые выше. Но здесь я иду с jwt-redis.
Итак, чтобы уничтожить токен на стороне сервера, вы можете использовать пакет jwt-redis вместо JWT.
Эта библиотека (jwt-redis) полностью повторяет всю функциональность библиотеки jsonwebtoken с одним важным дополнением. Jwt-redis позволяет сохранить метку токена в redis для проверки действительности. Отсутствие метки токена в Redis делает токен недействительным. Чтобы уничтожить токен в jwt-redis, есть метод destroy
это работает так:
1) Установите jwt-redis из npm
2) Создать -
var redis = require('redis');
var JWTR = require('jwt-redis').default;
var redisClient = redis.createClient();
var jwtr = new JWTR(redisClient);
jwtr.sign(payload, secret)
.then((token)=>{
// your code
})
.catch((error)=>{
// error handling
});
3) Для проверки -
jwtr.verify(token, secret);
4) Уничтожить -
jwtr.destroy(token)
Примечание: вы можете указать expiresIn во время входа в токен так же, как это предусмотрено в JWT.
Может это кому-то поможет
Я немного опоздал, но думаю, у меня есть достойное решение.
В моей базе данных есть столбец "last_password_change", в котором хранятся дата и время последнего изменения пароля. Я также храню дату / время выпуска в JWT. При проверке токена я проверяю, был ли пароль изменен после того, как токен был выпущен, и был ли он токен отклонен, даже если срок его действия еще не истек.
Храните список в памяти, как это
user_id revoke_tokens_issued_before
-------------------------------------
123 2018-07-02T15:55:33
567 2018-07-01T12:34:21
Если срок действия ваших токенов истекает через одну неделю, то очистите или проигнорируйте записи старше этого. Также сохраняйте только самые последние записи каждого пользователя. Размер списка будет зависеть от того, как долго вы храните свои токены и как часто пользователи отзывают свои токены. Используйте БД только тогда, когда таблица меняется. Загрузите таблицу в память при запуске приложения.
У вас может быть поле "last_key_used" в вашей БД в документе / записи вашего пользователя.
Когда пользователь входит в систему с помощью user и pass, генерирует новую случайную строку, сохраняет ее в поле last_key_used и добавляет ее в полезную нагрузку при подписании токена.
Когда пользователь входит в систему с помощью токена, проверьте last_key_used в БД, чтобы он соответствовал номеру в токене.
Затем, когда пользователь выходит из системы, например, или если вы хотите аннулировать токен, просто измените это поле "last_key_used" на другое случайное значение, и любые последующие проверки не пройдут, что вынудит пользователя войти в систему с пользователем и снова пройти.
Я сделал это следующим образом:
- Создать
unique hash
, а затем сохранить его в Redis и ваш JWT. Это можно назвать сессией- Мы также будем хранить количество запросов, сделанных конкретным JWT. Каждый раз, когда jwt отправляется на сервер, мы увеличиваем число запросов на целое число. (это необязательно)
Поэтому, когда пользователь входит в систему, создается уникальный хэш, который сохраняется в Redis и внедряется в ваш JWT.
Когда пользователь пытается посетить защищенную конечную точку, вы извлекаете уникальный хэш сеанса из вашего JWT, запрашиваете повторный запрос и смотрите, соответствует ли он!
Мы можем расширить это и сделать наш JWT еще более безопасным, вот как:
Каждый X запрашивает определенный JWT, мы генерируем новый уникальный сеанс, сохраняем его в нашем JWT, а затем помещаем в черный список предыдущий.
Это означает, что JWT постоянно меняется и останавливает несанкционированный взлом JWT, кражу или что-то еще.
Уникальная для пользователя строка и глобальная строка хешируются вместе
служить секретной частью JWT, допускающей как индивидуальную, так и глобальную аннулирование токенов. Максимальная гибкость за счет поиска / чтения БД при авторизации запроса. Также легко кешировать, так как они редко меняются.Поздно к вечеринке МОИ два цента даны ниже после некоторого исследования. Во время выхода из системы убедитесь, что происходит следующее:
Очистить хранилище / сеанс клиента
Обновляйте таблицу времени последнего входа в систему и дату и время выхода из системы всякий раз, когда происходит вход или выход из системы соответственно. Таким образом, время входа в систему всегда должно быть больше, чем выход из системы (или оставить дату выхода из системы нулевой, если текущий статус - вход в систему и еще не выполнен выход из системы)
Это намного проще, чем хранить дополнительную таблицу в черном списке и регулярно очищать. Для поддержки нескольких устройств требуется дополнительная таблица для входа в систему, даты выхода из системы с некоторыми дополнительными сведениями, такими как сведения об ОС или клиенте.
Почему бы просто не использовать утверждение jti (nonce) и сохранить его в списке как поле записи пользователя (зависит от БД, но, по крайней мере, список через запятую подходит)? Нет необходимости в отдельном поиске, так как другие указали, что, по-видимому, вы все равно хотите получить запись пользователя, и таким образом вы можете иметь несколько действительных токенов для разных клиентских экземпляров ("везде выйти" может сбросить список до пустого)
Если вы хотите иметь возможность отзывать пользовательские токены, вы можете отслеживать все выпущенные токены в вашей БД и проверять, действительны ли они (существуют) в сессионной таблице. Недостатком является то, что вы будете нажимать на БД при каждом запросе.
Я не пробовал, но я предлагаю следующий метод, чтобы разрешить отзыв токена, сохраняя при этом количество обращений к БД к минимуму -
Чтобы снизить частоту проверки базы данных, разделите все выпущенные токены JWT на X групп в соответствии с некоторой детерминистической ассоциацией (например, 10 групп по первой цифре идентификатора пользователя).
Каждый токен JWT будет содержать идентификатор группы и метку времени, созданную при создании токена. например, { "group_id": 1, "timestamp": 1551861473716 }
Сервер будет хранить все групповые идентификаторы в памяти, и каждая группа будет иметь временную метку, которая указывает, когда было последнее событие выхода пользователя из этой группы. например, { "group1": 1551861473714, "group2": 1551861487293, ... }
Запросы с токеном JWT, имеющим более старую групповую временную метку, будут проверены на достоверность (попадание в БД), и, если он действителен, будет выдан новый токен JWT со свежей временной меткой для будущего использования клиентом. Если временная метка группы токена новее, мы доверяем JWT (без попадания в БД).
Так -
- Мы проверяем токен JWT с использованием БД только в том случае, если токен имеет старую временную метку группы, в то время как будущие запросы не будут проверяться до тех пор, пока кто-то из группы пользователей не выйдет из системы.
- Мы используем группы, чтобы ограничить количество изменений меток времени (скажем, если пользователь входит и выходит, как будто завтра не будет - это повлияет только на ограниченное количество пользователей вместо всех)
- Мы ограничиваем количество групп, чтобы ограничить количество временных меток, хранящихся в памяти
- Отменить токен очень просто - просто удалите его из таблицы сеансов и сгенерируйте новую метку времени для группы пользователей.
Опоздание
Хороший подход к признанию токена недействительным, все равно потребует обращений к базе данных. Для цели, которая включает в себя изменение некоторых частей записи пользователя, например изменение ролей, изменение паролей, электронной почты и т. Д. В запись пользователя можно добавить поле или, в котором записывается время этого изменения, а затем вы включаете это в утверждения. Поэтому, когда JWT аутентифицирован, вы сравниваете время в заявках с записанным в БД, если это утверждение было раньше, то токен недействителен. Этот подход также аналогичен хранению
Примечание. Если вы используете
- Дайте 1 день истечения срока действия токенов
- Поддерживать ежедневный черный список.
- Поместите недействительные токены / токены выхода в черный список
Для проверки токена сначала проверьте время истечения токена, а затем черный список, если токен не истек.
Для длительных сеансов должен существовать механизм продления срока действия токена.
Очередь сообщений Kafka и локальные черные списки
Я подумал об использовании системы обмена сообщениями, такой как кафка. Позволь мне объяснить:
Например, у вас может быть одна микрослужба (назовем ее userMgmtMs service), которая отвечает заlogin
а также logout
и произвести токен JWT. Затем этот токен передается клиенту.
Теперь клиент может использовать этот токен для вызова различных микросервисов (назовем его priceMs), в пределах priceMs не будет НИКАКОЙ проверки базы данных дляusers
таблица, из которой было запущено первоначальное создание токена. Эта база данных должна существовать только в userMgmtMs. Также токен JWT должен включать разрешения / роли, чтобы priceM не нужно было искать что-либо в БД, чтобы обеспечить работу Spring безопасности.
Вместо перехода к базе данных в файле priceMs JwtRequestFilter может предоставить объект UserDetails, созданный данными, указанными в токене JWT (очевидно, без пароля).
Итак, как выйти из системы или аннулировать токен? Поскольку мы не хотим вызывать базу данных userMgmtM с каждым запросом priecesMs (что может привести к появлению большого количества нежелательных зависимостей), решением может быть использование этого черного списка токенов.
Вместо того, чтобы держать этот черный список в центре и иметь зависимость от одной таблицы от всех микросервисов, я предлагаю использовать очередь сообщений kafka.
UserMgmtMs по-прежнему несет ответственность за logout
и как только это будет сделано, он помещает его в свой собственный черный список (таблица, НЕ разделяемая между микросервисами). Кроме того, он отправляет событие kafka с содержимым этого токена во внутреннюю службу kafka, на которую подписаны все остальные микросервисы.
Как только другие микросервисы получат событие kafka, они также поместят его в свой внутренний черный список.
Даже если некоторые микросервисы не работают во время выхода из системы, они в конечном итоге снова заработают и получат сообщение в более позднем состоянии.
Поскольку kafka разработана таким образом, чтобы у клиентов была своя собственная ссылка на то, какие сообщения они прочитали, гарантируется, что ни один из клиентов, находящихся в нижнем или верхнем положении, не пропустит ни одного из этих недопустимых токенов.
Единственная проблема, о которой я могу думать, заключается в том, что служба обмена сообщениями kafka снова представит единую точку отказа. Но это как бы обратное, потому что если у нас есть одна глобальная таблица, в которой сохранены все недопустимые токены JWT, и этот db или микросервис не работает, ничего не работает. При подходе kafka + удаление токенов JWT на стороне клиента для выхода обычного пользователя из системы, простои kafka в большинстве случаев даже не будут заметны. Поскольку черные списки распространяются по всем микросервисам как внутренняя копия.
В случае выключения, когда вам нужно сделать недействительным пользователя, который был взломан, а kafka не работает, здесь начинаются проблемы. В этом случае может помочь изменение секрета в крайнем случае. Или просто убедитесь, что кафка активирован, прежде чем делать это.
Отказ от ответственности: я еще не реализовал это решение, но почему-то мне кажется, что большая часть предложенного решения отрицает идею токенов JWT с центральным поиском в базе данных. Итак, я думал о другом решении.
Пожалуйста, дайте мне знать, что вы думаете, имеет ли это смысл или есть очевидная причина, по которой это не удается?
ИСПОЛЬЗОВАНИЕ ОСВЕЩЕНИЯ JWT...
Подход, который я считаю практичным, заключается в хранении токена обновления (который может быть GUID) и идентификатора дублирующего токена обновления (который не меняется независимо от того, сколько обновлений выполняется) в базе данных и добавления их в качестве утверждений для user, когда создается пользовательский JWT. Можно использовать альтернативу базе данных, например, кэш памяти. Но в этом ответе я использую базу данных.
Затем создайте конечную точку веб-API обновления JWT, которую клиент может вызвать до истечения срока действия JWT. Когда вызывается обновление, получите токен обновления из утверждений в JWT.
При любом вызове конечной точки обновления JWT проверьте текущий токен обновления и идентификатор токена обновления как пару в базе данных. Создайте новый токен обновления и используйте его для замены старого токена обновления в базе данных, используя идентификатор токена обновления. Помните, что это утверждения, которые можно извлечь из JWT.
Извлеките утверждения пользователя из текущего JWT. Начните процесс создания нового JWT. Замените значение старого утверждения токена обновления вновь созданным токеном обновления, который также был недавно сохранен в базе данных. При этом сгенерируйте новый JWT и отправьте его клиенту.
Таким образом, после использования токена обновления, будь то предполагаемый пользователь или злоумышленник, любая другая попытка использовать токен обновления, который не является парным, в базе данных с его идентификатором токена обновления, не приведет к создание нового JWT, что не позволяет любому клиенту, имеющему этот идентификатор токена обновления, больше использовать серверную часть, что приводит к полному выходу из системы таких клиентов (включая законного клиента).
Это объясняет основную информацию.
Следующее, что нужно добавить к этому, - иметь окно, в котором можно обновить JWT, чтобы все, что за пределами этого окна, было бы подозрительным действием. Например, окно может быть за 10 минут до истечения JWT. Дата и время создания JWT могут быть сохранены как утверждение в самом JWT. И когда возникает такая подозрительная активность, то есть когда кто-то другой пытается повторно использовать этот идентификатор токена обновления за пределами или внутри окна после того, как он уже был использован в окне, следует отметить идентификатор токена обновления как недействительный. Следовательно, даже действительный владелец идентификатора токена обновления должен будет войти в систему заново.
Токен обновления, который не может быть найден в паре в базе данных с представленным идентификатором токена обновления, означает, что идентификатор токена обновления должен быть признан недействительным. Потому что бездействующий пользователь может попытаться использовать токен обновления, который, например, уже использовал злоумышленник.
JWT, который был украден и использован злоумышленником до того, как это сделал предполагаемый пользователь, также будет помечен как недействительный, когда пользователь попытается использовать токен обновления, как объяснялось ранее.
Единственная не охваченная ситуация, когда клиент никогда не пытается обновить свой JWT, даже если злоумышленник уже мог его украсть. Но это вряд ли произойдет с клиентом, который не находится под стражей (или чем-то подобным) злоумышленника, а это означает, что злоумышленник не может предсказать клиента в отношении того, когда клиент перестанет использовать серверную часть.
Если клиент инициирует обычный выход из системы. Выход из системы должен быть выполнен для удаления идентификатора токена обновления и связанных записей из базы данных, что предотвращает создание клиентом JWT обновления.
Если опция "Выйти со всех устройств" приемлема (в большинстве случаев это так):
- Добавьте поле версии токена в запись пользователя.
- Добавьте значение в этом поле к утверждениям, хранящимся в JWT.
- Увеличивайте версию каждый раз, когда пользователь выходит из системы.
- При проверке токена сравните его версию с версией, сохраненной в пользовательской записи, и отклоните, если она не совпадает.
В любом случае для получения пользовательской записи в любом случае требуется отключение базы данных, так что это не увеличивает накладных расходов на процесс проверки. В отличие от ведения черного списка, где нагрузка на БД значительна из-за необходимости использовать соединение или отдельный вызов, чистить старые записи и так далее.
БЕЗ ОСВЕЩЕНИЯ JWT...
На ум приходят 2 сценария нападения. Один касается скомпрометированных учетных данных для входа. А другой - фактическая кража JWT.
Для скомпрометированных учетных данных, когда происходит новый вход, обычно отправляют пользователю уведомление по электронной почте. Итак, если клиент не соглашается быть тем, кто вошел в систему, ему следует посоветовать выполнить сброс учетных данных, который должен сохранить в базе данных / кеше дату и время последней установки пароля (и установить это тоже, когда пользователь устанавливает пароль при первичной регистрации). Всякий раз, когда действие пользователя авторизуется, дата-время, когда пользователь изменил свой пароль, должна быть получена из базы данных / кеша и сравнена с датой-временем, когда был сгенерирован данный JWT, и запретить действие для JWT, которые были сгенерированы до указанной даты и времени сброса учетных данных, что по существу делает такие JWT бесполезными. Это означает сохранение даты и времени создания JWT как утверждения в самом JWT. В ASP.NET Core для выполнения этого сравнения можно использовать политику / требование, и в случае сбоя клиент запрещен. Следовательно, это приводит к глобальному выходу пользователя из системы на бэкэнде при каждом сбросе учетных данных.
Для фактического кражи JWT... Кражу JWT нелегко обнаружить, но JWT, срок действия которого истекает, легко решает эту проблему. Но что можно сделать, чтобы остановить злоумышленника до истечения срока действия JWT? Это с фактическим глобальным выходом из системы. Это похоже на то, что было описано выше для сброса учетных данных. Для этого обычно сохраняйте в базе данных / кеше дату и время, когда пользователь инициировал глобальный выход из системы, и при авторизации действия пользователя, получите его и сравните его с датой времени создания данного JWT, а также запретите действие для JWT, которые были сгенерированы до указанной даты и времени глобального выхода из системы, что по существу делает такие JWT бесполезными. Это можно сделать с помощью политики / требования в ASP.NET Core, как описано ранее.
А как определить кражу JWT? На данный момент я отвечу на этот вопрос: время от времени предупреждать пользователя о необходимости глобального выхода из системы и повторного входа в систему, поскольку это определенно приведет к выходу злоумышленника из системы.
Следующий подход может дать лучшее из обоих решений:
Пусть "немедленно" означает "~1 минута".
Кейсы:
Пользователь пытается успешно войти в систему:
A. Добавьте к токену поле "Время выпуска" и сохраните время истечения срока действия по мере необходимости.
B. Сохраните хэш пароля пользователя или создайте новое поле, скажем, tokenhash, в таблице пользователя. Сохраните tokenhash в сгенерированном токене.
Пользователь получает доступ к URL:
A. Если "время выпуска" находится в "немедленном" диапазоне, обработайте токен обычным образом. Не меняйте "время выпуска". В зависимости от продолжительности "немедленного", это продолжительность, в которой человек уязвим. Но короткое время, например, минута или две, не должно быть слишком рискованным. (Это баланс между производительностью и безопасностью). Тройка здесь не нужна.
B. Если токен не находится в "немедленном" диапазоне, проверьте хеш-токен на соответствие db. Если все в порядке, обновите поле "Время выпуска". Если не все в порядке, не обрабатывайте запрос (наконец, обеспечивается безопасность).
Пользователь меняет токенхэш, чтобы защитить учетную запись. В "ближайшем" будущем аккаунт находится под защитой.
Мы сохраняем поиск в базе данных в "непосредственном" диапазоне. Это наиболее выгодно, если есть пачки запросов от клиента в "немедленный" период времени.
Просто создайте и добавьте следующий объект в свою пользовательскую схему:
const userSchema = new mongoose.Schema({
{
... your schema code,
destroyAnyJWTbefore: Date
}
и всякий раз, когда вы получаете POST-запрос на /login, меняйте дату этого документа на
Date.now()
наконец, в вашем коде проверки аутентификации, т. е. в вашем промежуточном программном обеспечении, где вы проверяете
isAuthanticated
или же
protected
или любое другое имя, которое вы используете, просто добавьте проверку, которая проверяет
myjwt.iat
больше, чем
userDoc.destroyAnyJWTbefore
.
- Это решение является лучшим с точки зрения безопасности, если вы хотите уничтожить JWT на стороне сервера.
- Это решение больше не зависит от клиентской стороны, оно нарушает основную цель использования JWT, которая заключается в прекращении хранения токенов на стороне сервера.
- это зависит от контекста вашего проекта, но, скорее всего, вы захотите удалить JWT с сервера.
Если вы хотите уничтожить токен только со стороны клиента, просто удалите куки из браузера (если ваш клиент — браузер), то же самое можно сделать на смартфонах или любом другом клиенте.
В случае, если вы решили уничтожить токен со стороны сервера, я предлагаю вам использовать Radis для быстрого выполнения этой операции, реализуя стиль черного списка, упомянутый другими пользователями.
Теперь главный вопрос: бесполезны ли JWT?Бог знает.
Даже если вы удалите токен из хранилища, он все еще действителен, но только в течение короткого периода времени, чтобы снизить вероятность его злонамеренного использования.
Вы можете создать
deny-listing
и как только вы удалите токен из хранилища, вы можете добавить токен в этот список. Если у вас есть служба микрослужбы, все другие службы, использующие этот токен, должны добавить дополнительную логику для проверки этого списка. Это сделает вашу аутентификацию централизованной, потому что каждый сервер должен проверять централизованную структуру данных.
В итоге я получил токены обновления доступа, где uuid токенов обновления хранятся в базе данных, а uuid токенов доступа хранятся на сервере кеша в виде белого списка действительных токенов доступа. Например, у меня есть критические изменения в данных пользователя, например, его права доступа, следующее, что я делаю - я удаляю его токен доступа из белого списка кэш-сервера, и при следующем доступе к любому ресурсу моего API будет запрошен сервис авторизации. достоверность токена, то, если он отсутствует в белом списке кэш-сервера, я отклоню токен доступа пользователя и заставлю его повторно авторизоваться с помощью токена обновления. Если я хочу удалить сеанс пользователя или все его сеансы, я просто удаляю все его токены из белого списка и удаляю токены обновления из базы данных, поэтому он должен повторно ввести учетные данные, чтобы продолжить доступ к ресурсам.
Я знаю, что моя аутентификация больше не является безгосударственной, но, честно говоря, зачем мне вообще нужна аутентификация без состояния?
Решение IAM, такое как Keycloak (над которым я работал), обеспечивает конечную точку отзыва токена, например
Конечная точка отзыва токена/realms/{realm-name}/protocol/openid-connect/revoke
Если вы просто хотите выйти из системы агента пользователя (или пользователя), вы также можете вызвать конечную точку (это просто сделает токены недействительными). Опять же, в случае Keycloak проверяющей стороне просто нужно вызвать конечную точку
/realms/{realm-name}/protocol/openid-connect/logout
Вот как это сделать, не обращаясь к базе данных при каждом запросе:
- Храните хэш-карту действительных токенов в кеше памяти (например, LRU с ограниченным размером)
- При проверке токена: если токен находится в кеше, немедленно вернуть результат, запрос к базе данных не требуется (в большинстве случаев). В противном случае выполните полную проверку (запросите базу данных, проверьте статус пользователя и недействительные токены ...). Затем обновите кеш.
- При признании токена недействительным: добавьте его в черный список в базе данных, затем обновите кеш, отправьте сигнал на все серверы, если необходимо.
Имейте в виду, что кеш должен иметь ограниченный размер, как LRU, иначе у вас может закончиться память.
В этом примере я предполагаю, что у конечного пользователя также есть учетная запись. Если это не так, то остальной подход вряд ли сработает.
Когда вы создаете JWT, сохраняйте его в базе данных, связанной с учетной записью, в которой выполняется вход. Это означает, что только из JWT вы можете получить дополнительную информацию о пользователе, поэтому в зависимости от среды это может или не может будет хорошо.
После каждого последующего запроса вы не только выполняете стандартную проверку, которая (я надеюсь) идет с любой используемой вами структурой (которая проверяет, что JWT действительна), но также включает что-то вроде идентификатора пользователя или другого токена (который должен соответствовать что в базе).
При выходе из системы удалите файл cookie (если он используется) и аннулируйте JWT (строку) из базы данных. Если файл cookie не может быть удален со стороны клиента, то, по крайней мере, процесс выхода из системы гарантирует, что токен будет уничтожен.
Я обнаружил, что этот подход в сочетании с другим уникальным идентификатором (так что в базе данных есть 2 постоянных элемента, которые доступны для внешнего интерфейса), при этом сеанс очень устойчив.
Альтернативой может быть сценарий промежуточного программного обеспечения только для критических конечных точек API.
Этот сценарий промежуточного программного обеспечения проверяет базу данных, если токен аннулирован администратором.
Это решение может быть полезно в случаях, когда нет необходимости сразу полностью блокировать доступ пользователя.
Я собираюсь ответить, если нам нужно обеспечить выход из системы со всех устройств, когда мы используем JWT. Этот подход будет использовать поиск в базе данных для каждого запроса. Потому что нам нужно постоянное состояние безопасности даже в случае сбоя сервера. В пользовательской таблице у нас будет два столбца
- LastValidTime (по умолчанию: время создания)
- Авторизован (по умолчанию: true)
Всякий раз, когда от пользователя поступает запрос на выход, мы обновляем LastValidTime до текущего времени и Logged-In до false. Если есть запрос входа в систему, мы не будем изменять LastValidTime, но для параметра Logged-In будет установлено значение true.
Когда мы создаем JWT, у нас будет время создания JWT в полезной нагрузке. Когда мы авторизуем услугу, мы проверим 3 условия
- JWT действителен
- Время создания полезной нагрузки JWT больше времени LastValidTime пользователя
- Пользователь вошел в систему
Давайте посмотрим на практический сценарий.
У пользователя X есть два устройства A и B. Он вошел на наш сервер в 19:00, используя устройство A и устройство B. (допустим, время истечения JWT составляет 12 часов). У A и B есть JWT с createdTime: 7pm
В 21:00 он потерял свое устройство B. Он немедленно вышел из системы с устройства A. Это означает, что теперь запись пользователя X в нашей базе данных имеет LastValidTime как "ThatDate:9:00:xx:xxx" и "Logged-In" как "false".
В 9:30 мистер Вор пытается войти в систему с помощью устройства B. Мы проверим базу данных, даже если вход в систему ложный, поэтому мы не допустим.
В 22:00 г-н X входит в систему со своего устройства A. Теперь устройство A имеет JWT с созданным временем: 22:00. Теперь для входа в базу данных установлено значение "true"
В 22:30 мистер Вор пытается войти в систему. Несмотря на то, что вход верен. LastValidTime - это 21:00 в базе данных, но JWT B создал время как 19:00. Так что ему не разрешат доступ к сервису. Таким образом, используя устройство B без пароля, он не может использовать уже созданный JWT после выхода одного устройства из системы.