Что такое Single Sign On?


Single Sign On — это технология, с помощью которой пользователь, будучи аутентифицированным на удостоверяющем центре (далее Identity Provider, IdP), будет автоматически аутентифицирован на другом сервисе (далее Service Provider, SP или Consumer[1-N]) этой компании.

Механизм Single Sign On используют такие сайты, как ХабраХабр, Yandex, Google. Приемущества такого подхода к аутентификации пользователей очевидны:

  • Пользователь вводит пароль только 1 раз
  • Или вовсе не вводит пароль на IdP, если там был использован вход через социальную сеть или с использованием OpenID
  • Автоматически аутентифицируется на всех проектах компании
  • Данные пользователя могут плавать между сервисами от IdP до SP прозрачно для пользователя

Минусы, конечно, вытекают, как всегда, из плюсов:

  • Потеря пароля от IdP влечет за собой проблему входа во все сервисы
  • Потенциально возросший риск кражи мастер сессии с IdP (может быть уменьшен с помощью привязки сессии к подсети провайдера, а также использования HTTPS, HTTP Only Cookies и SSL Only Cookies)
  • Потенциально возросший риск кражи пароля от IdP
  • ...

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

Перед тем как приступить к имплементации SSO в компании, хорошо было бы убедиться, что вы хорошо знаете, что такое:


а еще лучше, как применять их самому.

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

На хабре есть еще одна отличная статья по базовым принципам работы с Cookies и как надо правильно ставить Cookies, чтобы не остаться без штанов: habrahabr.ru/company/mailru/blog/228997.

Итак, после ознакомления с базовой теорией: что такое SSO, аспектами безопасности, которые связаны с этой задачей, — мы можем приступить к ее реализации.

Как это будет работать


В общем случае аутентификация будет проходить по следующему сценарию:

image

Рассмотрим сценарий, когда пользователь через закладки переходит на какую-либо защищенную авторизацией страницу (п. 1 на схеме).
Далее в Symfony2 активируется механизм Entry point и переадресовывает нас на наш IdP, где нам должны докинуть OTP. Тут есть несколько сценариев развития событий:

  1. Пользователь аутентифицирован на IdP, тогда IdP просто докинет в цепочку переадресаций OTP (п. 3 на схеме, зеленая линия)
  2. Пользователь не аутентифицирован на IdP, тогда его надо отправить на форму ввода логина/пароля (п. 3 на схеме, красная линия)
  3. Пользователь вообще в первый раз нас видит, но хочет зарегистрироваться и уходит на форму регистрации (В этот момент в сессии на IdP сохранен SP, с которого он пришел.)

После того как пользователь, например, прошел регистрацию, его надо перенаправить на п. 3 по зеленой линии на валидацию OTP на SP, с которого он пришел к нам на IdP. Когда мы на SP валидируем OTP, мы делаем доверенный REST запрос к нашему IdP, чтобы удостовериться, что такой OTP действительно существует и еще не истек по времени. В этот момент REST сервис должен инвалидировать этот OTP. Ставьте лок, эта операция должна быть атомарна. Дальнейшие запросы с таким OTP должны возвращать либо HTTP 400, либо HTTP 404 для SP.

В случае, когда IdP ответил, что такой OTP существует и валиден, SP аутентифицирует пользователя посредством выдачи ему PreAuthenticatedToken'а.

Выход будет работать по следующей схеме:

image

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

Предположим, что пользователь был на некой странице /secured_area и нажал на «Выход». В этот момент происходит локальный логаут в рамках SP. Затем мы уходим на IdP на специальный URL /sso/logout, который будет управлять процессом выхода со всех сервисов для этого пользователя. Т.к. пользователь уже пришел с SP, то IdP выбирает следующий сервис, который есть в компании, и отправляет на него делать выход. Тот сервис, в свою очередь, снова по завершению, отправляет нас на IdP и в случае, если сервисы кончились, выполняет локальный выход (п. 5 на схеме). После пользователь отправляется обратно на SP, с которого он начал делать выход.

Есть и другой вариант развития событий, в котором пользователь начинает процесс выхода не с SP а с IdP. И выглядит это примерно так:

image

Удостоверяющий центр (IdentityProvider)


Чтобы сделать удостоверяющий центр, сначала вы должны выбрать приложение в вашей компании, которое будет за это отвечать, наподобие, как это сделано у Yandex (Яндекс.Паспорт) или у Google (Google Accounts).

В это приложение мы будем устанавливаеть первую часть: SingleSignOnIdentityProviderBundle

SingleSignOnIdentityProviderBundle отвечает за:

  • Генерацию одноразовых паролей (OTP)
  • Запоминает в сессию, с какого SP пришел пользователь
  • Функциональность для выхода со всех SP-ов

Ставим через composer:

php composer.phar require "korotovsky/sso-idp-bundle:~0.2.0"

Далее обновляем зависимости и прописываем наш бандл в AppKernel:
app/AppKernel.php
// app/AppKernel.php
$bundles[] = new \Krtv\Bundle\SingleSignOnIdentityProviderBundle\KrtvSingleSignOnIdentityProviderBundle();

Подключаем роуты /sso/login и /sso/logout из бандла:
app/config/routing.yml
# app/config/routing.yml:
sso:
    resource: .
    type:     sso

Теперь настраиваем IdP бандл:
app/config/config.yml
# app/config/config.yml:
krtv_single_sign_on_identity_provider:
    host:             idp.example.com # Хост нашего IdP приложения
    host_scheme:      http            # Схема нашего IdP приложения.

    login_path:       /sso/login/ # Путь, где будет вызваться OTP
    logout_path:      /sso/logout # Путь, где будет осуществляться централизованный контроль для выхода

    services:
        - consumer1 # Кодовое имя SP может быть любым, но обязательно уникальным среди всех остальных

    otp_parameter:    _otp   # Имя OTP параметра
    secret_parameter: secret # Имя параметра в Dependency Injection для подписи всех урлов переадресации, 
                             # значение этого параметра должно быть одинаковое на всех приложениях.

Правим security.yml:
app/config/security.yml
# app/config/security.yml
security:
    access_control:
        - { path: ^/sso/login$, roles: [ROLE_USER, IS_AUTHENTICATED_FULLY] }

Теперь необходимо зарегистрировать SP в наш бандл, для этого создадим класс, который имлементирует интерфейс \Krtv\Bundle\SingleSignOnIdentityProviderBundle\Manager\ServiceProviderInterface и зарегистрируем его в сервис контейнере с помощью тега
src/Acme/Bundle/AppBundle/Resources/config/security.yml
services:
    acme_bundle.sso.consumer1:
        class: Acme\Bundle\AppBundle\Sso\ServiceProviders\ServiceProvider1
        tags:
            - { name: sso.service_provider, service: consumer1 }

Публичный API бандла

Данный бандл регистрирует в Dependency Injection несколько сервисов, которые пригодятся при кастомизации процессов SSO в конечном проекте.

  • sso_identity_provider.service_manager — Менеджер для работы с SP. По идентификатору SP возвращает инстанс класса \Krtv\Bundle\SingleSignOnIdentityProviderBundle\Manager\ServiceProviderInterface
  • sso_identity_provider.otp_manager — Менеджер для работы с OTP. Инвалидация, проверка и получение.
  • sso_identity_provider.uri_signer — Сервис для подписи урлов, если потребуется переадресовывать людей на /sso/login самому.

На этом настройка IdP закончена, переходим к настройке SP части.

Приложение, в котором вы хотите аутентифицировать пользователя через IdP должно устанавливать себе SingleSignOnServiceProviderBundle

SingleSignOnServiceProviderBundle отвечает за:

  • Автоматическую аутентификацию через IdP

Ставим через composer:

php composer.phar require "korotovsky/sso-sp-bundle:~0.2.0"

Далее обновляем зависимости и прописываем наш бандл в AppKernel:
app/AppKernel.php
// app/AppKernel.php
$bundles[] = new \Krtv\Bundle\SingleSignOnServiceProviderBundle\SingleSignOnServiceProviderBundle();

Подключаем роут /otp/validate/ для валидации OTP:
app/config/routing.yml
# app/config/routing.yml:
otp:
    # this needs to be the same as the check_path, specified later on in security.yml
    path: /otp/validate/

Теперь настраиваем IdP бандл:
app/config/config.yml
# app/config/config.yml:
krtv_single_sign_on_service_provider:
    host:             idp.example.com # Хост нашего IdP приложения
    host_scheme:      http            # Схема нашего IdP приложения.

    login_path:       /sso/login/ # Путь где будет вызваться OTP

    # Configuration for OTP managers
    otp_manager:
        name:       http
        managers:
            http:
                provider:    service # Active provider for HTTP OTP manager
                providers:           # Available HTTP providers
                    service:
                        id: acme_bundle.your_own_fetch_service.id

                    guzzle:
                        client:   acme_bundle.guzzle_service.id
                        resource: http://idp.example.com/internal/v1/sso

    otp_parameter:    _otp   # Имя OTP параметра
    secret_parameter: secret # Имя параметра в Dependency Injection для подписи всех урлов переадресации, 
                             # значение этого параметра должно быть одинаковое на всех приложениях.

Чтобы у нас был «честный» SSO в качестве менеджера необходимо выбирать http метод, а в качестве провайдера выбирать service. Для этого надо имплементировать интерфейс Krtv\SingleSignOn\Manager\Http\Provider\ProviderInterface.

Правим security.yml:
app/config/security.yml
# app/config/security.yml
    firewalls:
        main:
            pattern: ^/
            sso:
                require_previous_session: false
                provider:                 main
                check_path:               /otp/validate/ # Same as in app/config/routing.yml

                sso_scheme:       http              # Required
                sso_host:         idp.example.com   # Required
                sso_otp_scheme:   http              # Optional
                sso_otp_host:     consumer1.com     # Optional
                sso_failure_path: /login
                sso_path:         /sso/login/       # SSO endpoint on IdP.
                sso_service:      consumer1         # Consumer name

            logout:
                invalidate_session: true
                path:               /logout
                target:             http://idp.example.com/sso/logout?service=consumer1

Публичный API бандла

Данный бандл регистрирует в Dependency Injection несколько сервисов, которые пригодятся при кастомизации процессов SSO в конечном проекте.


На этом настройка наших приложений завершена.

Чтобы следить за обновлениями бандлов, ставьте «звездочки» для них:

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


  1. Chikey
    13.06.2015 01:42
    +5

    Проще реализовать все через свой OAuth2 IdP типа fb connect.


    1. korotovsky Автор
      13.06.2015 17:02
      +3

      Перед тем, как я все это начинал делать, я сделал большой ресеч на тему SSO, и компаний, которые его используют. Так вот, Хабр, Яндекс, Google которые я смотрел, почему-то тоже не используют OAuth2 как IdP.

      И в какой то мере мне понятна такая политика — для своих же сервисов мы всегда знаем, какие права они хотят получить, и каким права мы можем им дать, в т.ч. данные, которые мы можем им отдавать. OAuth2 тут будет довольно избыточен, т.к. все что нам надо сделать — это аутентифицировать пользователя. Остальное все равно размазано в бизнеслогике приложений.

      Далее, если мы будем даже использовать OAuth2 как IdP, то потребность в реализации такого функционала как замоминание SP, с которого пришел пользователь все равно ложится на наши плечи. Да, в OAuth2 есть client_id, но если вы смотрели исодники IdP бандла, то там можно заметить (да и из статьи тоже), что в приложении мы будем оперировать SP не как строкой, которая его идентифицирует, а как инстансом класса этого SP, в который мы можем поместить нашу дополнительную логику, для удобной работы с этим SP из IdP.

      Также нам все-равно придется делать всю логику для механизма logout'а. Чтобы юзер был разлогинен на всех сервисах, после того, как нажмет «Выход». Примерно так, как это происходит, когда жмем «Logout» в Google Accounts.

      Вернемся к моменту, когда мы выбрали OAuth2 для IdP, и теперь прикинем каким количеством «кастома» нам придется его обернуть/написать, что начинаешь думать, а так ли нужен этот OAuth2? И проще-ли получается?

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


      1. Razaz
        14.06.2015 14:47
        +1

        Вообще гугл использует OpenID Connect, который базируется на OAuth2 — Google Identity Platform/OpenID Connect.
        Страницы логина, логаута и self-service у каждого реализуются по-своему, так как протокол не специфицирует сам вид страницы. Максимум — хинты для отображения.

        ClientId запоминается при выдаче токена и хранится в сторадже выданных токенов.

        Логика логаута есть в спеке OpenID Connect Session Management: OpenID Connect Session Management 1.0 — draft 23

        Я понимаю, что у вас PHP, но посмотрите на практически готовое решение: https://github.com/IdentityServer/IdentityServer3.
        Какой-то из билдов собирали уже на vNext. Так что и хостинг на *Nix где-то рядом.
        Там решены все ваши вопросы. На крайняк — просто портируйте) лицензия позволяет. Будут вопросы по коду — обращайтесь)


        1. korotovsky Автор
          14.06.2015 15:12
          +1

          Судя по вашему комментарию, вы тоже собаку съели на этом. При всем желании, не изобретать велосипед, намерение внеднить что-то масштаба enterprise и на Java/.NET ведет к неумолимому росту сложности проекта в геометрической прогрессии.

          Просто представьте себе среднего PHP разработчика, который это увидит? :) В лучшем случае будет разбираться неопределенное кол-во времени, в худшем вообще уйдет из компании )

          За ссылку на OpenID Connect Session Management 1.0 — draft 23 спасибо. Думаю добавлю это в бандл, полезный функционал.

          Так или иначе, из-за того что все на PHP — приходится искать рациональный баланс.


          1. Razaz
            14.06.2015 15:22

            У нас просто свой сервак который поддерживает еще пачку протоколов кроме OIDC и эта реализация — донор имплементации протокола)

            Это еще не enterprise) Как раз просто полная реализация спеки и дополнений. Недавно они прошли верификацию.
            Можно попробовать портировать на PHP. Код там достаточно простой. Зато будет по стандарту и все клиенты смогут нормально подключаться. Плюс можно будет полагаться на инфу о best practices при организации доступа и разделении прав.

            Если проект сам в себе — то и смысла прикручивать это нет. Если хочется SSO — То почти всегда это внешнее приложение. А там пофиг на чем оно написано. Но SSO практически всегда усложняет процесс) Особенно при включении валидации сертификатов ;D

            Для своих разработчиков мы просто настраиваем все сами. Им говорим только то, что в конфиг вписать. Остальное за них делает middleware.


            1. korotovsky Автор
              14.06.2015 15:52

              В таком случае можно попробовать оживить проект bitbucket.org/PEOFIAMP/phpoidc, который вроде как является полноценной реализацией OIDC. Хотя-бы не в .NET придется ковыряться :)


              1. Razaz
                14.06.2015 16:00

                Это отличный вариант. А в PHP есть что-то типа middleware из ноды или asp.net?

                А проект то вроде живой. Последний коммит — 2015-04-09.
                И если не врут, то уже сделаны.:
                OpenID Connect Core 1.0
                OpenID Connect Discovery 1.0
                OpenID Connect Registration 1.0
                OpenID Connect Dynamic Registration 1.0
                OpenID Connect Session Management 1.0


                1. korotovsky Автор
                  14.06.2015 16:04

                  В PHP как таковом — нет, но в архитектуре Symfony2 есть события, например kernel.request. Обычно туда все такие штуки вешают, чтобы для приложеньки это все было «прозрачно».

                  Тут вижу такой вариант. Из той либы выделить OP, RP. А дальше обновить свои бандлы, например в версии 0.3.x чтобы они были просто враппером для тех библиотек и как раз добавляли этот middleware слой.


                  1. Razaz
                    14.06.2015 16:09

                    Вполне вариант. Мы вот вынуждены вырезать часть с OIDC, так как страницу логина жестко зашили, а мы используем динамическую генерацию страницы и мультфакторный вход. Так что норм)


                    1. korotovsky Автор
                      14.06.2015 16:12

                      Несмотря на то, что проект живой (технически) я не представляю, кто в своем уме потянет к себе в проект тот код, который там есть. «Привет из 00-х» грубо говоря. :)


                      1. Razaz
                        14.06.2015 16:19

                        Ну можно просто портировать куски логики с поправкой на современные реалии. Потом уже почистить саму логику. Главное что есть от чего отталкиваться и не надо с 0 писать тонны кода по проверке всех условий протокола )


                        1. korotovsky Автор
                          14.06.2015 16:20

                          Ну естественно :)


                  1. Fesor
                    15.06.2015 09:18
                    +1

                    Есть PSR-7 и совместимые с ним мидлвэры. Симфони предоставляет бридж для PSR-7 так что можно спокойно делать мидлвэры.


  1. hudson
    13.06.2015 11:08

    А SSO через LDAP по такой же схеме реализуется? Скоро надо будет реализовывать для web-приложения внутри корпоративной сети, хочу предварительно знания матчасти подкачать.


    1. Fedot
      13.06.2015 12:35
      +1

      Ldap это просто база данных, так что нужно будет просто написать userProvider который будет смотреть туда


  1. justabaka
    13.06.2015 11:31
    -1

    Слово «атака» присутствует в русском языке и пишется с одной «Т».


  1. BigD
    13.06.2015 18:56

    SAML?


  1. TpeHep
    14.06.2015 10:59

    Почему не использовать готовые решения, к примеру forgerock.org: OpenAM, OpenIDM и др.?


    1. Razaz
      14.06.2015 14:24

      Они хотят денюжек если в проде заиспользуете вроде как.


  1. ruFog
    15.06.2015 14:53

    Спасибо за публикацию и бандлы. Очень актуально для меня.


    1. korotovsky Автор
      15.06.2015 16:07

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