Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit a737cc2

Browse files
authored
Implement MSC2858 support (#9183)
Fixes #8928.
1 parent 2547d9d commit a737cc2

File tree

9 files changed

+230
-21
lines changed

9 files changed

+230
-21
lines changed

changelog.d/9183.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add experimental support for allowing clients to pick an SSO Identity Provider ([MSC2858](https://github.com/matrix-org/matrix-doc/pull/2858).

synapse/config/_base.pyi

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ from synapse.config import (
99
consent_config,
1010
database,
1111
emailconfig,
12+
experimental,
1213
groups,
1314
jwt_config,
1415
key,
@@ -48,6 +49,7 @@ def path_exists(file_path: str): ...
4849

4950
class RootConfig:
5051
server: server.ServerConfig
52+
experimental: experimental.ExperimentalConfig
5153
tls: tls.TlsConfig
5254
database: database.DatabaseConfig
5355
logging: logger.LoggingConfig

synapse/config/experimental.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2021 The Matrix.org Foundation C.I.C.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
from synapse.config._base import Config
17+
from synapse.types import JsonDict
18+
19+
20+
class ExperimentalConfig(Config):
21+
"""Config section for enabling experimental features"""
22+
23+
section = "experimental"
24+
25+
def read_config(self, config: JsonDict, **kwargs):
26+
experimental = config.get("experimental_features") or {}
27+
28+
# MSC2858 (multiple SSO identity providers)
29+
self.msc2858_enabled = experimental.get("msc2858_enabled", False) # type: bool

synapse/config/homeserver.py

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from .consent_config import ConsentConfig
2525
from .database import DatabaseConfig
2626
from .emailconfig import EmailConfig
27+
from .experimental import ExperimentalConfig
2728
from .federation import FederationConfig
2829
from .groups import GroupsConfig
2930
from .jwt_config import JWTConfig
@@ -57,6 +58,7 @@ class HomeServerConfig(RootConfig):
5758

5859
config_classes = [
5960
ServerConfig,
61+
ExperimentalConfig,
6062
TlsConfig,
6163
FederationConfig,
6264
CacheConfig,

synapse/handlers/sso.py

+18-5
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from twisted.web.http import Request
2424

2525
from synapse.api.constants import LoginType
26-
from synapse.api.errors import Codes, RedirectException, SynapseError
26+
from synapse.api.errors import Codes, NotFoundError, RedirectException, SynapseError
2727
from synapse.handlers.ui_auth import UIAuthSessionDataConstants
2828
from synapse.http import get_request_user_agent
2929
from synapse.http.server import respond_with_html
@@ -235,14 +235,18 @@ def render_error(
235235
respond_with_html(request, code, html)
236236

237237
async def handle_redirect_request(
238-
self, request: SynapseRequest, client_redirect_url: bytes,
238+
self,
239+
request: SynapseRequest,
240+
client_redirect_url: bytes,
241+
idp_id: Optional[str],
239242
) -> str:
240243
"""Handle a request to /login/sso/redirect
241244
242245
Args:
243246
request: incoming HTTP request
244247
client_redirect_url: the URL that we should redirect the
245248
client to after login.
249+
idp_id: optional identity provider chosen by the client
246250
247251
Returns:
248252
the URI to redirect to
@@ -252,10 +256,19 @@ async def handle_redirect_request(
252256
400, "Homeserver not configured for SSO.", errcode=Codes.UNRECOGNIZED
253257
)
254258

259+
# if the client chose an IdP, use that
260+
idp = None # type: Optional[SsoIdentityProvider]
261+
if idp_id:
262+
idp = self._identity_providers.get(idp_id)
263+
if not idp:
264+
raise NotFoundError("Unknown identity provider")
265+
255266
# if we only have one auth provider, redirect to it directly
256-
if len(self._identity_providers) == 1:
257-
ap = next(iter(self._identity_providers.values()))
258-
return await ap.handle_redirect_request(request, client_redirect_url)
267+
elif len(self._identity_providers) == 1:
268+
idp = next(iter(self._identity_providers.values()))
269+
270+
if idp:
271+
return await idp.handle_redirect_request(request, client_redirect_url)
259272

260273
# otherwise, redirect to the IDP picker
261274
return "/_synapse/client/pick_idp?" + urlencode(

synapse/http/server.py

+36-8
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,22 @@
2222
import urllib
2323
from http import HTTPStatus
2424
from io import BytesIO
25-
from typing import Any, Callable, Dict, Iterator, List, Tuple, Union
25+
from typing import (
26+
Any,
27+
Awaitable,
28+
Callable,
29+
Dict,
30+
Iterable,
31+
Iterator,
32+
List,
33+
Pattern,
34+
Tuple,
35+
Union,
36+
)
2637

2738
import jinja2
2839
from canonicaljson import iterencode_canonical_json
40+
from typing_extensions import Protocol
2941
from zope.interface import implementer
3042

3143
from twisted.internet import defer, interfaces
@@ -168,24 +180,40 @@ async def wrapped_async_request_handler(self, request):
168180
return preserve_fn(wrapped_async_request_handler)
169181

170182

171-
class HttpServer:
183+
# Type of a callback method for processing requests
184+
# it is actually called with a SynapseRequest and a kwargs dict for the params,
185+
# but I can't figure out how to represent that.
186+
ServletCallback = Callable[
187+
..., Union[None, Awaitable[None], Tuple[int, Any], Awaitable[Tuple[int, Any]]]
188+
]
189+
190+
191+
class HttpServer(Protocol):
172192
""" Interface for registering callbacks on a HTTP server
173193
"""
174194

175-
def register_paths(self, method, path_patterns, callback):
195+
def register_paths(
196+
self,
197+
method: str,
198+
path_patterns: Iterable[Pattern],
199+
callback: ServletCallback,
200+
servlet_classname: str,
201+
) -> None:
176202
""" Register a callback that gets fired if we receive a http request
177203
with the given method for a path that matches the given regex.
178204
179205
If the regex contains groups these gets passed to the callback via
180206
an unpacked tuple.
181207
182208
Args:
183-
method (str): The method to listen to.
184-
path_patterns (list<SRE_Pattern>): The regex used to match requests.
185-
callback (function): The function to fire if we receive a matched
209+
method: The HTTP method to listen to.
210+
path_patterns: The regex used to match requests.
211+
callback: The function to fire if we receive a matched
186212
request. The first argument will be the request object and
187213
subsequent arguments will be any matched groups from the regex.
188-
This should return a tuple of (code, response).
214+
This should return either tuple of (code, response), or None.
215+
servlet_classname (str): The name of the handler to be used in prometheus
216+
and opentracing logs.
189217
"""
190218
pass
191219

@@ -354,7 +382,7 @@ def register_paths(self, method, path_patterns, callback, servlet_classname):
354382

355383
def _get_handler_for_request(
356384
self, request: SynapseRequest
357-
) -> Tuple[Callable, str, Dict[str, str]]:
385+
) -> Tuple[ServletCallback, str, Dict[str, str]]:
358386
"""Finds a callback method to handle the given request.
359387
360388
Returns:

synapse/rest/client/v1/login.py

+49-6
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
from synapse.api.errors import Codes, LoginError, SynapseError
2020
from synapse.api.ratelimiting import Ratelimiter
2121
from synapse.appservice import ApplicationService
22-
from synapse.http.server import finish_request
22+
from synapse.handlers.sso import SsoIdentityProvider
23+
from synapse.http.server import HttpServer, finish_request
2324
from synapse.http.servlet import (
2425
RestServlet,
2526
parse_json_object_from_request,
@@ -60,11 +61,14 @@ def __init__(self, hs: "HomeServer"):
6061
self.saml2_enabled = hs.config.saml2_enabled
6162
self.cas_enabled = hs.config.cas_enabled
6263
self.oidc_enabled = hs.config.oidc_enabled
64+
self._msc2858_enabled = hs.config.experimental.msc2858_enabled
6365

6466
self.auth = hs.get_auth()
6567

6668
self.auth_handler = self.hs.get_auth_handler()
6769
self.registration_handler = hs.get_registration_handler()
70+
self._sso_handler = hs.get_sso_handler()
71+
6872
self._well_known_builder = WellKnownBuilder(hs)
6973
self._address_ratelimiter = Ratelimiter(
7074
clock=hs.get_clock(),
@@ -89,8 +93,17 @@ def on_GET(self, request: SynapseRequest):
8993
flows.append({"type": LoginRestServlet.CAS_TYPE})
9094

9195
if self.cas_enabled or self.saml2_enabled or self.oidc_enabled:
92-
flows.append({"type": LoginRestServlet.SSO_TYPE})
93-
# While its valid for us to advertise this login type generally,
96+
sso_flow = {"type": LoginRestServlet.SSO_TYPE} # type: JsonDict
97+
98+
if self._msc2858_enabled:
99+
sso_flow["org.matrix.msc2858.identity_providers"] = [
100+
_get_auth_flow_dict_for_idp(idp)
101+
for idp in self._sso_handler.get_identity_providers().values()
102+
]
103+
104+
flows.append(sso_flow)
105+
106+
# While it's valid for us to advertise this login type generally,
94107
# synapse currently only gives out these tokens as part of the
95108
# SSO login flow.
96109
# Generally we don't want to advertise login flows that clients
@@ -311,8 +324,20 @@ async def _do_jwt_login(self, login_submission: JsonDict) -> Dict[str, str]:
311324
return result
312325

313326

327+
def _get_auth_flow_dict_for_idp(idp: SsoIdentityProvider) -> JsonDict:
328+
"""Return an entry for the login flow dict
329+
330+
Returns an entry suitable for inclusion in "identity_providers" in the
331+
response to GET /_matrix/client/r0/login
332+
"""
333+
e = {"id": idp.idp_id, "name": idp.idp_name} # type: JsonDict
334+
if idp.idp_icon:
335+
e["icon"] = idp.idp_icon
336+
return e
337+
338+
314339
class SsoRedirectServlet(RestServlet):
315-
PATTERNS = client_patterns("/login/(cas|sso)/redirect", v1=True)
340+
PATTERNS = client_patterns("/login/(cas|sso)/redirect$", v1=True)
316341

317342
def __init__(self, hs: "HomeServer"):
318343
# make sure that the relevant handlers are instantiated, so that they
@@ -324,13 +349,31 @@ def __init__(self, hs: "HomeServer"):
324349
if hs.config.oidc_enabled:
325350
hs.get_oidc_handler()
326351
self._sso_handler = hs.get_sso_handler()
352+
self._msc2858_enabled = hs.config.experimental.msc2858_enabled
353+
354+
def register(self, http_server: HttpServer) -> None:
355+
super().register(http_server)
356+
if self._msc2858_enabled:
357+
# expose additional endpoint for MSC2858 support
358+
http_server.register_paths(
359+
"GET",
360+
client_patterns(
361+
"/org.matrix.msc2858/login/sso/redirect/(?P<idp_id>[A-Za-z0-9_.~-]+)$",
362+
releases=(),
363+
unstable=True,
364+
),
365+
self.on_GET,
366+
self.__class__.__name__,
367+
)
327368

328-
async def on_GET(self, request: SynapseRequest):
369+
async def on_GET(
370+
self, request: SynapseRequest, idp_id: Optional[str] = None
371+
) -> None:
329372
client_redirect_url = parse_string(
330373
request, "redirectUrl", required=True, encoding=None
331374
)
332375
sso_url = await self._sso_handler.handle_redirect_request(
333-
request, client_redirect_url
376+
request, client_redirect_url, idp_id,
334377
)
335378
logger.info("Redirecting to %s", sso_url)
336379
request.redirect(sso_url)

0 commit comments

Comments
 (0)