feat(auth): native OIDC group → role mapping + SSO docs fixes (#3485)#3489
Conversation
Adds direct-OIDC group mapping (no reverse proxy required), plus corrects and expands the SSO docs. Feature: - New env vars OIDC_GROUPS_CLAIM (default "groups", dot-notation for nested claims like realm_access.roles), OIDC_ADMIN_GROUPS, OIDC_ALLOWED_GROUPS. - handleOIDCCallback resolves the groups claim and derives admin/allowed. When OIDC_ADMIN_GROUPS is set, admin status tracks group membership on every login (promote AND demote) on create/migrate/update paths and the IdP is authoritative; when unset, the first-login bootstrap + manual promotion is preserved. OIDC_ALLOWED_GROUPS gates login (admins always pass). Changes apply on next login (OIDC is session-based, unlike per-request proxy auth). - Extract the shared claim helpers (getNestedValue, normalizeGroups, groupsContainAny) into src/server/auth/claims.ts and reuse them in both proxyAuth and oidcAuth; add a pure, unit-tested resolveGroupRole(). proxyAuth re-exports normalizeGroups for back-compat. No schema/migration changes. Docs: - Document DISABLE_LOCAL_AUTH in the SSO guide (was missing) and fix the inaccurate "still available via API" claim — /api/auth/login returns 403 unconditionally when disabled, no bypass. - Add a Group → Role Mapping section with Keycloak/Authentik examples and a pointer to the proxy-auth group vars; list the new vars in the config index. Tests: src/server/auth/claims.test.ts (helpers + resolveGroupRole incl. allowed-gate, demotion, fail-closed). Full suite green (6496). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @Yeraze's task —— View job PR Review: feat(auth): native OIDC group → role mapping + SSO docs fixes
This is a well-structured PR that fills a genuine gap in the auth stack. The core logic is clean and the factoring into Security
Username derived from const username = preferredUsername || email?.split('@')[0] || sub.substring(0, 20);The
Code QualityUnnecessary let user: User | null = null;
user = await databaseService.auth.getUserByOidcSubject(sub) as User | null;This is a pre-existing pattern, but the new code around line 239-245 repeats it for let existingUser: User | null = null;
existingUser = await databaseService.findUserByUsernameAsync(username) as User | null;These can be
databaseService.auditLogAsync(
user!.id,
'user_migrated_to_oidc',
...
);This is a pre-existing pattern in the file, but it means audit log failures are silent. A
Test CoverageThe
Minor / Nits
SummaryThe PR is solid: the abstraction layer in
Items 2 and 3 are pre-existing patterns — whether to fix them in this PR is up to the maintainer. |
Implements the feature requested in #3485 (native OIDC group→role mapping) and fixes the SSO documentation gaps the reporter flagged.
Background — verification of the existing claims
Before building, I verified the maintainer's reply on #3485 against the code:
DISABLE_LOCAL_AUTH=trueexists and hard-blocks/api/auth/login(403), frontend hides the form.PROXY_AUTH_ENABLED,PROXY_AUTH_ADMIN_GROUPS,PROXY_AUTH_NORMAL_USER_GROUPSwith "empty = all",PROXY_AUTH_JWT_GROUPS_CLAIM) all exist and re-evaluate on every request.sso.mddidn't nameDISABLE_LOCAL_AUTHand falsely claimed local auth "remains available via API" when disabled (it 403s with no bypass). Both fixed here.Feature
Three new env vars (parsed in
environment.ts, same conventions asPROXY_AUTH_*):OIDC_GROUPS_CLAIMgroupsrealm_access.roles).OIDC_ADMIN_GROUPSOIDC_ALLOWED_GROUPShandleOIDCCallbacknow resolves the groups claim and:OIDC_ADMIN_GROUPSis set — promote and demote; the IdP becomes authoritative and the first-login bootstrap no longer applies. When unset, the existing bootstrap + manual-promotion behaviour is unchanged.OIDC_ALLOWED_GROUPSgates login for all users (admins always pass), applied before user create/update so revoking a group blocks the next login.The dot-notation traversal + group normalization are factored into
src/server/auth/claims.tsand shared byproxyAuthandoidcAuth(the maintainer's plan called for reuse).proxyAuthre-exportsnormalizeGroupsfor back-compat. No schema/migration —isAdminis an existing boolean column already updated per-login by proxy auth.Design decisions (flagged in the issue scoping)
OIDC_ADMIN_GROUPSis set, a user not in an admin group is demoted (mirrors proxy auth). Docs warn to keep your account in the admin group or a break-glass local admin.Docs
sso.md: documentedDISABLE_LOCAL_AUTH, corrected the false "available via API" line, added a Group → Role Mapping section (Keycloakrealm_access.roles, Authentikgroups) and a pointer to the proxy-auth group vars.index.md: added the three new vars to the config reference.Tests
src/server/auth/claims.test.tscoversgetNestedValue/normalizeGroups/groupsContainAnyand the pureresolveGroupRole(admin match, demotion, allowed-gate, admins-bypass-gate, fail-closed). ExistingproxyAuth.test.tspasses unchanged after the refactor.Validation
tsc --noEmit: cleanCloses #3485.
🤖 Generated with Claude Code