Сегодня расскажу о небольшом лайфхаке OAuth авторизации в SPA, а именно в Angular приложении. Многие, наверняка, сталкивались с авторизацией через социальные сети, и все хорошо представляют этот процесс: отправляем GET запрос и делаем редирект к OAuth серверу с необходимыми параметрами (id приложения, скопами, и главное с URL редиректа); в случае не авторизованного пользователя сервер отвечает html’ем с вводом креденшалов пользователя; после успешной авторизации OAuth сервер переходит на наше приложение, т.е. на заданный нами URL; и все, далее пользователь, будучи авторизованным, начинает пользовать наше приложение. Как бы тут все просто. Но…

Бывают случаи, когда очень нежелательно уходить с приложения:

  • Пользователь не захочет вводить логин\пароль и просто уйдет, закрыв вкладку
  • Пользователь, вместо ввода авторизации в социальной сети, нажмет назад и его вернет, допустим, на начальный шаг какой-то процедуры, т.к. это может находиться на одном пути (route), SPA же.
  • Обновление токена в один клик, без перезагрузки приложения И тд…

Очевидно, что без всплывающих окон тут не обойтись. Но вопрос, куда же направлять редирект. «Может быть прописать отдельный путь для редиректа, добавить компонент, который обработает коллбек и синхронизирует с основным.» Но нет, это драгоценные секунды загрузки и уж очень костыльно. Но не надо отчаиваться, сейчас расскажу свой менее болезненный и уже проверенный временем способ. Нужно всего лишь создать html-файл в корне приложения, например, oauth-callback.html. И делать редирект именно на этот файл https://{my-domain}/oauth-callback.html

Вы можете сказать, мол «как бы ok, и что, а как в родительском окне получить токен?». Здесь тоже все просто, из родительского окна мы имеем доступ к объекту window дочернего окна. «То есть мы можем сделать setInterval и проверять какие данные, пользователь вводит данные или уже ввел, и мы имеем callback, а потом по window.location получить токен?» Ну… Нет. Добавим в oauth-callback.html следующие строки:

<!DOCTYPE html>
<html>
<head>  
    <script>
        var oAuthCallbackEvent = new CustomEvent("OAuthCallback");
        window.opener.dispatchEvent(oAuthCallbackEvent);
        window.close();
    </script>
</head>
<body>
</body>
</html>

Создадим пользовательское событие, которое будем слушать в родительском окне, и которое будет означать успешную авторизацию в социальной сети и то, что был возвращен callback.

Кстати, создается всплывающим окном, примерно, следующим способом:

let width = 860;
let height = 500;
let left = (screen.width / 2) - (width / 2);
let top = (screen.height / 2) - (height / 2);
let windowOptions = `menubar=no,location=no,resizable=no,scrollbars=no,status=no, width=${width}, height=${height}, top=${top}, left=${left}`;
let type = 'auth';

window.open(url, type, windowOptions);

А в компонент, где создается окно авторизации, добавим слушатель через Renderer2:

this._renderer.listen('window', 'OAuthCallback',
    (evt) => {
        this._globalOAuthCallbackListenFn();
	// тут сохраняем наш токен или производим другие действия после успешной авторизации
    });

Добавим в событие деталей, для передачи токена.

var oAuthCallbackEvent = new CustomEvent("OAuthCallback", {'detail': window.location.search});

Теперь мы можем получить данные через эвент: evt.detail.

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

Update

Были изменения в сторону поддержки старых браузеров. CustomEvent поддерживается с 9ой версии IE, но наблюдались проблемы и с Edge. Можно было бы использовать простой Event, но в нашем случае необходимо передать данные. Поэтому было решено использовать postMessage.

<!DOCTYPE html>
<html>
<head>
    <script>
        (function () {
            window.opener.postMessage(window.location.search, window.opener.location.origin);
            window.close();
        })();
    </script>
</head>
<body>
</body>
</html>

Соответственно, изменяем наш компонент.

this._globalOAuthCallbackListenFn = this._renderer.listen('window', 'message',
    (evt) => {
        this._globalOAuthCallbackListenFn();
});