diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e168ea54..aebe5541f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to ## Added +🔧(backend) add option to configure list of required OIDC claims #525 🔧(helm) add option to disable default tls setting by @dominikkaminski #519 ## Changed diff --git a/src/backend/core/authentication/backends.py b/src/backend/core/authentication/backends.py index db115f82d..4d7f80e99 100644 --- a/src/backend/core/authentication/backends.py +++ b/src/backend/core/authentication/backends.py @@ -57,6 +57,18 @@ def get_userinfo(self, access_token, id_token, payload): _("Invalid response format or token verification failed") ) from e + # Validate required claims + missing_claims = [ + claim + for claim in settings.USER_OIDC_REQUIRED_CLAIMS + if claim not in userinfo + ] + if missing_claims: + raise SuspiciousOperation( + _("Missing required claims in user info: %(claims)s") + % {"claims": ", ".join(missing_claims)} + ) + return userinfo def get_or_create_user(self, access_token, id_token, payload): diff --git a/src/backend/core/tests/authentication/test_backends.py b/src/backend/core/tests/authentication/test_backends.py index 8bf8b9f2d..dc937da67 100644 --- a/src/backend/core/tests/authentication/test_backends.py +++ b/src/backend/core/tests/authentication/test_backends.py @@ -365,3 +365,87 @@ def get_userinfo_mocked(*args): klass.get_or_create_user(access_token="test-token", id_token=None, payload=None) assert models.User.objects.count() == 1 + + +# Required claims + + +@override_settings( + OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo", + USER_OIDC_REQUIRED_CLAIMS=["email", "sub", "address"], +) +@responses.activate +def test_authentication_get_userinfo_required_claims_missing(): + """Ensure SuspiciousOperation is raised if required claims are missing.""" + + responses.add( + responses.GET, + re.compile(r".*/userinfo"), + json={ + "last_name": "Doe", + "email": "john.doe@example.com", + }, + status=200, + ) + + oidc_backend = OIDCAuthenticationBackend() + + with pytest.raises( + SuspiciousOperation, match="Missing required claims in user info: sub, address" + ): + oidc_backend.get_userinfo("fake_access_token", None, None) + + +@override_settings( + OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo", + USER_OIDC_REQUIRED_CLAIMS=["email", "Sub"], +) +@responses.activate +def test_authentication_get_userinfo_required_claims_case_sensitivity(): + """Ensure the system respects case sensitivity for required claims.""" + + responses.add( + responses.GET, + re.compile(r".*/userinfo"), + json={ + "sub": "123", + "last_name": "Doe", + "email": "john.doe@example.com", + }, + status=200, + ) + + oidc_backend = OIDCAuthenticationBackend() + + with pytest.raises( + SuspiciousOperation, match="Missing required claims in user info: Sub" + ): + oidc_backend.get_userinfo("fake_access_token", None, None) + + +@override_settings( + OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo", + USER_OIDC_REQUIRED_CLAIMS=["email", "sub"], +) +@responses.activate +def test_authentication_get_userinfo_required_claims_success(): + """Ensure user is authenticated when required claims are present.""" + + responses.add( + responses.GET, + re.compile(r".*/userinfo"), + json={ + "sub": "123", + "last_name": "Doe", + "email": "john.doe@example.com", + }, + status=200, + ) + + oidc_backend = OIDCAuthenticationBackend() + result = oidc_backend.get_userinfo("fake_access_token", None, None) + + assert result["sub"] == "123" + assert result.get("first_name") is None + assert result["last_name"] == "Doe" + assert result["email"] == "john.doe@example.com" diff --git a/src/backend/impress/settings.py b/src/backend/impress/settings.py index 86be1e558..f29dce1ac 100755 --- a/src/backend/impress/settings.py +++ b/src/backend/impress/settings.py @@ -474,6 +474,9 @@ class Base(Configuration): environ_prefix=None, ) + USER_OIDC_REQUIRED_CLAIMS = values.ListValue( + default=[], environ_name="USER_OIDC_REQUIRED_CLAIMS", environ_prefix=None + ) USER_OIDC_FIELDS_TO_FULLNAME = values.ListValue( default=["first_name", "last_name"], environ_name="USER_OIDC_FIELDS_TO_FULLNAME",