diff --git a/.travis.yml b/.travis.yml index 0ddaacc3..2d923162 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ script: - 'coverage run --source=src/onelogin/saml2 --rcfile=tests/coverage.rc setup.py test' - 'coverage report -m --rcfile=tests/coverage.rc' # - 'pylint src/onelogin/saml2 --rcfile=tests/pylint.rc' - - 'pep8 tests/src/OneLogin/saml2_tests/*.py demo-flask/*.py demo-django/*.py src/onelogin/saml2/*.py --config=tests/pep8.rc' + - 'pycodestyle tests/src/OneLogin/saml2_tests/*.py demo-flask/*.py demo-django/*.py src/onelogin/saml2/*.py --config=tests/pep8.rc' - 'pyflakes src/onelogin/saml2 demo-django demo-flask tests/src/OneLogin/saml2_tests' after_success: 'coveralls' diff --git a/LICENSE b/LICENSE index 5504f2bb..1c8f814e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,24 +1,23 @@ -Copyright (c) 2011-2014, OneLogin, Inc. -All rights reserved. +Copyright (c) 2010-2018 OneLogin, Inc. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of the nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL ONELOGIN, INC. BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 7a10e277..0f988a1c 100644 --- a/README.md +++ b/README.md @@ -2,22 +2,44 @@ [![Build Status](https://api.travis-ci.org/onelogin/python-saml.png?branch=master)](http://travis-ci.org/onelogin/python-saml) [![Coverage Status](https://coveralls.io/repos/onelogin/python-saml/badge.png)](https://coveralls.io/r/onelogin/python-saml) -[![PyPi Version](https://pypip.in/v/python-saml/badge.png)](https://pypi.python.org/pypi/python-saml) -![PyPi Downloads](https://pypip.in/d/python-saml/badge.png) +[![PyPi Version](https://img.shields.io/pypi/v/python-saml.svg)](https://pypi.python.org/pypi/python-saml) +![Python versions](https://img.shields.io/pypi/pyversions/python-saml.svg) Add SAML support to your Python software using this library. -Forget those complicated libraries and use that open source library provided +Forget those complicated libraries and use the open source library provided and supported by OneLogin Inc. +This version supports Python2. There is a separate version that supports +Python3: [python3-saml](https://github.com/onelogin/python3-saml). + +#### Warning #### + +Version 2.7.0 sets strict mode active by default + +Update ``python-saml`` to ``2.5.0``, this version includes security improvements for preventing XEE and Xpath Injections. + +Update ``python-saml`` to ``2.4.0``, this version includes a fix for the [CVE-2017-11427](https://www.cvedetails.com/cve/CVE-2017-11427/) vulnerability. + +This version also changes how the calculate fingerprint method works, and will expect as input a formatted X.509 certificate + +Update ``python-saml`` to ``2.2.3``, this version replaces some etree.tostring calls, that were introduced recently, by the sanitized call provided by ``defusedxml`` + +Update ``python-saml`` to ``2.2.0``, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks. [CVE-2016-1000252](https://github.com/distributedweaknessfiling/DWF-Database-Artifacts/blob/master/DWF/2016/1000252/CVE-2016-1000252.json) + +``python-saml`` < ``v2.2.0`` is vulnerable and allows signature wrapping! + +#### Security Guidelines #### + +If you believe you have discovered a security vulnerability in this toolkit, please report it at https://www.onelogin.com/security with a description. We follow responsible disclosure guidelines, and will work with you to quickly find a resolution. Why add SAML support to my software? ------------------------------------ SAML is an XML-based standard for web browser single sign-on and is defined by -the OASIS Security Services Technical Committee. The standard has been around +the OASIS Security Services Technical Committee. The standard has been around since 2002, but lately it is becoming popular due its advantages: - * **Usability** - One-click access from portals or intranets, deep linking, + * **Usability** - One-click access from portals or intranets, deep linking, password elimination and automatically renewing sessions make life easier for the user. * **Security** - Based on strong digital signatures for authentication and @@ -30,85 +52,98 @@ since 2002, but lately it is becoming popular due its advantages: * **IT Friendly** - SAML simplifies life for IT because it centralizes authentication, provides greater visibility and makes directory integration easier. - * **Opportunity** - B2B cloud vendor should support SAML to facilitate the + * **Opportunity** - B2B cloud vendor should support SAML to facilitate the integration of their product. -General description +General Description ------------------- -OneLogin's SAML Python toolkit let you build a SP (Service Provider) over -your Python application and connect it to any IdP (Identity Provider). +OneLogin's SAML Python toolkit lets you turn your Python application into a SP +(Service Provider) that can be connected to an IdP (Identity Provider). -Supports: +**Supports:** * SSO and SLO (SP-Initiated and IdP-Initiated). * Assertion and nameId encryption. - * Assertion signature. - * Message signature: AuthNRequest, LogoutRequest, LogoutResponses. + * Assertion signatures. + * Message signatures: ``AuthNRequest``, ``LogoutRequest``, ``LogoutResponses``. * Enable an Assertion Consumer Service endpoint. * Enable a Single Logout Service endpoint. * Publish the SP metadata (which can be signed). -Key features: +**Key features:** * **saml2int** - Implements the SAML 2.0 Web Browser SSO Profile. * **Session-less** - Forget those common conflicts between the SP and the final app, the toolkit delegate session in the final app. - * **Easy to use** - Programmer will be allowed to code high-level and + * **Easy to use** - Programmer will be allowed to code high-level and low-level programming, 2 easy to use APIs are available. * **Tested** - Thoroughly tested. - * **Popular** - OneLogin's customers use it. Add easy support to your django/flask/bottle web projects. + * **Popular** - OneLogin's customers use it. Add easy support to your Django/Flask/Bottle/Pyramid web projects. Installation ------------ -### Dependences ### +### Dependencies ### * python 2.7 - * [M2Crypto](https://pypi.python.org/pypi/M2Crypto) A Python crypto and SSL toolkit (depends on openssl, swig) * [dm.xmlsec.binding](https://pypi.python.org/pypi/dm.xmlsec.binding) Cython/lxml based binding for the XML security library (depends on python-dev libxml2-dev libxmlsec1-dev) * [isodate](https://pypi.python.org/pypi/isodate) An ISO 8601 date/time/duration parser and formater * [defusedxml](https://pypi.python.org/pypi/defusedxml) XML bomb protection for Python stdlib modules + Review the setup.py file to know the version of the library that python-saml is using +### OSX Dependencies ### + + * python 2.7 + * libxmlsec1 + +```sh +# using brew +$ brew install libxmlsec1 +``` + + ### Code ### -#### Option 1. Download from github #### +#### Option 1. Download from Github #### -The toolkit is hosted on github. You can download it from: +The toolkit is hosted on Github. You can download it from: * Lastest release: https://github.com/onelogin/python-saml/releases/latest * Master repo: https://github.com/onelogin/python-saml/tree/master -Copy the core of the library (src/onelogin/saml2 folder) and merge the setup.py inside the python application. (each application has its structure so take your time to locate the Python SAML toolkit in the best place). +Copy the core of the library ``(src/onelogin/saml2 folder)`` and merge the setup.py inside the Python application. (Each application has its structure so take your time to locate the Python SAML toolkit in the best place). #### Option 2. Download from pypi #### -The toolkit is hosted in pypi, you can find the python-saml package at https://pypi.python.org/pypi/python-saml +The toolkit is hosted in pypi, you can find the ``python-saml`` package at https://pypi.python.org/pypi/python-saml You can install it executing: ``` - pip install python-saml +$ pip install python-saml ``` If you want to know how a project can handle python packages review this [guide](https://packaging.python.org/en/latest/tutorial.html) and review this [sampleproject](https://github.com/pypa/sampleproject) -Security warning +Security Warning ---------------- -In production, the **strict** parameter MUST be set as **"true"**. Otherwise +In production, the **strict** parameter MUST be set as **"true"**. Otherwise your environment is not secure and will be exposed to attacks. -Getting started +In production also we highly recommend to register on the settings the IdP certificate instead of using the fingerprint method. The fingerprint, is a hash, so at the end is open to a collision attack that can end on a signature validation bypass. Other SAML toolkits deprecated that mechanism, we maintain it for compatibility and also to be used on test environment. + +Getting Started --------------- ### Knowing the toolkit ### -The new OneLogin SAML Toolkit contains different folders (certs, lib, demo-django, demo-flask, demo-bottle and tests) and some files. +The new OneLogin SAML Toolkit contains different folders (``cert``, ``lib``, ``demo-django``, ``demo-flask``, ``demo-bottle`` and ``tests``) and some files. Let's start describing them: @@ -119,20 +154,23 @@ the classes and methods that are described in a later section. #### demo-django #### -This folder contains a Django project that will be used as demo to show how to add SAML support to the Django Framework. 'demo' is the main folder of the django project (with its settings.py, views.py, urls.py), 'templates' is the django templates of the project and 'saml' is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json). +This folder contains a Django project that will be used as demo to show how to add SAML support to the Django Framework. **demo** is the main folder of the Django project (with its ``settings.py``, ``views.py``, ``urls.py``), **templates** is the Django templates of the project and **saml** is a folder that contains the 'certs' folder that could be used to store the X.509 public and private key, and the SAML toolkit settings (``settings.json`` and ``advanced_settings.json``). -***Notice about certs*** +*** Notice about certs *** -SAML requires a x.509 cert to sign and encrypt elements like NameID, Message, Assertion, Metadata. +SAML requires a x.509 cert to sign and encrypt elements like ``NameID``, ``Message``, ``Assertion``, ``Metadata``. -If our environment requires sign or encrypt support, the certs folder may contain the x509 cert and the private key that the SP will use: +If our environment requires sign or encrypt support, the certs folder may contain the X.509 cert and the private key that the SP will use: * sp.crt The public cert of the SP -* sp.key The privake key of the SP +* sp.key The private key of the SP -Or also we can provide those data in the setting file at the 'x509cert' and the privateKey' json parameters of the 'sp' element. +Or also we can provide those data in the setting file at the 'x509cert' and the privateKey' JSON parameters of the ``sp`` element. -Sometimes we could need a signature on the metadata published by the SP, in this case we could use the x.509 cert previously mentioned or use a new x.509 cert: metadata.crt and metadata.key. +Sometimes we could need a signature on the metadata published by the SP, in this case we could use the x.509 cert previously mentioned or use a new x.509 cert: ``metadata.crt`` and ``metadata.key``. + +Use ``sp_new.crt`` if you are in a key rollover process and you want to +publish that X.509 certificate on Service Provider metadata. If you want to create self-signed certs, you can do it at the https://www.samltool.com/self_signed_certs.php service, or using the command: @@ -142,12 +180,18 @@ openssl req -new -x509 -days 3652 -nodes -out sp.crt -keyout saml.key #### demo-bottle #### -This folder contains a Bottle project that will be used as demo to show how to add SAML support to the Bottle Framework. index.py contains all the logic of the demo project, 'templates' is the Bottle templates of the project and 'saml' is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json). +This folder contains a Bottle project that will be used as demo to show how to add SAML support to the Bottle Framework. ``index.py`` contains all the logic of the demo project, **templates** is the Bottle templates of the project and **saml** is a folder that contains the 'certs' folder that could be used to store the X.509 public and private key, and the SAML toolkit settings (``settings.json`` and ``advanced_settings.json``). #### demo-flask #### -This folder contains a Flask project that will be used as demo to show how to add SAML support to the Flask Framework. 'index.py' is the main flask file that has all the code, this file uses the templates stored at the 'templates' folder. In the 'saml' folder we found the 'certs' folder to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json). +This folder contains a Flask project that will be used as demo to show how to add SAML support to the Flask Framework. ``index.py`` is the main Flask file that has all the code, this file uses the templates stored at the 'templates' folder. In the 'saml' folder we found the 'certs' folder to store the X.509 public and private key, and the SAML toolkit settings (``settings.json`` and ``advanced_settings.json``). + + +#### demo_pyramid #### + +This folder contains a Pyramid project that will be used as demo to show how to add SAML support to the [Pyramid Web Framework](http://docs.pylonsproject.org/projects/pyramid/en/latest/). ``\_\_init__.py`` is the main file that configures the app and its routes, ``views.py`` is where all the logic and SAML handling takes place, and the templates are stored in the **templates** folder. The **saml** folder is the same as in the other two demos. + #### setup.py #### @@ -158,7 +202,13 @@ Read more at https://pythonhosted.org/an_example_pypi_project/setuptools.html Contains the unit test of the toolkit. -In order to execute the test you only need to load the virtualenv with the toolkit installed on it and execute: +In order to execute the test you need to load the ``virtualenv`` with the toolkit installed on it and execute: +``` +pip install -e ".[test]" +``` +that will install dependences that the test requires. + +and later execute: ``` python setup.py test ``` @@ -167,9 +217,9 @@ The previous line will run the tests for the whole toolkit. You can also run the python setup.py test --test-suite tests.src.OneLogin.saml2_tests.auth_test.OneLogin_Saml2_Auth_Test ``` -With the --test-suite parameter you can specify the module to test. You'll find all the module available and their class names at tests/src/OneLogin/saml2_tests/ +With the ``--test-suite`` parameter you can specify the module to test. You'll find all the module available and their class names at ``tests/src/OneLogin/saml2_tests/`` -### How it works ### +### How it Works ### #### Settings #### @@ -177,17 +227,17 @@ First of all we need to configure the toolkit. The SP's info, the IdP's info, an There are two ways to provide the settings information: -* Use a settings.json file that we should locate in any folder, but indicates its path with the 'custom_base_path' parameter. +* Use a ``settings.json`` file that we should locate in any folder, but indicates its path with the ``custom_base_path`` parameter. -* Use a json object with the setting data and provide it directly to the constructor of the class (if your toolkit integation requires certs, remember to provide the 'custom_base_path' as part of the settings or as a parameter in the constructor. +* Use a JSON object with the setting data and provide it directly to the constructor of the class (if your toolkit integation requires certs, remember to provide the ``custom_base_path`` as part of the settings or as a parameter in the constructor. -In the demo-django, demo-flask and demo-bottle folders you will find a 'saml' folder, inside there is a 'certs' folder and a settings.json and a advanced_settings.json files. Those files contain the settings for the saml toolkit. Copy them in your project and set the correct values. +In the ``demo-django``, ``demo-flask``, ``demo-pyramid`` and ``demo-bottle`` folders you will find a ``saml`` folder, inside there is a ``certs`` folder and a ``settings.json`` and a ``advanced_settings.json`` files. Those files contain the settings for the SAML toolkit. Copy them in your project and set the correct values. -This is the settings.json file: +This is the ``settings.json`` file: ```javascript { - // If strict is True, then the Python Toolkit will reject unsigned + // If strict is True, then the Python Toolkit will reject unsigned // or unencrypted messages if it expects them to be signed or encrypted. // Also it will reject the messages if the SAML standard is not strictly // followed. Destination, NameId, Conditions ... are validated too. @@ -206,12 +256,28 @@ This is the settings.json file: // URL Location where the from the IdP will be returned "url": "https:///?acs", // SAML protocol binding to be used when returning the - // message. OneLogin Toolkit supports this endpoint for the + // message. OneLogin Toolkit supports this endpoint for the // HTTP-POST binding only. "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" }, + // If you need to specify requested attributes, set a + // attributeConsumingService. nameFormat, attributeValue and + // friendlyName can be omitted + "attributeConsumingService": { + "serviceName": "SP test", + "serviceDescription": "Test Service", + "requestedAttributes": [ + { + "name": "", + "isRequired": false, + "nameFormat": "", + "friendlyName": "", + "attributeValue": [] + } + ] + }, // Specifies info about where and how the message MUST be - // returned to the requester, in this case our SP. + // returned to the requester, in this case our SP. "singleLogoutService": { // URL Location where the from the IdP will be returned "url": "https:///?sls", @@ -223,11 +289,20 @@ This is the settings.json file: // Specifies the constraints on the name identifier to be used to // represent the requested subject. // Take a look on src/onelogin/saml2/constants.py to see the NameIdFormat that are supported. - "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified", - // Usually x509cert and privateKey of the SP are provided by files placed at + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + // Usually X.509 cert and privateKey of the SP are provided by files placed at // the certs folder. But we can also provide them with the following parameters "x509cert": "", "privateKey": "" + + /* + * Key rollover + * If you plan to update the SP X.509 cert and privateKey + * you can define here the new X.509 cert and it will be + * published on the SP metadata so Identity Providers can + * read them and get ready for rollover. + */ + // 'x509certNew': '', }, // Identity Provider Data that we want connected with our SP. @@ -236,7 +311,7 @@ This is the settings.json file: "entityId": "https://app.onelogin.com/saml/metadata/", // SSO endpoint info of the IdP. (Authentication Request protocol) "singleSignOnService": { - // URL Target of the IdP where the Authentication Request Message + // URL Target of the IdP where the Authentication Request Message // will be sent. "url": "https://app.onelogin.com/trust/saml2/http-post/sso/", // SAML protocol binding to be used when returning the @@ -253,24 +328,48 @@ This is the settings.json file: // only for this endpoint. "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, - // Public x509 certificate of the IdP + // Public X.509 certificate of the IdP "x509cert": "" /* - * Instead of use the whole x509cert you can use a fingerprint + * Instead of using the whole X.509 cert you can use a fingerprint in order to + * validate a SAMLResponse (but you still need the X.509 cert to validate LogoutRequest and LogoutResponse using the HTTP-Redirect binding). + * But take in mind that the fingerprint, is a hash, so at the end is open to a collision attack that can end on a signature validation bypass, + * that why we don't recommend it use for production environments. + * * (openssl x509 -noout -fingerprint -in "idp.crt" to generate it, - * or add for example the -sha256 , -sha384 or -sha512 parameter) + * or add for example the -sha256 , -sha384 or -sha512 parameter) * * If a fingerprint is provided, then the certFingerprintAlgorithm is required in order to * let the toolkit know which algorithm was used. Possible values: sha1, sha256, sha384 or sha512 * 'sha1' is the default value. + * + * Notice that if you want to validate any SAML Message sent by the HTTP-Redirect binding, you + * will need to provide the whole X.509 cert. + * */ - // 'certFingerprint' => '', - // 'certFingerprintAlgorithm' => 'sha1', + // 'certFingerprint': '', + // 'certFingerprintAlgorithm': 'sha1', + + /* In some scenarios the IdP uses different certificates for + * signing/encryption, or is under key rollover phase and + * more than one certificate is published on IdP metadata. + * In order to handle that the toolkit offers that parameter. + * (when used, 'x509cert' and 'certFingerprint' values are + * ignored). + */ + // 'x509certMulti': { + // 'signing': [ + // '' + // ], + // 'encryption': [ + // '' + // ] + // } } } ``` -In addition to the required settings data (idp, sp), there is extra information that could be defined at advanced_settings.json +In addition to the required settings data (idp, sp), extra settings can be defined in ``advanced_settings.json``: ```javascript { @@ -283,15 +382,15 @@ In addition to the required settings data (idp, sp), there is extra information // will be encrypted. "nameIdEncrypted": false, - // Indicates whether the messages sent by this SP + // Indicates whether the messages sent by this SP // will be signed. [Metadata of the SP will offer this info] "authnRequestsSigned": false, - // Indicates whether the messages sent by this SP + // Indicates whether the messages sent by this SP // will be signed. "logoutRequestSigned": false, - // Indicates whether the messages sent by this SP + // Indicates whether the messages sent by this SP // will be signed. "logoutResponseSigned": false, @@ -313,22 +412,43 @@ In addition to the required settings data (idp, sp), there is extra information // this SP to be signed. [Metadata of the SP will offer this info] "wantAssertionsSigned": false, + // Indicates a requirement for the + // elements received by this SP to be encrypted. + "wantAssertionsEncrypted": false, + + // Indicates a requirement for the NameID element on the SAMLResponse + // received by this SP to be present. + "wantNameId": true, + // Indicates a requirement for the NameID received by // this SP to be encrypted. "wantNameIdEncrypted": false, + // Indicates a requirement for the AttributeStatement element + "wantAttributeStatement": true, + + // Rejects SAML responses with a InResponseTo attribute when request_id + // not provided in the process_response method that later call the + // response is_valid method with that parameter. + "rejectUnsolicitedResponsesWithInResponseTo": false, + // Authentication context. // Set to false and no AuthContext will be sent in the AuthNRequest, - // Set true or don't present thi parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' + // Set true or don't present this parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' // Set an array with the possible auth context values: array ('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509'), - 'requestedAuthnContext': true, + "requestedAuthnContext": true, + // Allows the authn comparison parameter to be set, defaults to 'exact' if the setting is not present. + "requestedAuthnContextComparison": "exact", + + // Set to true to check that the AuthnContext received matches the one requested. + "failOnAuthnContextMismatch": false, // In some environment you will need to set how long the published metadata of the Service Provider gonna be valid. // is possible to not set the 2 following parameters (or set to null) and default values will be set (2 days, 1 week) // Provide the desired Timestamp, for example 2015-06-26T20:00:00Z - 'metadataValidUntil': null, + "metadataValidUntil": null, // Provide the desired duration, for example PT518400S (6 days) - 'metadataCacheDuration': null, + "metadataCacheDuration": null, // Algorithm that the toolkit will use on signing process. Options: // 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' @@ -336,10 +456,17 @@ In addition to the required settings data (idp, sp), there is extra information // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384' // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512' - 'signatureAlgorithm' => 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' + "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + + // Algorithm that the toolkit will use on digest process. Options: + // 'http://www.w3.org/2000/09/xmldsig#sha1' + // 'http://www.w3.org/2001/04/xmlenc#sha256' + // 'http://www.w3.org/2001/04/xmldsig-more#sha384' + // 'http://www.w3.org/2001/04/xmlenc#sha512' + "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1" }, - // Contact information template, it is recommended to suply a + // Contact information template, it is recommended to supply // technical and support contacts. "contactPerson": { "technical": { @@ -353,7 +480,7 @@ In addition to the required settings data (idp, sp), there is extra information }, // Organization information template, the info in en_US lang is - // recomended, add more if required. + // recommended, add more if required. "organization": { "en-US": { "name": "sp_test", @@ -364,11 +491,11 @@ In addition to the required settings data (idp, sp), there is extra information } ``` -In the security section, you can set the way that the SP will handle the messages and assertions. Contact the admin of the IdP and ask him what the IdP expects, and decide what validations will handle the SP and what requirements the SP will have and communicate them to the IdP's admin too. +In the ``security`` section, you can set the way that the SP will handle the messages and assertions. Contact the admin of the IdP and ask them what the IdP expects, and decide what validations will handle the SP and what requirements the SP will have and communicate them to the IdP's admin too. Once we know what kind of data could be configured, let's talk about the way settings are handled within the toolkit. -The settings files described (settings.json and advanced_settings.json) are loaded by the toolkit if not other dict with settings info is provided in the constructors of the toolkit. Let's see some examples. +The settings files described (``settings.json`` and ``advanced_settings.json``) are loaded by the toolkit if not other dict with settings info is provided in the constructors of the toolkit. Let's see some examples. ```python # Initializes toolkit with settings.json & advanced_settings.json files. @@ -388,7 +515,7 @@ auth = OneLogin_Saml2_Auth(req, settings_data) settings = OneLogin_Saml2_Settings(settings_data) ``` -You can declare the settings_data in the file that constains the constructor execution or locate them in any file and load the file in order to get the dict available as we see in the following example: +You can declare the ``settings_data`` in the file that constains the constructor execution or locate them in any file and load the file in order to get the dict available as we see in the following example: ```python filename = "/var/www/django-project/custom_settings.json" # The custom_settings.json contains a @@ -399,6 +526,24 @@ json_data_file.close() auth = OneLogin_Saml2_Auth(req, settings_data) ``` +#### Metadata Based Configuration + +The method above requires a little extra work to manually specify attributes about the IdP. (And your SP application) + +There's an easier method -- use a metadata exchange. Metadata is just an XML file that defines the capabilities of both the IdP and the SP application. It also contains the X.509 public key certificates which add to the trusted relationship. The IdP administrator can also configure custom settings for an SP based on the metadata. + +Using ````parse_remote```` IdP metadata can be obtained and added to the settings withouth further ado. + +`` +idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://example.com/auth/saml2/idp/metadata') +`` + +If the Metadata contains several entities, the relevant ``EntityDescriptor`` can be specified when retrieving the settings from the ``IdpMetadataParser`` by its ``EntityId`` value: +``` +idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(https://example.com/metadatas, entity_id='idp_entity_id') +``` + + #### How load the library #### In order to use the toolkit library you need to import the file that contains the class that you will need @@ -412,7 +557,7 @@ from onelogin.saml2.utils import OneLogin_Saml2_Utils #### The Request #### -Building an OneLogin_Saml2_Auth object requires a 'request' parameter. +Building an ``OneLogin_Saml2_Auth object`` requires a ``request`` parameter. ```python auth = OneLogin_Saml2_Auth(req) @@ -422,6 +567,7 @@ This parameter has the following scheme: ```javascript req = { + "https": "" "http_host": "", "script_name": "", "server_port": "", @@ -430,7 +576,7 @@ req = { } ``` -Each python framework built its own request object, you may map its data to match what the saml toolkit expects. +Each Python framework built its own ``request`` object, you may map its data to match what the SAML toolkit expects. Let`s see some examples: ```python @@ -453,7 +599,7 @@ def prepare_from_flask_request(request): 'post_data': request.form.copy() } ``` - +The ``https`` dictionary entry should be set to ``on`` for https requests and ``off`` for http #### Initiate SSO #### @@ -469,27 +615,35 @@ auth = OneLogin_Saml2_Auth(req) # Constructor of the SP, loads settings.json auth.login() # Method that builds and sends the AuthNRequest ``` -The AuthNRequest will be sent signed or unsigned based on the security info of the advanced_settings.json ('authnRequestsSigned'). +The ``AuthNRequest`` will be sent signed or unsigned based on the security info of the ``advanced_settings.json`` (``authnRequestsSigned``). -The IdP will then return the SAML Response to the user's client. The client is then forwarded to the Attribute Consumer Service of the SP with this information. +The IdP will then return the SAML Response to the user's client. The client is then forwarded to the **Attribute Consumer Service (ACS)** of the SP with this information. -We can set a 'return_to' url parameter to the login function and that will be converted as a 'RelayState' parameter: +We can set a ``return_to`` url parameter to the login function and that will be converted as a ``RelayState`` parameter: ```python target_url = 'https://example.com' auth.login(return_to=target_url) ``` -The login method can recieve 2 more optional parameters: +The login method can recieve 4 more optional parameters: + +* ``force_authn`` When ``true`` the ``AuthNReuqest`` will set the ``ForceAuthn='true'`` +* ``is_passive`` When ``true`` the ``AuthNReuqest`` will set the ``Ispassive='true'`` +* ``set_nameid_policy`` When ``true`` the ``AuthNReuqest`` will set a ``nameIdPolicy`` element. +* ``name_id_value_req`` Indicates to the IdP the ``Subject`` that should be authenticated + +If a match on the future SAMLResponse ID and the AuthNRequest ID to be sent is required, that AuthNRequest ID must to be extracted and stored for future validation, we can get that ID by -* force_authn When true the AuthNReuqest will set the ForceAuthn='true' -* is_passive When true the AuthNReuqest will set the Ispassive='true' +```python +auth.get_last_request_id() +``` #### The SP Endpoints #### -Related to the SP there are 3 important endpoints: The metadata view, the ACS view and the SLS view. +Related to the SP there are 3 important endpoints: The metadata view, the ACS view and the SLS view. The toolkit provides examples of those views in the demos, but lets see an example. -***SP Metadata*** +*** SP Metadata *** This code will provide the XML metadata file of our SP, based on the info that we provided in the settings files. @@ -505,17 +659,17 @@ else: print "Error found on Metadata: %s" % (', '.join(errors)) ``` -The get_sp_metadata will return the metadata signed or not based on the security info of the advanced_settings.json ('signMetadata'). +The ``get_sp_metadata`` will return the metadata signed or not based on the security info of the ``advanced_settings.json`` (``signMetadata``). Before the XML metadata is exposed, a check takes place to ensure that the info to be provided is valid. -Instead of use the Auth object, you can directly use +Instead of using the ``Auth`` object, you can directly use ``` saml_settings = OneLogin_Saml2_Settings(settings=None, custom_base_path=None, sp_validation_only=True) ``` -to get the settings object and with the sp_validation_only=True parameter we will avoid the IdP Settings validation. +to get the settings object and with the ``sp_validation_only=True`` parameter we will avoid the IdP settings validation. -***Attribute Consumer Service(ACS)*** +*** Attribute Consumer Service (ACS) *** This code handles the SAML response that the IdP forwards to the SP through the user's client. @@ -527,7 +681,7 @@ errors = auth.get_errors() if not errors: if auth.is_authenticated(): request.session['samlUserdata'] = auth.get_attributes() - if 'RelayState' in req['post_data'] and + if 'RelayState' in req['post_data'] and OneLogin_Saml2_Utils.get_self_url(req) != req['post_data']['RelayState']: auth.redirect_to(req['post_data']['RelayState']) else: @@ -543,10 +697,10 @@ The SAML response is processed and then checked that there are no errors. It als At that point there are 2 possible alternatives: -* If no RelayState is provided, we could show the user data in this view or however we wanted. -* If RelayState is provided, a rediretion take place. +* If no ``RelayState`` is provided, we could show the user data in this view or however we wanted. +* If ``RelayState`` is provided, a rediretion take place. -Notice that we saved the user data in the session before the redirection to have the user data available at the RelayState view. +Notice that we saved the user data in the session before the redirection to have the user data available at the ``RelayState`` view. In order to retrieve attributes we use: @@ -565,7 +719,7 @@ If we execute print attributes we could get: "mail": ["Doe"], "groups": ["users", "members"] } -``` +``` Each attribute name can be used as a key to obtain the value. Every attribute is a list of values. A single-valued attribute is a listy of a single element. @@ -578,10 +732,10 @@ print attributes['cn'] print auth.get_attribute('cn') ``` -Before trying to get an attribute, check that the user is authenticated. If the user isn't authenticated, an empty dict will be returned. For example, if we call to get_attributes before a auth.process_response, the get_attributes() will return an empty dict. +Before trying to get an attribute, check that the user is authenticated. If the user isn't authenticated, an empty dict will be returned. For example, if we call to ``auth.get_attributes`` before a ``auth.process_response``, the ``auth.get_attributes`` will return an empty dict. -***Single Logout Service (SLS)*** +*** Single Logout Service (SLS) *** This code handles the Logout Request and the Logout Responses. @@ -608,7 +762,7 @@ if not logout_response.is_valid(self.__request_data, request_id): elif logout_response.get_status() != OneLogin_Saml2_Constants.STATUS_SUCCESS: self.__errors.append('logout_not_success') elif not keep_local_session: - OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) + OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) ``` If the SLS endpoints receives an Logout Request, the request is validated, the session is closed and a Logout Response is sent to the SLS endpoint of the IdP. @@ -639,7 +793,7 @@ else: return self.redirect_to(self.get_slo_url(), parameters) ``` -If we don't want that process_slo to destroy the session, pass a true parameter to the process_slo method +If we don't want that ``process_slo`` to destroy the session, pass a ``true`` parameter to the ``process_slo`` method ```python keepLocalSession = true @@ -649,25 +803,46 @@ auth.process_slo(keep_local_session=keepLocalSession); #### Initiate SLO #### In order to send a Logout Request to the IdP: +```python +from onelogin.saml2.auth import OneLogin_Saml2_Auth + +req = prepare_request_for_toolkit(request) +auth = OneLogin_Saml2_Auth(req) # Constructor of the SP, loads settings.json + # and advanced_settings.json + +auth.logout() # Method that builds and sends the LogoutRequest +``` -The Logout Request will be sent signed or unsigned based on the security info of the advanced_settings.json ('logoutRequestSigned'). +The Logout Request will be sent signed or unsigned based on the security info of the ``advanced_settings.json`` (``logoutRequestSigned``). -The IdP will return the Logout Response through the user's client to the Single Logout Service of the SP. +The IdP will return the Logout Response through the user's client to the Single Logout Service (SLS) of the SP. -We can set a 'return_to' url parameter to the logout function and that will be converted as a 'RelayState' parameter: +We can set a ``return_to`` url parameter to the logout function and that will be converted as a ``RelayState`` parameter: ```python target_url = 'https://example.com' auth.logout(return_to=target_url) ``` -Also there are 2 optional parameters that can be set: +Also there are another 5 optional parameters that can be set: -* name_id. That will be used to build the LogoutRequest. If not name_id parameter is set and the auth object processed a +* ``name_id``. That will be used to build the LogoutRequest. If not ``name_id`` parameter is set and the auth object processed a SAML Response with a NameId, then this NameId will be used. -* session_index. SessionIndex that identifies the session of the user. +* ``session_index``. SessionIndex that identifies the session of the user. +* ``nq``. IDP Name Qualifier +* ``name_id_format``. The NameID Format that will be set in the LogoutRequest +* ``spnq``: The ``NameID SP NameQualifier`` will be set in the ``LogoutRequest``. -####Example of a view that initiates the SSO request and handles the response (is the acs target)#### +If no name_id is provided, the LogoutRequest will contain a NameID with the entity Format. +If name_id is provided and no name_id_format is provided, the NameIDFormat of the settings will be used. + +If a match on the LogoutResponse ID and the LogoutRequest ID to be sent is required, that LogoutRequest ID must to be extracted and stored for future validation, we can get that ID by: + +```python +auth.get_last_request_id() +``` + +#### Example of a view that initiates the SSO request and handles the response (is the acs target) #### We can code a unique file that initiates the SSO process, handle the response, get the attributes, initiate the slo and processes the logout response. @@ -681,21 +856,31 @@ auth = OneLogin_Saml2_Auth(req) # Initialize the SP SAML instance if 'sso' in request.args: # SSO action (SP-SSO initited). Will send an AuthNRequest to the IdP return redirect(auth.login()) -elif 'sso2' in request.args: # Another SSO init action +elif 'sso2' in request.args: # Another SSO init action return_to = '%sattrs/' % request.host_url # but set a custom RelayState URL return redirect(auth.login(return_to)) elif 'slo' in request.args: # SLO action. Will sent a Logout Request to IdP - return redirect(auth.logout()) + nameid = request.session['samlNameId'] + nameid_format = request.session['samlNameIdFormat'] + nameid_nq = request.session['samlNameIdNameQualifier'] + nameid_spnq = request.session['samlNameIdSPNameQualifier'] + session_index = request.session['samlSessionIndex'] + return redirect(auth.logout(None, nameid, session_index, nameid_nq, nameid_format, nameid_spnq)) elif 'acs' in request.args: # Assertion Consumer Service auth.process_response() # Process the Response of the IdP errors = auth.get_errors() # This method receives an array with the errors - if len(errors) == 0: # that could took place during the process + if len(errors) == 0: # that could took place during the process if not auth.is_authenticated(): # This check if the response was ok and the user msg = "Not authenticated" # data retrieved or not (user authenticated) else: request.session['samlUserdata'] = auth.get_attributes() # Retrieves user data + request.session['samlNameId'] = auth.get_nameid() + request.session['samlNameIdFormat'] = auth.get_nameid_format() + request.session['samlNameIdNameQualifier'] = auth.get_nameid_nq() + request.session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq() + request.session['samlSessionIndex'] = auth.get_session_index() self_url = OneLogin_Saml2_Utils.get_self_url(req) - if 'RelayState' in request.form and self_url != request.form['RelayState']: + if 'RelayState' in request.form and self_url != request.form['RelayState']: return redirect(auth.redirect_to(request.form['RelayState'])) # Redirect if there is a relayState else: # If there is user data we save that to print it later. msg = '' @@ -717,12 +902,38 @@ else: print ', '.join(errors) ``` +### SP Key rollover ### + +If you plan to update the SP X.509 cert and privateKey you can define the new X.509 cert as ``settings['sp']['x509certNew']`` and it will be +published on the SP metadata so Identity Providers can read them and get ready for rollover. + + +### IdP with multiple certificates ### + +In some scenarios the IdP uses different certificates for +signing/encryption, or is under key rollover phase and more than one certificate is published on IdP metadata. + +In order to handle that the toolkit offers the ``settings['idp']['x509certMulti']`` parameter. + +When that parameter is used, ``x509cert`` and ``certFingerprint`` values will be ignored by the toolkit. + +The ``x509certMulti`` is an array with 2 keys: +- ``signing``. An array of certs that will be used to validate IdP signature +- ``encryption`` An array with one unique cert that will be used to encrypt data to be sent to the IdP + + +### Replay attacks ### + + In order to avoid reply attacks, you can store the ID of the SAML messages already processed, to avoid processing them twice. Since the Messages expires and will be invalidated due that fact, you don't need to store those IDs longer than the time frame that you currently accepting. + + Get the ID of the last processed message/assertion with the ``get_last_message_id``/``get_last_assertion_id method`` of the ``Auth`` object. + ### Main classes and methods ### Described below are the main classes and methods that can be invoked from the SAML2 library. -####OneLogin_Saml2_Auth - auth.py#### +#### OneLogin_Saml2_Auth - auth.py #### Main class of OneLogin Python Toolkit @@ -735,27 +946,35 @@ Main class of OneLogin Python Toolkit * ***is_authenticated*** Checks if the user is authenticated or not. * ***get_attributes*** Returns the set of SAML attributes. * ***get_attribute*** Returns the requested SAML attribute. -* ***get_nameid*** Returns the nameID. -* ***get_session_index*** Gets the SessionIndex from the AuthnStatement. +* ***get_nameid*** Returns the ``nameID``. +* ***get_session_index*** Gets the ``SessionIndex`` from the ``AuthnStatement``. +* ***get_session_expiration*** Gets the ``SessionNotOnOrAfter`` from the ``AuthnStatement``. * ***get_errors*** Returns a list with code errors if something went wrong. * ***get_last_error_reason*** Returns the reason of the last error * ***get_sso_url*** Gets the SSO url. * ***get_slo_url*** Gets the SLO url. +* ***get_last_request_id*** The ``ID`` of the last Request SAML message generated (``AuthNRequest``, ``LogoutRequest``). +* ***get_last_authn_contexts*** Returns the list of authentication contexts sent in the last SAML Response. * ***build_request_signature*** Builds the Signature of the SAML Request. * ***build_response_signature*** Builds the Signature of the SAML Response. * ***get_settings*** Returns the settings info. * ***set_strict*** Set the strict mode active/disable. +* ***get_last_request_xml*** Returns the most recently-constructed/processed XML SAML request (AuthNRequest, LogoutRequest) +* ***get_last_response_xml*** Returns the most recently-constructed/processed XML SAML response (``SAMLResponse``, ``LogoutResponse``). If the SAMLResponse had an encrypted assertion, decrypts it. +* ***get_last_message_id*** The ``ID`` of the last Response SAML message processed. +* ***get_last_assertion_id*** The ``ID`` of the last assertion processed. +* ***get_last_assertion_not_on_or_after*** The ``NotOnOrAfter`` value of the valid SubjectConfirmationData node (if any) of the last assertion processed (is only calculated with strict = true) -####OneLogin_Saml2_Auth - authn_request.py#### +#### OneLogin_Saml2_Auth - authn_request.py #### SAML 2 Authentication Request class -* `__init__` This class handles an AuthNRequest. It builds an AuthNRequest object. +* `__init__` This class handles an AuthNRequest. It builds an ``AuthNRequest`` object. * ***get_request*** Returns unsigned AuthnRequest. * ***get_id*** Returns the AuthNRequest ID. +* ***get_xml*** Returns the XML that will be sent as part of the request. - -####OneLogin_Saml2_Response - response.py#### +#### OneLogin_Saml2_Response - response.py #### SAML 2 Authentication Response class @@ -764,30 +983,35 @@ SAML 2 Authentication Response class * ***check_status*** Check if the status of the response is success or not * ***get_audiences*** Gets the audiences * ***get_issuers*** Gets the issuers (from message and from assertion) -* ***get_nameid_data*** Gets the NameID Data provided by the SAML Response from the IdP (returns a dict) +* ***get_nameid_data*** Gets the ``NameID`` Data provided by the SAML Response from the IdP (returns a dict) * ***get_nameid*** Gets the NameID provided by the SAML Response from the IdP (returns a string) * ***get_session_not_on_or_after*** Gets the SessionNotOnOrAfter from the AuthnStatement -* ***get_session_index*** Gets the SessionIndex from the AuthnStatement -* ***get_attributes*** Gets the Attributes from the AttributeStatement element. +* ***get_session_index*** Gets the ``SessionIndex`` from the ``AuthnStatement`` +* ***get_attributes*** Gets the ``Attributes`` from the ``AttributeStatement`` element. * ***validate_num_assertions*** Verifies that the document only contains a single Assertion (encrypted or not) -* ***validate_timestamps*** Verifies that the document is valid according to Conditions Element +* ***validate_timestamps*** Verifies that the document is valid according to ``Conditions`` element * ***get_error*** After execute a validation process, if fails this method returns the cause +* ***get_xml_document*** Returns the SAML Response document (If contains an encrypted assertion, decrypts it). +* ***get_id*** the ID of the response +* ***get_assertion_id*** the ``ID`` of the assertion in the response +* ***get_assertion_not_on_or_after*** the ``NotOnOrAfter`` value of the valid SubjectConfirmationData if any -####OneLogin_Saml2_LogoutRequest - logout_request.py#### +#### OneLogin_Saml2_LogoutRequest - logout_request.py #### SAML 2 Logout Request class * `__init__` Constructs the Logout Request object. -* ***get_request*** Returns the Logout Request defated, base64encoded. +* ***get_request*** Returns the Logout Request defated, base64-encoded. * ***get_id*** Returns the ID of the Logout Request. (If you have the object you can access to the id attribute) * ***get_nameid_data*** Gets the NameID Data of the the Logout Request (returns a dict). * ***get_nameid*** Gets the NameID of the Logout Request Message (returns a string). * ***get_issuer*** Gets the Issuer of the Logout Request Message. -* ***get_session_indexes*** Gets the SessionIndexes from the Logout Request. +* ***get_session_indexes*** Gets the ``SessionIndexes`` from the Logout Request. * ***is_valid*** Checks if the Logout Request recieved is valid. * ***get_error*** After execute a validation process, if fails this method returns the cause. +* ***get_xml*** Returns the XML that will be sent as part of the request or that was received at the SP -####OneLogin_Saml2_LogoutResponse - logout_response.py#### +#### OneLogin_Saml2_LogoutResponse - logout_response.py #### SAML 2 Logout Response class @@ -796,11 +1020,12 @@ SAML 2 Logout Response class * ***get_status*** Gets the Status of the Logout Response. * ***is_valid*** Determines if the SAML LogoutResponse is valid * ***build*** Creates a Logout Response object. -* ***get_response*** Returns a Logout Response object. +* ***get_response*** Returns a Logout Response object. * ***get_error*** After execute a validation process, if fails this method returns the cause. +* ***get_xml*** Returns the XML that will be sent as part of the response or that was received at the SP -####OneLogin_Saml2_Settings - settings.py#### +#### OneLogin_Saml2_Settings - settings.py #### Configuration of the OneLogin Python Toolkit @@ -816,38 +1041,41 @@ Configuration of the OneLogin Python Toolkit * ***get_lib_path*** Returns lib path. * ***get_ext_lib_path*** Returns external lib path. * ***get_schemas_path*** Returns schema path. -* ***check_sp_certs*** Checks if the x509 certs of the SP exists and are valid. -* ***get_sp_key*** Returns the x509 private key of the SP. -* ***get_sp_cert*** Returns the x509 public cert of the SP. -* ***get_idp_cert*** Returns the x509 public cert of the IdP. +* ***check_sp_certs*** Checks if the X.509 certs of the SP exists and are valid. +* ***get_sp_key*** Returns the X.509 private key of the SP. +* ***get_sp_cert*** Returns the X.509 public cert of the SP. +* ***get_sp_cert_new*** Returns the future X.509 public cert of the SP. +* ***get_idp_cert*** Returns the X.509 public cert of the IdP. * ***get_sp_data*** Gets the SP data. * ***get_idp_data*** Gets the IdP data. * ***get_security_data*** Gets security data. * ***get_contacts*** Gets contacts data. * ***get_organization*** Gets organization data. * ***format_idp_cert*** Formats the IdP cert. +* ***format_idp_cert_multi*** Formats all registered IdP certs. * ***format_sp_cert*** Formats the SP cert. +* ***format_sp_cert_new*** Formats the SP cert new. * ***format_sp_key*** Formats the private key. * ***set_strict*** Activates or deactivates the strict mode. -* ***is_strict*** Returns if the 'strict' mode is active. +* ***is_strict*** Returns if the ``strict`` mode is active. * ***is_debug_active*** Returns if the debug is active. -####OneLogin_Saml2_Metadata - metadata.py#### +#### OneLogin_Saml2_Metadata - metadata.py #### A class that contains functionality related to the metadata of the SP -* ***builder*** Generates the metadata of the SP based on the settings. +* ***builder*** Generates the metadata of the SP based on the settings. * ***sign_metadata*** Signs the metadata with the key/cert provided. -* ***add_x509_key_descriptors*** Adds the x509 descriptors (sign/encriptation) to the metadata +* ***add_x509_key_descriptors*** Adds the X.509 descriptors (sign/encriptation) to the metadata -####OneLogin_Saml2_Utils - utils.py#### +#### OneLogin_Saml2_Utils - utils.py #### Auxiliary class that contains several methods * ***decode_base64_and_inflate*** Base64 decodes and then inflates according to RFC1951. * ***deflate_and_base64_encode*** Deflates and the base64 encodes a string. * ***validate_xml*** Validates a xml against a schema. -* ***format_cert*** Returns a x509 cert (adding header & footer if required). +* ***format_cert*** Returns a X.509 cert (adding header & footer if required). * ***format_private_key*** Returns a private key (adding header & footer if required). * ***redirect*** Executes a redirection to the provided url (or return the target url). * ***get_self_url_host*** Returns the protocol + the current host + the port (if different than common ports). @@ -864,7 +1092,7 @@ Auxiliary class that contains several methods * ***get_expire_time*** Compares 2 dates and returns the earliest. * ***query*** Extracts nodes that match the query from the Element. * ***delete_local_session*** Deletes the local session. -* ***calculate_x509_fingerprint*** Calculates the fingerprint of a x509cert. +* ***calculate_x509_fingerprint*** Calculates the fingerprint of a X.509 cert. * ***format_finger_print*** Formates a fingerprint. * ***generate_name_id*** Generates a nameID. * ***get_status*** Gets Status from a Response. @@ -873,13 +1101,27 @@ Auxiliary class that contains several methods * ***add_sign*** Adds signature key and senders certificate to an element (Message or Assertion). * ***validate_sign*** Validates a signature (Message or Assertion). * ***validate_binary_sign*** Validates signed bynary data (Used to validate GET Signature). +* ***def get_encoded_parameter*** Return an url encoded get parameter value +* ***extract_raw_query_parameter*** + +#### OneLogin_Saml2_IdPMetadataParser - idp_metadata_parser.py #### + +A class that contains methods to obtain and parse metadata from IdP + +* ***get_metadata*** Get the metadata XML from the provided URL +* ***parse_remote*** Get the metadata XML from the provided URL and parse it, returning a dict with extracted data +* ***parse*** Parse the Identity Provider metadata and returns a dict with extracted data +* ***merge_settings*** Will update the settings with the provided new settings data extracted from the IdP metadata + +For more info, look at the source code. Each method is documented and details about what does and how to use it are provided. Make sure to also check the doc folder where HTML documentation about the classes and methods is provided. + + -For more info, look at the source code; each method is documented and details about what does and how to use it are provided. Make sure to also check the doc folder where HTML documentation about the classes and methods is provided. Demos included in the toolkit ----------------------------- -The toolkit includes 2 demos to teach how use the toolkit (A django and a flask project), take a look on it. +The toolkit includes 4 demos to teach how use the toolkit (Django, Flask, Pyramid and Bootle projects), take a look on them. Demos require that SP and IdP are well configured before test it, so edit the settings files. Notice that each python framework has it own way to handle routes/urls and process request, so focus on @@ -887,10 +1129,9 @@ how it deployed. New demos using other python frameworks are welcome as a contri ### Getting Started ### -We said that this toolkit includes a django application demo and a flask applicacion demo, -lets see how fast is deploy them. +We said that this toolkit includes a demos, lets see how fast is deploy some of them. -***Virtualenv*** +*** Virtualenv *** The use of a [virtualenv](http://virtualenv.readthedocs.org/en/latest/) is highly recommended. @@ -933,51 +1174,51 @@ Now, with the virtualenv loaded, you can run the demo like this: python index.py ``` -You'll have the demo running at http://localhost:8000 +You'll have the demo running at ``http://localhost:8000`` -####Content#### +#### Content #### The flask project contains: * ***index.py*** Is the main flask file, where or the SAML handle take place. -* ***templates***. Is the folder where flask stores the templates of the project. It was implemented a base.html template that is extended by index.html and attrs.html, the templates of our simple demo that shows messages, user attributes when available and login and logout links. +* ***templates***. Is the folder where flask stores the templates of the project. It was implemented a base.html template that is extended by ``index.html`` and ``attrs.html``, the templates of our simple demo that shows messages, user attributes when available and login and logout links. -* ***saml*** Is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json). +* ***saml*** Is a folder that contains the 'certs' folder that could be used to store the X.509 public and private key, and the saml toolkit settings (``settings.json`` and ``advanced_settings.json``). -####SP setup#### +#### SP setup #### -The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In the demo-flask it used the first method. +The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: Settings files or define a setting dict. In the ``demo-flask``, it uses the first method. -In the index.py file we define the app.config['SAML_PATH'], that will target to the 'saml' folder. We require it in order to load the settings files. +In the index.py file we define the ``app.config['SAML_PATH']``, that will target to the ``saml`` folder. We require it in order to load the settings files. -First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt. +First we need to edit the ``saml/settings.json``, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt. -####IdP setup#### +#### IdP setup #### -Once the SP is configured, the metadata of the SP is published at the /metadata url. Based on that info, configure the IdP. +Once the SP is configured, the metadata of the SP is published at the ``/metadata`` url. Based on that info, configure the IdP. -####How it works#### +#### How it works #### -1. First time you access to the main view 'http://localhost:8000', you can select to login and return to the same view or login and be redirected to /?attrs (attrs view). +1. First time you access to the main view ``http://localhost:8000``, you can select to login and return to the same view or login and be redirected to ``/?attrs`` (attrs view). 2. When you click: - 2.1 in the first link, we access to /?sso (index view). An AuthNRequest is sent to the IdP, we authenticate at the IdP and then a Response is sent through the user's client to the SP, specifically the Assertion Consumer Service view: /?acs. Notice that a RelayState parameter is set to the url that initiated the process, the index view. + 2.1 in the first link, we access to ``/?sso`` (index view). An ``AuthNRequest`` is sent to the IdP, we authenticate at the IdP and then a Response is sent through the user's client to the SP, specifically the Assertion Consumer Service view: /?acs. Notice that a RelayState parameter is set to the url that initiated the process, the index view. - 2.2 in the second link we access to /?attrs (attrs view), we will expetience have the same process described at 2.1 with the diference that as RelayState is set the attrs url. + 2.2 in the second link we access to ``/?attrs`` (attrs view), we will expetience have the same process described at 2.1 with the diference that as ``RelayState`` is set the attrs url. - 3. The SAML Response is processed in the ACS /?acs, if the Response is not valid, the process stops here and a message is shown. Otherwise we are redirected to the RelayState view. a) / or b) /?attrs + 3. The ``SAMLResponse`` is processed in the ACS ``/?acs``, if the Response is not valid, the process stops here and a message is shown. Otherwise we are redirected to the ``RelayState`` view. a) / or b) ``/?attrs`` 4. We are logged in the app and the user attributes are showed. At this point, we can test the single log out functionality. The single log out funcionality could be tested by 2 ways. - 5.1 SLO Initiated by SP. Click on the "logout" link at the SP, after that a Logout Request is sent to the IdP, the session at the IdP is closed and replies through the client to the SP with a Logout Response (sent to the Single Logout Service endpoint). The SLS endpoint /?sls of the SP process the Logout Response and if is valid, close the user session of the local app. Notice that the SLO Workflow starts and ends at the SP. + 5.1 SLO Initiated by SP. Click on the ``logout`` link at the SP, after that a Logout Request is sent to the IdP, the session at the IdP is closed and replies through the client to the SP with a Logout Response (sent to the Single Logout Service endpoint). The SLS endpoint ``/?sls`` of the SP process the Logout Response and if is valid, close the user session of the local app. Notice that the SLO Workflow starts and ends at the SP. - 5.2 SLO Initiated by IdP. In this case, the action takes place on the IdP side, the logout process is initiated at the IdP, sends a Logout Request to the SP (SLS endpoint, /?sls). The SLS endpoint of the SP process the Logout Request and if is valid, close the session of the user at the local app and send a Logout Response to the IdP (to the SLS endpoint of the IdP). The IdP receives the Logout Response, process it and close the session at of the IdP. Notice that the SLO Workflow starts and ends at the IdP. + 5.2 SLO Initiated by IdP. In this case, the action takes place on the IdP side, the logout process is initiated at the IdP, sends a Logout Request to the SP (SLS endpoint, ``/?sls``). The SLS endpoint of the SP process the Logout Request and if is valid, close the session of the user at the local app and send a Logout Response to the IdP (to the SLS endpoint of the IdP). The IdP receives the Logout Response, process it and close the session at of the IdP. Notice that the SLO Workflow starts and ends at the IdP. Notice that all the SAML Requests and Responses are handled at a unique view (index) and how GET paramters are used to know the action that must be done. @@ -989,7 +1230,7 @@ To run the demo you need to install the requirements first. Load your virtualenv and execute: ``` pip install -r demo-django/requirements.txt -``` +``` This will install django and its dependences. Once it has finished, you have to complete the configuration of the toolkit. Later, with the virtualenv loaded, you can run the demo like this: @@ -998,40 +1239,113 @@ Later, with the virtualenv loaded, you can run the demo like this: python manage.py runserver 0.0.0.0:8000 ``` -You'll have the demo running at http://localhost:8000. +You'll have the demo running at ``http://localhost:8000``. Note that many of the configuration files expect HTTPS. This is not required by the demo, as replacing these SP URLs with HTTP will work just fine. HTTPS is however highly encouraged, and left as an exercise for the reader for their specific needs. If you want to integrate a production django application, take a look on this SAMLServiceProviderBackend that uses our toolkit to add SAML support: https://github.com/KristianOellegaard/django-saml-service-provider -####Content#### +#### Content #### The django project contains: -* ***manage.py***. A file that is automatically created in each Django project. Is a thin wrapper around django-admin.py that takes care of putting the project’s package on sys.path and sets the DJANGO_SETTINGS_MODULE environment variable. +* ***manage.py***. A file that is automatically created in each Django project. Is a thin wrapper around ``django-admin.py`` that takes care of putting the project’s package on ``sys.path`` and sets the ``DJANGO_SETTINGS_MODULE`` environment variable. -* ***saml*** Is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json). +* ***saml*** Is a folder that contains the ``certs`` folder that could be used to store the X.509 public and private key, and the saml toolkit settings (``settings.json`` and ``advanced_settings.json``). * ***demo*** Is the main folder of the django project, that contains the typical files: * ***settings.py*** Contains the default parameters of a django project except the SAML_FOLDER parameter, that may contain the path where is located the 'saml' folder. - * ***urls.py*** A file that define url routes. In the demo we defined '/' that is related to the index view, '/attrs' that is related with the attrs view and '/metadata', related to th metadata view. + * ***urls.py*** A file that define url routes. In the demo we defined ``/`` that is related to the index view, ``/attrs`` that is related with the attrs view and ``/metadata``, related to th metadata view. * ***views.py*** This file contains the views of the django project and some aux methods. * ***wsgi.py*** A file that let as deploy django using WSGI, the Python standard for web servers and applications. -* ***templates***. Is the folder where django stores the templates of the project. It was implemented a base.html template that is extended by index.html and attrs.html, the templates of our simple demo that shows messages, user attributes when available and login and logout links. +* ***templates***. Is the folder where django stores the templates of the project. It was implemented a base.html template that is extended by ``index.html`` and ``attrs.html``, the templates of our simple demo that shows messages, user attributes when available and login and logout links. + +#### SP setup #### + +The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: Settings files or define a setting dict. In the ``demo-django``, it uses the first method. + +After set the ``SAML_FOLDER`` in the ``demo/settings.py``, the settings of the python toolkit will be loaded on the django web. + +First we need to edit the ``saml/settings.json``, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt. + +#### IdP setup #### + +Once the SP is configured, the metadata of the SP is published at the ``/metadata`` url. Based on that info, configure the IdP. + +#### How it works #### + +This demo works very similar to the ``flask-demo`` (We did it intentionally). -####SP setup#### -The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In the demo-django it used the first method. +### Demo Pyramid ### -After set the SAML_FOLDER in the demo/settings.py, the settings of the python toolkit will be loaded on the django web. +Unlike the other two projects, you don't need a pre-existing virtualenv to get +up and running here, since Pyramid comes from the +[buildout](http://www.buildout.org/en/latest/) school of thought. -First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt. +To run the demo you need to install Pyramid, the requirements, etc.: +``` + cd demo_pyramid + python -m venv env + env/bin/pip install --upgrade pip setuptools + env/bin/pip install -e ".[testing]" +``` + +Next, edit the settings in ``demo_pyramid/saml/settings.json``. (Pyramid runs on +port 6543 by default.) + +Now you can run the demo like this: +``` + env/bin/pserve development.ini +``` + +If that worked, the demo is now running at ``http://localhost:6543``. + +#### Content #### + +The Pyramid project contains: + + +* ***\_\_init__.py*** is the main Pyramid file that configures the app and its routes. + +* ***views.py*** is where all the SAML handling takes place. + +* ***templates*** is the folder where Pyramid stores the templates of the project. It was implemented a ``layout.jinja2`` template that is extended by ``index.jinja2`` and ``attrs.jinja2``, the templates of our simple demo that shows messages, user attributes when available and login and logout links. + +* ***saml*** is a folder that contains the 'certs' folder that could be used to store the X.509 public and private key, and the saml toolkit settings (``settings.json`` and ``advanced_settings.json``). + + +#### SP setup #### -####IdP setup#### +The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: Settings files or define a setting dict. In ``demo_pyramid`` the first method is used. + +In the views.py file we define the ``SAML_PATH``, which will target the ``saml`` folder. We require it in order to load the settings files. + +First we need to edit the ``saml/settings.json``, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the ``saml/advanced_settings.json`` files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt. + +#### IdP setup #### + +Once the SP is configured, the metadata of the SP is published at the ``/metadata/`` url. Based on that info, configure the IdP. + +#### How it works #### + +1. First time you access to the main view ``http://localhost:6543``, you can select to login and return to the same view or login and be redirected to ``/?attrs`` (attrs view). + + 2. When you click: + + 2.1 in the first link, we access to ``/?sso`` (index view). An ``AuthNRequest`` is sent to the IdP, we authenticate at the IdP and then a Response is sent through the user's client to the SP, specifically the Assertion Consumer Service view: ``/?acs``. Notice that a RelayState parameter is set to the url that initiated the process, the index view. + + 2.2 in the second link we access to ``/?attrs`` (attrs view), we will expetience have the same process described at 2.1 with the diference that as ``RelayState`` is set the attrs url. + + 3. The SAML Response is processed in the ACS ``/?acs``, if the Response is not valid, the process stops here and a message is shown. Otherwise we are redirected to the ``RelayState`` view. a) / or b) ``/?attrs`` + + 4. We are logged in the app and the user attributes are showed. At this point, we can test the single log out functionality. + + The single log out funcionality could be tested by 2 ways. -Once the SP is configured, the metadata of the SP is published at the /metadata url. Based on that info, configure the IdP. + 5.1 SLO Initiated by SP. Click on the ``logout`` link at the SP, after that a Logout Request is sent to the IdP, the session at the IdP is closed and replies through the client to the SP with a Logout Response (sent to the Single Logout Service endpoint). The SLS endpoint ``/?sls`` of the SP process the Logout Response and if is valid, close the user session of the local app. Notice that the SLO Workflow starts and ends at the SP. -####How it works#### + 5.2 SLO Initiated by IdP. In this case, the action takes place on the IdP side, the logout process is initiated at the IdP, sends a Logout Request to the SP (SLS endpoint, ``/?sls``). The SLS endpoint of the SP process the Logout Request and if is valid, close the session of the user at the local app and send a Logout Response to the IdP (to the SLS endpoint of the IdP). The IdP receives the Logout Response, process it and close the session at of the IdP. Notice that the SLO Workflow starts and ends at the IdP. -This demo works very similar to the flask-demo (We did it intentionally). +Notice that all the SAML Requests and Responses are handled at a unique view (index) and how GET parameters are used to know the action that must be done. diff --git a/changelog.md b/changelog.md new file mode 100644 index 00000000..8ce586f9 --- /dev/null +++ b/changelog.md @@ -0,0 +1,205 @@ +# python-saml changelog +### 2.7.0 (Sep 11, 2019) +* Set true as the default value for strict setting + +### 2.6.0 (Jul 02, 2019) +* Adjusted acs endpoint to extract NameQualifier and SPNameQualifier from SAMLResponse. Adjusted single logout service to provide NameQualifier and SPNameQualifier to logout method. Add getNameIdNameQualifier to Auth and SamlResponse. Extend logout method from Auth and LogoutRequest constructor to support SPNameQualifier parameter. Align LogoutRequest constructor with SAML specs +* Added get_in_response_to method to Response and LogoutResponse classes +* Add get_last_authn_contexts method +* Fix bug on friendlyName/nameFormat parameters on RequestedAttribute elements. Wrong variable name caused FriendlyName to overwrite NameFormat +* Add support for Subjects on AuthNRequests by the new name_id_value_req parameeter.Fix testshib test. Improve README: Added inline markup to important references +* Update defusedxml +* Fix path in flask demo + +### 2.5.0 (Jan 29, 2019) +* Security improvements. Use of tagid to prevent XPath injection. Disable DTD on fromstring defusedxml method +* [#239](https://github.com/onelogin/python-saml/issues/239) Check that the response has all of the AuthnContexts that we provided +* Fixed a ValidationError misspelling +* Don't require compression on LogoutResponse messages by relaxing the decode_base64_and_inflate method +* Add expected/received in WRONG_ISSUER error +* If debug enable, print reason for the SAMLResponse invalidation +* [#238](https://github.com/onelogin/python-saml/issues/238) Fix DSA constant +* Start using flake8 for code quality + +### 2.4.2 (Sep 05, 2018) +* Update dm.xmlsec.binding dependency to 1.3.7 +* Update pylint dependency to 1.9.1 +* Update Django demo to use LTS version of Django + +### 2.4.1 (Apr 25, 2018) +* Add ID to EntityDescriptor before sign it on add_sign method. Improve the way ds namespace is handled in add_sign method +* Update defusedxml, coveralls and coverage dependencies +* Update copyright and license reference + +### 2.4.0 (Feb 27, 2018) +* Fix vulnerability [CVE-2017-11427](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11427). Process text of nodes properly, ignoring comments +* Improve how fingerprint is calcultated +* Fix issue with LogoutRequest rejected by ADFS due NameID with unspecified format instead no format attribute +* Be able to invalidate a SAMLResponse if it contains InResponseTo value but no RequestId parameter provided at the is_valid method. See rejectUnsolicitedResponsesWithInResponseTo security parameter (By default deactivated) +* Fix signature position in the SP metadata +* Redefine NSMAP constant + +### 2.3.0 (Sep 15, 2017) +* [#205](https://github.com/onelogin/python-saml/pull/205) Improve decrypt method, Add an option to decrypt an element in place or copy it before decryption. +* [#204](https://github.com/onelogin/python-saml/pull/204) On a LogoutRequest if the NameIdFormat is entity, NameQualifier and SPNameQualifier will be ommited. If the NameIdFormat is not entity and a NameQualifier is provided, then the SPNameQualifier will be also added. +* Be able to get at the auth object the last processed ID (response/assertion) and the last generated ID. +* Reset errorReason attribute of the auth object before each Process method +* Fix issue on getting multiple certs when only sign or encryption certs +* Allow empty nameid if setting wantNameId is false. Only raise Exceptions when strict mode is enabled + +### 2.2.3 (Jun 15, 2017) +* Replace some etree.tostring calls, that were introduced recfently, by the sanitized call provided by defusedxml +* Update dm.xmlsec.binding requirement to 1.3.3 version + +### 2.2.2 (May 18, 2017) +* Be able to relax SSL Certificate verification when retrieving idp metadata +* [#195](https://github.com/onelogin/python-saml/pull/195) Be able to register future SP x509cert on the settings and publish it on SP metadata +* [#195](https://github.com/onelogin/python-saml/pull/195) Be able to register more than 1 Identity Provider x509cert, linked with an specific use (signing or encryption +* [#195](https://github.com/onelogin/python-saml/pull/195) Allow metadata to be retrieved from source containing data of multiple entities +* [#195](https://github.com/onelogin/python-saml/pull/195) Adapt IdP XML metadata parser to take care of multiple IdP certtificates and be able to inject the data obtained on the settings. +* [#194](https://github.com/onelogin/python-saml/pull/194) Publish KeyDescriptor[use=encryption] only when required +* [#190](https://github.com/onelogin/python-saml/pull/190) Checking the status of response before assertion count +* Add Pyramid demo example +* Allows underscores in URL hosts +* NameID Format improvements +* [#184](https://github.com/onelogin/python-saml/pull/184) Be able to provide a NameIDFormat to LogoutRequest +* [#180](https://github.com/onelogin/python-saml/pull/180) Add DigestMethod support. (Add sign_algorithm and digest_algorithm parameters to sign_metadata and add_sign) +* Validate serial number as string to work around libxml2 limitation +* Make the Issuer on the Response Optional + + +### 2.2.1 (Jan 11, 2017) +* [#175](https://github.com/onelogin/python-saml/pull/175) Optionally raise detailed exceptions vs. returning False. +Implement a more specific exception class for handling some validation errors. Improve/Fix tests +* [#171](https://github.com/onelogin/python-saml/pull/171) Add hooks to retrieve last-sent and last-received requests and responses +* Improved inResponse validation on Responses +* [#173](https://github.com/onelogin/python-saml/pull/173) Fix attributeConsumingService serviceName format in README + + +### 2.2.0 (Oct 14, 2016) +* Several security improvements: + * Conditions element required and unique. + * AuthnStatement element required and unique. + * SPNameQualifier must math the SP EntityID + * Reject saml:Attribute element with same “Name” attribute + * Reject empty nameID + * Require Issuer element. (Must match IdP EntityID). + * Destination value can't be blank (if present must match ACS URL). + * Check that the EncryptedAssertion element only contains 1 Assertion element. +* Improve Signature validation process +* [#149](https://github.com/onelogin/python-saml/pull/149) Work-around for xmlsec.initialize +* [#151](https://github.com/onelogin/python-saml/pull/151) Fix flask demo error handling and improve documentation +* [#152](https://github.com/onelogin/python-saml/pull/152) Update LICENSE to include MIT rather than BSD license +* [#155](https://github.com/onelogin/python-saml/pull/155) Fix typographical errors in docstring +* Fix RequestedAttribute Issue +* Fix __build_signature method. If relay_state is null not be part of the SignQuery +* [#164](https://github.com/onelogin/python-saml/pull/164) Add support for non-ascii fields in settings + + +### 2.1.9 (Jun 27, 2016) +* Change the decrypt assertion process. +* Add 2 extra validations to prevent Signature wrapping attacks. + +### 2.1.8 (Jun 02, 2016) +* Fix Metadata XML (RequestedAttribute) +* Fix Windows specific Unix date formatting bug. +* Docs for OSx instlltion of libsecxml1 +* Fix SHA384 Constant URI +* [#142](https://github.com/onelogin/python-saml/pull/142) Refactor of settings.py to make it a little more readable. +* Bugfix for ADFS lowercase signatures +* READMEs suggested wrong cert name + +### 2.1.7 (May 14, 2016) +* [#117](https://github.com/onelogin/python-saml/pull/117) AttributeConsumingService support +* [#114](https://github.com/onelogin/python-saml/pull/114) Compare Assertion InResponseTo if not None +* Return empty list when there are no audience values +* Passing NameQualifier through to logout request +* Make deflate process when retrieving built SAML messages optional +* Add debug parameter to decrypt method +* Fix Idp Metadata parser +* Add documentation related to the new IdP metadata parser methods +* Extract the already encoded value directly from get_data +* [#133](https://github.com/onelogin/python-saml/pull/133) Fix typo and add extra assertions in util decrypt test +* Fix Signature with empty URI support +* Allow AuthnRequest with no NameIDPolicy +* Remove requirement of NameID on SAML responses + +### 2.1.6 (Feb 15, 2016) +* Prevent signature wrapping attack!! +* [#111](https://github.com/onelogin/python-saml/pull/111) Add support for nested `NameID` children inside `AttributeValue`s +* ALOWED Misspell +* Improve how we obtain the settings path. +* Update docs adding reference to test depencence installation +* Fix Organization element on SP metadata. +* [#100](https://github.com/onelogin/python-saml/pull/100) Support Responses that don't have AttributeStatements. + +### 2.1.5 (Nov 3, 2015) +* [#86](https://github.com/onelogin/python-saml/pull/86) Make idp settings optional (Usefull when validating SP metadata) +* [#79](https://github.com/onelogin/python-saml/pull/79) Remove unnecesary dependence. M2crypto is not used. +* [#77](https://github.com/onelogin/python-saml/pull/77) Fix server_port can be None +* Fix bug on settings constructor related to sp_validation_only +* Make SPNameQualifier optional on the generateNameId method. Avoid the use of SPNameQualifier when generating the NameID on the LogoutRequest builder. +* Allows the RequestedAuthnContext Comparison attribute to be set via settings +* Be able to retrieve Session Timeout after processResponse +* Update documentation. Clarify the use of the certFingerprint + +### 2.1.4 (Jul 17, 2015) +* Now the SP is able to select the algorithm to be used on signatures (DSA_SHA1, RSA_SHA1, RSA_SHA256, RSA_SHA384, RSA_SHA512). +* Support sign validation of different kinds of algorithm +* Add demo example of the Bottle framework. +* [#73](https://github.com/onelogin/python-saml/pull/73) Improve decrypt method +* Handle valid but uncommon dsig block with no URI in the reference +* Split the setting check methods. Now 1 method for IdP settings and other for SP settings +* Let the setting object to avoid the IdP setting check. required if we want to publish SP * SAML Metadata when the IdP data is still not provided. + +### 2.1.3 (Jun 25, 2015) +* Do accesible the ID of the object Logout Request (id attribute) +* Add SAMLServiceProviderBackend reference to the README.md +* Solve HTTPs issue on demos +* Fix PHP-style array element in settings json +* Add fingerprint algorithm support. Previously the toolkit assumed SHA-1 algorithm +* Fix creation of metadata with no SLS, when using settings.get_sp_metadata() +* Allow configuration of metadata caching/expiry via settings +* Allow metadata signing with SP key specified as config value, not file +* Set NAMEID_UNSPECIFIED as default NameIDFormat to prevent conflicts +* Improve validUntil/cacheDuration metadata settings + +### 2.1.2 (Feb 26, 2015) +* Fix wrong element order in generated metadata (SLS before NameID). metadata xsd updated +* Added SLO with nameID and SessionIndex in the demos +* Fix Exception message on Destination validation of the Logout_request + +### 2.1.0 (Jan 14, 2015) +* Update the dm.xmlsec.binding library to 1.3.2 (Improved transform support, Workaround for buildout problem) +* Fix flask demo settings example. +* Add nameID & sessionIndex support on Logout Request +* Reject SAML Response if not signed and strict = false +* Add ForceAuh and IsPassive support on AuthN Request + +### 2.0.2 (Dec 5, 2014) +* Adding AuthnContextClassRef support +* Process nested StatusCode +* Fix settings bug + +### 2.0.1 (Nov 13, 2014) +* SSO and SLO (SP-Initiated and IdP-Initiated). +* Assertion and nameId encryption. +* Assertion signature. +* Message signature: AuthNRequest, LogoutRequest, LogoutResponses. +* Enable an Assertion Consumer Service endpoint. +* Enable a Single Logout Service endpoint. +* Publish the SP metadata (which can be signed). + +### 1.1.0 (Sep 4, 2014) +* Security improved, added more checks at the SAMLResponse validation + +### 1.0.0 (Jun 26, 2014) +* OneLogin's SAML Python Toolkit v1.0.0 + + + + + + + + diff --git a/demo-bottle/index.py b/demo-bottle/index.py index bf201066..f6b5d78a 100644 --- a/demo-bottle/index.py +++ b/demo-bottle/index.py @@ -30,12 +30,15 @@ def init_saml_auth(req): def prepare_bottle_request(req): url_data = urlparse(req.url) return { + 'https': 'on' if req.urlparts.scheme == 'https' else 'off', 'http_host': req.get_header('host'), 'server_port': url_data.port, 'script_name': req.fullpath, 'get_data': req.query, 'post_data': req.forms, - 'https': 'on' if req.urlparts.scheme == 'https' else 'off' + # Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144 + # 'lowercase_urlencoding': True, + 'query_string': req.query_string } diff --git a/demo-bottle/saml/advanced_settings.json b/demo-bottle/saml/advanced_settings.json index 4ea002ad..47ec8c28 100644 --- a/demo-bottle/saml/advanced_settings.json +++ b/demo-bottle/saml/advanced_settings.json @@ -7,8 +7,11 @@ "signMetadata": false, "wantMessagesSigned": false, "wantAssertionsSigned": false, + "wantNameId" : true, "wantNameIdEncrypted": false, - "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" + "wantAssertionsEncrypted": false, + "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1" }, "contactPerson": { "technical": { diff --git a/demo-bottle/saml/certs/README b/demo-bottle/saml/certs/README index bcb87f11..7cf0c143 100644 --- a/demo-bottle/saml/certs/README +++ b/demo-bottle/saml/certs/README @@ -2,10 +2,11 @@ Take care of this folder that could contain private key. Be sure that this folde Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as: - * sp.key Private Key - * sp.cert Public cert + * sp.key Private Key + * sp.crt Public cert + * sp_new.crt Future Public cert Also you can use other cert to sign the metadata of the SP using the: * metadata.key - * metadata.cert + * metadata.crt diff --git a/demo-bottle/saml/settings.json b/demo-bottle/saml/settings.json index fdb13acd..7f861e97 100644 --- a/demo-bottle/saml/settings.json +++ b/demo-bottle/saml/settings.json @@ -11,7 +11,7 @@ "url": "https:///?sls", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, - "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified", + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", "x509cert": "", "privateKey": "" }, diff --git a/demo-django/demo/settings.py b/demo-django/demo/settings.py index 3428fdab..5305f382 100644 --- a/demo-django/demo/settings.py +++ b/demo-django/demo/settings.py @@ -22,10 +22,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -TEMPLATE_DEBUG = True - -ALLOWED_HOSTS = [] - +ALLOWED_HOSTS = ['pitbulk.no-ip.org'] # Application definition @@ -85,6 +82,15 @@ SESSION_ENGINE = 'django.contrib.sessions.backends.file' -TEMPLATE_DIRS = ( - os.path.join(BASE_DIR, 'templates'), -) +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': { + 'django.contrib.auth.context_processors.auth' + } + }, + }, +] diff --git a/demo-django/demo/urls.py b/demo-django/demo/urls.py index 4f55a0b3..1f329074 100644 --- a/demo-django/demo/urls.py +++ b/demo-django/demo/urls.py @@ -1,11 +1,12 @@ -from django.conf.urls import patterns, url - +from django.conf.urls import url from django.contrib import admin +from demo.views import index, attrs, metadata + admin.autodiscover() -urlpatterns = patterns( - '', - url(r'^$', 'demo.views.index', name='index'), - url(r'^attrs/$', 'demo.views.attrs', name='attrs'), - url(r'^metadata/$', 'demo.views.metadata', name='metadata'), -) +urlpatterns = [ + url(r'^$', index, name='index'), + url(r'^attrs/$', attrs, name='attrs'), + url(r'^metadata/$', metadata, name='metadata') +] + diff --git a/demo-django/demo/views.py b/demo-django/demo/views.py index a2277366..40ebdd66 100644 --- a/demo-django/demo/views.py +++ b/demo-django/demo/views.py @@ -2,8 +2,7 @@ from django.core.urlresolvers import reverse from django.http import (HttpResponse, HttpResponseRedirect, HttpResponseServerError) -from django.shortcuts import render_to_response -from django.template import RequestContext +from django.shortcuts import render from onelogin.saml2.auth import OneLogin_Saml2_Auth from onelogin.saml2.settings import OneLogin_Saml2_Settings @@ -23,7 +22,10 @@ def prepare_django_request(request): 'script_name': request.META['PATH_INFO'], 'server_port': request.META['SERVER_PORT'], 'get_data': request.GET.copy(), - 'post_data': request.POST.copy() + 'post_data': request.POST.copy(), + # Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144 + # 'lowercase_urlencoding': True, + 'query_string': request.META['QUERY_STRING'] } return result @@ -32,6 +34,7 @@ def index(request): req = prepare_django_request(request) auth = init_saml_auth(req) errors = [] + error_reason = None not_auth_warn = False success_slo = False attributes = False @@ -39,31 +42,61 @@ def index(request): if 'sso' in req['get_data']: return HttpResponseRedirect(auth.login()) + # If AuthNRequest ID need to be stored in order to later validate it, do instead + # sso_built_url = auth.login() + # request.session['AuthNRequestID'] = auth.get_last_request_id() + # return HttpResponseRedirect(sso_built_url) elif 'sso2' in req['get_data']: return_to = OneLogin_Saml2_Utils.get_self_url(req) + reverse('attrs') return HttpResponseRedirect(auth.login(return_to)) elif 'slo' in req['get_data']: - name_id = None - session_index = None + name_id = session_index = name_id_format = name_id_nq = name_id_spnq = None if 'samlNameId' in request.session: name_id = request.session['samlNameId'] if 'samlSessionIndex' in request.session: session_index = request.session['samlSessionIndex'] - - return HttpResponseRedirect(auth.logout(name_id=name_id, session_index=session_index)) + if 'samlNameIdFormat' in request.session: + name_id_format = request.session['samlNameIdFormat'] + if 'samlNameIdNameQualifier' in request.session: + name_id_nq = request.session['samlNameIdNameQualifier'] + if 'samlNameIdSPNameQualifier' in request.session: + name_id_spnq = request.session['samlNameIdSPNameQualifier'] + + return HttpResponseRedirect(auth.logout(name_id=name_id, session_index=session_index, nq=name_id_nq, name_id_format=name_id_format, spnq=name_id_spnq)) + + # If LogoutRequest ID need to be stored in order to later validate it, do instead + # slo_built_url = auth.logout(name_id=name_id, session_index=session_index) + # request.session['LogoutRequestID'] = auth.get_last_request_id() + #return HttpResponseRedirect(slo_built_url) elif 'acs' in req['get_data']: - auth.process_response() + request_id = None + if 'AuthNRequestID' in request.session: + request_id = request.session['AuthNRequestID'] + + auth.process_response(request_id=request_id) errors = auth.get_errors() not_auth_warn = not auth.is_authenticated() + if not errors: + if 'AuthNRequestID' in request.session: + del request.session['AuthNRequestID'] request.session['samlUserdata'] = auth.get_attributes() request.session['samlNameId'] = auth.get_nameid() + request.session['samlNameIdFormat'] = auth.get_nameid_format() + request.session['samlNameIdNameQualifier'] = auth.get_nameid_nq() + request.session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq() request.session['samlSessionIndex'] = auth.get_session_index() if 'RelayState' in req['post_data'] and OneLogin_Saml2_Utils.get_self_url(req) != req['post_data']['RelayState']: return HttpResponseRedirect(auth.redirect_to(req['post_data']['RelayState'])) + else: + if auth.get_settings().is_debug_active(): + error_reason = auth.get_last_error_reason() elif 'sls' in req['get_data']: + request_id = None + if 'LogoutRequestID' in request.session: + request_id = request.session['LogoutRequestID'] dscb = lambda: request.session.flush() - url = auth.process_slo(delete_session_cb=dscb) + url = auth.process_slo(request_id=request_id, delete_session_cb=dscb) errors = auth.get_errors() if len(errors) == 0: if url is not None: @@ -76,13 +109,8 @@ def index(request): if len(request.session['samlUserdata']) > 0: attributes = request.session['samlUserdata'].items() - return render_to_response('index.html', - {'errors': errors, - 'not_auth_warn': not_auth_warn, - 'success_slo': success_slo, - 'attributes': attributes, - 'paint_logout': paint_logout}, - context_instance=RequestContext(request)) + return render(request, 'index.html', {'errors': errors, 'error_reason': error_reason, not_auth_warn: not_auth_warn, 'success_slo': success_slo, + 'attributes': attributes, 'paint_logout': paint_logout}) def attrs(request): @@ -94,10 +122,9 @@ def attrs(request): if len(request.session['samlUserdata']) > 0: attributes = request.session['samlUserdata'].items() - return render_to_response('attrs.html', - {'paint_logout': paint_logout, - 'attributes': attributes}, - context_instance=RequestContext(request)) + return render(request, 'attrs.html', + {'paint_logout': paint_logout, + 'attributes': attributes}) def metadata(request): diff --git a/demo-django/requirements.txt b/demo-django/requirements.txt index 5a55855e..f305b2f2 100644 --- a/demo-django/requirements.txt +++ b/demo-django/requirements.txt @@ -1 +1 @@ -Django==1.6.5 +Django==1.11 diff --git a/demo-django/saml/advanced_settings.json b/demo-django/saml/advanced_settings.json index 97f3a374..7efb5d1b 100644 --- a/demo-django/saml/advanced_settings.json +++ b/demo-django/saml/advanced_settings.json @@ -7,8 +7,11 @@ "signMetadata": false, "wantMessagesSigned": false, "wantAssertionsSigned": false, + "wantNameId" : true, "wantNameIdEncrypted": false, - "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" + "wantAssertionsEncrypted": false, + "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1" }, "contactPerson": { "technical": { diff --git a/demo-django/saml/certs/README b/demo-django/saml/certs/README index bcb87f11..7cf0c143 100644 --- a/demo-django/saml/certs/README +++ b/demo-django/saml/certs/README @@ -2,10 +2,11 @@ Take care of this folder that could contain private key. Be sure that this folde Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as: - * sp.key Private Key - * sp.cert Public cert + * sp.key Private Key + * sp.crt Public cert + * sp_new.crt Future Public cert Also you can use other cert to sign the metadata of the SP using the: * metadata.key - * metadata.cert + * metadata.crt diff --git a/demo-django/saml/settings.json b/demo-django/saml/settings.json index cafd090b..3758746c 100644 --- a/demo-django/saml/settings.json +++ b/demo-django/saml/settings.json @@ -2,29 +2,29 @@ "strict": true, "debug": true, "sp": { - "entityId": "https:///metadata/", + "entityId": "http://pitbulk.no-ip.org:8000/metadata/", "assertionConsumerService": { - "url": "https:///?acs", + "url": "http://pitbulk.no-ip.org:8000/?acs", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" }, "singleLogoutService": { - "url": "https:///?sls", + "url": "http://pitbulk.no-ip.org:8000/?sls", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, - "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified", - "x509cert": "", - "privateKey": "" + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "x509cert": "MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m", + "privateKey": "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAECgYEA0wDXZPS9hKqMTNh+nnfONioXBjhA6fQ7GVtWKDxa3ofMoPyt7ejGL/Hnvcv13Vn02UAsFx1bKrCstDqVtYwrWrnmywXyH+o9paJnTmd+cRIjWU8mRvCrxzH5I/Bcvbp1qZoASuqZEaGwNjM6JpW2o3QTmHGMALcLUPfEvhApssECQQDy2e65E86HcFhi/Ta8TQ0odDCNbiWA0bI1Iu8B7z+NAy1D1+WnCd7w2u9U6CF/k2nFHCsvxEoeANM0z7h5T/XvAkEA8e4JqKmDrfdiakQT7nf9svU2jXZtxSbPiIRMafNikDvzZ1vJCZkvdmaWYL70GlDZIwc9ad67rHZ/n/fqX1d0MQJAbRpRsJ5gY+KqItbFt3UaWzlP8sowWR5cZJjsLb9RmsV5mYguKYw6t5R0f33GRu1wUFimYlBaR/5w5MIJi57LywJATO1a5uWX+G5MPewNxmsjIY91XEAHIYR4wzkGLz5z3dciS4BVCZdLD0QJlxPA/MkuckPwFET9uhYn+M7VGKHvUQJBANSDwsY+BdCGpi/WRV37HUfwLl07damaFbW3h08PQx8G8SuF7DpN+FPBcI6VhzrIWNRBxWprkgeGioKNfFWzSaM=" }, "idp": { - "entityId": "https://app.onelogin.com/saml/metadata/", + "entityId": "https://app.onelogin.com/saml/metadata/3dbd155e-be64-4a4d-8fab-e44788bce74f", "singleSignOnService": { - "url": "https://app.onelogin.com/trust/saml2/http-post/sso/", + "url": "https://sgarcia-us-preprod.onelogin.com/trust/saml2/http-redirect/sso/850162", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, "singleLogoutService": { - "url": "https://app.onelogin.com/trust/saml2/http-redirect/slo/", + "url": "https://sgarcia-us-preprod.onelogin.com/trust/saml2/http-redirect/slo/850162", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, - "x509cert": "" + "x509cert": "MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJjaWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwWT25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUyMjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRwtnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xxVRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCBpIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaCFD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXMGI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65chjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIBvlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZWQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==" } -} \ No newline at end of file +} diff --git a/demo-django/templates/index.html b/demo-django/templates/index.html index f7d51101..87f2f08b 100644 --- a/demo-django/templates/index.html +++ b/demo-django/templates/index.html @@ -10,6 +10,9 @@
  • {{err}}
  • {% endfor %} + {% if error_reason %} +

    Reason: {{error_reason}}

    + {% endif %} {% endif %} diff --git a/demo-flask/index.py b/demo-flask/index.py index a2eb1cb1..e4c11c6f 100644 --- a/demo-flask/index.py +++ b/demo-flask/index.py @@ -11,7 +11,7 @@ app = Flask(__name__) app.config['SECRET_KEY'] = 'onelogindemopytoolkit' -app.config['SAML_PATH'] = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'saml') +app.config['SAML_PATH'] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'saml') def init_saml_auth(req): @@ -28,7 +28,10 @@ def prepare_flask_request(request): 'server_port': url_data.port, 'script_name': request.path, 'get_data': request.args.copy(), - 'post_data': request.form.copy() + 'post_data': request.form.copy(), + # Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144 + # 'lowercase_urlencoding': True, + 'query_string': request.query_string } @@ -44,32 +47,56 @@ def index(): if 'sso' in request.args: return redirect(auth.login()) + # If AuthNRequest ID need to be stored in order to later validate it, do instead + # sso_built_url = auth.login() + # request.session['AuthNRequestID'] = auth.get_last_request_id() + # return redirect(sso_built_url) elif 'sso2' in request.args: return_to = '%sattrs/' % request.host_url return redirect(auth.login(return_to)) elif 'slo' in request.args: - name_id = None - session_index = None + name_id = session_index = name_id_format = name_id_nq = name_id_spnq = None if 'samlNameId' in session: name_id = session['samlNameId'] if 'samlSessionIndex' in session: session_index = session['samlSessionIndex'] - - return redirect(auth.logout(name_id=name_id, session_index=session_index)) + if 'samlNameIdFormat' in session: + name_id_format = session['samlNameIdFormat'] + if 'samlNameIdNameQualifier' in session: + name_id_nq = session['samlNameIdNameQualifier'] + if 'samlNameIdSPNameQualifier' in session: + name_id_spnq = session['samlNameIdSPNameQualifier'] + + return redirect(auth.logout(name_id=name_id, session_index=session_index, nq=name_id_nq, name_id_format=name_id_format, spnq=name_id_spnq)) + # If LogoutRequest ID need to be stored in order to later validate it, do instead + # slo_built_url = auth.logout(name_id=name_id, session_index=session_index) + # session['LogoutRequestID'] = auth.get_last_request_id() + # return redirect(slo_built_url) elif 'acs' in request.args: - auth.process_response() + request_id = None + if 'AuthNRequestID' in session: + request_id = session['AuthNRequestID'] + + auth.process_response(request_id=request_id) errors = auth.get_errors() not_auth_warn = not auth.is_authenticated() if len(errors) == 0: + if 'AuthNRequestID' in session: + del session['AuthNRequestID'] session['samlUserdata'] = auth.get_attributes() - session['samlNameId'] = auth.get_nameid() + session['samlNameIdFormat'] = auth.get_nameid_format() + session['samlNameIdNameQualifier'] = auth.get_nameid_nq() + session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq() session['samlSessionIndex'] = auth.get_session_index() self_url = OneLogin_Saml2_Utils.get_self_url(req) if 'RelayState' in request.form and self_url != request.form['RelayState']: return redirect(auth.redirect_to(request.form['RelayState'])) elif 'sls' in request.args: + request_id = None + if 'LogoutRequestID' in session: + request_id = session['LogoutRequestID'] dscb = lambda: session.clear() - url = auth.process_slo(delete_session_cb=dscb) + url = auth.process_slo(request_id=request_id, delete_session_cb=dscb) errors = auth.get_errors() if len(errors) == 0: if url is not None: @@ -118,7 +145,7 @@ def metadata(): resp = make_response(metadata, 200) resp.headers['Content-Type'] = 'text/xml' else: - resp = make_response(errors.join(', '), 500) + resp = make_response(', '.join(errors), 500) return resp diff --git a/demo-flask/saml/advanced_settings.json b/demo-flask/saml/advanced_settings.json index 97f3a374..7efb5d1b 100644 --- a/demo-flask/saml/advanced_settings.json +++ b/demo-flask/saml/advanced_settings.json @@ -7,8 +7,11 @@ "signMetadata": false, "wantMessagesSigned": false, "wantAssertionsSigned": false, + "wantNameId" : true, "wantNameIdEncrypted": false, - "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" + "wantAssertionsEncrypted": false, + "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1" }, "contactPerson": { "technical": { diff --git a/demo-flask/saml/certs/README b/demo-flask/saml/certs/README index bcb87f11..7cf0c143 100644 --- a/demo-flask/saml/certs/README +++ b/demo-flask/saml/certs/README @@ -2,10 +2,11 @@ Take care of this folder that could contain private key. Be sure that this folde Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as: - * sp.key Private Key - * sp.cert Public cert + * sp.key Private Key + * sp.crt Public cert + * sp_new.crt Future Public cert Also you can use other cert to sign the metadata of the SP using the: * metadata.key - * metadata.cert + * metadata.crt diff --git a/demo-flask/saml/settings.json b/demo-flask/saml/settings.json index 142911f1..ec40b674 100644 --- a/demo-flask/saml/settings.json +++ b/demo-flask/saml/settings.json @@ -11,7 +11,7 @@ "url": "https:///?sls", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, - "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified", + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", "x509cert": "", "privateKey": "" }, diff --git a/demo_pyramid/.coveragerc b/demo_pyramid/.coveragerc new file mode 100644 index 00000000..fd429eb0 --- /dev/null +++ b/demo_pyramid/.coveragerc @@ -0,0 +1,3 @@ +[run] +source = demo_pyramid +omit = demo_pyramid/test* diff --git a/demo_pyramid/.gitignore b/demo_pyramid/.gitignore new file mode 100644 index 00000000..bd31ad13 --- /dev/null +++ b/demo_pyramid/.gitignore @@ -0,0 +1,22 @@ +*.egg +*.egg-info +*.pyc +*$py.class +*~ +.coverage +coverage.xml +build/ +dist/ +.tox/ +nosetests.xml +env*/ +tmp/ +.cache/* +Data.fs* +*.sublime-project +*.sublime-workspace +.*.sw? +.sw? +.DS_Store +coverage +test diff --git a/demo_pyramid/CHANGES.txt b/demo_pyramid/CHANGES.txt new file mode 100644 index 00000000..14b902fd --- /dev/null +++ b/demo_pyramid/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version. diff --git a/demo_pyramid/MANIFEST.in b/demo_pyramid/MANIFEST.in new file mode 100644 index 00000000..3b3962e4 --- /dev/null +++ b/demo_pyramid/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include demo_pyramid *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2 diff --git a/demo_pyramid/README.txt b/demo_pyramid/README.txt new file mode 100644 index 00000000..13d16ded --- /dev/null +++ b/demo_pyramid/README.txt @@ -0,0 +1,29 @@ +demo_pyramid +=============================== + +Getting Started +--------------- + +- Change directory into your newly created project. + + cd demo_pyramid + +- Create a Python virtual environment. + + python -m venv env + +- Upgrade packaging tools. + + env/bin/pip install --upgrade pip setuptools + +- Install the project in editable mode with its testing requirements. + + env/bin/pip install -e ".[testing]" + +- Run your project's tests. + + env/bin/pytest + +- Run your project. + + env/bin/pserve development.ini diff --git a/demo_pyramid/demo_pyramid/__init__.py b/demo_pyramid/demo_pyramid/__init__.py new file mode 100644 index 00000000..805e51d1 --- /dev/null +++ b/demo_pyramid/demo_pyramid/__init__.py @@ -0,0 +1,19 @@ +from pyramid.config import Configurator +from pyramid.session import SignedCookieSessionFactory + + +session_factory = SignedCookieSessionFactory('onelogindemopytoolkit') + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + config = Configurator(settings=settings) + config.set_session_factory(session_factory) + config.include('pyramid_jinja2') + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('index', '/') + config.add_route('attrs', '/attrs/') + config.add_route('metadata', '/metadata/') + config.scan() + return config.make_wsgi_app() diff --git a/demo_pyramid/demo_pyramid/saml/advanced_settings.json b/demo_pyramid/demo_pyramid/saml/advanced_settings.json new file mode 100644 index 00000000..3115e17e --- /dev/null +++ b/demo_pyramid/demo_pyramid/saml/advanced_settings.json @@ -0,0 +1,33 @@ +{ + "security": { + "nameIdEncrypted": false, + "authnRequestsSigned": false, + "logoutRequestSigned": false, + "logoutResponseSigned": false, + "signMetadata": false, + "wantMessagesSigned": false, + "wantAssertionsSigned": false, + "wantNameId" : true, + "wantNameIdEncrypted": false, + "wantAssertionsEncrypted": false, + "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1" + }, + "contactPerson": { + "technical": { + "givenName": "technical_name", + "emailAddress": "technical@example.com" + }, + "support": { + "givenName": "support_name", + "emailAddress": "support@example.com" + } + }, + "organization": { + "en-US": { + "name": "sp_test", + "displayname": "SP test", + "url": "http://sp.example.com" + } + } +} \ No newline at end of file diff --git a/demo_pyramid/demo_pyramid/saml/certs/README b/demo_pyramid/demo_pyramid/saml/certs/README new file mode 100644 index 00000000..7cf0c143 --- /dev/null +++ b/demo_pyramid/demo_pyramid/saml/certs/README @@ -0,0 +1,12 @@ +Take care of this folder that could contain private key. Be sure that this folder never is published. + +Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as: + + * sp.key Private Key + * sp.crt Public cert + * sp_new.crt Future Public cert + +Also you can use other cert to sign the metadata of the SP using the: + + * metadata.key + * metadata.crt diff --git a/demo_pyramid/demo_pyramid/saml/settings.json b/demo_pyramid/demo_pyramid/saml/settings.json new file mode 100644 index 00000000..ec40b674 --- /dev/null +++ b/demo_pyramid/demo_pyramid/saml/settings.json @@ -0,0 +1,30 @@ +{ + "strict": true, + "debug": true, + "sp": { + "entityId": "https:///metadata/", + "assertionConsumerService": { + "url": "https:///?acs", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + "singleLogoutService": { + "url": "https:///?sls", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "x509cert": "", + "privateKey": "" + }, + "idp": { + "entityId": "https://app.onelogin.com/saml/metadata/", + "singleSignOnService": { + "url": "https://app.onelogin.com/trust/saml2/http-post/sso/", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "singleLogoutService": { + "url": "https://app.onelogin.com/trust/saml2/http-redirect/slo/", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "x509cert": "" + } +} diff --git a/demo_pyramid/demo_pyramid/static/pyramid-16x16.png b/demo_pyramid/demo_pyramid/static/pyramid-16x16.png new file mode 100644 index 00000000..97920311 Binary files /dev/null and b/demo_pyramid/demo_pyramid/static/pyramid-16x16.png differ diff --git a/demo_pyramid/demo_pyramid/static/pyramid.png b/demo_pyramid/demo_pyramid/static/pyramid.png new file mode 100644 index 00000000..4ab837be Binary files /dev/null and b/demo_pyramid/demo_pyramid/static/pyramid.png differ diff --git a/demo_pyramid/demo_pyramid/static/theme.css b/demo_pyramid/demo_pyramid/static/theme.css new file mode 100644 index 00000000..0f4b1a4d --- /dev/null +++ b/demo_pyramid/demo_pyramid/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/demo_pyramid/demo_pyramid/templates/attrs.jinja2 b/demo_pyramid/demo_pyramid/templates/attrs.jinja2 new file mode 100644 index 00000000..f83ea3be --- /dev/null +++ b/demo_pyramid/demo_pyramid/templates/attrs.jinja2 @@ -0,0 +1,31 @@ +{% extends "layout.jinja2" %} + +{% block content %} + +{% if paint_logout %} + {% if attributes %} +

    You have the following attributes:

    + + + + + + {% for attr in attributes %} + + + {% endfor %} + +
    NameValues
    {{ attr.0 }}
      + {% for val in attr.1 %} +
    • {{ val }}
    • + {% endfor %} +
    + {% else %} + + {% endif %} + Logout +{% else %} + Login and access again to this page +{% endif %} + +{% endblock %} diff --git a/demo_pyramid/demo_pyramid/templates/index.jinja2 b/demo_pyramid/demo_pyramid/templates/index.jinja2 new file mode 100644 index 00000000..65bcedd6 --- /dev/null +++ b/demo_pyramid/demo_pyramid/templates/index.jinja2 @@ -0,0 +1,55 @@ +{% extends "layout.jinja2" %} + +{% block content %} + +
    +

    Pyramid Starter project

    +

    Welcome to demo_pyramid, a Pyramid application generated by
    Cookiecutter.

    +
    + +{% if errors %} + +{% endif %} + +{% if not_auth_warn %} + +{% endif %} + +{% if success_slo %} + +{% endif %} + +{% if paint_logout %} + {% if attributes %} + + + + + + {% for attr in attributes %} + + + {% endfor %} + +
    NameValues
    {{ attr.0 }}
      + {% for val in attr.1 %} +
    • {{ val }}
    • + {% endfor %} +
    + {% else %} + + {% endif %} + Logout +{% else %} + Login Login and access to attrs page +{% endif %} + +{% endblock %} diff --git a/demo_pyramid/demo_pyramid/templates/layout.jinja2 b/demo_pyramid/demo_pyramid/templates/layout.jinja2 new file mode 100644 index 00000000..57ad87c0 --- /dev/null +++ b/demo_pyramid/demo_pyramid/templates/layout.jinja2 @@ -0,0 +1,64 @@ + + + + + + + + + + + Cookiecutter Starter project for the Pyramid Web Framework + + + + + + + + + + + + + +
    +
    +
    +
    + +
    +
    + {% block content %} +

    No content

    + {% endblock content %} +
    +
    + +
    + +
    +
    +
    + + + + + + + + diff --git a/demo_pyramid/demo_pyramid/views.py b/demo_pyramid/demo_pyramid/views.py new file mode 100644 index 00000000..86814a99 --- /dev/null +++ b/demo_pyramid/demo_pyramid/views.py @@ -0,0 +1,126 @@ +import os + +from pyramid.httpexceptions import (HTTPFound, HTTPInternalServerError, HTTPOk,) +from pyramid.view import view_config + +from onelogin.saml2.auth import OneLogin_Saml2_Auth +from onelogin.saml2.utils import OneLogin_Saml2_Utils + +SAML_PATH = os.path.join(os.path.dirname(__file__), 'saml') + + +def init_saml_auth(req): + auth = OneLogin_Saml2_Auth(req, custom_base_path=SAML_PATH) + return auth + + +def prepare_pyramid_request(request): + # If server is behind proxys or balancers use the HTTP_X_FORWARDED fields + return { + 'https': 'on' if request.scheme == 'https' else 'off', + 'http_host': request.host, + 'server_port': request.server_port, + 'script_name': request.path, + 'get_data': request.GET.copy(), + # Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144 + # 'lowercase_urlencoding': True, + 'post_data': request.POST.copy(), + } + + +@view_config(route_name='index', renderer='templates/index.jinja2') +def index(request): + req = prepare_pyramid_request(request) + auth = init_saml_auth(req) + errors = [] + error_reason = "" + not_auth_warn = False + success_slo = False + attributes = False + paint_logout = False + + session = request.session + + if 'sso' in request.GET: + return HTTPFound(auth.login()) + elif 'sso2' in request.GET: + return_to = '%s/attrs/' % request.host_url + return HTTPFound(auth.login(return_to)) + elif 'slo' in request.GET: + name_id = None + session_index = None + if 'samlNameId' in session: + name_id = session['samlNameId'] + if 'samlSessionIndex' in session: + session_index = session['samlSessionIndex'] + + return HTTPFound(auth.logout(name_id=name_id, session_index=session_index)) + elif 'acs' in request.GET: + auth.process_response() + errors = auth.get_errors() + not_auth_warn = not auth.is_authenticated() + if len(errors) == 0: + session['samlUserdata'] = auth.get_attributes() + session['samlNameId'] = auth.get_nameid() + session['samlSessionIndex'] = auth.get_session_index() + self_url = OneLogin_Saml2_Utils.get_self_url(req) + if 'RelayState' in request.POST and self_url != request.POST['RelayState']: + return HTTPFound(auth.redirect_to(request.POST['RelayState'])) + else: + error_reason = auth.get_last_error_reason() + elif 'sls' in request.GET: + dscb = lambda: session.clear() + url = auth.process_slo(delete_session_cb=dscb) + errors = auth.get_errors() + if len(errors) == 0: + if url is not None: + return HTTPFound(url) + else: + success_slo = True + + if 'samlUserdata' in session: + paint_logout = True + if len(session['samlUserdata']) > 0: + attributes = session['samlUserdata'].items() + + return { + 'errors': errors, + 'error_reason': error_reason, + 'not_auth_warn': not_auth_warn, + 'success_slo': success_slo, + 'attributes': attributes, + 'paint_logout': paint_logout, + } + + +@view_config(route_name='attrs', renderer='templates/attrs.jinja2') +def attrs(request): + paint_logout = False + attributes = False + + session = request.session + + if 'samlUserdata' in session: + paint_logout = True + if len(session['samlUserdata']) > 0: + attributes = session['samlUserdata'].items() + + return { + 'paint_logout': paint_logout, + 'attributes': attributes, + } + + +@view_config(route_name='metadata', renderer='html') +def metadata(request): + req = prepare_pyramid_request(request) + auth = init_saml_auth(req) + settings = auth.get_settings() + metadata = settings.get_sp_metadata() + errors = settings.validate_metadata(metadata) + + if len(errors) == 0: + resp = HTTPOk(body=metadata, headers={'Content-Type': 'text/xml'}) + else: + resp = HTTPInternalServerError(body=', '.join(errors)) + return resp diff --git a/demo_pyramid/development.ini b/demo_pyramid/development.ini new file mode 100644 index 00000000..64042ee7 --- /dev/null +++ b/demo_pyramid/development.ini @@ -0,0 +1,59 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:demo_pyramid + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = 127.0.0.1:6543 [::1]:6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, demo_pyramid + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_demo_pyramid] +level = DEBUG +handlers = +qualname = demo_pyramid + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/demo_pyramid/production.ini b/demo_pyramid/production.ini new file mode 100644 index 00000000..84b482ae --- /dev/null +++ b/demo_pyramid/production.ini @@ -0,0 +1,53 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:demo_pyramid + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = *:6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, demo_pyramid + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_demo_pyramid] +level = WARN +handlers = +qualname = demo_pyramid + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/demo_pyramid/setup.py b/demo_pyramid/setup.py new file mode 100644 index 00000000..a30a8ddd --- /dev/null +++ b/demo_pyramid/setup.py @@ -0,0 +1,45 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'waitress', + 'xmlsec', + 'isodate', + 'python-saml', +] + +setup( + name='demo_pyramid', + version='0.0', + description='demo_pyramid', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + 'Programming Language :: Python', + 'Framework :: Pyramid', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', + ], + author='', + author_email='', + url='', + keywords='web pyramid pylons', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + install_requires=requires, + entry_points={ + 'paste.app_factory': [ + 'main = demo_pyramid:main', + ], + }, +) diff --git a/docs/saml2/_modules/saml2/auth.html b/docs/saml2/_modules/saml2/auth.html index 82d6e960..243bd4db 100644 --- a/docs/saml2/_modules/saml2/auth.html +++ b/docs/saml2/_modules/saml2/auth.html @@ -51,8 +51,8 @@

    Navigation

    Source code for saml2.auth

     # -*- coding: utf-8 -*-
     
    -# Copyright (c) 2014, OneLogin, Inc.
    -# All rights reserved.
    +# Copyright (c) 2010-2018 OneLogin, Inc.
    +# MIT License
     
     from base64 import b64encode
     from urllib import urlencode, quote
    diff --git a/docs/saml2/_modules/saml2/authn_request.html b/docs/saml2/_modules/saml2/authn_request.html
    index 674eae42..207ee7a6 100644
    --- a/docs/saml2/_modules/saml2/authn_request.html
    +++ b/docs/saml2/_modules/saml2/authn_request.html
    @@ -51,8 +51,8 @@ 

    Navigation

    Source code for saml2.authn_request

     # -*- coding: utf-8 -*-
     
    -# Copyright (c) 2014, OneLogin, Inc.
    -# All rights reserved.
    +# Copyright (c) 2010-2018 OneLogin, Inc.
    +# MIT License
     
     from base64 import b64encode
     from datetime import datetime
    diff --git a/docs/saml2/_modules/saml2/constants.html b/docs/saml2/_modules/saml2/constants.html
    index 3b098931..306ec1b3 100644
    --- a/docs/saml2/_modules/saml2/constants.html
    +++ b/docs/saml2/_modules/saml2/constants.html
    @@ -51,8 +51,8 @@ 

    Navigation

    Source code for saml2.constants

     # -*- coding: utf-8 -*-
     
    -# Copyright (c) 2014, OneLogin, Inc.
    -# All rights reserved.
    +# Copyright (c) 2010-2018 OneLogin, Inc.
    +# MIT License
     
     
     
    [docs]class OneLogin_Saml2_Constants: diff --git a/docs/saml2/_modules/saml2/errors.html b/docs/saml2/_modules/saml2/errors.html index e33ebb39..ef958f31 100644 --- a/docs/saml2/_modules/saml2/errors.html +++ b/docs/saml2/_modules/saml2/errors.html @@ -51,8 +51,8 @@

    Navigation

    Source code for saml2.errors

     # -*- coding: utf-8 -*-
     
    -# Copyright (c) 2014, OneLogin, Inc.
    -# All rights reserved.
    +# Copyright (c) 2010-2018 OneLogin, Inc.
    +# MIT License
     
     
     
    [docs]class OneLogin_Saml2_Error(Exception): diff --git a/docs/saml2/_modules/saml2/logout_request.html b/docs/saml2/_modules/saml2/logout_request.html index b6cddbc1..1b690dd4 100644 --- a/docs/saml2/_modules/saml2/logout_request.html +++ b/docs/saml2/_modules/saml2/logout_request.html @@ -51,8 +51,8 @@

    Navigation

    Source code for saml2.logout_request

     # -*- coding: utf-8 -*-
     
    -# Copyright (c) 2014, OneLogin, Inc.
    -# All rights reserved.
    +# Copyright (c) 2010-2018 OneLogin, Inc.
    +# MIT License
     
     from base64 import b64decode
     from datetime import datetime
    diff --git a/docs/saml2/_modules/saml2/logout_response.html b/docs/saml2/_modules/saml2/logout_response.html
    index dc6037d1..3ea3bbf7 100644
    --- a/docs/saml2/_modules/saml2/logout_response.html
    +++ b/docs/saml2/_modules/saml2/logout_response.html
    @@ -51,8 +51,8 @@ 

    Navigation

    Source code for saml2.logout_response

     # -*- coding: utf-8 -*-
     
    -# Copyright (c) 2014, OneLogin, Inc.
    -# All rights reserved.
    +# Copyright (c) 2010-2018 OneLogin, Inc.
    +# MIT License
     
     from base64 import b64decode
     from datetime import datetime
    diff --git a/docs/saml2/_modules/saml2/metadata.html b/docs/saml2/_modules/saml2/metadata.html
    index 055db317..7b0a1be5 100644
    --- a/docs/saml2/_modules/saml2/metadata.html
    +++ b/docs/saml2/_modules/saml2/metadata.html
    @@ -51,8 +51,8 @@ 

    Navigation

    Source code for saml2.metadata

     # -*- coding: utf-8 -*-
     
    -# Copyright (c) 2014, OneLogin, Inc.
    -# All rights reserved.
    +# Copyright (c) 2010-2018 OneLogin, Inc.
    +# MIT License
     
     from time import gmtime, strftime
     from datetime import datetime
    diff --git a/docs/saml2/_modules/saml2/response.html b/docs/saml2/_modules/saml2/response.html
    index 219be1b8..d3fee4e3 100644
    --- a/docs/saml2/_modules/saml2/response.html
    +++ b/docs/saml2/_modules/saml2/response.html
    @@ -51,8 +51,8 @@ 

    Navigation

    Source code for saml2.response

     # -*- coding: utf-8 -*-
     
    -# Copyright (c) 2014, OneLogin, Inc.
    -# All rights reserved.
    +# Copyright (c) 2010-2018 OneLogin, Inc.
    +# MIT License
     
     from base64 import b64decode
     from copy import deepcopy
    diff --git a/docs/saml2/_modules/saml2/settings.html b/docs/saml2/_modules/saml2/settings.html
    index c18ebdf9..42800b51 100644
    --- a/docs/saml2/_modules/saml2/settings.html
    +++ b/docs/saml2/_modules/saml2/settings.html
    @@ -51,8 +51,8 @@ 

    Navigation

    Source code for saml2.settings

     # -*- coding: utf-8 -*-
     
    -# Copyright (c) 2014, OneLogin, Inc.
    -# All rights reserved.
    +# Copyright (c) 2010-2018 OneLogin, Inc.
    +# MIT License
     
     from datetime import datetime
     import json
    diff --git a/docs/saml2/_modules/saml2/utils.html b/docs/saml2/_modules/saml2/utils.html
    index 726ba9dd..613d2a09 100644
    --- a/docs/saml2/_modules/saml2/utils.html
    +++ b/docs/saml2/_modules/saml2/utils.html
    @@ -51,8 +51,8 @@ 

    Navigation

    Source code for saml2.utils

     # -*- coding: utf-8 -*-
     
    -# Copyright (c) 2014, OneLogin, Inc.
    -# All rights reserved.
    +# Copyright (c) 2010-2018 OneLogin, Inc.
    +# MIT License
     
     import base64
     from datetime import datetime
    diff --git a/setup.py b/setup.py
    index faf8612b..7d545fb0 100644
    --- a/setup.py
    +++ b/setup.py
    @@ -1,18 +1,18 @@
     #! /usr/bin/env python
     # -*- coding: utf-8 -*-
     
    -# Copyright (c) 2014, OneLogin, Inc.
    -# All rights reserved.
    +# Copyright (c) 2010-2018 OneLogin, Inc.
    +# MIT License
     
     from setuptools import setup
     
     
     setup(
         name='python-saml',
    -    version='2.1.4',
    +    version='2.7.0',
         description='Onelogin Python Toolkit. Add SAML support to your Python software using this library',
         classifiers=[
    -        'Development Status :: 4 - Beta',
    +        'Development Status :: 5 - Production/Stable',
             'Intended Audience :: Developers',
             'Intended Audience :: System Administrators',
             'Operating System :: OS Independent',
    @@ -22,9 +22,9 @@
         author_email='support@onelogin.com',
         license='MIT',
         url='https://github.com/onelogin/python-saml',
    -    packages=['onelogin','onelogin/saml2'],
    +    packages=['onelogin', 'onelogin/saml2'],
         include_package_data=True,
    -    package_data = {
    +    package_data={
                 'onelogin/saml2/schemas': ['*.xsd'],
         },
         package_dir={
    @@ -32,18 +32,17 @@
         },
         test_suite='tests',
         install_requires=[
    -        'M2Crypto==0.22.3',
    -        'dm.xmlsec.binding==1.3.2',
    -        'isodate==0.5.0',
    -        'defusedxml==0.4.1',
    +        'dm.xmlsec.binding==1.3.7',
    +        'isodate>=0.5.0',
    +        'defusedxml>=0.6.0',
         ],
         extras_require={
             'test': (
    -            'coverage==3.7.1',
    -            'pylint==1.3.1',
    -            'pep8==1.5.7',
    -            'pyflakes==0.8.1',
    -            'coveralls==0.4.4',
    +            'coverage>=3.6',
    +            'freezegun==0.3.5',
    +            'pylint==1.9.1',
    +            'flake8==3.6.0',
    +            'coveralls==1.1',
             ),
         },
         keywords='saml saml2 xmlsec django flask',
    diff --git a/src/onelogin/__init__.py b/src/onelogin/__init__.py
    index 110bc9df..ba664a65 100644
    --- a/src/onelogin/__init__.py
    +++ b/src/onelogin/__init__.py
    @@ -1,8 +1,8 @@
     # -*- coding: utf-8 -*-
     
     """
    -Copyright (c) 2014, OneLogin, Inc.
    -All rights reserved.
    +Copyright (c) 2010-2018 OneLogin, Inc.
    +MIT License
     
     Add SAML support to your Python softwares using this library.
     Forget those complicated libraries and use that open source
    diff --git a/src/onelogin/saml2/__init__.py b/src/onelogin/saml2/__init__.py
    index 110bc9df..ba664a65 100644
    --- a/src/onelogin/saml2/__init__.py
    +++ b/src/onelogin/saml2/__init__.py
    @@ -1,8 +1,8 @@
     # -*- coding: utf-8 -*-
     
     """
    -Copyright (c) 2014, OneLogin, Inc.
    -All rights reserved.
    +Copyright (c) 2010-2018 OneLogin, Inc.
    +MIT License
     
     Add SAML support to your Python softwares using this library.
     Forget those complicated libraries and use that open source
    diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
    index 49cbfbce..188e289e 100644
    --- a/src/onelogin/saml2/auth.py
    +++ b/src/onelogin/saml2/auth.py
    @@ -2,8 +2,8 @@
     
     """ OneLogin_Saml2_Auth class
     
    -Copyright (c) 2014, OneLogin, Inc.
    -All rights reserved.
    +Copyright (c) 2010-2018 OneLogin, Inc.
    +MIT License
     
     Main class of OneLogin's Python Toolkit.
     
    @@ -13,15 +13,14 @@
     
     from base64 import b64encode
     from urllib import quote_plus
    -
    -import dm.xmlsec.binding as xmlsec
    +from defusedxml.lxml import tostring
     
     from onelogin.saml2.settings import OneLogin_Saml2_Settings
     from onelogin.saml2.response import OneLogin_Saml2_Response
     from onelogin.saml2.errors import OneLogin_Saml2_Error
     from onelogin.saml2.logout_response import OneLogin_Saml2_Logout_Response
     from onelogin.saml2.constants import OneLogin_Saml2_Constants
    -from onelogin.saml2.utils import OneLogin_Saml2_Utils
    +from onelogin.saml2.utils import OneLogin_Saml2_Utils, xmlsec
     from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request
     from onelogin.saml2.authn_request import OneLogin_Saml2_Authn_Request
     
    @@ -32,7 +31,7 @@ class OneLogin_Saml2_Auth(object):
         This class implements the SP SAML instance.
     
         Defines the methods that you can invoke in your application in
    -    order to add SAML support (initiates sso, initiates slo, processes a
    +    order to add SAML support (initiates SSO, initiates SLO, processes a
         SAML Response, a Logout Request or a Logout Response).
         """
     
    @@ -43,8 +42,8 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None):
             :param request_data: Request Data
             :type request_data: dict
     
    -        :param settings: Optional. SAML Toolkit Settings
    -        :type settings: dict|object
    +        :param old_settings: Optional. SAML Toolkit Settings
    +        :type old_settings: dict
     
             :param custom_base_path: Optional. Path where are stored the settings file and the cert folder
             :type custom_base_path: string
    @@ -53,10 +52,21 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None):
             self.__settings = OneLogin_Saml2_Settings(old_settings, custom_base_path)
             self.__attributes = []
             self.__nameid = None
    +        self.__nameid_format = None
    +        self.__nameid_nq = None
    +        self.__nameid_spnq = None
             self.__session_index = None
    +        self.__session_expiration = None
             self.__authenticated = False
             self.__errors = []
             self.__error_reason = None
    +        self.__last_request_id = None
    +        self.__last_message_id = None
    +        self.__last_assertion_id = None
    +        self.__last_assertion_not_on_or_after = None
    +        self.__last_authn_contexts = []
    +        self.__last_request = None
    +        self.__last_response = None
     
         def get_settings(self):
             """
    @@ -80,21 +90,30 @@ def process_response(self, request_id=None):
             """
             Process the SAML Response sent by the IdP.
     
    -        :param request_id: Is an optional argumen. Is the ID of the AuthNRequest sent by this SP to the IdP.
    +        :param request_id: Is an optional argument. Is the ID of the AuthNRequest sent by this SP to the IdP.
             :type request_id: string
     
             :raises: OneLogin_Saml2_Error.SAML_RESPONSE_NOT_FOUND, when a POST with a SAMLResponse is not found
             """
             self.__errors = []
    +        self.__error_reason = None
     
             if 'post_data' in self.__request_data and 'SAMLResponse' in self.__request_data['post_data']:
                 # AuthnResponse -- HTTP_POST Binding
                 response = OneLogin_Saml2_Response(self.__settings, self.__request_data['post_data']['SAMLResponse'])
    -
    +            self.__last_response = response.get_xml_document()
                 if response.is_valid(self.__request_data, request_id):
                     self.__attributes = response.get_attributes()
                     self.__nameid = response.get_nameid()
    +                self.__nameid_format = response.get_nameid_format()
    +                self.__nameid_nq = response.get_nameid_nq()
    +                self.__nameid_spnq = response.get_nameid_spnq()
                     self.__session_index = response.get_session_index()
    +                self.__session_expiration = response.get_session_not_on_or_after()
    +                self.__last_message_id = response.get_id()
    +                self.__last_assertion_id = response.get_assertion_id()
    +                self.__last_authn_contexts = response.get_authn_contexts()
    +                self.__last_assertion_not_on_or_after = response.get_assertion_not_on_or_after()
                     self.__authenticated = True
     
                 else:
    @@ -118,22 +137,27 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
             :param request_id: The ID of the LogoutRequest sent by this SP to the IdP
             :type request_id: string
     
    -        :returns: Redirection url
    +        :returns: Redirection URL
             """
             self.__errors = []
    +        self.__error_reason = None
     
             if 'get_data' in self.__request_data and 'SAMLResponse' in self.__request_data['get_data']:
                 logout_response = OneLogin_Saml2_Logout_Response(self.__settings, self.__request_data['get_data']['SAMLResponse'])
    +            self.__last_response = logout_response.get_xml()
                 if not logout_response.is_valid(self.__request_data, request_id):
                     self.__errors.append('invalid_logout_response')
                     self.__error_reason = logout_response.get_error()
                 elif logout_response.get_status() != OneLogin_Saml2_Constants.STATUS_SUCCESS:
                     self.__errors.append('logout_not_success')
    -            elif not keep_local_session:
    -                OneLogin_Saml2_Utils.delete_local_session(delete_session_cb)
    +            else:
    +                self.__last_message_id = logout_response.id
    +                if not keep_local_session:
    +                    OneLogin_Saml2_Utils.delete_local_session(delete_session_cb)
     
             elif 'get_data' in self.__request_data and 'SAMLRequest' in self.__request_data['get_data']:
                 logout_request = OneLogin_Saml2_Logout_Request(self.__settings, self.__request_data['get_data']['SAMLRequest'])
    +            self.__last_request = logout_request.get_xml()
                 if not logout_request.is_valid(self.__request_data):
                     self.__errors.append('invalid_logout_request')
                     self.__error_reason = logout_request.get_error()
    @@ -142,13 +166,17 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
                         OneLogin_Saml2_Utils.delete_local_session(delete_session_cb)
     
                     in_response_to = logout_request.id
    +                self.__last_message_id = logout_request.id
                     response_builder = OneLogin_Saml2_Logout_Response(self.__settings)
                     response_builder.build(in_response_to)
    +                self.__last_response = response_builder.get_xml()
                     logout_response = response_builder.get_response()
     
                     parameters = {'SAMLResponse': logout_response}
                     if 'RelayState' in self.__request_data['get_data']:
                         parameters['RelayState'] = self.__request_data['get_data']['RelayState']
    +                # else:
    +                #    parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data)
     
                     security = self.__settings.get_security_data()
                     if 'logoutResponseSigned' in security and security['logoutResponseSigned']:
    @@ -165,19 +193,26 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
     
         def redirect_to(self, url=None, parameters={}):
             """
    -        Redirects the user to the url past by parameter or to the url that we defined in our SSO Request.
    +        Redirects the user to the URL passed by parameter or to the URL that we defined in our SSO Request.
     
             :param url: The target URL to redirect the user
             :type url: string
    -        :param parameters: Extra parameters to be passed as part of the url
    +        :param parameters: Extra parameters to be passed as part of the URL
             :type parameters: dict
     
    -        :returns: Redirection url
    +        :returns: Redirection URL
             """
             if url is None and 'RelayState' in self.__request_data['get_data']:
                 url = self.__request_data['get_data']['RelayState']
             return OneLogin_Saml2_Utils.redirect(url, parameters, request_data=self.__request_data)
     
    +    def get_last_authn_contexts(self):
    +        """
    +        :returns: The list of authentication contexts sent in the last SAML Response.
    +        :rtype: list
    +        """
    +        return self.__last_authn_contexts
    +
         def is_authenticated(self):
             """
             Checks if the user is authenticated or not.
    @@ -201,10 +236,37 @@ def get_nameid(self):
             Returns the nameID.
     
             :returns: NameID
    -        :rtype: string
    +        :rtype: string|None
             """
             return self.__nameid
     
    +    def get_nameid_format(self):
    +        """
    +        Returns the nameID Format.
    +
    +        :returns: NameID Format
    +        :rtype: string|None
    +        """
    +        return self.__nameid_format
    +
    +    def get_nameid_nq(self):
    +        """
    +        Returns the nameID NameQualifier of the Assertion.
    +
    +        :returns: NameID NameQualifier
    +        :rtype: string|None
    +        """
    +        return self.__nameid_nq
    +
    +    def get_nameid_spnq(self):
    +        """
    +        Returns the nameID SP NameQualifier of the Assertion.
    +
    +        :returns: NameID SP NameQualifier
    +        :rtype: string|None
    +        """
    +        return self.__nameid_spnq
    +
         def get_session_index(self):
             """
             Returns the SessionIndex from the AuthnStatement.
    @@ -213,6 +275,21 @@ def get_session_index(self):
             """
             return self.__session_index
     
    +    def get_session_expiration(self):
    +        """
    +        Returns the SessionNotOnOrAfter from the AuthnStatement.
    +        :returns: The SessionNotOnOrAfter of the assertion
    +        :rtype: DateTime|None
    +        """
    +        return self.__session_expiration
    +
    +    def get_last_assertion_not_on_or_after(self):
    +        """
    +        The NotOnOrAfter value of the valid SubjectConfirmationData node
    +        (if any) of the last assertion processed
    +        """
    +        return self.__last_assertion_not_on_or_after
    +
         def get_errors(self):
             """
             Returns a list with code errors if something went wrong
    @@ -238,7 +315,7 @@ def get_attribute(self, name):
             :param name: Name of the attribute
             :type name: string
     
    -        :returns: Attribute value if exists or []
    +        :returns: Attribute value if exists or None
             :rtype: string
             """
             assert isinstance(name, basestring)
    @@ -247,26 +324,55 @@ def get_attribute(self, name):
                 value = self.__attributes[name]
             return value
     
    -    def login(self, return_to=None, force_authn=False, is_passive=False):
    +    def get_last_request_id(self):
    +        """
    +        :returns: The ID of the last Request SAML message generated.
    +        :rtype: string
    +        """
    +        return self.__last_request_id
    +
    +    def get_last_message_id(self):
    +        """
    +        :returns: The ID of the last Response SAML message processed.
    +        :rtype: string
    +        """
    +        return self.__last_message_id
    +
    +    def get_last_assertion_id(self):
    +        """
    +        :returns: The ID of the last assertion processed.
    +        :rtype: string
    +        """
    +        return self.__last_assertion_id
    +
    +    def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_policy=True, name_id_value_req=None):
             """
             Initiates the SSO process.
     
             :param return_to: Optional argument. The target URL the user should be redirected to after login.
             :type return_to: string
     
    -        :param force_authn: Optional argument. When true the AuthNReuqest will set the ForceAuthn='true'.
    -        :type force_authn: string
    +        :param force_authn: Optional argument. When true the AuthNRequest will set the ForceAuthn='true'.
    +        :type force_authn: bool
     
    -        :param is_passive: Optional argument. When true the AuthNReuqest will set the Ispassive='true'.
    -        :type is_passive: string
    +        :param is_passive: Optional argument. When true the AuthNRequest will set the Ispassive='true'.
    +        :type is_passive: bool
     
    -        :returns: Redirection url
    -        """
    -        authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive)
    +        :param set_nameid_policy: Optional argument. When true the AuthNRequest will set a nameIdPolicy element.
    +        :type set_nameid_policy: bool
    +
    +        :param name_id_value_req: Optional argument. Indicates to the IdP the subject that should be authenticated
    +        :type name_id_value_req: string
     
    +        :returns: Redirection URL
    +        :rtype: string
    +        """
    +        authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, set_nameid_policy, name_id_value_req)
    +        self.__last_request = authn_request.get_xml()
    +        self.__last_request_id = authn_request.get_id()
             saml_request = authn_request.get_request()
    -        parameters = {'SAMLRequest': saml_request}
     
    +        parameters = {'SAMLRequest': saml_request}
             if return_to is not None:
                 parameters['RelayState'] = return_to
             else:
    @@ -278,7 +384,7 @@ def login(self, return_to=None, force_authn=False, is_passive=False):
                 parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState'], security['signatureAlgorithm'])
             return self.redirect_to(self.get_sso_url(), parameters)
     
    -    def logout(self, return_to=None, name_id=None, session_index=None):
    +    def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name_id_format=None, spnq=None):
             """
             Initiates the SLO process.
     
    @@ -291,6 +397,15 @@ def logout(self, return_to=None, name_id=None, session_index=None):
             :param session_index: SessionIndex that identifies the session of the user.
             :type session_index: string
     
    +        :param nq: IDP Name Qualifier
    +        :type: string
    +
    +        :param name_id_format: The NameID Format that will be set in the LogoutRequest.
    +        :type: string
    +
    +        :param spnq: SP Name Qualifier
    +        :type: string
    +
             :returns: Redirection url
             """
             slo_url = self.get_slo_url()
    @@ -302,9 +417,19 @@ def logout(self, return_to=None, name_id=None, session_index=None):
     
             if name_id is None and self.__nameid is not None:
                 name_id = self.__nameid
    -
    -        logout_request = OneLogin_Saml2_Logout_Request(self.__settings, name_id=name_id, session_index=session_index)
    -
    +        if name_id_format is None and self.__nameid_format is not None:
    +            name_id_format = self.__nameid_format
    +
    +        logout_request = OneLogin_Saml2_Logout_Request(
    +            self.__settings,
    +            name_id=name_id,
    +            session_index=session_index,
    +            nq=nq,
    +            name_id_format=name_id_format,
    +            spnq=spnq
    +        )
    +        self.__last_request = logout_request.get_xml()
    +        self.__last_request_id = logout_request.id
             saml_request = logout_request.get_request()
     
             parameters = {'SAMLRequest': logout_request.get_request()}
    @@ -321,7 +446,7 @@ def logout(self, return_to=None, name_id=None, session_index=None):
     
         def get_sso_url(self):
             """
    -        Gets the SSO url.
    +        Gets the SSO URL.
     
             :returns: An URL, the SSO endpoint of the IdP
             :rtype: string
    @@ -331,7 +456,7 @@ def get_sso_url(self):
     
         def get_slo_url(self):
             """
    -        Gets the SLO url.
    +        Gets the SLO URL.
     
             :returns: An URL, the SLO endpoint of the IdP
             :rtype: string
    @@ -394,20 +519,16 @@ def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=On
             if not key:
                 raise OneLogin_Saml2_Error(
                     "Trying to sign the %s but can't load the SP private key" % saml_type,
    -                OneLogin_Saml2_Error.SP_CERTS_NOT_FOUND
    +                OneLogin_Saml2_Error.PRIVATE_KEY_NOT_FOUND
                 )
     
    -        xmlsec.initialize()
    -
             dsig_ctx = xmlsec.DSigCtx()
             dsig_ctx.signKey = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None)
     
    -        saml_data_str = '%s=%s' % (saml_type, quote_plus(saml_data))
    -        relay_state_str = 'RelayState=%s' % quote_plus(relay_state)
    -        alg_str = 'SigAlg=%s' % quote_plus(sign_algorithm)
    -
    -        sign_data = [saml_data_str, relay_state_str, alg_str]
    -        msg = '&'.join(sign_data)
    +        msg = '%s=%s' % (saml_type, quote_plus(saml_data))
    +        if relay_state is not None:
    +            msg += '&RelayState=%s' % quote_plus(relay_state)
    +        msg += '&SigAlg=%s' % quote_plus(sign_algorithm)
     
             # Sign the metadata with our private key.
             sign_algorithm_transform_map = {
    @@ -421,3 +542,28 @@ def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=On
     
             signature = dsig_ctx.signBinary(str(msg), sign_algorithm_transform)
             return b64encode(signature)
    +
    +    def get_last_response_xml(self, pretty_print_if_possible=False):
    +        """
    +        Retrieves the raw XML (decrypted) of the last SAML response,
    +        or the last Logout Response generated or processed
    +
    +        :returns: SAML response XML
    +        :rtype: string|None
    +        """
    +        response = None
    +        if self.__last_response is not None:
    +            if isinstance(self.__last_response, basestring):
    +                response = self.__last_response
    +            else:
    +                response = tostring(self.__last_response, pretty_print=pretty_print_if_possible)
    +        return response
    +
    +    def get_last_request_xml(self):
    +        """
    +        Retrieves the raw XML sent in the last SAML request
    +
    +        :returns: SAML request XML
    +        :rtype: string|None
    +        """
    +        return self.__last_request or None
    diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py
    index 8e8495e7..d7be43fe 100644
    --- a/src/onelogin/saml2/authn_request.py
    +++ b/src/onelogin/saml2/authn_request.py
    @@ -2,18 +2,16 @@
     
     """ OneLogin_Saml2_Authn_Request class
     
    -Copyright (c) 2014, OneLogin, Inc.
    -All rights reserved.
    +Copyright (c) 2010-2018 OneLogin, Inc.
    +MIT License
     
     AuthNRequest class of OneLogin's Python Toolkit.
     
     """
    -
     from base64 import b64encode
    -from zlib import compress
     
    -from onelogin.saml2.utils import OneLogin_Saml2_Utils
     from onelogin.saml2.constants import OneLogin_Saml2_Constants
    +from onelogin.saml2.utils import OneLogin_Saml2_Utils
     
     
     class OneLogin_Saml2_Authn_Request(object):
    @@ -24,18 +22,24 @@ class OneLogin_Saml2_Authn_Request(object):
     
         """
     
    -    def __init__(self, settings, force_authn=False, is_passive=False):
    +    def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_policy=True, name_id_value_req=None):
             """
             Constructs the AuthnRequest object.
     
             :param settings: OSetting data
    -        :type return_to: OneLogin_Saml2_Settings
    +        :type settings: OneLogin_Saml2_Settings
     
    -        :param force_authn: Optional argument. When true the AuthNReuqest will set the ForceAuthn='true'.
    +        :param force_authn: Optional argument. When true the AuthNRequest will set the ForceAuthn='true'.
             :type force_authn: bool
     
    -        :param is_passive: Optional argument. When true the AuthNReuqest will set the Ispassive='true'.
    +        :param is_passive: Optional argument. When true the AuthNRequest will set the Ispassive='true'.
             :type is_passive: bool
    +
    +        :param set_nameid_policy: Optional argument. When true the AuthNRequest will set a nameIdPolicy element.
    +        :type set_nameid_policy: bool
    +
    +        :param name_id_value_req: Optional argument. Indicates to the IdP the subject that should be authenticated
    +        :type name_id_value_req: string
             """
             self.__settings = settings
     
    @@ -49,10 +53,6 @@ def __init__(self, settings, force_authn=False, is_passive=False):
     
             destination = idp_data['singleSignOnService']['url']
     
    -        name_id_policy_format = sp_data['NameIDFormat']
    -        if 'wantNameIdEncrypted' in security and security['wantNameIdEncrypted']:
    -            name_id_policy_format = OneLogin_Saml2_Constants.NAMEID_ENCRYPTED
    -
             provider_name_str = ''
             organization_data = settings.get_organization()
             if isinstance(organization_data, dict) and organization_data:
    @@ -62,45 +62,66 @@ def __init__(self, settings, force_authn=False, is_passive=False):
                 else:
                     lang = langs[0]
                 if 'displayname' in organization_data[lang] and organization_data[lang]['displayname'] is not None:
    -                provider_name_str = 'ProviderName="%s"' % organization_data[lang]['displayname']
    +                provider_name_str = "\n" + '    ProviderName="%s"' % organization_data[lang]['displayname']
     
             force_authn_str = ''
             if force_authn is True:
    -            force_authn_str = 'ForceAuthn="true"'
    +            force_authn_str = "\n" + '    ForceAuthn="true"'
     
             is_passive_str = ''
             if is_passive is True:
    -            is_passive_str = 'IsPassive="true"'
    +            is_passive_str = "\n" + '    IsPassive="true"'
    +
    +        subject_str = ''
    +        if name_id_value_req:
    +            subject_str = """
    +    
    +        %s
    +        
    +    """ % (sp_data['NameIDFormat'], name_id_value_req)
    +
    +        nameid_policy_str = ''
    +        if set_nameid_policy:
    +            name_id_policy_format = sp_data['NameIDFormat']
    +            if 'wantNameIdEncrypted' in security and security['wantNameIdEncrypted']:
    +                name_id_policy_format = OneLogin_Saml2_Constants.NAMEID_ENCRYPTED
    +
    +            nameid_policy_str = """
    +    """ % name_id_policy_format
     
             requested_authn_context_str = ''
             if 'requestedAuthnContext' in security.keys() and security['requestedAuthnContext'] is not False:
    +            authn_comparison = 'exact'
    +            if 'requestedAuthnContextComparison' in security.keys():
    +                authn_comparison = security['requestedAuthnContextComparison']
    +
                 if security['requestedAuthnContext'] is True:
    -                requested_authn_context_str = """    
    +                requested_authn_context_str = "\n" + """    
             urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
    -    """
    +    """ % authn_comparison
                 else:
    -                requested_authn_context_str = '     '
    +                requested_authn_context_str = "\n" + '     ' % authn_comparison
                     for authn_context in security['requestedAuthnContext']:
                         requested_authn_context_str += '%s' % authn_context
                     requested_authn_context_str += '    '
     
    +        attr_consuming_service_str = ''
    +        if 'attributeConsumingService' in sp_data and sp_data['attributeConsumingService']:
    +            attr_consuming_service_str = 'AttributeConsumingServiceIndex="1"'
    +
             request = """
    -    %(entity_id)s
    -    
    -%(requested_authn_context_str)s
    +    AssertionConsumerServiceURL="%(assertion_url)s"
    +    %(attr_consuming_service_str)s>
    +    %(entity_id)s%(subject_str)s%(nameid_policy_str)s%(requested_authn_context_str)s
     """ % \
                 {
                     'id': uid,
    @@ -111,20 +132,27 @@ def __init__(self, settings, force_authn=False, is_passive=False):
                     'destination': destination,
                     'assertion_url': sp_data['assertionConsumerService']['url'],
                     'entity_id': sp_data['entityId'],
    -                'name_id_policy': name_id_policy_format,
    +                'subject_str': subject_str,
    +                'nameid_policy_str': nameid_policy_str,
                     'requested_authn_context_str': requested_authn_context_str,
    +                'attr_consuming_service_str': attr_consuming_service_str
                 }
     
             self.__authn_request = request
     
    -    def get_request(self):
    +    def get_request(self, deflate=True):
             """
             Returns unsigned AuthnRequest.
    -        :return: Unsigned AuthnRequest
    +        :param deflate: It makes the deflate process optional
    +        :type: bool
    +        :return: AuthnRequest maybe deflated and base64 encoded
             :rtype: str object
             """
    -        deflated_request = compress(self.__authn_request)[2:-4]
    -        return b64encode(deflated_request)
    +        if deflate:
    +            request = OneLogin_Saml2_Utils.deflate_and_base64_encode(self.__authn_request)
    +        else:
    +            request = b64encode(self.__authn_request)
    +        return request
     
         def get_id(self):
             """
    @@ -133,3 +161,11 @@ def get_id(self):
             :rtype: string
             """
             return self.__id
    +
    +    def get_xml(self):
    +        """
    +        Returns the XML that will be sent as part of the request
    +        :return: XML request body
    +        :rtype: string
    +        """
    +        return self.__authn_request
    diff --git a/src/onelogin/saml2/constants.py b/src/onelogin/saml2/constants.py
    index f004bbbd..87cb0e6f 100644
    --- a/src/onelogin/saml2/constants.py
    +++ b/src/onelogin/saml2/constants.py
    @@ -2,8 +2,8 @@
     
     """ OneLogin_Saml2_Constants class
     
    -Copyright (c) 2014, OneLogin, Inc.
    -All rights reserved.
    +Copyright (c) 2010-2018 OneLogin, Inc.
    +MIT License
     
     Constants class of OneLogin's Python Toolkit.
     
    @@ -19,7 +19,10 @@ class OneLogin_Saml2_Constants(object):
         """
     
         # Value added to the current time in time condition validations
    -    ALOWED_CLOCK_DRIFT = 300
    +    ALLOWED_CLOCK_DRIFT = 300
    +
    +    XML = 'http://www.w3.org/XML/1998/namespace'
    +    XSI = 'http://www.w3.org/2001/XMLSchema-instance'
     
         # NameID Formats
         NAMEID_EMAIL_ADDRESS = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
    @@ -47,6 +50,24 @@ class OneLogin_Saml2_Constants(object):
         NS_XENC = 'http://www.w3.org/2001/04/xmlenc#'
         NS_DS = 'http://www.w3.org/2000/09/xmldsig#'
     
    +    # Namespace Prefixes
    +    NS_PREFIX_SAML = 'saml'
    +    NS_PREFIX_SAMLP = 'samlp'
    +    NS_PREFIX_MD = 'md'
    +    NS_PREFIX_XS = 'xs'
    +    NS_PREFIX_XSI = 'xsi'
    +    NS_PREFIX_XENC = 'xenc'
    +    NS_PREFIX_DS = 'ds'
    +
    +    # Prefix:Namespace Mappings
    +    NSMAP = {
    +        NS_PREFIX_SAMLP: NS_SAMLP,
    +        NS_PREFIX_SAML: NS_SAML,
    +        NS_PREFIX_DS: NS_DS,
    +        NS_PREFIX_XENC: NS_XENC,
    +        NS_PREFIX_MD: NS_MD
    +    }
    +
         # Bindings
         BINDING_HTTP_POST = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
         BINDING_HTTP_REDIRECT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
    @@ -76,21 +97,13 @@ class OneLogin_Saml2_Constants(object):
         STATUS_PARTIAL_LOGOUT = 'urn:oasis:names:tc:SAML:2.0:status:PartialLogout'
         STATUS_PROXY_COUNT_EXCEEDED = 'urn:oasis:names:tc:SAML:2.0:status:ProxyCountExceeded'
     
    -    # Namespaces
    -    NSMAP = {
    -        'samlp': NS_SAMLP,
    -        'saml': NS_SAML,
    -        'ds': NS_DS,
    -        'xenc': NS_XENC
    -    }
    -
         # Sign & Crypto
         SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1'
         SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256'
    -    SHA384 = 'http://www.w3.org/2001/04/xmlencsha384'
    +    SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#sha384'
         SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512'
     
    -    DSA_SHA1 = 'http://www.w3.org/2000/09/xmld/sig#dsa-sha1'
    +    DSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1'
         RSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
         RSA_SHA256 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
         RSA_SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384'
    diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py
    index 63d98744..560ec8e6 100644
    --- a/src/onelogin/saml2/errors.py
    +++ b/src/onelogin/saml2/errors.py
    @@ -2,8 +2,8 @@
     
     """ OneLogin_Saml2_Error class
     
    -Copyright (c) 2014, OneLogin, Inc.
    -All rights reserved.
    +Copyright (c) 2010-2018 OneLogin, Inc.
    +MIT License
     
     Error class of OneLogin's Python Toolkit.
     
    @@ -25,7 +25,9 @@ class OneLogin_Saml2_Error(Exception):
         SETTINGS_INVALID_SYNTAX = 1
         SETTINGS_INVALID = 2
         METADATA_SP_INVALID = 3
    +    # SP_CERTS_NOT_FOUND is deprecated, use CERT_NOT_FOUND instead
         SP_CERTS_NOT_FOUND = 4
    +    CERT_NOT_FOUND = 4
         REDIRECT_INVALID_URL = 5
         PUBLIC_CERT_FILE_NOT_FOUND = 6
         PRIVATE_KEY_FILE_NOT_FOUND = 7
    @@ -34,6 +36,83 @@ class OneLogin_Saml2_Error(Exception):
         SAML_LOGOUTREQUEST_INVALID = 10
         SAML_LOGOUTRESPONSE_INVALID = 11
         SAML_SINGLE_LOGOUT_NOT_SUPPORTED = 12
    +    PRIVATE_KEY_NOT_FOUND = 13
    +    UNSUPPORTED_SETTINGS_OBJECT = 14
    +
    +    def __init__(self, message, code=0, errors=None):
    +        """
    +        Initializes the Exception instance.
    +
    +        Arguments are:
    +            * (str)   message.   Describes the error.
    +            * (int)   code.      The code error (defined in the error class).
    +        """
    +        assert isinstance(message, basestring)
    +        assert isinstance(code, int)
    +
    +        if errors is not None:
    +            message = message % errors
    +
    +        Exception.__init__(self, message)
    +        self.code = code
    +
    +
    +class OneLogin_Saml2_ValidationError(Exception):
    +    """
    +
    +    This class implements another custom Exception handler, related
    +    to exceptions that happens during validation process.
    +    Defines custom error codes .
    +
    +    """
    +
    +    # Validation Errors
    +    UNSUPPORTED_SAML_VERSION = 0
    +    MISSING_ID = 1
    +    WRONG_NUMBER_OF_ASSERTIONS = 2
    +    MISSING_STATUS = 3
    +    MISSING_STATUS_CODE = 4
    +    STATUS_CODE_IS_NOT_SUCCESS = 5
    +    WRONG_SIGNED_ELEMENT = 6
    +    ID_NOT_FOUND_IN_SIGNED_ELEMENT = 7
    +    DUPLICATED_ID_IN_SIGNED_ELEMENTS = 8
    +    INVALID_SIGNED_ELEMENT = 9
    +    DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS = 10
    +    UNEXPECTED_SIGNED_ELEMENTS = 11
    +    WRONG_NUMBER_OF_SIGNATURES_IN_RESPONSE = 12
    +    WRONG_NUMBER_OF_SIGNATURES_IN_ASSERTION = 13
    +    INVALID_XML_FORMAT = 14
    +    WRONG_INRESPONSETO = 15
    +    NO_ENCRYPTED_ASSERTION = 16
    +    NO_ENCRYPTED_NAMEID = 17
    +    MISSING_CONDITIONS = 18
    +    ASSERTION_TOO_EARLY = 19
    +    ASSERTION_EXPIRED = 20
    +    WRONG_NUMBER_OF_AUTHSTATEMENTS = 21
    +    NO_ATTRIBUTESTATEMENT = 22
    +    ENCRYPTED_ATTRIBUTES = 23
    +    WRONG_DESTINATION = 24
    +    EMPTY_DESTINATION = 25
    +    WRONG_AUDIENCE = 26
    +    ISSUER_MULTIPLE_IN_RESPONSE = 27
    +    ISSUER_NOT_FOUND_IN_ASSERTION = 28
    +    WRONG_ISSUER = 29
    +    SESSION_EXPIRED = 30
    +    WRONG_SUBJECTCONFIRMATION = 31
    +    NO_SIGNED_MESSAGE = 32
    +    NO_SIGNED_ASSERTION = 33
    +    NO_SIGNATURE_FOUND = 34
    +    KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA = 35
    +    CHILDREN_NODE_NOT_FOUND_IN_KEYINFO = 36
    +    UNSUPPORTED_RETRIEVAL_METHOD = 37
    +    NO_NAMEID = 38
    +    EMPTY_NAMEID = 39
    +    SP_NAME_QUALIFIER_NAME_MISMATCH = 40
    +    DUPLICATED_ATTRIBUTE_NAME_FOUND = 41
    +    INVALID_SIGNATURE = 42
    +    WRONG_NUMBER_OF_SIGNATURES = 43
    +    RESPONSE_EXPIRED = 44
    +    AUTHN_CONTEXT_MISMATCH = 45
     
         def __init__(self, message, code=0, errors=None):
             """
    diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py
    new file mode 100644
    index 00000000..55055921
    --- /dev/null
    +++ b/src/onelogin/saml2/idp_metadata_parser.py
    @@ -0,0 +1,270 @@
    +# -*- coding: utf-8 -*-
    +
    +""" OneLogin_Saml2_IdPMetadataParser class
    +
    +Copyright (c) 2010-2018 OneLogin, Inc.
    +MIT License
    +
    +Metadata class of OneLogin's Python Toolkit.
    +
    +"""
    +
    +import urllib2
    +import ssl
    +
    +from copy import deepcopy
    +from defusedxml.lxml import fromstring
    +
    +from onelogin.saml2.constants import OneLogin_Saml2_Constants
    +from onelogin.saml2.utils import OneLogin_Saml2_Utils
    +
    +
    +class OneLogin_Saml2_IdPMetadataParser(object):
    +    """
    +    A class that contain methods related to obtaining and parsing metadata from IdP
    +    """
    +
    +    @staticmethod
    +    def get_metadata(url, validate_cert=True):
    +        """
    +        Gets the metadata XML from the provided URL
    +
    +        :param url: Url where the XML of the Identity Provider Metadata is published.
    +        :type url: string
    +
    +        :param validate_cert: If the url uses https schema, that flag enables or not the verification of the associated certificate.
    +        :type validate_cert: bool
    +
    +        :returns: metadata XML
    +        :rtype: string
    +        """
    +        valid = False
    +        if validate_cert:
    +            response = urllib2.urlopen(url)
    +        else:
    +            ctx = ssl.create_default_context()
    +            ctx.check_hostname = False
    +            ctx.verify_mode = ssl.CERT_NONE
    +            response = urllib2.urlopen(url, context=ctx)
    +        xml = response.read()
    +
    +        if xml:
    +            try:
    +                dom = fromstring(xml, forbid_dtd=True)
    +                idp_descriptor_nodes = OneLogin_Saml2_Utils.query(dom, '//md:IDPSSODescriptor')
    +                if idp_descriptor_nodes:
    +                    valid = True
    +            except Exception:
    +                pass
    +
    +        if not valid:
    +            raise Exception('Not valid IdP XML found from URL: %s' % (url))
    +
    +        return xml
    +
    +    @staticmethod
    +    def parse_remote(url, validate_cert=True, entity_id=None, **kwargs):
    +        """
    +        Gets the metadata XML from the provided URL and parse it, returning a dict with extracted data
    +
    +        :param url: Url where the XML of the Identity Provider Metadata is published.
    +        :type url: string
    +
    +        :param validate_cert: If the url uses https schema, that flag enables or not the verification of the associated certificate.
    +        :type validate_cert: bool
    +
    +        :param entity_id: Specify the entity_id of the EntityDescriptor that you want to parse a XML
    +                          that contains multiple EntityDescriptor.
    +        :type entity_id: string
    +
    +        :returns: settings dict with extracted data
    +        :rtype: dict
    +        """
    +        idp_metadata = OneLogin_Saml2_IdPMetadataParser.get_metadata(url, validate_cert)
    +        return OneLogin_Saml2_IdPMetadataParser.parse(idp_metadata, entity_id=entity_id, **kwargs)
    +
    +    @staticmethod
    +    def parse(
    +            idp_metadata,
    +            required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT,
    +            required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT,
    +            entity_id=None):
    +        """
    +        Parse the Identity Provider metadata and return a dict with extracted data.
    +
    +        If there are multiple  tags, parse only the first.
    +
    +        Parse only those SSO endpoints with the same binding as given by
    +        the `required_sso_binding` parameter.
    +
    +        Parse only those SLO endpoints with the same binding as given by
    +        the `required_slo_binding` parameter.
    +
    +        If the metadata specifies multiple SSO endpoints with the required
    +        binding, extract only the first (the same holds true for SLO
    +        endpoints).
    +
    +        :param idp_metadata: XML of the Identity Provider Metadata.
    +        :type idp_metadata: string
    +
    +        :param required_sso_binding: Parse only POST or REDIRECT SSO endpoints.
    +        :type required_sso_binding: one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT
    +            or OneLogin_Saml2_Constants.BINDING_HTTP_POST
    +
    +        :param required_slo_binding: Parse only POST or REDIRECT SLO endpoints.
    +        :type required_slo_binding: one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT
    +            or OneLogin_Saml2_Constants.BINDING_HTTP_POST
    +
    +        :param entity_id: Specify the entity_id of the EntityDescriptor that you want to parse a XML
    +                          that contains multiple EntityDescriptor.
    +        :type entity_id: string
    +
    +        :returns: settings dict with extracted data
    +        :rtype: dict
    +        """
    +        data = {}
    +
    +        dom = fromstring(idp_metadata, forbid_dtd=True)
    +
    +        entity_desc_path = '//md:EntityDescriptor'
    +        if entity_id:
    +            entity_desc_path += "[@entityID='%s']" % entity_id
    +
    +        entity_descriptor_nodes = OneLogin_Saml2_Utils.query(dom, entity_desc_path)
    +
    +        idp_entity_id = want_authn_requests_signed = idp_name_id_format = idp_sso_url = idp_slo_url = certs = None
    +
    +        if len(entity_descriptor_nodes) > 0:
    +            entity_descriptor_node = entity_descriptor_nodes[0]
    +            idp_descriptor_nodes = OneLogin_Saml2_Utils.query(entity_descriptor_node, './md:IDPSSODescriptor')
    +            if len(idp_descriptor_nodes) > 0:
    +                idp_descriptor_node = idp_descriptor_nodes[0]
    +
    +                idp_entity_id = entity_descriptor_node.get('entityID', None)
    +
    +                want_authn_requests_signed = entity_descriptor_node.get('WantAuthnRequestsSigned', None)
    +
    +                name_id_format_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, './md:NameIDFormat')
    +                if len(name_id_format_nodes) > 0:
    +                    idp_name_id_format = OneLogin_Saml2_Utils.element_text(name_id_format_nodes[0])
    +
    +                sso_nodes = OneLogin_Saml2_Utils.query(
    +                    idp_descriptor_node,
    +                    "./md:SingleSignOnService[@Binding='%s']" % required_sso_binding
    +                )
    +
    +                if len(sso_nodes) > 0:
    +                    idp_sso_url = sso_nodes[0].get('Location', None)
    +
    +                slo_nodes = OneLogin_Saml2_Utils.query(
    +                    idp_descriptor_node,
    +                    "./md:SingleLogoutService[@Binding='%s']" % required_slo_binding
    +                )
    +                if len(slo_nodes) > 0:
    +                    idp_slo_url = slo_nodes[0].get('Location', None)
    +
    +                signing_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate")
    +                encryption_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate")
    +
    +                if len(signing_nodes) > 0 or len(encryption_nodes) > 0:
    +                    certs = {}
    +                    if len(signing_nodes) > 0:
    +                        certs['signing'] = []
    +                        for cert_node in signing_nodes:
    +                            certs['signing'].append(''.join(OneLogin_Saml2_Utils.element_text(cert_node).split()))
    +                    if len(encryption_nodes) > 0:
    +                        certs['encryption'] = []
    +                        for cert_node in encryption_nodes:
    +                            certs['encryption'].append(''.join(OneLogin_Saml2_Utils.element_text(cert_node).split()))
    +
    +                data['idp'] = {}
    +
    +                if idp_entity_id is not None:
    +                    data['idp']['entityId'] = idp_entity_id
    +
    +                if idp_sso_url is not None:
    +                    data['idp']['singleSignOnService'] = {}
    +                    data['idp']['singleSignOnService']['url'] = idp_sso_url
    +                    data['idp']['singleSignOnService']['binding'] = required_sso_binding
    +
    +                if idp_slo_url is not None:
    +                    data['idp']['singleLogoutService'] = {}
    +                    data['idp']['singleLogoutService']['url'] = idp_slo_url
    +                    data['idp']['singleLogoutService']['binding'] = required_slo_binding
    +
    +                if certs is not None:
    +                    if (len(certs) == 1 and
    +                        (('signing' in certs and len(certs['signing']) == 1) or
    +                         ('encryption' in certs and len(certs['encryption']) == 1))) or \
    +                        (('signing' in certs and len(certs['signing']) == 1) and
    +                         ('encryption' in certs and len(certs['encryption']) == 1 and
    +                         certs['signing'][0] == certs['encryption'][0])):
    +                        if 'signing' in certs:
    +                            data['idp']['x509cert'] = certs['signing'][0]
    +                        else:
    +                            data['idp']['x509cert'] = certs['encryption'][0]
    +                    else:
    +                        data['idp']['x509certMulti'] = certs
    +
    +                if want_authn_requests_signed is not None:
    +                    data['security'] = {}
    +                    data['security']['authnRequestsSigned'] = want_authn_requests_signed
    +
    +                if idp_name_id_format:
    +                    data['sp'] = {}
    +                    data['sp']['NameIDFormat'] = idp_name_id_format
    +        return data
    +
    +    @staticmethod
    +    def merge_settings(settings, new_metadata_settings):
    +        """
    +        Will update the settings with the provided new settings data extracted from the IdP metadata
    +
    +        :param settings: Current settings dict data
    +        :type settings: string
    +
    +        :param new_metadata_settings: Settings to be merged (extracted from IdP metadata after parsing)
    +        :type new_metadata_settings: string
    +
    +        :returns: merged settings
    +        :rtype: dict
    +        """
    +        for d in (settings, new_metadata_settings):
    +            if not isinstance(d, dict):
    +                raise TypeError('Both arguments must be dictionaries.')
    +
    +        # Guarantee to not modify original data (`settings.copy()` would not
    +        # be sufficient, as it's just a shallow copy).
    +        result_settings = deepcopy(settings)
    +
    +        # previously I will take care of cert stuff
    +        if 'idp' in new_metadata_settings and 'idp' in result_settings:
    +            if new_metadata_settings['idp'].get('x509cert', None) and result_settings['idp'].get('x509certMulti', None):
    +                del result_settings['idp']['x509certMulti']
    +            if new_metadata_settings['idp'].get('x509certMulti', None) and result_settings['idp'].get('x509cert', None):
    +                del result_settings['idp']['x509cert']
    +
    +        # Merge `new_metadata_settings` into `result_settings`.
    +        dict_deep_merge(result_settings, new_metadata_settings)
    +        return result_settings
    +
    +
    +def dict_deep_merge(a, b, path=None):
    +    """Deep-merge dictionary `b` into dictionary `a`.
    +    Kudos to http://stackoverflow.com/a/7205107/145400
    +    """
    +    if path is None:
    +        path = []
    +    for key in b:
    +        if key in a:
    +            if isinstance(a[key], dict) and isinstance(b[key], dict):
    +                dict_deep_merge(a[key], b[key], path + [str(key)])
    +            elif a[key] == b[key]:
    +                # Key conflict, but equal value.
    +                pass
    +            else:
    +                # Key/value conflict. Prioritize b over a.
    +                a[key] = b[key]
    +        else:
    +            a[key] = b[key]
    +    return a
    diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
    index 39281a83..1cb75efd 100644
    --- a/src/onelogin/saml2/logout_request.py
    +++ b/src/onelogin/saml2/logout_request.py
    @@ -2,22 +2,22 @@
     
     """ OneLogin_Saml2_Logout_Request class
     
    -Copyright (c) 2014, OneLogin, Inc.
    -All rights reserved.
    +Copyright (c) 2010-2018 OneLogin, Inc.
    +MIT License
     
     Logout Request class of OneLogin's Python Toolkit.
     
     """
     
     from zlib import decompress
    -from base64 import b64decode
    +from base64 import b64encode, b64decode
     from lxml import etree
     from defusedxml.lxml import fromstring
    -from urllib import quote_plus
     from xml.dom.minidom import Document
     
     from onelogin.saml2.constants import OneLogin_Saml2_Constants
     from onelogin.saml2.utils import OneLogin_Saml2_Utils
    +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
     
     
     class OneLogin_Saml2_Logout_Request(object):
    @@ -29,7 +29,7 @@ class OneLogin_Saml2_Logout_Request(object):
     
         """
     
    -    def __init__(self, settings, request=None, name_id=None, session_index=None):
    +    def __init__(self, settings, request=None, name_id=None, session_index=None, nq=None, name_id_format=None, spnq=None):
             """
             Constructs the Logout Request object.
     
    @@ -44,6 +44,16 @@ def __init__(self, settings, request=None, name_id=None, session_index=None):
     
             :param session_index: SessionIndex that identifies the session of the user.
             :type session_index: string
    +
    +        :param nq: IDP Name Qualifier
    +        :type: string
    +
    +        :param name_id_format: The NameID Format that will be set in the LogoutRequest.
    +        :type: string
    +
    +        :param spnq: SP Name Qualifier
    +        :type: string
    +
             """
             self.__settings = settings
             self.__error = None
    @@ -61,19 +71,39 @@ def __init__(self, settings, request=None, name_id=None, session_index=None):
     
                 cert = None
                 if 'nameIdEncrypted' in security and security['nameIdEncrypted']:
    -                cert = idp_data['x509cert']
    +                exists_multix509enc = 'x509certMulti' in idp_data and \
    +                    'encryption' in idp_data['x509certMulti'] and \
    +                    idp_data['x509certMulti']['encryption']
    +                if exists_multix509enc:
    +                    cert = idp_data['x509certMulti']['encryption'][0]
    +                else:
    +                    cert = idp_data['x509cert']
     
                 if name_id is not None:
    -                nameIdFormat = sp_data['NameIDFormat']
    +                if not name_id_format and sp_data['NameIDFormat'] != OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED:
    +                    name_id_format = sp_data['NameIDFormat']
                 else:
                     name_id = idp_data['entityId']
    -                nameIdFormat = OneLogin_Saml2_Constants.NAMEID_ENTITY
    +                name_id_format = OneLogin_Saml2_Constants.NAMEID_ENTITY
    +
    +            # From saml-core-2.0-os 8.3.6, when the entity Format is used:
    +            # "The NameQualifier, SPNameQualifier, and SPProvidedID attributes
    +            # MUST be omitted.
    +            if name_id_format and name_id_format == OneLogin_Saml2_Constants.NAMEID_ENTITY:
    +                nq = None
    +                spnq = None
    +
    +            # NameID Format UNSPECIFIED omitted
    +            if name_id_format and name_id_format == OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED:
    +                name_id_format = None
     
                 name_id_obj = OneLogin_Saml2_Utils.generate_name_id(
                     name_id,
    -                sp_data['entityId'],
    -                nameIdFormat,
    -                cert
    +                spnq,
    +                name_id_format,
    +                cert,
    +                False,
    +                nq
                 )
     
                 if session_index:
    @@ -112,13 +142,28 @@ def __init__(self, settings, request=None, name_id=None, session_index=None):
     
             self.__logout_request = logout_request
     
    -    def get_request(self):
    +    def get_request(self, deflate=True):
             """
    -        Returns the Logout Request defated, base64encoded
    -        :return: Deflated base64 encoded Logout Request
    +        Returns the Logout Request deflated, base64encoded
    +        :param deflate: It makes the deflate process optional
    +        :type: bool
    +        :return: Logout Request maybe deflated and base64 encoded
             :rtype: str object
             """
    -        return OneLogin_Saml2_Utils.deflate_and_base64_encode(self.__logout_request)
    +        if deflate:
    +            request = OneLogin_Saml2_Utils.deflate_and_base64_encode(self.__logout_request)
    +        else:
    +            request = b64encode(self.__logout_request)
    +        return request
    +
    +    def get_xml(self):
    +        """
    +        Returns the XML that will be sent as part of the request
    +        or that was received at the SP
    +        :return: XML request body
    +        :rtype: string
    +        """
    +        return self.__logout_request
     
         @staticmethod
         def get_id(request):
    @@ -134,7 +179,7 @@ def get_id(request):
             else:
                 if isinstance(request, Document):
                     request = request.toxml()
    -            elem = fromstring(request)
    +            elem = fromstring(request, forbid_dtd=True)
             return elem.get('ID', None)
     
         @staticmethod
    @@ -153,14 +198,17 @@ def get_nameid_data(request, key=None):
             else:
                 if isinstance(request, Document):
                     request = request.toxml()
    -            elem = fromstring(request)
    +            elem = fromstring(request, forbid_dtd=True)
     
             name_id = None
             encrypted_entries = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:EncryptedID')
     
             if len(encrypted_entries) == 1:
                 if key is None:
    -                raise Exception('Key is required in order to decrypt the NameID')
    +                raise OneLogin_Saml2_Error(
    +                    'Private Key is required in order to decrypt the NameID, check settings',
    +                    OneLogin_Saml2_Error.PRIVATE_KEY_NOT_FOUND
    +                )
     
                 encrypted_data_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:EncryptedID/xenc:EncryptedData')
                 if len(encrypted_data_nodes) == 1:
    @@ -172,10 +220,13 @@ def get_nameid_data(request, key=None):
                     name_id = entries[0]
     
             if name_id is None:
    -            raise Exception('Not NameID found in the Logout Request')
    +            raise OneLogin_Saml2_ValidationError(
    +                'NameID not found in the Logout Request',
    +                OneLogin_Saml2_ValidationError.NO_NAMEID
    +            )
     
             name_id_data = {
    -            'Value': name_id.text
    +            'Value': OneLogin_Saml2_Utils.element_text(name_id)
             }
             for attr in ['Format', 'SPNameQualifier', 'NameQualifier']:
                 if attr in name_id.attrib.keys():
    @@ -197,6 +248,23 @@ def get_nameid(request, key=None):
             name_id = OneLogin_Saml2_Logout_Request.get_nameid_data(request, key)
             return name_id['Value']
     
    +    @staticmethod
    +    def get_nameid_format(request, key=None):
    +        """
    +        Gets the NameID Format of the Logout Request Message
    +        :param request: Logout Request Message
    +        :type request: string|DOMDocument
    +        :param key: The SP key
    +        :type key: string
    +        :return: Name ID Value
    +        :rtype: string
    +        """
    +        name_id_format = None
    +        name_id_data = OneLogin_Saml2_Logout_Request.get_nameid_data(request, key)
    +        if name_id_data and 'Format' in name_id_data.keys():
    +            name_id_format = name_id_data['Format']
    +        return name_id_format
    +
         @staticmethod
         def get_issuer(request):
             """
    @@ -211,12 +279,12 @@ def get_issuer(request):
             else:
                 if isinstance(request, Document):
                     request = request.toxml()
    -            elem = fromstring(request)
    +            elem = fromstring(request, forbid_dtd=True)
     
             issuer = None
             issuer_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:Issuer')
             if len(issuer_nodes) == 1:
    -            issuer = issuer_nodes[0].text
    +            issuer = OneLogin_Saml2_Utils.element_text(issuer_nodes[0])
             return issuer
     
         @staticmethod
    @@ -233,26 +301,28 @@ def get_session_indexes(request):
             else:
                 if isinstance(request, Document):
                     request = request.toxml()
    -            elem = fromstring(request)
    +            elem = fromstring(request, forbid_dtd=True)
     
             session_indexes = []
             session_index_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/samlp:SessionIndex')
             for session_index_node in session_index_nodes:
    -            session_indexes.append(session_index_node.text)
    +            session_indexes.append(OneLogin_Saml2_Utils.element_text(session_index_node))
             return session_indexes
     
    -    def is_valid(self, request_data):
    +    def is_valid(self, request_data, raise_exceptions=False):
             """
    -        Checks if the Logout Request recieved is valid
    +        Checks if the Logout Request received is valid
             :param request_data: Request Data
             :type request_data: dict
    -
    +        :param raise_exceptions: Whether to return false on failure or raise an exception
    +        :type raise_exceptions: Boolean
             :return: If the Logout Request is or not valid
             :rtype: boolean
             """
             self.__error = None
    +        lowercase_urlencoding = False
             try:
    -            dom = fromstring(self.__logout_request)
    +            dom = fromstring(self.__logout_request, forbid_dtd=True)
     
                 idp_data = self.__settings.get_idp_data()
                 idp_entity_id = idp_data['entityId']
    @@ -262,10 +332,16 @@ def is_valid(self, request_data):
                 else:
                     get_data = {}
     
    +            if 'lowercase_urlencoding' in request_data.keys():
    +                lowercase_urlencoding = request_data['lowercase_urlencoding']
    +
                 if self.__settings.is_strict():
                     res = OneLogin_Saml2_Utils.validate_xml(dom, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
                     if not isinstance(res, Document):
    -                    raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd')
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd',
    +                        OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT
    +                    )
     
                     security = self.__settings.get_security_data()
     
    @@ -275,7 +351,10 @@ def is_valid(self, request_data):
                     if dom.get('NotOnOrAfter', None):
                         na = OneLogin_Saml2_Utils.parse_SAML_to_time(dom.get('NotOnOrAfter'))
                         if na <= OneLogin_Saml2_Utils.now():
    -                        raise Exception('Timing issues (please check your clock settings)')
    +                        raise OneLogin_Saml2_ValidationError(
    +                            'Could not validate timestamp: expired. Check system clock.',
    +                            OneLogin_Saml2_ValidationError.RESPONSE_EXPIRED
    +                        )
     
                     # Check destination
                     if dom.get('Destination', None):
    @@ -288,17 +367,28 @@ def is_valid(self, request_data):
                                     {
                                         'currentURL': current_url,
                                         'destination': destination,
    -                                }
    +                                },
    +                                OneLogin_Saml2_ValidationError.WRONG_DESTINATION
                                 )
     
                     # Check issuer
                     issuer = OneLogin_Saml2_Logout_Request.get_issuer(dom)
                     if issuer is not None and issuer != idp_entity_id:
    -                    raise Exception('Invalid issuer in the Logout Request')
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'Invalid issuer in the Logout Request (expected %(idpEntityId)s, got %(issuer)s)' %
    +                        {
    +                            'idpEntityId': idp_entity_id,
    +                            'issuer': issuer
    +                        },
    +                        OneLogin_Saml2_ValidationError.WRONG_ISSUER
    +                    )
     
                     if security['wantMessagesSigned']:
                         if 'Signature' not in get_data:
    -                        raise Exception('The Message of the Logout Request is not signed and the SP require it')
    +                        raise OneLogin_Saml2_ValidationError(
    +                            'The Message of the Logout Request is not signed and the SP require it',
    +                            OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE
    +                        )
     
                 if 'Signature' in get_data:
                     if 'SigAlg' not in get_data:
    @@ -306,29 +396,50 @@ def is_valid(self, request_data):
                     else:
                         sign_alg = get_data['SigAlg']
     
    -                signed_query = 'SAMLRequest=%s' % quote_plus(get_data['SAMLRequest'])
    +                signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLRequest', lowercase_urlencoding=lowercase_urlencoding)
                     if 'RelayState' in get_data:
    -                    signed_query = '%s&RelayState=%s' % (signed_query, quote_plus(get_data['RelayState']))
    -                signed_query = '%s&SigAlg=%s' % (signed_query, quote_plus(sign_alg))
    -
    -                if 'x509cert' not in idp_data or idp_data['x509cert'] is None:
    -                    raise Exception('In order to validate the sign on the Logout Request, the x509cert of the IdP is required')
    -                cert = idp_data['x509cert']
    -
    -                if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
    -                    raise Exception('Signature validation failed. Logout Request rejected')
    +                    signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding))
    +                signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding))
    +
    +                exists_x509cert = 'x509cert' in idp_data and idp_data['x509cert']
    +                exists_multix509sign = 'x509certMulti' in idp_data and \
    +                    'signing' in idp_data['x509certMulti'] and \
    +                    idp_data['x509certMulti']['signing']
    +
    +                if not (exists_x509cert or exists_multix509sign):
    +                    raise OneLogin_Saml2_Error(
    +                        'In order to validate the sign on the Logout Request, the x509cert of the IdP is required',
    +                        OneLogin_Saml2_Error.CERT_NOT_FOUND
    +                    )
    +                if exists_multix509sign:
    +                    for cert in idp_data['x509certMulti']['signing']:
    +                        if OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
    +                            return True
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'Signature validation failed. Logout Request rejected',
    +                        OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
    +                    )
    +                else:
    +                    cert = idp_data['x509cert']
     
    +                    if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
    +                        raise OneLogin_Saml2_ValidationError(
    +                            'Signature validation failed. Logout Request rejected',
    +                            OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
    +                        )
                 return True
             except Exception as err:
                 # pylint: disable=R0801sign_alg
                 self.__error = err.__str__()
                 debug = self.__settings.is_debug_active()
                 if debug:
    -                print err.__str__()
    +                print(err.__str__())
    +            if raise_exceptions:
    +                raise err
                 return False
     
         def get_error(self):
             """
    -        After execute a validation process, if fails this method returns the cause
    +        After executing a validation process, if it fails this method returns the cause
             """
             return self.__error
    diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py
    index 3c79a989..6d326caf 100644
    --- a/src/onelogin/saml2/logout_response.py
    +++ b/src/onelogin/saml2/logout_response.py
    @@ -2,22 +2,22 @@
     
     """ OneLogin_Saml2_Logout_Response class
     
    -Copyright (c) 2014, OneLogin, Inc.
    -All rights reserved.
    +Copyright (c) 2010-2018 OneLogin, Inc.
    +MIT License
     
     Logout Response class of OneLogin's Python Toolkit.
     
     """
     
    -from base64 import b64decode
    +from base64 import b64encode, b64decode
     from defusedxml.lxml import fromstring
     
    -from urllib import quote_plus
     from xml.dom.minidom import Document
     from defusedxml.minidom import parseString
     
     from onelogin.saml2.constants import OneLogin_Saml2_Constants
     from onelogin.saml2.utils import OneLogin_Saml2_Utils
    +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
     
     
     class OneLogin_Saml2_Logout_Response(object):
    @@ -40,10 +40,12 @@ def __init__(self, settings, response=None):
             """
             self.__settings = settings
             self.__error = None
    +        self.id = None
     
             if response is not None:
                 self.__logout_response = OneLogin_Saml2_Utils.decode_base64_and_inflate(response)
    -            self.document = parseString(self.__logout_response)
    +            self.document = parseString(self.__logout_response, forbid_dtd=True)
    +            self.id = self.document.documentElement.getAttribute('ID')
     
         def get_issuer(self):
             """
    @@ -54,7 +56,7 @@ def get_issuer(self):
             issuer = None
             issuer_nodes = self.__query('/samlp:LogoutResponse/saml:Issuer')
             if len(issuer_nodes) == 1:
    -            issuer = issuer_nodes[0].text
    +            issuer = OneLogin_Saml2_Utils.element_text(issuer_nodes[0])
             return issuer
     
         def get_status(self):
    @@ -69,37 +71,55 @@ def get_status(self):
             status = entries[0].attrib['Value']
             return status
     
    -    def is_valid(self, request_data, request_id=None):
    +    def is_valid(self, request_data, request_id=None, raise_exceptions=False):
             """
             Determines if the SAML LogoutResponse is valid
             :param request_id: The ID of the LogoutRequest sent by this SP to the IdP
             :type request_id: string
    +        :param raise_exceptions: Whether to return false on failure or raise an exception
    +        :type raise_exceptions: Boolean
             :return: Returns if the SAML LogoutResponse is or not valid
             :rtype: boolean
             """
             self.__error = None
    +        lowercase_urlencoding = False
             try:
                 idp_data = self.__settings.get_idp_data()
                 idp_entity_id = idp_data['entityId']
                 get_data = request_data['get_data']
     
    +            if 'lowercase_urlencoding' in request_data.keys():
    +                lowercase_urlencoding = request_data['lowercase_urlencoding']
    +
                 if self.__settings.is_strict():
                     res = OneLogin_Saml2_Utils.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
                     if not isinstance(res, Document):
    -                    raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd')
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd',
    +                        OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT
    +                    )
     
                     security = self.__settings.get_security_data()
     
    -                # Check if the InResponseTo of the Logout Response matchs the ID of the Logout Request (requestId) if provided
    -                if request_id is not None and self.document.documentElement.hasAttribute('InResponseTo'):
    -                    in_response_to = self.document.documentElement.getAttribute('InResponseTo')
    -                    if request_id != in_response_to:
    -                        raise Exception('The InResponseTo of the Logout Response: %s, does not match the ID of the Logout request sent by the SP: %s' % (in_response_to, request_id))
    +                in_response_to = self.get_in_response_to()
    +                # Check if the InResponseTo of the Logout Response matches the ID of the Logout Request (requestId) if provided
    +                if request_id is not None and in_response_to and in_response_to != request_id:
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'The InResponseTo of the Logout Response: %s, does not match the ID of the Logout request sent by the SP: %s' % (in_response_to, request_id),
    +                        OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO
    +                    )
     
                     # Check issuer
                     issuer = self.get_issuer()
                     if issuer is not None and issuer != idp_entity_id:
    -                    raise Exception('Invalid issuer in the Logout Request')
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'Invalid issuer in the Logout Response (expected %(idpEntityId)s, got %(issuer)s)' %
    +                        {
    +                            'idpEntityId': idp_entity_id,
    +                            'issuer': issuer
    +                        },
    +                        OneLogin_Saml2_ValidationError.WRONG_ISSUER
    +                    )
     
                     current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data)
     
    @@ -108,11 +128,17 @@ def is_valid(self, request_data, request_id=None):
                         destination = self.document.documentElement.getAttribute('Destination')
                         if destination != '':
                             if current_url not in destination:
    -                            raise Exception('The LogoutRequest was received at $currentURL instead of $destination')
    +                            raise OneLogin_Saml2_ValidationError(
    +                                'The LogoutResponse was received at %s instead of %s' % (current_url, destination),
    +                                OneLogin_Saml2_ValidationError.WRONG_DESTINATION
    +                            )
     
                     if security['wantMessagesSigned']:
                         if 'Signature' not in get_data:
    -                        raise Exception('The Message of the Logout Response is not signed and the SP require it')
    +                        raise OneLogin_Saml2_ValidationError(
    +                            'The Message of the Logout Response is not signed and the SP require it',
    +                            OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE
    +                        )
     
                 if 'Signature' in get_data:
                     if 'SigAlg' not in get_data:
    @@ -120,17 +146,37 @@ def is_valid(self, request_data, request_id=None):
                     else:
                         sign_alg = get_data['SigAlg']
     
    -                signed_query = 'SAMLResponse=%s' % quote_plus(get_data['SAMLResponse'])
    +                signed_query = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLResponse', lowercase_urlencoding=lowercase_urlencoding)
                     if 'RelayState' in get_data:
    -                    signed_query = '%s&RelayState=%s' % (signed_query, quote_plus(get_data['RelayState']))
    -                signed_query = '%s&SigAlg=%s' % (signed_query, quote_plus(sign_alg))
    -
    -                if 'x509cert' not in idp_data or idp_data['x509cert'] is None:
    -                    raise Exception('In order to validate the sign on the Logout Response, the x509cert of the IdP is required')
    -                cert = idp_data['x509cert']
    +                    signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding))
    +                signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding))
    +
    +                exists_x509cert = 'x509cert' in idp_data and idp_data['x509cert']
    +                exists_multix509sign = 'x509certMulti' in idp_data and \
    +                    'signing' in idp_data['x509certMulti'] and \
    +                    idp_data['x509certMulti']['signing']
    +
    +                if not (exists_x509cert or exists_multix509sign):
    +                    raise OneLogin_Saml2_Error(
    +                        'In order to validate the sign on the Logout Response, the x509cert of the IdP is required',
    +                        OneLogin_Saml2_Error.CERT_NOT_FOUND
    +                    )
    +                if exists_multix509sign:
    +                    for cert in idp_data['x509certMulti']['signing']:
    +                        if OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
    +                            return True
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'Signature validation failed. Logout Response rejected',
    +                        OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
    +                    )
    +                else:
    +                    cert = idp_data['x509cert']
     
    -                if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
    -                    raise Exception('Signature validation failed. Logout Response rejected')
    +                    if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
    +                        raise OneLogin_Saml2_ValidationError(
    +                            'Signature validation failed. Logout Response rejected',
    +                            OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
    +                        )
     
                 return True
             # pylint: disable=R0801
    @@ -138,7 +184,9 @@ def is_valid(self, request_data, request_id=None):
                 self.__error = err.__str__()
                 debug = self.__settings.is_debug_active()
                 if debug:
    -                print err.__str__()
    +                print(err.__str__())
    +            if raise_exceptions:
    +                raise err
                 return False
     
         def __query(self, query):
    @@ -151,7 +199,7 @@ def __query(self, query):
             """
             # Switch to lxml for querying
             xml = self.document.toxml()
    -        return OneLogin_Saml2_Utils.query(fromstring(xml), query)
    +        return OneLogin_Saml2_Utils.query(fromstring(xml, forbid_dtd=True), query)
     
         def build(self, in_response_to):
             """
    @@ -188,16 +236,39 @@ def build(self, in_response_to):
     
             self.__logout_response = logout_response
     
    -    def get_response(self):
    +    def get_in_response_to(self):
    +        """
    +        Gets the ID of the LogoutRequest which this response is in response to
    +        :returns: ID of LogoutRequest this LogoutResponse is in response to or None if it is not present
    +        :rtype: str
    +        """
    +        return self.document.documentElement.getAttribute('InResponseTo')
    +
    +    def get_response(self, deflate=True):
    +        """
    +        Returns the Logout Response defated, base64encoded
    +        :param deflate: It makes the deflate process optional
    +        :type: bool
    +        :return: Logout Response maybe deflated and base64 encoded
    +        :rtype: string
    +        """
    +        if deflate:
    +            response = OneLogin_Saml2_Utils.deflate_and_base64_encode(self.__logout_response)
    +        else:
    +            response = b64encode(self.__logout_response)
    +        return response
    +
    +    def get_xml(self):
             """
    -        Returns a Logout Response object.
    -        :return: Logout Response deflated and base64 encoded
    +        Returns the XML that will be sent as part of the response
    +        or that was received at the SP
    +        :return: XML response body
             :rtype: string
             """
    -        return OneLogin_Saml2_Utils.deflate_and_base64_encode(self.__logout_response)
    +        return self.__logout_response
     
         def get_error(self):
             """
    -        After execute a validation process, if fails this method returns the cause
    +        After executing a validation process, if it fails this method returns the cause
             """
             return self.__error
    diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py
    index 891500e5..2260bb8b 100644
    --- a/src/onelogin/saml2/metadata.py
    +++ b/src/onelogin/saml2/metadata.py
    @@ -2,14 +2,14 @@
     
     """ OneLogin_Saml2_Metadata class
     
    -Copyright (c) 2014, OneLogin, Inc.
    -All rights reserved.
    +Copyright (c) 2010-2018 OneLogin, Inc.
    +MIT License
     
     Metadata class of OneLogin's Python Toolkit.
     
     """
     
    -from time import gmtime, strftime
    +from time import gmtime, strftime, time
     from datetime import datetime
     from defusedxml.minidom import parseString
     
    @@ -50,11 +50,11 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
             :param contacts: Contacts info
             :type contacts: dict
     
    -        :param organization: Organization ingo
    +        :param organization: Organization info
             :type organization: dict
             """
             if valid_until is None:
    -            valid_until = int(datetime.now().strftime("%s")) + OneLogin_Saml2_Metadata.TIME_VALID
    +            valid_until = int(time()) + OneLogin_Saml2_Metadata.TIME_VALID
             if not isinstance(valid_until, basestring):
                 if isinstance(valid_until, datetime):
                     valid_until_time = valid_until.timetuple()
    @@ -76,6 +76,61 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
             if organization is None:
                 organization = {}
     
    +        str_attribute_consuming_service = ''
    +        if 'attributeConsumingService' in sp and len(sp['attributeConsumingService']):
    +            attr_cs_desc_str = ''
    +            if "serviceDescription" in sp['attributeConsumingService']:
    +                attr_cs_desc_str = """            %s
    +""" % sp['attributeConsumingService']['serviceDescription']
    +
    +            requested_attribute_data = []
    +            for req_attribs in sp['attributeConsumingService']['requestedAttributes']:
    +                req_attr_nameformat_str = req_attr_friendlyname_str = req_attr_isrequired_str = ''
    +                req_attr_aux_str = ' />'
    +
    +                if 'nameFormat' in req_attribs.keys() and req_attribs['nameFormat']:
    +                    req_attr_nameformat_str = " NameFormat=\"%s\"" % req_attribs['nameFormat']
    +                if 'friendlyName' in req_attribs.keys() and req_attribs['friendlyName']:
    +                    req_attr_friendlyname_str = " FriendlyName=\"%s\"" % req_attribs['friendlyName']
    +                if 'isRequired' in req_attribs.keys() and req_attribs['isRequired']:
    +                    req_attr_isrequired_str = " isRequired=\"%s\"" % 'true' if req_attribs['isRequired'] else 'false'
    +
    +                if 'attributeValue' in req_attribs.keys() and req_attribs['attributeValue']:
    +                    if isinstance(req_attribs['attributeValue'], basestring):
    +                        req_attribs['attributeValue'] = [req_attribs['attributeValue']]
    +
    +                    req_attr_aux_str = ">"
    +                    for attrValue in req_attribs['attributeValue']:
    +                        req_attr_aux_str += """
    +                %(attributeValue)s""" % \
    +                            {
    +                                'attributeValue': attrValue
    +                            }
    +                    req_attr_aux_str += """
    +            """
    +
    +                requested_attribute = """            
    +            %(service_name)s
    +%(attr_cs_desc)s%(requested_attribute_str)s
    +        
    +""" % \
    +                {
    +                    'service_name': sp['attributeConsumingService']['serviceName'],
    +                    'attr_cs_desc': attr_cs_desc_str,
    +                    'requested_attribute_str': '\n'.join(requested_attribute_data)
    +                }
    +
             sls = ''
             if 'singleLogoutService' in sp and 'url' in sp['singleLogoutService']:
                 sls = """         0:
    -            organization_info = []
    +            organization_names = []
    +            organization_displaynames = []
    +            organization_urls = []
                 for (lang, info) in organization.items():
    -                org = """    
    -        %(name)s
    -        %(display_name)s
    -        %(url)s
    -    """ % \
    -                    {
    -                        'lang': lang,
    -                        'name': info['name'],
    -                        'display_name': info['displayname'],
    -                        'url': info['url'],
    -                    }
    -                organization_info.append(org)
    -            str_organization = '\n'.join(organization_info)
    +                organization_names.append("""        %s""" % (lang, info['name']))
    +                organization_displaynames.append("""        %s""" % (lang, info['displayname']))
    +                organization_urls.append("""        %s""" % (lang, info['url']))
    +            org_data = '\n'.join(organization_names) + '\n' + '\n'.join(organization_displaynames) + '\n' + '\n'.join(organization_urls)
    +            str_organization = """    
    +%(org)s
    +    \n""" % {'org': org_data}
     
             str_contacts = ''
             if len(contacts) > 0:
    @@ -120,9 +171,9 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
                             'email': info['emailAddress'],
                         }
                     contacts_info.append(contact)
    -            str_contacts = '\n'.join(contacts_info)
    +            str_contacts = '\n'.join(contacts_info) + '\n'
     
    -        metadata = """
    +        metadata = u"""
     
    -    
    -%(organization)s
    -%(contacts)s
    -""" % \
    +%(attribute_consuming_service)s    
    +%(organization)s%(contacts)s""" % \
                 {
                     'valid': ('validUntil="%s"' % valid_until_str) if valid_until_str else '',
                     'cache': ('cacheDuration="%s"' % cache_duration_str) if cache_duration_str else '',
    @@ -148,12 +197,12 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
                     'sls': sls,
                     'organization': str_organization,
                     'contacts': str_contacts,
    +                'attribute_consuming_service': str_attribute_consuming_service
                 }
    -
             return metadata
     
         @staticmethod
    -    def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1):
    +    def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1, digest_algorithm=OneLogin_Saml2_Constants.SHA1):
             """
             Signs the metadata with the key/cert provided
     
    @@ -169,15 +218,18 @@ def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.R
             :param sign_algorithm: Signature algorithm method
             :type sign_algorithm: string
     
    +        :param digest_algorithm: Digest algorithm method
    +        :type digest_algorithm: string
    +
             :returns: Signed Metadata
             :rtype: string
             """
    -        return OneLogin_Saml2_Utils.add_sign(metadata, key, cert, False, sign_algorithm)
    +        return OneLogin_Saml2_Utils.add_sign(metadata, key, cert, False, sign_algorithm, digest_algorithm)
     
         @staticmethod
    -    def add_x509_key_descriptors(metadata, cert=None):
    +    def add_x509_key_descriptors(metadata, cert=None, add_encryption=True):
             """
    -        Adds the x509 descriptors (sign/encriptation) to the metadata
    +        Adds the x509 descriptors (sign/encryption) to the metadata
             The same cert will be used for sign/encrypt
     
             :param metadata: SAML Metadata XML
    @@ -186,13 +238,16 @@ def add_x509_key_descriptors(metadata, cert=None):
             :param cert: x509 cert
             :type cert: string
     
    +        :param add_encryption: Determines if the KeyDescriptor[use="encryption"] should be added.
    +        :type add_encryption: boolean
    +
             :returns: Metadata with KeyDescriptors
             :rtype: string
             """
             if cert is None or cert == '':
                 return metadata
             try:
    -            xml = parseString(metadata)
    +            xml = parseString(metadata.encode('utf-8'), forbid_dtd=True)
             except Exception as e:
                 raise Exception('Error parsing metadata. ' + e.message)
     
    @@ -213,18 +268,18 @@ def add_x509_key_descriptors(metadata, cert=None):
     
             sp_sso_descriptor = entity_descriptor.getElementsByTagName('md:SPSSODescriptor')[0]
             sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild)
    -        sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild)
    +        if add_encryption:
    +            sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild)
     
             signing = xml.getElementsByTagName('md:KeyDescriptor')[0]
             signing.setAttribute('use', 'signing')
    -
    -        encryption = xml.getElementsByTagName('md:KeyDescriptor')[1]
    -        encryption.setAttribute('use', 'encryption')
    -
             signing.appendChild(key_info)
    -        encryption.appendChild(key_info.cloneNode(True))
    -
             signing.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS)
    -        encryption.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS)
    +
    +        if add_encryption:
    +            encryption = xml.getElementsByTagName('md:KeyDescriptor')[1]
    +            encryption.setAttribute('use', 'encryption')
    +            encryption.appendChild(key_info.cloneNode(True))
    +            encryption.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS)
     
             return xml.toxml()
    diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
    index f93f756e..73c24f9a 100644
    --- a/src/onelogin/saml2/response.py
    +++ b/src/onelogin/saml2/response.py
    @@ -2,8 +2,8 @@
     
     """ OneLogin_Saml2_Response class
     
    -Copyright (c) 2014, OneLogin, Inc.
    -All rights reserved.
    +Copyright (c) 2010-2018 OneLogin, Inc.
    +MIT License
     
     SAML Response class of OneLogin's Python Toolkit.
     
    @@ -11,12 +11,12 @@
     
     from base64 import b64decode
     from copy import deepcopy
    -from lxml import etree
    -from defusedxml.lxml import fromstring
    +from defusedxml.lxml import tostring, fromstring
     from xml.dom.minidom import Document
     
     from onelogin.saml2.constants import OneLogin_Saml2_Constants
    -from onelogin.saml2.utils import OneLogin_Saml2_Utils
    +from onelogin.saml2.utils import OneLogin_Saml2_Utils, return_false_on_exception
    +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
     
     
     class OneLogin_Saml2_Response(object):
    @@ -40,9 +40,10 @@ def __init__(self, settings, response):
             self.__settings = settings
             self.__error = None
             self.response = b64decode(response)
    -        self.document = fromstring(self.response)
    +        self.document = fromstring(self.response, forbid_dtd=True)
             self.decrypted_document = None
             self.encrypted = None
    +        self.valid_scd_not_on_or_after = None
     
             # Quick check for the presence of EncryptedAssertion
             encrypted_assertion_nodes = self.__query('/samlp:Response/saml:EncryptedAssertion')
    @@ -51,7 +52,7 @@ def __init__(self, settings, response):
                 self.encrypted = True
                 self.decrypted_document = self.__decrypt_assertion(decrypted_document)
     
    -    def is_valid(self, request_data, request_id=None):
    +    def is_valid(self, request_data, request_id=None, raise_exceptions=False):
             """
             Validates the response object.
     
    @@ -61,6 +62,9 @@ def is_valid(self, request_data, request_id=None):
             :param request_id: Optional argument. The ID of the AuthNRequest sent by this SP to the IdP
             :type request_id: string
     
    +        :param raise_exceptions: Whether to return false on failure or raise an exception
    +        :type raise_exceptions: Boolean
    +
             :returns: True if the SAML Response is valid, False if not
             :rtype: bool
             """
    @@ -68,90 +72,186 @@ def is_valid(self, request_data, request_id=None):
             try:
                 # Checks SAML version
                 if self.document.get('Version', None) != '2.0':
    -                raise Exception('Unsupported SAML version')
    +                raise OneLogin_Saml2_ValidationError(
    +                    'Unsupported SAML version',
    +                    OneLogin_Saml2_ValidationError.UNSUPPORTED_SAML_VERSION
    +                )
     
                 # Checks that ID exists
                 if self.document.get('ID', None) is None:
    -                raise Exception('Missing ID attribute on SAML Response')
    -
    -            # Checks that the response only has one assertion
    -            if not self.validate_num_assertions():
    -                raise Exception('SAML Response must contain 1 assertion')
    +                raise OneLogin_Saml2_ValidationError(
    +                    'Missing ID attribute on SAML Response',
    +                    OneLogin_Saml2_ValidationError.MISSING_ID
    +                )
     
                 # Checks that the response has the SUCCESS status
                 self.check_status()
     
    +            # Checks that the response only has one assertion
    +            if not self.validate_num_assertions():
    +                raise OneLogin_Saml2_ValidationError(
    +                    'SAML Response must contain 1 assertion',
    +                    OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_ASSERTIONS
    +                )
    +
                 idp_data = self.__settings.get_idp_data()
                 idp_entity_id = idp_data.get('entityId', '')
                 sp_data = self.__settings.get_sp_data()
                 sp_entity_id = sp_data.get('entityId', '')
     
    -            sign_nodes = self.__query('//ds:Signature')
    +            signed_elements = self.process_signed_elements()
     
    -            signed_elements = []
    -            for sign_node in sign_nodes:
    -                signed_elements.append(sign_node.getparent().tag)
    +            has_signed_response = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements
    +            has_signed_assertion = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML in signed_elements
     
                 if self.__settings.is_strict():
    -                res = OneLogin_Saml2_Utils.validate_xml(etree.tostring(self.document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
    +                no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd'
    +                res = OneLogin_Saml2_Utils.validate_xml(
    +                    tostring(self.document),
    +                    'saml-schema-protocol-2.0.xsd',
    +                    self.__settings.is_debug_active()
    +                )
                     if not isinstance(res, Document):
    -                    raise Exception('Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd')
    +                    raise OneLogin_Saml2_ValidationError(
    +                        no_valid_xml_msg,
    +                        OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT
    +                    )
    +
    +                # If encrypted, check also the decrypted document
    +                if self.encrypted:
    +                    res = OneLogin_Saml2_Utils.validate_xml(
    +                        tostring(self.decrypted_document),
    +                        'saml-schema-protocol-2.0.xsd',
    +                        self.__settings.is_debug_active()
    +                    )
    +                    if not isinstance(res, Document):
    +                        raise OneLogin_Saml2_ValidationError(
    +                            no_valid_xml_msg,
    +                            OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT
    +                        )
     
                     security = self.__settings.get_security_data()
                     current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data)
     
    +                in_response_to = self.get_in_response_to()
    +                if request_id is None and in_response_to is not None and security.get('rejectUnsolicitedResponsesWithInResponseTo', False):
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'The Response has an InResponseTo attribute: %s while no InResponseTo was expected' % in_response_to,
    +                        OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO
    +                    )
    +
                     # Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided
    -                in_response_to = self.document.get('InResponseTo', None)
    -                if in_response_to and request_id:
    -                    if in_response_to != request_id:
    -                        raise Exception('The InResponseTo of the Response: %s, does not match the ID of the AuthNRequest sent by the SP: %s' % (in_response_to, request_id))
    +                if request_id is not None and in_response_to != request_id:
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'The InResponseTo of the Response: %s, does not match the ID of the AuthNRequest sent by the SP: %s' % (in_response_to, request_id),
    +                        OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO
    +                    )
     
                     if not self.encrypted and security.get('wantAssertionsEncrypted', False):
    -                    raise Exception('The assertion of the Response is not encrypted and the SP require it')
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'The assertion of the Response is not encrypted and the SP require it',
    +                        OneLogin_Saml2_ValidationError.NO_ENCRYPTED_ASSERTION
    +                    )
     
                     if security.get('wantNameIdEncrypted', False):
                         encrypted_nameid_nodes = self.__query_assertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData')
    -                    if len(encrypted_nameid_nodes) == 0:
    -                        raise Exception('The NameID of the Response is not encrypted and the SP require it')
    -
    -                # Checks that there is at least one AttributeStatement
    +                    if len(encrypted_nameid_nodes) != 1:
    +                        raise OneLogin_Saml2_ValidationError(
    +                            'The NameID of the Response is not encrypted and the SP require it',
    +                            OneLogin_Saml2_ValidationError.NO_ENCRYPTED_NAMEID
    +                        )
    +
    +                # Checks that a Conditions element exists
    +                if not self.check_one_condition():
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'The Assertion must include a Conditions element',
    +                        OneLogin_Saml2_ValidationError.MISSING_CONDITIONS
    +                    )
    +
    +                # Validates Assertion timestamps
    +                self.validate_timestamps(raise_exceptions=True)
    +
    +                # Checks that an AuthnStatement element exists and is unique
    +                if not self.check_one_authnstatement():
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'The Assertion must include an AuthnStatement element',
    +                        OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_AUTHSTATEMENTS
    +                    )
    +
    +                # Checks that the response has all of the AuthnContexts that we provided in the request.
    +                # Only check if failOnAuthnContextMismatch is true and requestedAuthnContext is set to a list.
    +                requested_authn_contexts = security.get('requestedAuthnContext', True)
    +
    +                if security.get('failOnAuthnContextMismatch', False) and requested_authn_contexts and requested_authn_contexts is not True:
    +                    authn_contexts = self.get_authn_contexts()
    +                    unmatched_contexts = set(requested_authn_contexts).difference(authn_contexts)
    +                    if unmatched_contexts:
    +                        raise OneLogin_Saml2_ValidationError(
    +                            'The AuthnContext "%s" didn\'t include requested context "%s"' % (', '.join(authn_contexts), ', '.join(unmatched_contexts)),
    +                            OneLogin_Saml2_ValidationError.AUTHN_CONTEXT_MISMATCH
    +                        )
    +
    +                # Checks that there is at least one AttributeStatement if required
                     attribute_statement_nodes = self.__query_assertion('/saml:AttributeStatement')
    -                if not attribute_statement_nodes:
    -                    raise Exception('There is no AttributeStatement on the Response')
    -
    -                # Validates Asserion timestamps
    -                if not self.validate_timestamps():
    -                    raise Exception('Timing issues (please check your clock settings)')
    +                if security.get('wantAttributeStatement', True) and not attribute_statement_nodes:
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'There is no AttributeStatement on the Response',
    +                        OneLogin_Saml2_ValidationError.NO_ATTRIBUTESTATEMENT
    +                    )
     
                     encrypted_attributes_nodes = self.__query_assertion('/saml:AttributeStatement/saml:EncryptedAttribute')
                     if encrypted_attributes_nodes:
    -                    raise Exception('There is an EncryptedAttribute in the Response and this SP not support them')
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'There is an EncryptedAttribute in the Response and this SP not support them',
    +                        OneLogin_Saml2_ValidationError.ENCRYPTED_ATTRIBUTES
    +                    )
     
                     # Checks destination
    -                destination = self.document.get('Destination', '')
    +                destination = self.document.get('Destination', None)
                     if destination:
                         if not destination.startswith(current_url):
                             # TODO: Review if following lines are required, since we can control the
                             # request_data
                             #  current_url_routed = OneLogin_Saml2_Utils.get_self_routed_url_no_query(request_data)
                             #  if not destination.startswith(current_url_routed):
    -                        raise Exception('The response was received at %s instead of %s' % (current_url, destination))
    +                        raise OneLogin_Saml2_ValidationError(
    +                            'The response was received at %s instead of %s' % (current_url, destination),
    +                            OneLogin_Saml2_ValidationError.WRONG_DESTINATION
    +                        )
    +                elif destination == '':
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'The response has an empty Destination value',
    +                        OneLogin_Saml2_ValidationError.EMPTY_DESTINATION
    +                    )
     
                     # Checks audience
                     valid_audiences = self.get_audiences()
                     if valid_audiences and sp_entity_id not in valid_audiences:
    -                    raise Exception('%s is not a valid audience for this Response' % sp_entity_id)
    +                    raise OneLogin_Saml2_ValidationError(
    +                        '%s is not a valid audience for this Response' % sp_entity_id,
    +                        OneLogin_Saml2_ValidationError.WRONG_AUDIENCE
    +                    )
     
                     # Checks the issuers
                     issuers = self.get_issuers()
                     for issuer in issuers:
                         if issuer is None or issuer != idp_entity_id:
    -                        raise Exception('Invalid issuer in the Assertion/Response')
    +                        raise OneLogin_Saml2_ValidationError(
    +                            'Invalid issuer in the Assertion/Response (expected %(idpEntityId)s, got %(issuer)s)' %
    +                            {
    +                                'idpEntityId': idp_entity_id,
    +                                'issuer': issuer
    +                            },
    +                            OneLogin_Saml2_ValidationError.WRONG_ISSUER
    +                        )
     
                     # Checks the session Expiration
                     session_expiration = self.get_session_not_on_or_after()
                     if session_expiration and session_expiration <= OneLogin_Saml2_Utils.now():
    -                    raise Exception('The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response')
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response',
    +                        OneLogin_Saml2_ValidationError.SESSION_EXPIRED
    +                    )
     
                     # Checks the SubjectConfirmation, at least one SubjectConfirmation must be valid
                     any_subject_confirmation = False
    @@ -166,7 +266,9 @@ def is_valid(self, request_data, request_id=None):
                             continue
                         else:
                             irt = sc_data.get('InResponseTo', None)
    -                        if irt != in_response_to:
    +                        if (in_response_to is None and irt is not None and
    +                           security.get('rejectUnsolicitedResponsesWithInResponseTo', False)) or \
    +                           in_response_to and irt and irt != in_response_to:
                                 continue
                             recipient = sc_data.get('Recipient', None)
                             if recipient and current_url not in recipient:
    @@ -181,42 +283,67 @@ def is_valid(self, request_data, request_id=None):
                                 parsed_nb = OneLogin_Saml2_Utils.parse_SAML_to_time(nb)
                                 if parsed_nb > OneLogin_Saml2_Utils.now():
                                     continue
    +
    +                        if nooa:
    +                            self.valid_scd_not_on_or_after = OneLogin_Saml2_Utils.parse_SAML_to_time(nooa)
    +
                             any_subject_confirmation = True
                             break
     
                     if not any_subject_confirmation:
    -                    raise Exception('A valid SubjectConfirmation was not found on this Response')
    -
    -                if security.get('wantAssertionsSigned', False) and ('{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML) not in signed_elements:
    -                    raise Exception('The Assertion of the Response is not signed and the SP require it')
    -
    -                if security.get('wantMessagesSigned', False) and ('{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP) not in signed_elements:
    -                    raise Exception('The Message of the Response is not signed and the SP require it')
    -
    -            if len(signed_elements) > 0:
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'A valid SubjectConfirmation was not found on this Response',
    +                        OneLogin_Saml2_ValidationError.WRONG_SUBJECTCONFIRMATION
    +                    )
    +
    +                if security.get('wantAssertionsSigned', False) and not has_signed_assertion:
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'The Assertion of the Response is not signed and the SP require it',
    +                        OneLogin_Saml2_ValidationError.NO_SIGNED_ASSERTION
    +                    )
    +
    +                if security.get('wantMessagesSigned', False) and not has_signed_response:
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'The Message of the Response is not signed and the SP require it',
    +                        OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE
    +                    )
    +
    +            if not signed_elements or (not has_signed_response and not has_signed_assertion):
    +                raise OneLogin_Saml2_ValidationError(
    +                    'No Signature found. SAML Response rejected',
    +                    OneLogin_Saml2_ValidationError.NO_SIGNATURE_FOUND
    +                )
    +            else:
                     cert = idp_data.get('x509cert', None)
                     fingerprint = idp_data.get('certFingerprint', None)
                     fingerprintalg = idp_data.get('certFingerprintAlgorithm', None)
     
    -                # Only validates the first sign found
    -                if '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements:
    -                    document_to_validate = self.document
    -                else:
    -                    if self.encrypted:
    -                        document_to_validate = self.decrypted_document
    -                    else:
    -                        document_to_validate = self.document
    -                if not OneLogin_Saml2_Utils.validate_sign(document_to_validate, cert, fingerprint, fingerprintalg):
    -                    raise Exception('Signature validation failed. SAML Response rejected')
    -            else:
    -                raise Exception('No Signature found. SAML Response rejected')
    +                multicerts = None
    +                if 'x509certMulti' in idp_data and 'signing' in idp_data['x509certMulti'] and idp_data['x509certMulti']['signing']:
    +                    multicerts = idp_data['x509certMulti']['signing']
    +
    +                # If find a Signature on the Response, validates it checking the original response
    +                if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH, multicerts=multicerts, raise_exceptions=False):
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'Signature validation failed. SAML Response rejected',
    +                        OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
    +                    )
    +
    +                document_check_assertion = self.decrypted_document if self.encrypted else self.document
    +                if has_signed_assertion and not OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH, multicerts=multicerts, raise_exceptions=False):
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'Signature validation failed. SAML Response rejected',
    +                        OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
    +                    )
     
                 return True
             except Exception as err:
                 self.__error = err.__str__()
                 debug = self.__settings.is_debug_active()
                 if debug:
    -                print err.__str__()
    +                print(err.__str__())
    +            if raise_exceptions:
    +                raise err
                 return False
     
         def check_status(self):
    @@ -234,7 +361,30 @@ def check_status(self):
                 status_msg = status.get('msg', None)
                 if status_msg:
                     status_exception_msg += ' -> ' + status_msg
    -            raise Exception(status_exception_msg)
    +            raise OneLogin_Saml2_ValidationError(
    +                status_exception_msg,
    +                OneLogin_Saml2_ValidationError.STATUS_CODE_IS_NOT_SUCCESS
    +            )
    +
    +    def check_one_condition(self):
    +        """
    +        Checks that the samlp:Response/saml:Assertion/saml:Conditions element exists and is unique.
    +        """
    +        condition_nodes = self.__query_assertion('/saml:Conditions')
    +        if len(condition_nodes) == 1:
    +            return True
    +        else:
    +            return False
    +
    +    def check_one_authnstatement(self):
    +        """
    +        Checks that the samlp:Response/saml:Assertion/saml:AuthnStatement element exists and is unique.
    +        """
    +        authnstatement_nodes = self.__query_assertion('/saml:AuthnStatement')
    +        if len(authnstatement_nodes) == 1:
    +            return True
    +        else:
    +            return False
     
         def get_audiences(self):
             """
    @@ -243,12 +393,25 @@ def get_audiences(self):
             :returns: The valid audiences for the SAML Response
             :rtype: list
             """
    -        audiences = []
    -
             audience_nodes = self.__query_assertion('/saml:Conditions/saml:AudienceRestriction/saml:Audience')
    -        for audience_node in audience_nodes:
    -            audiences.append(audience_node.text)
    -        return audiences
    +        return [OneLogin_Saml2_Utils.element_text(node) for node in audience_nodes if OneLogin_Saml2_Utils.element_text(node) is not None]
    +
    +    def get_authn_contexts(self):
    +        """
    +        Gets the authentication contexts
    +         :returns: The authentication classes for the SAML Response
    +        :rtype: list
    +        """
    +        authn_context_nodes = self.__query_assertion('/saml:AuthnStatement/saml:AuthnContext/saml:AuthnContextClassRef')
    +        return [OneLogin_Saml2_Utils.element_text(node) for node in authn_context_nodes]
    +
    +    def get_in_response_to(self):
    +        """
    +        Gets the ID of the request which this response is in response to
    +        :returns: ID of AuthNRequest this Response is in response to or None if it is not present
    +        :rtype: str
    +        """
    +        return self.document.get('InResponseTo')
     
         def get_issuers(self):
             """
    @@ -259,13 +422,24 @@ def get_issuers(self):
             """
             issuers = []
     
    -        message_issuer_nodes = self.__query('/samlp:Response/saml:Issuer')
    -        if message_issuer_nodes:
    -            issuers.append(message_issuer_nodes[0].text)
    +        message_issuer_nodes = OneLogin_Saml2_Utils.query(self.document, '/samlp:Response/saml:Issuer')
    +        if len(message_issuer_nodes) > 0:
    +            if len(message_issuer_nodes) == 1:
    +                issuers.append(OneLogin_Saml2_Utils.element_text(message_issuer_nodes[0]))
    +            else:
    +                raise OneLogin_Saml2_ValidationError(
    +                    'Issuer of the Response is multiple.',
    +                    OneLogin_Saml2_ValidationError.ISSUER_MULTIPLE_IN_RESPONSE
    +                )
     
             assertion_issuer_nodes = self.__query_assertion('/saml:Issuer')
    -        if assertion_issuer_nodes:
    -            issuers.append(assertion_issuer_nodes[0].text)
    +        if len(assertion_issuer_nodes) == 1:
    +            issuers.append(OneLogin_Saml2_Utils.element_text(assertion_issuer_nodes[0]))
    +        else:
    +            raise OneLogin_Saml2_ValidationError(
    +                'Issuer of the Assertion not found or multiple.',
    +                OneLogin_Saml2_ValidationError.ISSUER_NOT_FOUND_IN_ASSERTION
    +            )
     
             return list(set(issuers))
     
    @@ -277,6 +451,8 @@ def get_nameid_data(self):
             :rtype: dict
             """
             nameid = None
    +        nameid_data = {}
    +
             encrypted_id_data_nodes = self.__query_assertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData')
             if encrypted_id_data_nodes:
                 encrypted_data = encrypted_id_data_nodes[0]
    @@ -286,14 +462,36 @@ def get_nameid_data(self):
                 nameid_nodes = self.__query_assertion('/saml:Subject/saml:NameID')
                 if nameid_nodes:
                     nameid = nameid_nodes[0]
    -        if nameid is None:
    -            raise Exception('Not NameID found in the assertion of the Response')
     
    -        nameid_data = {'Value': nameid.text}
    -        for attr in ['Format', 'SPNameQualifier', 'NameQualifier']:
    -            value = nameid.get(attr, None)
    -            if value:
    -                nameid_data[attr] = value
    +        is_strict = self.__settings.is_strict()
    +        want_nameid = self.__settings.get_security_data().get('wantNameId', True)
    +        if nameid is None:
    +            if is_strict and want_nameid:
    +                raise OneLogin_Saml2_ValidationError(
    +                    'NameID not found in the assertion of the Response',
    +                    OneLogin_Saml2_ValidationError.NO_NAMEID
    +                )
    +        else:
    +            if is_strict and want_nameid and not OneLogin_Saml2_Utils.element_text(nameid):
    +                raise OneLogin_Saml2_ValidationError(
    +                    'An empty NameID value found',
    +                    OneLogin_Saml2_ValidationError.EMPTY_NAMEID
    +                )
    +
    +            nameid_data = {'Value': OneLogin_Saml2_Utils.element_text(nameid)}
    +            for attr in ['Format', 'SPNameQualifier', 'NameQualifier']:
    +                value = nameid.get(attr, None)
    +                if value:
    +                    if is_strict and attr == 'SPNameQualifier':
    +                        sp_data = self.__settings.get_sp_data()
    +                        sp_entity_id = sp_data.get('entityId', '')
    +                        if sp_entity_id != value:
    +                            raise OneLogin_Saml2_ValidationError(
    +                                'The SPNameQualifier value mistmatch the SP entityID value.',
    +                                OneLogin_Saml2_ValidationError.SP_NAME_QUALIFIER_NAME_MISMATCH
    +                            )
    +
    +                    nameid_data[attr] = value
             return nameid_data
     
         def get_nameid(self):
    @@ -301,10 +499,52 @@ def get_nameid(self):
             Gets the NameID provided by the SAML Response from the IdP
     
             :returns: NameID (value)
    -        :rtype: string
    +        :rtype: string|None
    +        """
    +        nameid_value = None
    +        nameid_data = self.get_nameid_data()
    +        if nameid_data and 'Value' in nameid_data.keys():
    +            nameid_value = nameid_data['Value']
    +        return nameid_value
    +
    +    def get_nameid_format(self):
    +        """
    +        Gets the NameID Format provided by the SAML Response from the IdP
    +
    +        :returns: NameID Format
    +        :rtype: string|None
             """
    +        nameid_format = None
             nameid_data = self.get_nameid_data()
    -        return nameid_data['Value']
    +        if nameid_data and 'Format' in nameid_data.keys():
    +            nameid_format = nameid_data['Format']
    +        return nameid_format
    +
    +    def get_nameid_nq(self):
    +        """
    +        Gets the NameID NameQualifier provided by the SAML Response from the IdP
    +
    +        :returns: NameID NameQualifier
    +        :rtype: string|None
    +        """
    +        nameid_nq = None
    +        nameid_data = self.get_nameid_data()
    +        if nameid_data and 'NameQualifier' in nameid_data.keys():
    +            nameid_nq = nameid_data['NameQualifier']
    +        return nameid_nq
    +
    +    def get_nameid_spnq(self):
    +        """
    +        Gets the NameID SP NameQualifier provided by the SAML response from the IdP.
    +
    +        :returns: NameID SP NameQualifier
    +        :rtype: string|None
    +        """
    +        nameid_spnq = None
    +        nameid_data = self.get_nameid_data()
    +        if nameid_data and 'SPNameQualifier' in nameid_data.keys():
    +            nameid_spnq = nameid_data['SPNameQualifier']
    +        return nameid_spnq
     
         def get_session_not_on_or_after(self):
             """
    @@ -320,6 +560,12 @@ def get_session_not_on_or_after(self):
                 not_on_or_after = OneLogin_Saml2_Utils.parse_SAML_to_time(authn_statement_nodes[0].get('SessionNotOnOrAfter'))
             return not_on_or_after
     
    +    def get_assertion_not_on_or_after(self):
    +        """
    +        Returns the NotOnOrAfter value of the valid SubjectConfirmationData node if any
    +        """
    +        return self.valid_scd_not_on_or_after
    +
         def get_session_index(self):
             """
             Gets the SessionIndex from the AuthnStatement
    @@ -345,9 +591,32 @@ def get_attributes(self):
             attribute_nodes = self.__query_assertion('/saml:AttributeStatement/saml:Attribute')
             for attribute_node in attribute_nodes:
                 attr_name = attribute_node.get('Name')
    +            if attr_name in attributes.keys():
    +                raise OneLogin_Saml2_ValidationError(
    +                    'Found an Attribute element with duplicated Name',
    +                    OneLogin_Saml2_ValidationError.DUPLICATED_ATTRIBUTE_NAME_FOUND
    +                )
    +
                 values = []
    -            for attr in attribute_node.iterchildren('{%s}AttributeValue' % OneLogin_Saml2_Constants.NSMAP['saml']):
    -                values.append(attr.text)
    +            for attr in attribute_node.iterchildren('{%s}AttributeValue' % OneLogin_Saml2_Constants.NSMAP[OneLogin_Saml2_Constants.NS_PREFIX_SAML]):
    +                # Remove any whitespace (which may be present where attributes are
    +                # nested inside NameID children).
    +                attr_text = OneLogin_Saml2_Utils.element_text(attr)
    +                if attr_text:
    +                    attr_text = attr_text.strip()
    +                    if attr_text:
    +                        values.append(attr_text)
    +
    +                # Parse any nested NameID children
    +                for nameid in attr.iterchildren('{%s}NameID' % OneLogin_Saml2_Constants.NSMAP[OneLogin_Saml2_Constants.NS_PREFIX_SAML]):
    +                    values.append({
    +                        'NameID': {
    +                            'Format': nameid.get('Format'),
    +                            'NameQualifier': nameid.get('NameQualifier'),
    +                            'value': OneLogin_Saml2_Utils.element_text(nameid)
    +                        }
    +                    })
    +
                 attributes[attr_name] = values
             return attributes
     
    @@ -358,14 +627,136 @@ def validate_num_assertions(self):
             :returns: True if only 1 assertion encrypted or not
             :rtype: bool
             """
    -        encrypted_assertion_nodes = self.__query('/samlp:Response/saml:EncryptedAssertion')
    -        assertion_nodes = self.__query('/samlp:Response/saml:Assertion')
    -        return (len(encrypted_assertion_nodes) + len(assertion_nodes)) == 1
    +        encrypted_assertion_nodes = OneLogin_Saml2_Utils.query(self.document, '//saml:EncryptedAssertion')
    +        assertion_nodes = OneLogin_Saml2_Utils.query(self.document, '//saml:Assertion')
    +
    +        valid = len(encrypted_assertion_nodes) + len(assertion_nodes) == 1
    +
    +        if (self.encrypted):
    +            assertion_nodes = OneLogin_Saml2_Utils.query(self.decrypted_document, '//saml:Assertion')
    +            valid = valid and len(assertion_nodes) == 1
    +
    +        return valid
    +
    +    def process_signed_elements(self):
    +        """
    +        Verifies the signature nodes:
    +         - Checks that are Response or Assertion
    +         - Check that IDs and reference URI are unique and consistent.
    +
    +        :returns: The signed elements tag names
    +        :rtype: list
    +        """
    +        sign_nodes = self.__query('//ds:Signature')
    +
    +        signed_elements = []
    +        verified_seis = []
    +        verified_ids = []
    +        response_tag = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP
    +        assertion_tag = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML
    +
    +        for sign_node in sign_nodes:
    +            signed_element = sign_node.getparent().tag
    +            if signed_element != response_tag and signed_element != assertion_tag:
    +                raise OneLogin_Saml2_ValidationError(
    +                    'Invalid Signature Element %s SAML Response rejected' % signed_element,
    +                    OneLogin_Saml2_ValidationError.WRONG_SIGNED_ELEMENT
    +                )
    +
    +            if not sign_node.getparent().get('ID'):
    +                raise OneLogin_Saml2_ValidationError(
    +                    'Signed Element must contain an ID. SAML Response rejected',
    +                    OneLogin_Saml2_ValidationError.ID_NOT_FOUND_IN_SIGNED_ELEMENT
    +                )
    +
    +            id_value = sign_node.getparent().get('ID')
    +            if id_value in verified_ids:
    +                raise OneLogin_Saml2_ValidationError(
    +                    'Duplicated ID. SAML Response rejected',
    +                    OneLogin_Saml2_ValidationError.DUPLICATED_ID_IN_SIGNED_ELEMENTS
    +                )
    +            verified_ids.append(id_value)
    +
    +            # Check that reference URI matches the parent ID and no duplicate References or IDs
    +            ref = OneLogin_Saml2_Utils.query(sign_node, './/ds:Reference')
    +            if ref:
    +                ref = ref[0]
    +                if ref.get('URI'):
    +                    sei = ref.get('URI')[1:]
    +
    +                    if sei != id_value:
    +                        raise OneLogin_Saml2_ValidationError(
    +                            'Found an invalid Signed Element. SAML Response rejected',
    +                            OneLogin_Saml2_ValidationError.INVALID_SIGNED_ELEMENT
    +                        )
    +
    +                    if sei in verified_seis:
    +                        raise OneLogin_Saml2_ValidationError(
    +                            'Duplicated Reference URI. SAML Response rejected',
    +                            OneLogin_Saml2_ValidationError.DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS
    +                        )
    +                    verified_seis.append(sei)
    +
    +            signed_elements.append(signed_element)
    +
    +        if signed_elements:
    +            if not self.validate_signed_elements(signed_elements, raise_exceptions=True):
    +                raise OneLogin_Saml2_ValidationError(
    +                    'Found an unexpected Signature Element. SAML Response rejected',
    +                    OneLogin_Saml2_ValidationError.UNEXPECTED_SIGNED_ELEMENTS
    +                )
    +        return signed_elements
    +
    +    @return_false_on_exception
    +    def validate_signed_elements(self, signed_elements):
    +        """
    +        Verifies that the document has the expected signed nodes.
    +
    +        :param signed_elements: The signed elements to be checked
    +        :type signed_elements: list
    +
    +        :param raise_exceptions: Whether to return false on failure or raise an exception
    +        :type raise_exceptions: Boolean
    +        """
    +        if len(signed_elements) > 2:
    +            return False
    +
    +        response_tag = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP
    +        assertion_tag = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML
     
    +        if (response_tag in signed_elements and signed_elements.count(response_tag) > 1) or \
    +           (assertion_tag in signed_elements and signed_elements.count(assertion_tag) > 1) or \
    +           (response_tag not in signed_elements and assertion_tag not in signed_elements):
    +            return False
    +
    +        # Check that the signed elements found here, are the ones that will be verified
    +        # by OneLogin_Saml2_Utils.validate_sign
    +        if response_tag in signed_elements:
    +            expected_signature_nodes = OneLogin_Saml2_Utils.query(self.document, OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH)
    +            if len(expected_signature_nodes) != 1:
    +                raise OneLogin_Saml2_ValidationError(
    +                    'Unexpected number of Response signatures found. SAML Response rejected.',
    +                    OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_SIGNATURES_IN_RESPONSE
    +                )
    +
    +        if assertion_tag in signed_elements:
    +            expected_signature_nodes = self.__query(OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH)
    +            if len(expected_signature_nodes) != 1:
    +                raise OneLogin_Saml2_ValidationError(
    +                    'Unexpected number of Assertion signatures found. SAML Response rejected.',
    +                    OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_SIGNATURES_IN_ASSERTION
    +                )
    +
    +        return True
    +
    +    @return_false_on_exception
         def validate_timestamps(self):
             """
             Verifies that the document is valid according to Conditions Element
     
    +        :param raise_exceptions: Whether to return false on failure or raise an exception
    +        :type raise_exceptions: Boolean
    +
             :returns: True if the condition is valid, False otherwise
             :rtype: bool
             """
    @@ -374,10 +765,16 @@ def validate_timestamps(self):
             for conditions_node in conditions_nodes:
                 nb_attr = conditions_node.get('NotBefore')
                 nooa_attr = conditions_node.get('NotOnOrAfter')
    -            if nb_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nb_attr) > OneLogin_Saml2_Utils.now() + OneLogin_Saml2_Constants.ALOWED_CLOCK_DRIFT:
    -                return False
    -            if nooa_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nooa_attr) + OneLogin_Saml2_Constants.ALOWED_CLOCK_DRIFT <= OneLogin_Saml2_Utils.now():
    -                return False
    +            if nb_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nb_attr) > OneLogin_Saml2_Utils.now() + OneLogin_Saml2_Constants.ALLOWED_CLOCK_DRIFT:
    +                raise OneLogin_Saml2_ValidationError(
    +                    'Could not validate timestamp: not yet valid. Check system clock.',
    +                    OneLogin_Saml2_ValidationError.ASSERTION_TOO_EARLY
    +                )
    +            if nooa_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nooa_attr) + OneLogin_Saml2_Constants.ALLOWED_CLOCK_DRIFT <= OneLogin_Saml2_Utils.now():
    +                raise OneLogin_Saml2_ValidationError(
    +                    'Could not validate timestamp: expired. Check system clock.',
    +                    OneLogin_Saml2_ValidationError.ASSERTION_EXPIRED
    +                )
             return True
     
         def __query_assertion(self, xpath_expr):
    @@ -390,13 +787,11 @@ def __query_assertion(self, xpath_expr):
             :returns: The queried nodes
             :rtype: list
             """
    -        if self.encrypted:
    -            assertion_expr = '/saml:EncryptedAssertion/saml:Assertion'
    -        else:
    -            assertion_expr = '/saml:Assertion'
    +        assertion_expr = '/saml:Assertion'
             signature_expr = '/ds:Signature/ds:SignedInfo/ds:Reference'
             signed_assertion_query = '/samlp:Response' + assertion_expr + signature_expr
             assertion_reference_nodes = self.__query(signed_assertion_query)
    +        tagid = None
     
             if not assertion_reference_nodes:
                 # Check if the message is signed
    @@ -404,23 +799,28 @@ def __query_assertion(self, xpath_expr):
                 message_reference_nodes = self.__query(signed_message_query)
                 if message_reference_nodes:
                     message_id = message_reference_nodes[0].get('URI')
    -                final_query = "/samlp:Response[@ID='%s']/" % message_id[1:]
    +                final_query = "/samlp:Response[@ID=$tagid]/"
    +                tagid = message_id[1:]
                 else:
                     final_query = "/samlp:Response"
                 final_query += assertion_expr
             else:
                 assertion_id = assertion_reference_nodes[0].get('URI')
    -            final_query = '/samlp:Response' + assertion_expr + "[@ID='%s']" % assertion_id[1:]
    +            final_query = '/samlp:Response' + assertion_expr + "[@ID=$tagid]"
    +            tagid = assertion_id[1:]
             final_query += xpath_expr
    -        return self.__query(final_query)
    +        return self.__query(final_query, tagid)
     
    -    def __query(self, query):
    +    def __query(self, query, tagid=None):
             """
             Extracts nodes that match the query from the Response
     
             :param query: Xpath Expresion
             :type query: String
     
    +        :param tagid: Tag ID
    +        :type query: String
    +
             :returns: The queried nodes
             :rtype: list
             """
    @@ -428,52 +828,100 @@ def __query(self, query):
                 document = self.decrypted_document
             else:
                 document = self.document
    -        return OneLogin_Saml2_Utils.query(document, query)
    +        return OneLogin_Saml2_Utils.query(document, query, None, tagid)
     
         def __decrypt_assertion(self, dom):
             """
             Decrypts the Assertion
     
             :raises: Exception if no private key available
    +
             :param dom: Encrypted Assertion
             :type dom: Element
    +
             :returns: Decrypted Assertion
             :rtype: Element
             """
             key = self.__settings.get_sp_key()
    +        debug = self.__settings.is_debug_active()
     
             if not key:
    -            raise Exception('No private key available, check settings')
    +            raise OneLogin_Saml2_Error(
    +                'No private key available to decrypt the assertion, check settings',
    +                OneLogin_Saml2_Error.PRIVATE_KEY_NOT_FOUND
    +            )
     
    -        encrypted_assertion_nodes = OneLogin_Saml2_Utils.query(dom, '//saml:EncryptedAssertion')
    +        encrypted_assertion_nodes = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/saml:EncryptedAssertion')
             if encrypted_assertion_nodes:
                 encrypted_data_nodes = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData')
                 if encrypted_data_nodes:
                     keyinfo = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData/ds:KeyInfo')
                     if not keyinfo:
    -                    raise Exception('No KeyInfo present, invalid Assertion')
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'No KeyInfo present, invalid Assertion',
    +                        OneLogin_Saml2_ValidationError.KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA
    +                    )
                     keyinfo = keyinfo[0]
                     children = keyinfo.getchildren()
                     if not children:
    -                    raise Exception('No child to KeyInfo, invalid Assertion')
    +                    raise OneLogin_Saml2_ValidationError(
    +                        'KeyInfo has no children nodes, invalid Assertion',
    +                        OneLogin_Saml2_ValidationError.CHILDREN_NODE_NOT_FOUND_IN_KEYINFO
    +                    )
                     for child in children:
                         if 'RetrievalMethod' in child.tag:
                             if child.attrib['Type'] != 'http://www.w3.org/2001/04/xmlenc#EncryptedKey':
    -                            raise Exception('Unsupported Retrieval Method found')
    +                            raise OneLogin_Saml2_ValidationError(
    +                                'Unsupported Retrieval Method found',
    +                                OneLogin_Saml2_ValidationError.UNSUPPORTED_RETRIEVAL_METHOD
    +                            )
                             uri = child.attrib['URI']
                             if not uri.startswith('#'):
                                 break
                             uri = uri.split('#')[1]
    -                        encrypted_key = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], './xenc:EncryptedKey[@Id="' + uri + '"]')
    +                        encrypted_key = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], './xenc:EncryptedKey[@Id=$tagid]', None, uri)
                             if encrypted_key:
                                 keyinfo.append(encrypted_key[0])
     
                     encrypted_data = encrypted_data_nodes[0]
    -                OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key)
    +                decrypted = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug=debug, inplace=True)
    +                dom.replace(encrypted_assertion_nodes[0], decrypted)
    +
             return dom
     
         def get_error(self):
             """
    -        After execute a validation process, if fails this method returns the cause
    +        After executing a validation process, if it fails this method returns the cause
             """
             return self.__error
    +
    +    def get_xml_document(self):
    +        """
    +        Returns the SAML Response document (If contains an encrypted assertion, decrypts it)
    +
    +        :return: Decrypted XML response document
    +        :rtype: DOMDocument
    +        """
    +        if self.encrypted:
    +            return self.decrypted_document
    +        else:
    +            return self.document
    +
    +    def get_id(self):
    +        """
    +        :returns: the ID of the response
    +        :rtype: string
    +        """
    +        return self.document.get('ID', None)
    +
    +    def get_assertion_id(self):
    +        """
    +        :returns: the ID of the assertion in the response
    +        :rtype: string
    +        """
    +        if not self.validate_num_assertions():
    +            raise OneLogin_Saml2_ValidationError(
    +                'SAML Response must contain 1 assertion',
    +                OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_ASSERTIONS
    +            )
    +        return self.__query_assertion('')[0].get('ID', None)
    diff --git a/src/onelogin/saml2/schemas/xmldsig-core-schema.xsd b/src/onelogin/saml2/schemas/xmldsig-core-schema.xsd
    index dd5254bb..6f5acc75 100644
    --- a/src/onelogin/saml2/schemas/xmldsig-core-schema.xsd
    +++ b/src/onelogin/saml2/schemas/xmldsig-core-schema.xsd
    @@ -188,7 +188,7 @@
      
        
          
    -     
    +     
       
     
     
    diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py
    index abf713f6..75498ebe 100644
    --- a/src/onelogin/saml2/settings.py
    +++ b/src/onelogin/saml2/settings.py
    @@ -2,17 +2,17 @@
     
     """ OneLogin_Saml2_Settings class
     
    -Copyright (c) 2014, OneLogin, Inc.
    -All rights reserved.
    +Copyright (c) 2010-2018 OneLogin, Inc.
    +MIT License
     
     Setting class of OneLogin's Python Toolkit.
     
     """
     
    -from datetime import datetime
     import json
     import re
    -from os.path import dirname, exists, join, sep
    +from time import time
    +from os.path import dirname, exists, join, sep, abspath
     from xml.dom.minidom import Document
     
     from onelogin.saml2.constants import OneLogin_Saml2_Constants
    @@ -25,7 +25,7 @@
     # Released under a BSD 3-Clause License
     url_regex = re.compile(
         r'^(?:[a-z0-9\.\-]*)://'  # scheme is validated separately
    -    r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'  # domain...
    +    r'(?:(?:[A-Z0-9_](?:[A-Z0-9-_]{0,61}[A-Z0-9_])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'  # domain...
         r'localhost|'  # localhost...
         r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|'  # ...or ipv4
         r'\[?[A-F0-9]*:[A-F0-9:]+\]?)'  # ...or ipv6
    @@ -65,14 +65,14 @@ def __init__(self, settings=None, custom_base_path=None, sp_validation_only=Fals
             - Loads settings info from settings file or array/object provided
     
             :param settings: SAML Toolkit Settings
    -        :type settings: dict|object
    +        :type settings: dict
     
             :param custom_base_path: Path where are stored the settings file and the cert folder
             :type custom_base_path: string
             """
             self.__sp_validation_only = sp_validation_only
             self.__paths = {}
    -        self.__strict = False
    +        self.__strict = True
             self.__debug = False
             self.__sp = {}
             self.__idp = {}
    @@ -104,18 +104,25 @@ def __init__(self, settings=None, custom_base_path=None, sp_validation_only=Fals
                         ','.join(self.__errors)
                     )
             else:
    -            raise Exception('Unsupported settings object')
    +            raise OneLogin_Saml2_Error(
    +                'Unsupported settings object',
    +                OneLogin_Saml2_Error.UNSUPPORTED_SETTINGS_OBJECT
    +            )
     
             self.format_idp_cert()
             self.format_sp_cert()
    +        if 'x509certNew' in self.__sp:
    +            self.format_sp_cert_new()
             self.format_sp_key()
    +        if 'x509certMulti' in self.__idp:
    +            self.format_idp_cert_multi()
     
         def __load_paths(self, base_path=None):
             """
    -        Sets the paths of the different folders
    +        Set the paths of the different folders
             """
             if base_path is None:
    -            base_path = dirname(dirname(dirname(__file__)))
    +            base_path = dirname(dirname(dirname(abspath(__file__))))
             if not base_path.endswith(sep):
                 base_path += sep
             self.__paths = {
    @@ -134,7 +141,7 @@ def __update_paths(self, settings):
     
             if 'custom_base_path' in settings:
                 base_path = settings['custom_base_path']
    -            base_path = join(dirname(__file__), base_path)
    +            base_path = join(dirname(abspath(__file__)), base_path)
                 self.__load_paths(base_path)
     
         def get_base_path(self):
    @@ -196,20 +203,13 @@ def __load_settings_from_dict(self, settings):
             if len(errors) == 0:
                 self.__errors = []
                 self.__sp = settings['sp']
    -            self.__idp = settings['idp']
    -
    -            if 'strict' in settings:
    -                self.__strict = settings['strict']
    -            if 'debug' in settings:
    -                self.__debug = settings['debug']
    -            if 'security' in settings:
    -                self.__security = settings['security']
    -            else:
    -                self.__security = {}
    -            if 'contactPerson' in settings:
    -                self.__contacts = settings['contactPerson']
    -            if 'organization' in settings:
    -                self.__organization = settings['organization']
    +
    +            self.__idp = settings.get('idp', {})
    +            self.__strict = settings.get('strict', True)
    +            self.__debug = settings.get('debug', False)
    +            self.__security = settings.get('security', {})
    +            self.__contacts = settings.get('contactPerson', {})
    +            self.__organization = settings.get('organization', {})
     
                 self.__add_default_values()
                 return True
    @@ -251,68 +251,60 @@ def __add_default_values(self):
             """
             Add default values if the settings info is not complete
             """
    -        if 'assertionConsumerService' not in self.__sp.keys():
    -            self.__sp['assertionConsumerService'] = {}
    -        if 'binding' not in self.__sp['assertionConsumerService'].keys():
    -            self.__sp['assertionConsumerService']['binding'] = OneLogin_Saml2_Constants.BINDING_HTTP_POST
    +        self.__sp.setdefault('assertionConsumerService', {})
    +        self.__sp['assertionConsumerService'].setdefault('binding', OneLogin_Saml2_Constants.BINDING_HTTP_POST)
    +
    +        self.__sp.setdefault('attributeConsumingService', {})
     
    -        if 'singleLogoutService' not in self.__sp.keys():
    -            self.__sp['singleLogoutService'] = {}
    -        if 'binding' not in self.__sp['singleLogoutService']:
    -            self.__sp['singleLogoutService']['binding'] = OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT
    +        self.__sp.setdefault('singleLogoutService', {})
    +        self.__sp['singleLogoutService'].setdefault('binding', OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT)
     
             # Related to nameID
    -        if 'NameIDFormat' not in self.__sp:
    -            self.__sp['NameIDFormat'] = OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED
    -        if 'nameIdEncrypted' not in self.__security:
    -            self.__security['nameIdEncrypted'] = False
    +        self.__sp.setdefault('NameIDFormat', OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED)
    +        self.__security.setdefault('nameIdEncrypted', False)
     
             # Metadata format
    -        if 'metadataValidUntil' not in self.__security.keys():
    -            self.__security['metadataValidUntil'] = None  # None means use default
    -        if 'metadataCacheDuration' not in self.__security.keys():
    -            self.__security['metadataCacheDuration'] = None  # None means use default
    +        self.__security.setdefault('metadataValidUntil', None)  # None means use default
    +        self.__security.setdefault('metadataCacheDuration', None)  # None means use default
     
             # Sign provided
    -        if 'authnRequestsSigned' not in self.__security.keys():
    -            self.__security['authnRequestsSigned'] = False
    -        if 'logoutRequestSigned' not in self.__security.keys():
    -            self.__security['logoutRequestSigned'] = False
    -        if 'logoutResponseSigned' not in self.__security.keys():
    -            self.__security['logoutResponseSigned'] = False
    -        if 'signMetadata' not in self.__security.keys():
    -            self.__security['signMetadata'] = False
    +        self.__security.setdefault('authnRequestsSigned', False)
    +        self.__security.setdefault('logoutRequestSigned', False)
    +        self.__security.setdefault('logoutResponseSigned', False)
    +        self.__security.setdefault('signMetadata', False)
     
             # Sign expected
    -        if 'wantMessagesSigned' not in self.__security.keys():
    -            self.__security['wantMessagesSigned'] = False
    -        if 'wantAssertionsSigned' not in self.__security.keys():
    -            self.__security['wantAssertionsSigned'] = False
    +        self.__security.setdefault('wantMessagesSigned', False)
    +        self.__security.setdefault('wantAssertionsSigned', False)
    +
    +        # NameID element expected
    +        self.__security.setdefault('wantNameId', True)
    +
    +        # SAML responses with a InResponseTo attribute not rejected when requestId not passed
    +        self.__security.setdefault('rejectUnsolicitedResponsesWithInResponseTo', False)
     
             # Encrypt expected
    -        if 'wantAssertionsEncrypted' not in self.__security.keys():
    -            self.__security['wantAssertionsEncrypted'] = False
    -        if 'wantNameIdEncrypted' not in self.__security.keys():
    -            self.__security['wantNameIdEncrypted'] = False
    +        self.__security.setdefault('wantAssertionsEncrypted', False)
    +        self.__security.setdefault('wantNameIdEncrypted', False)
     
             # Signature Algorithm
    -        if 'signatureAlgorithm' not in self.__security.keys():
    -            self.__security['signatureAlgorithm'] = OneLogin_Saml2_Constants.RSA_SHA1
    +        self.__security.setdefault('signatureAlgorithm', OneLogin_Saml2_Constants.RSA_SHA1)
     
    -        if 'x509cert' not in self.__idp:
    -            self.__idp['x509cert'] = ''
    -        if 'certFingerprint' not in self.__idp:
    -            self.__idp['certFingerprint'] = ''
    -        if 'certFingerprintAlgorithm' not in self.__idp:
    -            self.__idp['certFingerprintAlgorithm'] = 'sha1'
    +        # Digest Algorithm
    +        self.__security.setdefault('digestAlgorithm', OneLogin_Saml2_Constants.SHA1)
     
    -        if 'x509cert' not in self.__sp:
    -            self.__sp['x509cert'] = ''
    -        if 'privateKey' not in self.__sp:
    -            self.__sp['privateKey'] = ''
    +        # AttributeStatement required by default
    +        self.__security.setdefault('wantAttributeStatement', True)
     
    -        if 'requestedAuthnContext' not in self.__security.keys():
    -            self.__security['requestedAuthnContext'] = True
    +        self.__idp.setdefault('x509cert', '')
    +        self.__idp.setdefault('certFingerprint', '')
    +        self.__idp.setdefault('certFingerprintAlgorithm', 'sha1')
    +
    +        self.__sp.setdefault('x509cert', '')
    +        self.__sp.setdefault('privateKey', '')
    +
    +        self.__security.setdefault('requestedAuthnContext', True)
    +        self.__security.setdefault('failOnAuthnContextMismatch', False)
     
         def check_settings(self, settings):
             """
    @@ -353,42 +345,43 @@ def check_idp_settings(self, settings):
             if not isinstance(settings, dict) or len(settings) == 0:
                 errors.append('invalid_syntax')
             else:
    -            if 'idp' not in settings or len(settings['idp']) == 0:
    +            if not settings.get('idp'):
                     errors.append('idp_not_found')
                 else:
                     idp = settings['idp']
    -                if 'entityId' not in idp or len(idp['entityId']) == 0:
    +                if not idp.get('entityId'):
                         errors.append('idp_entityId_not_found')
     
    -                if 'singleSignOnService' not in idp or \
    -                    'url' not in idp['singleSignOnService'] or \
    -                        len(idp['singleSignOnService']['url']) == 0:
    +                if not idp.get('singleSignOnService', {}).get('url'):
                         errors.append('idp_sso_not_found')
                     elif not validate_url(idp['singleSignOnService']['url']):
                         errors.append('idp_sso_url_invalid')
     
    -                if 'singleLogoutService' in idp and \
    -                    'url' in idp['singleLogoutService'] and \
    -                    len(idp['singleLogoutService']['url']) > 0 and \
    -                        not validate_url(idp['singleLogoutService']['url']):
    +                slo_url = idp.get('singleLogoutService', {}).get('url')
    +                if slo_url and not validate_url(slo_url):
                         errors.append('idp_slo_url_invalid')
     
                     if 'security' in settings:
                         security = settings['security']
     
    -                    exists_x509 = ('x509cert' in idp and
    -                                   len(idp['x509cert']) > 0)
    -                    exists_fingerprint = ('certFingerprint' in idp and
    -                                          len(idp['certFingerprint']) > 0)
    +                    exists_x509 = bool(idp.get('x509cert'))
    +                    exists_fingerprint = bool(idp.get('certFingerprint'))
    +
    +                    exists_multix509sign = 'x509certMulti' in idp and \
    +                        'signing' in idp['x509certMulti'] and \
    +                        idp['x509certMulti']['signing']
    +                    exists_multix509enc = 'x509certMulti' in idp and \
    +                        'encryption' in idp['x509certMulti'] and \
    +                        idp['x509certMulti']['encryption']
     
    -                    want_assert_sign = 'wantAssertionsSigned' in security.keys() and security['wantAssertionsSigned']
    -                    want_mes_signed = 'wantMessagesSigned' in security.keys() and security['wantMessagesSigned']
    -                    nameid_enc = 'nameIdEncrypted' in security.keys() and security['nameIdEncrypted']
    +                    want_assert_sign = bool(security.get('wantAssertionsSigned'))
    +                    want_mes_signed = bool(security.get('wantMessagesSigned'))
    +                    nameid_enc = bool(security.get('nameIdEncrypted'))
     
                         if (want_assert_sign or want_mes_signed) and \
    -                            not(exists_x509 or exists_fingerprint):
    +                            not(exists_x509 or exists_fingerprint or exists_multix509sign):
                             errors.append('idp_cert_or_fingerprint_not_found_and_required')
    -                    if nameid_enc and not exists_x509:
    +                    if nameid_enc and not (exists_x509 or exists_multix509enc):
                             errors.append('idp_cert_not_found_and_required')
     
             return errors
    @@ -406,10 +399,10 @@ def check_sp_settings(self, settings):
             assert isinstance(settings, dict)
     
             errors = []
    -        if not isinstance(settings, dict) or len(settings) == 0:
    +        if not isinstance(settings, dict) or not settings:
                 errors.append('invalid_syntax')
             else:
    -            if 'sp' not in settings or len(settings['sp']) == 0:
    +            if not settings.get('sp'):
                     errors.append('sp_not_found')
                 else:
                     # check_sp_certs uses self.__sp so I add it
    @@ -417,24 +410,43 @@ def check_sp_settings(self, settings):
                     self.__sp = settings['sp']
     
                     sp = settings['sp']
    -                security = {}
    -                if 'security' in settings:
    -                    security = settings['security']
    +                security = settings.get('security', {})
     
    -                if 'entityId' not in sp or len(sp['entityId']) == 0:
    +                if not sp.get('entityId'):
                         errors.append('sp_entityId_not_found')
     
    -                if 'assertionConsumerService' not in sp or \
    -                    'url' not in sp['assertionConsumerService'] or \
    -                        len(sp['assertionConsumerService']['url']) == 0:
    +                if not sp.get('assertionConsumerService', {}).get('url'):
                         errors.append('sp_acs_not_found')
                     elif not validate_url(sp['assertionConsumerService']['url']):
                         errors.append('sp_acs_url_invalid')
     
    -                if 'singleLogoutService' in sp and \
    -                    'url' in sp['singleLogoutService'] and \
    -                    len(sp['singleLogoutService']['url']) > 0 and \
    -                        not validate_url(sp['singleLogoutService']['url']):
    +                if sp.get('attributeConsumingService'):
    +                    attributeConsumingService = sp['attributeConsumingService']
    +                    if 'serviceName' not in attributeConsumingService:
    +                        errors.append('sp_attributeConsumingService_serviceName_not_found')
    +                    elif not isinstance(attributeConsumingService['serviceName'], basestring):
    +                        errors.append('sp_attributeConsumingService_serviceName_type_invalid')
    +
    +                    if 'requestedAttributes' not in attributeConsumingService:
    +                        errors.append('sp_attributeConsumingService_requestedAttributes_not_found')
    +                    elif not isinstance(attributeConsumingService['requestedAttributes'], list):
    +                        errors.append('sp_attributeConsumingService_serviceName_type_invalid')
    +                    else:
    +                        for req_attrib in attributeConsumingService['requestedAttributes']:
    +                            if 'name' not in req_attrib:
    +                                errors.append('sp_attributeConsumingService_requestedAttributes_name_not_found')
    +                            if 'name' in req_attrib and not req_attrib['name'].strip():
    +                                errors.append('sp_attributeConsumingService_requestedAttributes_name_invalid')
    +                            if 'attributeValue' in req_attrib and type(req_attrib['attributeValue']) != list:
    +                                errors.append('sp_attributeConsumingService_requestedAttributes_attributeValue_type_invalid')
    +                            if 'isRequired' in req_attrib and type(req_attrib['isRequired']) != bool:
    +                                errors.append('sp_attributeConsumingService_requestedAttributes_isRequired_type_invalid')
    +
    +                    if "serviceDescription" in attributeConsumingService and not isinstance(attributeConsumingService['serviceDescription'], basestring):
    +                        errors.append('sp_attributeConsumingService_serviceDescription_type_invalid')
    +
    +                slo_url = sp.get('singleLogoutService', {}).get('url')
    +                if slo_url and not validate_url(slo_url):
                         errors.append('sp_sls_url_invalid')
     
                     if 'signMetadata' in security and isinstance(security['signMetadata'], dict):
    @@ -442,11 +454,11 @@ def check_sp_settings(self, settings):
                                 'certFileName' not in security['signMetadata']:
                             errors.append('sp_signMetadata_invalid')
     
    -                authn_sign = 'authnRequestsSigned' in security.keys() and security['authnRequestsSigned']
    -                logout_req_sign = 'logoutRequestSigned' in security.keys() and security['logoutRequestSigned']
    -                logout_res_sign = 'logoutResponseSigned' in security.keys() and security['logoutResponseSigned']
    -                want_assert_enc = 'wantAssertionsEncrypted' in security.keys() and security['wantAssertionsEncrypted']
    -                want_nameid_enc = 'wantNameIdEncrypted' in security.keys() and security['wantNameIdEncrypted']
    +                authn_sign = bool(security.get('authnRequestsSigned'))
    +                logout_req_sign = bool(security.get('logoutRequestSigned'))
    +                logout_res_sign = bool(security.get('logoutResponseSigned'))
    +                want_assert_enc = bool(security.get('wantAssertionsEncrypted'))
    +                want_nameid_enc = bool(security.get('wantNameIdEncrypted'))
     
                     if not self.check_sp_certs():
                         if authn_sign or logout_req_sign or logout_res_sign or \
    @@ -498,40 +510,49 @@ def get_sp_key(self):
             Returns the x509 private key of the SP.
     
             :returns: SP private key
    -        :rtype: string
    +        :rtype: string or None
             """
    -        key = None
    +        key = self.__sp.get('privateKey')
    +        key_file_name = self.__paths['cert'] + 'sp.key'
     
    -        if 'privateKey' in self.__sp.keys() and self.__sp['privateKey']:
    -            key = self.__sp['privateKey']
    -        else:
    -            key_file_name = self.__paths['cert'] + 'sp.key'
    +        if not key and exists(key_file_name):
    +            with open(key_file_name) as f:
    +                key = f.read()
     
    -            if exists(key_file_name):
    -                f_key = open(key_file_name, 'r')
    -                key = f_key.read()
    -                f_key.close()
    -        return key
    +        return key or None
     
         def get_sp_cert(self):
             """
             Returns the x509 public cert of the SP.
     
             :returns: SP public cert
    -        :rtype: string
    +        :rtype: string or None
             """
    -        cert = None
    +        cert = self.__sp.get('x509cert')
    +        cert_file_name = self.__paths['cert'] + 'sp.crt'
     
    -        if 'x509cert' in self.__sp.keys() and self.__sp['x509cert']:
    -            cert = self.__sp['x509cert']
    -        else:
    -            cert_file_name = self.__paths['cert'] + 'sp.crt'
    -            if exists(cert_file_name):
    -                f_cert = open(cert_file_name, 'r')
    -                cert = f_cert.read()
    -                f_cert.close()
    +        if not cert and exists(cert_file_name):
    +            with open(cert_file_name) as f:
    +                cert = f.read()
     
    -        return cert
    +        return cert or None
    +
    +    def get_sp_cert_new(self):
    +        """
    +        Returns the x509 public of the SP planned
    +        to be used soon instead the other public cert
    +
    +        :returns: SP public cert new
    +        :rtype: string or None
    +        """
    +        cert = self.__sp.get('x509certNew')
    +        cert_file_name = self.__paths['cert'] + 'sp_new.crt'
    +
    +        if not cert and exists(cert_file_name):
    +            with open(cert_file_name) as f:
    +                cert = f.read()
    +
    +        return cert or None
     
         def get_idp_cert(self):
             """
    @@ -540,11 +561,7 @@ def get_idp_cert(self):
             :returns: IdP public cert
             :rtype: string
             """
    -        cert = None
    -
    -        if 'x509cert' in self.__idp.keys() and self.__idp['x509cert']:
    -            cert = self.__idp['x509cert']
    -        return cert
    +        return self.__idp.get('x509cert')
     
         def get_idp_data(self):
             """
    @@ -605,8 +622,14 @@ def get_sp_metadata(self):
                 self.__security['metadataCacheDuration'],
                 self.get_contacts(), self.get_organization()
             )
    +
    +        add_encryption = self.__security['wantNameIdEncrypted'] or self.__security['wantAssertionsEncrypted']
    +
    +        cert_new = self.get_sp_cert_new()
    +        metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert_new, add_encryption)
    +
             cert = self.get_sp_cert()
    -        metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert)
    +        metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert, add_encryption)
     
             # Sign metadata
             if 'signMetadata' in self.__security and self.__security['signMetadata'] is not False:
    @@ -657,7 +680,10 @@ def get_sp_metadata(self):
                             cert_metadata_file
                         )
     
    -            metadata = OneLogin_Saml2_Metadata.sign_metadata(metadata, key_metadata, cert_metadata)
    +            signature_algorithm = self.__security['signatureAlgorithm']
    +            digest_algorithm = self.__security['digestAlgorithm']
    +
    +            metadata = OneLogin_Saml2_Metadata.sign_metadata(metadata, key_metadata, cert_metadata, signature_algorithm, digest_algorithm)
     
             return metadata
     
    @@ -698,7 +724,7 @@ def validate_metadata(self, xml):
                             cache_duration = element.getAttribute('cacheDuration')
     
                         expire_time = OneLogin_Saml2_Utils.get_expire_time(cache_duration, valid_until)
    -                    if expire_time is not None and int(datetime.now().strftime('%s')) > int(expire_time):
    +                    if expire_time is not None and int(time()) > int(expire_time):
                             errors.append('expired_xml')
     
             # TODO: Validate Sign
    @@ -711,12 +737,31 @@ def format_idp_cert(self):
             """
             self.__idp['x509cert'] = OneLogin_Saml2_Utils.format_cert(self.__idp['x509cert'])
     
    +    def format_idp_cert_multi(self):
    +        """
    +        Formats the Multple IdP certs.
    +        """
    +        if 'x509certMulti' in self.__idp:
    +            if 'signing' in self.__idp['x509certMulti']:
    +                for idx in range(len(self.__idp['x509certMulti']['signing'])):
    +                    self.__idp['x509certMulti']['signing'][idx] = OneLogin_Saml2_Utils.format_cert(self.__idp['x509certMulti']['signing'][idx])
    +
    +            if 'encryption' in self.__idp['x509certMulti']:
    +                for idx in range(len(self.__idp['x509certMulti']['encryption'])):
    +                    self.__idp['x509certMulti']['encryption'][idx] = OneLogin_Saml2_Utils.format_cert(self.__idp['x509certMulti']['encryption'][idx])
    +
         def format_sp_cert(self):
             """
             Formats the SP cert.
             """
             self.__sp['x509cert'] = OneLogin_Saml2_Utils.format_cert(self.__sp['x509cert'])
     
    +    def format_sp_cert_new(self):
    +        """
    +        Formats the SP cert.
    +        """
    +        self.__sp['x509certNew'] = OneLogin_Saml2_Utils.format_cert(self.__sp['x509certNew'])
    +
         def format_sp_key(self):
             """
             Formats the private key.
    diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
    index 9cf84900..8fea81d1 100644
    --- a/src/onelogin/saml2/utils.py
    +++ b/src/onelogin/saml2/utils.py
    @@ -2,14 +2,15 @@
     
     """ OneLogin_Saml2_Utils class
     
    -Copyright (c) 2014, OneLogin, Inc.
    -All rights reserved.
    +Copyright (c) 2010-2018 OneLogin, Inc.
    +MIT License
     
     Auxiliary class of OneLogin's Python Toolkit.
     
     """
     
     import base64
    +from copy import deepcopy
     from datetime import datetime
     import calendar
     from hashlib import sha1, sha256, sha384, sha512
    @@ -25,6 +26,7 @@
     from uuid import uuid4
     from xml.dom.minidom import Document, Element
     from defusedxml.minidom import parseString
    +from functools import wraps
     
     import zlib
     
    @@ -32,12 +34,35 @@
     from dm.xmlsec.binding.tmpl import EncData, Signature
     
     from onelogin.saml2.constants import OneLogin_Saml2_Constants
    -from onelogin.saml2.errors import OneLogin_Saml2_Error
    +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
    +
    +
    +if not globals().get('xmlsec_setup', False):
    +    xmlsec.initialize()
    +    globals()['xmlsec_setup'] = True
    +
    +
    +def return_false_on_exception(func):
    +    """
    +    Decorator. When applied to a function, it will, by default, suppress any exceptions
    +    raised by that function and return False. It may be overridden by passing a
    +    "raise_exceptions" keyword argument when calling the wrapped function.
    +    """
    +    @wraps(func)
    +    def exceptfalse(*args, **kwargs):
    +        if not kwargs.pop('raise_exceptions', False):
    +            try:
    +                return func(*args, **kwargs)
    +            except Exception:
    +                return False
    +        else:
    +            return func(*args, **kwargs)
    +    return exceptfalse
     
     
     def print_xmlsec_errors(filename, line, func, error_object, error_subject, reason, msg):
         """
    -    Auxiliary method. It override the default xmlsec debug message.
    +    Auxiliary method. It overrides the default xmlsec debug message.
         """
     
         info = []
    @@ -50,7 +75,7 @@ def print_xmlsec_errors(filename, line, func, error_object, error_subject, reaso
         if reason != 1:
             info.append("errno=%d" % reason)
         if info:
    -        print "%s:%d(%s)" % (filename, line, func), " ".join(info)
    +        print("%s:%d(%s)" % (filename, line, func), " ".join(info))
     
     
     class OneLogin_Saml2_Utils(object):
    @@ -61,6 +86,9 @@ class OneLogin_Saml2_Utils(object):
     
         """
     
    +    RESPONSE_SIGNATURE_XPATH = '/samlp:Response/ds:Signature'
    +    ASSERTION_SIGNATURE_XPATH = '/samlp:Response/saml:Assertion/ds:Signature'
    +
         @staticmethod
         def decode_base64_and_inflate(value):
             """
    @@ -70,19 +98,25 @@ def decode_base64_and_inflate(value):
             :returns: the string after decoding and inflating
             :rtype: string
             """
    +        decoded = base64.b64decode(value)
    +        # We try to inflate
    +        try:
    +            result = zlib.decompress(decoded, -15)
    +        except Exception:
    +            result = decoded
     
    -        return zlib.decompress(base64.b64decode(value), -15)
    +        return result.decode('utf-8')
     
         @staticmethod
         def deflate_and_base64_encode(value):
             """
    -        Deflates and the base64 encodes a string
    +        Deflates and then base64 encodes a string
             :param value: The string to deflate and encode
             :type value: string
             :returns: The deflated and encoded string
             :rtype: string
             """
    -        return base64.b64encode(zlib.compress(value)[2:-4])
    +        return base64.b64encode(zlib.compress(value.encode('utf-8'))[2:-4])
     
         @staticmethod
         def validate_xml(xml, schema, debug=False):
    @@ -103,11 +137,11 @@ def validate_xml(xml, schema, debug=False):
             if isinstance(xml, Document):
                 xml = xml.toxml()
             elif isinstance(xml, etree._Element):
    -            xml = tostring(xml)
    +            xml = tostring(xml, encoding='unicode')
     
             # Switch to lxml for schema validation
             try:
    -            dom = fromstring(str(xml))
    +            dom = fromstring(xml.encode('utf-8'), forbid_dtd=True)
             except Exception:
                 return 'unloaded_xml'
     
    @@ -126,20 +160,25 @@ def validate_xml(xml, schema, debug=False):
     
                 return 'invalid_xml'
     
    -        return parseString(etree.tostring(dom))
    +        return parseString(tostring(dom, encoding='unicode').encode('utf-8'), forbid_dtd=True)
    +
    +    @staticmethod
    +    def element_text(node):
    +        etree.strip_tags(node, etree.Comment)
    +        return node.text
     
         @staticmethod
         def format_cert(cert, heads=True):
             """
             Returns a x509 cert (adding header & footer if required).
     
    -        :param cert: A x509 unformated cert
    +        :param cert: A x509 unformatted cert
             :type: string
     
             :param heads: True if we want to include head and footer
             :type: boolean
     
    -        :returns: Formated cert
    +        :returns: Formatted cert
             :rtype: string
             """
             x509_cert = cert.replace('\x0D', '')
    @@ -166,7 +205,7 @@ def format_private_key(key, heads=True):
             :param heads: True if we want to include head and footer
             :type: boolean
     
    -        :returns: Formated private key
    +        :returns: Formatted private key
             :rtype: string
             """
             private_key = key.replace('\x0D', '')
    @@ -395,7 +434,7 @@ def generate_unique_id():
     
         @staticmethod
         def parse_time_to_SAML(time):
    -        """
    +        r"""
             Converts a UNIX timestamp to SAML2 timestamp on the form
             yyyy-mm-ddThh:mm:ss(\.s+)?Z.
     
    @@ -410,7 +449,7 @@ def parse_time_to_SAML(time):
     
         @staticmethod
         def parse_SAML_to_time(timestr):
    -        """
    +        r"""
             Converts a SAML2 timestamp on the form yyyy-mm-ddThh:mm:ss(\.s+)?Z
             to a UNIX timestamp. The sub-second part is ignored.
     
    @@ -491,7 +530,7 @@ def get_expire_time(cache_duration=None, valid_until=None):
             return None
     
         @staticmethod
    -    def query(dom, query, context=None):
    +    def query(dom, query, context=None, tagid=None):
             """
             Extracts nodes that match the query from the Element
     
    @@ -504,13 +543,21 @@ def query(dom, query, context=None):
             :param context: Context Node
             :type: DOMElement
     
    +        :param tagid: Tag ID
    +        :type: string
    +
             :returns: The queried nodes
             :rtype: list
             """
             if context is None:
    -            return dom.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP)
    +            source = dom
    +        else:
    +            source = context
    +
    +        if tagid is None:
    +            return source.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP)
             else:
    -            return context.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP)
    +            return source.xpath(query, tagid=tagid, namespaces=OneLogin_Saml2_Constants.NSMAP)
     
         @staticmethod
         def delete_local_session(callback=None):
    @@ -524,9 +571,9 @@ def delete_local_session(callback=None):
         @staticmethod
         def calculate_x509_fingerprint(x509_cert, alg='sha1'):
             """
    -        Calculates the fingerprint of a x509cert.
    +        Calculates the fingerprint of a formatted x509cert.
     
    -        :param x509_cert: x509 cert
    +        :param x509_cert: x509 cert formatted
             :type: string
     
             :param alg: The algorithm to build the fingerprint
    @@ -539,23 +586,27 @@ def calculate_x509_fingerprint(x509_cert, alg='sha1'):
     
             lines = x509_cert.split('\n')
             data = ''
    +        inData = False
     
             for line in lines:
                 # Remove '\r' from end of line if present.
                 line = line.rstrip()
    -            if line == '-----BEGIN CERTIFICATE-----':
    -                # Delete junk from before the certificate.
    -                data = ''
    -            elif line == '-----END CERTIFICATE-----':
    -                # Ignore data after the certificate.
    -                break
    -            elif line == '-----BEGIN PUBLIC KEY-----' or line == '-----BEGIN RSA PRIVATE KEY-----':
    -                # This isn't an X509 certificate.
    -                return None
    +            if not inData:
    +                if line == '-----BEGIN CERTIFICATE-----':
    +                    inData = True
    +                elif line == '-----BEGIN PUBLIC KEY-----' or line == '-----BEGIN RSA PRIVATE KEY-----':
    +                    # This isn't an X509 certificate.
    +                    return None
                 else:
    +                if line == '-----END CERTIFICATE-----':
    +                    break
    +
                     # Append the current line to the certificate data.
                     data += line
     
    +        if not data:
    +            return None
    +
             decoded_data = base64.b64decode(data)
     
             if alg == 'sha512':
    @@ -572,19 +623,19 @@ def calculate_x509_fingerprint(x509_cert, alg='sha1'):
         @staticmethod
         def format_finger_print(fingerprint):
             """
    -        Formates a fingerprint.
    +        Formats a fingerprint.
     
             :param fingerprint: fingerprint
             :type: string
     
    -        :returns: Formated fingerprint
    +        :returns: Formatted fingerprint
             :rtype: string
             """
             formated_fingerprint = fingerprint.replace(':', '')
             return formated_fingerprint.lower()
     
         @staticmethod
    -    def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False):
    +    def generate_name_id(value, sp_nq, sp_format=None, cert=None, debug=False, nq=None):
             """
             Generates a nameID.
     
    @@ -603,6 +654,9 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False):
             :param debug: Activate the xmlsec debug
             :type: bool
     
    +        :param nq: IDP Name Qualifier
    +        :type: string
    +
             :returns: DOMElement | XMLSec nameID
             :rtype: string
             """
    @@ -611,19 +665,23 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False):
             name_id_container.setAttribute("xmlns:saml", OneLogin_Saml2_Constants.NS_SAML)
     
             name_id = doc.createElement('saml:NameID')
    -        name_id.setAttribute('SPNameQualifier', sp_nq)
    -        name_id.setAttribute('Format', sp_format)
    +        if sp_nq is not None:
    +            name_id.setAttribute('SPNameQualifier', sp_nq)
    +        if nq is not None:
    +            name_id.setAttribute('NameQualifier', nq)
    +        if sp_format is not None:
    +            name_id.setAttribute('Format', sp_format)
             name_id.appendChild(doc.createTextNode(value))
             name_id_container.appendChild(name_id)
     
             if cert is not None:
                 xml = name_id_container.toxml()
    -            elem = fromstring(xml)
    -
    -            xmlsec.initialize()
    +            elem = fromstring(xml, forbid_dtd=True)
     
    +            error_callback_method = None
                 if debug:
    -                xmlsec.set_error_callback(print_xmlsec_errors)
    +                error_callback_method = print_xmlsec_errors
    +            xmlsec.set_error_callback(error_callback_method)
     
                 # Load the public cert
                 mngr = xmlsec.KeysMngr()
    @@ -647,7 +705,7 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False):
     
                 edata = enc_ctx.encryptXml(enc_data, elem[0])
     
    -            newdoc = parseString(etree.tostring(edata))
    +            newdoc = parseString(tostring(edata, encoding='unicode').encode('utf-8'), forbid_dtd=True)
     
                 if newdoc.hasChildNodes():
                     child = newdoc.firstChild
    @@ -686,29 +744,34 @@ def get_status(dom):
             status = {}
     
             status_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status')
    -        if len(status_entry) == 0:
    -            raise Exception('Missing Status on response')
    +        if len(status_entry) != 1:
    +            raise OneLogin_Saml2_ValidationError(
    +                'Missing Status on response',
    +                OneLogin_Saml2_ValidationError.MISSING_STATUS
    +            )
     
             code_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status/samlp:StatusCode', status_entry[0])
    -        if len(code_entry) == 0:
    -            raise Exception('Missing Status Code on response')
    +        if len(code_entry) != 1:
    +            raise OneLogin_Saml2_ValidationError(
    +                'Missing Status Code on response',
    +                OneLogin_Saml2_ValidationError.MISSING_STATUS_CODE
    +            )
             code = code_entry[0].values()[0]
             status['code'] = code
     
    +        status['msg'] = ''
             message_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status/samlp:StatusMessage', status_entry[0])
             if len(message_entry) == 0:
                 subcode_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status/samlp:StatusCode/samlp:StatusCode', status_entry[0])
    -            if len(subcode_entry) > 0:
    +            if len(subcode_entry) == 1:
                     status['msg'] = subcode_entry[0].values()[0]
    -            else:
    -                status['msg'] = ''
    -        else:
    -            status['msg'] = message_entry[0].text
    +        elif len(message_entry) == 1:
    +            status['msg'] = OneLogin_Saml2_Utils.element_text(message_entry[0])
     
             return status
     
         @staticmethod
    -    def decrypt_element(encrypted_data, key, debug=False):
    +    def decrypt_element(encrypted_data, key, debug=False, inplace=False):
             """
             Decrypts an encrypted element.
     
    @@ -721,18 +784,23 @@ def decrypt_element(encrypted_data, key, debug=False):
             :param debug: Activate the xmlsec debug
             :type: bool
     
    +        :param inplace: update passed data with decrypted result
    +        :type: bool
    +
             :returns: The decrypted element.
             :rtype: lxml.etree.Element
             """
             if isinstance(encrypted_data, Element):
    -            encrypted_data = fromstring(str(encrypted_data.toxml()))
    +            encrypted_data = fromstring(str(encrypted_data.toxml()), forbid_dtd=True)
             elif isinstance(encrypted_data, basestring):
    -            encrypted_data = fromstring(str(encrypted_data))
    -
    -        xmlsec.initialize()
    +            encrypted_data = fromstring(str(encrypted_data), forbid_dtd=True)
    +        elif not inplace and isinstance(encrypted_data, etree._Element):
    +            encrypted_data = deepcopy(encrypted_data)
     
    +        error_callback_method = None
             if debug:
    -            xmlsec.set_error_callback(print_xmlsec_errors)
    +            error_callback_method = print_xmlsec_errors
    +        xmlsec.set_error_callback(error_callback_method)
     
             mngr = xmlsec.KeysMngr()
     
    @@ -759,7 +827,7 @@ def write_temp_file(content):
             return f_temp
     
         @staticmethod
    -    def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1):
    +    def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1, digest_algorithm=OneLogin_Saml2_Constants.SHA1):
             """
             Adds signature key and senders certificate to an element (Message or
             Assertion).
    @@ -778,6 +846,12 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
     
             :param sign_algorithm: Signature algorithm method
             :type sign_algorithm: string
    +
    +        :param digest_algorithm: Digest algorithm method
    +        :type digest_algorithm: string
    +
    +        :returns: Signed XML
    +        :rtype: string
             """
             if xml is None or xml == '':
                 raise Exception('Empty string supplied as input')
    @@ -785,7 +859,7 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
                 elem = xml
             elif isinstance(xml, Document):
                 xml = xml.toxml()
    -            elem = fromstring(str(xml))
    +            elem = fromstring(xml.encode('utf-8'), forbid_dtd=True)
             elif isinstance(xml, Element):
                 xml.setAttributeNS(
                     unicode(OneLogin_Saml2_Constants.NS_SAMLP),
    @@ -798,18 +872,17 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
                     unicode(OneLogin_Saml2_Constants.NS_SAML)
                 )
                 xml = xml.toxml()
    -            elem = fromstring(str(xml))
    +            elem = fromstring(xml.encode('utf-8'), forbid_dtd=True)
             elif isinstance(xml, basestring):
    -            elem = fromstring(str(xml))
    +            elem = fromstring(xml.encode('utf-8'), forbid_dtd=True)
             else:
                 raise Exception('Error parsing xml string')
     
    -        xmlsec.initialize()
    -
    +        error_callback_method = None
             if debug:
    -            xmlsec.set_error_callback(print_xmlsec_errors)
    +            error_callback_method = print_xmlsec_errors
    +        xmlsec.set_error_callback(error_callback_method)
     
    -        # Sign the metadata with our private key.
             sign_algorithm_transform_map = {
                 OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1,
                 OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1,
    @@ -819,16 +892,44 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
             }
             sign_algorithm_transform = sign_algorithm_transform_map.get(sign_algorithm, xmlsec.TransformRsaSha1)
     
    -        signature = Signature(xmlsec.TransformExclC14N, sign_algorithm_transform)
    +        signature = Signature(xmlsec.TransformExclC14N, sign_algorithm_transform, nsPrefix='ds')
     
             issuer = OneLogin_Saml2_Utils.query(elem, '//saml:Issuer')
             if len(issuer) > 0:
                 issuer = issuer[0]
                 issuer.addnext(signature)
    +            elem_to_sign = issuer.getparent()
             else:
    -            elem[0].insert(0, signature)
    +            entity_descriptor = OneLogin_Saml2_Utils.query(elem, '//md:EntityDescriptor')
    +            if len(entity_descriptor) > 0:
    +                elem.insert(0, signature)
    +            else:
    +                elem[0].insert(0, signature)
    +            elem_to_sign = elem
    +
    +        elem_id = elem_to_sign.get('ID', None)
    +        if elem_id is not None:
    +            if elem_id:
    +                elem_id = '#' + elem_id
    +        else:
    +            generated_id = generated_id = OneLogin_Saml2_Utils.generate_unique_id()
    +            elem_id = '#' + generated_id
    +            elem_to_sign.attrib['ID'] = generated_id
    +
    +        xmlsec.addIDs(elem_to_sign, ["ID"])
    +
    +        digest_algorithm_transform_map = {
    +            OneLogin_Saml2_Constants.SHA1: xmlsec.TransformSha1,
    +            OneLogin_Saml2_Constants.SHA256: xmlsec.TransformSha256,
    +            OneLogin_Saml2_Constants.SHA384: xmlsec.TransformSha384,
    +            OneLogin_Saml2_Constants.SHA512: xmlsec.TransformSha512
    +        }
    +        digest_algorithm_transform = digest_algorithm_transform_map.get(digest_algorithm, xmlsec.TransformSha1)
    +
    +        ref = signature.addReference(digest_algorithm_transform)
    +        if elem_id:
    +            ref.attrib['URI'] = elem_id
     
    -        ref = signature.addReference(xmlsec.TransformSha1)
             ref.addTransform(xmlsec.TransformEnveloped)
             ref.addTransform(xmlsec.TransformExclC14N)
     
    @@ -845,26 +946,103 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
             dsig_ctx.signKey = sign_key
             dsig_ctx.sign(signature)
     
    -        newdoc = parseString(etree.tostring(elem))
    +        return tostring(elem, encoding='unicode').encode('utf-8')
     
    -        signature_nodes = newdoc.getElementsByTagName("Signature")
    +    @staticmethod
    +    @return_false_on_exception
    +    def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False, xpath=None, multicerts=None):
    +        """
    +        Validates a signature (Message or Assertion).
     
    -        for signature in signature_nodes:
    -            signature.removeAttribute('xmlns')
    -            signature.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS)
    -            if not signature.tagName.startswith('ds:'):
    -                signature.tagName = 'ds:' + signature.tagName
    -            nodes = signature.getElementsByTagName("*")
    -            for node in nodes:
    -                if not node.tagName.startswith('ds:'):
    -                    node.tagName = 'ds:' + node.tagName
    +        :param xml: The element we should validate
    +        :type: string | Document
    +
    +        :param cert: The pubic cert
    +        :type: string
    +
    +        :param fingerprint: The fingerprint of the public cert
    +        :type: string
    +
    +        :param fingerprintalg: The algorithm used to build the fingerprint
    +        :type: string
    +
    +        :param validatecert: If true, will verify the signature and if the cert is valid.
    +        :type: bool
    +
    +        :param debug: Activate the xmlsec debug
    +        :type: bool
    +
    +        :param xpath: The xpath of the signed element
    +        :type: string
    +
    +        :param multicerts: Multiple public certs
    +        :type: list
    +
    +        :param raise_exceptions: Whether to return false on failure or raise an exception
    +        :type raise_exceptions: Boolean
    +        """
    +        if xml is None or xml == '':
    +            raise Exception('Empty string supplied as input')
    +        elif isinstance(xml, etree._Element):
    +            elem = xml
    +        elif isinstance(xml, Document):
    +            xml = xml.toxml()
    +            elem = fromstring(str(xml), forbid_dtd=True)
    +        elif isinstance(xml, Element):
    +            xml.setAttributeNS(
    +                unicode(OneLogin_Saml2_Constants.NS_SAMLP),
    +                'xmlns:samlp',
    +                unicode(OneLogin_Saml2_Constants.NS_SAMLP)
    +            )
    +            xml.setAttributeNS(
    +                unicode(OneLogin_Saml2_Constants.NS_SAML),
    +                'xmlns:saml',
    +                unicode(OneLogin_Saml2_Constants.NS_SAML)
    +            )
    +            xml = xml.toxml()
    +            elem = fromstring(str(xml), forbid_dtd=True)
    +        elif isinstance(xml, basestring):
    +            elem = fromstring(str(xml), forbid_dtd=True)
    +        else:
    +            raise Exception('Error parsing xml string')
    +
    +        error_callback_method = None
    +        if debug:
    +            error_callback_method = print_xmlsec_errors
    +        xmlsec.set_error_callback(error_callback_method)
    +
    +        xmlsec.addIDs(elem, ["ID"])
    +
    +        if xpath:
    +            signature_nodes = OneLogin_Saml2_Utils.query(elem, xpath)
    +        else:
    +            signature_nodes = OneLogin_Saml2_Utils.query(elem, OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH)
    +
    +            if len(signature_nodes) == 0:
    +                signature_nodes = OneLogin_Saml2_Utils.query(elem, OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH)
     
    -        return newdoc.saveXML(newdoc.firstChild)
    +        if len(signature_nodes) == 1:
    +            signature_node = signature_nodes[0]
    +
    +            if not multicerts:
    +                return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True)
    +            else:
    +                # If multiple certs are provided, I may ignore cert and
    +                # fingerprint provided by the method and just check the
    +                # certs multicerts
    +                fingerprint = fingerprintalg = None
    +                for cert in multicerts:
    +                    if OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, False, raise_exceptions=False):
    +                        return True
    +                raise OneLogin_Saml2_ValidationError('Signature validation failed. SAML Response rejected.')
    +        else:
    +            raise OneLogin_Saml2_ValidationError('Expected exactly one signature node; got {}.'.format(len(signature_nodes)), OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_SIGNATURES)
     
         @staticmethod
    -    def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False):
    +    @return_false_on_exception
    +    def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False):
             """
    -        Validates a signature (Message or Assertion).
    +        Validates a signature of a EntityDescriptor.
     
             :param xml: The element we should validate
             :type: string | Document
    @@ -883,89 +1061,143 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
     
             :param debug: Activate the xmlsec debug
             :type: bool
    +
    +        :param raise_exceptions: Whether to return false on failure or raise an exception
    +        :type raise_exceptions: Boolean
             """
    -        try:
    -            if xml is None or xml == '':
    -                raise Exception('Empty string supplied as input')
    -            elif isinstance(xml, etree._Element):
    -                elem = xml
    -            elif isinstance(xml, Document):
    -                xml = xml.toxml()
    -                elem = fromstring(str(xml))
    -            elif isinstance(xml, Element):
    -                xml.setAttributeNS(
    -                    unicode(OneLogin_Saml2_Constants.NS_SAMLP),
    -                    'xmlns:samlp',
    -                    unicode(OneLogin_Saml2_Constants.NS_SAMLP)
    -                )
    -                xml.setAttributeNS(
    -                    unicode(OneLogin_Saml2_Constants.NS_SAML),
    -                    'xmlns:saml',
    -                    unicode(OneLogin_Saml2_Constants.NS_SAML)
    -                )
    -                xml = xml.toxml()
    -                elem = fromstring(str(xml))
    -            elif isinstance(xml, basestring):
    -                elem = fromstring(str(xml))
    -            else:
    -                raise Exception('Error parsing xml string')
    +        if xml is None or xml == '':
    +            raise Exception('Empty string supplied as input')
    +        elif isinstance(xml, etree._Element):
    +            elem = xml
    +        elif isinstance(xml, Document):
    +            xml = xml.toxml()
    +            elem = fromstring(str(xml), forbid_dtd=True)
    +        elif isinstance(xml, Element):
    +            xml.setAttributeNS(
    +                unicode(OneLogin_Saml2_Constants.NS_MD),
    +                'xmlns:md',
    +                unicode(OneLogin_Saml2_Constants.NS_MD)
    +            )
    +            xml = xml.toxml()
    +            elem = fromstring(str(xml), forbid_dtd=True)
    +        elif isinstance(xml, basestring):
    +            elem = fromstring(str(xml), forbid_dtd=True)
    +        else:
    +            raise Exception('Error parsing xml string')
     
    -            xmlsec.initialize()
    +        error_callback_method = None
    +        if debug:
    +            error_callback_method = print_xmlsec_errors
    +        xmlsec.set_error_callback(error_callback_method)
     
    -            if debug:
    -                xmlsec.set_error_callback(print_xmlsec_errors)
    +        xmlsec.addIDs(elem, ["ID"])
     
    -            xmlsec.addIDs(elem, ["ID"])
    +        signature_nodes = OneLogin_Saml2_Utils.query(elem, '/md:EntitiesDescriptor/ds:Signature')
     
    -            signature_nodes = OneLogin_Saml2_Utils.query(elem, '//ds:Signature')
    +        if len(signature_nodes) == 0:
    +            signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/ds:Signature')
     
    -            if len(signature_nodes) > 0:
    -                signature_node = signature_nodes[0]
    +            if len(signature_nodes) == 0:
    +                signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/md:SPSSODescriptor/ds:Signature')
    +                signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/md:IDPSSODescriptor/ds:Signature')
     
    -                if (cert is None or cert == '') and fingerprint:
    -                    x509_certificate_nodes = OneLogin_Saml2_Utils.query(signature_node, '//ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate')
    -                    if len(x509_certificate_nodes) > 0:
    -                        x509_certificate_node = x509_certificate_nodes[0]
    -                        x509_cert_value = x509_certificate_node.text
    -                        x509_fingerprint_value = OneLogin_Saml2_Utils.calculate_x509_fingerprint(x509_cert_value, fingerprintalg)
    -                        if fingerprint == x509_fingerprint_value:
    -                            cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value)
    +        if len(signature_nodes) > 0:
    +            for signature_node in signature_nodes:
    +                OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True)
    +            return True
    +        else:
    +            raise Exception('Could not validate metadata signature: No signature nodes found.')
     
    -                if cert is None or cert == '':
    -                    return False
    +    @staticmethod
    +    @return_false_on_exception
    +    def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False):
    +        """
    +        Validates a signature node.
     
    -                # Check if Reference URI is empty
    -                reference_elem = OneLogin_Saml2_Utils.query(signature_node, '//ds:Reference')
    -                if len(reference_elem) > 0:
    -                    if reference_elem[0].get('URI') == '':
    -                        reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID'))
    +        :param signature_node: The signature node
    +        :type: Node
     
    -                dsig_ctx = xmlsec.DSigCtx()
    +        :param xml: The element we should validate
    +        :type: Document
     
    -                file_cert = OneLogin_Saml2_Utils.write_temp_file(cert)
    +        :param cert: The public cert
    +        :type: string
     
    -                if validatecert:
    -                    mngr = xmlsec.KeysMngr()
    -                    mngr.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem, xmlsec.KeyDataTypeTrusted)
    -                    dsig_ctx = xmlsec.DSigCtx(mngr)
    -                else:
    -                    dsig_ctx = xmlsec.DSigCtx()
    -                    dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None)
    +        :param fingerprint: The fingerprint of the public cert
    +        :type: string
     
    -                file_cert.close()
    +        :param fingerprintalg: The algorithm used to build the fingerprint
    +        :type: string
     
    -                dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509])
    -                dsig_ctx.verify(signature_node)
    -                return True
    -            else:
    -                return False
    -        except Exception:
    -            return False
    +        :param validatecert: If true, will verify the signature and if the cert is valid.
    +        :type: bool
    +
    +        :param debug: Activate the xmlsec debug
    +        :type: bool
    +
    +        :param raise_exceptions: Whether to return false on failure or raise an exception
    +        :type raise_exceptions: Boolean
    +        """
    +        error_callback_method = None
    +        if debug:
    +            error_callback_method = print_xmlsec_errors
    +        xmlsec.set_error_callback(error_callback_method)
    +
    +        xmlsec.addIDs(elem, ["ID"])
    +
    +        if (cert is None or cert == '') and fingerprint:
    +            x509_certificate_nodes = OneLogin_Saml2_Utils.query(signature_node, '//ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate')
    +            if len(x509_certificate_nodes) > 0:
    +                x509_certificate_node = x509_certificate_nodes[0]
    +                x509_cert_value = OneLogin_Saml2_Utils.element_text(x509_certificate_node)
    +                x509_cert_value_formatted = OneLogin_Saml2_Utils.format_cert(x509_cert_value)
    +                x509_fingerprint_value = OneLogin_Saml2_Utils.calculate_x509_fingerprint(x509_cert_value_formatted, fingerprintalg)
    +
    +                if fingerprint == x509_fingerprint_value:
    +                    cert = x509_cert_value_formatted
    +
    +        # Check if Reference URI is empty
    +        # reference_elem = OneLogin_Saml2_Utils.query(signature_node, '//ds:Reference')
    +        # if len(reference_elem) > 0:
    +        #    if reference_elem[0].get('URI') == '':
    +        #        reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID'))
    +
    +        if cert is None or cert == '':
    +            raise OneLogin_Saml2_Error(
    +                'Could not validate node signature: No certificate provided.',
    +                OneLogin_Saml2_Error.CERT_NOT_FOUND
    +            )
    +
    +        file_cert = OneLogin_Saml2_Utils.write_temp_file(cert)
    +
    +        if validatecert:
    +            mngr = xmlsec.KeysMngr()
    +            mngr.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem, xmlsec.KeyDataTypeTrusted)
    +            dsig_ctx = xmlsec.DSigCtx(mngr)
    +        else:
    +            dsig_ctx = xmlsec.DSigCtx()
    +            dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None)
    +
    +        file_cert.close()
    +
    +        dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509])
    +
    +        try:
    +            dsig_ctx.verify(signature_node)
    +        except Exception as err:
    +            raise OneLogin_Saml2_ValidationError(
    +                'Signature validation failed. SAML Response rejected. %s',
    +                OneLogin_Saml2_ValidationError.INVALID_SIGNATURE,
    +                err.__str__()
    +            )
    +
    +        return True
     
         @staticmethod
    +    @return_false_on_exception
         def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_Saml2_Constants.RSA_SHA1, debug=False):
             """
    -        Validates signed bynary data (Used to validate GET Signature).
    +        Validates signed binary data (Used to validate GET Signature).
     
             :param signed_query: The element we should validate
             :type: string
    @@ -974,7 +1206,7 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_
             :param signature: The signature that will be validate
             :type: string
     
    -        :param cert: The pubic cert
    +        :param cert: The public cert
             :type: string
     
             :param algorithm: Signature algorithm
    @@ -982,30 +1214,58 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_
     
             :param debug: Activate the xmlsec debug
             :type: bool
    +
    +        :param raise_exceptions: Whether to return false on failure or raise an exception
    +        :type raise_exceptions: Boolean
             """
    -        try:
    -            xmlsec.initialize()
    +        error_callback_method = None
    +        if debug:
    +            error_callback_method = print_xmlsec_errors
    +        xmlsec.set_error_callback(error_callback_method)
     
    -            if debug:
    -                xmlsec.set_error_callback(print_xmlsec_errors)
    +        dsig_ctx = xmlsec.DSigCtx()
     
    -            dsig_ctx = xmlsec.DSigCtx()
    +        file_cert = OneLogin_Saml2_Utils.write_temp_file(cert)
    +        dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None)
    +        file_cert.close()
     
    -            file_cert = OneLogin_Saml2_Utils.write_temp_file(cert)
    -            dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None)
    -            file_cert.close()
    +        # Sign the metadata with our private key.
    +        sign_algorithm_transform_map = {
    +            OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1,
    +            OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1,
    +            OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.TransformRsaSha256,
    +            OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.TransformRsaSha384,
    +            OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.TransformRsaSha512
    +        }
    +        sign_algorithm_transform = sign_algorithm_transform_map.get(algorithm, xmlsec.TransformRsaSha1)
     
    -            # Sign the metadata with our private key.
    -            sign_algorithm_transform_map = {
    -                OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1,
    -                OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1,
    -                OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.TransformRsaSha256,
    -                OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.TransformRsaSha384,
    -                OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.TransformRsaSha512
    -            }
    -            sign_algorithm_transform = sign_algorithm_transform_map.get(algorithm, xmlsec.TransformRsaSha1)
    -
    -            dsig_ctx.verifyBinary(signed_query, sign_algorithm_transform, signature)
    -            return True
    -        except Exception:
    -            return False
    +        dsig_ctx.verifyBinary(signed_query, sign_algorithm_transform, signature)
    +        return True
    +
    +    @staticmethod
    +    def get_encoded_parameter(get_data, name, default=None, lowercase_urlencoding=False):
    +        """Return a URL encoded get parameter value
    +        Prefer to extract the original encoded value directly from query_string since URL
    +        encoding is not canonical. The encoding used by ADFS 3.0 is not compatible with
    +        python's quote_plus (ADFS produces lower case hex numbers and quote_plus produces
    +        upper case hex numbers)
    +        """
    +
    +        if name not in get_data:
    +            return OneLogin_Saml2_Utils.case_sensitive_urlencode(default, lowercase_urlencoding)
    +        if 'query_string' in get_data:
    +            return OneLogin_Saml2_Utils.extract_raw_query_parameter(get_data['query_string'], name)
    +        return OneLogin_Saml2_Utils.case_sensitive_urlencode(get_data[name], lowercase_urlencoding)
    +
    +    @staticmethod
    +    def extract_raw_query_parameter(query_string, parameter, default=''):
    +        m = re.search('%s=([^&]+)' % parameter, query_string)
    +        if m:
    +            return m.group(1)
    +        else:
    +            return default
    +
    +    @staticmethod
    +    def case_sensitive_urlencode(to_encode, lowercase=False):
    +        encoded = quote_plus(to_encode)
    +        return re.sub(r"%[A-F0-9]{2}", lambda m: m.group(0).lower(), encoded) if lowercase else encoded
    diff --git a/tests/data/logout_requests/invalids/invalid_issuer.xml b/tests/data/logout_requests/invalids/invalid_issuer.xml
    index d5bf6609..e1edabde 100644
    --- a/tests/data/logout_requests/invalids/invalid_issuer.xml
    +++ b/tests/data/logout_requests/invalids/invalid_issuer.xml
    @@ -9,6 +9,6 @@
                          >
         https://example.hello.com/access/saml
         ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c
     
    diff --git a/tests/data/logout_requests/invalids/invalid_issuer.xml.base64 b/tests/data/logout_requests/invalids/invalid_issuer.xml.base64
    index 26912f3d..56de49b0 100644
    --- a/tests/data/logout_requests/invalids/invalid_issuer.xml.base64
    +++ b/tests/data/logout_requests/invalids/invalid_issuer.xml.base64
    @@ -1 +1 @@
    -jZPJbsIwEEDvfAXKncR2lhILgpBoq0gU2lL10EvlOpMSKbHTjFPx+XXSjQNB+GTZ4zdv7PFscajK8Sc0WGg1d6hLnEUymqGoypqv9btuzSN8tIBmbAMV8n5n7rSN4lpggVyJCpAbyXfLuzVnLuF1o42WunRG41Pjn3MeIxChMVZrgJOu5s52c73e3qabV0bDaSBllgsp/EjQIBQQxyQIWCazOHrLISI0pDDAev69AJt4KB1iC6lCI5SxcYT6E8omlDyRgPsx9+nLwMGVvbxCCdPz98bU3PPQtHnuSl15oLJaF8rg0QxLdOt9PcDbaLNV22aZG2g6EeZPSHiJSNIv92/L+2qapNNB6wMHUdUluHsoS917CSkBrYoNnnnHR44gG/tm6Wq8u+8mD60oi7zonC6inpC80U0lzPmu6FaKbJL3obxVWIPs0mYneMlfe1CwnSApi4HmU8bklMQRmdo+ocSXIQCTV1IAlT+lfhdm/4F34iMkXw==
    +jZPfb4IwEMffTfwfDO9CW8BJoxgTt8XEyTaXPexl6coxSaBlXFn881dwP3yQxT411+v3+7n2brY4lMXoE2rMtZo71CXOIh4OZijKouIb/a4b8wgfDaAZ2UyFvDuZO02tuBaYI1eiBORG8t3ybsOZS3hVa6OlLpzhYHRu/Qn9ryMQoTYWrE9ovZo7yfZ6k9yut6+MhtNAyjQTUvgTQYNQQBSRIGCpTKPJWwYTQkMKfWLPP49grXsNERtYKzRCGZtIqD+mbEzJEwm4H3GfvvTdXNkXzJUwncPemIp7Hpomy1ypSw9UWulcGTzZYYFuta/6BLfaJCqpl5mBukVh/piEF6HEx3j3x7yrqI5bIrRIcBBlVYC7h6LQHZqQEtDS2OSZd3rlVGVr/269Gu3u281DI4o8y1usi2TPcd7ouhSmvz2oS7tIno6zLpU3CiuQrW96TjD+7RMKtiUkZRHQbMqYnJJoQqa2YSjxZQjA5JUUQOV3tcfS2pnwzgxF/AU=
    \ No newline at end of file
    diff --git a/tests/data/logout_requests/invalids/not_after_failed.xml b/tests/data/logout_requests/invalids/not_after_failed.xml
    index 8f4e2beb..1f825036 100644
    --- a/tests/data/logout_requests/invalids/not_after_failed.xml
    +++ b/tests/data/logout_requests/invalids/not_after_failed.xml
    @@ -9,6 +9,6 @@
                          >
         http://idp.example.com/
         ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c
     
    diff --git a/tests/data/logout_requests/invalids/not_after_failed.xml.base64 b/tests/data/logout_requests/invalids/not_after_failed.xml.base64
    index 753d4b81..8ba510d0 100644
    --- a/tests/data/logout_requests/invalids/not_after_failed.xml.base64
    +++ b/tests/data/logout_requests/invalids/not_after_failed.xml.base64
    @@ -1 +1 @@
    -jVJNT8MwDL3vV1S9r036xRptnSYNUKWxAUMcuKCQuqxSm4Q6Rfv5ZGXADutETpbz/Pxsv+l839TOJ7RYKTlzqUfceTaaIm9qzVbqXXXmET46QONYoETW/8zcrpVMcayQSd4AMiPYdnG3YoFHmG6VUULV7sg59/54LtNwRGiNlTXAky9n7mZ9vdrc5uvXgMaTSIii5IKHCadRzCFNSRQFhSjS5K2EhNCYwgDX888CbOOhdogd5BINl8biCA3HNBhT8kQiFqYspC8DhUu7vEpy0/PvjNHM99F0ZekJ1fggC60qafAkwho9vdMDfGtlNnLTLkoD7VEIif8jJOvT/W1ZP02bHeVUhfZgzxtdQy9q6p+CTsrW9kr50tneH4KHjtdVWR1UDPCcEXKj2oaby5c/ZKpiXPZQ1knUIA6NijN82a8FKNhrCxqkQMtJEIgJSRMysV6gJBQxQCCuBAcqjsN9j2K97p8xe/YF
    +jVJNT8MwDL1P2n+Yel+b9Is12jpNGqBKYwOGOHBBIXVZpTYJdYr282m7ATu0iJwsx37v2X7z5bEsJp9QYa7kwqI2sZbxeDRHXhaabdS7qs0jfNSAZtJUSmTdz8KqK8kUxxyZ5CUgM4LtV3cb5tqE6UoZJVRhjUeTvvcL9DcOR4TKNMKGgJL1wtptrze722T76tJg5guRZlxwL+TUDzhEEfF9NxVpFL5lEBIaUBgCe/5eQkM9SIhYQyLRcGmaQkK9KXWnlDwRn3kR8+jLUOe62WAuuekYDsZo5jho6iyzhSodkKlWuTR4EWGBtj7oIcCtMju5q1aZgeoshQT/khKf8t2NWTdRFZ8V5am24chLXUCna+5cFl32bZtrJevJ/r4NHmpe5FneChkA6tNyo6qSm2ELUJt2mTydZl0pqyVqEC1T2gcY/3iBQnN2Qd0IaDZzXTEjUUhmjSko8UQA4IorwYGK83ynYVrfOz3Gj78A
    \ No newline at end of file
    diff --git a/tests/data/logout_requests/logout_request.xml b/tests/data/logout_requests/logout_request.xml
    index b3b99bee..10904a28 100644
    --- a/tests/data/logout_requests/logout_request.xml
    +++ b/tests/data/logout_requests/logout_request.xml
    @@ -8,6 +8,6 @@
                          >
         http://idp.example.com/
         ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c
     
    diff --git a/tests/data/logout_requests/logout_request.xml.base64 b/tests/data/logout_requests/logout_request.xml.base64
    index adca75e1..870a6383 100644
    --- a/tests/data/logout_requests/logout_request.xml.base64
    +++ b/tests/data/logout_requests/logout_request.xml.base64
    @@ -1 +1 @@
    -PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6TG9nb3V0UmVxdWVzdCB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIg0KICAgICAgICAgICAgICAgICAgICAgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiINCiAgICAgICAgICAgICAgICAgICAgIElEPSJPTkVMT0dJTl8yMTU4NGNjZGZhY2EzNmExNDVhZTk5MDQ0MmRjZDk2YmZlNjAxNTFlIg0KICAgICAgICAgICAgICAgICAgICAgVmVyc2lvbj0iMi4wIg0KICAgICAgICAgICAgICAgICAgICAgSXNzdWVJbnN0YW50PSIyMDEzLTEyLTEwVDA0OjM5OjMxWiINCiAgICAgICAgICAgICAgICAgICAgIERlc3RpbmF0aW9uPSJodHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9lbmRwb2ludHMvc2xzLnBocCINCiAgICAgICAgICAgICAgICAgICAgID4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPg0KICAgIDxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHA6Ly9pZHAuZXhhbXBsZS5jb20vIg0KICAgICAgICAgICAgICAgICBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnVuc3BlY2lmaWVkIg0KICAgICAgICAgICAgICAgICA+T05FTE9HSU5fMWU0NDJjMTI5ZTFmODIyYzgwOTYwODZhMTEwM2M1ZWUyYzdjYWUxYzwvc2FtbDpOYW1lSUQ+DQo8L3NhbWxwOkxvZ291dFJlcXVlc3Q+
    +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6TG9nb3V0UmVxdWVzdCB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIg0KICAgICAgICAgICAgICAgICAgICAgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiINCiAgICAgICAgICAgICAgICAgICAgIElEPSJPTkVMT0dJTl8yMTU4NGNjZGZhY2EzNmExNDVhZTk5MDQ0MmRjZDk2YmZlNjAxNTFlIg0KICAgICAgICAgICAgICAgICAgICAgVmVyc2lvbj0iMi4wIg0KICAgICAgICAgICAgICAgICAgICAgSXNzdWVJbnN0YW50PSIyMDEzLTEyLTEwVDA0OjM5OjMxWiINCiAgICAgICAgICAgICAgICAgICAgIERlc3RpbmF0aW9uPSJodHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9lbmRwb2ludHMvc2xzLnBocCINCiAgICAgICAgICAgICAgICAgICAgID4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPg0KICAgIDxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHA6Ly9pZHAuZXhhbXBsZS5jb20vIg0KICAgICAgICAgICAgICAgICBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OnVuc3BlY2lmaWVkIg0KICAgICAgICAgICAgICAgICA+T05FTE9HSU5fMWU0NDJjMTI5ZTFmODIyYzgwOTYwODZhMTEwM2M1ZWUyYzdjYWUxYzwvc2FtbDpOYW1lSUQ+DQo8L3NhbWxwOkxvZ291dFJlcXVlc3Q+
    \ No newline at end of file
    diff --git a/tests/data/logout_requests/logout_request_deflated.xml.base64 b/tests/data/logout_requests/logout_request_deflated.xml.base64
    index 4c0a089e..894de1ec 100644
    --- a/tests/data/logout_requests/logout_request_deflated.xml.base64
    +++ b/tests/data/logout_requests/logout_request_deflated.xml.base64
    @@ -1 +1 @@
    -fZLfT8IwEMff+SvI3tnabUzWwIgJapYgqBgffDG1u0mTra27zvDnWyYiMYw+Xa53n/vxvel8V1fDL2hQajXzqE+8eTaYIq8rw5b6Q7f2CT5bQDt0gQpZ9zPz2kYxzVEiU7wGZFawzfX9koU+YabRVgtdeYPhuffHuYzhiNBY11YPJ1/MvPXqZrm+y1dvIR1PYiGKkgseJZzGYw5pSuI4LESRJu8lJISOKfSwXn4X4Ar3lUNsIVdoubIujtBoRMMRJc8kZlHKIvrak7hwy5OK246/tdawIEDblqUvdB2AKoyWyuKJhRX6Zmt6eFnn7iRiXVNNdqDKwviw47WpoGNPg9Ogk7SVW3a+GG4e9sZjyytZSmiO3f3nnGnkVjc1t5cF3HtkMSq7UNYqNCD2hYozvOyoJAUnmqBhCrSchKGYkDQhEycpJZEYA4TiSnCg4jDczyjuZIMzN5t9Aw==
    +fZJNT4NAEIbvTfofCPfCLh8VNi2NSdWQ1Fat8eDFrMtgSWB3ZRbTny+ltTYG3NNkduaZj3dmi31VWl9QY6Hk3KYOsRfJeDRDXpWardSHaswTfDaAxmojJbLuZ243tWSKY4FM8gqQGcG21/cr5jmE6VoZJVRpj0dW3/sF/c/hiFCbtrEhULqc25v1zWpzl67fPBpGgRBZzgX3p5wGIYc4JkHgZSKLp+85TAkNKQzBXn6W0JYeLIjYQCrRcGnaQEL9CfUmlDyTgPkx8+nrUOay3WAhuekq7IzRzHXRNHnuCFW5IDOtCmnwwsISHb3TQ8Dk6O+UYl1fdXLiFpl2YM8rXUJHn7mXQZd563bn6dLaPhyMx4aXRV5AfW7wL6ivl1tVV9wMC0kd2nmKbJJ3oayRqEEcKmV9wOSsKIVWPEG9GGgeeZ6ISDwlUSstJb4IATxxJThQcZrvOMzhet2e802+AQ==
    \ No newline at end of file
    diff --git a/tests/data/logout_requests/logout_request_with_sessionindex.xml b/tests/data/logout_requests/logout_request_with_sessionindex.xml
    index dd6de13a..5dbc3c32 100644
    --- a/tests/data/logout_requests/logout_request_with_sessionindex.xml
    +++ b/tests/data/logout_requests/logout_request_with_sessionindex.xml
    @@ -8,7 +8,7 @@
                          >
         http://idp.example.com/
         ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c
         _ac72a76526cb6ca19f8438e73879a0e6c8ae5131
     
    diff --git a/tests/data/metadata/expired_metadata_settings1.xml b/tests/data/metadata/expired_metadata_settings1.xml
    index 6311dd69..9af134d8 100644
    --- a/tests/data/metadata/expired_metadata_settings1.xml
    +++ b/tests/data/metadata/expired_metadata_settings1.xml
    @@ -3,7 +3,7 @@
         
             MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOoMIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo
             
    -        urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified
    +        urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
             
        
     
    diff --git a/tests/data/metadata/idp_metadata2.xml b/tests/data/metadata/idp_metadata2.xml
    new file mode 100644
    index 00000000..0b19f62a
    --- /dev/null
    +++ b/tests/data/metadata/idp_metadata2.xml
    @@ -0,0 +1 @@
    +                              MIIDPDCCAiQCCQDydJgOlszqbzANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEQMA4GA1UEChMHSmFua3lDbzESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTE0MDMxMjE5NDYzM1oXDTI3MTExOTE5NDYzM1owYDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xEDAOBgNVBAoTB0phbmt5Q28xEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMGvJpRTTasRUSPqcbqCG+ZnTAurnu0vVpIG9lzExnh11o/BGmzu7lB+yLHcEdwrKBBmpepDBPCYxpVajvuEhZdKFx/Fdy6j5mH3rrW0Bh/zd36CoUNjbbhHyTjeM7FN2yF3u9lcyubuvOzr3B3gX66IwJlU46+wzcQVhSOlMk2tXR+fIKQExFrOuK9tbX3JIBUqItpI+HnAow509CnM134svw8PTFLkR6/CcMqnDfDK1m993PyoC1Y+N4X9XkhSmEQoAlAHPI5LHrvuujM13nvtoVYvKYoj7ScgumkpWNEvX652LfXOnKYlkB8ZybuxmFfIkzedQrbJsyOhfL03cMECAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAeHwzqwnzGEkxjzSD47imXaTqtYyETZow7XwBc0ZaFS50qRFJUgKTAmKS1xQBP/qHpStsROT35DUxJAE6NY1Kbq3ZbCuhGoSlY0L7VzVT5tpu4EY8+Dq/u2EjRmmhoL7UkskvIZ2n1DdERtd+YUMTeqYl9co43csZwDno/IKomeN5qaPc39IZjikJ+nUC6kPFKeu/3j9rgHNlRtocI6S1FdtFz9OZMQlpr0JbUt2T3xS/YoQJn6coDmJL5GTiiKM6cOe+Ur1VwzS1JEDbSS2TWWhzq8ojLdrotYLGd9JOsoQhElmz+tMfCFQUFLExinPAyy7YHlSiVX13QH2XTu/iQQ==                          urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress    urn:oasis:names:tc:SAML:2.0:nameid-format:persistent    urn:oasis:names:tc:SAML:2.0:nameid-format:transient                                  
    \ No newline at end of file
    diff --git a/tests/data/metadata/idp_metadata_different_sign_and_encrypt_cert.xml b/tests/data/metadata/idp_metadata_different_sign_and_encrypt_cert.xml
    new file mode 100644
    index 00000000..df90353a
    --- /dev/null
    +++ b/tests/data/metadata/idp_metadata_different_sign_and_encrypt_cert.xml
    @@ -0,0 +1,72 @@
    +
    +
    +  
    +    
    +      
    +        
    +          MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET
    +MBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD
    +VQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2
    +MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
    +DApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u
    +ZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B
    +AQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z
    +0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT
    +gf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m
    +Tr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF
    +zRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ
    +UAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG
    +A1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV
    +HSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV
    +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw
    +DwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO
    +BgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu
    +AuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV
    +gG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ
    +sTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP
    +TbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu
    +QOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78
    +1sE=
    +        
    +      
    +    
    +    
    +      
    +        
    +          MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEF
    +BQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJj
    +aWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwW
    +T25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUy
    +MjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChz
    +Z2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNV
    +BAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IB
    +DwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo
    +3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRw
    +tnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xx
    +VRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5
    +L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t
    +1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/
    +BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCB
    +pIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYD
    +VQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQL
    +DAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaC
    +FD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0B
    +AQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXM
    +GI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65c
    +hjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIB
    +vlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37
    +MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZ
    +WQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==
    +        
    +      
    +    
    +    urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
    +    
    +    
    +    
    +  
    +  
    +    Support
    +    support@onelogin.com
    +  
    +
    \ No newline at end of file
    diff --git a/tests/data/metadata/idp_metadata_multi_certs.xml b/tests/data/metadata/idp_metadata_multi_certs.xml
    new file mode 100644
    index 00000000..f993f64a
    --- /dev/null
    +++ b/tests/data/metadata/idp_metadata_multi_certs.xml
    @@ -0,0 +1,75 @@
    +
    +
    +  
    +    
    +      
    +        
    +          MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEF
    +BQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJj
    +aWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwW
    +T25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUy
    +MjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChz
    +Z2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNV
    +BAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IB
    +DwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo
    +3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRw
    +tnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xx
    +VRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5
    +L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t
    +1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/
    +BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCB
    +pIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYD
    +VQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQL
    +DAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaC
    +FD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0B
    +AQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXM
    +GI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65c
    +hjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIB
    +vlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37
    +MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZ
    +WQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==
    +        
    +      
    +    
    +    
    +      
    +        
    +          MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEUMBIGA1UECAwLZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xNzA0MTUxNjMzMThaFw0xODA0MTUxNjMzMThaME8xCzAJBgNVBAYTAnVzMRQwEgYDVQQIDAtleGFtcGxlLmNvbTEUMBIGA1UECgwLZXhhbXBsZS5jb20xFDASBgNVBAMMC2V4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6GLkl5lDUZdHNDAojp5i24OoPlqrt5TGXJIPqAZYT1hQvJW5nv17MFDHrjmtEnmW4ACKEy0fAX80QWIcHunZSkbEGHb+NG/6oTi5RipXMvmHnfFnPJJ0AdtiLiPE478CV856gXekV4Xx5u3KrylcOgkpYsp0GMIQBDzleMUXlYQIDAQABo1AwTjAdBgNVHQ4EFgQUnP8vlYPGPL2n6ZzDYij2kMDC8wMwHwYDVR0jBBgwFoAUnP8vlYPGPL2n6ZzDYij2kMDC8wMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQAlQGAl+b8Cpot1g+65lLLjVoY7APJPWLW0klKQNlMU0s4MU+71Y3ExUEOXDAZgKcFoavb1fEOGMwEf38NaJAy1e/l6VNuixXShffq20ymqHQxOG0q8ujeNkgZF9k6XDfn/QZ3AD0o/IrCT7UMc/0QsfgIjWYxwCvp2syApc5CYfQ==
    +        
    +      
    +    
    +    
    +      
    +        
    +          MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEF
    +BQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJj
    +aWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwW
    +T25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUy
    +MjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChz
    +Z2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNV
    +BAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IB
    +DwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo
    +3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRw
    +tnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xx
    +VRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5
    +L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t
    +1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/
    +BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCB
    +pIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYD
    +VQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQL
    +DAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaC
    +FD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0B
    +AQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXM
    +GI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65c
    +hjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIB
    +vlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37
    +MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZ
    +WQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==
    +        
    +      
    +    
    +        
    +    urn:oasis:names:tc:SAML:2.0:nameid-format:transient
    +    
    +  
    +
    \ No newline at end of file
    diff --git a/tests/data/metadata/idp_metadata_multi_signing_certs.xml b/tests/data/metadata/idp_metadata_multi_signing_certs.xml
    new file mode 100644
    index 00000000..0cba257a
    --- /dev/null
    +++ b/tests/data/metadata/idp_metadata_multi_signing_certs.xml
    @@ -0,0 +1,75 @@
    +
    +
    +  
    +    
    +      
    +        
    +          MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEF
    +BQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJj
    +aWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwW
    +T25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUy
    +MjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChz
    +Z2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNV
    +BAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IB
    +DwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo
    +3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRw
    +tnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xx
    +VRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5
    +L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t
    +1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/
    +BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCB
    +pIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYD
    +VQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQL
    +DAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaC
    +FD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0B
    +AQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXM
    +GI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65c
    +hjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIB
    +vlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37
    +MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZ
    +WQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==
    +        
    +      
    +    
    +    
    +      
    +        
    +          MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEUMBIGA1UECAwLZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xNzA0MTUxNjMzMThaFw0xODA0MTUxNjMzMThaME8xCzAJBgNVBAYTAnVzMRQwEgYDVQQIDAtleGFtcGxlLmNvbTEUMBIGA1UECgwLZXhhbXBsZS5jb20xFDASBgNVBAMMC2V4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6GLkl5lDUZdHNDAojp5i24OoPlqrt5TGXJIPqAZYT1hQvJW5nv17MFDHrjmtEnmW4ACKEy0fAX80QWIcHunZSkbEGHb+NG/6oTi5RipXMvmHnfFnPJJ0AdtiLiPE478CV856gXekV4Xx5u3KrylcOgkpYsp0GMIQBDzleMUXlYQIDAQABo1AwTjAdBgNVHQ4EFgQUnP8vlYPGPL2n6ZzDYij2kMDC8wMwHwYDVR0jBBgwFoAUnP8vlYPGPL2n6ZzDYij2kMDC8wMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQAlQGAl+b8Cpot1g+65lLLjVoY7APJPWLW0klKQNlMU0s4MU+71Y3ExUEOXDAZgKcFoavb1fEOGMwEf38NaJAy1e/l6VNuixXShffq20ymqHQxOG0q8ujeNkgZF9k6XDfn/QZ3AD0o/IrCT7UMc/0QsfgIjWYxwCvp2syApc5CYfQ==
    +        
    +      
    +    
    +    
    +      
    +        
    +          MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEF
    +BQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJj
    +aWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwW
    +T25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUy
    +MjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChz
    +Z2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNV
    +BAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IB
    +DwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo
    +3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRw
    +tnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xx
    +VRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5
    +L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t
    +1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/
    +BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCB
    +pIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYD
    +VQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQL
    +DAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaC
    +FD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0B
    +AQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXM
    +GI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65c
    +hjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIB
    +vlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37
    +MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZ
    +WQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==
    +        
    +      
    +    
    +        
    +    urn:oasis:names:tc:SAML:2.0:nameid-format:transient
    +    
    +  
    +
    diff --git a/tests/data/metadata/idp_metadata_same_sign_and_encrypt_cert.xml b/tests/data/metadata/idp_metadata_same_sign_and_encrypt_cert.xml
    new file mode 100644
    index 00000000..e7fd250b
    --- /dev/null
    +++ b/tests/data/metadata/idp_metadata_same_sign_and_encrypt_cert.xml
    @@ -0,0 +1,71 @@
    +
    +
    +  
    +    
    +      
    +        
    +          MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET
    +MBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD
    +VQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2
    +MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
    +DApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u
    +ZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B
    +AQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z
    +0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT
    +gf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m
    +Tr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF
    +zRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ
    +UAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG
    +A1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV
    +HSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV
    +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw
    +DwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO
    +BgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu
    +AuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV
    +gG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ
    +sTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP
    +TbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu
    +QOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78
    +1sE=
    +        
    +      
    +    
    +    
    +      
    +        
    +          MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET
    +MBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD
    +VQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2
    +MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
    +DApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u
    +ZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B
    +AQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z
    +0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT
    +gf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m
    +Tr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF
    +zRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ
    +UAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG
    +A1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV
    +HSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV
    +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw
    +DwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO
    +BgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu
    +AuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV
    +gG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ
    +sTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP
    +TbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu
    +QOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78
    +1sE=
    +        
    +      
    +    
    +    urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
    +    
    +    
    +    
    +  
    +  
    +    Support
    +    support@onelogin.com
    +  
    +
    \ No newline at end of file
    diff --git a/tests/data/metadata/idp_multiple_descriptors.xml b/tests/data/metadata/idp_multiple_descriptors.xml
    new file mode 100644
    index 00000000..c77face7
    --- /dev/null
    +++ b/tests/data/metadata/idp_multiple_descriptors.xml
    @@ -0,0 +1,53 @@
    +
    +
    +  
    +    
    +      
    +        
    +          
    +            LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURxekNDQXhTZ0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBRENCaGpFTE1Ba0dBMVVFQmhNQ1FWVXgKRERBS0JnTlZCQWdUQTA1VFZ6RVBNQTBHQTFVRUJ4TUdVM2xrYm1WNU1Rd3dDZ1lEVlFRS0RBTlFTVlF4Q1RBSApCZ05WQkFzTUFERVlNQllHQTFVRUF3d1BiR0YzY21WdVkyVndhWFF1WTI5dE1TVXdJd1lKS29aSWh2Y05BUWtCCkRCWnNZWGR5Wlc1alpTNXdhWFJBWjIxaGFXd3VZMjl0TUI0WERURXlNRFF4T1RJeU5UUXhPRm9YRFRNeU1EUXgKTkRJeU5UUXhPRm93Z1lZeEN6QUpCZ05WQkFZVEFrRlZNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVApCbE41Wkc1bGVURU1NQW9HQTFVRUNnd0RVRWxVTVFrd0J3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psCmJtTmxjR2wwTG1OdmJURWxNQ01HQ1NxR1NJYjNEUUVKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnYKYlRDQm56QU5CZ2txaGtpRzl3MEJBUUVGQUFPQmpRQXdnWWtDZ1lFQXFqaWUzUjJvaStwRGFldndJeXMvbWJVVApubkdsa3h0ZGlrcnExMXZleHd4SmlQTmhtaHFSVzNtVXVKRXpsbElkVkw2RW14R1lUcXBxZjkzSGxoa3NhZUowCjhVZ2pQOVVtTVlyaFZKdTFqY0ZXVjdmei9yKzIxL2F3VG5EVjlzTVlRcXVJUllZeTdiRzByMU9iaXdkb3ZudGsKN2dGSTA2WjB2WmFjREU1Ym9xVUNBd0VBQWFPQ0FTVXdnZ0VoTUFrR0ExVWRFd1FDTUFBd0N3WURWUjBQQkFRRApBZ1VnTUIwR0ExVWREZ1FXQkJTUk9OOEdKOG8rOGpnRnRqa3R3WmRxeDZCUnlUQVRCZ05WSFNVRUREQUtCZ2dyCkJnRUZCUWNEQVRBZEJnbGdoa2dCaHZoQ0FRMEVFQllPVkdWemRDQllOVEE1SUdObGNuUXdnYk1HQTFVZEl3U0IKcXpDQnFJQVVrVGpmQmlmS1B2STRCYlk1TGNHWGFzZWdVY21oZ1l5a2dZa3dnWVl4Q3pBSkJnTlZCQVlUQWtGVgpNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVEJsTjVaRzVsZVRFTU1Bb0dBMVVFQ2d3RFVFbFVNUWt3CkJ3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psYm1ObGNHbDBMbU52YlRFbE1DTUdDU3FHU0liM0RRRUoKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnZiWUlCQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9CZ1FDRQpUQWVKVERTQVc2ejFVRlRWN1FyZWg0VUxGT1JhajkrZUN1RjNLV0RIYyswSVFDajlyZG5ERzRRL3dmNy9yYVEwCkpuUFFDU0NkclBMSmV5b1BIN1FhVHdvYUY3ZHpWdzRMQ3N5TkpURld4NGNNNTBWdzZSNWZET2dpQzhic2ZmUzgKQkptb3VscnJaRE5OVmpHOG1XNmNMeHJZdlZRT3JSVmVjQ0ZJZ3NzQ2JBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
    +          
    +        
    +      
    +      
    +        
    +          
    +            LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURxekNDQXhTZ0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBRENCaGpFTE1Ba0dBMVVFQmhNQ1FWVXgKRERBS0JnTlZCQWdUQTA1VFZ6RVBNQTBHQTFVRUJ4TUdVM2xrYm1WNU1Rd3dDZ1lEVlFRS0RBTlFTVlF4Q1RBSApCZ05WQkFzTUFERVlNQllHQTFVRUF3d1BiR0YzY21WdVkyVndhWFF1WTI5dE1TVXdJd1lKS29aSWh2Y05BUWtCCkRCWnNZWGR5Wlc1alpTNXdhWFJBWjIxaGFXd3VZMjl0TUI0WERURXlNRFF4T1RJeU5UUXhPRm9YRFRNeU1EUXgKTkRJeU5UUXhPRm93Z1lZeEN6QUpCZ05WQkFZVEFrRlZNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVApCbE41Wkc1bGVURU1NQW9HQTFVRUNnd0RVRWxVTVFrd0J3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psCmJtTmxjR2wwTG1OdmJURWxNQ01HQ1NxR1NJYjNEUUVKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnYKYlRDQm56QU5CZ2txaGtpRzl3MEJBUUVGQUFPQmpRQXdnWWtDZ1lFQXFqaWUzUjJvaStwRGFldndJeXMvbWJVVApubkdsa3h0ZGlrcnExMXZleHd4SmlQTmhtaHFSVzNtVXVKRXpsbElkVkw2RW14R1lUcXBxZjkzSGxoa3NhZUowCjhVZ2pQOVVtTVlyaFZKdTFqY0ZXVjdmei9yKzIxL2F3VG5EVjlzTVlRcXVJUllZeTdiRzByMU9iaXdkb3ZudGsKN2dGSTA2WjB2WmFjREU1Ym9xVUNBd0VBQWFPQ0FTVXdnZ0VoTUFrR0ExVWRFd1FDTUFBd0N3WURWUjBQQkFRRApBZ1VnTUIwR0ExVWREZ1FXQkJTUk9OOEdKOG8rOGpnRnRqa3R3WmRxeDZCUnlUQVRCZ05WSFNVRUREQUtCZ2dyCkJnRUZCUWNEQVRBZEJnbGdoa2dCaHZoQ0FRMEVFQllPVkdWemRDQllOVEE1SUdObGNuUXdnYk1HQTFVZEl3U0IKcXpDQnFJQVVrVGpmQmlmS1B2STRCYlk1TGNHWGFzZWdVY21oZ1l5a2dZa3dnWVl4Q3pBSkJnTlZCQVlUQWtGVgpNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVEJsTjVaRzVsZVRFTU1Bb0dBMVVFQ2d3RFVFbFVNUWt3CkJ3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psYm1ObGNHbDBMbU52YlRFbE1DTUdDU3FHU0liM0RRRUoKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnZiWUlCQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9CZ1FDRQpUQWVKVERTQVc2ejFVRlRWN1FyZWg0VUxGT1JhajkrZUN1RjNLV0RIYyswSVFDajlyZG5ERzRRL3dmNy9yYVEwCkpuUFFDU0NkclBMSmV5b1BIN1FhVHdvYUY3ZHpWdzRMQ3N5TkpURld4NGNNNTBWdzZSNWZET2dpQzhic2ZmUzgKQkptb3VscnJaRE5OVmpHOG1XNmNMeHJZdlZRT3JSVmVjQ0ZJZ3NzQ2JBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
    +          
    +        
    +      
    +      
    +      urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
    +      urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
    +      urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
    +      
    +      
    +      
    +    
    +  
    +  
    +    
    +      
    +        
    +          
    +            LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURxekNDQXhTZ0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBRENCaGpFTE1Ba0dBMVVFQmhNQ1FWVXgKRERBS0JnTlZCQWdUQTA1VFZ6RVBNQTBHQTFVRUJ4TUdVM2xrYm1WNU1Rd3dDZ1lEVlFRS0RBTlFTVlF4Q1RBSApCZ05WQkFzTUFERVlNQllHQTFVRUF3d1BiR0YzY21WdVkyVndhWFF1WTI5dE1TVXdJd1lKS29aSWh2Y05BUWtCCkRCWnNZWGR5Wlc1alpTNXdhWFJBWjIxaGFXd3VZMjl0TUI0WERURXlNRFF4T1RJeU5UUXhPRm9YRFRNeU1EUXgKTkRJeU5UUXhPRm93Z1lZeEN6QUpCZ05WQkFZVEFrRlZNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVApCbE41Wkc1bGVURU1NQW9HQTFVRUNnd0RVRWxVTVFrd0J3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psCmJtTmxjR2wwTG1OdmJURWxNQ01HQ1NxR1NJYjNEUUVKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnYKYlRDQm56QU5CZ2txaGtpRzl3MEJBUUVGQUFPQmpRQXdnWWtDZ1lFQXFqaWUzUjJvaStwRGFldndJeXMvbWJVVApubkdsa3h0ZGlrcnExMXZleHd4SmlQTmhtaHFSVzNtVXVKRXpsbElkVkw2RW14R1lUcXBxZjkzSGxoa3NhZUowCjhVZ2pQOVVtTVlyaFZKdTFqY0ZXVjdmei9yKzIxL2F3VG5EVjlzTVlRcXVJUllZeTdiRzByMU9iaXdkb3ZudGsKN2dGSTA2WjB2WmFjREU1Ym9xVUNBd0VBQWFPQ0FTVXdnZ0VoTUFrR0ExVWRFd1FDTUFBd0N3WURWUjBQQkFRRApBZ1VnTUIwR0ExVWREZ1FXQkJTUk9OOEdKOG8rOGpnRnRqa3R3WmRxeDZCUnlUQVRCZ05WSFNVRUREQUtCZ2dyCkJnRUZCUWNEQVRBZEJnbGdoa2dCaHZoQ0FRMEVFQllPVkdWemRDQllOVEE1SUdObGNuUXdnYk1HQTFVZEl3U0IKcXpDQnFJQVVrVGpmQmlmS1B2STRCYlk1TGNHWGFzZWdVY21oZ1l5a2dZa3dnWVl4Q3pBSkJnTlZCQVlUQWtGVgpNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVEJsTjVaRzVsZVRFTU1Bb0dBMVVFQ2d3RFVFbFVNUWt3CkJ3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psYm1ObGNHbDBMbU52YlRFbE1DTUdDU3FHU0liM0RRRUoKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnZiWUlCQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9CZ1FDRQpUQWVKVERTQVc2ejFVRlRWN1FyZWg0VUxGT1JhajkrZUN1RjNLV0RIYyswSVFDajlyZG5ERzRRL3dmNy9yYVEwCkpuUFFDU0NkclBMSmV5b1BIN1FhVHdvYUY3ZHpWdzRMQ3N5TkpURld4NGNNNTBWdzZSNWZET2dpQzhic2ZmUzgKQkptb3VscnJaRE5OVmpHOG1XNmNMeHJZdlZRT3JSVmVjQ0ZJZ3NzQ2JBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
    +          
    +        
    +      
    +      
    +        
    +          
    +            LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURxekNDQXhTZ0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBRENCaGpFTE1Ba0dBMVVFQmhNQ1FWVXgKRERBS0JnTlZCQWdUQTA1VFZ6RVBNQTBHQTFVRUJ4TUdVM2xrYm1WNU1Rd3dDZ1lEVlFRS0RBTlFTVlF4Q1RBSApCZ05WQkFzTUFERVlNQllHQTFVRUF3d1BiR0YzY21WdVkyVndhWFF1WTI5dE1TVXdJd1lKS29aSWh2Y05BUWtCCkRCWnNZWGR5Wlc1alpTNXdhWFJBWjIxaGFXd3VZMjl0TUI0WERURXlNRFF4T1RJeU5UUXhPRm9YRFRNeU1EUXgKTkRJeU5UUXhPRm93Z1lZeEN6QUpCZ05WQkFZVEFrRlZNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVApCbE41Wkc1bGVURU1NQW9HQTFVRUNnd0RVRWxVTVFrd0J3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psCmJtTmxjR2wwTG1OdmJURWxNQ01HQ1NxR1NJYjNEUUVKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnYKYlRDQm56QU5CZ2txaGtpRzl3MEJBUUVGQUFPQmpRQXdnWWtDZ1lFQXFqaWUzUjJvaStwRGFldndJeXMvbWJVVApubkdsa3h0ZGlrcnExMXZleHd4SmlQTmhtaHFSVzNtVXVKRXpsbElkVkw2RW14R1lUcXBxZjkzSGxoa3NhZUowCjhVZ2pQOVVtTVlyaFZKdTFqY0ZXVjdmei9yKzIxL2F3VG5EVjlzTVlRcXVJUllZeTdiRzByMU9iaXdkb3ZudGsKN2dGSTA2WjB2WmFjREU1Ym9xVUNBd0VBQWFPQ0FTVXdnZ0VoTUFrR0ExVWRFd1FDTUFBd0N3WURWUjBQQkFRRApBZ1VnTUIwR0ExVWREZ1FXQkJTUk9OOEdKOG8rOGpnRnRqa3R3WmRxeDZCUnlUQVRCZ05WSFNVRUREQUtCZ2dyCkJnRUZCUWNEQVRBZEJnbGdoa2dCaHZoQ0FRMEVFQllPVkdWemRDQllOVEE1SUdObGNuUXdnYk1HQTFVZEl3U0IKcXpDQnFJQVVrVGpmQmlmS1B2STRCYlk1TGNHWGFzZWdVY21oZ1l5a2dZa3dnWVl4Q3pBSkJnTlZCQVlUQWtGVgpNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVEJsTjVaRzVsZVRFTU1Bb0dBMVVFQ2d3RFVFbFVNUWt3CkJ3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psYm1ObGNHbDBMbU52YlRFbE1DTUdDU3FHU0liM0RRRUoKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnZiWUlCQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9CZ1FDRQpUQWVKVERTQVc2ejFVRlRWN1FyZWg0VUxGT1JhajkrZUN1RjNLV0RIYyswSVFDajlyZG5ERzRRL3dmNy9yYVEwCkpuUFFDU0NkclBMSmV5b1BIN1FhVHdvYUY3ZHpWdzRMQ3N5TkpURld4NGNNNTBWdzZSNWZET2dpQzhic2ZmUzgKQkptb3VscnJaRE5OVmpHOG1XNmNMeHJZdlZRT3JSVmVjQ0ZJZ3NzQ2JBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
    +          
    +        
    +      
    +      
    +      urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
    +      urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
    +      urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
    +      
    +      
    +      
    +    
    +  
    +
    \ No newline at end of file
    diff --git a/tests/data/metadata/metadata_bad_order_settings1.xml b/tests/data/metadata/metadata_bad_order_settings1.xml
    index 1d1ff9d2..9d100de4 100644
    --- a/tests/data/metadata/metadata_bad_order_settings1.xml
    +++ b/tests/data/metadata/metadata_bad_order_settings1.xml
    @@ -4,7 +4,7 @@
                          cacheDuration="PT1594475551S"
                          entityID="http://stuff.com/endpoints/metadata.php">
         
    -        urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified
    +        urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
             
    diff --git a/tests/data/metadata/metadata_settings1.xml b/tests/data/metadata/metadata_settings1.xml
    index 449a8f01..c9529f00 100644
    --- a/tests/data/metadata/metadata_settings1.xml
    +++ b/tests/data/metadata/metadata_settings1.xml
    @@ -6,7 +6,7 @@
         
             
    -        urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified
    +        urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
             
    diff --git a/tests/data/metadata/no_expiration_mark_metadata.xml b/tests/data/metadata/no_expiration_mark_metadata.xml
    index 0d0b8085..92c42ba8 100644
    --- a/tests/data/metadata/no_expiration_mark_metadata.xml
    +++ b/tests/data/metadata/no_expiration_mark_metadata.xml
    @@ -4,7 +4,7 @@
         
             
    -        urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified
    +        urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
             
    diff --git a/tests/data/metadata/noentity_metadata_settings1.xml b/tests/data/metadata/noentity_metadata_settings1.xml
    index af1ff3eb..b773de92 100644
    --- a/tests/data/metadata/noentity_metadata_settings1.xml
    +++ b/tests/data/metadata/noentity_metadata_settings1.xml
    @@ -3,7 +3,7 @@
         
             MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOoMIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo
             
    -        urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified
    +        urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
             
        
     
    diff --git a/tests/data/metadata/testshib-providers.xml b/tests/data/metadata/testshib-providers.xml
    new file mode 100644
    index 00000000..47c2a873
    --- /dev/null
    +++ b/tests/data/metadata/testshib-providers.xml
    @@ -0,0 +1,306 @@
    +
    +
    +    
    +
    +    
    +        
    +        
    +            
    +            
    +            
    +            
    +            
    +            
    +            
    +            
    +        
    +
    +        
    +            
    +                testshib.org
    +                
    +                    TestShib Test IdP
    +                    TestShib IdP. Use this as a source of attributes
    +                        for your test SP.
    +                    https://www.testshib.org/testshibtwo.jpg
    +                
    +
    +            
    +            
    +                
    +                    
    +                        
    +                            MIIDAzCCAeugAwIBAgIVAPX0G6LuoXnKS0Muei006mVSBXbvMA0GCSqGSIb3DQEB
    +                            CwUAMBsxGTAXBgNVBAMMEGlkcC50ZXN0c2hpYi5vcmcwHhcNMTYwODIzMjEyMDU0
    +                            WhcNMzYwODIzMjEyMDU0WjAbMRkwFwYDVQQDDBBpZHAudGVzdHNoaWIub3JnMIIB
    +                            IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAg9C4J2DiRTEhJAWzPt1S3ryh
    +                            m3M2P3hPpwJwvt2q948vdTUxhhvNMuc3M3S4WNh6JYBs53R+YmjqJAII4ShMGNEm
    +                            lGnSVfHorex7IxikpuDPKV3SNf28mCAZbQrX+hWA+ann/uifVzqXktOjs6DdzdBn
    +                            xoVhniXgC8WCJwKcx6JO/hHsH1rG/0DSDeZFpTTcZHj4S9MlLNUtt5JxRzV/MmmB
    +                            3ObaX0CMqsSWUOQeE4nylSlp5RWHCnx70cs9kwz5WrflnbnzCeHU2sdbNotBEeTH
    +                            ot6a2cj/pXlRJIgPsrL/4VSicPZcGYMJMPoLTJ8mdy6mpR6nbCmP7dVbCIm/DQID
    +                            AQABoz4wPDAdBgNVHQ4EFgQUUfaDa2mPi24x09yWp1OFXmZ2GPswGwYDVR0RBBQw
    +                            EoIQaWRwLnRlc3RzaGliLm9yZzANBgkqhkiG9w0BAQsFAAOCAQEASKKgqTxhqBzR
    +                            OZ1eVy++si+eTTUQZU4+8UywSKLia2RattaAPMAcXUjO+3cYOQXLVASdlJtt+8QP
    +                            dRkfp8SiJemHPXC8BES83pogJPYEGJsKo19l4XFJHPnPy+Dsn3mlJyOfAa8RyWBS
    +                            80u5lrvAcr2TJXt9fXgkYs7BOCigxtZoR8flceGRlAZ4p5FPPxQR6NDYb645jtOT
    +                            MVr3zgfjP6Wh2dt+2p04LG7ENJn8/gEwtXVuXCsPoSCDx9Y0QmyXTJNdV1aB0AhO
    +                            RkWPlFYwp+zOyOIR+3m1+pqWFpn0eT/HrxpdKa74FA3R2kq4R7dXe4G0kUgXTdqX
    +                            MLRKhDgdmA==
    +                        
    +                    
    +                
    +                
    +                
    +                
    +                
    +                
    +                
    +            
    +
    +            
    +            
    +
    +            urn:mace:shibboleth:1.0:nameIdentifier
    +            urn:oasis:names:tc:SAML:2.0:nameid-format:transient
    +            
    +            
    +            
    +            
    +            
    +
    +        
    +
    +
    +        
    +
    +            
    +                
    +                    
    +                        
    +                            MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzEV
    +                            MBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMREwDwYD
    +                            VQQKEwhUZXN0U2hpYjEZMBcGA1UEAxMQaWRwLnRlc3RzaGliLm9yZzAeFw0wNjA4
    +                            MzAyMTEyMjVaFw0xNjA4MjcyMTEyMjVaMGcxCzAJBgNVBAYTAlVTMRUwEwYDVQQI
    +                            EwxQZW5uc3lsdmFuaWExEzARBgNVBAcTClBpdHRzYnVyZ2gxETAPBgNVBAoTCFRl
    +                            c3RTaGliMRkwFwYDVQQDExBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0B
    +                            AQEFAAOCAQ8AMIIBCgKCAQEArYkCGuTmJp9eAOSGHwRJo1SNatB5ZOKqDM9ysg7C
    +                            yVTDClcpu93gSP10nH4gkCZOlnESNgttg0r+MqL8tfJC6ybddEFB3YBo8PZajKSe
    +                            3OQ01Ow3yT4I+Wdg1tsTpSge9gEz7SrC07EkYmHuPtd71CHiUaCWDv+xVfUQX0aT
    +                            NPFmDixzUjoYzbGDrtAyCqA8f9CN2txIfJnpHE6q6CmKcoLADS4UrNPlhHSzd614
    +                            kR/JYiks0K4kbRqCQF0Dv0P5Di+rEfefC6glV8ysC8dB5/9nb0yh/ojRuJGmgMWH
    +                            gWk6h0ihjihqiu4jACovUZ7vVOCgSE5Ipn7OIwqd93zp2wIDAQABo4HEMIHBMB0G
    +                            A1UdDgQWBBSsBQ869nh83KqZr5jArr4/7b+QazCBkQYDVR0jBIGJMIGGgBSsBQ86
    +                            9nh83KqZr5jArr4/7b+Qa6FrpGkwZzELMAkGA1UEBhMCVVMxFTATBgNVBAgTDFBl
    +                            bm5zeWx2YW5pYTETMBEGA1UEBxMKUGl0dHNidXJnaDERMA8GA1UEChMIVGVzdFNo
    +                            aWIxGTAXBgNVBAMTEGlkcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zAN
    +                            BgkqhkiG9w0BAQUFAAOCAQEAjR29PhrCbk8qLN5MFfSVk98t3CT9jHZoYxd8QMRL
    +                            I4j7iYQxXiGJTT1FXs1nd4Rha9un+LqTfeMMYqISdDDI6tv8iNpkOAvZZUosVkUo
    +                            93pv1T0RPz35hcHHYq2yee59HJOco2bFlcsH8JBXRSRrJ3Q7Eut+z9uo80JdGNJ4
    +                            /SJy5UorZ8KazGj16lfJhOBXldgrhppQBb0Nq6HKHguqmwRfJ+WkxemZXzhediAj
    +                            Geka8nz8JjwxpUjAiSWYKLtJhGEaTqCYxCCX2Dw+dOTqUzHOZ7WKv4JXPK5G/Uhr
    +                            8K/qhmFT2nIQi538n6rVYLeWj8Bbnl+ev0peYzxFyF5sQA==
    +                        
    +                    
    +                
    +                
    +                
    +                
    +                
    +                
    +                
    +            
    +
    +
    +            
    +            
    +
    +            urn:mace:shibboleth:1.0:nameIdentifier
    +            urn:oasis:names:tc:SAML:2.0:nameid-format:transient
    +
    +        
    +
    +        
    +            TestShib Two Identity Provider
    +            TestShib Two
    +            http://www.testshib.org/testshib-two/
    +        
    +        
    +            Nate
    +            Klingenstein
    +            ndk@internet2.edu
    +        
    +    
    +
    +    
    +    
    +    
    +
    +    
    +
    +         
    +            
    +            
    +            
    +            
    +            
    +            
    +            
    +            
    +            
    +            
    +            
    +            
    +            
    +            
    +            
    +            
    +        
    +
    +        
    +        
    +        
    +
    +            
    +                
    +                
    + 
    +                
    +                    TestShib Test SP
    +                    TestShib SP. Log into this to test your machine.
    +                        Once logged in check that all attributes that you expected have been
    +                        released.
    +                    https://www.testshib.org/testshibtwo.jpg
    +                
    +            
    +
    +            
    +                
    +                    
    +                        
    +                            MIIEPjCCAyagAwIBAgIBADANBgkqhkiG9w0BAQUFADB3MQswCQYDVQQGEwJVUzEV
    +                            MBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMSIwIAYD
    +                            VQQKExlUZXN0U2hpYiBTZXJ2aWNlIFByb3ZpZGVyMRgwFgYDVQQDEw9zcC50ZXN0
    +                            c2hpYi5vcmcwHhcNMDYwODMwMjEyNDM5WhcNMTYwODI3MjEyNDM5WjB3MQswCQYD
    +                            VQQGEwJVUzEVMBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1
    +                            cmdoMSIwIAYDVQQKExlUZXN0U2hpYiBTZXJ2aWNlIFByb3ZpZGVyMRgwFgYDVQQD
    +                            Ew9zcC50ZXN0c2hpYi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
    +                            AQDJyR6ZP6MXkQ9z6RRziT0AuCabDd3x1m7nLO9ZRPbr0v1LsU+nnC363jO8nGEq
    +                            sqkgiZ/bSsO5lvjEt4ehff57ERio2Qk9cYw8XCgmYccVXKH9M+QVO1MQwErNobWb
    +                            AjiVkuhWcwLWQwTDBowfKXI87SA7KR7sFUymNx5z1aoRvk3GM++tiPY6u4shy8c7
    +                            vpWbVfisfTfvef/y+galxjPUQYHmegu7vCbjYP3On0V7/Ivzr+r2aPhp8egxt00Q
    +                            XpilNai12LBYV3Nv/lMsUzBeB7+CdXRVjZOHGuQ8mGqEbsj8MBXvcxIKbcpeK5Zi
    +                            JCVXPfarzuriM1G5y5QkKW+LAgMBAAGjgdQwgdEwHQYDVR0OBBYEFKB6wPDxwYrY
    +                            StNjU5P4b4AjBVQVMIGhBgNVHSMEgZkwgZaAFKB6wPDxwYrYStNjU5P4b4AjBVQV
    +                            oXukeTB3MQswCQYDVQQGEwJVUzEVMBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYD
    +                            VQQHEwpQaXR0c2J1cmdoMSIwIAYDVQQKExlUZXN0U2hpYiBTZXJ2aWNlIFByb3Zp
    +                            ZGVyMRgwFgYDVQQDEw9zcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zAN
    +                            BgkqhkiG9w0BAQUFAAOCAQEAc06Kgt7ZP6g2TIZgMbFxg6vKwvDL0+2dzF11Onpl
    +                            5sbtkPaNIcj24lQ4vajCrrGKdzHXo9m54BzrdRJ7xDYtw0dbu37l1IZVmiZr12eE
    +                            Iay/5YMU+aWP1z70h867ZQ7/7Y4HW345rdiS6EW663oH732wSYNt9kr7/0Uer3KD
    +                            9CuPuOidBacospDaFyfsaJruE99Kd6Eu/w5KLAGG+m0iqENCziDGzVA47TngKz2v
    +                            PVA+aokoOyoz3b53qeti77ijatSEoKjxheBWpO+eoJeGq/e49Um3M2ogIX/JAlMa
    +                            Inh+vYSYngQB2sx9LGkR9KHaMKNIGCDehk93Xla4pWJx1w== 
    +                        
    +                    
    +                
    +                
    +                
    +                
    +                
    +                
    +                
    +                
    +                
    +                
    +            
    +
    +            
    +
    +            
    +            
    +            
    +            
    +
    +
    +            
    +            urn:oasis:names:tc:SAML:2.0:nameid-format:transient
    +            urn:mace:shibboleth:1.0:nameIdentifier
    +
    +            
    +
    +            
    +            
    +            
    +            
    +            
    +            
    +
    +            
    +
    +            
    +            
    +
    +        
    +
    +        
    +        
    +            TestShib Two Service Provider
    +            TestShib Two
    +            http://www.testshib.org/testshib-two/
    +        
    +        
    +            Nate
    +            Klingenstein
    +            ndk@internet2.edu
    +        
    +
    +    
    +
    +
    +
    +
    diff --git a/tests/data/metadata/unparsed_metadata.xml b/tests/data/metadata/unparsed_metadata.xml
    index 6e6c567a..691824da 100644
    --- a/tests/data/metadata/unparsed_metadata.xml
    +++ b/tests/data/metadata/unparsed_metadata.xml
    @@ -6,7 +6,7 @@
         
             
    -        urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified
    +        urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
             
    diff --git a/tests/data/misc/sp4.key b/tests/data/misc/sp4.key
    new file mode 100644
    index 00000000..8be7be60
    --- /dev/null
    +++ b/tests/data/misc/sp4.key
    @@ -0,0 +1,28 @@
    +-----BEGIN PRIVATE KEY-----
    +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD4ZrcXcjCBOQS7
    +stUabuXPYnXKvcoJUrMVPRX1zfrXvpfghCrykbL1TKoqGfmEA9oNRoMBOmZCgLlK
    +eb0TfuEO/u1jf4rRFcK7U/dYEiX74bQgUnJUWTfFlhwPjxGhn9zDrc2tSpworJBV
    +amyBZIo5Beap5OJLote/Wqp1DZjNyEZ2m8m+lv8udmejmlo5RMoIzuG3VdH6ADC9
    +LKF+QsXC/HRZBhLE/y+75/XrNODvX8eM8+9Xp21QlVF1EIZDfNQ2iHsA8GEpJDC5
    +aomTW/xExBysejnwP2ROrfm3PIfP64EbB4G01f8eErlXeUD0oQ0gECgIXsJpfBkD
    +IWMHwx3/AgMBAAECggEAdbLNvFlJ7GDlAj75RJ4ZXAuOPrNw4LwDyON53U9tNP7F
    +HgfiBa/NuPdLhclq9geRMUsg1dsjCw3NPiGy2mL7JszaFJQhZXLHI1Xk1CE9SD0o
    +yUvniln/2CqJP0IOG6QQydM3qo24snkZpq9XnHPUHrLSGdwu8aHGUpAWRoJbzdzR
    +tBWBn6SlkuaE52vcGh7eMdKSICRCg2/gg6LIi89pkiI9tfozAL2LPcDTRGp3DA3w
    +U6OO8k+d1La4s9G0i22OGSwPxGerTHnBIzpeM/ivRwBypFy3EV9bbjQlheI53xAo
    +ZMmGeSnQ89MWgY64pnWrX862Mf1EZYTjumDe2dl1kQKBgQD9pBG2BbcQ8qieTf84
    +92LeOYTPRdd0N+gdyDKKorRO772zgxBwpSwO285nzy/FKSnpJIDtuee6OFClnDor
    +Ui6lG1WPQeoSEdH1V10XkfSaoFOz7Hyv9H2dCLvW/VO9KYq07VAmQcvNZnqIW+tI
    +edSHcQ3I8tnw4CiFa0BPvdhk9wKBgQD6tiuN2NvuNFFLvwpBGp3hjGyn6siyXDyP
    +8IXQmP66NxKqcX/NafVO3bVh6VrPGd7PL1PloQZ5EBG2PPtRdf/g4aeZKZleCUXm
    +9OgMEOUqdbTP9TGrmgNPtNBx3jnhnX/GTy/7GK77YlXEVplezWaerwRM7NCFCtp2
    +W6K1M961OQKBgQDDSznr2hirrvuP8GRMW4a/rrAI3DDZplZN4CCySDbm9IcvGgJl
    +iXgT9MDHg2q3t0sy3U18PYEkDEpkSZcsVfneXN6TEGCHCzuLWXovNM2O5VWtmrAi
    +1vCFIf1nuuRoKP1I89SbsFuYyogcSBIwWsX+h1ji2cJfSmlI2VzKSVW93wKBgQDA
    +sqwfRoMkP0oM8jUrfQ3Egm4xUiAYFxTlfXUcs7t13UaXgs08USifCYGUVAvcCoJa
    +tIHDiVS0UEmMzKpOHmghrM9oxbR/tpjnv21reMDrNbVX8ZnPz3ykEtHz816BrtC6
    +17qFQJ+d0CMj2XvghfdOGC8yAQL0fzcSqbQRmmCe4QKBgFWY9fqHEKdG/UlxZfBB
    +C/QRNTJsrbZf9Ok/o1h6BHnK64xUc4elShEwV9IdC4QNW0UCr7WXoGLUkhfUphId
    +q//KUDNc7VrWj5URsZcGi7WMkqNm9kPkpeuh3iSvh3+q7tK0/yfuj9ZQOjKzQnit
    +VZBooJAJGdSqYgitpyxB71/n
    +-----END PRIVATE KEY-----
    diff --git a/tests/data/responses/decrypted_valid_encrypted_assertion.xml b/tests/data/responses/decrypted_valid_encrypted_assertion.xml
    new file mode 100644
    index 00000000..0237994f
    --- /dev/null
    +++ b/tests/data/responses/decrypted_valid_encrypted_assertion.xml
    @@ -0,0 +1,7 @@
    +
    +    http://idp.example.com/
    +    
    +        
    +    
    +    http://idp.example.com/_68392312d490db6d355555cfbbd8ec95d746516f60http://stuff.com/endpoints/metadata.phpurn:oasis:names:tc:SAML:2.0:ac:classes:Passwordtesttest@example.comtestwaa2useradmin
    +    
    \ No newline at end of file
    diff --git a/tests/data/responses/invalids/duplicated_attributes.xml.base64 b/tests/data/responses/invalids/duplicated_attributes.xml.base64
    new file mode 100644
    index 00000000..a571b6d3
    --- /dev/null
    +++ b/tests/data/responses/invalids/duplicated_attributes.xml.base64
    @@ -0,0 +1 @@
    +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDQ0OTkyZWJiLTRiMzgtZTQzMi1kYjgyLTk5NTI0MTBkOWFhYiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMjFUMTM6NDI6MzFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzE5MWMwM2U2OGQ3MWQ5Nzk2ZjVlMDdlNjI2MmNhNGFkODgzYTc0YjEiPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDQ0OTkyZWJiLTRiMzgtZTQzMi1kYjgyLTk5NTI0MTBkOWFhYiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+Z3ZScnJneHBBZHlsSUEvMnNyRm1KZCtqaXM4PTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5LZHA4VDhybndQY0JVb2hjcVBNMGVpTlhwTWgzbGMrZXBIVERIcUxFbk9Kcmd1NS9qaitpN0VhQW1nTzBSSlRraERFWTBWOEZuZVQ0dm92Y0FiZzlmYk04ZlRPMWxYODJ3SW1zRWRxMkwzU0U4NHFCdWFDbURWNVlvMDdDSGJRT1FqYWV0VGt0SnVvRjA4QWQ2bCs1aFJPL3BKeG1yRXlHKzRLaWhGWUJ1dWs9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDgwYmFhZWY2LTI5MmItODc0Ny1jZmNhLWRlMWVlM2YxYTQxNSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMjFUMTM6NDI6MzFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng4MGJhYWVmNi0yOTJiLTg3NDctY2ZjYS1kZTFlZTNmMWE0MTUiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPmFSOU00ZXdOczN1K25KYVFDRDI2WjBBd0Q2TT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+NGQ4WEo1bXBOaW1vQkhkenNXZi9aemxVTlE3SmlVeEl4K1B5TjRuM0EvbWExcGwvQ0FPSUtOUzZ0clR6STg5N1ZjbGxneFhhTTljUFZqOUhLYU9aRW4wSE5Qa2FWR3VjeVVPVzFUd2dWdnJVdkNNQXVRTzdRZ21aekd1SVhsblVKS3FpTDRZMThNT1M1VGpLaExoSG4xbGE4TEFucmRVVEJobUx5eGtjZjhVPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6dHJhbnNpZW50Ij5fMjEyNmRkMTliOGE5YTI4MjM4ZDg4ZmRjNzM4NWU2MDk5NTAwNGE3NzgyPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIzLTA5LTIyVDE5OjAyOjMxWiIgUmVjaXBpZW50PSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fMTkxYzAzZTY4ZDcxZDk3OTZmNWUwN2U2MjYyY2E0YWQ4ODNhNzRiMSIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAzLTIxVDEzOjQyOjAxWiIgTm90T25PckFmdGVyPSIyMDIzLTA5LTIyVDE5OjAyOjMxWiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDMtMjFUMTM6NDE6MDlaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTAzLTIxVDIxOjQyOjMxWiIgU2Vzc2lvbkluZGV4PSJfZTY1NzhkNmFmOTdiOWY3ZjA2NzJkODUwZDI5ZGI0YWRkMWEyODZkYzI0Ij48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3Q8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0Mjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0QGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj53YWEyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+
    \ No newline at end of file
    diff --git a/tests/data/responses/invalids/empty_destination.xml.base64 b/tests/data/responses/invalids/empty_destination.xml.base64
    new file mode 100644
    index 00000000..352988ce
    --- /dev/null
    +++ b/tests/data/responses/invalids/empty_destination.xml.base64
    @@ -0,0 +1 @@
    +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDc2ZWY5MjAxLTY4OGItYzJkZC1mY2Q2LTQxMzEyNzE3ODk0OSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDc2ZWY5MjAxLTY4OGItYzJkZC1mY2Q2LTQxMzEyNzE3ODk0OSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+TVJEd3dSTXZtalQ1VEhLUTBCNWRUNDVBNWhNPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5wRFlrTFNKM2Z3TUQ0cnJNbWF4cFUwQkZVemZHQlVwNklURmovejNOTnFrQmdaTzdBMGIvQlFGbVBOQ202UE82NGdYNmVySGhhMVQ3aW5PTGRIY2crT0Q2Z2h2R0lpbGJzM1RjUkRwUmVTVkpZVWRiUS9jVk85aC9VdWNielBqZ3gyb3dpakk2aVh1dXhYcmpVeHEzYS9DbHcyTGJiVWJHMCtmQStud0ZuOVE9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDk0Y2Q5YTMzLWQyOWMtMTMyMi1kYzMzLTFkOGU0ZDJiNTQzNSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng5NGNkOWEzMy1kMjljLTEzMjItZGMzMy0xZDhlNGQyYjU0MzUiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPnBYMkV3c1pVVUdCTGhYSTBVOVVMc3d0S3hDYz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+TW5aVU04U0VmN3RVMlc1VGwvb0ZYTVBJYTZUVlcvUTczRmJUNUcxdW14eHZFRkM1UDlsR08reFVkdlBBTXdkTGc1aEN0R29QenB6amxCSnVFemhJU3VYblNZdkVCbllqdGJKVzcxcU9iM25WcTFjYVZtZXRhQjk4aUZzTDFvS0FWTVZ0Q0VST2E1SFpoT3VtQWJONU5qeHYvcUJlYW1lK0ExaStjV0FNaW13PTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==
    \ No newline at end of file
    diff --git a/tests/data/responses/invalids/empty_nameid.xml.base64 b/tests/data/responses/invalids/empty_nameid.xml.base64
    new file mode 100644
    index 00000000..72350d1f
    --- /dev/null
    +++ b/tests/data/responses/invalids/empty_nameid.xml.base64
    @@ -0,0 +1 @@
    +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDQ0MTM5Y2JkLWE2NTQtOWM1Mi00Njk3LTdjMDVkMzAyM2QyZiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDQ0MTM5Y2JkLWE2NTQtOWM1Mi00Njk3LTdjMDVkMzAyM2QyZiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+VEVFTFhxT0tmZVRqSFI5aUhPb2hrQWlCSDVVPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT51ZW1SeWgyQkcyTXBsbG5kWFNsV0tiaEgzZTRNQVd0VHNJYS9waWJndXZaRmhSTTVJNzUrRkFxYkl4UFVoWDlGYjlOTWRVRzdacWJJS2J0aitLZGxCdVlYaDdTdEIyQWMwY1VzamFQTHVLa2RTc0IzUzdESXFYRThmcEdNeHBSblNNZDZWc1RXM2RId3FYaTJiZklYblBDM0N0RjMwWUhXditwR081MFpCcjg9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDVhMTU1NmIwLTE1NmYtZjNhNS04OGUyLTc1MzRkNjdiNjg0MyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng1YTE1NTZiMC0xNTZmLWYzYTUtODhlMi03NTM0ZDY3YjY4NDMiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPnhpTEtIa05OcllPWTdWOFhkSjVET3pQNFp0ND08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+WHZDRURGdDBJM1VXWlMwN3JWa1VmNTA0Mjg3ZHJTbEI2bDBSdS9OTWMzZFlIT2E1V0NCNXZRanpGVURMSFZSQWlueWR0WXh3ejRTN1NKd081V3RKVFdTOStQNU9SMnpRTjRpYVpnclVGRm5xV0FDZW4rUTMzaXZVaFY0elVTcDU0cjVVdUxLNE96UnVhNmhlWUYrM0Y5TXZMK3VPV2hFZVc3NXZjODk0VXlVPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIi8+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxNC0wMi0xOVQwOTozNzowMVoiIFNlc3Npb25JbmRleD0iXzYyNzNkNzdiOGNkZTBjMzMzZWM3OWQyMmE5ZmEwMDAzYjlmZTJkNzVjYiI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNtYXJ0aW5AeWFjby5lczwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+U2l4dG8zPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5NYXJ0aW4yPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+
    \ No newline at end of file
    diff --git a/tests/data/responses/invalids/no_authnstatement.xml.base64 b/tests/data/responses/invalids/no_authnstatement.xml.base64
    new file mode 100644
    index 00000000..d116b7b5
    --- /dev/null
    +++ b/tests/data/responses/invalids/no_authnstatement.xml.base64
    @@ -0,0 +1 @@
    +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGRmNWRiN2JiLTYwZDgtMWZhNi00OTBhLWFjMWMyZThjYWFhMiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeGRmNWRiN2JiLTYwZDgtMWZhNi00OTBhLWFjMWMyZThjYWFhMiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+bGJTZmtFR0JsNmZEN0JBc1prU25wYmQyNGJFPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT56Y3YwSitsZ0V4R2tjSVVKYVp5ajdvZkFrY1VZc3dvckpiei9xdEo0WDBmSEtMYXB1eE0xYmlEbnJMTm5wUXhNSkJ3K092WG9sdWdHdVZBeEVyYmE5NTV2QlFtQTRCZXRZS0tKR09XcTkyMWpxKzVhdThtOWQzM2M1UTR6cDYzZld4UnRKV3AyVU05UnZ0aWd6enk2WWg0SE5yNVNkdUhzd1FJeFM2ZEQ2Lzg9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGI0ZWM5YzhhLTQ4ZWItZmRhMi03Zjc0LWZhMWExMDVhOTlmZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj40OTI4ODI2MTVhY2YzMWM4MDk2YjYyNzI0NWQ3NmFlNTMwMzZjMDkwPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiIgUmVjaXBpZW50PSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNWZlOWQ2ZTQ5OWIyZjA5MTMyMDZhYWIzZjcxOTE3MjkwNDliYjgwNyIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAyLTE5VDAxOjM2OjMxWiIgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==
    \ No newline at end of file
    diff --git a/tests/data/responses/invalids/no_conditions.xml.base64 b/tests/data/responses/invalids/no_conditions.xml.base64
    new file mode 100644
    index 00000000..4b73a83e
    --- /dev/null
    +++ b/tests/data/responses/invalids/no_conditions.xml.base64
    @@ -0,0 +1 @@
    +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGQyMjJkZWI1LTZkMjktNWFiZC05NmM0LWFlOTk5ODZhYmVkNSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeGQyMjJkZWI1LTZkMjktNWFiZC05NmM0LWFlOTk5ODZhYmVkNSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+Sm03Qm5JTEJ3V2h2TW1ZTjd4WG01dDR0ZEZVPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5XRGw3K2RMclN4Ym95bTNWZzBXWTBrLzVDNWZxaDNNUWZPcXQraExDTXMwKzl3ekY4SHduWlJwLzRCMlJGOVBiUVAzc1d6VUY5QWNWeUErUFM4bU5aUnRzRzN4amFabE5BMWV3ZlQ3blFHZ1EvUkxLckhHeW9Bc3VaT0pLTDNqVjJiOGFSTE8rdSsrcmdoZUZSWm1wTkxVanBFTkdFZ3ZWc3ptcGN5aHFCd2c9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGI0ZWM5YzhhLTQ4ZWItZmRhMi03Zjc0LWZhMWExMDVhOTlmZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj40OTI4ODI2MTVhY2YzMWM4MDk2YjYyNzI0NWQ3NmFlNTMwMzZjMDkwPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiIgUmVjaXBpZW50PSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNWZlOWQ2ZTQ5OWIyZjA5MTMyMDZhYWIzZjcxOTE3MjkwNDliYjgwNyIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==
    \ No newline at end of file
    diff --git a/tests/data/responses/invalids/no_issuer_assertion.xml.base64 b/tests/data/responses/invalids/no_issuer_assertion.xml.base64
    new file mode 100644
    index 00000000..46094a6f
    --- /dev/null
    +++ b/tests/data/responses/invalids/no_issuer_assertion.xml.base64
    @@ -0,0 +1 @@
    +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDBlNmM5NjUzLTEwNjgtYzhjNS1iNzVjLWU2OTA1ZTE0M2Q0NCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDBlNmM5NjUzLTEwNjgtYzhjNS1iNzVjLWU2OTA1ZTE0M2Q0NCI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+KzhLSEl6dHh6SXNzMzNZZzlzRTVjTDlBRFpBPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5yK21CNi9NU3pSS0VGNi9NZGY4M29QeE9ZelFWQ2IvUVIvNlVieG10cmVqbnRFRnN2ZFZSckhmMmd5TUUyZTBGd21ta3JQbEtzcHl2ZDhXbVN2ckV0T0pZaERLRWRYUThtUnRmZWgvY1M4M3pFYmRGSG9ubTd2YkJiU2VxSDBIN2g3S1UxSStqeEwyZVRpQWlubkpHeWhhVHNmaVAxNzdXZmlXVmQ4SHBOY289PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGI0ZWM5YzhhLTQ4ZWItZmRhMi03Zjc0LWZhMWExMDVhOTlmZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxNC0wMi0xOVQwOTozNzowMVoiIFNlc3Npb25JbmRleD0iXzYyNzNkNzdiOGNkZTBjMzMzZWM3OWQyMmE5ZmEwMDAzYjlmZTJkNzVjYiI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNtYXJ0aW5AeWFjby5lczwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+U2l4dG8zPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5NYXJ0aW4yPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+
    \ No newline at end of file
    diff --git a/tests/data/responses/invalids/no_issuer_response.xml.base64 b/tests/data/responses/invalids/no_issuer_response.xml.base64
    new file mode 100644
    index 00000000..0e498d44
    --- /dev/null
    +++ b/tests/data/responses/invalids/no_issuer_response.xml.base64
    @@ -0,0 +1 @@
    +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZnhmMTA1MTkwNy0wZDZjLWI0NjctZjBiNC1kMDI4YTU4ZjNmNzIiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgRGVzdGluYXRpb249Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl81ZmU5ZDZlNDk5YjJmMDkxMzIwNmFhYjNmNzE5MTcyOTA0OWJiODA3Ij48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpBc3NlcnRpb24geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiBJRD0icGZ4NGFhZGFlMTQtMmY5MC0xZDI1LWJlOTAtYjdjMzI3NzdkODU5IiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDRhYWRhZTE0LTJmOTAtMWQyNS1iZTkwLWI3YzMyNzc3ZDg1OSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+T1R6Slg2cmNnUXdnM3dsOEZGMUZkUWFYY1QwPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5GQlRWMXVGVk1WQ0NXelNvdTFxK3kvMzRZVVp1RnlLUzFyaktEREV0aHNVV0ZnVU10S3pQcU9VOFc2enN2MmdZaG0xQ09qd01yenFZUG5WTGViWmtQZ0VNYUlRZW9DR1M0M0pqYllzWk9sakgxZWo5Z3Z6SDM3NHBZMUd6UUx1QXllYmxlL3B4ZmZSMEY5NklYbnFjbjFySnJQM1puR0k1RGcxV3BpbVphWTQ9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWw6U3ViamVjdD48c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocCIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPjQ5Mjg4MjYxNWFjZjMxYzgwOTZiNjI3MjQ1ZDc2YWU1MzAzNmMwOTA8L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDgtMjNUMDY6NTc6MDFaIiBSZWNpcGllbnQ9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl81ZmU5ZDZlNDk5YjJmMDkxMzIwNmFhYjNmNzE5MTcyOTA0OWJiODA3Ii8+PC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+PC9zYW1sOlN1YmplY3Q+PHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTQtMDItMTlUMDE6MzY6MzFaIiBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDgtMjNUMDY6NTc6MDFaIj48c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjxzYW1sOkF1ZGllbmNlPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==
    \ No newline at end of file
    diff --git a/tests/data/responses/invalids/signature_wrapping_attack.xml.base64 b/tests/data/responses/invalids/signature_wrapping_attack.xml.base64
    new file mode 100644
    index 00000000..dc2c9ca9
    --- /dev/null
    +++ b/tests/data/responses/invalids/signature_wrapping_attack.xml.base64
    @@ -0,0 +1 @@
    +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZnhjM2QyYjU0Mi0wZjdlLTg3NjctOGU4Ny01YjBkYzY5MTMzNzUiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTIxVDEzOjQxOjA5WiIgRGVzdGluYXRpb249Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl81ZDllMzE5YzFiOGE2N2RhNDgyMjc5NjRjMjhkMjgwZTc4NjBmODA0Ij48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1scDpTdGF0dXM+PHNhbWxwOlN0YXR1c0RldGFpbD48c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzZDJiNTQyLTBmN2UtODc2Ny04ZTg3LTViMGRjNjkxMzM3NSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMjFUMTM6NDE6MDlaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVkOWUzMTljMWI4YTY3ZGE0ODIyNzk2NGMyOGQyODBlNzg2MGY4MDQiPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeGMzZDJiNTQyLTBmN2UtODc2Ny04ZTg3LTViMGRjNjkxMzM3NSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+MWRRRmlZVTBvMk9GN2MvUlZWOEdwZ2I0dTNJPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT53UmdCWE9xL0ZpTFpjMm11cmVUQy9qNnpZNzA5T2lrSjVIZVVTcnVIVGRZakVnOWFaeTFSYnhsS0lZRUlmWHBuWDdOQm9LeGZBTW0rTzBmc3JxT2pnY1l4VFZrcVpqT3I3MXFpWE5idHdqZUFrZFlTcGs1YnJzQWNuZmNQZHY4UVJlWXIzRDd0NVpWQ2dZdXZYUStkTkVMS2VhZzdlMUFTT3pWcU9kcDVaOVk9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9Il9jY2NkNjAyNDExNjY0MWZlNDhlMGFlMmM1MTIyMGQwMjc1NWY5NmM5OGQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTIxVDEzOjQxOjA5WiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnRyYW5zaWVudCI+X2I5OGY5OGJiMWFiNTEyY2VkNjUzYjU4YmFhZmY1NDM0NDhkYWVkNTM1ZDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOS0yMlQxOTowMTowOVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVkOWUzMTljMWI4YTY3ZGE0ODIyNzk2NGMyOGQyODBlNzg2MGY4MDQiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMy0yMVQxMzo0MDozOVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOS0yMlQxOTowMTowOVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAzLTIxVDEzOjQxOjA5WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxNC0wMy0yMVQyMTo0MTowOVoiIFNlc3Npb25JbmRleD0iXzlmZTBjOGRjZDMzMDJlNzM2NGZjYWIyMmE1Mjc0OGViZjIyMjRkZjBhYSI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3RAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3Q8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ic24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPndhYTI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iZWR1UGVyc29uQWZmaWxpYXRpb24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+YWRtaW48L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT48L3NhbWxwOlN0YXR1c0RldGFpbD48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9Il9jY2NkNjAyNDExNjY0MWZlNDhlMGFlMmM1MTIyMGQwMjc1NWY5NmM5OGQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTIxVDEzOjQxOjA5WiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnRyYW5zaWVudCI+X2I5OGY5OGJiMWFiNTEyY2VkNjUzYjU4YmFhZmY1NDM0NDhkYWVkNTM1ZDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOS0yMlQxOTowMTowOVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVkOWUzMTljMWI4YTY3ZGE0ODIyNzk2NGMyOGQyODBlNzg2MGY4MDQiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMy0yMVQxMzo0MDozOVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOS0yMlQxOTowMTowOVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAzLTIxVDEzOjQxOjA5WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAyMy0wMy0yMVQyMTo0MTowOVoiIFNlc3Npb25JbmRleD0iXzlmZTBjOGRjZDMzMDJlNzM2NGZjYWIyMmE1Mjc0OGViZjIyMjRkZjBhYSI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5oYWNrZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+aGFja2VyQGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5oYWNrZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ic24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPndhYTI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iZWR1UGVyc29uQWZmaWxpYXRpb24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+YWRtaW48L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4=
    \ No newline at end of file
    diff --git a/tests/data/responses/invalids/signed_assertion_response.xml.base64 b/tests/data/responses/invalids/signed_assertion_response.xml.base64
    new file mode 100644
    index 00000000..b169ac9c
    --- /dev/null
    +++ b/tests/data/responses/invalids/signed_assertion_response.xml.base64
    @@ -0,0 +1 @@
    +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIERlc3RpbmF0aW9uPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIElEPSJwZnhkYjRkOWVmZS1kMGFkLTAwODYtY2U4OC1jMjg4Njg3Y2FjNjEiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNjEyYmJmOWIxNjQ1Mjk0YWEwYjQ2MzdiMWJjNWYzOWRlOGI3OWNlYiIgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTMxVDAwOjM3OjE2WiIgVmVyc2lvbj0iMi4wIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZnhkYjRkOWVmZS1kMGFkLTAwODYtY2U4OC1jMjg4Njg3Y2FjNjEiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPmpjMklRWFNoc3dzTG85TkdJSHp2cGtBaXY4ND08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+aUVqR2QrdWFqSVArYU9ucGo4MjYxUzRBaWdMeXJqc0pheTJzdVFKakhhVHlETlh4TFhWQ3AxZG1PR0JhZGhmRUtnWVJsaTFBZDA1QktBejlpd3NBME14OGZ6SmFhSlBUbHM2NS93ODZTSEN4NTdrNXhteDBSUjhuR09MOU1vb2lidnZWeTVRODl2Z2lnVWN5cWJUY0dxaU5uSVNCWGZuYVR2dnpQYS9QbWJ3PTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNWekNDQWNBQ0NRRElWSGFOU0JZTDZUQU5CZ2txaGtpRzl3MEJBUXNGQURCd01Rc3dDUVlEVlFRR0V3SkdVakVPTUF3R0ExVUVDQXdGVUdGeWFYTXhEakFNQmdOVkJBY01CVkJoY21sek1SWXdGQVlEVlFRS0RBMU9iM1poY0c5emRDQlVSVk5VTVNrd0p3WUpLb1pJaHZjTkFRa0JGaHBtYkc5eVpXNTBMbkJwWjI5MWRFQnViM1poY0c5emRDNW1jakFlRncweE5EQXlNVE14TXpVek5EQmFGdzB4TlRBeU1UTXhNelV6TkRCYU1IQXhDekFKQmdOVkJBWVRBa1pTTVE0d0RBWURWUVFJREFWUVlYSnBjekVPTUF3R0ExVUVCd3dGVUdGeWFYTXhGakFVQmdOVkJBb01EVTV2ZG1Gd2IzTjBJRlJGVTFReEtUQW5CZ2txaGtpRzl3MEJDUUVXR21ac2IzSmxiblF1Y0dsbmIzVjBRRzV2ZG1Gd2IzTjBMbVp5TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FDaExGSG4zTG5ONEpRLzdXQ2RZdXB4a1VnY05PUW5QRit5bGwrL0RQcHV4OW5wZlkwNTlQSVVhdEI4WDdrQ241aTh0UndJeS9pa0hKUjZNcjgrTVB2YzZWT1pEeFBOZFp2TW8vOGxoeHJiTjNKZHJ3M3doWm1VL0tQUjlGM0JkRmR1K1NMenJNbDFURFVabFB0WTlYelVGWGNxTjhJWGN5OFRKekNCZU5leTNRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkN3VUFBNEdCQUN0SjhmZUd6ZTFOSEI1VncxOGpNVVB2SG83SDNHd21qNlpEQVhRbGFpQVhNdU5CeE5YVldWd2lmbDZWK25XM3c5UWE3RmVvL25aL080VFVPSDFueithZGtsY0NENFFwWmFFSWJtQWJyaVBXSktnYjRMV0docVFydXdZUjdJdFRSMU1OWDlnTGJQMHowenZERVFubnQvVlVXRkVCTFNKcTRaNE5yZThMRm1TMjwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1scDpTdGF0dXM+PHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbHA6U3RhdHVzPjxzYW1sOkFzc2VydGlvbiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIElEPSJwZng3ZTNmMWYxMS0zZDM4LTdkYTUtNTVlZC05YjRkNmMwYTQ0ZWIiIElzc3VlSW5zdGFudD0iMjAxNC0wMy0zMVQwMDozNzoxNloiIFZlcnNpb249IjIuMCI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPgogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPgogIDxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4N2UzZjFmMTEtM2QzOC03ZGE1LTU1ZWQtOWI0ZDZjMGE0NGViIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPjxkczpEaWdlc3RWYWx1ZT42d1dzemxmRllidGRzNnR5K24rT3RESnZLRUE9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPmVVRTkxaFA2bTZ3VlVtd0liVkpTZnhWdkppOVFwd3QwZGpIUDRpcW5yMk42Y2ZWVmV3eERVM0dXQTlsOVpWanltV292RkltL1k0dGR3VTM0R2RiaS8yaWhvMmd0OGVWR3c4ajNSdVFoTVVIc1ZmK2hIaDJlSDhuMHhqZEFqdGRoTkhIT3pMMnREV3hYazg2T2VZbmw4Slp1VTdCRUVTZUtlQzlieDBPUW5ZTT08L2RzOlNpZ25hdHVyZVZhbHVlPgo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDVnpDQ0FjQUNDUURJVkhhTlNCWUw2VEFOQmdrcWhraUc5dzBCQVFzRkFEQndNUXN3Q1FZRFZRUUdFd0pHVWpFT01Bd0dBMVVFQ0F3RlVHRnlhWE14RGpBTUJnTlZCQWNNQlZCaGNtbHpNUll3RkFZRFZRUUtEQTFPYjNaaGNHOXpkQ0JVUlZOVU1Ta3dKd1lKS29aSWh2Y05BUWtCRmhwbWJHOXlaVzUwTG5CcFoyOTFkRUJ1YjNaaGNHOXpkQzVtY2pBZUZ3MHhOREF5TVRNeE16VXpOREJhRncweE5UQXlNVE14TXpVek5EQmFNSEF4Q3pBSkJnTlZCQVlUQWtaU01RNHdEQVlEVlFRSURBVlFZWEpwY3pFT01Bd0dBMVVFQnd3RlVHRnlhWE14RmpBVUJnTlZCQW9NRFU1dmRtRndiM04wSUZSRlUxUXhLVEFuQmdrcWhraUc5dzBCQ1FFV0dtWnNiM0psYm5RdWNHbG5iM1YwUUc1dmRtRndiM04wTG1aeU1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRQ2hMRkhuM0xuTjRKUS83V0NkWXVweGtVZ2NOT1FuUEYreWxsKy9EUHB1eDlucGZZMDU5UElVYXRCOFg3a0NuNWk4dFJ3SXkvaWtISlI2TXI4K01QdmM2Vk9aRHhQTmRadk1vLzhsaHhyYk4zSmRydzN3aFptVS9LUFI5RjNCZEZkdStTTHpyTWwxVERVWmxQdFk5WHpVRlhjcU44SVhjeThUSnpDQmVOZXkzUUlEQVFBQk1BMEdDU3FHU0liM0RRRUJDd1VBQTRHQkFDdEo4ZmVHemUxTkhCNVZ3MThqTVVQdkhvN0gzR3dtajZaREFYUWxhaUFYTXVOQnhOWFZXVndpZmw2VituVzN3OVFhN0Zlby9uWi9PNFRVT0gxbnorYWRrbGNDRDRRcFphRUlibUFicmlQV0pLZ2I0TFdHaHFRcnV3WVI3SXRUUjFNTlg5Z0xiUDB6MHp2REVRbm50L1ZVV0ZFQkxTSnE0WjROcmU4TEZtUzI8L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnRyYW5zaWVudCIgU1BOYW1lUXVhbGlmaWVyPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocCI+XzNhZjYyZjFkMDM1MTNiZGQ2MWRkNWJmMDRkM2RlYjdhYTYxNzQ4MGUyMjwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNjEyYmJmOWIxNjQ1Mjk0YWEwYjQ2MzdiMWJjNWYzOWRlOGI3OWNlYiIgTm90T25PckFmdGVyPSIyMDIzLTEwLTAyVDA1OjU3OjE2WiIgUmVjaXBpZW50PSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMy0zMVQwMDozNjo0NloiIE5vdE9uT3JBZnRlcj0iMjAyMy0xMC0wMlQwNTo1NzoxNloiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAzLTMxVDAwOjM3OjE2WiIgU2Vzc2lvbkluZGV4PSJfODVlN2NmZTE2ZDZlN2U2MDBiZDk4YmJjMmI0MzcxZTFjNjk1ODhhNGRhIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTAzLTMxVDA4OjM3OjE2WiI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4=
    diff --git a/tests/data/responses/invalids/wrong_spnamequalifier.xml.base64 b/tests/data/responses/invalids/wrong_spnamequalifier.xml.base64
    new file mode 100644
    index 00000000..48e1fbff
    --- /dev/null
    +++ b/tests/data/responses/invalids/wrong_spnamequalifier.xml.base64
    @@ -0,0 +1 @@
    +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDEzNzFkZmU1LTdlMmYtMTdiNy1hODE1LTVmNWU5YTRiNjVkYSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDEzNzFkZmU1LTdlMmYtMTdiNy1hODE1LTVmNWU5YTRiNjVkYSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+c3lrMGNHOHAxOGpSMUtINThKNnR3Z0JNYlhzPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT4yaytTbmhjWWhSV0FLQWdLR2hyMVpZN1ZsSWtWSytsbEFKZncxVnllKzRkWFpZTGh0TkwrMUJ3bWRlVHlLY1BEYjNWSmZtNXRzRGFWRDNtTHVUd2E5L0EvUCt0ZnY0d0t6YVQrdmJvTDl0RVFnNFAwR3hmaWRzbkYrQUM0Z2lLb0VBRE5RbmlaamExR1hhM3VOdi85TEVYV1h4YmZaVGJPNmxLNnlhbGR6UkU9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGI0ZWM5YzhhLTQ4ZWItZmRhMi03Zjc0LWZhMWExMDVhOTlmZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0id3Jvbmctc3AtZW50aXR5aWQiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj50ZXN0QGV4YW1wbGUuY29tPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiIgUmVjaXBpZW50PSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNWZlOWQ2ZTQ5OWIyZjA5MTMyMDZhYWIzZjcxOTE3MjkwNDliYjgwNyIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAyLTE5VDAxOjM2OjMxWiIgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTAyLTE5VDA5OjM3OjAxWiIgU2Vzc2lvbkluZGV4PSJfNjI3M2Q3N2I4Y2RlMGMzMzNlYzc5ZDIyYTlmYTAwMDNiOWZlMmQ3NWNiIj48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNtYXJ0aW48L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdEBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+U2l4dG8zPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5NYXJ0aW4yPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+
    \ No newline at end of file
    diff --git a/tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml b/tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml
    new file mode 100644
    index 00000000..fbc5942f
    --- /dev/null
    +++ b/tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml
    @@ -0,0 +1,7 @@
    +
    +    http://idp.example.com/
    +    
    +        
    +    
    +    http://idp.example.com/_68392312d490db6d355555cfbbd8ec95d746516f60http://stuff.com/endpoints/metadata.phpurn:oasis:names:tc:SAML:2.0:ac:classes:Passwordtesttest@example.comtestwaa2useradmin
    +    
    diff --git a/tests/data/responses/pretty_signed_message_response.xml b/tests/data/responses/pretty_signed_message_response.xml
    new file mode 100644
    index 00000000..7dcb65ee
    --- /dev/null
    +++ b/tests/data/responses/pretty_signed_message_response.xml
    @@ -0,0 +1,48 @@
    +
    +  https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php
    +  
    +  
    +    
    +  1dQFiYU0o2OF7c/RVV8Gpgb4u3I=wRgBXOq/FiLZc2mureTC/j6zY709OikJ5HeUSruHTdYjEg9aZy1RbxlKIYEIfXpnX7NBoKxfAMm+O0fsrqOjgcYxTVkqZjOr71qiXNbtwjeAkdYSpk5brsAcnfcPdv8QReYr3D7t5ZVCgYuvXQ+dNELKeag7e1ASOzVqOdp5Z9Y=
    +MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo
    +  
    +    
    +  
    +  
    +    https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php
    +    
    +      _b98f98bb1ab512ced653b58baaff543448daed535d
    +      
    +        
    +      
    +    
    +    
    +      
    +        https://pitbulk.no-ip.org/newonelogin/demo1/metadata.php
    +      
    +    
    +    
    +      
    +        urn:oasis:names:tc:SAML:2.0:ac:classes:Password
    +      
    +    
    +    
    +      
    +        test
    +      
    +      
    +        test@example.com
    +      
    +      
    +        test
    +      
    +      
    +        waa2
    +      
    +      
    +        user
    +        admin
    +      
    +    
    +  
    +
    diff --git a/tests/data/responses/response_encrypted_nameid.xml.base64 b/tests/data/responses/response_encrypted_nameid.xml.base64
    index d95040d0..d3bcbb6e 100644
    --- a/tests/data/responses/response_encrypted_nameid.xml.base64
    +++ b/tests/data/responses/response_encrypted_nameid.xml.base64
    @@ -1 +1 @@
    -PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZng4NDMwYjFkZS1mNTU0LTQ5M2YtNjMyZS00YTIxZTBhMGRkZDQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTA5VDEyOjIzOjM3WiIgRGVzdGluYXRpb249Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl9iZjM3MmI5ZDY3ZDBjODlkMGNmMWFmM2ZmNjI1ZWE3YzA1MWM5ODg1Ij48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng4NDMwYjFkZS1mNTU0LTQ5M2YtNjMyZS00YTIxZTBhMGRkZDQiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPkhENDgxVEsxVmtPMGVaNGUxL2Vma1VJTVVCaz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+bWZrcnh5SlU4MG1hMWwwQ2xUU2tObTgzajVZQ0Y3YUt2bDJqRmVsVU04M3RuaitwNDB1RXRITjkrbHBJQVgrZEZkNnNCR3JEdUk0Q21lOTZuQ2lDK05mb2Y5MnpYaUcrWExCbFR2K0Z0cWxaZUgxc0ZNTFhuOWwxTHNLT0dPbjM5alA4bklUSGtYL0VkYjRucENNZlpQeWZ2b3M1dkMwbUtnWlFKZ1JtcG5vPTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGQwOGM5NTY3LWU3NTEtNjY4Mi0wYTBhLWE0NTBkNmYxNTFlMCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZnhkMDhjOTU2Ny1lNzUxLTY2ODItMGEwYS1hNDUwZDZmMTUxZTAiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPjZqVkYxVkQwYnBVbUFWbURHOUt4bGoyYU5RMD08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+VTdhN0VVZ3F4MWJEL1lpb25OV09VZUwwdElHR1FJTlp3VXo3UUE4M2FibGVWcTl0a0ptSy9abzBqTXQyeW5YeDF6UDgrQzVuM0NFNjhOdnhqd3lLZEVBQTFSMDBuWmd4NExHQmdlaXZZUGtrNEFvSnpDREFsTXUzVUt6VnRJT3R1NXVWMU0yZUZzQ0ljZFRxcFEwa3ZvalVBRVlZWE5tK3pjbGRydVlYZDV3PTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWw6U3ViamVjdD48c2FtbDpFbmNyeXB0ZWRJRD48eGVuYzpFbmNyeXB0ZWREYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyIgeG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyIgVHlwZT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjRWxlbWVudCI+PHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI2FlczEyOC1jYmMiLz48ZHNpZzpLZXlJbmZvIHhtbG5zOmRzaWc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjx4ZW5jOkVuY3J5cHRlZEtleT48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjcnNhLTFfNSIvPjx4ZW5jOkNpcGhlckRhdGE+PHhlbmM6Q2lwaGVyVmFsdWU+RjJQVjV4TjVFM3EzODFJV2Qya25CdjAvUEpPN1ExR01oMGhkUm1USUxSYnJGZ0toNlFtN2VoTFlBS1B5QW52b0lFRDZSK0g4VkhweEtsZ2lybm9xNWYvSUxCajhOV1FpOEFKV21qZkMyWTBxQ3duOHJxVFRWQzVlZFhFZUMzU094ZHBTbzNzWGlOakhKUms3VndRUEV5RXNNeWcvSkN1cytGeUJYbXRickMwPTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHNpZzpLZXlJbmZvPgogICA8eGVuYzpDaXBoZXJEYXRhPgogICAgICA8eGVuYzpDaXBoZXJWYWx1ZT5sbjMrVTFWTTJjQ0I1TjF3SHJnM2llTDM0VEg3RXQvRGVYbytBTlJHRE52OXhtRk9zdWFKbmcwdS9XbHl6RjRNdHFsaWViRGVDQjVTWVRPRURxNFRsVVJ6NkFYSHBINVEzWWthK25rbWV5YWMrVnNpcjRwenlwblJoL29MeWRRV3dNcUlmTUIwM3hrc3l4SVM1Qk5kSzRjYnpaa3Z0bGtYL0ZrRnZUQnJLUzBsU1A4M2VlV2l4STRNell2QTVtK1R1SXpjRXQrUUdvc0FQZ0pCdERITEdYaUlhenRSWGFCSzV0eUxLcDdCMnVHVytWY2JaUTVmZVdlUTVtQ0p4WFBYYjJ0OXNMQ1ExRUVBQ2dOQitJMGFiejFhS1l4TitFandQd2pDVVFaZnFnYjlVY0xQRXBDanFzckdEMFpPSWxQYjJRcmJjNHFWc2NMaUlWRDZvaWxGWFM5QzhzbTA3dmJJTzA3SUhoU1ovRm9jK3AzbCsvQVRVb0I4eTc1SEp2TzE8L3hlbmM6Q2lwaGVyVmFsdWU+CiAgIDwveGVuYzpDaXBoZXJEYXRhPgo8L3hlbmM6RW5jcnlwdGVkRGF0YT48L3NhbWw6RW5jcnlwdGVkSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOS0xMFQxNzo0MzozN1oiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOX2JmMzcyYjlkNjdkMGM4OWQwY2YxYWYzZmY2MjVlYTdjMDUxYzk4ODUiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMy0wOVQxMjoyMzowN1oiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOS0xMFQxNzo0MzozN1oiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAzLTA5VDEyOjIzOjM3WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxNC0wMy0wOVQyMDoyMzozN1oiIFNlc3Npb25JbmRleD0iXzk0NGJmY2FjYjBkODMyYjEyZTRiY2Y3NzRlMDJiYmU1ZjY0NTVjNjgwMyI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3RAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3Q8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ic24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPndhYTI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iZWR1UGVyc29uQWZmaWxpYXRpb24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+YWRtaW48L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4=
    +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDQyN2JmMGJiLTI3MmMtMGFkMC05MGYyLTVmN2ZkOWJhYzQ2OCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOX2JmMzcyYjlkNjdkMGM4OWQwY2YxYWYzZmY2MjVlYTdjMDUxYzk4ODUiPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDQyN2JmMGJiLTI3MmMtMGFkMC05MGYyLTVmN2ZkOWJhYzQ2OCI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+eHRBaE5WaUtVa3VmcUR1eUNHRDBVVE9SbVdBPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5yL0E3SHhVVC9mZ1pXMVBGVkdHOXJTbW5sQU1QVjdnQm4zTjBVdFJ1V0VtTmlaQTVNUUVCSE9JeTA0dFFuSmRXYksrKzAxemh0TmdqRlhGeGduaE5VWFdKdVF4Rnp3SzdvaTRJbHc0amZjQ2tpSFJLVWxubGQ4U3Q1YVBGMUE5bFlWbWxtU0d5dUgzSnlrSjNnVjNaeVJRMjhDc0IvQnBtdi9mbmhleHBhYzQ9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDczY2ZlZWM0LWZkOTMtMzE1OC05ZDI1LWVmZDgzYWM4MjQyYiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng3M2NmZWVjNC1mZDkzLTMxNTgtOWQyNS1lZmQ4M2FjODI0MmIiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPjJpdzJPZUpRSjA1ellQTkIvSUp3TXdETFRjdz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+cFFiaUFReWEvKzVxbkhKWWlPQ0dFaUt5RHNSZThKdTlBUlJiVlFlUkd5NWF4VlU5aDYxV0tYSWcwcDI5NlFQMEJBem0rWmt5cXErTUdSVWE4OFNpaWFlbWRma3dRZnd1TS8rTk1FTkRqQmgrR2tob0RmcjN1ZDAzWjNDcHA3dDdTSldWVnF5NGZZVUtzNllaYmJRTG5PVjZPVVphb3R6eFBzUXVSVGlsWmVrPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjA0My0wOS0xMFQxNzo0MzozN1oiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOX2JmMzcyYjlkNjdkMGM4OWQwY2YxYWYzZmY2MjVlYTdjMDUxYzk4ODUiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48c2FtbDpFbmNyeXB0ZWRJRD48eGVuYzpFbmNyeXB0ZWREYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyIgeG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyIgVHlwZT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjRWxlbWVudCI+PHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI2FlczEyOC1jYmMiLz48ZHNpZzpLZXlJbmZvIHhtbG5zOmRzaWc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjx4ZW5jOkVuY3J5cHRlZEtleT48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjcnNhLTFfNSIvPjx4ZW5jOkNpcGhlckRhdGE+PHhlbmM6Q2lwaGVyVmFsdWU+T0J0UmN6b2dyZWc1VmpOS29zL2JOaEEvMjZ1MnFiWko5ZzBvbWpsRGphWEp5N2FJRkJ4WTMyV3ZvcjY3T0VhNm5VV3dUcFpVT3FWR2NLQ1puMGtCWVZJUjFjSGJsTUdhRUNLdEthU25wdUR5UXZ4UDA1Ulp5cG5nUVJ3ZXdSZ0VaZkE5NmhCM2w1bmgzMFNqYmh4U09IbXk0Qi9lWEx1UHlOUWhiMEdadEFzPTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHNpZzpLZXlJbmZvPg0KICAgPHhlbmM6Q2lwaGVyRGF0YT4NCiAgICAgIDx4ZW5jOkNpcGhlclZhbHVlPnNwZm8yNndVUlVCc3RnYzdBbG1PM25nem91OUFqUVU0NFhUOUt2ZWcvVTh6Q1FzNU43WXRyazZwREs2Tktic2gvQ1RMaFZKUy9kRFNwd3hzRy9QRDU0WlUrcmdKNGtndGt5YWduSzhydnBwNDJuYjc1YzBzUE1KK0M4dW42VHExeFBRRjlkcEJaVU1sZDI2WUJKQ0d6TG0vcmVDUlpiSk5iNEx3TmRjd3pQTnc4cnJsb1Z2SWhyQjlvWS80a3hwdExFemJmczVHcklxZXZNQnZjeWxpQVhBbFE1QitEZUdPWkhtODJnQmFBVHdxcXNuWkRsbmMyTUtiUWJyVXdLUTZ5NXdPcVByUmxXbFBJUnRLbzRZc2R3PT08L3hlbmM6Q2lwaGVyVmFsdWU+DQogICA8L3hlbmM6Q2lwaGVyRGF0YT4NCjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDpFbmNyeXB0ZWRJRD48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMy0wOVQxMjoyMzowN1oiIE5vdE9uT3JBZnRlcj0iMjA0My0wOS0xMFQxNzo0MzozN1oiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMy0wOVQxMjoyMzozN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwNDMtMDMtMDlUMjA6MjM6MzdaIiBTZXNzaW9uSW5kZXg9Il85NDRiZmNhY2IwZDgzMmIxMmU0YmNmNzc0ZTAyYmJlNWY2NDU1YzY4MDMiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0QGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj53YWEyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+
    \ No newline at end of file
    diff --git a/tests/data/responses/response_node_text_attack.xml.base64 b/tests/data/responses/response_node_text_attack.xml.base64
    new file mode 100644
    index 00000000..ba9f2f12
    --- /dev/null
    +++ b/tests/data/responses/response_node_text_attack.xml.base64
    @@ -0,0 +1 @@
    +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIElEPSJHT1NBTUxSMTI5MDExNzQ1NzE3OTQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiIgRGVzdGluYXRpb249IntyZWNpcGllbnR9Ij4NCiAgPHNhbWxwOlN0YXR1cz4NCiAgICA8c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+DQogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIFZlcnNpb249IjIuMCIgSUQ9InBmeGE0NjU3NGRmLWIzYjAtYTA2YS0yM2M4LTYzNjQxMzE5ODc3MiIgSXNzdWVJbnN0YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiI+DQogICAgPHNhbWw6SXNzdWVyPmh0dHBzOi8vYXBwLm9uZWxvZ2luLmNvbS9zYW1sL21ldGFkYXRhLzEzNTkwPC9zYW1sOklzc3Vlcj4NCiAgICA8ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4NCiAgICAgIDxkczpTaWduZWRJbmZvPg0KICAgICAgICA8ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgICAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+DQogICAgICAgIDxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4YTQ2NTc0ZGYtYjNiMC1hMDZhLTIzYzgtNjM2NDEzMTk4NzcyIj4NCiAgICAgICAgICA8ZHM6VHJhbnNmb3Jtcz4NCiAgICAgICAgICAgIDxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPg0KICAgICAgICAgICAgPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgICAgICAgIDwvZHM6VHJhbnNmb3Jtcz4NCiAgICAgICAgICA8ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz4NCiAgICAgICAgICA8ZHM6RGlnZXN0VmFsdWU+cEpRN01TL2VrNEtSUldHbXYvSDQzUmVIWU1zPTwvZHM6RGlnZXN0VmFsdWU+DQogICAgICAgIDwvZHM6UmVmZXJlbmNlPg0KICAgICAgPC9kczpTaWduZWRJbmZvPg0KICAgICAgPGRzOlNpZ25hdHVyZVZhbHVlPnlpdmVLY1BkRHB1RE5qNnNoclEzQUJ3ci9jQTNDcnlEMnBoRy94TFpzektXeFU1L21sYUt0OGV3YlpPZEtLdnRPczJwSEJ5NUR1YTNrOTRBRnp4R3llbDVnT293bW95WEpyQU9ya1BPMHZsaTFWOG8zaFBQVVp3UmdTWDZROXBTMUNxUWdoS2lFYXNSeXlscXFKVWFQWXptT3pPRTgvWGxNa3dpV21PMD08L2RzOlNpZ25hdHVyZVZhbHVlPg0KICAgICAgPGRzOktleUluZm8+DQogICAgICAgIDxkczpYNTA5RGF0YT4NCiAgICAgICAgICA8ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUJyVENDQWFHZ0F3SUJBZ0lCQVRBREJnRUFNR2N4Q3pBSkJnTlZCQVlUQWxWVE1STXdFUVlEVlFRSURBcERZV3hwWm05eWJtbGhNUlV3RXdZRFZRUUhEQXhUWVc1MFlTQk5iMjVwWTJFeEVUQVBCZ05WQkFvTUNFOXVaVXh2WjJsdU1Sa3dGd1lEVlFRRERCQmhjSEF1YjI1bGJHOW5hVzR1WTI5dE1CNFhEVEV3TURNd09UQTVOVGcwTlZvWERURTFNRE13T1RBNU5UZzBOVm93WnpFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ01Da05oYkdsbWIzSnVhV0V4RlRBVEJnTlZCQWNNREZOaGJuUmhJRTF2Ym1sallURVJNQThHQTFVRUNnd0lUMjVsVEc5bmFXNHhHVEFYQmdOVkJBTU1FR0Z3Y0M1dmJtVnNiMmRwYmk1amIyMHdnWjh3RFFZSktvWklodmNOQVFFQkJRQURnWTBBTUlHSkFvR0JBT2pTdTFmalB5OGQ1dzRReUwxemQ0aEl3MU1ra2ZmNFdZL1RMRzhPWmtVNVlUU1dtbUhQRDVrdllINXVvWFMvNnFRODFxWHBSMndWOENUb3daSlVMZzA5ZGRSZFJuOFFzcWoxRnlPQzVzbEUzeTJiWjJvRnVhNzJvZi80OWZwdWpuRlQ2S25RNjFDQk1xbERvVFFxT1Q2MnZHSjhuUDZNWld2QTZzeHF1ZDVBZ01CQUFFd0F3WUJBQU1CQUE9PTwvZHM6WDUwOUNlcnRpZmljYXRlPg0KICAgICAgICA8L2RzOlg1MDlEYXRhPg0KICAgICAgPC9kczpLZXlJbmZvPg0KICAgIDwvZHM6U2lnbmF0dXJlPg0KICAgIDxzYW1sOlN1YmplY3Q+DQogICAgICA8c2FtbDpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPnN1cHBvcnQ8IS0tIGF0dGFjayEgLS0+QG9uZWxvZ2luLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOFQyMjowMjozN1oiIFJlY2lwaWVudD0ie3JlY2lwaWVudH0iLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0xMS0xOFQyMTo1MjozN1oiIE5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOFQyMjowMjozN1oiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+e2F1ZGllbmNlfTwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMC0xMS0xOFQyMTo1NzozN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTAtMTEtMTlUMjE6NTc6MzdaIiBTZXNzaW9uSW5kZXg9Il81MzFjMzJkMjgzYmRmZjdlMDRlNDg3YmNkYmM0ZGQ4ZCI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9InN1cm5hbWUiPg0KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPnM8IS0tIGF0dGFjayEgLS0+bWl0aDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0iYW5vdGhlcl92YWx1ZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+dmFsdWUxPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPg0KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPnZhbHVlMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0icm9sZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+cm9sZTE8L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9ImZpcnN0bmFtZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+Ym9iPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPg0KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4gIA0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9ImF0dHJpYnV0ZV93aXRoX25pbF92YWx1ZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOm5pbD0idHJ1ZSIvPg0KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJhdHRyaWJ1dGVfd2l0aF9uaWxzX2FuZF9lbXB0eV9zdHJpbmdzIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUvPg0KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZT52YWx1ZVByZXNlbnQ8L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOm5pbD0idHJ1ZSIvPg0KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTpuaWw9IjEiLz4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+
    \ No newline at end of file
    diff --git a/tests/data/responses/response_with_nested_nameid_values.xml.base64 b/tests/data/responses/response_with_nested_nameid_values.xml.base64
    new file mode 100644
    index 00000000..3092c3cf
    --- /dev/null
    +++ b/tests/data/responses/response_with_nested_nameid_values.xml.base64
    @@ -0,0 +1,71 @@
    +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDph
    +c3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9j
    +b2wiIElEPSJHT1NBTUxSMTI5MDExNzQ1NzE3OTQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50
    +PSIyMDEwLTExLTE4VDIxOjU3OjM3WiIgRGVzdGluYXRpb249IntyZWNpcGllbnR9Ij4KICA8c2Ft
    +bHA6U3RhdHVzPgogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0
    +YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbHA6U3RhdHVzPgogIDxzYW1sOkFzc2Vy
    +dGlvbiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhz
    +aT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIFZlcnNpb249IjIu
    +MCIgSUQ9InBmeGE0NjU3NGRmLWIzYjAtYTA2YS0yM2M4LTYzNjQxMzE5ODc3MiIgSXNzdWVJbnN0
    +YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiI+CiAgICA8c2FtbDpJc3N1ZXI+aHR0cHM6Ly9hcHAu
    +b25lbG9naW4uY29tL3NhbWwvbWV0YWRhdGEvMTM1OTA8L3NhbWw6SXNzdWVyPgogICAgPGRzOlNp
    +Z25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAg
    +ICAgIDxkczpTaWduZWRJbmZvPgogICAgICAgIDxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFs
    +Z29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICAg
    +ICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAv
    +MDkveG1sZHNpZyNyc2Etc2hhMSIvPgogICAgICAgIDxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4YTQ2
    +NTc0ZGYtYjNiMC1hMDZhLTIzYzgtNjM2NDEzMTk4NzcyIj4KICAgICAgICAgIDxkczpUcmFuc2Zv
    +cm1zPgogICAgICAgICAgICA8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5v
    +cmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz4KICAgICAgICAgICAgPGRz
    +OlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1j
    +MTRuIyIvPgogICAgICAgICAgPC9kczpUcmFuc2Zvcm1zPgogICAgICAgICAgPGRzOkRpZ2VzdE1l
    +dGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+
    +CiAgICAgICAgICA8ZHM6RGlnZXN0VmFsdWU+cEpRN01TL2VrNEtSUldHbXYvSDQzUmVIWU1zPTwv
    +ZHM6RGlnZXN0VmFsdWU+CiAgICAgICAgPC9kczpSZWZlcmVuY2U+CiAgICAgIDwvZHM6U2lnbmVk
    +SW5mbz4KICAgICAgPGRzOlNpZ25hdHVyZVZhbHVlPnlpdmVLY1BkRHB1RE5qNnNoclEzQUJ3ci9j
    +QTNDcnlEMnBoRy94TFpzektXeFU1L21sYUt0OGV3YlpPZEtLdnRPczJwSEJ5NUR1YTNrOTRBRit6
    +eEd5ZWw1Z09vd21veVhKcitBT3Ira1BPMHZsaTFWOG8zaFBQVVp3UmdTWDZROXBTMUNxUWdoS2lF
    +YXNSeXlscXFKVWFQWXptT3pPRTgvWGxNa3dpV21PMD08L2RzOlNpZ25hdHVyZVZhbHVlPgogICAg
    +ICA8ZHM6S2V5SW5mbz4KICAgICAgICA8ZHM6WDUwOURhdGE+CiAgICAgICAgICA8ZHM6WDUwOUNl
    +cnRpZmljYXRlPk1JSUJyVENDQWFHZ0F3SUJBZ0lCQVRBREJnRUFNR2N4Q3pBSkJnTlZCQVlUQWxW
    +VE1STXdFUVlEVlFRSURBcERZV3hwWm05eWJtbGhNUlV3RXdZRFZRUUhEQXhUWVc1MFlTQk5iMjVw
    +WTJFeEVUQVBCZ05WQkFvTUNFOXVaVXh2WjJsdU1Sa3dGd1lEVlFRRERCQmhjSEF1YjI1bGJHOW5h
    +VzR1WTI5dE1CNFhEVEV3TURNd09UQTVOVGcwTlZvWERURTFNRE13T1RBNU5UZzBOVm93WnpFTE1B
    +a0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ01Da05oYkdsbWIzSnVhV0V4RlRBVEJnTlZCQWNNREZO
    +aGJuUmhJRTF2Ym1sallURVJNQThHQTFVRUNnd0lUMjVsVEc5bmFXNHhHVEFYQmdOVkJBTU1FR0Z3
    +Y0M1dmJtVnNiMmRwYmk1amIyMHdnWjh3RFFZSktvWklodmNOQVFFQkJRQURnWTBBTUlHSkFvR0JB
    +T2pTdTFmalB5OGQ1dzRReUwxK3pkNGhJdzFNa2tmZjRXWS9UTEc4T1prVTVZVFNXbW1IUEQ1a3ZZ
    +SDV1b1hTLzZxUTgxcVhwUjJ3VjhDVG93WkpVTGcwOWRkUmRSbjhRc3FqMUZ5T0M1c2xFM3kyYloy
    +b0Z1YTcyb2YvNDlmcHVqbkZUNktuUTYxQ0JNcWxEb1RRcU9UNjJ2R0o4blA2TVpXdkE2c3hxdWQ1
    +QWdNQkFBRXdBd1lCQUFNQkFBPT08L2RzOlg1MDlDZXJ0aWZpY2F0ZT4KICAgICAgICA8L2RzOlg1
    +MDlEYXRhPgogICAgICA8L2RzOktleUluZm8+CiAgICA8L2RzOlNpZ25hdHVyZT4KICAgIDxzYW1s
    +OlN1YmplY3Q+CiAgICAgIDxzYW1sOk5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpT
    +QU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c3VwcG9ydEBvbmVsb2dpbi5jb208
    +L3NhbWw6TmFtZUlEPgogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJu
    +Om9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+CiAgICAgICAgPHNhbWw6U3ViamVj
    +dENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDEwLTExLTE4VDIyOjAyOjM3WiIgUmVj
    +aXBpZW50PSJ7cmVjaXBpZW50fSIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPgogICAgPC9z
    +YW1sOlN1YmplY3Q+CiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0xMS0xOFQy
    +MTo1MjozN1oiIE5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOFQyMjowMjozN1oiPgogICAgICA8c2Ft
    +bDpBdWRpZW5jZVJlc3RyaWN0aW9uPgogICAgICAgIDxzYW1sOkF1ZGllbmNlPnthdWRpZW5jZX08
    +L3NhbWw6QXVkaWVuY2U+CiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPgogICAgPC9z
    +YW1sOkNvbmRpdGlvbnM+CiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIw
    +MTAtMTEtMThUMjE6NTc6MzdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDEwLTExLTE5VDIxOjU3
    +OjM3WiIgU2Vzc2lvbkluZGV4PSJfNTMxYzMyZDI4M2JkZmY3ZTA0ZTQ4N2JjZGJjNGRkOGQiPgog
    +ICAgICA8c2FtbDpBdXRobkNvbnRleHQ+CiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NS
    +ZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6
    +QXV0aG5Db250ZXh0Q2xhc3NSZWY+CiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+CiAgICA8L3Nh
    +bWw6QXV0aG5TdGF0ZW1lbnQ+CiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+CiAgICAgIDxz
    +YW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiPgogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHht
    +bG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRw
    +Oi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmlu
    +ZyI+ZGVtbzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4KICAg
    +ICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9ImFub3RoZXJfdmFsdWUiPgogICAgICAgIDxzYW1sOkF0
    +dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIg
    +eG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNp
    +OnR5cGU9InhzOnN0cmluZyI+CiAgICAgICAgICAgIDxzYW1sOk5hbWVJRCBGb3JtYXQ9InVybjpv
    +YXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnBlcnNpc3RlbnQiIE5hbWVRdWFs
    +aWZpZXI9Imh0dHBzOi8vaWRwSUQiIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9zcElEIj52YWx1
    +ZTwvc2FtbDpOYW1lSUQ+CiAgICAgICAgPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPgogICAgICA8L3Nh
    +bWw6QXR0cmlidXRlPgogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4KICA8L3NhbWw6QXNz
    +ZXJ0aW9uPgo8L3NhbWxwOlJlc3BvbnNlPgo=
    diff --git a/tests/data/responses/valid_response.xml.base64 b/tests/data/responses/valid_response.xml.base64
    index c727dc13..5a917f2f 100644
    --- a/tests/data/responses/valid_response.xml.base64
    +++ b/tests/data/responses/valid_response.xml.base64
    @@ -1 +1 @@
    -PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZngwNWYzY2UxMC0xNjE1LWYzZWEtYTk4OC02MGUzODBiMzI5OWYiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgRGVzdGluYXRpb249Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl81ZmU5ZDZlNDk5YjJmMDkxMzIwNmFhYjNmNzE5MTcyOTA0OWJiODA3Ij48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZngwNWYzY2UxMC0xNjE1LWYzZWEtYTk4OC02MGUzODBiMzI5OWYiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPmpBZ290RjBKK1JLMS9LODd3MjRNTUMyK1pScz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+cFhmM3Z3T1p2dGZtZjdNTWNPU0cwMzkyU213bnBJb0FqZ2dzVmErUlNJRE1Td0tTckwzcWw3SnlZQjVTaXZxL0xYODlUYXF5WTJ4MFBnTWl4YXY0bjFHMTFDM3NtbFJBTXJEZTZ2UnRJbUpVc2xTR2s5N3pQaHlvUStKNW9nUVBkNlZsTVR6OEtXemZxdE9QRGY1ZGwyWXlKcG9rZU9OVE0xemM0SkdNM0dBPTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGI0ZWM5YzhhLTQ4ZWItZmRhMi03Zjc0LWZhMWExMDVhOTlmZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZnhiNGVjOWM4YS00OGViLWZkYTItN2Y3NC1mYTFhMTA1YTk5ZmUiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPmhucFQ5Y3VlNnRCSzRJWk9qNTBlTW0zbUNnST08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+ckEwZkxaN0trbDNKd3NMS1hEd2FGY2c2TG5LSFJ6SVVyeDZtTlgxVG85ek1DdkNVRC9vUFlWYUVlTXRueFlZNDZmc21hTnZzS2l1Z2l4ZFZ6TEwzOEttQVk4dU1UZXpoR3Z6MlZlSFVvcjR1NGh4ZlFsc090MmZ1ekFFNkRicXZJVkswd2p4M1JZMDhYL3VPQ1I5dVBjcHk0QkJzUTN4UGw0UXZpcEpqU05NPTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWw6U3ViamVjdD48c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocCIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPjQ5Mjg4MjYxNWFjZjMxYzgwOTZiNjI3MjQ1ZDc2YWU1MzAzNmMwOTA8L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDgtMjNUMDY6NTc6MDFaIiBSZWNpcGllbnQ9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl81ZmU5ZDZlNDk5YjJmMDkxMzIwNmFhYjNmNzE5MTcyOTA0OWJiODA3Ii8+PC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+PC9zYW1sOlN1YmplY3Q+PHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTQtMDItMTlUMDE6MzY6MzFaIiBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDgtMjNUMDY6NTc6MDFaIj48c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjxzYW1sOkF1ZGllbmNlPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==
    +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDQyYmU0MGJmLTM5YzMtNzdmMC1jNmFlLThiZjJlMjNhMWEyZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDQyYmU0MGJmLTM5YzMtNzdmMC1jNmFlLThiZjJlMjNhMWEyZSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+M1JNaTI0V0F2cjlnTHdWZ0NtUDlsM2NneCtFPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5FR2ZSVnRTblJqVkJwdkpMdjExNnhWcmovaDVVQnJlTDNnV0V3ZnNORHkrMU9iaC9XTHZlR01uS2xIN0draHU5eXNIUVkxYzRnSER4SVgyaXZtM3YzVFhqK0V3ZDVhMVE2dXgvbXZJSFRvSUR5SnFLL25LSUtVZFEwTWhIcHVXUnF1OEJoWGZQcnRGdWkzOHRhamJpNjFTUnMxN0ZJc3YvbFJLb1JScWJIYlU9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDU3ZGZkYTYwLWIyMTEtNGNkYS0wZjYzLTZkNWRlYjY5ZTViYiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng1N2RmZGE2MC1iMjExLTRjZGEtMGY2My02ZDVkZWI2OWU1YmIiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPndIVUpDalpLRWVtd3E2eGZzMkNIbUd3UXNIND08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+Y2VFd0NtbFQ2d3lpdGVLZE5JaVNFLzBoTG5rMkRweEh0K24vdzlhTHp4MmpneDJOTzBiUTFjb0xyYlBmc1A1SjhNYkNBQWRUb20yUkxaTUxIdTNwZjBHeXJ6cUUxTkhIMmthaGJiSWtKYnZJWkhhaE1JaFZNUW1RMzhJMDdQRC8wc3BjdkJqQS9lblk0SWtWR2VQdmUwV3FhaU5ZUGNOeDNaTDJPdENMZEtzPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjA1NC0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjA1NC0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwNTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==
    \ No newline at end of file
    diff --git a/tests/data/responses/valid_response_with_namequalifier.xml.base64 b/tests/data/responses/valid_response_with_namequalifier.xml.base64
    new file mode 100644
    index 00000000..a98de3ff
    --- /dev/null
    +++ b/tests/data/responses/valid_response_with_namequalifier.xml.base64
    @@ -0,0 +1 @@
    +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDhmZWI5YWNkLTFlODYtYWMxMi05MDIzLTEzYjg0NDc5YjI1YiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDhmZWI5YWNkLTFlODYtYWMxMi05MDIzLTEzYjg0NDc5YjI1YiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+NVRWZURYbGQ3YzhURmtybVlDeFpuL2ZHRTRzPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5hZlFaVUE2REpHa0hLNjVMMENBaTJBSDJkOWNwbExuekNPTHBCYm9hUmVmaWdtVC92L0tJZGcyYXpWRzY2Ykk1aFA1NTBNR0c2ZVVzaWJ1N2N3ZytFbG9tejVBalE3dzlGZG8waHdWWWhib3JaSkN2TUxLUzBEWkFzc01XZnZ3RGNUNmhra3UreXFlS2RhZ1BBOTYwQ25YcUMxeHpjMk43WS82dlBCU081bVU9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDQxN2ZiOTc2LTk0NGEtNDNiZi05ZTUyLWZiOWM1OTYxNzYxZiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng0MTdmYjk3Ni05NDRhLTQzYmYtOWU1Mi1mYjljNTk2MTc2MWYiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPmxSbTJ3UW13ZGhmZVZuMDFaS1Ewb05CN1JqQT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+aktwQUNmMWkxR0FMSWQ5Y0liQlFsTkJQMVhpZDhhYXFKOUxyTkFIZ1lpR2VIc0NscldVUkZJREprOGI0T3RmdHdXTGZKeXBXbXgwWm15M2hpTTJyVHBIbDBLMGVqSFNsOS9Ed0pabkNEQW1CS1lhZ0ZFR0xxWXYwaXI0Y2lYaForTkdXSDY1czhBRlVibjU2SytaS3lpMFkwMWc4TmVqaS92OTNlZFZ6ZTZnPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vdGVzdC5leGFtcGxlLmNvbS9zYW1sL21ldGFkYXRhIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjA1NC0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjA1NC0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwNTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==
    \ No newline at end of file
    diff --git a/tests/data/responses/valid_response_without_inresponseto.xml.base64 b/tests/data/responses/valid_response_without_inresponseto.xml.base64
    new file mode 100644
    index 00000000..388e2709
    --- /dev/null
    +++ b/tests/data/responses/valid_response_without_inresponseto.xml.base64
    @@ -0,0 +1 @@
    +PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzYW1scDpSZXNwb25zZSB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0icGZ4MDVmM2NlMTAtMTYxNS1mM2VhLWE5ODgtNjBlMzgwYjMyOTlmIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIERlc3RpbmF0aW9uPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiPgogIDxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+CiAgPGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAgICA8ZHM6U2lnbmVkSW5mbz4KICAgICAgPGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPgogICAgICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDA1ZjNjZTEwLTE2MTUtZjNlYS1hOTg4LTYwZTM4MGIzMjk5ZiI+CiAgICAgICAgPGRzOlRyYW5zZm9ybXM+CiAgICAgICAgICA8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz4KICAgICAgICAgIDxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgICAgICA8L2RzOlRyYW5zZm9ybXM+CiAgICAgICAgPGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+CiAgICAgICAgPGRzOkRpZ2VzdFZhbHVlPkRjUWNDL1BoS05qRTlLa29YRXZZRlhXMHZGdz08L2RzOkRpZ2VzdFZhbHVlPgogICAgICA8L2RzOlJlZmVyZW5jZT4KICAgIDwvZHM6U2lnbmVkSW5mbz4KICAgIDxkczpTaWduYXR1cmVWYWx1ZT5xVjcvc2YvVEt1S0x5allaMGNDSlhCWnZSYmF1RXNoMXQvaEtJeStpVHJRSjYxWG0rMXZDcEtvVXdleGNuL1ZpCitsemZlaHZjL2tDMjE5TjZVTUUxZnRLTDY2OSsxYkpFb1NLejQrN2VhWi9XTFdYL0hRYndMVmh6dlh3bWdMQVAKUEhLNmZJZHpocGRkLzRydjlXVnpjaGoveGcxWVNkaXFrcnU3YUhhS2FEOD08L2RzOlNpZ25hdHVyZVZhbHVlPgogIDwvZHM6U2lnbmF0dXJlPgogIDxzYW1scDpTdGF0dXM+CiAgICA8c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+CiAgPC9zYW1scDpTdGF0dXM+CiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGI0ZWM5YzhhLTQ4ZWItZmRhMi03Zjc0LWZhMWExMDVhOTlmZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj4KICAgIDxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+CiAgICA8c2FtbDpTdWJqZWN0PgogICAgICA8c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocCIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPjQ5Mjg4MjYxNWFjZjMxYzgwOTZiNjI3MjQ1ZDc2YWU1MzAzNmMwOTA8L3NhbWw6TmFtZUlEPgogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+CiAgICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiIgUmVjaXBpZW50PSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNWZlOWQ2ZTQ5OWIyZjA5MTMyMDZhYWIzZjcxOTE3MjkwNDliYjgwNyIvPgogICAgICA8L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4KICAgIDwvc2FtbDpTdWJqZWN0PgogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTQtMDItMTlUMDE6MzY6MzFaIiBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDgtMjNUMDY6NTc6MDFaIj4KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4KICAgICAgICA8c2FtbDpBdWRpZW5jZT5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT4KICAgICAgPC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+CiAgICA8L3NhbWw6Q29uZGl0aW9ucz4KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPgogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+CiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+CiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+CiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+CiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+CiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPgogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNtYXJ0aW48L3NhbWw6QXR0cmlidXRlVmFsdWU+CiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+CiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+CiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+CiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+CiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+U2l4dG8zPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPgogICAgICA8L3NhbWw6QXR0cmlidXRlPgogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ic24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPgogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPk1hcnRpbjI8L3NhbWw6QXR0cmlidXRlVmFsdWU+CiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+CiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+CiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+CiAgPC9zYW1sOkFzc2VydGlvbj4KPC9zYW1scDpSZXNwb25zZT4K
    \ No newline at end of file
    diff --git a/tests/pep8.rc b/tests/pep8.rc
    index b60901e4..ad7c254b 100644
    --- a/tests/pep8.rc
    +++ b/tests/pep8.rc
    @@ -1,3 +1,3 @@
    -[pep8]
    -ignore = E501
    -max-line-length = 160
    \ No newline at end of file
    +[pycodestyle]
    +ignore = E501, E731, W504
    +max-line-length = 160
    diff --git a/tests/settings/settings1.json b/tests/settings/settings1.json
    index d809b7d6..69d7d25e 100644
    --- a/tests/settings/settings1.json
    +++ b/tests/settings/settings1.json
    @@ -10,7 +10,7 @@
             "singleLogoutService": {
                 "url": "http://stuff.com/endpoints/endpoints/sls.php"
             },
    -        "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified"
    +        "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
         },
         "idp": {
             "entityId": "http://idp.example.com/",
    diff --git a/tests/settings/settings2.json b/tests/settings/settings2.json
    index d18b518f..22f92dc1 100644
    --- a/tests/settings/settings2.json
    +++ b/tests/settings/settings2.json
    @@ -10,7 +10,7 @@
             "singleLogoutService": {
                 "url": "http://stuff.com/endpoints/endpoints/sls.php"
             },
    -        "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified"
    +        "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
         },
         "idp": {
             "entityId": "https://idp.example.com/simplesaml/saml2/idp/metadata.php",
    diff --git a/tests/settings/settings3.json b/tests/settings/settings3.json
    index c291c615..de72e50d 100644
    --- a/tests/settings/settings3.json
    +++ b/tests/settings/settings3.json
    @@ -10,7 +10,7 @@
             "singleLogoutService": {
                 "url": "http://pytoolkit.com:8000/?sls"
             },
    -        "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified"
    +        "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
         },
         "idp": {
             "entityId": "https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php",
    diff --git a/tests/settings/settings4.json b/tests/settings/settings4.json
    new file mode 100644
    index 00000000..c217c7d8
    --- /dev/null
    +++ b/tests/settings/settings4.json
    @@ -0,0 +1,83 @@
    +{
    +    "strict": false,
    +    "debug": false,
    +    "custom_base_path": "../../../tests/data/customPath/",
    +    "sp": {
    +        "entityId": "http://pytoolkit.com:8000/metadata/",
    +        "assertionConsumerService": {
    +            "url": "http://pytoolkit.com:8000/?acs"
    +        },
    +        "attributeConsumingService": {
    +            "isDefault": false,
    +            "serviceName": "Test Service",
    +            "serviceDescription": "Test Service",
    +            "requestedAttributes": [ {
    +                    "name": "urn:oid:2.5.4.42",
    +                    "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
    +                    "friendlyName": "givenName",
    +                    "isRequired": false
    +                },
    +                {
    +                    "name": "urn:oid:2.5.4.4",
    +                    "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
    +                    "friendlyName": "sn",
    +                    "isRequired": false
    +                },
    +                {
    +                    "name": "urn:oid:2.16.840.1.113730.3.1.241",
    +                    "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
    +                    "friendlyName": "displayName",
    +                    "isRequired": false
    +                },
    +                {
    +                    "name": "urn:oid:0.9.2342.19200300.100.1.3",
    +                    "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
    +                    "friendlyName": "mail",
    +                    "isRequired": false
    +                },
    +                {
    +                    "name": "urn:oid:0.9.2342.19200300.100.1.1",
    +                    "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
    +                    "friendlyName": "uid",
    +                    "isRequired": false
    +                }
    +            ]
    +        },
    +        "singleLogoutService": {
    +            "url": "http://pytoolkit.com:8000/?sls"
    +        },
    +        "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
    +    },
    +    "idp": {
    +        "entityId": "https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php",
    +        "singleSignOnService": {
    +            "url": "http://pitbulk.no-ip.org/SSOService.php"
    +        },
    +        "singleLogoutService": {
    +            "url": "http://pitbulk.no-ip.org/SingleLogoutService.php"
    +        },
    +        "x509cert": "MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m"
    +    },
    +    "security": {
    +        "authnRequestsSigned": false,
    +        "wantAssertionsSigned": false,
    +        "signMetadata": false
    +    },
    +    "contactPerson": {
    +        "technical": {
    +            "givenName": "technical_name",
    +            "emailAddress": "technical@example.com"
    +        },
    +        "support": {
    +            "givenName": "support_name",
    +            "emailAddress": "support@example.com"
    +        }
    +    },
    +    "organization": {
    +        "en-US": {
    +            "name": "sp_test",
    +            "displayname": "SP test",
    +            "url": "http://sp.example.com"
    +        }
    +    }
    +}
    diff --git a/tests/settings/settings5.json b/tests/settings/settings5.json
    new file mode 100644
    index 00000000..e399d217
    --- /dev/null
    +++ b/tests/settings/settings5.json
    @@ -0,0 +1,65 @@
    +{
    +    "strict": false,
    +    "debug": false,
    +    "custom_base_path": "../../../tests/data/customPath/",
    +    "sp": {
    +        "entityId": "http://pytoolkit.com:8000/metadata/",
    +        "assertionConsumerService": {
    +            "url": "http://pytoolkit.com:8000/?acs"
    +        },
    +        "attributeConsumingService": {
    +            "isDefault": false,
    +            "serviceName": "Test Service",
    +            "serviceDescription": "Test Service",
    +            "requestedAttributes": [ {
    +                    "name": "userType",
    +                    "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
    +                    "isRequired": false,
    +                    "attributeValue": ["userType","admin"]
    +                },
    +                {
    +                    "name": "urn:oid:0.9.2342.19200300.100.1.1",
    +                    "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
    +                    "friendlyName": "uid",
    +                    "isRequired": false
    +                }
    +            ]
    +        },
    +        "singleLogoutService": {
    +            "url": "http://pytoolkit.com:8000/?sls"
    +        },
    +        "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
    +    },
    +    "idp": {
    +        "entityId": "https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php",
    +        "singleSignOnService": {
    +            "url": "http://pitbulk.no-ip.org/SSOService.php"
    +        },
    +        "singleLogoutService": {
    +            "url": "http://pitbulk.no-ip.org/SingleLogoutService.php"
    +        },
    +        "x509cert": "MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m"
    +    },
    +    "security": {
    +        "authnRequestsSigned": false,
    +        "wantAssertionsSigned": false,
    +        "signMetadata": false
    +    },
    +    "contactPerson": {
    +        "technical": {
    +            "givenName": "technical_name",
    +            "emailAddress": "technical@example.com"
    +        },
    +        "support": {
    +            "givenName": "support_name",
    +            "emailAddress": "support@example.com"
    +        }
    +    },
    +    "organization": {
    +        "en-US": {
    +            "name": "sp_test",
    +            "displayname": "SP test",
    +            "url": "http://sp.example.com"
    +        }
    +    }
    +}
    diff --git a/tests/settings/settings6.json b/tests/settings/settings6.json
    new file mode 100644
    index 00000000..f1258391
    --- /dev/null
    +++ b/tests/settings/settings6.json
    @@ -0,0 +1,47 @@
    +{
    +    "strict": false,
    +    "debug": false,
    +    "custom_base_path": "../../../tests/data/customPath/",
    +    "sp": {
    +        "entityId": "http://stuff.com/endpoints/metadata.php",
    +        "assertionConsumerService": {
    +            "url": "http://stuff.com/endpoints/endpoints/acs.php"
    +        },
    +        "singleLogoutService": {
    +            "url": "http://stuff.com/endpoints/endpoints/sls.php"
    +        },
    +        "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
    +    },
    +    "idp": {
    +        "entityId": "http://idp.example.com/",
    +        "singleSignOnService": {
    +            "url": "http://idp.example.com/SSOService.php"
    +        },
    +        "singleLogoutService": {
    +            "url": "http://idp.example.com/SingleLogoutService.php"
    +        },
    +        "x509cert": "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo"
    +    },
    +    "security": {
    +        "authnRequestsSigned": false,
    +        "wantAssertionsSigned": false,
    +        "signMetadata": false
    +    },
    +    "contactPerson": {
    +        "technical": {
    +            "givenName": "Téçhnïçäl Nämé",
    +            "emailAddress": "technical@example.com"
    +        },
    +        "support": {
    +            "givenName": "Süppört Nämé",
    +            "emailAddress": "support@example.com"
    +        }
    +    },
    +    "organization": {
    +        "en-US": {
    +            "name": "sp_test",
    +            "displayname": "Sérvïçé prövïdér",
    +            "url": "http://sp.example.com"
    +        }
    +    }
    +}
    diff --git a/tests/settings/settings7.json b/tests/settings/settings7.json
    new file mode 100644
    index 00000000..e573624b
    --- /dev/null
    +++ b/tests/settings/settings7.json
    @@ -0,0 +1,50 @@
    +{
    +    "strict": false,
    +    "debug": false,
    +    "custom_base_path": "../../../tests/data/customPath/",
    +    "sp": {
    +        "entityId": "http://stuff.com/endpoints/metadata.php",
    +        "assertionConsumerService": {
    +            "url": "http://stuff.com/endpoints/endpoints/acs.php"
    +        },
    +        "singleLogoutService": {
    +            "url": "http://stuff.com/endpoints/endpoints/sls.php"
    +        },
    +        "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
    +        "privateKey": "MIICXgIBAAKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABAoGAD4/Z4LWVWV6D1qMIp1Gzr0ZmdWTE1SPdZ7Ej8glGnCzPdguCPuzbhGXmIg0VJ5D+02wsqws1zd48JSMXXM8zkYZVwQYIPUsNn5FetQpwxDIMPmhHg+QNBgwOnk8JK2sIjjLPL7qY7Itv7LT7Gvm5qSOkZ33RCgXcgz+okEIQMYkCQQDzbTOyDL0c5WQV6A2k06T/azdhUdGXF9C0+WkWSfNaovmTgRXh1G+jMlr82Snz4p4/STt7P/XtyWzF3pkVgZr3AkEA7nPjXwHlttNEMo6AtxHd47nizK2NUN803ElIUT8P9KSCoERmSXq66PDekGNic4ldpsSvOeYCk8MAYoDBy9kvVwJBAMLgX4xg6lzhv7hR5+pWjTb1rIY6rCHbrPfU264+UZXz9v2BT/VUznLF81WMvStD9xAPHpFS6R0OLghSZhdzhI0CQQDL8Duvfxzrn4b9QlmduV8wLERoT6rEVxKLsPVz316TGrxJvBZLk/cV0SRZE1cZf4ukXSWMfEcJ/0Zt+LdG1CqjAkEAqwLSglJ9Dy3HpgMz4vAAyZWzAxvyA1zW0no9GOLcPQnYaNUN/Fy2SYtETXTb0CQ9X1rt8ffkFP7ya+5TC83aMg==",
    +        "x509cert": "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo",
    +        "x509certNew": "MIICVDCCAb2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBHMQswCQYDVQQGEwJ1czEQMA4GA1UECAwHZXhhbXBsZTEQMA4GA1UECgwHZXhhbXBsZTEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMTcwNDA3MDgzMDAzWhcNMjcwNDA1MDgzMDAzWjBHMQswCQYDVQQGEwJ1czEQMA4GA1UECAwHZXhhbXBsZTEQMA4GA1UECgwHZXhhbXBsZTEUMBIGA1UEAwwLZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKhPS4/0azxbQekHHewQGKD7Pivr3CDpsrKxY3xlVanxj427OwzOb5KUVzsDEazumt6sZFY8HfidsjXY4EYA4ZzyL7ciIAR5vlAsIYN9nJ4AwVDnN/RjVwj+TN6BqWPLpVIpHc6Dl005HyE0zJnk1DZDn2tQVrIzbD3FhCp7YeotAgMBAAGjUDBOMB0GA1UdDgQWBBRYZx4thASfNvR/E7NsCF2IaZ7wIDAfBgNVHSMEGDAWgBRYZx4thASfNvR/E7NsCF2IaZ7wIDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBACz4aobx9aG3kh+rNyrlgM3K6dYfnKG1/YH5sJCAOvg8kDr0fQAQifH8lFVWumKUMoAe0bFTfwWtp/VJ8MprrEJth6PFeZdczpuv+fpLcNj2VmNVJqvQYvS4m36OnBFh1QFZW8UrbFIfdtm2nuZ+twSKqfKwjLdqcoX0p39h7Uw/"
    +    },
    +    "idp": {
    +        "entityId": "http://idp.example.com/",
    +        "singleSignOnService": {
    +            "url": "http://idp.example.com/SSOService.php"
    +        },
    +        "singleLogoutService": {
    +            "url": "http://idp.example.com/SingleLogoutService.php"
    +        },
    +        "x509cert": "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo"
    +    },
    +    "security": {
    +        "authnRequestsSigned": false,
    +        "wantAssertionsSigned": false,
    +        "signMetadata": false
    +    },
    +    "contactPerson": {
    +        "technical": {
    +            "givenName": "technical_name",
    +            "emailAddress": "technical@example.com"
    +        },
    +        "support": {
    +            "givenName": "support_name",
    +            "emailAddress": "support@example.com"
    +        }
    +    },
    +    "organization": {
    +        "en-US": {
    +            "name": "sp_test",
    +            "displayname": "SP test",
    +            "url": "http://sp.example.com"
    +        }
    +    }
    +}
    \ No newline at end of file
    diff --git a/tests/settings/settings8.json b/tests/settings/settings8.json
    new file mode 100644
    index 00000000..ce30e498
    --- /dev/null
    +++ b/tests/settings/settings8.json
    @@ -0,0 +1,58 @@
    +{
    +    "strict": false,
    +    "debug": false,
    +    "custom_base_path": "../../../tests/data/customPath/",
    +    "sp": {
    +        "entityId": "http://stuff.com/endpoints/metadata.php",
    +        "assertionConsumerService": {
    +            "url": "http://stuff.com/endpoints/endpoints/acs.php"
    +        },
    +        "singleLogoutService": {
    +            "url": "http://stuff.com/endpoints/endpoints/sls.php"
    +        },
    +        "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
    +        "privateKey": "MIICXgIBAAKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABAoGAD4/Z4LWVWV6D1qMIp1Gzr0ZmdWTE1SPdZ7Ej8glGnCzPdguCPuzbhGXmIg0VJ5D+02wsqws1zd48JSMXXM8zkYZVwQYIPUsNn5FetQpwxDIMPmhHg+QNBgwOnk8JK2sIjjLPL7qY7Itv7LT7Gvm5qSOkZ33RCgXcgz+okEIQMYkCQQDzbTOyDL0c5WQV6A2k06T/azdhUdGXF9C0+WkWSfNaovmTgRXh1G+jMlr82Snz4p4/STt7P/XtyWzF3pkVgZr3AkEA7nPjXwHlttNEMo6AtxHd47nizK2NUN803ElIUT8P9KSCoERmSXq66PDekGNic4ldpsSvOeYCk8MAYoDBy9kvVwJBAMLgX4xg6lzhv7hR5+pWjTb1rIY6rCHbrPfU264+UZXz9v2BT/VUznLF81WMvStD9xAPHpFS6R0OLghSZhdzhI0CQQDL8Duvfxzrn4b9QlmduV8wLERoT6rEVxKLsPVz316TGrxJvBZLk/cV0SRZE1cZf4ukXSWMfEcJ/0Zt+LdG1CqjAkEAqwLSglJ9Dy3HpgMz4vAAyZWzAxvyA1zW0no9GOLcPQnYaNUN/Fy2SYtETXTb0CQ9X1rt8ffkFP7ya+5TC83aMg==",
    +        "x509cert": "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo"
    +    },
    +    "idp": {
    +        "entityId": "http://idp.example.com/",
    +        "singleSignOnService": {
    +            "url": "http://idp.example.com/SSOService.php"
    +        },
    +        "singleLogoutService": {
    +            "url": "http://idp.example.com/SingleLogoutService.php"
    +        },
    +        "x509cert": "",
    +        "x509certMulti": {
    +            "signing": [
    +                "MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m",
    +                "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo"
    +            ],
    +            "encryption": [
    +                "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo"
    +            ]
    +        }
    +    },
    +    "security": {
    +        "authnRequestsSigned": false,
    +        "wantAssertionsSigned": false,
    +        "signMetadata": false
    +    },
    +    "contactPerson": {
    +        "technical": {
    +            "givenName": "technical_name",
    +            "emailAddress": "technical@example.com"
    +        },
    +        "support": {
    +            "givenName": "support_name",
    +            "emailAddress": "support@example.com"
    +        }
    +    },
    +    "organization": {
    +        "en-US": {
    +            "name": "sp_test",
    +            "displayname": "SP test",
    +            "url": "http://sp.example.com"
    +        }
    +    }
    +}
    \ No newline at end of file
    diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
    index ea0b9fd0..3a57c438 100644
    --- a/tests/src/OneLogin/saml2_tests/auth_test.py
    +++ b/tests/src/OneLogin/saml2_tests/auth_test.py
    @@ -1,7 +1,7 @@
     # -*- coding: utf-8 -*-
     
    -# Copyright (c) 2014, OneLogin, Inc.
    -# All rights reserved.
    +# Copyright (c) 2010-2018 OneLogin, Inc.
    +# MIT License
     
     from base64 import b64decode, b64encode
     import json
    @@ -14,13 +14,15 @@
     from onelogin.saml2.settings import OneLogin_Saml2_Settings
     from onelogin.saml2.utils import OneLogin_Saml2_Utils
     from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request
    +from onelogin.saml2.errors import OneLogin_Saml2_Error
     
     
     class OneLogin_Saml2_Auth_Test(unittest.TestCase):
    -    data_path = join(dirname(__file__), '..', '..', '..', 'data')
    +    data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data')
    +    settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings')
     
    -    def loadSettingsJSON(self):
    -        filename = join(dirname(__file__), '..', '..', '..', 'settings', 'settings1.json')
    +    def loadSettingsJSON(self, name='settings1.json'):
    +        filename = join(self.settings_path, name)
             if exists(filename):
                 stream = open(filename, 'r')
                 settings = json.load(stream)
    @@ -96,6 +98,26 @@ def testGetSessionIndex(self):
             auth2.process_response()
             self.assertEqual('_6273d77b8cde0c333ec79d22a9fa0003b9fe2d75cb', auth2.get_session_index())
     
    +    def testGetSessionExpiration(self):
    +        """
    +        Tests the get_session_expiration method of the OneLogin_Saml2_Auth class
    +        """
    +        settings_info = self.loadSettingsJSON()
    +        auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info)
    +        self.assertIsNone(auth.get_session_expiration())
    +
    +        request_data = self.get_request()
    +        message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
    +        del request_data['get_data']
    +        request_data['post_data'] = {
    +            'SAMLResponse': message
    +        }
    +        auth2 = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON())
    +        self.assertIsNone(auth2.get_session_expiration())
    +
    +        auth2.process_response()
    +        self.assertEqual(2655106621, auth2.get_session_expiration())
    +
         def testGetLastErrorReason(self):
             """
             Tests the get_last_error_reason method of the OneLogin_Saml2_Auth class
    @@ -119,11 +141,8 @@ def testProcessNoResponse(self):
             """
             auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=self.loadSettingsJSON())
     
    -        try:
    +        with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'SAML Response not found'):
                 auth.process_response()
    -            self.assertFalse(True)
    -        except Exception as e:
    -            self.assertIn('SAML Response not found', e.message)
     
             self.assertEqual(auth.get_errors(), ['invalid_binding'])
     
    @@ -201,6 +220,9 @@ def testProcessResponseValid(self):
             self.assertEqual(auth.get_attribute('mail'), attributes['mail'])
             session_index = auth.get_session_index()
             self.assertEqual('_6273d77b8cde0c333ec79d22a9fa0003b9fe2d75cb', session_index)
    +        self.assertEqual("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", auth.get_nameid_format())
    +        self.assertIsNone(auth.get_nameid_nq())
    +        self.assertEqual("http://stuff.com/endpoints/metadata.php", auth.get_nameid_spnq())
     
         def testRedirectTo(self):
             """
    @@ -237,11 +259,8 @@ def testProcessNoSLO(self):
             Case No Message, An exception is throw
             """
             auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=self.loadSettingsJSON())
    -        try:
    +        with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'SAML LogoutRequest/LogoutResponse not found'):
                 auth.process_slo(True)
    -        except Exception as e:
    -            self.assertIn('SAML LogoutRequest/LogoutResponse not found', e.message)
    -        self.assertEqual(auth.get_errors(), ['invalid_binding'])
     
         def testProcessSLOResponseInvalid(self):
             """
    @@ -543,6 +562,20 @@ def testLogin(self):
             hostname = OneLogin_Saml2_Utils.get_self_host(request_data)
             self.assertIn(u'http://%s/index.html' % hostname, parsed_query['RelayState'])
     
    +    def testLoginWithUnicodeSettings(self):
    +        """
    +        Tests the login method of the OneLogin_Saml2_Auth class
    +        Case Login with unicode settings. An AuthnRequest is built an redirect executed
    +        """
    +        settings_info = self.loadSettingsJSON('settings6.json')
    +        request_data = self.get_request()
    +        auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_info)
    +
    +        target_url = auth.login()
    +        parsed_query = parse_qs(urlparse(target_url)[4])
    +        hostname = OneLogin_Saml2_Utils.get_self_host(request_data)
    +        self.assertIn(u'http://%s/index.html' % hostname, parsed_query['RelayState'])
    +
         def testLoginWithRelayState(self):
             """
             Tests the login method of the OneLogin_Saml2_Auth class
    @@ -585,7 +618,7 @@ def testLoginSigned(self):
         def testLoginForceAuthN(self):
             """
             Tests the login method of the OneLogin_Saml2_Auth class
    -        Case Logout with no parameters. A AuthN Request is built with ForceAuthn and redirect executed
    +        Case AuthN Request is built with ForceAuthn and redirect executed
             """
             settings_info = self.loadSettingsJSON()
             return_to = u'http://example.com/returnto'
    @@ -619,7 +652,7 @@ def testLoginForceAuthN(self):
         def testLoginIsPassive(self):
             """
             Tests the login method of the OneLogin_Saml2_Auth class
    -        Case Logout with no parameters. A AuthN Request is built with IsPassive and redirect executed
    +        Case AuthN Request is built with IsPassive and redirect executed
             """
             settings_info = self.loadSettingsJSON()
             return_to = u'http://example.com/returnto'
    @@ -650,6 +683,80 @@ def testLoginIsPassive(self):
             request_3 = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3['SAMLRequest'][0])
             self.assertIn('IsPassive="true"', request_3)
     
    +    def testLoginSetNameIDPolicy(self):
    +        """
    +        Tests the login method of the OneLogin_Saml2_Auth class
    +        Case AuthN Request is built with and without NameIDPolicy
    +        """
    +        settings_info = self.loadSettingsJSON()
    +        return_to = u'http://example.com/returnto'
    +        sso_url = settings_info['idp']['singleSignOnService']['url']
    +
    +        auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info)
    +        target_url = auth.login(return_to)
    +        parsed_query = parse_qs(urlparse(target_url)[4])
    +        sso_url = settings_info['idp']['singleSignOnService']['url']
    +        self.assertIn(sso_url, target_url)
    +        self.assertIn('SAMLRequest', parsed_query)
    +        request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0])
    +        self.assertIn('', request)
    +        self.assertNotIn('', request_2)
    +        self.assertIn('Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">testuser@example.com', request_2)
    +        self.assertIn('', request_2)
    +
    +        settings_info['sp']['NameIDFormat'] = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
    +        auth_3 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info)
    +        target_url_3 = auth_3.login(return_to, name_id_value_req='testuser@example.com')
    +        parsed_query_3 = parse_qs(urlparse(target_url_3)[4])
    +        self.assertIn(sso_url, target_url_3)
    +        self.assertIn('SAMLRequest', parsed_query_3)
    +        request_3 = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3['SAMLRequest'][0])
    +        self.assertIn('', request_3)
    +        self.assertIn('Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">testuser@example.com', request_3)
    +        self.assertIn('', request_3)
    +
         def testLogout(self):
             """
             Tests the logout method of the OneLogin_Saml2_Auth class
    @@ -718,12 +825,9 @@ def testLogoutNoSLO(self):
             del settings_info['idp']['singleLogoutService']
             auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info)
     
    -        try:
    +        with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'The IdP does not support Single Log Out'):
                 # The Header of the redirect produces an Exception
                 auth.logout('http://example.com/returnto')
    -            self.assertFalse(True)
    -        except Exception as e:
    -            self.assertIn('The IdP does not support Single Log Out', e.message)
     
         def testLogoutNameIDandSessionIndex(self):
             """
    @@ -763,6 +867,7 @@ def testLogoutNameID(self):
             auth.process_response()
     
             name_id_from_response = auth.get_nameid()
    +        name_id_format_from_response = auth.get_nameid_format()
     
             target_url = auth.logout()
             parsed_query = parse_qs(urlparse(target_url)[4])
    @@ -770,7 +875,21 @@ def testLogoutNameID(self):
             logout_request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0])
     
             name_id_from_request = OneLogin_Saml2_Logout_Request.get_nameid(logout_request)
    +        name_id_format_from_request = OneLogin_Saml2_Logout_Request.get_nameid_format(logout_request)
             self.assertEqual(name_id_from_response, name_id_from_request)
    +        self.assertEqual(name_id_format_from_response, name_id_format_from_request)
    +
    +        new_name_id = "new_name_id"
    +        new_name_id_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
    +        target_url_2 = auth.logout(name_id=new_name_id, name_id_format=new_name_id_format)
    +        parsed_query = parse_qs(urlparse(target_url_2)[4])
    +        self.assertIn('SAMLRequest', parsed_query)
    +        logout_request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0])
    +
    +        name_id_from_request = OneLogin_Saml2_Logout_Request.get_nameid(logout_request)
    +        name_id_format_from_request = OneLogin_Saml2_Logout_Request.get_nameid_format(logout_request)
    +        self.assertEqual(new_name_id, name_id_from_request)
    +        self.assertEqual(new_name_id_format, name_id_format_from_request)
     
         def testSetStrict(self):
             """
    @@ -791,11 +910,164 @@ def testSetStrict(self):
             settings = auth.get_settings()
             self.assertFalse(settings.is_strict())
     
    -        try:
    +        with self.assertRaises(AssertionError):
                 auth.set_strict('42')
    -            self.assertFalse(True)
    -        except Exception as e:
    -            self.assertTrue(isinstance(e, AssertionError))
    +
    +    def testIsAuthenticated(self):
    +        """
    +        Tests the is_authenticated method of the OneLogin_Saml2_Auth
    +        """
    +        request_data = self.get_request()
    +        del request_data['get_data']
    +        message = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
    +        request_data['post_data'] = {
    +            'SAMLResponse': message
    +        }
    +        auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON())
    +        auth.process_response()
    +        self.assertFalse(auth.is_authenticated())
    +
    +        message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
    +        request_data['post_data'] = {
    +            'SAMLResponse': message
    +        }
    +        auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON())
    +        auth.process_response()
    +        self.assertTrue(auth.is_authenticated())
    +
    +    def testGetNameId(self):
    +        """
    +        Tests the get_nameid method of the OneLogin_Saml2_Auth
    +        """
    +        settings = self.loadSettingsJSON()
    +        request_data = self.get_request()
    +        del request_data['get_data']
    +        message = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
    +        request_data['post_data'] = {
    +            'SAMLResponse': message
    +        }
    +        auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
    +        auth.process_response()
    +        self.assertFalse(auth.is_authenticated())
    +        self.assertEqual(auth.get_nameid(), None)
    +
    +        message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
    +        request_data['post_data'] = {
    +            'SAMLResponse': message
    +        }
    +        auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
    +        auth.process_response()
    +        self.assertTrue(auth.is_authenticated())
    +        self.assertEqual("492882615acf31c8096b627245d76ae53036c090", auth.get_nameid())
    +
    +        settings_2 = self.loadSettingsJSON('settings2.json')
    +        message = self.file_contents(join(self.data_path, 'responses', 'signed_message_encrypted_assertion2.xml.base64'))
    +        request_data['post_data'] = {
    +            'SAMLResponse': message
    +        }
    +        auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_2)
    +        auth.process_response()
    +        self.assertTrue(auth.is_authenticated())
    +        self.assertEqual("25ddd7d34a7d79db69167625cda56a320adf2876", auth.get_nameid())
    +
    +    def testGetNameIdFormat(self):
    +        """
    +        Tests the get_nameid_format method of the OneLogin_Saml2_Auth
    +        """
    +        settings = self.loadSettingsJSON()
    +        request_data = self.get_request()
    +        del request_data['get_data']
    +        message = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
    +        request_data['post_data'] = {
    +            'SAMLResponse': message
    +        }
    +        auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
    +        auth.process_response()
    +        self.assertFalse(auth.is_authenticated())
    +        self.assertEqual(auth.get_nameid_format(), None)
    +
    +        message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
    +        request_data['post_data'] = {
    +            'SAMLResponse': message
    +        }
    +        auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
    +        auth.process_response()
    +        self.assertTrue(auth.is_authenticated())
    +        self.assertEqual("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", auth.get_nameid_format())
    +
    +        settings_2 = self.loadSettingsJSON('settings2.json')
    +        message = self.file_contents(join(self.data_path, 'responses', 'signed_message_encrypted_assertion2.xml.base64'))
    +        request_data['post_data'] = {
    +            'SAMLResponse': message
    +        }
    +        auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_2)
    +        auth.process_response()
    +        self.assertTrue(auth.is_authenticated())
    +        self.assertEqual("urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified", auth.get_nameid_format())
    +
    +    def testGetNameIdNameQualifier(self):
    +        """
    +        Tests the get_nameid_nq method of the OneLogin_Saml2_Auth
    +        """
    +        settings = self.loadSettingsJSON()
    +        message = self.file_contents(join(self.data_path, 'responses', 'valid_response_with_namequalifier.xml.base64'))
    +        request_data = self.get_request()
    +        request_data['post_data'] = {
    +            'SAMLResponse': message
    +        }
    +        auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
    +        self.assertIsNone(auth.get_nameid_nq())
    +        auth.process_response()
    +        self.assertTrue(auth.is_authenticated())
    +        self.assertEqual("https://test.example.com/saml/metadata", auth.get_nameid_nq())
    +
    +    def testGetNameIdNameQualifier2(self):
    +        """
    +        Tests the get_nameid_nq method of the OneLogin_Saml2_Auth
    +        """
    +        settings = self.loadSettingsJSON()
    +        message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
    +        request_data = self.get_request()
    +        request_data['post_data'] = {
    +            'SAMLResponse': message
    +        }
    +        auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
    +        self.assertIsNone(auth.get_nameid_nq())
    +        auth.process_response()
    +        self.assertTrue(auth.is_authenticated())
    +        self.assertIsNone(auth.get_nameid_nq())
    +
    +    def testGetNameIdSPNameQualifier(self):
    +        """
    +        Tests the get_nameid_spnq method of the OneLogin_Saml2_Auth
    +        """
    +        settings = self.loadSettingsJSON()
    +        message = self.file_contents(join(self.data_path, 'responses', 'valid_response_with_namequalifier.xml.base64'))
    +        request_data = self.get_request()
    +        request_data['post_data'] = {
    +            'SAMLResponse': message
    +        }
    +        auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
    +        self.assertIsNone(auth.get_nameid_spnq())
    +        auth.process_response()
    +        self.assertTrue(auth.is_authenticated())
    +        self.assertIsNone(auth.get_nameid_spnq())
    +
    +    def testGetNameIdSPNameQualifier2(self):
    +        """
    +        Tests the get_nameid_spnq method of the OneLogin_Saml2_Auth
    +        """
    +        settings = self.loadSettingsJSON()
    +        message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
    +        request_data = self.get_request()
    +        request_data['post_data'] = {
    +            'SAMLResponse': message
    +        }
    +        auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
    +        self.assertIsNone(auth.get_nameid_spnq())
    +        auth.process_response()
    +        self.assertTrue(auth.is_authenticated())
    +        self.assertEqual("http://stuff.com/endpoints/metadata.php", auth.get_nameid_spnq())
     
         def testBuildRequestSignature(self):
             """
    @@ -807,16 +1079,14 @@ def testBuildRequestSignature(self):
             auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings)
     
             signature = auth.build_request_signature(message, relay_state)
    -        valid_signature = 'E17GU1STzanOXxBTKjweB1DovP8aMJdj5BEy0fnGoEslKdP6hpPc3enjT/bu7I8D8QzLoir8SxZVWdUDXgIxJIEgfK5snr+jJwfc5U2HujsOa/Xb3c4swoyPcyQhcxLRDhDjPq5cQxJfYoPeElvCuI6HAD1mtdd5PS/xDvbIxuw='
    +        valid_signature = 'Pb1EXAX5TyipSJ1SndEKZstLQTsT+1D00IZAhEepBM+OkAZQSToivu3njgJu47HZiZAqgXZFgloBuuWE/+GdcSsRYEMkEkiSDWTpUr25zKYLJDSg6GNo6iAHsKSuFt46Z54Xe/keYxYP03Hdy97EwuuSjBzzgRc5tmpV+KC7+a0='
             self.assertEqual(signature, valid_signature)
     
             settings['sp']['privatekey'] = ''
             settings['custom_base_path'] = u'invalid/path/'
             auth2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings)
    -        try:
    +        with self.assertRaisesRegexp(OneLogin_Saml2_Error, "Trying to sign the SAMLRequest but can't load the SP private key"):
                 auth2.build_request_signature(message, relay_state)
    -        except Exception as e:
    -            self.assertIn("Trying to sign the SAMLRequest but can't load the SP private key", e.message)
     
         def testBuildResponseSignature(self):
             """
    @@ -834,7 +1104,186 @@ def testBuildResponseSignature(self):
             settings['sp']['privatekey'] = ''
             settings['custom_base_path'] = u'invalid/path/'
             auth2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings)
    -        try:
    +        with self.assertRaisesRegexp(OneLogin_Saml2_Error, "Trying to sign the SAMLResponse but can't load the SP private key"):
                 auth2.build_response_signature(message, relay_state)
    -        except Exception as e:
    -            self.assertIn("Trying to sign the SAMLResponse but can't load the SP private key", e.message)
    +
    +    def testGetLastRequestID(self):
    +        settings_info = self.loadSettingsJSON()
    +        request_data = self.get_request()
    +        auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_info)
    +
    +        auth.login()
    +        id1 = auth.get_last_request_id()
    +        self.assertNotEqual(id1, None)
    +
    +        auth.logout()
    +        id2 = auth.get_last_request_id()
    +        self.assertNotEqual(id2, None)
    +
    +        self.assertNotEqual(id1, id2)
    +
    +    def testGetLastSAMLResponse(self):
    +        settings = self.loadSettingsJSON()
    +        message = self.file_contents(join(self.data_path, 'responses', 'signed_message_response.xml.base64'))
    +        message_wrapper = {'post_data': {'SAMLResponse': message}}
    +        auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings)
    +        auth.process_response()
    +        expected_message = self.file_contents(join(self.data_path, 'responses', 'pretty_signed_message_response.xml'))
    +        self.assertEqual(auth.get_last_response_xml(True), expected_message)
    +
    +        # with encrypted assertion
    +        message = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64'))
    +        message_wrapper = {'post_data': {'SAMLResponse': message}}
    +        auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings)
    +        auth.process_response()
    +        decrypted_response = self.file_contents(join(self.data_path, 'responses', 'decrypted_valid_encrypted_assertion.xml'))
    +        self.assertEqual(auth.get_last_response_xml(False), decrypted_response)
    +        pretty_decrypted_response = self.file_contents(join(self.data_path, 'responses', 'pretty_decrypted_valid_encrypted_assertion.xml'))
    +        self.assertEqual(auth.get_last_response_xml(True), pretty_decrypted_response)
    +
    +    def testGetLastAuthnRequest(self):
    +        settings = self.loadSettingsJSON()
    +        auth = OneLogin_Saml2_Auth({'http_host': 'localhost', 'script_name': 'thing'}, old_settings=settings)
    +        auth.login()
    +        expectedFragment = (
    +            'Destination="http://idp.example.com/SSOService.php"\n'
    +            '    ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"\n'
    +            '    AssertionConsumerServiceURL="http://stuff.com/endpoints/endpoints/acs.php"\n'
    +            '    >\n'
    +            '    http://stuff.com/endpoints/metadata.php\n'
    +            '    \n'
    +            '    \n'
    +            '        urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport\n'
    +            '    \n'
    +        )
    +        self.assertIn(expectedFragment, auth.get_last_request_xml())
    +
    +    def testGetLastAuthnContexts(self):
    +        settings = self.loadSettingsJSON()
    +        request_data = self.get_request()
    +        message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
    +        del request_data['get_data']
    +        request_data['post_data'] = {
    +            'SAMLResponse': message
    +        }
    +        auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
    +
    +        auth.process_response()
    +        self.assertEqual(auth.get_last_authn_contexts(), ['urn:oasis:names:tc:SAML:2.0:ac:classes:Password'])
    +
    +    def testGetLastLogoutRequest(self):
    +        settings = self.loadSettingsJSON()
    +        auth = OneLogin_Saml2_Auth({'http_host': 'localhost', 'script_name': 'thing'}, old_settings=settings)
    +        auth.logout()
    +        expectedFragment = (
    +            '        Destination="http://idp.example.com/SingleLogoutService.php">\n'
    +            '        http://stuff.com/endpoints/metadata.php\n'
    +            '        http://idp.example.com/\n'
    +            '        \n    '
    +        )
    +        self.assertIn(expectedFragment, auth.get_last_request_xml())
    +
    +        request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml'))
    +        message = OneLogin_Saml2_Utils.deflate_and_base64_encode(request)
    +        message_wrapper = {'get_data': {'SAMLRequest': message}}
    +        auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings)
    +        auth.process_slo()
    +        self.assertEqual(request, auth.get_last_request_xml())
    +
    +    def testGetLastLogoutResponse(self):
    +        settings = self.loadSettingsJSON()
    +        request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml'))
    +        message = OneLogin_Saml2_Utils.deflate_and_base64_encode(request)
    +        message_wrapper = {'get_data': {'SAMLRequest': message}}
    +        auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings)
    +        auth.process_slo()
    +        expectedFragment = (
    +            'Destination="http://idp.example.com/SingleLogoutService.php"\n'
    +            '                      InResponseTo="ONELOGIN_21584ccdfaca36a145ae990442dcd96bfe60151e"\n>\n'
    +            '    http://stuff.com/endpoints/metadata.php\n'
    +            '    \n'
    +            '        \n'
    +            '    \n'
    +            ''
    +        )
    +        self.assertIn(expectedFragment, auth.get_last_response_xml())
    +
    +        response = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response.xml'))
    +        message = OneLogin_Saml2_Utils.deflate_and_base64_encode(response)
    +        message_wrapper = {'get_data': {'SAMLResponse': message}}
    +        auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings)
    +        auth.process_slo()
    +        self.assertEqual(response, auth.get_last_response_xml())
    +
    +    def testGetInfoFromLastResponseReceived(self):
    +        """
    +        Tests the get_last_message_id, get_last_assertion_id and get_last_assertion_not_on_or_after
    +        of the OneLogin_Saml2_Auth class
    +        """
    +        settings = self.loadSettingsJSON()
    +        request_data = self.get_request()
    +        message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
    +        del request_data['get_data']
    +        request_data['post_data'] = {
    +            'SAMLResponse': message
    +        }
    +        auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
    +
    +        auth.process_response()
    +        self.assertEqual(auth.get_last_message_id(), 'pfx42be40bf-39c3-77f0-c6ae-8bf2e23a1a2e')
    +        self.assertEqual(auth.get_last_assertion_id(), 'pfx57dfda60-b211-4cda-0f63-6d5deb69e5bb')
    +        self.assertIsNone(auth.get_last_assertion_not_on_or_after())
    +
    +        # NotOnOrAfter is only calculated with strict = true
    +        # If invalid, response id and assertion id are not obtained
    +
    +        settings['strict'] = True
    +        auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
    +        auth.process_response()
    +        self.assertNotEqual(len(auth.get_errors()), 0)
    +        self.assertIsNone(auth.get_last_message_id())
    +        self.assertIsNone(auth.get_last_assertion_id())
    +        self.assertIsNone(auth.get_last_assertion_not_on_or_after())
    +
    +        request_data['https'] = 'on'
    +        request_data['http_host'] = 'pitbulk.no-ip.org'
    +        request_data['script_name'] = '/newonelogin/demo1/index.php?acs'
    +        auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
    +        auth.process_response()
    +        self.assertEqual(len(auth.get_errors()), 0)
    +        self.assertEqual(auth.get_last_message_id(), 'pfx42be40bf-39c3-77f0-c6ae-8bf2e23a1a2e')
    +        self.assertEqual(auth.get_last_assertion_id(), 'pfx57dfda60-b211-4cda-0f63-6d5deb69e5bb')
    +        self.assertEqual(auth.get_last_assertion_not_on_or_after(), 2671081021)
    +
    +    def testGetIdFromLogoutRequest(self):
    +        """
    +        Tests the get_last_message_id of the OneLogin_Saml2_Auth class
    +        Case Valid Logout request
    +        """
    +        settings = self.loadSettingsJSON()
    +        request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml'))
    +        message = OneLogin_Saml2_Utils.deflate_and_base64_encode(request)
    +        message_wrapper = {'get_data': {'SAMLRequest': message}}
    +        auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings)
    +        auth.process_slo()
    +        self.assertIn(auth.get_last_message_id(), 'ONELOGIN_21584ccdfaca36a145ae990442dcd96bfe60151e')
    +
    +    def testGetIdFromLogoutResponse(self):
    +        """
    +        Tests the get_last_message_id of the OneLogin_Saml2_Auth class
    +        Case Valid Logout response
    +        """
    +        settings = self.loadSettingsJSON()
    +        response = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response.xml'))
    +        message = OneLogin_Saml2_Utils.deflate_and_base64_encode(response)
    +        message_wrapper = {'get_data': {'SAMLResponse': message}}
    +        auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings)
    +        auth.process_slo()
    +        self.assertIn(auth.get_last_message_id(), '_f9ee61bd9dbf63606faa9ae3b10548d5b3656fb859')
    +
    +
    +if __name__ == '__main__':
    +    runner = unittest.TextTestRunner()
    +    unittest.main(testRunner=runner)
    diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py
    index 2d5a0a2f..5190d84a 100644
    --- a/tests/src/OneLogin/saml2_tests/authn_request_test.py
    +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py
    @@ -1,7 +1,7 @@
     # -*- coding: utf-8 -*-
     
    -# Copyright (c) 2014, OneLogin, Inc.
    -# All rights reserved.
    +# Copyright (c) 2010-2018 OneLogin, Inc.
    +# MIT License
     
     from base64 import b64decode
     import json
    @@ -17,8 +17,8 @@
     
     
     class OneLogin_Saml2_Authn_Request_Test(unittest.TestCase):
    -    def loadSettingsJSON(self):
    -        filename = join(dirname(__file__), '..', '..', '..', 'settings', 'settings1.json')
    +    def loadSettingsJSON(self, filename='settings1.json'):
    +        filename = join(dirname(dirname(dirname(dirname(__file__)))), 'settings', filename)
             if exists(filename):
                 stream = open(filename, 'r')
                 settings = json.load(stream)
    @@ -62,6 +62,34 @@ def testCreateRequest(self):
             self.assertRegexpMatches(inflated, '^', inflated)
    +
    +        authn_request_2 = OneLogin_Saml2_Authn_Request(settings, name_id_value_req='testuser@example.com')
    +        authn_request_encoded_2 = authn_request_2.get_request()
    +        decoded_2 = b64decode(authn_request_encoded_2)
    +        inflated_2 = decompress(decoded_2, -15)
    +        self.assertRegexpMatches(inflated_2, '^testuser@example.com', inflated_2)
    +        self.assertIn('', inflated_2)
    +
    +        saml_settings['sp']['NameIDFormat'] = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
    +        settings = OneLogin_Saml2_Settings(saml_settings)
    +        authn_request_3 = OneLogin_Saml2_Authn_Request(settings, name_id_value_req='testuser@example.com')
    +        authn_request_encoded_3 = authn_request_3.get_request()
    +        decoded_3 = b64decode(authn_request_encoded_3)
    +        inflated_3 = decompress(decoded_3, -15)
    +        self.assertRegexpMatches(inflated_3, '^testuser@example.com', inflated_3)
    +        self.assertIn('', inflated_3)
    +
         def testCreateDeflatedSAMLRequestURLParameter(self):
             """
             Tests the OneLogin_Saml2_Authn_Request Constructor.
    @@ -183,7 +304,7 @@ def testCreateDeflatedSAMLRequestURLParameter(self):
                 'SAMLRequest': authn_request.get_request()
             }
             auth_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SSOService.php', parameters, True)
    -        self.assertRegexpMatches(auth_url, '^http://idp\.example\.com\/SSOService\.php\?SAMLRequest=')
    +        self.assertRegexpMatches(auth_url, r'^http://idp\.example\.com\/SSOService\.php\?SAMLRequest=')
             exploded = urlparse(auth_url)
             exploded = parse_qs(exploded[4])
             payload = exploded['SAMLRequest'][0]
    @@ -212,7 +333,7 @@ def testCreateEncSAMLRequest(self):
                 'SAMLRequest': authn_request.get_request()
             }
             auth_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SSOService.php', parameters, True)
    -        self.assertRegexpMatches(auth_url, '^http://idp\.example\.com\/SSOService\.php\?SAMLRequest=')
    +        self.assertRegexpMatches(auth_url, r'^http://idp\.example\.com\/SSOService\.php\?SAMLRequest=')
             exploded = urlparse(auth_url)
             exploded = parse_qs(exploded[4])
             payload = exploded['SAMLRequest'][0]
    @@ -220,7 +341,36 @@ def testCreateEncSAMLRequest(self):
             inflated = decompress(decoded, -15)
     
             self.assertRegexpMatches(inflated, '^')
    +        self.assertRegexpMatches(inflated, 'AssertionConsumerServiceURL="http://stuff.com/endpoints/endpoints/acs.php"')
             self.assertRegexpMatches(inflated, 'http://stuff.com/endpoints/metadata.php')
             self.assertRegexpMatches(inflated, 'Format="urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted"')
             self.assertRegexpMatches(inflated, 'ProviderName="SP prueba"')
    +
    +    def testAttributeConsumingService(self):
    +        """
    +        Tests that the attributeConsumingServiceIndex is present as an attribute
    +        """
    +        saml_settings = self.loadSettingsJSON()
    +        settings = OneLogin_Saml2_Settings(saml_settings)
    +
    +        authn_request = OneLogin_Saml2_Authn_Request(settings)
    +        authn_request_encoded = authn_request.get_request()
    +        decoded = b64decode(authn_request_encoded)
    +        inflated = decompress(decoded, -15)
    +
    +        self.assertNotIn('AttributeConsumingServiceIndex="1"', inflated)
    +
    +        saml_settings = self.loadSettingsJSON('settings4.json')
    +        settings = OneLogin_Saml2_Settings(saml_settings)
    +
    +        authn_request = OneLogin_Saml2_Authn_Request(settings)
    +        authn_request_encoded = authn_request.get_request()
    +        decoded = b64decode(authn_request_encoded)
    +        inflated = decompress(decoded, -15)
    +
    +        self.assertRegexpMatches(inflated, 'AttributeConsumingServiceIndex="1"')
    +
    +
    +if __name__ == '__main__':
    +    runner = unittest.TextTestRunner()
    +    unittest.main(testRunner=runner)
    diff --git a/tests/src/OneLogin/saml2_tests/error_test.py b/tests/src/OneLogin/saml2_tests/error_test.py
    index 76e24308..9cc861ad 100644
    --- a/tests/src/OneLogin/saml2_tests/error_test.py
    +++ b/tests/src/OneLogin/saml2_tests/error_test.py
    @@ -1,7 +1,7 @@
     # -*- coding: utf-8 -*-
     
    -# Copyright (c) 2014, OneLogin, Inc.
    -# All rights reserved.
    +# Copyright (c) 2010-2018 OneLogin, Inc.
    +# MIT License
     
     import unittest
     from onelogin.saml2.errors import OneLogin_Saml2_Error
    @@ -11,7 +11,11 @@ class OneLogin_Saml2_Error_Test(unittest.TestCase):
         """
         Tests the OneLogin_Saml2_Error Constructor.
         """
    -
         def runTest(self):
             exception = OneLogin_Saml2_Error('test')
             self.assertEqual(exception.message, 'test')
    +
    +
    +if __name__ == '__main__':
    +    runner = unittest.TextTestRunner()
    +    unittest.main(testRunner=runner)
    diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
    new file mode 100644
    index 00000000..0359bb5a
    --- /dev/null
    +++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
    @@ -0,0 +1,647 @@
    +# -*- coding: utf-8 -*-
    +
    +# Copyright (c) 2010-2018 OneLogin, Inc.
    +# MIT License
    +
    +
    +from copy import deepcopy
    +import json
    +from os.path import dirname, join, exists
    +from lxml.etree import XMLSyntaxError
    +import unittest
    +from urllib2 import URLError
    +
    +from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser
    +from onelogin.saml2.constants import OneLogin_Saml2_Constants
    +
    +
    +class OneLogin_Saml2_IdPMetadataParser_Test(unittest.TestCase):
    +    # Instruct unittest to not hide diffs upon test failure, even for complex
    +    # dictionaries. This prevents the message "Diff is 907 characters long.
    +    # Set self.maxDiff to None to see it." from showing up.
    +    maxDiff = None
    +
    +    data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data')
    +    settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings')
    +
    +    def loadSettingsJSON(self, filename='settings1.json'):
    +        filename = join(self.settings_path, filename)
    +        if exists(filename):
    +            stream = open(filename, 'r')
    +            settings = json.load(stream)
    +            stream.close()
    +            return settings
    +        else:
    +            raise Exception('Settings json file does not exist')
    +
    +    def file_contents(self, filename):
    +        f = open(filename, 'r')
    +        content = f.read()
    +        f.close()
    +        return content
    +
    +    def testGetMetadata(self):
    +        """
    +        Tests the get_metadata method of the OneLogin_Saml2_IdPMetadataParser
    +        """
    +        with self.assertRaises(Exception):
    +            data = OneLogin_Saml2_IdPMetadataParser.get_metadata('http://google.es')
    +
    +        try:
    +            data = OneLogin_Saml2_IdPMetadataParser.get_metadata('https://idp.testshib.org/idp/shibboleth')
    +        except URLError:
    +            data = self.file_contents(join(self.data_path, 'metadata', 'testshib-providers.xml'))
    +        self.assertTrue(data is not None and data is not {})
    +
    +    def testParseRemote(self):
    +        """
    +        Tests the parse_remote method of the OneLogin_Saml2_IdPMetadataParser
    +        """
    +        with self.assertRaises(Exception):
    +            data = OneLogin_Saml2_IdPMetadataParser.parse_remote('http://google.es')
    +
    +        try:
    +            data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://idp.testshib.org/idp/shibboleth')
    +        except URLError:
    +            xml = self.file_contents(join(self.data_path, 'metadata', 'testshib-providers.xml'))
    +            data = OneLogin_Saml2_IdPMetadataParser.parse(xml)
    +
    +        self.assertTrue(data is not None and data is not {})
    +        expected_settings_json = """
    +        {
    +            "sp": {
    +                "NameIDFormat": "urn:mace:shibboleth:1.0:nameIdentifier"
    +            },
    +            "idp": {
    +                "x509cert": "MIIDAzCCAeugAwIBAgIVAPX0G6LuoXnKS0Muei006mVSBXbvMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNVBAMMEGlkcC50ZXN0c2hpYi5vcmcwHhcNMTYwODIzMjEyMDU0WhcNMzYwODIzMjEyMDU0WjAbMRkwFwYDVQQDDBBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAg9C4J2DiRTEhJAWzPt1S3ryhm3M2P3hPpwJwvt2q948vdTUxhhvNMuc3M3S4WNh6JYBs53R+YmjqJAII4ShMGNEmlGnSVfHorex7IxikpuDPKV3SNf28mCAZbQrX+hWA+ann/uifVzqXktOjs6DdzdBnxoVhniXgC8WCJwKcx6JO/hHsH1rG/0DSDeZFpTTcZHj4S9MlLNUtt5JxRzV/MmmB3ObaX0CMqsSWUOQeE4nylSlp5RWHCnx70cs9kwz5WrflnbnzCeHU2sdbNotBEeTHot6a2cj/pXlRJIgPsrL/4VSicPZcGYMJMPoLTJ8mdy6mpR6nbCmP7dVbCIm/DQIDAQABoz4wPDAdBgNVHQ4EFgQUUfaDa2mPi24x09yWp1OFXmZ2GPswGwYDVR0RBBQwEoIQaWRwLnRlc3RzaGliLm9yZzANBgkqhkiG9w0BAQsFAAOCAQEASKKgqTxhqBzROZ1eVy++si+eTTUQZU4+8UywSKLia2RattaAPMAcXUjO+3cYOQXLVASdlJtt+8QPdRkfp8SiJemHPXC8BES83pogJPYEGJsKo19l4XFJHPnPy+Dsn3mlJyOfAa8RyWBS80u5lrvAcr2TJXt9fXgkYs7BOCigxtZoR8flceGRlAZ4p5FPPxQR6NDYb645jtOTMVr3zgfjP6Wh2dt+2p04LG7ENJn8/gEwtXVuXCsPoSCDx9Y0QmyXTJNdV1aB0AhORkWPlFYwp+zOyOIR+3m1+pqWFpn0eT/HrxpdKa74FA3R2kq4R7dXe4G0kUgXTdqXMLRKhDgdmA==",
    +                "entityId": "https://idp.testshib.org/idp/shibboleth",
    +                "singleSignOnService": {
    +                    "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO",
    +                    "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
    +                }
    +            }
    +        }
    +        """
    +        expected_settings = json.loads(expected_settings_json)
    +        self.assertEqual(expected_settings, data)
    +
    +    def testParse(self):
    +        """
    +        Tests the parse method of the OneLogin_Saml2_IdPMetadataParser
    +        """
    +        with self.assertRaises(XMLSyntaxError):
    +            data = OneLogin_Saml2_IdPMetadataParser.parse('')
    +
    +        xml_sp_metadata = self.file_contents(join(self.data_path, 'metadata', 'metadata_settings1.xml'))
    +        data = OneLogin_Saml2_IdPMetadataParser.parse(xml_sp_metadata)
    +        self.assertEqual({}, data)
    +
    +        xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata.xml'))
    +        data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata)
    +
    +        # W/o further specification, expect to get the redirect binding SSO
    +        # URL extracted.
    +        expected_settings_json = """
    +        {
    +          "idp": {
    +            "singleSignOnService": {
    +              "url": "https://app.onelogin.com/trust/saml2/http-post/sso/383123",
    +              "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
    +            },
    +            "x509cert": "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9uZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sTgf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0mTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SFzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNVHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAOBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHuAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcVgG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClPTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWuQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh781sE=",
    +            "entityId": "https://app.onelogin.com/saml/metadata/383123"
    +          },
    +          "sp": {
    +            "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
    +          }
    +        }
    +        """
    +        expected_settings = json.loads(expected_settings_json)
    +        self.assertEqual(expected_settings, data)
    +
    +    def test_parse_testshib_required_binding_sso_redirect(self):
    +        """
    +        Test with testshib metadata.
    +        Especially test extracting SSO with REDIRECT binding.
    +        Note that the testshib metadata does not contain an SLO specification
    +        in the first  tag.
    +        """
    +        expected_settings_json = """
    +        {
    +          "sp": {
    +            "NameIDFormat": "urn:mace:shibboleth:1.0:nameIdentifier"
    +          },
    +          "idp": {
    +            "entityId": "https://idp.testshib.org/idp/shibboleth",
    +            "singleSignOnService": {
    +              "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO",
    +              "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
    +            },
    +            "x509cert": "MIIDAzCCAeugAwIBAgIVAPX0G6LuoXnKS0Muei006mVSBXbvMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNVBAMMEGlkcC50ZXN0c2hpYi5vcmcwHhcNMTYwODIzMjEyMDU0WhcNMzYwODIzMjEyMDU0WjAbMRkwFwYDVQQDDBBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAg9C4J2DiRTEhJAWzPt1S3ryhm3M2P3hPpwJwvt2q948vdTUxhhvNMuc3M3S4WNh6JYBs53R+YmjqJAII4ShMGNEmlGnSVfHorex7IxikpuDPKV3SNf28mCAZbQrX+hWA+ann/uifVzqXktOjs6DdzdBnxoVhniXgC8WCJwKcx6JO/hHsH1rG/0DSDeZFpTTcZHj4S9MlLNUtt5JxRzV/MmmB3ObaX0CMqsSWUOQeE4nylSlp5RWHCnx70cs9kwz5WrflnbnzCeHU2sdbNotBEeTHot6a2cj/pXlRJIgPsrL/4VSicPZcGYMJMPoLTJ8mdy6mpR6nbCmP7dVbCIm/DQIDAQABoz4wPDAdBgNVHQ4EFgQUUfaDa2mPi24x09yWp1OFXmZ2GPswGwYDVR0RBBQwEoIQaWRwLnRlc3RzaGliLm9yZzANBgkqhkiG9w0BAQsFAAOCAQEASKKgqTxhqBzROZ1eVy++si+eTTUQZU4+8UywSKLia2RattaAPMAcXUjO+3cYOQXLVASdlJtt+8QPdRkfp8SiJemHPXC8BES83pogJPYEGJsKo19l4XFJHPnPy+Dsn3mlJyOfAa8RyWBS80u5lrvAcr2TJXt9fXgkYs7BOCigxtZoR8flceGRlAZ4p5FPPxQR6NDYb645jtOTMVr3zgfjP6Wh2dt+2p04LG7ENJn8/gEwtXVuXCsPoSCDx9Y0QmyXTJNdV1aB0AhORkWPlFYwp+zOyOIR+3m1+pqWFpn0eT/HrxpdKa74FA3R2kq4R7dXe4G0kUgXTdqXMLRKhDgdmA=="
    +          }
    +        }
    +        """
    +        try:
    +            xmldoc = OneLogin_Saml2_IdPMetadataParser.get_metadata(
    +                'https://idp.testshib.org/idp/shibboleth')
    +        except Exception:
    +            xmldoc = self.file_contents(join(self.data_path, 'metadata', 'testshib-providers.xml'))
    +
    +        # Parse, require SSO REDIRECT binding, implicitly.
    +        settings1 = OneLogin_Saml2_IdPMetadataParser.parse(xmldoc)
    +        # Parse, require SSO REDIRECT binding, explicitly.
    +        settings2 = OneLogin_Saml2_IdPMetadataParser.parse(
    +            xmldoc,
    +            required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT
    +        )
    +        expected_settings = json.loads(expected_settings_json)
    +        self.assertEqual(expected_settings, settings1)
    +        self.assertEqual(expected_settings, settings2)
    +
    +    def test_parse_testshib_required_binding_sso_post(self):
    +        """
    +        Test with testshib metadata.
    +        Especially test extracting SSO with POST binding.
    +        """
    +        expected_settings_json = """
    +        {
    +          "sp": {
    +            "NameIDFormat": "urn:mace:shibboleth:1.0:nameIdentifier"
    +          },
    +          "idp": {
    +            "x509cert": "MIIDAzCCAeugAwIBAgIVAPX0G6LuoXnKS0Muei006mVSBXbvMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNVBAMMEGlkcC50ZXN0c2hpYi5vcmcwHhcNMTYwODIzMjEyMDU0WhcNMzYwODIzMjEyMDU0WjAbMRkwFwYDVQQDDBBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAg9C4J2DiRTEhJAWzPt1S3ryhm3M2P3hPpwJwvt2q948vdTUxhhvNMuc3M3S4WNh6JYBs53R+YmjqJAII4ShMGNEmlGnSVfHorex7IxikpuDPKV3SNf28mCAZbQrX+hWA+ann/uifVzqXktOjs6DdzdBnxoVhniXgC8WCJwKcx6JO/hHsH1rG/0DSDeZFpTTcZHj4S9MlLNUtt5JxRzV/MmmB3ObaX0CMqsSWUOQeE4nylSlp5RWHCnx70cs9kwz5WrflnbnzCeHU2sdbNotBEeTHot6a2cj/pXlRJIgPsrL/4VSicPZcGYMJMPoLTJ8mdy6mpR6nbCmP7dVbCIm/DQIDAQABoz4wPDAdBgNVHQ4EFgQUUfaDa2mPi24x09yWp1OFXmZ2GPswGwYDVR0RBBQwEoIQaWRwLnRlc3RzaGliLm9yZzANBgkqhkiG9w0BAQsFAAOCAQEASKKgqTxhqBzROZ1eVy++si+eTTUQZU4+8UywSKLia2RattaAPMAcXUjO+3cYOQXLVASdlJtt+8QPdRkfp8SiJemHPXC8BES83pogJPYEGJsKo19l4XFJHPnPy+Dsn3mlJyOfAa8RyWBS80u5lrvAcr2TJXt9fXgkYs7BOCigxtZoR8flceGRlAZ4p5FPPxQR6NDYb645jtOTMVr3zgfjP6Wh2dt+2p04LG7ENJn8/gEwtXVuXCsPoSCDx9Y0QmyXTJNdV1aB0AhORkWPlFYwp+zOyOIR+3m1+pqWFpn0eT/HrxpdKa74FA3R2kq4R7dXe4G0kUgXTdqXMLRKhDgdmA==",
    +            "entityId": "https://idp.testshib.org/idp/shibboleth",
    +            "singleSignOnService": {
    +              "url": "https://idp.testshib.org/idp/profile/SAML2/POST/SSO",
    +              "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
    +            }
    +          }
    +        }
    +        """
    +        try:
    +            xmldoc = OneLogin_Saml2_IdPMetadataParser.get_metadata(
    +                'https://idp.testshib.org/idp/shibboleth')
    +        except URLError:
    +            xmldoc = self.file_contents(join(self.data_path, 'metadata', 'testshib-providers.xml'))
    +
    +        # Parse, require POST binding.
    +        settings = OneLogin_Saml2_IdPMetadataParser.parse(
    +            xmldoc,
    +            required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST
    +        )
    +        expected_settings = json.loads(expected_settings_json)
    +        self.assertEqual(expected_settings, settings)
    +
    +    def test_parse_required_binding_all(self):
    +        """
    +        Test all combinations of the `require_slo_binding` and
    +        `require_sso_binding` parameters.
    +        Note: IdP metadata contains a single logout (SLO)
    +        service and does not specify any endpoint for the POST binding.
    +        """
    +        expected_settings_json = """
    +        {
    +          "sp": {
    +            "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
    +          },
    +          "idp": {
    +            "entityId": "urn:example:idp",
    +            "x509cert": "MIIDPDCCAiQCCQDydJgOlszqbzANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEQMA4GA1UEChMHSmFua3lDbzESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTE0MDMxMjE5NDYzM1oXDTI3MTExOTE5NDYzM1owYDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xEDAOBgNVBAoTB0phbmt5Q28xEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMGvJpRTTasRUSPqcbqCG+ZnTAurnu0vVpIG9lzExnh11o/BGmzu7lB+yLHcEdwrKBBmpepDBPCYxpVajvuEhZdKFx/Fdy6j5mH3rrW0Bh/zd36CoUNjbbhHyTjeM7FN2yF3u9lcyubuvOzr3B3gX66IwJlU46+wzcQVhSOlMk2tXR+fIKQExFrOuK9tbX3JIBUqItpI+HnAow509CnM134svw8PTFLkR6/CcMqnDfDK1m993PyoC1Y+N4X9XkhSmEQoAlAHPI5LHrvuujM13nvtoVYvKYoj7ScgumkpWNEvX652LfXOnKYlkB8ZybuxmFfIkzedQrbJsyOhfL03cMECAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAeHwzqwnzGEkxjzSD47imXaTqtYyETZow7XwBc0ZaFS50qRFJUgKTAmKS1xQBP/qHpStsROT35DUxJAE6NY1Kbq3ZbCuhGoSlY0L7VzVT5tpu4EY8+Dq/u2EjRmmhoL7UkskvIZ2n1DdERtd+YUMTeqYl9co43csZwDno/IKomeN5qaPc39IZjikJ+nUC6kPFKeu/3j9rgHNlRtocI6S1FdtFz9OZMQlpr0JbUt2T3xS/YoQJn6coDmJL5GTiiKM6cOe+Ur1VwzS1JEDbSS2TWWhzq8ojLdrotYLGd9JOsoQhElmz+tMfCFQUFLExinPAyy7YHlSiVX13QH2XTu/iQQ==",
    +            "singleSignOnService": {
    +              "url": "http://idp.example.com",
    +              "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
    +            },
    +            "singleLogoutService": {
    +              "url": "http://idp.example.com/logout",
    +              "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
    +            }
    +          }
    +        }
    +        """
    +        xmldoc = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata2.xml'))
    +
    +        expected_settings = json.loads(expected_settings_json)
    +
    +        # Parse, require SLO and SSO REDIRECT binding, implicitly.
    +        settings1 = OneLogin_Saml2_IdPMetadataParser.parse(xmldoc)
    +
    +        # Parse, require SLO and SSO REDIRECT binding, explicitly.
    +        settings2 = OneLogin_Saml2_IdPMetadataParser.parse(
    +            xmldoc,
    +            required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT,
    +            required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT
    +        )
    +        expected_settings1_2 = deepcopy(expected_settings)
    +        self.assertEqual(expected_settings1_2, settings1)
    +        self.assertEqual(expected_settings1_2, settings2)
    +
    +        settings3 = OneLogin_Saml2_IdPMetadataParser.parse(
    +            xmldoc,
    +            required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST,
    +            required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST
    +        )
    +
    +        expected_settings3 = deepcopy(expected_settings)
    +        del expected_settings3['idp']['singleLogoutService']
    +        del expected_settings3['idp']['singleSignOnService']
    +        self.assertEqual(expected_settings3, settings3)
    +
    +        settings4 = OneLogin_Saml2_IdPMetadataParser.parse(
    +            xmldoc,
    +            required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST,
    +            required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT
    +        )
    +        settings5 = OneLogin_Saml2_IdPMetadataParser.parse(
    +            xmldoc,
    +            required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST
    +        )
    +        expected_settings4_5 = deepcopy(expected_settings)
    +        del expected_settings4_5['idp']['singleSignOnService']
    +        self.assertEqual(expected_settings4_5, settings4)
    +        self.assertEqual(expected_settings4_5, settings5)
    +
    +        settings6 = OneLogin_Saml2_IdPMetadataParser.parse(
    +            xmldoc,
    +            required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT,
    +            required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST
    +        )
    +        settings7 = OneLogin_Saml2_IdPMetadataParser.parse(
    +            xmldoc,
    +            required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST
    +        )
    +        expected_settings6_7 = deepcopy(expected_settings)
    +        del expected_settings6_7['idp']['singleLogoutService']
    +        self.assertEqual(expected_settings6_7, settings6)
    +        self.assertEqual(expected_settings6_7, settings7)
    +
    +    def test_parse_with_entity_id(self):
    +        """
    +        Tests the parse method of the OneLogin_Saml2_IdPMetadataParser
    +        Case: Provide entity_id to identify the desired IdPDescriptor from
    +              EntitiesDescriptor
    +        """
    +        xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_multiple_descriptors.xml'))
    +
    +        # should find first descriptor
    +        data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata)
    +        self.assertEqual("https://foo.example.com/access/saml/idp.xml", data["idp"]["entityId"])
    +
    +        # should find desired descriptor
    +        data2 = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata, entity_id="https://bar.example.com/access/saml/idp.xml")
    +        self.assertEqual("https://bar.example.com/access/saml/idp.xml", data2["idp"]["entityId"])
    +
    +        expected_settings_json = """
    +        {
    +            "sp": {
    +                "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
    +            },
    +            "idp": {
    +                "singleLogoutService": {
    +                    "url": "https://hello.example.com/access/saml/logout",
    +                    "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
    +                },
    +                "entityId": "https://bar.example.com/access/saml/idp.xml",
    +                "x509cert": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURxekNDQXhTZ0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBRENCaGpFTE1Ba0dBMVVFQmhNQ1FWVXgKRERBS0JnTlZCQWdUQTA1VFZ6RVBNQTBHQTFVRUJ4TUdVM2xrYm1WNU1Rd3dDZ1lEVlFRS0RBTlFTVlF4Q1RBSApCZ05WQkFzTUFERVlNQllHQTFVRUF3d1BiR0YzY21WdVkyVndhWFF1WTI5dE1TVXdJd1lKS29aSWh2Y05BUWtCCkRCWnNZWGR5Wlc1alpTNXdhWFJBWjIxaGFXd3VZMjl0TUI0WERURXlNRFF4T1RJeU5UUXhPRm9YRFRNeU1EUXgKTkRJeU5UUXhPRm93Z1lZeEN6QUpCZ05WQkFZVEFrRlZNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVApCbE41Wkc1bGVURU1NQW9HQTFVRUNnd0RVRWxVTVFrd0J3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psCmJtTmxjR2wwTG1OdmJURWxNQ01HQ1NxR1NJYjNEUUVKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnYKYlRDQm56QU5CZ2txaGtpRzl3MEJBUUVGQUFPQmpRQXdnWWtDZ1lFQXFqaWUzUjJvaStwRGFldndJeXMvbWJVVApubkdsa3h0ZGlrcnExMXZleHd4SmlQTmhtaHFSVzNtVXVKRXpsbElkVkw2RW14R1lUcXBxZjkzSGxoa3NhZUowCjhVZ2pQOVVtTVlyaFZKdTFqY0ZXVjdmei9yKzIxL2F3VG5EVjlzTVlRcXVJUllZeTdiRzByMU9iaXdkb3ZudGsKN2dGSTA2WjB2WmFjREU1Ym9xVUNBd0VBQWFPQ0FTVXdnZ0VoTUFrR0ExVWRFd1FDTUFBd0N3WURWUjBQQkFRRApBZ1VnTUIwR0ExVWREZ1FXQkJTUk9OOEdKOG8rOGpnRnRqa3R3WmRxeDZCUnlUQVRCZ05WSFNVRUREQUtCZ2dyCkJnRUZCUWNEQVRBZEJnbGdoa2dCaHZoQ0FRMEVFQllPVkdWemRDQllOVEE1SUdObGNuUXdnYk1HQTFVZEl3U0IKcXpDQnFJQVVrVGpmQmlmS1B2STRCYlk1TGNHWGFzZWdVY21oZ1l5a2dZa3dnWVl4Q3pBSkJnTlZCQVlUQWtGVgpNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVEJsTjVaRzVsZVRFTU1Bb0dBMVVFQ2d3RFVFbFVNUWt3CkJ3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psYm1ObGNHbDBMbU52YlRFbE1DTUdDU3FHU0liM0RRRUoKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnZiWUlCQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9CZ1FDRQpUQWVKVERTQVc2ejFVRlRWN1FyZWg0VUxGT1JhajkrZUN1RjNLV0RIYyswSVFDajlyZG5ERzRRL3dmNy9yYVEwCkpuUFFDU0NkclBMSmV5b1BIN1FhVHdvYUY3ZHpWdzRMQ3N5TkpURld4NGNNNTBWdzZSNWZET2dpQzhic2ZmUzgKQkptb3VscnJaRE5OVmpHOG1XNmNMeHJZdlZRT3JSVmVjQ0ZJZ3NzQ2JBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=",
    +                "singleSignOnService": {
    +                    "url": "https://hello.example.com/access/saml/login",
    +                    "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
    +                }
    +            }
    +        }
    +        """
    +        expected_settings = json.loads(expected_settings_json)
    +        self.assertEqual(expected_settings, data2)
    +
    +    def test_parse_multi_certs(self):
    +        """
    +        Tests the parse method of the OneLogin_Saml2_IdPMetadataParser
    +        Case: IdP metadata contains multiple certs
    +        """
    +        xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata_multi_certs.xml'))
    +        data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata)
    +
    +        expected_settings_json = """
    +        {
    +            "sp": {
    +                "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
    +            },
    +            "idp": {
    +                "singleLogoutService": {
    +                    "url": "https://idp.examle.com/saml/slo",
    +                    "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
    +                },
    +                "x509certMulti": {
    +                    "encryption": [
    +                        "MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJjaWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwWT25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUyMjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRwtnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xxVRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCBpIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaCFD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXMGI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65chjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIBvlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZWQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw=="
    +                    ],
    +                    "signing": [
    +                        "MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJjaWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwWT25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUyMjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRwtnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xxVRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCBpIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaCFD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXMGI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65chjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIBvlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZWQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==",
    +                        "MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEUMBIGA1UECAwLZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xNzA0MTUxNjMzMThaFw0xODA0MTUxNjMzMThaME8xCzAJBgNVBAYTAnVzMRQwEgYDVQQIDAtleGFtcGxlLmNvbTEUMBIGA1UECgwLZXhhbXBsZS5jb20xFDASBgNVBAMMC2V4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6GLkl5lDUZdHNDAojp5i24OoPlqrt5TGXJIPqAZYT1hQvJW5nv17MFDHrjmtEnmW4ACKEy0fAX80QWIcHunZSkbEGHb+NG/6oTi5RipXMvmHnfFnPJJ0AdtiLiPE478CV856gXekV4Xx5u3KrylcOgkpYsp0GMIQBDzleMUXlYQIDAQABo1AwTjAdBgNVHQ4EFgQUnP8vlYPGPL2n6ZzDYij2kMDC8wMwHwYDVR0jBBgwFoAUnP8vlYPGPL2n6ZzDYij2kMDC8wMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQAlQGAl+b8Cpot1g+65lLLjVoY7APJPWLW0klKQNlMU0s4MU+71Y3ExUEOXDAZgKcFoavb1fEOGMwEf38NaJAy1e/l6VNuixXShffq20ymqHQxOG0q8ujeNkgZF9k6XDfn/QZ3AD0o/IrCT7UMc/0QsfgIjWYxwCvp2syApc5CYfQ=="
    +                    ]
    +                },
    +                "entityId": "https://idp.examle.com/saml/metadata",
    +                "singleSignOnService": {
    +                    "url": "https://idp.examle.com/saml/sso",
    +                    "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
    +                }
    +            }
    +        }
    +        """
    +        expected_settings = json.loads(expected_settings_json)
    +        self.assertEqual(expected_settings, data)
    +
    +    def test_parse_multi_singing_certs(self):
    +        """
    +        Tests the parse method of the OneLogin_Saml2_IdPMetadataParser
    +        Case: IdP metadata contains multiple signing certs and no encryption certs
    +        """
    +        xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata_multi_signing_certs.xml'))
    +        data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata)
    +
    +        expected_settings_json = """
    +        {
    +            "sp": {
    +                "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
    +            },
    +            "idp": {
    +                "singleLogoutService": {
    +                    "url": "https://idp.examle.com/saml/slo",
    +                    "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
    +                },
    +                "x509certMulti": {
    +                    "signing": [
    +                        "MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJjaWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwWT25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUyMjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRwtnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xxVRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCBpIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaCFD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXMGI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65chjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIBvlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZWQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==",
    +                        "MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEUMBIGA1UECAwLZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xNzA0MTUxNjMzMThaFw0xODA0MTUxNjMzMThaME8xCzAJBgNVBAYTAnVzMRQwEgYDVQQIDAtleGFtcGxlLmNvbTEUMBIGA1UECgwLZXhhbXBsZS5jb20xFDASBgNVBAMMC2V4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6GLkl5lDUZdHNDAojp5i24OoPlqrt5TGXJIPqAZYT1hQvJW5nv17MFDHrjmtEnmW4ACKEy0fAX80QWIcHunZSkbEGHb+NG/6oTi5RipXMvmHnfFnPJJ0AdtiLiPE478CV856gXekV4Xx5u3KrylcOgkpYsp0GMIQBDzleMUXlYQIDAQABo1AwTjAdBgNVHQ4EFgQUnP8vlYPGPL2n6ZzDYij2kMDC8wMwHwYDVR0jBBgwFoAUnP8vlYPGPL2n6ZzDYij2kMDC8wMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQAlQGAl+b8Cpot1g+65lLLjVoY7APJPWLW0klKQNlMU0s4MU+71Y3ExUEOXDAZgKcFoavb1fEOGMwEf38NaJAy1e/l6VNuixXShffq20ymqHQxOG0q8ujeNkgZF9k6XDfn/QZ3AD0o/IrCT7UMc/0QsfgIjWYxwCvp2syApc5CYfQ==",
    +                        "MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJjaWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwWT25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUyMjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRwtnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xxVRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCBpIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaCFD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXMGI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65chjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIBvlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZWQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw=="
    +                    ]
    +                },
    +                "entityId": "https://idp.examle.com/saml/metadata",
    +                "singleSignOnService": {
    +                    "url": "https://idp.examle.com/saml/sso",
    +                    "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
    +                }
    +            }
    +        }
    +        """
    +        expected_settings = json.loads(expected_settings_json)
    +        self.assertEqual(expected_settings, data)
    +
    +    def test_parse_multi_same_signing_and_encrypt_cert(self):
    +        """
    +        Tests the parse method of the OneLogin_Saml2_IdPMetadataParser
    +        Case: IdP metadata contains multiple signature cert and encrypt cert
    +              that is the same
    +        """
    +        xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata_same_sign_and_encrypt_cert.xml'))
    +        data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata)
    +
    +        expected_settings_json = """
    +        {
    +            "sp": {
    +                "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
    +            },
    +            "idp": {
    +                "x509cert": "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9uZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sTgf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0mTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SFzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNVHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAOBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHuAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcVgG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClPTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWuQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh781sE=",
    +                "entityId": "https://app.onelogin.com/saml/metadata/383123",
    +                "singleSignOnService": {
    +                    "url": "https://app.onelogin.com/trust/saml2/http-post/sso/383123",
    +                    "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
    +                }
    +            }
    +        }
    +        """
    +        expected_settings = json.loads(expected_settings_json)
    +        self.assertEqual(expected_settings, data)
    +
    +        xml_idp_metadata_2 = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata_different_sign_and_encrypt_cert.xml'))
    +        data_2 = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata_2)
    +        expected_settings_json_2 = """
    +        {
    +            "sp": {
    +                "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
    +            },
    +            "idp": {
    +                "x509certMulti": {
    +                    "encryption": [
    +                        "MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJjaWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwWT25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUyMjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRwtnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xxVRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCBpIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaCFD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXMGI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65chjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIBvlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZWQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw=="
    +                    ],
    +                    "signing": [
    +                        "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9uZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sTgf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0mTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SFzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNVHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAOBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHuAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcVgG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClPTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWuQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh781sE="
    +                    ]
    +                },
    +                "entityId": "https://app.onelogin.com/saml/metadata/383123",
    +                "singleSignOnService": {
    +                    "url": "https://app.onelogin.com/trust/saml2/http-post/sso/383123",
    +                    "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
    +                }
    +            }
    +        }
    +        """
    +        expected_settings_2 = json.loads(expected_settings_json_2)
    +        self.assertEqual(expected_settings_2, data_2)
    +
    +    def test_merge_settings(self):
    +        """
    +        Tests the merge_settings method of the OneLogin_Saml2_IdPMetadataParser
    +        """
    +        with self.assertRaises(TypeError):
    +            settings_result = OneLogin_Saml2_IdPMetadataParser.merge_settings(None, {})
    +
    +        with self.assertRaises(TypeError):
    +            settings_result = OneLogin_Saml2_IdPMetadataParser.merge_settings({}, None)
    +
    +        xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata.xml'))
    +
    +        # Parse XML metadata.
    +        data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata)
    +
    +        # Read base settings.
    +        settings = self.loadSettingsJSON()
    +
    +        # Merge settings from XML metadata into base settings,
    +        # let XML metadata have priority if there are conflicting
    +        # attributes.
    +        settings_result = OneLogin_Saml2_IdPMetadataParser.merge_settings(settings, data)
    +
    +        # Generate readable JSON representation:
    +        # print("%s" % json.dumps(settings_result, indent=2).replace(r'\n', r'\\n'))
    +
    +        expected_settings_json = """
    +        {
    +          "custom_base_path": "../../../tests/data/customPath/",
    +          "contactPerson": {
    +            "support": {
    +              "emailAddress": "support@example.com",
    +              "givenName": "support_name"
    +            },
    +            "technical": {
    +              "emailAddress": "technical@example.com",
    +              "givenName": "technical_name"
    +            }
    +          },
    +          "idp": {
    +            "singleSignOnService": {
    +              "url": "https://app.onelogin.com/trust/saml2/http-post/sso/383123",
    +              "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
    +            },
    +            "entityId": "https://app.onelogin.com/saml/metadata/383123",
    +            "singleLogoutService": {
    +              "url": "http://idp.example.com/SingleLogoutService.php"
    +            },
    +            "x509cert": "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9uZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sTgf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0mTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SFzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNVHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAOBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHuAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcVgG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClPTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWuQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh781sE="
    +          },
    +          "sp": {
    +            "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
    +            "entityId": "http://stuff.com/endpoints/metadata.php",
    +            "assertionConsumerService": {
    +              "url": "http://stuff.com/endpoints/endpoints/acs.php"
    +            },
    +            "singleLogoutService": {
    +              "url": "http://stuff.com/endpoints/endpoints/sls.php"
    +            }
    +          },
    +          "security": {
    +            "wantAssertionsSigned": false,
    +            "authnRequestsSigned": false,
    +            "signMetadata": false
    +          },
    +          "debug": false,
    +          "organization": {
    +            "en-US": {
    +              "displayname": "SP test",
    +              "url": "http://sp.example.com",
    +              "name": "sp_test"
    +            }
    +          },
    +          "strict": false
    +        }
    +        """
    +        expected_settings = json.loads(expected_settings_json)
    +        self.assertEqual(expected_settings, settings_result)
    +
    +        # Commute merge operation. As the order determines which settings
    +        # dictionary has priority, here we expect a different result.
    +        settings_result2 = OneLogin_Saml2_IdPMetadataParser.merge_settings(data, settings)
    +        expected_settings2_json = """
    +        {
    +          "debug": false,
    +          "idp": {
    +            "singleLogoutService": {
    +              "url": "http://idp.example.com/SingleLogoutService.php"
    +            },
    +            "singleSignOnService": {
    +              "url": "http://idp.example.com/SSOService.php",
    +              "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
    +            },
    +            "entityId": "http://idp.example.com/",
    +            "x509cert": "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo"
    +          },
    +          "security": {
    +            "authnRequestsSigned": false,
    +            "wantAssertionsSigned": false,
    +            "signMetadata": false
    +          },
    +          "contactPerson": {
    +            "technical": {
    +              "emailAddress": "technical@example.com",
    +              "givenName": "technical_name"
    +            },
    +            "support": {
    +              "emailAddress": "support@example.com",
    +              "givenName": "support_name"
    +            }
    +          },
    +          "strict": false,
    +          "sp": {
    +            "singleLogoutService": {
    +              "url": "http://stuff.com/endpoints/endpoints/sls.php"
    +            },
    +            "assertionConsumerService": {
    +              "url": "http://stuff.com/endpoints/endpoints/acs.php"
    +            },
    +            "entityId": "http://stuff.com/endpoints/metadata.php",
    +            "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
    +          },
    +          "custom_base_path": "../../../tests/data/customPath/",
    +          "organization": {
    +            "en-US": {
    +              "displayname": "SP test",
    +              "url": "http://sp.example.com",
    +              "name": "sp_test"
    +            }
    +          }
    +        }
    +        """
    +        expected_settings2 = json.loads(expected_settings2_json)
    +        self.assertEqual(expected_settings2, settings_result2)
    +
    +        # Test merging multiple certs
    +        xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata_multi_certs.xml'))
    +        data3 = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata)
    +        settings_result3 = OneLogin_Saml2_IdPMetadataParser.merge_settings(settings, data3)
    +        expected_settings3_json = """
    +        {
    +          "debug": false,
    +          "strict": false,
    +          "custom_base_path": "../../../tests/data/customPath/",
    +          "sp": {
    +            "singleLogoutService": {
    +              "url": "http://stuff.com/endpoints/endpoints/sls.php"
    +            },
    +            "assertionConsumerService": {
    +              "url": "http://stuff.com/endpoints/endpoints/acs.php"
    +            },
    +            "entityId": "http://stuff.com/endpoints/metadata.php",
    +            "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
    +          },
    +          "idp": {
    +            "singleLogoutService": {
    +              "url": "https://idp.examle.com/saml/slo",
    +              "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
    +            },
    +            "x509certMulti": {
    +              "encryption": [
    +                "MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJjaWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwWT25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUyMjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRwtnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xxVRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCBpIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaCFD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXMGI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65chjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIBvlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZWQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw=="
    +              ],
    +              "signing": [
    +                "MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJjaWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwWT25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUyMjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRwtnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xxVRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCBpIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaCFD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXMGI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65chjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIBvlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZWQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==",
    +                "MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEUMBIGA1UECAwLZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xNzA0MTUxNjMzMThaFw0xODA0MTUxNjMzMThaME8xCzAJBgNVBAYTAnVzMRQwEgYDVQQIDAtleGFtcGxlLmNvbTEUMBIGA1UECgwLZXhhbXBsZS5jb20xFDASBgNVBAMMC2V4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6GLkl5lDUZdHNDAojp5i24OoPlqrt5TGXJIPqAZYT1hQvJW5nv17MFDHrjmtEnmW4ACKEy0fAX80QWIcHunZSkbEGHb+NG/6oTi5RipXMvmHnfFnPJJ0AdtiLiPE478CV856gXekV4Xx5u3KrylcOgkpYsp0GMIQBDzleMUXlYQIDAQABo1AwTjAdBgNVHQ4EFgQUnP8vlYPGPL2n6ZzDYij2kMDC8wMwHwYDVR0jBBgwFoAUnP8vlYPGPL2n6ZzDYij2kMDC8wMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQAlQGAl+b8Cpot1g+65lLLjVoY7APJPWLW0klKQNlMU0s4MU+71Y3ExUEOXDAZgKcFoavb1fEOGMwEf38NaJAy1e/l6VNuixXShffq20ymqHQxOG0q8ujeNkgZF9k6XDfn/QZ3AD0o/IrCT7UMc/0QsfgIjWYxwCvp2syApc5CYfQ=="
    +              ]
    +            },
    +            "entityId": "https://idp.examle.com/saml/metadata",
    +            "singleSignOnService": {
    +              "url": "https://idp.examle.com/saml/sso",
    +              "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
    +            }
    +          },
    +          "security": {
    +            "authnRequestsSigned": false,
    +            "wantAssertionsSigned": false,
    +            "signMetadata": false
    +          },
    +          "contactPerson": {
    +            "technical": {
    +              "emailAddress": "technical@example.com",
    +              "givenName": "technical_name"
    +            },
    +            "support": {
    +              "emailAddress": "support@example.com",
    +              "givenName": "support_name"
    +            }
    +          },
    +          "organization": {
    +            "en-US": {
    +              "displayname": "SP test",
    +              "url": "http://sp.example.com",
    +              "name": "sp_test"
    +            }
    +          }
    +        }
    +        """
    +        expected_settings3 = json.loads(expected_settings3_json)
    +        self.assertEqual(expected_settings3, settings_result3)
    +
    +
    +if __name__ == '__main__':
    +    runner = unittest.TextTestRunner()
    +    unittest.main(testRunner=runner)
    diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py
    index da201817..de782f75 100644
    --- a/tests/src/OneLogin/saml2_tests/logout_request_test.py
    +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py
    @@ -1,7 +1,7 @@
     # -*- coding: utf-8 -*-
     
    -# Copyright (c) 2014, OneLogin, Inc.
    -# All rights reserved.
    +# Copyright (c) 2010-2018 OneLogin, Inc.
    +# MIT License
     
     from base64 import b64encode
     import json
    @@ -13,20 +13,20 @@
     from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request
     from onelogin.saml2.settings import OneLogin_Saml2_Settings
     from onelogin.saml2.utils import OneLogin_Saml2_Utils
    +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
     
     
     class OneLogin_Saml2_Logout_Request_Test(unittest.TestCase):
    -    data_path = join(dirname(__file__), '..', '..', '..', 'data')
    +    data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data')
    +    settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings')
     
    -    def loadSettingsJSON(self):
    -        filename = join(dirname(__file__), '..', '..', '..', 'settings', 'settings1.json')
    +    def loadSettingsJSON(self, name='settings1.json'):
    +        filename = join(self.settings_path, name)
             if exists(filename):
                 stream = open(filename, 'r')
                 settings = json.load(stream)
                 stream.close()
                 return settings
    -        else:
    -            raise Exception('Settings json file does not exist')
     
         def file_contents(self, filename):
             f = open(filename, 'r')
    @@ -46,7 +46,7 @@ def testConstructor(self):
     
             parameters = {'SAMLRequest': logout_request.get_request()}
             logout_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SingleLogoutService.php', parameters, True)
    -        self.assertRegexpMatches(logout_url, '^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLRequest=')
    +        self.assertRegexpMatches(logout_url, r'^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLRequest=')
             url_parts = urlparse(logout_url)
             exploded = parse_qs(url_parts.query)
             payload = exploded['SAMLRequest'][0]
    @@ -63,13 +63,71 @@ def testCreateDeflatedSAMLLogoutRequestURLParameter(self):
     
             parameters = {'SAMLRequest': logout_request.get_request()}
             logout_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SingleLogoutService.php', parameters, True)
    -        self.assertRegexpMatches(logout_url, '^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLRequest=')
    +        self.assertRegexpMatches(logout_url, r'^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLRequest=')
             url_parts = urlparse(logout_url)
             exploded = parse_qs(url_parts.query)
             payload = exploded['SAMLRequest'][0]
             inflated = OneLogin_Saml2_Utils.decode_base64_and_inflate(payload)
             self.assertRegexpMatches(inflated, '^')
    +
         def testGetIDFromSAMLLogoutRequest(self):
             """
             Tests the get_id method of the OneLogin_Saml2_LogoutRequest
    @@ -97,7 +155,7 @@ def testGetNameIdData(self):
             """
             expected_name_id_data = {
                 'Value': 'ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c',
    -            'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified',
    +            'Format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified',
                 'SPNameQualifier': 'http://idp.example.com/'
             }
     
    @@ -110,11 +168,8 @@ def testGetNameIdData(self):
             self.assertEqual(expected_name_id_data, name_id_data_2)
     
             request_2 = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_encrypted_nameid.xml'))
    -        try:
    +        with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'Key is required in order to decrypt the NameID'):
                 OneLogin_Saml2_Logout_Request.get_nameid_data(request_2)
    -            self.assertTrue(False)
    -        except Exception as e:
    -            self.assertIn('Key is required in order to decrypt the NameID', e.message)
     
             settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
             key = settings.get_sp_key()
    @@ -130,18 +185,42 @@ def testGetNameIdData(self):
             encrypted_id_nodes = dom_2.getElementsByTagName('saml:EncryptedID')
             encrypted_data = encrypted_id_nodes[0].firstChild.nextSibling
             encrypted_id_nodes[0].removeChild(encrypted_data)
    -        try:
    +        with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the Logout Request'):
                 OneLogin_Saml2_Logout_Request.get_nameid_data(dom_2.toxml(), key)
    -            self.assertTre(False)
    -        except Exception as e:
    -            self.assertIn('Not NameID found in the Logout Request', e.message)
    +
    +        idp_data = settings.get_idp_data()
    +        expected_name_id_data = {
    +            'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress',
    +            'NameQualifier': idp_data['entityId'],
    +            'Value': 'ONELOGIN_9c86c4542ab9d6fce07f2f7fd335287b9b3cdf69'
    +        }
     
             inv_request = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'no_nameId.xml'))
    -        try:
    +        with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the Logout Request'):
                 OneLogin_Saml2_Logout_Request.get_nameid_data(inv_request)
    -            self.assertTre(False)
    -        except Exception as e:
    -            self.assertIn('Not NameID found in the Logout Request', e.message)
    +
    +        logout_request = OneLogin_Saml2_Logout_Request(settings, None, expected_name_id_data['Value'], None, idp_data['entityId'], expected_name_id_data['Format'])
    +        dom = parseString(logout_request.get_xml())
    +        name_id_data_3 = OneLogin_Saml2_Logout_Request.get_nameid_data(dom)
    +        self.assertEqual(expected_name_id_data, name_id_data_3)
    +
    +        expected_name_id_data = {
    +            'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress',
    +            'Value': 'ONELOGIN_9c86c4542ab9d6fce07f2f7fd335287b9b3cdf69'
    +        }
    +        logout_request = OneLogin_Saml2_Logout_Request(settings, None, expected_name_id_data['Value'], None, None, expected_name_id_data['Format'])
    +        dom = parseString(logout_request.get_xml())
    +        name_id_data_4 = OneLogin_Saml2_Logout_Request.get_nameid_data(dom)
    +        self.assertEqual(expected_name_id_data, name_id_data_4)
    +
    +        expected_name_id_data = {
    +            'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:entity',
    +            'Value': 'http://idp.example.com/'
    +        }
    +        logout_request = OneLogin_Saml2_Logout_Request(settings)
    +        dom = parseString(logout_request.get_xml())
    +        name_id_data_5 = OneLogin_Saml2_Logout_Request.get_nameid_data(dom)
    +        self.assertEqual(expected_name_id_data, name_id_data_5)
     
         def testGetNameId(self):
             """
    @@ -152,11 +231,8 @@ def testGetNameId(self):
             self.assertEqual(name_id, 'ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c')
     
             request_2 = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_encrypted_nameid.xml'))
    -        try:
    +        with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'Key is required in order to decrypt the NameID'):
                 OneLogin_Saml2_Logout_Request.get_nameid(request_2)
    -            self.assertTrue(False)
    -        except Exception as e:
    -            self.assertIn('Key is required in order to decrypt the NameID', e.message)
     
             settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
             key = settings.get_sp_key()
    @@ -235,12 +311,9 @@ def testIsInvalidIssuer(self):
             self.assertTrue(logout_request.is_valid(request_data))
     
             settings.set_strict(True)
    -        try:
    -            logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request))
    -            valid = logout_request2.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('Invalid issuer in the Logout Request', e.message)
    +        logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request))
    +        self.assertFalse(logout_request2.is_valid(request_data))
    +        self.assertIn('Invalid issuer in the Logout Request', logout_request2.get_error())
     
         def testIsInvalidDestination(self):
             """
    @@ -257,12 +330,9 @@ def testIsInvalidDestination(self):
             self.assertTrue(logout_request.is_valid(request_data))
     
             settings.set_strict(True)
    -        try:
    -            logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request))
    -            valid = logout_request2.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('The LogoutRequest was received at', e.message)
    +        logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request))
    +        self.assertFalse(logout_request2.is_valid(request_data))
    +        self.assertIn('The LogoutRequest was received at', logout_request2.get_error())
     
             dom = parseString(request)
             dom.documentElement.setAttribute('Destination', None)
    @@ -291,12 +361,9 @@ def testIsInvalidNotOnOrAfter(self):
             self.assertTrue(logout_request.is_valid(request_data))
     
             settings.set_strict(True)
    -        try:
    -            logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request))
    -            valid = logout_request2.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('Timing issues (please check your clock settings)', e.message)
    +        logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request))
    +        self.assertFalse(logout_request2.is_valid(request_data))
    +        self.assertIn('Could not validate timestamp: expired. Check system clock.', logout_request2.get_error())
     
         def testIsValid(self):
             """
    @@ -330,6 +397,22 @@ def testIsValid(self):
             logout_request5 = OneLogin_Saml2_Logout_Request(settings, b64encode(request))
             self.assertTrue(logout_request5.is_valid(request_data))
     
    +    def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self):
    +        request = OneLogin_Saml2_Utils.deflate_and_base64_encode('invalid')
    +        request_data = {
    +            'http_host': 'example.com',
    +            'script_name': 'index.html'
    +        }
    +        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
    +        settings.set_strict(True)
    +
    +        logout_request = OneLogin_Saml2_Logout_Request(settings, request)
    +
    +        self.assertFalse(logout_request.is_valid(request_data))
    +
    +        with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd"):
    +            logout_request.is_valid(request_data, raise_exceptions=True)
    +
         def testIsValidSign(self):
             """
             Tests the is_valid method of the OneLogin_Saml2_LogoutRequest
    @@ -359,22 +442,16 @@ def testIsValidSign(self):
             request_data['get_data']['RelayState'] = relayState
     
             settings.set_strict(True)
    -        try:
    -            logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request))
    -            valid = logout_request2.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('The LogoutRequest was received at', e.message)
    +        logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request))
    +        self.assertFalse(logout_request2.is_valid(request_data))
    +        self.assertIn('The LogoutRequest was received at', logout_request2.get_error())
     
             settings.set_strict(False)
             old_signature = request_data['get_data']['Signature']
             request_data['get_data']['Signature'] = 'vfWbbc47PkP3ejx4bjKsRX7lo9Ml1WRoE5J5owF/0mnyKHfSY6XbhO1wwjBV5vWdrUVX+xp6slHyAf4YoAsXFS0qhan6txDiZY4Oec6yE+l10iZbzvie06I4GPak4QrQ4gAyXOSzwCrRmJu4gnpeUxZ6IqKtdrKfAYRAcVf3333='
    -        try:
    -            logout_request3 = OneLogin_Saml2_Logout_Request(settings, b64encode(request))
    -            valid = logout_request3.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('Signature validation failed. Logout Request rejected', e.message)
    +        logout_request3 = OneLogin_Saml2_Logout_Request(settings, b64encode(request))
    +        self.assertFalse(logout_request3.is_valid(request_data))
    +        self.assertIn('Signature validation failed. Logout Request rejected', logout_request3.get_error())
     
             request_data['get_data']['Signature'] = old_signature
             old_signature_algorithm = request_data['get_data']['SigAlg']
    @@ -382,37 +459,25 @@ def testIsValidSign(self):
             self.assertTrue(logout_request3.is_valid(request_data))
     
             request_data['get_data']['RelayState'] = 'http://example.com/relaystate'
    -        try:
    -            valid = logout_request3.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('Signature validation failed. Logout Request rejected', e.message)
    +        self.assertFalse(logout_request3.is_valid(request_data))
    +        self.assertIn('Signature validation failed. Logout Request rejected', logout_request3.get_error())
     
             settings.set_strict(True)
             request_2 = request.replace('https://pitbulk.no-ip.org/newonelogin/demo1/index.php?sls', current_url)
             request_2 = request_2.replace('https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php', 'http://idp.example.com/')
             request_data['get_data']['SAMLRequest'] = OneLogin_Saml2_Utils.deflate_and_base64_encode(request_2)
    -        try:
    -            logout_request4 = OneLogin_Saml2_Logout_Request(settings, b64encode(request_2))
    -            valid = logout_request4.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('Signature validation failed. Logout Request rejected', e.message)
    +        logout_request4 = OneLogin_Saml2_Logout_Request(settings, b64encode(request_2))
    +        self.assertFalse(logout_request4.is_valid(request_data))
    +        self.assertIn('Signature validation failed. Logout Request rejected', logout_request4.get_error())
     
             settings.set_strict(False)
    -        try:
    -            logout_request5 = OneLogin_Saml2_Logout_Request(settings, b64encode(request_2))
    -            valid = logout_request5.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('Signature validation failed. Logout Request rejected', e.message)
    +        logout_request5 = OneLogin_Saml2_Logout_Request(settings, b64encode(request_2))
    +        self.assertFalse(logout_request5.is_valid(request_data))
    +        self.assertIn('Signature validation failed. Logout Request rejected', logout_request5.get_error())
     
             request_data['get_data']['SigAlg'] = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1'
    -        try:
    -            valid = logout_request5.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('Invalid signAlg in the recieved Logout Request', e.message)
    +        self.assertFalse(logout_request5.is_valid(request_data))
    +        self.assertIn('Signature validation failed. Logout Request rejected', logout_request5.get_error())
     
             settings_info = self.loadSettingsJSON()
             settings_info['strict'] = True
    @@ -421,20 +486,60 @@ def testIsValidSign(self):
             request_data['get_data']['SigAlg'] = old_signature_algorithm
             old_signature = request_data['get_data']['Signature']
             del request_data['get_data']['Signature']
    -        try:
    -            logout_request6 = OneLogin_Saml2_Logout_Request(settings, b64encode(request_2))
    -            valid = logout_request6.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('The Message of the Logout Request is not signed and the SP require it', e.message)
    +        logout_request6 = OneLogin_Saml2_Logout_Request(settings, b64encode(request_2))
    +        self.assertFalse(logout_request6.is_valid(request_data))
    +        self.assertIn('The Message of the Logout Request is not signed and the SP require it', logout_request6.get_error())
     
             request_data['get_data']['Signature'] = old_signature
             settings_info['idp']['certFingerprint'] = 'afe71c28ef740bc87425be13a2263d37971da1f9'
             del settings_info['idp']['x509cert']
             settings_2 = OneLogin_Saml2_Settings(settings_info)
    -        try:
    -            logout_request7 = OneLogin_Saml2_Logout_Request(settings_2, b64encode(request_2))
    -            valid = logout_request7.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('In order to validate the sign on the Logout Request, the x509cert of the IdP is required', e.message)
    +        logout_request7 = OneLogin_Saml2_Logout_Request(settings_2, b64encode(request_2))
    +        self.assertFalse(logout_request7.is_valid(request_data))
    +        self.assertEqual('In order to validate the sign on the Logout Request, the x509cert of the IdP is required', logout_request7.get_error())
    +
    +    def testIsValidSignUsingX509certMulti(self):
    +        """
    +        Tests the is_valid method of the OneLogin_Saml2_LogoutRequest
    +        """
    +        request_data = {
    +            'http_host': 'example.com',
    +            'script_name': 'index.html',
    +            'get_data': {
    +                'SAMLRequest': 'fZJNa+MwEIb/itHdiTz6sC0SQyEsBPoB27KHXoIsj7cGW3IlGfLzV7G7kN1DL2KYmeedmRcdgp7GWT26326JP/FzwRCz6zTaoNbKkSzeKqfDEJTVEwYVjXp9eHpUsKNq9i4640Zyh3xP6BDQx8FZkp1PR3KpqexAl72QmpUCS8SW01IiZz2TVVGD4X1VQYlAsl/oQyKPJAklPIQFzzZEbWNK0YLnlOVA3wqpQCoB7yQ7pWsGq+NKfcQ4q/0+xKXvd8ZNe7Td7AYbw10UxrCbP2aSPbv4Yl/8Qx/R3+SB5bTOoXiDQvFNvjnc7lXrIr75kh+6eYdXPc0jrkMO+/umjXhOtpxP2Q/nJx2/9+uWGbq8X1tV9NqGAW0kzaVvoe1AAJeCSWqYaUVRM2SilKKuqDTpFSlszdcK29RthVm9YriZebYdXpsLdhVAB7VJzif3haYMqqTVcl0JMBR4y+s2zak3sf/4v8l/vlHzBw==',
    +                'RelayState': '_1037fbc88ec82ce8e770b2bed1119747bb812a07e6',
    +                'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1',
    +                'Signature': 'Ouxo9BV6zmq4yrgamT9EbSKy/UmvSxGS8z26lIMgKOEP4LFR/N23RftdANmo4HafrzSfA0YTXwhKDqbOByS0j+Ql8OdQOes7vGioSjo5qq/Bi+5i6jXwQfphnfcHAQiJL4gYVIifkhhHRWpvYeiysF1Y9J02me0izwazFmoRXr4='
    +            }
    +        }
    +        settings_info = self.loadSettingsJSON('settings8.json')
    +        settings_info['strict'] = False
    +        settings = OneLogin_Saml2_Settings(settings_info)
    +        logout_request = OneLogin_Saml2_Logout_Request(settings, request_data['get_data']['SAMLRequest'])
    +        self.assertTrue(logout_request.is_valid(request_data))
    +
    +    def testGetXML(self):
    +        """
    +        Tests that we can get the logout request XML directly without
    +        going through intermediate steps
    +        """
    +        request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml'))
    +        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
    +
    +        logout_request_generated = OneLogin_Saml2_Logout_Request(settings)
    +
    +        expectedFragment = (
    +            'Destination="http://idp.example.com/SingleLogoutService.php">\n'
    +            '        http://stuff.com/endpoints/metadata.php\n'
    +            '        http://idp.example.com/\n'
    +            '        \n    '
    +        )
    +        self.assertIn(expectedFragment, logout_request_generated.get_xml())
    +
    +        logout_request_processed = OneLogin_Saml2_Logout_Request(settings, b64encode(request))
    +        self.assertEqual(request, logout_request_processed.get_xml())
    +
    +
    +if __name__ == '__main__':
    +    runner = unittest.TextTestRunner()
    +    unittest.main(testRunner=runner)
    diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py
    index 5b618b8d..38d9fab6 100644
    --- a/tests/src/OneLogin/saml2_tests/logout_response_test.py
    +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py
    @@ -1,7 +1,7 @@
     # -*- coding: utf-8 -*-
     
    -# Copyright (c) 2014, OneLogin, Inc.
    -# All rights reserved.
    +# Copyright (c) 2010-2018 OneLogin, Inc.
    +# MIT License
     
     import json
     from os.path import dirname, join, exists
    @@ -13,20 +13,20 @@
     from onelogin.saml2.logout_response import OneLogin_Saml2_Logout_Response
     from onelogin.saml2.settings import OneLogin_Saml2_Settings
     from onelogin.saml2.utils import OneLogin_Saml2_Utils
    +from onelogin.saml2.errors import OneLogin_Saml2_ValidationError
     
     
     class OneLogin_Saml2_Logout_Response_Test(unittest.TestCase):
    -    data_path = join(dirname(__file__), '..', '..', '..', 'data')
    +    data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data')
    +    settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings')
     
    -    def loadSettingsJSON(self):
    -        filename = join(dirname(__file__), '..', '..', '..', 'settings', 'settings1.json')
    +    def loadSettingsJSON(self, name='settings1.json'):
    +        filename = join(self.settings_path, name)
             if exists(filename):
                 stream = open(filename, 'r')
                 settings = json.load(stream)
                 stream.close()
                 return settings
    -        else:
    -            raise Exception('Settings json file does not exist')
     
         def file_contents(self, filename):
             f = open(filename, 'r')
    @@ -56,7 +56,7 @@ def testCreateDeflatedSAMLLogoutResponseURLParameter(self):
     
             logout_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SingleLogoutService.php', parameters, True)
     
    -        self.assertRegexpMatches(logout_url, '^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLResponse=')
    +        self.assertRegexpMatches(logout_url, r'^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLResponse=')
             url_parts = urlparse(logout_url)
             exploded = parse_qs(url_parts.query)
             inflated = OneLogin_Saml2_Utils.decode_base64_and_inflate(exploded['SAMLResponse'][0])
    @@ -157,11 +157,8 @@ def testIsInValidRequestId(self):
     
             settings.set_strict(True)
             response_2 = OneLogin_Saml2_Logout_Response(settings, message)
    -        try:
    -            valid = response_2.is_valid(request_data, request_id)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('The InResponseTo of the Logout Response:', e.message)
    +        self.assertFalse(response_2.is_valid(request_data, request_id))
    +        self.assertIn('The InResponseTo of the Logout Response:', response_2.get_error())
     
         def testIsInValidIssuer(self):
             """
    @@ -188,11 +185,8 @@ def testIsInValidIssuer(self):
     
             settings.set_strict(True)
             response_2 = OneLogin_Saml2_Logout_Response(settings, message)
    -        try:
    -            valid = response_2.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('Invalid issuer in the Logout Request', e.message)
    +        self.assertFalse(response_2.is_valid(request_data))
    +        self.assertIn('Invalid issuer in the Logout Response', response_2.get_error())
     
         def testIsInValidDestination(self):
             """
    @@ -213,11 +207,8 @@ def testIsInValidDestination(self):
     
             settings.set_strict(True)
             response_2 = OneLogin_Saml2_Logout_Response(settings, message)
    -        try:
    -            valid = response_2.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('The LogoutRequest was received at', e.message)
    +        self.assertFalse(response_2.is_valid(request_data))
    +        self.assertIn('The LogoutResponse was received at', response_2.get_error())
     
             # Empty destination
             dom = parseString(OneLogin_Saml2_Utils.decode_base64_and_inflate(message))
    @@ -234,6 +225,23 @@ def testIsInValidDestination(self):
             response_4 = OneLogin_Saml2_Logout_Response(settings, message_4)
             self.assertTrue(response_4.is_valid(request_data))
     
    +    def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self):
    +        message = OneLogin_Saml2_Utils.deflate_and_base64_encode('invalid')
    +        request_data = {
    +            'http_host': 'example.com',
    +            'script_name': 'index.html',
    +            'get_data': {}
    +        }
    +        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
    +        settings.set_strict(True)
    +
    +        response = OneLogin_Saml2_Logout_Response(settings, message)
    +
    +        self.assertFalse(response.is_valid(request_data))
    +
    +        with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd"):
    +            response.is_valid(request_data, raise_exceptions=True)
    +
         def testIsInValidSign(self):
             """
             Tests the is_valid method of the OneLogin_Saml2_LogoutResponse
    @@ -263,21 +271,15 @@ def testIsInValidSign(self):
     
             settings.set_strict(True)
             response_2 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse'])
    -        try:
    -            valid = response_2.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('Invalid issuer in the Logout Request', e.message)
    +        self.assertFalse(response_2.is_valid(request_data))
    +        self.assertIn('Invalid issuer in the Logout Response', response_2.get_error())
     
             settings.set_strict(False)
             old_signature = request_data['get_data']['Signature']
             request_data['get_data']['Signature'] = 'vfWbbc47PkP3ejx4bjKsRX7lo9Ml1WRoE5J5owF/0mnyKHfSY6XbhO1wwjBV5vWdrUVX+xp6slHyAf4YoAsXFS0qhan6txDiZY4Oec6yE+l10iZbzvie06I4GPak4QrQ4gAyXOSzwCrRmJu4gnpeUxZ6IqKtdrKfAYRAcVf3333='
             response_3 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse'])
    -        try:
    -            valid = response_3.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('Signature validation failed. Logout Response rejected', e.message)
    +        self.assertFalse(response_3.is_valid(request_data))
    +        self.assertIn('Signature validation failed. Logout Response rejected', response_3.get_error())
     
             request_data['get_data']['Signature'] = old_signature
             old_signature_algorithm = request_data['get_data']['SigAlg']
    @@ -287,11 +289,8 @@ def testIsInValidSign(self):
     
             request_data['get_data']['RelayState'] = 'http://example.com/relaystate'
             response_5 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse'])
    -        try:
    -            valid = response_5.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('Signature validation failed. Logout Response rejected', e.message)
    +        self.assertFalse(response_5.is_valid(request_data))
    +        self.assertIn('Signature validation failed. Logout Response rejected', response_5.get_error())
     
             settings.set_strict(True)
             current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data)
    @@ -301,27 +300,18 @@ def testIsInValidSign(self):
             request_data['get_data']['SAMLResponse'] = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message_6)
     
             response_6 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse'])
    -        try:
    -            valid = response_6.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('Signature validation failed. Logout Response rejected', e.message)
    +        self.assertFalse(response_6.is_valid(request_data))
    +        self.assertIn('Signature validation failed. Logout Response rejected', response_6.get_error())
     
             settings.set_strict(False)
             response_7 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse'])
    -        try:
    -            valid = response_7.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('Signature validation failed. Logout Response rejected', e.message)
    +        self.assertFalse(response_7.is_valid(request_data))
    +        self.assertIn('Signature validation failed. Logout Response rejected', response_7.get_error())
     
             request_data['get_data']['SigAlg'] = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1'
             response_8 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse'])
    -        try:
    -            valid = response_8.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('Invalid signAlg in the recieved Logout Response', e.message)
    +        self.assertFalse(response_8.is_valid(request_data))
    +        self.assertIn('Signature validation failed. Logout Response rejected', response_8.get_error())
     
             settings_info = self.loadSettingsJSON()
             settings_info['strict'] = True
    @@ -333,11 +323,8 @@ def testIsInValidSign(self):
             del request_data['get_data']['Signature']
             request_data['get_data']['SAMLResponse'] = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message_6)
             response_9 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse'])
    -        try:
    -            valid = response_9.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('The Message of the Logout Response is not signed and the SP require it', e.message)
    +        self.assertFalse(response_9.is_valid(request_data))
    +        self.assertIn('The Message of the Logout Response is not signed and the SP require it', response_9.get_error())
     
             request_data['get_data']['Signature'] = old_signature
             settings_info['idp']['certFingerprint'] = 'afe71c28ef740bc87425be13a2263d37971da1f9'
    @@ -345,11 +332,8 @@ def testIsInValidSign(self):
             settings_2 = OneLogin_Saml2_Settings(settings_info)
     
             response_10 = OneLogin_Saml2_Logout_Response(settings_2, request_data['get_data']['SAMLResponse'])
    -        try:
    -            valid = response_10.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('In order to validate the sign on the Logout Response, the x509cert of the IdP is required', e.message)
    +        self.assertFalse(response_10.is_valid(request_data))
    +        self.assertIn('In order to validate the sign on the Logout Response, the x509cert of the IdP is required', response_10.get_error())
     
         def testIsValid(self):
             """
    @@ -368,11 +352,8 @@ def testIsValid(self):
     
             settings.set_strict(True)
             response_2 = OneLogin_Saml2_Logout_Response(settings, message)
    -        try:
    -            valid = response_2.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertIn('The LogoutRequest was received at', e.message)
    +        self.assertFalse(response_2.is_valid(request_data))
    +        self.assertIn('The LogoutResponse was received at', response_2.get_error())
     
             plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message)
             current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data)
    @@ -381,3 +362,53 @@ def testIsValid(self):
     
             response_3 = OneLogin_Saml2_Logout_Response(settings, message_3)
             self.assertTrue(response_3.is_valid(request_data))
    +
    +    def testIsValidSignUsingX509certMulti(self):
    +        """
    +        Tests the is_valid method of the OneLogin_Saml2_LogoutResponse
    +        """
    +        request_data = {
    +            'http_host': 'example.com',
    +            'script_name': 'index.html',
    +            'get_data': {
    +                'SAMLResponse': 'fZHbasJAEIZfJey9ZrNZc1gSodRSBKtQxYveyGQz1kCyu2Q24OM3jS21UHo3p++f4Z+CoGud2th3O/hXJGcNYXDtWkNqapVs6I2yQA0pAx2S8lrtH142Ssy5cr31VtuW3SH/E0CEvW+sYcF6VbLTIktFLMWZgxQR8DSP85wDB4GJGMOqShYVaoBUsOCIPY1kyUahEScacG3Ig/FjiUdyxuOZ4IcoUVGq4vSNBSsk3xjwE3Xx3qkwJD+cz3NtuxBN7WxjPN1F1NLcXdwob77tONiS7bZPm93zenvCqopxgVJmuU50jREsZF4noKWAOuNZJbNznnBky+LTDDVd2S+/dje1m+MVOtfidEER3g8Vt2fsPfiBfmePtsbgCO2A/9tL07TaD1ojEQuXtw0/ouFfD19+AA==',
    +                'RelayState': 'http://stuff.com/endpoints/endpoints/index.php',
    +                'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1',
    +                'Signature': 'OV9c4R0COSjN69fAKCpV7Uj/yx6/KFxvbluVCzdK3UuortpNMpgHFF2wYNlMSG9GcYGk6p3I8nB7Z+1TQchMWZOlO/StjAqgtZhtpiwPcWryNuq8vm/6hnJ3zMDhHTS7F8KG4qkCXmJ9sQD3Y31UNcuygBwIbNakvhDT5Qo9Nsw='
    +            }
    +        }
    +        settings_info = self.loadSettingsJSON('settings8.json')
    +        settings_info['strict'] = False
    +        settings = OneLogin_Saml2_Settings(settings_info)
    +        logout_response = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse'])
    +        self.assertTrue(logout_response.is_valid(request_data))
    +
    +    def testGetXML(self):
    +        """
    +        Tests that we can get the logout response XML directly without
    +        going through intermediate steps
    +        """
    +        response = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response.xml'))
    +        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
    +
    +        logout_response_generated = OneLogin_Saml2_Logout_Response(settings)
    +        logout_response_generated.build("InResponseValue")
    +
    +        expectedFragment = (
    +            'Destination="http://idp.example.com/SingleLogoutService.php"\n'
    +            '                      InResponseTo="InResponseValue"\n>\n'
    +            '    http://stuff.com/endpoints/metadata.php\n'
    +            '    \n'
    +            '        \n'
    +            '    \n'
    +            ''
    +        )
    +        self.assertIn(expectedFragment, logout_response_generated.get_xml())
    +
    +        logout_response_processed = OneLogin_Saml2_Logout_Response(settings, OneLogin_Saml2_Utils.deflate_and_base64_encode(response))
    +        self.assertEqual(response, logout_response_processed.get_xml())
    +
    +
    +if __name__ == '__main__':
    +    runner = unittest.TextTestRunner()
    +    unittest.main(testRunner=runner)
    diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py
    index 6fbfbf50..60453026 100644
    --- a/tests/src/OneLogin/saml2_tests/metadata_test.py
    +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py
    @@ -1,7 +1,7 @@
     # -*- coding: utf-8 -*-
     
    -# Copyright (c) 2014, OneLogin, Inc.
    -# All rights reserved.
    +# Copyright (c) 2010-2018 OneLogin, Inc.
    +# MIT License
     
     
     import json
    @@ -12,11 +12,15 @@
     
     from onelogin.saml2.metadata import OneLogin_Saml2_Metadata
     from onelogin.saml2.settings import OneLogin_Saml2_Settings
    +from onelogin.saml2.utils import OneLogin_Saml2_Utils
    +from onelogin.saml2.constants import OneLogin_Saml2_Constants
     
     
     class OneLogin_Saml2_Metadata_Test(unittest.TestCase):
    -    def loadSettingsJSON(self):
    -        filename = join(dirname(__file__), '..', '..', '..', 'settings', 'settings1.json')
    +    settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings')
    +
    +    def loadSettingsJSON(self, filename='settings1.json'):
    +        filename = join(self.settings_path, filename)
             if exists(filename):
                 stream = open(filename, 'r')
                 settings = json.load(stream)
    @@ -59,7 +63,7 @@ def testBuilder(self):
             self.assertIn('urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified', metadata)
    +        self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', metadata)
     
             self.assertIn('sp_test', metadata)
             self.assertIn('', metadata)
    @@ -139,6 +143,50 @@ def testBuilder(self):
             parsed_datetime = strftime(r'%Y-%m-%dT%H:%M:%SZ', datetime_value.timetuple())
             self.assertIn('validUntil="%s"' % parsed_datetime, metadata6)
     
    +    def testBuilderAttributeConsumingService(self):
    +        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON('settings4.json'))
    +        sp_data = settings.get_sp_data()
    +        security = settings.get_security_data()
    +        organization = settings.get_organization()
    +        contacts = settings.get_contacts()
    +
    +        metadata = OneLogin_Saml2_Metadata.builder(
    +            sp_data, security['authnRequestsSigned'],
    +            security['wantAssertionsSigned'], None, None, contacts,
    +            organization
    +        )
    +        self.assertIn("""        
    +            Test Service
    +            Test Service
    +            
    +            
    +            
    +            
    +            
    +        """, metadata)
    +
    +    def testBuilderAttributeConsumingServiceWithMultipleAttributeValue(self):
    +        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON('settings5.json'))
    +        sp_data = settings.get_sp_data()
    +        security = settings.get_security_data()
    +        organization = settings.get_organization()
    +        contacts = settings.get_contacts()
    +
    +        metadata = OneLogin_Saml2_Metadata.builder(
    +            sp_data, security['authnRequestsSigned'],
    +            security['wantAssertionsSigned'], None, None, contacts,
    +            organization
    +        )
    +        self.assertIn("""        
    +            Test Service
    +            Test Service
    +            
    +                userType
    +                admin
    +            
    +            
    +        """, metadata)
    +
         def testSignMetadata(self):
             """
             Tests the signMetadata method of the OneLogin_Saml2_Metadata
    @@ -159,9 +207,11 @@ def testSignMetadata(self):
             cert = self.file_contents(join(cert_path, 'sp.crt'))
     
             signed_metadata = OneLogin_Saml2_Metadata.sign_metadata(metadata, key, cert)
    +        self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(signed_metadata, cert))
     
             self.assertIn('', signed_metadata)
     
    -        self.assertIn('urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified', signed_metadata)
    +        self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', signed_metadata)
     
             self.assertIn('', signed_metadata)
             self.assertIn('', signed_metadata)
    +        self.assertIn('', signed_metadata)
             self.assertIn('\n', signed_metadata)
     
    -        try:
    +        with self.assertRaisesRegexp(Exception, 'Empty string supplied as input'):
                 OneLogin_Saml2_Metadata.sign_metadata('', key, cert)
    -            self.assertTrue(False)
    -        except Exception as e:
    -            self.assertIn('Empty string supplied as input', e.message)
    +
    +        signed_metadata_2 = OneLogin_Saml2_Metadata.sign_metadata(metadata, key, cert, OneLogin_Saml2_Constants.RSA_SHA256, OneLogin_Saml2_Constants.SHA384)
    +        self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(signed_metadata_2, cert))
    +        self.assertIn('', signed_metadata_2)
    +
    +        self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', signed_metadata_2)
    +
    +        self.assertIn('', signed_metadata_2)
    +        self.assertIn('', signed_metadata_2)
    +        self.assertIn('', signed_metadata_2)
    +        self.assertIn('\n', signed_metadata_2)
     
         def testAddX509KeyDescriptors(self):
             """
    @@ -209,16 +278,15 @@ def testAddX509KeyDescriptors(self):
             self.assertIn(' something_is_wrong'):
                 response_3.check_status()
    -            1 / 0
    -        except Exception as e:
    -            self.assertIn('The status code of the Response was not Success, was Responder -> something_is_wrong', e.message)
    +
    +    def testCheckOneCondition(self):
    +        """
    +        Tests the check_one_condition method of SamlResponse
    +        """
    +        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
    +        xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_conditions.xml.base64'))
    +        response = OneLogin_Saml2_Response(settings, xml)
    +        self.assertFalse(response.check_one_condition())
    +
    +        self.assertTrue(response.is_valid(self.get_request_data()))
    +        settings.set_strict(True)
    +        response = OneLogin_Saml2_Response(settings, xml)
    +        self.assertFalse(response.is_valid(self.get_request_data()))
    +        self.assertEquals('The Assertion must include a Conditions element', response.get_error())
    +
    +        xml_2 = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
    +        response_2 = OneLogin_Saml2_Response(settings, xml_2)
    +        self.assertTrue(response_2.check_one_condition())
    +
    +    def testCheckOneAuthnStatement(self):
    +        """
    +        Tests the check_one_authnstatement method of SamlResponse
    +        """
    +        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
    +        xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_authnstatement.xml.base64'))
    +        response = OneLogin_Saml2_Response(settings, xml)
    +        self.assertFalse(response.check_one_authnstatement())
    +
    +        self.assertTrue(response.is_valid(self.get_request_data()))
    +        settings.set_strict(True)
    +        response = OneLogin_Saml2_Response(settings, xml)
    +        self.assertFalse(response.is_valid(self.get_request_data()))
    +        self.assertEquals('The Assertion must include an AuthnStatement element', response.get_error())
    +
    +        xml_2 = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
    +        response_2 = OneLogin_Saml2_Response(settings, xml_2)
    +        self.assertTrue(response_2.check_one_authnstatement())
     
         def testGetAudiences(self):
             """
    @@ -174,9 +578,9 @@ def testQueryAssertions(self):
             OneLogin_Saml2_Response using the get_issuers call
             """
             settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
    -        xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
    +        xml = self.file_contents(join(self.data_path, 'responses', 'adfs_response.xml.base64'))
             response = OneLogin_Saml2_Response(settings, xml)
    -        self.assertEqual(['https://app.onelogin.com/saml/metadata/13590'], response.get_issuers())
    +        self.assertEqual(['http://login.example.com/issuer'], response.get_issuers())
     
             xml_2 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64'))
             response_2 = OneLogin_Saml2_Response(settings, xml_2)
    @@ -207,9 +611,9 @@ def testGetIssuers(self):
             Tests the get_issuers method of the OneLogin_Saml2_Response
             """
             settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
    -        xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
    +        xml = self.file_contents(join(self.data_path, 'responses', 'adfs_response.xml.base64'))
             response = OneLogin_Saml2_Response(settings, xml)
    -        self.assertEqual(['https://app.onelogin.com/saml/metadata/13590'], response.get_issuers())
    +        self.assertEqual(['http://login.example.com/issuer'], response.get_issuers())
     
             xml_2 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64'))
             response_2 = OneLogin_Saml2_Response(settings, xml_2)
    @@ -219,6 +623,16 @@ def testGetIssuers(self):
             response_3 = OneLogin_Saml2_Response(settings, xml_3)
             self.assertEqual(['http://idp.example.com/', 'https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'], response_3.get_issuers())
     
    +        xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_issuer_response.xml.base64'))
    +        response_4 = OneLogin_Saml2_Response(settings, xml_4)
    +        response_4.get_issuers()
    +        self.assertEqual(['https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'], response_4.get_issuers())
    +
    +        xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_issuer_assertion.xml.base64'))
    +        response_5 = OneLogin_Saml2_Response(settings, xml_5)
    +        with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Issuer of the Assertion not found or multiple.'):
    +            response_5.get_issuers()
    +
         def testGetSessionIndex(self):
             """
             Tests the get_session_index method of the OneLogin_Saml2_Response
    @@ -256,20 +670,38 @@ def testGetAttributes(self):
             response_3 = OneLogin_Saml2_Response(settings, xml_3)
             self.assertEqual({}, response_3.get_attributes())
     
    +    def testGetNestedNameIDAttributes(self):
    +        """
    +        Tests the getAttributes method of the OneLogin_Saml2_Response with nested
    +        nameID data
    +        """
    +        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
    +        xml = self.file_contents(join(self.data_path, 'responses', 'response_with_nested_nameid_values.xml.base64'))
    +        response = OneLogin_Saml2_Response(settings, xml)
    +        expected_attributes = {
    +            'uid': ['demo'],
    +            'another_value': [{
    +                'NameID': {
    +                    'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
    +                    'NameQualifier': 'https://idpID',
    +                    'value': 'value'
    +                }
    +            }]
    +        }
    +        self.assertEqual(expected_attributes, response.get_attributes())
    +
         def testOnlyRetrieveAssertionWithIDThatMatchesSignatureReference(self):
             """
             Tests the get_nameid method of the OneLogin_Saml2_Response
    -        The Assertion is unsigned so the method fails
    +        The response is invalid, but the nameid is returned
             """
             settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
             xml = self.file_contents(join(self.data_path, 'responses', 'wrapped_response_2.xml.base64'))
             response = OneLogin_Saml2_Response(settings, xml)
    -        try:
    -            self.assertTrue(response.is_valid(self.get_request_data()))
    -            nameid = response.get_nameid()
    -            self.assertNotEqual('root@example.com', nameid)
    -        except:
    -            self.assertEqual('Signature validation failed. SAML Response rejected', response.get_error())
    +        self.assertFalse(response.is_valid(self.get_request_data()))
    +        self.assertEqual("Invalid Signature Element {urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor SAML Response rejected", response.get_error())
    +        nameid = response.get_nameid()
    +        self.assertEqual('root@example.com', nameid)
     
         def testDoesNotAllowSignatureWrappingAttack(self):
             """
    @@ -279,8 +711,21 @@ def testDoesNotAllowSignatureWrappingAttack(self):
             settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
             xml = self.file_contents(join(self.data_path, 'responses', 'response4.xml.base64'))
             response = OneLogin_Saml2_Response(settings, xml)
    -        self.assertEqual('test@onelogin.com', response.get_nameid())
             self.assertFalse(response.is_valid(self.get_request_data()))
    +        self.assertEqual('test@onelogin.com', response.get_nameid())
    +
    +    def testNodeTextAttack(self):
    +        """
    +        Tests the get_nameid and get_attributes methods of the OneLogin_Saml2_Response
    +        Test that the node text with comment attack (VU#475445) is not allowed
    +        """
    +        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
    +        xml = self.file_contents(join(self.data_path, 'responses', 'response_node_text_attack.xml.base64'))
    +        response = OneLogin_Saml2_Response(settings, xml)
    +        nameid = response.get_nameid()
    +        attributes = response.get_attributes()
    +        self.assertEqual("smith", attributes.get('surname')[0])
    +        self.assertEqual('support@onelogin.com', nameid)
     
         def testGetSessionNotOnOrAfter(self):
             """
    @@ -300,6 +745,22 @@ def testGetSessionNotOnOrAfter(self):
             response_3 = OneLogin_Saml2_Response(settings, xml_3)
             self.assertEqual(2696012228, response_3.get_session_not_on_or_after())
     
    +    def testGetInResponseTo(self):
    +        """
    +        Tests the retrieval of the InResponseTo attribute
    +        """
    +
    +        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
    +
    +        # Response without an InResponseTo element should return None
    +        xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
    +        response = OneLogin_Saml2_Response(settings, xml)
    +        self.assertIsNone(response.get_in_response_to())
    +
    +        xml_3 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64'))
    +        response_3 = OneLogin_Saml2_Response(settings, xml_3)
    +        self.assertEqual('ONELOGIN_be60b8caf8e9d19b7a3551b244f116c947ff247d', response_3.get_in_response_to())
    +
         def testIsInvalidXML(self):
             """
             Tests the is_valid method of the OneLogin_Saml2_Response
    @@ -368,11 +829,8 @@ def testValidateVersion(self):
             settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
             xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_saml2.xml.base64'))
             response = OneLogin_Saml2_Response(settings, xml)
    -        try:
    -            valid = response.is_valid(self.get_request_data())
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertEqual('Reference validation failed', e.message)
    +        self.assertFalse(response.is_valid(self.get_request_data()))
    +        self.assertEqual('Unsupported SAML version', response.get_error())
     
         def testValidateID(self):
             """
    @@ -382,11 +840,8 @@ def testValidateID(self):
             settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
             xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_id.xml.base64'))
             response = OneLogin_Saml2_Response(settings, xml)
    -        try:
    -            valid = response.is_valid(self.get_request_data())
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertEqual('Missing ID attribute on SAML Response', e.message)
    +        self.assertFalse(response.is_valid(self.get_request_data()))
    +        self.assertEqual('Missing ID attribute on SAML Response', response.get_error())
     
         def testIsInValidReference(self):
             """
    @@ -396,11 +851,8 @@ def testIsInValidReference(self):
             settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
             xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
             response = OneLogin_Saml2_Response(settings, xml)
    -        try:
    -            valid = response.is_valid(self.get_request_data())
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertEqual('Reference validation failed', e.message)
    +        self.assertFalse(response.is_valid(self.get_request_data()))
    +        self.assertEqual('Signature validation failed. SAML Response rejected', response.get_error())
     
         def testIsInValidExpired(self):
             """
    @@ -410,16 +862,13 @@ def testIsInValidExpired(self):
             settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
             xml = self.file_contents(join(self.data_path, 'responses', 'expired_response.xml.base64'))
             response = OneLogin_Saml2_Response(settings, xml)
    -        response.is_valid(self.get_request_data())
    +        self.assertFalse(response.is_valid(self.get_request_data()))
             self.assertEqual('No Signature found. SAML Response rejected', response.get_error())
     
             settings.set_strict(True)
             response_2 = OneLogin_Saml2_Response(settings, xml)
    -        try:
    -            valid = response_2.is_valid(self.get_request_data())
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertEqual('Timing issues (please check your clock settings)', e.message)
    +        self.assertFalse(response_2.is_valid(self.get_request_data()))
    +        self.assertEqual('Could not validate timestamp: expired. Check system clock.', response_2.get_error())
     
         def testIsInValidNoStatement(self):
             """
    @@ -434,11 +883,77 @@ def testIsInValidNoStatement(self):
     
             settings.set_strict(True)
             response_2 = OneLogin_Saml2_Response(settings, xml)
    -        try:
    -            valid = response_2.is_valid(self.get_request_data())
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertEqual('There is no AttributeStatement on the Response', e.message)
    +        self.assertFalse(response_2.is_valid(self.get_request_data()))
    +        self.assertEqual('There is no AttributeStatement on the Response', response_2.get_error())
    +
    +    def testIsValidOptionalStatement(self):
    +        """
    +        Tests the is_valid method of the OneLogin_Saml2_Response
    +        Case AttributeStatement is optional
    +        """
    +        # shortcut
    +        json_settings = self.loadSettingsJSON()
    +        # ensure valid entityid
    +        json_settings['sp']['entityId'] = 'https://pitbulk.no-ip.org/newonelogin/demo1/metadata.php'
    +        json_settings['idp']['entityId'] = 'https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'
    +        json_settings['idp']['x509cert'] = """
    +MIICVzCCAcACCQDIVHaNSBYL6TANBgkqhkiG9w0BAQsFADBwMQswCQYDVQQGEwJG
    +UjEOMAwGA1UECAwFUGFyaXMxDjAMBgNVBAcMBVBhcmlzMRYwFAYDVQQKDA1Ob3Zh
    +cG9zdCBURVNUMSkwJwYJKoZIhvcNAQkBFhpmbG9yZW50LnBpZ291dEBub3ZhcG9z
    +dC5mcjAeFw0xNDAyMTMxMzUzNDBaFw0xNTAyMTMxMzUzNDBaMHAxCzAJBgNVBAYT
    +AkZSMQ4wDAYDVQQIDAVQYXJpczEOMAwGA1UEBwwFUGFyaXMxFjAUBgNVBAoMDU5v
    +dmFwb3N0IFRFU1QxKTAnBgkqhkiG9w0BCQEWGmZsb3JlbnQucGlnb3V0QG5vdmFw
    +b3N0LmZyMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQChLFHn3LnN4JQ/7WCd
    +YupxkUgcNOQnPF+yll+/DPpux9npfY059PIUatB8X7kCn5i8tRwIy/ikHJR6Mr8+
    +MPvc6VOZDxPNdZvMo/8lhxrbN3Jdrw3whZmU/KPR9F3BdFdu+SLzrMl1TDUZlPtY
    +9XzUFXcqN8IXcy8TJzCBeNey3QIDAQABMA0GCSqGSIb3DQEBCwUAA4GBACtJ8feG
    +ze1NHB5Vw18jMUPvHo7H3Gwmj6ZDAXQlaiAXMuNBxNXVWVwifl6V+nW3w9Qa7Feo
    +/nZ/O4TUOH1nz+adklcCD4QpZaEIbmAbriPWJKgb4LWGhqQruwYR7ItTR1MNX9gL
    +bP0z0zvDEQnnt/VUWFEBLSJq4Z4Nre8LFmS2
    +""".strip()
    +
    +        settings = OneLogin_Saml2_Settings(json_settings)
    +        settings.set_strict(True)
    +
    +        # want AttributeStatement True by default
    +        self.assertTrue(settings.get_security_data()['wantAttributeStatement'])
    +
    +        xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'signed_assertion_response.xml.base64'))
    +
    +        not_on_or_after = datetime.strptime('2014-03-31T08:37:16Z', '%Y-%m-%dT%H:%M:%SZ')
    +        not_on_or_after -= timedelta(seconds=150)
    +
    +        response = OneLogin_Saml2_Response(settings, xml)
    +        with freeze_time(not_on_or_after):
    +            self.assertFalse(response.is_valid({
    +                'https': 'on',
    +                'http_host': 'pitbulk.no-ip.org',
    +                'script_name': 'newonelogin/demo1/index.php?acs'
    +            }))
    +        self.assertEqual('There is no AttributeStatement on the Response', response.get_error())
    +
    +        security = settings.get_security_data()
    +        self.assertTrue(security['wantAttributeStatement'])
    +
    +        # change wantAttributeStatement to optional
    +        json_settings['security']['wantAttributeStatement'] = False
    +        settings = OneLogin_Saml2_Settings(json_settings)
    +        settings.set_strict(True)
    +
    +        # check settings
    +        self.assertFalse(settings.get_security_data()['wantAttributeStatement'])
    +
    +        response = OneLogin_Saml2_Response(settings, xml)
    +        response.is_valid(self.get_request_data())
    +
    +        # check response
    +        with freeze_time(not_on_or_after):
    +            self.assertTrue(response.is_valid({
    +                'https': 'on',
    +                'http_host': 'pitbulk.no-ip.org',
    +                'script_name': 'newonelogin/demo1/index.php?acs'
    +            }))
    +        self.assertIsNone(response.get_error())
     
         def testIsInValidNoKey(self):
             """
    @@ -448,11 +963,8 @@ def testIsInValidNoKey(self):
             settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
             xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_key.xml.base64'))
             response = OneLogin_Saml2_Response(settings, xml)
    -        try:
    -            valid = response.is_valid(self.get_request_data())
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertEqual('Signature validation failed. SAML Response rejected', e.message)
    +        self.assertFalse(response.is_valid(self.get_request_data()))
    +        self.assertEqual('Signature validation failed. SAML Response rejected', response.get_error())
     
         def testIsInValidMultipleAssertions(self):
             """
    @@ -463,11 +975,8 @@ def testIsInValidMultipleAssertions(self):
             settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
             xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'multiple_assertions.xml.base64'))
             response = OneLogin_Saml2_Response(settings, xml)
    -        try:
    -            valid = response.is_valid(self.get_request_data())
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertEqual('SAML Response must contain 1 assertion', e.message)
    +        self.assertFalse(response.is_valid(self.get_request_data()))
    +        self.assertEqual('SAML Response must contain 1 assertion', response.get_error())
     
         def testIsInValidEncAttrs(self):
             """
    @@ -477,16 +986,25 @@ def testIsInValidEncAttrs(self):
             settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
             xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'encrypted_attrs.xml.base64'))
             response = OneLogin_Saml2_Response(settings, xml)
    -        response.is_valid(self.get_request_data())
    +        self.assertFalse(response.is_valid(self.get_request_data()))
             self.assertEqual('No Signature found. SAML Response rejected', response.get_error())
     
             settings.set_strict(True)
             response_2 = OneLogin_Saml2_Response(settings, xml)
    -        try:
    -            valid = response_2.is_valid(self.get_request_data())
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertEqual('There is an EncryptedAttribute in the Response and this SP not support them', e.message)
    +        self.assertFalse(response_2.is_valid(self.get_request_data()))
    +        self.assertEqual('There is an EncryptedAttribute in the Response and this SP not support them', response_2.get_error())
    +
    +    def testIsInValidDuplicatedAttrs(self):
    +        """
    +        Tests the getAttributes method of the OneLogin_Saml2_Response
    +        Case duplicated Attrs
    +        """
    +        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
    +        xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'duplicated_attributes.xml.base64'))
    +        response = OneLogin_Saml2_Response(settings, xml)
    +        self.assertTrue(response.is_valid(self.get_request_data()))
    +        with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Found an Attribute element with duplicated Name'):
    +            response.get_attributes()
     
         def testIsInValidDestination(self):
             """
    @@ -504,18 +1022,25 @@ def testIsInValidDestination(self):
             self.assertFalse(response_2.is_valid(self.get_request_data()))
             self.assertIn('The response was received at', response_2.get_error())
     
    +        # Empty Destination
             dom = parseString(b64decode(message))
             dom.firstChild.setAttribute('Destination', '')
             message_2 = b64encode(dom.toxml())
             response_3 = OneLogin_Saml2_Response(settings, message_2)
             self.assertFalse(response_3.is_valid(self.get_request_data()))
    -        self.assertIn('A valid SubjectConfirmation was not found on this Response', response_3.get_error())
    +        self.assertIn('The response has an empty Destination value', response_3.get_error())
     
    -        dom.firstChild.removeAttribute('Destination')
    -        message_3 = b64encode(dom.toxml())
    +        message_3 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_destination.xml.base64'))
             response_4 = OneLogin_Saml2_Response(settings, message_3)
             self.assertFalse(response_4.is_valid(self.get_request_data()))
    -        self.assertIn('A valid SubjectConfirmation was not found on this Response', response_4.get_error())
    +        self.assertEquals('The response has an empty Destination value', response_4.get_error())
    +
    +        # No Destination
    +        dom.firstChild.removeAttribute('Destination')
    +        message_4 = b64encode(dom.toxml())
    +        response_5 = OneLogin_Saml2_Response(settings, message_4)
    +        self.assertFalse(response_5.is_valid(self.get_request_data()))
    +        self.assertIn('A valid SubjectConfirmation was not found on this Response', response_5.get_error())
     
         def testIsInValidAudience(self):
             """
    @@ -539,6 +1064,44 @@ def testIsInValidAudience(self):
             self.assertFalse(response_2.is_valid(request_data))
             self.assertIn('is not a valid audience for this Response', response_2.get_error())
     
    +    def testIsInValidAuthenticationContext(self):
    +        """
    +        Tests that requestedAuthnContext, when set, is compared against the
    +        response AuthnContext, which is what you use for two-factor
    +        authentication. Without this check you can get back a valid response
    +        that didn't complete the two-factor step.
    +        """
    +        request_data = self.get_request_data()
    +        message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
    +        two_factor_context = 'urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken'
    +        password_context = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password'
    +        settings_dict = self.loadSettingsJSON()
    +        settings_dict['security']['requestedAuthnContext'] = [two_factor_context]
    +        settings_dict['security']['failOnAuthnContextMismatch'] = True
    +        settings_dict['strict'] = True
    +        settings = OneLogin_Saml2_Settings(settings_dict)
    +
    +        # check that we catch when the contexts don't match
    +        response = OneLogin_Saml2_Response(settings, message)
    +        self.assertFalse(response.is_valid(request_data))
    +        self.assertIn('The AuthnContext "%s" didn\'t include requested context "%s"' % (password_context, two_factor_context), response.get_error())
    +
    +        # now drop in the expected AuthnContextClassRef and see that it passes
    +        original_message = b64decode(message)
    +        two_factor_message = original_message.replace(password_context, two_factor_context)
    +        two_factor_message = b64encode(two_factor_message)
    +        response = OneLogin_Saml2_Response(settings, two_factor_message)
    +        response.is_valid(request_data)
    +        # check that we got as far as destination validation, which comes later
    +        self.assertIn('The response was received at', response.get_error())
    +
    +        # with the default setting, check that we succeed with our original context
    +        settings_dict['security']['requestedAuthnContext'] = True
    +        settings = OneLogin_Saml2_Settings(settings_dict)
    +        response = OneLogin_Saml2_Response(settings, message)
    +        response.is_valid(request_data)
    +        self.assertIn('The response was received at', response.get_error())
    +
         def testIsInValidIssuer(self):
             """
             Tests the is_valid method of the OneLogin_Saml2_Response class
    @@ -570,18 +1133,12 @@ def testIsInValidIssuer(self):
     
             settings.set_strict(True)
             response_3 = OneLogin_Saml2_Response(settings, message)
    -        try:
    -            valid = response_3.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertEqual('is not a valid audience for this Response', e.message)
    +        self.assertFalse(response_3.is_valid(request_data))
    +        self.assertEqual('Invalid issuer in the Assertion/Response (expected http://idp.example.com/, got http://invalid.issuer.example.com/)', response_3.get_error())
     
             response_4 = OneLogin_Saml2_Response(settings, message_2)
    -        try:
    -            valid = response_4.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertEqual('is not a valid audience for this Response', e.message)
    +        self.assertFalse(response_4.is_valid(request_data))
    +        self.assertEqual('Invalid issuer in the Assertion/Response (expected http://idp.example.com/, got http://invalid.isser.example.com/)', response_4.get_error())
     
         def testIsInValidSessionIndex(self):
             """
    @@ -605,11 +1162,8 @@ def testIsInValidSessionIndex(self):
     
             settings.set_strict(True)
             response_2 = OneLogin_Saml2_Response(settings, message)
    -        try:
    -            valid = response_2.is_valid(request_data)
    -            self.assertFalse(valid)
    -        except Exception as e:
    -            self.assertEqual('The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response', e.message)
    +        self.assertFalse(response_2.is_valid(request_data))
    +        self.assertEqual('The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response', response_2.get_error())
     
         def testDatetimeWithMiliseconds(self):
             """
    @@ -699,40 +1253,28 @@ def testIsInValidSubjectConfirmation(self):
     
             settings.set_strict(True)
             response = OneLogin_Saml2_Response(settings, message)
    -        try:
    -            self.assertFalse(response.is_valid(request_data))
    -        except Exception as e:
    -            self.assertEqual('A valid SubjectConfirmation was not found on this Response', e.message)
    +        self.assertFalse(response.is_valid(request_data))
    +        self.assertEqual('A valid SubjectConfirmation was not found on this Response', response.get_error())
     
             response_2 = OneLogin_Saml2_Response(settings, message_2)
    -        try:
    -            self.assertFalse(response_2.is_valid(request_data))
    -        except Exception as e:
    -            self.assertEqual('A valid SubjectConfirmation was not found on this Response', e.message)
    +        self.assertFalse(response_2.is_valid(request_data))
    +        self.assertEqual('A valid SubjectConfirmation was not found on this Response', response_2.get_error())
     
             response_3 = OneLogin_Saml2_Response(settings, message_3)
    -        try:
    -            self.assertFalse(response_3.is_valid(request_data))
    -        except Exception as e:
    -            self.assertEqual('A valid SubjectConfirmation was not found on this Response', e.message)
    +        self.assertFalse(response_3.is_valid(request_data))
    +        self.assertEqual('A valid SubjectConfirmation was not found on this Response', response_3.get_error())
     
             response_4 = OneLogin_Saml2_Response(settings, message_4)
    -        try:
    -            self.assertFalse(response_4.is_valid(request_data))
    -        except Exception as e:
    -            self.assertEqual('A valid SubjectConfirmation was not found on this Response', e.message)
    +        self.assertFalse(response_4.is_valid(request_data))
    +        self.assertEqual('A valid SubjectConfirmation was not found on this Response', response_4.get_error())
     
             response_5 = OneLogin_Saml2_Response(settings, message_5)
    -        try:
    -            self.assertFalse(response_5.is_valid(request_data))
    -        except Exception as e:
    -            self.assertEqual('A valid SubjectConfirmation was not found on this Response', e.message)
    +        self.assertFalse(response_5.is_valid(request_data))
    +        self.assertEqual('A valid SubjectConfirmation was not found on this Response', response_5.get_error())
     
             response_6 = OneLogin_Saml2_Response(settings, message_6)
    -        try:
    -            self.assertFalse(response_6.is_valid(request_data))
    -        except Exception as e:
    -            self.assertEqual('A valid SubjectConfirmation was not found on this Response', e.message)
    +        self.assertFalse(response_6.is_valid(request_data))
    +        self.assertEqual('A valid SubjectConfirmation was not found on this Response', response_6.get_error())
     
         def testIsInValidRequestId(self):
             """
    @@ -757,15 +1299,61 @@ def testIsInValidRequestId(self):
     
             settings.set_strict(True)
             response = OneLogin_Saml2_Response(settings, message)
    -        try:
    -            self.assertFalse(response.is_valid(request_data, request_id))
    -        except Exception as e:
    -            self.assertEqual('The InResponseTo of the Response', e.message)
    +        self.assertFalse(response.is_valid(request_data, request_id))
    +        self.assertIn('The InResponseTo of the Response', response.get_error())
     
             valid_request_id = '_57bcbf70-7b1f-012e-c821-782bcb13bb38'
             response.is_valid(request_data, valid_request_id)
             self.assertEqual('No Signature found. SAML Response rejected', response.get_error())
     
    +    def testRejectUnsolicitedResponsesWithInResponseTo(self):
    +        settings_info = self.loadSettingsJSON()
    +        settings_info['strict'] = True
    +        settings_info['security']['rejectUnsolicitedResponsesWithInResponseTo'] = False
    +        settings = OneLogin_Saml2_Settings(settings_info)
    +        request_data = {
    +            'http_host': 'stuff.com',
    +            'script_name': 'endpoints/endpoints/acs.php'
    +        }
    +
    +        xml = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64'))
    +        response = OneLogin_Saml2_Response(settings, xml)
    +        response.is_valid(request_data)
    +        self.assertEqual('No Signature found. SAML Response rejected', response.get_error())
    +
    +        settings_info['security']['rejectUnsolicitedResponsesWithInResponseTo'] = True
    +        settings = OneLogin_Saml2_Settings(settings_info)
    +        response = OneLogin_Saml2_Response(settings, xml)
    +        response.is_valid(request_data)
    +        self.assertEqual('The Response has an InResponseTo attribute: _57bcbf70-7b1f-012e-c821-782bcb13bb38 while no InResponseTo was expected', response.get_error())
    +
    +        settings_info['idp']['entityId'] = 'https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'
    +        settings_info['sp']['entityId'] = 'https://pitbulk.no-ip.org/newonelogin/demo1/metadata.php'
    +        request_data = {
    +            'https': 'on',
    +            'http_host': 'pitbulk.no-ip.org',
    +            'script_name': 'newonelogin/demo1/index.php?acs'
    +        }
    +        not_on_or_after = datetime.strptime('2014-02-19T09:37:01Z', '%Y-%m-%dT%H:%M:%SZ')
    +        not_on_or_after -= timedelta(seconds=150)
    +
    +        # InResponseTo on the SubjectConfirmation only
    +        xml = self.file_contents(join(self.data_path, 'responses', 'valid_response_without_inresponseto.xml.base64'))
    +        settings_info['security']['rejectUnsolicitedResponsesWithInResponseTo'] = False
    +        settings = OneLogin_Saml2_Settings(settings_info)
    +        response = OneLogin_Saml2_Response(settings, xml)
    +
    +        with freeze_time(not_on_or_after):
    +            self.assertTrue(response.is_valid(request_data))
    +
    +        settings_info['security']['rejectUnsolicitedResponsesWithInResponseTo'] = True
    +        settings = OneLogin_Saml2_Settings(settings_info)
    +        response = OneLogin_Saml2_Response(settings, xml)
    +
    +        with freeze_time(not_on_or_after):
    +            self.assertFalse(response.is_valid(request_data))
    +            self.assertEquals("A valid SubjectConfirmation was not found on this Response", response.get_error())
    +
         def testIsInValidSignIssues(self):
             """
             Tests the is_valid method of the OneLogin_Saml2_Response class
    @@ -804,10 +1392,8 @@ def testIsInValidSignIssues(self):
             settings_info['security']['wantAssertionsSigned'] = True
             settings_4 = OneLogin_Saml2_Settings(settings_info)
             response_4 = OneLogin_Saml2_Response(settings_4, message)
    -        try:
    -            self.assertFalse(response_4.is_valid(request_data))
    -        except Exception as e:
    -            self.assertEqual('The Assertion of the Response is not signed and the SP require it', e.message)
    +        self.assertFalse(response_4.is_valid(request_data))
    +        self.assertEqual('The Assertion of the Response is not signed and the SP require it', response_4.get_error())
     
             settings_info['security']['wantAssertionsSigned'] = False
             settings_info['strict'] = False
    @@ -828,16 +1414,14 @@ def testIsInValidSignIssues(self):
             settings_info['security']['wantMessagesSigned'] = False
             settings_7 = OneLogin_Saml2_Settings(settings_info)
             response_7 = OneLogin_Saml2_Response(settings_7, message)
    -        response_7.is_valid(request_data)
    +        self.assertFalse(response_7.is_valid(request_data))
             self.assertEqual('No Signature found. SAML Response rejected', response_7.get_error())
     
             settings_info['security']['wantMessagesSigned'] = True
             settings_8 = OneLogin_Saml2_Settings(settings_info)
             response_8 = OneLogin_Saml2_Response(settings_8, message)
    -        try:
    -            self.assertFalse(response_8.is_valid(request_data))
    -        except Exception as e:
    -            self.assertEqual('The Message of the Response is not signed and the SP require it', e.message)
    +        self.assertFalse(response_8.is_valid(request_data))
    +        self.assertEqual('The Message of the Response is not signed and the SP require it', response_8.get_error())
     
         def testIsInValidEncIssues(self):
             """
    @@ -913,15 +1497,13 @@ def testIsInValidCert(self):
             Case invalid cert
             """
             settings_info = self.loadSettingsJSON()
    +        settings_info['debug'] = False
             settings_info['idp']['x509cert'] = 'NotValidCert'
             settings = OneLogin_Saml2_Settings(settings_info)
             xml = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
             response = OneLogin_Saml2_Response(settings, xml)
    -
    -        try:
    -            self.assertFalse(response.is_valid(self.get_request_data()))
    -        except Exception as e:
    -            self.assertIn('openssl_x509_read(): supplied parameter cannot be', e.message)
    +        self.assertFalse(response.is_valid(self.get_request_data()))
    +        self.assertIn('Signature validation failed. SAML Response rejected', response.get_error())
     
         def testIsInValidCert2(self):
             """
    @@ -968,7 +1550,7 @@ def testIsValid2(self):
             self.assertTrue(response_2.is_valid(self.get_request_data()))
     
             settings_info_3 = self.loadSettingsJSON('settings2.json')
    -        idp_cert = settings_info_3['idp']['x509cert']
    +        idp_cert = OneLogin_Saml2_Utils.format_cert(settings_info_3['idp']['x509cert'])
             settings_info_3['idp']['certFingerprint'] = OneLogin_Saml2_Utils.calculate_x509_fingerprint(idp_cert)
             settings_info_3['idp']['x509cert'] = ''
             settings_3 = OneLogin_Saml2_Settings(settings_info_3)
    @@ -1041,6 +1623,18 @@ def testIsValidEnc(self):
             response_7.is_valid(request_data)
             self.assertEqual('No Signature found. SAML Response rejected', response_7.get_error())
     
    +    def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self):
    +        message = b64encode('invalid')
    +        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
    +        settings.set_strict(True)
    +
    +        response = OneLogin_Saml2_Response(settings, message)
    +
    +        self.assertFalse(response.is_valid(self.get_request_data()))
    +
    +        with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Unsupported SAML version"):
    +            response.is_valid(self.get_request_data(), raise_exceptions=True)
    +
         def testIsValidSign(self):
             """
             Tests the is_valid method of the OneLogin_Saml2_Response
    @@ -1098,6 +1692,16 @@ def testIsValidSign(self):
             # Modified message
             self.assertFalse(response_9.is_valid(self.get_request_data()))
     
    +    def testIsValidSignUsingX509certMulti(self):
    +        """
    +        Tests the is_valid method of the OneLogin_Saml2_Response
    +        Case Using x509certMulti
    +        """
    +        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON('settings8.json'))
    +        xml = self.file_contents(join(self.data_path, 'responses', 'signed_message_response.xml.base64'))
    +        response = OneLogin_Saml2_Response(settings, xml)
    +        self.assertTrue(response.is_valid(self.get_request_data()))
    +
         def testIsValidSignWithEmptyReferenceURI(self):
             settings_info = self.loadSettingsJSON()
             del settings_info['idp']['x509cert']
    @@ -1105,4 +1709,95 @@ def testIsValidSignWithEmptyReferenceURI(self):
             settings = OneLogin_Saml2_Settings(settings_info)
             xml = self.file_contents(join(self.data_path, 'responses', 'response_without_reference_uri.xml.base64'))
             response = OneLogin_Saml2_Response(settings, xml)
    -        self.assertTrue(response.is_valid(self.get_request_data()))
    +        self.assertFalse(response.is_valid(self.get_request_data()))
    +
    +    def testIsValidWithoutInResponseTo(self):
    +        """
    +        If assertion contains InResponseTo but not the Response tag, we should
    +        not compare the assertion InResponseTo value to None.
    +        """
    +
    +        # prepare strict settings
    +        settings_info = self.loadSettingsJSON()
    +        settings_info['strict'] = True
    +        settings_info['idp']['entityId'] = 'https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'
    +        settings_info['sp']['entityId'] = 'https://pitbulk.no-ip.org/newonelogin/demo1/metadata.php'
    +
    +        settings = OneLogin_Saml2_Settings(settings_info)
    +
    +        xml = self.file_contents(join(self.data_path, 'responses', 'valid_response_without_inresponseto.xml.base64'))
    +        response = OneLogin_Saml2_Response(settings, xml)
    +
    +        not_on_or_after = datetime.strptime('2014-02-19T09:37:01Z', '%Y-%m-%dT%H:%M:%SZ')
    +        not_on_or_after -= timedelta(seconds=150)
    +
    +        with freeze_time(not_on_or_after):
    +            self.assertTrue(response.is_valid({
    +                'https': 'on',
    +                'http_host': 'pitbulk.no-ip.org',
    +                'script_name': 'newonelogin/demo1/index.php?acs'
    +            }))
    +
    +    def testStatusCheckBeforeAssertionCheck(self):
    +        """
    +        Tests the status of a response is checked before the assertion count. As failed statuses will have no assertions
    +        """
    +        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
    +        xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'status_code_responder.xml.base64'))
    +        response = OneLogin_Saml2_Response(settings, xml)
    +        with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The status code of the Response was not Success, was Responder'):
    +            response.is_valid(self.get_request_data(), raise_exceptions=True)
    +
    +    def testGetId(self):
    +        """
    +        Tests that we can retrieve the ID of the Response
    +        """
    +        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
    +        xml = self.file_contents(join(self.data_path, 'responses', 'signed_message_response.xml.base64'))
    +        response = OneLogin_Saml2_Response(settings, xml)
    +        self.assertEqual(response.get_id(), 'pfxc3d2b542-0f7e-8767-8e87-5b0dc6913375')
    +
    +    def testGetAssertionId(self):
    +        """
    +        Tests that we can retrieve the ID of the Assertion
    +        """
    +        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
    +        xml = self.file_contents(join(self.data_path, 'responses', 'signed_message_response.xml.base64'))
    +        response = OneLogin_Saml2_Response(settings, xml)
    +        self.assertEqual(response.get_assertion_id(), '_cccd6024116641fe48e0ae2c51220d02755f96c98d')
    +
    +    def testGetAssertionNotOnOrAfter(self):
    +        """
    +        Tests that we can retrieve the NotOnOrAfter value of
    +        the valid SubjectConfirmationData
    +        """
    +        settings_data = self.loadSettingsJSON()
    +        request_data = self.get_request_data()
    +        settings = OneLogin_Saml2_Settings(settings_data)
    +        message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
    +        response = OneLogin_Saml2_Response(settings, message)
    +        self.assertIsNone(response.get_assertion_not_on_or_after())
    +
    +        response.is_valid(request_data)
    +        self.assertIsNone(response.get_error())
    +        self.assertIsNone(response.get_assertion_not_on_or_after())
    +
    +        settings_data['strict'] = True
    +        settings = OneLogin_Saml2_Settings(settings_data)
    +        response = OneLogin_Saml2_Response(settings, message)
    +
    +        response.is_valid(request_data)
    +        self.assertNotEqual(response.get_error(), None)
    +        self.assertIsNone(response.get_assertion_not_on_or_after())
    +
    +        request_data['https'] = 'on'
    +        request_data['http_host'] = 'pitbulk.no-ip.org'
    +        request_data['script_name'] = '/newonelogin/demo1/index.php?acs'
    +        response.is_valid(request_data)
    +        self.assertIsNone(response.get_error())
    +        self.assertEqual(response.get_assertion_not_on_or_after(), 2671081021)
    +
    +
    +if __name__ == '__main__':
    +    runner = unittest.TextTestRunner()
    +    unittest.main(testRunner=runner)
    diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py
    index 73f6a0b5..bf2b547d 100644
    --- a/tests/src/OneLogin/saml2_tests/settings_test.py
    +++ b/tests/src/OneLogin/saml2_tests/settings_test.py
    @@ -1,7 +1,7 @@
     # -*- coding: utf-8 -*-
     
    -# Copyright (c) 2014, OneLogin, Inc.
    -# All rights reserved.
    +# Copyright (c) 2010-2018 OneLogin, Inc.
    +# MIT License
     
     import json
     from os.path import dirname, join, exists, sep
    @@ -13,11 +13,11 @@
     
     
     class OneLogin_Saml2_Settings_Test(unittest.TestCase):
    -    data_path = join(dirname(__file__), '..', '..', '..', 'data')
    -    settings_path = join(dirname(__file__), '..', '..', '..', 'settings')
    +    data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data')
    +    settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings')
     
    -    def loadSettingsJSON(self):
    -        filename = join(self.settings_path, 'settings1.json')
    +    def loadSettingsJSON(self, name='settings1.json'):
    +        filename = join(self.settings_path, name)
             if exists(filename):
                 stream = open(filename, 'r')
                 settings = json.load(stream)
    @@ -57,46 +57,29 @@ def testLoadSettingsFromDict(self):
             self.assertEqual(len(settings.get_errors()), 0)
     
             settings_info['idp']['singleSignOnService']['url'] = 'invalid_url'
    -        try:
    -            settings_2 = OneLogin_Saml2_Settings(settings_info)
    -            self.assertNotEqual(len(settings_2.get_errors()), 0)
    -        except Exception as e:
    -            self.assertIn('Invalid dict settings: idp_sso_url_invalid', e.message)
    +        with self.assertRaisesRegexp(Exception, 'Invalid dict settings: idp_sso_url_invalid'):
    +            OneLogin_Saml2_Settings(settings_info)
     
             settings_info['idp']['singleSignOnService']['url'] = 'http://invalid_domain'
    -        try:
    -            settings_3 = OneLogin_Saml2_Settings(settings_info)
    -            self.assertNotEqual(len(settings_3.get_errors()), 0)
    -        except Exception as e:
    -            self.assertIn('Invalid dict settings: idp_sso_url_invalid', e.message)
    +        with self.assertRaisesRegexp(Exception, 'Invalid dict settings: idp_sso_url_invalid'):
    +            OneLogin_Saml2_Settings(settings_info)
     
             del settings_info['sp']
             del settings_info['idp']
    -        try:
    -            settings_4 = OneLogin_Saml2_Settings(settings_info)
    -            self.assertNotEqual(len(settings_4.get_errors()), 0)
    -        except Exception as e:
    -            self.assertIn('Invalid dict settings', e.message)
    -            self.assertIn('idp_not_found', e.message)
    -            self.assertIn('sp_not_found', e.message)
    +        with self.assertRaisesRegexp(Exception, 'Invalid dict settings: idp_not_found,sp_not_found'):
    +            OneLogin_Saml2_Settings(settings_info)
     
             settings_info = self.loadSettingsJSON()
             settings_info['security']['authnRequestsSigned'] = True
             settings_info['custom_base_path'] = dirname(__file__)
    -        try:
    -            settings_5 = OneLogin_Saml2_Settings(settings_info)
    -            self.assertNotEqual(len(settings_5.get_errors()), 0)
    -        except Exception as e:
    -            self.assertIn('Invalid dict settings: sp_cert_not_found_and_required', e.message)
    +        with self.assertRaisesRegexp(Exception, 'Invalid dict settings: sp_cert_not_found_and_required'):
    +            OneLogin_Saml2_Settings(settings_info)
     
             settings_info = self.loadSettingsJSON()
             settings_info['security']['nameIdEncrypted'] = True
             del settings_info['idp']['x509cert']
    -        try:
    -            settings_6 = OneLogin_Saml2_Settings(settings_info)
    -            self.assertNotEqual(len(settings_6.get_errors()), 0)
    -        except Exception as e:
    -            self.assertIn('Invalid dict settings: idp_cert_not_found_and_required', e.message)
    +        with self.assertRaisesRegexp(Exception, 'Invalid dict settings: idp_cert_not_found_and_required'):
    +            OneLogin_Saml2_Settings(settings_info)
     
         def testLoadSettingsFromInvalidData(self):
             """
    @@ -105,11 +88,8 @@ def testLoadSettingsFromInvalidData(self):
             """
             invalid_settings = ('param1', 'param2')
     
    -        try:
    +        with self.assertRaisesRegexp(Exception, 'Unsupported settings object'):
                 settings = OneLogin_Saml2_Settings(invalid_settings)
    -            self.assertTrue(False)
    -        except Exception as e:
    -            self.assertIn('Unsupported settings object', e.message)
     
             settings = OneLogin_Saml2_Settings(custom_base_path=self.settings_path)
             self.assertEqual(len(settings.get_errors()), 0)
    @@ -119,17 +99,15 @@ def testLoadSettingsFromFile(self):
             Tests the OneLogin_Saml2_Settings Constructor.
             Case load setting from file
             """
    -        custom_base_path = join(dirname(__file__), '..', '..', '..', 'settings')
    +        custom_base_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings')
             settings = OneLogin_Saml2_Settings(custom_base_path=custom_base_path)
             self.assertEqual(len(settings.get_errors()), 0)
     
             custom_base_path = dirname(__file__)
    -        try:
    +        with self.assertRaisesRegexp(Exception, 'Settings file not found'):
                 OneLogin_Saml2_Settings(custom_base_path=custom_base_path)
    -        except Exception as e:
    -            self.assertIn('Settings file not found', e.message)
     
    -        custom_base_path = join(dirname(__file__), '..', '..', '..', 'data', 'customPath')
    +        custom_base_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data', 'customPath')
             settings_3 = OneLogin_Saml2_Settings(custom_base_path=custom_base_path)
             self.assertEqual(len(settings_3.get_errors()), 0)
     
    @@ -185,6 +163,21 @@ def testGetSPCert(self):
             settings_3 = OneLogin_Saml2_Settings(settings_data, custom_base_path=custom_base_path)
             self.assertIsNone(settings_3.get_sp_cert())
     
    +    def testGetSPCertNew(self):
    +        """
    +        Tests the get_sp_cert_new method of the OneLogin_Saml2_Settings
    +        """
    +        settings_data = self.loadSettingsJSON()
    +        cert = "-----BEGIN CERTIFICATE-----\nMIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMC\nTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYD\nVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG\n9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4\nMTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xi\nZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2Zl\naWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5v\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LO\nNoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHIS\nKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d\n1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8\nBUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7n\nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2Qar\nQ4/67OZfHd7R+POBXhophSMv1ZOo\n-----END CERTIFICATE-----\n"
    +        settings = OneLogin_Saml2_Settings(settings_data)
    +        self.assertEqual(cert, settings.get_sp_cert())
    +        self.assertIsNone(settings.get_sp_cert_new())
    +
    +        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON('settings7.json'))
    +        cert_new = "-----BEGIN CERTIFICATE-----\nMIICVDCCAb2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBHMQswCQYDVQQGEwJ1czEQ\nMA4GA1UECAwHZXhhbXBsZTEQMA4GA1UECgwHZXhhbXBsZTEUMBIGA1UEAwwLZXhh\nbXBsZS5jb20wHhcNMTcwNDA3MDgzMDAzWhcNMjcwNDA1MDgzMDAzWjBHMQswCQYD\nVQQGEwJ1czEQMA4GA1UECAwHZXhhbXBsZTEQMA4GA1UECgwHZXhhbXBsZTEUMBIG\nA1UEAwwLZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKhP\nS4/0azxbQekHHewQGKD7Pivr3CDpsrKxY3xlVanxj427OwzOb5KUVzsDEazumt6s\nZFY8HfidsjXY4EYA4ZzyL7ciIAR5vlAsIYN9nJ4AwVDnN/RjVwj+TN6BqWPLpVIp\nHc6Dl005HyE0zJnk1DZDn2tQVrIzbD3FhCp7YeotAgMBAAGjUDBOMB0GA1UdDgQW\nBBRYZx4thASfNvR/E7NsCF2IaZ7wIDAfBgNVHSMEGDAWgBRYZx4thASfNvR/E7Ns\nCF2IaZ7wIDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBACz4aobx9aG3\nkh+rNyrlgM3K6dYfnKG1/YH5sJCAOvg8kDr0fQAQifH8lFVWumKUMoAe0bFTfwWt\np/VJ8MprrEJth6PFeZdczpuv+fpLcNj2VmNVJqvQYvS4m36OnBFh1QFZW8UrbFIf\ndtm2nuZ+twSKqfKwjLdqcoX0p39h7Uw/\n-----END CERTIFICATE-----\n"
    +        self.assertEqual(cert, settings.get_sp_cert())
    +        self.assertEqual(cert_new, settings.get_sp_cert_new())
    +
         def testGetSPKey(self):
             """
             Tests the get_sp_key method of the OneLogin_Saml2_Settings
    @@ -267,19 +260,12 @@ def testCheckSettings(self):
             The checkSettings method is private and is used at the constructor
             """
             settings_info = {}
    -        try:
    +        with self.assertRaisesRegexp(Exception, 'Invalid dict settings: invalid_syntax'):
                 OneLogin_Saml2_Settings(settings_info)
    -            self.assertTrue(False)
    -        except Exception as e:
    -            self.assertIn('Invalid dict settings: invalid_syntax', e.message)
     
             settings_info['strict'] = True
    -        try:
    +        with self.assertRaisesRegexp(Exception, 'Invalid dict settings: idp_not_found,sp_not_found'):
                 OneLogin_Saml2_Settings(settings_info)
    -            self.assertTrue(False)
    -        except Exception as e:
    -            self.assertIn('idp_not_found', e.message)
    -            self.assertIn('sp_not_found', e.message)
     
             settings_info['idp'] = {}
             settings_info['idp']['x509cert'] = ''
    @@ -287,14 +273,30 @@ def testCheckSettings(self):
             settings_info['sp']['entityID'] = 'SPentityId'
             settings_info['security'] = {}
             settings_info['security']['signMetadata'] = False
    -        try:
    +        with self.assertRaisesRegexp(Exception, 'Invalid dict settings: idp_entityId_not_found,idp_sso_not_found,sp_entityId_not_found,sp_acs_not_found'):
    +            OneLogin_Saml2_Settings(settings_info)
    +
    +        # AttributeConsumingService tests
    +        # serviceName, requestedAttributes are required
    +        settings_info['sp']['attributeConsumingService'] = {
    +            "serviceDescription": "Test Service"
    +        }
    +        with self.assertRaisesRegexp(Exception, 'Invalid dict settings: idp_entityId_not_found,idp_sso_not_found,sp_entityId_not_found,sp_acs_not_found,sp_attributeConsumingService_serviceName_not_found,sp_attributeConsumingService_requestedAttributes_not_found'):
    +            OneLogin_Saml2_Settings(settings_info)
    +
    +        # requestedAttributes/name is required
    +        settings_info['sp']['attributeConsumingService'] = {
    +            "serviceName": {},
    +            "serviceDescription": ["Test Service"],
    +            "requestedAttributes": [{
    +                "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
    +                "friendlyName": "givenName",
    +                "isRequired": "False"
    +            }
    +            ]
    +        }
    +        with self.assertRaisesRegexp(Exception, 'Invalid dict settings: idp_entityId_not_found,idp_sso_not_found,sp_entityId_not_found,sp_acs_not_found,sp_attributeConsumingService_serviceName_type_invalid,sp_attributeConsumingService_requestedAttributes_name_not_found,sp_attributeConsumingService_requestedAttributes_isRequired_type_invalid,sp_attributeConsumingService_serviceDescription_type_invalid'):
                 OneLogin_Saml2_Settings(settings_info)
    -            self.assertTrue(False)
    -        except Exception as e:
    -            self.assertIn('idp_entityId_not_found', e.message)
    -            self.assertIn('idp_sso_not_found', e.message)
    -            self.assertIn('sp_entityId_not_found', e.message)
    -            self.assertIn('sp_acs_not_found', e.message)
     
             settings_info['idp']['entityID'] = 'entityId'
             settings_info['idp']['singleSignOnService'] = {}
    @@ -305,21 +307,12 @@ def testCheckSettings(self):
             settings_info['sp']['assertionConsumerService']['url'] = 'invalid_value'
             settings_info['sp']['singleLogoutService'] = {}
             settings_info['sp']['singleLogoutService']['url'] = 'invalid_value'
    -        try:
    +        with self.assertRaisesRegexp(Exception, 'Invalid dict settings: idp_entityId_not_found,idp_sso_url_invalid,idp_slo_url_invalid,sp_entityId_not_found,sp_acs_url_invalid,sp_attributeConsumingService_serviceName_type_invalid,sp_attributeConsumingService_requestedAttributes_name_not_found,sp_attributeConsumingService_requestedAttributes_isRequired_type_invalid,sp_attributeConsumingService_serviceDescription_type_invalid,sp_sls_url_invalid'):
                 OneLogin_Saml2_Settings(settings_info)
    -            self.assertTrue(False)
    -        except Exception as e:
    -            self.assertIn('idp_sso_url_invalid', e.message)
    -            self.assertIn('idp_slo_url_invalid', e.message)
    -            self.assertIn('sp_acs_url_invalid', e.message)
    -            self.assertIn('sp_sls_url_invalid', e.message)
     
             settings_info['security']['wantAssertionsSigned'] = True
    -        try:
    +        with self.assertRaisesRegexp(Exception, 'Invalid dict settings: idp_entityId_not_found,idp_sso_url_invalid,idp_slo_url_invalid,idp_cert_or_fingerprint_not_found_and_required,sp_entityId_not_found,sp_acs_url_invalid,sp_attributeConsumingService_serviceName_type_invalid,sp_attributeConsumingService_requestedAttributes_name_not_found,sp_attributeConsumingService_requestedAttributes_isRequired_type_invalid,sp_attributeConsumingService_serviceDescription_type_invalid,sp_sls_url_invalid'):
                 OneLogin_Saml2_Settings(settings_info)
    -            self.assertTrue(False)
    -        except Exception as e:
    -            self.assertIn('idp_cert_or_fingerprint_not_found_and_required', e.message)
     
             settings_info = self.loadSettingsJSON()
             settings_info['security']['signMetadata'] = {}
    @@ -338,20 +331,18 @@ def testCheckSettings(self):
                     'emailAddress': 'auxiliar@example.com'
                 }
             }
    -        try:
    +        with self.assertRaisesRegexp(Exception, 'Invalid dict settings: sp_signMetadata_invalid,contact_type_invalid,contact_not_enought_data,organization_not_enought_data'):
                 OneLogin_Saml2_Settings(settings_info)
    -            self.assertTrue(False)
    -        except Exception as e:
    -            self.assertIn('sp_signMetadata_invalid', e.message)
    -            self.assertIn('organization_not_enought_data', e.message)
    -            self.assertIn('contact_type_invalid', e.message)
     
         def testGetSPMetadata(self):
             """
             Tests the getSPMetadata method of the OneLogin_Saml2_Settings
             Case unsigned metadata
             """
    -        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
    +        settings_info = self.loadSettingsJSON()
    +        settings_info['security']['wantNameIdEncrypted'] = False
    +        settings_info['security']['wantAssertionsEncrypted'] = False
    +        settings = OneLogin_Saml2_Settings(settings_info)
             metadata = settings.get_sp_metadata()
     
             self.assertNotEqual(len(metadata), 0)
    @@ -361,7 +352,57 @@ def testGetSPMetadata(self):
             self.assertIn('WantAssertionsSigned="false"', metadata)
             self.assertIn('', metadata)
             self.assertIn('', metadata)
    -        self.assertIn('urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified', metadata)
    +        self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', metadata)
    +        self.assertEquals(1, metadata.count('', metadata)
    +        self.assertIn(u'Sérvïçé prövïdér', metadata)
    +        self.assertIn(u'Téçhnïçäl Nämé', metadata)
    +        self.assertIn(u'Süppört Nämé', metadata)
     
         def testGetSPMetadataSigned(self):
             """
    @@ -416,7 +457,7 @@ def generateAndCheckMetadata(self, settings):
             self.assertIn('WantAssertionsSigned="false"', metadata)
             self.assertIn('', metadata)
             self.assertIn('', metadata)
    -        self.assertIn('urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified', metadata)
    +        self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', metadata)
             self.assertIn('', metadata)
             self.assertIn('', metadata)
             self.assertIn(' parsed_duration)
     
             invalid_duration = 'PT1Y'
    -        try:
    -            parsed_duration_3 = OneLogin_Saml2_Utils.parse_duration(invalid_duration)
    -            self.assertEqual(parsed_duration_3, 42)
    -        except Exception as e:
    -            self.assertIn('Unrecognised ISO 8601 date format', e.message)
    +        with self.assertRaisesRegexp(Exception, 'Unrecognised ISO 8601 date format'):
    +            OneLogin_Saml2_Utils.parse_duration(invalid_duration)
     
             new_duration = 'P1Y1M'
             parsed_duration_4 = OneLogin_Saml2_Utils.parse_duration(new_duration, timestamp)
    @@ -472,11 +469,8 @@ def testParseSAML2Time(self):
             saml_time = '2013-12-10T04:39:31Z'
             self.assertEqual(time, OneLogin_Saml2_Utils.parse_SAML_to_time(saml_time))
     
    -        try:
    +        with self.assertRaisesRegexp(Exception, 'does not match format'):
                 OneLogin_Saml2_Utils.parse_SAML_to_time('invalidSAMLTime')
    -            self.assertTrue(False)
    -        except Exception as e:
    -            self.assertIn('does not match format', e.message)
     
             # Now test if toolkit supports miliseconds
             saml_time2 = '2013-12-10T04:39:31.120Z'
    @@ -490,11 +484,8 @@ def testParseTime2SAML(self):
             saml_time = '2013-12-10T04:39:31Z'
             self.assertEqual(saml_time, OneLogin_Saml2_Utils.parse_time_to_SAML(time))
     
    -        try:
    +        with self.assertRaisesRegexp(Exception, 'could not convert string to float'):
                 OneLogin_Saml2_Utils.parse_time_to_SAML('invalidtime')
    -            self.assertTrue(False)
    -        except Exception as e:
    -            self.assertIn('could not convert string to float', e.message)
     
         def testGetExpireTime(self):
             """
    @@ -557,16 +548,53 @@ def testQuery(self):
             signature_nodes_5 = OneLogin_Saml2_Utils.query(dom, './/ds:SignatureValue', assertion)
             self.assertEqual(1, len(signature_nodes_5))
     
    -    def testGenerateNameId(self):
    +    def _generate_name_id_element(self, name_qualifier):
    +        name_id_value = 'value'
    +        entity_id = 'sp-entity-id'
    +        name_id_format = 'name-id-format'
    +
    +        raw_name_id = OneLogin_Saml2_Utils.generate_name_id(
    +            name_id_value,
    +            entity_id,
    +            name_id_format,
    +            nq=name_qualifier,
    +        )
    +        parser = etree.XMLParser(recover=True)
    +        return etree.fromstring(raw_name_id, parser)
    +
    +    def testNameidGenerationIncludesNameQualifierAttribute(self):
    +        """
    +        Tests the inclusion of NameQualifier in the generateNameId method of the OneLogin_Saml2_Utils
    +        """
    +        idp_name_qualifier = 'idp-name-qualifier'
    +        idp_name_qualifier_attribute = ('NameQualifier', idp_name_qualifier)
    +
    +        name_id = self._generate_name_id_element(idp_name_qualifier)
    +
    +        self.assertIn(idp_name_qualifier_attribute, name_id.attrib.items())
    +
    +    def testNameidGenerationDoesNotIncludeNameQualifierAttribute(self):
    +        """
    +        Tests the (not) inclusion of NameQualifier in the generateNameId method of the OneLogin_Saml2_Utils
    +        """
    +        idp_name_qualifier = None
    +        not_expected_attribute = 'NameQualifier'
    +
    +        name_id = self._generate_name_id_element(idp_name_qualifier)
    +
    +        self.assertNotIn(not_expected_attribute, name_id.attrib.keys())
    +
    +    def testGenerateNameIdWithSPNameQualifier(self):
             """
             Tests the generateNameId method of the OneLogin_Saml2_Utils
    +        Adding a SPNameQualifier
             """
             name_id_value = 'ONELOGIN_ce998811003f4e60f8b07a311dc641621379cfde'
             entity_id = 'http://stuff.com/endpoints/metadata.php'
    -        name_id_format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified'
    +        name_id_format = 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified'
     
             name_id = OneLogin_Saml2_Utils.generate_name_id(name_id_value, entity_id, name_id_format)
    -        expected_name_id = 'ONELOGIN_ce998811003f4e60f8b07a311dc641621379cfde'
    +        expected_name_id = 'ONELOGIN_ce998811003f4e60f8b07a311dc641621379cfde'
             self.assertEqual(name_id, expected_name_id)
     
             settings_info = self.loadSettingsJSON()
    @@ -577,6 +605,36 @@ def testGenerateNameId(self):
             expected_name_id_enc = ''
             self.assertIn(expected_name_id_enc, name_id_enc)
     
    +    def testGenerateNameIdWithoutFormat(self):
    +        """
    +        Tests the generateNameId method of the OneLogin_Saml2_Utils
    +        """
    +        name_id_value = 'ONELOGIN_ce998811003f4e60f8b07a311dc641621379cfde'
    +        name_id_format = None
    +
    +        name_id = OneLogin_Saml2_Utils.generate_name_id(name_id_value, None, name_id_format)
    +        expected_name_id = 'ONELOGIN_ce998811003f4e60f8b07a311dc641621379cfde'
    +        self.assertEqual(name_id, expected_name_id)
    +
    +    def testGenerateNameIdWithoutSPNameQualifier(self):
    +        """
    +        Tests the generateNameId method of the OneLogin_Saml2_Utils
    +        """
    +        name_id_value = 'ONELOGIN_ce998811003f4e60f8b07a311dc641621379cfde'
    +        name_id_format = 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified'
    +
    +        name_id = OneLogin_Saml2_Utils.generate_name_id(name_id_value, None, name_id_format)
    +        expected_name_id = 'ONELOGIN_ce998811003f4e60f8b07a311dc641621379cfde'
    +        self.assertEqual(name_id, expected_name_id)
    +
    +        settings_info = self.loadSettingsJSON()
    +        x509cert = settings_info['idp']['x509cert']
    +        key = OneLogin_Saml2_Utils.format_cert(x509cert)
    +
    +        name_id_enc = OneLogin_Saml2_Utils.generate_name_id(name_id_value, None, name_id_format, key)
    +        expected_name_id_enc = ''
    +        self.assertIn(expected_name_id_enc, name_id_enc)
    +
         def testCalculateX509Fingerprint(self):
             """
             Tests the calculateX509Fingerprint method of the OneLogin_Saml2_Utils
    @@ -597,6 +655,9 @@ def testCalculateX509Fingerprint(self):
     
             self.assertEqual('3db29251b97559c67988ea0754cb0573fc409b6f75d89282d57cfb75089539b0bbdb2dcd9ec6e032549ecbc466439d5992e18db2cf5494ca2fe1b2e16f348dff', OneLogin_Saml2_Utils.calculate_x509_fingerprint(cert, 'sha512'))
     
    +    def dscb(self):
    +        return self.session_clear()
    +
         def testDeleteLocalSession(self):
             """
             Tests the delete_local_session method of the OneLogin_Saml2_Utils
    @@ -607,11 +668,10 @@ def testDeleteLocalSession(self):
             OneLogin_Saml2_Utils.delete_local_session()
             self.assertEqual(1, local_session_test)
     
    -        dscb = lambda: self.session_cear()
    -        OneLogin_Saml2_Utils.delete_local_session(dscb)
    +        OneLogin_Saml2_Utils.delete_local_session(self.dscb)
             self.assertEqual(0, local_session_test)
     
    -    def session_cear(self):
    +    def session_clear(self):
             """
             Auxiliar method to test the delete_local_session method of the OneLogin_Saml2_Utils
             """
    @@ -642,11 +702,11 @@ def testDecryptElement(self):
             encrypted_data = encrypted_nameid_nodes[0].firstChild
             encrypted_data_str = str(encrypted_nameid_nodes[0].firstChild.toxml())
             decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key)
    -        self.assertEqual('{%s}NameID' % (OneLogin_Saml2_Constants.NS_SAML), decrypted_nameid.tag)
    +        self.assertIn('NameID', decrypted_nameid.tag)
             self.assertEqual('2de11defd199f8d5bb63f9b7deb265ba5c675c10', decrypted_nameid.text)
     
             decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data_str, key)
    -        self.assertEqual('{%s}NameID' % (OneLogin_Saml2_Constants.NS_SAML), decrypted_nameid.tag)
    +        self.assertIn('NameID', decrypted_nameid.tag)
             self.assertEqual('2de11defd199f8d5bb63f9b7deb265ba5c675c10', decrypted_nameid.text)
     
             xml_assertion_enc = b64decode(self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion_encrypted_nameid.xml.base64')))
    @@ -662,7 +722,7 @@ def testDecryptElement(self):
             encrypted_nameid_nodes = decrypted_assertion.xpath('/saml:Assertion/saml:Subject/saml:EncryptedID', namespaces=OneLogin_Saml2_Constants.NSMAP)
             encrypted_data = encrypted_nameid_nodes[0][0]
             decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key)
    -        self.assertEqual('{%s}NameID' % (OneLogin_Saml2_Constants.NS_SAML), decrypted_nameid.tag)
    +        self.assertIn('{%s}NameID' % (OneLogin_Saml2_Constants.NS_SAML), decrypted_nameid.tag)
             self.assertEqual('457bdb600de717891c77647b0806ce59c089d5b8', decrypted_nameid.text)
     
             key_2_file_name = join(self.data_path, 'misc', 'sp2.key')
    @@ -670,41 +730,71 @@ def testDecryptElement(self):
             key2 = f.read()
             f.close()
     
    -        try:
    -            OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key2)
    -            self.assertTrue(False)
    -        except:
    -            pass
    +        # sp.key and sp2.key are equivalent we should be able to decrypt the nameID again
    +        decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key2)
    +        self.assertIn('{%s}NameID' % (OneLogin_Saml2_Constants.NS_SAML), decrypted_nameid.tag)
    +        self.assertEqual('457bdb600de717891c77647b0806ce59c089d5b8', decrypted_nameid.text)
     
    -        key_3_file_name = join(self.data_path, 'misc', 'sp2.key')
    +        key_3_file_name = join(self.data_path, 'misc', 'sp3.key')
             f = open(key_3_file_name, 'r')
             key3 = f.read()
             f.close()
    -        try:
    -            OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key3)
    -            self.assertTrue(False)
    -        except:
    -            pass
    +
    +        # sp.key and sp3.key are equivalent we should be able to decrypt the nameID again
    +        decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key3)
    +        self.assertIn('{%s}NameID' % (OneLogin_Saml2_Constants.NS_SAML), decrypted_nameid.tag)
    +        self.assertEqual('457bdb600de717891c77647b0806ce59c089d5b8', decrypted_nameid.text)
    +
    +        key_4_file_name = join(self.data_path, 'misc', 'sp4.key')
    +        f = open(key_4_file_name, 'r')
    +        key4 = f.read()
    +        f.close()
    +
    +        with self.assertRaisesRegexp(Exception, "('failed to decrypt', -1)"):
    +            OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key4)
     
             xml_nameid_enc_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'encrypted_nameID_without_EncMethod.xml.base64')))
             dom_nameid_enc_2 = parseString(xml_nameid_enc_2)
             encrypted_nameid_nodes_2 = dom_nameid_enc_2.getElementsByTagName('saml:EncryptedID')
             encrypted_data_2 = encrypted_nameid_nodes_2[0].firstChild
    -        try:
    +
    +        with self.assertRaisesRegexp(Exception, "('failed to decrypt', -1)"):
                 OneLogin_Saml2_Utils.decrypt_element(encrypted_data_2, key)
    -            self.assertTrue(False)
    -        except:
    -            pass
     
             xml_nameid_enc_3 = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'encrypted_nameID_without_keyinfo.xml.base64')))
             dom_nameid_enc_3 = parseString(xml_nameid_enc_3)
             encrypted_nameid_nodes_3 = dom_nameid_enc_3.getElementsByTagName('saml:EncryptedID')
             encrypted_data_3 = encrypted_nameid_nodes_3[0].firstChild
    -        try:
    +
    +        with self.assertRaisesRegexp(Exception, "('failed to decrypt', -1)"):
                 OneLogin_Saml2_Utils.decrypt_element(encrypted_data_3, key)
    -            self.assertTrue(False)
    -        except:
    -            pass
    +
    +    def testDecryptElementInplace(self):
    +        """
    +        Tests the decrypt_element method of the OneLogin_Saml2_Utils with inplace=True
    +        """
    +        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
    +
    +        key = settings.get_sp_key()
    +
    +        xml_nameid_enc = b64decode(self.file_contents(join(self.data_path, 'responses', 'response_encrypted_nameid.xml.base64')))
    +        dom = fromstring(xml_nameid_enc)
    +        encrypted_node = dom.xpath('//saml:EncryptedID/xenc:EncryptedData', namespaces=OneLogin_Saml2_Constants.NSMAP)[0]
    +
    +        # can be decrypted twice when copy the node first
    +        for _ in range(2):
    +            decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_node, key, inplace=False)
    +            self.assertIn('NameID', decrypted_nameid.tag)
    +            self.assertEqual('2de11defd199f8d5bb63f9b7deb265ba5c675c10', decrypted_nameid.text)
    +
    +        # can only be decrypted once in place
    +        decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_node, key, inplace=True)
    +        self.assertIn('NameID', decrypted_nameid.tag)
    +        self.assertEqual('2de11defd199f8d5bb63f9b7deb265ba5c675c10', decrypted_nameid.text)
    +
    +        # can't be decrypted twice since it has been dcrypted inplace
    +        with self.assertRaisesRegexp(Exception, "('failed to decrypt', -1)"):
    +            OneLogin_Saml2_Utils.decrypt_element(encrypted_node, key, inplace=True)
     
         def testAddSign(self):
             """
    @@ -720,59 +810,85 @@ def testAddSign(self):
     
             res = parseString(xml_authn_signed)
             ds_signature = res.firstChild.firstChild.nextSibling.nextSibling
    -        self.assertIn('ds:Signature', ds_signature.tagName)
    +        self.assertEqual('ds:Signature', ds_signature.tagName)
     
             xml_authn_dom = parseString(xml_authn)
             xml_authn_signed_2 = OneLogin_Saml2_Utils.add_sign(xml_authn_dom, key, cert)
             self.assertIn('', xml_authn_signed_2)
             res_2 = parseString(xml_authn_signed_2)
             ds_signature_2 = res_2.firstChild.firstChild.nextSibling.nextSibling
    -        self.assertIn('ds:Signature', ds_signature_2.tagName)
    +        self.assertEqual('ds:Signature', ds_signature_2.tagName)
     
             xml_authn_signed_3 = OneLogin_Saml2_Utils.add_sign(xml_authn_dom.firstChild, key, cert)
             self.assertIn('', xml_authn_signed_3)
             res_3 = parseString(xml_authn_signed_3)
             ds_signature_3 = res_3.firstChild.firstChild.nextSibling.nextSibling
    -        self.assertIn('ds:Signature', ds_signature_3.tagName)
    +        self.assertEqual('ds:Signature', ds_signature_3.tagName)
     
             xml_authn_etree = etree.fromstring(xml_authn)
             xml_authn_signed_4 = OneLogin_Saml2_Utils.add_sign(xml_authn_etree, key, cert)
             self.assertIn('', xml_authn_signed_4)
             res_4 = parseString(xml_authn_signed_4)
             ds_signature_4 = res_4.firstChild.firstChild.nextSibling.nextSibling
    -        self.assertIn('ds:Signature', ds_signature_4.tagName)
    +        self.assertEqual('ds:Signature', ds_signature_4.tagName)
     
             xml_authn_signed_5 = OneLogin_Saml2_Utils.add_sign(xml_authn_etree, key, cert)
             self.assertIn('', xml_authn_signed_5)
             res_5 = parseString(xml_authn_signed_5)
             ds_signature_5 = res_5.firstChild.firstChild.nextSibling.nextSibling
    -        self.assertIn('ds:Signature', ds_signature_5.tagName)
    +        self.assertEqual('ds:Signature', ds_signature_5.tagName)
     
             xml_logout_req = b64decode(self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml.base64')))
             xml_logout_req_signed = OneLogin_Saml2_Utils.add_sign(xml_logout_req, key, cert)
             self.assertIn('', xml_logout_req_signed)
             res_6 = parseString(xml_logout_req_signed)
             ds_signature_6 = res_6.firstChild.firstChild.nextSibling.nextSibling
    -        self.assertIn('ds:Signature', ds_signature_6.tagName)
    +        self.assertEqual('ds:Signature', ds_signature_6.tagName)
     
             xml_logout_res = b64decode(self.file_contents(join(self.data_path, 'logout_responses', 'logout_response.xml.base64')))
             xml_logout_res_signed = OneLogin_Saml2_Utils.add_sign(xml_logout_res, key, cert)
             self.assertIn('', xml_logout_res_signed)
             res_7 = parseString(xml_logout_res_signed)
             ds_signature_7 = res_7.firstChild.firstChild.nextSibling.nextSibling
    -        self.assertIn('ds:Signature', ds_signature_7.tagName)
    +        self.assertEqual('ds:Signature', ds_signature_7.tagName)
     
             xml_metadata = self.file_contents(join(self.data_path, 'metadata', 'metadata_settings1.xml'))
             xml_metadata_signed = OneLogin_Saml2_Utils.add_sign(xml_metadata, key, cert)
             self.assertIn('', xml_metadata_signed)
             res_8 = parseString(xml_metadata_signed)
    -        ds_signature_8 = res_8.firstChild.firstChild.nextSibling.firstChild.nextSibling
    -        self.assertIn('ds:Signature', ds_signature_8.tagName)
    +        ds_signature_8 = res_8.firstChild.firstChild.nextSibling
    +        self.assertEqual('ds:Signature', ds_signature_8.tagName)
     
    -        try:
    +        with self.assertRaisesRegexp(Exception, 'Error parsing xml string'):
                 OneLogin_Saml2_Utils.add_sign(1, key, cert)
    -        except Exception as e:
    -            self.assertEqual('Error parsing xml string', e.message)
    +
    +    def testAddSignCheckAlg(self):
    +        """
    +        Tests the add_sign method of the OneLogin_Saml2_Utils
    +        Case: Review signature & digest algorithm
    +        """
    +        settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
    +        key = settings.get_sp_key()
    +        cert = settings.get_sp_cert()
    +
    +        xml_authn = b64decode(self.file_contents(join(self.data_path, 'requests', 'authn_request.xml.base64')))
    +        xml_authn_signed = OneLogin_Saml2_Utils.add_sign(xml_authn, key, cert)
    +        self.assertIn('', xml_authn_signed)
    +        self.assertIn('', xml_authn_signed)
    +        self.assertIn('', xml_authn_signed)
    +        self.assertIn('', xml_authn_signed)
    +
    +        xml_authn_signed_2 = OneLogin_Saml2_Utils.add_sign(xml_authn, key, cert, False, OneLogin_Saml2_Constants.RSA_SHA256, OneLogin_Saml2_Constants.SHA384)
    +        self.assertIn('', xml_authn_signed_2)
    +        self.assertIn('', xml_authn_signed_2)
    +        self.assertIn('', xml_authn_signed_2)
    +        self.assertIn('', xml_authn_signed_2)
    +
    +        xml_authn_signed_3 = OneLogin_Saml2_Utils.add_sign(xml_authn, key, cert, False, OneLogin_Saml2_Constants.RSA_SHA384, OneLogin_Saml2_Constants.SHA512)
    +        self.assertIn('', xml_authn_signed_3)
    +        self.assertIn('', xml_authn_signed_3)
    +        self.assertIn('', xml_authn_signed_3)
    +        self.assertIn('', xml_authn_signed_3)
     
         def testValidateSign(self):
             """
    @@ -788,25 +904,26 @@ def testValidateSign(self):
             fingerprint_2 = OneLogin_Saml2_Utils.calculate_x509_fingerprint(cert_2)
             fingerprint_2_256 = OneLogin_Saml2_Utils.calculate_x509_fingerprint(cert_2, 'sha256')
     
    -        try:
    -            self.assertFalse(OneLogin_Saml2_Utils.validate_sign('', cert))
    -        except Exception as e:
    -            self.assertEqual('Empty string supplied as input', e.message)
    +        self.assertFalse(OneLogin_Saml2_Utils.validate_sign('', cert, raise_exceptions=False))
    +        self.assertFalse(OneLogin_Saml2_Utils.validate_sign(1, cert, raise_exceptions=False))
     
    -        try:
    -            self.assertFalse(OneLogin_Saml2_Utils.validate_sign(1, cert))
    -        except Exception as e:
    -            self.assertEqual('Error parsing xml string', e.message)
    +        with self.assertRaisesRegexp(Exception, 'Empty string supplied as input'):
    +            OneLogin_Saml2_Utils.validate_sign('', cert, raise_exceptions=True)
    +
    +        with self.assertRaisesRegexp(Exception, 'Error parsing xml string'):
    +            OneLogin_Saml2_Utils.validate_sign(1, cert, raise_exceptions=True)
     
             # expired cert
             xml_metadata_signed = self.file_contents(join(self.data_path, 'metadata', 'signed_metadata_settings1.xml'))
    -        self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_metadata_signed, cert))
    +        self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert))
             # expired cert, verified it
    -        self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_metadata_signed, cert, validatecert=True))
    +        self.assertFalse(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert, validatecert=True))
    +        with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"):
    +            OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert, validatecert=True, raise_exceptions=True)
     
             xml_metadata_signed_2 = self.file_contents(join(self.data_path, 'metadata', 'signed_metadata_settings2.xml'))
    -        self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_metadata_signed_2, cert_2))
    -        self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_metadata_signed_2, None, fingerprint_2))
    +        self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, cert_2))
    +        self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, None, fingerprint_2))
     
             xml_response_msg_signed = b64decode(self.file_contents(join(self.data_path, 'responses', 'signed_message_response.xml.base64')))
     
    @@ -814,6 +931,8 @@ def testValidateSign(self):
             self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert))
             # expired cert, verified it
             self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert, validatecert=True))
    +        with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"):
    +            OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert, validatecert=True, raise_exceptions=True)
     
             # modified cert
             other_cert_path = join(dirname(__file__), '..', '..', '..', 'certs')
    @@ -822,6 +941,10 @@ def testValidateSign(self):
             f.close()
             self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x))
             self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, validatecert=True))
    +        with self.assertRaisesRegexp(Exception, "('signature verification failed', 2)"):
    +            OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, raise_exceptions=True)
    +        with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"):
    +            OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, validatecert=True, raise_exceptions=True)
     
             xml_response_msg_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'signed_message_response2.xml.base64')))
             self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed_2, cert_2))
    @@ -835,6 +958,8 @@ def testValidateSign(self):
             self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert))
             # expired cert, verified it
             self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert, validatecert=True))
    +        with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"):
    +            OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert, validatecert=True, raise_exceptions=True)
     
             xml_response_assert_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'signed_assertion_response2.xml.base64')))
             self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed_2, cert_2))
    @@ -846,6 +971,8 @@ def testValidateSign(self):
             self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert))
             # expired cert, verified it
             self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert, validatecert=True))
    +        with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"):
    +            OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert, validatecert=True, raise_exceptions=True)
     
             xml_response_double_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'double_signed_response2.xml.base64')))
             self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed_2, cert_2))
    @@ -859,23 +986,48 @@ def testValidateSign(self):
             dom.firstChild.getAttributeNode('ID').nodeValue = u'_34fg27g212d63k1f923845324475802ac0fc24530b'
             # Reference validation failed
             self.assertFalse(OneLogin_Saml2_Utils.validate_sign(dom, cert_2))
    +        with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"):
    +            OneLogin_Saml2_Utils.validate_sign(dom, cert_2, raise_exceptions=True)
     
             invalid_fingerprint = 'afe71c34ef740bc87434be13a2263d31271da1f9'
             # Wrong fingerprint
    -        self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_metadata_signed_2, None, invalid_fingerprint))
    +        self.assertFalse(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, None, invalid_fingerprint))
    +        with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'Could not validate node signature: No certificate provided.'):
    +            OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, None, invalid_fingerprint, raise_exceptions=True)
     
             dom_2 = parseString(xml_response_double_signed_2)
             self.assertTrue(OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2))
             dom_2.firstChild.firstChild.firstChild.nodeValue = 'https://example.com/other-idp'
             # Modified message
             self.assertFalse(OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2))
    +        with self.assertRaisesRegexp(Exception, "('signature verification failed', 2)"):
    +            OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2, raise_exceptions=True)
     
    +        # Try to validate directly the Assertion
             dom_3 = parseString(xml_response_double_signed_2)
             assert_elem_3 = dom_3.firstChild.firstChild.nextSibling.nextSibling.nextSibling
    -        self.assertTrue(OneLogin_Saml2_Utils.validate_sign(assert_elem_3, cert_2))
    +        self.assertFalse(OneLogin_Saml2_Utils.validate_sign(assert_elem_3, cert_2))
    +        with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Expected exactly one signature node; got 0."):
    +            OneLogin_Saml2_Utils.validate_sign(assert_elem_3, cert_2, raise_exceptions=True)
     
    +        # Wrong scheme
             no_signed = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_signature.xml.base64')))
             self.assertFalse(OneLogin_Saml2_Utils.validate_sign(no_signed, cert))
    +        with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Expected exactly one signature node; got 0."):
    +            OneLogin_Saml2_Utils.validate_sign(no_signed, cert, raise_exceptions=True)
     
             no_key = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_key.xml.base64')))
             self.assertFalse(OneLogin_Saml2_Utils.validate_sign(no_key, cert))
    +        with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"):
    +            OneLogin_Saml2_Utils.validate_sign(no_key, cert, raise_exceptions=True)
    +
    +        # Signature Wrapping attack
    +        wrapping_attack1 = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'signature_wrapping_attack.xml.base64')))
    +        self.assertFalse(OneLogin_Saml2_Utils.validate_sign(wrapping_attack1, cert))
    +        with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Expected exactly one signature node; got 0."):
    +            OneLogin_Saml2_Utils.validate_sign(wrapping_attack1, cert, raise_exceptions=True)
    +
    +
    +if __name__ == '__main__':
    +    runner = unittest.TextTestRunner()
    +    unittest.main(testRunner=runner)