Skip to content
 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

oidc-client

Вход через провайдера OpenID Connect и чистого OAuth 2.0 для OneScript / ОСени. Authorization Code Flow + PKCE (S256): discovery, URL авторизации, обмен кода на токены (server-to-server), userinfo, проверка клеймов id_token, выход у провайдера. Для провайдеров без OIDC (GitHub) — OAuth2-ветка: ручные конечные точки, профиль из userinfo, двухшаговый добор подтверждённого email. Пресеты GitHub / GitLab / Google.

Публичный интерфейс — один класс и пять методов. Всё остальное лежит в src/internal и меняется без оглядки на обратную совместимость.

Вход   = Новый ВходЧерезПровайдера(ДанныеПровайдера);
Начало = Вход.Начать();                              // → URL + Транзакция
Итог   = Вход.Завершить(Транзакция, Код, Состояние); // → исход входа
Адрес  = Вход.АдресВыхода(ИдТокен, АдресПосле);      // → URL выхода или ""
Данные = Вход.Описание();                            // → разрешённое описание

Один объект — один провайдер. HTTP целиком на 1connector.

Граница ответственности

Библиотека не хранит ничего. Она знает протокол и порядок проверок. Всё остальное — приложение:

Приложение Библиотека
какие провайдеры настроены, их метки, значки, порядок кнопок что нужно, чтобы сходить к провайдеру
где живёт транзакция между двумя запросами и когда её гасить что положить в транзакцию
куки, редиректы, HTTP-ответ — браузером двигает приложение сборка URL, на который увести браузер
кто этот человек, заводить ли его, что ему можно кем он представился провайдеру
логирование ничего не логирует

Браузер открывает приложение, всегда: в веб-сценарии он вообще на другой машине, а редиректом владеет HTTP-ответ. Библиотека может только отдать URL — отсюда два вызова вместо одного, и это протокол, а не выдумка библиотеки.

Модель доверия к id_token (важно, прочитайте)

Подпись id_token (RS256 и др.) локально НЕ проверяется. Это граница задачи библиотеки, а не ограничение платформы: проверка RS256 по ключам из JWKS на чистом OneScript существует — её делает jwt-verify. Здесь модель доверия строится на канале: токен берётся напрямую от token endpoint по TLS.

  1. Доверять id_token можно ТОЛЬКО когда он получен напрямую от token endpoint по TLS (server-to-server) — как внутри Завершить. Канал TLS плюс прямой ответ провайдера заменяют проверку подписи: подменить токен «в полёте» нельзя.
  2. Никогда не доверяйте id_token, пришедшему косвенно (из фронтенд-редиректа, из тела запроса от клиента): для таких сценариев нужна проверка подписи по ключам из jwks_uri — возьмите jwt-verify. Метода «проверь мне вот этот токен» в библиотеке нет намеренно: он был бы готовым ружьём.
  3. Полезная нагрузка сверяется всегда: iss (побайтно), aud (входит ли client_id), azp (если клейм есть — обязан указывать на нас), exp (с допуском на расхождение часов), nbf и nonce.
  4. Ответ userinfo принимается, только если его sub совпадает с sub из id_token (OIDC Core §5.3.2). Не совпал — профиль отбрасывается, вход продолжается на клеймах id_token.
  5. Ответ userinfo без sub при этом принимается целиком. Клейм по спецификации обязателен, но реальные провайдеры (Authentik, Gitea-подобные) его не отдают, а сам ответ получен по токену доступа ЭТОГО же входа. Проверка ловит подмену профиля, а не его неполноту.

PKCE защищает сам код авторизации: обменять его на токены сможет только владелец исходного code_verifier.

Библиотека ничего не логирует. Ни client_secret, ни code, ни токены не попадают ни в лог, ни в текст исключений. Одна оговорка: когда тело ответа провайдера не разбирается как JSON, в сообщение подмешивается платформенное ОписаниеОшибки(), а оно может процитировать фрагмент этого тела. Завершить такое исключение гасит и превращает в exchange_failed, ничего не записывая.

Установка

opm install oidc-client
.ЗависитОт("oidc-client", "0.5.1")

Вход целиком

// --- настройка, один раз ---
Описание = Новый Структура();
Описание.Вставить("Имя", "keycloak");
Описание.Вставить("Издатель", "https://keycloak.example.com/realms/main");
Описание.Вставить("ИдентификаторКлиента", "openhub");
Описание.Вставить("СекретКлиента", "s3cr3t");
Описание.Вставить("АдресВозврата", "https://hub.example.com/oidc/callback");
Описание.Вставить("Область", "openid profile email groups");

Вход = Новый ВходЧерезПровайдера(Описание);

// --- GET /login/keycloak ---
Начало = Вход.Начать();
Ключ = МоёХранилище.Сохранить(Начало.Транзакция);   // сессия, кэш, БД — ваше дело
Ответ.УстановитьКуку(Ключ, "HttpOnly; Secure; SameSite=Lax");
Ответ.Перенаправить(Начало.URL);

// --- GET /oidc/callback?code=…&state=… ---
Транзакция = МоёХранилище.Прочитать(КлючИзКуки);
Если Транзакция = Неопределено Тогда
	Возврат СтраницаВойтиЗаново();     // ссылка устарела — штатный случай
КонецЕсли;

Итог = Вход.Завершить(Транзакция, Запрос.Параметр("code"), Запрос.Параметр("state"));
МоёХранилище.Удалить(КлючИзКуки);      // ВСЕГДА, в любом исходе: транзакция одноразовая

Если НЕ Итог.Успех Тогда
	// Итог.Причина: state_mismatch | unknown_provider | exchange_failed
	//               | id_token_invalid | no_subject
	Возврат;
КонецЕсли;

// связка «пользователь ↔ личность провайдера» — по паре Издатель + Субъект

Исход входа

Поле Что внутри
Успех Булево
Причина "" либо код отказа
Провайдер имя из конфигурации
Субъект устойчивый идентификатор у провайдера
Издатель issuer ИЗ КОНФИГУРАЦИИ (для OIDC уже сверен с iss)
Email, EmailПодтверждён вычислены (см. ниже)
Клеймы сырьё от провайдера
Токены сырой ответ token endpoint (access_token, id_token, …)
ПрофильПолучен удался ли userinfo

Клеймы — то, что приехало: для OIDC клеймы id_token, поверх них userinfo; для OAuth2 — userinfo. Оттуда берите groups, preferred_username, name, picture и всё прочее. Имя клейма групп ни в одном стандарте не закреплено — держите его в своей конфигурации.

Вычисляются ровно три поля, и только потому, что приложению их не повторить: Субъект, Email и EmailПодтверждён. У GitHub субъект зовётся id (а не login, который переименовывается), а подтверждённый адрес живёт во второй точке /user/emails.

Пресеты OAuth2 дополнительно дописывают в Клеймы стандартные имена OIDC поверх сырых (subid, preferred_usernamelogin, pictureavatar_url). Сырые поля остаются на месте: библиотека добавляет словарь, а не переписывает ответ провайдера. Смысл — читать клеймы одинаково у всех.

Порядок проверок в Завершить (и почему он такой)

  1. state — сравнением, безопасным по времени, и ДО любого обращения к сети. Иначе по чужому редиректу приложение само сходит к провайдеру и обменяет подсунутый код. Пустое ожидаемое значение совпадением не считается;
  2. тот ли провайдер (unknown_provider) — объект ведёт одного, и завершать им чужой вход нельзя;
  3. обмен кода на токены (exchange_failed) — сюда же попадают беды discovery.

Дальше ветки расходятся — это перестановка шагов, а не пара условий.

OIDC (id_token обязателен): проверка id_token (id_token_invalid) → наличие sub (no_subject) → userinfo с деградацией: недоступный или отдавший чужой sub профиль не отменяет вход.

OAuth2 (id_token не существует): userinfo без деградации — субъект живёт только здесь, недоступный профиль означает no_subject → наличие субъекта по маппингу профиля.

Завершить не бросает исключений на исходах протокола: «вход не удался» — штатное событие веб-страницы, а не авария.

Провайдер без discovery

Точки = Новый Структура();
Точки.Вставить("Авторизация", "https://idp.example.com/authorize");
Точки.Вставить("Токен", "https://idp.example.com/token");
Точки.Вставить("Профиль", "https://idp.example.com/userinfo");
Точки.Вставить("Выход", "https://idp.example.com/logout");
Описание.Вставить("КонечныеТочки", Точки);

Ещё принимаются Ключи (jwks_uri) и Emails (вторая точка добора email), а также сырые имена discovery. Для Тип = "oauth2" первые три обязательны.

Выход

Адрес = Вход.АдресВыхода(ИдТокенИзМоейСессии, "https://hub.example.com/");
Если ЗначениеЗаполнено(Адрес) Тогда
	Ответ.Перенаправить(Адрес);   // погасить сессию и у провайдера
КонецЕсли;
// свою сессию приложение гасит в любом случае само

RP-Initiated Logout 1.0: end_session_endpoint из discovery плюс id_token_hint, post_logout_redirect_uri и client_id. Возвращает пустую строку, а не исключение, если точки нет (у OAuth2 её не бывает никогда) или провайдер недоступен — это штатный случай, а не авария.

id_token для id_token_hint возьмите из Итог.Токены на входе и сохраните в своей сессии.

Криптопримитивы и решения

  • SHA-256 — встроенный ХешированиеДанных.
  • base64url — свой: url-safe алфавит без дополнения (иначе PKCE-вызов и сегменты JWT не сойдутся). Декодер лоялен к стандартному алфавиту и дополнению.
  • CSPRNGУникальныйИдентификатор (системный CSPRNG .NET) с отбеливанием SHA-256; встроенный ГенераторСлучайныхЧисел (обёртка System.Random) сознательно не используется.
  • Аутентификация клиентаclient_secret_post (по умолчанию), client_secret_basic (RFC 6749 §2.3.1: client_id и секрет url-кодируются до base64, иначе секрет с : ломает разбор) и none. private_key_jwt не реализован: нужна подпись RSA/EC, которой на чистом OneScript нет.

Лицензия

MIT — см. LICENSE.

About

Вход через провайдера OpenID Connect и OAuth 2.0 для OneScript

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages