imageВ данном туториале мы кратко разберем, как реализовываются REST-запросы к API, требующие, чтобы пользователь был авторизован, и создадим асинхронную «обертку» для запроса, которая будет проверять авторизацию и своевременно ее обновлять.

Данные для авторизации


Сделав REST-запрос к api, куда мы отправили логин и пароль, в ответ мы получаем json следующего формата (значения взяты рандомные и строки обычно длиннее):

{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSld",
"refresh_token": "1eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgS",
"expires_in": 124234149563
}

Полей в ответе может быть больше, например еще «token_type», «expires_on» и т. д., но, для данной реализации, нам нужны только три поля, приведенные выше.
Давайте их рассмотрим подробнее:

  • access_token — токен, который нам нужно будет отправлять в шапке каждого запроса, для получения данных в ответ
  • refresh_token — токен, который нам нужно будет отправлять, для получения нового токена, когда истечет время жизни старого
  • expires_in — время жизни токена в секундах

Получение токена


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

Хранить данные для авторизации мы будем в sessionStorage или localStorage, в зависимости от наших нужд. В первом случае данные хранятся до тех пор, пока пользователь не завершит сеанс или не закроет браузер, во втором случае данные в браузере будут храниться неограниченное время, пока по каким-либо причинам localStorage не будет очищен.

Функция для сохранения токена в sessionStorage:


function saveToken(token) {
    sessionStorage.setItem('tokenData', JSON.stringify(token));
}

Функция для получения токена:



function getTokenData(login, password) {
    return fetch('api/auth', {
        method: 'POST',
        credentials: 'include',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            login,
            password,
        }),
    })
        .then((res) => {
            if (res.status === 200) {
                const tokenData = res.json();
                saveToken(JSON.stringify(tokenData)); // сохраняем полученный токен в sessionStorage, с помощью функции, заданной ранее
                return Promise.resolve()
            }
            return Promise.reject();
        });
}

Таким образом мы получили токен с полями «access_token», «refresh_token» и «expires_in» и сохранили его в sessionStorage для дальнейшего использования.

Обновление токена


Токен полученный нами ранее имеет ограниченное время жизни, которое задано в поле «expires_in». После того как его время жизни истечет, пользователь не сможет получить новые данные, отправляя данный токен в запросе, поэтому нужно получить новый токен.

Получить токен мы можем двумя способами: первый способ это заново авторизовавшись, отправив логин и пароль на сервер. Но это нам не подходит, т. к. заставлять пользователя каждый раз заново вводить данные авторизации по истечению какого-то отрезка времени — неправильно, это надо делать автоматически. Но хранить где-то в памяти пару логин/пароль для автоматической отправки небезопасно, именно для этого и нужен «refresh_token», который был получен ранее вместе с «access_token» и хранится в sessionStorage. Отправив данный токен на другой адрес, который предоставляет api, мы сможем получить в ответ новый «свежий» токен.

Функция для обновления токена



function refreshToken(token) {
    return fetch('api/auth/refreshToken', {
        method: 'POST',
        credentials: 'include',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            token,
        }),
    })
        .then((res) => {
            if (res.status === 200) {
                const tokenData = res.json();
                saveToken(JSON.stringify(tokenData)); // сохраняем полученный обновленный токен в sessionStorage, с помощью функции, заданной ранее
                return Promise.resolve();
            }
            return Promise.reject();
        });
}

С помощью кода выше мы перезаписали токен в sessionStorage и теперь по новой можем отправлять запросы к api.

Создание функции-обертки


Теперь создадим функцию, которая будет добавлять данные для авторизации в шапку запроса, и при необходимости их автоматически обновлять перед совершением запроса.

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

Функция-обертка



export async function fetchWithAuth(url, options) {
    
    const loginUrl = '/login'; // url страницы для авторизации
    let tokenData = null; // объявляем локальную переменную tokenData

    if (sessionStorage.authToken) { // если в sessionStorage присутствует tokenData, то берем её
        tokenData = JSON.parse(localStorage.tokenData);
    } else {
       return window.location.replace(loginUrl); // если токен отсутствует, то перенаправляем пользователя на страницу авторизации
    }

    if (!options.headers) { // если в запросе отсутствует headers, то задаем их
        options.headers = {};
    }
    
    if (tokenData) {
        if (Date.now() >= tokenData.expires_on * 1000) { // проверяем не истек ли срок жизни токена
            try {
                const newToken = await refreshToken(tokenData.refresh_token); // если истек, то обновляем токен с помощью refresh_token
                saveToken(newToken);
            } catch () { // если тут что-то пошло не так, то перенаправляем пользователя на страницу авторизации
               return  window.location.replace(loginUrl);
            }
        }

        options.headers.Authorization = `Bearer ${tokenData.token}`; // добавляем токен в headers запроса
    }

    return fetch(url, options); // возвращаем изначальную функцию, но уже с валидным токеном в headers
}

С помощью кода выше мы создали функцию, которая будет добавлять токен к запросам в api. На эту функцию мы можем заменить fetch в нужных нам запросах, где требуется авторизация и для этого нам не потребуется менять синтаксис или добавлять в аргументы еще какие-либо данные.
Просто достаточно будет «импортнуть» ее в файл и заменить на нее стандартный fetch.

import fetchWithAuth from './api';

function getData() {
    return fetchWithAuth('api/data', options)
}

Комментарии (53)


  1. 1c80
    15.06.2019 16:45

    Спасибо за пример, в принципе если не охота со всякими либами морочиться, то вроде как вполне рабочая тема в статье.


  1. alexesDev
    15.06.2019 16:54

    Токены стоит сохранять только в HttpOnly cookie.


    1. Flaminestone
      15.06.2019 18:25

      Почему же? Разве хранение токена в local storage + защита от CSRF не является безопасным вариантом?


      1. alexesDev
        15.06.2019 18:45
        +1

        Извините, тут не выйдет использовать HttpOnly куки.
        Но я про то, что любое расширение может читать обычные куки и localstorage. В xss внедренный код тоже.
        CSRF спасает от выполнение со стороннего сайта, а не от кражи куки.


      1. Taraflex
        15.06.2019 21:42
        +1

        Какая еще защита от CSRF, если уже используется нестандартный заголовок Authorization? Зачем ??? Он уже сам по себе защита.


    1. apapacy
      15.06.2019 18:47

      Я согласен что хранить токены в сторах небезопасно. Но их основное применение это ajax запросы. Что же делать? Я например храню в куки и потом получаю с сервера запросом который эту куклу мне расшифровует. Не знаю насколько это нормально.


    1. Taraflex
      15.06.2019 21:40

      Токены стоит сохранять только в HttpOnly cookie.

      Это шутка такая? Чем он тогда будет отличаться от обычного идентификатора сессии?

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


      1. apapacy
        16.06.2019 12:56

        Токен токену рознь. Я общался с одним программистом который свою речь строит на комбинации слов токен и кейс. Такая вот двоичная арифметика. Поэтому я рассмотрю конкретно jwt. Jwt ниразу не сессия т.к. в общем случае выдается отдельным сервером авторизации и является неизменяемым. Поэтому jwt не является сессией. Насколько jwt более секьюрный чем традиционная сессия. Секьюрно от его лежит немного в другой плоскости. Его должен выдавать хорошо защищённый сервер основная функция которого выдавать токены. Топ есть не получится найти очередную лазейку в коде очередной cms чтобы получить доступ к сессии. А в остальном, особенно если тот же jwt выдает не специализированный сервис а то же самое веб приложение то конечно по секьюрности все один к одному.

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

        Но согласен с Вами в той части что я часто встречаю реализацию jwt которая не использукт ни возможность секьюрности ни возможность масштабирования. То есть токены выдает тот же сервер на котором работает api и в токена хранится только id клиента по которому каждый раз запрашивается база данных.

        Это уже чистыймаркетинг


        1. VolCh
          16.06.2019 15:45

          Как минимум даже такое испольщование jwt это готовность к масштабированию без изменения фронта.


        1. PaulMaly
          17.06.2019 09:31

          А в остальном, особенно если тот же jwt выдает не специализированный сервис а то же самое веб приложение то конечно по секьюрности все один к одному.

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


          1. apapacy
            17.06.2019 12:53

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


            1. PaulMaly
              17.06.2019 14:00
              +1

              Если бы плагином браузера))) Вот год назад где-то я веселился по мотивам этой статьи.



  1. voodoo144
    15.06.2019 17:04

    Почему бы не закодировать все сразу в одном токене? Посмотрите как работает jwt, в нем все реализовано довольно логично и понятно.


    1. Pydeg
      15.06.2019 17:49
      +1

      JWT не отменяет refresh токен, это совсем про другое.

      Почему бы не закодировать все сразу в одном токене?
      Why Does OAuth v2 Have Both Access and Refresh Tokens?


      1. PaulMaly
        16.06.2019 17:51

        По идее отменяет. Jwt вроде не рефрешат с помощью рефреш токена


        1. lair
          16.06.2019 18:01

          Ни в OIDC, ни в OAuth 2 нет ограничений на то, что лежит в access token, так что там может быть и JWT (и, например, IdentityServer так и делает, если поставить "значимые токены").


          1. PaulMaly
            16.06.2019 19:14

            JWT был создан для stateless серверов, как более-менее доверенное хранилище информацию о пользователе/запросе. Иными словами суть использовани JWT — если сервер смог его расшифровать, то дополнительно валидировать его не надо. Также не надо лазить в базу проверять просрочен он или нет. Иначе использование JWT также теряет смысл.


            1. lair
              16.06.2019 19:19

              Во-первых, то, что вы описали, не противоречит использованию refresh-токенов. Во-вторых, эта схема подразумевает, что у вас нет отзывов… что не всегда так.


              1. PaulMaly
                17.06.2019 09:19

                Я писал чисто про JWT, как про криптографию для доверенного хранения данных на клиенте между запросами. Он особо не говорит как именно должен проходить процесс авторизации. С другой стороны, а как вы его будете отзывать? Можете рассказать подробнее? Да и какой смысл в JWT, если при каждом запросе все равно надо сверяться с базой?


                1. apapacy
                  17.06.2019 12:48

                  Конечно отзыв JWT токена не прописан в стандарте. Но здесь схема лежит на поверхности. JWT токен как я уже говорил имеет смысл только при его постоянной ротации. И все же иногда для особо критических случаев важно мгновенно этот токен аннулировать. Ничего не поделаешь, придется составить «черный» список идентификаторов клиентов и моментов времени когда поменялись его учетные данные. Хранить запись в этом черном списке нужно только на время действия токена. То есть это список будет содержать не так уж много данных и его можно хранить в каком-то быстром хранилище в зависимости от потребностей — в кассандре или в редисе.


                  1. PaulMaly
                    17.06.2019 14:04

                    Ну то есть, получается для каждого авторизованного запроса нужно будет слазить таки в какое-то «быстрое» хранилище и весь наш заявленный stateless тут же исчезает. Получается JWT не решает проблему сессий и вообще ни чем не лучше. Сессию тоже можно положить в «быстрое» хранилище и ратировать чаще даже чем JWT. И без всякой крипты.

                    На самом деле, так как вы написали делать конечно не надо. Если мы «доверяем» JWT настолько, что готовы не лазить в хранилища и пользоваться всеми благами stateless серверов, то чтобы анулировать JWT достаточно в него же положить expired дату. После расшифровки ее прочитать и не пускать дальше. Ну а получить новый JWT юзер должен уметь в любой вообще момент времени, даже если его текущий токен не просрочился. И конечно же хранить сам JWT на серверах нельзя, stateless же.


                    1. apapacy
                      17.06.2019 16:25

                      Обратите внимание, что я написал об том что «в особо ответственных случаях» если критично что в течение продолжительности жизни (пусть это будет 5 минут) клиента нужно разлогинить. Например он за это время может снять деньги со счета который заблокирован или админ сайта хакнуть сайт после несправедливого с его точки зрения увольнения с работы. То да черный список должен быть. Ходить за ним в базу данных специально нет необходимости т.к. есть средства асинхронного обмена. Но в большинстве случаев этого совершенно справедливо делать не нужно.

                      Что касается реальных приложений я действительно пока еще не юзал токены со сроком действия (веренее юзал для временных токенов) А клиенту после логина сообщал клал в токен только его идентификатор. После чего чтобы не лазить в базу хранил юзера в редисе со сроком действия и перезапрашивал в базе по истечению срока. В основном это было сделано исключительно из за того что согласовать с мобильными разработчиками рефреш токена было достаточно неперспектовной идеей. Т.к. если бы я их даже уговорил, добиться беспроблемной ротации при отстутсвии хорошо отлаженной билиотек, которая делала бы примерно то о чем эта статья сверху — было бы очень долго и не факт что не было бы сбоев у клиентов.


                1. lair
                  17.06.2019 19:00
                  +1

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

                  Вот именно поэтому утверждение "Jwt вроде не рефрешат с помощью рефреш токена" и ошибочно.


                  С другой стороны, а как вы его будете отзывать?

                  Как обычно — держать список валидных, и проверять в нем.


                  Да и какой смысл в JWT, если при каждом запросе все равно надо сверяться с базой?

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


        1. apapacy
          16.06.2019 18:10

          Сппцификация jwt просто о другом. Она не определяет протокол получения и использования токена. Она содержит его статическое что ли определение как документа а не ка процедуры его. Издания и т.п. с точки зрения бэкэнда к нему приходит некоторый jwt. После этого проверяется его подпись, время создания и если все нормально он принимается в работу. В связии с этим возникает вопрос о том что если давать бессрочный jwt то мы лишается возможности Отозвать такой токен. Следовательно нужен сравнительно короткий срок его действия. Если у токена короткий срок действия то у клиента должен быть путь для обновления токена. Конечно совсем не обязательно что это рефреш токен. Может быть сертификат или ещё что то что будет использоваться клиентом в автоматическом режиме без участия пользователя. Как один из вариантов рефреш токен. То есть резюмируя jwt подразумевает постоянную ротацию токенов. Одним из вариантов такой ротации организуется при помощи рефреш токена.


  1. lair
    15.06.2019 18:21
    -1

    Судя по характерным признакам, речь идет про OAuth 2. Речь именно о нем, или о каком-то своем велосипеде?


    1. apapacy
      15.06.2019 18:56

      Доступ с двумя токенами это совсем не обязательно oauth2 если просто клиенту отдать после какого то подтвкрждения два токена например jwt то мы имеем два положительных результата
      1 я могу не ходить в базу данных за учётной записью пользователя т.к. в токена пришла актуальная информация которая отстаёт максимум на время действия токена
      2 многие утверждают что это более безопасно хотя я не уверен. Во всяком случае выглядит более секьюрно


      1. lair
        15.06.2019 19:06
        -2

        Доступ с двумя токенами это совсем не обязательно oauth2

        Я знаю, что это не обязательно oauth, поэтому и решил сначала уточнить, что же это.


        если просто клиенту отдать после какого то подтвкрждения два токена например jwt то мы имеем два положительных результата

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


        1. apapacy
          15.06.2019 20:35

          По поводу велосипеда. На стороне сервера все уже готово в технологии jwt. Теперь есть вопрос к клиентам. Насколко я понимаю общепринятого клиента для ротации тоаенов пока нет. Поэтому я собственно использую их только в том случае если пишу сам и бэк и фронт. Иначе будут одни траблы.


          1. lair
            15.06.2019 20:50
            +1

            На стороне сервера все уже готово в технологии jwt.

            Не, совсем не все. В "технологии JWT" описано, что делать в случае кражи токена, или это все-таки просто "a compact, URL-safe means of representing claims to be transferred between two parties"? А то там написано, знаете ли, что "The contents of a JWT cannot be relied upon in a trust decision unless its contents have been cryptographically secured and bound to the context necessary for the trust decision."


            Насколко я понимаю общепринятого клиента для ротации тоаенов пока нет.

            Если речь об OAuth 2, то клиентская часть там стандартизирована, и я встречал ее имплементации в виде промежуточных хэндлеров. Другое дело, что всегда есть вопрос "что делать, если рефреш тоже протух".


            1. apapacy
              15.06.2019 23:12

              если рефреш протух то это как бы заново авторизоваться.


              1. lair
                15.06.2019 23:14

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


            1. PaulMaly
              16.06.2019 17:55

              А как он может протухнуть, если он не экспайрится? AcesssToken экспайрится по времени, RefreshToken одноразовый просто


              1. lair
                16.06.2019 18:00

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


                (в-четвертых он не обязан быть одноразовым)


                1. PaulMaly
                  16.06.2019 19:09

                  Во-первых, он прекрасно экспайрится, он не бессрочный.

                  С чего вы это взяли? Тогда он практически теряет свой смысл

                  Во-вторых, он может быть отозван.

                  Это конечно. Более того, он «отзывается» при каждом использовании. Поэтому без реальных реквизитов надолго получить доступ к юзеру не получится.

                  В-третьих, он может быть кем-то использован, и, как следствие, тоже перестать быть валидным.

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

                  (в-четвертых он не обязан быть одноразовым)

                  Я вас ни к чему не обязываю вообще. Просто это наиболее рабочая схема.


                  1. lair
                    16.06.2019 19:18

                    С чего вы это взяли?

                    RFC 6749 (OAuth 2) 5.2 Error Response: "invalid_grant [...] refresh token is invalid, expired, revoked".


                    На практике как минимум Azure AD выдает рефреш-токены со сроком жизни дней в 90.


                    Тогда он практически теряет свой смысл

                    Нет, не теряет. Просто мы контролируем доступ в долгосрочной перспективе.


                    Более того, он «отзывается» при каждом использовании.

                    Не обязательно. "The authorization server MAY revoke the old refresh token after issuing a new refresh token to the client."


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

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


                    (это одна из причин, почему refresh token делают ограниченным по времени)


                    1. PaulMaly
                      17.06.2019 09:13

                      RFC 6749 (OAuth 2) 5.2 Error Response: «invalid_grant [...] refresh token is invalid, expired, revoked».

                      И что тут написано? Что так можно делать, да. Я не говорил что так нельзя делать. Я говорил не нужно.

                      На практике как минимум Azure AD выдает рефреш-токены со сроком жизни дней в 90.

                      Для вас MS — это образец как нужно? Это не тот ли Azure на котором еще 4 года назад было практически невозможно поднять linux-сервер?

                      Нет, не теряет. Просто мы контролируем доступ в долгосрочной перспективе.

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

                      Не обязательно. «The authorization server MAY revoke the old refresh token after issuing a new refresh token to the client.»

                      Вы воспринимаете то, что я говорю слишком мандаторно. Я говорю как лучше делать, а не то, что сделать по-другому невозможно. Мы с вами даже переписываемся в комментариях к статье со своим велосипедом. Сделать можно что хочешь, вопрос в необходимости.

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

                      Да, приложение потеряет доступ и будет показывать форму логина. Юзер введет логин, получит новые токены, а злоумышленник останется с носом. Именно то, что рефреш одноразовый делает очевидным факт его кражи. Если кто-то украл его и воспользовался, юзера сразу «выкинет» из системы, а не в тихую будет продолжать работать.


                      1. lair
                        17.06.2019 18:57
                        +1

                        И что тут написано? Что так можно делать, да. Я не говорил что так нельзя делать. Я говорил не нужно.

                        Вы спросили, с чего я это взял. Вот с этого и взял.


                        Для вас MS — это образец как нужно?

                        Для меня это образец, как есть, и к чему надо быть готовым разработчикам клиентской части.


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

                        Как уже говорилось, на это есть разные взгляды.


                        Вы воспринимаете то, что я говорю слишком мандаторно. Я говорю как лучше делать, а не то, что сделать по-другому невозможно.

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


                        Юзер введет логин, получит новые токены, а злоумышленник останется с носом.

                        Для этого вам надо при логине пользователя инвалидировать все ранее выданные ему токены. Это точно удобное и желаемое поведение?


                      1. apapacy
                        17.06.2019 22:13

                        Да, кстати интересный вопрос. Как технически отзывать рефреш токену после использования. Если с акцес токенами все более или менее ванильно. Тк 1 отзываться они будут сравнительно редко и 2 хранить их в черном списке нужно на время действия. То с рефреш токенами все сложнее. Во-первых отзываться они будут все и хранить черный список нужно будет долго. Второй вариант который мне недавно озвучили это хранить таблицу идентификатор клиента плюс идентификатор валидного токена. Но тут мы приходим к тому что на каждом запросе обращаться в базу данных.

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


                        1. lair
                          17.06.2019 22:41

                          Но тут мы приходим к тому что на каждом запросе обращаться в базу данных.

                          Вообще-то, для рефрешей это практически данность, потому что там надо очень желательно проверять валидность самого клиента, а это client credentials, и запрос, что клиент не заблокирован (и еще много интересного).


                  1. apapacy
                    16.06.2019 19:29

                    По поводу одноразового использования рефреш-токена все не так просто как хотелось бы. Я как раз на днях обсуждал эту тему. Если рисовать на бумаге то все выглядит просто и красиво. В реальной асинхронной и не надежной сетевой среде все получается очень сложно. Я перечислю только первые пришедшие в голову ситуации
                    1. Токен был запрошен с клиента и аннулирван сервером после чего запрос клиенту был отправлена новая пара токенов. Запрос до клиент ане дошел из-за сетевых проблем и теперь у клиента есть просроченный акцес с токен, аннулированный рефреш токен и нет возможности их обновить. Никакой
                    2. То же самое но теперь из-за медленной работы сети клиент после таймаута сделал дублирующий запрос (это например так устроен клиент REST на Андроиде) Первый из запросов клиент сбросил а второй запрос заканчивается с ошибкой т.к. идет повторное обращение с одним и тем же рефреш токеном.
                    3. Просто два конкурирующие запроса которые вычислили что нужно обновить токены и все отправили запросы — в результате токен обновился один первый а все остальные завершились с отказом и с ошибкой


                    1. PaulMaly
                      17.06.2019 09:15

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


                      1. apapacy
                        17.06.2019 12:33

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

                        Когда говорится о защищенности рефреш токена то имеется в виду то что Вы его не присылаете на сервер приложения а обмениваетесь им по гарантированно защищенному каналу с сервером токенов. Прямая аналогия это то что вы не присылаете в интернет-магазин номер своей платежной карточки. Если его кроме всего прочего не сохранять в сторах которые могут быть скомпрометированы или в глобальных переменных которые могут быть прочитаны то украсть его модно только вместе с устройством.


                        1. PaulMaly
                          17.06.2019 14:12

                          Про «пару раз в день» сказать не могу. Нет статистики на руках, хотя все же думаю, что не все так печально. Как миниму пару раз в день не получится, если aceessToken будет жить сутки, к примеру.


    1. dima_bur Автор
      16.06.2019 12:27

      Идея была показать общий способ реализации авторизации на фронтенде, когда сервер отдает ответ вида: token и refresh token.


      1. lair
        16.06.2019 12:31

        Гм. Это OAuth 2 Resource Server, или это какая-то своя авторизация?


        1. dima_bur Автор
          16.06.2019 12:36

          Когда писал статью, да, основывался на OAuth


          1. lair
            16.06.2019 13:58

            Если это OAuth 2, то есть несколько нюансов.


            Во-первых, обновление токена может требовать (и часто требует) client credentials, которые у вас не фигурируют.


            Во-вторых, в браузере намного чаще используется Implicit Grant, в котором refresh token не выдают (в частности, потому, что в браузере сложно выполнить требование "Refresh tokens MUST be kept confidential in transit and storage, and shared only among the authorization server and the client to whom the refresh tokens were issued.")


  1. klesh
    16.06.2019 08:50
    +1

    В этом способе есть минус — если вы сделаете несколько обращений к API подряд — то получите сразу несколько запросов на refresh.
    Логичней будет ввести метод getToken, унести в него всю логику работы с токеном и использовать его в fetchWithAuth.
    Если токен есть и он не expired — getToken вернёт уже resolved промис с токеном, а если токен надо обновлять — вернёт промис на refresh. Этот промис надо сохранить и возвращать его всегда если он в статусе pending — таким образом любое количество API-запросов инициирует только один запрос на refresh.
    Только нативные промисы не дают узнать свой статус. Поэтому или колхозить некий свой статус промиса, или использовать drop-in альтернативы типа bluebird.


    1. dima_bur Автор
      16.06.2019 12:46

      Спасибо за замечание, согласен, этот момент можно учесть и улучшить способ.


  1. noodles
    16.06.2019 13:36

    Функция для обновления токена
    const newToken = await refreshToken(tokenData.refresh_token); // если истек, то обновляем токен с помощью refresh_token
    saveToken(newToken);


    По-моему кроме обновления самого токена, ваша функция должна ТАКЖЕ обновлять и refresh-token.

    if (Date.now() >= tokenData.expires_on * 1000) { // проверяем не истек ли срок жизни токена

    Разве можно полагаться на Date.now()? Если «хакер» подменит время в системе?

    Хранить данные для авторизации мы будем в sessionStorage или localStorage, в зависимости от наших нужд. В первом случае данные хранятся до тех пор, пока пользователь не завершит сеанс или не закроет браузер

    Какой смысл хранить токены в sessionStorage? Почему не в самом клиентском коде? Раз уж он будет доступен только на время жизни клиентского приложения.


    1. dima_bur Автор
      16.06.2019 13:55

      По-моему кроме обновления самого токена, ваша функция должна ТАКЖЕ обновлять и refresh-token.

      refreshToken() возвращает объект такого же формата, что и при авторизации по логину/паролю: с полями token, refresh_token и т. д.

      Какой смысл хранить токены в sessionStorage? Почему не в самом клиентском коде? Раз уж он будет доступен только на время жизни клиентского приложения.


      В статье указано, что можно хранить не только там, это зависит от наших нужд.


    1. VolCh
      16.06.2019 15:50

      Проверка на клиенте как обычно лишь способ улучшить UX и снизить трафик и нагрузку на сервер.


  1. PaulMaly
    16.06.2019 17:56

    А точно надо в сторейдж сохранять промис? Может лучше сами токену?