Skip to content

Commit b12dcdf

Browse files
clokepphil-flex
authored andcommitted
Support CAS in UI Auth flows. (matrix-org#7186)
1 parent 553ac04 commit b12dcdf

File tree

5 files changed

+131
-83
lines changed

5 files changed

+131
-83
lines changed

changelog.d/7186.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support SSO in the user interactive authentication workflow.

synapse/handlers/auth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def __init__(self, hs):
116116
self.hs = hs # FIXME better possibility to access registrationHandler later?
117117
self.macaroon_gen = hs.get_macaroon_generator()
118118
self._password_enabled = hs.config.password_enabled
119-
self._saml2_enabled = hs.config.saml2_enabled
119+
self._sso_enabled = hs.config.saml2_enabled or hs.config.cas_enabled
120120

121121
# we keep this as a list despite the O(N^2) implication so that we can
122122
# keep PASSWORD first and avoid confusing clients which pick the first
@@ -136,7 +136,7 @@ def __init__(self, hs):
136136
# necessarily identical. Login types have SSO (and other login types)
137137
# added in the rest layer, see synapse.rest.client.v1.login.LoginRestServerlet.on_GET.
138138
ui_auth_types = login_types.copy()
139-
if self._saml2_enabled:
139+
if self._sso_enabled:
140140
ui_auth_types.append(LoginType.SSO)
141141
self._supported_ui_auth_types = ui_auth_types
142142

synapse/handlers/cas_handler.py

Lines changed: 89 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import logging
1717
import xml.etree.ElementTree as ET
18-
from typing import AnyStr, Dict, Optional, Tuple
18+
from typing import Dict, Optional, Tuple
1919

2020
from six.moves import urllib
2121

@@ -48,26 +48,47 @@ def __init__(self, hs):
4848

4949
self._http_client = hs.get_proxied_http_client()
5050

51-
def _build_service_param(self, client_redirect_url: AnyStr) -> str:
51+
def _build_service_param(self, args: Dict[str, str]) -> str:
52+
"""
53+
Generates a value to use as the "service" parameter when redirecting or
54+
querying the CAS service.
55+
56+
Args:
57+
args: Additional arguments to include in the final redirect URL.
58+
59+
Returns:
60+
The URL to use as a "service" parameter.
61+
"""
5262
return "%s%s?%s" % (
5363
self._cas_service_url,
5464
"/_matrix/client/r0/login/cas/ticket",
55-
urllib.parse.urlencode({"redirectUrl": client_redirect_url}),
65+
urllib.parse.urlencode(args),
5666
)
5767

58-
async def _handle_cas_response(
59-
self, request: SynapseRequest, cas_response_body: str, client_redirect_url: str
60-
) -> None:
68+
async def _validate_ticket(
69+
self, ticket: str, service_args: Dict[str, str]
70+
) -> Tuple[str, Optional[str]]:
6171
"""
62-
Retrieves the user and display name from the CAS response and continues with the authentication.
72+
Validate a CAS ticket with the server, parse the response, and return the user and display name.
6373
6474
Args:
65-
request: The original client request.
66-
cas_response_body: The response from the CAS server.
67-
client_redirect_url: The URl to redirect the client to when
68-
everything is done.
75+
ticket: The CAS ticket from the client.
76+
service_args: Additional arguments to include in the service URL.
77+
Should be the same as those passed to `get_redirect_url`.
6978
"""
70-
user, attributes = self._parse_cas_response(cas_response_body)
79+
uri = self._cas_server_url + "/proxyValidate"
80+
args = {
81+
"ticket": ticket,
82+
"service": self._build_service_param(service_args),
83+
}
84+
try:
85+
body = await self._http_client.get_raw(uri, args)
86+
except PartialDownloadError as pde:
87+
# Twisted raises this error if the connection is closed,
88+
# even if that's being used old-http style to signal end-of-data
89+
body = pde.response
90+
91+
user, attributes = self._parse_cas_response(body)
7192
displayname = attributes.pop(self._cas_displayname_attribute, None)
7293

7394
for required_attribute, required_value in self._cas_required_attributes.items():
@@ -82,7 +103,7 @@ async def _handle_cas_response(
82103
if required_value != actual_value:
83104
raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED)
84105

85-
await self._on_successful_auth(user, request, client_redirect_url, displayname)
106+
return user, displayname
86107

87108
def _parse_cas_response(
88109
self, cas_response_body: str
@@ -127,78 +148,74 @@ def _parse_cas_response(
127148
)
128149
return user, attributes
129150

130-
async def _on_successful_auth(
131-
self,
132-
username: str,
133-
request: SynapseRequest,
134-
client_redirect_url: str,
135-
user_display_name: Optional[str] = None,
136-
) -> None:
137-
"""Called once the user has successfully authenticated with the SSO.
138-
139-
Registers the user if necessary, and then returns a redirect (with
140-
a login token) to the client.
151+
def get_redirect_url(self, service_args: Dict[str, str]) -> str:
152+
"""
153+
Generates a URL for the CAS server where the client should be redirected.
141154
142155
Args:
143-
username: the remote user id. We'll map this onto
144-
something sane for a MXID localpath.
156+
service_args: Additional arguments to include in the final redirect URL.
145157
146-
request: the incoming request from the browser. We'll
147-
respond to it with a redirect.
158+
Returns:
159+
The URL to redirect the client to.
160+
"""
161+
args = urllib.parse.urlencode(
162+
{"service": self._build_service_param(service_args)}
163+
)
148164

149-
client_redirect_url: the redirect_url the client gave us when
150-
it first started the process.
165+
return "%s/login?%s" % (self._cas_server_url, args)
151166

152-
user_display_name: if set, and we have to register a new user,
153-
we will set their displayname to this.
167+
async def handle_ticket(
168+
self,
169+
request: SynapseRequest,
170+
ticket: str,
171+
client_redirect_url: Optional[str],
172+
session: Optional[str],
173+
) -> None:
154174
"""
155-
localpart = map_username_to_mxid_localpart(username)
156-
user_id = UserID(localpart, self._hostname).to_string()
157-
registered_user_id = await self._auth_handler.check_user_exists(user_id)
158-
if not registered_user_id:
159-
registered_user_id = await self._registration_handler.register_user(
160-
localpart=localpart, default_display_name=user_display_name
161-
)
175+
Called once the user has successfully authenticated with the SSO.
176+
Validates a CAS ticket sent by the client and completes the auth process.
162177
163-
self._auth_handler.complete_sso_login(
164-
registered_user_id, request, client_redirect_url
165-
)
178+
If the user interactive authentication session is provided, marks the
179+
UI Auth session as complete, then returns an HTML page notifying the
180+
user they are done.
166181
167-
def handle_redirect_request(self, client_redirect_url: bytes) -> bytes:
168-
"""
169-
Generates a URL to the CAS server where the client should be redirected.
182+
Otherwise, this registers the user if necessary, and then returns a
183+
redirect (with a login token) to the client.
170184
171185
Args:
172-
client_redirect_url: The final URL the client should go to after the
173-
user has negotiated SSO.
186+
request: the incoming request from the browser. We'll
187+
respond to it with a redirect or an HTML page.
174188
175-
Returns:
176-
The URL to redirect to.
177-
"""
178-
args = urllib.parse.urlencode(
179-
{"service": self._build_service_param(client_redirect_url)}
180-
)
189+
ticket: The CAS ticket provided by the client.
181190
182-
return ("%s/login?%s" % (self._cas_server_url, args)).encode("ascii")
191+
client_redirect_url: the redirectUrl parameter from the `/cas/ticket` HTTP request, if given.
192+
This should be the same as the redirectUrl from the original `/login/sso/redirect` request.
183193
184-
async def handle_ticket_request(
185-
self, request: SynapseRequest, client_redirect_url: str, ticket: str
186-
) -> None:
194+
session: The session parameter from the `/cas/ticket` HTTP request, if given.
195+
This should be the UI Auth session id.
187196
"""
188-
Validates a CAS ticket sent by the client for login/registration.
197+
args = {}
198+
if client_redirect_url:
199+
args["redirectUrl"] = client_redirect_url
200+
if session:
201+
args["session"] = session
202+
username, user_display_name = await self._validate_ticket(ticket, args)
189203

190-
On a successful request, writes a redirect to the request.
191-
"""
192-
uri = self._cas_server_url + "/proxyValidate"
193-
args = {
194-
"ticket": ticket,
195-
"service": self._build_service_param(client_redirect_url),
196-
}
197-
try:
198-
body = await self._http_client.get_raw(uri, args)
199-
except PartialDownloadError as pde:
200-
# Twisted raises this error if the connection is closed,
201-
# even if that's being used old-http style to signal end-of-data
202-
body = pde.response
204+
localpart = map_username_to_mxid_localpart(username)
205+
user_id = UserID(localpart, self._hostname).to_string()
206+
registered_user_id = await self._auth_handler.check_user_exists(user_id)
203207

204-
await self._handle_cas_response(request, body, client_redirect_url)
208+
if session:
209+
self._auth_handler.complete_sso_ui_auth(
210+
registered_user_id, session, request,
211+
)
212+
213+
else:
214+
if not registered_user_id:
215+
registered_user_id = await self._registration_handler.register_user(
216+
localpart=localpart, default_display_name=user_display_name
217+
)
218+
219+
self._auth_handler.complete_sso_login(
220+
registered_user_id, request, client_redirect_url
221+
)

synapse/rest/client/v1/login.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,9 @@ def __init__(self, hs):
425425
self._cas_handler = hs.get_cas_handler()
426426

427427
def get_sso_url(self, client_redirect_url: bytes) -> bytes:
428-
return self._cas_handler.handle_redirect_request(client_redirect_url)
428+
return self._cas_handler.get_redirect_url(
429+
{"redirectUrl": client_redirect_url}
430+
).encode("ascii")
429431

430432

431433
class CasTicketServlet(RestServlet):
@@ -436,10 +438,20 @@ def __init__(self, hs):
436438
self._cas_handler = hs.get_cas_handler()
437439

438440
async def on_GET(self, request: SynapseRequest) -> None:
439-
client_redirect_url = parse_string(request, "redirectUrl", required=True)
441+
client_redirect_url = parse_string(request, "redirectUrl")
440442
ticket = parse_string(request, "ticket", required=True)
441-
await self._cas_handler.handle_ticket_request(
442-
request, client_redirect_url, ticket
443+
444+
# Maybe get a session ID (if this ticket is from user interactive
445+
# authentication).
446+
session = parse_string(request, "session")
447+
448+
# Either client_redirect_url or session must be provided.
449+
if not client_redirect_url and not session:
450+
message = "Missing string query parameter redirectUrl or session"
451+
raise SynapseError(400, message, errcode=Codes.MISSING_PARAM)
452+
453+
await self._cas_handler.handle_ticket(
454+
request, ticket, client_redirect_url, session
443455
)
444456

445457

synapse/rest/client/v2_alpha/auth.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ def __init__(self, hs):
111111
self._saml_enabled = hs.config.saml2_enabled
112112
if self._saml_enabled:
113113
self._saml_handler = hs.get_saml_handler()
114+
self._cas_enabled = hs.config.cas_enabled
115+
if self._cas_enabled:
116+
self._cas_handler = hs.get_cas_handler()
117+
self._cas_server_url = hs.config.cas_server_url
118+
self._cas_service_url = hs.config.cas_service_url
114119

115120
def on_GET(self, request, stagetype):
116121
session = parse_string(request, "session")
@@ -133,14 +138,27 @@ def on_GET(self, request, stagetype):
133138
% (CLIENT_API_PREFIX, LoginType.TERMS),
134139
}
135140

136-
elif stagetype == LoginType.SSO and self._saml_enabled:
141+
elif stagetype == LoginType.SSO:
137142
# Display a confirmation page which prompts the user to
138143
# re-authenticate with their SSO provider.
139-
client_redirect_url = ""
140-
sso_redirect_url = self._saml_handler.handle_redirect_request(
141-
client_redirect_url, session
142-
)
144+
if self._cas_enabled:
145+
# Generate a request to CAS that redirects back to an endpoint
146+
# to verify the successful authentication.
147+
sso_redirect_url = self._cas_handler.get_redirect_url(
148+
{"session": session},
149+
)
150+
151+
elif self._saml_enabled:
152+
client_redirect_url = ""
153+
sso_redirect_url = self._saml_handler.handle_redirect_request(
154+
client_redirect_url, session
155+
)
156+
157+
else:
158+
raise SynapseError(400, "Homeserver not configured for SSO.")
159+
143160
html = self.auth_handler.start_sso_ui_auth(sso_redirect_url, session)
161+
144162
else:
145163
raise SynapseError(404, "Unknown auth stage type")
146164

0 commit comments

Comments
 (0)