auth: Fix bug with subdomains and GitHub auth causing apparent logouts.

This adds a new settings, SOCIAL_AUTH_SUBDOMAIN, which specifies which
domain should be used for GitHub auth and other python-social-auth
backends.

If one is running a single-realm Zulip server like chat.zulip.org, one
doesn't need to use this setting, but for multi-realm servers using
social auth, this fixes an annoying bug where the session cookie that
python-social-auth sets early in the auth process on the root domain
ends up masking the session cookie that would have been used to
determine a user is logged in.  The end result was that logging in
with GitHub on one domain on a multi-realm server like zulipchat.com
would appear to log you out from all the others!

We fix this by moving python-social-auth to a separate subdomain.

Fixes: #9847.
This commit is contained in:
Aditya Bansal 2018-07-10 11:37:23 +05:30 committed by Tim Abbott
parent 4bbccd8287
commit 9b485f3ef4
8 changed files with 65 additions and 6 deletions

View File

@ -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

View File

@ -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

View File

@ -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'])

View File

@ -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:

View File

@ -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', '')

View File

@ -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 = <your team id>
#SOCIAL_AUTH_GITHUB_ORG_NAME = <your 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.
#

View File

@ -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': '',

View File

@ -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.