This commit is contained in:
Vasileios Markopoulos 2022-01-04 00:09:26 +02:00 committed by GitHub
commit 991f45f3f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 920 additions and 236 deletions

View file

@ -37,117 +37,6 @@ SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
# MAIL_PASSWORD = None
# MAIL_DEFAULT_SENDER = ('PowerDNS-Admin', 'noreply@domain.ltd')
# SAML Authnetication
SAML_ENABLED = False
# SAML_DEBUG = True
# SAML_PATH = os.path.join(os.path.dirname(__file__), 'saml')
# ##Example for ADFS Metadata-URL
# SAML_METADATA_URL = 'https://<hostname>/FederationMetadata/2007-06/FederationMetadata.xml'
# #Cache Lifetime in Seconds
# SAML_METADATA_CACHE_LIFETIME = 1
# # SAML SSO binding format to use
# ## Default: library default (urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect)
# #SAML_IDP_SSO_BINDING = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
# ## EntityID of the IdP to use. Only needed if more than one IdP is
# ## in the SAML_METADATA_URL
# ### Default: First (only) IdP in the SAML_METADATA_URL
# ### Example: https://idp.example.edu/idp
# #SAML_IDP_ENTITY_ID = 'https://idp.example.edu/idp'
# ## NameID format to request
# ### Default: The SAML NameID Format in the metadata if present,
# ### otherwise urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
# ### Example: urn:oid:0.9.2342.19200300.100.1.1
# #SAML_NAMEID_FORMAT = 'urn:oid:0.9.2342.19200300.100.1.1'
# Following parameter defines RequestedAttributes section in SAML metadata
# since certain iDPs require explicit attribute request. If not provided section
# will not be available in metadata.
#
# Possible attributes:
# name (mandatory), nameFormat, isRequired, friendlyName
#
# NOTE: This parameter requires to be entered in valid JSON format as displayed below
# and multiple attributes can given
#
# Following example:
#
# SAML_SP_REQUESTED_ATTRIBUTES = '[ \
# {"name": "urn:oid:0.9.2342.19200300.100.1.3", "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "isRequired": true, "friendlyName": "email"}, \
# {"name": "mail", "isRequired": false, "friendlyName": "test-field"} \
# ]'
#
# produces following metadata section:
# <md:AttributeConsumingService index="1">
# <md:RequestedAttribute Name="urn:oid:0.9.2342.19200300.100.1.3" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="email" isRequired="true"/>
# <md:RequestedAttribute Name="mail" FriendlyName="test-field"/>
# </md:AttributeConsumingService>
# ## Attribute to use for Email address
# ### Default: email
# ### Example: urn:oid:0.9.2342.19200300.100.1.3
# #SAML_ATTRIBUTE_EMAIL = 'urn:oid:0.9.2342.19200300.100.1.3'
# ## Attribute to use for Given name
# ### Default: givenname
# ### Example: urn:oid:2.5.4.42
# #SAML_ATTRIBUTE_GIVENNAME = 'urn:oid:2.5.4.42'
# ## Attribute to use for Surname
# ### Default: surname
# ### Example: urn:oid:2.5.4.4
# #SAML_ATTRIBUTE_SURNAME = 'urn:oid:2.5.4.4'
# ## Attribute to use for username
# ### Default: Use NameID instead
# ### Example: urn:oid:0.9.2342.19200300.100.1.1
# #SAML_ATTRIBUTE_USERNAME = 'urn:oid:0.9.2342.19200300.100.1.1'
# ## Attribute to get admin status from
# ### Default: Don't control admin with SAML attribute
# ### Example: https://example.edu/pdns-admin
# ### If set, look for the value 'true' to set a user as an administrator
# ### If not included in assertion, or set to something other than 'true',
# ### the user is set as a non-administrator user.
# #SAML_ATTRIBUTE_ADMIN = 'https://example.edu/pdns-admin'
# ## Attribute to get account names from
# ### Default: Don't control accounts with SAML attribute
# ### If set, the user will be added and removed from accounts to match
# ### what's in the login assertion. Accounts that don't exist will
# ### be created and the user added to them.
# SAML_ATTRIBUTE_ACCOUNT = 'https://example.edu/pdns-account'
# SAML_SP_ENTITY_ID = 'http://<SAML SP Entity ID>'
# SAML_SP_CONTACT_NAME = '<contact name>'
# SAML_SP_CONTACT_MAIL = '<contact mail>'
# Configures the path to certificate file and it's respective private key file
# This pair is used for signing metadata, encrypting tokens and all other signing/encryption
# tasks during communication between iDP and SP
# NOTE: if this two parameters aren't explicitly provided, self-signed certificate-key pair
# will be generated in "PowerDNS-Admin" root directory
# ###########################################################################################
# CAUTION: For production use, usage of self-signed certificates it's highly discouraged.
# Use certificates from trusted CA instead
# ###########################################################################################
# SAML_CERT_FILE = '/etc/pki/powerdns-admin/cert.crt'
# SAML_CERT_KEY = '/etc/pki/powerdns-admin/key.pem'
# Configures if SAML tokens should be encrypted.
# SAML_SIGN_REQUEST = False
# #Use SAML standard logout mechanism retreived from idp metadata
# #If configured false don't care about SAML session on logout.
# #Logout from PowerDNS-Admin only and keep SAML session authenticated.
# SAML_LOGOUT = False
# #Configure to redirect to a different url then PowerDNS-Admin login after SAML logout
# #for example redirect to google.com after successful saml logout
# #SAML_LOGOUT_URL = 'https://google.com'
# #SAML_ASSERTION_ENCRYPTED = True
# Remote authentication settings
# Whether to enable remote user authentication or not

View file

@ -27,8 +27,4 @@ SQLALCHEMY_DATABASE_URI = 'mysql://{}:{}@{}/{}'.format(
)
### DATABASE - SQLite
# SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
# SAML Authnetication
SAML_ENABLED = False
SAML_ASSERTION_ENCRYPTED = True
# SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')

View file

@ -110,6 +110,51 @@ class Setting(db.Model):
'oidc_oauth_email': 'email',
'oidc_oauth_account_name_property': '',
'oidc_oauth_account_description_property': '',
'saml_enabled': False,
'saml_debug': True,
'saml_metadata_url': 'https://example.com/metadata.xml',
'saml_metadata_cache_lifetime': '15',
'saml_idp_sso_binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
'saml_idp_slo_binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
'saml_sp_acs_binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
'saml_sp_sls_binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
'saml_idp_entity_id': 'https://idp.example.com/idp',
'saml_nameid_format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified',
'saml_sp_requested_attributes': '[ \
{"name": "urn:oid:0.9.2342.19200300.100.1.1", "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "isRequired": true, "friendlyName": "username" }, \
{"name": "urn:oid:0.9.2342.19200300.100.1.3", "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "isRequired": true, "friendlyName": "email"}, \
{"name": "urn:oid:2.5.4.42", "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "isRequired": false, "friendlyName": "givenname"}, \
{"name": "urn:oid:2.5.4.4", "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "isRequired": false, "friendlyName": "surname" } \
]',
'saml_attribute_email': 'urn:oid:0.9.2342.19200300.100.1.3',
'saml_attribute_givenname': 'urn:oid:2.5.4.42',
'saml_attribute_surname': 'urn:oid:2.5.4.4',
'saml_attribute_username': 'urn:oid:0.9.2342.19200300.100.1.1',
'saml_attribute_admin': 'https://example.edu/pdns-admin',
'saml_attribute_account': 'https://example.edu/pdns-account',
'saml_attribute_name': None,
'saml_attribute_group': None,
'saml_group_admin_name': None,
'saml_group_to_account_mapping': None,
'saml_sp_entity_id': 'https://pdnsa.example.com',
'saml_sp_contact_name': 'admin',
'saml_sp_contact_mail': 'powerdnsadmin@organization.com',
'saml_cert_file': '/etc/pki/powerdns-admin/cert.crt',
'saml_cert_key': '/etc/pki/powerdns-admin/key.pem',
'saml_sign_authn_request': False,
'saml_sign_logout_request_response': False,
'saml_logout': False,
'saml_logout_url': 'https://google.com',
'saml_want_assertions_encrypted': False,
'saml_digest_algorithm': 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
'saml_signature_algorithm': 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
'saml_want_assertions_signed': False,
'saml_sign_metadata': False,
'saml_want_message_signed': False,
'saml_nameid_encrypted': False,
'saml_want_nameid_encrypted': False,
'saml_metadata_cache_duration': None,
'saml_metadata_valid_until': None,
'forward_records_allow_edit': {
'A': True,
'AAAA': True,

View file

@ -669,11 +669,11 @@ class User(db.Model):
current_app.logger.warning("Cannot apply autoprovisioning on user: {}".format(e))
return entitlements
def updateUser(self, Entitlements):
def updateUser(self, Entitlements, urn_value):
"""
Update user associations based on ldap attribute
"""
entitlements= getCorrectEntitlements(Entitlements)
entitlements= getCorrectEntitlements(Entitlements, urn_value)
if len(entitlements)!=0:
self.revoke_privilege(True)
for entitlement in entitlements:
@ -712,12 +712,11 @@ class User(db.Model):
if account!=None:
account.add_user(user)
def getCorrectEntitlements(Entitlements):
def getCorrectEntitlements(Entitlements, urn_value):
"""
Gather a list of valid records from the ldap attribute given
"""
from ..models.role import Role
urn_value=Setting().get('urn_value')
urnArgs=[x.lower() for x in urn_value.split(':')]
entitlements=[]
for Entitlement in Entitlements:

View file

@ -22,7 +22,6 @@ from ..models.domain_template import DomainTemplate
from ..models.domain_template_record import DomainTemplateRecord
from ..models.api_key import ApiKey
from ..models.base import db
from ..lib.schema import ApiPlainKeySchema
apikey_plain_schema = ApiPlainKeySchema(many=True)
@ -1367,7 +1366,8 @@ def has_an_auth_method(local_db_enabled=None,
google_oauth_enabled=None,
github_oauth_enabled=None,
oidc_oauth_enabled=None,
azure_oauth_enabled=None):
azure_oauth_enabled=None,
saml_enabled=None):
if local_db_enabled is None:
local_db_enabled = Setting().get('local_db_enabled')
if ldap_enabled is None:
@ -1380,6 +1380,8 @@ def has_an_auth_method(local_db_enabled=None,
oidc_oauth_enabled = Setting().get('oidc_oauth_enabled')
if azure_oauth_enabled is None:
azure_oauth_enabled = Setting().get('azure_oauth_enabled')
if saml_enabled is None:
saml_enabled = Setting().get('saml_enabled')
return local_db_enabled or ldap_enabled or google_oauth_enabled or github_oauth_enabled or oidc_oauth_enabled or azure_oauth_enabled
@ -1621,6 +1623,115 @@ def setting_authentication():
'msg':
'Saved successfully. Please reload PDA to take effect.'
}
elif conf_type == 'saml':
saml_enabled = True if request.form.get('saml_enabled') else False
if not has_an_auth_method(saml_enabled=saml_enabled):
result = {
'status':
False,
'msg':
'Must have at least one authentication method enabled.'
}
else:
Setting().set(
'saml_enabled',
True if request.form.get('saml_enabled') else False)
Setting().set('saml_metadata_url',
request.form.get('saml_metadata_url'))
if request.form.get('saml_metadata_cache_lifetime'):
Setting().set('saml_metadata_cache_lifetime',
request.form.get('saml_metadata_cache_lifetime'))
else:
Setting().set('saml_metadata_cache_lifetime',
Setting().defaults['saml_metadata_cache_lifetime'])
Setting().set('saml_idp_sso_binding',
request.form.get('saml_idp_sso_binding'))
Setting().set('saml_idp_slo_binding',
request.form.get('saml_idp_slo_binding'))
Setting().set('saml_idp_entity_id',
request.form.get('saml_idp_entity_id'))
Setting().set('saml_nameid_format',
request.form.get('saml_nameid_format'))
Setting().set('saml_sp_acs_binding',
request.form.get('saml_sp_acs_binding'))
Setting().set('saml_sp_sls_binding',
request.form.get('saml_sp_sls_binding'))
Setting().set('saml_sp_requested_attributes',
request.form.get('saml_sp_requested_attributes'))
Setting().set('saml_attribute_email',
request.form.get('saml_attribute_email'))
Setting().set('saml_attribute_givenname',
request.form.get('saml_attribute_givenname'))
Setting().set('saml_attribute_surname',
request.form.get('saml_attribute_surname'))
Setting().set('saml_attribute_username',
request.form.get('saml_attribute_username'))
Setting().set('saml_attribute_admin',
request.form.get('saml_attribute_admin'))
Setting().set('saml_attribute_account',
request.form.get('saml_attribute_account'))
Setting().set('saml_sp_entity_id',
request.form.get('saml_sp_entity_id'))
if request.form.get('saml_sp_contact_name'):
Setting().set('saml_sp_contact_name',
request.form.get('saml_sp_contact_name'))
else:
Setting().set('saml_sp_contact_name',
Setting().defaults['saml_sp_contact_name'])
if request.form.get('saml_sp_contact_mail'):
Setting().set('saml_sp_contact_mail',
request.form.get('saml_sp_contact_mail'))
else:
Setting().set('saml_sp_contact_mail',
Setting().defaults['saml_sp_contact_mail'])
Setting().set('saml_cert_file',
request.form.get('saml_cert_file'))
Setting().set('saml_cert_key',
request.form.get('saml_cert_key'))
Setting().set(
'saml_sign_authn_request',
True if request.form.get('saml_sign_authn_request') else False)
Setting().set(
'saml_sign_logout_request_response',
True if request.form.get('saml_sign_logout_request_response') else False)
Setting().set(
'saml_logout',
True if request.form.get('saml_logout') else False)
if request.form.get('saml_logout_url'):
Setting().set('saml_logout_url',
request.form.get('saml_logout_url'))
Setting().set(
'saml_want_assertions_encrypted',
True if request.form.get('saml_want_assertions_encrypted') else False)
Setting().set(
'saml_want_assertions_signed',
True if request.form.get('saml_want_assertions_signed') else False)
Setting().set(
'saml_want_nameid_encrypted',
True if request.form.get('saml_want_nameid_encrypted') else False)
Setting().set(
'saml_nameid_encrypted',
True if request.form.get('saml_nameid_encrypted') else False)
Setting().set('saml_digest_algorithm',
request.form.get('saml_digest_algorithm'))
Setting().set('saml_signature_algorithm',
request.form.get('saml_signature_algorithm'))
Setting().set(
'saml_want_message_signed',
True if request.form.get('saml_want_message_signed') else False)
Setting().set(
'saml_sign_metadata',
True if request.form.get('saml_sign_metadata') else False)
Setting().set('saml_metadata_cache_duration',
request.form.get('saml_metadata_cache_duration'))
Setting().set('saml_metadata_valid_until',
request.form.get('saml_metadata_valid_until'))
result = {
'status': True,
'msg':
'Saved successfully. Please reload PDA to take effect.'
}
else:
return abort(400)

View file

@ -142,7 +142,7 @@ def oidc_login():
@index_bp.route('/login', methods=['GET', 'POST'])
def login():
SAML_ENABLED = current_app.config.get('SAML_ENABLED')
SAML_ENABLED = Setting().get('saml_enabled')
if g.user is not None and current_user.is_authenticated:
return redirect(url_for('dashboard.dashboard'))
@ -496,7 +496,7 @@ def login():
elif len(Entitlements)!=0:
if checkForPDAEntries(Entitlements, urn_value):
user.updateUser(Entitlements)
user.updateUser(Entitlements, urn_value)
else:
current_app.logger.warning('Not a single powerdns-admin record was found, possibly a typo in the prefix')
if Setting().get('purge'):
@ -594,26 +594,33 @@ def prepare_welcome_user(user_id):
@index_bp.route('/logout')
def logout():
if current_app.config.get(
'SAML_ENABLED'
) and 'samlSessionIndex' in session and current_app.config.get(
'SAML_LOGOUT'):
if Setting().get('saml_enabled'
) and 'samlSessionIndex' in session and Setting().get(
'saml_logout'):
req = saml.prepare_flask_request(request)
auth = saml.init_saml_auth(req)
if current_app.config.get('SAML_LOGOUT_URL'):
if Setting().get('saml_logout_url'):
try:
return redirect(
auth.logout(
name_id_format=
Setting().get('saml_nameid_format'),
return_to=Setting().get('saml_logout_url'),
session_index=session['samlSessionIndex'],
name_id=session['samlNameId']))
except:
current_app.logger.info(
"SAML: Your IDP does not support Single Logout.")
try:
return redirect(
auth.logout(
name_id_format=
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
return_to=current_app.config.get('SAML_LOGOUT_URL'),
Setting().get('saml_nameid_format'),
session_index=session['samlSessionIndex'],
name_id=session['samlNameId']))
return redirect(
auth.logout(
name_id_format=
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
session_index=session['samlSessionIndex'],
name_id=session['samlNameId']))
except:
current_app.logger.info(
"SAML: Your IDP does not support Single Logout.")
redirect_uri = url_for('index.login')
oidc_logout = Setting().get('oidc_oauth_logout_url')
@ -930,25 +937,42 @@ def dyndns_update():
### START SAML AUTHENTICATION ###
@index_bp.route('/saml/login')
def saml_login():
if not current_app.config.get('SAML_ENABLED'):
if not Setting().get('saml_enabled'):
abort(400)
global saml
req = saml.prepare_flask_request(request)
auth = saml.init_saml_auth(req)
try:
auth = saml.init_saml_auth(req)
except:
current_app.logger.info(
"SAML: IDP Metadata were not successfully initialized. Reinitializing...")
saml = SAML()
req = saml.prepare_flask_request(request)
auth = saml.init_saml_auth(req)
redirect_url = OneLogin_Saml2_Utils.get_self_url(req) + url_for(
'index.saml_authorized')
if auth is None:
return render_template('errors/SAML.html')
return redirect(auth.login(return_to=redirect_url))
@index_bp.route('/saml/metadata')
def saml_metadata():
if not current_app.config.get('SAML_ENABLED'):
if not Setting().get('saml_enabled'):
current_app.logger.error("SAML authentication is disabled.")
abort(400)
req = saml.prepare_flask_request(request)
auth = saml.init_saml_auth(req)
if auth is None:
return render_template('errors/SAML.html')
settings = auth.get_settings()
metadata = settings.get_sp_metadata()
try:
metadata = settings.get_sp_metadata()
except:
current_app.logger.error(
"SAML: Error fetching SP Metadata")
return render_template('errors/SAML.html')
errors = settings.validate_metadata(metadata)
if len(errors) == 0:
@ -958,15 +982,16 @@ def saml_metadata():
resp = make_response(errors.join(', '), 500)
return resp
@index_bp.route('/saml/authorized', methods=['GET', 'POST'])
def saml_authorized():
errors = []
if not current_app.config.get('SAML_ENABLED'):
if not Setting().get('saml_enabled'):
current_app.logger.error("SAML authentication is disabled.")
abort(400)
req = saml.prepare_flask_request(request)
auth = saml.init_saml_auth(req)
if auth is None:
return render_template('errors/SAML.html')
auth.process_response()
current_app.logger.debug( auth.get_attributes() )
errors = auth.get_errors()
@ -979,9 +1004,9 @@ def saml_authorized():
if 'RelayState' in request.form and self_url != request.form[
'RelayState']:
return redirect(auth.redirect_to(request.form['RelayState']))
if current_app.config.get('SAML_ATTRIBUTE_USERNAME', False):
if Setting().get('saml_attribute_username'):
username = session['samlUserdata'][
current_app.config['SAML_ATTRIBUTE_USERNAME']][0].lower()
Setting().get('saml_attribute_username')][0].lower()
else:
username = session['samlNameId'].lower()
user = User.query.filter_by(username=username).first()
@ -992,22 +1017,38 @@ def saml_authorized():
email=session['samlNameId'])
user.create_local_user()
session['user_id'] = user.id
email_attribute_name = current_app.config.get('SAML_ATTRIBUTE_EMAIL',
'email')
givenname_attribute_name = current_app.config.get(
'SAML_ATTRIBUTE_GIVENNAME', 'givenname')
surname_attribute_name = current_app.config.get(
'SAML_ATTRIBUTE_SURNAME', 'surname')
name_attribute_name = current_app.config.get('SAML_ATTRIBUTE_NAME',
None)
account_attribute_name = current_app.config.get(
'SAML_ATTRIBUTE_ACCOUNT', None)
admin_attribute_name = current_app.config.get('SAML_ATTRIBUTE_ADMIN',
None)
group_attribute_name = current_app.config.get('SAML_ATTRIBUTE_GROUP',
None)
admin_group_name = current_app.config.get('SAML_GROUP_ADMIN_NAME',
None)
if Setting().get('saml_attribute_email'):
email_attribute_name = Setting().get('saml_attribute_email')
else:
email_attribute_name = 'email'
if Setting().get('saml_attribute_givenname'):
givenname_attribute_name = Setting().get('saml_attribute_givenname')
else:
givenname_attribute_name = 'givenname'
if Setting().get('saml_attribute_surname'):
surname_attribute_name = Setting().get('saml_attribute_surname')
else:
surname_attribute_name = 'surname'
if Setting().get('saml_attribute_name'):
name_attribute_name = Setting().get('saml_attribute_name')
else:
name_attribute_name = None
if Setting().get('saml_attribute_account'):
account_attribute_name = Setting().get('saml_attribute_account')
else:
account_attribute_name = None
if Setting().get('saml_attribute_admin'):
admin_attribute_name = Setting().get('saml_attribute_admin')
else:
admin_attribute_name = None
if Setting().get('saml_attribute_group'):
group_attribute_name = Setting().get('saml_attribute_group')
else:
group_attribute_name = None
if Setting().get('saml_group_admin_name'):
admin_group_name = Setting().get('saml_group_admin_name')
else:
admin_group_name = None
group_to_account_mapping = create_group_to_account_mapping()
if email_attribute_name in session['samlUserdata']:
@ -1068,6 +1109,7 @@ def saml_authorized():
user.username),
created_by='SAML Assertion')
history.add()
user.plain_text_password = None
user.update_profile()
session['authentication_type'] = 'SAML'
@ -1077,8 +1119,7 @@ def saml_authorized():
def create_group_to_account_mapping():
group_to_account_mapping_string = current_app.config.get(
'SAML_GROUP_TO_ACCOUNT_MAPPING', None)
group_to_account_mapping_string = Setting().get('saml_group_to_account_mapping')
if group_to_account_mapping_string and len(
group_to_account_mapping_string.strip()) > 0:
group_to_account_mapping = group_to_account_mapping_string.split(',')
@ -1119,17 +1160,23 @@ def uplift_to_admin(user):
@index_bp.route('/saml/sls')
@login_required
def saml_logout():
if not Setting().get('saml_enabled'):
current_app.logger.error("SAML authentication is disabled.")
abort(400)
req = saml.prepare_flask_request(request)
auth = saml.init_saml_auth(req)
if auth is None:
return render_template('errors/SAML.html')
url = auth.process_slo()
errors = auth.get_errors()
if len(errors) == 0:
clear_session()
if url is not None:
return redirect(url)
elif current_app.config.get('SAML_LOGOUT_URL') is not None:
return redirect(current_app.config.get('SAML_LOGOUT_URL'))
elif Setting().get('saml_logout_url') is not None:
return redirect(Setting().get('saml_logout_url'))
else:
return redirect(url_for('login'))
else:

View file

@ -6,11 +6,17 @@ import os
from ..lib.certutil import KEY_FILE, CERT_FILE, create_self_signed_cert
from ..lib.utils import urlparse
from ..models.setting import Setting
# The python3-saml library currently supports only the Redirect binding for IDP endpoints.
# For SP, the Assertion Consumer Service endpoint supports HTTP-POST binding,
# while the Single Logout Service endpoint uses HTTP-Redirect.
# Therefore, to protect users from using unsupported features, settings
# 'saml_idp_sso_binding', 'saml_idp_slo_binding', 'saml_sp_acs_binding' and 'saml_sp_sls_binding'
# are not exposed on the front end SAML interface.
class SAML(object):
def __init__(self):
if current_app.config['SAML_ENABLED']:
if Setting().get('saml_enabled'):
from onelogin.saml2.auth import OneLogin_Saml2_Auth
from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser
@ -19,29 +25,34 @@ class SAML(object):
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'])
if Setting().get('saml_idp_entity_id'):
try:
self.idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(
Setting().get('saml_metadata_url'),
entity_id=Setting().get('saml_idp_entity_id'),
required_sso_binding=Setting().get('saml_idp_sso_binding'))
except:
self.idp_data = None
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))
try:
self.idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(
Setting().get('saml_metadata_url'),
entity_id=None)
except:
self.idp_data = None
if self.idp_data is None:
current_app.logger.info(
current_app.logger.error(
'SAML: IDP Metadata initial load failed')
exit(-1)
Setting().set('saml_enabled', False)
def get_idp_data(self):
lifetime = timedelta(
minutes=current_app.config['SAML_METADATA_CACHE_LIFETIME'])
if self.idp_timestamp + lifetime < datetime.now():
minutes=int(Setting().get('saml_metadata_cache_lifetime')))
if not hasattr(self,'idp_timestamp'):
self.retrieve_idp_data()
elif self.idp_timestamp + lifetime < datetime.now():
background_thread = Thread(target=self.retrieve_idp_data())
background_thread.start()
@ -49,22 +60,27 @@ class SAML(object):
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']
)
if Setting().get('saml_idp_sso_binding'):
try:
new_idp_data = self.OneLogin_Saml2_IdPMetadataParser.parse_remote(
Setting().get('saml_metadata_url'),
entity_id=Setting().get('saml_idp_entity_id'),
required_sso_binding=Setting().get('saml_idp_sso_binding'))
except:
new_idp_data = None
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))
try:
new_idp_data = self.OneLogin_Saml2_IdPMetadataParser.parse_remote(
Setting().get('saml_metadata_url'),
entity_id=Setting().get('saml_idp_entity_id'))
except:
new_idp_data = 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'])
Setting().get('saml_metadata_url'))
else:
current_app.logger.info(
"SAML: IDP Metadata could not be retrieved")
@ -94,20 +110,19 @@ class SAML(object):
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']
if Setting().get('saml_nameid_format'):
settings['sp']['NameIDFormat'] = Setting().get('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']
settings['sp']['entityId'] = Setting().get('saml_sp_entity_id')
if ('SAML_CERT' in current_app.config) and ('SAML_KEY' in current_app.config):
if (Setting().get('saml_cert_file')) and (Setting().get('saml_cert_key')):
saml_cert_file = current_app.config['SAML_CERT']
saml_key_file = current_app.config['SAML_KEY']
saml_cert_file = Setting().get('saml_cert_file')
saml_key_file = Setting().get('saml_cert_key')
if os.path.isfile(saml_cert_file):
cert = open(saml_cert_file, "r").readlines()
@ -130,8 +145,8 @@ class SAML(object):
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'])
if Setting().get('saml_sp_requested_attributes'):
saml_req_attr = json.loads(Setting().get('saml_sp_requested_attributes'))
settings['sp']['attributeConsumingService'] = {
"serviceName": "PowerDNSAdmin",
"serviceDescription": "PowerDNS-Admin - PowerDNS administration utility",
@ -143,55 +158,52 @@ class SAML(object):
settings['sp']['assertionConsumerService'] = {}
settings['sp']['assertionConsumerService'][
'binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
'binding'] = Setting().get('saml_sp_acs_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'
'binding'] = Setting().get('saml_sp_sls_binding')#'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
settings['sp']['singleLogoutService']['url'] = own_url + '/saml/sls'
settings['idp'] = metadata['idp']
if metadata is not None and 'idp' in metadata:
settings['idp'] = metadata['idp']
settings['strict'] = True
settings['debug'] = current_app.config['SAML_DEBUG']
settings['debug'] = bool(Setting().get('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
'digestAlgorithm'] = Setting().get('saml_digest_algorithm')
settings['security']['metadataCacheDuration'] = Setting().get('saml_metadata_cache_duration') if Setting().get('saml_metadata_cache_duration') else None
settings['security']['metadataValidUntil'] = Setting().get('saml_metadata_valid_until') if Setting().get('saml_metadata_valid_until') else None
settings['security']['requestedAuthnContext'] = True
settings['security'][
'signatureAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
settings['security']['wantAssertionsEncrypted'] = current_app.config.get(
'SAML_ASSERTION_ENCRYPTED', True)
'signatureAlgorithm'] = Setting().get('saml_signature_algorithm')
settings['security']['wantAssertionsEncrypted'] = bool(Setting().get('saml_want_assertions_encrypted'))
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['security']['authnRequestsSigned'] = bool(Setting().get('saml_sign_authn_request'))
settings['security']['logoutRequestSigned'] = bool(Setting().get('saml_sign_logout_request_response'))
settings['security']['logoutResponseSigned'] = bool(Setting().get('saml_sign_logout_request_response'))
settings['security']['nameIdEncrypted'] = bool(Setting().get('saml_nameid_encrypted'))
settings['security']['signMetadata'] = bool(Setting().get('saml_sign_metadata'))
settings['security']['wantAssertionsSigned'] = bool(Setting().get('saml_want_assertions_signed'))
settings['security']['wantMessagesSigned'] = bool(Setting().get('saml_want_message_signed'))
settings['security']['wantNameIdEncrypted'] = bool(Setting().get('saml_want_nameid_encrypted'))
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']['support']['emailAddress'] = Setting().get('saml_sp_contact_mail')
settings['contactPerson']['support']['givenName'] = Setting().get('saml_sp_contact_name')
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['contactPerson']['technical']['emailAddress'] = Setting().get('saml_sp_contact_mail')
settings['contactPerson']['technical']['givenName'] = Setting().get('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)
try:
auth = self.OneLogin_Saml2_Auth(req, settings)
except:
current_app.logger.error(
"SAML: SAML Authentication failed")
auth = None
return auth

View file

@ -56,6 +56,7 @@
<li><a href="#tabs-github" data-toggle="tab">Github OAuth</a></li>
<li><a href="#tabs-azure" data-toggle="tab">Microsoft OAuth</a></li>
<li><a href="#tabs-oidc" data-toggle="tab">OpenID Connect OAuth</a></li>
<li><a href="#tabs-saml" data-toggle="tab">SAML</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="tabs-general">
@ -677,6 +678,438 @@
</div>
</div>
</div>
<div class="tab-pane" id="tabs-saml">
<div class="row">
<div class="col-md-4">
<form role="form" method="post" data-toggle="validator">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<input type="hidden" value="saml" name="config_tab" />
<fieldset>
<legend>GENERAL</legend>
<div class="form-group">
<input type="checkbox" id="saml_enabled" name="saml_enabled" class="checkbox" {% if SETTING.get('saml_enabled') %}checked{% endif %}>
<label for="saml_enabled">Enable SAML</label>
</div>
</fieldset>
<fieldset>
<legend>IDP</legend>
<div class="form-group">
<label for="saml_idp_entity_id">IDP Entity ID</label>
<input type="text" class="form-control" name="saml_idp_entity_id" id="saml_idp_entity_id" placeholder="e.g. https://idp.example.edu/idp" data-error="Please input SAML IDP Entity ID" value="{{ SETTING.get('saml_idp_entity_id') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="saml_metadata_url">IDP Metadata URL</label>
<input type="text" class="form-control" name="saml_metadata_url" id="saml_metadata_url" placeholder="SAML Metadata URL" data-error="Please input SAML Metadata URL" value="{{ SETTING.get('saml_metadata_url') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="saml_metadata_cache_lifetime">IDP Metadata Cache Lifetime</label>
<input type="text" class="form-control" name="saml_metadata_cache_lifetime" id="saml_metadata_cache_lifetime" placeholder="SAML Metadata Cache Lifetime" data-error="Please input SAML Metadata Cache Lifetime" value="{{ SETTING.get('saml_metadata_cache_lifetime') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="saml_idp_sso_binding">IDP SSO Binding</label>
<input type="text" class="form-control" name="saml_idp_sso_binding" id="saml_idp_sso_binding" placeholder="e.g. urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" data-error="Please input SAML IDP SSO Binding" value="{{ SETTING.get('saml_idp_sso_binding') }}" readonly>
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="saml_idp_slo_binding">IDP SLO Binding</label>
<input type="text" class="form-control" name="saml_idp_slo_binding" id="saml_idp_slo_binding" placeholder="e.g. urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" data-error="Please input SAML IDP SLO Binding" value="{{ SETTING.get('saml_idp_slo_binding') }}" readonly>
<span class="help-block with-errors"></span>
</div>
</fieldset>
<fieldset>
<legend>SP</legend>
<div class="form-group">
<label for="saml_sp_entity_id">SP Entity ID</label>
<input type="text" class="form-control" name="saml_sp_entity_id" id="saml_sp_entity_id" placeholder="http://<SAML SP Entity ID>" data-error="Please input SAML SP Entity ID" value="{{ SETTING.get('saml_sp_entity_id') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="saml_nameid_format">SP NameID Format</label>
<input type="text" class="form-control" name="saml_nameid_format" id="saml_nameid_format" placeholder="e.g. urn:oid:0.9.2342.19200300.100.1.1" data-error="Please input NameID Format" value="{{ SETTING.get('saml_nameid_format') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="saml_metadata_cache_duration">SP Metadata Cache Duration</label>
<input type="text" class="form-control" name="saml_metadata_cache_duration" id="saml_metadata_cache_duration" placeholder="eg PT5M" data-error="Please input Metadata Cache Duration" value="{{ SETTING.get('saml_metadata_cache_duration') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="saml_metadata_valid_until">SP Metadata Valid Until</label>
<input type="text" class="form-control" name="saml_metadata_valid_until" id="saml_metadata_valid_until" placeholder="YYYY-MM-DDThh:mm:ssZ" data-error="Please input Metadata Expiration Date" value="{{ SETTING.get('saml_metadata_valid_until') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<input type="checkbox" id="saml_sign_metadata" name="saml_sign_metadata" class="checkbox" {% if SETTING.get('saml_sign_metadata') %}checked{% endif %}>
<label for="saml_sign_metadata">Sign SP Metadata </label>
</div>
<div class="form-group">
<label for="saml_sp_acs_binding">SP ACS Binding</label>
<input type="text" class="form-control" name="saml_sp_acs_binding" id="saml_sp_acs_binding" placeholder="e.g. urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" data-error="Please input SAML SP ACS Binding" value="{{ SETTING.get('saml_sp_acs_binding') }}" readonly>
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="saml_sp_acs_binding">SP SLS Binding</label>
<input type="text" class="form-control" name="saml_sp_sls_binding" id="saml_sp_sls_binding" placeholder="e.g. urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" data-error="Please input SAML SP SLS Binding" value="{{ SETTING.get('saml_sp_sls_binding') }}" readonly>
<span class="help-block with-errors"></span>
</div>
</fieldset>
<fieldset>
<legend>SP ATTRIBUTES</legend>
<div class="form-group">
<label for="saml_sp_requested_attributes">Requested Attributes</label>
<input type="text" class="form-control" name="saml_sp_requested_attributes" id="saml_sp_requested_attributes" placeholder="must be valid JSON" data-error="Plesae input Requested Attributes" value="{{ SETTING.get('saml_sp_requested_attributes') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="saml_attribute_email">Email</label>
<input type="text" class="form-control" name="saml_attribute_email" id="saml_attribute_email" placeholder="e.g. urn:oid:0.9.2342.19200300.100.1.3" data-error="Please input SAML Email Attribute" value="{{ SETTING.get('saml_attribute_email') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="saml_attribute_givenname">Given Name</label>
<input type="text" class="form-control" name="saml_attribute_givenname" id="saml_attribute_givenname" placeholder="e.g. urn:oid:2.5.4.42" data-error="Please input SAML Given Name Attribute" value="{{ SETTING.get('saml_attribute_givenname') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="saml_attribute_surname">Surname</label>
<input type="text" class="form-control" name="saml_attribute_surname" id="saml_attribute_surname" placeholder="e.g. urn:oid:2.5.4.4" data-error="Please input SAML Surname Attribute" value="{{ SETTING.get('saml_attribute_surname') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="saml_attribute_username">Username</label>
<input type="text" class="form-control" name="saml_attribute_username" id="saml_attribute_username" placeholder="e.g. urn:oid:0.9.2342.19200300.100.1.1" data-error="Please input SAML Username Attribute" value="{{ SETTING.get('saml_attribute_username') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="saml_sp_contact_name">SP Contact Name</label>
<input type="text" class="form-control" name="saml_sp_contact_name" id="saml_sp_contact_name" placeholder="<contact name>" data-error="Please input SAML SP contact name" value="{{ SETTING.get('saml_sp_contact_name') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="saml_sp_contact_mail">SP Contact Mail</label>
<input type="text" class="form-control" name="saml_sp_contact_mail" id="saml_sp_contact_mail" placeholder="<contact mail>" data-error="Please input SAML SP contact mail" value="{{ SETTING.get('saml_sp_contact_mail') }}">
<span class="help-block with-errors"></span>
</div>
</fieldset>
<fieldset>
<legend>SIGNING & ENCRYPTION</legend>
<div class="form-group">
<label for="saml_cert_file">Cert File</label>
<input type="text" class="form-control" name="saml_cert_file" id="saml_cert_file" placeholder="e.g. opt/web/PowerDNS-Admin/cert.crt" data-error="Please input SAML cert file path" value="{{ SETTING.get('saml_cert_file') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="saml_cert_key">Cert Key</label>
<input type="text" class="form-control" name="saml_cert_key" id="saml_cert_key" placeholder="e.g. opt/web/PowerDNS-Admin/key.pem" data-error="Please input SAML key file path" value="{{ SETTING.get('saml_cert_key') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<input type="checkbox" id="saml_sign_authn_request" name="saml_sign_authn_request" class="checkbox" {% if SETTING.get('saml_sign_authn_request') %}checked{% endif %}>
<label for="saml_sign_authn_request">Sign Authentication Request</label>
</div>
<div class="form-group">
<input type="checkbox" id="saml_sign_logout_request_response" name="saml_sign_logout_request_response" class="checkbox" {% if SETTING.get('saml_sign_logout_request_response') %}checked{% endif %}>
<label for="saml_sign_logout_request_response">Sign Logout Request & Response</label>
</div>
<div class="form-group">
<input type="checkbox" id="saml_want_assertions_encrypted" name="saml_want_assertions_encrypted" class="checkbox" {% if SETTING.get('saml_want_assertions_encrypted') %}checked{% endif %}>
<label for="saml_want_assertions_encrypted">Want Assertions Encrypted</label>
</div>
<div class="form-group">
<input type="checkbox" id="saml_want_assertions_signed" name="saml_want_assertions_signed" class="checkbox" {% if SETTING.get('saml_want_assertions_signed') %}checked{% endif %}>
<label for="saml_want_assertions_signed">Want Assertions Signed</label>
</div>
<div class="form-group">
<input type="checkbox" id="saml_want_message_signed" name="saml_want_message_signed" class="checkbox" {% if SETTING.get('saml_want_message_signed') %}checked{% endif %}>
<label for="saml_want_message_signed">Want Message Signed </label>
</div>
<div class="form-group">
<input type="checkbox" id="saml_nameid_encrypted" name="saml_nameid_encrypted" class="checkbox" {% if SETTING.get('saml_nameid_encrypted') %}checked{% endif %}>
<label for="saml_nameid_encrypted">NameID Encrypted</label>
</div>
<div class="form-group">
<input type="checkbox" id="saml_want_nameid_encrypted" name="saml_want_nameid_encrypted" class="checkbox" {% if SETTING.get('saml_want_nameid_encrypted') %}checked{% endif %}>
<label for="saml_want_nameid_encrypted">Want NameID Encrypted</label>
</div>
<div class="form-group">
<label for="saml_digest_algorithm">Digest Algorithm</label>
<input type="text" class="form-control" name="saml_digest_algorithm" id="saml_digest_algorithm" placeholder="must be a valid algorithm" data-error="Please input SAML digest algorithm" value="{{ SETTING.get('saml_digest_algorithm') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="saml_signature_algorithm">Signature Algorithm</label>
<input type="text" class="form-control" name="saml_signature_algorithm" id="saml_signature_algorithm" placeholder="must be a valid algorithm" data-error="Please input SAML signature algorithm" value="{{ SETTING.get('saml_signature_algorithm') }}">
<span class="help-block with-errors"></span>
</div>
</fieldset>
<fieldset>
<legend>LOGOUT</legend>
<div class="form-group">
<input type="checkbox" id="saml_logout" name="saml_logout" class="checkbox" {% if SETTING.get('saml_logout') %}checked{% endif %}>
<label for="saml_logout">SAML Logout</label>
</div>
<div class="form-group">
<label for="saml_logout_url">Logout URL</label>
<input type="text" class="form-control" name="saml_logout_url" id="saml_logout_url" placeholder="must be a valid logout URL" data-error="Please input SAML logout URL" value="{{ SETTING.get('saml_logout_url') }}">
<span class="help-block with-errors"></span>
</div>
</fieldset>
<fieldset>
<legend>AUTOPROVISION</legend>
<div class="form-group">
<label for="saml_attribute_admin">Admin SP Attribute</label>
<input type="text" class="form-control" name="saml_attribute_admin" id="saml_attribute_admin" placeholder="e.g. https://example.edu/pdns-admin" data-error="Please input SAML Admin Attribute" value="{{ SETTING.get('saml_attribute_admin') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="saml_attribute_account">Account SP Attribute</label>
<input type="text" class="form-control" name="saml_attribute_account" id="saml_attribute_account" placeholder="e.g. https://example.edu/pdns-account" data-error="Please input SAML Account Attribute" value="{{ SETTING.get('saml_attribute_account') }}">
<span class="help-block with-errors"></span>
</div>
</fieldset>
<div class="form-group">
<button type="submit" class="btn btn-flat btn-primary">Save</button>
</div>
</form>
</div>
<div class="col-md-8">
<legend>Help</legend>
<dl class="dl-horizontal">
<dt>General</dt>
<dd> <br>
<b>Enable SAML</b> - Enables or disables SAML Authentication. <br>
If toggled on, the following fields must be filled in the left form:
<ul>
<li>
<b>IDP Entity ID</b>
</li>
<li>
<b>IDP Metadata URL</b>
</li>
<li>
<b>SP Entity ID</b>
</li>
<li>
<b>SP NameID Format</b>
</li>
<li>
<b>SP Requested Attributes</b>
</li>
<li>
<b>SP Username Attribute</b>
</li>
<li>
<b>Cert File</b>
</li>
<li>
<b>Cert Key</b>
</li>
<li>
<b>Digest Algorithm</b>
</li>
<li>
<b>Signature Algorithm</b>
</li>
<li>
<b>Logout URL</b>, if <i>SAML Logout</i> is toggled on
</li>
<li>
<b>Roles Provisioning Field</b> and <b>Urn Prefix</b>, if <i>Roles Autoprovisioning</i> is toggled on.
</li>
</ul>
The rest can be filled, or left empty to revert to the Default settings.
</dd>
<dt>IDP</dt>
<dd> <br>
<ul>
<li>
<b>IDP Entity ID</b> - Specify the EntityID of the IDP to use.<br>
Only needed if more the XML provided in the SAML_METADATA_URL contains more than 1 IDP Entity.
</li>
<li>
<b>IDP Metadata URL</b> - Url where the XML of the Identity Provider Metadata is published.
</li>
<li>
<b>IDP Metadata Cache Lifetime</b> - Cache Lifetime in minutes before fresh metadata are fetched from the IDP Metadata URL
</li>
<li>
<b>IDP SSO Binding</b> - SAML SSO binding format required for the IDP to use<br>
</li>
<li>
<b>IDP SLO Binding</b> - SAML SLO binding format required for the IDP to use<br>
</li>
<b>NOTE:</b>:The Binding settings are currently disabled, as the underlying saml library currently supports only the Redirect binding for IDP endpoints.<br>
</ul>
</dd>
<dt>SP</dt>
<dd> <br>
<ul>
<li>
<b>SP Entity ID</b> - Specify the EntityID of your Service Provider (SP).
</li>
<li>
<b>SP NameID Format</b> - NameID format to request. This specifies the content of the NameID and any associated processing rules.
</li>
<li>
<b>SP Metadata Cache Duration</b> - Set the cache duration of generated metadata.<br>
Use PT5M to set cache duration to 5 minutes.
</li>
<li>
<b>SP Metadata Valid Until</b> - Set the expiration date, in XML DateTime String format, for generated metadata.<br>
XML DateTime String Format: "YYYY-MM-DDThh:mm:ssZ", Z can be Z for timezone 0 or "+-hh:mm" for other timezones.
</li>
<li>
<b>Sign SP Metadata</b> - Choose whether metadata produced is signed.
</li>
<li>
<b>SP ACS Binding</b> - SAML Assertion Consumer Service Binding Format for the SP to use on login.
</li>
<li>
<b>SP SLS Binding</b> - SAML Single Logout Service Binding Format for the SP to use on logout.
</li>
<b>NOTE:</b>:The Binding settings are currently disabled, as in the underlying saml library, the ACS endpoint currently supports
only the HTTP-POST binding, while the SLS endpoint supports only HTTP-Redirect.
</ul>
</dd>
<dt>SP ATTRIBUTES</dt>
<dd> <br>
<ul>
<li>
<b>Requested Attributes</b> - The following parameter defines RequestedAttributes section in SAML metadata
since certain IDPs require explicitly requesting attributes.<br>
If not provided, the Attribute Consuming Service Section will not be available in metadata.
<br>
<u>Possible attributes</u>:<br>
<i>name (mandatory), nameFormat, isRequired, friendlyName</i>
<br>
<b>NOTE:</b> This parameter requires to be entered in valid JSON format as displayed below
and multiple attributes can be given.
<br>
Following <i>example</i>:
<br>
<code>SAML_SP_REQUESTED_ATTRIBUTES = '[ <br>
{"name": "urn:oid:0.9.2342.19200300.100.1.3", "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "isRequired": true, "friendlyName": "email"}, <br>
{"name": "mail", "isRequired": false, "friendlyName": "test-field"} <br>
]'</code>
<br>
produces following <i>metadata section</i>:
<br><code>
&ltmd:AttributeConsumingService index="1"&gt<br>
&ltmd:RequestedAttribute Name="urn:oid:0.9.2342.19200300.100.1.3" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="email" isRequired="true"/&gt<br>
&ltmd:RequestedAttribute Name="mail" FriendlyName="test-field"/&gt<br>
&lt/md:AttributeConsumingService&gt</code>
</li>
</ul>
The following attribute values must be <i>derived</i> from <b>Requested Attributes</b>, and must be in the form of a <b>valid URN</b> (e.g. <i>urn:oid:2.5.4.4</i>):
<ul>
<li>
<b>Email</b> - Attribute to use for Email address.
</li>
<li>
<b>Given Name</b> - Attribute to use for Given name.
</li>
<li>
<b>Surname</b> - Attribute to use for Surname.
</li>
<li>
<b>Username</b> - Attribute to use for username.
</li>
</ul>
These may be generic strings containing your information:
<ul>
<li>
<b>SP Entity Name</b> - Contact information about your SP, to be included in the generated metadata.
</li>
<li>
<b>SP Entity Mail</b> - Contact information about your SP, to be included in the generated metadata.
</li>
</ul>
</dd>
<dt>ENCRYPTION</dt>
<dd> <br>
<ul>
<li>
The <b>Cert File</b> - <b>Cert Key</b> pair configures the path
to certificate file and it's respective private key file.<br>
It is used for signing metadata, encrypting tokens and all other
signing/encryption tasks during communication between IDP and SP.<br>
<b>NOTE</b>: If these two parameters aren't explicitly provided,
a self-signed certificate-key pair will be generated.<br>
</li>
<li>
<b>Sign Authentication Request</b> - Configures if the SP should sign <b>outgoing</b> authentication requests.
</li>
<li>
<b>Sign Logout Request & Response</b> - Configures if the SP should sign <b>outgoing</b> Logout requests & Logout responses.
</li>
<li>
<b>Want Assertions Encrypted</b> - Choose whether the SP expects <b>incoming</b> assertions received from the IDP to be encrypted.
</li>
<li>
<b>Want Assertions Signed</b> - Choose whether the SP expects <b>incoming</b> assertions to be signed.
</li>
<li>
<b>NameID Encrypted</b> - Indicates that the <b>outgoing</b> nameID of the logoutRequest sent by this SP will be encrypted.
</li>
<li>
<b>Want NameID Encrypted</b> - Indicates a requirement for the <b>incoming</b> NameID received by this SP to be encrypted.
</li>
<li>
<b>Want Message Signed</b> - Choose whether the SP expects <b>incoming</b> messages to be signed.
</li>
<li>
<b>Digest Algorithm</b> - Encryption algorithm for the DigestValue, which is part of the validation process to ensure the integrity of the XML message.
</li>
<li>
<b>Signature Algorithm</b> - Encryption algorithm for the message Signature.
</li>
</ul>
</dd>
<dt>LOGOUT</dt>
<dd> <br>
<ul>
<li>
<b>SAML Logout</b> - Choose whether user is logged out of the SAML session using SLO.
<ul>
<li>
If enabled, use SAML standard logout mechanism retreived from IDP metadata.
</li>
<li>
If disabled, don't care about SAML session on logout.<br>
Logout from PowerDNS-Admin only and keep SAML session authenticated.
</li>
</ul>
</li>
<li>
<b>Logout URL</b> - Configure to redirect to a url different than PowerDNS-Admin login after a successful SAML logout.
</li>
</ul>
</dd>
<dt>AUTOPROVISION</dt>
<dd> <br>
Assert user Admin status and associated Accounts with SAML Attributes.
<ul>
<li>
<b>Admin</b> - Attribute to get admin status from.<br>
If set, look for the value 'true' to set a user as an administrator.<br>
If not included in assertion, or set to something other than 'true',
the user is set as a non-administrator user.
</li>
<li>
<b>Account</b> - Attribute to get account names from.<br>
If set, the user will be added and removed from accounts to match
what's in the login assertion.<br>Accounts that don't exist will
be created and the user added to them.
</li>
</ul>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</div>
@ -691,6 +1124,13 @@
{%- endassets %}
<script>
$(document).ready(function() {
// Focus on text attribute of modal
$("#modal_requested_attributes").on('shown.bs.modal', function() {
$("#txt_req_attr").focus();
});
});
$(function() {
$('#tabs').tabs({
@ -1050,6 +1490,129 @@
{% endif %}
//END: OIDC Tab JS
// START: SAML tab js
$('#saml_enabled').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
}).on('ifChanged', function(e) {
var is_enabled = e.currentTarget.checked;
if (is_enabled){
$('#saml_metadata_url').prop('required', true);
$('#saml_idp_sso_binding').prop('required', true);
$('#saml_idp_slo_binding').prop('required', true);
$('#saml_sp_entity_id').prop('required', true);
$('#saml_nameid_format').prop('required', true);
$('#saml_sp_acs_binding').prop('required', true);
$('#saml_sp_sls_binding').prop('required', true);
$('#saml_sp_requested_attributes').prop('required', true);
$('#saml_attribute_username').prop('required', true);
$('#saml_cert_file').prop('required', true);
$('#saml_cert_key').prop('required', true);
$('#saml_digest_algorithm').prop('required', true);
$('#saml_signature_algorithm').prop('required', true);
if ($('#saml_logout').is(":checked")) {
$('#saml_logout_url').prop('required', true);
}
} else {
$('#saml_metadata_url').prop('required', false);
$('#saml_idp_sso_binding').prop('required', false);
$('#saml_idp_slo_binding').prop('required', false);
$('#saml_sp_entity_id').prop('required', false);
$('#saml_nameid_format').prop('required', false);
$('#saml_sp_acs_binding').prop('required', false);
$('#saml_sp_sls_binding').prop('required', false);
$('#saml_sp_requested_attributes').prop('required', false);
$('#saml_attribute_username').prop('required', false);
$('#saml_cert_file').prop('required', false);
$('#saml_cert_key').prop('required', false);
$('#saml_digest_algorithm').prop('required', false);
$('#saml_signature_algorithm').prop('required', false);
$('#saml_logout_url').prop('required', false);
}
});
// init validation requirement at first time page load
{% if SETTING.get('saml_enabled') %}
$('#saml_metadata_url').prop('required', true);
$('#saml_idp_sso_binding').prop('required', true);
$('#saml_idp_slo_binding').prop('required', true);
$('#saml_sp_entity_id').prop('required', true);
$('#saml_nameid_format').prop('required', true);
$('#saml_sp_acs_binding').prop('required', true);
$('#saml_sp_sls_binding').prop('required', true);
$('#saml_sp_requested_attributes').prop('required', true);
$('#saml_attribute_username').prop('required', true);
$('#saml_cert_file').prop('required', true);
$('#saml_cert_key').prop('required', true);
$('#saml_digest_algorithm').prop('required', true);
$('#saml_signature_algorithm').prop('required', true);
if ($('#saml_logout').is(":checked")) {
$('#saml_logout_url').prop('required', true);
}
{% endif %}
$(document.body).on("focus", "#saml_sp_requested_attributes", function (e) {
var req_attr = $(this);
req_attr.blur();
var txt_data = req_attr.val();
var modal = $("#modal_requested_attributes");
var form = "<textarea spellcheck=\"false\" style=\"min-width: 100%;color: #333;\" placeholder=\"SP Requested Attributes in JSON format\" \
rows=\"10\" id=\"txt_req_attr\" name=\"txt_req_attr\">" + txt_data + "</textarea>";
modal.find('.modal-body p').html(form);
modal.find('#button_save').click(function() {
data = modal.find('#txt_req_attr').val();
req_attr.val(data);
modal.modal('hide');
});
modal.modal('show');
});
$('#saml_logout').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
}).on('ifChanged', function(e) {
var is_enabled = e.currentTarget.checked;
if (is_enabled){
$('#saml_logout_url').prop('required', true);
}
else{
$('#saml_logout_url').prop('required', false);
}
});
$('#saml_sign_authn_request').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
})
$('#saml_sign_logout_request_response').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
})
$('#saml_nameid_encrypted').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
})
$('#saml_want_nameid_encrypted').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
})
$('#saml_want_assertions_encrypted').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
})
$('#saml_want_assertions_signed').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
})
$('#saml_want_message_signed').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
})
$('#saml_sign_metadata').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
})
// END: SAML Tab js
</script>
{% endblock %}
@ -1098,5 +1661,26 @@
<!-- /.modal-dialog -->
</div>
<div class="modal fade modal-primary" id="modal_requested_attributes">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">SP Requested Attributes</h4>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-flat btn-primary" id="button_save">Save</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
{% endblock %}

View file

@ -26,14 +26,15 @@
<i class="fa fa-warning text-yellow"></i> Oops! Something went wrong
</h3><br>
<p>
Login failed.<br>
Error(s) when processing SAML Response:<br>
Login failed.
{% if error %}
<br>Error(s) when processing SAML Response:<br>
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
You may <a href="{{ url_for('dashboard.dashboard') }}">return to the dashboard</a>.
</p>
</div>