feat(auth): OIDC link by preferred_username (opt-in)

Adds OIDC_LINK_BY_USERNAME as a last-resort linking step after
(issuer, sub) and email both miss. Matches IdP preferred_username
against users.username.

Why: local accounts created before OIDC never collected an email
(no UI for it), so the email fallback cannot relink them. A new
SSO login therefore falls into JIT and creates username-1. On a
single-tenant homelab where the IdP owns the namespace, matching
by username is safe and makes first-time SSO transparent for
pre-existing users. Gated behind a flag so multi-tenant deployments
keep the stricter default.
This commit is contained in:
root
2026-04-22 22:24:13 +02:00
parent e8e1adcf37
commit 80dd9d0a8b
3 changed files with 25 additions and 0 deletions

View File

@@ -130,6 +130,13 @@ class Settings(BaseSettings):
# promotes the user to role=admin; otherwise role=user. Role is
# refreshed on every sign-in so removals demote automatically.
oidc_admin_groups: str = Field(default="", env="OIDC_ADMIN_GROUPS")
# Last-resort link step: if (issuer, sub) AND email fallback both
# miss, try matching the IdP's `preferred_username` claim against
# `users.username`. Safe in single-tenant setups where the IdP is
# the source of truth for usernames (homelab, family instance).
# Leave off in multi-tenant — a name collision would hand someone
# else's account to a new SSO user.
oidc_link_by_username: bool = Field(default=False, env="OIDC_LINK_BY_USERNAME")
# Starlette session cookie secret — only used to hold PKCE/state
# during the brief OIDC round-trip. Falls back to secret_key when
# unset.