mirror of https://github.com/zulip/zulip.git
auth: Create a new page hop for desktop auth.
Create a new page for desktop auth flow, in which users can select one from going to the app or continue the flow in the browser. Co-authored-by: Mateusz Mandera <mateusz.mandera@protonmail.com>
This commit is contained in:
parent
aaee506fb2
commit
020a263a67
|
@ -1719,3 +1719,17 @@ input.new-organization-button {
|
||||||
.table .json-api-example code {
|
.table .json-api-example code {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.desktop-redirect-box {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-redirect-links {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-decoration: underline;
|
||||||
|
color: hsl(0, 0%, 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-redirect-image {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
{% extends "zerver/portico.html" %}
|
||||||
|
|
||||||
|
{% block customhead %}
|
||||||
|
<meta http-equiv="Refresh" content="2; {{ desktop_url }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="flex">
|
||||||
|
<div class="desktop-redirect-box">
|
||||||
|
<img class="avatar desktop-redirect-image" src="{{ realm_icon_url }}" alt=""/><br>
|
||||||
|
Redirecting to your Zulip app...
|
||||||
|
<div class="desktop-redirect-links">
|
||||||
|
<a id="desktop-redirect-link" href="{{ desktop_url }}">If you weren't redirected, click here.</a><br>
|
||||||
|
<a href="{{ browser_url }}">Or, continue in your browser.</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -1,4 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
|
@ -598,7 +599,45 @@ class CheckPasswordStrengthTest(ZulipTestCase):
|
||||||
# Good password:
|
# Good password:
|
||||||
self.assertTrue(check_password_strength('f657gdGGk9'))
|
self.assertTrue(check_password_strength('f657gdGGk9'))
|
||||||
|
|
||||||
class SocialAuthBase(ZulipTestCase):
|
class DesktopFlowTestingLib(ZulipTestCase):
|
||||||
|
def verify_desktop_flow_end_page(self, response: HttpResponse, email: str,
|
||||||
|
desktop_flow_otp: str) -> None:
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
soup = BeautifulSoup(response.content, "html.parser")
|
||||||
|
links = [a['href'] for a in soup.find_all('a', href=True)]
|
||||||
|
self.assert_length(links, 2)
|
||||||
|
for url in links:
|
||||||
|
if url.startswith("zulip://"):
|
||||||
|
desktop_app_url = url
|
||||||
|
else:
|
||||||
|
browser_url = url
|
||||||
|
meta_refresh_content = soup.find('meta', {'http-equiv': lambda value: value == "Refresh"})['content']
|
||||||
|
auto_redirect_url = meta_refresh_content.split('; ')[1]
|
||||||
|
|
||||||
|
self.assertEqual(auto_redirect_url, desktop_app_url)
|
||||||
|
|
||||||
|
decrypted_key = self.verify_desktop_app_url_and_return_key(desktop_app_url, email, desktop_flow_otp)
|
||||||
|
self.assertEqual(browser_url, 'http://zulip.testserver/accounts/login/subdomain/%s' % (decrypted_key,))
|
||||||
|
|
||||||
|
result = self.client_get(browser_url)
|
||||||
|
self.assertEqual(result.status_code, 302)
|
||||||
|
realm = get_realm("zulip")
|
||||||
|
user_profile = get_user(email, realm)
|
||||||
|
self.assert_logged_in_user_id(user_profile.id)
|
||||||
|
|
||||||
|
def verify_desktop_app_url_and_return_key(self, url: str, email: str, desktop_flow_otp: str) -> str:
|
||||||
|
parsed_url = urllib.parse.urlparse(url)
|
||||||
|
query_params = urllib.parse.parse_qs(parsed_url.query)
|
||||||
|
self.assertEqual(parsed_url.scheme, 'zulip')
|
||||||
|
self.assertEqual(query_params["realm"], ['http://zulip.testserver'])
|
||||||
|
self.assertEqual(query_params["email"], [email])
|
||||||
|
|
||||||
|
encrypted_key = query_params["otp_encrypted_login_key"][0]
|
||||||
|
decrypted_key = otp_decrypt_api_key(encrypted_key, desktop_flow_otp)
|
||||||
|
return decrypted_key
|
||||||
|
|
||||||
|
class SocialAuthBase(DesktopFlowTestingLib, 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:
|
||||||
|
|
||||||
|
@ -736,9 +775,8 @@ class SocialAuthBase(ZulipTestCase):
|
||||||
# TODO: Generalize this testing code for use with other
|
# TODO: Generalize this testing code for use with other
|
||||||
# authentication backends; for now, we just assert that
|
# authentication backends; for now, we just assert that
|
||||||
# it's definitely the GitHub authentication backend.
|
# it's definitely the GitHub authentication backend.
|
||||||
|
if self.AUTH_FINISH_URL == "/complete/github/":
|
||||||
self.assert_in_success_response(["Select account"], result)
|
self.assert_in_success_response(["Select account"], result)
|
||||||
assert self.AUTH_FINISH_URL == "/complete/github/"
|
|
||||||
|
|
||||||
result = self.client_get(self.AUTH_FINISH_URL,
|
result = self.client_get(self.AUTH_FINISH_URL,
|
||||||
dict(state=csrf_state, email=account_data_dict['email']), **headers)
|
dict(state=csrf_state, email=account_data_dict['email']), **headers)
|
||||||
elif self.AUTH_FINISH_URL == "/complete/github/":
|
elif self.AUTH_FINISH_URL == "/complete/github/":
|
||||||
|
@ -894,22 +932,7 @@ class SocialAuthBase(ZulipTestCase):
|
||||||
result = self.social_auth_test(account_data_dict, subdomain='zulip',
|
result = self.social_auth_test(account_data_dict, subdomain='zulip',
|
||||||
expect_choose_email_screen=True,
|
expect_choose_email_screen=True,
|
||||||
desktop_flow_otp=desktop_flow_otp)
|
desktop_flow_otp=desktop_flow_otp)
|
||||||
self.assertEqual(result.status_code, 302)
|
self.verify_desktop_flow_end_page(result, self.email, desktop_flow_otp)
|
||||||
|
|
||||||
redirect_url = result['Location']
|
|
||||||
parsed_url = urllib.parse.urlparse(redirect_url)
|
|
||||||
query_params = urllib.parse.parse_qs(parsed_url.query)
|
|
||||||
self.assertEqual(parsed_url.scheme, 'zulip')
|
|
||||||
self.assertEqual(query_params["realm"], ['http://zulip.testserver'])
|
|
||||||
self.assertEqual(query_params["email"], [self.example_email("hamlet")])
|
|
||||||
|
|
||||||
encrypted_key = query_params["otp_encrypted_login_key"][0]
|
|
||||||
decrypted_key = otp_decrypt_api_key(encrypted_key, desktop_flow_otp)
|
|
||||||
auth_url = 'http://zulip.testserver/accounts/login/subdomain/{}'.format(decrypted_key)
|
|
||||||
|
|
||||||
result = self.client_get(auth_url)
|
|
||||||
self.assertEqual(result.status_code, 302)
|
|
||||||
self.assert_logged_in_user_id(self.user_profile.id)
|
|
||||||
|
|
||||||
def test_social_auth_session_fields_cleared_correctly(self) -> None:
|
def test_social_auth_session_fields_cleared_correctly(self) -> None:
|
||||||
mobile_flow_otp = '1234abcd' * 8
|
mobile_flow_otp = '1234abcd' * 8
|
||||||
|
@ -1019,22 +1042,11 @@ class SocialAuthBase(ZulipTestCase):
|
||||||
self.assertIn(otp_decrypt_api_key(encrypted_api_key, mobile_flow_otp), user_api_keys)
|
self.assertIn(otp_decrypt_api_key(encrypted_api_key, mobile_flow_otp), user_api_keys)
|
||||||
return
|
return
|
||||||
elif desktop_flow_otp:
|
elif desktop_flow_otp:
|
||||||
|
self.verify_desktop_flow_end_page(result, email, desktop_flow_otp)
|
||||||
|
# Now the desktop app is logged in, continue with the logged in check.
|
||||||
|
else:
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 302)
|
||||||
redirect_url = result['Location']
|
|
||||||
parsed_url = urllib.parse.urlparse(redirect_url)
|
|
||||||
query_params = urllib.parse.parse_qs(parsed_url.query)
|
|
||||||
self.assertEqual(parsed_url.scheme, 'zulip')
|
|
||||||
self.assertEqual(query_params["realm"], ['http://zulip.testserver'])
|
|
||||||
self.assertEqual(query_params["email"], [email])
|
|
||||||
|
|
||||||
encrypted_key = query_params["otp_encrypted_login_key"][0]
|
|
||||||
decrypted_key = otp_decrypt_api_key(encrypted_key, desktop_flow_otp)
|
|
||||||
auth_url = 'http://zulip.testserver/accounts/login/subdomain/{}'.format(decrypted_key)
|
|
||||||
|
|
||||||
result = self.client_get(auth_url)
|
|
||||||
# Now the desktop app is logged in, continue with the logged in check:
|
|
||||||
|
|
||||||
self.assertEqual(result.status_code, 302)
|
|
||||||
user_profile = get_user(email, realm)
|
user_profile = get_user(email, realm)
|
||||||
self.assert_logged_in_user_id(user_profile.id)
|
self.assert_logged_in_user_id(user_profile.id)
|
||||||
self.assertEqual(user_profile.full_name, expected_final_name)
|
self.assertEqual(user_profile.full_name, expected_final_name)
|
||||||
|
@ -2468,7 +2480,7 @@ class TestDevAuthBackend(ZulipTestCase):
|
||||||
response = self.client_post('/accounts/login/local/', data)
|
response = self.client_post('/accounts/login/local/', data)
|
||||||
self.assertRedirects(response, reverse('dev_not_supported'))
|
self.assertRedirects(response, reverse('dev_not_supported'))
|
||||||
|
|
||||||
class TestZulipRemoteUserBackend(ZulipTestCase):
|
class TestZulipRemoteUserBackend(DesktopFlowTestingLib, ZulipTestCase):
|
||||||
def test_login_success(self) -> None:
|
def test_login_success(self) -> None:
|
||||||
user_profile = self.example_user('hamlet')
|
user_profile = self.example_user('hamlet')
|
||||||
email = user_profile.email
|
email = user_profile.email
|
||||||
|
@ -2657,21 +2669,7 @@ class TestZulipRemoteUserBackend(ZulipTestCase):
|
||||||
result = self.client_post('/accounts/login/sso/',
|
result = self.client_post('/accounts/login/sso/',
|
||||||
dict(desktop_flow_otp=desktop_flow_otp),
|
dict(desktop_flow_otp=desktop_flow_otp),
|
||||||
REMOTE_USER=email)
|
REMOTE_USER=email)
|
||||||
self.assertEqual(result.status_code, 302)
|
self.verify_desktop_flow_end_page(result, email, desktop_flow_otp)
|
||||||
redirect_url = result['Location']
|
|
||||||
parsed_url = urllib.parse.urlparse(redirect_url)
|
|
||||||
query_params = urllib.parse.parse_qs(parsed_url.query)
|
|
||||||
self.assertEqual(parsed_url.scheme, 'zulip')
|
|
||||||
self.assertEqual(query_params["realm"], ['http://zulip.testserver'])
|
|
||||||
self.assertEqual(query_params["email"], [self.example_email("hamlet")])
|
|
||||||
|
|
||||||
encrypted_key = query_params["otp_encrypted_login_key"][0]
|
|
||||||
decrypted_key = otp_decrypt_api_key(encrypted_key, desktop_flow_otp)
|
|
||||||
auth_url = 'http://zulip.testserver/accounts/login/subdomain/{}'.format(decrypted_key)
|
|
||||||
|
|
||||||
result = self.client_get(auth_url)
|
|
||||||
self.assertEqual(result.status_code, 302)
|
|
||||||
self.assert_logged_in_user_id(user_profile.id)
|
|
||||||
|
|
||||||
@override_settings(SEND_LOGIN_EMAILS=True)
|
@override_settings(SEND_LOGIN_EMAILS=True)
|
||||||
@override_settings(SSO_APPEND_DOMAIN="zulip.com")
|
@override_settings(SSO_APPEND_DOMAIN="zulip.com")
|
||||||
|
@ -2701,21 +2699,7 @@ class TestZulipRemoteUserBackend(ZulipTestCase):
|
||||||
result = self.client_post('/accounts/login/sso/',
|
result = self.client_post('/accounts/login/sso/',
|
||||||
dict(desktop_flow_otp=desktop_flow_otp),
|
dict(desktop_flow_otp=desktop_flow_otp),
|
||||||
REMOTE_USER=remote_user)
|
REMOTE_USER=remote_user)
|
||||||
self.assertEqual(result.status_code, 302)
|
self.verify_desktop_flow_end_page(result, email, desktop_flow_otp)
|
||||||
redirect_url = result['Location']
|
|
||||||
parsed_url = urllib.parse.urlparse(redirect_url)
|
|
||||||
query_params = urllib.parse.parse_qs(parsed_url.query)
|
|
||||||
self.assertEqual(parsed_url.scheme, 'zulip')
|
|
||||||
self.assertEqual(query_params["realm"], ['http://zulip.testserver'])
|
|
||||||
self.assertEqual(query_params["email"], [self.example_email("hamlet")])
|
|
||||||
|
|
||||||
encrypted_key = query_params["otp_encrypted_login_key"][0]
|
|
||||||
decrypted_key = otp_decrypt_api_key(encrypted_key, desktop_flow_otp)
|
|
||||||
auth_url = 'http://zulip.testserver/accounts/login/subdomain/{}'.format(decrypted_key)
|
|
||||||
|
|
||||||
result = self.client_get(auth_url)
|
|
||||||
self.assertEqual(result.status_code, 302)
|
|
||||||
self.assert_logged_in_user_id(user_profile.id)
|
|
||||||
|
|
||||||
def test_redirect_to(self) -> None:
|
def test_redirect_to(self) -> None:
|
||||||
"""This test verifies the behavior of the redirect_to logic in
|
"""This test verifies the behavior of the redirect_to logic in
|
||||||
|
|
|
@ -26,6 +26,7 @@ from zerver.forms import HomepageForm, OurAuthenticationForm, \
|
||||||
AuthenticationTokenForm
|
AuthenticationTokenForm
|
||||||
from zerver.lib.mobile_auth_otp import otp_encrypt_api_key
|
from zerver.lib.mobile_auth_otp import otp_encrypt_api_key
|
||||||
from zerver.lib.push_notifications import push_notifications_enabled
|
from zerver.lib.push_notifications import push_notifications_enabled
|
||||||
|
from zerver.lib.realm_icon import realm_icon_url
|
||||||
from zerver.lib.redis_utils import get_redis_client, get_dict_from_redis, put_dict_in_redis
|
from zerver.lib.redis_utils import get_redis_client, get_dict_from_redis, put_dict_in_redis
|
||||||
from zerver.lib.request import REQ, has_request_variables, JsonableError
|
from zerver.lib.request import REQ, has_request_variables, JsonableError
|
||||||
from zerver.lib.response import json_success, json_error
|
from zerver.lib.response import json_success, json_error
|
||||||
|
@ -289,9 +290,13 @@ def finish_desktop_flow(request: HttpRequest, user_profile: UserProfile,
|
||||||
data = {'email': user_profile.delivery_email,
|
data = {'email': user_profile.delivery_email,
|
||||||
'subdomain': realm.subdomain}
|
'subdomain': realm.subdomain}
|
||||||
token = store_login_data(data)
|
token = store_login_data(data)
|
||||||
|
response = create_response_for_otp_flow(token, otp, user_profile,
|
||||||
return create_response_for_otp_flow(token, otp, user_profile,
|
|
||||||
encrypted_key_field_name='otp_encrypted_login_key')
|
encrypted_key_field_name='otp_encrypted_login_key')
|
||||||
|
browser_url = user_profile.realm.uri + reverse('zerver.views.auth.log_into_subdomain', args=[token])
|
||||||
|
context = {'desktop_url': response['Location'],
|
||||||
|
'browser_url': browser_url,
|
||||||
|
'realm_icon_url': realm_icon_url(realm)}
|
||||||
|
return render(request, 'zerver/desktop_redirect.html', context=context)
|
||||||
|
|
||||||
def finish_mobile_flow(request: HttpRequest, user_profile: UserProfile, otp: str) -> HttpResponse:
|
def finish_mobile_flow(request: HttpRequest, user_profile: UserProfile, otp: str) -> HttpResponse:
|
||||||
# For the mobile Oauth flow, we send the API key and other
|
# For the mobile Oauth flow, we send the API key and other
|
||||||
|
|
Loading…
Reference in New Issue