mirror of https://github.com/zulip/zulip.git
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:
parent
4bbccd8287
commit
9b485f3ef4
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'])
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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', '')
|
||||
|
|
|
@ -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.
|
||||
#
|
||||
|
|
|
@ -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': '',
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue