Релогин и HTTP Basic Auth

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

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

    Суть проблемы в том, что стандартом HTTP Basic Authorization не предусмотрена возможность разлогина. Вообще. При заходе на страницу, защищённую Basic Authorization, браузер сам выводит вам своё собственное окно с запросом на логин/пароль и при успешном логине сохраняет их где-то у себя в глубинных недрах. Затем все последующие запросы к другим защищённым страницам данного сайта автоматически использует эти значения в заголовке Authorization:
    Authorization: Basic bm9uZTpub25l
    где bm9uZTpub25l — это base64-кодированная связка логин/пароль none:none (у вас логин и пароль, возможно, будут другие)

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

    Хуже того, поскольку окно запроса логина/пароля является собственным окном браузера, реагирующее уже на одни только HTTP заголовки страницы, это окно всплывает до какой бы то ни было обработки тела страницы. То есть выполнить какой-нибудь javascript при получении ответа сервера со статусом 401 не получится. Перед пользователем снова и снова будет вылезать это окно с повторным запросом на логин/пароль.

    На самом деле этот момент критичен, так как почти для всех браузеров единственным способом разлогиниться является отправка на сервер запроса с заведомо неправильными логином/паролем, получение ответа сервера со статусом 401 и последующий сброс браузером своего логин/парольного кэша. Но если при этом браузер не даёт вам обработать ответ 401 и продолжить процесс логина уже для нового пользователя, перехватывая управление ещё на этапе чтения HTTP заголовков и выкидывания вам в лицо ту самую форму авторизации, в которую что ни вводи, работать не будет, то это уже проблема. Причём не имеет значения, обычный ли это запрос по ссылке или XMLHttpRequest или fetch. Браузер всё равно перехватывает управление на этапе разбора заголовков.

    Итак…

    Исходные данные:


    1. Есть отдельная страница logout.html, на которой в javascript скрипте находится вся логика, части которого приводятся по ходу изложения
    2. Задан url — адрес страницы для перенаправления после успешного логина
    3. Задан url401 — адрес страницы, всегда возвращающей HTTP ошибку 401 (не авторизован)

    // file logout.html
    const url = new URL("http://mysite.com/");
    const url401 = new URL("http://mysite.com/401");
    

    Internet Explorer


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

    if (document.execCommand("ClearAuthenticationCache")) {
        window.location.assign(url);
    }
    

    В IE существует метод ClearAuthenticationCache, который сбрасывает «те самые глубинные недра». Просто и элегантно. И никаких плясок со вспомогательной страницей 401. Работает ли данный метод в Edge, не знаю. Скорее всего да.

    Конструкция document.execCommand возвращает true, если метод существует и «сработал». После чего window.location.assign(url) перенаправляет пользователя для ввода новых логина и пароля

    Firefox (72.0.1)


    В контексте нашей задачи это самый проблемный браузер. Для него полноценного решения не существует до сих пор. В баг-трекере команды его разработчиков вот уже лет 15-20 висит запрос на указанную проблему. Но «воз и ныне там». Максимум чего можно добиться — это кривой разлогин.

    Вводные данные:
    Firefox не сбрасывает парольный кэш после получения ответа 401 ни через XMLHttpRequest, ни через fetch запрос. Только через обычный запрос с указанием логина/пароля в самом URL. То есть что-то вроде
    http ://none:none@mysite.com/
    Код:

    else if (/firefox|iceweasel|fxios/i.test(window.navigator.userAgent)) {
        url.username = 'none';
        url.password = 'none';
        window.location.assign(url);
    }
    

    После чего пользователь получает форму ввода логина/пароля, в которую что ни вводи, она будет выскакивать вновь и вновь. Дело в том, что введённые значения не будут переопределять значения логина/пароля, заданные в URL-е. То есть вместо введённых значений на сервер всякий раз будет уходить связка none:none. И чтобы перелогиниться под другим именем, пользователь должен нажать отмену, перейти на стартовую страницу (http ://mysite.com/) и уже там ввести новые логин/пароль.

    Криво? Но увы, другого решения нет

    Google Chrome (79.0.3945.88)


    Для Хрома замечательно работает метод fetch. А вот XMLHttpRequest не работает ((( Кэш не сбрасывается, и разлогина не происходит. Причём пробовал логин/пароль задавать и как параметрами к методу open, так и установкой заголовка.

    else if (/chrome/i.test(window.navigator.userAgent)) {
        fetch(url401, {
          credentials: 'include',
          headers: {
            'Authorization': 'Basic ' + btoa('none:none'),
            'X-Requested-With': 'XMLHttpRequest'
          }
        }).then(() => {
          window.location.assign(url);
        }).catch(err => console.error(err));
    }
    

    Делаем fetch запрос на страницу 401 с заведомо неверными логином/паролем, получаем ответ 401 от сервера, браузер сбрасывает свой кэш.

    ВАЖНО! Сервер НЕ должен возвращать заголовок WWW-Authenticate. Иначе браузер перехватит управление, и перенаправления со страницы 401 не произойдёт никогда. По общепринятому соглашению сервер не должен возвращать этот заголовок, если в запросе указано X-Requested-With: XMLHttpRequest. Поэтому в запрос добавлен заголовок X-Requested-With.

    Safari (12)


    Для Сафари ситуация в точности до наоборот: работает XMLHttpRequest, но не работает fetch

    else {
        const xhr = new XMLHttpRequest();
        xhr.open("GET", url401, true, 'none', 'none');
        xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
        xhr.onerror = function(err) {
          console.error(err);
        };
        xhr.onload = function () {
          window.location.assign(url);
        };
        xhr.send()
    }
    

    Действия те же, что и в Хроме, только через XMLHttpRequest

    Должно работать для версий Сафари 6+. В более ранних версиях были свои баги. В частности, например, в версиях от 5.1 при каждом перенаправлении браузер принудительно перезапрашивал авторизацию, из-за чего авторизация с перенаправлением на конечную страницу не работала в принципе. А в версиях до 5.0 не работал разлогин.

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

      +1

      Я в таких ситуациях (Basic, NTLM etc...) всегда решал данную проблему на сервере — по API токен помечал как отозванный. В сторону браузера даже не смотрел — спасибо за интересную статью. Но, наверное, продолжу решать данную проблему на бэкенде.

        0

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

        0

        Тоже недавно столкнулся.
        Поменяли пароль для basic, хром в xhr запросах
        в url упорноо подставляет старый пароль, не смотря на то, что на сайт уже вошёл с новым через ввод в форму браузера.
        Помогает только вручную вписать логин/пароль в url — только тогда кеш обновляется, и в xhr начинает уходить новый пароль.
        Странно что нативная форма basic-авторизации не сбрасывает кеш — забыли добавить событие в хроме.
        Нативная форма пишет хэш в заголовки, а кеш реагирует только на url.

          –1
          Минздрав СССР предупреждает Специалисты по ИБ предупреждают: использование Basic Auth не защищает пароль от MITM
            0

            даже в случае https?

              –1
              Ну так в случае HTTPS защиту обеспечивает SSL/TLS, а не Basic Auth
                0

                и что? нам так важно кто именно обепспечивает защиту? или то, что защита обеспечена?
                что же до HTTP — MITM тут может наделать кучу бед и без раскрытия пароля.

                  –1
                  Посчитал своим долгом предупредить. Кто виноват и что делать, каждый решает сам, пользуясь своими интеллектуальными ресурсами самостоятельно
              0

              В чистом виде её обычно никто и не использует

                0
                Не все руководствуются разумными принципами, к сожалению
                  0

                  Значит им настоящая безопасность по большому счёту не нужна. Те, кому безопасность действительно нужна, как минимум читают документацию. Много документации)

                    0
                    Для части их них можно надеяться, что они образумятся и перейдут на светлую сторону
              0
              можно сделать basic-авторизацию через форму и тогда легко разлогинивать
                –1
                В пространстве терминов HTTP нет термина «форма», ЕМНИП.
                А HTTP используется не только для передачи HTML
                  0

                  Ваше решение под капотом использует куки. Авторизоваться через куки можно и без использования basic-авторизации. Здесь речь о ситуациях, когда ни куки, ни oauth, ни что-то ещё подобное не доступно.

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

                Самое читаемое