Hi, following up on @zhivkod’s thread, SSO or User Provisioning, with a concrete proposal and a working prototype.
@zaidka - I’ve implemented this against current master and would value your input on a few design choices before I polish it into a PR.
Branch here: GitHub - ggiesen/genieacs at auth/proxy-headers · GitHub
What and why
For deployments that need GenieACS to fit into a centralized identity stack (Keycloak, Okta, Entra, FreeIPA, Authelia, etc.), the proposal is to let GenieACS trust authenticated identity + group membership asserted by an upstream reverse proxy, derive roles from groups via a configurable mapping, and JIT-provision the local users record with role re-sync on every login. Behaviour is unchanged for deployments that don’t turn the feature on.
What already works in your code
Reading lib/ui.ts, lib/auth.ts, and lib/common/authorizer.ts, the architecture is already most of the way there:
- The JWT payload carries
{ username, authMethod }andlib/ui.tsexplicitly rejects anyauthMethod !== "local", which suggests the field was designed with other methods in mind. - The per-request
Authorizeris built from a role list and has no coupling to theuserscollection - where roles come from is opaque to downstream handlers. - The
permissionscollection is keyed on role, not user, so only the user->role resolution needs an alternate path.
In other words, the change is localized: one new middleware, one small mapping lookup, a relaxed authMethod check, and a small schema addition. The authorization pipeline doesn’t move.
Explicit non-goal: native OIDC inside GenieACS. Header-based trust from a proxy is the right level of abstraction; OIDC discovery / JWKS / auth code flow / refresh is what oauth2-proxy, Authelia, and
Pomerium already do well.
What the prototype does
Configuration (five new options, all fail-closed by default)
UI_AUTH_PROXY_ENABLED = false
UI_AUTH_PROXY_TRUSTED_PROXIES = "" # CIDR list; empty = disabled
UI_AUTH_PROXY_USER_HEADER = X-Remote-User
UI_AUTH_PROXY_GROUPS_HEADER = X-Remote-Groups
UI_AUTH_PROXY_GROUPS_SEPARATOR = ,
Middleware behaviour
A new middleware runs after koa-jwt (with passthrough: true). If the feature is enabled and the request origin IP matches a trusted CIDR and the user header is present, the header is accepted as the authenticated identity. Groups from the groups header are mapped to GenieACS roles through a new group_roles MongoDB collection. The user’s users row is upserted with authMethod: "proxy" (no password/salt) and the resolved role list.
An existing authMethod: "local" row is explicitly protected — if a local user with that username exists, the proxy header is ignored for that user, so local accounts can’t be hijacked by proxy headers.
Schema changes
User.authMethodbecomes optional ("local"|"proxy"; missing values default to"local", so no migration needed for existing rows).User.passwordandUser.saltbecome optional (proxy users have neither).- New
group_rolescollection:{ _id: <group>, roles: <csv> }, cached via the existinglocalCachesnapshot pattern.
User lifecycle
Each proxy-authenticated user gets a real row in users with authMethod: "proxy". The row’s roles are overwritten from the current group claim on every login, so group changes in the IdP propagate at next login and user offboarding in the IdP flows through naturally (removed from IdP → can’t auth to proxy → can’t reach GenieACS). Admins retain visibility of these users in the
existing user-management UI and can set disabled: true on a specific user for emergency lockout independent of IdP state.
Open design questions
These are all things I had to pick a side on in the prototype and I’d rather have your opinion before PR polish:
-
Are you open to this in principle? If not, the rest of the questions are moot.
-
Group-mapping storage - MongoDB or
config.json? I went with a Mongogroup_rolescollection in the prototype, which fits the existing idioms (matchesusers/permissions) and is editable without redeploy, but needs a CRUD API and a new admin UI screen.config.jsonis ~60% less code and no UI work, at the cost of a redeploy to change the mapping. Both are workable — want your preference before I build out the UI. -
PR staging - one or two? The prototype is backend only (~150 LoC). The UI admin screen + CRUD for
group_roleswould be another ~200 LoC matching the existingusers-page.ts/
permissions-page.tspatterns, plus tests and docs. One bundled PR, or stage backend first and UI in a follow-up? -
Local-account protection. I chose silent fallthrough - if a row with
authMethod: "local"exists, proxy headers are ignored for that user (they log in with their password as normal). The
alternative is rejecting the request, which is stricter but disruptive to existing deployments the moment the feature is turned on. I’d like your security instinct here. -
Upsert cadence. The prototype writes to MongoDB on every proxy-authenticated request - simple and correct, but one write per request could be heavy for large admin teams. Should I
read-compare-write only when roles actually change, or is every-request fine?
Smaller things (reuse of FORWARDED_HEADER vs a separate trusted list, session model for proxy users, logout URL for the proxy session) I’d prefer to sort out in PR review rather than litigate
here.
Happy to finish the work
Prototype is functional (lint + build pass). If the shape is acceptable I’ll iterate on whichever of the choices above you call, add the UI admin screen, the CRUD API, docs/roles-and-permissions.rst updates, and tests, and open a PR.
Thanks