mirror of https://github.com/watcha-fr/synapse
Implement SAML2 authentication (#4267)
This implements both a SAML2 metadata endpoint (at `/_matrix/saml2/metadata.xml`), and a SAML2 response receiver (at `/_matrix/saml2/authn_response`). If the SAML2 response matches what's been configured, we complete the SSO login flow by redirecting to the client url (aka `RelayState` in SAML2 jargon) with a login token. What we don't yet have is anything to build a SAML2 request and redirect the user to the identity provider. That is left as an exercise for the reader.pull/14/head
parent
c588b9b9e4
commit
c7401a697f
@ -0,0 +1 @@ |
||||
Rework SAML2 authentication |
@ -0,0 +1,110 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Copyright 2018 New Vector Ltd. |
||||
# |
||||
# 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. |
||||
|
||||
from ._base import Config, ConfigError |
||||
|
||||
|
||||
class SAML2Config(Config): |
||||
def read_config(self, config): |
||||
self.saml2_enabled = False |
||||
|
||||
saml2_config = config.get("saml2_config") |
||||
|
||||
if not saml2_config or not saml2_config.get("enabled", True): |
||||
return |
||||
|
||||
self.saml2_enabled = True |
||||
|
||||
import saml2.config |
||||
self.saml2_sp_config = saml2.config.SPConfig() |
||||
self.saml2_sp_config.load(self._default_saml_config_dict()) |
||||
self.saml2_sp_config.load(saml2_config.get("sp_config", {})) |
||||
|
||||
config_path = saml2_config.get("config_path", None) |
||||
if config_path is not None: |
||||
self.saml2_sp_config.load_file(config_path) |
||||
|
||||
def _default_saml_config_dict(self): |
||||
import saml2 |
||||
|
||||
public_baseurl = self.public_baseurl |
||||
if public_baseurl is None: |
||||
raise ConfigError( |
||||
"saml2_config requires a public_baseurl to be set" |
||||
) |
||||
|
||||
metadata_url = public_baseurl + "_matrix/saml2/metadata.xml" |
||||
response_url = public_baseurl + "_matrix/saml2/authn_response" |
||||
return { |
||||
"entityid": metadata_url, |
||||
|
||||
"service": { |
||||
"sp": { |
||||
"endpoints": { |
||||
"assertion_consumer_service": [ |
||||
(response_url, saml2.BINDING_HTTP_POST), |
||||
], |
||||
}, |
||||
"required_attributes": ["uid"], |
||||
"optional_attributes": ["mail", "surname", "givenname"], |
||||
}, |
||||
} |
||||
} |
||||
|
||||
def default_config(self, config_dir_path, server_name, **kwargs): |
||||
return """ |
||||
# Enable SAML2 for registration and login. Uses pysaml2. |
||||
# |
||||
# saml2_config: |
||||
# |
||||
# # The following is the configuration for the pysaml2 Service Provider. |
||||
# # See pysaml2 docs for format of config. |
||||
# # |
||||
# # Default values will be used for the 'entityid' and 'service' settings, |
||||
# # so it is not normally necessary to specify them unless you need to |
||||
# # override them. |
||||
# |
||||
# sp_config: |
||||
# # point this to the IdP's metadata. You can use either a local file or |
||||
# # (preferably) a URL. |
||||
# metadata: |
||||
# # local: ["saml2/idp.xml"] |
||||
# remote: |
||||
# - url: https://our_idp/metadata.xml |
||||
# |
||||
# # The following is just used to generate our metadata xml, and you |
||||
# # may well not need it, depending on your setup. Alternatively you |
||||
# # may need a whole lot more detail - see the pysaml2 docs! |
||||
# |
||||
# description: ["My awesome SP", "en"] |
||||
# name: ["Test SP", "en"] |
||||
# |
||||
# organization: |
||||
# name: Example com |
||||
# display_name: |
||||
# - ["Example co", "en"] |
||||
# url: "http://example.com" |
||||
# |
||||
# contact_person: |
||||
# - given_name: Bob |
||||
# sur_name: "the Sysadmin" |
||||
# email_address": ["admin@example.com"] |
||||
# contact_type": technical |
||||
# |
||||
# # Instead of putting the config inline as above, you can specify a |
||||
# # separate pysaml2 configuration file: |
||||
# # |
||||
# # config_path: "%(config_dir_path)s/sp_conf.py" |
||||
""" % {"config_dir_path": config_dir_path} |
@ -0,0 +1,29 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Copyright 2018 New Vector Ltd |
||||
# |
||||
# 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 |
||||
|
||||
from twisted.web.resource import Resource |
||||
|
||||
from synapse.rest.saml2.metadata_resource import SAML2MetadataResource |
||||
from synapse.rest.saml2.response_resource import SAML2ResponseResource |
||||
|
||||
logger = logging.getLogger(__name__) |
||||
|
||||
|
||||
class SAML2Resource(Resource): |
||||
def __init__(self, hs): |
||||
Resource.__init__(self) |
||||
self.putChild(b"metadata.xml", SAML2MetadataResource(hs)) |
||||
self.putChild(b"authn_response", SAML2ResponseResource(hs)) |
@ -0,0 +1,36 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Copyright 2018 New Vector Ltd |
||||
# |
||||
# 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 saml2.metadata |
||||
|
||||
from twisted.web.resource import Resource |
||||
|
||||
|
||||
class SAML2MetadataResource(Resource): |
||||
"""A Twisted web resource which renders the SAML metadata""" |
||||
|
||||
isLeaf = 1 |
||||
|
||||
def __init__(self, hs): |
||||
Resource.__init__(self) |
||||
self.sp_config = hs.config.saml2_sp_config |
||||
|
||||
def render_GET(self, request): |
||||
metadata_xml = saml2.metadata.create_metadata_string( |
||||
configfile=None, config=self.sp_config, |
||||
) |
||||
request.setHeader(b"Content-Type", b"text/xml; charset=utf-8") |
||||
return metadata_xml |
@ -0,0 +1,71 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# |
||||
# Copyright 2018 New Vector Ltd |
||||
# |
||||
# 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 saml2 |
||||
from saml2.client import Saml2Client |
||||
|
||||
from twisted.web.resource import Resource |
||||
from twisted.web.server import NOT_DONE_YET |
||||
|
||||
from synapse.api.errors import CodeMessageException |
||||
from synapse.http.server import 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(Resource): |
||||
"""A Twisted web resource which handles the SAML response""" |
||||
|
||||
isLeaf = 1 |
||||
|
||||
def __init__(self, hs): |
||||
Resource.__init__(self) |
||||
|
||||
self._saml_client = Saml2Client(hs.config.saml2_sp_config) |
||||
self._sso_auth_handler = SSOAuthHandler(hs) |
||||
|
||||
def render_POST(self, request): |
||||
self._async_render_POST(request) |
||||
return NOT_DONE_YET |
||||
|
||||
@wrap_html_request_handler |
||||
def _async_render_POST(self, request): |
||||
resp_bytes = parse_string(request, 'SAMLResponse', required=True) |
||||
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] |
||||
return self._sso_auth_handler.on_successful_auth( |
||||
username, request, relay_state, |
||||
) |
Loading…
Reference in new issue