feat(proxy): OIDC + Forge OAuth, auto-provisioning, always-emit X-Hero-Claims #41

Merged
lee merged 2 commits from development_lee_oauth_forge into development 2026-05-04 13:35:35 +00:00
Member

Closes the path where a successful OAuth login granted full admin: the
callback only wrote oauth_sessions, never a users row, so the request
hot path injected X-Hero-Context: 0 and skipped X-Hero-Claims — which
hero_context defines as "trusted system call, full rights inside context".

Changes

  • OIDC discovery at config time: oauth.set_provider for provider_type = forge (or oidc) with an issuer fetches the .well-known doc and stores
    auth/token/userinfo/jwks URLs. Adds issuer/jwks_uri to oauth_providers.
  • New oidc module: id_token validation against cached JWKS (10-min TTL),
    PKCE (S256) verifier+challenge, OIDC nonce generation.
  • Auto-provision users on first OAuth login. Match by
    (external_provider, external_id) — partial unique index lets manually-
    created users coexist. New users get default_user_context (defaults to
    1; 0 stays reserved for system/admin per hero_context).
  • Username derivation: sanitise preferred_username to
    ^[a-z0-9][a-z0-9_-]{0,62}$ with a numeric collision suffix; fall back
    through email → sub.
  • inject_authenticated_identity always emits X-Hero-Claims for any
    authenticated user — synthetic users.<username> claim guarantees the
    header is never absent. Used by all four auth_modes.
  • Reserved-prefix enforcement on roles.add_claim rejects admin,
    users.*, groups.* so role assignments can't forge proxy-managed
    synthetic claims.

Security hardening (paired with the OAuth work because they touch the
same code paths)

  • Cookie: add Secure flag and __Host- name prefix; tighten matcher
    to require exact name (adjacent prefixed cookies no longer match).
  • oauth.get_provider masks client_secret (mirrored in
    oauth.set_provider write path).
  • CSRF state map gets a 10-minute TTL, swept every minute.
  • New /oauth/logout handler clears the cookie and deletes the session.
  • Hourly background task runs cleanup_expired_sessions.
  • users.delete cascades to oauth_sessions.
  • return_to constrained to relative same-origin paths.
  • PKCE on every flow (verifier+challenge stored alongside the state map).
  • OIDC nonce validated against the id_token claim.

Tests

  • New unit tests in oauth (sanitiser, cookie matcher, cookie flags) and
    oidc (PKCE RFC 7636 vector, nonce uniqueness).
  • make test (workspace --lib --bins) green: 39 passed, 0 failed.
  • Integration test test_oauth_crud updated to expect masked secret on
    oauth.get_provider.
  • Six pre-existing proxy_request-based integration tests still fail on
    serve_unix not supplying ConnectInfo<SocketAddr> (regression on
    development since 54b0694, not introduced by this change).
Closes the path where a successful OAuth login granted full admin: the callback only wrote `oauth_sessions`, never a `users` row, so the request hot path injected `X-Hero-Context: 0` and skipped `X-Hero-Claims` — which hero_context defines as "trusted system call, full rights inside context". Changes ------- - OIDC discovery at config time: `oauth.set_provider` for `provider_type = forge` (or `oidc`) with an `issuer` fetches the .well-known doc and stores auth/token/userinfo/jwks URLs. Adds `issuer`/`jwks_uri` to oauth_providers. - New `oidc` module: id_token validation against cached JWKS (10-min TTL), PKCE (S256) verifier+challenge, OIDC nonce generation. - Auto-provision users on first OAuth login. Match by (external_provider, external_id) — partial unique index lets manually- created users coexist. New users get `default_user_context` (defaults to 1; 0 stays reserved for system/admin per hero_context). - Username derivation: sanitise `preferred_username` to `^[a-z0-9][a-z0-9_-]{0,62}$` with a numeric collision suffix; fall back through email → sub. - `inject_authenticated_identity` always emits `X-Hero-Claims` for any authenticated user — synthetic `users.<username>` claim guarantees the header is never absent. Used by all four auth_modes. - Reserved-prefix enforcement on `roles.add_claim` rejects `admin`, `users.*`, `groups.*` so role assignments can't forge proxy-managed synthetic claims. Security hardening (paired with the OAuth work because they touch the same code paths) ------------------------------------------------------------------------- - Cookie: add `Secure` flag and `__Host-` name prefix; tighten matcher to require exact name (adjacent prefixed cookies no longer match). - `oauth.get_provider` masks `client_secret` (mirrored in `oauth.set_provider` write path). - CSRF state map gets a 10-minute TTL, swept every minute. - New `/oauth/logout` handler clears the cookie and deletes the session. - Hourly background task runs `cleanup_expired_sessions`. - `users.delete` cascades to `oauth_sessions`. - `return_to` constrained to relative same-origin paths. - PKCE on every flow (verifier+challenge stored alongside the state map). - OIDC `nonce` validated against the id_token claim. Tests ----- - New unit tests in `oauth` (sanitiser, cookie matcher, cookie flags) and `oidc` (PKCE RFC 7636 vector, nonce uniqueness). - `make test` (workspace --lib --bins) green: 39 passed, 0 failed. - Integration test `test_oauth_crud` updated to expect masked secret on `oauth.get_provider`. - Six pre-existing `proxy_request`-based integration tests still fail on `serve_unix` not supplying `ConnectInfo<SocketAddr>` (regression on `development` since 54b0694, not introduced by this change).
feat(proxy): OIDC + Forge OAuth, auto-provisioning, always-emit X-Hero-Claims
All checks were successful
Build & Test / check (push) Successful in 2m0s
Build & Test / check (pull_request) Successful in 2m1s
f6f8ad5627
Closes the path where a successful OAuth login granted full admin: the
callback only wrote `oauth_sessions`, never a `users` row, so the request
hot path injected `X-Hero-Context: 0` and skipped `X-Hero-Claims` — which
hero_context defines as "trusted system call, full rights inside context".

Changes
-------
- OIDC discovery at config time: `oauth.set_provider` for `provider_type =
  forge` (or `oidc`) with an `issuer` fetches the .well-known doc and stores
  auth/token/userinfo/jwks URLs. Adds `issuer`/`jwks_uri` to oauth_providers.
- New `oidc` module: id_token validation against cached JWKS (10-min TTL),
  PKCE (S256) verifier+challenge, OIDC nonce generation.
- Auto-provision users on first OAuth login. Match by
  (external_provider, external_id) — partial unique index lets manually-
  created users coexist. New users get `default_user_context` (defaults to
  1; 0 stays reserved for system/admin per hero_context).
- Username derivation: sanitise `preferred_username` to
  `^[a-z0-9][a-z0-9_-]{0,62}$` with a numeric collision suffix; fall back
  through email → sub.
- `inject_authenticated_identity` always emits `X-Hero-Claims` for any
  authenticated user — synthetic `users.<username>` claim guarantees the
  header is never absent. Used by all four auth_modes.
- Reserved-prefix enforcement on `roles.add_claim` rejects `admin`,
  `users.*`, `groups.*` so role assignments can't forge proxy-managed
  synthetic claims.

Security hardening (paired with the OAuth work because they touch the
same code paths)
-------------------------------------------------------------------------
- Cookie: add `Secure` flag and `__Host-` name prefix; tighten matcher
  to require exact name (adjacent prefixed cookies no longer match).
- `oauth.get_provider` masks `client_secret` (mirrored in
  `oauth.set_provider` write path).
- CSRF state map gets a 10-minute TTL, swept every minute.
- New `/oauth/logout` handler clears the cookie and deletes the session.
- Hourly background task runs `cleanup_expired_sessions`.
- `users.delete` cascades to `oauth_sessions`.
- `return_to` constrained to relative same-origin paths.
- PKCE on every flow (verifier+challenge stored alongside the state map).
- OIDC `nonce` validated against the id_token claim.

Tests
-----
- New unit tests in `oauth` (sanitiser, cookie matcher, cookie flags) and
  `oidc` (PKCE RFC 7636 vector, nonce uniqueness).
- `make test` (workspace --lib --bins) green: 39 passed, 0 failed.
- Integration test `test_oauth_crud` updated to expect masked secret on
  `oauth.get_provider`.
- Six pre-existing `proxy_request`-based integration tests still fail on
  `serve_unix` not supplying `ConnectInfo<SocketAddr>` (regression on
  `development` since 54b0694, not introduced by this change).
feat(proxy-ui): forge/oidc provider type with issuer-driven discovery
Some checks failed
Build & Test / check (pull_request) Successful in 1m37s
Build & Test / check (push) Has been cancelled
a8051cd137
Adds Forge (OIDC) and OIDC (generic) entries to the OAuth provider modal.
For these types the URL inputs are hidden and an Issuer URL field is
shown instead; the server's oauth.set_provider then fills auth/token/
userinfo/jwks endpoints from {issuer}/.well-known/openid-configuration.
lee merged commit f47c95ae98 into development 2026-05-04 13:35:35 +00:00
lee deleted branch development_lee_oauth_forge 2026-05-04 13:35:46 +00:00
Sign in to join this conversation.
No reviewers
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
lhumina_code/hero_proxy!41
No description provided.