Кратко об SSO через OAuth2

Устройство SSO

Как устроено SSO? Принцип всегда один — всегда есть кто-то, кто авторизует первым — это провайдер.

Дальше все сервисы получают от провайдера какой-то вид ключа (токен). Точнее в OAuth2 пара токенов — access и refresh, но не суть. С токеном ты идёшь в API провайдера и спрашиваешь — это кто? Провайдер говорит — ну типа вот, это юзер такой-то. Вот и всё SSO. Второй вариант — ты сам извлекаешь детали пользователя из токена и сам его валидируешь с помощью сертификата, например, так можно сделать с JWT (JSON Web Token). И, на самом деле, похожим образом устроен даже Kerberos.

При кроссдоменном SSO способа передачи токена через браузер я знаю два:

  1. Двойной редирект: пришли к вам, вы не знаете кто это, cookie у вас нет, вы редиректите его на провайдера, а в провайдере у него cookie уже есть. Провайдер достаёт токен из куки и редиректит обратно, но уже с токеном. Либо куки ещё нет — тогда провайдер показывает страничку входа, и после входа так же редиректит обратно.
  2. Подключение js с другого домена: провайдер выставляет у себя URL, по которому, в случае если сессия пользователя уже активна, отдаётся js, делающий программно путём js вызов/редирект, передающий токен в ваше приложение.

А дальше всё то же самое — берёте токен и идёте по нему в API провайдера, спрашиваете что за юзер, сохраняете себе токен на время жизни сессии.

Во всех протоколах — что OpenID, что OAuth2, что CAS обычно в итоге используется 1-й способ в итоге, но для публичных сайтов 2-й на самом деле прикольней, так как:

  • с ним не бывает проблемы бесконечного цикла редиректов (юзера нет -> редирект -> произошла ошибка -> редирект обратно с косым кодом/без кода/короче что-то не сработало -> упс опять юзера нет -> редирект…)
  • для публичных сайтов позволяет НЕ создавать по сессии на каждого пришедшего юзера и НЕ делать лишних редиректов для анонимусов = экономия места в хранилище сессий и удовлетворение поисковиков

По идее передача токена через браузер (через обратный редирект с Authorization URL’а) предусмотрена непосредственно в самом OAuth2 стандарте, так что плагины должны её поддерживать. Только стандарт требует HTTPS, а то токены голые по сети летают.

Еще есть такой протокол как CAS — но реализации всего две, https://github.com/rubycas/rubycas-server и https://wiki.jasig.org/display/CAS/Home, обе в виде отдельных серверов, один из которых на Ruby. Смысла это использовать, наверное, нет. OpenID — тоже, так как устарел и вообще больше склонен к ошибкам взаимодействия. Принцип везде по сути тот же (да если и самому написать — он останется тот же), а так как OAuth2 более популярен (много готовых плагинов под разные CMS и языки) и решает более широкий спектр задач, лучше брать его, либо OpenID Connect, который является его надмножеством (с OpenID Connect-сервером можно работать как с OAuth2 сервером). Разве что для публичного сайта следует задуматься о реализации механизма (2), описанного выше.

Для SSO между разными приложениями применение OAuth2 вполне нормальное. Однако, если у вас одно приложение, поделённое на микросервисы — то вот для шаринга сессий между микросервисами OAuth2 юзать реально странно. Так как по идее, наверное, просто должен быть отдельный «сервис сессий» или «сервис юзеров», логически являющий собой разделяемое между всеми компонентами приложения хранилище сессий и инкапсулирующее всю логику работы с пользователями (бизнес/не бизнес — всю), и все остальные компоненты должны в него ходить просто, без всяких Authorization URL, рефреш-токенов, консьюмеров и т. п.

Режимы OAuth2

По поводу режимов OAuth2 — SSO соответствует Authorization Code Flow, https://tools.ietf.org/html/rfc6749#section-1.3.1

Другие Flow:

  • Client Credentials — в приложение зашивается client id и client secret, автоматом авторизует по ним приложение без ввода пользовательских данных
  • Implicit — то же самое, но без client id и client secret (типа приложение сделано на js, secret сунуть некуда, так как он всем будет виден), доступ проверяется только по обратному redirect URL’у (то есть типа для приложения с известным адресом)
  • Resource Owner Password Credentials — это когда пароль вводится на твоём сайте, и ты прямо логин-пароль передаёшь серверу авторизации (соответственно видишь его сам)

Ну и refresh token ещё есть, но это общий принцип для упрощения проверок доступа — OAuth2 сервер всегда даёт пару токенов, access token и refresh token. В теории, если сервер сделан нормально, то прямо в самом access токене зашиты выданные привилегии («scopes») и время жизни, и ты можешь их прямо руками оттуда выдрать и проверить. Вот потом, когда время жизни токена кончается, ты должен взять refresh token и пойти к серверу, чтобы он тебе снова выдал (или не выдал) новый access token. Таким образом снимается нагрузка на сервер — клиенту больше не нужно спрашивать привилегии «на каждый чих».

В Authorization Code Flow всё просто, нужны 3 урла:

  • Authorization URL — на него юзера перекидываешь для авторизации;
  • Access Token URL — там рефреш токен рефрешишь и получаешь новый access токен, когда он протух;
  • User Details URL — там по токену получаешь параметры юзера. Вообще говоря, в OAuth2 является необязательным, но практически всегда присутствует (userinfo нужно всем!); в OpenID Connect — обязателен. Если в OAuth2 сервере отсутствует данный URL/endpoint, для проверки Token’а можно использовать Token Introspection Endpoint.

Сам клиент занимает строчек 400 на php: https://github.com/vitalif/oauth2-client/tree/master/src (достаточно AccessToken.php, AbstractProvider.php и GenericProvider.php).

Keycloak

Дополнение. Есть такой Identity-сервер, как Keycloak. Лучше стараться его НЕ использовать, так как он достаточно кривой, сложный в отладке и плохо документированный. Пример — мы, работая с ним, прямо сейчас обнаружили баг: после протухания ОНЛАЙН-сессии юзера по ОФФЛАЙН токену keycloak перестаёт отдавать userinfo. Документация по этому поводу ничего не объясняет, да и в целом довольно скудна; по-видимому, это всё-таки баг keycloak, т.к. token introspection endpoint в этом случае по оффлайн-токену тоже отдаёт неуспех. Да и логически каждому активному токену должна соответствовать активная сессия — например, чтобы можно было из UI администрирования её принудительно завершить, отозвав token.

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