diff --git a/docs/overview/changelog.md b/docs/overview/changelog.md index f42dc8ea07..79abc03974 100644 --- a/docs/overview/changelog.md +++ b/docs/overview/changelog.md @@ -20,6 +20,9 @@ in bursts. - Added an organization setting to control who can edit topics. - Added ctrl+K keyboard shortcut for getting to search (same as /, but works even when you're inside compose). +- Added the new `SOCIAL_AUTH_SUBDOMAIN` setting, which all servers using + both GitHub authentication and hosting multiple Zulip organizations + should set (see [the docs for details](../production/multiple-organizations.html#social-authentication)). - Optimized the performance of loading Zulip in an organization with thousands of users and hundreds of bot users. - Removed the "Delete streams" administration page; one can delete diff --git a/docs/production/multiple-organizations.md b/docs/production/multiple-organizations.md index 4aaf94c62b..e38677f492 100644 --- a/docs/production/multiple-organizations.md +++ b/docs/production/multiple-organizations.md @@ -41,6 +41,9 @@ things: new organization. Review [the install instructions](install.html) if you need a refresher on how this works. +* If you're planning on using GitHub auth or another social + authentication method, review + [the notes on `SOCIAL_AUTH_SUBDOMAIN` below](#social-authentication). For servers hosting a large number of organizations, like [zulipchat.com](https://zulipchat.com), one can set @@ -80,6 +83,17 @@ visible to the subdomain (so it's not possible for a single browser/client to be logged into both). So we don't recommend that configuration. +### Social authentication + +If you're using GitHub authentication (or any other authentication +backend that we implement using python-social-auth), you will likely +want to set the `SOCIAL_AUTH_SUBDOMAIN` setting to something (`'auth'` +is a good choice) and update the GitHub authentication callback URL to +be that subdomain. Otherwise, your users will experience confusing +behavior where attempting to login using a social authentication +backend will appear to log them out of the other organizations on your +server. + ### The system bot realm This is very much an implementation detail, but worth documenting to diff --git a/zerver/lib/name_restrictions.py b/zerver/lib/name_restrictions.py index 69561bd7e5..20fc36db9c 100644 --- a/zerver/lib/name_restrictions.py +++ b/zerver/lib/name_restrictions.py @@ -34,6 +34,8 @@ ZULIP_RESERVED_SUBDOMAINS = frozenset([ 'contribute', 'floss', 'foss', 'free', 'opensource', 'open', 'code', 'license', # intership programs 'intern', 'outreachy', 'gsoc', 'gci', 'externship', + # Things that sound like security + 'auth', 'authentication', 'security', # tech blogs 'engineering', 'infrastructure', 'tooling', 'tools', 'javascript', 'python']) diff --git a/zerver/tests/test_auth_backends.py b/zerver/tests/test_auth_backends.py index 745a6eaf07..5042a6de6f 100644 --- a/zerver/tests/test_auth_backends.py +++ b/zerver/tests/test_auth_backends.py @@ -443,7 +443,13 @@ class GitHubAuthBackendTest(ZulipTestCase): url += "?%s" % (urllib.parse.urlencode(params)) result = self.client_get(url, **headers) - if result.status_code != 302 or 'http://testserver/login/github/' not in result.url: + + expected_result_url_prefix = 'http://testserver/login/github/' + if settings.SOCIAL_AUTH_SUBDOMAIN is not None: + expected_result_url_prefix = ('http://%s.testserver/login/github/' % + settings.SOCIAL_AUTH_SUBDOMAIN) + + if result.status_code != 302 or not result.url.startswith(expected_result_url_prefix): return result result = self.client_get(result.url, **headers) @@ -527,6 +533,22 @@ class GitHubAuthBackendTest(ZulipTestCase): parsed_url.path) self.assertTrue(uri.startswith('http://zulip.testserver/accounts/login/subdomain/')) + @override_settings(SOCIAL_AUTH_SUBDOMAIN=None) + def test_github_when_social_auth_subdomain_is_not_set(self) -> None: + account_data_dict = dict(email=self.email, name=self.name) + result = self.github_oauth2_test(account_data_dict, + subdomain='zulip', next='/user_uploads/image') + data = load_subdomain_token(result) + self.assertEqual(data['email'], self.example_email("hamlet")) + self.assertEqual(data['name'], 'Hamlet') + self.assertEqual(data['subdomain'], 'zulip') + self.assertEqual(data['next'], '/user_uploads/image') + self.assertEqual(result.status_code, 302) + parsed_url = urllib.parse.urlparse(result.url) + uri = "{}://{}{}".format(parsed_url.scheme, parsed_url.netloc, + parsed_url.path) + self.assertTrue(uri.startswith('http://zulip.testserver/accounts/login/subdomain/')) + def test_github_oauth2_email_not_verified(self) -> None: account_data_dict = dict(email=self.email, name=self.name) with mock.patch('logging.warning') as mock_warning: diff --git a/zerver/views/auth.py b/zerver/views/auth.py index 6a492cc649..2d053ccdc8 100644 --- a/zerver/views/auth.py +++ b/zerver/views/auth.py @@ -275,8 +275,15 @@ def google_oauth2_csrf(request: HttpRequest, value: str) -> str: def reverse_on_root(viewname: str, args: List[str]=None, kwargs: Dict[str, str]=None) -> str: return settings.ROOT_DOMAIN_URI + reverse(viewname, args=args, kwargs=kwargs) -def oauth_redirect_to_root(request: HttpRequest, url: str, is_signup: bool=False) -> HttpResponse: +def oauth_redirect_to_root(request: HttpRequest, url: str, + sso_type: str, is_signup: bool=False) -> HttpResponse: main_site_uri = settings.ROOT_DOMAIN_URI + url + if settings.SOCIAL_AUTH_SUBDOMAIN is not None and sso_type == 'social': + main_site_uri = (settings.EXTERNAL_URI_SCHEME + + settings.SOCIAL_AUTH_SUBDOMAIN + + "." + + settings.EXTERNAL_HOST) + url + params = { 'subdomain': get_subdomain(request), 'is_signup': '1' if is_signup else '0', @@ -303,7 +310,7 @@ def start_google_oauth2(request: HttpRequest) -> HttpResponse: return redirect_to_config_error("google") is_signup = bool(request.GET.get('is_signup')) - return oauth_redirect_to_root(request, url, is_signup=is_signup) + return oauth_redirect_to_root(request, url, 'google', is_signup=is_signup) def start_social_login(request: HttpRequest, backend: str) -> HttpResponse: backend_url = reverse('social:begin', args=[backend]) @@ -311,11 +318,11 @@ def start_social_login(request: HttpRequest, backend: str) -> HttpResponse: settings.SOCIAL_AUTH_GITHUB_SECRET): return redirect_to_config_error("github") - return oauth_redirect_to_root(request, backend_url) + return oauth_redirect_to_root(request, backend_url, 'social') def start_social_signup(request: HttpRequest, backend: str) -> HttpResponse: backend_url = reverse('social:begin', args=[backend]) - return oauth_redirect_to_root(request, backend_url, is_signup=True) + return oauth_redirect_to_root(request, backend_url, 'social', is_signup=True) def send_oauth_request_to_google(request: HttpRequest) -> HttpResponse: subdomain = request.GET.get('subdomain', '') diff --git a/zproject/prod_settings_template.py b/zproject/prod_settings_template.py index 1d9f333525..b550da8fbc 100644 --- a/zproject/prod_settings_template.py +++ b/zproject/prod_settings_template.py @@ -146,7 +146,7 @@ AUTHENTICATION_BACKENDS = ( # https://github.com/organizations/ORGNAME/settings/developers # Fill in "Callback URL" with a value like # https://zulip.example.com/complete/github/ as -# based on your value for EXTERNAL_HOST. +# based on your values for EXTERNAL_HOST and SOCIAL_AUTH_SUBDOMAIN. # # (2) You should get a page with settings for your new application, # showing a client ID and a client secret. Use the client ID as @@ -161,6 +161,15 @@ AUTHENTICATION_BACKENDS = ( #SOCIAL_AUTH_GITHUB_TEAM_ID = #SOCIAL_AUTH_GITHUB_ORG_NAME = +# (4) If you are serving multiple Zulip organizations on different +# subdomains, you need to set SOCIAL_AUTH_SUBDOMAIN. You can set it +# to any subdomain on which you do not plan to host a Zulip +# organization. The default recommendation, `auth`, is a reserved +# subdomain; if you're using this setting, the "Callback URL" should be e.g.: +# https://auth.zulip.example.com/complete/github/ +# +#SOCIAL_AUTH_SUBDOMAIN = 'auth' + ######## # SSO via REMOTE_USER. # diff --git a/zproject/settings.py b/zproject/settings.py index 192c715f36..1e12064eb4 100644 --- a/zproject/settings.py +++ b/zproject/settings.py @@ -145,6 +145,7 @@ DEFAULT_SETTINGS = { 'SOCIAL_AUTH_GITHUB_KEY': get_secret('social_auth_github_key', development_only=True), 'SOCIAL_AUTH_GITHUB_ORG_NAME': None, 'SOCIAL_AUTH_GITHUB_TEAM_ID': None, + 'SOCIAL_AUTH_SUBDOMAIN': None, # Email gateway 'EMAIL_GATEWAY_PATTERN': '', diff --git a/zproject/test_settings.py b/zproject/test_settings.py index b6070598fa..1e3b7a0820 100644 --- a/zproject/test_settings.py +++ b/zproject/test_settings.py @@ -145,6 +145,7 @@ GOOGLE_OAUTH2_CLIENT_SECRET = "secret" SOCIAL_AUTH_GITHUB_KEY = "key" SOCIAL_AUTH_GITHUB_SECRET = "secret" +SOCIAL_AUTH_SUBDOMAIN = 'www' # By default two factor authentication is disabled in tests. # Explicitly set this to True within tests that must have this on.