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

Commit dd69110

Browse files
authored
Add support for stable MSC2858 API (#9617)
The stable format uses different brand identifiers, so we need to support two identifiers for each IdP.
1 parent 5b5bc18 commit dd69110

File tree

10 files changed

+88
-28
lines changed

10 files changed

+88
-28
lines changed

changelog.d/9617.feature

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

docs/openid.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ Synapse config:
226226
oidc_providers:
227227
- idp_id: github
228228
idp_name: Github
229-
idp_brand: "org.matrix.github" # optional: styling hint for clients
229+
idp_brand: "github" # optional: styling hint for clients
230230
discover: false
231231
issuer: "https://github.com/"
232232
client_id: "your-client-id" # TO BE FILLED
@@ -252,7 +252,7 @@ oidc_providers:
252252
oidc_providers:
253253
- idp_id: google
254254
idp_name: Google
255-
idp_brand: "org.matrix.google" # optional: styling hint for clients
255+
idp_brand: "google" # optional: styling hint for clients
256256
issuer: "https://accounts.google.com/"
257257
client_id: "your-client-id" # TO BE FILLED
258258
client_secret: "your-client-secret" # TO BE FILLED
@@ -299,7 +299,7 @@ Synapse config:
299299
oidc_providers:
300300
- idp_id: gitlab
301301
idp_name: Gitlab
302-
idp_brand: "org.matrix.gitlab" # optional: styling hint for clients
302+
idp_brand: "gitlab" # optional: styling hint for clients
303303
issuer: "https://gitlab.com/"
304304
client_id: "your-client-id" # TO BE FILLED
305305
client_secret: "your-client-secret" # TO BE FILLED
@@ -334,7 +334,7 @@ Synapse config:
334334
```yaml
335335
- idp_id: facebook
336336
idp_name: Facebook
337-
idp_brand: "org.matrix.facebook" # optional: styling hint for clients
337+
idp_brand: "facebook" # optional: styling hint for clients
338338
discover: false
339339
issuer: "https://facebook.com"
340340
client_id: "your-client-id" # TO BE FILLED

docs/sample_config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1919,7 +1919,7 @@ oidc_providers:
19191919
#
19201920
#- idp_id: github
19211921
# idp_name: Github
1922-
# idp_brand: org.matrix.github
1922+
# idp_brand: github
19231923
# discover: false
19241924
# issuer: "https://github.com/"
19251925
# client_id: "your-client-id" # TO BE FILLED

synapse/config/oidc_config.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs):
237237
#
238238
#- idp_id: github
239239
# idp_name: Github
240-
# idp_brand: org.matrix.github
240+
# idp_brand: github
241241
# discover: false
242242
# issuer: "https://github.com/"
243243
# client_id: "your-client-id" # TO BE FILLED
@@ -272,7 +272,12 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs):
272272
"idp_icon": {"type": "string"},
273273
"idp_brand": {
274274
"type": "string",
275-
# MSC2758-style namespaced identifier
275+
"minLength": 1,
276+
"maxLength": 255,
277+
"pattern": "^[a-z][a-z0-9_.-]*$",
278+
},
279+
"idp_unstable_brand": {
280+
"type": "string",
276281
"minLength": 1,
277282
"maxLength": 255,
278283
"pattern": "^[a-z][a-z0-9_.-]*$",
@@ -466,6 +471,7 @@ def _parse_oidc_config_dict(
466471
idp_name=oidc_config.get("idp_name", "OIDC"),
467472
idp_icon=idp_icon,
468473
idp_brand=oidc_config.get("idp_brand"),
474+
unstable_idp_brand=oidc_config.get("unstable_idp_brand"),
469475
discover=oidc_config.get("discover", True),
470476
issuer=oidc_config["issuer"],
471477
client_id=oidc_config["client_id"],
@@ -512,6 +518,9 @@ class OidcProviderConfig:
512518
# Optional brand identifier for this IdP.
513519
idp_brand = attr.ib(type=Optional[str])
514520

521+
# Optional brand identifier for the unstable API (see MSC2858).
522+
unstable_idp_brand = attr.ib(type=Optional[str])
523+
515524
# whether the OIDC discovery mechanism is used to discover endpoints
516525
discover = attr.ib(type=bool)
517526

synapse/handlers/cas_handler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ def __init__(self, hs: "HomeServer"):
8383
# the SsoIdentityProvider protocol type.
8484
self.idp_icon = None
8585
self.idp_brand = None
86+
self.unstable_idp_brand = None
8687

8788
self._sso_handler = hs.get_sso_handler()
8889

synapse/handlers/oidc_handler.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,9 @@ def __init__(
330330
# optional brand identifier for this auth provider
331331
self.idp_brand = provider.idp_brand
332332

333+
# Optional brand identifier for the unstable API (see MSC2858).
334+
self.unstable_idp_brand = provider.unstable_idp_brand
335+
333336
self._sso_handler = hs.get_sso_handler()
334337

335338
self._sso_handler.register_identity_provider(self)

synapse/handlers/saml_handler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ def __init__(self, hs: "HomeServer"):
8181
# the SsoIdentityProvider protocol type.
8282
self.idp_icon = None
8383
self.idp_brand = None
84+
self.unstable_idp_brand = None
8485

8586
# a map from saml session id to Saml2SessionData object
8687
self._outstanding_requests_dict = {} # type: Dict[str, Saml2SessionData]

synapse/handlers/sso.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ def idp_brand(self) -> Optional[str]:
9898
"""Optional branding identifier"""
9999
return None
100100

101+
@property
102+
def unstable_idp_brand(self) -> Optional[str]:
103+
"""Optional brand identifier for the unstable API (see MSC2858)."""
104+
return None
105+
101106
@abc.abstractmethod
102107
async def handle_redirect_request(
103108
self,

synapse/rest/client/v1/login.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
# limitations under the License.
1515

1616
import logging
17+
import re
1718
from typing import TYPE_CHECKING, Awaitable, Callable, Dict, Optional
1819

1920
from synapse.api.errors import Codes, LoginError, SynapseError
2021
from synapse.api.ratelimiting import Ratelimiter
22+
from synapse.api.urls import CLIENT_API_PREFIX
2123
from synapse.appservice import ApplicationService
2224
from synapse.handlers.sso import SsoIdentityProvider
2325
from synapse.http import get_request_uri
@@ -94,11 +96,21 @@ def on_GET(self, request: SynapseRequest):
9496
flows.append({"type": LoginRestServlet.CAS_TYPE})
9597

9698
if self.cas_enabled or self.saml2_enabled or self.oidc_enabled:
97-
sso_flow = {"type": LoginRestServlet.SSO_TYPE} # type: JsonDict
99+
sso_flow = {
100+
"type": LoginRestServlet.SSO_TYPE,
101+
"identity_providers": [
102+
_get_auth_flow_dict_for_idp(
103+
idp,
104+
)
105+
for idp in self._sso_handler.get_identity_providers().values()
106+
],
107+
} # type: JsonDict
98108

99109
if self._msc2858_enabled:
110+
# backwards-compatibility support for clients which don't
111+
# support the stable API yet
100112
sso_flow["org.matrix.msc2858.identity_providers"] = [
101-
_get_auth_flow_dict_for_idp(idp)
113+
_get_auth_flow_dict_for_idp(idp, use_unstable_brands=True)
102114
for idp in self._sso_handler.get_identity_providers().values()
103115
]
104116

@@ -331,22 +343,38 @@ async def _do_jwt_login(self, login_submission: JsonDict) -> Dict[str, str]:
331343
return result
332344

333345

334-
def _get_auth_flow_dict_for_idp(idp: SsoIdentityProvider) -> JsonDict:
346+
def _get_auth_flow_dict_for_idp(
347+
idp: SsoIdentityProvider, use_unstable_brands: bool = False
348+
) -> JsonDict:
335349
"""Return an entry for the login flow dict
336350
337351
Returns an entry suitable for inclusion in "identity_providers" in the
338352
response to GET /_matrix/client/r0/login
353+
354+
Args:
355+
idp: the identity provider to describe
356+
use_unstable_brands: whether we should use brand identifiers suitable
357+
for the unstable API
339358
"""
340359
e = {"id": idp.idp_id, "name": idp.idp_name} # type: JsonDict
341360
if idp.idp_icon:
342361
e["icon"] = idp.idp_icon
343362
if idp.idp_brand:
344363
e["brand"] = idp.idp_brand
364+
# use the stable brand identifier if the unstable identifier isn't defined.
365+
if use_unstable_brands and idp.unstable_idp_brand:
366+
e["brand"] = idp.unstable_idp_brand
345367
return e
346368

347369

348370
class SsoRedirectServlet(RestServlet):
349-
PATTERNS = client_patterns("/login/(cas|sso)/redirect$", v1=True)
371+
PATTERNS = list(client_patterns("/login/(cas|sso)/redirect$", v1=True)) + [
372+
re.compile(
373+
"^"
374+
+ CLIENT_API_PREFIX
375+
+ "/r0/login/sso/redirect/(?P<idp_id>[A-Za-z0-9_.~-]+)$"
376+
)
377+
]
350378

351379
def __init__(self, hs: "HomeServer"):
352380
# make sure that the relevant handlers are instantiated, so that they
@@ -364,7 +392,8 @@ def __init__(self, hs: "HomeServer"):
364392
def register(self, http_server: HttpServer) -> None:
365393
super().register(http_server)
366394
if self._msc2858_enabled:
367-
# expose additional endpoint for MSC2858 support
395+
# expose additional endpoint for MSC2858 support: backwards-compat support
396+
# for clients which don't yet support the stable endpoints.
368397
http_server.register_paths(
369398
"GET",
370399
client_patterns(

tests/rest/client/v1/test_login.py

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -437,14 +437,16 @@ def test_get_login_flows(self):
437437
channel = self.make_request("GET", "/_matrix/client/r0/login")
438438
self.assertEqual(channel.code, 200, channel.result)
439439

440-
expected_flows = [
441-
{"type": "m.login.cas"},
442-
{"type": "m.login.sso"},
443-
{"type": "m.login.token"},
444-
{"type": "m.login.password"},
445-
] + ADDITIONAL_LOGIN_FLOWS
440+
expected_flow_types = [
441+
"m.login.cas",
442+
"m.login.sso",
443+
"m.login.token",
444+
"m.login.password",
445+
] + [f["type"] for f in ADDITIONAL_LOGIN_FLOWS]
446446

447-
self.assertCountEqual(channel.json_body["flows"], expected_flows)
447+
self.assertCountEqual(
448+
[f["type"] for f in channel.json_body["flows"]], expected_flow_types
449+
)
448450

449451
@override_config({"experimental_features": {"msc2858_enabled": True}})
450452
def test_get_msc2858_login_flows(self):
@@ -636,22 +638,25 @@ def test_multi_sso_redirect_to_unknown(self):
636638
)
637639
self.assertEqual(channel.code, 400, channel.result)
638640

639-
def test_client_idp_redirect_msc2858_disabled(self):
640-
"""If the client tries to pick an IdP but MSC2858 is disabled, return a 400"""
641-
channel = self._make_sso_redirect_request(True, "oidc")
642-
self.assertEqual(channel.code, 400, channel.result)
643-
self.assertEqual(channel.json_body["errcode"], "M_UNRECOGNIZED")
644-
645-
@override_config({"experimental_features": {"msc2858_enabled": True}})
646641
def test_client_idp_redirect_to_unknown(self):
647642
"""If the client tries to pick an unknown IdP, return a 404"""
648-
channel = self._make_sso_redirect_request(True, "xxx")
643+
channel = self._make_sso_redirect_request(False, "xxx")
649644
self.assertEqual(channel.code, 404, channel.result)
650645
self.assertEqual(channel.json_body["errcode"], "M_NOT_FOUND")
651646

652-
@override_config({"experimental_features": {"msc2858_enabled": True}})
653647
def test_client_idp_redirect_to_oidc(self):
654648
"""If the client pick a known IdP, redirect to it"""
649+
channel = self._make_sso_redirect_request(False, "oidc")
650+
self.assertEqual(channel.code, 302, channel.result)
651+
oidc_uri = channel.headers.getRawHeaders("Location")[0]
652+
oidc_uri_path, oidc_uri_query = oidc_uri.split("?", 1)
653+
654+
# it should redirect us to the auth page of the OIDC server
655+
self.assertEqual(oidc_uri_path, TEST_OIDC_AUTH_ENDPOINT)
656+
657+
@override_config({"experimental_features": {"msc2858_enabled": True}})
658+
def test_client_msc2858_redirect_to_oidc(self):
659+
"""Test the unstable API"""
655660
channel = self._make_sso_redirect_request(True, "oidc")
656661
self.assertEqual(channel.code, 302, channel.result)
657662
oidc_uri = channel.headers.getRawHeaders("Location")[0]
@@ -660,6 +665,12 @@ def test_client_idp_redirect_to_oidc(self):
660665
# it should redirect us to the auth page of the OIDC server
661666
self.assertEqual(oidc_uri_path, TEST_OIDC_AUTH_ENDPOINT)
662667

668+
def test_client_idp_redirect_msc2858_disabled(self):
669+
"""If the client tries to use the MSC2858 endpoint but MSC2858 is disabled, return a 400"""
670+
channel = self._make_sso_redirect_request(True, "oidc")
671+
self.assertEqual(channel.code, 400, channel.result)
672+
self.assertEqual(channel.json_body["errcode"], "M_UNRECOGNIZED")
673+
663674
def _make_sso_redirect_request(
664675
self, unstable_endpoint: bool = False, idp_prov: Optional[str] = None
665676
):

0 commit comments

Comments
 (0)