auth: Migrate google auth to python-social-auth.

This replaces the two custom Google authentication backends originally
written in 2012 with using the shared python-social-auth codebase that
we already use for the GitHub authentication backend.  These are:

* GoogleMobileOauth2Backend, the ancient code path for mobile
  authentication last used by the EOL original Zulip Android app.

* The `finish_google_oauth2` code path in zerver/views/auth.py, which
  was the webapp (and modern mobile app) Google authentication code
  path.

This change doesn't fix any known bugs; its main benefit is that we
get to remove hundreds of lines of security-sensitive semi-duplicated
code, replacing it with a widely trusted, high quality third-party
library.
This commit is contained in:
Harshit Bansal 2019-02-02 15:51:26 +00:00 committed by Tim Abbott
parent 5fc37c5f9b
commit bf14a0af4d
22 changed files with 146 additions and 732 deletions

View File

@ -26,7 +26,7 @@ creating the initial realm and user. You can disable it after that.
With just a few lines of configuration, your Zulip server can With just a few lines of configuration, your Zulip server can
authenticate users with any of several single-sign-on (SSO) authenticate users with any of several single-sign-on (SSO)
authentication providers: authentication providers:
* Google accounts, with `GoogleMobileOauth2Backend` * Google accounts, with `GoogleAuthBackend`
* GitHub accounts, with `GitHubAuthBackend` * GitHub accounts, with `GitHubAuthBackend`
* Microsoft Azure Active Directory, with `AzureADAuthBackend` * Microsoft Azure Active Directory, with `AzureADAuthBackend`

View File

@ -32,17 +32,17 @@ Here are the full procedures for dev:
services" > "Credentials". Create a "Project" which will correspond services" > "Credentials". Create a "Project" which will correspond
to your dev environment. to your dev environment.
* Navigate to "APIs & services" > "Library", and find the "Google+ * Navigate to "APIs & services" > "Library", and find the "Identity
API". Choose "Enable". Toolkit API". Choose "Enable".
* Return to "Credentials", and select "Create credentials". Choose * Return to "Credentials", and select "Create credentials". Choose
"OAuth client ID", and follow prompts to create a consent screen, etc. "OAuth client ID", and follow prompts to create a consent screen, etc.
For "Authorized redirect URIs", fill in For "Authorized redirect URIs", fill in
`https://zulipdev.com:9991/accounts/login/google/done/` . `http://zulipdev.com:9991/complete/google/` .
* You should get a client ID and a client secret. Copy them. In * You should get a client ID and a client secret. Copy them. In
`dev-secrets.conf`, set `google_auth2_client_id` to the client ID `dev-secrets.conf`, set `social_auth_google_key` to the client ID
and `google_oauth2_client_secret` to the client secret. and `social_auth_google_secret` to the client secret.
### GitHub ### GitHub

View File

@ -46,9 +46,6 @@ django-statsd-mozilla==0.4.0
# Needed for Android push notifications # Needed for Android push notifications
python-gcm==0.4 python-gcm==0.4
# Needed for Google Apps mobile auth
google-api-python-client==1.7.4
# Needed for the email mirror # Needed for the email mirror
html2text==2018.1.9 html2text==2018.1.9
httplib2==0.12.3 httplib2==0.12.3
@ -72,8 +69,6 @@ markdown-include==0.5.1
# Needed for mock objects in decorators # Needed for mock objects in decorators
mock==2.0.0 mock==2.0.0
oauth2client==4.1.3
# Needed to access rabbitmq # Needed to access rabbitmq
# See #8466 for why we're not using the latest version. # See #8466 for why we're not using the latest version.
pika==0.13.0 pika==0.13.0

View File

@ -29,7 +29,7 @@ beautifulsoup4==4.7.1
boto3==1.9.183 # via moto boto3==1.9.183 # via moto
boto==2.49.0 boto==2.49.0
botocore==1.12.183 # via boto3, moto, s3transfer botocore==1.12.183 # via boto3, moto, s3transfer
cachetools==3.1.1 # via google-auth, premailer cachetools==3.1.1 # via premailer
cchardet==2.1.4 cchardet==2.1.4
certifi==2019.3.9 # via requests certifi==2019.3.9 # via requests
cffi==1.12.3 # via argon2-cffi, cryptography cffi==1.12.3 # via argon2-cffi, cryptography
@ -62,9 +62,6 @@ ecdsa==0.13.2 # via python-jose
fakeldap==0.6.1 fakeldap==0.6.1
first==2.0.2 # via pip-tools first==2.0.2 # via pip-tools
gitlint==0.11.0 gitlint==0.11.0
google-api-python-client==1.7.4
google-auth-httplib2==0.0.3 # via google-api-python-client
google-auth==1.6.3 # via google-api-python-client, google-auth-httplib2
h2==2.6.2 # via hyper h2==2.6.2 # via hyper
hpack==3.0.0 # via h2 hpack==3.0.0 # via h2
html2text==2018.1.9 html2text==2018.1.9
@ -95,7 +92,6 @@ mock==2.0.0
moto==1.3.7 moto==1.3.7
mypy-extensions==0.4.1 mypy-extensions==0.4.1
mypy==0.670 mypy==0.670
oauth2client==4.1.3
oauthlib==3.0.1 # via requests-oauthlib, social-auth-core oauthlib==3.0.1 # via requests-oauthlib, social-auth-core
packaging==19.0 # via sphinx packaging==19.0 # via sphinx
parsel==1.5.1 # via scrapy parsel==1.5.1 # via scrapy
@ -115,8 +111,8 @@ ptyprocess==0.6.0 # via pexpect
py3dns==3.2.0 py3dns==3.2.0
pyahocorasick==1.4.0 pyahocorasick==1.4.0
pyaml==19.4.1 # via moto pyaml==19.4.1 # via moto
pyasn1-modules==0.2.5 # via google-auth, oauth2client, python-ldap, service-identity pyasn1-modules==0.2.5 # via python-ldap, service-identity
pyasn1==0.4.5 # via oauth2client, pyasn1-modules, python-ldap, rsa, service-identity pyasn1==0.4.5 # via pyasn1-modules, python-ldap, service-identity
pycodestyle==2.5.0 pycodestyle==2.5.0
pycparser==2.19 # via cffi pycparser==2.19 # via cffi
pycryptodome==3.8.2 # via python-jose pycryptodome==3.8.2 # via python-jose
@ -150,7 +146,6 @@ regex==2019.6.8
requests-oauthlib==1.0.0 # via python-twitter, social-auth-core requests-oauthlib==1.0.0 # via python-twitter, social-auth-core
requests[security]==2.22.0 # via aws-xray-sdk, docker, hypchat, matrix-client, moto, premailer, pyoembed, python-digitalocean, python-gcm, python-twitter, requests-oauthlib, responses, social-auth-core, sphinx, stripe, twilio requests[security]==2.22.0 # via aws-xray-sdk, docker, hypchat, matrix-client, moto, premailer, pyoembed, python-digitalocean, python-gcm, python-twitter, requests-oauthlib, responses, social-auth-core, sphinx, stripe, twilio
responses==0.10.6 # via moto responses==0.10.6 # via moto
rsa==4.0 # via google-auth, oauth2client
s3transfer==0.2.1 # via boto3 s3transfer==0.2.1 # via boto3
scrapy==1.6.0 scrapy==1.6.0
service-identity==18.1.0 # via scrapy service-identity==18.1.0 # via scrapy
@ -178,7 +173,6 @@ twilio==6.26.2
twisted==19.2.1 twisted==19.2.1
typed-ast==1.3.5 # via mypy typed-ast==1.3.5 # via mypy
typing==3.6.6 typing==3.6.6
uritemplate==3.0.0 # via google-api-python-client
urllib3==1.25.3 # via botocore, requests, transifex-client urllib3==1.25.3 # via botocore, requests, transifex-client
virtualenv-clone==0.5.3 virtualenv-clone==0.5.3
w3lib==1.20.0 # via parsel, scrapy w3lib==1.20.0 # via parsel, scrapy

View File

@ -22,7 +22,7 @@ babel==2.7.0 # via django-phonenumber-field
backcall==0.1.0 # via ipython backcall==0.1.0 # via ipython
beautifulsoup4==4.7.1 beautifulsoup4==4.7.1
boto==2.49.0 boto==2.49.0
cachetools==3.1.1 # via google-auth, premailer cachetools==3.1.1 # via premailer
cchardet==2.1.4 cchardet==2.1.4
certifi==2019.3.9 # via requests certifi==2019.3.9 # via requests
cffi==1.12.3 # via argon2-cffi, cryptography cffi==1.12.3 # via argon2-cffi, cryptography
@ -45,9 +45,6 @@ django-statsd-mozilla==0.4.0
django-two-factor-auth==1.8.0 django-two-factor-auth==1.8.0
django-webpack-loader==0.6.0 django-webpack-loader==0.6.0
django==1.11.22 django==1.11.22
google-api-python-client==1.7.4
google-auth-httplib2==0.0.3 # via google-api-python-client
google-auth==1.6.3 # via google-api-python-client, google-auth-httplib2
h2==2.6.2 # via hyper h2==2.6.2 # via hyper
hpack==3.0.0 # via h2 hpack==3.0.0 # via h2
html2text==2018.1.9 html2text==2018.1.9
@ -69,7 +66,6 @@ markupsafe==1.1.1 # via jinja2
matrix-client==0.3.2 matrix-client==0.3.2
mock==2.0.0 mock==2.0.0
mypy_extensions==0.4.1 mypy_extensions==0.4.1
oauth2client==4.1.3
oauthlib==3.0.1 # via requests-oauthlib, social-auth-core oauthlib==3.0.1 # via requests-oauthlib, social-auth-core
parso==0.5.0 # via jedi parso==0.5.0 # via jedi
pbr==5.3.1 # via mock pbr==5.3.1 # via mock
@ -85,8 +81,8 @@ psycopg2==2.8.2
ptyprocess==0.6.0 # via pexpect ptyprocess==0.6.0 # via pexpect
py3dns==3.2.0 py3dns==3.2.0
pyahocorasick==1.4.0 pyahocorasick==1.4.0
pyasn1-modules==0.2.5 # via google-auth, oauth2client, python-ldap pyasn1-modules==0.2.5 # via python-ldap
pyasn1==0.4.5 # via oauth2client, pyasn1-modules, python-ldap, rsa pyasn1==0.4.5 # via pyasn1-modules, python-ldap
pycparser==2.19 # via cffi pycparser==2.19 # via cffi
pygments==2.3.1 pygments==2.3.1
pyjwt==1.7.1 pyjwt==1.7.1
@ -107,7 +103,6 @@ redis==2.10.6
regex==2019.6.8 regex==2019.6.8
requests-oauthlib==1.0.0 # via python-twitter, social-auth-core requests-oauthlib==1.0.0 # via python-twitter, social-auth-core
requests[security]==2.22.0 # via hypchat, matrix-client, premailer, pyoembed, python-gcm, python-twitter, requests-oauthlib, social-auth-core, stripe, twilio requests[security]==2.22.0 # via hypchat, matrix-client, premailer, pyoembed, python-gcm, python-twitter, requests-oauthlib, social-auth-core, stripe, twilio
rsa==4.0 # via google-auth, oauth2client
simplegeneric==0.8.1 # via ipython simplegeneric==0.8.1 # via ipython
six==1.12.0 six==1.12.0
social-auth-app-django==3.1.0 social-auth-app-django==3.1.0
@ -122,7 +117,6 @@ tornado==4.5.3
traitlets==4.3.2 # via ipython traitlets==4.3.2 # via ipython
twilio==6.26.2 twilio==6.26.2
typing==3.6.6 typing==3.6.6
uritemplate==3.0.0 # via google-api-python-client
urllib3==1.25.3 # via requests urllib3==1.25.3 # via requests
uwsgi==2.0.17.1 uwsgi==2.0.17.1
virtualenv-clone==0.5.3 virtualenv-clone==0.5.3

View File

@ -118,8 +118,7 @@ $(function () {
https://stackoverflow.com/questions/5283395/url-hash-is-persisting-between-redirects */ https://stackoverflow.com/questions/5283395/url-hash-is-persisting-between-redirects */
var email_formaction = $("#login_form").attr('action'); var email_formaction = $("#login_form").attr('action');
$("#login_form").attr('action', email_formaction + '/' + window.location.hash); $("#login_form").attr('action', email_formaction + '/' + window.location.hash);
$("#google_login_form input[name='next']").attr('value', '/' + window.location.hash); $(".social_login_form input[name='next']").attr('value', '/' + window.location.hash);
$("#social_login_form input[name='next']").attr('value', '/' + window.location.hash);
var sso_address = $("#sso-login").attr('href'); var sso_address = $("#sso-login").attr('href');
$("#sso-login").attr('href', sso_address + window.location.hash); $("#sso-login").attr('href', sso_address + window.location.hash);

View File

@ -621,8 +621,9 @@ button.login-social-button:active {
box-shadow: 0px 1px 1px hsla(0, 0%, 0%, 0.3); box-shadow: 0px 1px 1px hsla(0, 0%, 0%, 0.3);
} }
button.login-google-button { .google-wrapper button.login-social-button {
background-image: url('/static/images/landing-page/logos/googl_e-icon.png'); background-image: url('/static/images/landing-page/logos/googl_e-icon.png');
width: 100%;
} }
.github-wrapper::before { .github-wrapper::before {

View File

@ -73,18 +73,6 @@ page can be easily identified in it's respective JavaScript file -->
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if google_auth_enabled %}
<div class="login-social">
<form class="form-inline" action="{{ url('zerver.views.auth.start_google_oauth2') }}" method="get">
<input type='hidden' name='is_signup' value='1' />
<input type='hidden' name='multiuse_object_key' value='{{ multiuse_object_key }}' />
<button class="login-social-button login-google-button full-width">
{{ _('Sign up with %(identity_provider)s', identity_provider="Google") }}
</button>
</form>
</div>
{% endif %}
{% for backend in social_backends %} {% for backend in social_backends %}
<div class="login-social"> <div class="login-social">
<form class="form-inline {{ backend.name }}-wrapper" action="{{ backend.signup_url }}" method="get"> <form class="form-inline {{ backend.name }}-wrapper" action="{{ backend.signup_url }}" method="get">

View File

@ -50,9 +50,9 @@
{% if google_error %} {% if google_error %}
{% if development_environment %} {% if development_environment %}
{{ render_markdown_path('zerver/google-error.md', {"root_domain_uri": root_domain_uri, "settings_path": secrets_path, "secrets_path": secrets_path, "client_id_key_name": "google_oauth2_client_id"}) }} {{ render_markdown_path('zerver/google-error.md', {"root_domain_uri": root_domain_uri, "settings_path": secrets_path, "secrets_path": secrets_path, "client_id_key_name": "social_auth_google_key"}) }}
{% else %} {% else %}
{{ render_markdown_path('zerver/google-error.md', {"root_domain_uri": root_domain_uri, "settings_path": settings_path, "secrets_path": secrets_path, "client_id_key_name": "GOOGLE_OAUTH2_CLIENT_ID"}) }} {{ render_markdown_path('zerver/google-error.md', {"root_domain_uri": root_domain_uri, "settings_path": settings_path, "secrets_path": secrets_path, "client_id_key_name": "SOCIAL_AUTH_GOOGLE_KEY"}) }}
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@ -1,13 +1,13 @@
You are using the **Google auth backend**, but it is not properly You are using the **Google auth backend**, but it is not properly
configured. Please check the following: configured. Please check the following:
* You have created a Google Oauth2 client and enabled the Google+ API. * You have created a Google Oauth2 client and enabled the Identity Toolkit API.
You can create OAuth2 apps at [the Google developer console](https://console.developers.google.com). You can create OAuth2 apps at [the Google developer console](https://console.developers.google.com).
* You have configured your OAuth2 client to allow redirects to your * You have configured your OAuth2 client to allow redirects to your
server's Google auth URL: `{{ root_domain_uri }}/accounts/login/google/done/`. server's Google auth URL: `{{ root_domain_uri }}/accounts/login/google/done/`.
* You have set `{{ client_id_key_name }}` in `{{ settings_path }}` and * You have set `{{ client_id_key_name }}` in `{{ settings_path }}` and
`google_oauth2_client_secret` in `{{ secrets_path }}`. `social_auth_google_secret` in `{{ secrets_path }}`.
* Navigate back to the login page and attempt the Google auth flow again. * Navigate back to the login page and attempt the Google auth flow again.

View File

@ -140,20 +140,9 @@
{% endif %} <!-- if password_auth_enabled --> {% endif %} <!-- if password_auth_enabled -->
{% if google_auth_enabled %}
<div class="login-social">
<form id='google_login_form' class="form-inline" action="{{ url('zerver.views.auth.start_google_oauth2') }}" method="get">
<input type="hidden" name="next" value="{{ next }}">
<button class="login-social-button login-google-button full-width">
{{ _('Log in with %(identity_provider)s', identity_provider="Google") }}
</button>
</form>
</div>
{% endif %}
{% for backend in social_backends %} {% for backend in social_backends %}
<div class="login-social"> <div class="login-social">
<form id='social_login_form' class="form-inline {{ backend.name }}-wrapper" action="{{ backend.login_url }}" method="get"> <form class="social_login_form form-inline {{ backend.name }}-wrapper" action="{{ backend.login_url }}" method="get">
<input type="hidden" name="next" value="{{ next }}"> <input type="hidden" name="next" value="{{ next }}">
<button class="login-social-button"> <button class="login-social-button">
{{ _('Log in with %(identity_provider)s', identity_provider=backend.display_name) }} {{ _('Log in with %(identity_provider)s', identity_provider=backend.display_name) }}

View File

@ -26,4 +26,4 @@ LATEST_RELEASE_ANNOUNCEMENT = "https://blog.zulip.org/2019/03/01/zulip-2-0-relea
# historical commits sharing the same major version, in which case a # historical commits sharing the same major version, in which case a
# minor version bump suffices. # minor version bump suffices.
PROVISION_VERSION = '40.0' PROVISION_VERSION = '41.0'

View File

@ -7,7 +7,6 @@ from django_auth_ldap.backend import LDAPBackend, _LDAPUser
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
from typing import Any, Callable, Dict, List, Optional, Set, Tuple from typing import Any, Callable, Dict, List, Optional, Set, Tuple
from oauth2client.crypt import AppIdentityError
from django.core import signing from django.core import signing
from django.urls import reverse from django.urls import reverse
@ -51,9 +50,9 @@ from zerver.signals import JUST_CREATED_THRESHOLD
from confirmation.models import Confirmation, create_confirmation_link from confirmation.models import Confirmation, create_confirmation_link
from zproject.backends import ZulipDummyBackend, EmailAuthBackend, \ from zproject.backends import ZulipDummyBackend, EmailAuthBackend, \
GoogleMobileOauth2Backend, ZulipRemoteUserBackend, ZulipLDAPAuthBackend, \ GoogleAuthBackend, ZulipRemoteUserBackend, ZulipLDAPAuthBackend, \
ZulipLDAPUserPopulator, DevAuthBackend, GitHubAuthBackend, ZulipAuthMixin, \ ZulipLDAPUserPopulator, DevAuthBackend, GitHubAuthBackend, ZulipAuthMixin, \
dev_auth_enabled, password_auth_enabled, github_auth_enabled, \ dev_auth_enabled, password_auth_enabled, github_auth_enabled, google_auth_enabled, \
require_email_format_usernames, AUTH_BACKEND_NAME_MAP, \ require_email_format_usernames, AUTH_BACKEND_NAME_MAP, \
ZulipLDAPConfigurationError, ZulipLDAPExceptionOutsideDomain, \ ZulipLDAPConfigurationError, ZulipLDAPExceptionOutsideDomain, \
ZulipLDAPException, query_ldap, sync_user_from_ldap, SocialAuthMixin ZulipLDAPException, query_ldap, sync_user_from_ldap, SocialAuthMixin
@ -251,7 +250,7 @@ class AuthBackendTest(ZulipTestCase):
result = self.client_get('/register/') result = self.client_get('/register/')
self.assert_in_success_response(["No authentication backends are enabled"], result) self.assert_in_success_response(["No authentication backends are enabled"], result)
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.GoogleMobileOauth2Backend',)) @override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.GoogleAuthBackend',))
def test_any_backend_enabled(self) -> None: def test_any_backend_enabled(self) -> None:
# testing to avoid false error messages. # testing to avoid false error messages.
@ -261,46 +260,6 @@ class AuthBackendTest(ZulipTestCase):
result = self.client_get('/register/') result = self.client_get('/register/')
self.assert_not_in_success_response(["No authentication backends are enabled"], result) self.assert_not_in_success_response(["No authentication backends are enabled"], result)
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.GoogleMobileOauth2Backend',))
def test_google_backend(self) -> None:
user_profile = self.example_user('hamlet')
email = user_profile.email
backend = GoogleMobileOauth2Backend()
payload = dict(email_verified=True,
email=email)
with mock.patch('apiclient.sample_tools.client.verify_id_token', return_value=payload):
self.verify_backend(backend,
good_kwargs=dict(google_oauth2_token="",
realm=get_realm("zulip")),
bad_kwargs=dict(google_oauth2_token="",
realm=get_realm("zephyr")))
# Verify valid_attestation parameter is set correctly
unverified_payload = dict(email_verified=False)
with mock.patch('apiclient.sample_tools.client.verify_id_token',
return_value=unverified_payload):
ret = dict() # type: Dict[str, str]
result = backend.authenticate(
google_oauth2_token="", realm=get_realm("zulip"), return_data=ret)
self.assertIsNone(result)
self.assertFalse(ret["valid_attestation"])
nonexistent_user_payload = dict(email_verified=True, email="invalid@zulip.com")
with mock.patch('apiclient.sample_tools.client.verify_id_token',
return_value=nonexistent_user_payload):
ret = dict()
result = backend.authenticate(
google_oauth2_token="", realm=get_realm("zulip"), return_data=ret)
self.assertIsNone(result)
self.assertTrue(ret["valid_attestation"])
with mock.patch('apiclient.sample_tools.client.verify_id_token',
side_effect=AppIdentityError):
ret = dict()
result = backend.authenticate(
google_oauth2_token="", realm=get_realm("zulip"), return_data=ret)
self.assertIsNone(result)
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',)) @override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
def test_ldap_backend(self) -> None: def test_ldap_backend(self) -> None:
user_profile = self.example_user('hamlet') user_profile = self.example_user('hamlet')
@ -373,7 +332,8 @@ class AuthBackendTest(ZulipTestCase):
bad_kwargs=dict(remote_user=username, bad_kwargs=dict(remote_user=username,
realm=get_realm('zephyr'))) realm=get_realm('zephyr')))
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.GitHubAuthBackend',)) @override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.GitHubAuthBackend',
'zproject.backends.GoogleAuthBackend'))
def test_social_auth_backends(self) -> None: def test_social_auth_backends(self) -> None:
user = self.example_user('hamlet') user = self.example_user('hamlet')
token_data_dict = { token_data_dict = {
@ -389,7 +349,27 @@ class AuthBackendTest(ZulipTestCase):
dict(email="ignored@example.com", dict(email="ignored@example.com",
verified=False), verified=False),
] ]
google_email_data = dict(email=user.email,
name=user.full_name,
email_verified=True)
backends_to_test = { backends_to_test = {
'google': {
'urls': [
{
'url': "https://accounts.google.com/o/oauth2/token",
'method': httpretty.POST,
'status': 200,
'body': json.dumps(token_data_dict),
},
{
'url': "https://www.googleapis.com/oauth2/v3/userinfo",
'method': httpretty.GET,
'status': 200,
'body': json.dumps(google_email_data),
},
],
'backend': GoogleAuthBackend,
},
'github': { 'github': {
'urls': [ 'urls': [
{ {
@ -435,6 +415,9 @@ class AuthBackendTest(ZulipTestCase):
result = orig_authenticate(backend, **kwargs) result = orig_authenticate(backend, **kwargs)
return result return result
def patched_get_verified_emails(*args: Any, **kwargs: Any) -> Any:
return google_email_data['email']
for backend_name in backends_to_test: for backend_name in backends_to_test:
httpretty.enable(allow_net_connect=False) httpretty.enable(allow_net_connect=False)
urls = backends_to_test[backend_name]['urls'] # type: List[Dict[str, Any]] urls = backends_to_test[backend_name]['urls'] # type: List[Dict[str, Any]]
@ -447,8 +430,13 @@ class AuthBackendTest(ZulipTestCase):
backend_class = backends_to_test[backend_name]['backend'] backend_class = backends_to_test[backend_name]['backend']
backend = backend_class() backend = backend_class()
backend.strategy = DjangoStrategy(storage=BaseDjangoStorage()) backend.strategy = DjangoStrategy(storage=BaseDjangoStorage())
orig_authenticate = backend_class.authenticate orig_authenticate = backend_class.authenticate
backend.authenticate = patched_authenticate backend.authenticate = patched_authenticate
orig_get_verified_emails = backend_class.get_verified_emails
if backend_name == "google":
backend.get_verified_emails = patched_get_verified_emails
good_kwargs = dict(backend=backend, strategy=backend.strategy, good_kwargs = dict(backend=backend, strategy=backend.strategy,
storage=backend.strategy.storage, storage=backend.strategy.storage,
response=token_data_dict, response=token_data_dict,
@ -464,21 +452,10 @@ class AuthBackendTest(ZulipTestCase):
good_kwargs=good_kwargs, good_kwargs=good_kwargs,
bad_kwargs=bad_kwargs) bad_kwargs=bad_kwargs)
backend.authenticate = orig_authenticate backend.authenticate = orig_authenticate
backend.get_verified_emails = orig_get_verified_emails
httpretty.disable() httpretty.disable()
httpretty.reset() httpretty.reset()
class ResponseMock:
def __init__(self, status_code: int, data: Any) -> None:
self.status_code = status_code
self.data = data
def json(self) -> str:
return self.data
@property
def text(self) -> str:
return "Response text"
class SocialAuthBase(ZulipTestCase): class SocialAuthBase(ZulipTestCase):
"""This is a base class for testing social-auth backends. These """This is a base class for testing social-auth backends. These
methods are often overriden by subclasses: methods are often overriden by subclasses:
@ -509,6 +486,11 @@ class SocialAuthBase(ZulipTestCase):
from social_core.backends.utils import load_backends from social_core.backends.utils import load_backends
load_backends(settings.AUTHENTICATION_BACKENDS, force_load=True) load_backends(settings.AUTHENTICATION_BACKENDS, force_load=True)
def register_extra_endpoints(self,
account_data_dict: Dict[str, str],
**extra_data: Any) -> None:
pass
def social_auth_test(self, account_data_dict: Dict[str, str], def social_auth_test(self, account_data_dict: Dict[str, str],
*, subdomain: Optional[str]=None, *, subdomain: Optional[str]=None,
mobile_flow_otp: Optional[str]=None, mobile_flow_otp: Optional[str]=None,
@ -1170,123 +1152,35 @@ class GitHubAuthBackendTest(SocialAuthBase):
mock_warning.assert_called_once_with("Social auth (GitHub) failed because user has no verified" mock_warning.assert_called_once_with("Social auth (GitHub) failed because user has no verified"
" emails associated with the account") " emails associated with the account")
class GoogleOAuthTest(ZulipTestCase): class GoogleAuthBackendTest(SocialAuthBase):
def google_oauth2_test(self, token_response: ResponseMock, account_response: ResponseMock, __unittest_skip__ = False
*, subdomain: Optional[str]=None,
mobile_flow_otp: Optional[str]=None,
is_signup: Optional[str]=None,
next: str='',
multiuse_object_key: str='') -> HttpResponse:
url = "/accounts/login/google/"
params = {}
headers = {}
if subdomain is not None:
headers['HTTP_HOST'] = subdomain + ".testserver"
if mobile_flow_otp is not None:
params['mobile_flow_otp'] = mobile_flow_otp
headers['HTTP_USER_AGENT'] = "ZulipAndroid"
if is_signup is not None:
params['is_signup'] = is_signup
params['next'] = next
params['multiuse_object_key'] = multiuse_object_key
if len(params) > 0:
url += "?%s" % (urllib.parse.urlencode(params),)
result = self.client_get(url, **headers) BACKEND_CLASS = GoogleAuthBackend
if result.status_code != 302 or '/accounts/login/google/send/' not in result.url: CLIENT_KEY_SETTING = "SOCIAL_AUTH_GOOGLE_KEY"
return result LOGIN_URL = "/accounts/login/social/google"
SIGNUP_URL = "/accounts/register/social/google"
AUTHORIZATION_URL = "https://accounts.google.com/o/oauth2/auth"
ACCESS_TOKEN_URL = "https://accounts.google.com/o/oauth2/token"
USER_INFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo"
AUTH_FINISH_URL = "/complete/google/"
CONFIG_ERROR_URL = "/config-error/google"
# Now do the /google/send/ request def get_account_data_dict(self, email: str, name: str) -> Dict[str, Any]:
result = self.client_get(result.url, **headers) return dict(email=email, name=name, email_verified=True)
self.assertEqual(result.status_code, 302)
if 'google' not in result.url:
return result
self.client.cookies = result.cookies def test_social_auth_email_not_verified(self) -> None:
# Now extract the CSRF token from the redirect URL account_data_dict = dict(email=self.email, name=self.name)
parsed_url = urllib.parse.urlparse(result.url) with mock.patch('logging.warning') as mock_warning:
csrf_state = urllib.parse.parse_qs(parsed_url.query)['state'] result = self.social_auth_test(account_data_dict,
with mock.patch("requests.post", return_value=token_response), (
mock.patch("requests.get", return_value=account_response)):
result = self.client_get("/accounts/login/google/done/",
dict(state=csrf_state), **headers)
return result
class GoogleSubdomainLoginTest(GoogleOAuthTest):
def test_google_oauth2_start(self) -> None:
result = self.client_get('/accounts/login/google/', subdomain="zulip")
self.assertEqual(result.status_code, 302)
parsed_url = urllib.parse.urlparse(result.url)
subdomain = urllib.parse.parse_qs(parsed_url.query)['subdomain']
self.assertEqual(subdomain, ['zulip'])
def test_google_oauth2_success(self) -> None:
token_response = ResponseMock(200, {'access_token': "unique_token"})
account_data = dict(name="Full Name",
email_verified=True,
email=self.example_email("hamlet"))
account_response = ResponseMock(200, account_data)
result = self.google_oauth2_test(token_response, account_response,
subdomain='zulip', next='/user_uploads/image')
data = load_subdomain_token(result)
self.assertEqual(data['email'], self.example_email("hamlet"))
self.assertEqual(data['name'], 'Full Name')
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_user_cannot_log_without_verified_email(self) -> None:
token_response = ResponseMock(200, {'access_token': "unique_token"})
account_data = dict(name="Full Name",
email_verified=False,
email=self.example_email("hamlet"))
account_response = ResponseMock(200, account_data)
result = self.google_oauth2_test(token_response, account_response,
subdomain='zulip') subdomain='zulip')
self.assertEqual(result.status_code, 400)
def test_google_oauth2_mobile_success(self) -> None:
self.user_profile = self.example_user('hamlet')
self.user_profile.date_joined = timezone_now() - datetime.timedelta(seconds=JUST_CREATED_THRESHOLD + 1)
self.user_profile.save()
mobile_flow_otp = '1234abcd' * 8
token_response = ResponseMock(200, {'access_token': "unique_token"})
account_data = dict(name="Full Name",
email_verified=True,
email=self.user_profile.email)
account_response = ResponseMock(200, account_data)
self.assertEqual(len(mail.outbox), 0)
with self.settings(SEND_LOGIN_EMAILS=True):
# Verify that the right thing happens with an invalid-format OTP
result = self.google_oauth2_test(token_response, account_response, subdomain='zulip',
mobile_flow_otp="1234")
self.assert_json_error(result, "Invalid OTP")
result = self.google_oauth2_test(token_response, account_response, subdomain='zulip',
mobile_flow_otp="invalido" * 8)
self.assert_json_error(result, "Invalid OTP")
# Now do it correctly
result = self.google_oauth2_test(token_response, account_response, subdomain='zulip',
mobile_flow_otp=mobile_flow_otp)
self.assertEqual(result.status_code, 302) self.assertEqual(result.status_code, 302)
redirect_url = result['Location'] self.assertEqual(result.url, "/login/")
parsed_url = urllib.parse.urlparse(redirect_url) mock_warning.assert_called_once_with("Social auth (Google) failed "
query_params = urllib.parse.parse_qs(parsed_url.query) "because user has no verified emails")
self.assertEqual(parsed_url.scheme, 'zulip')
self.assertEqual(query_params["realm"], ['http://zulip.testserver']) def test_google_auth_enabled(self) -> None:
self.assertEqual(query_params["email"], [self.user_profile.email]) with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.GoogleAuthBackend',)):
encrypted_api_key = query_params["otp_encrypted_api_key"][0] self.assertTrue(google_auth_enabled())
hamlet_api_keys = get_all_api_keys(self.user_profile)
self.assertIn(otp_decrypt_api_key(encrypted_api_key, mobile_flow_otp), hamlet_api_keys)
self.assertEqual(len(mail.outbox), 1)
self.assertIn('Zulip on Android', mail.outbox[0].body)
def get_log_into_subdomain(self, data: Dict[str, Any], *, key: Optional[str]=None, subdomain: str='zulip') -> HttpResponse: def get_log_into_subdomain(self, data: Dict[str, Any], *, key: Optional[str]=None, subdomain: str='zulip') -> HttpResponse:
token = signing.dumps(data, salt=_subdomain_token_salt, key=key) token = signing.dumps(data, salt=_subdomain_token_salt, key=key)
@ -1474,33 +1368,6 @@ class GoogleSubdomainLoginTest(GoogleOAuthTest):
result = self.get_log_into_subdomain(data) result = self.get_log_into_subdomain(data)
self.assert_in_success_response(["You need an invitation to join this organization."], result) self.assert_in_success_response(["You need an invitation to join this organization."], result)
def test_user_cannot_log_into_nonexisting_realm(self) -> None:
token_response = ResponseMock(200, {'access_token': "unique_token"})
account_data = dict(name="Full Name",
email_verified=True,
email=self.example_email("hamlet"))
account_response = ResponseMock(200, account_data)
result = self.google_oauth2_test(token_response, account_response,
subdomain='nonexistent')
self.assert_in_response("There is no Zulip organization hosted at this subdomain.",
result)
self.assertEqual(result.status_code, 404)
def test_user_cannot_log_into_wrong_subdomain(self) -> None:
token_response = ResponseMock(200, {'access_token': "unique_token"})
account_data = dict(name="Full Name",
email_verified=True,
email=self.example_email("hamlet"))
account_response = ResponseMock(200, account_data)
result = self.google_oauth2_test(token_response, account_response,
subdomain='zephyr')
self.assertEqual(result.status_code, 302)
self.assertTrue(result.url.startswith("http://zephyr.testserver/accounts/login/subdomain/"))
result = self.client_get(result.url.replace('http://zephyr.testserver', ''),
subdomain="zephyr")
self.assert_in_success_response(['Your email address, hamlet@zulip.com, is not in one of the domains ',
'that are allowed to register for accounts in this organization.'], result)
def test_user_cannot_log_into_wrong_subdomain_with_cookie(self) -> None: def test_user_cannot_log_into_wrong_subdomain_with_cookie(self) -> None:
data = {'name': 'Full Name', data = {'name': 'Full Name',
'email': self.example_email("hamlet"), 'email': self.example_email("hamlet"),
@ -1510,216 +1377,6 @@ class GoogleSubdomainLoginTest(GoogleOAuthTest):
mock_warning.assert_called_with("Login attempt on invalid subdomain") mock_warning.assert_called_with("Login attempt on invalid subdomain")
self.assertEqual(result.status_code, 400) self.assertEqual(result.status_code, 400)
def test_google_oauth2_registration(self) -> None:
"""If the user doesn't exist yet, Google auth can be used to register an account"""
email = "newuser@zulip.com"
realm = get_realm("zulip")
token_response = ResponseMock(200, {'access_token': "unique_token"})
account_data = dict(name="Full Name",
email_verified=True,
email=email)
account_response = ResponseMock(200, account_data)
result = self.google_oauth2_test(token_response, account_response, subdomain='zulip',
is_signup='1')
data = load_subdomain_token(result)
name = 'Full Name'
self.assertEqual(data['email'], email)
self.assertEqual(data['name'], name)
self.assertEqual(data['subdomain'], 'zulip')
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/'))
result = self.client_get(result.url)
self.assertEqual(result.status_code, 302)
confirmation = Confirmation.objects.all().first()
confirmation_key = confirmation.confirmation_key
self.assertIn('do_confirm/' + confirmation_key, result.url)
result = self.client_get(result.url)
self.assert_in_response('action="/accounts/register/"', result)
data = {"from_confirmation": "1",
"full_name": name,
"key": confirmation_key}
result = self.client_post('/accounts/register/', data)
self.assert_in_response("We just need you to do one last thing", result)
# Verify that the user is asked for name but not password
self.assert_not_in_success_response(['id_password'], result)
self.assert_in_success_response(['id_full_name'], result)
# Click confirm registration button.
result = self.client_post(
'/accounts/register/',
{'full_name': name,
'key': confirmation_key,
'terms': True})
self.assertEqual(result.status_code, 302)
user_profile = get_user(email, realm)
self.assert_logged_in_user_id(user_profile.id)
def test_google_oauth2_registration_using_multiuse_invite(self) -> None:
"""If the user doesn't exist yet, Google auth can be used to register an account"""
email = "newuser@zulip.com"
realm = get_realm("zulip")
realm.invite_required = True
realm.save()
stream_names = ["new_stream_1", "new_stream_2"]
streams = []
for stream_name in set(stream_names):
stream = ensure_stream(realm, stream_name)
streams.append(stream)
referrer = self.example_user("hamlet")
multiuse_obj = MultiuseInvite.objects.create(realm=realm, referred_by=referrer)
multiuse_obj.streams.set(streams)
link = create_confirmation_link(multiuse_obj, realm.host, Confirmation.MULTIUSE_INVITE)
multiuse_confirmation = Confirmation.objects.all().last()
multiuse_object_key = multiuse_confirmation.confirmation_key
input_element = "name=\'multiuse_object_key\' value=\'{}\' /".format(multiuse_object_key)
response = self.client_get(link)
self.assert_in_success_response([input_element], response)
# First, try to signup for closed realm without using an invitation
token_response = ResponseMock(200, {'access_token': "unique_token"})
account_data = dict(name="Full Name",
email_verified=True,
email=email)
account_response = ResponseMock(200, account_data)
result = self.google_oauth2_test(token_response, account_response, subdomain='zulip',
is_signup='1', multiuse_object_key="")
result = self.client_get(result.url)
# Verify that we're unable to signup, since this is a closed realm
self.assertEqual(result.status_code, 200)
self.assert_in_success_response(["Sign up"], result)
result = self.google_oauth2_test(token_response, account_response, subdomain='zulip',
is_signup='1', multiuse_object_key=multiuse_object_key)
data = load_subdomain_token(result)
name = 'Full Name'
self.assertEqual(data['name'], name)
self.assertEqual(data['subdomain'], 'zulip')
self.assertEqual(data['multiuse_object_key'], multiuse_object_key)
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/'))
result = self.client_get(result.url)
self.assertEqual(result.status_code, 302)
confirmation = Confirmation.objects.all().last()
confirmation_key = confirmation.confirmation_key
self.assertIn('do_confirm/' + confirmation_key, result.url)
result = self.client_get(result.url)
self.assert_in_response('action="/accounts/register/"', result)
data = {"from_confirmation": "1",
"full_name": name,
"key": confirmation_key}
result = self.client_post('/accounts/register/', data)
self.assert_in_response("We just need you to do one last thing", result)
# Verify that the user is asked for name but not password
self.assert_not_in_success_response(['id_password'], result)
self.assert_in_success_response(['id_full_name'], result)
# Click confirm registration button.
result = self.client_post(
'/accounts/register/',
{'full_name': name,
'key': confirmation_key,
'terms': True})
self.assertEqual(result.status_code, 302)
user_profile = get_user(email, realm)
self.assert_logged_in_user_id(user_profile.id)
self.assertEqual(sorted(self.get_streams(email, realm)), stream_names)
class GoogleLoginTest(GoogleOAuthTest):
@override_settings(ROOT_DOMAIN_LANDING_PAGE=True)
def test_google_oauth2_subdomains_homepage(self) -> None:
token_response = ResponseMock(200, {'access_token': "unique_token"})
account_data = dict(name=dict(formatted="Full Name"),
emails=[dict(type="account",
value=self.example_email("hamlet"))])
account_response = ResponseMock(200, account_data)
result = self.google_oauth2_test(token_response, account_response, subdomain="")
self.assertEqual(result.status_code, 302)
self.assertIn('subdomain=1', result.url)
def test_google_oauth2_400_token_response(self) -> None:
token_response = ResponseMock(400, {})
with mock.patch("logging.warning") as m:
result = self.google_oauth2_test(token_response, ResponseMock(500, {}))
self.assertEqual(result.status_code, 400)
self.assertEqual(m.call_args_list[0][0][0],
"User error converting Google oauth2 login to token: Response text")
def test_google_oauth2_500_token_response(self) -> None:
token_response = ResponseMock(500, {})
with mock.patch("logging.error") as m:
result = self.google_oauth2_test(token_response, ResponseMock(500, {}))
self.assertEqual(result.status_code, 400)
self.assertEqual(m.call_args_list[0][0][0],
"Could not convert google oauth2 code to access_token: Response text")
def test_google_oauth2_400_account_response(self) -> None:
token_response = ResponseMock(200, {'access_token': "unique_token"})
account_response = ResponseMock(400, {})
with mock.patch("logging.warning") as m:
result = self.google_oauth2_test(token_response, account_response)
self.assertEqual(result.status_code, 400)
self.assertEqual(m.call_args_list[0][0][0],
"Google login failed making info API call: Response text")
def test_google_oauth2_500_account_response(self) -> None:
token_response = ResponseMock(200, {'access_token': "unique_token"})
account_response = ResponseMock(500, {})
with mock.patch("logging.error") as m:
result = self.google_oauth2_test(token_response, account_response)
self.assertEqual(result.status_code, 400)
self.assertEqual(m.call_args_list[0][0][0],
"Google login failed making API call: Response text")
def test_google_oauth2_error_access_denied(self) -> None:
result = self.client_get("/accounts/login/google/done/?error=access_denied")
self.assertEqual(result.status_code, 302)
path = urllib.parse.urlparse(result.url).path
self.assertEqual(path, "/")
def test_google_oauth2_error_other(self) -> None:
with mock.patch("logging.warning") as m:
result = self.client_get("/accounts/login/google/done/?error=some_other_error")
self.assertEqual(result.status_code, 400)
self.assertEqual(m.call_args_list[0][0][0],
"Error from google oauth2 login: some_other_error")
def test_google_oauth2_missing_csrf(self) -> None:
with mock.patch("logging.warning") as m:
result = self.client_get("/accounts/login/google/done/")
self.assertEqual(result.status_code, 400)
self.assertEqual(m.call_args_list[0][0][0],
'Missing Google oauth2 CSRF state')
def test_google_oauth2_csrf_malformed(self) -> None:
with mock.patch("logging.warning") as m:
result = self.client_get("/accounts/login/google/done/?state=badstate")
self.assertEqual(result.status_code, 400)
self.assertEqual(m.call_args_list[0][0][0],
'Missing Google oauth2 CSRF state')
def test_google_oauth2_csrf_badstate(self) -> None:
with mock.patch("logging.warning") as m:
result = self.client_get("/accounts/login/google/done/?state=badstate:otherbadstate:more::::")
self.assertEqual(result.status_code, 400)
self.assertEqual(m.call_args_list[0][0][0],
'Google oauth2 CSRF error')
class JSONFetchAPIKeyTest(ZulipTestCase): class JSONFetchAPIKeyTest(ZulipTestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.user_profile = self.example_user('hamlet') self.user_profile = self.example_user('hamlet')
@ -1769,49 +1426,6 @@ class FetchAPIKeyTest(ZulipTestCase):
password="wrong")) password="wrong"))
self.assert_json_error(result, "Your username or password is incorrect.", 403) self.assert_json_error(result, "Your username or password is incorrect.", 403)
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.GoogleMobileOauth2Backend',),
SEND_LOGIN_EMAILS=True)
def test_google_oauth2_token_success(self) -> None:
self.assertEqual(len(mail.outbox), 0)
self.user_profile.date_joined = timezone_now() - datetime.timedelta(seconds=JUST_CREATED_THRESHOLD + 1)
self.user_profile.save()
with mock.patch(
'apiclient.sample_tools.client.verify_id_token',
return_value={
"email_verified": True,
"email": self.example_email("hamlet"),
}):
result = self.client_post("/api/v1/fetch_api_key",
dict(username="google-oauth2-token",
password="token"))
self.assert_json_success(result)
self.assertEqual(len(mail.outbox), 1)
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.GoogleMobileOauth2Backend',))
def test_google_oauth2_token_failure(self) -> None:
payload = dict(email_verified=False)
with mock.patch('apiclient.sample_tools.client.verify_id_token', return_value=payload):
result = self.client_post("/api/v1/fetch_api_key",
dict(username="google-oauth2-token",
password="token"))
self.assert_json_error(result, "Your username or password is incorrect.", 403)
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.GoogleMobileOauth2Backend',))
def test_google_oauth2_token_unregistered(self) -> None:
with mock.patch(
'apiclient.sample_tools.client.verify_id_token',
return_value={
"email_verified": True,
"email": "nobody@zulip.com",
}):
result = self.client_post("/api/v1/fetch_api_key",
dict(username="google-oauth2-token",
password="token"))
self.assert_json_error(
result,
"This user is not registered; do so from a browser.",
403)
def test_password_auth_disabled(self) -> None: def test_password_auth_disabled(self) -> None:
with mock.patch('zproject.backends.password_auth_enabled', return_value=False): with mock.patch('zproject.backends.password_auth_enabled', return_value=False):
result = self.client_post("/api/v1/fetch_api_key", result = self.client_post("/api/v1/fetch_api_key",
@ -1984,7 +1598,7 @@ class FetchAuthBackends(ZulipTestCase):
result[backend_name] = backend_name in expected_backends result[backend_name] = backend_name in expected_backends
return result return result
backends = [GoogleMobileOauth2Backend(), DevAuthBackend()] backends = [GoogleAuthBackend(), DevAuthBackend()]
with mock.patch('django.contrib.auth.get_backends', return_value=backends): with mock.patch('django.contrib.auth.get_backends', return_value=backends):
result = self.client_get("/api/v1/get_auth_backends") result = self.client_get("/api/v1/get_auth_backends")
self.assert_json_success(result) self.assert_json_success(result)

View File

@ -364,32 +364,32 @@ class AboutPageTest(ZulipTestCase):
self.assertEqual(split_by(flat_list, 3, None), expected_result) self.assertEqual(split_by(flat_list, 3, None), expected_result)
class ConfigErrorTest(ZulipTestCase): class ConfigErrorTest(ZulipTestCase):
@override_settings(GOOGLE_OAUTH2_CLIENT_ID=None) @override_settings(SOCIAL_AUTH_GOOGLE_KEY=None)
def test_google(self) -> None: def test_google(self) -> None:
result = self.client_get("/accounts/login/google/") result = self.client_get("/accounts/login/social/google")
self.assertEqual(result.status_code, 302) self.assertEqual(result.status_code, 302)
self.assertEqual(result.url, '/config-error/google') self.assertEqual(result.url, '/config-error/google')
result = self.client_get(result.url) result = self.client_get(result.url)
self.assert_in_success_response(["google_oauth2_client_id"], result) self.assert_in_success_response(["social_auth_google_key"], result)
self.assert_in_success_response(["google_oauth2_client_secret"], result) self.assert_in_success_response(["social_auth_google_secret"], result)
self.assert_in_success_response(["zproject/dev-secrets.conf"], result) self.assert_in_success_response(["zproject/dev-secrets.conf"], result)
self.assert_not_in_success_response(["GOOGLE_OAUTH2_CLIENT_ID"], result) self.assert_not_in_success_response(["SOCIAL_AUTH_GOOGLE_KEY"], result)
self.assert_not_in_success_response(["zproject/dev_settings.py"], result) self.assert_not_in_success_response(["zproject/dev_settings.py"], result)
self.assert_not_in_success_response(["/etc/zulip/settings.py"], result) self.assert_not_in_success_response(["/etc/zulip/settings.py"], result)
self.assert_not_in_success_response(["/etc/zulip/zulip-secrets.conf"], result) self.assert_not_in_success_response(["/etc/zulip/zulip-secrets.conf"], result)
@override_settings(GOOGLE_OAUTH2_CLIENT_ID=None) @override_settings(SOCIAL_AUTH_GOOGLE_KEY=None)
@override_settings(DEVELOPMENT=False) @override_settings(DEVELOPMENT=False)
def test_google_production_error(self) -> None: def test_google_production_error(self) -> None:
result = self.client_get("/accounts/login/google/") result = self.client_get("/accounts/login/social/google")
self.assertEqual(result.status_code, 302) self.assertEqual(result.status_code, 302)
self.assertEqual(result.url, '/config-error/google') self.assertEqual(result.url, '/config-error/google')
result = self.client_get(result.url) result = self.client_get(result.url)
self.assert_in_success_response(["GOOGLE_OAUTH2_CLIENT_ID"], result) self.assert_in_success_response(["SOCIAL_AUTH_GOOGLE_KEY"], result)
self.assert_in_success_response(["/etc/zulip/settings.py"], result) self.assert_in_success_response(["/etc/zulip/settings.py"], result)
self.assert_in_success_response(["google_oauth2_client_secret"], result) self.assert_in_success_response(["social_auth_google_secret"], result)
self.assert_in_success_response(["/etc/zulip/zulip-secrets.conf"], result) self.assert_in_success_response(["/etc/zulip/zulip-secrets.conf"], result)
self.assert_not_in_success_response(["google_oauth2_client_id"], result) self.assert_not_in_success_response(["social_auth_google_key"], result)
self.assert_not_in_success_response(["zproject/dev_settings.py"], result) self.assert_not_in_success_response(["zproject/dev_settings.py"], result)
self.assert_not_in_success_response(["zproject/dev-secrets.conf"], result) self.assert_not_in_success_response(["zproject/dev-secrets.conf"], result)

View File

@ -1649,7 +1649,7 @@ class EventsRegisterTest(ZulipTestCase):
'zproject.backends.DevAuthBackend', 'zproject.backends.DevAuthBackend',
'zproject.backends.EmailAuthBackend', 'zproject.backends.EmailAuthBackend',
'zproject.backends.GitHubAuthBackend', 'zproject.backends.GitHubAuthBackend',
'zproject.backends.GoogleMobileOauth2Backend', 'zproject.backends.GoogleAuthBackend',
'zproject.backends.ZulipLDAPAuthBackend', 'zproject.backends.ZulipLDAPAuthBackend',
) )
return self.settings(AUTHENTICATION_BACKENDS=backends) return self.settings(AUTHENTICATION_BACKENDS=backends)

View File

@ -9,7 +9,6 @@ from zerver.decorator import require_post, \
process_client, do_login, log_view_func process_client, do_login, log_view_func
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.template.response import SimpleTemplateResponse from django.template.response import SimpleTemplateResponse
from django.middleware.csrf import get_token
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_GET from django.views.decorators.http import require_GET
@ -41,12 +40,8 @@ from zproject.backends import password_auth_enabled, dev_auth_enabled, \
AUTH_BACKEND_NAME_MAP, auth_enabled_helper AUTH_BACKEND_NAME_MAP, auth_enabled_helper
from version import ZULIP_VERSION from version import ZULIP_VERSION
import hashlib
import hmac
import jwt import jwt
import logging import logging
import requests
import time
from two_factor.forms import BackupTokenForm from two_factor.forms import BackupTokenForm
from two_factor.views import LoginView as BaseTwoFactorLoginView from two_factor.views import LoginView as BaseTwoFactorLoginView
@ -319,16 +314,6 @@ def remote_user_jwt(request: HttpRequest) -> HttpResponse:
return login_or_register_remote_user(request, email, user_profile, remote_user) return login_or_register_remote_user(request, email, user_profile, remote_user)
def google_oauth2_csrf(request: HttpRequest, value: str) -> str:
# In Django 1.10, get_token returns a salted token which changes
# every time get_token is called.
from django.middleware.csrf import _unsalt_cipher_token
token = _unsalt_cipher_token(get_token(request))
return hmac.new(token.encode('utf-8'), value.encode("utf-8"), hashlib.sha256).hexdigest()
def reverse_on_root(viewname: str, *args: str, **kwargs: str) -> str:
return settings.ROOT_DOMAIN_URI + reverse(viewname, args=args, kwargs=kwargs)
def oauth_redirect_to_root(request: HttpRequest, url: str, def oauth_redirect_to_root(request: HttpRequest, url: str,
sso_type: str, is_signup: bool=False) -> HttpResponse: sso_type: str, is_signup: bool=False) -> HttpResponse:
main_site_uri = settings.ROOT_DOMAIN_URI + url main_site_uri = settings.ROOT_DOMAIN_URI + url
@ -359,20 +344,14 @@ def oauth_redirect_to_root(request: HttpRequest, url: str,
return redirect(main_site_uri + '?' + urllib.parse.urlencode(params)) return redirect(main_site_uri + '?' + urllib.parse.urlencode(params))
def start_google_oauth2(request: HttpRequest) -> HttpResponse:
url = reverse('zerver.views.auth.send_oauth_request_to_google')
if not (settings.GOOGLE_OAUTH2_CLIENT_ID and settings.GOOGLE_OAUTH2_CLIENT_SECRET):
return redirect_to_config_error("google")
is_signup = bool(request.GET.get('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])
if (backend == "github") and not (settings.SOCIAL_AUTH_GITHUB_KEY and if (backend == "github") and not (settings.SOCIAL_AUTH_GITHUB_KEY and
settings.SOCIAL_AUTH_GITHUB_SECRET): settings.SOCIAL_AUTH_GITHUB_SECRET):
return redirect_to_config_error("github") return redirect_to_config_error("github")
if (backend == "google") and not (settings.SOCIAL_AUTH_GOOGLE_KEY and
settings.SOCIAL_AUTH_GOOGLE_SECRET):
return redirect_to_config_error("google")
# TODO: Add a similar block of AzureAD. # TODO: Add a similar block of AzureAD.
return oauth_redirect_to_root(request, backend_url, 'social') return oauth_redirect_to_root(request, backend_url, 'social')
@ -381,114 +360,6 @@ 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, 'social', 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', '')
is_signup = request.GET.get('is_signup', '')
next = request.GET.get('next', '')
mobile_flow_otp = request.GET.get('mobile_flow_otp', '0')
multiuse_object_key = request.GET.get('multiuse_object_key', '')
if ((settings.ROOT_DOMAIN_LANDING_PAGE and subdomain == '') or
not Realm.objects.filter(string_id=subdomain).exists()):
return redirect_to_subdomain_login_url()
google_uri = 'https://accounts.google.com/o/oauth2/auth?'
cur_time = str(int(time.time()))
csrf_state = '%s:%s:%s:%s:%s:%s' % (cur_time, subdomain, mobile_flow_otp, is_signup,
next, multiuse_object_key)
# Now compute the CSRF hash with the other parameters as an input
csrf_state += ":%s" % (google_oauth2_csrf(request, csrf_state),)
params = {
'response_type': 'code',
'client_id': settings.GOOGLE_OAUTH2_CLIENT_ID,
'redirect_uri': reverse_on_root('zerver.views.auth.finish_google_oauth2'),
'scope': 'profile email',
'state': csrf_state,
'prompt': 'select_account',
}
return redirect(google_uri + urllib.parse.urlencode(params))
@log_view_func
def finish_google_oauth2(request: HttpRequest) -> HttpResponse:
error = request.GET.get('error')
if error == 'access_denied':
return redirect('/')
elif error is not None:
logging.warning('Error from google oauth2 login: %s' % (request.GET.get("error"),))
return HttpResponse(status=400)
csrf_state = request.GET.get('state')
if csrf_state is None or len(csrf_state.split(':')) != 7:
logging.warning('Missing Google oauth2 CSRF state')
return HttpResponse(status=400)
(csrf_data, hmac_value) = csrf_state.rsplit(':', 1)
if hmac_value != google_oauth2_csrf(request, csrf_data):
logging.warning('Google oauth2 CSRF error')
return HttpResponse(status=400)
cur_time, subdomain, mobile_flow_otp, is_signup, next, multiuse_object_key = csrf_data.split(':')
if mobile_flow_otp == '0':
mobile_flow_otp = None
is_signup = bool(is_signup == '1')
resp = requests.post(
'https://www.googleapis.com/oauth2/v3/token',
data={
'code': request.GET.get('code'),
'client_id': settings.GOOGLE_OAUTH2_CLIENT_ID,
'client_secret': settings.GOOGLE_OAUTH2_CLIENT_SECRET,
'redirect_uri': reverse_on_root('zerver.views.auth.finish_google_oauth2'),
'grant_type': 'authorization_code',
},
)
if resp.status_code == 400:
logging.warning('User error converting Google oauth2 login to token: %s' % (resp.text,))
return HttpResponse(status=400)
elif resp.status_code != 200:
logging.error('Could not convert google oauth2 code to access_token: %s' % (resp.text,))
return HttpResponse(status=400)
access_token = resp.json()['access_token']
resp = requests.get(
'https://www.googleapis.com/oauth2/v3/userinfo',
params={'access_token': access_token}
)
if resp.status_code == 400:
logging.warning('Google login failed making info API call: %s' % (resp.text,))
return HttpResponse(status=400)
elif resp.status_code != 200:
logging.error('Google login failed making API call: %s' % (resp.text,))
return HttpResponse(status=400)
body = resp.json()
if not body['email_verified']:
logging.error('Google oauth2 account email not verified.')
return HttpResponse(status=400)
# Extract the user info from the Google response
full_name = body['name']
email_address = body['email']
try:
realm = Realm.objects.get(string_id=subdomain)
except Realm.DoesNotExist: # nocoverage
return redirect_to_subdomain_login_url()
if mobile_flow_otp is not None:
# When request was not initiated from subdomain.
user_profile = authenticate_remote_user(realm, email_address)
return login_or_register_remote_user(request, email_address, user_profile,
full_name,
mobile_flow_otp=mobile_flow_otp,
is_signup=is_signup,
redirect_to=next)
return redirect_and_log_into_subdomain(
realm, full_name, email_address, is_signup=is_signup,
redirect_to=next, multiuse_object_key=multiuse_object_key)
def authenticate_remote_user(realm: Realm, email_address: str) -> UserProfile: def authenticate_remote_user(realm: Realm, email_address: str) -> UserProfile:
if email_address is None: if email_address is None:
# No need to authenticate if email address is None. We already # No need to authenticate if email address is None. We already
@ -847,17 +718,10 @@ def api_fetch_api_key(request: HttpRequest, username: str=REQ(), password: str=R
return_data = {} # type: Dict[str, bool] return_data = {} # type: Dict[str, bool]
subdomain = get_subdomain(request) subdomain = get_subdomain(request)
realm = get_realm(subdomain) realm = get_realm(subdomain)
if username == "google-oauth2-token":
# This code path is auth for the legacy Android app
user_profile = authenticate(google_oauth2_token=password,
realm=realm,
return_data=return_data)
else:
if not ldap_auth_enabled(realm=get_realm_from_request(request)): if not ldap_auth_enabled(realm=get_realm_from_request(request)):
# In case we don't authenticate against LDAP, check for a valid # In case we don't authenticate against LDAP, check for a valid
# email. LDAP backend can authenticate against a non-email. # email. LDAP backend can authenticate against a non-email.
validate_login_email(username) validate_login_email(username)
user_profile = authenticate(username=username, user_profile = authenticate(username=username,
password=password, password=password,
realm=realm, realm=realm,
@ -872,11 +736,6 @@ def api_fetch_api_key(request: HttpRequest, username: str=REQ(), password: str=R
return json_error(_("Password auth is disabled in your team."), return json_error(_("Password auth is disabled in your team."),
data={"reason": "password auth disabled"}, status=403) data={"reason": "password auth disabled"}, status=403)
if user_profile is None: if user_profile is None:
if return_data.get("valid_attestation"):
# We can leak that the user is unregistered iff
# they present a valid authentication string for the user.
return json_error(_("This user is not registered; do so from a browser."),
data={"reason": "unregistered"}, status=403)
return json_error(_("Your username or password is incorrect."), return json_error(_("Your username or password is incorrect."),
data={"reason": "incorrect_creds"}, status=403) data={"reason": "incorrect_creds"}, status=403)

View File

@ -31,6 +31,7 @@ from social_core.backends.github import GithubOAuth2, GithubOrganizationOAuth2,
GithubTeamOAuth2 GithubTeamOAuth2
from social_core.backends.azuread import AzureADOAuth2 from social_core.backends.azuread import AzureADOAuth2
from social_core.backends.base import BaseAuth from social_core.backends.base import BaseAuth
from social_core.backends.google import GoogleOAuth2
from social_core.backends.oauth import BaseOAuth2 from social_core.backends.oauth import BaseOAuth2
from social_core.pipeline.partial import partial from social_core.pipeline.partial import partial
from social_core.exceptions import AuthFailed, SocialAuthBaseException from social_core.exceptions import AuthFailed, SocialAuthBaseException
@ -194,45 +195,6 @@ class EmailAuthBackend(ZulipAuthMixin):
return user_profile return user_profile
return None return None
class GoogleMobileOauth2Backend(ZulipAuthMixin):
"""
Google Apps authentication for the legacy Android app.
DummyAuthBackend is what's actually used for our modern Google auth,
both for web and mobile (the latter via the mobile_flow_otp feature).
Allows a user to sign in using a Google-issued OAuth2 token.
Ref:
https://developers.google.com/+/mobile/android/sign-in#server-side_access_for_your_app
https://developers.google.com/accounts/docs/CrossClientAuth#offlineAccess
"""
def authenticate(self, *, google_oauth2_token: str, realm: Realm,
return_data: Optional[Dict[str, Any]]=None) -> Optional[UserProfile]:
# We lazily import apiclient as part of optimizing the base
# import time for a Zulip management command, since it's only
# used in this one code path and takes 30-50ms to import.
from apiclient.sample_tools import client as googleapiclient
from oauth2client.crypt import AppIdentityError
if return_data is None:
return_data = {}
if not google_auth_enabled(realm=realm):
return_data["google_auth_disabled"] = True
return None
try:
token_payload = googleapiclient.verify_id_token(google_oauth2_token, settings.GOOGLE_CLIENT_ID)
except AppIdentityError:
return None
if token_payload["email_verified"] not in (True, "true"):
return_data["valid_attestation"] = False
return None
return_data["valid_attestation"] = True
return common_get_active_user(token_payload["email"], realm, return_data)
class ZulipRemoteUserBackend(RemoteUserBackend): class ZulipRemoteUserBackend(RemoteUserBackend):
"""Authentication backend that reads the Apache REMOTE_USER variable. """Authentication backend that reads the Apache REMOTE_USER variable.
Used primarily in enterprise environments with an SSO solution Used primarily in enterprise environments with an SSO solution
@ -966,14 +928,26 @@ class AzureADAuthBackend(SocialAuthMixin, AzureADOAuth2):
sort_order = 50 sort_order = 50
auth_backend_name = "AzureAD" auth_backend_name = "AzureAD"
class GoogleAuthBackend(SocialAuthMixin, GoogleOAuth2):
sort_order = 150
auth_backend_name = "Google"
name = "google"
def get_verified_emails(self, *args: Any, **kwargs: Any) -> List[str]:
verified_emails = [] # type: List[str]
details = kwargs["response"]
email_verified = details.get("email_verified")
if email_verified:
verified_emails.append(details["email"])
return verified_emails
AUTH_BACKEND_NAME_MAP = { AUTH_BACKEND_NAME_MAP = {
'Dev': DevAuthBackend, 'Dev': DevAuthBackend,
'Email': EmailAuthBackend, 'Email': EmailAuthBackend,
'Google': GoogleMobileOauth2Backend,
'LDAP': ZulipLDAPAuthBackend, 'LDAP': ZulipLDAPAuthBackend,
'RemoteUser': ZulipRemoteUserBackend, 'RemoteUser': ZulipRemoteUserBackend,
} # type: Dict[str, Any] } # type: Dict[str, Any]
OAUTH_BACKEND_NAMES = ["Google"] # type: List[str] OAUTH_BACKEND_NAMES = [] # type: List[str]
SOCIAL_AUTH_BACKENDS = [] # type: List[BaseOAuth2] SOCIAL_AUTH_BACKENDS = [] # type: List[BaseOAuth2]
# Authomatically add all of our social auth backends to relevant data structures. # Authomatically add all of our social auth backends to relevant data structures.
@ -982,3 +956,7 @@ for social_auth_subclass in SocialAuthMixin.__subclasses__():
if issubclass(social_auth_subclass, BaseOAuth2): if issubclass(social_auth_subclass, BaseOAuth2):
OAUTH_BACKEND_NAMES.append(social_auth_subclass.auth_backend_name) OAUTH_BACKEND_NAMES.append(social_auth_subclass.auth_backend_name)
SOCIAL_AUTH_BACKENDS.append(social_auth_subclass) SOCIAL_AUTH_BACKENDS.append(social_auth_subclass)
# Provide this alternative name for backwards compatibility with
# installations that had the old backend enabled.
GoogleMobileOauth2Backend = GoogleAuthBackend

View File

@ -43,7 +43,7 @@ AUTHENTICATION_BACKENDS = (
'zproject.backends.DevAuthBackend', 'zproject.backends.DevAuthBackend',
'zproject.backends.EmailAuthBackend', 'zproject.backends.EmailAuthBackend',
'zproject.backends.GitHubAuthBackend', 'zproject.backends.GitHubAuthBackend',
'zproject.backends.GoogleMobileOauth2Backend', 'zproject.backends.GoogleAuthBackend',
# 'zproject.backends.AzureADAuthBackend', # 'zproject.backends.AzureADAuthBackend',
) )

View File

@ -112,7 +112,7 @@ EXTERNAL_HOST = 'zulip.example.com'
# initial realm and user. # initial realm and user.
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'zproject.backends.EmailAuthBackend', # Email and password; just requires SMTP setup 'zproject.backends.EmailAuthBackend', # Email and password; just requires SMTP setup
# 'zproject.backends.GoogleMobileOauth2Backend', # Google Apps, setup below # 'zproject.backends.GoogleAuthBackend', # Google auth, setup below
# 'zproject.backends.GitHubAuthBackend', # GitHub auth, setup below # 'zproject.backends.GitHubAuthBackend', # GitHub auth, setup below
# 'zproject.backends.AzureADAuthBackend', # Microsoft Azure Active Directory auth, setup below # 'zproject.backends.AzureADAuthBackend', # Microsoft Azure Active Directory auth, setup below
# 'zproject.backends.ZulipLDAPAuthBackend', # LDAP, setup below # 'zproject.backends.ZulipLDAPAuthBackend', # LDAP, setup below
@ -129,7 +129,7 @@ AUTHENTICATION_BACKENDS = (
# correspond to your Zulip instance. # correspond to your Zulip instance.
# #
# (2) Navigate to "APIs & services" > "Library", and find the # (2) Navigate to "APIs & services" > "Library", and find the
# "Google+ API". Choose "Enable". # "Identity Toolkit API". Choose "Enable".
# #
# (3) Return to "Credentials", and select "Create credentials". # (3) Return to "Credentials", and select "Create credentials".
# Choose "OAuth client ID", and follow prompts to create a consent # Choose "OAuth client ID", and follow prompts to create a consent
@ -138,9 +138,9 @@ AUTHENTICATION_BACKENDS = (
# based on your value for EXTERNAL_HOST. # based on your value for EXTERNAL_HOST.
# #
# (4) You should get a client ID and a client secret. Copy them. # (4) You should get a client ID and a client secret. Copy them.
# Use the client ID as `GOOGLE_OAUTH2_CLIENT_ID` here, and put the # Use the client ID as `SOCIAL_AUTH_GOOGLE_KEY` here, and put the
# client secret in zulip-secrets.conf as `google_oauth2_client_secret`. # client secret in zulip-secrets.conf as `social_auth_google_secret`.
#GOOGLE_OAUTH2_CLIENT_ID = <your client ID from Google> #SOCIAL_AUTH_GOOGLE_KEY = <your client ID from Google>
######## ########
# GitHub OAuth. # GitHub OAuth.

View File

@ -154,11 +154,13 @@ DEFAULT_SETTINGS = {
# Social auth; we support providing values for some of these # Social auth; we support providing values for some of these
# settings in zulip-secrets.conf instead of settings.py in development. # settings in zulip-secrets.conf instead of settings.py in development.
'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),
'GOOGLE_OAUTH2_CLIENT_ID': get_secret('google_oauth2_client_id', 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, 'SOCIAL_AUTH_SUBDOMAIN': None,
'SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET': get_secret('azure_oauth2_secret'), 'SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET': get_secret('azure_oauth2_secret'),
'SOCIAL_AUTH_GOOGLE_KEY': get_secret('social_auth_google_key', development_only=True),
# Historical name for SOCIAL_AUTH_GITHUB_KEY; still allowed in production.
'GOOGLE_OAUTH2_CLIENT_ID': None,
# Other auth # Other auth
'SSO_APPEND_DOMAIN': None, 'SSO_APPEND_DOMAIN': None,
@ -769,8 +771,6 @@ if LOCAL_UPLOADS_DIR is not None:
# https://cloud.google.com/console/project/apps~zulip-android/apiui/credential # https://cloud.google.com/console/project/apps~zulip-android/apiui/credential
ANDROID_GCM_API_KEY = get_secret("android_gcm_api_key") ANDROID_GCM_API_KEY = get_secret("android_gcm_api_key")
GOOGLE_OAUTH2_CLIENT_SECRET = get_secret('google_oauth2_client_secret')
DROPBOX_APP_KEY = get_secret("dropbox_app_key") DROPBOX_APP_KEY = get_secret("dropbox_app_key")
MAILCHIMP_API_KEY = get_secret("mailchimp_api_key") MAILCHIMP_API_KEY = get_secret("mailchimp_api_key")
@ -1348,6 +1348,14 @@ SOCIAL_AUTH_GITHUB_ORG_SECRET = SOCIAL_AUTH_GITHUB_SECRET
SOCIAL_AUTH_GITHUB_TEAM_KEY = SOCIAL_AUTH_GITHUB_KEY SOCIAL_AUTH_GITHUB_TEAM_KEY = SOCIAL_AUTH_GITHUB_KEY
SOCIAL_AUTH_GITHUB_TEAM_SECRET = SOCIAL_AUTH_GITHUB_SECRET SOCIAL_AUTH_GITHUB_TEAM_SECRET = SOCIAL_AUTH_GITHUB_SECRET
SOCIAL_AUTH_GOOGLE_SECRET = get_secret('social_auth_google_secret')
# Fallback to google-oauth settings in case social auth settings for
# google are missing; this is for backwards-compatibility with older
# Zulip versions where /etc/zulip/settings.py has not been migrated yet.
GOOGLE_OAUTH2_CLIENT_SECRET = get_secret('google_oauth2_client_secret')
SOCIAL_AUTH_GOOGLE_KEY = SOCIAL_AUTH_GOOGLE_KEY or GOOGLE_OAUTH2_CLIENT_ID
SOCIAL_AUTH_GOOGLE_SECRET = SOCIAL_AUTH_GOOGLE_SECRET or GOOGLE_OAUTH2_CLIENT_SECRET
SOCIAL_AUTH_PIPELINE = [ SOCIAL_AUTH_PIPELINE = [
'social_core.pipeline.social_auth.social_details', 'social_core.pipeline.social_auth.social_details',
'zproject.backends.social_auth_associate_user', 'zproject.backends.social_auth_associate_user',

View File

@ -155,6 +155,8 @@ 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_GOOGLE_KEY = "key"
SOCIAL_AUTH_GOOGLE_SECRET = "secret"
SOCIAL_AUTH_SUBDOMAIN = 'www' SOCIAL_AUTH_SUBDOMAIN = 'www'
# By default two factor authentication is disabled in tests. # By default two factor authentication is disabled in tests.

View File

@ -423,13 +423,6 @@ i18n_urls = [
url(r'^accounts/register/social/([\w,-]+)$', url(r'^accounts/register/social/([\w,-]+)$',
zerver.views.auth.start_social_signup, zerver.views.auth.start_social_signup,
name='signup-social'), name='signup-social'),
url(r'^accounts/login/google/$', zerver.views.auth.start_google_oauth2,
name='zerver.views.auth.start_google_oauth2'),
url(r'^accounts/login/google/send/$',
zerver.views.auth.send_oauth_request_to_google,
name='zerver.views.auth.send_oauth_request_to_google'),
url(r'^accounts/login/google/done/$', zerver.views.auth.finish_google_oauth2,
name='zerver.views.auth.finish_google_oauth2'),
url(r'^accounts/login/subdomain/([^/]+)$', zerver.views.auth.log_into_subdomain, url(r'^accounts/login/subdomain/([^/]+)$', zerver.views.auth.log_into_subdomain,
name='zerver.views.auth.log_into_subdomain'), name='zerver.views.auth.log_into_subdomain'),
url(r'^accounts/login/local/$', zerver.views.auth.dev_direct_login, url(r'^accounts/login/local/$', zerver.views.auth.dev_direct_login,