summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Kanakarakis <ivan.kanak@gmail.com>2019-12-26 20:52:23 +0200
committerIvan Kanakarakis <ivan.kanak@gmail.com>2019-12-26 20:53:35 +0200
commit324656e6321fdf5a80184951d2ecaea644a737d8 (patch)
tree0f4bba69c9f77f4987ca5f6a0f20154e43d2eb1e
parentb3635ec9792e0d2bc9682a14987248f37242a659 (diff)
parent9030d036e4b0473ff57763e638e4afcbf7b3f481 (diff)
downloadpysaml2-324656e6321fdf5a80184951d2ecaea644a737d8.tar.gz
Merge branch 'feature-add-metadata-freshness'
Define a period for which the metadata fetched from an MDQ are considered valid. Signed-off-by: Ivan Kanakarakis <ivan.kanak@gmail.com>
-rw-r--r--docs/howto/config.rst230
-rw-r--r--src/saml2/mdstore.py109
-rw-r--r--src/saml2/time_util.py15
-rw-r--r--tests/test_10_time_util.py6
-rw-r--r--tests/test_30_mdstore.py59
5 files changed, 282 insertions, 137 deletions
diff --git a/docs/howto/config.rst b/docs/howto/config.rst
index 0e3be8a8..4ce09873 100644
--- a/docs/howto/config.rst
+++ b/docs/howto/config.rst
@@ -16,29 +16,37 @@ The basic structure of the configuration file is therefore like this::
from saml2 import BINDING_HTTP_REDIRECT
CONFIG = {
- "entityid" : "http://saml.example.com:saml/idp.xml",
- "name" : "Rolands IdP",
+ "entityid": "http://saml.example.com:saml/idp.xml",
+ "name": "Rolands IdP",
"service": {
"idp": {
- "endpoints" : {
- "single_sign_on_service" : [
- ("http://saml.example.com:saml:8088/sso",
- BINDING_HTTP_REDIRECT)],
+ "endpoints": {
+ "single_sign_on_service": [
+ (
+ "http://saml.example.com:saml:8088/sso",
+ BINDING_HTTP_REDIRECT,
+ ),
+ ],
"single_logout_service": [
- ("http://saml.example.com:saml:8088/slo",
- BINDING_HTTP_REDIRECT)]
+ (
+ "http://saml.example.com:saml:8088/slo",
+ BINDING_HTTP_REDIRECT,
+ ),
+ ],
},
...
}
},
- "key_file" : "my.key",
- "cert_file" : "ca.pem",
- "xmlsec_binary" : "/usr/local/bin/xmlsec1",
+ "key_file": "my.key",
+ "cert_file": "ca.pem",
+ "xmlsec_binary": "/usr/local/bin/xmlsec1",
"delete_tmpfiles": True,
"metadata": {
- "local": ["edugain.xml"],
+ "local": [
+ "edugain.xml",
+ ],
},
- "attribute_map_dir" : "attributemaps",
+ "attribute_map_dir": "attributemaps",
...
}
@@ -93,7 +101,7 @@ A typical map file will look like this::
'urn:mace:dir:attribute-def:associatedDomain': 'associatedDomain',
'urn:mace:dir:attribute-def:associatedName': 'associatedName',
...
- },
+ },
"to": {
'aRecord': 'urn:mace:dir:attribute-def:aRecord',
'aliasedEntryName': 'urn:mace:dir:attribute-def:aliasedEntryName',
@@ -135,19 +143,22 @@ about the service or if support is needed. The possible types are according to
the standard **technical**, **support**, **administrative**, **billing**
and **other**.::
- contact_person: [{
- "givenname": "Derek",
- "surname": "Jeter",
- "company": "Example Co.",
- "mail": ["jeter@example.com"],
- "type": "technical",
- },{
- "givenname": "Joe",
- "surname": "Girardi",
- "company": "Example Co.",
- "mail": "girardi@example.com",
- "type": "administrative",
- }]
+ contact_person: [
+ {
+ "givenname": "Derek",
+ "surname": "Jeter",
+ "company": "Example Co.",
+ "mail": ["jeter@example.com"],
+ "type": "technical",
+ },
+ {
+ "givenname": "Joe",
+ "surname": "Girardi",
+ "company": "Example Co.",
+ "mail": "girardi@example.com",
+ "type": "administrative",
+ },
+ ]
debug
^^^^^
@@ -193,7 +204,7 @@ Contains a list of places where metadata can be found. This can be
For example::
- "metadata" : {
+ "metadata": {
"local": [
"/opt/metadata"
"metadata.xml",
@@ -209,6 +220,7 @@ For example::
{
"url": "http://mdq.ukfederation.org.uk/",
"cert": "ukfederation-mdq.pem",
+ "freshness_period": "P0Y0M0DT2H0M0S",
},
],
},
@@ -221,6 +233,17 @@ metadata signing certificates should be used. These public keys must be
acquired by some secure out-of-band method before being placed on the local
file system.
+When using MDQ, the `freshness_period` option can be set to define a period for
+which the metadata fetched from the the MDQ server are considered fresh. After
+that period has passed the metadata are not valid anymore and must be fetched
+again. The period must be in the format defined in
+`ISO 8601 <https://www.iso.org/iso-8601-date-and-time-format.html>`_
+or `RFC3999 <https://tools.ietf.org/html/rfc3339#appendix-A>`_.
+
+By default, if `freshness_period` is not defined, the metadata are refreshed
+every 12 hours (`P0Y0M0DT12H0M0S`).
+
+
organization
^^^^^^^^^^^^
@@ -228,9 +251,15 @@ Only used by *make_metadata.py*.
Where you describe the organization responsible for the service.::
"organization": {
- "name": [("Example Company","en"), ("Exempel AB","se")],
+ "name": [
+ ("Example Company", "en"),
+ ("Exempel AB", "se")
+ ],
"display_name": ["Exempel AB"],
- "url": [("http://example.com","en"),("http://exempel.se","se")],
+ "url": [
+ ("http://example.com", "en"),
+ ("http://exempel.se", "se"),
+ ],
}
.. note:: You can specify the language of the name, or the language used on
@@ -280,14 +309,22 @@ So if a server is a Service Provider (SP) then the configuration
could look something like this::
"service": {
- "sp":{
- "name" : "Rolands SP",
- "endpoints":{
+ "sp": {
+ "name": "Rolands SP",
+ "endpoints": {
"assertion_consumer_service": ["http://localhost:8087/"],
- "single_logout_service" : [("http://localhost:8087/slo",
- 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect')],
+ "single_logout_service": [
+ (
+ "http://localhost:8087/slo",
+ 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
+ ),
+ ],
},
- "required_attributes": ["surname", "givenname", "edupersonaffiliation"],
+ "required_attributes": [
+ "surname",
+ "givenname",
+ "edupersonaffiliation",
+ ],
"optional_attributes": ["title"],
"idp": {
"urn:mace:umu.se:saml:roland:idp": None,
@@ -384,7 +421,7 @@ An example might be::
},
"urn:mace:example.com:saml:roland:sp": {
"lifetime": {"minutes": 5},
- "attribute_restrictions":{
+ "attribute_restrictions": {
"givenName": None,
"surName": None,
}
@@ -420,7 +457,7 @@ regular expressions.::
"policy": {
"urn:mace:umu.se:saml:roland:sp": {
"lifetime": {"minutes": 5},
- "attribute_restrictions":{
+ "attribute_restrictions": {
"mail": [".*\.umu\.se$"],
}
}
@@ -661,7 +698,11 @@ Example::
"service": {
"sp": {
- "required_attributes": ["surname", "givenName", "mail"],
+ "required_attributes": [
+ "surname",
+ "givenName",
+ "mail",
+ ],
}
}
@@ -708,7 +749,7 @@ Example::
"sp": {
"want_response_signed": False,
"want_assertions_signed": False,
- "want_assertions_or_response_signed": True
+ "want_assertions_or_response_signed": True,
}
}
@@ -757,11 +798,13 @@ Example::
"service":
"idp": {
- "endpoints" : {
- "single_sign_on_service" : [
- ("http://localhost:8088/sso", BINDING_HTTP_REDIRECT)],
+ "endpoints": {
+ "single_sign_on_service": [
+ ("http://localhost:8088/sso", BINDING_HTTP_REDIRECT),
+ ],
"single_logout_service": [
- ("http://localhost:8088/slo", BINDING_HTTP_REDIRECT)]
+ ("http://localhost:8088/slo", BINDING_HTTP_REDIRECT),
+ ],
},
},
},
@@ -810,9 +853,9 @@ virtual_organization
Gives information about common identifiers for virtual_organizations::
- "virtual_organization" : {
- "urn:mace:example.com:it:tek":{
- "nameid_format" : "urn:oid:1.3.6.1.4.1.1466.115.121.1.15-NameID",
+ "virtual_organization": {
+ "urn:mace:example.com:it:tek": {
+ "nameid_format": "urn:oid:1.3.6.1.4.1.1466.115.121.1.15-NameID",
"common_identifier": "umuselin",
}
},
@@ -830,35 +873,38 @@ We start with a simple but fairly complete Service provider configuration::
from saml2 import BINDING_HTTP_REDIRECT
CONFIG = {
- "entityid" : "http://example.com/sp/metadata.xml",
+ "entityid": "http://example.com/sp/metadata.xml",
"service": {
- "sp":{
- "name" : "Example SP",
- "endpoints":{
+ "sp": {
+ "name": "Example SP",
+ "endpoints": {
"assertion_consumer_service": ["http://example.com/sp"],
- "single_logout_service" : [("http://example.com/sp/slo",
- BINDING_HTTP_REDIRECT)],
+ "single_logout_service": [
+ ("http://example.com/sp/slo", BINDING_HTTP_REDIRECT),
+ ],
},
}
},
- "key_file" : "./mykey.pem",
- "cert_file" : "./mycert.pem",
- "xmlsec_binary" : "/usr/local/bin/xmlsec1",
+ "key_file": "./mykey.pem",
+ "cert_file": "./mycert.pem",
+ "xmlsec_binary": "/usr/local/bin/xmlsec1",
"delete_tmpfiles": True,
"attribute_map_dir": "./attributemaps",
"metadata": {
"local": ["idp.xml"]
}
"organization": {
- "display_name":["Example identities"]
+ "display_name": ["Example identities"]
}
- "contact_person": [{
- "givenname": "Roland",
- "surname": "Hedberg",
- "phone": "+46 90510",
- "mail": "roland@example.com",
- "type": "technical",
- }]
+ "contact_person": [
+ {
+ "givenname": "Roland",
+ "surname": "Hedberg",
+ "phone": "+46 90510",
+ "mail": "roland@example.com",
+ "type": "technical",
+ },
+ ]
}
This is the typical setup for an SP.
@@ -872,45 +918,51 @@ A slightly more complex configuration::
from saml2 import BINDING_HTTP_REDIRECT
CONFIG = {
- "entityid" : "http://sp.example.com/metadata.xml",
+ "entityid": "http://sp.example.com/metadata.xml",
"service": {
- "sp":{
- "name" : "Example SP",
- "endpoints":{
+ "sp": {
+ "name": "Example SP",
+ "endpoints": {
"assertion_consumer_service": ["http://sp.example.com/"],
- "single_logout_service" : [("http://sp.example.com/slo",
- BINDING_HTTP_REDIRECT)],
+ "single_logout_service": [
+ ("http://sp.example.com/slo", BINDING_HTTP_REDIRECT),
+ ],
},
"subject_data": ("memcached", "localhost:12121"),
- "virtual_organization" : {
- "urn:mace:example.com:it:tek":{
- "nameid_format" : "urn:oid:1.3.6.1.4.1.1466.115.121.1.15-NameID",
+ "virtual_organization": {
+ "urn:mace:example.com:it:tek": {
+ "nameid_format": "urn:oid:1.3.6.1.4.1.1466.115.121.1.15-NameID",
"common_identifier": "eduPersonPrincipalName",
}
},
}
},
- "key_file" : "./mykey.pem",
- "cert_file" : "./mycert.pem",
- "xmlsec_binary" : "/usr/local/bin/xmlsec1",
+ "key_file": "./mykey.pem",
+ "cert_file": "./mycert.pem",
+ "xmlsec_binary": "/usr/local/bin/xmlsec1",
"delete_tmpfiles": True,
- "metadata" : {
+ "metadata": {
"local": ["example.xml"],
- "remote": [{
- "url":"https://kalmar2.org/simplesaml/module.php/aggregator/?id=kalmarcentral2&set=saml2",
- "cert":"kalmar2.pem"}]
+ "remote": [
+ {
+ "url":"https://kalmar2.org/simplesaml/module.php/aggregator/?id=kalmarcentral2&set=saml2",
+ "cert":"kalmar2.pem",
+ }
+ ]
},
- "attribute_maps" : "attributemaps",
+ "attribute_maps": "attributemaps",
"organization": {
- "display_name":["Example identities"]
+ "display_name": ["Example identities"]
}
- "contact_person": [{
- "givenname": "Roland",
- "surname": "Hedberg",
- "phone": "+46 90510",
- "mail": "roland@example.com",
- "type": "technical",
- }]
+ "contact_person": [
+ {
+ "givenname": "Roland",
+ "surname": "Hedberg",
+ "phone": "+46 90510",
+ "mail": "roland@example.com",
+ "type": "technical",
+ },
+ ]
}
Uses metadata files, both local and remote, and will talk to whatever
diff --git a/src/saml2/mdstore.py b/src/saml2/mdstore.py
index 36891abb..32778af9 100644
--- a/src/saml2/mdstore.py
+++ b/src/saml2/mdstore.py
@@ -33,6 +33,10 @@ from saml2.s_utils import UnknownSystemEntity
from saml2.sigver import split_len
from saml2.validate import valid_instance
from saml2.time_util import valid
+from saml2.time_util import instant
+from saml2.time_util import add_duration
+from saml2.time_util import before
+from saml2.time_util import str_to_time
from saml2.validate import NotValid
from saml2.sigver import security_context
from saml2.extension.mdattr import NAMESPACE as NS_MDATTR
@@ -72,6 +76,11 @@ ENTITY_CATEGORY = "http://macedir.org/entity-category"
ENTITY_CATEGORY_SUPPORT = "http://macedir.org/entity-category-support"
ASSURANCE_CERTIFICATION = "urn:oasis:names:tc:SAML:attribute:assurance-certification"
+SAML_METADATA_CONTENT_TYPE = "application/samlmetadata+xml"
+DEFAULT_FRESHNESS_PERIOD = "P0Y0M0DT12H0M0S"
+
+
+
REQ2SRV = {
# IDP
"authn_request": "single_sign_on_service",
@@ -664,22 +673,21 @@ class InMemoryMetaData(MetaData):
def parse_and_check_signature(self, txt):
self.parse(txt)
- if self.cert:
- if not self.signed():
- return True
-
- node_name = self.node_name \
- or "%s:%s" % (md.EntitiesDescriptor.c_namespace,
- md.EntitiesDescriptor.c_tag)
+ if not self.cert:
+ return True
- if self.security.verify_signature(
- txt, node_name=node_name, cert_file=self.cert):
- return True
- else:
- return False
- else:
+ if not self.signed():
return True
+ fallback_name = "{ns}:{tag}".format(
+ ns=md.EntitiesDescriptor.c_namespace, tag=md.EntitiesDescriptor.c_tag
+ )
+ node_name = self.node_name or fallback_name
+
+ return self.security.verify_signature(
+ txt, node_name=node_name, cert_file=self.cert
+ )
+
class MetaDataFile(InMemoryMetaData):
"""
@@ -805,9 +813,6 @@ class MetaDataMD(InMemoryMetaData):
self.entity[key] = item
-SAML_METADATA_CONTENT_TYPE = 'application/samlmetadata+xml'
-
-
class MetaDataMDX(InMemoryMetaData):
"""
Uses the MDQ protocol to fetch entity information.
@@ -817,11 +822,12 @@ class MetaDataMDX(InMemoryMetaData):
@staticmethod
def sha1_entity_transform(entity_id):
- return "{{sha1}}{}".format(
- hashlib.sha1(entity_id.encode("utf-8")).hexdigest())
+ entity_id_sha1 = hashlib.sha1(entity_id.encode("utf-8")).hexdigest()
+ transform = "{{sha1}}{digest}".format(digest=entity_id_sha1)
+ return transform
def __init__(self, url=None, security=None, cert=None,
- entity_transform=None, **kwargs):
+ entity_transform=None, freshness_period=None, **kwargs):
"""
:params url: mdx service url
:params security: SecurityContext()
@@ -831,6 +837,8 @@ class MetaDataMDX(InMemoryMetaData):
hash) the entity id. It is applied to the entity id before it is
concatenated with the request URL sent to the MDX server. Defaults to
sha1 transformation.
+ :params freshness_period: a duration in the format described at
+ https://www.w3.org/TR/xmlschema-2/#duration
"""
super(MetaDataMDX, self).__init__(None, **kwargs)
if not url:
@@ -845,6 +853,8 @@ class MetaDataMDX(InMemoryMetaData):
self.cert = cert
self.security = security
+ self.freshness_period = freshness_period or DEFAULT_FRESHNESS_PERIOD
+ self.expiration_date = {}
# We assume that the MDQ server will return a single entity
# described by a single <EntityDescriptor> element. The protocol
@@ -852,28 +862,53 @@ class MetaDataMDX(InMemoryMetaData):
# <EntitiesDescriptor> element but we will not currently support
# that use case since it is unlikely to be leveraged for most
# flows.
- self.node_name = "%s:%s" % (md.EntityDescriptor.c_namespace,
- md.EntityDescriptor.c_tag)
+ self.node_name = "{ns}:{tag}".format(
+ ns=md.EntityDescriptor.c_namespace, tag=md.EntityDescriptor.c_tag
+ )
def load(self, *args, **kwargs):
# Do nothing
pass
+ def _fetch_metadata(self, item):
+ mdx_url = "{url}/entities/{id}".format(
+ url=self.url, id=self.entity_transform(item)
+ )
+
+ response = requests.get(mdx_url, headers={"Accept": SAML_METADATA_CONTENT_TYPE})
+ if response.status_code != 200:
+ error_msg = "Fething {item}: Got response status {status}".format(
+ item=item, status=response.status_code
+ )
+ logger.info(error_msg)
+ raise KeyError(error_msg)
+
+ _txt = response.content
+ if not self.parse_and_check_signature(_txt):
+ error_msg = "Fething {item}: invalid signature".format(
+ item=item, status=response.status_code
+ )
+ logger.info(error_msg)
+ raise KeyError(error_msg)
+
+ curr_time = str_to_time(instant())
+ self.expiration_date[item] = add_duration(curr_time, self.freshness_period)
+ return self.entity[item]
+
+ def _is_metadata_fresh(self, item):
+ return before(self.expiration_date[item])
+
def __getitem__(self, item):
- try:
- return self.entity[item]
- except KeyError:
- mdx_url = "%s/entities/%s" % (self.url, self.entity_transform(item))
- response = requests.get(mdx_url, headers={
- 'Accept': SAML_METADATA_CONTENT_TYPE})
- if response.status_code == 200:
- _txt = response.content
-
- if self.parse_and_check_signature(_txt):
- return self.entity[item]
- else:
- logger.info("Response status: %s", response.status_code)
- raise KeyError
+ if item not in self.entity:
+ entity = self._fetch_metadata(item)
+ elif not self._is_metadata_fresh(item):
+ msg = "Metadata for {} have expired; refreshing metadata".format(item)
+ logger.info(msg)
+ old_entity = self.entity.pop(item)
+ entity = self._fetch_metadata(item)
+ else:
+ entity = self.entity[item]
+ return entity
def single_sign_on_service(self, entity_id, binding=None, typ="idpsso"):
if binding is None:
@@ -960,9 +995,11 @@ class MetadataStore(MetaData):
key = kwargs['url']
url = kwargs['url']
cert = kwargs.get('cert')
+ freshness_period = kwargs.get('freshness_period', None)
security = self.security
entity_transform = kwargs.get('entity_transform', None)
- _md = MetaDataMDX(url, security, cert, entity_transform)
+ _md = MetaDataMDX(url, security, cert, entity_transform,
+ freshness_period=freshness_period)
else:
key = args[1]
url = args[1]
diff --git a/src/saml2/time_util.py b/src/saml2/time_util.py
index 62ac3cc8..efe57144 100644
--- a/src/saml2/time_util.py
+++ b/src/saml2/time_util.py
@@ -140,19 +140,20 @@ def add_duration(tid, duration):
carry = f_quotient(temp, 60)
# hours
temp = tid.tm_hour + dur["tm_hour"] + carry
- hour = modulo(temp, 60)
- carry = f_quotient(temp, 60)
+ hour = modulo(temp, 24)
+ carry = f_quotient(temp, 24)
# days
- if dur["tm_mday"] > maximum_day_in_month_for(year, month):
+ if tid.tm_mday > maximum_day_in_month_for(year, month):
temp_days = maximum_day_in_month_for(year, month)
- elif dur["tm_mday"] < 1:
+ elif tid.tm_mday < 1:
temp_days = 1
else:
- temp_days = dur["tm_mday"]
- days = temp_days + tid.tm_mday + carry
+ temp_days = tid.tm_mday
+ days = temp_days + dur["tm_mday"] + carry
while True:
if days < 1:
- pass
+ days = days + maximum_day_in_month_for(year, month - 1)
+ carry = -1
elif days > maximum_day_in_month_for(year, month):
days -= maximum_day_in_month_for(year, month)
carry = 1
diff --git a/tests/test_10_time_util.py b/tests/test_10_time_util.py
index f0608939..1c1f8198 100644
--- a/tests/test_10_time_util.py
+++ b/tests/test_10_time_util.py
@@ -92,7 +92,7 @@ def test_parse_duration_n():
assert d == _val
def test_add_duration_1():
- #2000-01-12T12:13:14Z P1Y3M5DT7H10M3S 2001-04-17T19:23:17Z
+ #2000-01-12T12:13:14Z P1Y3M5DT7H10M3S 2001-04-17T19:23:17Z
t = add_duration(str_to_time("2000-01-12T12:13:14Z"), "P1Y3M5DT7H10M3S")
assert t.tm_year == 2001
assert t.tm_mon == 4
@@ -107,7 +107,7 @@ def test_add_duration_2():
t = add_duration(str_to_time("2000-01-12T00:00:00Z"), "PT33H")
assert t.tm_year == 2000
assert t.tm_mon == 1
- assert t.tm_mday == 14
+ assert t.tm_mday == 13
assert t.tm_hour == 9
assert t.tm_min == 0
assert t.tm_sec == 0
@@ -119,7 +119,7 @@ def test_str_to_time():
#t = time.mktime(str_to_time("2000-01-12T00:00:00Z"))
#assert t == 947631600.0
#TODO: add something to show how this time was arrived at
- # do this as an external method in the
+ # do this as an external method in the
assert t == 947635200
# some IdPs omit the trailing Z, and SAML spec is unclear if it is actually required
t = calendar.timegm(str_to_time("2000-01-12T00:00:00"))
diff --git a/tests/test_30_mdstore.py b/tests/test_30_mdstore.py
index c77293bb..59dc7da6 100644
--- a/tests/test_30_mdstore.py
+++ b/tests/test_30_mdstore.py
@@ -4,8 +4,11 @@ import datetime
import os
import re
from collections import OrderedDict
+from unittest.mock import Mock
from unittest.mock import patch
+import responses
+
from six.moves.urllib import parse
from saml2.config import Config
@@ -26,8 +29,6 @@ from saml2.attribute_converter import d_to_local_name
from saml2.s_utils import UnknownPrincipal
from pathutils import full_path
-import responses
-
TESTS_DIR = os.path.dirname(__file__)
@@ -334,6 +335,60 @@ def test_mdx_single_sign_on_service():
assert sso_loc[0]["location"] == "http://xenosmilus.umdc.umu.se/simplesaml/saml2/idp/metadata.php"
+@responses.activate
+def test_mdx_metadata_freshness_period_not_expired():
+ """Ensure that metadata is not refreshed if not expired."""
+
+ entity_id = "http://xenosmilus.umdc.umu.se/simplesaml/saml2/idp/metadata.php"
+ url = "http://mdx.example.com/entities/{}".format(
+ parse.quote_plus(MetaDataMDX.sha1_entity_transform(entity_id))
+ )
+
+ responses.add(
+ responses.GET,
+ url,
+ body=TEST_METADATA_STRING,
+ status=200,
+ content_type=SAML_METADATA_CONTENT_TYPE,
+ )
+
+ mdx = MetaDataMDX("http://mdx.example.com", freshness_period="P0Y0M0DT0H2M0S")
+ mdx._is_metadata_fresh = Mock(return_value=True)
+
+ mdx.single_sign_on_service(entity_id, BINDING_HTTP_REDIRECT)
+ assert entity_id in mdx.entity
+
+ mdx.single_sign_on_service(entity_id, BINDING_HTTP_REDIRECT)
+ assert len(responses.calls) == 1
+
+
+@responses.activate
+def test_mdx_metadata_freshness_period_expired():
+ """Ensure that metadata is not refreshed if not expired."""
+
+ entity_id = "http://xenosmilus.umdc.umu.se/simplesaml/saml2/idp/metadata.php"
+ url = "http://mdx.example.com/entities/{}".format(
+ parse.quote_plus(MetaDataMDX.sha1_entity_transform(entity_id))
+ )
+
+ responses.add(
+ responses.GET,
+ url,
+ body=TEST_METADATA_STRING,
+ status=200,
+ content_type=SAML_METADATA_CONTENT_TYPE,
+ )
+
+ mdx = MetaDataMDX("http://mdx.example.com", freshness_period="P0Y0M0DT0H2M0S")
+ mdx._is_metadata_fresh = Mock(return_value=False)
+
+ mdx.single_sign_on_service(entity_id, BINDING_HTTP_REDIRECT)
+ assert entity_id in mdx.entity
+
+ mdx.single_sign_on_service(entity_id, BINDING_HTTP_REDIRECT)
+ assert len(responses.calls) == 2
+
+
# pyff-test not available
# def test_mdx_service():
# sec_config.xmlsec_binary = sigver.get_xmlsec_binary(["/opt/local/bin"])