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

Commit

Permalink
Merge pull request #9091 from matrix-org/rav/error_on_bad_sso
Browse files Browse the repository at this point in the history
Give the user a better error when they present bad SSO creds
  • Loading branch information
richvdh authored Jan 15, 2021
2 parents 1a08e0c + 26d1033 commit 14950a4
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 31 deletions.
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)

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>
</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}}
)

0 comments on commit 14950a4

Please sign in to comment.