2019-12-13 14:55:11 +00:00
|
|
|
from datetime import datetime, timedelta
|
|
|
|
from threading import Thread
|
|
|
|
from flask import current_app
|
2019-12-20 02:24:26 +00:00
|
|
|
import json
|
2019-12-14 17:59:59 +00:00
|
|
|
import os
|
2019-12-13 14:55:11 +00:00
|
|
|
|
2019-12-18 23:40:25 +00:00
|
|
|
from ..lib.certutil import KEY_FILE, CERT_FILE, create_self_signed_cert
|
2019-12-14 17:59:59 +00:00
|
|
|
from ..lib.utils import urlparse
|
2019-12-13 14:55:11 +00:00
|
|
|
|
2019-12-15 02:40:05 +00:00
|
|
|
|
2019-12-13 14:55:11 +00:00
|
|
|
class SAML(object):
|
|
|
|
def __init__(self):
|
|
|
|
if current_app.config['SAML_ENABLED']:
|
|
|
|
from onelogin.saml2.auth import OneLogin_Saml2_Auth
|
|
|
|
from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser
|
2019-12-14 17:59:59 +00:00
|
|
|
|
2019-12-14 01:31:23 +00:00
|
|
|
self.idp_timestamp = datetime.now()
|
2019-12-14 17:59:59 +00:00
|
|
|
self.OneLogin_Saml2_Auth = OneLogin_Saml2_Auth
|
|
|
|
self.OneLogin_Saml2_IdPMetadataParser = OneLogin_Saml2_IdPMetadataParser
|
2019-12-14 01:31:23 +00:00
|
|
|
self.idp_data = None
|
2019-12-14 17:59:59 +00:00
|
|
|
|
2019-12-13 14:55:11 +00:00
|
|
|
if 'SAML_IDP_ENTITY_ID' in current_app.config:
|
2019-12-14 01:31:23 +00:00
|
|
|
self.idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(
|
2019-12-13 14:55:11 +00:00
|
|
|
current_app.config['SAML_METADATA_URL'],
|
|
|
|
entity_id=current_app.config.get('SAML_IDP_ENTITY_ID',
|
|
|
|
None),
|
|
|
|
required_sso_binding=current_app.
|
|
|
|
config['SAML_IDP_SSO_BINDING'])
|
|
|
|
else:
|
2019-12-14 01:31:23 +00:00
|
|
|
self.idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(
|
2019-12-13 14:55:11 +00:00
|
|
|
current_app.config['SAML_METADATA_URL'],
|
|
|
|
entity_id=current_app.config.get('SAML_IDP_ENTITY_ID',
|
|
|
|
None))
|
2019-12-14 01:31:23 +00:00
|
|
|
if self.idp_data is None:
|
2019-12-13 14:55:11 +00:00
|
|
|
current_app.logger.info(
|
|
|
|
'SAML: IDP Metadata initial load failed')
|
|
|
|
exit(-1)
|
|
|
|
|
2019-12-14 01:31:23 +00:00
|
|
|
def get_idp_data(self):
|
2019-12-14 17:59:59 +00:00
|
|
|
|
2019-12-15 02:40:05 +00:00
|
|
|
lifetime = timedelta(
|
|
|
|
minutes=current_app.config['SAML_METADATA_CACHE_LIFETIME'])
|
2019-12-14 17:59:59 +00:00
|
|
|
|
2019-12-14 20:45:51 +00:00
|
|
|
if self.idp_timestamp + lifetime < datetime.now():
|
|
|
|
background_thread = Thread(target=self.retrieve_idp_data())
|
|
|
|
background_thread.start()
|
2019-12-14 17:59:59 +00:00
|
|
|
|
2019-12-14 01:31:23 +00:00
|
|
|
return self.idp_data
|
2019-12-13 14:55:11 +00:00
|
|
|
|
2019-12-14 01:31:23 +00:00
|
|
|
def retrieve_idp_data(self):
|
2019-12-14 17:59:59 +00:00
|
|
|
|
2019-12-15 02:40:05 +00:00
|
|
|
if 'SAML_IDP_SSO_BINDING' in current_app.config:
|
2019-12-14 17:59:59 +00:00
|
|
|
new_idp_data = self.OneLogin_Saml2_IdPMetadataParser.parse_remote(
|
2019-12-13 14:55:11 +00:00
|
|
|
current_app.config['SAML_METADATA_URL'],
|
|
|
|
entity_id=current_app.config.get('SAML_IDP_ENTITY_ID', None),
|
|
|
|
required_sso_binding=current_app.config['SAML_IDP_SSO_BINDING']
|
|
|
|
)
|
2019-12-15 02:40:05 +00:00
|
|
|
else:
|
2019-12-14 17:59:59 +00:00
|
|
|
new_idp_data = self.OneLogin_Saml2_IdPMetadataParser.parse_remote(
|
2019-12-13 14:55:11 +00:00
|
|
|
current_app.config['SAML_METADATA_URL'],
|
|
|
|
entity_id=current_app.config.get('SAML_IDP_ENTITY_ID', None))
|
2019-12-15 02:40:05 +00:00
|
|
|
if new_idp_data is not None:
|
2019-12-14 01:31:23 +00:00
|
|
|
self.idp_data = new_idp_data
|
|
|
|
self.idp_timestamp = datetime.now()
|
2019-12-13 14:55:11 +00:00
|
|
|
current_app.logger.info(
|
|
|
|
"SAML: IDP Metadata successfully retrieved from: " +
|
|
|
|
current_app.config['SAML_METADATA_URL'])
|
2019-12-15 02:40:05 +00:00
|
|
|
else:
|
2019-12-13 14:55:11 +00:00
|
|
|
current_app.logger.info(
|
|
|
|
"SAML: IDP Metadata could not be retrieved")
|
|
|
|
|
2019-12-14 01:31:23 +00:00
|
|
|
def prepare_flask_request(self, request):
|
2019-12-13 14:55:11 +00:00
|
|
|
# If server is behind proxys or balancers use the HTTP_X_FORWARDED fields
|
|
|
|
url_data = urlparse(request.url)
|
2022-05-20 02:00:38 +00:00
|
|
|
proto = request.headers.get('HTTP_X_FORWARDED_PROTO', request.scheme)
|
2019-12-13 14:55:11 +00:00
|
|
|
return {
|
2022-05-20 02:00:38 +00:00
|
|
|
'https': 'on' if proto == 'https' else 'off',
|
2019-12-13 14:55:11 +00:00
|
|
|
'http_host': request.host,
|
|
|
|
'server_port': url_data.port,
|
|
|
|
'script_name': request.path,
|
|
|
|
'get_data': request.args.copy(),
|
|
|
|
'post_data': request.form.copy(),
|
|
|
|
# Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144
|
|
|
|
'lowercase_urlencoding': True,
|
|
|
|
'query_string': request.query_string
|
|
|
|
}
|
|
|
|
|
2019-12-14 01:31:23 +00:00
|
|
|
def init_saml_auth(self, req):
|
2019-12-13 14:55:11 +00:00
|
|
|
own_url = ''
|
|
|
|
if req['https'] == 'on':
|
|
|
|
own_url = 'https://'
|
|
|
|
else:
|
|
|
|
own_url = 'http://'
|
|
|
|
own_url += req['http_host']
|
2019-12-14 01:31:23 +00:00
|
|
|
metadata = self.get_idp_data()
|
2019-12-13 14:55:11 +00:00
|
|
|
settings = {}
|
|
|
|
settings['sp'] = {}
|
|
|
|
if 'SAML_NAMEID_FORMAT' in current_app.config:
|
|
|
|
settings['sp']['NameIDFormat'] = current_app.config[
|
|
|
|
'SAML_NAMEID_FORMAT']
|
|
|
|
else:
|
2019-12-14 17:59:59 +00:00
|
|
|
settings['sp']['NameIDFormat'] = self.idp_data.get('sp', {}).get(
|
2019-12-13 14:55:11 +00:00
|
|
|
'NameIDFormat',
|
|
|
|
'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified')
|
|
|
|
settings['sp']['entityId'] = current_app.config['SAML_SP_ENTITY_ID']
|
2019-12-18 23:40:25 +00:00
|
|
|
|
|
|
|
|
2021-05-07 21:36:55 +00:00
|
|
|
if ('SAML_CERT' in current_app.config) and ('SAML_KEY' in current_app.config):
|
2019-12-18 23:40:25 +00:00
|
|
|
|
2021-05-07 21:36:55 +00:00
|
|
|
saml_cert_file = current_app.config['SAML_CERT']
|
|
|
|
saml_key_file = current_app.config['SAML_KEY']
|
2019-12-18 23:40:25 +00:00
|
|
|
|
|
|
|
if os.path.isfile(saml_cert_file):
|
|
|
|
cert = open(saml_cert_file, "r").readlines()
|
|
|
|
settings['sp']['x509cert'] = "".join(cert)
|
|
|
|
if os.path.isfile(saml_key_file):
|
|
|
|
key = open(saml_key_file, "r").readlines()
|
|
|
|
settings['sp']['privateKey'] = "".join(key)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
2019-12-20 02:24:26 +00:00
|
|
|
if (os.path.isfile(CERT_FILE)) and (os.path.isfile(KEY_FILE)):
|
2019-12-18 23:40:25 +00:00
|
|
|
cert = open(CERT_FILE, "r").readlines()
|
|
|
|
key = open(KEY_FILE, "r").readlines()
|
2019-12-20 02:24:26 +00:00
|
|
|
else:
|
|
|
|
create_self_signed_cert()
|
|
|
|
cert = open(CERT_FILE, "r").readlines()
|
|
|
|
key = open(KEY_FILE, "r").readlines()
|
|
|
|
|
|
|
|
settings['sp']['x509cert'] = "".join(cert)
|
|
|
|
settings['sp']['privateKey'] = "".join(key)
|
|
|
|
|
|
|
|
|
|
|
|
if 'SAML_SP_REQUESTED_ATTRIBUTES' in current_app.config:
|
|
|
|
saml_req_attr = json.loads(current_app.config['SAML_SP_REQUESTED_ATTRIBUTES'])
|
|
|
|
settings['sp']['attributeConsumingService'] = {
|
|
|
|
"serviceName": "PowerDNSAdmin",
|
|
|
|
"serviceDescription": "PowerDNS-Admin - PowerDNS administration utility",
|
|
|
|
"requestedAttributes": saml_req_attr
|
|
|
|
}
|
|
|
|
else:
|
|
|
|
settings['sp']['attributeConsumingService'] = {}
|
2019-12-18 23:40:25 +00:00
|
|
|
|
|
|
|
|
2019-12-13 14:55:11 +00:00
|
|
|
settings['sp']['assertionConsumerService'] = {}
|
|
|
|
settings['sp']['assertionConsumerService'][
|
|
|
|
'binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
|
|
|
|
settings['sp']['assertionConsumerService'][
|
|
|
|
'url'] = own_url + '/saml/authorized'
|
|
|
|
settings['sp']['singleLogoutService'] = {}
|
|
|
|
settings['sp']['singleLogoutService'][
|
|
|
|
'binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
|
|
|
|
settings['sp']['singleLogoutService']['url'] = own_url + '/saml/sls'
|
|
|
|
settings['idp'] = metadata['idp']
|
|
|
|
settings['strict'] = True
|
|
|
|
settings['debug'] = current_app.config['SAML_DEBUG']
|
|
|
|
settings['security'] = {}
|
|
|
|
settings['security'][
|
|
|
|
'digestAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
|
|
|
|
settings['security']['metadataCacheDuration'] = None
|
|
|
|
settings['security']['metadataValidUntil'] = None
|
|
|
|
settings['security']['requestedAuthnContext'] = True
|
|
|
|
settings['security'][
|
|
|
|
'signatureAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
|
2020-04-03 13:44:03 +00:00
|
|
|
settings['security']['wantAssertionsEncrypted'] = current_app.config.get(
|
|
|
|
'SAML_ASSERTION_ENCRYPTED', True)
|
2022-05-19 21:49:46 +00:00
|
|
|
settings['security']['wantAttributeStatement'] = current_app.config.get(
|
|
|
|
'SAML_WANT_ATTRIBUTE_STATEMENT', True)
|
2019-12-13 14:55:11 +00:00
|
|
|
settings['security']['wantNameId'] = True
|
|
|
|
settings['security']['authnRequestsSigned'] = current_app.config[
|
|
|
|
'SAML_SIGN_REQUEST']
|
|
|
|
settings['security']['logoutRequestSigned'] = current_app.config[
|
|
|
|
'SAML_SIGN_REQUEST']
|
|
|
|
settings['security']['logoutResponseSigned'] = current_app.config[
|
|
|
|
'SAML_SIGN_REQUEST']
|
|
|
|
settings['security']['nameIdEncrypted'] = False
|
|
|
|
settings['security']['signMetadata'] = True
|
|
|
|
settings['security']['wantAssertionsSigned'] = True
|
|
|
|
settings['security']['wantMessagesSigned'] = current_app.config.get(
|
|
|
|
'SAML_WANT_MESSAGE_SIGNED', True)
|
|
|
|
settings['security']['wantNameIdEncrypted'] = False
|
|
|
|
settings['contactPerson'] = {}
|
|
|
|
settings['contactPerson']['support'] = {}
|
|
|
|
settings['contactPerson']['support'][
|
|
|
|
'emailAddress'] = current_app.config['SAML_SP_CONTACT_NAME']
|
|
|
|
settings['contactPerson']['support']['givenName'] = current_app.config[
|
|
|
|
'SAML_SP_CONTACT_MAIL']
|
|
|
|
settings['contactPerson']['technical'] = {}
|
|
|
|
settings['contactPerson']['technical'][
|
2019-12-14 17:59:59 +00:00
|
|
|
'emailAddress'] = current_app.config['SAML_SP_CONTACT_MAIL']
|
2019-12-13 14:55:11 +00:00
|
|
|
settings['contactPerson']['technical'][
|
2019-12-14 17:59:59 +00:00
|
|
|
'givenName'] = current_app.config['SAML_SP_CONTACT_NAME']
|
2019-12-13 14:55:11 +00:00
|
|
|
settings['organization'] = {}
|
|
|
|
settings['organization']['en-US'] = {}
|
|
|
|
settings['organization']['en-US']['displayname'] = 'PowerDNS-Admin'
|
|
|
|
settings['organization']['en-US']['name'] = 'PowerDNS-Admin'
|
|
|
|
settings['organization']['en-US']['url'] = own_url
|
2019-12-14 17:59:59 +00:00
|
|
|
auth = self.OneLogin_Saml2_Auth(req, settings)
|
2019-12-14 20:45:51 +00:00
|
|
|
return auth
|