JWT (JSON Web Token) автоматическое продление срока действия

Я хотел бы реализовать аутентификацию на основе JWT в нашем новом REST API. Но так как срок действия задан в токене, можно ли его автоматически продлить? Я не хочу, чтобы пользователям приходилось регистрироваться через каждые X минут, если они активно использовали приложение в этот период. Это было бы огромным провалом UX.

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

Я нашел, как Auth0 решил это. Они используют не только токен JWT, но также токен обновления: https://docs.auth0.com/refresh-token

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

Есть ли другие варианты? Использование JWT не подходит для этого сценария?

17 ответов

Решение

Я работаю в Auth0 и участвовал в разработке функции обновления токенов.

Все зависит от типа приложения, и вот наш рекомендуемый подход.

Веб-приложения

Хорошим примером является обновление токена до его истечения.

Установите срок действия токена на одну неделю и обновляйте токен каждый раз, когда пользователь открывает веб-приложение, и каждый час. Если пользователь не открывает приложение более недели, ему придется снова войти в систему, и это приемлемый UX веб-приложения.

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

Мобильные / родные приложения

Большинство нативных приложений регистрируются один раз и только один раз.

Идея состоит в том, что токен обновления никогда не истекает, и его всегда можно обменять на действительный JWT.

Проблема с токеном, который никогда не истекает, состоит в том, что никогда не значит никогда. Что вы делаете, если вы потеряете свой телефон? Таким образом, пользователь должен каким-то образом идентифицировать его, а приложение должно предоставлять способ отзыва доступа. Мы решили использовать имя устройства, например, "maryo's iPad". Затем пользователь может зайти в приложение и отозвать доступ к "iPad от maryo".

Другой подход заключается в отзыве маркера обновления для определенных событий. Интересным событием является смена пароля.

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

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

  1. Выпустите токен JWT с относительно коротким сроком действия, скажем, 15 минут.
  2. Приложение проверяет дату истечения токена перед любой транзакцией, требующей токен (токен содержит дату истечения срока действия). Если срок действия токена истек, он сначала просит API "обновить" токен (это делается прозрачно для UX).
  3. API получает запрос на обновление токена, но сначала проверяет базу данных пользователей, чтобы узнать, был ли установлен флаг "reauth" для этого профиля пользователя (токен может содержать идентификатор пользователя). Если флаг присутствует, то обновление токена отклоняется, в противном случае выдается новый токен.
  4. Повторение.

Флаг 'reauth' в базе данных базы данных будет установлен, когда, например, пользователь сбросил свой пароль. Флаг удаляется, когда пользователь входит в следующий раз.

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

Ниже приведены инструкции по отзыву вашего токена доступа JWT:

1) Когда вы входите в систему, отправьте 2 токена (токен доступа, токен обновления) в ответ клиенту.
2) У маркера доступа будет меньше время истечения, а у обновления будет долгое время истечения.
3) Клиент (Front end) будет хранить токен обновления в своем локальном хранилище и токен доступа в куки.
4) Клиент будет использовать токен доступа для вызова API. Но когда он истекает, выберите токен обновления из локального хранилища и вызовите сервер аутентификации api, чтобы получить новый токен.
5) На вашем сервере аутентификации будет открыт API, который примет токен обновления, проверит его действительность и вернет новый токен доступа.
6) Когда срок действия маркера обновления истечет, пользователь выйдет из системы.

Пожалуйста, дайте мне знать, если вам нужна дополнительная информация, я также могу поделиться кодом (Java + Spring boot).

Я возился при переносе наших приложений на HTML5 с RESTful apis в бэкэнде. Решение, которое я придумал, было:

  1. Клиенту выдается токен с временем сеанса 30 минут (или любым другим обычным временем сеанса на стороне сервера) после успешного входа в систему.
  2. Таймер на стороне клиента создается для вызова службы для обновления токена до истечения срока его действия. Новый токен заменит существующие в будущем звонки.

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

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

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

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

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

При проверке JWT, jwt_version поле сравнивается с user_id и авторизация предоставляется, только если она совпадает.

Сегодня многие люди выбирают для выполнения управления сеансами с JWTs, не зная о том, что они отказываются от ради предполагаемой простоты. Мой ответ подробно описывает вторую часть вопросов:

В чем тогда реальная выгода? Почему бы не иметь только один токен (не JWT) и не хранить срок действия на сервере?

Есть ли другие варианты? Не подходит ли для этого сценария использование JWT?

JWT могут поддерживать базовое управление сеансами с некоторыми ограничениями. Будучи самоописывающимися токенами, они не требуют какого-либо состояния на стороне сервера. Это делает их привлекательными. Например, если у службы нет уровня сохраняемости, нет необходимости вводить его только для управления сеансом.

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

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

Я написал пост, в котором более подробно объяснил эти недостатки. Чтобы было ясно, вы можете обойти их, добавив больше сложности (скользящие сеансы, токены обновления и т. Д.)

Что касается других вариантов, если ваши клиенты взаимодействуют с вашим сервисом только через браузер, я настоятельно рекомендую использовать решение для управления сеансами на основе файлов cookie. Я также составил список методов аутентификации, широко используемых в настоящее время в сети.

JWT-автообновление

Если вы используете узел (React / Redux / Universal JS), вы можете установить npm i -S jwt-autorefresh,

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

Полный пример реализации

import autorefresh from 'jwt-autorefresh'

/** Events in your app that are triggered when your user becomes authorized or deauthorized. */
import { onAuthorize, onDeauthorize } from './events'

/** Your refresh token mechanism, returning a promise that resolves to the new access tokenFunction (library does not care about your method of persisting tokens) */
const refresh = () => {
  const init =  { method: 'POST'
                , headers: { 'Content-Type': `application/x-www-form-urlencoded` }
                , body: `refresh_token=${localStorage.refresh_token}&grant_type=refresh_token`
                }
  return fetch('/oauth/token', init)
    .then(res => res.json())
    .then(({ token_type, access_token, expires_in, refresh_token }) => {
      localStorage.access_token = access_token
      localStorage.refresh_token = refresh_token
      return access_token
    })
}

/** You supply a leadSeconds number or function that generates a number of seconds that the refresh should occur prior to the access token expiring */
const leadSeconds = () => {
  /** Generate random additional seconds (up to 30 in this case) to append to the lead time to ensure multiple clients dont schedule simultaneous refresh */
  const jitter = Math.floor(Math.random() * 30)

  /** Schedule autorefresh to occur 60 to 90 seconds prior to token expiration */
  return 60 + jitter
}

let start = autorefresh({ refresh, leadSeconds })
let cancel = () => {}
onAuthorize(access_token => {
  cancel()
  cancel = start(access_token)
})

onDeauthorize(() => cancel())

отказ от ответственности: я сопровождающий

Хороший вопрос - и в самом вопросе много информации.

Статья " Обновить токены: когда их использовать и как они взаимодействуют с JWT" дает хорошую идею для этого сценария. Некоторые моменты:-

  • Токены обновления содержат информацию, необходимую для получения нового токена доступа.
  • Жетоны обновления также могут истечь, но они довольно долгоживущие.
  • Для маркеров обновления обычно применяются строгие требования к хранению, чтобы они не были утечки.
  • Они также могут быть внесены в черный список сервером авторизации.

Также взгляните на https://github.com/auth0/angular-jwt angularjs

Для веб-API. Прочитайте Включить маркеры обновления OAuth в приложении AngularJS с помощью ASP .NET Web API 2 и Owin

Я на самом деле реализовал это в PHP, используя клиент Guzzle для создания клиентской библиотеки для API, но концепция должна работать для других платформ.

По сути, я выпускаю два токена: короткий (5 минут) и длинный, срок действия которого истекает через неделю. Клиентская библиотека использует промежуточное ПО для попытки одного обновления короткого токена, если она получает ответ 401 на какой-либо запрос. Затем он снова попытается выполнить исходный запрос, и, если ему удалось обновить, он получит правильный ответ, прозрачно для пользователя. Если это не удалось, он просто отправит 401 пользователю.

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

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

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

Я решил эту проблему, добавив переменную в данные токена:

softexp - I set this to 5 mins (300 seconds)

Я поставил expiresIn вариант моего желаемого времени, прежде чем пользователь будет вынужден снова войти в систему. Мой установлен на 30 минут. Это должно быть больше, чем значение softexp,

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

Если токен все еще действителен на основе expiredIn значение, но оно уже превысило softexp значение, сервер ответит отдельным статусом для этой ошибки, например. EXPIRED_TOKEN:

(Math.floor(Date.now() / 1000) > decoded.softexp)

На стороне клиента, если он получил EXPIRED_TOKEN В ответ он должен автоматически обновить токен, отправив запрос на обновление на сервер. Это прозрачно для пользователя и автоматически заботится о клиентском приложении.

Метод обновления на сервере должен проверить, является ли токен все еще действующим:

jwt.verify(token, secret, (err, decoded) => {})

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

Как насчет этого подхода:

  • Для каждого запроса клиента сервер сравнивает значение expirationTime токена с (currentTime - lastAccessTime)
  • Если expirationTime <(currentTime - lastAccessedTime), он меняет последний lastAccessedTime на currentTime.
  • В случае неактивности в браузере в течение времени, превышающего expirationTime, или в случае, если окно браузера было закрыто и expirationTime> (currentTime - lastAccessedTime), а затем сервер может истечь токен и попросить пользователя снова войти в систему.

В этом случае нам не требуется дополнительная конечная точка для обновления токена. Был бы признателен любой обратной связи.

Ref - обновить пример JWT с истекшим сроком действия

Другой альтернативой является то, что по истечении срока действия JWT пользователь / система будет вызывать другой URL-адрес, предположим /refreshtoken. Также вместе с этим запросом должен быть передан истекший JWT. Затем сервер вернет новый JWT, который может использоваться пользователем / системой.

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

Используйте два токена:

  1. Токен доступа (для вызовов API)
  2. Обновить токен (для обновления токена доступа)

Шаги по реализации JWT, которые продлевают

  1. Если сеанс рассчитан на 1 час, установите срок действия токена доступа на 1 час, а срок действия токена обновления — на 2 часа.
  2. Поддерживайте таймер на 1 час для каждого вызова API, и если время превышает 1 час, отправьте токен обновления в заголовке Auth. Серверная часть определяет тип токена (AT/RT), проверяет срок его действия и, соответственно, генерирует новый токен.
  3. Если таймер клиента превышает 1 час с момента последнего вызова, клиент вызовет API выхода из системы, который удалит эти токены из категории белого списка или добавит их в черный список в зависимости от того, какой метод вам подходит.(Я выбрал белый список, потому что очистка не требуется).
  4. Если это токен доступа, сервер просто обслужит запрос, а если токен является токеном обновления, серверная часть сгенерирует новый токен доступа и токен обновления и отправит эти два в заголовках ответа.

Примечание :

Шаг 2 можно выполнить по-другому: как только таймер превысит порог в 1 час, определите разницу между последним вызовом API и текущим временем, если оно превышает 1 час, затем принудительно выполните выход из системы, иначе на 59-й минуте отправьте время последнего вызова API и сгенерируйте новое. жетоны со сроком действия (время последнего звонка + 1 час). В этом случае вам не нужен токен обновления.

А для внесения токенов в белый или черный список вы можете использовать redis(вместо db) для поиска.

Идея JWT хороша, вы помещаете то, что вам нужно, в JWT и теряете состояние. Две проблемы:

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

Решение 1. Используйте пользовательский JSON:

       {"userId": "12345", "role": "regular_user"}

Зашифруйте его с помощью симметричного (AES) алгоритма (это быстрее, чем подписывание с помощью асимметричного) и поместите его в файл cookie с быстрым сроком действия. Я бы по-прежнему называл это JWT, так как это JSON и используется как токен в веб-приложении. Теперь сервер проверяет наличие файла cookie и возможность расшифровки его значения.

Решение 2. Используйте токен обновления:

БратьuserIdкак12345, зашифруйте его и поместите в файл cookie с длительным сроком действия. Не нужно создавать специальное поле для токена обновления в БД.

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

Самая простая альтернатива — использовать токен обновления в качестве токена доступа, т. е. вообще не использовать JWT.

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

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

  1. Стандартный JWT, используемый для аутентификации запросов. Срок действия этого токена истекает через 15 минут.
  2. JWT, который действует как токен обновления, помещаемый в безопасный файл cookie. Только одна конечная точка (на самом деле это ее собственная микрослужба) принимает этот токен, и это конечная точка обновления JWT. Он должен сопровождаться токеном CSRF в теле сообщения, чтобы предотвратить CRSF на этой конечной точке. Конечная точка обновления JWT сохраняет сеанс в базе данных (идентификатор сеанса и пользователя закодированы в JWT обновления). Это позволяет пользователю или администратору аннулировать токен обновления, поскольку токен должен одновременно проверять и соответствовать сеансу для этого пользователя.

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

services.Configure(Configuration.GetSection("ApplicationSettings"));

              services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); 

        services.AddDbContext<AuthenticationContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("IdentityConnection")));

        services.AddDefaultIdentity<ApplicationUser>()
            .AddEntityFrameworkStores<AuthenticationContext>();

        services.Configure<IdentityOptions>(options =>
        {
            options.Password.RequireDigit = false;
            options.Password.RequireNonAlphanumeric = false;
            options.Password.RequireLowercase = false;
            options.Password.RequireUppercase = false;
            options.Password.RequiredLength = 4;
        }
        );

        services.AddCors();

        //Jwt Authentication

        var key = Encoding.UTF8.GetBytes(Configuration["ApplicationSettings:JWT_Secret"].ToString());

        services.AddAuthentication(x =>
        {
            x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        }).AddJwtBearer(x=> {
            x.RequireHttpsMetadata = false;
            x.SaveToken = false;
            x.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(key),
                ValidateIssuer = false,
                ValidateAudience = false,
                ClockSkew = TimeSpan.Zero
            };
        });
    }

Если вы используете AWS Amplify & Cognito, это поможет вам:

Используйте Auth.currentSession(), чтобы получить текущий действительный токен или получить новый, если срок действия текущего истек. Amplify справится с этим. В качестве запасного варианта используйте какое-нибудь интервальное задание для обновления токенов по запросу каждые x минут, может быть, 10 минут. Это необходимо, когда у вас есть длительный процесс, такой как загрузка очень большого видео, которое займет более часа (возможно, из-за медленной сети), тогда срок действия вашего токена истечет во время загрузки, и amplify не будет автоматически обновляться для вас. В этом случае эта стратегия будет работать. Продолжайте обновлять свои токены с некоторым интервалом. Как обновлять по запросу, в документах не упоминается, так что вот оно.

      import { Auth } from 'aws-amplify';

try {
  const cognitoUser = await Auth.currentAuthenticatedUser();
  const currentSession = await Auth.currentSession();
  cognitoUser.refreshSession(currentSession.refreshToken, (err, session) => {
    console.log('session', err, session);
    const { idToken, refreshToken, accessToken } = session;
    // do whatever you want to do now :)
  });
} catch (e) {
  console.log('Unable to refresh Token', e);
}

Происхождение: https://github.com/aws-amplify/amplify-js/issues/2560

Другие вопросы по тегам