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

Complete the SAML2 implementation #5422

Merged
merged 15 commits into from
Jul 2, 2019
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
Synapse 1.0.0rc3 (2019-06-10)
=============================

Security: Fix authentication bug introduced in 1.0.0rc1. Please upgrade to rc3 immediately


Synapse 1.0.0rc2 (2019-06-10)
=============================

Expand Down
1 change: 1 addition & 0 deletions changelog.d/5418.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix bug where attempting to send transactions with large number of EDUs can fail.
1 change: 1 addition & 0 deletions changelog.d/5422.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fully support SAML2 authentication. Contributed by [Alexander Trost](https://github.com/galexrt) - thank you!
2 changes: 1 addition & 1 deletion synapse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@
except ImportError:
pass

__version__ = "1.0.0rc2"
__version__ = "1.0.0rc3"
6 changes: 6 additions & 0 deletions synapse/config/saml2_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# 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 synapse.python_dependencies import DependencyException, check_requirements

from ._base import Config, ConfigError

Expand All @@ -25,6 +26,11 @@ def read_config(self, config):
if not saml2_config or not saml2_config.get("enabled", True):
return

try:
check_requirements('saml2')
except DependencyException as e:
raise ConfigError(e.message)

self.saml2_enabled = True

import saml2.config
Expand Down
1 change: 1 addition & 0 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ def _check_threepid(self, medium, authdict, password_servlet=False, **kwargs):
medium,
threepid_creds["client_secret"],
sid=threepid_creds["sid"],
validated=True,
)

threepid = {
Expand Down
62 changes: 54 additions & 8 deletions synapse/rest/client/v1/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def __init__(self, hs):
self.jwt_enabled = hs.config.jwt_enabled
self.jwt_secret = hs.config.jwt_secret
self.jwt_algorithm = hs.config.jwt_algorithm
self.saml2_enabled = hs.config.saml2_enabled
self.cas_enabled = hs.config.cas_enabled
self.auth_handler = self.hs.get_auth_handler()
self.registration_handler = hs.get_registration_handler()
Expand All @@ -104,6 +105,9 @@ def on_GET(self, request):
flows = []
if self.jwt_enabled:
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:
flows.append({"type": LoginRestServlet.SSO_TYPE})

Expand Down Expand Up @@ -370,28 +374,49 @@ def do_jwt_login(self, login_submission):
defer.returnValue(result)


class CasRedirectServlet(RestServlet):
class BaseSsoRedirectServlet(RestServlet):
richvdh marked this conversation as resolved.
Show resolved Hide resolved
"""Common base class for /login/sso/redirect impls"""
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(RestServlet):
def __init__(self, hs):
super(CasRedirectServlet, self).__init__()
self.cas_server_url = hs.config.cas_server_url.encode('ascii')
self.cas_service_url = hs.config.cas_service_url.encode('ascii')

def on_GET(self, request):
args = request.args
if b"redirectUrl" not in args:
return (400, "Redirect URL not specified for CAS auth")
def get_sso_url(self, client_redirect_url):
client_redirect_url_param = urllib.parse.urlencode({
b"redirectUrl": args[b"redirectUrl"][0]
b"redirectUrl": client_redirect_url
}).encode('ascii')
hs_redirect_url = (self.cas_service_url +
b"/_matrix/client/r0/login/cas/ticket")
service_param = urllib.parse.urlencode({
b"service": b"%s?%s" % (hs_redirect_url, client_redirect_url_param)
}).encode('ascii')
request.redirect(b"%s/login?%s" % (self.cas_server_url, service_param))
finish_request(request)
return b"%s/login?%s" % (self.cas_server_url, service_param)


class CasTicketServlet(RestServlet):
Expand Down Expand Up @@ -474,6 +499,25 @@ def parse_cas_response(self, cas_response_body):
return user, attributes


class SAMLRedirectServlet(BaseSsoRedirectServlet):
PATTERNS = client_patterns("/login/sso/redirect", v1=True)

def __init__(self, hs):
self._saml_client = hs.get_saml_client()

def get_sso_url(self, client_redirect_url):
reqid, info = self._saml_client.prepare_for_authenticate(
relay_state=client_redirect_url,
)

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")


class SSOAuthHandler(object):
"""
Utility class for Resources and Servlets which handle the response from a SSO
Expand Down Expand Up @@ -549,3 +593,5 @@ def register_servlets(hs, http_server):
if hs.config.cas_enabled:
CasRedirectServlet(hs).register(http_server)
CasTicketServlet(hs).register(http_server)
elif hs.config.saml2_enabled:
SAMLRedirectServlet(hs).register(http_server)
4 changes: 1 addition & 3 deletions synapse/rest/saml2/response_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import logging

import saml2
from saml2.client import Saml2Client

from twisted.web.resource import Resource
from twisted.web.server import NOT_DONE_YET
Expand All @@ -36,8 +35,7 @@ class SAML2ResponseResource(Resource):

def __init__(self, hs):
Resource.__init__(self)

self._saml_client = Saml2Client(hs.config.saml2_sp_config)
self._saml_client = hs.get_saml_client()
self._sso_auth_handler = SSOAuthHandler(hs)

def render_POST(self, request):
Expand Down
5 changes: 5 additions & 0 deletions synapse/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ def build_DEPENDENCY(self)
'registration_handler',
'account_validity_handler',
'event_client_serializer',
'saml_client',
]

REQUIRED_ON_MASTER_STARTUP = [
Expand Down Expand Up @@ -522,6 +523,10 @@ def build_account_validity_handler(self):
def build_event_client_serializer(self):
return EventClientSerializer(self)

def build_saml_client(self):
from saml2.client import Saml2Client
return Saml2Client(self.config.saml2_sp_config)

def remove_pusher(self, app_id, push_key, user_id):
return self.get_pusherpool().remove_pusher(app_id, push_key, user_id)

Expand Down
6 changes: 4 additions & 2 deletions synapse/static/client/login/js/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ var show_login = function() {
}

if (matrixLogin.serverAcceptsSso) {
$("#sso_form").attr("action", "/_matrix/client/r0/login/sso/redirect");
$("#sso_flow").show();
} else if (matrixLogin.serverAcceptsCas) {
$("#sso_form").attr("action", "/_matrix/client/r0/login/cas/redirect");
Expand All @@ -79,7 +80,7 @@ var fetch_info = function(cb) {
$.get(matrixLogin.endpoint, function(response) {
var serverAcceptsPassword = false;
var serverAcceptsCas = false;
for (var i=0; i<response.flows.length; i++) {
for (var i = 0; i < response.flows.length; i++) {
var flow = response.flows[i];
if ("m.login.cas" === flow.type) {
matrixLogin.serverAcceptsCas = true;
Expand Down Expand Up @@ -121,6 +122,7 @@ matrixLogin.onLogin = function(response) {
// clobber this function
console.log("onLogin - This function should be replaced to proceed.");
console.log(response);
alert("Login successful!");
};

var parseQsFromUrl = function(query) {
Expand All @@ -143,7 +145,7 @@ var try_token = function() {
if (pos == -1) {
return false;
}
var qs = parseQsFromUrl(window.location.href.substr(pos+1));
var qs = parseQsFromUrl(window.location.href.substr(pos + 1));

var loginToken = qs.loginToken;

Expand Down
4 changes: 4 additions & 0 deletions synapse/storage/deviceinbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ def get_new_device_msgs_for_remote(
if not has_changed or last_stream_id == current_stream_id:
return defer.succeed(([], current_stream_id))

if limit <= 0:
# This can happen if we run out of room for EDUs in the transaction.
return defer.succeed(([], last_stream_id))

def get_new_messages_for_remote_destination_txn(txn):
sql = (
"SELECT stream_id, messages_json FROM device_federation_outbox"
Expand Down
2 changes: 1 addition & 1 deletion synapse/storage/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -998,7 +998,7 @@ def get_threepid_validation_session(
client_secret,
address=None,
sid=None,
validated=None,
validated=True,
):
"""Gets a session_id and last_send_attempt (if available) for a
client_secret/medium/(address|session_id) combo
Expand Down