Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Give the user a better error when they present bad SSO creds #9091

Merged
merged 3 commits into from
Jan 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/9091.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
During user-interactive authentication via single-sign-on, give a better error if the user uses the wrong account on the SSO IdP.
8 changes: 8 additions & 0 deletions docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1969,6 +1969,14 @@ sso:
#
# This template has no additional variables.
#
# * HTML page shown after a user-interactive authentication session which
# does not map correctly onto the expected user: 'sso_auth_bad_user.html'.
#
# When rendering, this template is given the following variables:
# * server_name: the homeserver's name.
# * user_id_to_verify: the MXID of the user that we are trying to
# validate.
#
# * HTML page shown during single sign-on if a deactivated user (according to Synapse's database)
# attempts to login: 'sso_account_deactivated.html'.
#
Expand Down
10 changes: 10 additions & 0 deletions synapse/config/sso.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def read_config(self, config, **kwargs):
self.sso_error_template,
sso_account_deactivated_template,
sso_auth_success_template,
self.sso_auth_bad_user_template,
) = self.read_templates(
[
"sso_login_idp_picker.html",
Expand All @@ -45,6 +46,7 @@ def read_config(self, config, **kwargs):
"sso_error.html",
"sso_account_deactivated.html",
"sso_auth_success.html",
"sso_auth_bad_user.html",
],
template_dir,
)
Expand Down Expand Up @@ -160,6 +162,14 @@ def generate_config_section(self, **kwargs):
#
# This template has no additional variables.
#
# * HTML page shown after a user-interactive authentication session which
# does not map correctly onto the expected user: 'sso_auth_bad_user.html'.
#
# When rendering, this template is given the following variables:
# * server_name: the homeserver's name.
# * user_id_to_verify: the MXID of the user that we are trying to
# validate.
#
# * HTML page shown during single sign-on if a deactivated user (according to Synapse's database)
# attempts to login: 'sso_account_deactivated.html'.
#
Expand Down
25 changes: 0 additions & 25 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,6 @@ def __init__(self, hs: "HomeServer"):
# authenticating for an operation to occur on their account.
self._sso_auth_confirm_template = hs.config.sso_auth_confirm_template

# The following template is shown after a successful user interactive
# authentication session. It tells the user they can close the window.
self._sso_auth_success_template = hs.config.sso_auth_success_template

# The following template is shown during the SSO authentication process if
# the account is deactivated.
self._sso_account_deactivated_template = (
Expand Down Expand Up @@ -1394,27 +1390,6 @@ async def start_sso_ui_auth(self, request: SynapseRequest, session_id: str) -> s
description=session.description, redirect_url=redirect_url,
)

async def complete_sso_ui_auth(
self, registered_user_id: str, session_id: str, request: Request,
):
"""Having figured out a mxid for this user, complete the HTTP request

Args:
registered_user_id: The registered user ID to complete SSO login for.
session_id: The ID of the user-interactive auth session.
request: The request to complete.
"""
# Mark the stage of the authentication as successful.
# Save the user who authenticated with SSO, this will be used to ensure
# that the account be modified is also the person who logged in.
await self.store.mark_ui_auth_stage_complete(
session_id, LoginType.SSO, registered_user_id
)

# Render the HTML and return.
html = self._sso_auth_success_template
respond_with_html(request, 200, html)

async def complete_sso_login(
self,
registered_user_id: str,
Expand Down
45 changes: 39 additions & 6 deletions synapse/handlers/sso.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@

from twisted.web.http import Request

from synapse.api.constants import LoginType
from synapse.api.errors import Codes, RedirectException, SynapseError
from synapse.handlers.ui_auth import UIAuthSessionDataConstants
from synapse.http import get_request_user_agent
from synapse.http.server import respond_with_html
from synapse.http.site import SynapseRequest
Expand Down Expand Up @@ -146,8 +148,13 @@ def __init__(self, hs: "HomeServer"):
self._store = hs.get_datastore()
self._server_name = hs.hostname
self._registration_handler = hs.get_registration_handler()
self._error_template = hs.config.sso_error_template
self._auth_handler = hs.get_auth_handler()
self._error_template = hs.config.sso_error_template
self._bad_user_template = hs.config.sso_auth_bad_user_template

# The following template is shown after a successful user interactive
# authentication session. It tells the user they can close the window.
self._sso_auth_success_template = hs.config.sso_auth_success_template

# a lock on the mappings
self._mapping_lock = Linearizer(name="sso_user_mapping", clock=hs.get_clock())
Expand Down Expand Up @@ -577,19 +584,45 @@ async def complete_sso_ui_auth_request(
auth_provider_id, remote_user_id,
)

user_id_to_verify = await self._auth_handler.get_session_data(
ui_auth_session_id, UIAuthSessionDataConstants.REQUEST_USER_ID
) # type: str

if not user_id:
logger.warning(
"Remote user %s/%s has not previously logged in here: UIA will fail",
auth_provider_id,
remote_user_id,
)
# Let the UIA flow handle this the same as if they presented creds for a
# different user.
user_id = ""
elif user_id != user_id_to_verify:
logger.warning(
"Remote user %s/%s mapped onto incorrect user %s: UIA will fail",
auth_provider_id,
remote_user_id,
user_id,
)
else:
# success!
# Mark the stage of the authentication as successful.
await self._store.mark_ui_auth_stage_complete(
ui_auth_session_id, LoginType.SSO, user_id
)

# Render the HTML confirmation page and return.
html = self._sso_auth_success_template
respond_with_html(request, 200, html)
return

# the user_id didn't match: mark the stage of the authentication as unsuccessful
await self._store.mark_ui_auth_stage_complete(
ui_auth_session_id, LoginType.SSO, ""
)

await self._auth_handler.complete_sso_ui_auth(
user_id, ui_auth_session_id, request
# render an error page.
html = self._bad_user_template.render(
server_name=self._server_name, user_id_to_verify=user_id_to_verify,
)
respond_with_html(request, 200, html)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wasn't quite sure if 200 was the right thing here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can make it a 400, but it shouldn't really matter since it will just be shown in a browser? I guess it matters how we want it to show up in the logs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well right...


async def check_username_availability(
self, localpart: str, session_id: str,
Expand Down
18 changes: 18 additions & 0 deletions synapse/res/templates/sso_auth_bad_user.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<html>
<head>
<title>Authentication Failed</title>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part of me wonders if this really needs to be a separate template or if we can use the sso_error.html template?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that to start with, but the phrasing of the sso_error.html template is very much geared to server-side or integration problems. This particular failure is really a user error, so the generic error wording didn't seem to fit.

</head>
<body>
<div>
<p>
We were unable to validate your <tt>{{server_name | e}}</tt> account via
single-sign-on (SSO), because the SSO Identity Provider returned
different details than when you logged in.
</p>
<p>
Try the operation again, and ensure that you use the same details on
the Identity Provider as when you log into your account.
</p>
</div>
</body>
</html>
27 changes: 27 additions & 0 deletions tests/rest/client/v2_alpha/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,3 +457,30 @@ def test_offers_both_flows_for_upgraded_user(self):
self.assertIn({"stages": ["m.login.password"]}, flows)
self.assertIn({"stages": ["m.login.sso"]}, flows)
self.assertEqual(len(flows), 2)

@skip_unless(HAS_OIDC, "requires OIDC")
@override_config({"oidc_config": TEST_OIDC_CONFIG})
def test_ui_auth_fails_for_incorrect_sso_user(self):
"""If the user tries to authenticate with the wrong SSO user, they get an error
"""
# log the user in
login_resp = self.helper.login_via_oidc(UserID.from_string(self.user).localpart)
self.assertEqual(login_resp["user_id"], self.user)

# start a UI Auth flow by attempting to delete a device
channel = self.delete_device(self.user_tok, self.device_id, 401)

flows = channel.json_body["flows"]
self.assertIn({"stages": ["m.login.sso"]}, flows)
session_id = channel.json_body["session"]

# do the OIDC auth, but auth as the wrong user
channel = self.helper.auth_via_oidc("wrong_user", ui_auth_session_id=session_id)

# that should return a failure message
self.assertSubstring("We were unable to validate", channel.text_body)

# ... and the delete op should now fail with a 403
self.delete_device(
self.user_tok, self.device_id, 403, body={"auth": {"session": session_id}}
)