Repository: zulip/zulip

Issue:
zulip 5 oidc potential bug.
I've configured OpenId connect in settings.py from the main git branch. I'm pretty sure I've configured it correctly but I am getting an error when trying to login:

500 Internal Server error with this url in the browser bar:
https://site.tld/login/oidc/?subdomain=&is_signup=0&multiuse_object_key=&next=%2F

I'm using Keycloak and my oidc_url is "https://adiffsite.tld/auth/realms/MyRealm/"







Discussion:
Upon further investigation it looks like zulip was flaking out because keycloak was passing back only a username instead of passing it a first & last name in the oidc session.

`  File "/home/zulip/deployments/2022-01-18-07-14-28/./zproject/backends.py", line 1573, in social_associate_user_helper
    raise AssertionError("Social auth backend doesn't provide name")
`
Hello @zulip/server-authentication members, this issue was labeled with the "area: authentication" label, so you may want to check it out!

<!-- areaLabelAddition -->

@ericbets can you stop by production help in chat.zulip.org to debug?

@mateuszmandera can you help investigate?
Closing since requiring a first and lastname in/from the oidc session is a reasonable requirement.
@ericbet If some providers, like Keycloak, by default may not send the first/last name, then I think we want to allow that (and if we didn't, we should be documenting the requirement clearly), so I'll reopen.

Resolving patch:
diff --git a/zproject/backends.py b/zproject/backends.py
--- a/zproject/backends.py
+++ b/zproject/backends.py
@@ -1557,14 +1557,17 @@ def social_associate_user_helper(
     full_name = kwargs["details"].get("fullname")
     first_name = kwargs["details"].get("first_name")
     last_name = kwargs["details"].get("last_name")
+
     if all(name is None for name in [full_name, first_name, last_name]) and backend.name not in [
         "apple",
         "saml",
+        "oidc",
     ]:
         # (1) Apple authentication provides the user's name only the very first time a user tries to log in.
         # So if the user aborts login or otherwise is doing this the second time,
         # we won't have any name data.
-        # (2) Some IdPs may not send any name value if the user doesn't have them set in the IdP's directory.
+        # (2) Some SAML or OIDC IdPs may not send any name value if the user doesn't
+        # have them set in the IdP's directory.
         #
         # The name will just default to the empty string in the code below.
 


Test patch:
diff --git a/zerver/tests/test_auth_backends.py b/zerver/tests/test_auth_backends.py
--- a/zerver/tests/test_auth_backends.py
+++ b/zerver/tests/test_auth_backends.py
@@ -3336,13 +3336,21 @@ def generate_access_token_url_payload(self, account_data_dict: Dict[str, str]) -
             }
         )
 
-    def get_account_data_dict(self, email: str, name: str) -> Dict[str, Any]:
+    def get_account_data_dict(self, email: str, name: Optional[str]) -> Dict[str, Any]:
+        if name is not None:
+            name_parts = name.split(" ")
+            given_name = name_parts[0]
+            family_name = name_parts[1]
+        else:
+            given_name = None
+            family_name = None
+
         return dict(
             email=email,
             name=name,
             nickname="somenickname",
-            given_name=name.split(" ")[0],
-            family_name=name.split(" ")[1],
+            given_name=given_name,
+            family_name=family_name,
         )
 
     @override_settings(TERMS_OF_SERVICE_VERSION=None)
@@ -3377,6 +3385,28 @@ def test_social_auth_registration_auto_signup(self) -> None:
                 expect_confirm_registration_page=False,
             )
 
+    def test_auth_registration_with_no_name_provided(self) -> None:
+        """
+        The OIDC IdP may not send the name information. The
+        signup flow should proceed normally, without pre-filling the name in the
+        registration form.
+        """
+        email = "newuser@zulip.com"
+        subdomain = "zulip"
+        realm = get_realm("zulip")
+        account_data_dict = self.get_account_data_dict(email=email, name=None)
+        result = self.social_auth_test(account_data_dict, subdomain=subdomain, is_signup=True)
+        self.stage_two_of_registration(
+            result,
+            realm,
+            subdomain,
+            email,
+            "",
+            "Full Name",
+            skip_registration_form=False,
+            expect_full_name_prepopulated=False,
+        )
+
     def test_social_auth_no_key(self) -> None:
         """
         Requires overriding because client key/secret are configured