Токен обновления JWT. Может ли он быть автономным?

#security #authentication #oauth-2.0 #jwt

#Безопасность #аутентификация #oauth-2.0 #jwt

Вопрос:

Я хочу сохранить токены, подобные этому (полезная нагрузка):

access_token

 {
  "user": "john_doe",
  "iat": 1444262543,
  "exp": 1444262563, // 15 minutes
  "type": "access"
}
  

refresh_token

 {
  "user": "john_doe",
  "iat": 1444262543,
  "exp": 1444262563, // 24 hours
  "type": "refresh"
}
  

Как вы можете видеть, access_token и refresh_token почти идентичны, за исключением времени жизни и типа.

Это проблема? Может ли это быть дырой в безопасности при использовании токена обновления таким образом?

PS: Причина этого в том, что я не хочу хранить токен обновления в хранилище (DB / Redis).

Ответ №1:

С точки зрения безопасности, автономные токены обновления уязвимы.

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

Токен обновления ДОЛЖЕН храниться на стороне сервера. Мы не должны использовать «автономное» свойство JWT для токена обновления. Это не оставляет нам возможности отозвать токены обновления, кроме как изменить наш закрытый ключ.

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

Шаги более высокого уровня для создания access_token из refresh_token могут быть примерно такими:

Давайте сначала заложим основу:

Таблица токенов в базе данных может содержать следующие поля:

     user_id<uuid>: id of the user
    access_token<text>: access token
    access_token_expiry<date>: access token expiring timestamp
    refresh_token<text>: refresh token (sha)
    refresh_token_expiry<date>: refresh token expiring timestamp
    refresh_count: number of times the refresh token has been used to access access_token (just in case if you need this field)
  

Обратите внимание, что мы должны хранить хэш токена обновления в базе данных.

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

 id<BIGINT>: primary key for the table
user_id<uuid>: id of the user who logged in
map_id<uuid>: This will be the key which maps one access token to its refresh token counterpart. Both tokens of the pair will contain this id in their body.
status<String>: could be ACTIVE|INACTIVE(logged out, INACTIVE user will not be allowed to get access_token from the refresh_token)
created_on<Date>: timestamp for record created on
modified_on<Date>: timestamp for record modified on
  

тело refresh_token:

 {
  "iss": "ABC Service", (The Issuer of the token)
  "sub": "4953fag3-ec5e-4ed3-b09e-d847f3f376c6", (The user_id, subject of the token)
  "aud": "UI", (audience who will use the token)
  "typ": "refresh", (type)
  "iat": 1599326501,
  "exp": 1601918501,
  "jti": "749c77e5-bac0-43f1-aeea-1618ada0224f", (unique identifier for this token)
  "mid": "e392692b-6d77-49a9-9928-ac3c3d5208a3" (unique identifier for access-refresh token pair - refers to map_id in login_sessions table)
}
  

тело access_token:

 {
  "iss": "ABC Service", (The Issuer of the token)
  "sub": "4953fag3-ec5e-4ed3-b09e-d847f3f376c6", (The user_id, subject of the token)
  "aud": "UI", (audience who will use the token)
  "typ": "access", (type)
  "iat": 1599326501,
  "exp": 1599412901,
  "jti": "3d2985ef-e767-495e-af88-448fc0ecb167", (unique identifier for this token)
  "mid": "e392692b-6d77-49a9-9928-ac3c3d5208a3", (unique identifier for access-refresh token pair - refers to map_id in login_sessions table)
}
  "fname": "john",
  "lname": "doe",
  "roles": [99b18377-5b4c-4e68-8ff4-bac4aea93bd2]
  "other_key": "other_value"
  

Генерация токена доступа из токена обновления:

Как только срок действия токена доступа истек, мы запускаем API сервера авторизации, чтобы получить токен доступа, передавая refresh_token (jwt) в теле. Серверу авторизации будет предоставлен этот API, который примет токен обновления и выполнит следующие шаги, а затем вернет токен доступа.

Шаги:

  1. Verify the refresh_token with public key (whose counterpart private formed the token initially)
 2. Pick the mid(map_id) from the token body
 3. Get login session record containing this mid from the login_sessions table. Go to next step if there exists some login_session record.
 4. If the status of the login_session record is ACTIVE, create one access_token (using the private key) with relevant details in it's body
 5. Send back the access token as a response.
  

Если какой-либо из описанных выше шагов 1-4 завершается неудачей, мы отправляем ответ об ошибке с соответствующим сообщением.

Комментарии:

1. Не могли бы вы добавить больше информации о создании токена обновления из токена доступа?

2. Эй, Иван, я обновил информацию о создании токена доступа из токена обновления (это то, что мы хотели, верно?). Я бы посоветовал вам прочитать для этого часть «Генерация токена доступа из токена обновления». Перед этой частью я заложил основу для этого, что может помочь. Спасибо

3. @IvanStudenikin Токены обновления не создаются из токенов доступа. Создание долгосрочного токена (обновление) из краткосрочного токена (доступ) является проблемой безопасности. Токены обновления используются для создания токенов доступа и токенов идентификации.

4. @ManishJuriani спасибо за ваше объяснение (очень подробное)! Вопрос: поскольку нам все еще нужно обращаться к базе данных, не лучше ли использовать простой Guid как refresh_token и сопоставить его с утверждениями access_token (скажем, именем пользователя) в базе данных?

5. @IvanStudenikin Да, это тоже хорошо. Возможно, вам потребуется немного изменить реализацию. Например, вместо шага 1. Verify the refresh_token with public key мы можем проверить срок действия токена обновления из таблицы токенов. Любопытно узнать ваши мысли о том, почему токен обновления guid будет лучше, чем jwt?