Repository: matrix-org/synapse

Issue:
Allow disabling registration via OIDC and only limiting SSO to sign-in
**Description:**

For some setups that don't allow user registration it may be worthwhile to allow OIDC or OAuth2.0 sign-in for user convenience. However, I haven't found a way to disable creation of new users via OIDC. Sign-in works, but it also enables new users to sign-in to the server freely.

It would be great to change `allow_existing_users` (or have a new setting) with an enum:

- `no` = don't allow sign-in for an existing user
- `yes` = allow sign-in for a pre-existing user w/o OIDC mapping
- `only` = implies `yes`, but additionaly restricts the sign-in to match only pre-existing users


Discussion:
If you want to limit using Synapse to a subset of your SSO users you can likely use probably use the [`attribute_requirements`](https://github.com/matrix-org/synapse/blob/6f440fd859c411af8c32478b4353f7550619c3bd/docs/sample_config.yaml#L1956-L1972) flag on the OIDC provider config. (SAML has something similar too.)
Good point, but it doesn't cover my use-case unfortunately. In my case users are created on my homeserver and I would like them to be able to authenticate via their Google accounts.

There is no distinction between a Google account of a user who is allowed to access my server and Google account of a user who shouldn't access my server - therefore attribute_requirements won't give me an option to prevent an arbitrary user from signing up.
I'm a bit confused about you use-case, is your hope that you can:

1. Configure SSO to Google.
2. Register a handful of users (by logging in once with them).
3. Disable new users from logging in.

Does that match what you're trying to do?

If so, I think you can implement 3 by putting something bogus in `attribute_requirements` since it is only checked during creation of the user.

I do wonder if the SSO code should check `enable_registration` flag though. Although I suspect it can't really cause that's for different flows...
> Does that match what you're trying to do?

Precisely. I didn't know that `attribute_requirements` is checked only during user creation and is not checked during the user login. That's indeed a suitable workaround, although rather fragile IMO.

> I do wonder if the SSO code should check enable_registration flag though. Although I suspect it can't really cause that's for different flows...

I was wonder about that as well. Creation of a new user is effectively registration, imagine how surprised I was when I saw new users on my server created despite `enable_registration` being `false` :smile: 
> > Does that match what you're trying to do?
> 
> Precisely. I didn't know that `attribute_requirements` is checked only during user creation and is not checked during the user login. That's indeed a suitable workaround, although rather fragile IMO.

It is a bit of a hack (and probably a bug that it doesn't check it at each login...)

> > I do wonder if the SSO code should check enable_registration flag though. Although I suspect it can't really cause that's for different flows...
> 
> I was wonder about that as well. Creation of a new user is effectively registration, imagine how surprised I was when I saw new users on my server created despite `enable_registration` being `false` 😄

👍 Yeah, I understand now what you're hoping for!

Resolving patch:
diff --git a/synapse/config/oidc.py b/synapse/config/oidc.py
--- a/synapse/config/oidc.py
+++ b/synapse/config/oidc.py
@@ -136,6 +136,7 @@ def oidc_enabled(self) -> bool:
             "type": "array",
             "items": SsoAttributeRequirement.JSON_SCHEMA,
         },
+        "enable_registration": {"type": "boolean"},
     },
 }
 
@@ -306,6 +307,7 @@ def _parse_oidc_config_dict(
         user_mapping_provider_class=user_mapping_provider_class,
         user_mapping_provider_config=user_mapping_provider_config,
         attribute_requirements=attribute_requirements,
+        enable_registration=oidc_config.get("enable_registration", True),
     )
 
 
@@ -405,3 +407,6 @@ class OidcProviderConfig:
 
     # required attributes to require in userinfo to allow login/registration
     attribute_requirements: List[SsoAttributeRequirement]
+
+    # Whether automatic registrations are enabled in the ODIC flow. Defaults to True
+    enable_registration: bool
diff --git a/synapse/handlers/oidc.py b/synapse/handlers/oidc.py
--- a/synapse/handlers/oidc.py
+++ b/synapse/handlers/oidc.py
@@ -1239,6 +1239,7 @@ async def grandfather_existing_users() -> Optional[str]:
             grandfather_existing_users,
             extra_attributes,
             auth_provider_session_id=sid,
+            registration_enabled=self._config.enable_registration,
         )
 
     def _remote_id_from_userinfo(self, userinfo: UserInfo) -> str:
diff --git a/synapse/handlers/sso.py b/synapse/handlers/sso.py
--- a/synapse/handlers/sso.py
+++ b/synapse/handlers/sso.py
@@ -383,6 +383,7 @@ async def complete_sso_login_request(
         grandfather_existing_users: Callable[[], Awaitable[Optional[str]]],
         extra_login_attributes: Optional[JsonDict] = None,
         auth_provider_session_id: Optional[str] = None,
+        registration_enabled: bool = True,
     ) -> None:
         """
         Given an SSO ID, retrieve the user ID for it and possibly register the user.
@@ -435,6 +436,10 @@ async def complete_sso_login_request(
 
             auth_provider_session_id: An optional session ID from the IdP.
 
+            registration_enabled: An optional boolean to enable/disable automatic
+            registrations of new users. If false and the user does not exist then the
+            flow is aborted. Defaults to true.
+
         Raises:
             MappingException if there was a problem mapping the response to a user.
             RedirectException: if the mapping provider needs to redirect the user
@@ -462,8 +467,16 @@ async def complete_sso_login_request(
                         auth_provider_id, remote_user_id, user_id
                     )
 
-            # Otherwise, generate a new user.
-            if not user_id:
+            if not user_id and not registration_enabled:
+                logger.info(
+                    "User does not exist and registration are disabled for IdP '%s' and remote_user_id '%s'",
+                    auth_provider_id,
+                    remote_user_id,
+                )
+                raise MappingException(
+                    "User does not exist and registrations are disabled"
+                )
+            elif not user_id:  # Otherwise, generate a new user.
                 attributes = await self._call_attribute_mapper(sso_to_matrix_id_mapper)
 
                 next_step_url = self._get_url_for_next_new_user_step(


Test patch:
diff --git a/tests/handlers/test_oidc.py b/tests/handlers/test_oidc.py
--- a/tests/handlers/test_oidc.py
+++ b/tests/handlers/test_oidc.py
@@ -922,7 +922,7 @@ def test_extra_attributes(self) -> None:
             auth_provider_session_id=None,
         )
 
-    @override_config({"oidc_config": DEFAULT_CONFIG})
+    @override_config({"oidc_config": {**DEFAULT_CONFIG, "enable_registration": True}})
     def test_map_userinfo_to_user(self) -> None:
         """Ensure that mapping the userinfo returned from a provider to an MXID works properly."""
         userinfo: dict = {
@@ -975,6 +975,21 @@ def test_map_userinfo_to_user(self) -> None:
             "Mapping provider does not support de-duplicating Matrix IDs",
         )
 
+    @override_config({"oidc_config": {**DEFAULT_CONFIG, "enable_registration": False}})
+    def test_map_userinfo_to_user_does_not_register_new_user(self) -> None:
+        """Ensures new users are not registered if the enabled registration flag is disabled."""
+        userinfo: dict = {
+            "sub": "test_user",
+            "username": "test_user",
+        }
+        request, _ = self.start_authorization(userinfo)
+        self.get_success(self.handler.handle_oidc_callback(request))
+        self.complete_sso_login.assert_not_called()
+        self.assertRenderedError(
+            "mapping_error",
+            "User does not exist and registrations are disabled",
+        )
+
     @override_config({"oidc_config": {**DEFAULT_CONFIG, "allow_existing_users": True}})
     def test_map_userinfo_to_existing_user(self) -> None:
         """Existing users can log in with OpenID Connect when allow_existing_users is True."""