From a910d8084f3e59e8d93b3b16da97239552f79071 Mon Sep 17 00:00:00 2001 From: kaiyou <pierre@jaury.eu> Date: Thu, 12 Sep 2019 21:45:16 +0200 Subject: [PATCH] Implement a very first naive saml idp --- requirements.txt | 2 + trurt/__init__.py | 3 +- trurt/sso/__init__.py | 6 ++ trurt/sso/oidc.py | 0 trurt/sso/saml.py | 117 ++++++++++++++++++++++++++ trurt/sso/templates/sso_redirect.html | 23 +++++ 6 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 trurt/sso/__init__.py create mode 100644 trurt/sso/oidc.py create mode 100644 trurt/sso/saml.py create mode 100644 trurt/sso/templates/sso_redirect.html diff --git a/requirements.txt b/requirements.txt index 506e3e39..5a6cbdbc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,5 @@ passlib gunicorn PyYAML bcrypt +pysaml2 +xmlsec diff --git a/trurt/__init__.py b/trurt/__init__.py index aa31c85c..1d4e78d7 100644 --- a/trurt/__init__.py +++ b/trurt/__init__.py @@ -30,8 +30,9 @@ def create_app_from_config(config): return dict(config=app.config) # Import views - from trurt import account + from trurt import account, sso app.register_blueprint(account.blueprint, url_prefix='/account') + app.register_blueprint(sso.blueprint, url_prefix='/sso') return app diff --git a/trurt/sso/__init__.py b/trurt/sso/__init__.py new file mode 100644 index 00000000..43c4d023 --- /dev/null +++ b/trurt/sso/__init__.py @@ -0,0 +1,6 @@ +from flask import Blueprint + + +blueprint = Blueprint("sso", __name__, template_folder="templates") + +from trurt.sso import saml, oidc diff --git a/trurt/sso/oidc.py b/trurt/sso/oidc.py new file mode 100644 index 00000000..e69de29b diff --git a/trurt/sso/saml.py b/trurt/sso/saml.py new file mode 100644 index 00000000..ff8d3e32 --- /dev/null +++ b/trurt/sso/saml.py @@ -0,0 +1,117 @@ +from trurt.sso import blueprint +from saml2 import sigver + +# We monkey-patch the security context factory, so that we can silently +# replace it with our own xmlsec-based implementation. +def security_context(conf): + return SecurityContext(conf) +sigver.security_context = security_context + +from saml2 import server, saml, config, mdstore, assertion +import saml2, base64, flask, xmlsec, lxml.etree, flask_login + + +class MetaData(mdstore.InMemoryMetaData): + """ Implements dynamic metadata for a given service. + Metadata and configuration are generated per service, even general IDP + configuration, so that the IDP can use different keys for different SPs. + """ + + def __init__(self, attrc, entityid, **kwargs): + (super(MetaData, self).__init__)(attrc, **kwargs) + self.entityid = entityid + + def load(self, *args, **kwargs): + """ Load the service metadata for asserion generation. + """ + self.entity.update({self.entityid: {'spsso_descriptor': [ + {'attribute_consuming_service': [{'requested_attribute': []}]} + ]}}) + + @classmethod + def get_config(cls, service_slug): + """ Load the IDP configuration. + """ + idp_service = { + 'endpoints':{}, 'policy':{'default': {'lifetime':{'minutes': 15}, + 'attribute_restrictions':None, + 'name_form':saml2.saml.NAME_FORMAT_URI, + 'entity_categories':[]}}, + 'name_id_format':[saml2.saml.NAMEID_FORMAT_PERSISTENT] + } + config_dict = { + 'key_file':'-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA0X5UX/rQkpXRd0IIDqdqhJCew8Pes37Vroobqkt5mtdB6nFa\n5O8N+ZhS0iLWU7B3C0s14ZxrKeVcIoOlBsJuM88DHH+5679WAXEGG7hE7yBBhVxr\nhrMee0c4XSWQFEkh2UleeBKSKGMJpPvHvLJfwg9nf4wGkXsDiKFjPYGoQdhk+i/k\nmT43xgo8FpJES8QHwCfyDpb3hg0JPT8db0Dtc5D259mdjMYY2q7PsH3uCEww9VXN\nBsJAIdj2iiNlQGRwvHkG2+/LjdvvJPPQ1baFcM0KNSZvlc6oSe7oHR9yNL0DxcK8\nFIa7EEY0+8qEngE7zGIGDo2CfnFbnIbn9fiy2QIDAQABAoIBAQCLTM9aCvNJpWll\nPXkSFWyUvX10evfIrxvzNU50DD/OIDhqZfmkpPjL8OeRZyzQ9VQTJG2tmU8Aysxa\n/uJq/jo9JPfSqXO9OLs9tiPzprHft7kZrnypUs1/97mY5nNJqd9iFpFEkkSxqjkt\nhWYpKQrXhVqyyy9K6VtOLNJKgb6aGM7n0zTM3M8k1A8l5D/yEvnYYD/hTAbqQJ6y\nwUuNqNTnGTVR5iACt2rIvBKqSQtaQsANYlccbYeBGcIVyr5QXkcI0V2mLlMOKS6t\nrDr14DAs6d/2IT+OiD15S7HRb+Z9Y8e5eXbmSfasWDSz/SNrPwdSEY2URNi1r956\nXZDBXtYBAoGBAPv+3rVqvEBuaJd8Ss0VSndzfshE1ahk6QX2+AB89IcgWVfG9PJ7\nDz3R17qV3sDAfm15bn8BF8/3R3eMVmqbZb/R/HawXA6rrs1SxlXaFMb3cG//ZJxE\nxb+BwBSaiEj+CWBa02M85a7EuWH2dflDg22OOvA+1AW9JQ/SSIWUxUIhAoGBANTS\njxwf0tu78xBjgjY9GI1gtEC5k8S/Xio87/Tyde3l0jySK0fR7puWunccyQp13ANr\n6SiAvrqpzZjjkmvUC92CSCviDT09Ltqy8X9vaXQ8s3tAT/qhUTaBMDTAFi4tCttS\n9bx/ycO7LzOk0xLFT6bsjOeOdMLq0NYKKINCgMm5AoGAE9JJbFW39w14Nqo1LAqH\nr/uqtlALylIdrjVt7oPlrBdUT747mDMr0L4HzQpq2hiKGUxa76yDVf1qZrHoPjx4\n9WysAh3/L7w7ZLUlGq2rwrbF5lldbZlPQLARDs3U+IDa9fRO+lhY7LVWq6j6QKAZ\n3203n5whi04Ec0kkITXBimECgYEAtjP+aamlMJJcqm9HD4CHAKMGL1Ox+wOLbsX0\n+dSKuj3EHC9X9oj4qyQER+3RAK+eyR8d4ps2r0Co0Hgk50QHVIExoMBLbV5wOrRw\npRWRRv6g+qg40O5DRVKdHsxFMQtG/DauQ89zwasD4kb+nldmthZXG/eOZ0H5wQW5\nYYcSE6ECgYAfB5WGROqAr2KL0JPaGkNKBPtfaVJBkSGWz/kWsP4ih15XWTKB1HiH\nZ7sDgIcDUvAC7VOnnDvErt4AWlaoMormicovQQOMcO/K0dYJKtGgReGb4gwXm3je\n/wttmV+PDWQeaQXdv3oPpKtsgqNoca2S9EBZWUybdKVZ9YO9+kWfmQ==\n-----END RSA PRIVATE KEY-----', + 'service':{'idp': idp_service}, + 'metadata':[ + {'class':'trurt.sso.saml.MetaData', + 'metadata':[('https://status.kaiyou.fr/auth/auth/saml/callback', )]} + ] + } + return config.config_factory('idp', config_dict) + + +class CertHandler(object): + """ Dummy implementation of a CertHandler that we can instanciate for + the security context. + """ + + def generate_cert(self): + return False + + +class SecurityContext(sigver.SecurityContext): + """ Replacement for pysmal2 security context. + + This implementation only provides stuff useful for an IDP. It is based on + Python integration of the xmlsec1 lib intead of the original exec-based + implementation. + """ + + def __init__(self, conf): + self.cert_handler = CertHandler() + self.conf = conf + + def _check_signature(self, decoded_xml, item, node_name=None, origdoc=None, id_attr='', must=False, only_valid_cert=False, issuer=None): + # TODO: actually check the signature from authentication requests + # (not critical, but required for production) + return item + + def sign_statement(self, statement, node_name, key=None, key_file=None, node_id=None, id_attr=''): + xml = lxml.etree.fromstring(statement) + # Specify the id attribute so that xmlsec can find the object referenced + # in the signature block. + xmlsec.tree.add_ids(xml, ['ID']) + signature = xmlsec.tree.find_node(xml, xmlsec.constants.NodeSignature) + context = xmlsec.SignatureContext() + context.key = xmlsec.Key.from_memory( + self.conf.getattr('key_file', ''), + xmlsec.constants.KeyDataFormatPem + ) + context.sign(signature) + return lxml.etree.tostring(xml) + + +@blueprint.route('/saml/<service_slug>/redirect') +def auth(service_slug): + idp = server.Server(config=(MetaData.get_config(service_slug))) + request = idp.parse_authn_request( + flask.request.args['SAMLRequest'], saml2.BINDING_HTTP_REDIRECT + ) + target = request.message.assertion_consumer_service_url + authn = assertion.authn_statement(saml2.saml.AUTHN_PASSWORD) + response = idp.create_authn_response( + identity={ + 'uid':'test12', + 'email':'admintest12@tedomum.net' + }, + in_response_to=(request.message.id), + destination=target, + sp_entity_id='https://status.kaiyou.fr/auth/auth/saml/callback', + userid=(flask_login.current_user.username), + authn={'class_ref': saml2.saml.AUTHN_PASSWORD}, + sign_assertion=True + ) + return flask.render_template('sso_redirect.html', target=target, data={ + 'SAMLResponse':base64.b64encode(response).decode('ascii'), + 'RelayState':flask.request.args.get('RelayState', '') + }) diff --git a/trurt/sso/templates/sso_redirect.html b/trurt/sso/templates/sso_redirect.html new file mode 100644 index 00000000..fd378f21 --- /dev/null +++ b/trurt/sso/templates/sso_redirect.html @@ -0,0 +1,23 @@ +<html> + <head> + <meta charset="utf-8" /> + </head> + <body onloaed="document.forms[0].submit()"> + <noscript> + <p> + <strong>Note:</strong> + Since your browser does not support JavaScript, + you must press the Continue button once to proceed. + </p> + </noscript> + <form action="{{ target }}" method="post"> + {% for key, value in data.items() %} + <input type="hidden" name="{{ key }}" value="{{ value }}"> + {% endfor %} + <noscript> + <input type="submit" value="Continue"/> + </noscript> + <input type="submit" value="Continue"/> + </form> + </body> +</html> -- GitLab