diff --git a/examples/idp.py b/examples/idp.py index f6d029b..d1d2556 100755 --- a/examples/idp.py +++ b/examples/idp.py @@ -1,14 +1,17 @@ #!/usr/bin/env python3 import logging +import pathlib from flask import Flask, abort, redirect, request, session, url_for from flask.views import MethodView from flask_saml2.idp import IdentityProvider +from flask_saml2.utils import certificate_from_file from tests.idp.base import CERTIFICATE, PRIVATE_KEY, User from tests.sp.base import CERTIFICATE as SP_CERTIFICATE logger = logging.getLogger(__name__) +keys = pathlib.Path(__file__).parent / 'keys' class ExampleIdentityProvider(IdentityProvider): @@ -27,6 +30,9 @@ def logout(self): def get_current_user(self): return users[session['user']] + def get_slo_url(self): + return None + users = {user.username: user for user in [ User('alex', 'alex@example.com'), @@ -82,7 +88,16 @@ def post(self): 'acs_url': 'http://localhost:9000/saml/acs/', 'certificate': SP_CERTIFICATE, }, - } + }, + { + 'CLASS': 'flask_saml2.idp.SPHandler', + 'OPTIONS': { + 'display_name': 'samltest.id', + 'entity_id': 'https://samltest.id/saml/sp', + 'acs_url': 'https://samltest.id/Shibboleth.sso/SAML2/POST', + 'certificate': certificate_from_file(keys / 'samltest-sp.key'), + } + }, ] app.add_url_rule('/login/', view_func=Login.as_view('login')) diff --git a/examples/keys/samltest-sp.key b/examples/keys/samltest-sp.key new file mode 100644 index 0000000..32808b1 --- /dev/null +++ b/examples/keys/samltest-sp.key @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIERTCCAq2gAwIBAgIJAKmtzjCD1+tqMA0GCSqGSIb3DQEBCwUAMDUxMzAxBgNV +BAMTKmlwLTE3Mi0zMS0yOC02NC51cy13ZXN0LTIuY29tcHV0ZS5pbnRlcm5hbDAe +Fw0xODA4MTgyMzI0MjNaFw0yODA4MTUyMzI0MjNaMDUxMzAxBgNVBAMTKmlwLTE3 +Mi0zMS0yOC02NC51cy13ZXN0LTIuY29tcHV0ZS5pbnRlcm5hbDCCAaIwDQYJKoZI +hvcNAQEBBQADggGPADCCAYoCggGBALhUlY3SkIOze+l8y6dBzM6p7B8OykJWlwiz +szU16Lih8D7KLhNJfahoVxbPxB3YFM/81PJLOeK2krvJ5zY6CJyQY3sPQAkZKI7I +8qq9lmZ2g4QPqybNstXS6YUXJNUt/ixbbK/N97+LKTiSutbD1J7AoFnouMuLjlhN +5VRZ43jez4xLSHVZaYuUFKn01Y9oLKbj46LQnZnJCAGpTgPqEQJr6GpVGw43bKyU +pGoaPrdDRgRgtPMUWgFDkgcI3QiV1lsKfBs1t1E2UA7ACFnlJZpEuBtwgivzo3Ve +itiSaF3Jxh25EY5/vABpcgQQRz3RH2l8MMKdRsxb8VT3yh2S+CX55s+cN67LiCPr +6f2u+KS1iKfB9mWN6o2S4lcmo82HIBbsuXJV0oA1HrGMyyc4Y9nng/I8iuAp8or1 +JrWRHQ+8NzO85DWK0rtvtLPxkvw0HK32glyuOP/9F05Z7+tiVIgn67buC0EdoUm1 +RSpibqmB1ST2PikslOlVbJuy4Ah93wIDAQABo1gwVjA1BgNVHREELjAsgippcC0x +NzItMzEtMjgtNjQudXMtd2VzdC0yLmNvbXB1dGUuaW50ZXJuYWwwHQYDVR0OBBYE +FAdsTxYfulJ5yunYtgYJHC9IcevzMA0GCSqGSIb3DQEBCwUAA4IBgQB3J6i7Krei +HL8NPMglfWLHk1PZOgvIEEpKL+GRebvcbyqgcuc3VVPylq70VvGqhJxp1q/mzLfr +aUiypzfWFGm9zfwIg0H5TqRZYEPTvgIhIICjaDWRwZBDJG8D5G/KoV60DlUG0crP +BlIuCCr/SRa5ZoDQqvucTfr3Rx4Ha6koXFSjoSXllR+jn4GnInhm/WH137a+v35P +UcffNxfuehoGn6i4YeXF3cwJK4e35cOFW+dLbnaLk+Ty7HOGvpw86h979C6mJ9qE +HYgq9rQyzlSPbLZGZSgVcIezunOaOsWm81BsXRNNJjzHGCqKf8RMhd8oZP55+2/S +VRBwnkGyUNCuDPrJcymC95ZT2NW/KeWkz28HF2i31xQmecT2r3lQRSM8acvOXQsN +EDCDvJvCzJT9c2AnsnO24r6arPXs/UWAxOI+MjclXPLkLD6uTHV+Oo8XZ7bOjegD +5hL6/bKUWnNMurQNGrmi/jvqsCFLDKftl7ajuxKjtodnSuwhoY7NQy8= +-----END CERTIFICATE----- diff --git a/flask_saml2/constants.py b/flask_saml2/constants.py new file mode 100644 index 0000000..20e204c --- /dev/null +++ b/flask_saml2/constants.py @@ -0,0 +1,5 @@ +NAMEID_SAML1_1_EMAIL = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' +NAMEID_SAML2_0_EMAIL = 'urn:oasis:names:tc:SAML:2.0:attrname-­format:basic' +NAMEID_EMAIL = {NAMEID_SAML1_1_EMAIL, NAMEID_SAML2_0_EMAIL} + +ATTR_SAML2_0_BASIC = 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic' diff --git a/flask_saml2/idp/idp.py b/flask_saml2/idp/idp.py index bfe0fac..236ec30 100644 --- a/flask_saml2/idp/idp.py +++ b/flask_saml2/idp/idp.py @@ -7,6 +7,7 @@ from flask_saml2.types import X509, PKey from flask_saml2.utils import certificate_to_string, import_string +from ..constants import NAMEID_EMAIL, NAMEID_SAML1_1_EMAIL from .sphandler import SPHandler from .views import ( CannotHandleAssertionView, LoginBegin, LoginProcess, Logout, Metadata, @@ -29,6 +30,7 @@ class IdentityProvider(Generic[U]): blueprint_name = 'flask_saml2_idp' + nameid_format = NAMEID_SAML1_1_EMAIL #: The specific :class:`digest <~flask_saml2.signing.Digester>` method to #: use in this IdP when creating responses. #: @@ -110,6 +112,9 @@ def get_idp_digester(self) -> Digester: """Get the method used to compute digests for the IdP.""" return self.idp_digester_class() + def get_idp_nameid_format(self) -> str: + return self.nameid_format + def get_service_providers(self) -> Iterable[Tuple[str, dict]]: """ Get an iterable of service provider ``config`` dicts. ``config`` should @@ -177,7 +182,7 @@ def get_user_nameid(self, user: U, attribute: str): Subclasses can override this to allow more attributes to be extracted. By default, only email addresses are extracted using :meth:`get_user_email`. """ - if attribute == 'urn:oasis:names:tc:SAML:2.0:nameid-format:email': + if attribute in NAMEID_EMAIL: return self.get_user_email(user) raise NotImplementedError("Can't fetch attribute {} from user".format(attribute)) @@ -210,10 +215,12 @@ def get_metadata_context(self) -> dict: Suggested extra context variables include 'org' and 'contacts'. """ return { + 'idp': self, + 'nameid_format': self.get_idp_nameid_format(), 'entity_id': self.get_idp_entity_id(), 'certificate': certificate_to_string(self.get_idp_certificate()), - 'slo_url': self.get_slo_url(), 'sso_url': self.get_sso_url(), + 'slo_url': self.get_slo_url(), 'org': None, 'contacts': [], } diff --git a/flask_saml2/idp/sphandler.py b/flask_saml2/idp/sphandler.py index fee4300..98a2a6d 100644 --- a/flask_saml2/idp/sphandler.py +++ b/flask_saml2/idp/sphandler.py @@ -11,6 +11,7 @@ from flask_saml2.utils import get_random_id, utcnow from flask_saml2.xml_templates import XmlTemplate +from ..constants import NAMEID_SAML1_1_EMAIL from .parser import AuthnRequestParser, LogoutRequestParser from .xml_templates import AssertionTemplate, ResponseTemplate @@ -27,7 +28,7 @@ class SPHandler(object): certificate: Optional[X509] = None display_name: str = None - subject_format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:email' + subject_format = NAMEID_SAML1_1_EMAIL assertion_template = AssertionTemplate response_template = ResponseTemplate diff --git a/flask_saml2/idp/templates/flask_saml2_idp/metadata.xml b/flask_saml2/idp/templates/flask_saml2_idp/metadata.xml index 89cbbf3..76ac271 100644 --- a/flask_saml2/idp/templates/flask_saml2_idp/metadata.xml +++ b/flask_saml2/idp/templates/flask_saml2_idp/metadata.xml @@ -15,9 +15,11 @@ - urn:oasis:names:tc:SAML:2.0:nameid-format:email + {{ nameid_format }} + {% if slo_url %} + {% endif %} {% if org %} diff --git a/flask_saml2/idp/xml_templates.py b/flask_saml2/idp/xml_templates.py index fa41f8f..6baf18c 100644 --- a/flask_saml2/idp/xml_templates.py +++ b/flask_saml2/idp/xml_templates.py @@ -5,6 +5,8 @@ from flask_saml2.types import XmlNode from flask_saml2.xml_templates import NameIDTemplate, XmlTemplate +from ..constants import ATTR_SAML2_0_BASIC + class AttributeTemplate(XmlTemplate): """ @@ -17,11 +19,12 @@ class AttributeTemplate(XmlTemplate): """ namespace = 'saml' + default_attribute_format = ATTR_SAML2_0_BASIC def generate_xml(self): return self.element('Attribute', attrs={ 'Name': self.params['ATTRIBUTE_NAME'], - 'NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', + 'NameFormat': self.params.get('ATTRIBUTE_FORMAT', self.default_attribute_format), }, children=[ self.element('AttributeValue', text=self.params['ATTRIBUTE_VALUE']), ]) diff --git a/setup.cfg b/setup.cfg index c45a58a..1ae8072 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,10 +2,10 @@ universal = 1 [metadata] -description-file = README.rst +description_file = README.rst [flake8] -max-line-length = 100 +max_line_length = 100 ignore = E501, E731 [isort]