Локальный Keycloak и учебный realm
Публичная страница готова, но вход пока некому подтверждать. Добавим отдельный локальный Keycloak и учебный realm professorweb. Realm является пространством пользователей, клиентов и политик. Для «Пропуска» зарегистрируем OIDC-клиент с точным callback, а для двух вымышленных пользователей зададим разные роли. Новый результат — декларативные файлы лаборатории, которые читатель сможет импортировать самостоятельно. Контейнер, база и аккаунты при подготовке не создавались.
В lesson-02 есть compose.yaml и realm.json. Образ закреплён как quay.io/keycloak/keycloak:26.5.2. Название latest не используется: изменяющийся образ сделал бы инструкции зависимыми от дня запуска. Node-приложение пока остаётся простым, а Keycloak запускается отдельно. Пароль первого администратора задаёт читатель через LAB_ADMIN_PASSWORD; архив содержит лишь .env.example с указанием заменить значение. Не помещайте настоящий пароль своей панели или хостинга в эти файлы.
Realm и зарегистрированный клиент
Имя realm professorweb входит в будущий issuer: http://localhost:8080/realms/professorweb. Master-realm нужен для администрирования, а пользователи приложения находятся в отдельном пространстве. Такой выбор помогает не смешивать человека, управляющего сервером идентичности, с редактором сайта. Анна может иметь editor в учебном realm, не становясь администратором всего Keycloak. Роль в приложении и полномочия управления провайдером являются разными наборами, даже если обе операции выполняются через браузер.
Клиент propusk зарегистрирован как public. В этом учебном профиле у него нет client secret, но включён standard flow и обязательно используется PKCE S256. Direct access grants, implicit flow и service accounts выключены. Мы изучаем обычный вход с перенаправлением, а не сбор пароля приложением или машинный доступ. Public client означает способ аутентификации клиента на token endpoint; это не значит, что пользовательские данные становятся публичными или что можно отказаться от проверки state и callback.
Redirect URI задан точно: http://localhost:5100/callback. Широкая маска адресов выглядела бы удобнее для разработки, но увеличивает число допустимых мест, куда провайдер может отправить код. Путь, схема, имя узла и порт являются частью регистрации. localhost и 127.0.0.1 не стоит свободно менять по настроению: позже это повлияет и на WebAuthn RP ID. Сервер Node слушает loopback, но пользовательские адреса всей серии строятся с localhost. Такая последовательность сокращает неочевидные расхождения окружения.
Пользователи и учебные данные
В realm.json есть anna и boris. Анна имеет editor и reader; Борис только reader. Их адреса используют example.invalid, чтобы явно не представлять настоящих получателей. Пароли указаны в README и исходнике исключительно как воспроизводимые локальные fixtures. Они не описывают учётные записи реального ProfessorWeb. При переносе лаборатории на публичную машину эти значения недопустимы, как и сам режим start-dev. Явная вымышленность данных помогает читать архив без догадок о том, какие сведения требуют скрытия.
Поля emailVerified заданы для упрощения стенда: почтовый сервер в серию не входит. Это не подтверждение настоящих почтовых адресов. Если ваш продукт связывает доступ с проверкой почты, потребуется реальный verification flow и политика изменений адреса. Нельзя перенести лабораторное true в импорт настоящих пользователей и считать их адреса проверенными. Отдельное объяснение fixture особенно важно для identity-примеров, где короткая настройка часто выглядит как универсальная бизнес-логика.
Access token имеет короткий срок, а SSO-сессия живёт дольше. Это позволит позже увидеть refresh без ожидания многих часов. Короткий срок токена сам по себе не удаляет локальную Map-сессию Node. Мы ещё только создаём провайдера, а время полномочий приложения появится отдельно. Не следует читать поля realm как единственный общий таймер всей системы. В серии будет несколько сроков: попытка входа, access token, локальная сессия и SSO, каждый с собственным назначением.
Импорт и сохранённый том
Compose запускает Keycloak с --import-realm и монтирует JSON только для чтения. Данные провайдера находятся в named volume. Важная граница: импорт при запуске не обновляет уже существующий realm автоматически. Если вы поправили realm.json и перезапустили прежний compose-проект, база может остаться со старой конфигурацией. Это не ошибка файла и не странное поведение OIDC. Импорт рассматривает новый realm, а не выполняет каждое изменение декларативной миграцией поверх существующих данных.
Поэтому для независимого снимка выбирайте новый COMPOSE_PROJECT_NAME, например propusk2. При переходе на другой урок можно использовать новый учебный проект с отдельным томом либо вручную внести описанное изменение через Admin Console. Не удаляйте все Docker volumes ради получения свежего realm: среди них могут быть чужие данные. README объясняет, что именно требуется согласовать. Самостоятельность папок относится к исходникам; она не означает магическое обновление сохранённой базы контейнера.
Порт Keycloak публикуется на 127.0.0.1. Это ограничивает обычное сетевое прослушивание стенда локальной машиной. Пароль администратора передаётся переменной окружения, однако Docker и оператор компьютера всё равно относятся к доверенному окружению. Этот способ не является промышленным хранилищем секретов. Для реального сервера нужны отдельная база, защищённая конфигурация, актуальные исправления и план резервирования. Мы сохраняем dev-профиль небольшим, чтобы он служил объяснению протокола, а не обещал готовое сопровождение.
Ожидаемое состояние
После самостоятельного запуска контейнера и импорта ожидается realm professorweb, клиент propusk и две fixture-учётные записи. Проверьте точный callback, public client, standard flow и PKCE S256 в конфигурации выбранной версии. Ни один из этих шагов не выполнялся при подготовке. Исходники ниже позволяют заранее прочитать принятые параметры. Если провайдер недоступен, страница Node пока остаётся обычной публичной страницей: фактическая связность появится в следующем уроке через Discovery.
Не начинайте диагностику с ослабления redirect URI до *. Сначала сопоставьте issuer, имя realm, порт и фактический URL приложения. Если сохранённый том содержит прошлую конфигурацию, разберитесь с ним отдельно. Такая последовательность помогает решить конкретную причину и не удалить важные свойства протокола ради прохождения одного запроса. Следующий шаг подключит openid-client и получит метаданные только от заранее выбранного провайдера, удерживая origin приложения и issuer как доверенные настройки.
В манифесте клиента также явно задано fullScopeAllowed. Для небольшой лаборатории это делает передачу назначенных realm-ролей обозримой, но у настоящего приложения стоит ограничить набор теми полномочиями, которые ему нужны. Такое сокращение выполняется на стороне provider вместе с mapper и scope-моделью. Оно не заменяет серверную проверку роли, а уменьшает объём принимаемого контекста. Не считайте широкий учебный scope обязательным свойством любого нового клиента вашей организации.
Полный файл compose.yaml из состояния lesson-02:
services:
keycloak:
image: quay.io/keycloak/keycloak:26.5.2
command: ["start-dev", "--import-realm", "--hostname=http://localhost:8080"]
ports: ["127.0.0.1:8080:8080"]
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: lab-admin
KC_BOOTSTRAP_ADMIN_PASSWORD: ${LAB_ADMIN_PASSWORD:?Set your local LAB_ADMIN_PASSWORD}
volumes:
- ./realm.json:/opt/keycloak/data/import/realm.json:ro
- keycloak-data:/opt/keycloak/data
volumes:
keycloak-data:
Полный файл realm.json из состояния lesson-02:
{
"realm": "professorweb",
"enabled": true,
"sslRequired": "none",
"registrationAllowed": false,
"resetPasswordAllowed": false,
"accessTokenLifespan": 120,
"ssoSessionIdleTimeout": 1800,
"ssoSessionMaxLifespan": 3600,
"roles": {
"realm": [
{
"name": "editor"
},
{
"name": "reader"
}
]
},
"clients": [
{
"clientId": "propusk",
"enabled": true,
"protocol": "openid-connect",
"publicClient": true,
"fullScopeAllowed": true,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"implicitFlowEnabled": false,
"serviceAccountsEnabled": false,
"redirectUris": [
"http://localhost:5100/callback"
],
"webOrigins": [],
"attributes": {
"pkce.code.challenge.method": "S256",
"post.logout.redirect.uris": "http://localhost:5100/"
},
"defaultClientScopes": [
"profile",
"email",
"roles"
]
}
],
"users": [
{
"username": "anna",
"enabled": true,
"firstName": "Анна",
"lastName": "Редактор",
"email": "anna@example.invalid",
"emailVerified": true,
"realmRoles": [
"editor",
"reader"
],
"credentials": [
{
"type": "password",
"value": "Local-Anna-only-2026!",
"temporary": false
}
]
},
{
"username": "boris",
"enabled": true,
"firstName": "Борис",
"lastName": "Читатель",
"email": "boris@example.invalid",
"emailVerified": true,
"realmRoles": [
"reader"
],
"credentials": [
{
"type": "password",
"value": "Local-Boris-only-2026!",
"temporary": false
}
]
}
]
}
Оглавление серии · Исходники всех шагов · Учебные маршруты
Пример остаётся локальным черновиком. При подготовке он не выполнялся; все описанные эффекты являются ожидаемыми, а не результатами запущенных проверок.