feat(auth): OIDC SSO для веб-интерфейса (Authelia/LLDAP и любые OIDC-провайдеры) #1

Closed
hermes-bot wants to merge 0 commits from feat/oidc-sso into main
Collaborator

Что это

Фича-форк поверх upstream vestigiumincaligne/polka (v0.1.22, master 01c2712): OIDC Authorization Code flow для веб-UI. В upstream есть только локальные аккаунты и HTTP Basic для OPDS — OIDC/LDAP/header-auth там нет (проверено по исходникам).

Зачем: в кластере уже есть Authelia (поверх LLDAP) с полноценным OIDC — централизованный SSO вместо отдельного аккаунта Polka. Реализован стандартный flow, так что работает с любым провайдером: Authelia, Keycloak, Dex, OIDC-endpoint LLDAP и т.д.

Как работает

  • Discovery /.well-known/openid-configuration на старте (endpoints + поддержка PKCE). Конфиг не валиден → сервер падает, а не стартует в полуконфигурации.
  • /auth/oidc/login → 302 на провайдера: state + nonce в короткоживущей HttpOnly-cookie (10 мин), PKCE S256 если провайдер умеет.
  • /auth/oidc/callback: обмен кода → claims из id_token payload + userinfo fallback (группы: groups / member_of / roles), JIT-проvisioning локального аккаунта по sub (логин из preferred_username/email local-part, коллизии → суффикс), открытие штатной Polka-сессии (cookie).
  • Промоция в admin по настроенной группе (POLKA_OIDC_ADMIN_GROUP), без авто-демоции — админы управляются локально.
  • Схема users.db: колонки oidc_sub (unique) + oidc_name, миграция в стиле проекта (migrateProgress), StartSession вынесен из Login для непарольного пути.
  • Локальный пароль-логин сохранён (bootstrap-admin), OPDS Basic не тронут — читалки ходят со своими Polka-кредами, SSO покрывает браузер.
  • auth/me отдаёт блок oidc → кнопка «Войти через {provider}» на LoginPage (i18n ru/en).

Конфиг (env/флаги, по умолчанию выключено)

POLKA_OIDC_ISSUER=https://auth.ppvn.ru
POLKA_OIDC_CLIENT_ID=polka
POLKA_OIDC_CLIENT_SECRET=…
POLKA_OIDC_REDIRECT_URL=https://polka.ppvn.ru/auth/oidc/callback
POLKA_OIDC_ADMIN_GROUP=devops
POLKA_OIDC_PROVIDER_NAME=Authelia

Тесты

  • Unit (internal/auth/oidc_test.go): JIT-проvisioning, обновление имени, промо/нет-демоция по группе, disabled-аккаунт, коллизия логина с локальным аккаунтом.
  • E2E (internal/server/oidc_e2e_test.go): полный браузерный флоу (hop-by-hop, cookie jar) против фейкового IdP — реклама кнопки в auth/me, provision+login, промоция по группе, forged state → 400, без конфига SSO невидим (SPA fallback).
  • Smoke (oidc_smoke.py): собранный бинарь + мок-IdP — discovery, PKCE в auth-запросе, callback, сессия, промоция, локальный путь цел (401 на плохие креды). EXIT=0.
  • go vet чист, go test ./... зелёный (один разовый флак SQLITE_BUSY при параллельном прогоне, повторные прогоны чистые), веб-тесты 24/24.

Известные упрощения

  • id_token payload парсится без проверки подписи: токен получен напрямую с token endpoint по TLS (это клиент credentials, не публичный клиент); userinfo — авторитетный fallback. При желании можно добавить JWKS-верификацию отдельным шагом.
  • end_session_endpoint пока не используется (logout локальный).
## Что это Фича-форк поверх upstream `vestigiumincaligne/polka` (v0.1.22, master 01c2712): **OIDC Authorization Code flow** для веб-UI. В upstream есть только локальные аккаунты и HTTP Basic для OPDS — OIDC/LDAP/header-auth там нет (проверено по исходникам). Зачем: в кластере уже есть Authelia (поверх LLDAP) с полноценным OIDC — централизованный SSO вместо отдельного аккаунта Polka. Реализован стандартный flow, так что работает с любым провайдером: Authelia, Keycloak, Dex, OIDC-endpoint LLDAP и т.д. ## Как работает - Discovery `/.well-known/openid-configuration` на старте (endpoints + поддержка PKCE). Конфиг не валиден → сервер падает, а не стартует в полуконфигурации. - `/auth/oidc/login` → 302 на провайдера: `state` + `nonce` в короткоживущей HttpOnly-cookie (10 мин), PKCE S256 если провайдер умеет. - `/auth/oidc/callback`: обмен кода → claims из id_token payload + userinfo fallback (группы: `groups` / `member_of` / `roles`), JIT-проvisioning локального аккаунта по `sub` (логин из `preferred_username`/email local-part, коллизии → суффикс), открытие штатной Polka-сессии (cookie). - Промоция в admin по настроенной группе (`POLKA_OIDC_ADMIN_GROUP`), без авто-демоции — админы управляются локально. - Схема users.db: колонки `oidc_sub` (unique) + `oidc_name`, миграция в стиле проекта (`migrateProgress`), `StartSession` вынесен из `Login` для непарольного пути. - Локальный пароль-логин сохранён (bootstrap-admin), OPDS Basic не тронут — читалки ходят со своими Polka-кредами, SSO покрывает браузер. - `auth/me` отдаёт блок `oidc` → кнопка «Войти через {provider}» на LoginPage (i18n ru/en). ## Конфиг (env/флаги, по умолчанию выключено) ``` POLKA_OIDC_ISSUER=https://auth.ppvn.ru POLKA_OIDC_CLIENT_ID=polka POLKA_OIDC_CLIENT_SECRET=… POLKA_OIDC_REDIRECT_URL=https://polka.ppvn.ru/auth/oidc/callback POLKA_OIDC_ADMIN_GROUP=devops POLKA_OIDC_PROVIDER_NAME=Authelia ``` ## Тесты - Unit (`internal/auth/oidc_test.go`): JIT-проvisioning, обновление имени, промо/нет-демоция по группе, disabled-аккаунт, коллизия логина с локальным аккаунтом. - E2E (`internal/server/oidc_e2e_test.go`): полный браузерный флоу (hop-by-hop, cookie jar) против фейкового IdP — реклама кнопки в auth/me, provision+login, промоция по группе, forged state → 400, без конфига SSO невидим (SPA fallback). - Smoke (`oidc_smoke.py`): собранный бинарь + мок-IdP — discovery, PKCE в auth-запросе, callback, сессия, промоция, локальный путь цел (401 на плохие креды). EXIT=0. - `go vet` чист, `go test ./...` зелёный (один разовый флак SQLITE_BUSY при параллельном прогоне, повторные прогоны чистые), веб-тесты 24/24. ## Известные упрощения - id_token payload парсится без проверки подписи: токен получен напрямую с token endpoint по TLS (это клиент credentials, не публичный клиент); userinfo — авторитетный fallback. При желании можно добавить JWKS-верификацию отдельным шагом. - `end_session_endpoint` пока не используется (logout локальный).
Adds a standard OIDC Authorization Code flow (discovery, state+nonce in
a short-lived HttpOnly cookie, PKCE S256 when the provider supports it)
alongside the existing local password login. Configured via POLKA_OIDC_*
env/flags; off by default.

- internal/auth: users gain oidc_sub/oidc_name columns (backwards-safe
  migration in the same pattern as migrateProgress); EnsureOIDCLogin
  links the IdP subject to a local account, JIT-provisions on first
  sign-in, refreshes display name, and can promote to admin from a
  configured group claim (never demotes). StartSession extracted from
  Login for the non-password path.
- internal/server: discovery client, /auth/oidc/login + /auth/oidc/callback
  handlers, claims parsing (id_token payload + userinfo fallback, group
  claim fallbacks groups/member_of/roles), /auth/me advertises the SSO
  button, userJSON gains an oidc flag.
- web: SSO button on the login page (i18n ru/en) driven by the oidc
  block of /auth/me.
- tests: unit tests for provisioning/promotion/disabled/collision and a
  full browser-style e2e against a fake IdP; oidc_smoke.py drives the
  compiled binary against a mock provider (flow, PKCE, promotion, local
  path intact).

OPDS/Basic auth is intentionally unchanged: readers authenticate with
their Polka credentials, SSO covers the browser UI.
POLKA_OIDC_ADMIN_GROUP becomes authoritative in both directions: being
in the group means admin, not being in the group means user.

- ApplyOIDCRole: single strict-mirror path used at login and by the
  background loop; demotion of the last active administrator is refused
  (same guard as local user management) so an IdP-side group change
  cannot orphan the deployment.
- Background re-sync loop (POLKA_OIDC_SYNC_INTERVAL, default 5m,
  negative disables): every round re-validates each OIDC user with a
  stored refresh token (refresh grant → userinfo) and applies the
  strict mirror — a deposed admin loses the role within one interval
  without re-logging in. A failing IdP only skips the round.
- Refresh tokens are stored per user (oidc_refresh table) at callback
  time and rotated when the provider issues a new one.
- First sign-in provisions with the role implied by current membership.
- Tests: strict demotion, last-admin guard, refresh-token store; live
  smoke now revokes the group mid-session and asserts background
  demotion (POLKA_OIDC_SYNC_INTERVAL=1s).
wailorman closed this pull request 2026-10-06 21:01:30 +00:00

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
wailorman/polka!1
No description provided.