Do not use a separate endpoint for UI Auth for CAS.

pull/7186/head
Patrick Cloke 2020-04-01 14:13:53 -04:00
parent 32581bf832
commit 7e7e48628d
3 changed files with 52 additions and 94 deletions

View File

@ -48,44 +48,37 @@ class CasHandler:
self._http_client = hs.get_proxied_http_client()
def _build_service_param(self, service_redirect_endpoint: str, **kwargs) -> 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:
service_redirect_endpoint: The homeserver endpoint to redirect
the client to after successful SSO negotiation.
kwargs: Additional arguments to include in the final redirect URL.
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,
service_redirect_endpoint,
urllib.parse.urlencode(kwargs),
"/_matrix/client/r0/login/cas/ticket",
urllib.parse.urlencode(args),
)
async def _validate_ticket(
self, ticket: str, service_redirect_endpoint: str, client_redirect_url: str
self, ticket: str, service_args: Dict[str, str]
) -> Tuple[str, Optional[str]]:
"""
Validate a CAS ticket with the server, parse the response, and return the user and display name.
Args:
ticket: The CAS ticket from the client.
service_redirect_endpoint: The homeserver endpoint that the client
accessed to validate the ticket.
client_redirect_url: The URL to redirect the client to after
validation is done.
service_args: Additional arguments to include in the service URL.
"""
uri = self._cas_server_url + "/proxyValidate"
args = {
"ticket": ticket,
"service": self._build_service_param(
service_redirect_endpoint, redirectUrl=client_redirect_url
),
"service": self._build_service_param(service_args),
}
try:
body = await self._http_client.get_raw(uri, args)
@ -154,26 +147,28 @@ class CasHandler:
)
return user, attributes
def get_redirect_url(self, service_redirect_endpoint: str, **kwargs) -> str:
def get_redirect_url(self, service_args: Dict[str, str]) -> str:
"""
Generates a URL to the CAS server where the client should be redirected.
Args:
service_redirect_endpoint: The homeserver endpoint to redirect
the client to after successful SSO negotiation.
kwargs: Additional arguments to include in the final redirect URL.
service_args: Additional arguments to include in the final redirect URL.
Returns:
The URL to redirect the client to.
"""
args = urllib.parse.urlencode(
{"service": self._build_service_param(service_redirect_endpoint, **kwargs)}
{"service": self._build_service_param(service_args)}
)
return "%s/login?%s" % (self._cas_server_url, args)
async def handle_ticket_for_login(
self, request: SynapseRequest, client_redirect_url: str, ticket: str,
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,
@ -186,52 +181,35 @@ class CasHandler:
request: the incoming request from the browser. We'll
respond to it with a redirect.
ticket: The CAS ticket provided by the client.
client_redirect_url: the redirect_url the client gave us when
it first started the process.
ticket: The CAS ticket provided by the client.
session_id: The UI Auth session ID, if applicable.
"""
username, user_display_name = await self._validate_ticket(
ticket, request.path, client_redirect_url
)
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
)
async def handle_ticket_for_ui_auth(
self, request: SynapseRequest, ticket: str, session_id: str
) -> None:
"""
Called once the user has successfully authenticated with the SSO,
validates a CAS ticket sent by the client and completes user interactive
authentication.
If successful, this completes the SSO step of UI auth and returns a
an HTML page to the client.
Args:
request: the incoming request from the browser.
ticket: The CAS ticket provided by the client.
session_id: The UI Auth session ID.
"""
client_redirect_url = ""
user, _ = await self._validate_ticket(ticket, request.path, client_redirect_url)
localpart = map_username_to_mxid_localpart(user)
user_id = UserID(localpart, self._hostname).to_string()
registered_user_id = await self._auth_handler.check_user_exists(user_id)
self._auth_handler.complete_sso_ui_auth(
registered_user_id, session_id, request,
)
self._auth_handler.complete_sso_login(
registered_user_id, request, client_redirect_url
)

View File

@ -426,7 +426,7 @@ class CasRedirectServlet(BaseSSORedirectServlet):
def get_sso_url(self, client_redirect_url: bytes) -> bytes:
return self._cas_handler.get_redirect_url(
"/_matrix/client/r0/login/cas/ticket", redirectUrl=client_redirect_url
{"redirectUrl": client_redirect_url}
).encode("ascii")
@ -438,10 +438,20 @@ class CasTicketServlet(RestServlet):
self._cas_handler = hs.get_cas_handler()
async def on_GET(self, request: SynapseRequest) -> None:
client_redirect_url = parse_string(request, "redirectUrl", required=True)
client_redirect_url = parse_string(request, "redirectUrl")
ticket = parse_string(request, "ticket", required=True)
await self._cas_handler.handle_ticket_for_login(
request, client_redirect_url, ticket
# Maybe get a session ID (if this ticket is from user interactive
# authentication).
session = parse_string(request, "session")
# Either client_redirect_url or session must be provided.
if not client_redirect_url and not session:
message = "Missing string query parameter redirectUrl or session"
raise SynapseError(400, message, errcode=Codes.MISSING_PARAM)
await self._cas_handler.handle_ticket(
request, ticket, client_redirect_url, session
)

View File

@ -145,7 +145,7 @@ class AuthRestServlet(RestServlet):
# Generate a request to CAS that redirects back to an endpoint
# to verify the successful authentication.
sso_redirect_url = self._cas_handler.get_redirect_url(
"/_matrix/client/r0/auth/cas/ticket", session=session,
{"session": session},
)
elif self._saml_enabled:
@ -239,35 +239,5 @@ class AuthRestServlet(RestServlet):
return 200, {}
class CasAuthTicketServlet(RestServlet):
"""
Completes a user interactive authentication session when using CAS.
It is called after the user has completed SSO with the CAS provider and
received a ticket in response. It does the following:
* Retrieves the CAS ticket and the UI auth session from the request.
* Validates the CAS ticket.
* Marks the UI auth session as complete.
"""
PATTERNS = client_patterns(r"/auth/cas/ticket")
def __init__(self, hs):
super(CasAuthTicketServlet, self).__init__()
self._cas_handler = hs.get_cas_handler()
async def on_GET(self, request):
ticket = parse_string(request, "ticket", required=True)
# Pull the UI Auth session ID out.
session_id = parse_string(request, "session", required=True)
return await self._cas_handler.handle_ticket_for_ui_auth(
request, ticket, session_id
)
def register_servlets(hs, http_server):
AuthRestServlet(hs).register(http_server)
if hs.config.cas_enabled:
CasAuthTicketServlet(hs).register(http_server)