Довелось мне в одном из наших проектов столкнуться с такой задачей. Вроде бы кажется тривиальной и понятной, но на самом деле реализаций масса и не все они могут подойти именно для твоего проекта. Но в этой статье я расскажу не о том как реализовать этот механизм, а о том как же он работает. И так приступим
В документации на официальном сайте, есть очень хорошо описанные API самой библиотеки, но нюансы скрыты.
Немного про JWT токен и проверку его истечения
Рассмотрим случай с использованием JWT токена, так как это наиболее приемлемый способ для получения информации о пользователе путем декодирования содержимого токена idToken и однозначно гарантированная проверка его подлинности.
Получение idToken осуществляется путем
firebase.auth().currentUser.getIdToken()
Этот метод возвращает promise объект, так как эта операция асинхронна и занимает некоторое время для того чтобы сходить на сервер google и получить ответ.
Для того чтобы на клиенте, в JS, проверить истечение этого токена, необходимо его декодировать. В этом нам поможет библиотека https://github.com/auth0/node-jsonwebtoken, которая позволяет без всякой проверки токена на валидность декодировать как header так и payload. Мы знаем что jwt токен состоит из трех частей
- header — в нем содержится служебная информация о времени создания токена, о времени истечении и так далее.
- payload — содержит данные о пользователе
- sign — ну и конечно же фишка jwt в подписи данных header и payload и дает возможность убедиться что они не были подправлены по пути от сервера клиенту или наоборот.
Как триггерится и как работает событие смены токена
Из документации firebase, ясно, что для того чтобы отловить событие обновления токена нужно подписаться на событие onIdTokenChanged.
onIdTokenChanged(nextOrObserver, error, completed) returns function()
Но фишка в том, что в документации не сказано о том, как же триггерится это самое событие истечения токена. И при чем при истечении 1 часа, при котором токен истечет, это событие само не будет сгенерировано, так как нужен триггер со стороны пользователя.
Как же его осуществить? Очень просто, как оказалось.
Что необходимо сделать:
- Декодировать токен при помощи jsonwebtoken библиотеки.
const decoded = jwt.decode(token, { complete: true })
тут используется опции { complete: true }, чтобы получить не просто декодированный токен, но и его header и payload. Нас интересует header
- Получить из payload значение поля exp
const expireTime = decoded.payload.exp
- Получить текущее время в формате данных поля exp, придется разделить текущее значение Date.now() на 1000.
- В итоге значение текущего таймстемпа будет выглядеть так Math.floor(Date.now() / 1000)
- Math.floor тут нужен прежде всего для того чтобы отбросить в нижнюю сторону без округления значение текущей даты, иначе будет больше разница между временем истечения и текущем временем.
- Теперь остается сравнить значения путем currentTime >= expireTime
- Если это условие срабатывает, то надо менять токен.
Есть нюанс, при вызове firebase.auth().currentUser.getIdToken(), можно указать параметр true, что позволит зафорсить обновление токена. Но это не тру, надо делать по правильному! Правильно проверять время истечения и делать запрос на смену токена.
ВАЖНО
Только после того как вы вызовите, по истечению токена, метод firebase.auth().currentUser.getIdToken(), будет вызвано событие изменения токена (и то только после того как гугл выдаст новый токен и shallow equal скажет, что токены разные) и в итоге вызовется firebase.auth().onIdTokenChanged. Уррраа!
Это очень важно знать и помнить, что позволит сэкономить кучу времени!
Важные моменты по поводу истечения времени
Приготовьтесь к тому, что время на разных серверах и в том числе на стороне клиента может быть очень и очень не синхронизировано. Так что проверка истечения токена может быть очень даже не точной.
Даже в самой библиотеке jsonwebtoken, есть толерантное отклонение :)
if (clockTimestamp >= payload.exp + (options.clockTolerance || 0)) { … }
Так что встает другой вопрос, а что если действительно обновление будет не во время что делать с запросами, которые могут получить 401 или 403 со стороны сервера, при запросе данных.
Ответ прост: Нужно использовать RETRY механизм. В самом простецкой его реализации (при использовании redux-saga), он выглядит так:
То есть вы пытаетесь несколько раз отправить один и тот же запрос с увеличивающимся интервалом по времени (backoff).
Заблуждения
Некоторые люди думают, что получится обхитрить систему путем перестановки текущей даты, но проверка истечения токена, на стороне firebase серверов, все равно покажет что токен еще не истек и будет выдан тот же самый токен, не новый (если конечно не был передан параметр true в getIdToken для форса обновления токена).
Ранее статья была опубликована тут.