Initial revision

This commit is contained in:
kriss 2024-06-16 19:16:22 +02:00
commit 2e1ca9292a
6 changed files with 306 additions and 0 deletions

132
app.py Normal file
View File

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

View File

@ -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"
}
}
}

29
config/settings.json Normal file
View File

@ -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=="
}
}

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
python3-saml
flask

26
templates/base.html Normal file
View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>A Python SAML Toolkit demo</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="container">
<h1>A Python SAML Toolkit demo</h1>
{% block content %}{% endblock %}
</div>
</body>
</html>

54
templates/index.html Normal file
View File

@ -0,0 +1,54 @@
{% extends "base.html" %}
{% block content %}
{% if errors %}
<div class="alert alert-danger" role="alert">
<strong>Errors:</strong>
<ul class="list-unstyled">
{% for err in errors %}
<li>{{err}}</li>
{% endfor %}
</ul>
{% if error_reason %}
<span>{{error_reason}}</span>
{% endif %}
</div>
{% endif %}
{% if not_auth_warn %}
<div class="alert alert-danger" role="alert">Not authenticated</div>
{% endif %}
{% if success_slo %}
<div class="alert alert-success" role="alert">Successfully logged out</div>
{% endif %}
{% if paint_logout %}
{% if attributes %}
<p>You have the following attributes:</p>
<table class="table table-striped">
<thead>
<th>Name</th><th>Values</th>
</thead>
<tbody>
{% for attr in attributes %}
<tr><td>{{ attr.0 }}</td>
<td><ul class="list-unstyled">
{% for val in attr.1 %}
<li>{{ val }}</li>
{% endfor %}
</ul></td></tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="alert alert-danger" role="alert">You don't have any attributes</div>
{% endif %}
<a href="?slo" class="btn btn-danger">Logout</a>
{% else %}
<a href="?sso" class="btn btn-primary">Login</a>
{% endif %}
<a href="/metadata" class="btn btn-info">Metadata</a>
{% endblock %}