Глобальная авторизация в веб-системах — различия между версиями
м |
|||
(не показано 11 промежуточных версий этого же участника) | |||
Строка 1: | Строка 1: | ||
+ | {{Box|{{Warning}} Ниже описывается Велосипедная Реализация SSO. В современном мире доступен OAuth2, про него можно почитать тут: [[Кратко об SSO через OAuth2]]}} | ||
+ | |||
…или как реализовать простой [[rupedia:Технология единого входа|Single Sign-on]] в веб-системах. | …или как реализовать простой [[rupedia:Технология единого входа|Single Sign-on]] в веб-системах. | ||
Строка 4: | Строка 6: | ||
* (П) Пользователь. | * (П) Пользователь. | ||
− | * (С1) Система 1 — клиент глобальной авторизации. В неё пришёл пользователь без авторизации. | + | * ({{/С1}}) Система 1 — клиент глобальной авторизации. В неё пришёл пользователь без авторизации. |
− | * (С2) Система 2 — сервер глобальной авторизации. В ней пользователь уже авторизован, скорее всего, через cookie. | + | * ({{/С2}}) Система 2 — сервер глобальной авторизации. В ней пользователь уже авторизован, скорее всего, через cookie. |
{| class="simpletable" | {| class="simpletable" | ||
Строка 12: | Строка 14: | ||
! кто | ! кто | ||
! что делает | ! что делает | ||
− | ! кому | + | !width=35%| кому |
+ | !width=45%| возможные ошибки | ||
|- | |- | ||
| | | | ||
|valign=top| (П) → | |valign=top| (П) → | ||
|valign=top| (переходит по ссылке) → | |valign=top| (переходит по ссылке) → | ||
− | |valign=top| | + | |valign=top| {{/С1}} с любыми параметрами.<br /> |
− | ⇒ | + | ⇒ {{/С1}} хочет перенять авторизацию у {{/С2}}. <br /> |
− | + | {{/С1}} генерирует случайный ID (ID) и ключ (KEY). Никаких ограничений на эти значения не накладывается, кроме того, что они должны быть достаточно стойки к подбору и, желательно, состоять из печатных символов. Например, за каждый из них можно взять 16 случайных байт, взятых из <tt>/dev/urandom</tt> в UNIX-системах и <tt>GetRandom()</tt> в Windows. | |
+ | |valign=top| | ||
|- | |- | ||
|valign=top align=center| 1 | |valign=top align=center| 1 | ||
− | |valign=top| | + | |valign=top| {{/С1}} → |
|valign=top| (делает GET-запрос напрямую) → | |valign=top| (делает GET-запрос напрямую) → | ||
− | |valign=top| | + | |valign=top| {{/С2}} с параметрами {{/p}}ga_id=ID&ga_key=KEY</tt> <br /> |
− | ⇒ | + | ⇒ {{/С2}} запоминает соответствие ID и KEY. |
+ | |valign=top| | ||
+ | {{/С2}} не ответила или ответ не в формате JSON («по факту», а не по MIME-типу ответа): | ||
+ | : '''Сервер авторизации XXX недоступен со стороны системы {{/С1}}; код HTTP: XXX; MIME-тип ответа: XXX.''' | ||
+ | {{/С2}} ответила хешем в формате JSON, в котором присутствует поле «<tt>error</tt>» с непустым значением EEE: | ||
+ | : '''Сервер авторизации XXX сообщает об ошибке начала сессии авторизации: EEE.''' | ||
|- | |- | ||
|valign=top align=center| 2 | |valign=top align=center| 2 | ||
− | |valign=top| | + | |valign=top| {{/С1}} → |
|valign=top| (перенаправление браузера пользователя) → | |valign=top| (перенаправление браузера пользователя) → | ||
− | |valign=top| | + | |valign=top| {{/С2}} с параметрами {{/p}}ga_id=ID&ga_url=URL&ga_check=CHECK</tt> и опционально параметром {{/p}}ga_message=TEXT</tt>. |
− | * URL — URL для возврата на <tt style="background-color: #e0ffe0">С1</tt>, на который | + | * URL — URL для возврата на <tt style="background-color: #e0ffe0">С1</tt>, на который {{/С2}} будет передавать данные и на который же {{/С2}} будет отправлять пользователя редиректом обратно. Если URL не передаётся, за него принимается HTTP-заголовок Referer. |
− | * Если CHECK=0 или не передаётся, и пользователь не авторизован в | + | * Если CHECK=0 или не передаётся, и пользователь не авторизован в {{/С2}}, она должна потребовать от него авторизоваться. |
+ | * TEXT — название {{/С1}} или любое сообщение, которое нужно показать пользователю в случае запроса авторизации. | ||
− | ⇒ | + | ⇒ {{/С2}} даётся возможность прочитать cookie пользователя и получить данные о нём. Если это необходимо, на этом же шаге {{/С2}} может запросить у пользователь подтверждение передачи его учётных данных внешней системе, и в случае отрицательного решения передать на следующем шаге NOLOGIN. |
+ | |valign=top| | ||
+ | URL, на который происходит перенаправление, недоступен пользователю. | ||
+ | : ''Увы, в данном случае никакого сообщения об ошибке выдать не получится :(. Некому — с {{/С1}} уже ушли, а на {{/С2}} ещё не пришли.'' | ||
+ | |||
+ | В запросе не передан или передан некорректный параметр ga_url, и также отсутствует HTTP-заголовок Referer, по крайней мере такой, который можно распарсить. | ||
+ | : '''В запросе авторизации от клиентской системы отсутствует URL (''или неверный: URL'') обратной связи.''' | ||
|- | |- | ||
|valign=top align=center| 3 | |valign=top align=center| 3 | ||
− | |valign=top| | + | |valign=top| {{/С2}} → |
|valign=top| (делает POST-запрос напрямую) → | |valign=top| (делает POST-запрос напрямую) → | ||
− | |valign=top| | + | |valign=top| {{/С1}} с параметрами {{/p}}ga_client=1&ga_id=ID&ga_key=KEY&ga_data=DATA&ga_nologin=NOLOGIN</tt> |
* DATA — данные о вошедшем пользователе в произвольном формате, кодированные в [[rupedia:JSON|JSON]]. | * DATA — данные о вошедшем пользователе в произвольном формате, кодированные в [[rupedia:JSON|JSON]]. | ||
− | * NOLOGIN=1 и DATA="" (пустой строке), если и только если CHECK=1 и пользователь не авторизован в | + | * NOLOGIN=1 и DATA="" (пустой строке), если и только если CHECK=1 и пользователь не авторизован в {{/С2}}. |
* Иначе NOLOGIN не передаётся или NOLOGIN=0. | * Иначе NOLOGIN не передаётся или NOLOGIN=0. | ||
⇒ <tt style="background-color: #e0ffe0">С1</tt> запоминает соответствие ID и переданных данных. | ⇒ <tt style="background-color: #e0ffe0">С1</tt> запоминает соответствие ID и переданных данных. | ||
+ | |valign=top| | ||
+ | |||
+ | ID неизвестен серверу. | ||
+ | : '''Попытка авторизоваться по неверному или устаревшему идентификатору сессии, попробуйте ещё раз ''(ссылка — URL к {{/С1}})''.''' | ||
+ | |||
+ | CHECK=0 и авторизовать пользователя в {{/С2}} невозможно. | ||
+ | : '''Сайт XXX ''(описание: TEXT)'' требует авторизации, а вы не авторизованы в {{/С2}}.''' (TEXT из предыдущего шага) | ||
+ | : Логично, если данное сообщение показывает пользователю {{/С2}}. | ||
+ | |||
+ | Пользователь не доверяет {{/С1}} и запретил {{/С2}} передавать ей свои авторизационные данные. | ||
+ | : Логично, если {{/С2}} покажет пользователю ошибку и/или страницу настройки доверия к {{/С1}}. | ||
+ | |||
+ | {{/С1}} не ответила или ответ не в формате JSON. MIME-тип, опять-таки, не проверяется. | ||
+ | : '''Сайт XXX ''(описание: TEXT)'' недоступен с сервера авторизации. Код HTTP: XXX; MIME-тип ответа: XXX.''' | ||
+ | |||
+ | {{/С1}} ответила хешем в формате JSON, в котором присутствует поле «<tt>error</tt>» с непустым значением EEE: | ||
+ | : '''Сайт XXX ''(описание: TEXT)'' сообщает об ошибке авторизации: EEE.''' | ||
+ | : Ошибками EEE могут быть, например: | ||
+ | :* ID неизвестен клиенту: <br /> '''Попытка авторизоваться по неверному или устаревшему идентификатору сессии, попробуйте ещё раз ''(ссылка)''.''' | ||
+ | :* Известному на клиенте ID соответствует неверный ключ KEY: <br /> '''Секретный ключ неверен, проверка подлинности сервера авторизации XXX не удалась. Возможно, вас дурят.''' | ||
+ | :* Не вышло декодировать DATA или считать NOLOGIN: <br /> '''Формат данных, переданных с сервера авторизации, неизвестен.''' ''Опционально — показать также и сами данные.'' | ||
+ | :* Не удалось сохранить данные в хранилище состояний клиента (базе данных, кэше и т. п.): <br /> '''Внутренняя ошибка клиента — хранилище состояний недоступно.''' | ||
|- | |- | ||
|valign=top align=center| 4 | |valign=top align=center| 4 | ||
− | |valign=top| | + | |valign=top| {{/С2}} → |
|valign=top| (перенаправление браузера пользователя) → | |valign=top| (перенаправление браузера пользователя) → | ||
− | |valign=top| | + | |valign=top| {{/С1}} с параметрами {{/p}}ga_client=1&ga_id=ID&ga_res=CODE</tt> |
* CODE — HTTP-код статуса, полученный от POST-запроса из предыдущего пункта. | * CODE — HTTP-код статуса, полученный от POST-запроса из предыдущего пункта. | ||
− | ⇒ | + | ⇒ {{/С1}} может взять сохранённые в предыдущем пункте данные и на их основе авторизовать пользователя. |
+ | |valign=top| | ||
+ | ID неизвестен клиенту: | ||
+ | : '''Попытка авторизации по неверному или устаревшему идентификатору сессии, либо сервер авторизации не смог передать данные учётной записи. Попробуйте ещё раз ''(ссылка)''.''' | ||
+ | |||
+ | <span style="color:red">Самая противная ошибка</span> — это ''Redirect-Loop'' — бесконечное перенаправление, которое можно спровоцировать, если при ошибке авторизации никак не сохранить факт ошибки в браузере пользователя (например, с помощью cookie) и вместо того, чтобы показать страницу с ошибкой, отправить пользователя обратно на шаг 2 (через шаг 1). | ||
|} | |} | ||
+ | |||
+ | Важный момент: после успешной обработки любого запроса глобальной авторизации (запроса с параметрами ga_id или ga_client и т. п.), пришедшего со стороны браузера пользователя, как клиентской, так и серверной стороной, должно осуществляться перенаправление (<tt>HTTP 302 Moved temporarily</tt>) на адрес, не содержащий в себе параметров ga_id и т. п., для исключения повторной отправки запроса пользователем при нажатии «Обновить страницу». | ||
И ID и ключ являются «секретными», но ID знают и сервера, и пользователь (ID передаётся в браузер), а ключ — только сами сервера. За счёт этого достигается безопасность: пользователь не может сам передать произвольные данные авторизации на сервер, не зная ключа. Для дополнительной защиты всё это можно просто пустить через HTTPS (SSL). Понятно, что кэш, в котором сохраняются соответствия ID и ключа, не должен быть доступен для чтения внешнему пользователю, иначе вся защита накрывается медным тазом. Для усиления защиты опять-таки можно дополнительно создать список доверенных серверов и при приёме данных авторизации проверять IP. | И ID и ключ являются «секретными», но ID знают и сервера, и пользователь (ID передаётся в браузер), а ключ — только сами сервера. За счёт этого достигается безопасность: пользователь не может сам передать произвольные данные авторизации на сервер, не зная ключа. Для дополнительной защиты всё это можно просто пустить через HTTPS (SSL). Понятно, что кэш, в котором сохраняются соответствия ID и ключа, не должен быть доступен для чтения внешнему пользователю, иначе вся защита накрывается медным тазом. Для усиления защиты опять-таки можно дополнительно создать список доверенных серверов и при приёме данных авторизации проверять IP. | ||
Строка 64: | Строка 109: | ||
OpenID, на самом деле, работает похоже, но ''почему-то'' адски глючит и его страшно использовать на своих внутренних ресурсах. :) | OpenID, на самом деле, работает похоже, но ''почему-то'' адски глючит и его страшно использовать на своих внутренних ресурсах. :) | ||
− | + | В реализации протокола очень желательна обработка ошибок и их показ пользователю в удобном виде, чтобы не получалось, как в OpenID — «она утонула» © (то есть «произошла ошибка» без каких-либо деталей). | |
− | == | + | == FoF_Sudo == |
− | + | FoF_Sudo — беспарольная авторизация типа «от системы к системе». | |
− | + | Идея такая: пусть есть некая система (например [[lib:FeedOnFeeds|FoF]] — фидридер), в которой действует глобальная авторизация (в качестве сервера выступает другая система), и пусть эта система должна авторизоваться на доверенных серверах (в примере — FoF должен забирать защищённые RSS-ленты от имени разных пользователей). Но при этом (!) пароль пользователя хранить в открытом виде в базе FoF при этом нельзя. Да если бы и было можно, в данных глобальной авторизации его просто нету. При этом нужно, чтобы при передаче такой вот беспарольной авторизации какой-то совершенно левый пользователь не смог сделать так же и зайти под произвольным пользователем. | |
− | * FoF видит в урле рсс’ки &fof_sudo= | + | Как это сделать? Ответ — одноразовые ключи на каждый запрос: |
+ | |||
+ | * FoF видит в урле рсс’ки &fof_sudo=(что_угодно). Что_угодно — совершенно что угодно, единственно, в случае FoF оно должно быть разное для разных пользователей, иначе FoF двух пользователей подпишет на один и тот же защищённый фид, а обновлять будет вообще как попало — то от имени одного, то от имени другого пользователя. | ||
+ | * FoF генерирует случайный ID, запоминает соответствие этому ID пользователя и добавляет в запрос кукис: | ||
** Cookie: fof_sudo_id=ID | ** Cookie: fof_sudo_id=ID | ||
− | * | + | * Система, к которой произошло обращение, видит этот кукис (fof_sudo_id) и делает обратный запрос к fof: /fof-sudo.php?id=ID |
− | * FoF отвечает {'user_name':'user@custis.ru'} и забывает ID | + | * FoF отвечает данными пользователя, которые он помнит по этому ID, в формате JSON ({'user_name':'user@custis.ru'}) и забывает ID |
− | * | + | * Внешняя система верит, что теперь надо авторизоваться под именем юзера user@custis.ru (и, например, отдаёт правильную RSS’ку) |
+ | |||
+ | {{Box|{{Warning}} И это тоже покрывается OAuth2.}} | ||
[[Категория:Разработка]] | [[Категория:Разработка]] |
Текущая версия на 18:50, 1 марта 2017
…или как реализовать простой Single Sign-on в веб-системах.
Ниже описан простейший протокол, который даёт возможность нам сказать внешней системе, кто к нам вошёл, так, что внешняя система знает, что это говорим ей именно мы, а мы знаем, что мы говорим это именно ей.
- (П) Пользователь.
- (C1) Система 1 — клиент глобальной авторизации. В неё пришёл пользователь без авторизации.
- (С2) Система 2 — сервер глобальной авторизации. В ней пользователь уже авторизован, скорее всего, через cookie.
шаг | кто | что делает | кому | возможные ошибки |
---|---|---|---|---|
(П) → | (переходит по ссылке) → | C1 с любыми параметрами. ⇒ C1 хочет перенять авторизацию у С2. |
||
1 | C1 → | (делает GET-запрос напрямую) → | С2 с параметрами ga_id=ID&ga_key=KEY ⇒ С2 запоминает соответствие ID и KEY. |
С2 не ответила или ответ не в формате JSON («по факту», а не по MIME-типу ответа):
С2 ответила хешем в формате JSON, в котором присутствует поле «error» с непустым значением EEE:
|
2 | C1 → | (перенаправление браузера пользователя) → | С2 с параметрами ga_id=ID&ga_url=URL&ga_check=CHECK и опционально параметром ga_message=TEXT.
⇒ С2 даётся возможность прочитать cookie пользователя и получить данные о нём. Если это необходимо, на этом же шаге С2 может запросить у пользователь подтверждение передачи его учётных данных внешней системе, и в случае отрицательного решения передать на следующем шаге NOLOGIN. |
URL, на который происходит перенаправление, недоступен пользователю.
В запросе не передан или передан некорректный параметр ga_url, и также отсутствует HTTP-заголовок Referer, по крайней мере такой, который можно распарсить.
|
3 | С2 → | (делает POST-запрос напрямую) → | C1 с параметрами ga_client=1&ga_id=ID&ga_key=KEY&ga_data=DATA&ga_nologin=NOLOGIN
⇒ С1 запоминает соответствие ID и переданных данных. |
ID неизвестен серверу.
CHECK=0 и авторизовать пользователя в С2 невозможно.
Пользователь не доверяет C1 и запретил С2 передавать ей свои авторизационные данные.
C1 не ответила или ответ не в формате JSON. MIME-тип, опять-таки, не проверяется.
C1 ответила хешем в формате JSON, в котором присутствует поле «error» с непустым значением EEE:
|
4 | С2 → | (перенаправление браузера пользователя) → | C1 с параметрами ga_client=1&ga_id=ID&ga_res=CODE
⇒ 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 — «она утонула» © (то есть «произошла ошибка» без каких-либо деталей).
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’ку)