Введение
Начнем с того, что важно уметь различать следующие два понятия: аутентификации и авторизации. Именно с помощью этих терминов почти все клиент-серверные приложения основывают разделение прав доступа в своих сервисах.
Что такое оauth2.0?
Разработку нового Auth мы решили начать с изучения доступных протоколов и технологий. Самый распространённый стандарт авторизации — фреймворк авторизации OAuth2.0.
Стандарт был принят в 2022 году, и за 8 лет протокол меняли и дополняли. RFC стало настолько много, что авторы оригинального протокола решили написать OAuth 2.1, который объединит все текущие изменения по OAuth 2.0 в одном документе. Пока он на стадии черновика.
Актуальная версия OAuth описанна в RFC 6749. Именно его мы и разберем.
OAuth 2.0 — это фреймворк авторизации.
Он описывает, как должно реализовываться взаимодействие между сервисами для обеспечения безопасной авторизации. Многие нюансы описаны достаточно подробно, например, flow взаимодействия узлов между собой, но некоторые отдаются на откуп конкретной реализации.
Особенности:
Разберёмся подробнее в особенностях.
Описание алгоритма расшифрования
Для того, чтобы расшифровать JWE Ciphertext необходимо выполнить следующие действия на VPNKeyTLS:
Проинициализировать устройство (
INIT_DECIPHER_ID
) путем передачи части с заголовком.Внести данные для расшифрования (
DECIPHER_ID
– 1 или более вызовов) и получить в ответ расшифрованные данные.
Что такое grant?
Grant — это данные, которые представляют из себя успешную авторизацию клиента владельцем ресурса, используемые клиентом для получения access token.
Например, когда мы где-либо аутентифицируемся с помощью Google, перед глазами всплывает уведомление. В нём говорится, что такой-то сервис хочет получить доступ к данным о вас или к вашим ресурсам (выводятся запрашиваемые scope-token). Это уведомление называется «Consent Screen».
В момент, когда нажимаем «ОК», в базу данных попадает тот самый grant: записываются данные о том, что такой-то пользователь дал такие-то доступы такому-то сервису. Клиент получает какой-то идентификатор успешной аутентификации, например строку, которая ассоциируется с данными в базе данных.
Существует 4 1 способа получения grant — grant type:
Authorization code
Самый распространённый flow на данный момент. В основном используется для confidential клиентов, но с появлением дополнительной проверки с помощью PKCE, может применяться и для public-клиентов.
Id token
ID Token:
Является основным расширением, которое спецификация OpenID Connect накладывает на протокол OAuth 2.0 и добавляется в ответ на запрос Access Token, если в состав scope включено значение openid;
Является токеном безопасности и содержит информацию об аутентификации пользователя с использованием СбреБизнес ID для входа в Приложение Партнера;
Представляет собой JSON Web Token (JWT).
В соответствии со спецификации JSON Web Token (JWT) ID Token представлен структурой вида:
Алгоритм подписи и тип токена (Header);
Полезная нагрузка (Payload);
Электронная подпись (Signature).
Каждая часть ответа, разделенная точкой должна декодироваться отдельно. Для декодирования ID Token следует воспользоваться алгоритмом Base64URL Encoding. Для проверки подписи в поле id_token на стороне Приложения Партнера, необходимо вычислить подпись публичным ключом Банка, декодировав блок Header и Payload по Base64URL (содержимое между двумя точками).
Resource owner password credentials flow
По текущим рекомендациям безопасности описанных в
, данный flow не рекомендуется использовать вовсе из-за явных проблем с безопасностью.
Абстрактный oauth 2.0. flow c применением access token
Мы рассмотрели роли, рассмотрели виды токенов, а также как выглядят scope. Посмотрим на flow предоставления доступа к сервису.
Ниже представлена абстрактная схема (или flow) взаимодействия между участниками. Все шаги на данной схеме выполняются строго сверху вниз. Разберём детальнее.
Клиент получает одобрение от resource owner, на основе которого ему выдаётся доступ к ресурсу. Всё просто. А будет ли так же просто, если мы добавим в эту схему работу с refresh token?
Абстрактный oauth 2.0. flow c применением refresh token
Первый и второй шаги опущены из данной схемы — они ничем не отличаются от схемы абстрактного flow выше.
Схема подробнее:
Заголовок
Это первая часть токена. Она служит прежде всего для хранения информации о токене, которая должна рассказать о том, как нам прочитать дальнейшие данные, передаваемые JWT. Заголовок представлен в виде JSON объекта, закодированного в Base64-URL Например:
Если раскодировать данную строку получим:
Задача auth
Проблема авторизации в десятках сервисов встречалась ещё несколько лет назад — в начале «
». Эту проблему решили новым сервисом, который назвали – Auth. Он помог реализовать бесшовную аутентификацию в различных сервисах и перенести данные о пользователях в отдельные базы данных.
У сервиса Auth есть три основные задачи:
Коды возврата
В случае возникновения ошибок при вызове ресурса /v2/oauth/token в ответ будут возвращены следующие параметры:
Параметр | Описание |
---|---|
error | Код возникшей ошибки |
error_description | Описание возникшей ошибки |
Перечень исключений, которые могут быть возвращены при вызове ресурса /v2/oauth/token:
Также могут быть получены следующие параметры в ответе при обращении к серверу:
Параметр | Описание |
---|---|
errorCode | Код возникшей ошибки |
errorMsg | Описание возникшей ошибки |
Данный тип параметров возвращается, если на сервере возникли следующие исключения:
Обновление токена
Токен полученный нами ранее имеет ограниченное время жизни, которое задано в поле
«expires_in»
. После того как его время жизни истечет, пользователь не сможет получить новые данные, отправляя данный токен в запросе, поэтому нужно получить новый токен.
Параметры запроса
Параметры | Ограничение | Описание |
---|---|---|
id | INIT_DECIPHER_ID | Тип = ID_FORMAT Соответствует id функции |
head | Для прошивок 399 : 2048 URL-encoded байтов base64; Для прошивок 500 : 2048 байт бинарных данных (размер base64, требуемый для их кодировки, не регламентирован) | Тип = BASE64 Данные начала CMS в base64 |
obj_id | 8 байт | Тип = HANDLE Id сертификата для расшифрования *с версии 500 игнорируется |
mode | 1,2 | Тип = ENUM Стандарт крипто операции: КС1 – токен только генерирует ключевую пару, данные шифруются «стартом.ехе» / КС2 – токен генерирует ключевую пару и шифрует данные. *Только нечетные версии 500 ; Опционально; по умолчанию: 2. |
Параметры запроса access token
Параметры, которые необходимо передавать при вызове ресурса /v2/oauth/token при запросе Access Token.
Параметры запроса на обновление access token
При обмене кода авторизации на токен, СберБизнес ID возвращает пару access_token и refresh_token.
Полученный refresh_token применяется для обновления access_token после истечения срока его действия или в случае его компрометации.
При использовании refresh_token формируется новая пара access_token и refresh_token.
Если по каким-то причинам сформированная пара не была получена от банка, то рекомендуется повторно отправить запрос на актуализацию ключей в течение 1 часа от момента отправки первой попытки, используя тот же refresh_token.
Для последующих обновлений ключей доступа необходимо использовать refresh_token из новой пары ключей (access_token/refresh_token).
Для обновления access_token по refresh_token, необходимо вызывать ресурс /v2/oauth/token со следующими параметрами:
Параметр | Описание |
---|---|
Заголовок | |
Content-Type | Обязательный параметр. Должен содержать значение application/x-www-form-urlencoded |
Accept | Необязательный параметр. |
Если ответ не требуется в зашифрованном виде, то может быть не указан или передан со значением application/json. | |
Если необходимо получить ответ на запрос токена в зашифрованном виде, то необходимо передать параметр со значением application/jose. | |
Параметры запроса (entity body) | |
grant_type | Обязательный параметр. Значение должно быть refresh_token |
client_id | Обязательный параметр. Уникальный идентификатор Приложения Партнера, полученный при регистрации приложения. |
refresh_token | Обязательный параметр. Значение refresh_token полученное при обмене кода авторизации на access_token |
client_secret | Обязательный параметр. Секрет Приложения Партнера, полученный при регистрации приложения. |
Параметры ответа
Параметры | Ограничение | Описание |
---|---|---|
retcode | 2 байт | Тип = ENUM код возврата функции |
ctx_handle | 8 байт | Тип = HANDLE хендл контекста операции |
body_displ | 2^32 – 1 | Тип = NUMBER Размер (смещение) начала CMS в чистом виде (до кодирования base64) |
Полезные данные
Перейдем наконец к полезным данным. Опять же – это JSON объект, который для удобства и безопасности передачи представляется строкой, закодированной в base64. Наглядный пример полезных данных (playload) токена может быть представлен следующей строкой:
Что в JSON формате представляет собой:
Полезные ссылки
5 Easy Steps to Understanding JSON Web Tokens (JWT)
JWT — как безопасный способ аутентификации и передачи данных
Securing React Redux Apps With JWT Tokens
Зачем нужен Refresh Token, если есть Access Token?
Получение токена
Теперь создадим функцию, которая будет получать json, описанный выше, и сохранять его.
Хранить данные для авторизации мы будем в sessionStorage или localStorage, в зависимости от наших нужд. В первом случае данные хранятся до тех пор, пока пользователь не завершит сеанс или не закроет браузер, во втором случае данные в браузере будут храниться неограниченное время, пока по каким-либо причинам localStorage не будет очищен.
Права доступа
Права доступа выдаются клиенту в виде scope. Scope – это параметр, который состоит из разделённых пробелами строк — scope-token.
Каждый из scope-token представляет определённые права, выдающиеся клиенту. Например, scope-token doc_read может предоставлять доступ на чтение к какому-то документу на resource server, а employee — доступ к функционалу приложения только для работников фирмы. Итоговый scope может выглядеть так: email doc_read employee.
В OAuth 2.0 мы сами создаём scope-token, настраивая их под свои нужды. Имена scope-token ограничиваются только фантазией и двумя символами таблицы ASCII — ” и .
На этапе регистрации клиента, в настройках сервиса авторизации клиенту выдаётся стандартный scope по умолчанию. Но клиент может запросить у сервера авторизации scope, отличный от стандартного. В зависимости от политик на сервере авторизации и выбора владельца ресурса, итоговый набор scope может выглядеть совсем иначе.
Пример декодированного (из base64) параметра ответа deciphered_blob
Обновлено20 апреля 2022
Пример зашифрованного access token и расшифроки его заголовка
Пример JWE Compact Serialization
Пример декодированного (из BASE64URL) Protected Header
Пример ответа
После декодирования из BASE64URL параметра ответа deciphered_blob получаем ответ в виде JSON
Проблемы
Первая версия Auth — часть монолита. Он использует свой собственный протокол общения с сервисами. Такая «схема» была необходима в тот момент, но за несколько лет работы проявились проблемы.
Auth — часть монолита. Следовательно, сервис привязан к релизному циклу, что лишает возможности независимой разработки и деплоя. Кроме того, придется разворачивать весь монолит, если захотелось развернуть Auth, например, при масштабировании сервиса.
Dodo IS зависит от Auth. В старой реализации внешние сервисы обращаются к Auth при каждом действии пользователя, чтобы валидировать данные о нём. Настолько сильная привязка может привести к остановке работы всей Dodo IS, если Auth «приляжет» по какой-то причине.
Auth зависит от Redis. Притом достаточно сильно — неисправность работы Redis’а приведёт к падению Auth’а. Мы используем Azure Redis, для которого заявленный SLA 99,9%. Это значит, что сервис может быть недоступен до 44 минут в месяц. Такие простои не позволительны.
Текущая реализация Auth использует свой протокол аутентификации, не опираясь на стандарты. В большинстве своих сервисов мы используем C# (если говорим о backend) и у нас нет проблем с поддержкой библиотеки для нашего протокола. Но если вдруг появятся сервисы на Python, Go или Rust, разработка и поддержка библиотек под эти языки потребует дополнительных затрат времени и принесет дополнительные сложности.
Текущий Auth использует схему Roles Based Access Control, которая базируется на ролях. Обычно с ролью выдаётся полный доступ к определённому сервису, вместо привязки к конкретному функционалу. Например, в пиццериях есть заместители управляющего, которые могут вести определенные проекты: составлять графики или учитывать сырьё.
Проблемы подтолкнули к тому, чтобы спроектировать и написать новую версию Auth. На старте проекта мы потратили 3 недели только на изучение стандартов авторизации и аутентификации OAuth 2.0 и OpenID Connect 1.0.
Примечание. Утрированно, статья — это пересказ RFC, который приходилось перечитывать несколько раз, чтобы понять, что происходит вокруг. Здесь я постарался уйти от этой сложности и рассказать всё максимально просто, структурировано, кратко и без описания сложных вещей, например, какие символы может содержать в себе ответ сервиса.
Регистрация клиента
Способ регистрации клиента, например, ручной или service discovery, вы выбираете сами, в зависимости от
фантазии
конкретной реализации. Но при любом способе при регистрации, кроме ID клиента, должны быть обязательно указаны 2 параметра: redirection URI и client type.
Redirection URI — адрес, на который отправится владелец ресурса после успешной авторизации. Кроме авторизации, адрес используется для подтверждения, что сервис, который обратился за авторизацией, тот, за кого себя выдаёт.
Client type — тип клиента, от которого зависит способ взаимодействия с ним. Тип клиента определяется его возможностью безопасно хранить свои учётные данные для авторизации — токен. Поэтому существует всего 2 типа клиентов:
Ресурс /v2/oauth/token
Ресурс /v2/oauth/token используется для получения Access Token.
Host и полный url для вызова ресурса на тестовом и промышленном контурах см. в разделе URL для вызова API СберБизнес ID.
Ресурс /v2/oauth/token позволяет получать Access Token путем обмена кода авторизации (см. получение кода авторизации в разделе Authorization code или токена обмена (Refresh Token), который предоставляется вместе с Access Token.
Направления взаимодействия, описанные в разделе, выделены на схеме красным.
При запросе ресурса /v2/oauth/token используются параметры, полученные при регистрации приложения Партнера.
Создание функции-обертки
Теперь создадим функцию, которая будет добавлять данные для авторизации в шапку запроса, и при необходимости их автоматически обновлять перед совершением запроса.
Так как в случае, если срок жизни токена истек, нам надо будет делать запрос нового токена, то наша функция будет асинхронной. Для этого мы будем использовать конструкцию async/await.
Структура токена
Пришло время обсудить структуру токена и тем самым лучше разобраться в его работе. Первое что следует отметить, что JWT токен состоит из трех частей, разделенных через точку:
Заголовок (header)
Полезные данные (playload)
Подпись (signature)

Рассмотрим каждую часть по подробнее.
Токены
Токен в OAuth 2.0 — это строка, непрозрачная для клиента. Обычно строка выглядит как случайно сгенерированная — её формат не имеет значения для клиента. Токен — это ключ доступа к чему-либо, например, к защищённому ресурсу (access token) или к новому токену (refresh Token).
У каждого токена своё время жизни. Но у refresh token оно должно быть больше, т.к. он используется для получения access token. Например, если срок жизни access token около часа, то refresh token можно оставить жить на целую неделю.
Refresh token опционален и доступен только для confedential клиентов. Пользуясь опциональностью токена, в некоторых реализациях время жизни access token сделано очень большим, а refresh token вообще не используется, чтобы не заморачиваться с обновлением.
За access token закреплён определённый набор прав доступа, который выдаётся клиенту во время авторизации. Давайте разберёмся, как выглядят права доступа в OAuth 2.0.
Формальное определение
Приступим наконец к работе самого токена. Как я сказал ранее в качестве токенов наиболее часто рассматривают JSON Web Tokens (JWT) и хотя реализации бывают разные, но токены JWT превратились в некий стандарт, именно поэтому будем рассматривать именно на его примере.
JSON Web Token (JWT) — это открытый стандарт (RFC 7519) для создания токенов доступа, основанный на формате JSON.
Фактически это просто строка символов (закодированная и подписанная определенными алгоритмами) с некоторой структурой, содержащая полезные данные пользователя, например ID, имя, уровень доступа и так далее. И эта строчка передается клиентом приложению при каждом запросе, когда есть необходимость идентифицировать и понять кто прислал этот запрос.
Функция для обновления токена
function refreshToken(token) {
return fetch('api/auth/refreshToken', {
method: 'POST',
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
token,
}),
})
.then((res) => {
if (res.status === 200) {
const tokenData = res.json();
saveToken(JSON.stringify(tokenData)); // сохраняем полученный обновленный токен в sessionStorage, с помощью функции, заданной ранее
return Promise.resolve();
}
return Promise.reject();
});
}
С помощью кода выше мы перезаписали токен в sessionStorage и теперь по новой можем отправлять запросы к api.
Функция для получения токена:
function getTokenData(login, password) {
return fetch('api/auth', {
method: 'POST',
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
login,
password,
}),
})
.then((res) => {
if (res.status === 200) {
const tokenData = res.json();
saveToken(JSON.stringify(tokenData)); // сохраняем полученный токен в sessionStorage, с помощью функции, заданной ранее
return Promise.resolve()
}
return Promise.reject();
});
}
Таким образом мы получили токен с полями
«access_token»«refresh_token»«expires_in»
и сохранили его в
sessionStorage
для дальнейшего использования.
Функция для сохранения токена в sessionstorage:
function saveToken(token) {
sessionStorage.setItem('tokenData', JSON.stringify(token));
}
Функция-обертка
export async function fetchWithAuth(url, options) {
const loginUrl = '/login'; // url страницы для авторизации
let tokenData = null; // объявляем локальную переменную tokenData
if (sessionStorage.authToken) { // если в sessionStorage присутствует tokenData, то берем её
tokenData = JSON.parse(localStorage.tokenData);
} else {
return window.location.replace(loginUrl); // если токен отсутствует, то перенаправляем пользователя на страницу авторизации
}
if (!options.headers) { // если в запросе отсутствует headers, то задаем их
options.headers = {};
}
if (tokenData) {
if (Date.now() >= tokenData.expires_on * 1000) { // проверяем не истек ли срок жизни токена
try {
const newToken = await refreshToken(tokenData.refresh_token); // если истек, то обновляем токен с помощью refresh_token
saveToken(newToken);
} catch () { // если тут что-то пошло не так, то перенаправляем пользователя на страницу авторизации
return window.location.replace(loginUrl);
}
}
options.headers.Authorization = `Bearer ${tokenData.token}`; // добавляем токен в headers запроса
}
return fetch(url, options); // возвращаем изначальную функцию, но уже с валидным токеном в headers
}
С помощью кода выше мы создали функцию, которая будет добавлять токен к запросам в api. На эту функцию мы можем заменить fetch в нужных нам запросах, где требуется авторизация и для этого нам не потребуется менять синтаксис или добавлять в аргументы еще какие-либо данные.
Просто достаточно будет «импортнуть» ее в файл и заменить на нее стандартный fetch.
import fetchWithAuth from './api';
function getData() {
return fetchWithAuth('api/data', options)
}
Вместо вывода
В этой статье я опустил много подробностей, чтобы максимально просто и доступно рассказать о самом важном. Например, типы запросов, как и в каком виде передавать параметры, какие символы допустимы в качестве значений для того.
Если хотите погрузиться в тематику детальнее, то рекомендую в RFC 6749 (для OAuth 2.0) и RFC 8628 (для Device Flow). Кроме того, следить за актуальными версиями RFC можно на ресурсе, посвящённому OAuth.
Если статья была полезна и захотите подробностей — пишите в комментариях, и в следующих статьях расскажу о PKCE, о протоколе аутентификации OpenID Connect 1.0, о нашей реализации сервера аутентификации и многом другом.
Полезные ссылки:
Заключение
В данной статье я постарался подробно рассмотреть работу клиент-серверных приложений с токеном доступа, а конкретно на примере JSON Web Token (JWT). Еще раз хочется отметить с какой сравнительной легкостью, но в тоже время хорошей надежностью, токен позволяет решать проблемы аутентификации и авторизации, что и сделало его таким популярным. Спасибо за уделенное время.