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

Локальный 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
        }
      ]
    }
  ]
}

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

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