mirror of
https://github.com/element-hq/synapse.git
synced 2026-10-22 17:32:43 +00:00
Support MSC4450: Identity Provider selection for User-Interactive Authentication with Legacy Single Sign-On. (#19693)
Closes: #19688 Part of: MSC4450 whose Experimental Feature tracking issue is #19691 Add an unstable, namespaced `idp_id` query parameter to `fallback/web` \ This allows clients to specify the identity provider they'd like to log in with for SSO when they have multiple upstream IdPs associated with their account. Previously, Synapse would just pick one arbitrarily. But this was undesirable as you may want to use a different one at that point in time. When logging in, the user is able to choose when IdP they use - during UIA (which uses fallback auth mechanism) they should be able to do the same. ----- Signed-off-by: Olivier 'reivilibre <oliverw@matrix.org> Co-authored-by: Andrew Morgan <andrew@amorgan.xyz> Co-authored-by: Eric Eastwood <madlittlemods@gmail.com>
This commit is contained in:
5 files changed
+190
-13
No files matched your search
@@ -43,6 +43,62 @@ from tests.rest.client.utils import TEST_OIDC_CONFIG, TEST_OIDC_ISSUER
|
||||
from tests.server import FakeChannel
|
||||
from tests.unittest import override_config, skip_unless
|
||||
|
||||
TEST_MULTIPLE_OIDC_IDP_ID1 = "friendface"
|
||||
TEST_MULTIPLE_OIDC_IDP_ID2 = "potatocloud"
|
||||
TEST_MULTIPLE_OIDC_IDP_ID3 = "svnswitch"
|
||||
|
||||
TEST_MULTIPLE_OIDC_ISSUER1 = "https://friendface.invalid/"
|
||||
TEST_MULTIPLE_OIDC_ISSUER2 = "https://potato.invalid/"
|
||||
TEST_MULTIPLE_OIDC_ISSUER3 = "https://subversionswitch.invalid/"
|
||||
|
||||
TEST_MULTIPLE_OIDC_PROVIDERS = [
|
||||
{
|
||||
"idp_id": TEST_MULTIPLE_OIDC_IDP_ID1,
|
||||
"idp_name": "FriendFace",
|
||||
"idp_icon": "mxc://example.invalid/friendface",
|
||||
"idp_brand": "friendface",
|
||||
"issuer": TEST_MULTIPLE_OIDC_ISSUER1,
|
||||
"client_id": "id-for-friendface",
|
||||
"client_secret": "secret-for-friendface",
|
||||
"scopes": ["openid"],
|
||||
"discover": False,
|
||||
"authorization_endpoint": f"{TEST_MULTIPLE_OIDC_ISSUER1}oidc/auth",
|
||||
"token_endpoint": f"{TEST_MULTIPLE_OIDC_ISSUER1}oidc/token",
|
||||
"jwks_uri": f"{TEST_MULTIPLE_OIDC_ISSUER1}oidc/jwks",
|
||||
},
|
||||
{
|
||||
"idp_id": TEST_MULTIPLE_OIDC_IDP_ID2,
|
||||
"idp_name": "Potato Cloud",
|
||||
"idp_icon": "mxc://example.invalid/potatocloud",
|
||||
"idp_brand": "potato",
|
||||
"issuer": TEST_MULTIPLE_OIDC_ISSUER2,
|
||||
"client_id": "id-for-potato",
|
||||
"client_secret": "secret-for-potato",
|
||||
"scopes": ["openid"],
|
||||
"discover": False,
|
||||
"authorization_endpoint": f"{TEST_MULTIPLE_OIDC_ISSUER2}oidc/auth",
|
||||
"token_endpoint": f"{TEST_MULTIPLE_OIDC_ISSUER2}oidc/token",
|
||||
"jwks_uri": f"{TEST_MULTIPLE_OIDC_ISSUER2}oidc/jwks",
|
||||
},
|
||||
{
|
||||
"idp_id": TEST_MULTIPLE_OIDC_IDP_ID3,
|
||||
"idp_name": "SubversionSwitch",
|
||||
"idp_icon": "mxc://example.invalid/svnswitch",
|
||||
"idp_brand": "svnswitch",
|
||||
"issuer": TEST_MULTIPLE_OIDC_ISSUER3,
|
||||
"client_id": "id-for-svnswitch",
|
||||
"client_secret": "secret-for-svnswitch",
|
||||
"scopes": ["openid"],
|
||||
"discover": False,
|
||||
"authorization_endpoint": f"{TEST_MULTIPLE_OIDC_ISSUER3}oidc/auth",
|
||||
"token_endpoint": f"{TEST_MULTIPLE_OIDC_ISSUER3}oidc/token",
|
||||
"jwks_uri": f"{TEST_MULTIPLE_OIDC_ISSUER3}oidc/jwks",
|
||||
},
|
||||
]
|
||||
"""
|
||||
`oidc_providers` config example for multiple OIDC providers.
|
||||
"""
|
||||
|
||||
|
||||
class DummyRecaptchaChecker(UserInteractiveAuthChecker):
|
||||
def __init__(self, hs: HomeServer) -> None:
|
||||
@@ -506,6 +562,90 @@ class UIAuthTests(unittest.HomeserverTestCase):
|
||||
body={"auth": {"session": session_id}},
|
||||
)
|
||||
|
||||
@skip_unless(HAS_OIDC, "requires OIDC")
|
||||
@override_config(
|
||||
{
|
||||
"oidc_providers": TEST_MULTIPLE_OIDC_PROVIDERS,
|
||||
"experimental_features": {"msc4450_enabled": True},
|
||||
}
|
||||
)
|
||||
def test_msc4450_select_idp_id(self) -> None:
|
||||
"""
|
||||
Test for MSC4450: Identity Provider selection for
|
||||
User-Interactive Authentication with Legacy Single Sign-On.
|
||||
|
||||
We configure 3 OIDC providers and then check that we can select
|
||||
which one to redirect to for User-Interactive Authentication.
|
||||
"""
|
||||
|
||||
# Attach the user to the OIDC providers manually
|
||||
for idp_id in (
|
||||
TEST_MULTIPLE_OIDC_IDP_ID1,
|
||||
TEST_MULTIPLE_OIDC_IDP_ID2,
|
||||
):
|
||||
self.get_success(
|
||||
self.hs.get_datastores().main.record_user_external_id(
|
||||
# `oidc-` is a magic prefix needed for OIDC providers
|
||||
f"oidc-{idp_id}",
|
||||
# arbitrary/opaque provider-specific external ID, doesn't really matter
|
||||
"some.ext.user.id",
|
||||
self.user,
|
||||
)
|
||||
)
|
||||
|
||||
# Start a user-interactive authentication session by
|
||||
# calling the device deletion API
|
||||
channel = self.delete_device(
|
||||
self.user_tok, self.device_id, HTTPStatus.UNAUTHORIZED
|
||||
)
|
||||
auth_session = channel.json_body["session"]
|
||||
flows = channel.json_body["flows"]
|
||||
self.assertCountEqual(
|
||||
flows, [{"stages": ["m.login.sso"]}, {"stages": ["m.login.password"]}]
|
||||
)
|
||||
|
||||
# Try to start the User-Interactive Auth against both identity providers.
|
||||
# Make sure we get the identity provider that we ask for.
|
||||
#
|
||||
# We need to try against both to be sure that Synapse doesn't just conveniently happen to
|
||||
# arbitrarily select the identity provider we test.
|
||||
for idp_id, provider_config in (
|
||||
(
|
||||
TEST_MULTIPLE_OIDC_IDP_ID1,
|
||||
TEST_MULTIPLE_OIDC_PROVIDERS[0],
|
||||
),
|
||||
(
|
||||
TEST_MULTIPLE_OIDC_IDP_ID2,
|
||||
TEST_MULTIPLE_OIDC_PROVIDERS[1],
|
||||
),
|
||||
):
|
||||
endpoint = f"/_matrix/client/v3/auth/m.login.sso/fallback/web?session={auth_session}&io.element.idp_id=oidc-{idp_id}"
|
||||
channel = self.make_request("GET", endpoint)
|
||||
self.assertEqual(
|
||||
channel.code,
|
||||
HTTPStatus.OK,
|
||||
f"Failed to use the {endpoint} endpoint as part of the UIA flow for idp_id={idp_id} : response_body={channel.text_body}",
|
||||
)
|
||||
|
||||
# This 'Continue with ...' text is templated by `synapse/res/templates/sso_auth_confirm.html`
|
||||
self.assertIn(
|
||||
f"Continue with {provider_config['idp_name']}",
|
||||
channel.text_body,
|
||||
)
|
||||
|
||||
self.assertIn(provider_config["authorization_endpoint"], channel.text_body)
|
||||
|
||||
# Test that we can't use the 3rd OIDC provider as we're not
|
||||
# registered with it
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
f"/_matrix/client/v3/auth/m.login.sso/fallback/web?session={auth_session}&io.element.idp_id=oidc-{TEST_MULTIPLE_OIDC_IDP_ID3}",
|
||||
)
|
||||
self.assertEqual(channel.code, HTTPStatus.BAD_REQUEST, channel.json_body)
|
||||
self.assertEqual(
|
||||
channel.json_body["errcode"], Codes.INVALID_PARAM, channel.json_body
|
||||
)
|
||||
|
||||
@skip_unless(HAS_OIDC, "requires OIDC")
|
||||
@override_config({"oidc_config": TEST_OIDC_CONFIG})
|
||||
def test_does_not_offer_password_for_sso_user(self) -> None:
|
||||
|
||||
Reference in new issue
Block a user