Merge remote-tracking branch 'Neven1986/saml_fixes'

This commit is contained in:
Khanh Ngo 2019-12-15 08:25:17 +07:00
commit 32236faae8
3 changed files with 179 additions and 111 deletions

View File

@ -3,15 +3,11 @@ import json
import requests
import hashlib
import ipaddress
import os
# from app import app
from distutils.version import StrictVersion
from urllib.parse import urlparse
from datetime import datetime, timedelta
from .certutil import KEY_FILE, CERT_FILE
def auth_from_url(url):
auth = None
@ -185,101 +181,6 @@ def email_to_gravatar_url(email="", size=100):
return "https://s.gravatar.com/avatar/{0}?s={1}".format(hash_string, size)
def prepare_flask_request(request):
# If server is behind proxys or balancers use the HTTP_X_FORWARDED fields
url_data = urlparse(request.url)
return {
'https': 'on' if request.scheme == 'https' else 'off',
'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
}
def init_saml_auth(req):
own_url = ''
if req['https'] == 'on':
own_url = 'https://'
else:
own_url = 'http://'
own_url += req['http_host']
metadata = get_idp_data()
settings = {}
settings['sp'] = {}
if 'SAML_NAMEID_FORMAT' in app.config:
settings['sp']['NameIDFormat'] = app.config['SAML_NAMEID_FORMAT']
else:
settings['sp']['NameIDFormat'] = idp_data.get('sp', {}).get(
'NameIDFormat',
'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified')
settings['sp']['entityId'] = app.config['SAML_SP_ENTITY_ID']
if os.path.isfile(CERT_FILE):
cert = open(CERT_FILE, "r").readlines()
settings['sp']['x509cert'] = "".join(cert)
if os.path.isfile(KEY_FILE):
key = open(KEY_FILE, "r").readlines()
settings['sp']['privateKey'] = "".join(key)
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']['attributeConsumingService'] = {}
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'] = 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'
settings['security']['wantAssertionsEncrypted'] = False
settings['security']['wantAttributeStatement'] = True
settings['security']['wantNameId'] = True
settings['security']['authnRequestsSigned'] = app.config[
'SAML_SIGN_REQUEST']
settings['security']['logoutRequestSigned'] = app.config[
'SAML_SIGN_REQUEST']
settings['security']['logoutResponseSigned'] = app.config[
'SAML_SIGN_REQUEST']
settings['security']['nameIdEncrypted'] = False
settings['security']['signMetadata'] = True
settings['security']['wantAssertionsSigned'] = True
settings['security']['wantMessagesSigned'] = app.config.get(
'SAML_WANT_MESSAGE_SIGNED', True)
settings['security']['wantNameIdEncrypted'] = False
settings['contactPerson'] = {}
settings['contactPerson']['support'] = {}
settings['contactPerson']['support']['emailAddress'] = app.config[
'SAML_SP_CONTACT_NAME']
settings['contactPerson']['support']['givenName'] = app.config[
'SAML_SP_CONTACT_MAIL']
settings['contactPerson']['technical'] = {}
settings['contactPerson']['technical']['emailAddress'] = app.config[
'SAML_SP_CONTACT_NAME']
settings['contactPerson']['technical']['givenName'] = app.config[
'SAML_SP_CONTACT_MAIL']
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
auth = OneLogin_Saml2_Auth(req, settings)
return auth
def display_setting_state(value):
if value == 1:
return "ON"

View File

@ -27,11 +27,13 @@ from ..services.google import google_oauth
from ..services.github import github_oauth
from ..services.azure import azure_oauth
from ..services.oidc import oidc_oauth
from ..services.saml import SAML
google = None
github = None
azure = None
oidc = None
saml = None
index_bp = Blueprint('index',
__name__,
@ -45,10 +47,12 @@ def register_modules():
global github
global azure
global oidc
global saml
google = google_oauth()
github = github_oauth()
azure = azure_oauth()
oidc = oidc_oauth()
saml = SAML()
@index_bp.before_request
@ -311,8 +315,8 @@ def logout():
'SAML_ENABLED'
) and 'samlSessionIndex' in session and current_app.config.get(
'SAML_LOGOUT'):
req = utils.prepare_flask_request(request)
auth = utils.init_saml_auth(req)
req = saml.prepare_flask_request(request)
auth = saml.init_saml_auth(req)
if current_app.config.get('SAML_LOGOUT_URL'):
return redirect(
auth.logout(
@ -520,10 +524,10 @@ def dyndns_update():
def saml_login():
if not current_app.config.get('SAML_ENABLED'):
abort(400)
req = utils.prepare_flask_request(request)
auth = utils.init_saml_auth(req)
req = saml.prepare_flask_request(request)
auth = saml.init_saml_auth(req)
redirect_url = OneLogin_Saml2_Utils.get_self_url(req) + url_for(
'saml_authorized')
'index.saml_authorized')
return redirect(auth.login(return_to=redirect_url))
@ -533,8 +537,8 @@ def saml_metadata():
current_app.logger.error("SAML authentication is disabled.")
abort(400)
req = utils.prepare_flask_request(request)
auth = utils.init_saml_auth(req)
req = saml.prepare_flask_request(request)
auth = saml.init_saml_auth(req)
settings = auth.get_settings()
metadata = settings.get_sp_metadata()
errors = settings.validate_metadata(metadata)
@ -553,8 +557,8 @@ def saml_authorized():
if not current_app.config.get('SAML_ENABLED'):
current_app.logger.error("SAML authentication is disabled.")
abort(400)
req = utils.prepare_flask_request(request)
auth = utils.init_saml_auth(req)
req = saml.prepare_flask_request(request)
auth = saml.init_saml_auth(req)
auth.process_response()
errors = auth.get_errors()
if len(errors) == 0:
@ -659,7 +663,7 @@ def saml_authorized():
user.update_profile()
session['authentication_type'] = 'SAML'
login_user(user, remember=False)
return redirect(url_for('index'))
return redirect(url_for('index.login'))
else:
return render_template('errors/SAML.html', errors=errors)
@ -705,8 +709,8 @@ def uplift_to_admin(user):
@index_bp.route('/saml/sls')
def saml_logout():
req = utils.prepare_flask_request(request)
auth = utils.init_saml_auth(req)
req = saml.prepare_flask_request(request)
auth = saml.init_saml_auth(req)
url = auth.process_slo()
errors = auth.get_errors()
if len(errors) == 0:

View File

@ -0,0 +1,163 @@
from datetime import datetime, timedelta
from threading import Thread
from flask import current_app
import os
from ..lib.certutil import KEY_FILE, CERT_FILE
from ..lib.utils import urlparse
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
self.idp_timestamp = datetime.now()
self.OneLogin_Saml2_Auth = OneLogin_Saml2_Auth
self.OneLogin_Saml2_IdPMetadataParser = OneLogin_Saml2_IdPMetadataParser
self.idp_data = None
if 'SAML_IDP_ENTITY_ID' in current_app.config:
self.idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(
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:
self.idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(
current_app.config['SAML_METADATA_URL'],
entity_id=current_app.config.get('SAML_IDP_ENTITY_ID',
None))
if self.idp_data is None:
current_app.logger.info(
'SAML: IDP Metadata initial load failed')
exit(-1)
def get_idp_data(self):
lifetime = timedelta(minutes=current_app.config['SAML_METADATA_CACHE_LIFETIME'])
if self.idp_timestamp + lifetime < datetime.now():
background_thread = Thread(target=self.retrieve_idp_data())
background_thread.start()
return self.idp_data
def retrieve_idp_data(self):
if 'SAML_IDP_SSO_BINDING' in current_app.config:
new_idp_data = self.OneLogin_Saml2_IdPMetadataParser.parse_remote(
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:
new_idp_data = self.OneLogin_Saml2_IdPMetadataParser.parse_remote(
current_app.config['SAML_METADATA_URL'],
entity_id=current_app.config.get('SAML_IDP_ENTITY_ID', None))
if new_idp_data is not None:
self.idp_data = new_idp_data
self.idp_timestamp = datetime.now()
current_app.logger.info(
"SAML: IDP Metadata successfully retrieved from: " +
current_app.config['SAML_METADATA_URL'])
else:
current_app.logger.info(
"SAML: IDP Metadata could not be retrieved")
def prepare_flask_request(self, request):
# If server is behind proxys or balancers use the HTTP_X_FORWARDED fields
url_data = urlparse(request.url)
return {
'https': 'on' if request.scheme == 'https' else 'off',
'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
}
def init_saml_auth(self, req):
own_url = ''
if req['https'] == 'on':
own_url = 'https://'
else:
own_url = 'http://'
own_url += req['http_host']
metadata = self.get_idp_data()
settings = {}
settings['sp'] = {}
if 'SAML_NAMEID_FORMAT' in current_app.config:
settings['sp']['NameIDFormat'] = current_app.config[
'SAML_NAMEID_FORMAT']
else:
settings['sp']['NameIDFormat'] = self.idp_data.get('sp', {}).get(
'NameIDFormat',
'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified')
settings['sp']['entityId'] = current_app.config['SAML_SP_ENTITY_ID']
if os.path.isfile(CERT_FILE):
cert = open(CERT_FILE, "r").readlines()
settings['sp']['x509cert'] = "".join(cert)
if os.path.isfile(KEY_FILE):
key = open(KEY_FILE, "r").readlines()
settings['sp']['privateKey'] = "".join(key)
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']['attributeConsumingService'] = {}
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'
settings['security']['wantAssertionsEncrypted'] = True
settings['security']['wantAttributeStatement'] = True
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'][
'emailAddress'] = current_app.config['SAML_SP_CONTACT_MAIL']
settings['contactPerson']['technical'][
'givenName'] = current_app.config['SAML_SP_CONTACT_NAME']
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
auth = self.OneLogin_Saml2_Auth(req, settings)
return auth