Pull to refresh

Comments 22

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

Спасибо за статью. Я тоже долгое время занимался данной задачей.
Скажите что вы делаете в следующих случаях:
1. Что происходит когда токен становится невалидным и его обновление проходит неудачно?
2. Что если время на устройстве пользователя не совпадает с временем на сервере? Ведь тогда isExpired() может работать не корректно.
3. Как быть если сайт открыт в двух вкладках одновременно и на одной из них токен обновился, а на другой нет? Ведь метод getToken() не проверяет наличие нового токена в localStorage.
4. Что если при истекшем токене несколько запросов посылаются одновременно? Т. е. в этом случае для каждого из запросов isExpired() вернет false и для запустится обновление токена.

Рад, что сие пригодилось) По вопросам:


  1. Внутри метода setToken(token), в случае пустого токена localStoarge будет очищен, после чего все подписчики узнают, что сессия превратилась в тыкву. Пример кода из самой статьи немного упрощен, и если сервер будет присылать к примеру объекты с описание ошибки, они будут благополучно складываться под видом токенов. В коде на GitHub я оставил реализацию обновления токена пользователям библиотеки и ожидаю, что в случае провала обновления вернется null (onUpdateToken?: (token: T) => Promise<T | null>)
  2. В документации к JWT говорится, что exp должен быть типа NumericDate (rfc7519(4.1.4)), там же говорится, что NumericDate это "A JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds". В общем все должно быть хорошо.
  3. Да, это хороший кейс, я его не учел. Чтобы его покрыть можно дополнительно подписаться на обновления localStorage. Данная подписка будет срабатывать только в том случае, когда обновление произошло в другой вкладке, собственно для синхронизации вкладок (Using the Web Storage API(MDN))
1. Понял. Спасибо.
2. Насколько я понимаю это избавляет от проблем с разными часовыми поясами, но если на устройстве часы просто отстают, тогда же NumericDate на устройстве будет меньше чем на сервере?! Хотя синхронизацию времени наверное тоже надо оставить на совести пользователя.
3. Спасибо. Я тоже пришел к такому варианту. Или же каждый раз брать токен из localStorage. Интересно было как вы это решили.
4. Четвертый вопрос я дописал позже, и вы на него не ответили. )
по пункту 4. из опыта могу предложить такую схему:
— обернуть все запросы в проверку токена (например с помощью промисов)
— если токен истёк — сначала выполнится запрос на получение нового токена, потом запрос данных
— тут важно, чтобы при нескольких запросах отправлялся только 1 запрос на получение нового токена, тоже реализуемо
  1. Я осознал. Да, если выставить на клиенте ручками время значительно отличающееся от серверного, то все может поломаться (и мы к примеру будем бесконечно обновлять токены, что не есть хорошо). Я бы наверно исходил из потребностей в данном случае: если мы ожидаем, что расхождение спокойно может быть в несколько минут, простым решением будет полагать, что за n минут (например 1, 5, 10...) до истечения токена уже идти обновлять его; если нужно точнее, то можно спрашивать у сервера не абсолютное время истечения, а относительное (через столько-то секунд), тогда погрешность будет в величину ожидания ответа от сервера.
    Я еще осознал, что я упустил момент, когда мы отправили истекший токен на сервер и получили ошибку в ответе. Для решения этой проблемы к данному решению нужно допилить обновление токена на этапе проверки ответа от сервера.
  2. Спамить localStorage тоже не очень хорошо, лучше когда наш js спит и дергается только по необходимости.
  3. Да, этот кейс имеет смысл исправить даже в текущей реализации, если не забуду — поправлю. Сходу могу придумать вариант, где из метода getToken (который асинхронный) мы возвращаем промис, который резолвится только после окончания обновления токена:


    let isUpdating = false;
    let resolvers = [];
    const checkExpiry = async () => {
        if (isExpired(_token)) {
            isUpdating = true;
    
            const newToken = await onUpdateToken(privateToken);
    
            resolvers.forEach(resolver => resolver(newToken));
            isUpdating = false;
            resolvers = [];
    
            ...
        }
    };
    
    const getToken = async () => {
        if (isUpdating) {
            return new Promise(resolve => {
                resolvers.push(resolve);
            });
        }
    
        await checkExpiry();
    
        ...
    };

    Но нужно быть очень аккуратным и обвесить все железными проверками на ексепшены =) а то можно начать терять промисы. Я думаю, если посидеть подумать, можно что-то поинтереснее придумать, но как пруф оф концепт должно работать)


  1. Нумерация сбивается… (здесь должно быть 2.)
    В предыдущем ответе нумерация должна была начинаться с двойки… сорри, если ввел в заблуждение
Я на клиенте вообще не проверяю, что время токена вышло. Пришел запрос на сервер, если время вышло и refresh token жив, то отправляю в заголовке новый. Клиент смотрит на этот заголовок и если он не пустой, то сохраняет новый токен.

Правильно я понимаю, что для этого вам приходится в каждом запросе отправлять refresh token?

Чтоооо??? 0_о Зачем он тогда вообще вам нужен? Refresh token нужен для повышения безопасности если украден access token. А у вас получается что если украли access token, то украли и refresh token.
Вот знал что прилетит. А Refresh token у Вас в блокнот записан, его никто не упрет? В моем случае refresh token определяет сессию. Также есть fingerprint клиента (хешь user agent + ip), тоже зашит в jwt. Допустим JWT уперли и начинают просится в гости, fingerprint не совпал сессия удаляется (вместе с refresh token).

Во первых не в блокнот. И упепеть его могут, но не так легко как access token.
Во вторых можно и без рефреш токена, но его тогда можно просто не обновлять. Зачем зашивать его в access?
Ну а отпечаток это хорошо и правильно, но к моему вопросу не относится.

Я не вижу причин этого не делать. У меня клиент отправляет только jwt, Нет заморочек с повторными запросами, перевыпусками токенов и т.д. Пользователь зашел в профиль увидел свою активность по сессиям (детальную). Увидел что то не то, дропул лишнее и живет спокойно.
Где вы храните access token и refresh token?
Я храню в localStorage или в куках — по разному.
Я не говорю что вы сделали что-то ужасное. Просто почему бы вам вообще не отказаться от refresh токена? Пусть будет только access и все.
Вот тогда у вас действительно не будет заморочек. А сейчас получается у вас возможен такой вариант:
1. Клиент отправит на сервер несколько запросов одновременно с устаревшим токеном
2. В запросе, который дойдет до сервера первым — будет заменен токен
3. Все остальные запросы будут отклонены, так как в них токен еще старый (истекший и с уже невалидным refresh токеном)
refresh token нужен чтобы идентифицировать сессию. Убил в базе refresh, новый access не получишь и все access выписанные c этим refresh тоже не работают. refresh живет два месяца, затем пользователь заходит по логину и паролю, текущая удаляется, новый refresh.

Отправил пользователь 5ть запросов параллельно получил 5ть access токенов, если они в запросе не валидны. Все они на клиенте друг друга перезапишут. Далее будет валидный токен. Минус лишь в том что при каждом получении access токена идет запрос в базу. Если вы контролируете клиент, то не проблема. Если сторонняя компания использует api, то ограничиваете ее на сервере (баните большом количестве запросов с невалидным токеном).
ОМГ. Я понял. Ваш JWT это не JWT.
ОМГ, очень аргументированно. Может объясните что в Вашем понимании JWT?
Что такое JWT вы можете загуглить.
Объясню что меня удивляет в ваших ответах:
1. Вы пишете:
Минус лишь в том что при каждом получении access токена идет запрос в базу.

Т. е. логично предположить что при обычном запросе вы к базе не обращаетесь. И это правильно. От части это преимущество использования JWT. Но в то же время вы пишете:
Убил в базе refresh, новый access не получишь и все access выписанные c этим refresh тоже не работают.

Получается что вы всеравно каждый раз лезете в базу чтобы достать оттуда refresh токен для проверки access токена.

2. При использовании JWT refresh токен используется для выдачи нового токена, а для валидации access токена он не требуется, так как там сверяется сигнатура. Исходя из ваших слов — у вас не так.

3. При использовании JWT refresh токен является ОДНОРАЗОВЫМ. И его нужно беречь еще больше чем access токен, так как даже если у вас украдут только access токен, то пользоваться им смогут только пока он не устареет (обычно минут 15, а в вашем случае всего 2 месяца). А если украдут оба токена, то пользоваться ими смогут бесконечно.

4. Вы пишете:
Отправил пользователь 5ть запросов параллельно получил 5ть access токенов, если они в запросе не валидны.

Итак пришел первый запрос. Сервер формирует новый access (у вас refresh) токен — старый стирает. Получается что остальные запросы со старыми токенами должны быть невалидными так как в них зашит старый refresh токен. Каким образом получается что у вас они всеравно получат новые токены?
Если у Вас упрут access, то и упрут refresh и будут безконтрольно выписывать access токены. Если он конечно не в памяти или не в блокноте. В базу я лезу раз в 30 минут (можно чаще, можно реже). Это позволяет контролировать access токены. Пользователь имеет возможность закрыть лавочку по выписыванию токенов. В моем случае не смогут им пользоваться, т.к fingerprint сразу прикроет лавочку (можно делать более детальный fingerprint на клиенте). Refresh токен живет два месяца или пока пользователь сессию не закроет.

P.S. Простите что не по библейским законам живу, зато на клиенте не надо париться по выписыванию токенов.
Storing tokens in local storage creates security vulnerability and allow attacker to stole token via:
— XSS attack
— NPM supply chain attack (e.g. by injecting malicious code in NPM dependency and it may not even be your direct dependency like [your app] -> [direct dependecy] ->… -> [malicios module])
— Browser extension supply chain attack or phishing attack (e.g. by injecting malicious code in existing extension or tricking user to install doubtful extension)

I highly recomend to store tokens in cookies with HttpOnly flag, that will prevent all listed types of attacks

Но ведь cookies отправляются безусловно, на любой запрос соответствующий настройке cookie. Разве это не является бОльшей уязвимостью, чем все описанные выше?

Thank you for the comment, that's true, I forgot even to mention this problem in the article.
The problem is that you may not always have own backend. And if the domains of the app and server differ you will not be able to use cookies.
Even in this case, it will be better not to store token in the local storage, but in the memory. It is much safer if you ask the user to log in again after closing the app.


But it is so annoying to ask people to input login/password every time, isn't it?)) And people are lazy… and what if we protected by VPN for example… is it enough to allow saving tokens in local storage? What your sales and information security department think about it? ^_^


I think the documentation of oauth0 might be useful in this question. They give recommendations about how to store tokens: Store Tokens (Auth0 Docs)

Sign up to leave a comment.

Articles