mirror of https://github.com/zulip/zulip.git
saml: Make SP-initiated SLO work in the desktop application.
This commit is contained in:
parent
3f55c10685
commit
5dd4dcdebb
|
@ -712,8 +712,9 @@ Logout endpoint with a `LogoutRequest`. If a successful
|
|||
`LogoutResponse` is received back, their current Zulip session will be
|
||||
terminated.
|
||||
|
||||
Note that this doesn't work in the case of desktop and mobile application
|
||||
and is reserved to the browser.
|
||||
Note that this doesn't work when logging out of the mobile application
|
||||
since the app doesn't use sessions and relies on just having the user's
|
||||
API key.
|
||||
|
||||
#### Caveats
|
||||
|
||||
|
|
|
@ -2125,6 +2125,34 @@ class SAMLAuthBackendTest(SocialAuthBase):
|
|||
self.client_get(result["Location"])
|
||||
self.assert_logged_in_user_id(None)
|
||||
|
||||
def test_saml_sp_initiated_logout_desktop_flow(self) -> None:
|
||||
desktop_flow_otp = "1234abcd" * 8
|
||||
account_data_dict = self.get_account_data_dict(email=self.email, name=self.name)
|
||||
with self.assertLogs(self.logger_string, level="INFO"):
|
||||
result = self.social_auth_test(
|
||||
account_data_dict,
|
||||
subdomain="zulip",
|
||||
expect_choose_email_screen=False,
|
||||
desktop_flow_otp=desktop_flow_otp,
|
||||
)
|
||||
# Flush the session to have a clean slate - since up till now
|
||||
# we were simulating the part of the flow that happens in the browser.
|
||||
# Now we will simulating the last part of the flow, which gets executed
|
||||
# in the desktop application - thus with a separate session.
|
||||
self.client.session.flush()
|
||||
self.verify_desktop_flow_end_page(result, self.email, desktop_flow_otp)
|
||||
|
||||
# Now we have a desktop app logged in session. Verify that the logout
|
||||
# request will trigger the SAML SLO flow - it should if we correctly
|
||||
# plumbed through the information about the SAML authentication to the
|
||||
# session in the app.
|
||||
result = self.client_post("/accounts/logout/")
|
||||
# A redirect to the IdP is returned.
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertIn(
|
||||
settings.SOCIAL_AUTH_SAML_ENABLED_IDPS["test_idp"]["slo_url"], result["Location"]
|
||||
)
|
||||
|
||||
def test_saml_sp_initiated_logout_invalid_logoutresponse(self) -> None:
|
||||
hamlet = self.example_user("hamlet")
|
||||
self.login("hamlet")
|
||||
|
|
|
@ -347,7 +347,17 @@ def login_or_register_remote_user(request: HttpRequest, result: ExternalAuthResu
|
|||
are doing authentication using the mobile_flow_otp or desktop_flow_otp flow.
|
||||
"""
|
||||
|
||||
for key, value in result.data_dict.get("params_to_store_in_authenticated_session", {}).items():
|
||||
params_to_store_in_authenticated_session = result.data_dict.get(
|
||||
"params_to_store_in_authenticated_session", {}
|
||||
)
|
||||
mobile_flow_otp = result.data_dict.get("mobile_flow_otp")
|
||||
desktop_flow_otp = result.data_dict.get("desktop_flow_otp")
|
||||
if not mobile_flow_otp and not desktop_flow_otp:
|
||||
# We don't want to store anything in the browser session if we're doing
|
||||
# mobile or desktop flows, since that's just an intermediary step and the
|
||||
# browser session is not to be used any further. Storing extra data in
|
||||
# it just risks bugs or leaking the data.
|
||||
for key, value in params_to_store_in_authenticated_session.items():
|
||||
request.session[key] = value
|
||||
|
||||
user_profile = result.user_profile
|
||||
|
@ -357,12 +367,12 @@ def login_or_register_remote_user(request: HttpRequest, result: ExternalAuthResu
|
|||
# account, and we need to do the right thing depending whether
|
||||
# or not they're using the mobile OTP flow or want a browser session.
|
||||
is_realm_creation = result.data_dict.get("is_realm_creation")
|
||||
mobile_flow_otp = result.data_dict.get("mobile_flow_otp")
|
||||
desktop_flow_otp = result.data_dict.get("desktop_flow_otp")
|
||||
if mobile_flow_otp is not None:
|
||||
return finish_mobile_flow(request, user_profile, mobile_flow_otp)
|
||||
elif desktop_flow_otp is not None:
|
||||
return finish_desktop_flow(request, user_profile, desktop_flow_otp)
|
||||
return finish_desktop_flow(
|
||||
request, user_profile, desktop_flow_otp, params_to_store_in_authenticated_session
|
||||
)
|
||||
|
||||
do_login(request, user_profile)
|
||||
|
||||
|
@ -377,7 +387,12 @@ def login_or_register_remote_user(request: HttpRequest, result: ExternalAuthResu
|
|||
return HttpResponseRedirect(redirect_to)
|
||||
|
||||
|
||||
def finish_desktop_flow(request: HttpRequest, user_profile: UserProfile, otp: str) -> HttpResponse:
|
||||
def finish_desktop_flow(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
otp: str,
|
||||
params_to_store_in_authenticated_session: Optional[Dict[str, str]] = None,
|
||||
) -> HttpResponse:
|
||||
"""
|
||||
The desktop otp flow returns to the app (through the clipboard)
|
||||
a token that allows obtaining (through log_into_subdomain) a logged in session
|
||||
|
@ -386,7 +401,14 @@ def finish_desktop_flow(request: HttpRequest, user_profile: UserProfile, otp: st
|
|||
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.
|
||||
"""
|
||||
result = ExternalAuthResult(user_profile=user_profile)
|
||||
data_dict = None
|
||||
if params_to_store_in_authenticated_session:
|
||||
data_dict = ExternalAuthDataDict(
|
||||
params_to_store_in_authenticated_session=params_to_store_in_authenticated_session
|
||||
)
|
||||
|
||||
result = ExternalAuthResult(user_profile=user_profile, data_dict=data_dict)
|
||||
|
||||
token = result.store_data()
|
||||
key = bytes.fromhex(otp)
|
||||
iv = secrets.token_bytes(12)
|
||||
|
|
|
@ -1306,10 +1306,8 @@ class ExternalAuthDataDict(TypedDict, total=False):
|
|||
desktop_flow_otp: Optional[str]
|
||||
multiuse_object_key: str
|
||||
full_name_validated: bool
|
||||
# TODO: This currently does not get plumbed through to the final session
|
||||
# in the desktop flow, but it should.
|
||||
# Also, the mobile app doesn't actually use a session, so this
|
||||
# data is not applicable there either.
|
||||
# The mobile app doesn't actually use a session, so this
|
||||
# data is not applicable there.
|
||||
params_to_store_in_authenticated_session: Dict[str, str]
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue