Skip to content

Commit aa275ad

Browse files
clokepphil-flex
authored andcommitted
Do not allow a deactivated user to login via SSO. (matrix-org#7240)
1 parent 0d35d0b commit aa275ad

File tree

8 files changed

+110
-10
lines changed

8 files changed

+110
-10
lines changed

changelog.d/7240.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Do not allow a deactivated user to login via SSO.

synapse/config/sso.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
15+
import os
1516
from typing import Any, Dict
1617

1718
import pkg_resources
@@ -36,6 +37,12 @@ def read_config(self, config, **kwargs):
3637
template_dir = pkg_resources.resource_filename("synapse", "res/templates",)
3738

3839
self.sso_redirect_confirm_template_dir = template_dir
40+
self.sso_account_deactivated_template = self.read_file(
41+
os.path.join(
42+
self.sso_redirect_confirm_template_dir, "sso_account_deactivated.html"
43+
),
44+
"sso_account_deactivated_template",
45+
)
3946

4047
self.sso_client_whitelist = sso_config.get("client_whitelist") or []
4148

synapse/handlers/auth.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ def __init__(self, hs):
161161
self._sso_auth_confirm_template = load_jinja2_templates(
162162
hs.config.sso_redirect_confirm_template_dir, ["sso_auth_confirm.html"],
163163
)[0]
164+
self._sso_account_deactivated_template = (
165+
hs.config.sso_account_deactivated_template
166+
)
164167

165168
self._server_name = hs.config.server_name
166169

@@ -644,9 +647,6 @@ def check_user_exists(self, user_id: str):
644647
Returns:
645648
defer.Deferred: (unicode) canonical_user_id, or None if zero or
646649
multiple matches
647-
648-
Raises:
649-
UserDeactivatedError if a user is found but is deactivated.
650650
"""
651651
res = yield self._find_user_id_and_pwd_hash(user_id)
652652
if res is not None:
@@ -1099,7 +1099,7 @@ def complete_sso_ui_auth(
10991099
request.write(html_bytes)
11001100
finish_request(request)
11011101

1102-
def complete_sso_login(
1102+
async def complete_sso_login(
11031103
self,
11041104
registered_user_id: str,
11051105
request: SynapseRequest,
@@ -1113,6 +1113,32 @@ def complete_sso_login(
11131113
client_redirect_url: The URL to which to redirect the user at the end of the
11141114
process.
11151115
"""
1116+
# If the account has been deactivated, do not proceed with the login
1117+
# flow.
1118+
deactivated = await self.store.get_user_deactivated_status(registered_user_id)
1119+
if deactivated:
1120+
html = self._sso_account_deactivated_template.encode("utf-8")
1121+
1122+
request.setResponseCode(403)
1123+
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
1124+
request.setHeader(b"Content-Length", b"%d" % (len(html),))
1125+
request.write(html)
1126+
finish_request(request)
1127+
return
1128+
1129+
self._complete_sso_login(registered_user_id, request, client_redirect_url)
1130+
1131+
def _complete_sso_login(
1132+
self,
1133+
registered_user_id: str,
1134+
request: SynapseRequest,
1135+
client_redirect_url: str,
1136+
):
1137+
"""
1138+
The synchronous portion of complete_sso_login.
1139+
1140+
This exists purely for backwards compatibility of synapse.module_api.ModuleApi.
1141+
"""
11161142
# Create a login token
11171143
login_token = self.macaroon_gen.generate_short_term_login_token(
11181144
registered_user_id

synapse/handlers/cas_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,6 @@ async def handle_ticket(
216216
localpart=localpart, default_display_name=user_display_name
217217
)
218218

219-
self._auth_handler.complete_sso_login(
219+
await self._auth_handler.complete_sso_login(
220220
registered_user_id, request, client_redirect_url
221221
)

synapse/handlers/saml_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ async def handle_saml_response(self, request):
154154
)
155155

156156
else:
157-
self._auth_handler.complete_sso_login(user_id, request, relay_state)
157+
await self._auth_handler.complete_sso_login(user_id, request, relay_state)
158158

159159
async def _map_saml_response_to_user(
160160
self, resp_bytes: str, client_redirect_url: str

synapse/module_api/__init__.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,13 +220,33 @@ def complete_sso_login(
220220
want their access token sent to `client_redirect_url`, or redirect them to that
221221
URL with a token directly if the URL matches with one of the whitelisted clients.
222222
223+
This is deprecated in favor of complete_sso_login_async.
224+
225+
Args:
226+
registered_user_id: The MXID that has been registered as a previous step of
227+
of this SSO login.
228+
request: The request to respond to.
229+
client_redirect_url: The URL to which to offer to redirect the user (or to
230+
redirect them directly if whitelisted).
231+
"""
232+
self._auth_handler._complete_sso_login(
233+
registered_user_id, request, client_redirect_url,
234+
)
235+
236+
async def complete_sso_login_async(
237+
self, registered_user_id: str, request: SynapseRequest, client_redirect_url: str
238+
):
239+
"""Complete a SSO login by redirecting the user to a page to confirm whether they
240+
want their access token sent to `client_redirect_url`, or redirect them to that
241+
URL with a token directly if the URL matches with one of the whitelisted clients.
242+
223243
Args:
224244
registered_user_id: The MXID that has been registered as a previous step of
225245
of this SSO login.
226246
request: The request to respond to.
227247
client_redirect_url: The URL to which to offer to redirect the user (or to
228248
redirect them directly if whitelisted).
229249
"""
230-
self._auth_handler.complete_sso_login(
250+
await self._auth_handler.complete_sso_login(
231251
registered_user_id, request, client_redirect_url,
232252
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>SSO account deactivated</title>
6+
</head>
7+
<body>
8+
<p>This account has been deactivated.</p>
9+
</body>
10+
</html>

tests/rest/client/v1/test_login.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ def _delete_device(self, access_token, user_id, password, device_id):
257257
self.assertEquals(channel.code, 200, channel.result)
258258

259259

260-
class CASRedirectConfirmTestCase(unittest.HomeserverTestCase):
260+
class CASTestCase(unittest.HomeserverTestCase):
261261

262262
servlets = [
263263
login.register_servlets,
@@ -274,6 +274,9 @@ def make_homeserver(self, reactor, clock):
274274
"service_url": "https://matrix.goodserver.com:8448",
275275
}
276276

277+
cas_user_id = "username"
278+
self.user_id = "@%s:test" % cas_user_id
279+
277280
async def get_raw(uri, args):
278281
"""Return an example response payload from a call to the `/proxyValidate`
279282
endpoint of a CAS server, copied from
@@ -282,10 +285,11 @@ async def get_raw(uri, args):
282285
This needs to be returned by an async function (as opposed to set as the
283286
mock's return value) because the corresponding Synapse code awaits on it.
284287
"""
285-
return """
288+
return (
289+
"""
286290
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
287291
<cas:authenticationSuccess>
288-
<cas:user>username</cas:user>
292+
<cas:user>%s</cas:user>
289293
<cas:proxyGrantingTicket>PGTIOU-84678-8a9d...</cas:proxyGrantingTicket>
290294
<cas:proxies>
291295
<cas:proxy>https://proxy2/pgtUrl</cas:proxy>
@@ -294,6 +298,8 @@ async def get_raw(uri, args):
294298
</cas:authenticationSuccess>
295299
</cas:serviceResponse>
296300
"""
301+
% cas_user_id
302+
)
297303

298304
mocked_http_client = Mock(spec=["get_raw"])
299305
mocked_http_client.get_raw.side_effect = get_raw
@@ -304,6 +310,9 @@ async def get_raw(uri, args):
304310

305311
return self.hs
306312

313+
def prepare(self, reactor, clock, hs):
314+
self.deactivate_account_handler = hs.get_deactivate_account_handler()
315+
307316
def test_cas_redirect_confirm(self):
308317
"""Tests that the SSO login flow serves a confirmation page before redirecting a
309318
user to the redirect URL.
@@ -370,3 +379,30 @@ def _test_redirect(self, redirect_url):
370379
self.assertEqual(channel.code, 302)
371380
location_headers = channel.headers.getRawHeaders("Location")
372381
self.assertEqual(location_headers[0][: len(redirect_url)], redirect_url)
382+
383+
@override_config({"sso": {"client_whitelist": ["https://legit-site.com/"]}})
384+
def test_deactivated_user(self):
385+
"""Logging in as a deactivated account should error."""
386+
redirect_url = "https://legit-site.com/"
387+
388+
# First login (to create the user).
389+
self._test_redirect(redirect_url)
390+
391+
# Deactivate the account.
392+
self.get_success(
393+
self.deactivate_account_handler.deactivate_account(self.user_id, False)
394+
)
395+
396+
# Request the CAS ticket.
397+
cas_ticket_url = (
398+
"/_matrix/client/r0/login/cas/ticket?redirectUrl=%s&ticket=ticket"
399+
% (urllib.parse.quote(redirect_url))
400+
)
401+
402+
# Get Synapse to call the fake CAS and serve the template.
403+
request, channel = self.make_request("GET", cas_ticket_url)
404+
self.render(request)
405+
406+
# Because the user is deactivated they are served an error template.
407+
self.assertEqual(channel.code, 403)
408+
self.assertIn(b"SSO account deactivated", channel.result["body"])

0 commit comments

Comments
 (0)