mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2024-10-01 11:49:51 -04:00
Complete the SAML2 implementation (#5422)
* SAML2 Improvements and redirect stuff Signed-off-by: Alexander Trost <galexrt@googlemail.com> * Code cleanups and simplifications. Also: share the saml client between redirect and response handlers. * changelog * Revert redundant changes to static js * Move all the saml stuff out to a centralised handler * Add support for tracking SAML2 sessions. This allows us to correctly handle `allow_unsolicited: False`. * update sample config * cleanups * update sample config * rename BaseSSORedirectServlet for consistency * Address review comments
This commit is contained in:
commit
6eecb6e500
1
changelog.d/5422.feature
Normal file
1
changelog.d/5422.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Fully support SAML2 authentication. Contributed by [Alexander Trost](https://github.com/galexrt) - thank you!
|
@ -997,6 +997,12 @@ signing_key_path: "CONFDIR/SERVERNAME.signing.key"
|
|||||||
# so it is not normally necessary to specify them unless you need to
|
# so it is not normally necessary to specify them unless you need to
|
||||||
# override them.
|
# override them.
|
||||||
#
|
#
|
||||||
|
# Once SAML support is enabled, a metadata file will be exposed at
|
||||||
|
# https://<server>:<port>/_matrix/saml2/metadata.xml, which you may be able to
|
||||||
|
# use to configure your SAML IdP with. Alternatively, you can manually configure
|
||||||
|
# the IdP to use an ACS location of
|
||||||
|
# https://<server>:<port>/_matrix/saml2/authn_response.
|
||||||
|
#
|
||||||
#saml2_config:
|
#saml2_config:
|
||||||
# sp_config:
|
# sp_config:
|
||||||
# # point this to the IdP's metadata. You can use either a local file or
|
# # point this to the IdP's metadata. You can use either a local file or
|
||||||
@ -1006,7 +1012,15 @@ signing_key_path: "CONFDIR/SERVERNAME.signing.key"
|
|||||||
# remote:
|
# remote:
|
||||||
# - url: https://our_idp/metadata.xml
|
# - url: https://our_idp/metadata.xml
|
||||||
#
|
#
|
||||||
# # The rest of sp_config is just used to generate our metadata xml, and you
|
# # By default, the user has to go to our login page first. If you'd like to
|
||||||
|
# # allow IdP-initiated login, set 'allow_unsolicited: True' in a
|
||||||
|
# # 'service.sp' section:
|
||||||
|
# #
|
||||||
|
# #service:
|
||||||
|
# # sp:
|
||||||
|
# # allow_unsolicited: True
|
||||||
|
#
|
||||||
|
# # The examples below are just used to generate our metadata xml, and you
|
||||||
# # may well not need it, depending on your setup. Alternatively you
|
# # may well not need it, depending on your setup. Alternatively you
|
||||||
# # may need a whole lot more detail - see the pysaml2 docs!
|
# # may need a whole lot more detail - see the pysaml2 docs!
|
||||||
#
|
#
|
||||||
@ -1029,6 +1043,12 @@ signing_key_path: "CONFDIR/SERVERNAME.signing.key"
|
|||||||
# # separate pysaml2 configuration file:
|
# # separate pysaml2 configuration file:
|
||||||
# #
|
# #
|
||||||
# config_path: "CONFDIR/sp_conf.py"
|
# config_path: "CONFDIR/sp_conf.py"
|
||||||
|
#
|
||||||
|
# # the lifetime of a SAML session. This defines how long a user has to
|
||||||
|
# # complete the authentication process, if allow_unsolicited is unset.
|
||||||
|
# # The default is 5 minutes.
|
||||||
|
# #
|
||||||
|
# # saml_session_lifetime: 5m
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
from synapse.python_dependencies import DependencyException, check_requirements
|
||||||
|
|
||||||
from ._base import Config, ConfigError
|
from ._base import Config, ConfigError
|
||||||
|
|
||||||
@ -25,6 +26,11 @@ class SAML2Config(Config):
|
|||||||
if not saml2_config or not saml2_config.get("enabled", True):
|
if not saml2_config or not saml2_config.get("enabled", True):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
check_requirements("saml2")
|
||||||
|
except DependencyException as e:
|
||||||
|
raise ConfigError(e.message)
|
||||||
|
|
||||||
self.saml2_enabled = True
|
self.saml2_enabled = True
|
||||||
|
|
||||||
import saml2.config
|
import saml2.config
|
||||||
@ -37,6 +43,11 @@ class SAML2Config(Config):
|
|||||||
if config_path is not None:
|
if config_path is not None:
|
||||||
self.saml2_sp_config.load_file(config_path)
|
self.saml2_sp_config.load_file(config_path)
|
||||||
|
|
||||||
|
# session lifetime: in milliseconds
|
||||||
|
self.saml2_session_lifetime = self.parse_duration(
|
||||||
|
saml2_config.get("saml_session_lifetime", "5m")
|
||||||
|
)
|
||||||
|
|
||||||
def _default_saml_config_dict(self):
|
def _default_saml_config_dict(self):
|
||||||
import saml2
|
import saml2
|
||||||
|
|
||||||
@ -72,6 +83,12 @@ class SAML2Config(Config):
|
|||||||
# so it is not normally necessary to specify them unless you need to
|
# so it is not normally necessary to specify them unless you need to
|
||||||
# override them.
|
# override them.
|
||||||
#
|
#
|
||||||
|
# Once SAML support is enabled, a metadata file will be exposed at
|
||||||
|
# https://<server>:<port>/_matrix/saml2/metadata.xml, which you may be able to
|
||||||
|
# use to configure your SAML IdP with. Alternatively, you can manually configure
|
||||||
|
# the IdP to use an ACS location of
|
||||||
|
# https://<server>:<port>/_matrix/saml2/authn_response.
|
||||||
|
#
|
||||||
#saml2_config:
|
#saml2_config:
|
||||||
# sp_config:
|
# sp_config:
|
||||||
# # point this to the IdP's metadata. You can use either a local file or
|
# # point this to the IdP's metadata. You can use either a local file or
|
||||||
@ -81,7 +98,15 @@ class SAML2Config(Config):
|
|||||||
# remote:
|
# remote:
|
||||||
# - url: https://our_idp/metadata.xml
|
# - url: https://our_idp/metadata.xml
|
||||||
#
|
#
|
||||||
# # The rest of sp_config is just used to generate our metadata xml, and you
|
# # By default, the user has to go to our login page first. If you'd like to
|
||||||
|
# # allow IdP-initiated login, set 'allow_unsolicited: True' in a
|
||||||
|
# # 'service.sp' section:
|
||||||
|
# #
|
||||||
|
# #service:
|
||||||
|
# # sp:
|
||||||
|
# # allow_unsolicited: True
|
||||||
|
#
|
||||||
|
# # The examples below are just used to generate our metadata xml, and you
|
||||||
# # may well not need it, depending on your setup. Alternatively you
|
# # may well not need it, depending on your setup. Alternatively you
|
||||||
# # may need a whole lot more detail - see the pysaml2 docs!
|
# # may need a whole lot more detail - see the pysaml2 docs!
|
||||||
#
|
#
|
||||||
@ -104,6 +129,12 @@ class SAML2Config(Config):
|
|||||||
# # separate pysaml2 configuration file:
|
# # separate pysaml2 configuration file:
|
||||||
# #
|
# #
|
||||||
# config_path: "%(config_dir_path)s/sp_conf.py"
|
# config_path: "%(config_dir_path)s/sp_conf.py"
|
||||||
|
#
|
||||||
|
# # the lifetime of a SAML session. This defines how long a user has to
|
||||||
|
# # complete the authentication process, if allow_unsolicited is unset.
|
||||||
|
# # The default is 5 minutes.
|
||||||
|
# #
|
||||||
|
# # saml_session_lifetime: 5m
|
||||||
""" % {
|
""" % {
|
||||||
"config_dir_path": config_dir_path
|
"config_dir_path": config_dir_path
|
||||||
}
|
}
|
||||||
|
123
synapse/handlers/saml_handler.py
Normal file
123
synapse/handlers/saml_handler.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# 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 logging
|
||||||
|
|
||||||
|
import attr
|
||||||
|
import saml2
|
||||||
|
from saml2.client import Saml2Client
|
||||||
|
|
||||||
|
from synapse.api.errors import SynapseError
|
||||||
|
from synapse.http.servlet import parse_string
|
||||||
|
from synapse.rest.client.v1.login import SSOAuthHandler
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SamlHandler:
|
||||||
|
def __init__(self, hs):
|
||||||
|
self._saml_client = Saml2Client(hs.config.saml2_sp_config)
|
||||||
|
self._sso_auth_handler = SSOAuthHandler(hs)
|
||||||
|
|
||||||
|
# a map from saml session id to Saml2SessionData object
|
||||||
|
self._outstanding_requests_dict = {}
|
||||||
|
|
||||||
|
self._clock = hs.get_clock()
|
||||||
|
self._saml2_session_lifetime = hs.config.saml2_session_lifetime
|
||||||
|
|
||||||
|
def handle_redirect_request(self, client_redirect_url):
|
||||||
|
"""Handle an incoming request to /login/sso/redirect
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client_redirect_url (bytes): the URL that we should redirect the
|
||||||
|
client to when everything is done
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bytes: URL to redirect to
|
||||||
|
"""
|
||||||
|
reqid, info = self._saml_client.prepare_for_authenticate(
|
||||||
|
relay_state=client_redirect_url
|
||||||
|
)
|
||||||
|
|
||||||
|
now = self._clock.time_msec()
|
||||||
|
self._outstanding_requests_dict[reqid] = Saml2SessionData(creation_time=now)
|
||||||
|
|
||||||
|
for key, value in info["headers"]:
|
||||||
|
if key == "Location":
|
||||||
|
return value
|
||||||
|
|
||||||
|
# this shouldn't happen!
|
||||||
|
raise Exception("prepare_for_authenticate didn't return a Location header")
|
||||||
|
|
||||||
|
def handle_saml_response(self, request):
|
||||||
|
"""Handle an incoming request to /_matrix/saml2/authn_response
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (SynapseRequest): the incoming request from the browser. We'll
|
||||||
|
respond to it with a redirect.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Deferred[none]: Completes once we have handled the request.
|
||||||
|
"""
|
||||||
|
resp_bytes = parse_string(request, "SAMLResponse", required=True)
|
||||||
|
relay_state = parse_string(request, "RelayState", required=True)
|
||||||
|
|
||||||
|
# expire outstanding sessions before parse_authn_request_response checks
|
||||||
|
# the dict.
|
||||||
|
self.expire_sessions()
|
||||||
|
|
||||||
|
try:
|
||||||
|
saml2_auth = self._saml_client.parse_authn_request_response(
|
||||||
|
resp_bytes,
|
||||||
|
saml2.BINDING_HTTP_POST,
|
||||||
|
outstanding=self._outstanding_requests_dict,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Exception parsing SAML2 response: %s", e)
|
||||||
|
raise SynapseError(400, "Unable to parse SAML2 response: %s" % (e,))
|
||||||
|
|
||||||
|
if saml2_auth.not_signed:
|
||||||
|
logger.warning("SAML2 response was not signed")
|
||||||
|
raise SynapseError(400, "SAML2 response was not signed")
|
||||||
|
|
||||||
|
if "uid" not in saml2_auth.ava:
|
||||||
|
logger.warning("SAML2 response lacks a 'uid' attestation")
|
||||||
|
raise SynapseError(400, "uid not in SAML2 response")
|
||||||
|
|
||||||
|
self._outstanding_requests_dict.pop(saml2_auth.in_response_to, None)
|
||||||
|
|
||||||
|
username = saml2_auth.ava["uid"][0]
|
||||||
|
displayName = saml2_auth.ava.get("displayName", [None])[0]
|
||||||
|
|
||||||
|
return self._sso_auth_handler.on_successful_auth(
|
||||||
|
username, request, relay_state, user_display_name=displayName
|
||||||
|
)
|
||||||
|
|
||||||
|
def expire_sessions(self):
|
||||||
|
expire_before = self._clock.time_msec() - self._saml2_session_lifetime
|
||||||
|
to_expire = set()
|
||||||
|
for reqid, data in self._outstanding_requests_dict.items():
|
||||||
|
if data.creation_time < expire_before:
|
||||||
|
to_expire.add(reqid)
|
||||||
|
for reqid in to_expire:
|
||||||
|
logger.debug("Expiring session id %s", reqid)
|
||||||
|
del self._outstanding_requests_dict[reqid]
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class Saml2SessionData:
|
||||||
|
"""Data we track about SAML2 sessions"""
|
||||||
|
|
||||||
|
# time the session was created, in milliseconds
|
||||||
|
creation_time = attr.ib()
|
@ -86,6 +86,7 @@ class LoginRestServlet(RestServlet):
|
|||||||
self.jwt_enabled = hs.config.jwt_enabled
|
self.jwt_enabled = hs.config.jwt_enabled
|
||||||
self.jwt_secret = hs.config.jwt_secret
|
self.jwt_secret = hs.config.jwt_secret
|
||||||
self.jwt_algorithm = hs.config.jwt_algorithm
|
self.jwt_algorithm = hs.config.jwt_algorithm
|
||||||
|
self.saml2_enabled = hs.config.saml2_enabled
|
||||||
self.cas_enabled = hs.config.cas_enabled
|
self.cas_enabled = hs.config.cas_enabled
|
||||||
self.auth_handler = self.hs.get_auth_handler()
|
self.auth_handler = self.hs.get_auth_handler()
|
||||||
self.registration_handler = hs.get_registration_handler()
|
self.registration_handler = hs.get_registration_handler()
|
||||||
@ -97,6 +98,9 @@ class LoginRestServlet(RestServlet):
|
|||||||
flows = []
|
flows = []
|
||||||
if self.jwt_enabled:
|
if self.jwt_enabled:
|
||||||
flows.append({"type": LoginRestServlet.JWT_TYPE})
|
flows.append({"type": LoginRestServlet.JWT_TYPE})
|
||||||
|
if self.saml2_enabled:
|
||||||
|
flows.append({"type": LoginRestServlet.SSO_TYPE})
|
||||||
|
flows.append({"type": LoginRestServlet.TOKEN_TYPE})
|
||||||
if self.cas_enabled:
|
if self.cas_enabled:
|
||||||
flows.append({"type": LoginRestServlet.SSO_TYPE})
|
flows.append({"type": LoginRestServlet.SSO_TYPE})
|
||||||
|
|
||||||
@ -351,27 +355,49 @@ class LoginRestServlet(RestServlet):
|
|||||||
defer.returnValue(result)
|
defer.returnValue(result)
|
||||||
|
|
||||||
|
|
||||||
class CasRedirectServlet(RestServlet):
|
class BaseSSORedirectServlet(RestServlet):
|
||||||
|
"""Common base class for /login/sso/redirect impls"""
|
||||||
|
|
||||||
PATTERNS = client_patterns("/login/(cas|sso)/redirect", v1=True)
|
PATTERNS = client_patterns("/login/(cas|sso)/redirect", v1=True)
|
||||||
|
|
||||||
|
def on_GET(self, request):
|
||||||
|
args = request.args
|
||||||
|
if b"redirectUrl" not in args:
|
||||||
|
return 400, "Redirect URL not specified for SSO auth"
|
||||||
|
client_redirect_url = args[b"redirectUrl"][0]
|
||||||
|
sso_url = self.get_sso_url(client_redirect_url)
|
||||||
|
request.redirect(sso_url)
|
||||||
|
finish_request(request)
|
||||||
|
|
||||||
|
def get_sso_url(self, client_redirect_url):
|
||||||
|
"""Get the URL to redirect to, to perform SSO auth
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client_redirect_url (bytes): the URL that we should redirect the
|
||||||
|
client to when everything is done
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bytes: URL to redirect to
|
||||||
|
"""
|
||||||
|
# to be implemented by subclasses
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class CasRedirectServlet(BaseSSORedirectServlet):
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
super(CasRedirectServlet, self).__init__()
|
super(CasRedirectServlet, self).__init__()
|
||||||
self.cas_server_url = hs.config.cas_server_url.encode("ascii")
|
self.cas_server_url = hs.config.cas_server_url.encode("ascii")
|
||||||
self.cas_service_url = hs.config.cas_service_url.encode("ascii")
|
self.cas_service_url = hs.config.cas_service_url.encode("ascii")
|
||||||
|
|
||||||
def on_GET(self, request):
|
def get_sso_url(self, client_redirect_url):
|
||||||
args = request.args
|
|
||||||
if b"redirectUrl" not in args:
|
|
||||||
return (400, "Redirect URL not specified for CAS auth")
|
|
||||||
client_redirect_url_param = urllib.parse.urlencode(
|
client_redirect_url_param = urllib.parse.urlencode(
|
||||||
{b"redirectUrl": args[b"redirectUrl"][0]}
|
{b"redirectUrl": client_redirect_url}
|
||||||
).encode("ascii")
|
).encode("ascii")
|
||||||
hs_redirect_url = self.cas_service_url + b"/_matrix/client/r0/login/cas/ticket"
|
hs_redirect_url = self.cas_service_url + b"/_matrix/client/r0/login/cas/ticket"
|
||||||
service_param = urllib.parse.urlencode(
|
service_param = urllib.parse.urlencode(
|
||||||
{b"service": b"%s?%s" % (hs_redirect_url, client_redirect_url_param)}
|
{b"service": b"%s?%s" % (hs_redirect_url, client_redirect_url_param)}
|
||||||
).encode("ascii")
|
).encode("ascii")
|
||||||
request.redirect(b"%s/login?%s" % (self.cas_server_url, service_param))
|
return b"%s/login?%s" % (self.cas_server_url, service_param)
|
||||||
finish_request(request)
|
|
||||||
|
|
||||||
|
|
||||||
class CasTicketServlet(RestServlet):
|
class CasTicketServlet(RestServlet):
|
||||||
@ -454,6 +480,16 @@ class CasTicketServlet(RestServlet):
|
|||||||
return user, attributes
|
return user, attributes
|
||||||
|
|
||||||
|
|
||||||
|
class SAMLRedirectServlet(BaseSSORedirectServlet):
|
||||||
|
PATTERNS = client_patterns("/login/sso/redirect", v1=True)
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
self._saml_handler = hs.get_saml_handler()
|
||||||
|
|
||||||
|
def get_sso_url(self, client_redirect_url):
|
||||||
|
return self._saml_handler.handle_redirect_request(client_redirect_url)
|
||||||
|
|
||||||
|
|
||||||
class SSOAuthHandler(object):
|
class SSOAuthHandler(object):
|
||||||
"""
|
"""
|
||||||
Utility class for Resources and Servlets which handle the response from a SSO
|
Utility class for Resources and Servlets which handle the response from a SSO
|
||||||
@ -529,3 +565,5 @@ def register_servlets(hs, http_server):
|
|||||||
if hs.config.cas_enabled:
|
if hs.config.cas_enabled:
|
||||||
CasRedirectServlet(hs).register(http_server)
|
CasRedirectServlet(hs).register(http_server)
|
||||||
CasTicketServlet(hs).register(http_server)
|
CasTicketServlet(hs).register(http_server)
|
||||||
|
elif hs.config.saml2_enabled:
|
||||||
|
SAMLRedirectServlet(hs).register(http_server)
|
||||||
|
@ -13,17 +13,8 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import logging
|
|
||||||
|
|
||||||
import saml2
|
|
||||||
from saml2.client import Saml2Client
|
|
||||||
|
|
||||||
from synapse.api.errors import CodeMessageException
|
|
||||||
from synapse.http.server import DirectServeResource, wrap_html_request_handler
|
from synapse.http.server import DirectServeResource, wrap_html_request_handler
|
||||||
from synapse.http.servlet import parse_string
|
|
||||||
from synapse.rest.client.v1.login import SSOAuthHandler
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class SAML2ResponseResource(DirectServeResource):
|
class SAML2ResponseResource(DirectServeResource):
|
||||||
@ -33,32 +24,8 @@ class SAML2ResponseResource(DirectServeResource):
|
|||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self._saml_handler = hs.get_saml_handler()
|
||||||
self._saml_client = Saml2Client(hs.config.saml2_sp_config)
|
|
||||||
self._sso_auth_handler = SSOAuthHandler(hs)
|
|
||||||
|
|
||||||
@wrap_html_request_handler
|
@wrap_html_request_handler
|
||||||
async def _async_render_POST(self, request):
|
async def _async_render_POST(self, request):
|
||||||
resp_bytes = parse_string(request, "SAMLResponse", required=True)
|
return await self._saml_handler.handle_saml_response(request)
|
||||||
relay_state = parse_string(request, "RelayState", required=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
saml2_auth = self._saml_client.parse_authn_request_response(
|
|
||||||
resp_bytes, saml2.BINDING_HTTP_POST
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning("Exception parsing SAML2 response", exc_info=1)
|
|
||||||
raise CodeMessageException(400, "Unable to parse SAML2 response: %s" % (e,))
|
|
||||||
|
|
||||||
if saml2_auth.not_signed:
|
|
||||||
raise CodeMessageException(400, "SAML2 response was not signed")
|
|
||||||
|
|
||||||
if "uid" not in saml2_auth.ava:
|
|
||||||
raise CodeMessageException(400, "uid not in SAML2 response")
|
|
||||||
|
|
||||||
username = saml2_auth.ava["uid"][0]
|
|
||||||
|
|
||||||
displayName = saml2_auth.ava.get("displayName", [None])[0]
|
|
||||||
return self._sso_auth_handler.on_successful_auth(
|
|
||||||
username, request, relay_state, user_display_name=displayName
|
|
||||||
)
|
|
||||||
|
@ -194,6 +194,7 @@ class HomeServer(object):
|
|||||||
"sendmail",
|
"sendmail",
|
||||||
"registration_handler",
|
"registration_handler",
|
||||||
"account_validity_handler",
|
"account_validity_handler",
|
||||||
|
"saml_handler",
|
||||||
"event_client_serializer",
|
"event_client_serializer",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -524,6 +525,11 @@ class HomeServer(object):
|
|||||||
def build_account_validity_handler(self):
|
def build_account_validity_handler(self):
|
||||||
return AccountValidityHandler(self)
|
return AccountValidityHandler(self)
|
||||||
|
|
||||||
|
def build_saml_handler(self):
|
||||||
|
from synapse.handlers.saml_handler import SamlHandler
|
||||||
|
|
||||||
|
return SamlHandler(self)
|
||||||
|
|
||||||
def build_event_client_serializer(self):
|
def build_event_client_serializer(self):
|
||||||
return EventClientSerializer(self)
|
return EventClientSerializer(self)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user