Медленно, но неотвратимо наступает смена решений SSO на основе SAML на решения OpenID стека. С недавних пор компания Google реализовала поддержку OpenID Connect протокола на своих серверах. Насколько он может быть приемлем для Вашего проекта и как с ним работать, оценить по спецификации протокола довольно трудно. Немного облегчить это решение должна статья одного из авторов спецификации в своём блоге, перевод которой я и предоставляю аудитории хабра. В целях упростить понимание, некоторые моменты были добавлены от себя, таким образом, чтобы не приходилось обязательно читать ссылки на используемые технологии, но ознакомится с некоторыми из них всё же я рекомендую.


Когда вы читаете спецификацию по OpenID Connect, вы можете испытывать довольно неприятные чувства от лёгкой испуганности до полнейшего разочарования. Всё это происходит потому, что они написаны на “сухом” языке спецификации, и по большей части они описывают граничные случаи, исключения и т.д. Тем не менее, когда вы переводите их на нормальный человеческий язык и переключаетесь на конкретные случаи, всё становится довольно очевидно. Итак, давайте приступим! (Ремарочка: большая часть текста совпадает с первоначальным предложением, написанным Дэвидом Рекордоном. В основном, мои правки затронули лишь некоторые из имен параметров и другие мелочи)


Создание запроса OpenID Connect


Для того, чтобы клиент мог совершить OpenID Connect запрос, он должен иметь следующую информацию о сервере:


  • Идентификатор клиента (client identifier) — уникальный идентификатор, выданный клиенту, чтобы идентифицировать себя на сервере авторизации.
  • Клиентский ключ (client secret) — общий секретный ключ, установленный между сервером авторизации и клиентом и используемый для подписи запросов.
  • Адрес (ендпоинт, но в данном контексте ендпоинт и адрес синонимичны. Дальше — адрес) пользовательской авторизации (end-user authorization endpoint) — URL адрес HTTP запроса к ресурсу сервера способного к аутентифицировать и авторизовать конечного пользователя.
  • Адрес выдачи токена (token endpoint) — ресурс на сервере авторизации обеспечивающий выдачу токенов.
  • Адрес пользовательской информации (user info endpoint) — Защищенный ресурс, который при предъявлении токена возвращает авторизованному клиенту информацию о текущем пользователе.
  • Адрес проверки идентификатора (check id endpoint) — защищенный ресурс, который при предъявлении идентификатора клиента проверяет подпись и возвращает информацию о сеансе пользователя. (Удалён 2012/3/3: он может вернуться в качестве общей OAuth точки интроспекции токена)
    Данная информация может быть получена как разработчиком клиента, прочитав документацию сервера и предварительно зарегистрировав приложение, так и путем выполнения Обнаружения (Discovery) и Динамической Регистрации (Dynamic Registration).

Построение клиентом запроса OAuth 2.0, для получения токена.


Для того чтобы превратить запрос OAuth 2.0 в запрос OpenID Connect, просто добавьте ключ OpenID в качестве одной из требуемых наборов данных (параметр scope). Установив в параметре ключ OpenID, клиент запрашивает идентификатор для пользователя, а также контекст аутентификации. Если вы хотите получить URL профиля пользователя, имя или фотографию, вы можете запросить дополнительные наборы данных (к примеру — профиль). Сервер (и пользователь) может выбирать информацию о профиле доступном клиенту. Если клиент хочет получить адрес электронной почты пользователя, он должен добавить ключ “email” в запросе. То же самое относится и к адресу (address) и телефону (phone).
Например:


GET /authorize?grant_type=token%20id_token&scope=openid%20proflie&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com

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


Получение OpenID Connect ответа


Если пользователь будет авторизован клиентским запросом, то клиент получит токен. Ответ авторизации OAuth 2.0 как правило, включает в себя два параметра: access_token и id_token. Информация в id_token закодирована и включает в себя объект JSON с такими полями:


  • aud (аудитория) — обязательное поле. Идентификатор клиента (сlient_id) для которого, этот id_token предназначен.
  • ехр (окончание) — обязательное поле. Время, после которого не может быть принят этот маркер.
  • sub — обязательное поле. Локально уникальный и никогда непереназначаемый идентификатор для пользователя (субъекта). Например, "24400320" или "AitOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4".
  • iss (эмитент) — обязательное поле. Адрес HTTPS: URI с указанием полного имени хоста эмитента, который в паре с user_id, создает глобально уникальный и никогда непереназначаемый идентификатор. Например, "https://aol.com", "https://google.com", или "https://sakimura.org".
  • nonce — обязательное поле. Установленное сервером значение отправленное в запросе.

Параметр id_token представляет простой способ, чтобы убедиться, что данные, полученные клиентом через User-Agent потоки (или других ненадежных каналов) не были изменены. Параметр подписывается сервером, используя клиентский ключ, который был ранее передан через доверенный канал. Эта кодировка называется JSON Web ТокенJWT вкратце и черновая спецификация). К примеру, вот такая строка :


eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

Она состоит из трёх частей которые отделены точками.
Первая часть — заголовок (Header), это JSON объект закодированный Base64url и описывающий алгоритм и тип токена:


{
  "alg": "HS256",
  "typ": "JWT"
}

Вторая часть — полезная нагрузка (Payload), это так-же JSON объект закодированный Base64url:


{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

Третью часть сервер получил след образом:


Алгоритм_цировой_подписи (HS256) (
  base64UrlEncode(первая часть) + "." +
  base64UrlEncode(вторая часть),
  клиентский_ключ
)

Обратите внимание, что base64url кодировка в отличии от base64 использует два других символа и не содержит отступов.
Сервер авторизации должен выдавать подтверждения об идентификаторах пользователей только в пределах своих доменов. Клиент в свою очередь должен убедиться, что aud соответствует его client_id, а iss соответствует домену (включая суб-домен) эмитента в client_id. Сервер авторизации отвечает за управление собственным локальным пространством имен и обеспечивает локальную уникальность и неповторяемость (непереназначаемость) каждого user_id.
Когда клиент сохраняет идентификатор пользователя, он должен сохранить кортеж из user_id и iss в своём локальном хранилище. Параметр user_id должен не превышать 255 ASCII символов в длину.
Что-бы удостовериться в аутентичности данных клиент может проверить подпись. Если клиент не проверяет подпись, он должен выполнить HTTP запрос к точке проверки идентификатора, чтобы проверить его. — немножко непонятно зачем ему это делать


Доступ к информации о пользователях (опционально)


Информация о пользователе является обычным OAuth 2.0 ресурсом, который возвращается вместе с токеном в виде документа в формате JSON. Клиент делает HTTPS "GET" запрос по адресу предоставления пользовательской информации и включает в себя токен в качестве параметра.
Ответ представляет собой объект JSON, который содержит некоторые (или все) из следующих зарезервированных ключей (json-объекта):


  • sub — например "AitOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4".
  • profile — URL страницы профиля конечного пользователя
  • name — отображаемое имя пользователя, например "Nat Sakimura".
  • given_name — например "Nat".
  • family_name — например "Sakimura".
  • email — например, "sakimura@example.com".
  • picture — например, "http://graph.facebook.com/sakimura/picture".

Сервер по необходимости может добавить дополнительные данные в этот ответ (к примеру такие как переносимые контакты) пока они не изменяют зарезервированные ключи OpenID Connect. (Примечание: есть более четко определенные ключи, но для краткости, я опущу их описание.)


Открытие (опционально)


При использовании OpenID Connect вполне вероятно, что клиент может иметь или кнопки для регистрации через популярные сервисы, или текстовое поле для ввода адреса электронной почты или URL. OpenID Connect напрямую не решает проблему NASCAR
(Проблема NASCAR — это отсылка на мешанину из значков брендов веб-сайтов, через которые осуществляется вход, подчёркивая схожесть страницы логина с коллажами наклеек спонсорской рекламы на трековых автомобилях в гонках NASCAR).
Цель этапов открытия и регистрации для клиента состоит в том, чтобы получить адрес сервера авторизации, конечный адрес точки выдачи токена, идентификатора клиента, секрет клиента, и получения API пользовательских данных. Если клиент предварительно зарегистрирован на сервере, то эта информация уже будет известна. В противном случае клиенту нужно будет получить их при помощи этапа открытия.
Пользователь нажимает на кнопку на клиенте, чтобы выбрать сервер. В этом случае разработчик клиента сможет выбрать предпочтительные сервера и таким образом, уже зная их адреса авторизации (и, возможно, другую информацию). Клиент может быть или не быть предварительно зарегистрированным.
В другом случае, пользователь (или User-Agent, действующим от его имени) вводит URL или адрес электронной почты. Для этого клиенту необходимо будет выполнить обнаружение и определить, является ли валидным URL-адрес сервера авторизации. Шаги:


  1. Проанализируем ввод данных пользователем, чтобы выяснить, является это адресом электронной почты или URL. Если это адрес электронной почты, ничего не делать. Если нет схемы, предположим протокол HTTPS.
  2. Восстановим идентификатор путем реконструкции различных частей.
    Например:
    https://joe.example.com -> https://joe.example.com/
    example.com -> https://example.com/
    joe@example.com -> joe@example.com
  3. Извлекаем домен и делаем WebFinger вызов через TLS/SSL.
    WebFinger служит для получения инфомации о людях или других сущностях в сети интернет при помощи стандартных HTTP методов по защищённому каналу. WebFinger возвращает JSON объект, описывающий запрашивамую сущность.
    GET /.well-known/webfinger?resource=acct%3Ajoe%40example.com&rel=http%3A%2F%2Fopenid.net%2Fspecs%2Fconnect%2F1.0%2Fissuer   HTTP/1.1
    Host: example.com
    HTTP/1.1 200 OK
    Content-Type: application/jrd+json
    {
    "subject": "acct:joe@example.com",
    "links":
        [{
        "rel": "http://openid.net/specs/connect/1.0/issuer",
        "href": "https://server.example.com"
        }]
    }
  4. Для того, чтобы получить конкретный URL, клиент добавляет "/.well-known/openid-configuration" эмитенту, и получает файл конфигурации эмитента через TLS/SSL следующим образом:
    GET /.well-known/openid-configuration HTTP/1.1
    Host: server.example.com

Ответ представляет собой объект JSON, который включает в себя конечную точку и другую информацию.
Например:


{
    "authorization_endpoint": "https://server.example.com/connect/authorize",
    "issuer" : "https://server.example.com",
    "token_endpoint": "https://server.example.com/connect/token",
    "token_endpoint_auth_types_supported":["client_secret_basic", "private_key_jwt"],
    "userinfo_endpoint": "https://server.example.com/connect/user",
    "check_id_endpoint": "https://server.example.com/connect/check_id",
    "registration_endpoint": "https://server.example.com/connect/register"
}

Незарегистрированные клиенты и динамическая регистрация (опционально)


Независимо от используемого механизма обнаружения (Discovery), клиент может быть или не быть уже зарегистрирован на сервере. Сервера могут иметь разные ограничения на то, какую информацию могут получить клиенты в зависимости от того, являются ли они предварительно зарегистрированными (что подразумевает согласие на условия предоставления услуг) или клиент использует динамическую регистрацию.
Если клиент не имеет валидный идентификатор клиента и ключ, он может сделать следующий HTTPS POST запрос на адрес регистрации сервера (см. Открытие) со перечисленными запрашиваемыми параметрами в формате JSON в теле POST запроса: redirect_uris — Массив URL адресов для получения ответов OpenID.
Например:


POST /connect/register HTTP/1.1
Content-Type: application/json
Accept: application/json
Host: server.example.com
{
"redirect_uris":
    ["https://client.example.org/callback",
    "https://client.example.org/callback2"]
}

Перед тем, как ответить на запросы, сервер должен проверить, зарегистрирован ли URL коллбек за пределами этого потока OpenID. Если да, сервер отправит информацию с реакцией на ошибку. Серверу необходимо будет разработать политику для обработки таких случаев, когда переданные redirect_uri были предварительно зарегистрированы разработчиком клиента, при запросах динамической регистрации. Такое поведение может означать, к примеру, что новые запросы динамической регистраций с этими redirect_uri приведут к ошибке, но запросы с использованием уже осуществлённой динамической регистраций продолжат работать, пока не устареют.
Для обеспечения динамической ассоциации, сервер включает в себя следующие параметры ответа JSON:


  • client_id — идентификатор клиента. Это значение может измениться с каждым ответом, на запрос серверу.
  • client_secret — ключ клиента. Он обязательно изменится с каждым ответом.
  • expires_at — количество секунд от 1970-01-01T0:0:0Z согласно UTC, пока client_id и client_secret не устареет или 0, если они не имеют срока давности.
  • registration_client_uri — uri для управления этими регистрационными данными.
  • registration_access_token — токен, который будет использоваться для доступа к registration_client_uri.

Клиенту необходимо хранить свои данные динамической регистрации для работы с токенами сервера. Для каждой динамической регистрации клиенту необходимо будет хранить идентификатор клиента, ключ клиента, время истечения, пользовательский URL, поддерживаемые потоки, а также API пользовательской информации. Время окончания срока следует хранить как абсолютное время или метку того, что регистрация будет длится вечно.
Как видите, основные процессы веб-клиента OpenID Connect достаточно просты, и так же просты, как те, которые предлагались первоначально. В то же время, могут быть использованы дополнительные функциональные возможности, например запрос конкретных наборов данных, а не набор по умолчанию. Эти дополнительные возможности доступны, когда они необходимы и не превращают простые взаимодействия в крупные проблемы для клиентов, с большим числом OpenID провайдеров.

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


  1. grossws
    13.04.2016 12:14

    Когда вы читаете спецификацию по OpenID Connect, вы можете испытывать довольно неприятные чувства от лёгкой испуганности до полнейшего разочарования. Всё это происходит потому, что они написаны на “сухом” языке спецификации, и по большей части они описывают граничные случаи, исключения и т.д. Тем не менее, когда вы переводите их на нормальный человеческий язык и переключаетесь на конкретные случаи, всё становится довольно очевидно. Итак, давайте приступим! (Ремарочка: большая часть текста совпадает с первоначальным предложением, написанным Дэвидом Рекордоном. В основном, мои правки затронули лишь некоторые из имен параметров и другие мелочи)

    Ещё потому, что приходится прочитать спеки на OAuth2 (RFC 6749, 6750). Без них читать спеку на oidc как-то сложно.


  1. grossws
    13.04.2016 13:00
    +1

    Информация в id_token кодируется в виде объекта JSON

    И access token, и id token кодируются в JWT (который может быть шифрован), в котором payload'ом является описываемый объект.


    1. Vovaka
      13.04.2016 13:17
      -1

      Спасибо за верное и дельное замечание.
      Не могли-бы, если появятся ещё в дальнейшем замечания, сообщать личным сообщением.
      Ещё раз спасибо.