auth: Add support for mobile_flow_otp for RemoteUserBackend.

Because we have a pretty good framework for the existing
mobile_flow_otp system, this requires very little new code.

Fixes #8291.
This commit is contained in:
Tim Abbott 2018-02-06 14:29:57 -08:00
parent 34efab9157
commit 710f5f7c97
2 changed files with 53 additions and 2 deletions

View File

@ -1586,6 +1586,45 @@ class TestZulipRemoteUserBackend(ZulipTestCase):
self.assertEqual(result.status_code, 302)
self.assertIs(get_session_dict_user(self.client.session), user_profile.id)
@override_settings(SEND_LOGIN_EMAILS=True)
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipRemoteUserBackend',))
def test_login_mobile_flow_otp_success(self) -> None:
user_profile = self.example_user('hamlet')
email = user_profile.email
mobile_flow_otp = '1234abcd' * 8
# Verify that the right thing happens with an invalid-format OTP
result = self.client_post('/accounts/login/sso/',
dict(mobile_flow_otp="1234"),
REMOTE_USER=email,
HTTP_USER_AGENT = "ZulipAndroid")
self.assertIs(get_session_dict_user(self.client.session), None)
self.assert_json_error_contains(result, "Invalid OTP", 400)
result = self.client_post('/accounts/login/sso/',
dict(mobile_flow_otp="invalido" * 8),
REMOTE_USER=email,
HTTP_USER_AGENT = "ZulipAndroid")
self.assertIs(get_session_dict_user(self.client.session), None)
self.assert_json_error_contains(result, "Invalid OTP", 400)
result = self.client_post('/accounts/login/sso/',
dict(mobile_flow_otp=mobile_flow_otp),
REMOTE_USER=email,
HTTP_USER_AGENT = "ZulipAndroid")
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"], [self.example_email("hamlet")])
encrypted_api_key = query_params["otp_encrypted_api_key"][0]
self.assertEqual(self.example_user('hamlet').api_key,
otp_decrypt_api_key(encrypted_api_key, mobile_flow_otp))
self.assertEqual(len(mail.outbox), 1)
self.assertIn('Zulip on Android', mail.outbox[0].body)
class TestJWTLogin(ZulipTestCase):
"""
JWT uses ZulipDummyBackend.

View File

@ -178,10 +178,14 @@ def login_or_register_remote_user(request: HttpRequest, remote_username: Optiona
return HttpResponseRedirect(user_profile.realm.uri)
@log_view_func
def remote_user_sso(request: HttpRequest) -> HttpResponse:
@has_request_variables
def remote_user_sso(request: HttpRequest,
mobile_flow_otp: Optional[str]=REQ(default=None)) -> HttpResponse:
try:
remote_user = request.META["REMOTE_USER"]
except KeyError:
# TODO: Arguably the JsonableError values here should be
# full-page HTML configuration errors instead.
raise JsonableError(_("No REMOTE_USER set."))
# Django invokes authenticate methods by matching arguments, and this
@ -190,12 +194,20 @@ def remote_user_sso(request: HttpRequest) -> HttpResponse:
# enabled.
validate_login_email(remote_user_to_email(remote_user))
# Here we support the mobile flow for REMOTE_USER_BACKEND; we
# validate the data format and then pass it through to
# login_or_register_remote_user if appropriate.
if mobile_flow_otp is not None:
if not is_valid_otp(mobile_flow_otp):
raise JsonableError(_("Invalid OTP"))
subdomain = get_subdomain(request)
realm = get_realm(subdomain)
# Since RemoteUserBackend will return None if Realm is None, we
# don't need to check whether `get_realm` returned None.
user_profile = authenticate(remote_user=remote_user, realm=realm)
return login_or_register_remote_user(request, remote_user, user_profile)
return login_or_register_remote_user(request, remote_user, user_profile,
mobile_flow_otp=mobile_flow_otp)
@csrf_exempt
@log_view_func