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]