diff --git a/changelog.d/18454.misc b/changelog.d/18454.misc
new file mode 100644
index 0000000000..892fbd1d94
--- /dev/null
+++ b/changelog.d/18454.misc
@@ -0,0 +1 @@
+Allow checking only for the existence of a field in an SSO provider's response, rather than requiring the value(s) to check.
\ No newline at end of file
diff --git a/docs/usage/configuration/config_documentation.md b/docs/usage/configuration/config_documentation.md
index e688bc5cd8..3927b9ca14 100644
--- a/docs/usage/configuration/config_documentation.md
+++ b/docs/usage/configuration/config_documentation.md
@@ -3782,17 +3782,23 @@ match particular values in the OIDC userinfo. The requirements can be listed und
```yaml
attribute_requirements:
- attribute: family_name
- value: "Stephensson"
+ one_of: ["Stephensson", "Smith"]
- attribute: groups
value: "admin"
+ # If `value` or `one_of` are not specified, the attribute only needs
+ # to exist, regardless of value.
+ - attribute: picture
```
+
+`attribute` is a required field, while `value` and `one_of` are optional.
+
All of the listed attributes must match for the login to be permitted. Additional attributes can be added to
userinfo by expanding the `scopes` section of the OIDC config to retrieve
additional information from the OIDC provider.
If the OIDC claim is a list, then the attribute must match any value in the list.
Otherwise, it must exactly match the value of the claim. Using the example
-above, the `family_name` claim MUST be "Stephensson", but the `groups`
+above, the `family_name` claim MUST be either "Stephensson" or "Smith", but the `groups`
claim MUST contain "admin".
Example configuration:
diff --git a/synapse/config/sso.py b/synapse/config/sso.py
index 97b85e47ea..cf27a7ee13 100644
--- a/synapse/config/sso.py
+++ b/synapse/config/sso.py
@@ -43,8 +43,7 @@ class SsoAttributeRequirement:
"""Object describing a single requirement for SSO attributes."""
attribute: str
- # If neither value nor one_of is given, the attribute must simply exist. This is
- # only true for CAS configs which use a different JSON schema than the one below.
+ # If neither `value` nor `one_of` is given, the attribute must simply exist.
value: Optional[str] = None
one_of: Optional[List[str]] = None
@@ -56,10 +55,6 @@ class SsoAttributeRequirement:
"one_of": {"type": "array", "items": {"type": "string"}},
},
"required": ["attribute"],
- "oneOf": [
- {"required": ["value"]},
- {"required": ["one_of"]},
- ],
}
diff --git a/tests/handlers/test_oidc.py b/tests/handlers/test_oidc.py
index e5f31d57ca..ff8e3c5cb6 100644
--- a/tests/handlers/test_oidc.py
+++ b/tests/handlers/test_oidc.py
@@ -1453,7 +1453,7 @@ class OidcHandlerTestCase(HomeserverTestCase):
}
}
)
- def test_attribute_requirements_one_of(self) -> None:
+ def test_attribute_requirements_one_of_succeeds(self) -> None:
"""Test that auth succeeds if userinfo attribute has multiple values and CONTAINS required value"""
# userinfo with "test": ["bar"] attribute should succeed.
userinfo = {
@@ -1479,6 +1479,81 @@ class OidcHandlerTestCase(HomeserverTestCase):
{
"oidc_config": {
**DEFAULT_CONFIG,
+ "attribute_requirements": [
+ {"attribute": "test", "one_of": ["foo", "bar"]}
+ ],
+ }
+ }
+ )
+ def test_attribute_requirements_one_of_fails(self) -> None:
+ """Test that auth fails if userinfo attribute has multiple values yet
+ DOES NOT CONTAIN a required value
+ """
+ # userinfo with "test": ["something else"] attribute should fail.
+ userinfo = {
+ "sub": "tester",
+ "username": "tester",
+ "test": ["something else"],
+ }
+ request, _ = self.start_authorization(userinfo)
+ self.get_success(self.handler.handle_oidc_callback(request))
+ self.complete_sso_login.assert_not_called()
+
+ @override_config(
+ {
+ "oidc_config": {
+ **DEFAULT_CONFIG,
+ "attribute_requirements": [{"attribute": "test"}],
+ }
+ }
+ )
+ def test_attribute_requirements_does_not_exist(self) -> None:
+ """OIDC login fails if the required attribute does not exist in the OIDC userinfo response."""
+ # userinfo lacking "test" attribute should fail.
+ userinfo = {
+ "sub": "tester",
+ "username": "tester",
+ }
+ request, _ = self.start_authorization(userinfo)
+ self.get_success(self.handler.handle_oidc_callback(request))
+ self.complete_sso_login.assert_not_called()
+
+ @override_config(
+ {
+ "oidc_config": {
+ **DEFAULT_CONFIG,
+ "attribute_requirements": [{"attribute": "test"}],
+ }
+ }
+ )
+ def test_attribute_requirements_exist(self) -> None:
+ """OIDC login succeeds if the required attribute exist (regardless of value)
+ in the OIDC userinfo response.
+ """
+ # userinfo with "test" attribute and random value should succeed.
+ userinfo = {
+ "sub": "tester",
+ "username": "tester",
+ "test": random_string(5), # value does not matter
+ }
+ request, _ = self.start_authorization(userinfo)
+ self.get_success(self.handler.handle_oidc_callback(request))
+
+ # check that the auth handler got called as expected
+ self.complete_sso_login.assert_called_once_with(
+ "@tester:test",
+ self.provider.idp_id,
+ request,
+ ANY,
+ None,
+ new_user=True,
+ auth_provider_session_id=None,
+ )
+
+ @override_config(
+ {
+ "oidc_config": {
+ **DEFAULT_CONFIG,
"attribute_requirements": [{"attribute": "test", "value": "foobar"}],
}
}
|