Merge pull request #3577 from Spinnich/feature/oidc-allow-registration

feat(auth): add OIDC_ALLOW_REGISTRATION toggle to gate OIDC auto-registration
This commit is contained in:
Georges-Antoine Assi
2026-06-22 18:06:13 -04:00
committed by GitHub
5 changed files with 105 additions and 0 deletions

View File

@@ -142,6 +142,9 @@ INVITE_TOKEN_EXPIRY_SECONDS: Final[int] = safe_int(
# OIDC
OIDC_ENABLED: Final[bool] = safe_str_to_bool(_get_env("OIDC_ENABLED"))
OIDC_AUTOLOGIN: Final[bool] = safe_str_to_bool(_get_env("OIDC_AUTOLOGIN"))
OIDC_ALLOW_REGISTRATION: Final[bool] = safe_str_to_bool(
_get_env("OIDC_ALLOW_REGISTRATION", "true")
)
OIDC_PROVIDER: Final[str] = _get_env("OIDC_PROVIDER", "")
OIDC_CLIENT_ID: Final[str] = _get_env("OIDC_CLIENT_ID", "")
OIDC_CLIENT_SECRET: Final[str] = _get_env("OIDC_CLIENT_SECRET", "")

View File

@@ -13,6 +13,7 @@ from starlette.requests import HTTPConnection
from config import (
INVITE_TOKEN_EXPIRY_SECONDS,
OIDC_ALLOW_REGISTRATION,
OIDC_CLAIM_ROLES,
OIDC_ENABLED,
OIDC_ROLE_ADMIN,
@@ -436,6 +437,15 @@ class OpenIDHandler:
user = db_user_handler.get_user_by_email(email)
if user is None:
if not OIDC_ALLOW_REGISTRATION:
log.error(
"User with email '%s' not found and OIDC registration is disabled",
hl(email, color=CYAN),
)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User registration is disabled. Please contact an administrator to create an account.",
)
log.info(
"User with email '%s' not found, creating new user",
hl(email, color=CYAN),

View File

@@ -23,6 +23,16 @@ def mock_oidc_enabled(mocker):
mocker.patch("handler.auth.base_handler.OIDC_ENABLED", True)
@pytest.fixture
def mock_oidc_allow_registration_enabled(mocker):
mocker.patch("handler.auth.base_handler.OIDC_ALLOW_REGISTRATION", True)
@pytest.fixture
def mock_oidc_allow_registration_disabled(mocker):
mocker.patch("handler.auth.base_handler.OIDC_ALLOW_REGISTRATION", False)
@pytest.fixture
def mock_token():
return {
@@ -171,6 +181,7 @@ async def test_oidc_valid_token_decoding(
async def test_oidc_valid_add_user(
mocker,
mock_oidc_enabled,
mock_oidc_allow_registration_enabled,
mock_token,
mock_openid_configuration,
config_override,
@@ -218,6 +229,85 @@ async def test_oidc_valid_add_user(
assert mock_add_user.call_args.args[0].role == romm_role
async def test_oidc_registration_disabled_rejects_unknown_user(
mocker,
mock_oidc_enabled,
mock_oidc_allow_registration_disabled,
mock_token,
mock_openid_configuration,
):
"""Test that a login from an unknown email is rejected when registration is disabled."""
mocker.patch(
"handler.database.db_user_handler.get_user_by_email", return_value=None
)
mock_add_user = mocker.patch("handler.database.db_user_handler.add_user")
mocker.patch.object(
StarletteOAuth2App,
"load_server_metadata",
return_value=mock_openid_configuration,
)
oidc_handler = OpenIDHandler()
with pytest.raises(HTTPException, match="registration is disabled"):
await oidc_handler.get_current_active_user_from_openid_token(mock_token)
mock_add_user.assert_not_called()
async def test_oidc_registration_enabled_creates_unknown_user(
mocker,
mock_oidc_enabled,
mock_oidc_allow_registration_enabled,
mock_token,
mock_openid_configuration,
):
"""Test that a login from an unknown email creates a new user when registration is enabled."""
mocker.patch(
"handler.database.db_user_handler.get_user_by_email", return_value=None
)
mock_user = MagicMock(enabled=True, role=Role.VIEWER)
mock_add_user = mocker.patch(
"handler.database.db_user_handler.add_user", return_value=mock_user
)
mocker.patch.object(
StarletteOAuth2App,
"load_server_metadata",
return_value=mock_openid_configuration,
)
oidc_handler = OpenIDHandler()
user, _ = await oidc_handler.get_current_active_user_from_openid_token(mock_token)
mock_add_user.assert_called_once()
assert user == mock_user
async def test_oidc_registration_disabled_allows_existing_user(
mocker,
mock_oidc_enabled,
mock_oidc_allow_registration_disabled,
mock_token,
mock_openid_configuration,
):
"""Test that an existing user can still log in when registration is disabled."""
mock_user = MagicMock(enabled=True, role=Role.VIEWER)
mocker.patch(
"handler.database.db_user_handler.get_user_by_email", return_value=mock_user
)
mock_add_user = mocker.patch("handler.database.db_user_handler.add_user")
mocker.patch.object(
StarletteOAuth2App,
"load_server_metadata",
return_value=mock_openid_configuration,
)
oidc_handler = OpenIDHandler()
user, _ = await oidc_handler.get_current_active_user_from_openid_token(mock_token)
assert user == mock_user
mock_add_user.assert_not_called()
async def test_oidc_valid_edit_user_role(
mocker,
mock_oidc_enabled,

View File

@@ -1512,6 +1512,7 @@ Falls back to `FakeRedis` in test mode.
| Variable | Default | Description |
| ------------------------------- | -------------------- | ------------------------------- |
| `OIDC_ENABLED` | `false` | Enable OpenID Connect |
| `OIDC_ALLOW_REGISTRATION` | `true` | Auto-create accounts on login |
| `OIDC_PROVIDER` | | Provider URL |
| `OIDC_CLIENT_ID` | | Client ID |
| `OIDC_CLIENT_SECRET` | | Client secret |

View File

@@ -38,6 +38,7 @@ DISABLE_LOGS_VIEWER=false # Disable the backend logs viewer
# OpenID Connect
OIDC_ENABLED=false # Enable OpenID Connect authentication
OIDC_AUTOLOGIN=false # Skip the OIDC button on the login page and auto-redirect
OIDC_ALLOW_REGISTRATION=true # Allow new accounts to be created automatically on first OIDC login
OIDC_PROVIDER= # Name of the OIDC provider in use
OIDC_CLIENT_ID= # Client ID for OIDC authentication
OIDC_CLIENT_SECRET= # Client secret for OIDC authentication