Глобальная авторизация в веб-системах

Warning Warning: Ниже описывается Велосипедная Реализация SSO. В современном мире доступен OAuth2, про него можно почитать тут: Кратко об SSO через OAuth2

…или как реализовать простой Single Sign-on в веб-системах.

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

  • (П) Пользователь.
  • (C1) Система 1 — клиент глобальной авторизации. В неё пришёл пользователь без авторизации.
  • (С2) Система 2 — сервер глобальной авторизации. В ней пользователь уже авторизован, скорее всего, через cookie.
шаг кто что делает кому возможные ошибки
(П) → (переходит по ссылке) → C1 с любыми параметрами.

C1 хочет перенять авторизацию у С2.
C1 генерирует случайный ID (ID) и ключ (KEY). Никаких ограничений на эти значения не накладывается, кроме того, что они должны быть достаточно стойки к подбору и, желательно, состоять из печатных символов. Например, за каждый из них можно взять 16 случайных байт, взятых из /dev/urandom в UNIX-системах и GetRandom() в Windows.

1 C1 → (делает GET-запрос напрямую) → С2 с параметрами ga_id=ID&ga_key=KEY

С2 запоминает соответствие ID и KEY.

С2 не ответила или ответ не в формате JSON («по факту», а не по MIME-типу ответа):

Сервер авторизации XXX недоступен со стороны системы C1; код HTTP: XXX; MIME-тип ответа: XXX.

С2 ответила хешем в формате JSON, в котором присутствует поле «error» с непустым значением EEE:

Сервер авторизации XXX сообщает об ошибке начала сессии авторизации: EEE.
2 C1 → (перенаправление браузера пользователя) → С2 с параметрами ga_id=ID&ga_url=URL&ga_check=CHECK и опционально параметром ga_message=TEXT.
  • URL — URL для возврата на С1, на который С2 будет передавать данные и на который же С2 будет отправлять пользователя редиректом обратно. Если URL не передаётся, за него принимается HTTP-заголовок Referer.
  • Если CHECK=0 или не передаётся, и пользователь не авторизован в С2, она должна потребовать от него авторизоваться.
  • TEXT — название C1 или любое сообщение, которое нужно показать пользователю в случае запроса авторизации.

С2 даётся возможность прочитать cookie пользователя и получить данные о нём. Если это необходимо, на этом же шаге С2 может запросить у пользователь подтверждение передачи его учётных данных внешней системе, и в случае отрицательного решения передать на следующем шаге NOLOGIN.

URL, на который происходит перенаправление, недоступен пользователю.

Увы, в данном случае никакого сообщения об ошибке выдать не получится :(. Некому — с C1 уже ушли, а на С2 ещё не пришли.

В запросе не передан или передан некорректный параметр ga_url, и также отсутствует HTTP-заголовок Referer, по крайней мере такой, который можно распарсить.

В запросе авторизации от клиентской системы отсутствует URL (или неверный: URL) обратной связи.
3 С2 → (делает POST-запрос напрямую) → C1 с параметрами ga_client=1&ga_id=ID&ga_key=KEY&ga_data=DATA&ga_nologin=NOLOGIN
  • DATA — данные о вошедшем пользователе в произвольном формате, кодированные в JSON.
  • NOLOGIN=1 и DATA="" (пустой строке), если и только если CHECK=1 и пользователь не авторизован в С2.
  • Иначе NOLOGIN не передаётся или NOLOGIN=0.

С1 запоминает соответствие ID и переданных данных.

ID неизвестен серверу.

Попытка авторизоваться по неверному или устаревшему идентификатору сессии, попробуйте ещё раз (ссылка — URL к C1).

CHECK=0 и авторизовать пользователя в С2 невозможно.

Сайт XXX (описание: TEXT) требует авторизации, а вы не авторизованы в С2. (TEXT из предыдущего шага)
Логично, если данное сообщение показывает пользователю С2.

Пользователь не доверяет C1 и запретил С2 передавать ей свои авторизационные данные.

Логично, если С2 покажет пользователю ошибку и/или страницу настройки доверия к C1.

C1 не ответила или ответ не в формате JSON. MIME-тип, опять-таки, не проверяется.

Сайт XXX (описание: TEXT) недоступен с сервера авторизации. Код HTTP: XXX; MIME-тип ответа: XXX.

C1 ответила хешем в формате JSON, в котором присутствует поле «error» с непустым значением EEE:

Сайт XXX (описание: TEXT) сообщает об ошибке авторизации: EEE.
Ошибками EEE могут быть, например:
  • ID неизвестен клиенту:
    Попытка авторизоваться по неверному или устаревшему идентификатору сессии, попробуйте ещё раз (ссылка).
  • Известному на клиенте ID соответствует неверный ключ KEY:
    Секретный ключ неверен, проверка подлинности сервера авторизации XXX не удалась. Возможно, вас дурят.
  • Не вышло декодировать DATA или считать NOLOGIN:
    Формат данных, переданных с сервера авторизации, неизвестен. Опционально — показать также и сами данные.
  • Не удалось сохранить данные в хранилище состояний клиента (базе данных, кэше и т. п.):
    Внутренняя ошибка клиента — хранилище состояний недоступно.
4 С2 → (перенаправление браузера пользователя) → C1 с параметрами ga_client=1&ga_id=ID&ga_res=CODE
  • CODE — HTTP-код статуса, полученный от POST-запроса из предыдущего пункта.

C1 может взять сохранённые в предыдущем пункте данные и на их основе авторизовать пользователя.

ID неизвестен клиенту:

Попытка авторизации по неверному или устаревшему идентификатору сессии, либо сервер авторизации не смог передать данные учётной записи. Попробуйте ещё раз (ссылка).

Самая противная ошибка — это Redirect-Loop — бесконечное перенаправление, которое можно спровоцировать, если при ошибке авторизации никак не сохранить факт ошибки в браузере пользователя (например, с помощью cookie) и вместо того, чтобы показать страницу с ошибкой, отправить пользователя обратно на шаг 2 (через шаг 1).

Важный момент: после успешной обработки любого запроса глобальной авторизации (запроса с параметрами ga_id или ga_client и т. п.), пришедшего со стороны браузера пользователя, как клиентской, так и серверной стороной, должно осуществляться перенаправление (HTTP 302 Moved temporarily) на адрес, не содержащий в себе параметров ga_id и т. п., для исключения повторной отправки запроса пользователем при нажатии «Обновить страницу».

И ID и ключ являются «секретными», но ID знают и сервера, и пользователь (ID передаётся в браузер), а ключ — только сами сервера. За счёт этого достигается безопасность: пользователь не может сам передать произвольные данные авторизации на сервер, не зная ключа. Для дополнительной защиты всё это можно просто пустить через HTTPS (SSL). Понятно, что кэш, в котором сохраняются соответствия ID и ключа, не должен быть доступен для чтения внешнему пользователю, иначе вся защита накрывается медным тазом. Для усиления защиты опять-таки можно дополнительно создать список доверенных серверов и при приёме данных авторизации проверять IP.

Как уже было сказано выше, данные авторизации — произвольные в JSON-формате. Однако, удобно специфицировать его чуть точнее: хеш, в котором есть поля user_name (логин), user_email (адрес электронной почты) и необязательные поля user_url (URL «домашней страницы» пользователя) и user_real_name («настоящее имя» пользователя).

OpenID, на самом деле, работает похоже, но почему-то адски глючит и его страшно использовать на своих внутренних ресурсах. :)

В реализации протокола очень желательна обработка ошибок и их показ пользователю в удобном виде, чтобы не получалось, как в OpenID — «она утонула» © (то есть «произошла ошибка» без каких-либо деталей).

Do you want to try some new features? By joining the beta, you will get access to experimental features, at the risk of encountering bugs and issues.

Ок Нет, спасибо

Содержание

FoF_Sudo

FoF_Sudo — беспарольная авторизация типа «от системы к системе».

Идея такая: пусть есть некая система (например FoF — фидридер), в которой действует глобальная авторизация (в качестве сервера выступает другая система), и пусть эта система должна авторизоваться на доверенных серверах (в примере — FoF должен забирать защищённые RSS-ленты от имени разных пользователей). Но при этом (!) пароль пользователя хранить в открытом виде в базе FoF при этом нельзя. Да если бы и было можно, в данных глобальной авторизации его просто нету. При этом нужно, чтобы при передаче такой вот беспарольной авторизации какой-то совершенно левый пользователь не смог сделать так же и зайти под произвольным пользователем.

Как это сделать? Ответ — одноразовые ключи на каждый запрос:

  • FoF видит в урле рсс’ки &fof_sudo=(что_угодно). Что_угодно — совершенно что угодно, единственно, в случае FoF оно должно быть разное для разных пользователей, иначе FoF двух пользователей подпишет на один и тот же защищённый фид, а обновлять будет вообще как попало — то от имени одного, то от имени другого пользователя.
  • FoF генерирует случайный ID, запоминает соответствие этому ID пользователя и добавляет в запрос кукис:
    • Cookie: fof_sudo_id=ID
  • Система, к которой произошло обращение, видит этот кукис (fof_sudo_id) и делает обратный запрос к fof: /fof-sudo.php?id=ID
  • FoF отвечает данными пользователя, которые он помнит по этому ID, в формате JSON ({'user_name':'user@custis.ru'}) и забывает ID
  • Внешняя система верит, что теперь надо авторизоваться под именем юзера user@custis.ru (и, например, отдаёт правильную RSS’ку)
Warning Warning: И это тоже покрывается OAuth2.