Перейти к содержанию

OIDC Discovery и проверка провайдера

У локального провайдера есть несколько endpoints: для начала входа, обмена кода, ключей, информации о пользователе и выхода. Переписывать каждый адрес вручную неудобно и рискованно. OIDC Discovery позволяет получить согласованные метаданные от известного issuer. Подключим openid-client и закрепим доверенную конфигурацию лаборатории. Новый результат — приложение знает, с каким провайдером работает, но ещё не начинает пользовательский вход и не принимает случайный issuer из браузера.

В lesson-03 появляется config.mjs, а package.json закрепляет openid-client 6.8.8. Используем Node.js 22.14.0 с встроенными Web API, на которые опирается эта версия библиотеки. Зависимости при подготовке не устанавливались. Команды npm в README адресованы читателю. Выбор точной версии важен: API шестой ветки отличается от старых примеров с Issuer.discover и Client, поэтому нельзя смешивать код разных поколений по сходству названий.

Issuer является исходным доверием

Приложение заранее знает http://localhost:8080/realms/professorweb. Discovery не отвечает на вопрос, можно ли доверять произвольному провайдеру, который сообщил о себе. Оно получает метаданные выбранного пространства. Поэтому issuer приходит из конфигурации процесса и проверяется на точное учебное значение. Если разрешить посетителю прислать другой адрес, он сможет предложить свой провайдер и выдать claims с любыми ролями. Хорошо оформленный discovery JSON не превращает такой адрес в принятую систему удостоверения личности.

В конфигурации также задаются port, APP_ORIGIN и CLIENT_ID. Разрешены только два локальных origin, которые понадобятся для первого и второго приложений. Callback строится как origin плюс /callback. Его не выводим из заголовка Host входящего запроса: клиент может прислать другое значение. Доверенный адрес должен совпадать с зарегистрированным redirect URI провайдера. Такой способ держит важные протокольные параметры в одном месте, вместо того чтобы случайно собирать их из текущей формы или прокси-заголовков.

При реальном выпуске понадобится другая проверенная конфигурация с HTTPS. Текущий файл намеренно отвергает произвольный внешний origin и issuer. Это не универсальный config для production. Нельзя просто убрать проверки и оставить разрешение HTTP во всех окружениях. Лучше создать отдельный профиль, описать, откуда приходят значения, какие proxy-заголовки принимаются и кто имеет право их менять. Первоначальная точность лаборатории помогает увидеть будущие изменения как осознанную работу, а не как набор незаметных исключений.

Public client и метод None

Вызов discovery получает issuer, clientId, метаданные клиента, метод аутентификации и параметры выполнения. Для public client используем oidc.None(). Это сообщает библиотеке, что на token endpoint не отправляется client secret; client_id остаётся частью запроса. Отсутствие секрета согласовано с зарегистрированным клиентом propusk. Если сделать его confidential в Keycloak, не изменив код, обмен потребует другой аутентификации. Нужно согласовать обе стороны, а не добавлять случайный secret в URL начала входа.

Для локального HTTP используется execute с allowInsecureRequests. Эта настройка распространяется на запросы полученного Configuration. Она нужна исключительно учебному localhost и не является исправлением любого TLS-сбоя. В production библиотека должна работать по HTTPS; неверный сертификат или неизвестный issuer следует исправлять в окружении, а не отключать проверки. Показанное исключение легко скопировать, поэтому в коде и тексте явно закреплена его граница. Само имя localhost тоже должно использоваться последовательно в провайдере и браузере.

В шестой версии discovery является асинхронной операцией. На уровне модуля ожидаем её завершение до запуска HTTP-сервера. Если Keycloak недоступен, приложение этого состояния не должно притвориться готовым к входу с пустыми endpoints. Это полезный строгий результат для лаборатории. В большой системе можно добавить readiness, повторный запуск или отдельный public route, но тогда посетитель должен понимать недоступность входа. Здесь зависимость видима: сначала провайдер, затем конфигурация, затем приём запросов.

Метаданные и пользовательские данные

Route /metadata показывает serverMetadata как экранированный JSON. Это открытые сведения конфигурации, а не токены конкретного посетителя. Мы добавили такой маршрут для изучения: можно сопоставить issuer, authorization endpoint и token endpoint с одним realm. Но не следует расширять его выводом client secret, содержимого сессий или полного процесса окружения. Важное различие проходит между публичным описанием протокола и секретами приложения; слово metadata само по себе не делает любой объект безопасным для публикации.

Discovery обычно сообщает и jwks_uri. Он связан с ключами провайдера, но не заменяет всю проверку ответа. Наличие ключей на endpoint не разрешает принимать любой JWT с удобным username. Библиотека обработает ожидаемый OIDC-обмен, его claims и привязку к попытке входа. Если ваш отдельный API принимает bearer access token, ему нужна собственная корректная проверка audience, issuer, срока и принятого алгоритма. Нельзя копировать из этого урока лишь JSON-декодирование и считать его готовым защитным middleware.

Разбор конфигурационной ошибки

Представьте, что clientId написан как propus вместо propusk. Получение метаданных провайдера может пройти, потому что Discovery описывает realm, а не подтверждает существование каждого зарегистрированного клиента. Ошибка проявится при дальнейшем authorization request или обмене. Значит, успешная загрузка конфигурации не доказывает, что вся лаборатория настроена. Для диагностики нужно отдельно проверить клиент, exact redirect URI и PKCE-политику. Такие слои полезно понимать прежде, чем общий catch превратит любой отказ в «неправильный пароль».

Другая ошибка — сохранённый realm из старого тома. Новый realm.json может содержать нужный clientId, но запущенный Keycloak продолжит использовать прежнее состояние. Просмотр файла и чтение Discovery не подтверждают совпадение клиента в базе. Сначала выясните, какой compose-проект и volume запущены. Этот порядок сохраняет причинность: сетевые метаданные, регистрация клиента и данные пользователя проверяются отдельно. Ослабление запретов или смена origin на случайный адрес часто лишь скрывает первоначальное расхождение.

Полный config.mjs ниже показывает принятые параметры. При самостоятельной установке зависимостей и запуске ожидается возможность открыть /metadata и увидеть конфигурацию professorweb. Эти действия не выполнялись. В следующем уроке создадим новую попытку входа с одноразовыми state, nonce и PKCE verifier. Она будет связана с браузером через отдельную короткую cookie, а доверенные endpoints останутся теми, что получил и сохранил этот слой.

Полный файл config.mjs из состояния lesson-03:

import * as oidc from 'openid-client';

export const port = Number(process.env.PORT || 5100);
export const origin = process.env.APP_ORIGIN || `http://localhost:${port}`;
export const issuer = new URL(process.env.OIDC_ISSUER || 'http://localhost:8080/realms/professorweb');
export const clientId = process.env.CLIENT_ID || 'propusk';
if (!['http://localhost:5100', 'http://localhost:5101'].includes(origin)) {
  throw new Error('This laboratory accepts only its two localhost origins');
}
if (issuer.href !== 'http://localhost:8080/realms/professorweb') {
  throw new Error('Unexpected issuer for the local laboratory');
}
export const callback = `${origin}/callback`;
export const config = await oidc.discovery(issuer, clientId, undefined, oidc.None(), {
  execute: [oidc.allowInsecureRequests], // Local laboratory only. Use HTTPS outside localhost.
});

Полный файл package.json из состояния lesson-03:

{
  "name": "propusk-local",
  "private": true,
  "type": "module",
  "engines": {
    "node": "22.x"
  },
  "scripts": {
    "start": "node server.mjs"
  },
  "dependencies": {
    "openid-client": "6.8.8"
  }
}

Оглавление серии · Исходники всех шагов · Учебные маршруты

Пример остаётся локальным черновиком. При подготовке он не выполнялся; все описанные эффекты являются ожидаемыми, а не результатами запущенных проверок.