Merge pull request #9091 from matrix-org/rav/error_on_bad_sso
Give the user a better error when they present bad SSO creds
This commit is contained in:
commit
14950a45d6
|
@ -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.
|
|
@ -1969,6 +1969,14 @@ sso:
|
||||||
#
|
#
|
||||||
# This template has no additional variables.
|
# 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)
|
# * HTML page shown during single sign-on if a deactivated user (according to Synapse's database)
|
||||||
# attempts to login: 'sso_account_deactivated.html'.
|
# attempts to login: 'sso_account_deactivated.html'.
|
||||||
#
|
#
|
||||||
|
|
|
@ -37,6 +37,7 @@ class SSOConfig(Config):
|
||||||
self.sso_error_template,
|
self.sso_error_template,
|
||||||
sso_account_deactivated_template,
|
sso_account_deactivated_template,
|
||||||
sso_auth_success_template,
|
sso_auth_success_template,
|
||||||
|
self.sso_auth_bad_user_template,
|
||||||
) = self.read_templates(
|
) = self.read_templates(
|
||||||
[
|
[
|
||||||
"sso_login_idp_picker.html",
|
"sso_login_idp_picker.html",
|
||||||
|
@ -45,6 +46,7 @@ class SSOConfig(Config):
|
||||||
"sso_error.html",
|
"sso_error.html",
|
||||||
"sso_account_deactivated.html",
|
"sso_account_deactivated.html",
|
||||||
"sso_auth_success.html",
|
"sso_auth_success.html",
|
||||||
|
"sso_auth_bad_user.html",
|
||||||
],
|
],
|
||||||
template_dir,
|
template_dir,
|
||||||
)
|
)
|
||||||
|
@ -160,6 +162,14 @@ class SSOConfig(Config):
|
||||||
#
|
#
|
||||||
# This template has no additional variables.
|
# 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)
|
# * HTML page shown during single sign-on if a deactivated user (according to Synapse's database)
|
||||||
# attempts to login: 'sso_account_deactivated.html'.
|
# attempts to login: 'sso_account_deactivated.html'.
|
||||||
#
|
#
|
||||||
|
|
|
@ -263,10 +263,6 @@ class AuthHandler(BaseHandler):
|
||||||
# authenticating for an operation to occur on their account.
|
# authenticating for an operation to occur on their account.
|
||||||
self._sso_auth_confirm_template = hs.config.sso_auth_confirm_template
|
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 following template is shown during the SSO authentication process if
|
||||||
# the account is deactivated.
|
# the account is deactivated.
|
||||||
self._sso_account_deactivated_template = (
|
self._sso_account_deactivated_template = (
|
||||||
|
@ -1394,27 +1390,6 @@ class AuthHandler(BaseHandler):
|
||||||
description=session.description, redirect_url=redirect_url,
|
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(
|
async def complete_sso_login(
|
||||||
self,
|
self,
|
||||||
registered_user_id: str,
|
registered_user_id: str,
|
||||||
|
|
|
@ -22,7 +22,9 @@ from typing_extensions import NoReturn, Protocol
|
||||||
|
|
||||||
from twisted.web.http import Request
|
from twisted.web.http import Request
|
||||||
|
|
||||||
|
from synapse.api.constants import LoginType
|
||||||
from synapse.api.errors import Codes, RedirectException, SynapseError
|
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 import get_request_user_agent
|
||||||
from synapse.http.server import respond_with_html
|
from synapse.http.server import respond_with_html
|
||||||
from synapse.http.site import SynapseRequest
|
from synapse.http.site import SynapseRequest
|
||||||
|
@ -146,8 +148,13 @@ class SsoHandler:
|
||||||
self._store = hs.get_datastore()
|
self._store = hs.get_datastore()
|
||||||
self._server_name = hs.hostname
|
self._server_name = hs.hostname
|
||||||
self._registration_handler = hs.get_registration_handler()
|
self._registration_handler = hs.get_registration_handler()
|
||||||
self._error_template = hs.config.sso_error_template
|
|
||||||
self._auth_handler = hs.get_auth_handler()
|
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
|
# a lock on the mappings
|
||||||
self._mapping_lock = Linearizer(name="sso_user_mapping", clock=hs.get_clock())
|
self._mapping_lock = Linearizer(name="sso_user_mapping", clock=hs.get_clock())
|
||||||
|
@ -577,19 +584,45 @@ class SsoHandler:
|
||||||
auth_provider_id, remote_user_id,
|
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:
|
if not user_id:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Remote user %s/%s has not previously logged in here: UIA will fail",
|
"Remote user %s/%s has not previously logged in here: UIA will fail",
|
||||||
auth_provider_id,
|
auth_provider_id,
|
||||||
remote_user_id,
|
remote_user_id,
|
||||||
)
|
)
|
||||||
# Let the UIA flow handle this the same as if they presented creds for a
|
elif user_id != user_id_to_verify:
|
||||||
# different user.
|
logger.warning(
|
||||||
user_id = ""
|
"Remote user %s/%s mapped onto incorrect user %s: UIA will fail",
|
||||||
|
auth_provider_id,
|
||||||
await self._auth_handler.complete_sso_ui_auth(
|
remote_user_id,
|
||||||
user_id, ui_auth_session_id, request
|
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, ""
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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(
|
async def check_username_availability(
|
||||||
self, localpart: str, session_id: str,
|
self, localpart: str, session_id: str,
|
||||||
|
|
|
@ -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>
|
|
@ -457,3 +457,30 @@ class UIAuthTests(unittest.HomeserverTestCase):
|
||||||
self.assertIn({"stages": ["m.login.password"]}, flows)
|
self.assertIn({"stages": ["m.login.password"]}, flows)
|
||||||
self.assertIn({"stages": ["m.login.sso"]}, flows)
|
self.assertIn({"stages": ["m.login.sso"]}, flows)
|
||||||
self.assertEqual(len(flows), 2)
|
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}}
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue