Tutorial: OpenID Connect

Аутентификация на сервере UB с помощью OpenID Connect

В версии 1.11 добавлен механизм аутентификации с ипользованием третьей удостоверяющей стороны по протоколу OpenID Connect.

В данной схеме аутентификация (проверка имени пользователя и пароля) производится провайдером OpenID Connect (например, Google), а последующая авторизация - с использованием протокола авторизации UB

Регистрация на стороне провайдере OpenID Connect

В соответствии с руководством вашего OpenID Connect провайдера необходимо зарегистрировать приложение. Для регистрации в Google инструкция по этой ссылке

При регистрации необходимо сообщить провайдеру адрес на который с него позволено редиректиться (не для всех провайдеров), и получить:

  • url авторизации (authUrl)
  • url получения токена (tokenUrl)
  • url получения информации о пользователе (userInfoUrl)
  • id нашего приложения (client_id)
  • secret нашего приложения (client_secret)

Настройка сервера UB

Изменения в конфигурационном файле

Для использование в AdminUI в секции authMethods добавляем значение "OpenIDConnect"

...
"authMethods": [...,"OpenIDConnect"...],
...

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

В модели прикладной области

// Подключаем модуль 'openIDConnect'
var openIDConnect = require('openIDConnect');
// Регистрируем точку доступа
var openIDConnectEndPoint = openIDConnect.registerEndpoint(<EndPoint name>);

<EndPoint name> - имя точки доступа. Для AdminUI нужно использовать имя "openIDConnect"

// Регистрируем провайдер
openIDConnectEndPoint.registerProvider(<Provider name>,{
    authUrl: <authUrl>,
    tokenUrl: <tokenUrl>,
    userInfoUrl: <userInfoUrl>,
    client_id: <client_id>,
    client_secret: <client_secret>,
    userInfoHTTPMethod: <userInfoHTTPMethod>,
    scope: <scope>,
    nonce: <nonce>,
    response_type: <response_type>,
    getUserID: function(userInfo) {
        ...
        return id
        ...
        return null
        ...
    },
    onFinish: <onFinish>
});

Пояснения и пример для авторизации при помощи Google:

  • <Provider name> - имя провайдера. Будет отображаться в списках доступный провайдеров данной точки доступа. Например, 'Google'

  • <authUrl> - url авторизации, полученный от провайдера. Например, 'https://accounts.google.com/o/oauth2/auth'

  • <tokenUrl> - url получения токена, полученный от провайдера. Например, 'https://accounts.google.com/o/oauth2/token'

  • <userInfoUrl> - url получения информации о пользователе, полученный от провайдера. Например, 'https://www.googleapis.com/oauth2/v1/userinfo'

  • <client_id> - id нашего приложения, полученный от провайдера. Например, '350085411136-lpj0qvr87ce0r0ae0a3imcm25joj2t2o.apps.googleusercontent.com' (для зарегистрированного мной тестового приложения)

  • <client_secret> - secret нашего приложения, полученный от провайдера. Например, 'dF4qmUxhHoBAj-E1R8YZUCqA' (для зарегистрированного мной тестового приложения)

  • <userInfoHTTPMethod> - HTTP метод отправки параметров для получения информации о пользователе. Зависит от провайдера. Если указан не 'POST', то используется 'GET'(по умолчанию). Например, Google требует чтобы параметры для получения информации о пользователе передавались именно 'GET'

  • <scope> - к каким api провайдера запрашиваем доступ(разделяем при помощи '+'). Обычно достаточно 'openid', но некоторые провайдеры могут требовать больший список. Или же нам для авторизации пользователя надо получить больше информации. Например, 'openid'

  • <nonce> - nonce, передаваемый провайдеру. В модуле его значение пока не проверяется. Параметр для модуля необязательный. Некоторые провайдеры требуют этот параметр как обязательный. Например, '1'

  • <response_type> - типы ответа провайдера(разделяем при помощи '+'). Для модуля важно чтобы среди типов ответа был 'code' - именно его модуль использует. Некоторые провайдеры могут требовать дополнительные типы ответов. Например, 'code'

  • <onFinish> - метод клиента, который мы вызываем после проверки на право доступа пользователя. Значение, используемое AdminUI - '(function (response) { opener.postMessage(response, "")})'*

  • getUserID - функция, на вход которой, передается ответ сервера на запрос информации о пользователе. Функция должна проверить имеет ли пользователь с такой информацией доступ к системе, возможно, создать нового пользователя, назначить ему права, и вернуть его id из сущности _ubauser. Если пользователь не имеет права на доступ к приложению функция должна вернуть null. Пример функции:

     getUserID: function(userInfo) {
         var inst = UB.Repository('uba_user').attrs(['ID'])
             .where('[name]', '=', userInfo.id).select();
         if (inst.eof)
             return null;
         else
             return inst.get('ID');
     }

Принцип работы

После добавления точки доступа на сервере регистрируется метод уровня приложения с именем <EndPoint name>.

GET запрос /<EndPoint name> без параметров вернет масив имен зарегистрированный провайдеров для данной точки доступа.

Дальше рекомендуется открыть в новом окне(window.open) адрес /<EndPoint name>/<Provider name> Сервер переадресует данное окно на форму ввода логина и пароля провайдера, то есть на authUrl (1-a).

Пользователь аутентифицируется, после чего провайдер переправляет его на адрес /<EndPoint name>/<Provider name>?code=...scope=... (1-b).

Сервер UB обращается к провайдеру, и выполняет обмен 'code' на 'acces_token'(2), при помощи которого запрашивает информацию о пользователе у провайдера(3). Полученная информация о пользователе передается функции getUserID, указанной при регистрации провайдера на сервере UB.

Задача ф-ии getUserID по полученной от провайдера информации о пользователе вернуть иденитификатор пользователя из сущности _ubauser, либо null если польщователю вход запрещён. На данном этапе возможно автоматическое создание пользователя.

  • Если ф-я вернула непустое значение, то сервер создает сессию для пользователя и возвращает html-страницу, которая вызывает метод <onFinish> с параметрами успешной авторизации и закрывает окно ввода логина/пароля провайдера.
  • Если ф-я вернула пусто, то возвращаемая html-страница вызывает метод <onFinish> с признаком неуспешной аутентификации {success: false}

Пример параметров при успешной аутентификации:

{
    success: true,
    data: {
        logonname: "<user login>"
        result: "<session word>"
        uData: "{<uData>}"
        },
    secretWord: "<secretWord>"
}

На данном этапе если клиент загружен не по протоколу HTTPS, некоторые браузеры могут ругать пользователя.

Реботающий пример для провайдера Google и IdentityServer3 реализован в приложении Autotest (see Autotest/models/TST/appLevelMethod.js)

Использоваие в PortalUI

AdminUI реализует механизм поддержки множества провайдеров и методов аутентификации, динамически отстраивая форму логина в зависимости от параметров приложения. В портальных решениях все гораздо проще. Допустим нам известно, что на серверной части зарегистрирован ендпоинт openIDConnect и провайдер IdentityServer. Наш портал для приложения autotest будет приблизительно таким:

<!doctype html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>«UB portal»</title>

        <script charset="utf-8" src="/autotest/compiled/ub-core.js"></script>
    </head>
    <body >
        <h1>User list</h1>
        <div id="UBData"></div>
        <script type="text/javascript">
            var conn = new UBConnection({
                host: window.location.origin,
                appName: 'autotest/',
                requestAuthParams: function(conn, isRepeat){
                    var deferred = Q.defer();
                    var url = window.location.origin + '/autotest/openIDConnect/IdentityServer';
                    function getWindowConfig() {

                        var width = 600,
                            height = 525,
                            left = Math.floor(window.screenX + (window.outerWidth - width) / 2),
                            top = Math.floor(window.screenY + (window.outerHeight - height) / 2);

                        return "width=" + width + ",height=" + height + 
                               ",left=" + left + ",top=" + top + 
                               ",toolbar=0,scrollbars=1,status=1,resizable=1,location=1,menuBar=0";
                    }                     
                    var loginWindowOpenID = window.open(url, 'login', getWindowConfig());

                    function loginListener(event) {
                        if (event.source === loginWindowOpenID) {
                            window.removeEventListener("message", loginListener);
                            if (event.origin.indexOf(window.location.origin) === 0) {
                                var response = event.data;
                                if (response.success) {
                                    response.authSchema = 'OpenIDConnect';
                                    deferred.resolve(response);
                                } else {
                                    deferred.reject('authOpenIDConnectFail');
                                }
                            } else {
                                deferred.reject('authOpenIDConnectFail');
                            }
                        }
                    }
                    window.addEventListener("message", loginListener);

                    return deferred.promise;
                }
            });
            conn.run({entity: 'uba_user', method: 'select', fieldList: ['ID', 'name']}).done(function(result){
                var htmlTpl = '<table cellspacing = "0" border ="1">'+
                              '<tr><td style = "text-align: center; padding: 2px;"><b>id</b></td>'+
                              '<td style = "text-align: center; padding: 2px;"><b>login</b></td></tr>'+
                              '<% _.forEach(resultData.data, function(user){%>'+
                              '<tr><td style = "padding: 2px;"><%- user[0] %></td>'+
                              '<td style = "padding: 2px;"><%- user[1] %></td></tr><%}); %>'+
                              '</table>';
                document.getElementById("UBData").innerHTML = _.template(htmlTpl)(result);
            });
        </script>
    </body>
</html>

Profit!