mirror of https://github.com/zulip/zulip.git
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:
parent
5fc37c5f9b
commit
bf14a0af4d
|
@ -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`
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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) }}
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue