Skip to content

Add external_auth_only flag for federated users + protect it with ROLE_ACCESS_USER_EXTERNAL_AUTH #399

@ambroisemaupate

Description

@ambroisemaupate

Roadiz supports OpenID Connect SSO authentication as documented here:
https://docs.roadiz.io/developer/first-steps/manual_config.html#openid-sso-authentication

OIDC configuration includes the requires_local_user option:

  • requires_local_user: false → stateless SSO accounts (no local User required). In this mode, there is no local password surface, so the SSO bypass issue does not apply.
  • requires_local_user: true → Roadiz requires a persisted local User entity to authenticate the user.

Important context: Roadiz currently does not implement just-in-time (JIT) provisioning. Users must already exist locally for authentication to succeed when requires_local_user: true.

Even without JIT, we still have a security gap: if a locally-existing user is intended to be “federated-only” (SSO-only), they must not be able to (1) authenticate via local password, or (2) enable password authentication later by setting/changing a password, or (3) remove such constraints from themselves or colleagues.

This issue proposes introducing an explicit external_auth_only flag on the local User entity plus a dedicated role ROLE_ACCESS_USER_EXTERNAL_AUTH that controls who is allowed to manage this flag. Federated admins must not be able to disable this flag on their own account or on other federated accounts unless they hold this dedicated role.

Goals

  • Provide a first-class way to mark a local user as “external authentication only” when using OIDC with requires_local_user: true.
  • Enforce that external-auth-only users can never use local password authentication or password management features.
  • Add a dedicated permission gate to prevent federated admins from removing this protection.
  • Keep stateless mode (requires_local_user: false) unchanged.

Non-goals

  • Implementing JIT provisioning (can be a follow-up issue).
  • Implementing SCIM provisioning / deprovisioning.
  • Refactoring the full authentication system beyond enforcing this constraint.

Proposed changes

Database / Entity

Add a boolean column on User:

  • Property: externalAuthOnly
  • DB column: external_auth_only
  • Type: boolean
  • Default: false
  • Not nullable

Add:

  • isExternalAuthOnly(): bool
  • setExternalAuthOnly(bool $value): self

New role

Introduce:

  • ROLE_ACCESS_USER_EXTERNAL_AUTH

Meaning:

  • Required to toggle external_auth_only on any user.
  • Without it, a user cannot change this flag (even if they have ROLE_ADMIN / ROLE_SUPER_ADMIN, unless explicitly granted this new role as well).

Note: this role is intentionally separate from other admin roles to avoid self-privilege escalation by federated admins.

Authentication enforcement (applies only with OIDC + requires_local_user: true)

If external_auth_only = true:

  • Reject any password-based authentication attempt at the security layer (UserChecker or equivalent).
  • Disable or reject password reset flows for that user.
  • Disable or reject password change flows for that user.
  • Prevent setting a local password for that user through admin UI / API.

This must be enforced server-side; UI-only is insufficient.

If requires_local_user: false, this enforcement is irrelevant (no local auth expected).

Admin UI / API restrictions

Core rules:

  • Only users with ROLE_ACCESS_USER_EXTERNAL_AUTH can toggle external_auth_only on any account.

Additionally:

  • A user without ROLE_ACCESS_USER_EXTERNAL_AUTH must not be able to unset external_auth_only:
    • on their own account
    • on any account currently marked as external-auth-only

If violated:

  • Return HTTP 403.
  • Log the attempt.
  • UI should hide/disable the field for unauthorized users, but server-side enforcement is mandatory.

Implementation suggestion:

  • Add a dedicated voter or policy service:
  • canManageExternalAuthFlag(User $currentUser, User $targetUser, bool $newValue): bool

Operational behavior (since there is no JIT yet)

Because users must exist locally when requires_local_user: true:

  • Admins will explicitly set external_auth_only = true on accounts that are meant to be federated-only (e.g. all Workspace admins).
  • This immediately hardens the instance against “SSO bypass” through password login or password resets.

Audit and logging

  • Log every change to external_auth_only (actor, target, old value, new value).

Acceptance criteria

  • Works only/primarily in the scenario OIDC is enabled and requires_local_user: true; stateless mode remains unchanged.
  • If a user has external_auth_only = true:
    • password-based login is denied
    • password reset is denied
    • password change is denied
    • admin UI/API cannot set a password for them
  • Users without ROLE_ACCESS_USER_EXTERNAL_AUTH cannot unset the flag:
    • on themselves
    • on other external-auth-only accounts
  • Users with ROLE_ACCESS_USER_EXTERNAL_AUTH can toggle the flag on any user.
  • DB migration included, defaulting existing users to false.

Suggested tests

Functional/security tests with requires_local_user: true:

  • Password login attempt for external_auth_only = true → denied.
  • Reset password request for external_auth_only = true → denied.
  • Change password attempt for external_auth_only = true → denied.
  • Admin without ROLE_ACCESS_USER_EXTERNAL_AUTH tries to unset the flag on self or colleague → 403.
  • Admin with ROLE_ACCESS_USER_EXTERNAL_AUTH unsets the flag → success.

Regression test with requires_local_user: false:

  • Stateless OIDC login works as before (no changes / no dependency on external_auth_only).

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions