summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPaul Marillonnet <pmarillonnet@entrouvert.com>2017-11-14 09:36:44 (GMT)
committerPaul Marillonnet <pmarillonnet@entrouvert.com>2018-01-09 16:50:25 (GMT)
commit63993e360c89b22ea8e70fabf12d4f7e223cfb90 (patch)
treeca29a75a9cea46d1956fa5c6058a7f371ab9d8b5
parent6d8e1ca517f0011bd66cb50310ad933d57d1564d (diff)
downloaddjango-mellon-63993e360c89b22ea8e70fabf12d4f7e223cfb90.zip
django-mellon-63993e360c89b22ea8e70fabf12d4f7e223cfb90.tar.gz
django-mellon-63993e360c89b22ea8e70fabf12d4f7e223cfb90.tar.bz2
support federation file loading (#19396)
-rw-r--r--README13
-rw-r--r--mellon/adapters.py86
-rw-r--r--mellon/app_settings.py20
-rw-r--r--mellon/federation_utils.py221
-rw-r--r--mellon/utils.py114
-rw-r--r--mellon/views.py4
-rwxr-xr-xsetup.py1
-rw-r--r--tests/dummy_md.xml367
-rw-r--r--tests/federation-sample.xml557
-rw-r--r--tests/test_default_adapter.py10
-rw-r--r--tests/test_federations_utils.py39
-rw-r--r--tests/test_sso_slo.py88
-rw-r--r--tests/test_utils.py59
-rw-r--r--tests/utils.py10
14 files changed, 1509 insertions, 80 deletions
diff --git a/README b/README
index a06e1e9..c9bc481 100644
--- a/README
+++ b/README
@@ -82,6 +82,19 @@ metadata file of the identity provider or if it starts with a slash
the absolute path toward a metadata file. All other keys are override
of generic settings.
+MELLON_FEDERATIONS
+------------------
+
+A list of dictionaries, only one key 'FEDERATION' is mandatory in those
+dictionaries. It should contain the local path or the remote URL for the
+metadata file describing the SAML-based federation to be loaded in mellon. Both
+relative and absolute paths are supported.
+Additional parameters can be given as key/value pairs in the dictionaries, on
+a similar basis as the aforementioned MELLON_IDENTITY_PROVIDERS config.
+For each dictionary describing a federation, these parameters will apply to
+any successfully-loaded provider belonging to that federation.
+These parameters also override the global settings.
+
MELLON_PUBLIC_KEYS
------------------
diff --git a/mellon/adapters.py b/mellon/adapters.py
index bbf56e1..8f3f93a 100644
--- a/mellon/adapters.py
+++ b/mellon/adapters.py
@@ -9,8 +9,12 @@ import requests.exceptions
from django.core.exceptions import PermissionDenied
from django.contrib import auth
from django.contrib.auth.models import Group
+from django.utils.text import slugify
from . import utils, app_settings, models
+from mellon.federation_utils import idp_metadata_store, url2filename, \
+ idp_metadata_extract_entity_id, idp_metadata_is_cached, \
+ idp_metadata_load, idp_settings_store, idp_settings_load
class UserCreationError(Exception):
@@ -23,15 +27,60 @@ class DefaultAdapter(object):
def get_idp(self, entity_id):
'''Find the first IdP definition matching entity_id'''
- for idp in self.get_idps():
- if entity_id == idp['ENTITY_ID']:
- return idp
+ idp = None
+ if idp_metadata_is_cached(entity_id):
+ metadata_content = idp_metadata_load(entity_id)
+ entity_id = idp_metadata_extract_entity_id(metadata_content)
+ idp = {'METADATA': metadata_content,
+ 'ENTITY_ID': entity_id}
+ else:
+ for extra_idp in self.get_identity_providers_setting():
+ if extra_idp.get('ENTITY_ID') == entity_id or \
+ idp_metadata_extract_entity_id(extra_idp) == entity_id:
+ idp = extra_idp.copy()
+
+ extra_idp_settings = idp_settings_load(entity_id)
+ if extra_idp_settings and idp:
+ idp.update(extra_idp_settings)
+ return idp
def get_identity_providers_setting(self):
- return app_settings.IDENTITY_PROVIDERS
+ for federation_data in self.get_federations():
+ if not isinstance(federation_data, dict) or \
+ 'FEDERATION' not in federation_data:
+ continue
+ fed_extra_attrs = federation_data.copy()
+ fed_content = fed_extra_attrs.pop('FEDERATION')
+ fed_filepath, _ = utils.get_federation_metadata(fed_content)
+
+ try:
+ tree = ET.parse(fed_filepath)
+ root = tree.getroot()
+ for child in root:
+ provider = {}
+ entity_id = idp_metadata_extract_entity_id(ET.tostring(child))
+ if not entity_id:
+ continue
+ provider['METADATA'] = idp_metadata_store(ET.tostring(child))
+ provider.update({'ENTITY_ID': entity_id})
+ provider.update(fed_extra_attrs)
+ idp_settings_store(provider)
+ yield provider
+ except:
+ self.logger.error('Couldn\'t load federation metadata file %r',
+ fed_filepath)
+ continue
+
+ for extra_provider in app_settings.IDENTITY_PROVIDERS:
+ yield extra_provider
+
+ def get_federations(self):
+ for federation in getattr(app_settings, 'FEDERATIONS', []):
+ yield federation
def get_idps(self):
for i, idp in enumerate(self.get_identity_providers_setting()):
+ entity_id = idp.get('ENTITY_ID')
if 'METADATA_URL' in idp and 'METADATA' not in idp:
verify_ssl_certificate = utils.get_setting(
idp, 'VERIFY_SSL_CERTIFICATE')
@@ -43,28 +92,17 @@ class DefaultAdapter(object):
u'retrieval of metadata URL %r failed with error %s for %d-th idp',
idp['METADATA_URL'], e, i)
continue
- idp['METADATA'] = response.content
- elif 'METADATA' in idp:
- if idp['METADATA'].startswith('/'):
- idp['METADATA'] = file(idp['METADATA']).read()
- else:
+ md_content = response.content
+ if not entity_id:
+ entity_id = idp_metadata_extract_entity_id(md_content)
+ idp['METADATA'] = idp_metadata_store(md_content)
+ elif not idp.get('METADATA'):
self.logger.error(u'missing METADATA or METADATA_URL in %d-th idp', i)
continue
- if 'ENTITY_ID' not in idp:
- try:
- doc = ET.fromstring(idp['METADATA'])
- except (TypeError, ET.ParseError):
- self.logger.error(u'METADATA of %d-th idp is invalid', i)
- continue
- if doc.tag != '{%s}EntityDescriptor' % lasso.SAML2_METADATA_HREF:
- self.logger.error(u'METADATA of %d-th idp has no EntityDescriptor root tag', i)
- continue
-
- if not 'entityID' in doc.attrib:
- self.logger.error(
- u'METADATA of %d-th idp has no entityID attribute on its root tag', i)
- continue
- idp['ENTITY_ID'] = doc.attrib['entityID']
+ # load federation-specific configuration
+ extra_idp_settings = idp_settings_load(entity_id)
+ if extra_idp_settings:
+ idp.update(idp_settings_load(entity_id))
yield idp
def authorize(self, idp, saml_attributes):
diff --git a/mellon/app_settings.py b/mellon/app_settings.py
index aeeab73..3558d14 100644
--- a/mellon/app_settings.py
+++ b/mellon/app_settings.py
@@ -36,16 +36,32 @@ class AppSettings(object):
'LOGIN_URL': 'mellon_login',
'LOGOUT_URL': 'mellon_logout',
'ARTIFACT_RESOLVE_TIMEOUT': 10.0,
+ 'FEDERATIONS': [],
}
@property
+ def FEDERATIONS(self):
+ from django.conf import settings
+ if settings.hasattr('MELLON_FEDERATIONS'):
+ federations = settings.MELLON_FEDERATIONS
+ if isinstance(federations, dict):
+ federations = [federations]
+ return federations
+
+ @property
def IDENTITY_PROVIDERS(self):
from django.conf import settings
+ idps = []
try:
- idps = settings.MELLON_IDENTITY_PROVIDERS
+ if hasattr(settings, 'MELLON_IDENTITY_PROVIDERS'):
+ idps = settings.MELLON_IDENTITY_PROVIDERS
+ elif not hasattr(settings, 'MELLON_FEDERATIONS'):
+ raise AttributeError
except AttributeError:
from django.core.exceptions import ImproperlyConfigured
- raise ImproperlyConfigured('The MELLON_IDENTITY_PROVIDERS setting is mandatory')
+ raise ImproperlyConfigured('Either the MELLON_IDENTITY_PROVIDERS '
+ 'or the MELLON_FEDERATIONS settings '
+ 'are mandatory')
if isinstance(idps, dict):
idps = [idps]
return idps
diff --git a/mellon/federation_utils.py b/mellon/federation_utils.py
new file mode 100644
index 0000000..2484c1a
--- /dev/null
+++ b/mellon/federation_utils.py
@@ -0,0 +1,221 @@
+import fcntl
+import json
+import lasso
+import logging
+import tempfile
+from datetime import timedelta
+
+from django.utils.text import slugify
+from datetime import datetime
+
+import requests
+from xml.etree import ElementTree as ET
+import os
+import hashlib
+import os.path
+
+from django.core.files.storage import default_storage
+
+
+def truncate_unique(s, length=250):
+ if len(s) < length:
+ return s
+ md5 = hashlib.md5(s.encode('ascii')).hexdigest()
+ # we should be the first and last characters from the URL
+ l = (length - len(md5)) / 2 - 2 # four additional characters
+ assert l > 20
+ return s[:l] + '...' + s[-l:] + '_' + md5
+
+
+def url2filename(url):
+ return truncate_unique(slugify(url), 230)
+
+
+def load_federation_cache(url):
+ logger = logging.getLogger(__name__)
+ try:
+ filename = url2filename(url)
+ path = os.path.join('metadata-cache', filename)
+
+ unix_path = default_storage.path(path)
+ if not os.path.exists('metadata-cache'):
+ os.makedirs('metadata-cache')
+ f = open(unix_path, 'w')
+ try:
+ fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ except IOError:
+ return
+ else:
+ with tempfile.NamedTemporaryFile(dir=os.path.dirname(unix_path), delete=False) as temp:
+ try:
+ # increase modified time by one hour to prevent too many updates
+ st = os.stat(unix_path)
+ os.utime(unix_path, (st.st_atime, st.st_mtime + 3600))
+ response = requests.get(url)
+ response.raise_for_status()
+ temp.write(response.content)
+ temp.flush()
+ os.rename(temp.name, unix_path)
+ except:
+ logger.error('Could\'nt fetch %r', url)
+ os.unlink(temp.name)
+ finally:
+ fcntl.lockf(f, fcntl.LOCK_UN)
+ finally:
+ f.close()
+ except OSError:
+ logger.exception(u"could create the intermediary 'metadata-cache' "
+ "folder")
+ return
+ except:
+ logger.exception(u'failed to load federation from %s', url)
+
+
+def get_federation_from_url(url, update_cache=False):
+ logger = logging.getLogger(__name__)
+ filename = url2filename(url)
+ path = os.path.join('metadata-cache', filename)
+ if not default_storage.exists(path) or update_cache or \
+ default_storage.created_time(path) < datetime.now() - timedelta(days=1):
+ load_federation_cache(url)
+ else:
+ logger.warning('federation %s has not been loaded', url)
+ return path
+
+
+def idp_metadata_filepath(entity_id):
+ filename = url2filename(entity_id)
+ return os.path.join('./metadata-cache', filename)
+
+
+def idp_settings_filepath(entity_id):
+ filename = url2filename(entity_id) + "_settings.json"
+ return os.path.join('./metadata-cache', filename)
+
+
+def idp_metadata_is_cached(entity_id):
+ filepath = idp_metadata_filepath(entity_id)
+ if not default_storage.exists(filepath):
+ return False
+ return True
+
+
+def idp_metadata_is_file(metadata):
+ # XXX too restrictive (e.g. 'metadata/http-somemetadataserver-com-md00.xml'
+ # could be a file too...)
+ # On the opposite, `if "http://" in metadata or "https://" in metadata:" is
+ # equally restrictive.
+ # Using a URLValidator doesn't seem adequate either.
+ if metadata.startswith('/') or metadata.startswith('./'):
+ return True
+
+
+def idp_metadata_needs_refresh(entity_id, update_cache=False):
+ filepath = idp_metadata_filepath(entity_id)
+ if not default_storage.exists(filepath) or update_cache or \
+ default_storage.created_time(filepath) < datetime.now() - timedelta(days=1):
+ return True
+ return False
+
+
+def idp_settings_needs_refresh(entity_id, update_cache=False):
+ filepath = idp_settings_filepath(entity_id)
+ if not default_storage.exists(filepath) or update_cache or \
+ default_storage.created_time(filepath) < datetime.now() - timedelta(days=1):
+ return True
+ return False
+
+
+def idp_metadata_store(metadata_content):
+ entity_id = idp_metadata_extract_entity_id(metadata_content)
+ if not entity_id:
+ return
+ logger = logging.getLogger(__name__)
+ filepath = idp_metadata_filepath(entity_id)
+ if idp_metadata_needs_refresh(entity_id):
+ with open(filepath, 'w') as f:
+ try:
+ fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ f.write(metadata_content)
+ fcntl.lockf(f, fcntl.LOCK_UN)
+ except:
+ logger.error('Couldn\'t store metadata for EntityID %r',
+ entity_id)
+ return
+ return filepath
+
+
+def idp_metadata_load(entity_id):
+ logger = logging.getLogger(__name__)
+ filepath = idp_metadata_filepath(entity_id)
+ if default_storage.exists(filepath):
+ logger.info('Loading metadata for EntityID %r', entity_id)
+ with open(filepath, 'r') as f:
+ return f.read()
+ else:
+ logger.warning('No metadata file for EntityID %r', entity_id)
+
+
+def idp_settings_store(idp):
+ """
+ Stores an IDP settings when loaded from a federation.
+ """
+ logger = logging.getLogger(__name__)
+ entity_id = idp.get('ENTITY_ID')
+ filepath = idp_settings_filepath(entity_id)
+ idp_settings = {}
+
+ if not entity_id:
+ return
+
+ for key, value in idp.items():
+ if key not in ('METADATA', 'ENTITY_ID'):
+ idp_settings.update({key: value})
+
+ if idp_settings_needs_refresh(entity_id) and idp_settings:
+ with open(filepath, 'w') as f:
+ try:
+ fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ f.write(json.dumps(idp_settings))
+ fcntl.lockf(f, fcntl.LOCK_UN)
+ except:
+ logger.error('Couldn\'t store settings for EntityID %r',
+ entity_id)
+ return
+ return filepath
+
+
+def idp_settings_load(entity_id):
+ logger = logging.getLogger(__name__)
+ filepath = idp_settings_filepath(entity_id)
+ if default_storage.exists(filepath):
+ logger.info('Loading JSON settings for EntityID %r', entity_id)
+ with open(filepath, 'r') as f:
+ try:
+ idp_settings = json.loads(f.read())
+ except:
+ logger.warning('Couldn\'t load JSON settings for EntityID %r',
+ entity_id)
+ else:
+ return idp_settings
+ else:
+ logger.warning('No JSON settings file for EntityID %r', entity_id)
+
+
+def idp_metadata_extract_entity_id(metadata_content):
+ logger = logging.getLogger(__name__)
+ try:
+ doc = ET.fromstring(metadata_content)
+ except (TypeError, ET.ParseError):
+ logger.error(u'METADATA of idp %r is invalid', metadata_content)
+ return
+ if doc.tag != '{%s}EntityDescriptor' % lasso.SAML2_METADATA_HREF:
+ logger.error(u'METADATA of idp %r has no EntityDescriptor root tag',
+ metadata_content)
+ return
+ if not 'entityID' in doc.attrib:
+ logger.error(
+ u'METADATA of idp %r has no entityID attribute on its root tag',
+ metadata_content)
+ return
+ return doc.attrib['entityID']
diff --git a/mellon/utils.py b/mellon/utils.py
index 90464c1..48fe391 100644
--- a/mellon/utils.py
+++ b/mellon/utils.py
@@ -5,14 +5,18 @@ from functools import wraps
import isodate
from django.contrib import auth
+from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
+from django.core.validators import URLValidator
from django.template.loader import render_to_string
+from django.utils.text import slugify
from django.utils.timezone import make_aware, now, make_naive, is_aware, get_default_timezone
from django.conf import settings
from django.utils.six.moves.urllib.parse import urlparse
import lasso
from . import app_settings
+from federation_utils import get_federation_from_url, idp_metadata_is_file
def create_metadata(request):
@@ -48,44 +52,71 @@ SERVERS = {}
def create_server(request):
logger = logging.getLogger(__name__)
root = request.build_absolute_uri('/')
- cache = getattr(settings, '_MELLON_SERVER_CACHE', {})
- if root not in cache:
- metadata = create_metadata(request)
- if app_settings.PRIVATE_KEY:
- private_key = app_settings.PRIVATE_KEY
- private_key_password = app_settings.PRIVATE_KEY_PASSWORD
- elif app_settings.PRIVATE_KEYS:
- private_key = app_settings.PRIVATE_KEYS[0]
- private_key_password = None
- if isinstance(private_key, (tuple, list)):
- private_key_password = private_key[1]
- private_key = private_key[0]
- else: # no signature
- private_key = None
- private_key_password = None
- server = lasso.Server.newFromBuffers(metadata, private_key_content=private_key,
- private_key_password=private_key_password)
- server.setEncryptionPrivateKeyWithPassword(private_key, private_key_password)
- private_keys = app_settings.PRIVATE_KEYS
- # skip first key if it is already loaded
- if not app_settings.PRIVATE_KEY:
- private_keys = app_settings.PRIVATE_KEYS[1:]
- for key in private_keys:
- password = None
- if isinstance(key, (tuple, list)):
- password = key[1]
- key = key[0]
- server.setEncryptionPrivateKeyWithPassword(key, password)
- for idp in get_idps():
- try:
- server.addProviderFromBuffer(lasso.PROVIDER_ROLE_IDP, idp['METADATA'])
- except lasso.Error as e:
- logger.error(u'bad metadata in idp %r', idp['ENTITY_ID'])
- logger.debug(u'lasso error: %s', e)
- continue
- cache[root] = server
- settings._MELLON_SERVER_CACHE = cache
- return settings._MELLON_SERVER_CACHE.get(root)
+ metadata = create_metadata(request)
+ if app_settings.PRIVATE_KEY:
+ private_key = app_settings.PRIVATE_KEY
+ private_key_password = app_settings.PRIVATE_KEY_PASSWORD
+ elif app_settings.PRIVATE_KEYS:
+ private_key = app_settings.PRIVATE_KEYS[0]
+ private_key_password = None
+ if isinstance(private_key, (tuple, list)):
+ private_key_password = private_key[1]
+ private_key = private_key[0]
+ else: # no signature
+ private_key = None
+ private_key_password = None
+ server = lasso.Server.newFromBuffers(metadata, private_key_content=private_key,
+ private_key_password=private_key_password)
+ server.setEncryptionPrivateKeyWithPassword(private_key, private_key_password)
+ private_keys = app_settings.PRIVATE_KEYS
+ # skip first key if it is already loaded
+ if not app_settings.PRIVATE_KEY:
+ private_keys = app_settings.PRIVATE_KEYS[1:]
+ for key in private_keys:
+ password = None
+ if isinstance(key, (tuple, list)):
+ password = key[1]
+ key = key[0]
+ server.setEncryptionPrivateKeyWithPassword(key, password)
+ for idp in get_idps():
+ try:
+ metadata = idp.get('METADATA')
+ if idp_metadata_is_file(metadata):
+ with open(metadata, 'r') as f:
+ metadata = f.read()
+ server.addProviderFromBuffer(lasso.PROVIDER_ROLE_IDP, metadata)
+ except lasso.Error, e:
+ logger.error(u'bad metadata in idp %r', idp)
+ logger.debug(u'lasso error: %s', e)
+ except IOError, e:
+ logger.warning('No such metadata file: %r', metadata)
+ continue
+ return server
+
+
+def get_federation_metadata(federation):
+ logger = logging.getLogger(__name__)
+ fedmd = None
+ pemcert = None
+ if (isinstance(federation, tuple) and len(federation) == 2):
+ logger.info('Loading local cert-based federation %r',
+ federation)
+ if federation[1].endswith('.pem'):
+ fedmd = federation[0]
+ pemcert = federation[1]
+ else:
+ urlval = URLValidator()
+ try:
+ urlval(federation)
+ except ValidationError as e:
+ logger.info('Loading file-based federation %s',
+ federation)
+ fedmd = federation
+ else:
+ logger.info('Fetching and loading url-based federation %s',
+ federation)
+ fedmd = get_federation_from_url(federation)
+ return (fedmd, pemcert)
def create_login(request):
@@ -112,6 +143,13 @@ def get_idps():
yield idp
+def get_federations():
+ for adapter in get_adapters():
+ if hasattr(adapter, 'get_federations'):
+ for federation in adapter.get_federations():
+ yield federation
+
+
def flatten_datetime(d):
d = d.copy()
for key, value in d.iteritems():
diff --git a/mellon/views.py b/mellon/views.py
index 2ecb0b3..d6d58e9 100644
--- a/mellon/views.py
+++ b/mellon/views.py
@@ -342,9 +342,9 @@ class LoginView(ProfileMixin, LogMixin, View):
if idp is None:
return HttpResponseBadRequest('no idp found')
self.profile = login = utils.create_login(request)
- self.log.debug('authenticating to %r', idp['ENTITY_ID'])
+ self.log.debug('authenticating to %r', idp.get('ENTITY_ID') or idp['METADATA'])
try:
- login.initAuthnRequest(idp['ENTITY_ID'], lasso.HTTP_METHOD_REDIRECT)
+ login.initAuthnRequest(idp.get('ENTITY_ID'), lasso.HTTP_METHOD_REDIRECT)
authn_request = login.request
# configure NameID policy
policy = authn_request.nameIdPolicy
diff --git a/setup.py b/setup.py
index fb0fc00..2186277 100755
--- a/setup.py
+++ b/setup.py
@@ -94,6 +94,7 @@ setup(name="django-mellon",
'django>=1.5,<2.0',
'requests',
'isodate',
+ 'pytz',
],
setup_requires=[
'django>=1.5,<2.0',
diff --git a/tests/dummy_md.xml b/tests/dummy_md.xml
new file mode 100644
index 0000000..dc16725
--- /dev/null
+++ b/tests/dummy_md.xml
@@ -0,0 +1,367 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?><md:EntitiesDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute" xmlns:mdrpi="urn:oasis:names:tc:SAML:metadata:rpi" xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui" xmlns:pyff="http://pyff.io/NS" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:shibmd="urn:mace:shibboleth:metadata:1.0" xmlns:xrd="http://docs.oasis-open.org/ns/xri/xrd-1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ID="_20171018T113001Z" Name="https://federation.renater.fr/" cacheDuration="PT1H" validUntil="2017-10-27T11:30:01Z"><ds:Signature>
+<ds:SignedInfo>
+<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+<ds:Reference URI="">
+<ds:Transforms>
+<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
+<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+</ds:Transforms>
+<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+<ds:DigestValue>JKdLdd5yGvkFdb1fCAByMMnurIKYhZepRouZfOjIUrg=</ds:DigestValue>
+</ds:Reference>
+</ds:SignedInfo>
+<ds:SignatureValue>
+OTexfi8c63TsP1V9j5m6digA2NomUfqBtT8pPKhwdqEDQS5qLh6fxvT+wWkP6JaIhkP8nxwpbArl
+7cUHkRv5ibZzcknIAjXYMhsSTtFQUq89OMcDHtZHG54jiKyHPhu2+XEbvv6DsAYanYC6SHEnGjNG
+opnOEUB2XqeycsvvTQQIuWZEoABTVcKYyk2CW7Ij5EUmPOAPiidtbt8lzrtkV6dwLbkyoEbChAyj
+emrL/oS01aJgT9sQoJxR8lyRMGiZ/BwQqYTareiKwOXLPdGThzsfZXD8de9T1xuysILaAM7sHPJV
+QfrQJm80Zo2MM/GnhJTO9rc4m3kRnRhqmA6qMw==
+</ds:SignatureValue>
+<ds:KeyInfo>
+<ds:KeyValue>
+<ds:RSAKeyValue>
+<ds:Modulus>
+71+vTf66BPgYUF7sm4T++W69qMVyGQn9wNqpBLc6sp53eq/JRTOUD26Yehjsld5qN52Bv2r5QG7o
+4VU123akXUYzupvq1f+tmF9NwYa7MPEPFzCzJHhNXjZNRxcsW1WLW34fhQCm0oak3oSPoNo5qeGi
+jNsTSkgSt1mPH0P8d95af2VJnT6zbrclxvH4emqpT9oGLsWqKWLlIbZ7u1PUjuNVwLHuj909/apm
+C13RBIpV52fey4qey34bnRHdCTknZeN/TJLTJ9hMWzz9TbdjfIFaiF7MeY+OYRXzUJeQuHHMu/2I
+emkoR26mYi6irvmx8AdPcPCwcRKw2Ca4xLhbNw==
+</ds:Modulus>
+<ds:Exponent>AQAB</ds:Exponent>
+</ds:RSAKeyValue>
+</ds:KeyValue>
+<ds:X509Data>
+<ds:X509Certificate>
+MIIC9zCCAd+gAwIBAgIEfe6j3jANBgkqhkiG9w0BAQsFADAsMSowKAYDVQQDEyFTQU1MIE1ldGFk
+YXRhIFNpZ25pbmcgQ2VydGlmaWNhdGUwHhcNMTYwNzI5MDczNjM4WhcNMjYwNjA3MDczNjM4WjAs
+MSowKAYDVQQDEyFTQU1MIE1ldGFkYXRhIFNpZ25pbmcgQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3
+DQEBAQUAA4IBDwAwggEKAoIBAQDvX69N/roE+BhQXuybhP75br2oxXIZCf3A2qkEtzqynnd6r8lF
+M5QPbph6GOyV3mo3nYG/avlAbujhVTXbdqRdRjO6m+rV/62YX03Bhrsw8Q8XMLMkeE1eNk1HFyxb
+VYtbfh+FAKbShqTehI+g2jmp4aKM2xNKSBK3WY8fQ/x33lp/ZUmdPrNutyXG8fh6aqlP2gYuxaop
+YuUhtnu7U9SO41XAse6P3T39qmYLXdEEilXnZ97Lip7LfhudEd0JOSdl439MktMn2ExbPP1Nt2N8
+gVqIXsx5j45hFfNQl5C4ccy7/Yh6aShHbqZiLqKu+bHwB09w8LBxErDYJrjEuFs3AgMBAAGjITAf
+MB0GA1UdDgQWBBTT88iZzWO+hN9SBUkpx871lmTuLTANBgkqhkiG9w0BAQsFAAOCAQEABoPpODry
+XwiM5jjtqk6veR02FevCKHpZP6Od7Kqcfs6lg5LcQmGUOgpmW3Gg4UMjBYkgARsT2Nsnah1CJqa8
+cjvv8p5KEIhY0hVS8iMJnrb3PDeiFSeP4xSfct/6z/ebV4+QFl22bsm2zpAC6BpFz8+IJ/jAmQzT
+Vob4MAUeQPnwwzm3xz6yanLZx7BK5cfrTCa+hrarNQCboRjXPwiejF8WRCxpgRHH6yNs5QH/Z6o5
+e3tUP7uEpn2Ob+kcLsEMGb9DghkoDAgkHCOZeTy+7hgxt+/T94cLTa58gVtvEOnd0GuL7Vfd+IVd
+XgSard8RfR3OyZlf6M4aSGQA73sskQ==
+</ds:X509Certificate>
+</ds:X509Data>
+</ds:KeyInfo>
+</ds:Signature><md:EntityDescriptor entityID="https://aishib.agropolis.fr/idp/shibboleth">
+ <md:Extensions>
+ <mdrpi:RegistrationInfo registrationAuthority="https://federation.renater.fr/" registrationInstant="2013-06-06T11:49:20Z">
+ <mdrpi:RegistrationPolicy xml:lang="en">https://services.renater.fr/federation/en/metadata_registration_practice_statement</mdrpi:RegistrationPolicy>
+ </mdrpi:RegistrationInfo>
+ </md:Extensions>
+ <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0 urn:oasis:names:tc:SAML:2.0:protocol">
+ <md:Extensions>
+ <shibmd:Scope regexp="false">agropolis.fr</shibmd:Scope>
+
+ <mdui:UIInfo>
+
+ <mdui:DisplayName xml:lang="en">Agropolis International</mdui:DisplayName>
+
+ <mdui:Logo height="16" width="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAABIAAAASABGyWs+AAAACXZwQWcAAAAQAAAAEABcxq3DAAACPUlEQVQoz21SPW8TQRR8u7d3ju34Er6UCHACRqK4Bjk0oDSEjiYSBQVS8k9AkWgp+AuUASGEaCASkoPcgARVMF8iTnxJwMFJfL47397u3u6jsGVFwLzmFTN6ozdDEBEAAOD4kiRJsVgkhMA/YMfZzc3NVstnzGq1/KXlJfgf2Ii9XquVZ2YWbi5Io4MsbfPeZK4wRm0AOH6KICIivq2tV6/O5cYLHztbW9FvYTKuZaLk7PjpW+UrJXtspCHGmGazCQBnZ8svtj/0tbAINYhcy1DyThpRhHtztyedwkBDAWBnZ6dSqbzZ+7THu0IraTJplDSZ0JnQ2Y+482jj1cgSRUTHdrhW9fa3SKaxErFKIyUiKaIsjVQaKf5s631HxP1+X2vNwjB0J9zd+OgnD044Ra4lI5ZBw7XqyeRQREciaqe9xoE/K/OFfIENnqBM1uZBqlWBOYxQjYZnqieTThru8zCU3AASIAjIXNellGLePkjCUPICy1mEDAShSg7SqCsTBtQ7WfY3vlarVUYpdUsuAs5PXV7dfpe3HEYtjSbNZJwJoRUi3rl4fSrvfufctm0ySA0RD0V84+WDL1EbCCDCYIDApeKZ+uKK6faDIPA8jw7jIORUbry2eH+5Mu8ARTSAkCPW3QvX6osrE+B8bjQ8zxu2aACDaIwxxuzzsLbbePj88a8k0Mb4vr/2ei3LMoOIiGRU0iGGVuDp6pNz5fNSiKnpac/zgAABMuzSXwIggIjdbrdUKjHGYGQaAAD+AJBJecXxnEvWAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTA3LTIwVDEwOjQ4OjE1KzAyOjAwpWiGMQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNS0wNy0yMFQxMDo0ODoxNSswMjowMNQ1Po0AAAAASUVORK5CYII=</mdui:Logo>
+ <mdui:InformationURL xml:lang="fr">http://www.agropolis.fr</mdui:InformationURL>
+
+ <mdui:DisplayName xml:lang="fr">Agropolis International</mdui:DisplayName>
+
+ </mdui:UIInfo>
+ </md:Extensions>
+ <md:KeyDescriptor use="signing">
+<ds:KeyInfo>
+
+ <ds:X509Data>
+ <ds:X509Certificate>
+ MIIDNzCCAh+gAwIBAgIUYY3sGXwChkj2CRy6QFDvkdj2zlAwDQYJKoZIhvcNAQEF
+BQAwHjEcMBoGA1UEAxMTYWlzaGliLmFncm9wb2xpcy5mcjAeFw0xMzA1MTUxMzM3
+MTJaFw0zMzA1MTUxMzM3MTJaMB4xHDAaBgNVBAMTE2Fpc2hpYi5hZ3JvcG9saXMu
+ZnIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxrDy6lrhIBjcxv16n
+4UJ2cEMYPO4wSmfDwhO6feoSIEuIblYRHE2nQKirMokwD6seF4rbDHyxLXg/ColL
+VLv+0CJteIOZjSCgSN90WzQRrC1Ex5sJfPu6yPEXvW8H1906gEg6ok8rlCIHRGfE
+15pHK5eqxQS5f2n8c2t/Uk33/FBj79/hb3Cd7vE4mdlvReD3AFswC0lV4bPmj3Ka
+KUuMj9xwipwnfWCu6p2/ZJF4M3ADU5grXHJ2Vqmd8DWm5raaObKjYwJddbRBByI8
+bJJLIwAQQmX4Dh4hf1QKlf2oqWPWVQxLQp0erL1U8IWmj1RG8TTH9xOJl6kkEhYq
+Z2gfAgMBAAGjbTBrMEoGA1UdEQRDMEGCE2Fpc2hpYi5hZ3JvcG9saXMuZnKGKmh0
+dHBzOi8vYWlzaGliLmFncm9wb2xpcy5mci9pZHAvc2hpYmJvbGV0aDAdBgNVHQ4E
+FgQU9A7iQ8Qo+t2JCpKuOOV9YBoYs4MwDQYJKoZIhvcNAQEFBQADggEBAG0LOW6I
+F+M8n2NpzyQjfVCJCA6QhWjbXrfemiPJFZGZZb2dVmHof4yCpCUYgHOBoZaXPOlB
+nLYsUWvFZ6V2GELZpLHzHSSrYidieW07qQkh1DwcIYpvtZgLviOtT/tCEGsk925f
+DUoGdeIqpqt54WZcW9+TbKicvjg3JT4BFOQ17bFNwPW+YjTbvsWYxen+e0mRp4vM
+V0yMu2f3bccVhePASSZGL3yod3sJ1dPvlrJO9c35BekhtirolVjZqMQ0AYPVifua
+yIU0dWXsZkAOcBL9kZFbJcYRUIxMgvp8U2Zdv1+ZlwOyXnnWDOOh9wjuT7FAyObU
+ChvjHlgZHkvLwJI=
+ </ds:X509Certificate>
+ </ds:X509Data>
+
+ </ds:KeyInfo>
+ </md:KeyDescriptor>
+
+
+
+
+
+ <md:NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</md:NameIDFormat>
+ <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
+
+
+ <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://aishib.agropolis.fr/idp/profile/SAML2/POST/SSO"/>
+
+ <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://aishib.agropolis.fr/idp/profile/SAML2/Redirect/SSO"/>
+
+
+ <md:SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest" Location="https://aishib.agropolis.fr/idp/profile/Shibboleth/SSO"/>
+
+
+ </md:IDPSSODescriptor>
+
+
+
+ <md:Organization>
+
+ <md:OrganizationName xml:lang="en">Agropolis International</md:OrganizationName>
+ <md:OrganizationDisplayName xml:lang="en">Agropolis International</md:OrganizationDisplayName>
+ <md:OrganizationURL xml:lang="en">http://www.agropolis.fr</md:OrganizationURL>
+
+ </md:Organization>
+
+
+
+ <md:ContactPerson contactType="technical">
+ <md:SurName>Jean Cerda</md:SurName>
+ <md:EmailAddress>cerda@agropolis.fr</md:EmailAddress>
+ </md:ContactPerson>
+
+
+
+ <md:ContactPerson contactType="technical">
+ <md:SurName>Jean-Pierre Allano</md:SurName>
+ <md:EmailAddress>allano@agropolis.fr</md:EmailAddress>
+ </md:ContactPerson>
+
+
+
+
+ </md:EntityDescriptor><md:EntityDescriptor entityID="https://ambre.vetagro-sup.fr/idp/shibboleth">
+ <md:Extensions>
+ <mdrpi:RegistrationInfo registrationAuthority="https://federation.renater.fr/" registrationInstant="2013-01-14T16:11:53Z">
+ <mdrpi:RegistrationPolicy xml:lang="en">https://services.renater.fr/federation/en/metadata_registration_practice_statement</mdrpi:RegistrationPolicy>
+ </mdrpi:RegistrationInfo>
+ </md:Extensions>
+ <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0 urn:oasis:names:tc:SAML:2.0:protocol">
+ <md:Extensions>
+ <shibmd:Scope regexp="false">vetagro-sup.fr</shibmd:Scope>
+
+ <mdui:UIInfo>
+
+ <mdui:DisplayName xml:lang="en">Vetagro Sup</mdui:DisplayName>
+
+ <mdui:Logo height="16" width="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAABIAAAASABGyWs+AAAACXZwQWcAAAAQAAAAEABcxq3DAAAB/klEQVQ4y2NkQAP///+X+/jxI/PO7dujP3/7kf7v778ffDzciw2MDFaqq6t/Y2RkfIysngndAEZGxkezZ8/25xEQqVDTs5eRVrNU4RQUL7x9+7YtumasBjAwMDBISIgbcvBKct998p7h1fvfDKyc4nz//jF6yjAwSBNlwM+fP3/9/PmT4dbl4wysrCwMf//9Z/r7959UcFISD1EGPH107xgfD9sbTg52hrcvHjDw87Aw/Pz+7tLEefNuEmWAm6ffxvu3Ll4zMjZleHz/KsO7lw+f3bh+czs2tVgNsLS0fPfs4b3VnKy//wrycW84e2L/uobmtg0MpIKurt6N////Z8jPzxcnWTMDAwPD79//tbq7+9bhU8OMTfD//8cMP79L1X/8tjxMQYHXT0Uh88aBgyuvEW3Aly+8ntw8jLU3bx20cnH/xPz2jTyvk9OGpTt3NhIOxP//GRgkJGR8tPVeyElIvWA4dfIqg57Bff0nD+eHExULIb5dqkYmYgYm5r8YPH3vMzx//omBk+edpI4+vxVRBnz5zPqbi4vt7a9fv//9+cvAYG79jIGZmZWBg4NJiCgDdh4sfHD/7u9j714Z/BYWiGBQVkxjePNK8+erV5+uYDOABTMM/nP09y9Z8+OHgNjli9ahf//++8vGwrlAS0tp/v///7kYGRm/IasHAIVTy8DG/VpJAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTA3LTIwVDEwOjUwOjA5KzAyOjAwfDtz7wAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNS0wNy0yMFQxMDo1MDowOSswMjowMA1my1MAAAAASUVORK5CYII=</mdui:Logo>
+ <mdui:InformationURL xml:lang="fr">http://www.vetagro-sup.fr</mdui:InformationURL>
+
+ <mdui:DisplayName xml:lang="fr">Vetagro Sup</mdui:DisplayName>
+
+ </mdui:UIInfo>
+ </md:Extensions>
+ <md:KeyDescriptor use="signing">
+<ds:KeyInfo>
+
+ <ds:X509Data>
+ <ds:X509Certificate>
+ MIIDPDCCAiSgAwIBAgIVAL9PsuadPSIZcMHNxlK/oevezmzWMA0GCSqGSIb3DQEB
+BQUAMB8xHTAbBgNVBAMTFGFtYnJlLnZldGFncm8tc3VwLmZyMB4XDTEyMTEwODEw
+MTQwNFoXDTMyMTEwODEwMTQwNFowHzEdMBsGA1UEAxMUYW1icmUudmV0YWdyby1z
+dXAuZnIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCc/ptfpmkomwmT
+4RsID+1Ce1dX0eUjcLgSOZN8hVpHWLag2ERWkpmvB5aK7BAFcI5i//Gk80tAiasu
+JtlZhBnEw54aTJRGpyL2CVkHyl6SMRxprIi1Ji67IoGqEgUeGaheAxo+tG5e1WSc
+bIbldcSKdwvjAV+7HSB4C6NqLsAzJH25++yaRH2uf2LTD0TDzNR9Q2hVj/VyYWR+
+K3HWI1Snjn/i7aFfZZhYmBkwHuQOaPhwCM+khikg5XicMsxUhHCMi93UgHGIsdkr
+IEGj4xydBTUKsLaykeuFS8EgXbWwCLGkeX76w8xDoFIpnppU/yFd9v7Zg3EBfn4p
+kTW3GdIjAgMBAAGjbzBtMEwGA1UdEQRFMEOCFGFtYnJlLnZldGFncm8tc3VwLmZy
+hitodHRwczovL2FtYnJlLnZldGFncm8tc3VwLmZyL2lkcC9zaGliYm9sZXRoMB0G
+A1UdDgQWBBTPTqWkVHrHXFjmxMWkNt/sp2h5ozANBgkqhkiG9w0BAQUFAAOCAQEA
+FvXMtfBUmRZCzz8CjanGzr1TBUPmnkrKci5AtkseKw9YlfUmBXTHB01y697nYq6m
+RB6KhvfW212h9CF0IOEEjoadgDhXqGYhq8PnAOtT4Ty3XDy8SbRh8aQWfvnfSngv
+FdpHRiSpj5UXXuT5zTtkf59h58XKtEfCkMbUzvdOgUobJzpD0WISmQHPQnx+Neg6
+9j7oMRrDiZjS39Om8Imu9xvsnddDM3PlsDBIsvrr1o7K5iLkEdR1YYX0ZNDbiFuw
+QXXl2dwQPB8KrScPUvCe57slU2gFQvvIBzjQysxC6V6TPSuM3A/ee56lACuB3jKj
+oYkHQc5Gj/1rSMLmu9aLMg==
+ </ds:X509Certificate>
+ </ds:X509Data>
+
+ </ds:KeyInfo>
+ </md:KeyDescriptor>
+
+
+
+
+
+ <md:NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</md:NameIDFormat>
+ <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
+
+
+ <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ambre.vetagro-sup.fr/idp/profile/SAML2/POST/SSO"/>
+
+ <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://ambre.vetagro-sup.fr/idp/profile/SAML2/Redirect/SSO"/>
+
+
+ <md:SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest" Location="https://ambre.vetagro-sup.fr/idp/profile/Shibboleth/SSO"/>
+
+
+ </md:IDPSSODescriptor>
+
+
+
+ <md:Organization>
+
+ <md:OrganizationName xml:lang="en">Vetagro Sup</md:OrganizationName>
+ <md:OrganizationDisplayName xml:lang="en">Vetagro Sup</md:OrganizationDisplayName>
+ <md:OrganizationURL xml:lang="en">http://www.vetagro-sup.fr</md:OrganizationURL>
+
+ </md:Organization>
+
+
+
+ <md:ContactPerson contactType="technical">
+ <md:SurName>Nicolas Aulas</md:SurName>
+ <md:EmailAddress>nicolas.aulas@vetagro-sup.fr</md:EmailAddress>
+ </md:ContactPerson>
+
+
+
+
+
+
+ </md:EntityDescriptor><md:EntityDescriptor entityID="https://antimoine.insa-strasbourg.fr/idp/shibboleth">
+ <md:Extensions>
+ <mdrpi:RegistrationInfo registrationAuthority="https://federation.renater.fr/" registrationInstant="2014-02-11T08:44:08Z">
+ <mdrpi:RegistrationPolicy xml:lang="en">https://services.renater.fr/federation/en/metadata_registration_practice_statement</mdrpi:RegistrationPolicy>
+ </mdrpi:RegistrationInfo>
+ </md:Extensions>
+ <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0 urn:oasis:names:tc:SAML:2.0:protocol">
+ <md:Extensions>
+ <shibmd:Scope regexp="false">insa-strasbourg.fr</shibmd:Scope>
+
+ <mdui:UIInfo>
+
+ <mdui:DisplayName xml:lang="en">INSA Strasbourg</mdui:DisplayName>
+
+ <mdui:Logo height="16" width="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAq1BMVEXeAACEhoTWz87nQTn3oqXnIBjvgnvWlpT/5+fnEAjvZWOltrXnNDH3mpT/9/fnCAD/4+fvioz3x8bnVVL3qq3nKCnvkpTnHBjvcXP3sq3vgoT/7+/nFBDnODH3mpz////nDAj3z87vlpTveXv3vr3nAACMmpznRUL3pqXnJCHnFAj/+//nDADvjoz3y87vXVr3rq3nLCnvdXP3srXvhoT/8/fvODH3npz3lpQaie6kAAAACXBIWXMAAABIAAAASABGyWs+AAAACXZwQWcAAAAQAAAAEABcxq3DAAAAe0lEQVQY053OwQqCQBhF4VM22VSMMWrFGP2kkIpUQ1nz/m8Ws20TeFeHb3WRnzEBBjO8xRjZrURi0TFiy+UITjTpjIKaOndZuDSGxF0j9G1+e7CuLN1nE2F7VI28tAoqMMezP1ieFOOp7RPueBXKSqdnMq8Wg66nPP0LX4uIG4jEwJ0sAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTA3LTIwVDEwOjQ5OjA0KzAyOjAwIHfmJQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNS0wNy0yMFQxMDo0OTowNCswMjowMFEqXpkAAAAASUVORK5CYII=</mdui:Logo>
+ <mdui:InformationURL xml:lang="fr">http://www.insa-strasbourg.fr</mdui:InformationURL>
+
+ <mdui:DisplayName xml:lang="fr">INSA Strasbourg</mdui:DisplayName>
+
+ </mdui:UIInfo>
+ </md:Extensions>
+ <md:KeyDescriptor use="signing">
+<ds:KeyInfo>
+
+ <ds:X509Data>
+ <ds:X509Certificate>
+ MIIDUDCCAjigAwIBAgIVAIbX8U0uAqAhuXm1jWxiFpggtDTDMA0GCSqGSIb3DQEB
+CwUAMCQxIjAgBgNVBAMMGXNvdWZyZS5pbnNhLXN0cmFzYm91cmcuZnIwHhcNMTYw
+OTI3MTIzNjIxWhcNMzYwOTI3MTIzNjIxWjAkMSIwIAYDVQQDDBlzb3VmcmUuaW5z
+YS1zdHJhc2JvdXJnLmZyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
+sEE02sLRPAG5N81DMHEeGpI2MYF8yG/RiwH07cFIlLqgV80ewOmi0FWPYijxMb8A
+bmx0RwUMvJBVI6WMxtT9fykhID20k8rWOuYOzvaynzVqCktqVgKoEAxP1PFE9b0n
+iGKFprjjNl9ZD90GOUsxbAO7yXG9Q4WBa/eThl6XkUvNkSaZp5hcdWrgcAdsae3q
+iD/uxFa38NXNNeRLGyfxjd2K5qYSzbwBza9s9TOq1+pfw7sxu3/4BnfQ0RLGO6co
+4tH4Mufh0ome4cyYk4pvW5DOd1AznxDb8HpqvE0zwEsa69c/FDX0akgFZydmc77a
+j6USn6JKjjbO49yGtG1gVQIDAQABo3kwdzAdBgNVHQ4EFgQUjzMsxZYiokPYxper
+9zadM8J0F0kwVgYDVR0RBE8wTYIZc291ZnJlLmluc2Etc3RyYXNib3VyZy5mcoYw
+aHR0cHM6Ly9zb3VmcmUuaW5zYS1zdHJhc2JvdXJnLmZyL2lkcC9zaGliYm9sZXRo
+MA0GCSqGSIb3DQEBCwUAA4IBAQBFJKsiS3yfWuDB/E+iqQ0TuQJzL5+JIcloN0dw
+BFxW3VZOju15zeQ7LwRBg9S4SGLMPJU+LM1lvr68cK9brut/FjF51SETIXEeCWo3
+7+PIqgOCzraLNinmpU/OtN8ENalOPvpS6Jvbd23qB2t+IqOtZ+j15b0Yq4/on1E3
+W2F9CVzKpe4EwmmtCPQbe7U1wvhgFylEx797pex8veWs79YSYwqvcKMh79dzl8Fo
+/CgsO5pDrfKmc6SGMkByq75dZj+PqhZDzZ9EFTxbrXOTaS08VRN6a5Rh2iYRnGxq
+yZl66tPcaIm5PHgOEmu5X4lPkUoY+Jt36Gj3SGCbYt8qH5S0
+ </ds:X509Certificate>
+ </ds:X509Data>
+
+ </ds:KeyInfo>
+ </md:KeyDescriptor>
+
+
+
+ <md:KeyDescriptor use="signing">
+ <ds:KeyInfo>
+ <ds:X509Data>
+ <ds:X509Certificate>
+ MIIDXDCCAkSgAwIBAgIVAKI+qiqDCk9wTTqn7OVAoZrvj/CpMA0GCSqGSIb3DQEB
+BQUAMCcxJTAjBgNVBAMTHGFudGltb2luZS5pbnNhLXN0cmFzYm91cmcuZnIwHhcN
+MTQwMTEzMTAzOTU4WhcNMzQwMTEzMTAzOTU4WjAnMSUwIwYDVQQDExxhbnRpbW9p
+bmUuaW5zYS1zdHJhc2JvdXJnLmZyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEAtuM8lRjlVjjmrHq9VtguaOMQL+Wd99BiOs56kL3Mbctg1FwH69LYThCW
+6dOz6WJg/jU/naF7jEikXKc71xGyu7Ph7Iqa9S5hoXXAT8u/0q2nZDeTOraJqKe1
+FMF2RzXhEEMyQO3CiKNK9b+tbKoNZS7FQCixMZklWZPt4EcEKd6jyRq1WYX3dpnb
+r9I/aCdhtK/PGvGe5gKTDoTR2HKyWKJTc/obf8x/vlYIEwiaGgdlqI2KiBE0x48n
+zQdP6XVi3T8ZWbnkLmCfgJtP2C8PtEJuwDRAy0Z9N4DSwvxn5YCVYgBLSi0TLa10
+B/lUqqBezZrTrA9p9Lt8JtGXW5YGHwIDAQABo38wfTBcBgNVHREEVTBTghxhbnRp
+bW9pbmUuaW5zYS1zdHJhc2JvdXJnLmZyhjNodHRwczovL2FudGltb2luZS5pbnNh
+LXN0cmFzYm91cmcuZnIvaWRwL3NoaWJib2xldGgwHQYDVR0OBBYEFLFkjPZUc9JY
+qrWjldJ/iGGkKAt4MA0GCSqGSIb3DQEBBQUAA4IBAQBSk/wU1mRn4VF2ifmy261K
+DK7uX+t1H1hh8S38fKSFU7HoNXJTV3vQnmBOpYIGC1gtvmb+qjqpNtikU2zO84Gq
+Q0bXHxYF2d9RUP89mKaFxE5uNcXFmlOA3ChZY3pMT5zwAPI/T60tGrex7zci7OLn
+JDAQj/q4Yk9ejx6JTFggQSCCVh+oV/SDIMd2p5AY6H3mto3b6XCk7Lssa8a/D30k
+pEkZnhTKdN82eRyynuOR7UDU4tasV4d7Mi/j53f5ihnRcsvwh/pYodjoVYY8cEcZ
+JLnAXYF8coSwh8UN4D/0NHsvTuSOFQc85hGrqacMsvxiQiw9mv01AX5+A5YLEbVQ
+ </ds:X509Certificate>
+ </ds:X509Data>
+ </ds:KeyInfo>
+ </md:KeyDescriptor>
+
+
+
+ <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://antimoine.insa-strasbourg.fr/idp/profile/SAML2/Redirect/SLO"/>
+
+ <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://antimoine.insa-strasbourg.fr/idp/profile/SAML2/POST/SLO"/>
+
+ <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://antimoine.insa-strasbourg.fr/idp/profile/SAML2/SOAP/SLO"/>
+
+
+ <md:NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</md:NameIDFormat>
+ <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
+
+
+ <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://antimoine.insa-strasbourg.fr/idp/profile/SAML2/POST/SSO"/>
+
+ <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://antimoine.insa-strasbourg.fr/idp/profile/SAML2/Redirect/SSO"/>
+
+
+ <md:SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest" Location="https://antimoine.insa-strasbourg.fr/idp/profile/Shibboleth/SSO"/>
+
+
+ </md:IDPSSODescriptor>
+
+
+
+ <md:Organization>
+
+ <md:OrganizationName xml:lang="en">INSA Strasbourg</md:OrganizationName>
+ <md:OrganizationDisplayName xml:lang="en">INSA Strasbourg</md:OrganizationDisplayName>
+ <md:OrganizationURL xml:lang="en">http://www.insa-strasbourg.fr</md:OrganizationURL>
+
+ </md:Organization>
+
+
+
+ <md:ContactPerson contactType="technical">
+ <md:SurName>Lahsen BOUZID</md:SurName>
+ <md:EmailAddress>lahsen.bouzid@insa-strasbourg.fr</md:EmailAddress>
+ </md:ContactPerson>
+
+
+
+ <md:ContactPerson contactType="technical">
+ <md:SurName>Simon SCHERRER</md:SurName>
+ <md:EmailAddress>simon.scherrer@insa-strasbourg.fr</md:EmailAddress>
+ </md:ContactPerson>
+
+
+
+
+ </md:EntityDescriptor></md:EntitiesDescriptor>
diff --git a/tests/federation-sample.xml b/tests/federation-sample.xml
new file mode 100644
index 0000000..6102c46
--- /dev/null
+++ b/tests/federation-sample.xml
@@ -0,0 +1,557 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?><md:EntitiesDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute" xmlns:mdrpi="urn:oasis:names:tc:SAML:metadata:rpi" xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui" xmlns:pyff="http://pyff.io/NS" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:shibmd="urn:mace:shibboleth:metadata:1.0" xmlns:xrd="http://docs.oasis-open.org/ns/xri/xrd-1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ID="_20171018T113001Z" Name="https://federation.renater.fr/" cacheDuration="PT1H" validUntil="2017-10-27T11:30:01Z"><ds:Signature>
+<ds:SignedInfo>
+<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+<ds:Reference URI="">
+<ds:Transforms>
+<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
+<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+</ds:Transforms>
+<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+<ds:DigestValue>JKdLdd5yGvkFdb1fCAByMMnurIKYhZepRouZfOjIUrg=</ds:DigestValue>
+</ds:Reference>
+</ds:SignedInfo>
+<ds:SignatureValue>
+OTexfi8c63TsP1V9j5m6digA2NomUfqBtT8pPKhwdqEDQS5qLh6fxvT+wWkP6JaIhkP8nxwpbArl
+7cUHkRv5ibZzcknIAjXYMhsSTtFQUq89OMcDHtZHG54jiKyHPhu2+XEbvv6DsAYanYC6SHEnGjNG
+opnOEUB2XqeycsvvTQQIuWZEoABTVcKYyk2CW7Ij5EUmPOAPiidtbt8lzrtkV6dwLbkyoEbChAyj
+emrL/oS01aJgT9sQoJxR8lyRMGiZ/BwQqYTareiKwOXLPdGThzsfZXD8de9T1xuysILaAM7sHPJV
+QfrQJm80Zo2MM/GnhJTO9rc4m3kRnRhqmA6qMw==
+</ds:SignatureValue>
+<ds:KeyInfo>
+<ds:KeyValue>
+<ds:RSAKeyValue>
+<ds:Modulus>
+71+vTf66BPgYUF7sm4T++W69qMVyGQn9wNqpBLc6sp53eq/JRTOUD26Yehjsld5qN52Bv2r5QG7o
+4VU123akXUYzupvq1f+tmF9NwYa7MPEPFzCzJHhNXjZNRxcsW1WLW34fhQCm0oak3oSPoNo5qeGi
+jNsTSkgSt1mPH0P8d95af2VJnT6zbrclxvH4emqpT9oGLsWqKWLlIbZ7u1PUjuNVwLHuj909/apm
+C13RBIpV52fey4qey34bnRHdCTknZeN/TJLTJ9hMWzz9TbdjfIFaiF7MeY+OYRXzUJeQuHHMu/2I
+emkoR26mYi6irvmx8AdPcPCwcRKw2Ca4xLhbNw==
+</ds:Modulus>
+<ds:Exponent>AQAB</ds:Exponent>
+</ds:RSAKeyValue>
+</ds:KeyValue>
+<ds:X509Data>
+<ds:X509Certificate>
+MIIC9zCCAd+gAwIBAgIEfe6j3jANBgkqhkiG9w0BAQsFADAsMSowKAYDVQQDEyFTQU1MIE1ldGFk
+YXRhIFNpZ25pbmcgQ2VydGlmaWNhdGUwHhcNMTYwNzI5MDczNjM4WhcNMjYwNjA3MDczNjM4WjAs
+MSowKAYDVQQDEyFTQU1MIE1ldGFkYXRhIFNpZ25pbmcgQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3
+DQEBAQUAA4IBDwAwggEKAoIBAQDvX69N/roE+BhQXuybhP75br2oxXIZCf3A2qkEtzqynnd6r8lF
+M5QPbph6GOyV3mo3nYG/avlAbujhVTXbdqRdRjO6m+rV/62YX03Bhrsw8Q8XMLMkeE1eNk1HFyxb
+VYtbfh+FAKbShqTehI+g2jmp4aKM2xNKSBK3WY8fQ/x33lp/ZUmdPrNutyXG8fh6aqlP2gYuxaop
+YuUhtnu7U9SO41XAse6P3T39qmYLXdEEilXnZ97Lip7LfhudEd0JOSdl439MktMn2ExbPP1Nt2N8
+gVqIXsx5j45hFfNQl5C4ccy7/Yh6aShHbqZiLqKu+bHwB09w8LBxErDYJrjEuFs3AgMBAAGjITAf
+MB0GA1UdDgQWBBTT88iZzWO+hN9SBUkpx871lmTuLTANBgkqhkiG9w0BAQsFAAOCAQEABoPpODry
+XwiM5jjtqk6veR02FevCKHpZP6Od7Kqcfs6lg5LcQmGUOgpmW3Gg4UMjBYkgARsT2Nsnah1CJqa8
+cjvv8p5KEIhY0hVS8iMJnrb3PDeiFSeP4xSfct/6z/ebV4+QFl22bsm2zpAC6BpFz8+IJ/jAmQzT
+Vob4MAUeQPnwwzm3xz6yanLZx7BK5cfrTCa+hrarNQCboRjXPwiejF8WRCxpgRHH6yNs5QH/Z6o5
+e3tUP7uEpn2Ob+kcLsEMGb9DghkoDAgkHCOZeTy+7hgxt+/T94cLTa58gVtvEOnd0GuL7Vfd+IVd
+XgSard8RfR3OyZlf6M4aSGQA73sskQ==
+</ds:X509Certificate>
+</ds:X509Data>
+</ds:KeyInfo>
+</ds:Signature><md:EntityDescriptor entityID="https://access-check.edugain.org/simplesaml/saml2/idp/metadata.php">
+ <md:Extensions>
+ <mdrpi:RegistrationInfo registrationAuthority="https://federation.renater.fr/" registrationInstant="2015-01-30T15:32:58Z">
+ <mdrpi:RegistrationPolicy xml:lang="en">https://services.renater.fr/federation/en/metadata_registration_practice_statement</mdrpi:RegistrationPolicy>
+ </mdrpi:RegistrationInfo>
+ </md:Extensions>
+ <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+ <md:Extensions>
+ <shibmd:Scope regexp="false">access-check.edugain.org</shibmd:Scope>
+
+ <mdui:UIInfo>
+
+ <mdui:DisplayName xml:lang="en">eduGAIN Access Check</mdui:DisplayName>
+
+ <mdui:Logo height="16" width="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAABIAAAASABGyWs+AAAACXZwQWcAAAAQAAAAEABcxq3DAAABbUlEQVQoz52RPW4UQRCFv6ru6fnZNSxCJuVKpFwAicR3IUI+AakvhQhI7JU8M/1T3QQzsg1sAhWV3qtPVaonfPrCX9W+fpTP3y7otzceGJKJxCiujpVUkCv0+m3K967hixUfWgWSHwAPxG6UqtIFbEFHGoCqR721BX+wirO8LVGg+a6amEXXjqxhM5ZxNFPvX1Odocn5ZwB7HCn4hWI42wxD6KWUBVfIZxXd9J2zNhHFmro2Gx3QPKSReGCiV40ESM/A1NcxTDnn7HvLFViXBRZ6MKJnmwaktca/lFzO4fZG7n5e0D9cK4DjFI69yrGbwvhqP7KWp7kR99QrQAn3cW5N5/SQ1rivDgdUCUegFfkDkKF26TG7FijzZoS0sM7kM7D6gncvgG5d7cEdu6wN+v3R/cRwQgaAdnA2vACQvn9Tk6NNqjtwjis1Iyp4JOpafwvu3ZAYa3TV2fBjBnjfe8uLKErOZLk6fU//lcMvhuSrh7MzMwcAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMDctMjBUMTA6NTA6MTErMDI6MDCDfj0WAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE1LTA3LTIwVDEwOjUwOjExKzAyOjAw8iOFqgAAAABJRU5ErkJggg==</mdui:Logo>
+ <mdui:InformationURL xml:lang="fr">http://www.renater.fr</mdui:InformationURL>
+ <mdui:Description xml:lang="en">eduGAIN Access Check allows administrators of a Service Provider (SP) registered in eduGAIN to create test accounts with different profiles to validate the behaviour and test federated login. The test accounts can only be used to access own services.</mdui:Description>
+ <mdui:DisplayName xml:lang="fr">eduGAIN Access Check</mdui:DisplayName>
+ <mdui:Description xml:lang="fr">eduGAIN Access Check allows administrators of a Service Provider (SP) registered in eduGAIN to create test accounts with different profiles to validate the behaviour and test federated login. The test accounts can only be used to access own services.</mdui:Description>
+ </mdui:UIInfo>
+ </md:Extensions>
+ <md:KeyDescriptor use="signing">
+<ds:KeyInfo>
+
+ <ds:X509Data>
+ <ds:X509Certificate>
+ MIID2zCCAsOgAwIBAgIJAJpdV2MFitUqMA0GCSqGSIb3DQEBBQUAMIGDMQswCQYD
+VQQGEwJGUjEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MQ4wDAYDVQQKDAVHRUFOVDEd
+MBsGA1UEAwwUdGVzdC1pZHAuZWR1Z2Fpbi5vcmcxLjAsBgkqhkiG9w0BCQEWH3Rl
+c3RpZHBhY2NvdW50bWFuYWdlckBnZWFudC5uZXQwHhcNMTQxMjE4MTAxODU5WhcN
+MjQxMjE3MTAxODU5WjCBgzELMAkGA1UEBhMCRlIxFTATBgNVBAcMDERlZmF1bHQg
+Q2l0eTEOMAwGA1UECgwFR0VBTlQxHTAbBgNVBAMMFHRlc3QtaWRwLmVkdWdhaW4u
+b3JnMS4wLAYJKoZIhvcNAQkBFh90ZXN0aWRwYWNjb3VudG1hbmFnZXJAZ2VhbnQu
+bmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo48FFP0P/81e3WHb
+U91F/TYDZC/JypEqO2XQNH50baXpk2JrJFVFOWdgdK6qWHsLznuxngRsfOasAaVA
+Ob1Bf3g2xgPUd2htSLxds+o/Y24DOM6ZairxbWJk2rOvLhJFchlrcNWCpMtUCkfJ
+xmqGmeo93XAud5byj3wQ1NuH2o8rjTPAkMgQdr8D2b8EG1NYEH00AqRlXZTFCWGL
+KDEuZwyta6vgMQYT4K6UF/F+HWF2wzbmVgRTHguJ0rzNqz6t+9CtLkhyZO+/57Ro
+4U0ikshVWkUOENPKCnB1t+ebs/AsNozbIGA/HcdtwUwDgIowv/K0hdnLDC1vz6/S
+F3rnGQIDAQABo1AwTjAdBgNVHQ4EFgQUgWN9jmJxOEHYU5m8D0atl895HxowHwYD
+VR0jBBgwFoAUgWN9jmJxOEHYU5m8D0atl895HxowDAYDVR0TBAUwAwEB/zANBgkq
+hkiG9w0BAQUFAAOCAQEAXvlBHMaBK6m0PQNanTqGBRdRAFt8Xkr5texD5mPTmS/7
+nqnxlN0orqYWGCaARmQE+T77EB2a2n9g2s130pUXwJxcbUwIOdPKH6CMKEHT/512
+bndJXQ3DyhkuVSLtRFOdfleIhi8qUkNC9FWxM4jDHDTTQtNEHnCjFxlhxw+ri5QJ
+AVKpH9MkcuIkM6Jx+QhNwTDwCRIJffoDOH420yR5EWx/sQ4tjKQGiFOPv/WHFjXd
+LqHU+X8ErzxeNmUHHST6pHePWRCMtoPTdCPhEroJhou6NMHh8ylQOIVHt6gggc7r
+kUWMUybDUxPp49qMeNkdKqFPby2aW7ouKRoOXuxZhg==
+ </ds:X509Certificate>
+ </ds:X509Data>
+
+ </ds:KeyInfo>
+ </md:KeyDescriptor>
+
+
+
+
+
+
+ <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
+
+
+
+ <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://access-check.edugain.org/simplesaml/saml2/idp/SSOService.php"/>
+
+
+
+ </md:IDPSSODescriptor>
+
+
+
+ <md:Organization>
+
+ <md:OrganizationName xml:lang="en">eduGAIN Access Check</md:OrganizationName>
+ <md:OrganizationDisplayName xml:lang="en">eduGAIN Access Check</md:OrganizationDisplayName>
+ <md:OrganizationURL xml:lang="en">http://www.renater.fr</md:OrganizationURL>
+
+ </md:Organization>
+
+
+ <md:ContactPerson contactType="technical">
+ <md:EmailAddress>edugain-integration@geant.net</md:EmailAddress>
+ </md:ContactPerson>
+
+
+ </md:EntityDescriptor><md:EntityDescriptor entityID="https://aishib.agropolis.fr/idp/shibboleth">
+ <md:Extensions>
+ <mdrpi:RegistrationInfo registrationAuthority="https://federation.renater.fr/" registrationInstant="2013-06-06T11:49:20Z">
+ <mdrpi:RegistrationPolicy xml:lang="en">https://services.renater.fr/federation/en/metadata_registration_practice_statement</mdrpi:RegistrationPolicy>
+ </mdrpi:RegistrationInfo>
+ </md:Extensions>
+ <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0 urn:oasis:names:tc:SAML:2.0:protocol">
+ <md:Extensions>
+ <shibmd:Scope regexp="false">agropolis.fr</shibmd:Scope>
+
+ <mdui:UIInfo>
+
+ <mdui:DisplayName xml:lang="en">Agropolis International</mdui:DisplayName>
+
+ <mdui:Logo height="16" width="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAABIAAAASABGyWs+AAAACXZwQWcAAAAQAAAAEABcxq3DAAACPUlEQVQoz21SPW8TQRR8u7d3ju34Er6UCHACRqK4Bjk0oDSEjiYSBQVS8k9AkWgp+AuUASGEaCASkoPcgARVMF8iTnxJwMFJfL47397u3u6jsGVFwLzmFTN6ozdDEBEAAOD4kiRJsVgkhMA/YMfZzc3NVstnzGq1/KXlJfgf2Ii9XquVZ2YWbi5Io4MsbfPeZK4wRm0AOH6KICIivq2tV6/O5cYLHztbW9FvYTKuZaLk7PjpW+UrJXtspCHGmGazCQBnZ8svtj/0tbAINYhcy1DyThpRhHtztyedwkBDAWBnZ6dSqbzZ+7THu0IraTJplDSZ0JnQ2Y+482jj1cgSRUTHdrhW9fa3SKaxErFKIyUiKaIsjVQaKf5s631HxP1+X2vNwjB0J9zd+OgnD044Ra4lI5ZBw7XqyeRQREciaqe9xoE/K/OFfIENnqBM1uZBqlWBOYxQjYZnqieTThru8zCU3AASIAjIXNellGLePkjCUPICy1mEDAShSg7SqCsTBtQ7WfY3vlarVUYpdUsuAs5PXV7dfpe3HEYtjSbNZJwJoRUi3rl4fSrvfufctm0ySA0RD0V84+WDL1EbCCDCYIDApeKZ+uKK6faDIPA8jw7jIORUbry2eH+5Mu8ARTSAkCPW3QvX6osrE+B8bjQ8zxu2aACDaIwxxuzzsLbbePj88a8k0Mb4vr/2ei3LMoOIiGRU0iGGVuDp6pNz5fNSiKnpac/zgAABMuzSXwIggIjdbrdUKjHGYGQaAAD+AJBJecXxnEvWAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTA3LTIwVDEwOjQ4OjE1KzAyOjAwpWiGMQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNS0wNy0yMFQxMDo0ODoxNSswMjowMNQ1Po0AAAAASUVORK5CYII=</mdui:Logo>
+ <mdui:InformationURL xml:lang="fr">http://www.agropolis.fr</mdui:InformationURL>
+
+ <mdui:DisplayName xml:lang="fr">Agropolis International</mdui:DisplayName>
+
+ </mdui:UIInfo>
+ </md:Extensions>
+ <md:KeyDescriptor use="signing">
+<ds:KeyInfo>
+
+ <ds:X509Data>
+ <ds:X509Certificate>
+ MIIDNzCCAh+gAwIBAgIUYY3sGXwChkj2CRy6QFDvkdj2zlAwDQYJKoZIhvcNAQEF
+BQAwHjEcMBoGA1UEAxMTYWlzaGliLmFncm9wb2xpcy5mcjAeFw0xMzA1MTUxMzM3
+MTJaFw0zMzA1MTUxMzM3MTJaMB4xHDAaBgNVBAMTE2Fpc2hpYi5hZ3JvcG9saXMu
+ZnIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxrDy6lrhIBjcxv16n
+4UJ2cEMYPO4wSmfDwhO6feoSIEuIblYRHE2nQKirMokwD6seF4rbDHyxLXg/ColL
+VLv+0CJteIOZjSCgSN90WzQRrC1Ex5sJfPu6yPEXvW8H1906gEg6ok8rlCIHRGfE
+15pHK5eqxQS5f2n8c2t/Uk33/FBj79/hb3Cd7vE4mdlvReD3AFswC0lV4bPmj3Ka
+KUuMj9xwipwnfWCu6p2/ZJF4M3ADU5grXHJ2Vqmd8DWm5raaObKjYwJddbRBByI8
+bJJLIwAQQmX4Dh4hf1QKlf2oqWPWVQxLQp0erL1U8IWmj1RG8TTH9xOJl6kkEhYq
+Z2gfAgMBAAGjbTBrMEoGA1UdEQRDMEGCE2Fpc2hpYi5hZ3JvcG9saXMuZnKGKmh0
+dHBzOi8vYWlzaGliLmFncm9wb2xpcy5mci9pZHAvc2hpYmJvbGV0aDAdBgNVHQ4E
+FgQU9A7iQ8Qo+t2JCpKuOOV9YBoYs4MwDQYJKoZIhvcNAQEFBQADggEBAG0LOW6I
+F+M8n2NpzyQjfVCJCA6QhWjbXrfemiPJFZGZZb2dVmHof4yCpCUYgHOBoZaXPOlB
+nLYsUWvFZ6V2GELZpLHzHSSrYidieW07qQkh1DwcIYpvtZgLviOtT/tCEGsk925f
+DUoGdeIqpqt54WZcW9+TbKicvjg3JT4BFOQ17bFNwPW+YjTbvsWYxen+e0mRp4vM
+V0yMu2f3bccVhePASSZGL3yod3sJ1dPvlrJO9c35BekhtirolVjZqMQ0AYPVifua
+yIU0dWXsZkAOcBL9kZFbJcYRUIxMgvp8U2Zdv1+ZlwOyXnnWDOOh9wjuT7FAyObU
+ChvjHlgZHkvLwJI=
+ </ds:X509Certificate>
+ </ds:X509Data>
+
+ </ds:KeyInfo>
+ </md:KeyDescriptor>
+
+
+
+
+
+ <md:NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</md:NameIDFormat>
+ <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
+
+
+ <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://aishib.agropolis.fr/idp/profile/SAML2/POST/SSO"/>
+
+ <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://aishib.agropolis.fr/idp/profile/SAML2/Redirect/SSO"/>
+
+
+ <md:SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest" Location="https://aishib.agropolis.fr/idp/profile/Shibboleth/SSO"/>
+
+
+ </md:IDPSSODescriptor>
+
+
+
+ <md:Organization>
+
+ <md:OrganizationName xml:lang="en">Agropolis International</md:OrganizationName>
+ <md:OrganizationDisplayName xml:lang="en">Agropolis International</md:OrganizationDisplayName>
+ <md:OrganizationURL xml:lang="en">http://www.agropolis.fr</md:OrganizationURL>
+
+ </md:Organization>
+
+
+
+ <md:ContactPerson contactType="technical">
+ <md:SurName>Jean Cerda</md:SurName>
+ <md:EmailAddress>cerda@agropolis.fr</md:EmailAddress>
+ </md:ContactPerson>
+
+
+
+ <md:ContactPerson contactType="technical">
+ <md:SurName>Jean-Pierre Allano</md:SurName>
+ <md:EmailAddress>allano@agropolis.fr</md:EmailAddress>
+ </md:ContactPerson>
+
+
+
+
+ </md:EntityDescriptor><md:EntityDescriptor entityID="https://ambre.vetagro-sup.fr/idp/shibboleth">
+ <md:Extensions>
+ <mdrpi:RegistrationInfo registrationAuthority="https://federation.renater.fr/" registrationInstant="2013-01-14T16:11:53Z">
+ <mdrpi:RegistrationPolicy xml:lang="en">https://services.renater.fr/federation/en/metadata_registration_practice_statement</mdrpi:RegistrationPolicy>
+ </mdrpi:RegistrationInfo>
+ </md:Extensions>
+ <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0 urn:oasis:names:tc:SAML:2.0:protocol">
+ <md:Extensions>
+ <shibmd:Scope regexp="false">vetagro-sup.fr</shibmd:Scope>
+
+ <mdui:UIInfo>
+
+ <mdui:DisplayName xml:lang="en">Vetagro Sup</mdui:DisplayName>
+
+ <mdui:Logo height="16" width="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAABIAAAASABGyWs+AAAACXZwQWcAAAAQAAAAEABcxq3DAAAB/klEQVQ4y2NkQAP///+X+/jxI/PO7dujP3/7kf7v778ffDzciw2MDFaqq6t/Y2RkfIysngndAEZGxkezZ8/25xEQqVDTs5eRVrNU4RQUL7x9+7YtumasBjAwMDBISIgbcvBKct998p7h1fvfDKyc4nz//jF6yjAwSBNlwM+fP3/9/PmT4dbl4wysrCwMf//9Z/r7959UcFISD1EGPH107xgfD9sbTg52hrcvHjDw87Aw/Pz+7tLEefNuEmWAm6ffxvu3Ll4zMjZleHz/KsO7lw+f3bh+czs2tVgNsLS0fPfs4b3VnKy//wrycW84e2L/uobmtg0MpIKurt6N////Z8jPzxcnWTMDAwPD79//tbq7+9bhU8OMTfD//8cMP79L1X/8tjxMQYHXT0Uh88aBgyuvEW3Aly+8ntw8jLU3bx20cnH/xPz2jTyvk9OGpTt3NhIOxP//GRgkJGR8tPVeyElIvWA4dfIqg57Bff0nD+eHExULIb5dqkYmYgYm5r8YPH3vMzx//omBk+edpI4+vxVRBnz5zPqbi4vt7a9fv//9+cvAYG79jIGZmZWBg4NJiCgDdh4sfHD/7u9j714Z/BYWiGBQVkxjePNK8+erV5+uYDOABTMM/nP09y9Z8+OHgNjli9ahf//++8vGwrlAS0tp/v///7kYGRm/IasHAIVTy8DG/VpJAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTA3LTIwVDEwOjUwOjA5KzAyOjAwfDtz7wAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNS0wNy0yMFQxMDo1MDowOSswMjowMA1my1MAAAAASUVORK5CYII=</mdui:Logo>
+ <mdui:InformationURL xml:lang="fr">http://www.vetagro-sup.fr</mdui:InformationURL>
+
+ <mdui:DisplayName xml:lang="fr">Vetagro Sup</mdui:DisplayName>
+
+ </mdui:UIInfo>
+ </md:Extensions>
+ <md:KeyDescriptor use="signing">
+<ds:KeyInfo>
+
+ <ds:X509Data>
+ <ds:X509Certificate>
+ MIIDPDCCAiSgAwIBAgIVAL9PsuadPSIZcMHNxlK/oevezmzWMA0GCSqGSIb3DQEB
+BQUAMB8xHTAbBgNVBAMTFGFtYnJlLnZldGFncm8tc3VwLmZyMB4XDTEyMTEwODEw
+MTQwNFoXDTMyMTEwODEwMTQwNFowHzEdMBsGA1UEAxMUYW1icmUudmV0YWdyby1z
+dXAuZnIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCc/ptfpmkomwmT
+4RsID+1Ce1dX0eUjcLgSOZN8hVpHWLag2ERWkpmvB5aK7BAFcI5i//Gk80tAiasu
+JtlZhBnEw54aTJRGpyL2CVkHyl6SMRxprIi1Ji67IoGqEgUeGaheAxo+tG5e1WSc
+bIbldcSKdwvjAV+7HSB4C6NqLsAzJH25++yaRH2uf2LTD0TDzNR9Q2hVj/VyYWR+
+K3HWI1Snjn/i7aFfZZhYmBkwHuQOaPhwCM+khikg5XicMsxUhHCMi93UgHGIsdkr
+IEGj4xydBTUKsLaykeuFS8EgXbWwCLGkeX76w8xDoFIpnppU/yFd9v7Zg3EBfn4p
+kTW3GdIjAgMBAAGjbzBtMEwGA1UdEQRFMEOCFGFtYnJlLnZldGFncm8tc3VwLmZy
+hitodHRwczovL2FtYnJlLnZldGFncm8tc3VwLmZyL2lkcC9zaGliYm9sZXRoMB0G
+A1UdDgQWBBTPTqWkVHrHXFjmxMWkNt/sp2h5ozANBgkqhkiG9w0BAQUFAAOCAQEA
+FvXMtfBUmRZCzz8CjanGzr1TBUPmnkrKci5AtkseKw9YlfUmBXTHB01y697nYq6m
+RB6KhvfW212h9CF0IOEEjoadgDhXqGYhq8PnAOtT4Ty3XDy8SbRh8aQWfvnfSngv
+FdpHRiSpj5UXXuT5zTtkf59h58XKtEfCkMbUzvdOgUobJzpD0WISmQHPQnx+Neg6
+9j7oMRrDiZjS39Om8Imu9xvsnddDM3PlsDBIsvrr1o7K5iLkEdR1YYX0ZNDbiFuw
+QXXl2dwQPB8KrScPUvCe57slU2gFQvvIBzjQysxC6V6TPSuM3A/ee56lACuB3jKj
+oYkHQc5Gj/1rSMLmu9aLMg==
+ </ds:X509Certificate>
+ </ds:X509Data>
+
+ </ds:KeyInfo>
+ </md:KeyDescriptor>
+
+
+
+
+
+ <md:NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</md:NameIDFormat>
+ <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
+
+
+ <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ambre.vetagro-sup.fr/idp/profile/SAML2/POST/SSO"/>
+
+ <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://ambre.vetagro-sup.fr/idp/profile/SAML2/Redirect/SSO"/>
+
+
+ <md:SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest" Location="https://ambre.vetagro-sup.fr/idp/profile/Shibboleth/SSO"/>
+
+
+ </md:IDPSSODescriptor>
+
+
+
+ <md:Organization>
+
+ <md:OrganizationName xml:lang="en">Vetagro Sup</md:OrganizationName>
+ <md:OrganizationDisplayName xml:lang="en">Vetagro Sup</md:OrganizationDisplayName>
+ <md:OrganizationURL xml:lang="en">http://www.vetagro-sup.fr</md:OrganizationURL>
+
+ </md:Organization>
+
+
+
+ <md:ContactPerson contactType="technical">
+ <md:SurName>Nicolas Aulas</md:SurName>
+ <md:EmailAddress>nicolas.aulas@vetagro-sup.fr</md:EmailAddress>
+ </md:ContactPerson>
+
+
+
+
+
+
+ </md:EntityDescriptor><md:EntityDescriptor entityID="https://antimoine.insa-strasbourg.fr/idp/shibboleth">
+ <md:Extensions>
+ <mdrpi:RegistrationInfo registrationAuthority="https://federation.renater.fr/" registrationInstant="2014-02-11T08:44:08Z">
+ <mdrpi:RegistrationPolicy xml:lang="en">https://services.renater.fr/federation/en/metadata_registration_practice_statement</mdrpi:RegistrationPolicy>
+ </mdrpi:RegistrationInfo>
+ </md:Extensions>
+ <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0 urn:oasis:names:tc:SAML:2.0:protocol">
+ <md:Extensions>
+ <shibmd:Scope regexp="false">insa-strasbourg.fr</shibmd:Scope>
+
+ <mdui:UIInfo>
+
+ <mdui:DisplayName xml:lang="en">INSA Strasbourg</mdui:DisplayName>
+
+ <mdui:Logo height="16" width="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAq1BMVEXeAACEhoTWz87nQTn3oqXnIBjvgnvWlpT/5+fnEAjvZWOltrXnNDH3mpT/9/fnCAD/4+fvioz3x8bnVVL3qq3nKCnvkpTnHBjvcXP3sq3vgoT/7+/nFBDnODH3mpz////nDAj3z87vlpTveXv3vr3nAACMmpznRUL3pqXnJCHnFAj/+//nDADvjoz3y87vXVr3rq3nLCnvdXP3srXvhoT/8/fvODH3npz3lpQaie6kAAAACXBIWXMAAABIAAAASABGyWs+AAAACXZwQWcAAAAQAAAAEABcxq3DAAAAe0lEQVQY053OwQqCQBhF4VM22VSMMWrFGP2kkIpUQ1nz/m8Ws20TeFeHb3WRnzEBBjO8xRjZrURi0TFiy+UITjTpjIKaOndZuDSGxF0j9G1+e7CuLN1nE2F7VI28tAoqMMezP1ieFOOp7RPueBXKSqdnMq8Wg66nPP0LX4uIG4jEwJ0sAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTA3LTIwVDEwOjQ5OjA0KzAyOjAwIHfmJQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNS0wNy0yMFQxMDo0OTowNCswMjowMFEqXpkAAAAASUVORK5CYII=</mdui:Logo>
+ <mdui:InformationURL xml:lang="fr">http://www.insa-strasbourg.fr</mdui:InformationURL>
+
+ <mdui:DisplayName xml:lang="fr">INSA Strasbourg</mdui:DisplayName>
+
+ </mdui:UIInfo>
+ </md:Extensions>
+ <md:KeyDescriptor use="signing">
+<ds:KeyInfo>
+
+ <ds:X509Data>
+ <ds:X509Certificate>
+ MIIDUDCCAjigAwIBAgIVAIbX8U0uAqAhuXm1jWxiFpggtDTDMA0GCSqGSIb3DQEB
+CwUAMCQxIjAgBgNVBAMMGXNvdWZyZS5pbnNhLXN0cmFzYm91cmcuZnIwHhcNMTYw
+OTI3MTIzNjIxWhcNMzYwOTI3MTIzNjIxWjAkMSIwIAYDVQQDDBlzb3VmcmUuaW5z
+YS1zdHJhc2JvdXJnLmZyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
+sEE02sLRPAG5N81DMHEeGpI2MYF8yG/RiwH07cFIlLqgV80ewOmi0FWPYijxMb8A
+bmx0RwUMvJBVI6WMxtT9fykhID20k8rWOuYOzvaynzVqCktqVgKoEAxP1PFE9b0n
+iGKFprjjNl9ZD90GOUsxbAO7yXG9Q4WBa/eThl6XkUvNkSaZp5hcdWrgcAdsae3q
+iD/uxFa38NXNNeRLGyfxjd2K5qYSzbwBza9s9TOq1+pfw7sxu3/4BnfQ0RLGO6co
+4tH4Mufh0ome4cyYk4pvW5DOd1AznxDb8HpqvE0zwEsa69c/FDX0akgFZydmc77a
+j6USn6JKjjbO49yGtG1gVQIDAQABo3kwdzAdBgNVHQ4EFgQUjzMsxZYiokPYxper
+9zadM8J0F0kwVgYDVR0RBE8wTYIZc291ZnJlLmluc2Etc3RyYXNib3VyZy5mcoYw
+aHR0cHM6Ly9zb3VmcmUuaW5zYS1zdHJhc2JvdXJnLmZyL2lkcC9zaGliYm9sZXRo
+MA0GCSqGSIb3DQEBCwUAA4IBAQBFJKsiS3yfWuDB/E+iqQ0TuQJzL5+JIcloN0dw
+BFxW3VZOju15zeQ7LwRBg9S4SGLMPJU+LM1lvr68cK9brut/FjF51SETIXEeCWo3
+7+PIqgOCzraLNinmpU/OtN8ENalOPvpS6Jvbd23qB2t+IqOtZ+j15b0Yq4/on1E3
+W2F9CVzKpe4EwmmtCPQbe7U1wvhgFylEx797pex8veWs79YSYwqvcKMh79dzl8Fo
+/CgsO5pDrfKmc6SGMkByq75dZj+PqhZDzZ9EFTxbrXOTaS08VRN6a5Rh2iYRnGxq
+yZl66tPcaIm5PHgOEmu5X4lPkUoY+Jt36Gj3SGCbYt8qH5S0
+ </ds:X509Certificate>
+ </ds:X509Data>
+
+ </ds:KeyInfo>
+ </md:KeyDescriptor>
+
+
+
+ <md:KeyDescriptor use="signing">
+ <ds:KeyInfo>
+ <ds:X509Data>
+ <ds:X509Certificate>
+ MIIDXDCCAkSgAwIBAgIVAKI+qiqDCk9wTTqn7OVAoZrvj/CpMA0GCSqGSIb3DQEB
+BQUAMCcxJTAjBgNVBAMTHGFudGltb2luZS5pbnNhLXN0cmFzYm91cmcuZnIwHhcN
+MTQwMTEzMTAzOTU4WhcNMzQwMTEzMTAzOTU4WjAnMSUwIwYDVQQDExxhbnRpbW9p
+bmUuaW5zYS1zdHJhc2JvdXJnLmZyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEAtuM8lRjlVjjmrHq9VtguaOMQL+Wd99BiOs56kL3Mbctg1FwH69LYThCW
+6dOz6WJg/jU/naF7jEikXKc71xGyu7Ph7Iqa9S5hoXXAT8u/0q2nZDeTOraJqKe1
+FMF2RzXhEEMyQO3CiKNK9b+tbKoNZS7FQCixMZklWZPt4EcEKd6jyRq1WYX3dpnb
+r9I/aCdhtK/PGvGe5gKTDoTR2HKyWKJTc/obf8x/vlYIEwiaGgdlqI2KiBE0x48n
+zQdP6XVi3T8ZWbnkLmCfgJtP2C8PtEJuwDRAy0Z9N4DSwvxn5YCVYgBLSi0TLa10
+B/lUqqBezZrTrA9p9Lt8JtGXW5YGHwIDAQABo38wfTBcBgNVHREEVTBTghxhbnRp
+bW9pbmUuaW5zYS1zdHJhc2JvdXJnLmZyhjNodHRwczovL2FudGltb2luZS5pbnNh
+LXN0cmFzYm91cmcuZnIvaWRwL3NoaWJib2xldGgwHQYDVR0OBBYEFLFkjPZUc9JY
+qrWjldJ/iGGkKAt4MA0GCSqGSIb3DQEBBQUAA4IBAQBSk/wU1mRn4VF2ifmy261K
+DK7uX+t1H1hh8S38fKSFU7HoNXJTV3vQnmBOpYIGC1gtvmb+qjqpNtikU2zO84Gq
+Q0bXHxYF2d9RUP89mKaFxE5uNcXFmlOA3ChZY3pMT5zwAPI/T60tGrex7zci7OLn
+JDAQj/q4Yk9ejx6JTFggQSCCVh+oV/SDIMd2p5AY6H3mto3b6XCk7Lssa8a/D30k
+pEkZnhTKdN82eRyynuOR7UDU4tasV4d7Mi/j53f5ihnRcsvwh/pYodjoVYY8cEcZ
+JLnAXYF8coSwh8UN4D/0NHsvTuSOFQc85hGrqacMsvxiQiw9mv01AX5+A5YLEbVQ
+ </ds:X509Certificate>
+ </ds:X509Data>
+ </ds:KeyInfo>
+ </md:KeyDescriptor>
+
+
+
+ <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://antimoine.insa-strasbourg.fr/idp/profile/SAML2/Redirect/SLO"/>
+
+ <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://antimoine.insa-strasbourg.fr/idp/profile/SAML2/POST/SLO"/>
+
+ <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://antimoine.insa-strasbourg.fr/idp/profile/SAML2/SOAP/SLO"/>
+
+
+ <md:NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</md:NameIDFormat>
+ <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
+
+
+ <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://antimoine.insa-strasbourg.fr/idp/profile/SAML2/POST/SSO"/>
+
+ <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://antimoine.insa-strasbourg.fr/idp/profile/SAML2/Redirect/SSO"/>
+
+
+ <md:SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest" Location="https://antimoine.insa-strasbourg.fr/idp/profile/Shibboleth/SSO"/>
+
+
+ </md:IDPSSODescriptor>
+
+
+
+ <md:Organization>
+
+ <md:OrganizationName xml:lang="en">INSA Strasbourg</md:OrganizationName>
+ <md:OrganizationDisplayName xml:lang="en">INSA Strasbourg</md:OrganizationDisplayName>
+ <md:OrganizationURL xml:lang="en">http://www.insa-strasbourg.fr</md:OrganizationURL>
+
+ </md:Organization>
+
+
+
+ <md:ContactPerson contactType="technical">
+ <md:SurName>Lahsen BOUZID</md:SurName>
+ <md:EmailAddress>lahsen.bouzid@insa-strasbourg.fr</md:EmailAddress>
+ </md:ContactPerson>
+
+
+
+ <md:ContactPerson contactType="technical">
+ <md:SurName>Simon SCHERRER</md:SurName>
+ <md:EmailAddress>simon.scherrer@insa-strasbourg.fr</md:EmailAddress>
+ </md:ContactPerson>
+
+
+
+
+ </md:EntityDescriptor>
+
+<md:EntityDescriptor entityID="http://idp5/metadata">
+<md:IDPSSODescriptor
+ WantAuthnRequestsSigned="true"
+ protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+<md:KeyDescriptor use="signing">
+ <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+ <ds:X509Data><ds:X509Certificate>
+MIIDnjCCAoagAwIBAgIBATANBgkqhkiG9w0BAQUFADBUMQswCQYDVQQGEwJGUjEP
+MA0GA1UECBMGRnJhbmNlMQ4wDAYDVQQHEwVQYXJpczETMBEGA1UEChMKRW50cm91
+dmVydDEPMA0GA1UEAxMGRGFtaWVuMB4XDTA2MTAyNzA5MDc1NFoXDTExMTAyNjA5
+MDc1NFowVDELMAkGA1UEBhMCRlIxDzANBgNVBAgTBkZyYW5jZTEOMAwGA1UEBxMF
+UGFyaXMxEzARBgNVBAoTCkVudHJvdXZlcnQxDzANBgNVBAMTBkRhbWllbjCCASIw
+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM06Hx6VgHYR9wUf/tZVVTRkVWNq
+h9x+PvHA2qH4OYMuqGs4Af6lU2YsZvnrmRdcFWv0+UkdAgXhReCWAZgtB1pd/W9m
+6qDRldCCyysow6xPPKRz/pOTwRXm/fM0QGPeXzwzj34BXOIOuFu+n764vKn18d+u
+uVAEzk1576pxTp4pQPzJfdNLrLeQ8vyCshoFU+MYJtp1UA+h2JoO0Y8oGvywbUxH
+ioHN5PvnzObfAM4XaDQohmfxM9Uc7Wp4xKAc1nUq5hwBrHpjFMRSz6UCfMoJSGIi
++3xJMkNCjL0XEw5NKVc5jRKkzSkN5j8KTM/k1jPPsDHPRYzbWWhnNtd6JlkCAwEA
+AaN7MHkwCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0
+ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFP2WWMDShux3iF74+SoO1xf6qhqaMB8G
+A1UdIwQYMBaAFGjl6TRXbQDHzSlZu+e8VeBaZMB5MA0GCSqGSIb3DQEBBQUAA4IB
+AQAZ/imK7UMognXbs5RfSB8cMW6iNAI+JZqe9XWjvtmLfIIPbHM96o953SiFvrvQ
+BZjGmmPMK3UH29cjzDx1R/RQaYTyMrHyTePLh3BMd5mpJ/9eeJCSxPzE2ECqWRUa
+pkjukecFXqmRItwgTxSIUE9QkpzvuQRb268PwmgroE0mwtiREADnvTFkLkdiEMew
+fiYxZfJJLPBqwlkw/7f1SyzXoPXnz5QbNwDmrHelga6rKSprYKb3pueqaIe8j/AP
+NC1/bzp8cGOcJ88BD5+Ny6qgPVCrMLE5twQumJ12V3SvjGNtzFBvg2c/9S5OmVqR
+LlTxKnCrWAXftSm1rNtewTsF
+</ds:X509Certificate></ds:X509Data>
+ </ds:KeyInfo>
+ </md:KeyDescriptor>
+<md:KeyDescriptor use="encryption">
+ <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+ <ds:KeyValue>
+MIIDnjCCAoagAwIBAgIBATANBgkqhkiG9w0BAQUFADBUMQswCQYDVQQGEwJGUjEP
+MA0GA1UECBMGRnJhbmNlMQ4wDAYDVQQHEwVQYXJpczETMBEGA1UEChMKRW50cm91
+dmVydDEPMA0GA1UEAxMGRGFtaWVuMB4XDTA2MTAyNzA5MDc1NFoXDTExMTAyNjA5
+MDc1NFowVDELMAkGA1UEBhMCRlIxDzANBgNVBAgTBkZyYW5jZTEOMAwGA1UEBxMF
+UGFyaXMxEzARBgNVBAoTCkVudHJvdXZlcnQxDzANBgNVBAMTBkRhbWllbjCCASIw
+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM06Hx6VgHYR9wUf/tZVVTRkVWNq
+h9x+PvHA2qH4OYMuqGs4Af6lU2YsZvnrmRdcFWv0+UkdAgXhReCWAZgtB1pd/W9m
+6qDRldCCyysow6xPPKRz/pOTwRXm/fM0QGPeXzwzj34BXOIOuFu+n764vKn18d+u
+uVAEzk1576pxTp4pQPzJfdNLrLeQ8vyCshoFU+MYJtp1UA+h2JoO0Y8oGvywbUxH
+ioHN5PvnzObfAM4XaDQohmfxM9Uc7Wp4xKAc1nUq5hwBrHpjFMRSz6UCfMoJSGIi
++3xJMkNCjL0XEw5NKVc5jRKkzSkN5j8KTM/k1jPPsDHPRYzbWWhnNtd6JlkCAwEA
+AaN7MHkwCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0
+ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFP2WWMDShux3iF74+SoO1xf6qhqaMB8G
+A1UdIwQYMBaAFGjl6TRXbQDHzSlZu+e8VeBaZMB5MA0GCSqGSIb3DQEBBQUAA4IB
+AQAZ/imK7UMognXbs5RfSB8cMW6iNAI+JZqe9XWjvtmLfIIPbHM96o953SiFvrvQ
+BZjGmmPMK3UH29cjzDx1R/RQaYTyMrHyTePLh3BMd5mpJ/9eeJCSxPzE2ECqWRUa
+pkjukecFXqmRItwgTxSIUE9QkpzvuQRb268PwmgroE0mwtiREADnvTFkLkdiEMew
+fiYxZfJJLPBqwlkw/7f1SyzXoPXnz5QbNwDmrHelga6rKSprYKb3pueqaIe8j/AP
+NC1/bzp8cGOcJ88BD5+Ny6qgPVCrMLE5twQumJ12V3SvjGNtzFBvg2c/9S5OmVqR
+LlTxKnCrWAXftSm1rNtewTsF
+</ds:KeyValue>
+ </ds:KeyInfo>
+ </md:KeyDescriptor>
+
+ <md:ArtifactResolutionService isDefault="true" index="0"
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+ Location="http://idp5/artifact" />
+ <md:SingleLogoutService
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+ Location="http://idp5/singleLogoutSOAP" />
+ <md:SingleLogoutService
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ Location="http://idp5/singleLogout"
+ ResponseLocation="http://idp5/singleLogoutReturn" />
+ <md:ManageNameIDService
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+ Location="http://idp5/manageNameIdSOAP" />
+ <md:ManageNameIDService
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ Location="http://idp5/manageNameId"
+ ResponseLocation="http://idp5/manageNameIdReturn" />
+ <md:SingleSignOnService
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ Location="http://idp5/singleSignOn" />
+ <md:SingleSignOnService
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+ Location="http://idp5/singleSignOnSOAP" />
+</md:IDPSSODescriptor>
+<md:AuthnAuthorityDescriptor
+ protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+ <md:AuthnQueryService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="http://idp6/authnQueryService"/>
+ <md:AssertionIDRequestService Binding="urn:oasis:names:tc:SAML:2.0:bindings:URI" Location="http://idp6/authnAuthAssertionIDRequestService"/>
+ <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
+</md:AuthnAuthorityDescriptor>
+<md:PDPDescriptor
+ protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+ <md:AuthzService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="http://idp6/authzService"/>
+ <md:AssertionIDRequestService Binding="urn:oasis:names:tc:SAML:2.0:bindings:URI" Location="http://idp6/PDPAuthAssertionIDRequestService"/>
+ <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:kerberos</md:NameIDFormat>
+</md:PDPDescriptor>
+<md:AttributeAuthorityDescriptor
+ protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+ <md:AttributeService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="http://idp6/attributeService"/>
+ <md:AssertionIDRequestService Binding="urn:oasis:names:tc:SAML:2.0:bindings:URI" Location="http://idp6/AttributeAuthAssertionIDRequestService"/>
+ <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName</md:NameIDFormat>
+</md:AttributeAuthorityDescriptor>
+<md:Organization>
+ <md:OrganizationName xml:lang="en">Entr'ouvert</md:OrganizationName>
+</md:Organization>
+
+</md:EntityDescriptor>
+ </md:EntitiesDescriptor>
diff --git a/tests/test_default_adapter.py b/tests/test_default_adapter.py
index 8189a39..e2a6088 100644
--- a/tests/test_default_adapter.py
+++ b/tests/test_default_adapter.py
@@ -89,7 +89,7 @@ def test_provision_user_attributes(settings, django_user_model, caplog):
assert user.email == 'test@example.net'
assert user.is_superuser is False
assert user.is_staff is False
- assert len(caplog.records) == 4
+ assert len(caplog.records) == 6
assert 'created new user' in caplog.text
assert 'set field first_name' in caplog.text
assert 'set field last_name' in caplog.text
@@ -102,7 +102,7 @@ def test_provision_user_groups(settings, django_user_model, caplog):
user = SAMLBackend().authenticate(saml_attributes=saml_attributes)
assert user.groups.count() == 3
assert set(user.groups.values_list('name', flat=True)) == set(saml_attributes['group'])
- assert len(caplog.records) == 4
+ assert len(caplog.records) == 6
assert 'created new user' in caplog.text
assert 'adding group GroupA' in caplog.text
assert 'adding group GroupB' in caplog.text
@@ -112,7 +112,7 @@ def test_provision_user_groups(settings, django_user_model, caplog):
user = SAMLBackend().authenticate(saml_attributes=saml_attributes2)
assert user.groups.count() == 2
assert set(user.groups.values_list('name', flat=True)) == set(saml_attributes2['group'])
- assert len(caplog.records) == 5
+ assert len(caplog.records) == 9
assert 'removing group GroupA' in caplog.records[-1].message
@@ -142,7 +142,7 @@ def test_provision_absent_attribute(settings, django_user_model, caplog):
del local_saml_attributes['email']
user = SAMLBackend().authenticate(saml_attributes=local_saml_attributes)
assert not user.email
- assert len(caplog.records) == 4
+ assert len(caplog.records) == 6
assert 'created new user' in caplog.text
assert re.search(r'invalid reference.*email', caplog.text)
assert 'set field first_name' in caplog.text
@@ -160,7 +160,7 @@ def test_provision_long_attribute(settings, django_user_model, caplog):
local_saml_attributes['first_name'] = [('y' * 32)]
user = SAMLBackend().authenticate(saml_attributes=local_saml_attributes)
assert user.first_name == 'y' * 30
- assert len(caplog.records) == 4
+ assert len(caplog.records) == 6
assert 'created new user' in caplog.text
assert 'set field first_name' in caplog.text
assert 'to value %r ' % (u'y' * 30) in caplog.text
diff --git a/tests/test_federations_utils.py b/tests/test_federations_utils.py
new file mode 100644
index 0000000..5a101e0
--- /dev/null
+++ b/tests/test_federations_utils.py
@@ -0,0 +1,39 @@
+import os
+import time
+
+from django.utils.text import slugify
+from httmock import HTTMock
+
+from mellon.federation_utils import get_federation_from_url, truncate_unique
+from utils import sample_federation_response
+
+
+def test_mock_fedmd_caching():
+ url = u'https://dummy.mdserver/metadata.xml'
+ filepath = os.path.join('metadata-cache/', truncate_unique(slugify(url)))
+
+ if os.path.isfile(filepath):
+ os.remove(filepath)
+
+ with HTTMock(sample_federation_response):
+ tmp = get_federation_from_url(url)
+
+ assert tmp == filepath
+
+ st = os.stat(filepath)
+
+ assert os.path.isfile(filepath)
+ assert st.st_mtime < time.time() + 3600
+
+ with HTTMock(sample_federation_response):
+ get_federation_from_url(url)
+ stnew = os.stat(filepath)
+
+ assert stnew.st_ctime == st.st_ctime
+ assert stnew.st_mtime == st.st_mtime
+
+ storig = os.stat(os.path.join('tests', 'federation-sample.xml'))
+
+ assert storig.st_size == st.st_size
+
+ os.remove(filepath)
diff --git a/tests/test_sso_slo.py b/tests/test_sso_slo.py
index 795e53d..a5438d5 100644
--- a/tests/test_sso_slo.py
+++ b/tests/test_sso_slo.py
@@ -4,7 +4,8 @@ from pytest import fixture
from django.core.urlresolvers import reverse
-from mellon.utils import create_metadata
+from mellon.utils import create_metadata, create_server
+from django.utils.http import urlencode
from httmock import all_requests, HTTMock, response as mock_response
@@ -17,6 +18,11 @@ def idp_metadata():
@fixture
+def federation_metadata():
+ return './tests/federation-sample.xml'
+
+
+@fixture
def idp_private_key():
return open('tests/idp-private-key.pem').read()
@@ -44,11 +50,29 @@ def sp_settings(private_settings, idp_metadata, sp_private_key, public_key):
@fixture
+def federated_sp_settings(private_settings, federation_metadata, sp_private_key, public_key):
+ private_settings.MELLON_FEDERATIONS = [{
+ 'FEDERATION': federation_metadata,
+ }]
+ private_settings.MELLON_PUBLIC_KEYS = [public_key]
+ private_settings.MELLON_PRIVATE_KEYS = [sp_private_key]
+ private_settings.MELLON_NAME_ID_POLICY_FORMAT = lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT
+ private_settings.LOGIN_REDIRECT_URL = '/'
+ return private_settings
+
+
+@fixture
def sp_metadata(sp_settings, rf):
request = rf.get('/')
return create_metadata(request)
+@fixture
+def federated_sp_metadata(federated_sp_settings, rf):
+ request = rf.get('/')
+ return create_metadata(request)
+
+
class MockIdp(object):
def __init__(self, idp_metadata, private_key, sp_metadata):
self.server = server = lasso.Server.newFromBuffers(idp_metadata, private_key)
@@ -103,6 +127,11 @@ def idp(sp_settings, idp_metadata, idp_private_key, sp_metadata):
return MockIdp(idp_metadata, idp_private_key, sp_metadata)
+@fixture
+def federated_idp(federated_sp_settings, idp_metadata, idp_private_key, federated_sp_metadata):
+ return MockIdp(idp_metadata, idp_private_key, federated_sp_metadata)
+
+
def test_sso_slo(db, app, idp, caplog, sp_settings):
response = app.get(reverse('mellon_login'))
url, body = idp.process_authn_request_redirect(response['Location'])
@@ -171,3 +200,60 @@ def test_sso_artifact(db, app, caplog, sp_settings, idp_metadata, idp_private_ke
assert 'created new user' in caplog.text
assert 'logged in using SAML' in caplog.text
assert response['Location'].endswith(sp_settings.LOGIN_REDIRECT_URL)
+
+
+def test_login_federation(db, app, federated_idp, caplog, federated_sp_settings):
+ qs = urlencode({
+ 'entityID': 'http://idp5/metadata',
+ })
+ response = app.get('/login/?' + qs)
+ url, body = federated_idp.process_authn_request_redirect(response['Location'])
+ assert url.endswith(reverse('mellon_login'))
+ response = app.post(reverse('mellon_login'), params={'SAMLResponse': body})
+ assert 'created new user' in caplog.text
+ assert 'logged in using SAML' in caplog.text
+ assert response['Location'].endswith(federated_sp_settings.LOGIN_REDIRECT_URL)
+
+
+def test_sso_artifact_federation(db, app, caplog, federated_sp_settings, idp_metadata, idp_private_key, rf):
+ qs = urlencode({
+ 'entityID': 'http://idp5/metadata',
+ })
+ federated_sp_settings.MELLON_DEFAULT_ASSERTION_CONSUMER_BINDING = 'artifact'
+ request = rf.get('/')
+ federated_sp_metadata = create_metadata(request)
+ idp = MockIdp(idp_metadata, idp_private_key, federated_sp_metadata)
+ response = app.get('/login/?' + qs)
+ url, body = idp.process_authn_request_redirect(response['Location'])
+ assert body is None
+ assert reverse('mellon_login') in url
+ assert 'SAMLart' in url
+ acs_artifact_url = url.split('testserver', 1)[1]
+ with HTTMock(idp.mock_artifact_resolver()):
+ response = app.get(acs_artifact_url)
+ assert 'created new user' in caplog.text
+ assert 'logged in using SAML' in caplog.text
+ assert response['Location'].endswith(federated_sp_settings.LOGIN_REDIRECT_URL)
+ # force delog
+ app.session.flush()
+ assert 'dead artifact' not in caplog.text
+ with HTTMock(idp.mock_artifact_resolver()):
+ response = app.get(acs_artifact_url)
+ # verify retry login was asked
+ assert 'dead artifact' in caplog.text
+ assert response.status_code == 302
+ assert reverse('mellon_login') in url
+ response = response.follow()
+ url, body = idp.process_authn_request_redirect(response['Location'])
+ reset_caplog(caplog)
+ # verify caplog has been cleaned
+ assert 'created new user' not in caplog.text
+ assert body is None
+ assert reverse('mellon_login') in url
+ assert 'SAMLart' in url
+ acs_artifact_url = url.split('testserver', 1)[1]
+ with HTTMock(idp.mock_artifact_resolver()):
+ response = app.get(acs_artifact_url)
+ assert 'created new user' in caplog.text
+ assert 'logged in using SAML' in caplog.text
+ assert response['Location'].endswith(federated_sp_settings.LOGIN_REDIRECT_URL)
diff --git a/tests/test_utils.py b/tests/test_utils.py
index ca73ba6..80ab9e8 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,3 +1,4 @@
+import os
import re
import datetime
@@ -6,11 +7,13 @@ import lasso
import requests.exceptions
from httmock import HTTMock
-from mellon.utils import create_server, create_metadata, iso8601_to_datetime, flatten_datetime
+from mellon.utils import create_server, create_metadata, iso8601_to_datetime, \
+ flatten_datetime, get_idp
import mellon.utils
from xml_utils import assert_xml_constraints
-from utils import error_500, metadata_response
+from utils import error_500, metadata_response, sample_federation_response, \
+ html_response, dummy_md_response
def test_create_server_connection_error(mocker, rf, private_settings, caplog):
@@ -39,6 +42,48 @@ def test_create_server_internal_server_error(mocker, rf, private_settings, caplo
assert 'failed with error' in caplog.text
+def test_load_federation_file(mocker, rf, private_settings, caplog, tmpdir):
+ private_settings.MELLON_FEDERATIONS = [
+ {'FEDERATION': 'tests/federation-sample.xml'},
+ ]
+ request = rf.get('/')
+ assert 'failed with error' not in caplog.text
+ with HTTMock(html_response):
+ server = create_server(request)
+ assert len(server.providers) == 5
+
+
+def test_load_federation_url(mocker, rf, private_settings, caplog, tmpdir):
+ private_settings.MELLON_FEDERATIONS = [
+ {'FEDERATION': 'https://dummy.server/metadata.xml'},
+ ]
+ request = rf.get('/')
+ assert 'failed with error' not in caplog.text
+ with HTTMock(dummy_md_response):
+ server = create_server(request)
+ assert len(server.providers) == 3
+
+
+def test_federation_parameters(mocker, rf, private_settings, caplog, tmpdir):
+ private_settings.MELLON_FEDERATIONS = [{
+ 'FEDERATION': 'tests/federation-sample.xml',
+ 'VERIFY_SSL_CERTIFICATE': False,
+ 'ERROR_REDIRECT_AFTER_TIMEOUT': 150,
+ 'PROVISION': True
+ }]
+ request = rf.get('/')
+ assert 'failed with error' not in caplog.text
+ with HTTMock(html_response):
+ server = create_server(request)
+ assert len(server.providers) == 5
+ for entity_id in server.providers.keys():
+ idp = get_idp(entity_id)
+ assert idp
+ assert idp['VERIFY_SSL_CERTIFICATE'] is False
+ assert idp['ERROR_REDIRECT_AFTER_TIMEOUT'] == 150
+ assert idp['PROVISION'] is True
+
+
def test_create_server_invalid_metadata(mocker, rf, private_settings, caplog):
private_settings.MELLON_IDENTITY_PROVIDERS = [
{
@@ -49,8 +94,8 @@ def test_create_server_invalid_metadata(mocker, rf, private_settings, caplog):
assert not 'failed with error' in caplog.text
with HTTMock(error_500):
create_server(request)
- assert len(caplog.records) == 1
- assert re.search('METADATA.*is invalid', caplog.text)
+ assert len(caplog.records) == 5
+ assert re.search('METADATA.*is invalid|bad metadata in idp', caplog.text)
def test_create_server_invalid_metadata_file(mocker, rf, private_settings, caplog):
@@ -70,13 +115,11 @@ def test_create_server_invalid_metadata_file(mocker, rf, private_settings, caplo
def test_create_server_good_metadata_file(mocker, rf, private_settings, caplog):
private_settings.MELLON_IDENTITY_PROVIDERS = [
{
- 'METADATA': '/xxx',
+ 'METADATA': './tests/metadata.xml',
}
]
request = rf.get('/')
- with mock.patch(
- 'mellon.adapters.file', mock.mock_open(read_data=file('tests/metadata.xml').read()),
- create=True):
+ with HTTMock(html_response):
server = create_server(request)
assert 'ERROR' not in caplog.text
assert len(server.providers) == 1
diff --git a/tests/utils.py b/tests/utils.py
index 366fe7b..e88e21c 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -16,6 +16,16 @@ def metadata_response(url, request):
return response(200, content=file('tests/metadata.xml').read())
+@all_requests
+def dummy_md_response(url, request):
+ return response(200, content=file('tests/dummy_md.xml').read())
+
+
+@all_requests
+def sample_federation_response(url, request):
+ return response(200, content=file('tests/federation-sample.xml').read())
+
+
def reset_caplog(cap):
cap.handler.stream.truncate(0)
cap.handler.records = []