Веб-приложения создают с использованием клиент-серверной архитектуры, применяя в качестве коммуникационного протокола HTTP. HTTP — это протокол без сохранения состояния. Каждый раз, когда браузер отправляет серверу запрос, сервер обрабатывает этот запрос независимо от других запросов и не связывает его с предыдущими или последующими запросами того же самого браузера. Это, кроме прочего, означает, что получить доступ к серверным ресурсам, которые никак не защищены, может кто угодно. Если нужно защитить от посторонних некие серверные ресурсы, это значит, что нужно как-то ограничить то, что может запрашивать у сервера браузер. То есть — нужно аутентифицировать запросы и отвечать только на те из них, которые прошли проверку, игнорируя те, которые проверку не прошли. Для аутентификации запросов нужно владеть некими сведениями о запросах, хранящимися на стороне браузера. Так как протокол HTTP не хранит состояние запросов, нам для этого нужны некие дополнительные механизмы, которые позволяют серверу и браузеру совместно управлять состоянием соединений. Среди таких механизмов можно отметить использование куки-файлов, сессий, JWT.

Если речь идёт о каком-то одном веб-проекте, то сведения о состоянии конкретного сеанса взаимодействия клиента и сервера легко поддерживать с применением аутентификации пользователя при его входе в систему. Но если такая вот самостоятельная система эволюционирует, превращаясь в несколько систем, перед разработчиком встаёт вопрос о поддержании сведений о состоянии каждой из этих отдельных систем. На практике этот вопрос выглядит так: «Придётся ли пользователю этих систем входить в каждую из них по-отдельности и так же из них выходить?».
Есть одно хорошее правило, касающееся систем, сложность которых со временем растёт, и взаимодействия этих систем с их пользователями. А именно, нагрузка по решению задач, связанных с усложнением архитектуры проекта, ложится на систему, а не на её пользователей. При этом неважно то, насколько сложны внутренние механизмы веб-проекта. Для пользователя он должен выглядеть единой системой. Иными словами, пользователь, работающий с веб-системой, состоящей из множества компонентов, должен воспринимать происходящее так, будто он работает с одной системой. В частности, речь идёт об аутентификации в таких системах с использованием SSO (Single Sign-On) — технологии единого входа.
Как создавать системы, в которых используется SSO? Тут можно вспомнить старое доброе решение, основанное на куки-файлах, но это решение подвержено ограничениям. Ограничения касаются доменов, с которых устанавливаются куки. Обойти его можно, лишь собрав все доменные имена всех подсистем веб-приложения на одном домене верхнего уровня.
В современных условиях таким решениям препятствует широкое распространение микросервисных архитектур. Управление сессиями усложнилось в тот момент, когда при разработке веб-проектов стали использовать различные технологии, и когда разные службы иногда размещались на разных доменах. Кроме того, веб-службы, которые раньше писали на Java, начали писать, пользуясь возможностями платформы Node.js. Это усложнило работу с куки-файлами. Оказалось, что сессиями теперь управлять не так уж и просто.
Эти сложности привели к разработке новых методов входа в системы, в частности, речь идёт о технологии единого входа.
Базовый принцип, на котором основана технология единого входа, заключается в том, что пользователь может войти в одну систему проекта, состоящего из множества систем, и оказаться авторизованным и во всех остальных системах без необходимости повторного входа в них. При этом речь идёт и о централизованном выходе из всех систем.
Мы, в учебных целях, собираемся реализовать технологию SSO на платформе Node.js.
Надо отметить, что реализация этой технологии в корпоративных масштабах потребует ��ораздо больших усилий, чем мы собираемся приложить к разработке нашей учебной системы. Именно поэтому и существуют специализированные SSO-решения, предназначенные для крупномасштабных проектов.
В сердце реализации SSO находится единственный независимый сервер аутентификации, который способен принимать информацию, позволяющую аутентифицировать пользователей. Например — адрес электронной почты, имя пользователя, пароль. Другие системы не дают пользователю прямых механизмов входа в них. Они авторизуют пользователя непрямым способом, получая сведения о нём от сервера аутентификации. Механизмы непрямой авторизации реализуются с использованием токенов.
Вот репозиторий с кодом проекта simple-sso, реализацию которого я здесь опишу. Я использую платформу Node.js, но вы можете реализовать то же самое и используя что-то другое. Давайте пошагово разберём действия пользователя, работающего с системой, и механизмы, из которых состоит эта система
Пользователь пытается получить доступ к защищённому ресурсу системы (назовём этот ресурс «потребителем SSO», «sso-consumer»). Потребитель SSO выясняет то, что пользователь не вошёл в систему, и перенаправляет пользователя на «сервер SSO» («sso-server»), используя, в качестве параметра запроса, собственный адрес. На этот адрес будет перенаправлен пользователь, успешно прошедший проверку. Этот механизм представлен ПО промежуточного слоя для Express:
SSO-сервер выясняет то, что пользователь в систему не вошёл, и перенаправляет его на страницу входа в систему:
Сделаю тут некоторые комментарии относительно безопасности.
Мы проверяем
Вот как может выглядеть список URL служб, которым разрешено использование SSO-сервера:
Пользователь вводит имя пользователя и пароль, которые отправляются SSO-серверу в запросе на вход в систему.

Страница входа в систему
SSO-сервер аутентификации проверяет информацию пользователя и создаёт сессию между собой и пользователем. Это — так называемая «глобальная сессия». Тут же создаётся и токен авторизации. Токен представляет собой строку, состоящую из случайных символов. То, как именно генерируется эта строка, значения не имеет. Главное — это чтобы подобные строки у разных пользователей не повторялись, и чтобы такую строку сложно было бы подделать.
SSO-сервер берёт токен авторизации и передаёт его туда, откуда к нему пришёл только что авторизовавшийся пользователь (то есть — передаёт токен потребителю SSO).
Снова сделаю некоторые замечания о безопасности:
Потребитель SSO получает токен и обращается к серверу SSO для проверки токена. Сервер проверяет токен и возвращает ещё один токен с информацией о пользователе. Этот токен используется потребителем SSO для создания сессии с пользователем. Эта сессия называется локальной.
Вот код ПО промежуточного слоя, используемого в потребителе SSO, построенном на основе Express:
После получения запроса от потребителя SSO сервер проверяет токен на предмет его существования и срока его действия. Токен, прошедший проверку, считается действительным.
В нашем случае сервер SSO, после успешной проверки токена, возвращает подписанный JWT с информацией о пользователе.
Вот некоторые замечания о безопасности.
Кроме того, можно определить политику безопасности уровня приложения и организовать её централизованное хранение:
После того, как пользователь успешно войдёт в систему, создаются сессии между ним и SSO-сервером, а так же между ним и каждой подсистемой. Сессия, установленная между пользователем и SSO-сервером, называется глобальной сессией. Сессия, установленная между пользователем и подсистемой, предоставляющей пользователю какие-то услуги, называется локальной сессией. После того, как будет установлена локальная сессия, пользователь сможет работать с закрытыми для посторонних ресурсами подсистемы.

Установка локальной и глобальной сессий
Давайте сделаем краткий обзор функционала потребителя SSO и сервера SSO.
Аналогично тому, как была реализована технология единого входа, можно реализовать и «технологию единого выхода». Здесь нужно лишь учитывать следующие соображения:
В итоге можно отметить, что существует множество готовых реализаций технологии единого входа, которые можно интегрировать в свою систему. У всех них есть собственные преимущества и недостатки. Разработка подобной системы самостоятельно, с нуля, это — итеративный процесс, в ходе которого нужно анализировать характеристики каждой из систем. Сюда входят способы входа в систему, хранилища пользовательской информации, синхронизация данных и многое другое.
Используются ли в ваших проектах механизмы SSO?


Если речь идёт о каком-то одном веб-проекте, то сведения о состоянии конкретного сеанса взаимодействия клиента и сервера легко поддерживать с применением аутентификации пользователя при его входе в систему. Но если такая вот самостоятельная система эволюционирует, превращаясь в несколько систем, перед разработчиком встаёт вопрос о поддержании сведений о состоянии каждой из этих отдельных систем. На практике этот вопрос выглядит так: «Придётся ли пользователю этих систем входить в каждую из них по-отдельности и так же из них выходить?».
Есть одно хорошее правило, касающееся систем, сложность которых со временем растёт, и взаимодействия этих систем с их пользователями. А именно, нагрузка по решению задач, связанных с усложнением архитектуры проекта, ложится на систему, а не на её пользователей. При этом неважно то, насколько сложны внутренние механизмы веб-проекта. Для пользователя он должен выглядеть единой системой. Иными словами, пользователь, работающий с веб-системой, состоящей из множества компонентов, должен воспринимать происходящее так, будто он работает с одной системой. В частности, речь идёт об аутентификации в таких системах с использованием SSO (Single Sign-On) — технологии единого входа.
Как создавать системы, в которых используется SSO? Тут можно вспомнить старое доброе решение, основанное на куки-файлах, но это решение подвержено ограничениям. Ограничения касаются доменов, с которых устанавливаются куки. Обойти его можно, лишь собрав все доменные имена всех подсистем веб-приложения на одном домене верхнего уровня.
В современных условиях таким решениям препятствует широкое распространение микросервисных архитектур. Управление сессиями усложнилось в тот момент, когда при разработке веб-проектов стали использовать различные технологии, и когда разные службы иногда размещались на разных доменах. Кроме того, веб-службы, которые раньше писали на Java, начали писать, пользуясь возможностями платформы Node.js. Это усложнило работу с куки-файлами. Оказалось, что сессиями теперь управлять не так уж и просто.
Эти сложности привели к разработке новых методов входа в системы, в частности, речь идёт о технологии единого входа.
Технология единого входа
Базовый принцип, на котором основана технология единого входа, заключается в том, что пользователь может войти в одну систему проекта, состоящего из множества систем, и оказаться авторизованным и во всех остальных системах без необходимости повторного входа в них. При этом речь идёт и о централизованном выходе из всех систем.
Мы, в учебных целях, собираемся реализовать технологию SSO на платформе Node.js.
Надо отметить, что реализация этой технологии в корпоративных масштабах потребует ��ораздо больших усилий, чем мы собираемся приложить к разработке нашей учебной системы. Именно поэтому и существуют специализированные SSO-решения, предназначенные для крупномасштабных проектов.
Как организован вход в систему с использованием SSO?
В сердце реализации SSO находится единственный независимый сервер аутентификации, который способен принимать информацию, позволяющую аутентифицировать пользователей. Например — адрес электронной почты, имя пользователя, пароль. Другие системы не дают пользователю прямых механизмов входа в них. Они авторизуют пользователя непрямым способом, получая сведения о нём от сервера аутентификации. Механизмы непрямой авторизации реализуются с использованием токенов.
Вот репозиторий с кодом проекта simple-sso, реализацию которого я здесь опишу. Я использую платформу Node.js, но вы можете реализовать то же самое и используя что-то другое. Давайте пошагово разберём действия пользователя, работающего с системой, и механизмы, из которых состоит эта система
Шаг 1
Пользователь пытается получить доступ к защищённому ресурсу системы (назовём этот ресурс «потребителем SSO», «sso-consumer»). Потребитель SSO выясняет то, что пользователь не вошёл в систему, и перенаправляет пользователя на «сервер SSO» («sso-server»), используя, в качестве параметра запроса, собственный адрес. На этот адрес будет перенаправлен пользователь, успешно прошедший проверку. Этот механизм представлен ПО промежуточного слоя для Express:
const isAuthenticated = (req, res, next) => { // простая проверка того, аутентифицирован ли пользователь, // если это не так - нужно перенаправить пользователя на SSO-сервер для входа в систему и // передать серверу текущий URL как URL, на который должен быть перенаправлен // пользователь, успешно прошедший проверку const redirectURL = `${req.protocol}://${req.headers.host}${req.path}`; if (req.session.user == null) { return res.redirect( `http://sso.ankuranand.com:3010/simplesso/login?serviceURL=${redirectURL}` ); } next(); }; module.exports = isAuthenticated;
Шаг 2
SSO-сервер выясняет то, что пользователь в систему не вошёл, и перенаправляет его на страницу входа в систему:
const login = (req, res, next) => { // В req.query будет url, на который надо будет перенаправить пользователя //после успешного входа в систему, туда же надо передать sso-токен. // Эти данные о перенаправлении пользователя ещё можно использовать // для проверки источника поступления запроса const { serviceURL } = req.query; // Попытка прямого доступа приведёт к ошибке в новом URL. if (serviceURL != null) { const url = new URL(serviceURL); if (alloweOrigin[url.origin] !== true) { return res .status(400) .json({ message: "Your are not allowed to access the sso-server" }); } } if (req.session.user != null && serviceURL == null) { return res.redirect("/"); } // если сведения о пользователе уже имеются в глобальной сессии - перенаправить // пользователя с токеном if (req.session.user != null && serviceURL != null) { const url = new URL(serviceURL); const intrmid = encodedId(); storeApplicationInCache(url.origin, req.session.user, intrmid); return res.redirect(`${serviceURL}?ssoToken=${intrmid}`); } return res.render("login", { title: "SSO-Server | Login" }); };
Сделаю тут некоторые комментарии относительно безопасности.
Мы проверяем
serviceURL, поступающий в виде параметра запроса к SSO-серверу. Благодаря этому мы узнаём о том, зарегистрирован ли этот URL в системе, и о том, может ли представленная им служба пользоваться услугами SSO-сервера.Вот как может выглядеть список URL служб, которым разрешено использование SSO-сервера:
const alloweOrigin = { "http://consumer.ankuranand.in:3020": true, "http://consumertwo.ankuranand.in:3030": true, "http://test.tangledvibes.com:3080": true, "http://blog.tangledvibes.com:3080": fasle, };
Шаг 3
Пользователь вводит имя пользователя и пароль, которые отправляются SSO-серверу в запросе на вход в систему.

Страница входа в систему
Шаг 4
SSO-сервер аутентификации проверяет информацию пользователя и создаёт сессию между собой и пользователем. Это — так называемая «глобальная сессия». Тут же создаётся и токен авторизации. Токен представляет собой строку, состоящую из случайных символов. То, как именно генерируется эта строка, значения не имеет. Главное — это чтобы подобные строки у разных пользователей не повторялись, и чтобы такую строку сложно было бы подделать.
Шаг 5
SSO-сервер берёт токен авторизации и передаёт его туда, откуда к нему пришёл только что авторизовавшийся пользователь (то есть — передаёт токен потребителю SSO).
const doLogin = (req, res, next) => { // Выполнить проверку с использованием адреса электронной почты и пароля. // Тут мы не вдаёмся в подробности использования хранилищ данных, поэтому // userDB - это обычный объект, описанный тут же, в коде сервера const { email, password } = req.body; if (!(userDB[email] && password === userDB[email].password)) { return res.status(404).json({ message: "Invalid email and password" }); } // В противном случае выполнить перенаправление const { serviceURL } = req.query; const id = encodedId(); req.session.user = id; sessionUser[id] = email; if (serviceURL == null) { return res.redirect("/"); } const url = new URL(serviceURL); const intrmid = encodedId(); storeApplicationInCache(url.origin, id, intrmid); return res.redirect(`${serviceURL}?ssoToken=${intrmid}`); };
Снова сделаю некоторые замечания о безопасности:
- Этот токен нужно всегда рассматривать в роли промежуточного механизма, он используется для получения другого токена.
- Если вы используете JWT в роли промежуточного токена, постарайтесь не включать в его состав секретные данные.
Шаг 6
Потребитель SSO получает токен и обращается к серверу SSO для проверки токена. Сервер проверяет токен и возвращает ещё один токен с информацией о пользователе. Этот токен используется потребителем SSO для создания сессии с пользователем. Эта сессия называется локальной.
Вот код ПО промежуточного слоя, используемого в потребителе SSO, построенном на основе Express:
const ssoRedirect = () => { return async function(req, res, next) { // проверяется, есть ли в req queryParameter, представляющий ssoToken, // и то, что именно является реферером. const { ssoToken } = req.query; if (ssoToken != null) { // для удаления ssoToken в параметре запроса, задающем перенаправление. const redirectURL = url.parse(req.url).pathname; try { const response = await axios.get( `${ssoServerJWTURL}?ssoToken=${ssoToken}`, { headers: { Authorization: "Bearer l1Q7zkOL59cRqWBkQ12ZiGVW2DBL" } } ); const { token } = response.data; const decoded = await verifyJwtToken(token); // теперь у нас есть декодированный jwt, поэтому используем // global-session-id как id сессии, что позволит // реализовать процедуру выхода из системы с использованием глобальной сессии. req.session.user = decoded; } catch (err) { return next(err); } return res.redirect(`${redirectURL}`); } return next(); }; };
После получения запроса от потребителя SSO сервер проверяет токен на предмет его существования и срока его действия. Токен, прошедший проверку, считается действительным.
В нашем случае сервер SSO, после успешной проверки токена, возвращает подписанный JWT с информацией о пользователе.
const verifySsoToken = async (req, res, next) => { const appToken = appTokenFromRequest(req); const { ssoToken } = req.query; // Если нет токена приложения или запрос на ssoToken недействителен. // Усли ssoToken отсутствует в кеше - значит, нас пытаются обмануть. if ( appToken == null || ssoToken == null || intrmTokenCache[ssoToken] == null ) { return res.status(400).json({ message: "badRequest" }); } // Если appToken присутствует - проверяем его действительность для приложения const appName = intrmTokenCache[ssoToken][1]; const globalSessionToken = intrmTokenCache[ssoToken][0]; // Если appToken не соответствует токену, выданному выданному SSO-приложению при регистрации или на более поздней стадии работы if ( appToken !== appTokenDB[appName] || sessionApp[globalSessionToken][appName] !== true ) { return res.status(403).json({ message: "Unauthorized" }); } // проверяем, был ли сгенерирован переданный токен const payload = generatePayload(ssoToken); const token = await genJwtToken(payload); // удаляем из кеша ключ, который больше использоваться не будет delete intrmTokenCache[ssoToken]; return res.status(200).json({ token }); };
Вот некоторые замечания о безопасности.
- На SSO-сервере нужно зарегистрировать все приложения, которые будут использовать этот сервер для аутентификации. Им нужно назначить коды, которые будут использовать для их верификации при выполнении ими запросов к серверу. Это позволяет добиться более высокого уровня безопасности при организации взаимодействия сервера SSO и потребителей SSO.
- Можно сгенерировать различные «приватные» и «публичные» rsa-файлы для каждого приложения и позволить каждому из них верифицировать своими силами их JWT с помощью соответствующих публичных ключей.
Кроме того, можно определить политику безопасности уровня приложения и организовать её централизованное хранение:
const userDB = { "info@ankuranand.com": { password: "test", userId: encodedId(), // в том случае, если вы не хотите передавать адрес электронной почты пользователя. appPolicy: { sso_consumer: { role: "admin", shareEmail: true }, simple_sso_consumer: { role: "user", shareEmail: false } } } };
После того, как пользователь успешно войдёт в систему, создаются сессии между ним и SSO-сервером, а так же между ним и каждой подсистемой. Сессия, установленная между пользователем и SSO-сервером, называется глобальной сессией. Сессия, установленная между пользователем и подсистемой, предоставляющей пользователю какие-то услуги, называется локальной сессией. После того, как будет установлена локальная сессия, пользователь сможет работать с закрытыми для посторонних ресурсами подсистемы.

Установка локальной и глобальной сессий
Краткий обзор потребителя SSO и сервера SSO
Давайте сделаем краткий обзор функционала потребителя SSO и сервера SSO.
▍Потребитель SSO
- Подсистема-потребитель SSO не выполняет аутентификацию пользователя, перенаправляя пользователя на сервер SSO.
- Эта подсистема получает токен, передаваемый ей сервером SSO.
- Она взаимодействует с сервером, проверяя действительность токена.
- Она получает JWT и проверяет этот токен с использованием публичного ключа.
- Эта подсистема устанавливает локальную сессию.
▍Сервер SSO
- Сервер SSO проверяет данные, вводимые пользователем для входа в систему.
- Сервер создаёт глобальную сессию.
- Он создаёт токен авторизации.
- Токен авторизации отправляется потребителю SSO.
- Сервер проверяет действительность токенов, передаваемых ему потребителями SSO.
- Сервер отправляет потребителю SSO JWT с информацией о пользователе.
Организация централизованного выхода из системы
Аналогично тому, как была реализована технология единого входа, можно реализовать и «технологию единого выхода». Здесь нужно лишь учитывать следующие соображения:
- Если существует локальная сессия — обязательно существует и глобальная сессия.
- Если существует глобальная сессия, это необязательно означает существование локальной сессии.
- Если локальная сессия уничтожается — должна быть уничтожена и глобальная сессия.
Итоги
В итоге можно отметить, что существует множество готовых реализаций технологии единого входа, которые можно интегрировать в свою систему. У всех них есть собственные преимущества и недостатки. Разработка подобной системы самостоятельно, с нуля, это — итеративный процесс, в ходе которого нужно анализировать характеристики каждой из систем. Сюда входят способы входа в систему, хранилища пользовательской информации, синхронизация данных и многое другое.
Используются ли в ваших проектах механизмы SSO?

