Реализация Single Sign On в Symfony2 приложении

от автора


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

Single Sing 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 } 

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

Ставим через 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 

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

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

ссылка на оригинал статьи http://habrahabr.ru/post/260183/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *