-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Combine the SSO Redirect Servlets #9015
Changes from all commits
ffc8645
a575502
431f1a5
e3e698d
34dd3ca
47742b1
1df4714
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Add support for multiple SSO Identity Providers. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,15 +12,16 @@ | |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
import abc | ||
import logging | ||
from typing import TYPE_CHECKING, Awaitable, Callable, Dict, List, Optional | ||
|
||
import attr | ||
from typing_extensions import NoReturn | ||
from typing_extensions import NoReturn, Protocol | ||
|
||
from twisted.web.http import Request | ||
|
||
from synapse.api.errors import RedirectException, SynapseError | ||
from synapse.api.errors import Codes, RedirectException, SynapseError | ||
from synapse.http.server import respond_with_html | ||
from synapse.http.site import SynapseRequest | ||
from synapse.types import JsonDict, UserID, contains_invalid_mxid_characters | ||
|
@@ -40,6 +41,53 @@ class MappingException(Exception): | |
""" | ||
|
||
|
||
class SsoIdentityProvider(Protocol): | ||
"""Abstract base class to be implemented by SSO Identity Providers | ||
|
||
An Identity Provider, or IdP, is an external HTTP service which authenticates a user | ||
to say whether they should be allowed to log in, or perform a given action. | ||
|
||
Synapse supports various implementations of IdPs, including OpenID Connect, SAML, | ||
and CAS. | ||
|
||
The main entry point is `handle_redirect_request`, which should return a URI to | ||
redirect the user's browser to the IdP's authentication page. | ||
|
||
Each IdP should be registered with the SsoHandler via | ||
`hs.get_sso_handler().register_identity_provider()`, so that requests to | ||
`/_matrix/client/r0/login/sso/redirect` can be correctly dispatched. | ||
""" | ||
|
||
@property | ||
@abc.abstractmethod | ||
def idp_id(self) -> str: | ||
"""A unique identifier for this SSO provider | ||
|
||
Eg, "saml", "cas", "github" | ||
""" | ||
|
||
@abc.abstractmethod | ||
async def handle_redirect_request( | ||
self, | ||
request: SynapseRequest, | ||
client_redirect_url: Optional[bytes], | ||
ui_auth_session_id: Optional[str] = None, | ||
) -> str: | ||
"""Handle an incoming request to /login/sso/redirect | ||
|
||
Args: | ||
request: the incoming HTTP request | ||
client_redirect_url: the URL that we should redirect the | ||
client to after login (or None for UI Auth). | ||
ui_auth_session_id: The session ID of the ongoing UI Auth (or | ||
None if this is a login). | ||
|
||
Returns: | ||
URL to redirect to | ||
""" | ||
raise NotImplementedError() | ||
|
||
|
||
@attr.s | ||
class UserAttributes: | ||
# the localpart of the mxid that the mapper has assigned to the user. | ||
|
@@ -100,6 +148,14 @@ def __init__(self, hs: "HomeServer"): | |
# a map from session id to session data | ||
self._username_mapping_sessions = {} # type: Dict[str, UsernameMappingSession] | ||
|
||
# map from idp_id to SsoIdentityProvider | ||
self._identity_providers = {} # type: Dict[str, SsoIdentityProvider] | ||
|
||
def register_identity_provider(self, p: SsoIdentityProvider): | ||
p_id = p.idp_id | ||
assert p_id not in self._identity_providers | ||
self._identity_providers[p_id] = p | ||
|
||
def render_error( | ||
self, | ||
request: Request, | ||
|
@@ -124,6 +180,32 @@ def render_error( | |
) | ||
respond_with_html(request, code, html) | ||
|
||
async def handle_redirect_request( | ||
self, request: SynapseRequest, client_redirect_url: bytes, | ||
) -> str: | ||
"""Handle a request to /login/sso/redirect | ||
|
||
Args: | ||
request: incoming HTTP request | ||
client_redirect_url: the URL that we should redirect the | ||
client to after login. | ||
|
||
Returns: | ||
the URI to redirect to | ||
""" | ||
if not self._identity_providers: | ||
raise SynapseError( | ||
400, "Homeserver not configured for SSO.", errcode=Codes.UNRECOGNIZED | ||
) | ||
|
||
# if we only have one auth provider, redirect to it directly | ||
if len(self._identity_providers) == 1: | ||
ap = next(iter(self._identity_providers.values())) | ||
return await ap.handle_redirect_request(request, client_redirect_url) | ||
|
||
# otherwise, we have a configuration error | ||
raise Exception("Multiple SSO identity providers have been configured!") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is a slight behavior change, previously if you had multiple SSO providers enabled than we would prioritize CAS, then SAML, then OIDC. I think this was broken / kind of unsupported though so I don't mind making it more explicit. Should we raise this error during I assume there's a master plan here to support pivoting here eventually when there's multiple SSO provider support? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yup, a PR is about to arrive! I literally just threw in this exception so that there's something sensible between PRs. I think it's fine for now. |
||
|
||
async def get_sso_user_by_remote_user_id( | ||
self, auth_provider_id: str, remote_user_id: str | ||
) -> Optional[str]: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it always returned a
str
, but the declaration was incorrect. Again, it worked because you canrequest.redirect
to a bytes or a str.