commit 2e1ca9292a2addc7dd6b35c1308c3bd4dc67d41d Author: kriss Date: Sun Jun 16 19:16:22 2024 +0200 Initial revision diff --git a/app.py b/app.py new file mode 100644 index 0000000..aa9ddfb --- /dev/null +++ b/app.py @@ -0,0 +1,132 @@ +import json +import os + +from flask import Flask, request, redirect, session, make_response, render_template +from onelogin.saml2.auth import OneLogin_Saml2_Auth +from onelogin.saml2.utils import OneLogin_Saml2_Utils + +app = Flask(__name__) +app.config['SAML_PATH'] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config') +app.config['SECRET_KEY'] = 'onelogindemopytoolkit' + +def init_saml_auth(req): + auth = OneLogin_Saml2_Auth(req, custom_base_path=app.config['SAML_PATH']) + return auth + + +def prepare_flask_request(request): + url_data = request.args.copy() + url_data.update(request.form) + return { + 'https': 'on' if request.scheme == 'https' else 'off', + 'http_host': request.host, + 'server_port': request.host.split(':')[1] if ':' in request.host else '443' if request.scheme == 'https' else '80', + 'script_name': request.path, + 'get_data': url_data, + 'post_data': request.form.copy() + } + + +@app.route("/", methods=['GET', 'POST']) +def index(): + req = prepare_flask_request(request) + auth = init_saml_auth(req) + errors = [] + error_reason = None + not_auth_warn = False + success_slo = False + attributes = False + paint_logout = False + if 'sso' in request.args: + return redirect(auth.login()) + # If AuthNRequest ID need to be stored in order to later validate it, do instead + # sso_built_url = auth.login() + # request.session['AuthNRequestID'] = auth.get_last_request_id() + # return redirect(sso_built_url) + elif 'slo' in request.args: + name_id = session_index = name_id_format = name_id_nq = name_id_spnq = None + if 'samlNameId' in session: + name_id = session['samlNameId'] + if 'samlSessionIndex' in session: + session_index = session['samlSessionIndex'] + if 'samlNameIdFormat' in session: + name_id_format = session['samlNameIdFormat'] + if 'samlNameIdNameQualifier' in session: + name_id_nq = session['samlNameIdNameQualifier'] + if 'samlNameIdSPNameQualifier' in session: + name_id_spnq = session['samlNameIdSPNameQualifier'] + return redirect(auth.logout(name_id=name_id, session_index=session_index, nq=name_id_nq, name_id_format=name_id_format, spnq=name_id_spnq)) + elif 'acs' in request.args: + request_id = None + if 'AuthNRequestID' in session: + request_id = session['AuthNRequestID'] + auth.process_response(request_id=request_id) + errors = auth.get_errors() + not_auth_warn = not auth.is_authenticated() + if len(errors) == 0: + if 'AuthNRequestID' in session: + del session['AuthNRequestID'] + session['samlUserdata'] = auth.get_attributes() + session['samlNameId'] = auth.get_nameid() + session['samlNameIdFormat'] = auth.get_nameid_format() + session['samlNameIdNameQualifier'] = auth.get_nameid_nq() + session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq() + session['samlSessionIndex'] = auth.get_session_index() + self_url = OneLogin_Saml2_Utils.get_self_url(req) + if 'RelayState' in request.form and self_url != request.form['RelayState']: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the request.form['RelayState'] is a trusted URL. + return redirect(auth.redirect_to(request.form['RelayState'])) + elif auth.get_settings().is_debug_active(): + error_reason = auth.get_last_error_reason() + elif 'sls' in request.args: + request_id = None + if 'LogoutRequestID' in session: + request_id = session['LogoutRequestID'] + dscb = lambda: session.clear() + url = auth.process_slo(request_id=request_id, delete_session_cb=dscb) + errors = auth.get_errors() + if len(errors) == 0: + if url is not None: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the url is a trusted URL. + return redirect(url) + else: + success_slo = True + elif auth.get_settings().is_debug_active(): + error_reason = auth.get_last_error_reason() + + if 'samlUserdata' in session: + paint_logout = True + if len(session['samlUserdata']) > 0: + attributes = session['samlUserdata'].items() + + return render_template( + 'index.html', + errors=errors, + error_reason=error_reason, + not_auth_warn=not_auth_warn, + success_slo=success_slo, + attributes=attributes, + paint_logout=paint_logout + ) + + +@app.route('/metadata') +def metadata(): + req = prepare_flask_request(request) + auth = init_saml_auth(req) + settings = auth.get_settings() + metadata = settings.get_sp_metadata() + errors = settings.validate_metadata(metadata) + + if len(errors) == 0: + resp = make_response(metadata, 200) + resp.headers['Content-Type'] = 'text/xml' + else: + resp = make_response(', '.join(errors), 500) + return resp + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/config/advanced_settings.json b/config/advanced_settings.json new file mode 100644 index 0000000..f2e956d --- /dev/null +++ b/config/advanced_settings.json @@ -0,0 +1,63 @@ +{ + "security": { + + "authnRequestsSigned": true, + + "logoutRequestSigned": true, + + "logoutResponseSigned": false, + + "signMetadata": false, + + "wantMessagesSigned": false, + + "wantAssertionsSigned": true, + + "wantAssertionsEncrypted": false, + + "wantNameId": true, + + "wantNameIdEncrypted": false, + + "wantAttributeStatement": true, + + "requestedAuthnContext": true, + + "requestedAuthnContextComparison": "exact", + + "failOnAuthnContextMismatch": false, + + "metadataValidUntil": null, + + "metadataCacheDuration": null, + + "allowSingleLabelDomains": false, + + "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + + "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256", + + "allowRepeatAttributeName": false, + + "rejectDeprecatedAlgorithm": true + }, + + "contactPerson": { + "technical": { + "givenName": "Kriss", + "emailAddress": "kriss@vilanet.fr" + }, + "support": { + "givenName": "Kriss", + "emailAddress": "kriss@vilanet.fr" + } + }, + + "organization": { + "en-US": { + "name": "client-saml", + "displayname": "Test Client SAML", + "url": "http://localhost:5000" + } + } +} \ No newline at end of file diff --git a/config/settings.json b/config/settings.json new file mode 100644 index 0000000..2ad3d59 --- /dev/null +++ b/config/settings.json @@ -0,0 +1,29 @@ +{ + "strict": true, + "debug": true, + "sp": { + "entityId": "http://localhost:5000/metadata", + "assertionConsumerService": { + "url": "http://localhost:5000/?acs", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + "singleLogoutService": { + "url": "http://localhost:5000/?sls", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "x509cert": "MIIDIzCCAgugAwIBAgIUO3lW/e6/coBwiHOJzygNpSopoJMwDQYJKoZIhvcNAQELBQAwITELMAkGA1UEBhMCRlIxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yNDA2MTUxNjA5NTNaFw0yNDA3MTUxNjA5NTNaMCExCzAJBgNVBAYTAkZSMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVPpupF3onnm+H08x8fpL7AQk07iViukdcILT35gobz9WryP5txhp8VAcoCQqpPQmrnuY1txUZ3fCpnVA1+5FPUZ0o54DXDuaL/0oxeV7yOXvtwkgq6DsXzNIosLf1gwXbCN/f+xhbBRVkOxS+eKGKWG5CtkqyECw+k87rqolqz/6CK0D+M1aYjmLhilSIsnUzX/FfdPm5ntLHlU3K3RhvVb3TPi9CJgW5XnvAxvzMAWtzpNVDmbZ78kfekWR6+oWbr0icX73O/ubz/9hREWaxsvDtWQRFGy+xRHwd+fMdT4gemO6ImBKxdKl1YgIkqEwFRp4WhW0C\nyzr602j6+yODAgMBAAGjUzBRMB0GA1UdDgQWBBQE33DzxteoD18rR0hDK2FOGexgPjAfBgNVHSMEGDAWgBQE33DzxteoD18rR0hDK2FOGexgPjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCCEopUP4FTtvAL8k/w/Hu9HW5pwGlRivUsBP4Z4VvXLCyWy1lQdDRedIR9KOBTHwIhmgZadzPo/qTQd/9xlkX3G+DDkeEXDL/mdyW3tElGuHeQaMD+w3SCXMwOIYdUJteJS0lLVVg1KhSz95BkOU54lt2yVlQFms5DbAWMqEAUNfZLtr2ZmFHuCHLxEOxpuPCdY8VLkQWl0mwF3YlrSLAQTdgJbVu/wIkYtq49GOp+weHCHI64fhnKvODU/uNdS7WynCAweqd59noDlGApOhvTQNPz53Gpo2LdEICIovEHgB0X+RYWv8f3dIqIprX8tVkWTGo4bbpFLuZ1Q7uGupz9", + "privateKey": "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCVPpupF3onnm+H08x8fpL7AQk07iViukdcILT35gobz9WryP5txhp8VAcoCQqpPQmrnuY1txUZ3fCpnVA1+5FPUZ0o54DXDuaL/0oxeV7yOXvtwkgq6DsXzNIosLf1gwXbCN/f+xhbBRVkOxS+eKGKWG5CtkqyECw+k87rqolqz/6CK0D+M1aYjmLhilSIsnUzX/FfdPm5ntLHlU3K3RhvVb3TPi9CJgW5XnvAxvzMAWtzpNVDmbZ78kfekWR6+oWbr0icX73O/ubz/9hREWaxsvDtWQRFGy+xRHwd+fMdT4gemO6ImBKxdKl1YgIkqEwFRp4WhW0Cyzr602j6+yODAgMBAAECggEAEN5oOrcIHBVI1unmGYY29774uu5V9HBIo42g1so+B0G5DXck5uR21umqe2h2N4XKI4dMzvJ4JJnU218IE2nxVS1M3bQ4+FXbUiSTTr9Ar5E2xDtq68qe70Q71tSOnmXmUgkRQOBBMvyCm/cfk53P6HKxV8IB9FlmntYXE8UNTjCzCKBpOc0lbCWKHufPv/77vdmN7fOEwpQSsoll8IfJLzN7r2KWpr8uOzRlDupxQGT806U9EgPqjj5HNfhJ/d7pr4kDbudCMmy11MtDOqyub+3LB5tv7e+UqvgcGbaF0dZ2/YyTNrjHB3zus4nKIUTlTAxG/oaU38AJlH1BvFZqwQKBgQDIjdMbmasms2zEMZYvIkFCodtFzyxMgLOKU646SpysMCFRyGG3eAEA8j2bN+OGbAYKSjFoTKHrTYzDb0uleBMZ7xX5e0eRGqStPZb8LBXp/ufYfHp4gXYHzCpZ+n2kx5F+WiF378v27FfLk2NoczYCuF4X5JvysgTvj8S6Ts6zwwKBgQC+gV3WEjcr9mC70qvOuz5Id7oA0mXsjKU4o1CoDsjojuBF/zSGQi0Ar76ZDgd8Jup0eeLSKeRVjV86pDNPWAcKaHmeex2P/OD/uK/yuzdFc31SEopFbDEZ6fK3PBZ1M7VneakDOg1zspcUgVTsolNjRh1hsIkhEj368gA2G7WVQQKBgAhEwKVsqn/H+f4ExVpgISysG6w/JGZrD/vuA0rn9JmsylLi3hSAYBo34o5ZuYm7PmyCLpNMRYi8A8ey+P1ze+Yf01ob2RGEdbGmzmjLMIQbPFfSmgIJ5GHh6wUWrMN0bu00rhiRzGj7yYrdIsYVqe5mx4pYpI1XBZkS5luAEEmdAoGAE0Z1nx5StMEGApsLRSyO3bg3erPPGkMUyIlFtOtiCp3CNXLf9qGlegdOKqBPw5EQcd6PQ6J3duyJ8R4CDwoiFDyD6bQdRp9YiKdALjghHIbV7ELx+Jo80ZlpNH8A6rTjqueVYT0zdTxhqvJ3DEZUV5wVhvfcuBrnaIep28+r7MECgYEAveoBzdWVX6mOWuxZp8ASl0z694jjrWAWqWRa3hvVSxTSDbbE9IWMniNHGrm4FzDBvkgdb4Zglvyz4TSpX6o2UoVpq6Pzg5y4kMRoH1+Rk0jUpvIRMNyLCkS9wrB7EOnDYQwJnOInMGdUYkLgPbEfz2yczQVwt1F9EJOPBg668rA=" + }, + "idp": { + "entityId": "https://id.vilanet.fr/realms/vilanet", + "singleSignOnService": { + "url": "https://id.vilanet.fr/realms/vilanet/protocol/saml", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "singleLogoutService": { + "url": "https://id.vilanet.fr/realms/vilanet/protocol/saml", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "x509cert": "MIICnTCCAYUCBgGPz1WjUzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAd2aWxhbmV0MB4XDTI0MDUzMTE1NDU0N1oXDTM0MDUzMTE1NDcyN1owEjEQMA4GA1UEAwwHdmlsYW5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKC6k5iYxxu9F/ms6G5JYsIapewpqNCd4hr2DXcEfXrI1Et6gNxDNrHRwoYl4sG/6C1V8x1woynwEXe7AOH5f5Piwr2lvWq6Hl3NgPKBlUwIyCB9rjXVkHJxxCrMPwi4Isxuvw8LLMNYCAhx5rFKuo7lwNWMFPAmbAkb5TZqByG4kQvXT5aavtgSN3NdPOSTKSZOnqx3MY+j3b9nWbIhQ2EIfgj9pEe3FEknis0kQVUwUWIL68QdDg4AECiBOjgIStE+P9d7VLiWpu6azMgUxIcxNTW+ZQrL1yScXt+IOs6TvSILv0EnHX4Qj0kS1Uvs5tAwSXPznY9vSM980G51btECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAEbEtGGc6hfb6fEAPuu7tAg4wq2NGTVFiw9D5+kupz4R877DumIKExe1VHZyDuThPWDGAQ9n93BXkllyzkqwWpgzPGQggVQ88L1hIyg1cC9y1SAlJ7ZRMd74URTlV5sa2mYkMaPULsxO7hIFtEAdgeJEuvmZGEjmEOKG4JYZLBkqa5PqyfABKI/QcIWgxrHxEZXqqWSTJ/hYEiHQ9eZq8H5mUTmhAvQ0J8y6E2VOGrAnNgeFjxedJnTIBK+Lx3SE4+lJ9qLBoZRWcPu7aqZV7DmLpewH4nt/Q4CDm/5RgdblQUR3lUEH8VRzWR+2TbOliqlAkWxvoa9Cb6s6Mdtohig==" + } +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f0cf6e1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +python3-saml +flask diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..52beb06 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,26 @@ + + + + + + + + A Python SAML Toolkit demo + + + + + + + + +
+

A Python SAML Toolkit demo

+ + {% block content %}{% endblock %} +
+ + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..4ae4019 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,54 @@ +{% extends "base.html" %} + +{% block content %} + +{% if errors %} + +{% endif %} + +{% if not_auth_warn %} + +{% endif %} + +{% if success_slo %} + +{% endif %} + +{% if paint_logout %} + {% if attributes %} +

You have the following attributes:

+ + + + + + {% for attr in attributes %} + + + {% endfor %} + +
NameValues
{{ attr.0 }}
    + {% for val in attr.1 %} +
  • {{ val }}
  • + {% endfor %} +
+ {% else %} + + {% endif %} + Logout +{% else %} + Login +{% endif %} +Metadata + +{% endblock %} \ No newline at end of file