feat(proxy): OIDC + Forge OAuth, auto-provisioning, always-emit X-Hero-Claims #41
No reviewers
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_proxy!41
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "development_lee_oauth_forge"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes the path where a successful OAuth login granted full admin: the
callback only wrote
oauth_sessions, never ausersrow, so the requesthot path injected
X-Hero-Context: 0and skippedX-Hero-Claims— whichhero_context defines as "trusted system call, full rights inside context".
Changes
oauth.set_providerforprovider_type = forge(oroidc) with anissuerfetches the .well-known doc and storesauth/token/userinfo/jwks URLs. Adds
issuer/jwks_urito oauth_providers.oidcmodule: id_token validation against cached JWKS (10-min TTL),PKCE (S256) verifier+challenge, OIDC nonce generation.
(external_provider, external_id) — partial unique index lets manually-
created users coexist. New users get
default_user_context(defaults to1; 0 stays reserved for system/admin per hero_context).
preferred_usernameto^[a-z0-9][a-z0-9_-]{0,62}$with a numeric collision suffix; fall backthrough email → sub.
inject_authenticated_identityalways emitsX-Hero-Claimsfor anyauthenticated user — synthetic
users.<username>claim guarantees theheader is never absent. Used by all four auth_modes.
roles.add_claimrejectsadmin,users.*,groups.*so role assignments can't forge proxy-managedsynthetic claims.
Security hardening (paired with the OAuth work because they touch the
same code paths)
Secureflag and__Host-name prefix; tighten matcherto require exact name (adjacent prefixed cookies no longer match).
oauth.get_providermasksclient_secret(mirrored inoauth.set_providerwrite path)./oauth/logouthandler clears the cookie and deletes the session.cleanup_expired_sessions.users.deletecascades tooauth_sessions.return_toconstrained to relative same-origin paths.noncevalidated against the id_token claim.Tests
oauth(sanitiser, cookie matcher, cookie flags) andoidc(PKCE RFC 7636 vector, nonce uniqueness).make test(workspace --lib --bins) green: 39 passed, 0 failed.test_oauth_crudupdated to expect masked secret onoauth.get_provider.proxy_request-based integration tests still fail onserve_unixnot supplyingConnectInfo<SocketAddr>(regression ondevelopmentsince54b0694, 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` since54b0694, not introduced by this change).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.