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 an organization setting to control who can edit topics.
|
||||||
- Added ctrl+K keyboard shortcut for getting to search (same as /, but
|
- Added ctrl+K keyboard shortcut for getting to search (same as /, but
|
||||||
works even when you're inside compose).
|
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
|
- Optimized the performance of loading Zulip in an organization with
|
||||||
thousands of users and hundreds of bot users.
|
thousands of users and hundreds of bot users.
|
||||||
- Removed the "Delete streams" administration page; one can delete
|
- Removed the "Delete streams" administration page; one can delete
|
||||||
|
|
|
@ -41,6 +41,9 @@ things:
|
||||||
new organization. Review
|
new organization. Review
|
||||||
[the install instructions](install.html) if you need a
|
[the install instructions](install.html) if you need a
|
||||||
refresher on how this works.
|
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
|
For servers hosting a large number of organizations, like
|
||||||
[zulipchat.com](https://zulipchat.com), one can set
|
[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
|
browser/client to be logged into both). So we don't recommend that
|
||||||
configuration.
|
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
|
### The system bot realm
|
||||||
|
|
||||||
This is very much an implementation detail, but worth documenting to
|
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',
|
'contribute', 'floss', 'foss', 'free', 'opensource', 'open', 'code', 'license',
|
||||||
# intership programs
|
# intership programs
|
||||||
'intern', 'outreachy', 'gsoc', 'gci', 'externship',
|
'intern', 'outreachy', 'gsoc', 'gci', 'externship',
|
||||||
|
# Things that sound like security
|
||||||
|
'auth', 'authentication', 'security',
|
||||||
# tech blogs
|
# tech blogs
|
||||||
'engineering', 'infrastructure', 'tooling', 'tools', 'javascript', 'python'])
|
'engineering', 'infrastructure', 'tooling', 'tools', 'javascript', 'python'])
|
||||||
|
|
||||||
|
|
|
@ -443,7 +443,13 @@ class GitHubAuthBackendTest(ZulipTestCase):
|
||||||
url += "?%s" % (urllib.parse.urlencode(params))
|
url += "?%s" % (urllib.parse.urlencode(params))
|
||||||
|
|
||||||
result = self.client_get(url, **headers)
|
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
|
return result
|
||||||
|
|
||||||
result = self.client_get(result.url, **headers)
|
result = self.client_get(result.url, **headers)
|
||||||
|
@ -527,6 +533,22 @@ class GitHubAuthBackendTest(ZulipTestCase):
|
||||||
parsed_url.path)
|
parsed_url.path)
|
||||||
self.assertTrue(uri.startswith('http://zulip.testserver/accounts/login/subdomain/'))
|
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:
|
def test_github_oauth2_email_not_verified(self) -> None:
|
||||||
account_data_dict = dict(email=self.email, name=self.name)
|
account_data_dict = dict(email=self.email, name=self.name)
|
||||||
with mock.patch('logging.warning') as mock_warning:
|
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:
|
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)
|
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
|
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 = {
|
params = {
|
||||||
'subdomain': get_subdomain(request),
|
'subdomain': get_subdomain(request),
|
||||||
'is_signup': '1' if is_signup else '0',
|
'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")
|
return redirect_to_config_error("google")
|
||||||
|
|
||||||
is_signup = bool(request.GET.get('is_signup'))
|
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:
|
def start_social_login(request: HttpRequest, backend: str) -> HttpResponse:
|
||||||
backend_url = reverse('social:begin', args=[backend])
|
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):
|
settings.SOCIAL_AUTH_GITHUB_SECRET):
|
||||||
return redirect_to_config_error("github")
|
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:
|
def start_social_signup(request: HttpRequest, backend: str) -> HttpResponse:
|
||||||
backend_url = reverse('social:begin', args=[backend])
|
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:
|
def send_oauth_request_to_google(request: HttpRequest) -> HttpResponse:
|
||||||
subdomain = request.GET.get('subdomain', '')
|
subdomain = request.GET.get('subdomain', '')
|
||||||
|
|
|
@ -146,7 +146,7 @@ AUTHENTICATION_BACKENDS = (
|
||||||
# https://github.com/organizations/ORGNAME/settings/developers
|
# https://github.com/organizations/ORGNAME/settings/developers
|
||||||
# Fill in "Callback URL" with a value like
|
# Fill in "Callback URL" with a value like
|
||||||
# https://zulip.example.com/complete/github/ as
|
# 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,
|
# (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
|
# 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_TEAM_ID = <your team id>
|
||||||
#SOCIAL_AUTH_GITHUB_ORG_NAME = <your org name>
|
#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.
|
# 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_KEY': get_secret('social_auth_github_key', development_only=True),
|
||||||
'SOCIAL_AUTH_GITHUB_ORG_NAME': None,
|
'SOCIAL_AUTH_GITHUB_ORG_NAME': None,
|
||||||
'SOCIAL_AUTH_GITHUB_TEAM_ID': None,
|
'SOCIAL_AUTH_GITHUB_TEAM_ID': None,
|
||||||
|
'SOCIAL_AUTH_SUBDOMAIN': None,
|
||||||
|
|
||||||
# Email gateway
|
# Email gateway
|
||||||
'EMAIL_GATEWAY_PATTERN': '',
|
'EMAIL_GATEWAY_PATTERN': '',
|
||||||
|
|
|
@ -145,6 +145,7 @@ GOOGLE_OAUTH2_CLIENT_SECRET = "secret"
|
||||||
|
|
||||||
SOCIAL_AUTH_GITHUB_KEY = "key"
|
SOCIAL_AUTH_GITHUB_KEY = "key"
|
||||||
SOCIAL_AUTH_GITHUB_SECRET = "secret"
|
SOCIAL_AUTH_GITHUB_SECRET = "secret"
|
||||||
|
SOCIAL_AUTH_SUBDOMAIN = 'www'
|
||||||
|
|
||||||
# By default two factor authentication is disabled in tests.
|
# By default two factor authentication is disabled in tests.
|
||||||
# Explicitly set this to True within tests that must have this on.
|
# Explicitly set this to True within tests that must have this on.
|
||||||
|
|
Loading…
Reference in New Issue