Support CAS in UI Auth flows. (#7186)

This commit is contained in:
Patrick Cloke 2020-04-03 15:35:05 -04:00 committed by GitHub
parent b0db928c63
commit 694d8bed0e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 137 additions and 89 deletions

View file

@ -15,7 +15,7 @@
import logging
import xml.etree.ElementTree as ET
from typing import AnyStr, Dict, Optional, Tuple
from typing import Dict, Optional, Tuple
from six.moves import urllib
@ -48,26 +48,47 @@ class CasHandler:
self._http_client = hs.get_proxied_http_client()
def _build_service_param(self, client_redirect_url: AnyStr) -> str:
def _build_service_param(self, args: Dict[str, str]) -> str:
"""
Generates a value to use as the "service" parameter when redirecting or
querying the CAS service.
Args:
args: Additional arguments to include in the final redirect URL.
Returns:
The URL to use as a "service" parameter.
"""
return "%s%s?%s" % (
self._cas_service_url,
"/_matrix/client/r0/login/cas/ticket",
urllib.parse.urlencode({"redirectUrl": client_redirect_url}),
urllib.parse.urlencode(args),
)
async def _handle_cas_response(
self, request: SynapseRequest, cas_response_body: str, client_redirect_url: str
) -> None:
async def _validate_ticket(
self, ticket: str, service_args: Dict[str, str]
) -> Tuple[str, Optional[str]]:
"""
Retrieves the user and display name from the CAS response and continues with the authentication.
Validate a CAS ticket with the server, parse the response, and return the user and display name.
Args:
request: The original client request.
cas_response_body: The response from the CAS server.
client_redirect_url: The URl to redirect the client to when
everything is done.
ticket: The CAS ticket from the client.
service_args: Additional arguments to include in the service URL.
Should be the same as those passed to `get_redirect_url`.
"""
user, attributes = self._parse_cas_response(cas_response_body)
uri = self._cas_server_url + "/proxyValidate"
args = {
"ticket": ticket,
"service": self._build_service_param(service_args),
}
try:
body = await self._http_client.get_raw(uri, args)
except PartialDownloadError as pde:
# Twisted raises this error if the connection is closed,
# even if that's being used old-http style to signal end-of-data
body = pde.response
user, attributes = self._parse_cas_response(body)
displayname = attributes.pop(self._cas_displayname_attribute, None)
for required_attribute, required_value in self._cas_required_attributes.items():
@ -82,7 +103,7 @@ class CasHandler:
if required_value != actual_value:
raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED)
await self._on_successful_auth(user, request, client_redirect_url, displayname)
return user, displayname
def _parse_cas_response(
self, cas_response_body: str
@ -127,78 +148,74 @@ class CasHandler:
)
return user, attributes
async def _on_successful_auth(
self,
username: str,
request: SynapseRequest,
client_redirect_url: str,
user_display_name: Optional[str] = None,
) -> None:
"""Called once the user has successfully authenticated with the SSO.
Registers the user if necessary, and then returns a redirect (with
a login token) to the client.
def get_redirect_url(self, service_args: Dict[str, str]) -> str:
"""
Generates a URL for the CAS server where the client should be redirected.
Args:
username: the remote user id. We'll map this onto
something sane for a MXID localpath.
service_args: Additional arguments to include in the final redirect URL.
request: the incoming request from the browser. We'll
respond to it with a redirect.
client_redirect_url: the redirect_url the client gave us when
it first started the process.
user_display_name: if set, and we have to register a new user,
we will set their displayname to this.
Returns:
The URL to redirect the client to.
"""
args = urllib.parse.urlencode(
{"service": self._build_service_param(service_args)}
)
return "%s/login?%s" % (self._cas_server_url, args)
async def handle_ticket(
self,
request: SynapseRequest,
ticket: str,
client_redirect_url: Optional[str],
session: Optional[str],
) -> None:
"""
Called once the user has successfully authenticated with the SSO.
Validates a CAS ticket sent by the client and completes the auth process.
If the user interactive authentication session is provided, marks the
UI Auth session as complete, then returns an HTML page notifying the
user they are done.
Otherwise, this registers the user if necessary, and then returns a
redirect (with a login token) to the client.
Args:
request: the incoming request from the browser. We'll
respond to it with a redirect or an HTML page.
ticket: The CAS ticket provided by the client.
client_redirect_url: the redirectUrl parameter from the `/cas/ticket` HTTP request, if given.
This should be the same as the redirectUrl from the original `/login/sso/redirect` request.
session: The session parameter from the `/cas/ticket` HTTP request, if given.
This should be the UI Auth session id.
"""
args = {}
if client_redirect_url:
args["redirectUrl"] = client_redirect_url
if session:
args["session"] = session
username, user_display_name = await self._validate_ticket(ticket, args)
localpart = map_username_to_mxid_localpart(username)
user_id = UserID(localpart, self._hostname).to_string()
registered_user_id = await self._auth_handler.check_user_exists(user_id)
if not registered_user_id:
registered_user_id = await self._registration_handler.register_user(
localpart=localpart, default_display_name=user_display_name
if session:
self._auth_handler.complete_sso_ui_auth(
registered_user_id, session, request,
)
self._auth_handler.complete_sso_login(
registered_user_id, request, client_redirect_url
)
else:
if not registered_user_id:
registered_user_id = await self._registration_handler.register_user(
localpart=localpart, default_display_name=user_display_name
)
def handle_redirect_request(self, client_redirect_url: bytes) -> bytes:
"""
Generates a URL to the CAS server where the client should be redirected.
Args:
client_redirect_url: The final URL the client should go to after the
user has negotiated SSO.
Returns:
The URL to redirect to.
"""
args = urllib.parse.urlencode(
{"service": self._build_service_param(client_redirect_url)}
)
return ("%s/login?%s" % (self._cas_server_url, args)).encode("ascii")
async def handle_ticket_request(
self, request: SynapseRequest, client_redirect_url: str, ticket: str
) -> None:
"""
Validates a CAS ticket sent by the client for login/registration.
On a successful request, writes a redirect to the request.
"""
uri = self._cas_server_url + "/proxyValidate"
args = {
"ticket": ticket,
"service": self._build_service_param(client_redirect_url),
}
try:
body = await self._http_client.get_raw(uri, args)
except PartialDownloadError as pde:
# Twisted raises this error if the connection is closed,
# even if that's being used old-http style to signal end-of-data
body = pde.response
await self._handle_cas_response(request, body, client_redirect_url)
self._auth_handler.complete_sso_login(
registered_user_id, request, client_redirect_url
)