diff --git a/backend/app/config.py b/backend/app/config.py index 5fb83a5..b6fe530 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -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. diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 461cf7d..4b70d7b 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -385,6 +385,23 @@ async def oidc_callback(request: Request, db: AsyncSession = Depends(get_db)): select(User).where(User.email == email) )).scalar_one_or_none() + # 3. Last-resort link by preferred_username. Off by default; only + # used in trusted single-tenant setups where local accounts + # predate OIDC and never collected email (the app has no UI for + # it). Guarded by OIDC_LINK_BY_USERNAME to avoid hijacking + # accounts in shared instances. + if user is None and settings.oidc_link_by_username: + preferred = claims.get("preferred_username") + if preferred: + user = (await db.execute( + select(User).where(User.username == preferred) + )).scalar_one_or_none() + if user is not None: + logger.info( + "OIDC linked existing user %s by preferred_username", + preferred, + ) + if user is None: if not settings.oidc_allow_signup: logger.info("OIDC signup disabled — rejecting unknown sub=%s email=%s", sub, email) diff --git a/docker-compose.yml b/docker-compose.yml index 78dcc36..9a5ff42 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -69,6 +69,7 @@ services: - OIDC_PROVIDER_LABEL=${OIDC_PROVIDER_LABEL:-Authentik} - OIDC_ALLOW_SIGNUP=${OIDC_ALLOW_SIGNUP:-true} - OIDC_ADMIN_GROUPS=${OIDC_ADMIN_GROUPS:-} + - OIDC_LINK_BY_USERNAME=${OIDC_LINK_BY_USERNAME:-false} - SESSION_SECRET=${SESSION_SECRET:-} - LOG_LEVEL=${LOG_LEVEL:-INFO} - TZ=${TZ:-UTC}