Реализация SSO через SAML с примером

в 16:23, , рубрики: github, java, opensaml, saml, scim, SSO, информационная безопасность

Введение

Доброго времени суток, дорогой читатель. Я уже давно хотел написать статью на хабре и вот наконец-то этот момент настал. Из последних тем, которыми я занимался и о которых мне есть что рассказать — это была реализация SSO для сервиса realtimeboard.com — замечательный продукт для работы удаленной команды, который хочется постоянно развивать и совершенствовать. Хочу здесь сразу уточнить, что в принципе SSO через Facebook и Google уже было в сервисе до моего прихода. Моей же задачей было реализовать его через протокол SAML.

SSO (Single Sign-On), — технология единого входа пользователей, благодаря которой владея одной лишь учетной записью пользователь может посещать множество различных сервисов.

SAML — это популярный XML-протокол для реализации SSO. Как правило большие организации (enterprise) используют именно его, как проверенный и надежный вариант.

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

Технические детали

На момент написания статьи, последняя версия стандарта — SAML 2.0. Базируется стандарт на XML, а значит и все требования стандартов w3c к XML так же применимы и к нему, не забывайте про это. Так, например в момент реализации, я словил одну ошибку. В момент создания AuthnRequest я генерировал уникальный строковый идентификатор для атрибута ID, так вот по требованиям он не должен начинаться с цифры, а у меня периодически так было.

В аутентификации через SAML SSO участвует три стороны:

  • SAML identity Provider (IdP)
  • SAML Service Provider (SP)
  • браузер пользователя (User Agent)

Сама схема взаимодействия представлена на рисунке ниже

image

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

Вариант 1. Пользователь обращается к сервису. Сервис формирует сообщение AuthnRequest, кладет в параметр SAMLRequest и делает редирект через браузер к провайдеру на Login URL, где происходит аутентификация, затем провайдер формирует сообщение Response, кладет его в параметр SamlResponse и редиректит обратно в сервис на ACS URL.

Вариант 2. Пользователь уже аутентифицирован и находится в личном кабинете провайдера, откуда он может перейти в сервис, кликая по его ярлыку. В данной ситуации провайдер сразу формирует сообщение Response, кладет его в параметр SamlResponse и направляет в сервис на ACS URL.

ACS URL (Assertion Consumer Service URL) — урл на стороне нашего приложения, который принимает запросы с параметром SamlResponse, обрабатывает его (выдергивает сообщение Response, проверяет подпись по сертификату, различные правила) и если все хорошо, то создает рабочую сессию пользователя в приложении.

IdP Login URL — урл на стороне провайдера, который принимает запросы с параметром SAMLRequest и так же выполняет должную валидацию этого параметра, и если все хорошо, то отправляет на форму аутентификации.

В качестве IdP провайдeра может выступать один из онлайн-сервисов, таких как OneLogin, LastPass, Okta и другие. Также можно развернуть свой IdP с помощью Shibboleth или поднять AD.

Настройки

Все параметры для этого взаимодействия должны настраиваться и храниться на обеих сторонах (IdP, SP), то есть должны быть выстроены доверительные отношения.

SP должен хранить у себя сертификат, который выдаст IdP, а также Saml Login URL, на который будет отправлять SAMLRequest.

IdP обязательно должен хранить у себя для размещаемого приложения ACS URL...

image

… должен сформировать сертификат, выбрать алгоритм шифрования, предоставить Saml Login URL для сервиса...

image

… должен настроить (если необходимо) атрибуты пользователя или еще какие-то кастомные поля для конкретного сервиса. Сервис с провайдером должны также договориться о формате Subject NameID. Это по сути идентификатор пользователя, информация о котором будет передана в сообщениях. В нашем случае это email.

image

Помимо ручной настройки, SP и IdP должны уметь генерировать файл метаданных, содержащий все необходимые параметры для “коллеги”, чтобы тот мог и загрузить и выставить настройки сам. У нас такой задачи не было.

В дополнение еще скажу, что помимо SSO, провайдеры опционально предоставляют еще и услугу SLO (Single Logout) — механизм, который предполагает выход сразу из всех сервисов одновременно. Возможны также две точки входа:

  • Разлогиниться из сервиса, уйдет запрос на выход к провайдеру, а затем и по остальным сервисам, настроенным у провайдера;
  • Разлогиниться в личном кабинете провайдера, дальше уйдет запроса на логаут ко всем настроенным сервисам.

Для этого надо поддержать на стороне сервиса обработку запросов SP SLO URL и отправку запросов к IdP SLO URL. Такой задачи у меня также не было.

Исследования

И так, задача поставлена, с теорией ознакомился, пора и делать начать. Первым делом ознакомился со списком имеющихся библиотек. Backend нашего сервиса написан на Java, искал библиотеки именно для него. Наиболее полный список продуктов, связанных с SAML можно увидеть тут. Выбрал для себя наиболее очевидные решения: Okta SAML Toolkit for Java, SpringSecurity SAML, OIOSAML 2.0 Toolkit, lastpass/saml-sdk-java, OneLogin 2.0, OneLogin 1.1.2, OpenSAML 2.0. Далее нужно было определить критерии, по которым будет выбрано то или иное решение. Так была составлена следующая таблица.

Если честно, исходный код предложенных решений просто ужаснул — известные провайдеры решили написать свои высокоуровневые прикладные библиотеки (получилось не очень), большая часть (слава богу) решений была основана на низкоуровневой библиотеке работы с SAML OpenSaml 2.0. Учитывая, что все эти библиотеки так или иначе пришлось бы править затачивая под потребности нашей логики и встраивать в наш основной код со всеми юнит-тестами было решено также использовать уже имеющийся базис в виде OpenSaml 2.0 и на нем писать нашу высокоуровневую логику. Возможно, если бы у нас был Spring, я бы стал использовать SpringSecurity SAML, но его у нас нет.

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

image

Итого, решение выбрано, поведение определено.

Реализация

В качестве IdP для тестирования я выбрал, как можно увидеть по скринам выше, OneLogin. Он предоставляет бесплатный девeлоперский аккаунт, с ним проще всего было настроить тестовое приложение, а также он содержит набор утилит для работы с SAML, которые смогут облегчить Вам работу. Если не надо никакого полноценного конфигурируемого IdP, то можно использовать простую эту утилиту или ее аналог от Okta. Ими я тоже пользовался.

Сначала я написал свое тестовое локальное приложение чтобы обкатать в принципе эту технологию, его исходники я Вам и продемонстрирую. Затем уже перенес это решение на промышленную кодобазу. Логика приложения простая и применимая для любого сервиса. Приложение предлагает пользователю ввести email, если для этого домена не настроено SSO SAML, то приложение просит ввести внутренний пароль юзера (в примере ругается, что юзер не может SSO). Если же настроено, то смотрим на какой IdP настроен данный домен, формируем сообщение и перенаправляем запрос туда. После успешной аутентификации получаем сформированное сообщение на наш ACS URL от IdP, из сообщения берем email, берем сертификат для данного домена и проводим валидацию сообщения. В случае успешной проверки берем атрибуты из сообщения FirstName, LastName. Если пользователь уже существует меняем ему значения этих атрибутов в нашем сервисе. Если пользователя еще нет, то создаем его.

Это так называемый Just-in-Time Provisioning. Эта самая простая реализация провижининга (синхронизация) пользователей, которую можно сделать. Минусом такой синхронизации можно назвать отсрочку выполнения до следующего входа пользователя. Плюс невозможность удаления пользователя на стороне сервиса при удалении юзера из IdP. Чтобы полноценно запустить провижининг в сервисе необходимо реализовать стандарт SCIM, но этого я еще не делал — возможно это будет следующая история.

Надеюсь, эта статья была полезна для Вас.

Автор: AlexKarandash

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js