auth: Implement server side of desktop_flow_otp.

This commit is contained in:
Mateusz Mandera
2020-01-23 14:22:28 +01:00
committed by Tim Abbott
parent 8d987ba5ae
commit 859bde482d
4 changed files with 109 additions and 14 deletions

View File

@@ -198,6 +198,8 @@ def register_remote_user(request: HttpRequest, remote_username: str,
def login_or_register_remote_user(request: HttpRequest, remote_username: str,
user_profile: Optional[UserProfile], full_name: str='',
mobile_flow_otp: Optional[str]=None,
desktop_flow_otp: Optional[str]=None,
realm: Optional[Realm]=None,
is_signup: bool=False, redirect_to: str='',
multiuse_object_key: str='',
full_name_validated: bool=False) -> HttpResponse:
@@ -216,8 +218,8 @@ def login_or_register_remote_user(request: HttpRequest, remote_username: str,
Zulip account but is_signup is False (i.e. the user tried to login
and then did social authentication selecting an email address that does
not have a Zulip account in this organization).
* A zulip:// URL to send control back to the mobile apps if they
are doing authentication using the mobile_flow_otp flow.
* A zulip:// URL to send control back to the mobile or desktop apps if they
are doing authentication using the mobile_flow_otp or desktop_flow_otp flow.
"""
if user_profile is None or user_profile.is_mirror_dummy:
return register_remote_user(request, remote_username, full_name,
@@ -229,17 +231,38 @@ def login_or_register_remote_user(request: HttpRequest, remote_username: str,
# or not they're using the mobile OTP flow or want a browser session.
if mobile_flow_otp is not None:
return finish_mobile_flow(request, user_profile, mobile_flow_otp)
elif desktop_flow_otp is not None:
assert realm is not None
return finish_desktop_flow(request, user_profile, realm, desktop_flow_otp)
do_login(request, user_profile)
redirect_to = get_safe_redirect_to(redirect_to, user_profile.realm.uri)
return HttpResponseRedirect(redirect_to)
def finish_desktop_flow(request: HttpRequest, user_profile: UserProfile,
realm: Realm, otp: str) -> HttpResponse:
"""
The desktop otp flow returns to the app (through a zulip:// redirect)
a token that allows obtaining (through log_into_subdomain) a logged in session
for the user account we authenticated in this flow.
The token can only be used once and within LOGIN_KEY_EXPIRATION_SECONDS
of being created, as nothing more powerful is needed for the desktop flow
and this ensures the key can only be used for completing this authentication attempt.
"""
data = {'email': user_profile.delivery_email,
'subdomain': realm.subdomain}
token = store_login_data(data)
return create_response_for_otp_flow(token, otp, user_profile,
encrypted_key_field_name='otp_encrypted_login_key')
def finish_mobile_flow(request: HttpRequest, user_profile: UserProfile, otp: str) -> HttpResponse:
# For the mobile Oauth flow, we send the API key and other
# necessary details in a redirect to a zulip:// URI scheme.
api_key = get_api_key(user_profile)
response = create_response_for_otp_flow(api_key, otp, user_profile)
response = create_response_for_otp_flow(api_key, otp, user_profile,
encrypted_key_field_name='otp_encrypted_api_key')
# Since we are returning an API key instead of going through
# the Django login() function (which creates a browser
@@ -257,9 +280,10 @@ def finish_mobile_flow(request: HttpRequest, user_profile: UserProfile, otp: str
return response
def create_response_for_otp_flow(key: str, otp: str, user_profile: UserProfile) -> HttpResponse:
def create_response_for_otp_flow(key: str, otp: str, user_profile: UserProfile,
encrypted_key_field_name: str) -> HttpResponse:
params = {
'otp_encrypted_api_key': otp_encrypt_api_key(key, otp),
encrypted_key_field_name: otp_encrypt_api_key(key, otp),
'email': user_profile.delivery_email,
'realm': user_profile.realm.uri,
}
@@ -372,6 +396,12 @@ def oauth_redirect_to_root(request: HttpRequest, url: str,
raise JsonableError(_("Invalid OTP"))
params['mobile_flow_otp'] = mobile_flow_otp
desktop_flow_otp = request.GET.get('desktop_flow_otp')
if desktop_flow_otp is not None:
if not is_valid_otp(desktop_flow_otp):
raise JsonableError(_("Invalid OTP"))
params['desktop_flow_otp'] = desktop_flow_otp
next = request.GET.get('next')
if next:
params['next'] = next
@@ -451,6 +481,10 @@ def log_into_subdomain(request: HttpRequest, token: str) -> HttpResponse:
redirect_and_log_into_subdomain called on auth.zulip.example.com),
call login_or_register_remote_user, passing all the authentication
result data that has been stored in redis, associated with this token.
Obligatory fields for the data are 'subdomain' and 'email', because this endpoint
needs to know which user and realm to log into. Others are optional and only used
if the user account still needs to be made and they're passed as argument to the
register_remote_user function.
"""
if not has_api_key_format(token): # The tokens are intended to have the same format as API keys.
logging.warning("log_into_subdomain: Malformed token given: %s" % (token,))