Do not allow a deactivated user to login via SSO. (#7240)

This commit is contained in:
Patrick Cloke 2020-04-09 13:28:13 -04:00 committed by GitHub
parent 967f99b9f8
commit b85d7652ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 110 additions and 10 deletions

1
changelog.d/7240.bugfix Normal file
View File

@ -0,0 +1 @@
Do not allow a deactivated user to login via SSO.

View File

@ -12,6 +12,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import os
from typing import Any, Dict from typing import Any, Dict
import pkg_resources import pkg_resources
@ -36,6 +37,12 @@ class SSOConfig(Config):
template_dir = pkg_resources.resource_filename("synapse", "res/templates",) template_dir = pkg_resources.resource_filename("synapse", "res/templates",)
self.sso_redirect_confirm_template_dir = template_dir self.sso_redirect_confirm_template_dir = template_dir
self.sso_account_deactivated_template = self.read_file(
os.path.join(
self.sso_redirect_confirm_template_dir, "sso_account_deactivated.html"
),
"sso_account_deactivated_template",
)
self.sso_client_whitelist = sso_config.get("client_whitelist") or [] self.sso_client_whitelist = sso_config.get("client_whitelist") or []

View File

@ -161,6 +161,9 @@ class AuthHandler(BaseHandler):
self._sso_auth_confirm_template = load_jinja2_templates( self._sso_auth_confirm_template = load_jinja2_templates(
hs.config.sso_redirect_confirm_template_dir, ["sso_auth_confirm.html"], hs.config.sso_redirect_confirm_template_dir, ["sso_auth_confirm.html"],
)[0] )[0]
self._sso_account_deactivated_template = (
hs.config.sso_account_deactivated_template
)
self._server_name = hs.config.server_name self._server_name = hs.config.server_name
@ -644,9 +647,6 @@ class AuthHandler(BaseHandler):
Returns: Returns:
defer.Deferred: (unicode) canonical_user_id, or None if zero or defer.Deferred: (unicode) canonical_user_id, or None if zero or
multiple matches multiple matches
Raises:
UserDeactivatedError if a user is found but is deactivated.
""" """
res = yield self._find_user_id_and_pwd_hash(user_id) res = yield self._find_user_id_and_pwd_hash(user_id)
if res is not None: if res is not None:
@ -1099,7 +1099,7 @@ class AuthHandler(BaseHandler):
request.write(html_bytes) request.write(html_bytes)
finish_request(request) finish_request(request)
def complete_sso_login( async def complete_sso_login(
self, self,
registered_user_id: str, registered_user_id: str,
request: SynapseRequest, request: SynapseRequest,
@ -1113,6 +1113,32 @@ class AuthHandler(BaseHandler):
client_redirect_url: The URL to which to redirect the user at the end of the client_redirect_url: The URL to which to redirect the user at the end of the
process. process.
""" """
# If the account has been deactivated, do not proceed with the login
# flow.
deactivated = await self.store.get_user_deactivated_status(registered_user_id)
if deactivated:
html = self._sso_account_deactivated_template.encode("utf-8")
request.setResponseCode(403)
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
request.setHeader(b"Content-Length", b"%d" % (len(html),))
request.write(html)
finish_request(request)
return
self._complete_sso_login(registered_user_id, request, client_redirect_url)
def _complete_sso_login(
self,
registered_user_id: str,
request: SynapseRequest,
client_redirect_url: str,
):
"""
The synchronous portion of complete_sso_login.
This exists purely for backwards compatibility of synapse.module_api.ModuleApi.
"""
# Create a login token # Create a login token
login_token = self.macaroon_gen.generate_short_term_login_token( login_token = self.macaroon_gen.generate_short_term_login_token(
registered_user_id registered_user_id

View File

@ -216,6 +216,6 @@ class CasHandler:
localpart=localpart, default_display_name=user_display_name localpart=localpart, default_display_name=user_display_name
) )
self._auth_handler.complete_sso_login( await self._auth_handler.complete_sso_login(
registered_user_id, request, client_redirect_url registered_user_id, request, client_redirect_url
) )

View File

@ -154,7 +154,7 @@ class SamlHandler:
) )
else: else:
self._auth_handler.complete_sso_login(user_id, request, relay_state) await self._auth_handler.complete_sso_login(user_id, request, relay_state)
async def _map_saml_response_to_user( async def _map_saml_response_to_user(
self, resp_bytes: str, client_redirect_url: str self, resp_bytes: str, client_redirect_url: str

View File

@ -220,6 +220,8 @@ class ModuleApi(object):
want their access token sent to `client_redirect_url`, or redirect them to that want their access token sent to `client_redirect_url`, or redirect them to that
URL with a token directly if the URL matches with one of the whitelisted clients. URL with a token directly if the URL matches with one of the whitelisted clients.
This is deprecated in favor of complete_sso_login_async.
Args: Args:
registered_user_id: The MXID that has been registered as a previous step of registered_user_id: The MXID that has been registered as a previous step of
of this SSO login. of this SSO login.
@ -227,6 +229,24 @@ class ModuleApi(object):
client_redirect_url: The URL to which to offer to redirect the user (or to client_redirect_url: The URL to which to offer to redirect the user (or to
redirect them directly if whitelisted). redirect them directly if whitelisted).
""" """
self._auth_handler.complete_sso_login( self._auth_handler._complete_sso_login(
registered_user_id, request, client_redirect_url,
)
async def complete_sso_login_async(
self, registered_user_id: str, request: SynapseRequest, client_redirect_url: str
):
"""Complete a SSO login by redirecting the user to a page to confirm whether they
want their access token sent to `client_redirect_url`, or redirect them to that
URL with a token directly if the URL matches with one of the whitelisted clients.
Args:
registered_user_id: The MXID that has been registered as a previous step of
of this SSO login.
request: The request to respond to.
client_redirect_url: The URL to which to offer to redirect the user (or to
redirect them directly if whitelisted).
"""
await self._auth_handler.complete_sso_login(
registered_user_id, request, client_redirect_url, registered_user_id, request, client_redirect_url,
) )

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SSO account deactivated</title>
</head>
<body>
<p>This account has been deactivated.</p>
</body>
</html>

View File

@ -257,7 +257,7 @@ class LoginRestServletTestCase(unittest.HomeserverTestCase):
self.assertEquals(channel.code, 200, channel.result) self.assertEquals(channel.code, 200, channel.result)
class CASRedirectConfirmTestCase(unittest.HomeserverTestCase): class CASTestCase(unittest.HomeserverTestCase):
servlets = [ servlets = [
login.register_servlets, login.register_servlets,
@ -274,6 +274,9 @@ class CASRedirectConfirmTestCase(unittest.HomeserverTestCase):
"service_url": "https://matrix.goodserver.com:8448", "service_url": "https://matrix.goodserver.com:8448",
} }
cas_user_id = "username"
self.user_id = "@%s:test" % cas_user_id
async def get_raw(uri, args): async def get_raw(uri, args):
"""Return an example response payload from a call to the `/proxyValidate` """Return an example response payload from a call to the `/proxyValidate`
endpoint of a CAS server, copied from endpoint of a CAS server, copied from
@ -282,10 +285,11 @@ class CASRedirectConfirmTestCase(unittest.HomeserverTestCase):
This needs to be returned by an async function (as opposed to set as the This needs to be returned by an async function (as opposed to set as the
mock's return value) because the corresponding Synapse code awaits on it. mock's return value) because the corresponding Synapse code awaits on it.
""" """
return """ return (
"""
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'> <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationSuccess> <cas:authenticationSuccess>
<cas:user>username</cas:user> <cas:user>%s</cas:user>
<cas:proxyGrantingTicket>PGTIOU-84678-8a9d...</cas:proxyGrantingTicket> <cas:proxyGrantingTicket>PGTIOU-84678-8a9d...</cas:proxyGrantingTicket>
<cas:proxies> <cas:proxies>
<cas:proxy>https://proxy2/pgtUrl</cas:proxy> <cas:proxy>https://proxy2/pgtUrl</cas:proxy>
@ -294,6 +298,8 @@ class CASRedirectConfirmTestCase(unittest.HomeserverTestCase):
</cas:authenticationSuccess> </cas:authenticationSuccess>
</cas:serviceResponse> </cas:serviceResponse>
""" """
% cas_user_id
)
mocked_http_client = Mock(spec=["get_raw"]) mocked_http_client = Mock(spec=["get_raw"])
mocked_http_client.get_raw.side_effect = get_raw mocked_http_client.get_raw.side_effect = get_raw
@ -304,6 +310,9 @@ class CASRedirectConfirmTestCase(unittest.HomeserverTestCase):
return self.hs return self.hs
def prepare(self, reactor, clock, hs):
self.deactivate_account_handler = hs.get_deactivate_account_handler()
def test_cas_redirect_confirm(self): def test_cas_redirect_confirm(self):
"""Tests that the SSO login flow serves a confirmation page before redirecting a """Tests that the SSO login flow serves a confirmation page before redirecting a
user to the redirect URL. user to the redirect URL.
@ -370,3 +379,30 @@ class CASRedirectConfirmTestCase(unittest.HomeserverTestCase):
self.assertEqual(channel.code, 302) self.assertEqual(channel.code, 302)
location_headers = channel.headers.getRawHeaders("Location") location_headers = channel.headers.getRawHeaders("Location")
self.assertEqual(location_headers[0][: len(redirect_url)], redirect_url) self.assertEqual(location_headers[0][: len(redirect_url)], redirect_url)
@override_config({"sso": {"client_whitelist": ["https://legit-site.com/"]}})
def test_deactivated_user(self):
"""Logging in as a deactivated account should error."""
redirect_url = "https://legit-site.com/"
# First login (to create the user).
self._test_redirect(redirect_url)
# Deactivate the account.
self.get_success(
self.deactivate_account_handler.deactivate_account(self.user_id, False)
)
# Request the CAS ticket.
cas_ticket_url = (
"/_matrix/client/r0/login/cas/ticket?redirectUrl=%s&ticket=ticket"
% (urllib.parse.quote(redirect_url))
)
# Get Synapse to call the fake CAS and serve the template.
request, channel = self.make_request("GET", cas_ticket_url)
self.render(request)
# Because the user is deactivated they are served an error template.
self.assertEqual(channel.code, 403)
self.assertIn(b"SSO account deactivated", channel.result["body"])