diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..ac0a9f6c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [SAML-Toolkits] diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 00000000..0359fe32 --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,40 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python package + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-20.04 + container: + image: python:2.7.18-buster + environment: CI + steps: + - name: Checkout project + uses: actions/checkout@v3 + - name: Install dependencies + run: | + apt-get update -qq + apt-get install -qq swig python-dev libxml2-dev libxmlsec1-dev + pip install --disable-pip-version-check --no-cache-dir --force-reinstall --no-binary lxml lxml + pip install --disable-pip-version-check --no-cache-dir . + pip install --disable-pip-version-check --no-cache-dir -e ".[test]" + + - name: Lint + run: | + #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 + flake8 --ignore E226,E302,E41,E731,E501,C901,W504 + + - name: Test + run: | + coverage run --source=src/onelogin/saml2 --rcfile=tests/coverage.rc setup.py test + + - name: Coveralls + env: + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + run: | + pip install python-coveralls + coveralls diff --git a/.gitignore b/.gitignore index 31a3dfc8..c6000b9d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,11 +7,23 @@ *.pyo .*.swp +__pycache_ /parts /.installed.cfg /develop-eggs -/*.egg -/*.egg-info +/*.eg +*.egg-info /eggs /build /dist +/venv +.coverage +.pypirc +poetry.lock + +*.key +*.crt +*.pem + +settings.py +advanced_settings.py diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..30686740 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,29 @@ +language: python +python: + - '2.7' + +jobs: + include: + - name: "Python 2.7 on Focal Linux" + os: linux + dist: focal + python: 2.7 + - name: "Python 2.7 on macOS" + os: osx + python: 2.7 + +install: + - sudo apt-get update -qq + - sudo apt-get install -qq swig python-dev libxml2-dev libxmlsec1-dev + - 'travis_retry pip install --force-reinstall --no-binary lxml lxml' + - 'travis_retry pip install .' + - 'travis_retry pip install -e ".[test]"' + +script: + - 'coverage run --source=src/onelogin/saml2 --rcfile=tests/coverage.rc setup.py test' + - 'coverage report -m --rcfile=tests/coverage.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' + - flake8 --ignore E226,E302,E41,E731,E501,C901,W504 + +after_success: 'coveralls' diff --git a/LICENSE b/LICENSE index 8496f29a..c141165e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,24 +1,24 @@ -Copyright (c) 2011, OneLogin, Inc. -All rights reserved. +Copyright (c) 2010-2022 OneLogin, Inc. +Copyright (c) 2023 IAM Digital Services, SL. -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/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..9b0be815 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include README.md +include LICENSE +recursive-include src *.py +recursive-include src *.xsd +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] diff --git a/README.md b/README.md new file mode 100644 index 00000000..6a785196 --- /dev/null +++ b/README.md @@ -0,0 +1,1429 @@ +# SAML Python Toolkit + +[![Python package](https://github.com/SAML-Toolkits/python-saml/actions/workflows/python-package.yml/badge.svg)](https://github.com/SAML-Toolkits/python-saml/actions/workflows/python-package.yml) +![PyPI Downloads](https://img.shields.io/pypi/dm/python-saml.svg?label=PyPI%20Downloads) +[![Coverage Status](https://coveralls.io/repos/github/SAML-Toolkits/python-saml/badge.svg?branch=master)](https://coveralls.io/github/SAML-Toolkits/python-saml?branch=master) +[![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) + +``` +Python 2 was deprecated on January 1, 2020. We recommend to migrate your project +to Python 3 and use python3-saml +``` + + +Add SAML support to your Python software using this library. +Forget those complicated libraries and use the open source library. + +This version supports Python2. There is a separate version that supports +Python3: [python3-saml](https://github.com/SAML-Toolkits/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 by mail to the maintainer: sixto.martin.garcia+security@gmail.com + +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 +since 2002, but lately it is becoming popular due its advantages: + + * **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 + integrity, SAML is a secure single sign-on protocol that the largest + and most security conscious enterprises in the world rely on. + * **Speed** - SAML is fast. One browser redirect is all it takes to securely + sign a user into an application. + * **Phishing Prevention** - If you don’t have a password for an app, you + can’t be tricked into entering it on a fake login page. + * **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 + integration of their product. + +General Description +------------------- + +SAML Python toolkit lets you turn your Python application into a SP +(Service Provider) that can be connected to an IdP (Identity Provider). + +**Supports:** + + * SSO and SLO (SP-Initiated and IdP-Initiated). + * Assertion and nameId encryption. + * 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:** + + * **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 + low-level programming, 2 easy to use APIs are available. + * **Tested** - Thoroughly tested. + * **Popular** - Developers use it. Add easy support to your Django/Flask/Bottle/Pyramid web projects. + + +Installation +------------ + +### Dependencies ### + + * python 2.7 +* [lxml](https://pypi.python.org/pypi/lxml) Python bindings for the libxml2 and libxslt libraries. + * [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 #### + +The toolkit is hosted on Github. You can download it from: + + * Lastest release: https://github.com/SAML-Toolkits/python-saml/releases/latest + * Master repo: https://github.com/SAML-Toolkits/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). + +#### 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 + +You can install it executing: +``` +$ 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) + + +#### NOTE #### +To avoid ``libxml2`` library version incompatibilities between ``xmlsec`` and ``lxml`` it is recommended that ``lxml`` is not installed from binary. + +This can be ensured by executing: +``` +$ pip install --force-reinstall --no-binary lxml lxml +``` + +Security Warning +---------------- + +In production, the **strict** parameter MUST be set as **"true"**. Otherwise +your environment is not secure and will be exposed to attacks. + + +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. + +### Avoiding Open Redirect attacks ### + +Some implementations uses the RelayState parameter as a way to control the flow when SSO and SLO succeeded. So basically the +user is redirected to the value of the RelayState. + +If you are using Signature Validation on the HTTP-Redirect binding, you will have the RelayState value integrity covered, otherwise, and +on HTTP-POST binding, you can't trust the RelayState so before +executing the validation, you need to verify that its value belong +a trusted and expected URL. + +Read more about Open Redirect [CWE-601](https://cwe.mitre.org/data/definitions/601.html). + +### Avoiding Replay attacks ### + +A replay attack is basically try to reuse an intercepted valid SAML Message in order to impersonate a SAML action (SSO or SLO). + +SAML Messages have a limited timelife (NotBefore, NotOnOrAfter) that +make harder this kind of attacks, but they are still possible. + +In order to avoid them, the SP can keep a list of SAML Messages or Assertion IDs already valdidated and processed. Those values only need +to be stored the amount of time of the SAML Message life time, so +we don't need to store all processed message/assertion Ids, but the most recent ones. + +The OneLogin_Saml2_Auth class contains the [get_last_request_id](https://github.com/SAML-Toolkits/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L352), [get_last_message_id](https://github.com/SAML-Toolkits/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L359) and [get_last_assertion_id](https://github.com/SAML-Toolkits/python-saml/blob/00b1f823b6c668b0dfb5e4a40d3709a4ceb2a6ae/src/onelogin/saml2/auth.py#L366) methods to retrieve the IDs + +Checking that the ID of the current Message/Assertion does not exists in the lis of the ones already processed will prevent replay attacks. + + +Getting Started +--------------- + +### Knowing the toolkit ### + +The SAML Toolkit contains different folders (``cert``, ``lib``, ``demo-django``, ``demo-flask``, ``demo-bottle`` and ``tests``) and some files. + +Let's start describing them: + +#### src #### + +This folder contains the heart of the toolkit, **onelogin/saml2** folder contains the new version of +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 X.509 public and private key, and the SAML toolkit settings (``settings.json`` and ``advanced_settings.json``). + +*** Notice about certs *** + +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 X.509 cert and the private key that the SP will use: + +* sp.crt The public cert 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. + +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: + +```bash +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 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 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 #### + +Setup script is the centre of all activity in building, distributing, and installing modules. +Read more at https://pythonhosted.org/an_example_pypi_project/setuptools.html + +#### tests #### + +Contains the unit test of the toolkit. + +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 +``` +The previous line will run the tests for the whole toolkit. You can also run the tests for a specific module. To do so for the auth module you would have to execute this: +``` +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/`` + +### How it Works ### + +#### Settings #### + +First of all we need to configure the toolkit. The SP's info, the IdP's info, and in some cases, configure advanced security issues like signatures and encryption. + +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 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``, ``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: + +```javascript +{ + // 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. + "strict": true, + + // Enable debug mode (outputs errors). + "debug": true, + + // Service Provider Data that we are deploying. + "sp": { + // Identifier of the SP entity (must be a URI) + "entityId": "https:///metadata/", + // Specifies info about where and how the message MUST be + // returned to the requester, in this case our SP. + "assertionConsumerService": { + // URL Location where the from the IdP will be returned + "url": "https:///?acs", + // SAML protocol binding to be used when returning the + // message. SAML 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 sent. + "singleLogoutService": { + // URL Location where the from the IdP will be sent (IdP-initiated logout) + "url": "https:///?sls", + // URL Location where the from the IdP will sent (SP-initiated logout, reply) + // OPTIONAL: only specify if different from url parameter + //"responseUrl": "https:///?sls", + // SAML protocol binding to be used when returning the + // message. SAML Toolkit supports the HTTP-Redirect binding + // only for this endpoint. + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + // 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: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. + "idp": { + // Identifier of the IdP entity (must be a URI) + "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 + // will be sent. + "url": "https://app.onelogin.com/trust/saml2/http-post/sso/", + // SAML protocol binding to be used when returning the + // message. SAML Toolkit supports the HTTP-Redirect binding + // only for this endpoint. + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + // SLO endpoint info of the IdP. + "singleLogoutService": { + // URL Location where the from the IdP will be sent (IdP-initiated logout) + "url": "https://app.onelogin.com/trust/saml2/http-redirect/slo/", + // URL Location where the from the IdP will sent (SP-initiated logout, reply) + // OPTIONAL: only specify if different from url parameter + "responseUrl": "https://app.onelogin.com/trust/saml2/http-redirect/slo_return/", + // SAML protocol binding to be used when returning the + // message. SAML Toolkit supports the HTTP-Redirect binding + // only for this endpoint. + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + // Public X.509 certificate of the IdP + "x509cert": "" + /* + * 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) + * + * 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', + + /* 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), extra settings can be defined in ``advanced_settings.json``: + +```javascript +{ + // Security settings + "security": { + + /** signatures and encryptions offered **/ + + // Indicates that the nameID of the sent by this SP + // will be encrypted. + "nameIdEncrypted": false, + + // 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 + // will be signed. + "logoutRequestSigned": false, + + // Indicates whether the messages sent by this SP + // will be signed. + "logoutResponseSigned": false, + + /* Sign the Metadata + false || true (use sp certs) || { + "keyFileName": "metadata.key", + "certFileName": "metadata.crt" + } + */ + "signMetadata": false, + + /** signatures and encryptions required **/ + + // Indicates a requirement for the , + // and elements received by this SP to be signed. + "wantMessagesSigned": false, + + // Indicates a requirement for the elements received by + // 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 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, + // 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(s) received match(es) the 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, + // Provide the desired duration, for example PT518400S (6 days) + "metadataCacheDuration": null, + + // If enabled, URLs with single-label-domains will + // be allowed and not rejected by the settings validator (Enable it under Docker/Kubernetes/testing env, not recommended on production) + "allowSingleLabelDomains": false, + + // Algorithm that the toolkit will use on signing process. Options: + // 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' + // 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' + // '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/2001/04/xmldsig-more#rsa-sha256", + + // 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/2001/04/xmlenc#sha256", + + // If the toolkit receive a message signed with a + // deprecated algoritm (defined at the constant class) + // will raise an error and reject the message + "rejectDeprecatedAlgorithm": true + }, + + // Contact information template, it is recommended to supply + // technical and support contacts. + "contactPerson": { + "technical": { + "givenName": "technical_name", + "emailAddress": "technical@example.com" + }, + "support": { + "givenName": "support_name", + "emailAddress": "support@example.com" + } + }, + + // Organization information template, the info in en_US lang is + // recommended, add more if required. + "organization": { + "en-US": { + "name": "sp_test", + "displayname": "SP test", + "url": "http://sp.example.com" + } + } +} +``` + +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. + +```python +# Initializes toolkit with settings.json & advanced_settings.json files. +auth = OneLogin_Saml2_Auth(req) +# or +settings = OneLogin_Saml2_Settings() + +# Initializes toolkit with settings.json & advanced_settings.json files from a custom base path. +custom_folder = '/var/www/django-project' +auth = OneLogin_Saml2_Auth(req, custom_base_path=custom_folder) +# or +settings = OneLogin_Saml2_Settings(custom_base_path=custom_folder) + +# Initializes toolkit with the dict provided. +auth = OneLogin_Saml2_Auth(req, settings_data) +# or +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: + +```python +filename = "/var/www/django-project/custom_settings.json" # The custom_settings.json contains a +json_data_file = open(filename, 'r') # settings_data dict. +settings_data = json.load(json_data_file) +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 without further ado. + +But take in mind that the OneLogin_Saml2_IdPMetadataParser class does not validate in any way the URL that is introduced in order to be parsed. + +Usually the same administrator that handles the Service Provider also sets the URL to the IdP, which should be a trusted resource. + +But there are other scenarios, like a SAAS app where the administrator of the app delegates this functionality to other users. In this case, extra precaution should be taken in order to validate such URL inputs and avoid attacks like SSRF. + + +``` +idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://example.com/auth/saml2/idp/metadata') +``` + +You can specify a timeout in seconds for metadata retrieval, if not it is not guaranteed that the request will complete + +``` +idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://example.com/auth/saml2/idp/metadata', timeout=5) +``` + +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 +on the top of your python file. + +``` python +from onelogin.saml2.auth import OneLogin_Saml2_Auth +from onelogin.saml2.settings import OneLogin_Saml2_Settings +from onelogin.saml2.utils import OneLogin_Saml2_Utils +``` + +#### The Request #### + +Building an ``OneLogin_Saml2_Auth object`` requires a ``request`` parameter. + +```python +auth = OneLogin_Saml2_Auth(req) +``` + +This parameter has the following scheme: + +```javascript +req = { + "https": "" + "http_host": "", + "script_name": "", + "server_port": "", + "get_data": "", + "post_data": "" +} +``` + +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 +def prepare_from_django_request(request): + return { + 'http_host': request.META['HTTP_HOST'], + 'script_name': request.META['PATH_INFO'], + 'server_port': request.META['SERVER_PORT'], + 'get_data': request.GET.copy(), + 'post_data': request.POST.copy() + } + +def prepare_from_flask_request(request): + url_data = urlparse(request.url) + return { + 'http_host': request.host, + 'server_port': url_data.port, + 'script_name': request.path, + 'get_data': request.args.copy(), + 'post_data': request.form.copy() + } +``` +The ``https`` dictionary entry should be set to ``on`` for https requests and ``off`` for http + +#### Initiate SSO #### + +In order to send an AuthNRequest 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.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 IdP will then return the SAML Response to the user's client. The client is then forwarded to the **Assertion 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: + +```python +target_url = 'https://example.com' +auth.login(return_to=target_url) +``` +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 + +```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. +The toolkit provides examples of those views in the demos, but let's see an example. + +*** SP Metadata *** + +This code will provide the XML metadata file of our SP, based on the info that we provided in the settings files. + +```python +req = prepare_request_for_toolkit(request) +auth = OneLogin_Saml2_Auth(req) +saml_settings = auth.get_settings() +metadata = saml_settings.get_sp_metadata() +errors = saml_settings.validate_metadata(metadata) +if len(errors) == 0: + print metadata +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``). + +Before the XML metadata is exposed, a check takes place to ensure that the info to be provided is valid. + +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. + +*** Assertion Consumer Service (ACS) *** + +This code handles the SAML response that the IdP forwards to the SP through the user's client. + +```python +req = prepare_request_for_toolkit(request) +auth = OneLogin_Saml2_Auth(req) +auth.process_response() +errors = auth.get_errors() +if not errors: + if auth.is_authenticated(): + request.session['samlUserdata'] = auth.get_attributes() + if 'RelayState' in req['post_data'] and + OneLogin_Saml2_Utils.get_self_url(req) != req['post_data']['RelayState']: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the req['post_data']['RelayState'] is a trusted URL. + auth.redirect_to(req['post_data']['RelayState']) + else: + for attr_name in request.session['samlUserdata'].keys(): + print '%s ==> %s' % (attr_name, '|| '.join(request.session['samlUserdata'][attr_name])) + else: + print 'Not authenticated' +else: + print "Error when processing SAML Response: %s" % (', '.join(errors)) +``` + +The SAML response is processed and then checked that there are no errors. It also verifies that the user is authenticated and stored the userdata in session. + +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. + +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: + +```python +attributes = auth.get_attributes(); +``` + +With this method we get a dict with all the user data provided by the IdP in the Assertion of the SAML Response. + +If we execute print attributes we could get: + +```python +{ + "cn": ["Jhon"], + "sn": ["Doe"], + "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. + +The following code is equivalent: + +```python +attributes = auth.get_attributes(); +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 ``auth.get_attributes`` before a ``auth.process_response``, the ``auth.get_attributes`` will return an empty dict. + + +*** Single Logout Service (SLS) *** + +This code handles the Logout Request and the Logout Responses. + +```python +delete_session_callback = lambda: request.session.flush() +url = auth.process_slo(delete_session_cb=delete_session_callback) +errors = auth.get_errors() +if len(errors) == 0: + if url is not None: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the url is a trusted URL. + return redirect(url) + else: + print "Sucessfully Logged out" +else: + print "Error when processing SLO: %s" % (', '.join(errors)) +``` + +If the SLS endpoints receives a Logout Response, the response is validated and the session could be closed, using the callback. + +```python +# Part of the process_slo method +logout_response = OneLogin_Saml2_Logout_Response(self.__settings, self.__request_data['get_data']['SAMLResponse']) +if not logout_response.is_valid(self.__request_data, request_id): + self.__errors.append('invalid_logout_response') +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) +``` + +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. + +```python +# Part of the process_slo method +request = OneLogin_Saml2_Utils.decode_base64_and_inflate(self.__request_data['get_data']['SAMLRequest']) +if not OneLogin_Saml2_Logout_Request.is_valid(self.__settings, request, self.__request_data): + self.__errors.append('invalid_logout_request') +else: + if not keep_local_session: + OneLogin_Saml2_Utils.delete_local_session(delete_session_cb) + + in_response_to = request.id + response_builder = OneLogin_Saml2_Logout_Response(self.__settings) + response_builder.build(in_response_to) + 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'] + + security = self.__settings.get_security_data() + if 'logoutResponseSigned' in security and security['logoutResponseSigned']: + parameters['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA256 + parameters['Signature'] = self.build_response_signature(logout_response, parameters.get('RelayState', None)) + + 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 + +```python +keepLocalSession = true +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 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: + +```python +target_url = 'https://example.com' +auth.logout(return_to=target_url) +``` + +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 +SAML Response with a NameId, then this NameId will be used. +* ``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``. + +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. + +Note: Review the demos, in a later section we explain the demo use case further in detail. + +```python +req = prepare_request_for_toolkit(request) # Process the request and build the request dict that + # the toolkit expects + +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 + 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 + 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 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']: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the request.form['RelayState'] is a trusted URL. + return redirect(request.form['RelayState']) # Redirect if there is a relayState + else: # If there is user data we save that to print it later. + msg = '' + for attr_name in request.session['samlUserdata'].keys(): + msg += '%s ==> %s' % (attr_name, '|| '.join(request.session['samlUserdata'][attr_name])) +elif 'sls' in request.args: # Single Logout Service + delete_session_callback = lambda: session.clear() # Obtain session clear callback + url = auth.process_slo(delete_session_cb=delete_session_callback) # Process the Logout Request & Logout Response + errors = auth.get_errors() # Retrieves possible validation errors + if len(errors) == 0: + if url is not None: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the url is a trusted URL. + return redirect(url) + else: + msg = "Sucessfully logged out" + +if len(errors) == 0: + print msg +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 replay 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 #### + +Main class of SAML Python Toolkit + +* `__init__` Initializes the SP SAML instance. +* ***login*** Initiates the SSO process. +* ***logout*** Initiates the SLO process. +* ***process_response*** Process the SAML Response sent by the IdP. +* ***process_slo*** Process the SAML Logout Response / Logout Request sent by the IdP. +* ***redirect_to*** Redirects the user to the URL passed by parameter or to the URL that we defined in our SSO Request. +* ***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_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 #### + +SAML 2 Authentication Request class + +* `__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 #### + +SAML 2 Authentication Response class + +* `__init__` Constructs the SAML Response object. +* ***is_valid*** Determines if the SAML Response is valid. Includes checking of the signature by a certificate. +* ***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*** 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. +* ***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 +* ***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 #### + +SAML 2 Logout Request class + +* `__init__` Constructs the Logout Request object. +* ***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. +* ***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 #### + +SAML 2 Logout Response class + +* `__init__` Constructs a Logout Response object. +* ***get_issuer*** Gets the Issuer of the Logout Response Message +* ***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_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 #### + +Configuration of the SAML Python Toolkit + +* `__init__` Initializes the settings: Sets the paths of the different folders and Loads settings info from settings file or array/object provided. +* ***check_settings*** Checks the settings info. +* ***check_idp_settings*** Checks the IdP settings info. +* ***check_sp_settings*** Checks the SP settings info. +* ***get_errors*** Returns an array with the errors, the array is empty when the settings is ok. +* ***get_sp_metadata*** Gets the SP metadata. The XML representation. +* ***validate_metadata*** Validates an XML SP Metadata. +* ***get_base_path*** Returns base path. +* ***get_cert_path*** Returns cert path. +* ***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 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_debug_active*** Returns if the debug is active. + +#### 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. +* ***sign_metadata*** Signs the metadata with the key/cert provided. +* ***add_x509_key_descriptors*** Adds the X.509 descriptors (sign/encriptation) to the metadata + +#### 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 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). +* ***get_self_host*** Returns the current host. +* ***is_https*** Checks if https or http. +* ***get_self_url_no_query*** Returns the URL of the current host + current view. +* ***get_self_routed_url_no_query*** Returns the routed URL of the current host + current view. +* ***get_self_url*** Returns the URL of the current host + current view + query. +* ***generate_unique_id*** Generates an unique string (used for example as ID for assertions). +* ***parse_time_to_SAML*** Converts a UNIX timestamp to SAML2 timestamp on the form yyyy-mm-ddThh:mm:ss(\.s+)?Z. +* ***parse_SAML_to_time*** Converts a SAML2 timestamp on the form yyyy-mm-ddThh:mm:ss(\.s+)?Z to a UNIX timestamp. +* ***now*** Returns unix timestamp of actual time. +* ***parse_duration*** Interprets a ISO8601 duration value relative to a given timestamp. +* ***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 X.509 cert. +* ***format_finger_print*** Formates a fingerprint. +* ***generate_name_id*** Generates a nameID. +* ***get_status*** Gets Status from a Response. +* ***decrypt_element*** Decrypts an encrypted element. +* ***write_temp_file*** Writes some content into a temporary file and returns it. +* ***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. + + + + +Demos included in the toolkit +----------------------------- + +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 +how it deployed. New demos using other python frameworks are welcome as a contribution. + +### Getting Started ### + +We said that this toolkit includes a demos, let's see how fast is deploy some of them. + +*** Virtualenv *** + +The use of a [virtualenv](http://virtualenv.readthedocs.org/en/latest/) is +highly recommended. + +Virtualenv helps isolating the python enviroment used to run the toolkit. You +can find more details and an installation guide in the +[official documentation](http://virtualenv.readthedocs.org/en/latest/). + +Once you have your virtualenv ready and loaded, then you can install the +toolkit on it in development mode executing this: +``` + python setup.py develop +``` + +Using this method of deployment the toolkit files will be linked instead of +copied, so if you make changes on them you won't need to reinstall the toolkit. + +If you want install it in a normal mode, execute: +``` + python setup.py install +``` + +### Demo Flask ### + +You'll need a virtualenv with the toolkit installed on it. + +To run the demo you need to install the requirements first. Load your +virtualenv and execute: + +``` + pip install -r demo-flask/requirements.txt +``` + +This will install flask and its dependences. Once it has finished, you have to complete the configuration +of the toolkit. You'll find it at `demo-flask/settings.json` + +Now, with the virtualenv loaded, you can run the demo like this: +``` + cd demo-flask + python index.py +``` + +You'll have the demo running at ``http://localhost:8000`` + +#### 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. + +* ***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 #### + +The SAML 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. + +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: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.2 in the second link we access to ``/?attrs`` (attrs view), we will experience the same process as described at 2.1 with the difference that as ``RelayState`` is set the attrs url. + + 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 functionality 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.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. + +### Demo Django ### + +You'll need a virtualenv with the toolkit installed on it. + +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: +``` + cd demo-django + python manage.py runserver 0.0.0.0: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 #### + +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. + +* ***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 defines 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. + +#### SP setup #### + +The SAML 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). + + +### Demo Pyramid ### + +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. + +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 #### + +The 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 experience the same process as described at 2.1 with the difference 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 functionality 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.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 parameters are used to know the action that must be done. diff --git a/README.rst b/README.rst deleted file mode 100644 index a7ccdadb..00000000 --- a/README.rst +++ /dev/null @@ -1,152 +0,0 @@ -=========== -Python SAML -=========== - -This Python SAML toolkit provides functionality for creating SAML AuthnRequests -which can be sent to an identity provider and for verifying SAML Responses from -an identity provider. OneLogin is an example of an identity provider. - -Non-Python Requirements -======================= - -- libxml2 -- libxslt -- libxmlsec1 -- libxml2-dev -- libxslt-dev -- libxmlsec1-dev - -This toolkit makes use of the xmlsec binary in order to verify the response -signature. Make sure you add the xmlsec binary to your Windows or Linux PATH. -Unfortunately, the python bindings for libxml2, which xmlsec uses, do not -provide the functionality needed to programmatically verify the signature, -specifically the libxml2.xmlAddID function. -For more information see http://www.aleksey.com/xmlsec/faq.html Section 3.2 - -Python Requirements -=================== - -- python-setuptools -- python2.6 -- python2.6-dev -- lxml 2.3 or greater - -Test Requirements -================= - -- fudge 0.9.5 or greater -- nose 0.10.4 or greater - -Installing -========== -To install in the default Python library location:: - - python setup.py install - -To install in a custom Python library location in Linux:: - - MY_BASE_DIR=/home/foouser - export PYTHONPATH=$MY_BASE_DIR/lib/python2.6/site-packages - python setup.py install --prefix=$MY_BASE_DIR - -Testing -======= -To run the unit-tests:: - - python setup.py test - -Running the example app -======================= -To run with the default configuration file, example.cfg, which needs to be -filled out completely:: - - python setup.py example - -To run with your own configuration file in Linux:: - - python setup.py example --config-file=~/my_config.cfg - -Example app breakdown -===================== -The example app subclasses the BaseHTTPRequestHandler in order to process -GET and POST HTTP requests. It is by no means a secure application and should -not be used for anything other than exploring and testing. - -Creating and sending the AuthnRequest to the identity provider --------------------------------------------------------------- -The identification flow starts when a user requests a resource from the service -provider which, in this case, is implemented in our example app. In our example -the user requests a resource by going to the root path of our app, i.e,:: - - http://localhost:7070/ - -Our example app receives this request and in turn creates a SAML AuthnRequest -in the form of URL string which we use to redirect the user to our identity -provider--OneLogin:: - - from BaseHTTPServer import BaseHTTPRequestHandler - from onelogin.saml import AuthnRequest - ... - class SampleAppHTTPRequestHandler(BaseHTTPRequestHandler): - ... - def do_GET(self): - ... - url = AuthnRequest.create(**self.settings) - self.send_response(301) - self.send_header("Location", url) - self.end_headers() - -The self.settings variable is a dictionary with the following entries. These -entries are originally retrieved from the configuration file passed in as a -command line option to the example app:: - - settings = dict( - assertion_consumer_service_url='http://localhost:7070/example/saml/consume', - issuer='python-saml', - name_identifier_format='urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', - idp_sso_target_url='https://app.onelogin.com/saml/signon/', - ) - -The idp_sso_target_url is the SAML Login URL from the SAML Test (IdP) app. You -must register with OneLogin and add the SAML Test (IdP) app to your apps in -order to get the idp_sso_target_url. You must also register the -assertion_consumer_service_url with the SAML Test (IdP) app by entering it in -the SAML Consumer URL field. - -Receiving and verifying the response ------------------------------------- -The user will then be redirected to the OneLogin login page where they will -enter their credentials in order to verify their identity. After OneLogin has -verified their identity it will redirect the user to the -assertion_consumer_service_url--http://localhost:7070/example/saml/consume. - -Our example app then verifies the SAML Response from OneLogin using the fingerprint -of the public certificate originally obtained from OneLogin:: - - def do_POST(self): - ... - length = int(self.headers['Content-Length']) - data = self.rfile.read(length) - query = urlparse.parse_qs(data) - res = Response( - query['SAMLResponse'].pop(), - self.settings['idp_cert_fingerprint'], - ) - valid = res.is_valid() - name_id = res.name_id - if valid: - msg = 'The identity of {name_id} has been verified'.format( - name_id=name_id, - ) - self._serve_msg(200, msg) - else: - msg = '{name_id} is not authorized to use this resource'.format( - name_id=name_id, - ) - self._serve_msg(401, msg) - -Once again, the self.settings variable is populated from an entry in -the configuration file. You can find the public certificate under Security->SAML -after you login to OneLogin. - -For full details see **example.py** and **example.cfg**. diff --git a/changelog.md b/changelog.md new file mode 100644 index 00000000..f9e9ffdb --- /dev/null +++ b/changelog.md @@ -0,0 +1,239 @@ +# python-saml changelog +### 2.13.0 (Oct 9, 2023) +- Improve get_metadata method from Parser, allowing to set timeouts and headers +- Fix expired payloads used on tests +- Updated content from docs folder +- Remove references of OneLogin as maintainer + +### 2.12.0 (Dec 28, 2022) +- Remove version restriction on lxml dependency +- Update Demo Bottle +- Updated Travis file. Forced lxml to be installed using no-validate_binary + +### 2.11.1 (Jan 28, 2022) +- lxml fixed to be lower than 4.7.1 since it seems to have issues validating the signature of encrypted elements See https://github.com/onelogin/python3-saml/issues/292 + +### 2.11.0 (Jan 28, 2022) +- [#292](https://github.com/onelogin/python-saml/pull/292) Add rejectDeprecatedAlgorithm settings in order to be able reject messages signed with deprecated algorithms. +- Upgrade dm.xmlsec.binding to 2.1 +- Set sha256 and rsa-sha256 as default algorithms +- Added warning about Open Redirect and Reply attacks + +### 2.10.0 (Jul 23, 2021) +* Removed CC-BY-SA 3.0 non compliant implementation of dict_deep_merge +* Update expired dates from test responses +* Add warning about the use of OneLogin_Saml2_IdPMetadataParser class about SSRF attacks +* Migrate from Travis to Github Actions + +### 2.9.0 (Jan 13, 2021) +* Destination URL Comparison is now case-insensitive for netloc +* Support single-label-domains as valid. New security parameter allowSingleLabelDomains +* Added get_idp_sso_url, get_idp_slo_url and get_idp_slo_response_url methods to the Settings class and use it in the toolkit +* [#267](https://github.com/onelogin/python-saml/issues/267) Custom lxml parser based on the one defined at xmldefused. Parser will ignore comments and processing instructions and by default have deactivated huge_tree, DTD and access to external documents +* Add get_friendlyname_attributes support +* Remove external lib method get_ext_lib_path. Add set_cert_path in order to allow set the cert path in a different folder than the toolkit +* Add python2 deprecation info +* [#269](https://github.com/onelogin/python-saml/issues/269) Add sha256 instead sha1 algorithm for sign/digest as recommended value on documentation and settings + +### 2.8.0 (NOv 20, 2019) +* [#258](https://github.com/onelogin/python-saml/issues/258) Fix failOnAuthnContextMismatch feature +* [#250](https://github.com/onelogin/python-saml/issues/250) Allow any number of decimal places for seconds on SAML datetimes +* Update demo versions. Improve them and add Tornado demo. + + +### 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) +* SAML Python Toolkit v1.0.0 diff --git a/demo-bottle/index.py b/demo-bottle/index.py new file mode 100644 index 00000000..45172421 --- /dev/null +++ b/demo-bottle/index.py @@ -0,0 +1,184 @@ +import os + +from bottle import Bottle, run, redirect, request, response, ServerAdapter, jinja2_view +from beaker.middleware import SessionMiddleware + +from urlparse import urlparse + +from onelogin.saml2.auth import OneLogin_Saml2_Auth +from onelogin.saml2.utils import OneLogin_Saml2_Utils + + +app = Bottle(__name__) +app.config['SECRET_KEY'] = 'onelogindemopytoolkit' +app.config['SAML_PATH'] = os.path.join(os.path.dirname(__file__), 'saml') + + +session_opts = { + 'session.type': 'file', + 'session.cookie_expires': 300, + 'session.data_dir': './.data', + 'session.auto': True +} + + +def init_saml_auth(req): + auth = OneLogin_Saml2_Auth(req, custom_base_path=app.config['SAML_PATH']) + return auth + + +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, + # Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144 + # 'lowercase_urlencoding': True, + 'query_string': req.query_string + } + + +@app.route('/acs/', method='POST') +@jinja2_view('index.html', template_lookup=['templates']) +def acs(): + req = prepare_bottle_request(request) + auth = init_saml_auth(req) + paint_logout = False + attributes = False + + session = request.environ['beaker.session'] + + 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.forms and self_url != request.forms['RelayState']: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the request.forms['RelayState'] is a trusted URL. + return redirect(request.forms['RelayState']) + + if 'samlUserdata' in session: + paint_logout = True + if len(session['samlUserdata']) > 0: + attributes = session['samlUserdata'].items() + + return { + 'errors': errors, + 'not_auth_warn': not_auth_warn, + 'attributes': attributes, + 'paint_logout': paint_logout + } + + +@app.route('/', method='GET') +@jinja2_view('index.html', template_lookup=['templates']) +def index(): + req = prepare_bottle_request(request) + auth = init_saml_auth(req) + errors = [] + not_auth_warn = False + success_slo = False + attributes = False + paint_logout = False + + session = request.environ['beaker.session'] + + if 'sso' in request.query: + return_to = '{0}://{1}/'.format(request.urlparts.scheme, request.get_header('host')) + return redirect(auth.login(return_to)) + elif 'sso2' in request.query: + return_to = '{0}://{1}/attrs/'.format(request.urlparts.scheme, request.get_header('host')) + return redirect(auth.login(return_to)) + elif 'slo' in request.query: + name_id = None + session_index = 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)) + elif 'sls' in request.query: + 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: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the url is a trusted URL. + return redirect(url) + else: + success_slo = True + + if 'samlUserdata' in session: + paint_logout = True + if len(session['samlUserdata']) > 0: + attributes = session['samlUserdata'].items() + + return { + 'errors': errors, + 'not_auth_warn': not_auth_warn, + 'success_slo': success_slo, + 'attributes': attributes, + 'paint_logout': paint_logout + } + + +@app.route('/attrs/') +@jinja2_view('attrs.html', template_lookup=['templates']) +def attrs(): + paint_logout = False + attributes = False + session = request.environ['beaker.session'] + + if 'samlUserdata' in session: + paint_logout = True + if len(session['samlUserdata']) > 0: + attributes = session['samlUserdata'].items() + + return {'paint_logout': paint_logout, + 'attributes': attributes} + + +@app.route('/metadata/') +def metadata(): + req = prepare_bottle_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: + response.status = 200 + response.set_header('Content-Type', 'text/xml') + return metadata + else: + response.status = 500 + return ','.join(errors) + + +class SSLPasteServer(ServerAdapter): + def run(self, handler): + from paste import httpserver + + server = httpserver.serve(handler, '0.0.0.0', '8000', ssl_pem='local.pem', start_loop=False) + try: + server.serve_forever() + finally: + server.server_close() + + +if __name__ == "__main__": + # To run HTTPS + # run(SessionMiddleware(app, config=session_opts), host='0.0.0.0', port=8000, debug=True, reloader=True, server=SSLPasteServer) + + # To run HTTP + run(SessionMiddleware(app, config=session_opts), host='0.0.0.0', port=8000, debug=True, reloader=True, server='paste') diff --git a/demo-bottle/requirements.txt b/demo-bottle/requirements.txt new file mode 100644 index 00000000..1e0c9b01 --- /dev/null +++ b/demo-bottle/requirements.txt @@ -0,0 +1,4 @@ +bottle==0.12.20 +beaker==1.6.4 +paste==1.7.5.1 +jinja2==2.11.3 diff --git a/demo-bottle/saml/advanced_settings.json b/demo-bottle/saml/advanced_settings.json new file mode 100644 index 00000000..ed284e05 --- /dev/null +++ b/demo-bottle/saml/advanced_settings.json @@ -0,0 +1,35 @@ +{ + "security": { + "nameIdEncrypted": false, + "authnRequestsSigned": false, + "logoutRequestSigned": false, + "logoutResponseSigned": false, + "signMetadata": false, + "wantMessagesSigned": false, + "wantAssertionsSigned": false, + "wantNameId" : true, + "wantNameIdEncrypted": false, + "wantAssertionsEncrypted": false, + "allowSingleLabelDomains": false, + "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256", + "rejectDeprecatedAlgorithm": true + }, + "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/demo-bottle/saml/certs/README b/demo-bottle/saml/certs/README new file mode 100644 index 00000000..7cf0c143 --- /dev/null +++ b/demo-bottle/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-bottle/saml/settings.json b/demo-bottle/saml/settings.json new file mode 100644 index 00000000..7f861e97 --- /dev/null +++ b/demo-bottle/saml/settings.json @@ -0,0 +1,30 @@ +{ + "strict": true, + "debug": true, + "sp": { + "entityId": "https:///metadata/", + "assertionConsumerService": { + "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-bottle/templates/attrs.html b/demo-bottle/templates/attrs.html new file mode 100644 index 00000000..6c60f4c3 --- /dev/null +++ b/demo-bottle/templates/attrs.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} + +{% 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-bottle/templates/base.html b/demo-bottle/templates/base.html new file mode 100644 index 00000000..960ca0bc --- /dev/null +++ b/demo-bottle/templates/base.html @@ -0,0 +1,26 @@ + + + + + + + + A Python SAML Toolkit demo + + + + + + + + +
+

A Python SAML Toolkit demo

+ + {% block content %}{% endblock %} +
+ + diff --git a/demo-bottle/templates/index.html b/demo-bottle/templates/index.html new file mode 100644 index 00000000..c7425c6e --- /dev/null +++ b/demo-bottle/templates/index.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} + +{% block content %} + +{% 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/onelogin/saml/test/__init__.py b/demo-django/demo/__init__.py similarity index 100% rename from onelogin/saml/test/__init__.py rename to demo-django/demo/__init__.py diff --git a/demo-django/demo/settings.py b/demo-django/demo/settings.py new file mode 100644 index 00000000..f0c1726b --- /dev/null +++ b/demo-django/demo/settings.py @@ -0,0 +1,96 @@ +""" +Django settings for demo project. + +For more information on this file, see +https://docs.djangoproject.com/en/1.6/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.6/ref/settings/ +""" + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +import os +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '0c7216)gs^ne$%3+je20zuo+g0&^6yb@e68qdr!^!r0hmb-6y+' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ['localhost'] + +# Application definition + +INSTALLED_APPS = ( + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +) + +MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + # 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +) + +ROOT_URLCONF = 'demo.urls' + +WSGI_APPLICATION = 'demo.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.6/ref/settings/#databases + +# DATABASES = { +# 'default': { +# 'ENGINE': 'django.db.backends.sqlite3', +# 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), +# } +# } + +# Internationalization +# https://docs.djangoproject.com/en/1.6/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.6/howto/static-files/ + +STATIC_URL = '/static/' + +SAML_FOLDER = os.path.join(BASE_DIR, 'saml') + +SESSION_ENGINE = 'django.contrib.sessions.backends.file' + +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 new file mode 100644 index 00000000..a6008829 --- /dev/null +++ b/demo-django/demo/urls.py @@ -0,0 +1,11 @@ +from django.conf.urls import url +from django.contrib import admin +from demo.views import index, attrs, metadata + +admin.autodiscover() + +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 new file mode 100644 index 00000000..e1e44e27 --- /dev/null +++ b/demo-django/demo/views.py @@ -0,0 +1,147 @@ +from django.conf import settings +from django.core.urlresolvers import reverse +from django.http import (HttpResponse, HttpResponseRedirect, + HttpResponseServerError) +from django.shortcuts import render + +from onelogin.saml2.auth import OneLogin_Saml2_Auth +from onelogin.saml2.settings import OneLogin_Saml2_Settings +from onelogin.saml2.utils import OneLogin_Saml2_Utils + + +def init_saml_auth(req): + auth = OneLogin_Saml2_Auth(req, custom_base_path=settings.SAML_FOLDER) + return auth + + +def prepare_django_request(request): + # If server is behind proxys or balancers use the HTTP_X_FORWARDED fields + result = { + 'https': 'on' if request.is_secure() else 'off', + 'http_host': request.META['HTTP_HOST'], + 'script_name': request.META['PATH_INFO'], + 'server_port': request.META['SERVER_PORT'], + 'get_data': request.GET.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 + + +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 + paint_logout = False + + 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 = 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'] + 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']: + 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']: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the req['post_data']['RelayState'] is a trusted URL. + return HttpResponseRedirect(auth.redirect_to(req['post_data']['RelayState'])) + elif 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(request_id=request_id, delete_session_cb=dscb) + errors = auth.get_errors() + if len(errors) == 0: + if url is not None: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the url is a trusted URL. + return HttpResponseRedirect(url) + else: + success_slo = True + elif auth.get_settings().is_debug_active(): + error_reason = auth.get_last_error_reason() + + if 'samlUserdata' in request.session: + paint_logout = True + if len(request.session['samlUserdata']) > 0: + attributes = request.session['samlUserdata'].items() + + 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): + paint_logout = False + attributes = False + + if 'samlUserdata' in request.session: + paint_logout = True + if len(request.session['samlUserdata']) > 0: + attributes = request.session['samlUserdata'].items() + + return render(request, 'attrs.html', + {'paint_logout': paint_logout, + 'attributes': attributes}) + + +def metadata(request): + # req = prepare_django_request(request) + # auth = init_saml_auth(req) + # saml_settings = auth.get_settings() + saml_settings = OneLogin_Saml2_Settings(settings=None, custom_base_path=settings.SAML_FOLDER, sp_validation_only=True) + metadata = saml_settings.get_sp_metadata() + errors = saml_settings.validate_metadata(metadata) + + if len(errors) == 0: + resp = HttpResponse(content=metadata, content_type='text/xml') + else: + resp = HttpResponseServerError(content=', '.join(errors)) + return resp diff --git a/demo-django/demo/wsgi.py b/demo-django/demo/wsgi.py new file mode 100644 index 00000000..b58d97fe --- /dev/null +++ b/demo-django/demo/wsgi.py @@ -0,0 +1,14 @@ +""" +WSGI config for demo project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ +""" +import os +from django.core.wsgi import get_wsgi_application + + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") +application = get_wsgi_application() diff --git a/demo-django/manage.py b/demo-django/manage.py new file mode 100755 index 00000000..86cc0b09 --- /dev/null +++ b/demo-django/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/demo-django/requirements.txt b/demo-django/requirements.txt new file mode 100644 index 00000000..30eb6217 --- /dev/null +++ b/demo-django/requirements.txt @@ -0,0 +1 @@ +Django==1.11.29 diff --git a/demo-django/saml/advanced_settings.json b/demo-django/saml/advanced_settings.json new file mode 100644 index 00000000..ed284e05 --- /dev/null +++ b/demo-django/saml/advanced_settings.json @@ -0,0 +1,35 @@ +{ + "security": { + "nameIdEncrypted": false, + "authnRequestsSigned": false, + "logoutRequestSigned": false, + "logoutResponseSigned": false, + "signMetadata": false, + "wantMessagesSigned": false, + "wantAssertionsSigned": false, + "wantNameId" : true, + "wantNameIdEncrypted": false, + "wantAssertionsEncrypted": false, + "allowSingleLabelDomains": false, + "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256", + "rejectDeprecatedAlgorithm": true + }, + "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/demo-django/saml/certs/README b/demo-django/saml/certs/README new file mode 100644 index 00000000..7cf0c143 --- /dev/null +++ b/demo-django/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-django/saml/settings.json b/demo-django/saml/settings.json new file mode 100644 index 00000000..ec40b674 --- /dev/null +++ b/demo-django/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-django/templates/attrs.html b/demo-django/templates/attrs.html new file mode 100644 index 00000000..ca5f240b --- /dev/null +++ b/demo-django/templates/attrs.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} + +{% 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-django/templates/base.html b/demo-django/templates/base.html new file mode 100644 index 00000000..960ca0bc --- /dev/null +++ b/demo-django/templates/base.html @@ -0,0 +1,26 @@ + + + + + + + + A Python SAML Toolkit demo + + + + + + + + +
+

A Python SAML Toolkit demo

+ + {% block content %}{% endblock %} +
+ + diff --git a/demo-django/templates/index.html b/demo-django/templates/index.html new file mode 100644 index 00000000..87f2f08b --- /dev/null +++ b/demo-django/templates/index.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} + +{% block content %} + +{% 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-flask/index.py b/demo-flask/index.py new file mode 100644 index 00000000..42d539cd --- /dev/null +++ b/demo-flask/index.py @@ -0,0 +1,163 @@ +import os + +from flask import (Flask, request, render_template, redirect, session, + make_response) + +from urlparse import urlparse + +from onelogin.saml2.auth import OneLogin_Saml2_Auth +from onelogin.saml2.utils import OneLogin_Saml2_Utils + + +app = Flask(__name__) +app.config['SECRET_KEY'] = 'onelogindemopytoolkit' +app.config['SAML_PATH'] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'saml') + + +def init_saml_auth(req): + auth = OneLogin_Saml2_Auth(req, custom_base_path=app.config['SAML_PATH']) + return auth + + +def prepare_flask_request(request): + # If server is behind proxys or balancers use the HTTP_X_FORWARDED fields + url_data = urlparse(request.url) + return { + 'https': 'on' if request.scheme == 'https' else 'off', + 'http_host': request.host, + 'server_port': url_data.port, + 'script_name': request.path, + 'get_data': request.args.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 + } + + +@app.route('/', methods=['GET', 'POST']) +def index(): + req = prepare_flask_request(request) + auth = init_saml_auth(req) + errors = [] + error_reason = None + not_auth_warn = False + success_slo = False + attributes = False + paint_logout = False + + 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 = 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'] + 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: + 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['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']: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the request.form['RelayState'] is a trusted URL. + return redirect(auth.redirect_to(request.form['RelayState'])) + elif auth.get_settings().is_debug_active(): + error_reason = auth.get_last_error_reason() + elif 'sls' in request.args: + request_id = None + if 'LogoutRequestID' in session: + request_id = session['LogoutRequestID'] + dscb = lambda: session.clear() + 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: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the request.form['RelayState'] is a trusted URL. + return redirect(url) + else: + success_slo = True + elif auth.get_settings().is_debug_active(): + error_reason = auth.get_last_error_reason() + + if 'samlUserdata' in session: + paint_logout = True + if len(session['samlUserdata']) > 0: + attributes = session['samlUserdata'].items() + + return render_template( + 'index.html', + errors=errors, + error_reason=error_reason, + not_auth_warn=not_auth_warn, + success_slo=success_slo, + attributes=attributes, + paint_logout=paint_logout + ) + + +@app.route('/attrs/') +def attrs(): + paint_logout = False + attributes = False + + if 'samlUserdata' in session: + paint_logout = True + if len(session['samlUserdata']) > 0: + attributes = session['samlUserdata'].items() + + return render_template('attrs.html', paint_logout=paint_logout, + attributes=attributes) + + +@app.route('/metadata/') +def metadata(): + req = prepare_flask_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 = make_response(metadata, 200) + resp.headers['Content-Type'] = 'text/xml' + else: + resp = make_response(', '.join(errors), 500) + return resp + + +if __name__ == "__main__": + app.run(host='0.0.0.0', port=8000, debug=True) diff --git a/demo-flask/requirements.txt b/demo-flask/requirements.txt new file mode 100644 index 00000000..d9340937 --- /dev/null +++ b/demo-flask/requirements.txt @@ -0,0 +1 @@ +flask==1.0 diff --git a/demo-flask/saml/advanced_settings.json b/demo-flask/saml/advanced_settings.json new file mode 100644 index 00000000..ed284e05 --- /dev/null +++ b/demo-flask/saml/advanced_settings.json @@ -0,0 +1,35 @@ +{ + "security": { + "nameIdEncrypted": false, + "authnRequestsSigned": false, + "logoutRequestSigned": false, + "logoutResponseSigned": false, + "signMetadata": false, + "wantMessagesSigned": false, + "wantAssertionsSigned": false, + "wantNameId" : true, + "wantNameIdEncrypted": false, + "wantAssertionsEncrypted": false, + "allowSingleLabelDomains": false, + "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256", + "rejectDeprecatedAlgorithm": true + }, + "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/demo-flask/saml/certs/README b/demo-flask/saml/certs/README new file mode 100644 index 00000000..7cf0c143 --- /dev/null +++ b/demo-flask/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-flask/saml/settings.json b/demo-flask/saml/settings.json new file mode 100644 index 00000000..ec40b674 --- /dev/null +++ b/demo-flask/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-flask/templates/attrs.html b/demo-flask/templates/attrs.html new file mode 100644 index 00000000..6c60f4c3 --- /dev/null +++ b/demo-flask/templates/attrs.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} + +{% 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-flask/templates/base.html b/demo-flask/templates/base.html new file mode 100644 index 00000000..960ca0bc --- /dev/null +++ b/demo-flask/templates/base.html @@ -0,0 +1,26 @@ + + + + + + + + A Python SAML Toolkit demo + + + + + + + + +
+

A Python SAML Toolkit demo

+ + {% block content %}{% endblock %} +
+ + diff --git a/demo-flask/templates/index.html b/demo-flask/templates/index.html new file mode 100644 index 00000000..c7d5137c --- /dev/null +++ b/demo-flask/templates/index.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} + +{% block content %} + +{% 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/.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..3960911a --- /dev/null +++ b/demo_pyramid/demo_pyramid/saml/advanced_settings.json @@ -0,0 +1,35 @@ +{ + "security": { + "nameIdEncrypted": false, + "authnRequestsSigned": false, + "logoutRequestSigned": false, + "logoutResponseSigned": false, + "signMetadata": false, + "wantMessagesSigned": false, + "wantAssertionsSigned": false, + "wantNameId" : true, + "wantNameIdEncrypted": false, + "wantAssertionsEncrypted": false, + "allowSingleLabelDomains": false, + "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256", + "rejectDeprecatedAlgorithm": true + }, + "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..761d6e00 --- /dev/null +++ b/demo_pyramid/demo_pyramid/views.py @@ -0,0 +1,130 @@ +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']: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the request.POST['RelayState'] is a trusted URL. + 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: + # To avoid 'Open Redirect' attacks, before execute the redirection confirm + # the value of the url is a trusted URL. + 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/SAML_Python_Toolkit_Guide.pdf b/docs/SAML_Python_Toolkit_Guide.pdf new file mode 100644 index 00000000..509e64a9 Binary files /dev/null and b/docs/SAML_Python_Toolkit_Guide.pdf differ diff --git a/docs/saml2/_modules/index.html b/docs/saml2/_modules/index.html new file mode 100644 index 00000000..8757e8fd --- /dev/null +++ b/docs/saml2/_modules/index.html @@ -0,0 +1,110 @@ + + + + + + Overview: module code — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+ + +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/auth.html b/docs/saml2/_modules/onelogin/saml2/auth.html new file mode 100644 index 00000000..b7c47d8b --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/auth.html @@ -0,0 +1,696 @@ + + + + + + onelogin.saml2.auth — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for onelogin.saml2.auth

+# -*- coding: utf-8 -*-
+
+""" OneLogin_Saml2_Auth class
+
+MIT License
+
+Main class of Python Toolkit.
+
+Initializes the SP SAML instance
+
+"""
+
+from base64 import b64encode
+from urllib import quote_plus
+
+from onelogin.saml2.authn_request import OneLogin_Saml2_Authn_Request
+from onelogin.saml2.constants import OneLogin_Saml2_Constants
+from onelogin.saml2.errors import OneLogin_Saml2_Error
+from onelogin.saml2.logout_response import OneLogin_Saml2_Logout_Response
+from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request
+from onelogin.saml2.response import OneLogin_Saml2_Response
+from onelogin.saml2.settings import OneLogin_Saml2_Settings
+from onelogin.saml2.utils import OneLogin_Saml2_Utils, xmlsec
+from onelogin.saml2.xmlparser import tostring
+
+
+
[docs]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 + SAML Response, a Logout Request or a Logout Response). + """ + + def __init__(self, request_data, old_settings=None, custom_base_path=None): + """ + Initializes the SP SAML instance. + + :param request_data: Request Data + :type request_data: dict + + :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 + """ + self.__request_data = request_data + self.__settings = OneLogin_Saml2_Settings(old_settings, custom_base_path) + self.__attributes = [] + self.__friendlyname_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 + +
[docs] def get_settings(self): + """ + Returns the settings info + :return: Setting info + :rtype: OneLogin_Saml2_Setting object + """ + return self.__settings
+ +
[docs] def set_strict(self, value): + """ + Set the strict mode active/disable + + :param value: + :type value: bool + """ + assert isinstance(value, bool) + self.__settings.set_strict(value)
+ +
[docs] def process_response(self, request_id=None): + """ + Process the SAML Response sent by 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.__friendlyname_attributes = response.get_friendlyname_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: + self.__errors.append('invalid_response') + self.__error_reason = response.get_error() + else: + self.__errors.append('invalid_binding') + raise OneLogin_Saml2_Error( + 'SAML Response not found, Only supported HTTP_POST Binding', + OneLogin_Saml2_Error.SAML_RESPONSE_NOT_FOUND + )
+ +
[docs] def process_slo(self, keep_local_session=False, request_id=None, delete_session_cb=None): + """ + Process the SAML Logout Response / Logout Request sent by the IdP. + + :param keep_local_session: When false will destroy the local session, otherwise will destroy it + :type keep_local_session: bool + + :param request_id: The ID of the LogoutRequest sent by this SP to the IdP + :type request_id: string + + :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') + 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() + else: + if not keep_local_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']: + parameters['SigAlg'] = security['signatureAlgorithm'] + parameters['Signature'] = self.build_response_signature(logout_response, parameters.get('RelayState', None), security['signatureAlgorithm']) + + return self.redirect_to(self.get_slo_url(), parameters) + else: + self.__errors.append('invalid_binding') + raise OneLogin_Saml2_Error( + 'SAML LogoutRequest/LogoutResponse not found. Only supported HTTP_REDIRECT Binding', + OneLogin_Saml2_Error.SAML_LOGOUTMESSAGE_NOT_FOUND + )
+ +
[docs] def redirect_to(self, url=None, parameters={}): + """ + 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 + :type parameters: dict + + :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)
+ +
[docs] 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
+ +
[docs] def is_authenticated(self): + """ + Checks if the user is authenticated or not. + + :returns: True if is authenticated, False if not + :rtype: bool + """ + return self.__authenticated
+ +
[docs] def get_attributes(self): + """ + Returns the set of SAML attributes. + + :returns: SAML attributes + :rtype: dict + """ + return self.__attributes
+ +
[docs] def get_friendlyname_attributes(self): + """ + Returns the set of SAML attributes indexed by FiendlyName. + + :returns: SAML attributes + :rtype: dict + """ + return self.__friendlyname_attributes
+ +
[docs] def get_nameid(self): + """ + Returns the nameID. + + :returns: NameID + :rtype: string|None + """ + return self.__nameid
+ +
[docs] def get_nameid_format(self): + """ + Returns the nameID Format. + + :returns: NameID Format + :rtype: string|None + """ + return self.__nameid_format
+ +
[docs] def get_nameid_nq(self): + """ + Returns the nameID NameQualifier of the Assertion. + + :returns: NameID NameQualifier + :rtype: string|None + """ + return self.__nameid_nq
+ +
[docs] def get_nameid_spnq(self): + """ + Returns the nameID SP NameQualifier of the Assertion. + + :returns: NameID SP NameQualifier + :rtype: string|None + """ + return self.__nameid_spnq
+ +
[docs] def get_session_index(self): + """ + Returns the SessionIndex from the AuthnStatement. + :returns: The SessionIndex of the assertion + :rtype: string + """ + return self.__session_index
+ +
[docs] def get_session_expiration(self): + """ + Returns the SessionNotOnOrAfter from the AuthnStatement. + :returns: The SessionNotOnOrAfter of the assertion + :rtype: unix/posix timestamp|None + """ + return self.__session_expiration
+ +
[docs] 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
+ +
[docs] def get_errors(self): + """ + Returns a list with code errors if something went wrong + + :returns: List of errors + :rtype: list + """ + return self.__errors
+ +
[docs] def get_last_error_reason(self): + """ + Returns the reason for the last error + + :returns: Reason of the last error + :rtype: None | string + """ + return self.__error_reason
+ +
[docs] def get_attribute(self, name): + """ + Returns the requested SAML attribute. + + :param name: Name of the attribute + :type name: string + + :returns: Attribute value(s) if exists or None + :rtype: list + """ + assert isinstance(name, basestring) + value = None + if self.__attributes and name in self.__attributes.keys(): + value = self.__attributes[name] + return value
+ +
[docs] def get_friendlyname_attribute(self, friendlyname): + """ + Returns the requested SAML attribute searched by FriendlyName. + + :param friendlyname: FriendlyName of the attribute + :type friendlyname: string + + :returns: Attribute value(s) if exists or None + :rtype: list + """ + assert isinstance(friendlyname, basestring) + value = None + if self.__friendlyname_attributes and friendlyname in self.__friendlyname_attributes.keys(): + value = self.__friendlyname_attributes[friendlyname] + return value
+ +
[docs] def get_last_request_id(self): + """ + :returns: The ID of the last Request SAML message generated. + :rtype: string + """ + return self.__last_request_id
+ +
[docs] def get_last_message_id(self): + """ + :returns: The ID of the last Response SAML message processed. + :rtype: string + """ + return self.__last_message_id
+ +
[docs] def get_last_assertion_id(self): + """ + :returns: The ID of the last assertion processed. + :rtype: string + """ + return self.__last_assertion_id
+ +
[docs] 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 AuthNRequest will set the ForceAuthn='true'. + :type force_authn: bool + + :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 + + :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} + if return_to is not None: + parameters['RelayState'] = return_to + else: + parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data) + + security = self.__settings.get_security_data() + if security.get('authnRequestsSigned', False): + parameters['SigAlg'] = security['signatureAlgorithm'] + parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState'], security['signatureAlgorithm']) + return self.redirect_to(self.get_sso_url(), parameters)
+ +
[docs] def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name_id_format=None, spnq=None): + """ + Initiates the SLO process. + + :param return_to: Optional argument. The target URL the user should be redirected to after logout. + :type return_to: string + + :param name_id: The NameID that will be set in the LogoutRequest. + :type name_id: string + + :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() + if slo_url is None: + raise OneLogin_Saml2_Error( + 'The IdP does not support Single Log Out', + OneLogin_Saml2_Error.SAML_SINGLE_LOGOUT_NOT_SUPPORTED + ) + + if name_id is None and self.__nameid is not None: + name_id = self.__nameid + 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()} + if return_to is not None: + parameters['RelayState'] = return_to + else: + parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data) + + security = self.__settings.get_security_data() + if security.get('logoutRequestSigned', False): + parameters['SigAlg'] = security['signatureAlgorithm'] + parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState'], security['signatureAlgorithm']) + return self.redirect_to(slo_url, parameters)
+ +
[docs] def get_sso_url(self): + """ + Gets the IdP SSO URL. + + :returns: An URL, the SSO endpoint of the IdP + :rtype: string + """ + return self.__settings.get_idp_sso_url()
+ +
[docs] def get_slo_url(self): + """ + Gets the IdP SLO URL. + + :returns: An URL, the SLO endpoint of the IdP + :rtype: string + """ + return self.__settings.get_idp_slo_url()
+ +
[docs] def get_slo_response_url(self): + """ + Gets the SLO return URL for IdP-initiated logout. + + :returns: an URL, the SLO return endpoint of the IdP + :rtype: string + """ + return self.__settings.get_idp_slo_response_url()
+ +
[docs] def build_request_signature(self, saml_request, relay_state, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): + """ + Builds the Signature of the SAML Request. + + :param saml_request: The SAML Request + :type saml_request: string + + :param relay_state: The target URL the user should be redirected to + :type relay_state: string + + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string + """ + return self.__build_signature(saml_request, relay_state, 'SAMLRequest', sign_algorithm)
+ +
[docs] def build_response_signature(self, saml_response, relay_state, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): + """ + Builds the Signature of the SAML Response. + :param saml_request: The SAML Response + :type saml_request: string + + :param relay_state: The target URL the user should be redirected to + :type relay_state: string + + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string + """ + return self.__build_signature(saml_response, relay_state, 'SAMLResponse', sign_algorithm)
+ + def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): + """ + Builds the Signature + :param saml_data: The SAML Data + :type saml_data: string + + :param relay_state: The target URL the user should be redirected to + :type relay_state: string + + :param saml_type: The target URL the user should be redirected to + :type saml_type: string SAMLRequest | SAMLResponse + + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string + """ + assert saml_type in ['SAMLRequest', 'SAMLResponse'] + + # Load the key into the xmlsec context + key = self.__settings.get_sp_key() + + 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.PRIVATE_KEY_NOT_FOUND + ) + + dsig_ctx = xmlsec.DSigCtx() + dsig_ctx.signKey = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None) + + 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 = { + 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(sign_algorithm, xmlsec.TransformRsaSha1) + + signature = dsig_ctx.signBinary(str(msg), sign_algorithm_transform) + return b64encode(signature) + +
[docs] 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
+ +
[docs] 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
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/authn_request.html b/docs/saml2/_modules/onelogin/saml2/authn_request.html new file mode 100644 index 00000000..d9daac3d --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/authn_request.html @@ -0,0 +1,267 @@ + + + + + + onelogin.saml2.authn_request — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for onelogin.saml2.authn_request

+# -*- coding: utf-8 -*-
+
+""" OneLogin_Saml2_Authn_Request class
+
+MIT License
+
+AuthNRequest class of Python Toolkit.
+
+"""
+from base64 import b64encode
+
+from onelogin.saml2.constants import OneLogin_Saml2_Constants
+from onelogin.saml2.utils import OneLogin_Saml2_Utils
+
+
+
[docs]class OneLogin_Saml2_Authn_Request(object): + """ + + This class handles an AuthNRequest. It builds an + AuthNRequest object. + + """ + + 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 settings: OneLogin_Saml2_Settings + + :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 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 + + sp_data = self.__settings.get_sp_data() + idp_data = self.__settings.get_idp_data() + security = self.__settings.get_security_data() + + uid = OneLogin_Saml2_Utils.generate_unique_id() + self.__id = uid + issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) + + destination = idp_data['singleSignOnService']['url'] + + provider_name_str = '' + organization_data = settings.get_organization() + if isinstance(organization_data, dict) and organization_data: + langs = organization_data.keys() + if 'en-US' in langs: + lang = 'en-US' + else: + lang = langs[0] + if 'displayname' in organization_data[lang] and organization_data[lang]['displayname'] is not None: + provider_name_str = "\n" + ' ProviderName="%s"' % organization_data[lang]['displayname'] + + force_authn_str = '' + if force_authn is True: + force_authn_str = "\n" + ' ForceAuthn="true"' + + is_passive_str = '' + if is_passive is True: + is_passive_str = "\n" + ' IsPassive="true"' + + subject_str = '' + if name_id_value_req: + subject_str = """ + <saml:Subject> + <saml:NameID Format="%s">%s</saml:NameID> + <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"></saml:SubjectConfirmation> + </saml:Subject>""" % (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 = """ + <samlp:NameIDPolicy + Format="%s" + AllowCreate="true" />""" % name_id_policy_format + + requested_authn_context_str = '' + if 'requestedAuthnContext' in security.keys() and security['requestedAuthnContext'] is not False: + authn_comparison = security['requestedAuthnContextComparison'] + + if security['requestedAuthnContext'] is True: + requested_authn_context_str = "\n" + """ <samlp:RequestedAuthnContext Comparison="%s"> + <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef> + </samlp:RequestedAuthnContext>""" % authn_comparison + else: + requested_authn_context_str = "\n" + ' <samlp:RequestedAuthnContext Comparison="%s">' % authn_comparison + for authn_context in security['requestedAuthnContext']: + requested_authn_context_str += '<saml:AuthnContextClassRef>%s</saml:AuthnContextClassRef>' % authn_context + requested_authn_context_str += ' </samlp:RequestedAuthnContext>' + + attr_consuming_service_str = '' + if 'attributeConsumingService' in sp_data and sp_data['attributeConsumingService']: + attr_consuming_service_str = 'AttributeConsumingServiceIndex="1"' + + request = """<samlp:AuthnRequest + xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" + xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" + ID="%(id)s" + Version="2.0"%(provider_name)s%(force_authn_str)s%(is_passive_str)s + IssueInstant="%(issue_instant)s" + Destination="%(destination)s" + ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + AssertionConsumerServiceURL="%(assertion_url)s" + %(attr_consuming_service_str)s> + <saml:Issuer>%(entity_id)s</saml:Issuer>%(subject_str)s%(nameid_policy_str)s%(requested_authn_context_str)s +</samlp:AuthnRequest>""" % \ + { + 'id': uid, + 'provider_name': provider_name_str, + 'force_authn_str': force_authn_str, + 'is_passive_str': is_passive_str, + 'issue_instant': issue_instant, + 'destination': destination, + 'assertion_url': sp_data['assertionConsumerService']['url'], + 'entity_id': sp_data['entityId'], + '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 + +
[docs] def get_request(self, deflate=True): + """ + Returns unsigned AuthnRequest. + :param deflate: It makes the deflate process optional + :type: bool + :return: AuthnRequest maybe deflated and base64 encoded + :rtype: str object + """ + if deflate: + request = OneLogin_Saml2_Utils.deflate_and_base64_encode(self.__authn_request) + else: + request = b64encode(self.__authn_request) + return request
+ +
[docs] def get_id(self): + """ + Returns the AuthNRequest ID. + :return: AuthNRequest ID + :rtype: string + """ + return self.__id
+ +
[docs] 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
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/constants.html b/docs/saml2/_modules/onelogin/saml2/constants.html new file mode 100644 index 00000000..0f092f82 --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/constants.html @@ -0,0 +1,219 @@ + + + + + + onelogin.saml2.constants — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for onelogin.saml2.constants

+# -*- coding: utf-8 -*-
+
+""" OneLogin_Saml2_Constants class
+
+MIT License
+
+Constants class of Python Toolkit.
+
+"""
+
+
+
[docs]class OneLogin_Saml2_Constants(object): + """ + + This class defines all the constants that will be used + in the Python Toolkit. + + """ + + # Value added to the current time in time condition validations + 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' + NAMEID_X509_SUBJECT_NAME = 'urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName' + NAMEID_WINDOWS_DOMAIN_QUALIFIED_NAME = 'urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName' + NAMEID_UNSPECIFIED = 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified' + NAMEID_KERBEROS = 'urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos' + NAMEID_ENTITY = 'urn:oasis:names:tc:SAML:2.0:nameid-format:entity' + NAMEID_TRANSIENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + NAMEID_PERSISTENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent' + NAMEID_ENCRYPTED = 'urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted' + + # Attribute Name Formats + ATTRNAME_FORMAT_UNSPECIFIED = 'urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified' + ATTRNAME_FORMAT_URI = 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri' + ATTRNAME_FORMAT_BASIC = 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic' + + # Namespaces + NS_SAML = 'urn:oasis:names:tc:SAML:2.0:assertion' + NS_SAMLP = 'urn:oasis:names:tc:SAML:2.0:protocol' + NS_SOAP = 'http://schemas.xmlsoap.org/soap/envelope/' + NS_MD = 'urn:oasis:names:tc:SAML:2.0:metadata' + NS_XS = 'http://www.w3.org/2001/XMLSchema' + NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance' + 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' + BINDING_HTTP_ARTIFACT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact' + BINDING_SOAP = 'urn:oasis:names:tc:SAML:2.0:bindings:SOAP' + BINDING_DEFLATE = 'urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE' + + # Auth Context Class + AC_UNSPECIFIED = 'urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified' + AC_PASSWORD = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password' + AC_PASSWORD_PROTECTED = 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' + AC_X509 = 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509' + AC_SMARTCARD = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Smartcard' + AC_KERBEROS = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Kerberos' + + # Subject Confirmation + CM_BEARER = 'urn:oasis:names:tc:SAML:2.0:cm:bearer' + CM_HOLDER_KEY = 'urn:oasis:names:tc:SAML:2.0:cm:holder-of-key' + CM_SENDER_VOUCHES = 'urn:oasis:names:tc:SAML:2.0:cm:sender-vouches' + + # Status Codes + STATUS_SUCCESS = 'urn:oasis:names:tc:SAML:2.0:status:Success' + STATUS_REQUESTER = 'urn:oasis:names:tc:SAML:2.0:status:Requester' + STATUS_RESPONDER = 'urn:oasis:names:tc:SAML:2.0:status:Responder' + STATUS_VERSION_MISMATCH = 'urn:oasis:names:tc:SAML:2.0:status:VersionMismatch' + STATUS_NO_PASSIVE = 'urn:oasis:names:tc:SAML:2.0:status:NoPassive' + 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' + + # 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/xmldsig-more#sha384' + SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512' + + 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' + RSA_SHA512 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512' + + # Enc + TRIPLEDES_CBC = 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' + AES128_CBC = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' + AES192_CBC = 'http://www.w3.org/2001/04/xmlenc#aes192-cbc' + AES256_CBC = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' + RSA_1_5 = 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' + RSA_OAEP_MGF1P = 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' + + # Define here the deprecated algorithms + DEPRECATED_ALGORITHMS = [DSA_SHA1, RSA_SHA1, SHA1]
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/errors.html b/docs/saml2/_modules/onelogin/saml2/errors.html new file mode 100644 index 00000000..0b40179e --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/errors.html @@ -0,0 +1,232 @@ + + + + + + onelogin.saml2.errors — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for onelogin.saml2.errors

+# -*- coding: utf-8 -*-
+
+""" OneLogin_Saml2_Error class
+
+MIT License
+
+Error class of Python Toolkit.
+
+Defines common Error codes and has a custom initializator.
+
+"""
+
+
+
[docs]class OneLogin_Saml2_Error(Exception): + """ + + This class implements a custom Exception handler. + Defines custom error codes. + + """ + + # Errors + SETTINGS_FILE_NOT_FOUND = 0 + 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 + SAML_RESPONSE_NOT_FOUND = 8 + SAML_LOGOUTMESSAGE_NOT_FOUND = 9 + 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
+ + +
[docs]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 + DEPRECATED_SIGNATURE_METHOD = 46 + DEPRECATED_DIGEST_METHOD = 47 + + 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
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/idp_metadata_parser.html b/docs/saml2/_modules/onelogin/saml2/idp_metadata_parser.html new file mode 100644 index 00000000..f6622e58 --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/idp_metadata_parser.html @@ -0,0 +1,362 @@ + + + + + + onelogin.saml2.idp_metadata_parser — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for onelogin.saml2.idp_metadata_parser

+# -*- coding: utf-8 -*-
+
+""" OneLogin_Saml2_IdPMetadataParser class
+
+MIT License
+
+Metadata class of Python Toolkit.
+
+"""
+
+import urllib2
+import ssl
+
+from copy import deepcopy
+
+from onelogin.saml2.constants import OneLogin_Saml2_Constants
+from onelogin.saml2.utils import OneLogin_Saml2_Utils
+from onelogin.saml2.xmlparser import fromstring
+
+
+
[docs]class OneLogin_Saml2_IdPMetadataParser(object): + """ + A class that contain methods related to obtaining and parsing metadata from IdP + + This class does not validate in any way the URL that is introduced, + make sure to validate it properly before use it in a get_metadata method. + """ + +
[docs] @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
+ +
[docs] @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)
+ +
[docs] @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 <IDPSSODescriptor> 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
+ +
[docs] @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: dict + + :param new_metadata_settings: Settings to be merged (extracted from IdP metadata after parsing) + :type new_metadata_settings: dict + + :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
+ + +
[docs]def dict_deep_merge(lhs, rhs): + """Deep-merge dictionary `rhs` into dictionary `lhs`.""" + updated_rhs = {} + for key in rhs: + if key in lhs and isinstance(lhs[key], dict) and isinstance(rhs[key], dict): + updated_rhs[key] = dict_deep_merge(lhs[key], rhs[key]) + else: + updated_rhs[key] = rhs[key] + lhs.update(updated_rhs) + return lhs
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/logout_request.html b/docs/saml2/_modules/onelogin/saml2/logout_request.html new file mode 100644 index 00000000..3c896717 --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/logout_request.html @@ -0,0 +1,549 @@ + + + + + + onelogin.saml2.logout_request — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for onelogin.saml2.logout_request

+# -*- coding: utf-8 -*-
+
+""" OneLogin_Saml2_Logout_Request class
+
+MIT License
+
+Logout Request class of Python Toolkit.
+
+"""
+
+from zlib import decompress
+from base64 import b64encode, b64decode
+from lxml import etree
+from xml.dom.minidom import Document
+
+from onelogin.saml2.constants import OneLogin_Saml2_Constants
+from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
+from onelogin.saml2.utils import OneLogin_Saml2_Utils
+from onelogin.saml2.xmlparser import fromstring
+
+
+
[docs]class OneLogin_Saml2_Logout_Request(object): + """ + + This class handles a Logout Request. + + Builds a Logout Response object and validates it. + + """ + + 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. + + :param settings: Setting data + :type request_data: OneLogin_Saml2_Settings + + :param request: Optional. A LogoutRequest to be loaded instead build one. + :type request: string + + :param name_id: The NameID that will be set in the LogoutRequest. + :type name_id: string + + :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 + self.id = None + + if request is None: + sp_data = self.__settings.get_sp_data() + idp_data = self.__settings.get_idp_data() + security = self.__settings.get_security_data() + + uid = OneLogin_Saml2_Utils.generate_unique_id() + self.id = uid + + issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) + + cert = None + if 'nameIdEncrypted' in security and security['nameIdEncrypted']: + 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: + 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'] + 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, + spnq, + name_id_format, + cert, + False, + nq + ) + + if session_index: + session_index_str = '<samlp:SessionIndex>%s</samlp:SessionIndex>' % session_index + else: + session_index_str = '' + + logout_request = """<samlp:LogoutRequest + xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" + xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" + ID="%(id)s" + Version="2.0" + IssueInstant="%(issue_instant)s" + Destination="%(single_logout_url)s"> + <saml:Issuer>%(entity_id)s</saml:Issuer> + %(name_id)s + %(session_index)s + </samlp:LogoutRequest>""" % \ + { + 'id': uid, + 'issue_instant': issue_instant, + 'single_logout_url': self.__settings.get_idp_slo_url(), + 'entity_id': sp_data['entityId'], + 'name_id': name_id_obj, + 'session_index': session_index_str, + } + else: + decoded = b64decode(request) + # We try to inflate + try: + inflated = decompress(decoded, -15) + logout_request = inflated + except Exception: + logout_request = decoded + self.id = self.get_id(logout_request) + + self.__logout_request = logout_request + +
[docs] def get_request(self, deflate=True): + """ + 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 + """ + if deflate: + request = OneLogin_Saml2_Utils.deflate_and_base64_encode(self.__logout_request) + else: + request = b64encode(self.__logout_request) + return request
+ +
[docs] 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
+ +
[docs] @staticmethod + def get_id(request): + """ + Returns the ID of the Logout Request + :param request: Logout Request Message + :type request: string|DOMDocument + :return: string ID + :rtype: str object + """ + if isinstance(request, etree._Element): + elem = request + else: + if isinstance(request, Document): + request = request.toxml() + elem = fromstring(request, forbid_dtd=True) + return elem.get('ID', None)
+ +
[docs] @staticmethod + def get_nameid_data(request, key=None): + """ + Gets the NameID Data of the the Logout Request + :param request: Logout Request Message + :type request: string|DOMDocument + :param key: The SP key + :type key: string + :return: Name ID Data (Value, Format, NameQualifier, SPNameQualifier) + :rtype: dict + """ + if isinstance(request, etree._Element): + elem = request + else: + if isinstance(request, Document): + request = request.toxml() + 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 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: + encrypted_data = encrypted_data_nodes[0] + name_id = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) + else: + entries = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:NameID') + if len(entries) == 1: + name_id = entries[0] + + if name_id is None: + raise OneLogin_Saml2_ValidationError( + 'NameID not found in the Logout Request', + OneLogin_Saml2_ValidationError.NO_NAMEID + ) + + name_id_data = { + 'Value': OneLogin_Saml2_Utils.element_text(name_id) + } + for attr in ['Format', 'SPNameQualifier', 'NameQualifier']: + if attr in name_id.attrib.keys(): + name_id_data[attr] = name_id.attrib[attr] + + return name_id_data
+ +
[docs] @staticmethod + def get_nameid(request, key=None): + """ + Gets the NameID 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 = OneLogin_Saml2_Logout_Request.get_nameid_data(request, key) + return name_id['Value']
+ +
[docs] @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
+ +
[docs] @staticmethod + def get_issuer(request): + """ + Gets the Issuer of the Logout Request Message + :param request: Logout Request Message + :type request: string|DOMDocument + :return: The Issuer + :rtype: string + """ + if isinstance(request, etree._Element): + elem = request + else: + if isinstance(request, Document): + request = request.toxml() + 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 = OneLogin_Saml2_Utils.element_text(issuer_nodes[0]) + return issuer
+ +
[docs] @staticmethod + def get_session_indexes(request): + """ + Gets the SessionIndexes from the Logout Request + :param request: Logout Request Message + :type request: string|DOMDocument + :return: The SessionIndex value + :rtype: list + """ + if isinstance(request, etree._Element): + elem = request + else: + if isinstance(request, Document): + request = request.toxml() + 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(OneLogin_Saml2_Utils.element_text(session_index_node)) + return session_indexes
+ +
[docs] def is_valid(self, request_data, raise_exceptions=False): + """ + 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, forbid_dtd=True) + + idp_data = self.__settings.get_idp_data() + idp_entity_id = idp_data['entityId'] + + if 'get_data' in request_data.keys(): + get_data = request_data['get_data'] + else: + get_data = {} + + if 'lowercase_urlencoding' in request_data.keys(): + lowercase_urlencoding = request_data['lowercase_urlencoding'] + + security = self.__settings.get_security_data() + 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 OneLogin_Saml2_ValidationError( + 'Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd', + OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT + ) + + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + + # Check NotOnOrAfter + if dom.get('NotOnOrAfter', None): + na = OneLogin_Saml2_Utils.parse_SAML_to_time(dom.get('NotOnOrAfter')) + if na <= OneLogin_Saml2_Utils.now(): + raise OneLogin_Saml2_ValidationError( + 'Could not validate timestamp: expired. Check system clock.', + OneLogin_Saml2_ValidationError.RESPONSE_EXPIRED + ) + + # Check destination + destination = dom.get('Destination') + if destination: + if not OneLogin_Saml2_Utils.normalize_url(url=destination).startswith(OneLogin_Saml2_Utils.normalize_url(url=current_url)): + raise Exception( + 'The LogoutRequest was received at ' + '%(currentURL)s instead of %(destination)s' % + { + '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 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 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: + sign_alg = OneLogin_Saml2_Constants.RSA_SHA1 + else: + sign_alg = get_data['SigAlg'] + + reject_deprecated_alg = security.get('rejectDeprecatedAlgorithm', False) + if reject_deprecated_alg: + if sign_alg in OneLogin_Saml2_Constants.DEPRECATED_ALGORITHMS: + raise OneLogin_Saml2_ValidationError( + 'Deprecated signature algorithm found: %s' % sign_alg, + OneLogin_Saml2_ValidationError.DEPRECATED_SIGNATURE_METHOD + ) + + 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, 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', sign_alg, 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__()) + if raise_exceptions: + raise err + return False
+ +
[docs] def get_error(self): + """ + After executing a validation process, if it fails this method returns the cause + """ + return self.__error
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/logout_response.html b/docs/saml2/_modules/onelogin/saml2/logout_response.html new file mode 100644 index 00000000..714c701a --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/logout_response.html @@ -0,0 +1,377 @@ + + + + + + onelogin.saml2.logout_response — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for onelogin.saml2.logout_response

+# -*- coding: utf-8 -*-
+
+""" OneLogin_Saml2_Logout_Response class
+
+MIT License
+
+Logout Response class of Python Toolkit.
+
+"""
+
+from base64 import b64encode, b64decode
+from xml.dom.minidom import Document
+from defusedxml.minidom import parseString
+
+from onelogin.saml2.constants import OneLogin_Saml2_Constants
+from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
+from onelogin.saml2.utils import OneLogin_Saml2_Utils
+from onelogin.saml2.xmlparser import fromstring
+
+
+
[docs]class OneLogin_Saml2_Logout_Response(object): + """ + + This class handles a Logout Response. It Builds or parses a Logout Response object + and validates it. + + """ + + def __init__(self, settings, response=None): + """ + Constructs a Logout Response object (Initialize params from settings + and if provided load the Logout Response. + + Arguments are: + * (OneLogin_Saml2_Settings) settings. Setting data + * (string) response. An UUEncoded SAML Logout + response from the IdP. + """ + 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, forbid_dtd=True) + self.id = self.document.documentElement.getAttribute('ID') + +
[docs] def get_issuer(self): + """ + Gets the Issuer of the Logout Response Message + :return: The Issuer + :rtype: string + """ + issuer = None + issuer_nodes = self.__query('/samlp:LogoutResponse/saml:Issuer') + if len(issuer_nodes) == 1: + issuer = OneLogin_Saml2_Utils.element_text(issuer_nodes[0]) + return issuer
+ +
[docs] def get_status(self): + """ + Gets the Status + :return: The Status + :rtype: string + """ + entries = self.__query('/samlp:LogoutResponse/samlp:Status/samlp:StatusCode') + if len(entries) == 0: + return None + status = entries[0].attrib['Value'] + return status
+ +
[docs] 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'] + + security = self.__settings.get_security_data() + 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 OneLogin_Saml2_ValidationError( + 'Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd', + OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT + ) + + 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 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) + + # Check destination + if self.document.documentElement.hasAttribute('Destination'): + destination = self.document.documentElement.getAttribute('Destination') + if destination: + if not OneLogin_Saml2_Utils.normalize_url(url=destination).startswith(OneLogin_Saml2_Utils.normalize_url(url=current_url)): + 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 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: + sign_alg = OneLogin_Saml2_Constants.RSA_SHA1 + else: + sign_alg = get_data['SigAlg'] + + reject_deprecated_alg = security.get('rejectDeprecatedAlgorithm', False) + if reject_deprecated_alg: + if sign_alg in OneLogin_Saml2_Constants.DEPRECATED_ALGORITHMS: + raise OneLogin_Saml2_ValidationError( + 'Deprecated signature algorithm found: %s' % sign_alg, + OneLogin_Saml2_ValidationError.DEPRECATED_SIGNATURE_METHOD + ) + + 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, 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', sign_alg, 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 OneLogin_Saml2_ValidationError( + 'Signature validation failed. Logout Response rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) + + return True + # pylint: disable=R0801 + except Exception as err: + self.__error = err.__str__() + debug = self.__settings.is_debug_active() + if debug: + print(err.__str__()) + if raise_exceptions: + raise err + return False
+ + def __query(self, query): + """ + Extracts a node from the DOMDocument (Logout Response Menssage) + :param query: Xpath Expresion + :type query: string + :return: The queried node + :rtype: DOMNodeList + """ + # Switch to lxml for querying + xml = self.document.toxml() + return OneLogin_Saml2_Utils.query(fromstring(xml, forbid_dtd=True), query) + +
[docs] def build(self, in_response_to): + """ + Creates a Logout Response object. + :param in_response_to: InResponseTo value for the Logout Response. + :type in_response_to: string + """ + sp_data = self.__settings.get_sp_data() + + uid = OneLogin_Saml2_Utils.generate_unique_id() + issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) + + logout_response = """<samlp:LogoutResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" + xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" + ID="%(id)s" + Version="2.0" + IssueInstant="%(issue_instant)s" + Destination="%(destination)s" + InResponseTo="%(in_response_to)s" +> + <saml:Issuer>%(entity_id)s</saml:Issuer> + <samlp:Status> + <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /> + </samlp:Status> +</samlp:LogoutResponse>""" % \ + { + 'id': uid, + 'issue_instant': issue_instant, + 'destination': self.__settings.get_idp_slo_response_url(), + 'in_response_to': in_response_to, + 'entity_id': sp_data['entityId'], + } + + self.__logout_response = logout_response
+ +
[docs] 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')
+ +
[docs] 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
+ +
[docs] def get_xml(self): + """ + 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 self.__logout_response
+ +
[docs] def get_error(self): + """ + After executing a validation process, if it fails this method returns the cause + """ + return self.__error
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/metadata.html b/docs/saml2/_modules/onelogin/saml2/metadata.html new file mode 100644 index 00000000..6e16c080 --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/metadata.html @@ -0,0 +1,383 @@ + + + + + + onelogin.saml2.metadata — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for onelogin.saml2.metadata

+# -*- coding: utf-8 -*-
+
+""" OneLogin_Saml2_Metadata class
+
+MIT License
+
+Metadata class of Python Toolkit.
+
+"""
+
+from time import gmtime, strftime, time
+from datetime import datetime
+from defusedxml.minidom import parseString
+
+from onelogin.saml2.constants import OneLogin_Saml2_Constants
+from onelogin.saml2.utils import OneLogin_Saml2_Utils
+
+
+
[docs]class OneLogin_Saml2_Metadata(object): + """ + + A class that contains methods related to the metadata of the SP + + """ + + TIME_VALID = 172800 # 2 days + TIME_CACHED = 604800 # 1 week + +
[docs] @staticmethod + def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=None, contacts=None, organization=None): + """ + Builds the metadata of the SP + + :param sp: The SP data + :type sp: string + + :param authnsign: authnRequestsSigned attribute + :type authnsign: string + + :param wsign: wantAssertionsSigned attribute + :type wsign: string + + :param valid_until: Metadata's expiry date + :type valid_until: string|DateTime|Timestamp + + :param cache_duration: Duration of the cache in seconds + :type cache_duration: int|string + + :param contacts: Contacts info + :type contacts: dict + + :param organization: Organization info + :type organization: dict + """ + if valid_until is None: + 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() + else: + valid_until_time = gmtime(valid_until) + valid_until_str = strftime(r'%Y-%m-%dT%H:%M:%SZ', valid_until_time) + else: + valid_until_str = valid_until + + if cache_duration is None: + cache_duration = OneLogin_Saml2_Metadata.TIME_CACHED + if not isinstance(cache_duration, basestring): + cache_duration_str = 'PT%sS' % cache_duration # 'P'eriod of 'T'ime x 'S'econds + else: + cache_duration_str = cache_duration + + if contacts is None: + contacts = {} + 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 = """ <md:ServiceDescription xml:lang="en">%s</md:ServiceDescription> +""" % 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 += """ + <saml:AttributeValue xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">%(attributeValue)s</saml:AttributeValue>""" % \ + { + 'attributeValue': attrValue + } + req_attr_aux_str += """ + </md:RequestedAttribute>""" + + requested_attribute = """ <md:RequestedAttribute Name="%(req_attr_name)s"%(req_attr_nameformat_str)s%(req_attr_friendlyname_str)s%(req_attr_isrequired_str)s%(req_attr_aux_str)s""" % \ + { + 'req_attr_name': req_attribs['name'], + 'req_attr_nameformat_str': req_attr_nameformat_str, + 'req_attr_friendlyname_str': req_attr_friendlyname_str, + 'req_attr_isrequired_str': req_attr_isrequired_str, + 'req_attr_aux_str': req_attr_aux_str + } + + requested_attribute_data.append(requested_attribute) + + str_attribute_consuming_service = """ <md:AttributeConsumingService index="1"> + <md:ServiceName xml:lang="en">%(service_name)s</md:ServiceName> +%(attr_cs_desc)s%(requested_attribute_str)s + </md:AttributeConsumingService> +""" % \ + { + '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 = """ <md:SingleLogoutService Binding="%(binding)s" + Location="%(location)s" />\n""" % \ + { + 'binding': sp['singleLogoutService']['binding'], + 'location': sp['singleLogoutService']['url'], + } + + str_authnsign = 'true' if authnsign else 'false' + str_wsign = 'true' if wsign else 'false' + + str_organization = '' + if len(organization) > 0: + organization_names = [] + organization_displaynames = [] + organization_urls = [] + for (lang, info) in organization.items(): + organization_names.append(""" <md:OrganizationName xml:lang="%s">%s</md:OrganizationName>""" % (lang, info['name'])) + organization_displaynames.append(""" <md:OrganizationDisplayName xml:lang="%s">%s</md:OrganizationDisplayName>""" % (lang, info['displayname'])) + organization_urls.append(""" <md:OrganizationURL xml:lang="%s">%s</md:OrganizationURL>""" % (lang, info['url'])) + org_data = '\n'.join(organization_names) + '\n' + '\n'.join(organization_displaynames) + '\n' + '\n'.join(organization_urls) + str_organization = """ <md:Organization> +%(org)s + </md:Organization>\n""" % {'org': org_data} + + str_contacts = '' + if len(contacts) > 0: + contacts_info = [] + for (ctype, info) in contacts.items(): + contact = """ <md:ContactPerson contactType="%(type)s"> + <md:GivenName>%(name)s</md:GivenName> + <md:EmailAddress>%(email)s</md:EmailAddress> + </md:ContactPerson>""" % \ + { + 'type': ctype, + 'name': info['givenName'], + 'email': info['emailAddress'], + } + contacts_info.append(contact) + str_contacts = '\n'.join(contacts_info) + '\n' + + metadata = u"""<?xml version="1.0"?> +<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" + %(valid)s + %(cache)s + entityID="%(entity_id)s"> + <md:SPSSODescriptor AuthnRequestsSigned="%(authnsign)s" WantAssertionsSigned="%(wsign)s" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"> +%(sls)s <md:NameIDFormat>%(name_id_format)s</md:NameIDFormat> + <md:AssertionConsumerService Binding="%(binding)s" + Location="%(location)s" + index="1" /> +%(attribute_consuming_service)s </md:SPSSODescriptor> +%(organization)s%(contacts)s</md:EntityDescriptor>""" % \ + { + 'valid': ('validUntil="%s"' % valid_until_str) if valid_until_str else '', + 'cache': ('cacheDuration="%s"' % cache_duration_str) if cache_duration_str else '', + 'entity_id': sp['entityId'], + 'authnsign': str_authnsign, + 'wsign': str_wsign, + 'name_id_format': sp['NameIDFormat'], + 'binding': sp['assertionConsumerService']['binding'], + 'location': sp['assertionConsumerService']['url'], + 'sls': sls, + 'organization': str_organization, + 'contacts': str_contacts, + 'attribute_consuming_service': str_attribute_consuming_service + } + return metadata
+ +
[docs] @staticmethod + def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA256, digest_algorithm=OneLogin_Saml2_Constants.SHA256): + """ + Signs the metadata with the key/cert provided + + :param metadata: SAML Metadata XML + :type metadata: string + + :param key: x509 key + :type key: string + + :param cert: x509 cert + :type cert: string + + :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, digest_algorithm)
+ +
[docs] @staticmethod + def add_x509_key_descriptors(metadata, cert=None, add_encryption=True): + """ + Adds the x509 descriptors (sign/encryption) to the metadata + The same cert will be used for sign/encrypt + + :param metadata: SAML Metadata XML + :type metadata: string + + :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.encode('utf-8'), forbid_dtd=True, forbid_entities=True, forbid_external=True) + except Exception as e: + raise Exception('Error parsing metadata. ' + e.message) + + formated_cert = OneLogin_Saml2_Utils.format_cert(cert, False) + x509_certificate = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'ds:X509Certificate') + content = xml.createTextNode(formated_cert) + x509_certificate.appendChild(content) + + key_data = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'ds:X509Data') + key_data.appendChild(x509_certificate) + + key_info = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'ds:KeyInfo') + key_info.appendChild(key_data) + + key_descriptor = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'md:KeyDescriptor') + + entity_descriptor = xml.getElementsByTagName('md:EntityDescriptor')[0] + + sp_sso_descriptor = entity_descriptor.getElementsByTagName('md:SPSSODescriptor')[0] + 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') + signing.appendChild(key_info) + signing.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()
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/response.html b/docs/saml2/_modules/onelogin/saml2/response.html new file mode 100644 index 00000000..57e2502c --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/response.html @@ -0,0 +1,1087 @@ + + + + + + onelogin.saml2.response — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for onelogin.saml2.response

+# -*- coding: utf-8 -*-
+
+""" OneLogin_Saml2_Response class
+
+MIT License
+
+SAML Response class of Python Toolkit.
+
+"""
+
+from base64 import b64decode
+from copy import deepcopy
+from xml.dom.minidom import Document
+
+from onelogin.saml2.constants import OneLogin_Saml2_Constants
+from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
+from onelogin.saml2.utils import OneLogin_Saml2_Utils, return_false_on_exception
+from onelogin.saml2.xmlparser import tostring, fromstring
+
+
+
[docs]class OneLogin_Saml2_Response(object): + """ + + This class handles a SAML Response. It parses or validates + a Logout Response object. + + """ + + def __init__(self, settings, response): + """ + Constructs the response object. + + :param settings: The setting info + :type settings: OneLogin_Saml2_Setting object + + :param response: The base64 encoded, XML string containing the samlp:Response + :type response: string + """ + self.__settings = settings + self.__error = None + self.response = b64decode(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') + if encrypted_assertion_nodes: + decrypted_document = deepcopy(self.document) + self.encrypted = True + self.decrypted_document = self.__decrypt_assertion(decrypted_document) + +
[docs] def is_valid(self, request_data, request_id=None, raise_exceptions=False): + """ + Validates the response object. + + :param request_data: Request Data + :type request_data: dict + + :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 + """ + self.__error = None + try: + # Checks SAML version + if self.document.get('Version', None) != '2.0': + 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 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', '') + + signed_elements = self.process_signed_elements() + + 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 + + security = self.__settings.get_security_data() + if self.__settings.is_strict(): + 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 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 + ) + + 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 + 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 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) != 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(authn_contexts).difference(requested_authn_contexts) + if unmatched_contexts: + raise OneLogin_Saml2_ValidationError( + 'The AuthnContext "%s" was not a requested context "%s"' % (', '.join(unmatched_contexts), ', '.join(requested_authn_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 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 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', None) + if destination: + if not OneLogin_Saml2_Utils.normalize_url(url=destination).startswith(OneLogin_Saml2_Utils.normalize_url(url=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 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 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 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 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 + subject_confirmation_nodes = self.__query_assertion('/saml:Subject/saml:SubjectConfirmation') + + for scn in subject_confirmation_nodes: + method = scn.get('Method', None) + if method and method != OneLogin_Saml2_Constants.CM_BEARER: + continue + sc_data = scn.find('saml:SubjectConfirmationData', namespaces=OneLogin_Saml2_Constants.NSMAP) + if sc_data is None: + continue + else: + irt = sc_data.get('InResponseTo', None) + 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: + continue + nooa = sc_data.get('NotOnOrAfter', None) + if nooa: + parsed_nooa = OneLogin_Saml2_Utils.parse_SAML_to_time(nooa) + if parsed_nooa <= OneLogin_Saml2_Utils.now(): + continue + nb = sc_data.get('NotBefore', None) + if nb: + 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 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) + + 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__()) + if raise_exceptions: + raise err + return False
+ +
[docs] def check_status(self): + """ + Check if the status of the response is success or not + + :raises: Exception. If the status is not success + """ + status = OneLogin_Saml2_Utils.get_status(self.document) + code = status.get('code', None) + if code and code != OneLogin_Saml2_Constants.STATUS_SUCCESS: + splited_code = code.split(':') + printable_code = splited_code.pop() + status_exception_msg = 'The status code of the Response was not Success, was %s' % printable_code + status_msg = status.get('msg', None) + if status_msg: + status_exception_msg += ' -> ' + status_msg + raise OneLogin_Saml2_ValidationError( + status_exception_msg, + OneLogin_Saml2_ValidationError.STATUS_CODE_IS_NOT_SUCCESS + )
+ +
[docs] 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
+ +
[docs] 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
+ +
[docs] def get_audiences(self): + """ + Gets the audiences + + :returns: The valid audiences for the SAML Response + :rtype: list + """ + audience_nodes = self.__query_assertion('/saml:Conditions/saml:AudienceRestriction/saml:Audience') + return [OneLogin_Saml2_Utils.element_text(node) for node in audience_nodes if OneLogin_Saml2_Utils.element_text(node) is not None]
+ +
[docs] 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]
+ +
[docs] 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')
+ +
[docs] def get_issuers(self): + """ + Gets the issuers (from message and from assertion) + + :returns: The issuers + :rtype: list + """ + issuers = [] + + 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 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))
+ +
[docs] def get_nameid_data(self): + """ + Gets the NameID Data provided by the SAML Response from the IdP + + :returns: Name ID Data (Value, Format, NameQualifier, SPNameQualifier) + :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] + key = self.__settings.get_sp_key() + nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) + else: + nameid_nodes = self.__query_assertion('/saml:Subject/saml:NameID') + if nameid_nodes: + nameid = nameid_nodes[0] + + 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
+ +
[docs] def get_nameid(self): + """ + Gets the NameID provided by the SAML Response from the IdP + + :returns: NameID (value) + :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
+ +
[docs] 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() + if nameid_data and 'Format' in nameid_data.keys(): + nameid_format = nameid_data['Format'] + return nameid_format
+ +
[docs] 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
+ +
[docs] 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
+ +
[docs] def get_session_not_on_or_after(self): + """ + Gets the SessionNotOnOrAfter from the AuthnStatement + Could be used to set the local session expiration + + :returns: The SessionNotOnOrAfter value + :rtype: time|None + """ + not_on_or_after = None + authn_statement_nodes = self.__query_assertion('/saml:AuthnStatement[@SessionNotOnOrAfter]') + if authn_statement_nodes: + not_on_or_after = OneLogin_Saml2_Utils.parse_SAML_to_time(authn_statement_nodes[0].get('SessionNotOnOrAfter')) + return not_on_or_after
+ +
[docs] 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
+ +
[docs] def get_session_index(self): + """ + Gets the SessionIndex from the AuthnStatement + Could be used to be stored in the local session in order + to be used in a future Logout Request that the SP could + send to the SP, to set what specific session must be deleted + + :returns: The SessionIndex value + :rtype: string|None + """ + session_index = None + authn_statement_nodes = self.__query_assertion('/saml:AuthnStatement[@SessionIndex]') + if authn_statement_nodes: + session_index = authn_statement_nodes[0].get('SessionIndex') + return session_index
+ +
[docs] def get_attributes(self): + """ + Gets the Attributes from the AttributeStatement element. + EncryptedAttributes are not supported + """ + attributes = {} + 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[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
+ +
[docs] def get_friendlyname_attributes(self): + """ + Gets the Attributes from the AttributeStatement element indexed by FiendlyName. + EncryptedAttributes are not supported + """ + attributes = {} + attribute_nodes = self.__query_assertion('/saml:AttributeStatement/saml:Attribute') + for attribute_node in attribute_nodes: + attr_friendlyname = attribute_node.get('FriendlyName') + if attr_friendlyname: + if attr_friendlyname in attributes.keys(): + raise OneLogin_Saml2_ValidationError( + 'Found an Attribute element with duplicated FriendlyName', + OneLogin_Saml2_ValidationError.DUPLICATED_ATTRIBUTE_NAME_FOUND + ) + + values = [] + 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_friendlyname] = values + return attributes
+ +
[docs] def validate_num_assertions(self): + """ + Verifies that the document only contains a single Assertion (encrypted or not) + + :returns: True if only 1 assertion encrypted or not + :rtype: bool + """ + 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
+ +
[docs] 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') + + security = self.__settings.get_security_data() + reject_deprecated_alg = security.get('rejectDeprecatedAlgorithm', False) + + 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) + + # Check the signature and digest algorithm + if reject_deprecated_alg: + sig_method_node = OneLogin_Saml2_Utils.query(sign_node, './/ds:SignatureMethod') + if sig_method_node: + sig_method = sig_method_node[0].get("Algorithm") + if sig_method in OneLogin_Saml2_Constants.DEPRECATED_ALGORITHMS: + raise OneLogin_Saml2_ValidationError( + 'Deprecated signature algorithm found: %s' % sig_method, + OneLogin_Saml2_ValidationError.DEPRECATED_SIGNATURE_METHOD + ) + + dig_method_node = OneLogin_Saml2_Utils.query(sign_node, './/ds:DigestMethod') + if dig_method_node: + dig_method = dig_method_node[0].get("Algorithm") + if dig_method in OneLogin_Saml2_Constants.DEPRECATED_ALGORITHMS: + raise OneLogin_Saml2_ValidationError( + 'Deprecated digest algorithm found: %s' % dig_method, + OneLogin_Saml2_ValidationError.DEPRECATED_DIGEST_METHOD + ) + + 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
+ +
[docs] @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
+ +
[docs] @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 + """ + conditions_nodes = self.__query_assertion('/saml:Conditions') + + 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.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): + """ + Extracts nodes that match the query from the Assertion + + :param query: Xpath Expresion + :type query: String + + :returns: The queried nodes + :rtype: list + """ + 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 + signed_message_query = '/samlp:Response' + signature_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=$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=$tagid]" + tagid = assertion_id[1:] + final_query += xpath_expr + return self.__query(final_query, tagid) + + 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 + """ + if self.encrypted: + document = self.decrypted_document + else: + document = self.document + 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 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, '/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 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 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 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=$tagid]', None, uri) + if encrypted_key: + keyinfo.append(encrypted_key[0]) + + encrypted_data = encrypted_data_nodes[0] + decrypted = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug=debug, inplace=True) + dom.replace(encrypted_assertion_nodes[0], decrypted) + + return dom + +
[docs] def get_error(self): + """ + After executing a validation process, if it fails this method returns the cause + """ + return self.__error
+ +
[docs] 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
+ +
[docs] def get_id(self): + """ + :returns: the ID of the response + :rtype: string + """ + return self.document.get('ID', None)
+ +
[docs] 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)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/settings.html b/docs/saml2/_modules/onelogin/saml2/settings.html new file mode 100644 index 00000000..43aabe0c --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/settings.html @@ -0,0 +1,960 @@ + + + + + + onelogin.saml2.settings — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for onelogin.saml2.settings

+# -*- coding: utf-8 -*-
+
+""" OneLogin_Saml2_Settings class
+
+MIT License
+
+Setting class of Python Toolkit.
+
+"""
+
+import json
+import re
+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
+from onelogin.saml2.errors import OneLogin_Saml2_Error
+from onelogin.saml2.metadata import OneLogin_Saml2_Metadata
+from onelogin.saml2.utils import OneLogin_Saml2_Utils
+
+
+# Regex from Django Software Foundation and individual contributors.
+# 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'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
+    r'(?::\d+)?'  # optional port
+    r'(?:/?|[/?]\S+)$', re.IGNORECASE)
+url_regex_single_label_domain = 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_]))|'  # single-label-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
+    r'(?::\d+)?'  # optional port
+    r'(?:/?|[/?]\S+)$', re.IGNORECASE)
+url_schemes = ['http', 'https', 'ftp', 'ftps']
+
+
+
[docs]def validate_url(url, allow_single_label_domain=False): + """ + Auxiliary method to validate an urllib + :param url: An url to be validated + :type url: string + :param allow_single_label_domain: In order to allow or not single label domain + :type url: bool + :returns: True if the url is valid + :rtype: bool + """ + + scheme = url.split('://')[0].lower() + if scheme not in url_schemes: + return False + if allow_single_label_domain: + if not bool(url_regex_single_label_domain.search(url)): + return False + else: + if not bool(url_regex.search(url)): + return False + return True
+ + +
[docs]class OneLogin_Saml2_Settings(object): + """ + + Handles the settings of the Python toolkits. + + """ + + def __init__(self, settings=None, custom_base_path=None, sp_validation_only=False): + """ + Initializes the settings: + - Sets the paths of the different folders + - Loads settings info from settings file or array/object provided + + :param settings: SAML Toolkit Settings + :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 = True + self.__debug = False + self.__sp = {} + self.__idp = {} + self.__security = {} + self.__contacts = {} + self.__organization = {} + self.__errors = [] + + self.__load_paths(base_path=custom_base_path) + self.__update_paths(settings) + + if settings is None: + try: + valid = self.__load_settings_from_file() + except Exception as e: + raise e + if not valid: + raise OneLogin_Saml2_Error( + 'Invalid dict settings at the file: %s', + OneLogin_Saml2_Error.SETTINGS_INVALID, + ','.join(self.__errors) + ) + self.__add_default_values() + elif isinstance(settings, dict): + if not self.__load_settings_from_dict(settings): + raise OneLogin_Saml2_Error( + 'Invalid dict settings: %s', + OneLogin_Saml2_Error.SETTINGS_INVALID, + ','.join(self.__errors) + ) + else: + 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): + """ + Set the paths of the different folders + """ + if base_path is None: + base_path = dirname(dirname(dirname(abspath(__file__)))) + if not base_path.endswith(sep): + base_path += sep + self.__paths = { + 'base': base_path, + 'cert': base_path + 'certs' + sep, + 'lib': dirname(__file__) + sep + } + + def __update_paths(self, settings): + """ + Set custom paths if necessary + """ + if not isinstance(settings, dict): + return + + if 'custom_base_path' in settings: + base_path = settings['custom_base_path'] + base_path = join(dirname(abspath(__file__)), base_path) + self.__load_paths(base_path) + +
[docs] def get_base_path(self): + """ + Returns base path + + :return: The base toolkit folder path + :rtype: string + """ + return self.__paths['base']
+ +
[docs] def set_cert_path(self, path): + """ + Set a new cert path + """ + self.__paths['cert'] = path
+ +
[docs] def get_cert_path(self): + """ + Returns cert path + + :return: The cert folder path + :rtype: string + """ + return self.__paths['cert']
+ +
[docs] def get_lib_path(self): + """ + Returns lib path + + :return: The library folder path + :rtype: string + """ + return self.__paths['lib']
+ +
[docs] def get_schemas_path(self): + """ + Returns schema path + + :return: The schema folder path + :rtype: string + """ + return self.__paths['lib'] + 'schemas/'
+ + def __load_settings_from_dict(self, settings): + """ + Loads settings info from a settings Dict + + :param settings: SAML Toolkit Settings + :type settings: dict + + :returns: True if the settings info is valid + :rtype: boolean + """ + errors = self.check_settings(settings) + if len(errors) == 0: + self.__errors = [] + self.__sp = settings['sp'] + + 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 + + self.__errors = errors + return False + + def __load_settings_from_file(self): + """ + Loads settings info from the settings json file + + :returns: True if the settings info is valid + :rtype: boolean + """ + filename = self.get_base_path() + 'settings.json' + + if not exists(filename): + raise OneLogin_Saml2_Error( + 'Settings file not found: %s', + OneLogin_Saml2_Error.SETTINGS_FILE_NOT_FOUND, + filename + ) + + # In the php toolkit instead of being a json file it is a php file and + # it is directly included + json_data = open(filename, 'r') + settings = json.load(json_data) + json_data.close() + + advanced_filename = self.get_base_path() + 'advanced_settings.json' + if exists(advanced_filename): + json_data = open(advanced_filename, 'r') + settings.update(json.load(json_data)) # Merge settings + json_data.close() + + return self.__load_settings_from_dict(settings) + + def __add_default_values(self): + """ + Add default values if the settings info is not complete + """ + self.__sp.setdefault('assertionConsumerService', {}) + self.__sp['assertionConsumerService'].setdefault('binding', OneLogin_Saml2_Constants.BINDING_HTTP_POST) + + self.__sp.setdefault('attributeConsumingService', {}) + + self.__sp.setdefault('singleLogoutService', {}) + self.__sp['singleLogoutService'].setdefault('binding', OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT) + + self.__idp.setdefault('singleLogoutService', {}) + + # Related to nameID + self.__sp.setdefault('NameIDFormat', OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED) + self.__security.setdefault('nameIdEncrypted', False) + + # Metadata format + self.__security.setdefault('metadataValidUntil', None) # None means use default + self.__security.setdefault('metadataCacheDuration', None) # None means use default + + # Sign provided + self.__security.setdefault('authnRequestsSigned', False) + self.__security.setdefault('logoutRequestSigned', False) + self.__security.setdefault('logoutResponseSigned', False) + self.__security.setdefault('signMetadata', False) + + # Sign expected + 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 + self.__security.setdefault('wantAssertionsEncrypted', False) + self.__security.setdefault('wantNameIdEncrypted', False) + + # Signature Algorithm + self.__security.setdefault('signatureAlgorithm', OneLogin_Saml2_Constants.RSA_SHA256) + + # Digest Algorithm + self.__security.setdefault('digestAlgorithm', OneLogin_Saml2_Constants.SHA256) + + # Reject Deprecated Algorithms + self.__security.setdefault('rejectDeprecatedAlgorithm', False) + + # AttributeStatement required by default + self.__security.setdefault('wantAttributeStatement', 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('requestedAuthnContextComparison', 'exact') + self.__security.setdefault('failOnAuthnContextMismatch', False) + +
[docs] def check_settings(self, settings): + """ + Checks the settings info. + + :param settings: Dict with settings data + :type settings: dict + + :returns: Errors found on the settings data + :rtype: list + """ + assert isinstance(settings, dict) + + errors = [] + if not isinstance(settings, dict) or len(settings) == 0: + errors.append('invalid_syntax') + else: + if not self.__sp_validation_only: + errors += self.check_idp_settings(settings) + sp_errors = self.check_sp_settings(settings) + errors += sp_errors + + return errors
+ +
[docs] def check_idp_settings(self, settings): + """ + Checks the IdP settings info. + + :param settings: Dict with settings data + :type settings: dict + + :returns: Errors found on the IdP settings data + :rtype: list + """ + assert isinstance(settings, dict) + + errors = [] + if not isinstance(settings, dict) or len(settings) == 0: + errors.append('invalid_syntax') + else: + if not settings.get('idp'): + errors.append('idp_not_found') + else: + allow_single_domain_urls = self._get_allow_single_label_domain(settings) + idp = settings['idp'] + if not idp.get('entityId'): + errors.append('idp_entityId_not_found') + + if not idp.get('singleSignOnService', {}).get('url'): + errors.append('idp_sso_not_found') + elif not validate_url(idp['singleSignOnService']['url'], allow_single_domain_urls): + errors.append('idp_sso_url_invalid') + + slo_url = idp.get('singleLogoutService', {}).get('url') + if slo_url and not validate_url(slo_url, allow_single_domain_urls): + errors.append('idp_slo_url_invalid') + + if 'security' in settings: + security = settings['security'] + + 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 = 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 or exists_multix509sign): + errors.append('idp_cert_or_fingerprint_not_found_and_required') + if nameid_enc and not (exists_x509 or exists_multix509enc): + errors.append('idp_cert_not_found_and_required') + + return errors
+ +
[docs] def check_sp_settings(self, settings): + """ + Checks the SP settings info. + + :param settings: Dict with settings data + :type settings: dict + + :returns: Errors found on the SP settings data + :rtype: list + """ + assert isinstance(settings, dict) + + errors = [] + if not isinstance(settings, dict) or not settings: + errors.append('invalid_syntax') + else: + if not settings.get('sp'): + errors.append('sp_not_found') + else: + allow_single_domain_urls = self._get_allow_single_label_domain(settings) + # check_sp_certs uses self.__sp so I add it + old_sp = self.__sp + self.__sp = settings['sp'] + + sp = settings['sp'] + security = settings.get('security', {}) + + if not sp.get('entityId'): + errors.append('sp_entityId_not_found') + + if not sp.get('assertionConsumerService', {}).get('url'): + errors.append('sp_acs_not_found') + elif not validate_url(sp['assertionConsumerService']['url'], allow_single_domain_urls): + errors.append('sp_acs_url_invalid') + + 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, allow_single_domain_urls): + errors.append('sp_sls_url_invalid') + + if 'signMetadata' in security and isinstance(security['signMetadata'], dict): + if 'keyFileName' not in security['signMetadata'] or \ + 'certFileName' not in security['signMetadata']: + errors.append('sp_signMetadata_invalid') + + 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 \ + want_assert_enc or want_nameid_enc: + errors.append('sp_cert_not_found_and_required') + + if 'contactPerson' in settings: + types = settings['contactPerson'].keys() + valid_types = ['technical', 'support', 'administrative', 'billing', 'other'] + for c_type in types: + if c_type not in valid_types: + errors.append('contact_type_invalid') + break + + for c_type in settings['contactPerson']: + contact = settings['contactPerson'][c_type] + if ('givenName' not in contact or len(contact['givenName']) == 0) or \ + ('emailAddress' not in contact or len(contact['emailAddress']) == 0): + errors.append('contact_not_enough_data') + break + + if 'organization' in settings: + for org in settings['organization']: + organization = settings['organization'][org] + if ('name' not in organization or len(organization['name']) == 0) or \ + ('displayname' not in organization or len(organization['displayname']) == 0) or \ + ('url' not in organization or len(organization['url']) == 0): + errors.append('organization_not_enough_data') + break + # Restores the value that had the self.__sp + if 'old_sp' in locals(): + self.__sp = old_sp + + return errors
+ +
[docs] def check_sp_certs(self): + """ + Checks if the x509 certs of the SP exists and are valid. + + :returns: If the x509 certs of the SP exists and are valid + :rtype: boolean + """ + key = self.get_sp_key() + cert = self.get_sp_cert() + return key is not None and cert is not None
+ +
[docs] def get_idp_sso_url(self): + """ + Gets the IdP SSO URL. + + :returns: An URL, the SSO endpoint of the IdP + :rtype: string + """ + idp_data = self.get_idp_data() + return idp_data['singleSignOnService']['url']
+ +
[docs] def get_idp_slo_url(self): + """ + Gets the IdP SLO URL. + + :returns: An URL, the SLO endpoint of the IdP + :rtype: string + """ + idp_data = self.get_idp_data() + if 'url' in idp_data['singleLogoutService']: + return idp_data['singleLogoutService']['url']
+ +
[docs] def get_idp_slo_response_url(self): + """ + Gets the IdP SLO return URL for IdP-initiated logout. + + :returns: an URL, the SLO return endpoint of the IdP + :rtype: string + """ + idp_data = self.get_idp_data() + if 'url' in idp_data['singleLogoutService']: + return idp_data['singleLogoutService'].get('responseUrl', self.get_idp_slo_url())
+ +
[docs] def get_sp_key(self): + """ + Returns the x509 private key of the SP. + + :returns: SP private key + :rtype: string or None + """ + key = self.__sp.get('privateKey') + 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() + + return key or None
+ +
[docs] def get_sp_cert(self): + """ + Returns the x509 public cert of the SP. + + :returns: SP public cert + :rtype: string or None + """ + cert = self.__sp.get('x509cert') + cert_file_name = self.__paths['cert'] + 'sp.crt' + + if not cert and exists(cert_file_name): + with open(cert_file_name) as f: + cert = f.read() + + return cert or None
+ +
[docs] 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
+ +
[docs] def get_idp_cert(self): + """ + Returns the x509 public cert of the IdP. + + :returns: IdP public cert + :rtype: string + """ + return self.__idp.get('x509cert')
+ +
[docs] def get_idp_data(self): + """ + Gets the IdP data. + + :returns: IdP info + :rtype: dict + """ + return self.__idp
+ +
[docs] def get_sp_data(self): + """ + Gets the SP data. + + :returns: SP info + :rtype: dict + """ + return self.__sp
+ +
[docs] def get_security_data(self): + """ + Gets security data. + + :returns: Security info + :rtype: dict + """ + return self.__security
+ +
[docs] def get_contacts(self): + """ + Gets contact data. + + :returns: Contacts info + :rtype: dict + """ + return self.__contacts
+ +
[docs] def get_organization(self): + """ + Gets organization data. + + :returns: Organization info + :rtype: dict + """ + return self.__organization
+ +
[docs] def get_sp_metadata(self): + """ + Gets the SP metadata. The XML representation. + + :returns: SP metadata (xml) + :rtype: string + """ + metadata = OneLogin_Saml2_Metadata.builder( + self.__sp, self.__security['authnRequestsSigned'], + self.__security['wantAssertionsSigned'], + self.__security['metadataValidUntil'], + 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, add_encryption) + + # Sign metadata + if 'signMetadata' in self.__security and self.__security['signMetadata'] is not False: + if self.__security['signMetadata'] is True: + # Use the SP's normal key to sign the metadata: + if not cert: + raise OneLogin_Saml2_Error( + 'Cannot sign metadata: missing SP public key certificate.', + OneLogin_Saml2_Error.PUBLIC_CERT_FILE_NOT_FOUND + ) + cert_metadata = cert + key_metadata = self.get_sp_key() + if not key_metadata: + raise OneLogin_Saml2_Error( + 'Cannot sign metadata: missing SP private key.', + OneLogin_Saml2_Error.PRIVATE_KEY_FILE_NOT_FOUND + ) + else: + # Use a custom key to sign the metadata: + if ('keyFileName' not in self.__security['signMetadata'] or + 'certFileName' not in self.__security['signMetadata']): + raise OneLogin_Saml2_Error( + 'Invalid Setting: signMetadata value of the sp is not valid', + OneLogin_Saml2_Error.SETTINGS_INVALID_SYNTAX + ) + key_file_name = self.__security['signMetadata']['keyFileName'] + cert_file_name = self.__security['signMetadata']['certFileName'] + key_metadata_file = self.__paths['cert'] + key_file_name + cert_metadata_file = self.__paths['cert'] + cert_file_name + + try: + with open(key_metadata_file, 'r') as f_metadata_key: + key_metadata = f_metadata_key.read() + except IOError: + raise OneLogin_Saml2_Error( + 'Private key file not readable: %s', + OneLogin_Saml2_Error.PRIVATE_KEY_FILE_NOT_FOUND, + key_metadata_file + ) + + try: + with open(cert_metadata_file, 'r') as f_metadata_cert: + cert_metadata = f_metadata_cert.read() + except IOError: + raise OneLogin_Saml2_Error( + 'Public cert file not readable: %s', + OneLogin_Saml2_Error.PUBLIC_CERT_FILE_NOT_FOUND, + cert_metadata_file + ) + + 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
+ +
[docs] def validate_metadata(self, xml): + """ + Validates an XML SP Metadata. + + :param xml: Metadata's XML that will be validate + :type xml: string + + :returns: The list of found errors + :rtype: list + """ + + assert isinstance(xml, basestring) + + if len(xml) == 0: + raise Exception('Empty string supplied as input') + + errors = [] + res = OneLogin_Saml2_Utils.validate_xml(xml, 'saml-schema-metadata-2.0.xsd', self.__debug) + if not isinstance(res, Document): + errors.append(res) + else: + dom = res + element = dom.documentElement + if element.tagName not in 'md:EntityDescriptor': + errors.append('noEntityDescriptor_xml') + else: + if len(element.getElementsByTagName('md:SPSSODescriptor')) != 1: + errors.append('onlySPSSODescriptor_allowed_xml') + else: + valid_until = cache_duration = expire_time = None + + if element.hasAttribute('validUntil'): + valid_until = OneLogin_Saml2_Utils.parse_SAML_to_time(element.getAttribute('validUntil')) + if element.hasAttribute('cacheDuration'): + 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(time()) > int(expire_time): + errors.append('expired_xml') + + # TODO: Validate Sign + + return errors
+ +
[docs] def format_idp_cert(self): + """ + Formats the IdP cert. + """ + self.__idp['x509cert'] = OneLogin_Saml2_Utils.format_cert(self.__idp['x509cert'])
+ +
[docs] 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])
+ +
[docs] def format_sp_cert(self): + """ + Formats the SP cert. + """ + self.__sp['x509cert'] = OneLogin_Saml2_Utils.format_cert(self.__sp['x509cert'])
+ +
[docs] def format_sp_cert_new(self): + """ + Formats the SP cert. + """ + self.__sp['x509certNew'] = OneLogin_Saml2_Utils.format_cert(self.__sp['x509certNew'])
+ +
[docs] def format_sp_key(self): + """ + Formats the private key. + """ + self.__sp['privateKey'] = OneLogin_Saml2_Utils.format_private_key(self.__sp['privateKey'])
+ +
[docs] def get_errors(self): + """ + Returns an array with the errors, the array is empty when the settings is ok. + + :returns: Errors + :rtype: list + """ + return self.__errors
+ +
[docs] def set_strict(self, value): + """ + Activates or deactivates the strict mode. + + :param value: Strict parameter + :type xml: boolean + """ + assert isinstance(value, bool) + + self.__strict = value
+ +
[docs] def is_strict(self): + """ + Returns if the 'strict' mode is active. + + :returns: Strict parameter + :rtype: boolean + """ + return self.__strict
+ +
[docs] def is_debug_active(self): + """ + Returns if the debug is active. + + :returns: Debug parameter + :rtype: boolean + """ + return self.__debug
+ + def _get_allow_single_label_domain(self, settings): + security = settings.get('security', {}) + return 'allowSingleLabelDomains' in security.keys() and security['allowSingleLabelDomains']
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/utils.html b/docs/saml2/_modules/onelogin/saml2/utils.html new file mode 100644 index 00000000..d9b7f442 --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/utils.html @@ -0,0 +1,1402 @@ + + + + + + onelogin.saml2.utils — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for onelogin.saml2.utils

+# -*- coding: utf-8 -*-
+
+""" OneLogin_Saml2_Utils class
+
+MIT License
+
+Auxiliary class of Python Toolkit.
+
+"""
+
+import base64
+from copy import deepcopy
+from datetime import datetime
+import calendar
+from hashlib import sha1, sha256, sha384, sha512
+from isodate import parse_duration as duration_parser
+from lxml import etree
+from os.path import basename, dirname, join
+import re
+from sys import stderr
+from tempfile import NamedTemporaryFile
+from textwrap import wrap
+from urllib import quote_plus
+from urlparse import urlsplit, urlunsplit
+from uuid import uuid4
+from xml.dom.minidom import Document, Element
+from defusedxml.minidom import parseString
+from functools import wraps
+
+import zlib
+
+import dm.xmlsec.binding as xmlsec
+from dm.xmlsec.binding.tmpl import EncData, Signature
+
+from onelogin.saml2.constants import OneLogin_Saml2_Constants
+from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
+from onelogin.saml2.xmlparser import tostring, fromstring
+
+
+if not globals().get('xmlsec_setup', False):
+    xmlsec.initialize()
+    globals()['xmlsec_setup'] = True
+
+
+
[docs]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
+ + + + + +
[docs]class OneLogin_Saml2_Utils(object): + """ + + Auxiliary class that contains several utility methods to parse time, + urls, add sign, encrypt, decrypt, sign validation, handle xml ... + + """ + + RESPONSE_SIGNATURE_XPATH = '/samlp:Response/ds:Signature' + ASSERTION_SIGNATURE_XPATH = '/samlp:Response/saml:Assertion/ds:Signature' + + TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" + TIME_FORMAT_2 = "%Y-%m-%dT%H:%M:%S.%fZ" + TIME_FORMAT_WITH_FRAGMENT = re.compile(r'^(\d{4,4}-\d{2,2}-\d{2,2}T\d{2,2}:\d{2,2}:\d{2,2})(\.\d*)?Z?$') + +
[docs] @staticmethod + def decode_base64_and_inflate(value): + """ + base64 decodes and then inflates according to RFC1951 + :param value: a deflated and encoded string + :type value: string + :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 result.decode('utf-8')
+ +
[docs] @staticmethod + def deflate_and_base64_encode(value): + """ + 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.encode('utf-8'))[2:-4])
+ +
[docs] @staticmethod + def validate_xml(xml, schema, debug=False): + """ + Validates a xml against a schema + :param xml: The xml that will be validated + :type: string|DomDocument + :param schema: The schema + :type: string + :param debug: If debug is active, the parse-errors will be showed + :type: bool + :returns: Error code or the DomDocument of the xml + :rtype: string + """ + assert isinstance(xml, basestring) or isinstance(xml, Document) or isinstance(xml, etree._Element) + assert isinstance(schema, basestring) + + if isinstance(xml, Document): + xml = xml.toxml() + elif isinstance(xml, etree._Element): + xml = tostring(xml, encoding='unicode') + + # Switch to lxml for schema validation + try: + dom = fromstring(xml.encode('utf-8'), forbid_dtd=True) + except Exception: + return 'unloaded_xml' + + schema_file = join(dirname(__file__), 'schemas', schema) + f_schema = open(schema_file, 'r') + schema_doc = etree.parse(f_schema) + f_schema.close() + xmlschema = etree.XMLSchema(schema_doc) + + if not xmlschema.validate(dom): + if debug: + stderr.write('Errors validating the metadata') + stderr.write(':\n\n') + for error in xmlschema.error_log: + stderr.write('%s\n' % error.message) + + return 'invalid_xml' + + return parseString(tostring(dom, encoding='unicode').encode('utf-8'), forbid_dtd=True, forbid_entities=True, forbid_external=True)
+ +
[docs] @staticmethod + def element_text(node): + # Double check, the LXML Parser already removes comments + etree.strip_tags(node, etree.Comment) + return node.text
+ +
[docs] @staticmethod + def format_cert(cert, heads=True): + """ + Returns a x509 cert (adding header & footer if required). + + :param cert: A x509 unformatted cert + :type: string + + :param heads: True if we want to include head and footer + :type: boolean + + :returns: Formatted cert + :rtype: string + """ + x509_cert = cert.replace('\x0D', '') + x509_cert = x509_cert.replace('\r', '') + x509_cert = x509_cert.replace('\n', '') + if len(x509_cert) > 0: + x509_cert = x509_cert.replace('-----BEGIN CERTIFICATE-----', '') + x509_cert = x509_cert.replace('-----END CERTIFICATE-----', '') + x509_cert = x509_cert.replace(' ', '') + + if heads: + x509_cert = "-----BEGIN CERTIFICATE-----\n" + "\n".join(wrap(x509_cert, 64)) + "\n-----END CERTIFICATE-----\n" + + return x509_cert
+ +
[docs] @staticmethod + def format_private_key(key, heads=True): + """ + Returns a private key (adding header & footer if required). + + :param key A private key + :type: string + + :param heads: True if we want to include head and footer + :type: boolean + + :returns: Formatted private key + :rtype: string + """ + private_key = key.replace('\x0D', '') + private_key = private_key.replace('\r', '') + private_key = private_key.replace('\n', '') + if len(private_key) > 0: + if private_key.find('-----BEGIN PRIVATE KEY-----') != -1: + private_key = private_key.replace('-----BEGIN PRIVATE KEY-----', '') + private_key = private_key.replace('-----END PRIVATE KEY-----', '') + private_key = private_key.replace(' ', '') + if heads: + private_key = "-----BEGIN PRIVATE KEY-----\n" + "\n".join(wrap(private_key, 64)) + "\n-----END PRIVATE KEY-----\n" + else: + private_key = private_key.replace('-----BEGIN RSA PRIVATE KEY-----', '') + private_key = private_key.replace('-----END RSA PRIVATE KEY-----', '') + private_key = private_key.replace(' ', '') + if heads: + private_key = "-----BEGIN RSA PRIVATE KEY-----\n" + "\n".join(wrap(private_key, 64)) + "\n-----END RSA PRIVATE KEY-----\n" + return private_key
+ +
[docs] @staticmethod + def redirect(url, parameters={}, request_data={}): + """ + Executes a redirection to the provided url (or return the target url). + + :param url: The target url + :type: string + + :param parameters: Extra parameters to be passed as part of the url + :type: dict + + :param request_data: The request as a dict + :type: dict + + :returns: Url + :rtype: string + """ + assert isinstance(url, basestring) + assert isinstance(parameters, dict) + + if url.startswith('/'): + url = '%s%s' % (OneLogin_Saml2_Utils.get_self_url_host(request_data), url) + + # Verify that the URL is to a http or https site. + if re.search('^https?://', url) is None: + raise OneLogin_Saml2_Error( + 'Redirect to invalid URL: ' + url, + OneLogin_Saml2_Error.REDIRECT_INVALID_URL + ) + + # Add encoded parameters + if url.find('?') < 0: + param_prefix = '?' + else: + param_prefix = '&' + + for name, value in parameters.items(): + + if value is None: + param = quote_plus(name) + elif isinstance(value, list): + param = '' + for val in value: + param += quote_plus(name) + '[]=' + quote_plus(val) + '&' + if len(param) > 0: + param = param[0:-1] + else: + param = quote_plus(name) + '=' + quote_plus(value) + + if param: + url += param_prefix + param + param_prefix = '&' + + return url
+ +
[docs] @staticmethod + def get_self_url_host(request_data): + """ + Returns the protocol + the current host + the port (if different than + common ports). + + :param request_data: The request as a dict + :type: dict + + :return: Url + :rtype: string + """ + current_host = OneLogin_Saml2_Utils.get_self_host(request_data) + port = '' + if OneLogin_Saml2_Utils.is_https(request_data): + protocol = 'https' + else: + protocol = 'http' + + if 'server_port' in request_data and request_data['server_port'] is not None: + port_number = str(request_data['server_port']) + port = ':' + port_number + + if protocol == 'http' and port_number == '80': + port = '' + elif protocol == 'https' and port_number == '443': + port = '' + + return '%s://%s%s' % (protocol, current_host, port)
+ +
[docs] @staticmethod + def get_self_host(request_data): + """ + Returns the current host. + + :param request_data: The request as a dict + :type: dict + + :return: The current host + :rtype: string + """ + if 'http_host' in request_data: + current_host = request_data['http_host'] + elif 'server_name' in request_data: + current_host = request_data['server_name'] + else: + raise Exception('No hostname defined') + + if ':' in current_host: + current_host_data = current_host.split(':') + possible_port = current_host_data[-1] + try: + possible_port = float(possible_port) + current_host = current_host_data[0] + except ValueError: + current_host = ':'.join(current_host_data) + + return current_host
+ +
[docs] @staticmethod + def is_https(request_data): + """ + Checks if https or http. + + :param request_data: The request as a dict + :type: dict + + :return: False if https is not active + :rtype: boolean + """ + is_https = 'https' in request_data and request_data['https'] != 'off' + is_https = is_https or ('server_port' in request_data and str(request_data['server_port']) == '443') + return is_https
+ +
[docs] @staticmethod + def get_self_url_no_query(request_data): + """ + Returns the URL of the current host + current view. + + :param request_data: The request as a dict + :type: dict + + :return: The url of current host + current view + :rtype: string + """ + self_url_host = OneLogin_Saml2_Utils.get_self_url_host(request_data) + script_name = request_data['script_name'] + if script_name: + if script_name[0] != '/': + script_name = '/' + script_name + else: + script_name = '' + self_url_no_query = self_url_host + script_name + if 'path_info' in request_data: + self_url_no_query += request_data['path_info'] + + return self_url_no_query
+ +
[docs] @staticmethod + def get_self_routed_url_no_query(request_data): + """ + Returns the routed URL of the current host + current view. + + :param request_data: The request as a dict + :type: dict + + :return: The url of current host + current view + :rtype: string + """ + self_url_host = OneLogin_Saml2_Utils.get_self_url_host(request_data) + route = '' + if 'request_uri' in request_data.keys() and request_data['request_uri']: + route = request_data['request_uri'] + if 'query_string' in request_data.keys() and request_data['query_string']: + route = route.replace(request_data['query_string'], '') + + return self_url_host + route
+ +
[docs] @staticmethod + def get_self_url(request_data): + """ + Returns the URL of the current host + current view + query. + + :param request_data: The request as a dict + :type: dict + + :return: The url of current host + current view + query + :rtype: string + """ + self_url_host = OneLogin_Saml2_Utils.get_self_url_host(request_data) + + request_uri = '' + if 'request_uri' in request_data: + request_uri = request_data['request_uri'] + if not request_uri.startswith('/'): + match = re.search('^https?://[^/]*(/.*)', request_uri) + if match is not None: + request_uri = match.groups()[0] + + return self_url_host + request_uri
+ +
[docs] @staticmethod + def generate_unique_id(): + """ + Generates an unique string (used for example as ID for assertions). + + :return: A unique string + :rtype: string + """ + return 'ONELOGIN_%s' % sha1(uuid4().hex).hexdigest()
+ +
[docs] @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. + + :param time: The time we should convert (DateTime). + :type: string + + :return: SAML2 timestamp. + :rtype: string + """ + data = datetime.utcfromtimestamp(float(time)) + return data.strftime(OneLogin_Saml2_Utils.TIME_FORMAT)
+ +
[docs] @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. + + :param time: The time we should convert (SAML Timestamp). + :type: string + + :return: Converted to a unix timestamp. + :rtype: int + """ + try: + data = datetime.strptime(timestr, OneLogin_Saml2_Utils.TIME_FORMAT) + except ValueError: + try: + data = datetime.strptime(timestr, OneLogin_Saml2_Utils.TIME_FORMAT_2) + except ValueError: + elem = OneLogin_Saml2_Utils.TIME_FORMAT_WITH_FRAGMENT.match(timestr) + if not elem: + raise Exception("time data %s does not match format %s" % (timestr, r'yyyy-mm-ddThh:mm:ss(\.s+)?Z')) + data = datetime.strptime(elem.groups()[0] + "Z", OneLogin_Saml2_Utils.TIME_FORMAT) + + return calendar.timegm(data.utctimetuple())
+ +
[docs] @staticmethod + def now(): + """ + :return: unix timestamp of actual time. + :rtype: int + """ + return calendar.timegm(datetime.utcnow().utctimetuple())
+ +
[docs] @staticmethod + def parse_duration(duration, timestamp=None): + """ + Interprets a ISO8601 duration value relative to a given timestamp. + + :param duration: The duration, as a string. + :type: string + + :param timestamp: The unix timestamp we should apply the duration to. + Optional, default to the current time. + :type: string + + :return: The new timestamp, after the duration is applied. + :rtype: int + """ + assert isinstance(duration, basestring) + assert timestamp is None or isinstance(timestamp, int) + + timedelta = duration_parser(duration) + if timestamp is None: + data = datetime.utcnow() + timedelta + else: + data = datetime.utcfromtimestamp(timestamp) + timedelta + return calendar.timegm(data.utctimetuple())
+ +
[docs] @staticmethod + def get_expire_time(cache_duration=None, valid_until=None): + """ + Compares 2 dates and returns the earliest. + + :param cache_duration: The duration, as a string. + :type: string + + :param valid_until: The valid until date, as a string or as a timestamp + :type: string + + :return: The expiration time. + :rtype: int + """ + expire_time = None + + if cache_duration is not None: + expire_time = OneLogin_Saml2_Utils.parse_duration(cache_duration) + + if valid_until is not None: + if isinstance(valid_until, int): + valid_until_time = valid_until + else: + valid_until_time = OneLogin_Saml2_Utils.parse_SAML_to_time(valid_until) + if expire_time is None or expire_time > valid_until_time: + expire_time = valid_until_time + + if expire_time is not None: + return '%d' % expire_time + return None
+ +
[docs] @staticmethod + def query(dom, query, context=None, tagid=None): + """ + Extracts nodes that match the query from the Element + + :param dom: The root of the lxml objet + :type: Element + + :param query: Xpath Expresion + :type: string + + :param context: Context Node + :type: DOMElement + + :param tagid: Tag ID + :type: string + + :returns: The queried nodes + :rtype: list + """ + if context is None: + source = dom + else: + source = context + + if tagid is None: + return source.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP) + else: + return source.xpath(query, tagid=tagid, namespaces=OneLogin_Saml2_Constants.NSMAP)
+ +
[docs] @staticmethod + def delete_local_session(callback=None): + """ + Deletes the local session. + """ + + if callback is not None: + callback()
+ +
[docs] @staticmethod + def calculate_x509_fingerprint(x509_cert, alg='sha1'): + """ + Calculates the fingerprint of a formatted x509cert. + + :param x509_cert: x509 cert formatted + :type: string + + :param alg: The algorithm to build the fingerprint + :type: string + + :returns: fingerprint + :rtype: string + """ + assert isinstance(x509_cert, basestring) + + lines = x509_cert.split('\n') + data = '' + inData = False + + for line in lines: + # Remove '\r' from end of line if present. + line = line.rstrip() + 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': + fingerprint = sha512(decoded_data) + elif alg == 'sha384': + fingerprint = sha384(decoded_data) + elif alg == 'sha256': + fingerprint = sha256(decoded_data) + else: + fingerprint = sha1(decoded_data) + + return fingerprint.hexdigest().lower()
+ +
[docs] @staticmethod + def format_finger_print(fingerprint): + """ + Formats a fingerprint. + + :param fingerprint: fingerprint + :type: string + + :returns: Formatted fingerprint + :rtype: string + """ + formated_fingerprint = fingerprint.replace(':', '') + return formated_fingerprint.lower()
+ +
[docs] @staticmethod + def generate_name_id(value, sp_nq, sp_format=None, cert=None, debug=False, nq=None): + """ + Generates a nameID. + + :param value: fingerprint + :type: string + + :param sp_nq: SP Name Qualifier + :type: string + + :param sp_format: SP Format + :type: string + + :param cert: IdP Public Cert to encrypt the nameID + :type: string + + :param debug: Activate the xmlsec debug + :type: bool + + :param nq: IDP Name Qualifier + :type: string + + :returns: DOMElement | XMLSec nameID + :rtype: string + """ + doc = Document() + name_id_container = doc.createElementNS(OneLogin_Saml2_Constants.NS_SAML, 'container') + name_id_container.setAttribute("xmlns:saml", OneLogin_Saml2_Constants.NS_SAML) + + name_id = doc.createElement('saml:NameID') + 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, forbid_dtd=True) + + error_callback_method = None + if debug: + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) + + # Load the public cert + mngr = xmlsec.KeysMngr() + file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) + key_data = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) + key_data.name = basename(file_cert.name) + mngr.addKey(key_data) + file_cert.close() + + # Prepare for encryption + enc_data = EncData(xmlsec.TransformAes128Cbc, type=xmlsec.TypeEncElement) + enc_data.ensureCipherValue() + key_info = enc_data.ensureKeyInfo() + # enc_key = key_info.addEncryptedKey(xmlsec.TransformRsaPkcs1) + enc_key = key_info.addEncryptedKey(xmlsec.TransformRsaOaep) + enc_key.ensureCipherValue() + + # Encrypt! + enc_ctx = xmlsec.EncCtx(mngr) + enc_ctx.encKey = xmlsec.Key.generate(xmlsec.KeyDataAes, 128, xmlsec.KeyDataTypeSession) + + edata = enc_ctx.encryptXml(enc_data, elem[0]) + + newdoc = parseString(tostring(edata, encoding='unicode').encode('utf-8'), forbid_dtd=True, forbid_entities=True, forbid_external=True) + + if newdoc.hasChildNodes(): + child = newdoc.firstChild + child.removeAttribute('xmlns') + child.removeAttribute('xmlns:saml') + child.setAttribute('xmlns:xenc', OneLogin_Saml2_Constants.NS_XENC) + child.setAttribute('xmlns:dsig', OneLogin_Saml2_Constants.NS_DS) + + nodes = newdoc.getElementsByTagName("*") + for node in nodes: + if node.tagName == 'ns0:KeyInfo': + node.tagName = 'dsig:KeyInfo' + node.removeAttribute('xmlns:ns0') + node.setAttribute('xmlns:dsig', OneLogin_Saml2_Constants.NS_DS) + else: + node.tagName = 'xenc:' + node.tagName + + encrypted_id = newdoc.createElement('saml:EncryptedID') + encrypted_data = newdoc.replaceChild(encrypted_id, newdoc.firstChild) + encrypted_id.appendChild(encrypted_data) + return newdoc.saveXML(encrypted_id) + else: + return doc.saveXML(name_id)
+ +
[docs] @staticmethod + def get_status(dom): + """ + Gets Status from a Response. + + :param dom: The Response as XML + :type: Document + + :returns: The Status, an array with the code and a message. + :rtype: dict + """ + status = {} + + status_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status') + 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) != 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) == 1: + status['msg'] = subcode_entry[0].values()[0] + elif len(message_entry) == 1: + status['msg'] = OneLogin_Saml2_Utils.element_text(message_entry[0]) + + return status
+ +
[docs] @staticmethod + def decrypt_element(encrypted_data, key, debug=False, inplace=False): + """ + Decrypts an encrypted element. + + :param encrypted_data: The encrypted data. + :type: lxml.etree.Element | DOMElement | basestring + + :param key: The key. + :type: string + + :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()), forbid_dtd=True) + elif isinstance(encrypted_data, basestring): + 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: + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) + + mngr = xmlsec.KeysMngr() + + key = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None) + mngr.addKey(key) + enc_ctx = xmlsec.EncCtx(mngr) + + return enc_ctx.decrypt(encrypted_data)
+ +
[docs] @staticmethod + def write_temp_file(content): + """ + Writes some content into a temporary file and returns it. + + :param content: The file content + :type: string + + :returns: The temporary file + :rtype: file-like object + """ + f_temp = NamedTemporaryFile(delete=True) + f_temp.file.write(content) + f_temp.file.flush() + return f_temp
+ +
[docs] @staticmethod + def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA256, digest_algorithm=OneLogin_Saml2_Constants.SHA256): + """ + Adds signature key and senders certificate to an element (Message or + Assertion). + + :param xml: The element we should sign + :type: string | Document + + :param key: The private key + :type: string + + :param cert: The public + :type: string + + :param debug: Activate the xmlsec debug + :type: bool + + :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') + elif isinstance(xml, etree._Element): + elem = xml + elif isinstance(xml, Document): + xml = xml.toxml() + elem = fromstring(xml.encode('utf-8'), 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(xml.encode('utf-8'), forbid_dtd=True) + elif isinstance(xml, basestring): + elem = fromstring(xml.encode('utf-8'), 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) + + 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(sign_algorithm, xmlsec.TransformRsaSha1) + + 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: + 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.addTransform(xmlsec.TransformEnveloped) + ref.addTransform(xmlsec.TransformExclC14N) + + key_info = signature.ensureKeyInfo() + key_info.addX509Data() + + dsig_ctx = xmlsec.DSigCtx() + sign_key = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None) + + file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) + sign_key.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem) + file_cert.close() + + dsig_ctx.signKey = sign_key + dsig_ctx.sign(signature) + + return tostring(elem, encoding='unicode').encode('utf-8')
+ +
[docs] @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). + + :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) + + 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)
+ +
[docs] @staticmethod + @return_false_on_exception + def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False): + """ + Validates a signature of a EntityDescriptor. + + :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 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_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') + + error_callback_method = None + if debug: + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) + + xmlsec.addIDs(elem, ["ID"]) + + signature_nodes = OneLogin_Saml2_Utils.query(elem, '/md:EntitiesDescriptor/ds:Signature') + + if len(signature_nodes) == 0: + signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/ds:Signature') + + 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 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.')
+ +
[docs] @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. + + :param signature_node: The signature node + :type: Node + + :param xml: The element we should validate + :type: Document + + :param cert: The public 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 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
+ +
[docs] @staticmethod + @return_false_on_exception + def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_Saml2_Constants.RSA_SHA1, debug=False): + """ + Validates signed binary data (Used to validate GET Signature). + + :param signed_query: The element we should validate + :type: string + + :param signature: The signature that will be validate + :type: string + + :param cert: The public cert + :type: string + + :param algorithm: Signature algorithm + :type: string + + :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) + + 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() + + # 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
+ +
[docs] @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)
+ +
[docs] @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
+ +
[docs] @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
+ +
[docs] @staticmethod + def normalize_url(url): + """ + Returns normalized URL for comparison. + This method converts the netloc to lowercase, as it should be case-insensitive (per RFC 4343, RFC 7617) + If standardization fails, the original URL is returned + Python documentation indicates that URL split also normalizes query strings if empty query fields are present + + :param url: URL + :type url: String + + :returns: A normalized URL, or the given URL string if parsing fails + :rtype: String + """ + try: + scheme, netloc, path, query, fragment = urlsplit(url) + normalized_url = urlunsplit((scheme.lower(), netloc.lower(), path, query, fragment)) + return normalized_url + except Exception: + return url
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/saml2/_modules/onelogin/saml2/xmlparser.html b/docs/saml2/_modules/onelogin/saml2/xmlparser.html new file mode 100644 index 00000000..d8585182 --- /dev/null +++ b/docs/saml2/_modules/onelogin/saml2/xmlparser.html @@ -0,0 +1,244 @@ + + + + + + onelogin.saml2.xmlparser — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for onelogin.saml2.xmlparser

+# Based on the lxml example from defusedxml
+#
+# Copyright (c) 2013 by Christian Heimes <christian@python.org>
+# Licensed to PSF under a Contributor Agreement.
+# See https://www.python.org/psf/license for licensing details.
+"""lxml.etree protection"""
+
+from __future__ import print_function, absolute_import
+
+import threading
+
+from lxml import etree as _etree
+
+from defusedxml.lxml import DTDForbidden, EntitiesForbidden, NotSupportedError
+
+LXML3 = _etree.LXML_VERSION[0] >= 3
+
+__origin__ = "lxml.etree"
+
+tostring = _etree.tostring
+
+
+
[docs]class RestrictedElement(_etree.ElementBase): + """A restricted Element class that filters out instances of some classes + """ + + __slots__ = () + blacklist = (_etree._Entity, _etree._ProcessingInstruction, _etree._Comment) + + def _filter(self, iterator): + blacklist = self.blacklist + for child in iterator: + if isinstance(child, blacklist): + continue + yield child + + def __iter__(self): + iterator = super(RestrictedElement, self).__iter__() + return self._filter(iterator) + +
[docs] def iterchildren(self, tag=None, reversed=False): + iterator = super(RestrictedElement, self).iterchildren(tag=tag, reversed=reversed) + return self._filter(iterator)
+ +
[docs] def iter(self, tag=None, *tags): + iterator = super(RestrictedElement, self).iter(tag=tag, *tags) + return self._filter(iterator)
+ +
[docs] def iterdescendants(self, tag=None, *tags): + iterator = super(RestrictedElement, self).iterdescendants(tag=tag, *tags) + return self._filter(iterator)
+ +
[docs] def itersiblings(self, tag=None, preceding=False): + iterator = super(RestrictedElement, self).itersiblings(tag=tag, preceding=preceding) + return self._filter(iterator)
+ +
[docs] def getchildren(self): + iterator = super(RestrictedElement, self).__iter__() + return list(self._filter(iterator))
+ +
[docs] def getiterator(self, tag=None): + iterator = super(RestrictedElement, self).getiterator(tag) + return self._filter(iterator)
+ + +
[docs]class GlobalParserTLS(threading.local): + """Thread local context for custom parser instances + """ + + parser_config = { + "resolve_entities": False, + 'remove_comments': True, + 'no_network': True, + 'remove_pis': True, + 'huge_tree': False + } + + element_class = RestrictedElement + +
[docs] def createDefaultParser(self): + parser = _etree.XMLParser(**self.parser_config) + element_class = self.element_class + if self.element_class is not None: + lookup = _etree.ElementDefaultClassLookup(element=element_class) + parser.set_element_class_lookup(lookup) + return parser
+ +
[docs] def setDefaultParser(self, parser): + self._default_parser = parser
+ +
[docs] def getDefaultParser(self): + parser = getattr(self, "_default_parser", None) + if parser is None: + parser = self.createDefaultParser() + self.setDefaultParser(parser) + return parser
+ + +_parser_tls = GlobalParserTLS() +getDefaultParser = _parser_tls.getDefaultParser + + +
[docs]def check_docinfo(elementtree, forbid_dtd=False, forbid_entities=True): + """Check docinfo of an element tree for DTD and entity declarations + The check for entity declarations needs lxml 3 or newer. lxml 2.x does + not support dtd.iterentities(). + """ + docinfo = elementtree.docinfo + if docinfo.doctype: + if forbid_dtd: + raise DTDForbidden(docinfo.doctype, docinfo.system_url, docinfo.public_id) + if forbid_entities and not LXML3: + # lxml < 3 has no iterentities() + raise NotSupportedError("Unable to check for entity declarations " "in lxml 2.x") + + if forbid_entities: + for dtd in docinfo.internalDTD, docinfo.externalDTD: + if dtd is None: + continue + for entity in dtd.iterentities(): + raise EntitiesForbidden(entity.name, entity.content, None, None, None, None)
+ + +
[docs]def parse(source, parser=None, base_url=None, forbid_dtd=True, forbid_entities=True): + if parser is None: + parser = getDefaultParser() + elementtree = _etree.parse(source, parser, base_url=base_url) + check_docinfo(elementtree, forbid_dtd, forbid_entities) + return elementtree
+ + +
[docs]def fromstring(text, parser=None, base_url=None, forbid_dtd=True, forbid_entities=True): + if parser is None: + parser = getDefaultParser() + rootelement = _etree.fromstring(text, parser, base_url=base_url) + elementtree = rootelement.getroottree() + check_docinfo(elementtree, forbid_dtd, forbid_entities) + return rootelement
+ + +XML = fromstring + + +
[docs]def iterparse(*args, **kwargs): + raise NotSupportedError("iterparse not available")
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/saml2/_sources/index.rst.txt b/docs/saml2/_sources/index.rst.txt new file mode 100644 index 00000000..4057b939 --- /dev/null +++ b/docs/saml2/_sources/index.rst.txt @@ -0,0 +1,14 @@ +.. SAML Python Toolkit documentation master file, created by + sphinx-quickstart on Sun Oct 1 01:56:19 2023. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to SAML Python Toolkit's documentation! +=============================================== + +.. toctree:: + :maxdepth: 4 + :caption: Contents: + + onelogin + diff --git a/docs/saml2/_sources/modules.rst.txt b/docs/saml2/_sources/modules.rst.txt new file mode 100644 index 00000000..529aa5bd --- /dev/null +++ b/docs/saml2/_sources/modules.rst.txt @@ -0,0 +1,7 @@ +onelogin +======== + +.. toctree:: + :maxdepth: 4 + + onelogin diff --git a/docs/saml2/_sources/onelogin.rst.txt b/docs/saml2/_sources/onelogin.rst.txt new file mode 100644 index 00000000..87b2801a --- /dev/null +++ b/docs/saml2/_sources/onelogin.rst.txt @@ -0,0 +1,17 @@ +onelogin package +================ + +Subpackages +----------- + +.. toctree:: + + onelogin.saml2 + +Module contents +--------------- + +.. automodule:: onelogin + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/saml2/_sources/onelogin.saml2.rst.txt b/docs/saml2/_sources/onelogin.saml2.rst.txt new file mode 100644 index 00000000..0c0e62f8 --- /dev/null +++ b/docs/saml2/_sources/onelogin.saml2.rst.txt @@ -0,0 +1,110 @@ +onelogin.saml2 package +====================== + +Submodules +---------- + +onelogin.saml2.auth module +-------------------------- + +.. automodule:: onelogin.saml2.auth + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.authn\_request module +------------------------------------ + +.. automodule:: onelogin.saml2.authn_request + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.constants module +------------------------------- + +.. automodule:: onelogin.saml2.constants + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.errors module +---------------------------- + +.. automodule:: onelogin.saml2.errors + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.idp\_metadata\_parser module +------------------------------------------- + +.. automodule:: onelogin.saml2.idp_metadata_parser + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.logout\_request module +------------------------------------- + +.. automodule:: onelogin.saml2.logout_request + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.logout\_response module +-------------------------------------- + +.. automodule:: onelogin.saml2.logout_response + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.metadata module +------------------------------ + +.. automodule:: onelogin.saml2.metadata + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.response module +------------------------------ + +.. automodule:: onelogin.saml2.response + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.settings module +------------------------------ + +.. automodule:: onelogin.saml2.settings + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.utils module +--------------------------- + +.. automodule:: onelogin.saml2.utils + :members: + :undoc-members: + :show-inheritance: + +onelogin.saml2.xmlparser module +------------------------------- + +.. automodule:: onelogin.saml2.xmlparser + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: onelogin.saml2 + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/saml2/_static/ajax-loader.gif b/docs/saml2/_static/ajax-loader.gif new file mode 100644 index 00000000..61faf8ca Binary files /dev/null and b/docs/saml2/_static/ajax-loader.gif differ diff --git a/docs/saml2/_static/basic.css b/docs/saml2/_static/basic.css new file mode 100644 index 00000000..0807176e --- /dev/null +++ b/docs/saml2/_static/basic.css @@ -0,0 +1,676 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li div.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 450px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px 7px 0 7px; + background-color: #ffe; + width: 40%; + float: right; +} + +p.sidebar-title { + font-weight: bold; +} + +/* -- topics ---------------------------------------------------------------- */ + +div.topic { + border: 1px solid #ccc; + padding: 7px 7px 0 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +div.admonition dl { + margin-bottom: 0; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist td { + vertical-align: top; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +dl { + margin-bottom: 15px; +} + +dd p { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; +} + +td.linenos pre { + padding: 5px 0px; + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + margin-left: 0.5em; +} + +table.highlighttable td { + padding: 0 0.5em 0 0.5em; +} + +div.code-block-caption { + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +div.code-block-caption + div > div.highlight > pre { + margin-top: 0; +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + padding: 1em 1em 0; +} + +div.literal-block-wrapper div.highlight { + margin: 0; +} + +code.descname { + background-color: transparent; + font-weight: bold; + font-size: 1.2em; +} + +code.descclassname { + background-color: transparent; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: relative; + left: 0px; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/docs/saml2/_static/comment-bright.png b/docs/saml2/_static/comment-bright.png new file mode 100644 index 00000000..15e27edb Binary files /dev/null and b/docs/saml2/_static/comment-bright.png differ diff --git a/docs/saml2/_static/comment-close.png b/docs/saml2/_static/comment-close.png new file mode 100644 index 00000000..4d91bcf5 Binary files /dev/null and b/docs/saml2/_static/comment-close.png differ diff --git a/docs/saml2/_static/comment.png b/docs/saml2/_static/comment.png new file mode 100644 index 00000000..dfbc0cbd Binary files /dev/null and b/docs/saml2/_static/comment.png differ diff --git a/docs/saml2/_static/css/badge_only.css b/docs/saml2/_static/css/badge_only.css new file mode 100644 index 00000000..c718cee4 --- /dev/null +++ b/docs/saml2/_static/css/badge_only.css @@ -0,0 +1 @@ +.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} \ No newline at end of file diff --git a/docs/saml2/_static/css/fonts/Roboto-Slab-Bold.woff b/docs/saml2/_static/css/fonts/Roboto-Slab-Bold.woff new file mode 100644 index 00000000..6cb60000 Binary files /dev/null and b/docs/saml2/_static/css/fonts/Roboto-Slab-Bold.woff differ diff --git a/docs/saml2/_static/css/fonts/Roboto-Slab-Bold.woff2 b/docs/saml2/_static/css/fonts/Roboto-Slab-Bold.woff2 new file mode 100644 index 00000000..7059e231 Binary files /dev/null and b/docs/saml2/_static/css/fonts/Roboto-Slab-Bold.woff2 differ diff --git a/docs/saml2/_static/css/fonts/Roboto-Slab-Regular.woff b/docs/saml2/_static/css/fonts/Roboto-Slab-Regular.woff new file mode 100644 index 00000000..f815f63f Binary files /dev/null and b/docs/saml2/_static/css/fonts/Roboto-Slab-Regular.woff differ diff --git a/docs/saml2/_static/css/fonts/Roboto-Slab-Regular.woff2 b/docs/saml2/_static/css/fonts/Roboto-Slab-Regular.woff2 new file mode 100644 index 00000000..f2c76e5b Binary files /dev/null and b/docs/saml2/_static/css/fonts/Roboto-Slab-Regular.woff2 differ diff --git a/docs/saml2/_static/css/fonts/fontawesome-webfont.eot b/docs/saml2/_static/css/fonts/fontawesome-webfont.eot new file mode 100644 index 00000000..e9f60ca9 Binary files /dev/null and b/docs/saml2/_static/css/fonts/fontawesome-webfont.eot differ diff --git a/docs/saml2/_static/css/fonts/fontawesome-webfont.svg b/docs/saml2/_static/css/fonts/fontawesome-webfont.svg new file mode 100644 index 00000000..855c845e --- /dev/null +++ b/docs/saml2/_static/css/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/saml2/_static/css/fonts/fontawesome-webfont.ttf b/docs/saml2/_static/css/fonts/fontawesome-webfont.ttf new file mode 100644 index 00000000..35acda2f Binary files /dev/null and b/docs/saml2/_static/css/fonts/fontawesome-webfont.ttf differ diff --git a/docs/saml2/_static/css/fonts/fontawesome-webfont.woff b/docs/saml2/_static/css/fonts/fontawesome-webfont.woff new file mode 100644 index 00000000..400014a4 Binary files /dev/null and b/docs/saml2/_static/css/fonts/fontawesome-webfont.woff differ diff --git a/docs/saml2/_static/css/fonts/fontawesome-webfont.woff2 b/docs/saml2/_static/css/fonts/fontawesome-webfont.woff2 new file mode 100644 index 00000000..4d13fc60 Binary files /dev/null and b/docs/saml2/_static/css/fonts/fontawesome-webfont.woff2 differ diff --git a/docs/saml2/_static/css/fonts/lato-bold-italic.woff b/docs/saml2/_static/css/fonts/lato-bold-italic.woff new file mode 100644 index 00000000..88ad05b9 Binary files /dev/null and b/docs/saml2/_static/css/fonts/lato-bold-italic.woff differ diff --git a/docs/saml2/_static/css/fonts/lato-bold-italic.woff2 b/docs/saml2/_static/css/fonts/lato-bold-italic.woff2 new file mode 100644 index 00000000..c4e3d804 Binary files /dev/null and b/docs/saml2/_static/css/fonts/lato-bold-italic.woff2 differ diff --git a/docs/saml2/_static/css/fonts/lato-bold.woff b/docs/saml2/_static/css/fonts/lato-bold.woff new file mode 100644 index 00000000..c6dff51f Binary files /dev/null and b/docs/saml2/_static/css/fonts/lato-bold.woff differ diff --git a/docs/saml2/_static/css/fonts/lato-bold.woff2 b/docs/saml2/_static/css/fonts/lato-bold.woff2 new file mode 100644 index 00000000..bb195043 Binary files /dev/null and b/docs/saml2/_static/css/fonts/lato-bold.woff2 differ diff --git a/docs/saml2/_static/css/fonts/lato-normal-italic.woff b/docs/saml2/_static/css/fonts/lato-normal-italic.woff new file mode 100644 index 00000000..76114bc0 Binary files /dev/null and b/docs/saml2/_static/css/fonts/lato-normal-italic.woff differ diff --git a/docs/saml2/_static/css/fonts/lato-normal-italic.woff2 b/docs/saml2/_static/css/fonts/lato-normal-italic.woff2 new file mode 100644 index 00000000..3404f37e Binary files /dev/null and b/docs/saml2/_static/css/fonts/lato-normal-italic.woff2 differ diff --git a/docs/saml2/_static/css/fonts/lato-normal.woff b/docs/saml2/_static/css/fonts/lato-normal.woff new file mode 100644 index 00000000..ae1307ff Binary files /dev/null and b/docs/saml2/_static/css/fonts/lato-normal.woff differ diff --git a/docs/saml2/_static/css/fonts/lato-normal.woff2 b/docs/saml2/_static/css/fonts/lato-normal.woff2 new file mode 100644 index 00000000..3bf98433 Binary files /dev/null and b/docs/saml2/_static/css/fonts/lato-normal.woff2 differ diff --git a/docs/saml2/_static/css/theme.css b/docs/saml2/_static/css/theme.css new file mode 100644 index 00000000..19a446a0 --- /dev/null +++ b/docs/saml2/_static/css/theme.css @@ -0,0 +1,4 @@ +html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs>li{display:inline-block;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search>a:hover{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040;overflow-wrap:normal}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel,.rst-content .menuselection{font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .guilabel,.rst-content .menuselection{border:1px solid #7fbbe3;background:#e7f2fa}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} \ No newline at end of file diff --git a/docs/saml2/_static/doctools.js b/docs/saml2/_static/doctools.js new file mode 100644 index 00000000..344db17d --- /dev/null +++ b/docs/saml2/_static/doctools.js @@ -0,0 +1,315 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Sphinx JavaScript utilities for all documentation. + * + * :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/** + * select a different prefix for underscore + */ +$u = _.noConflict(); + +/** + * make the code below compatible with browsers without + * an installed firebug like debugger +if (!window.console || !console.firebug) { + var names = ["log", "debug", "info", "warn", "error", "assert", "dir", + "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", + "profile", "profileEnd"]; + window.console = {}; + for (var i = 0; i < names.length; ++i) + window.console[names[i]] = function() {}; +} + */ + +/** + * small helper function to urldecode strings + */ +jQuery.urldecode = function(x) { + return decodeURIComponent(x).replace(/\+/g, ' '); +}; + +/** + * small helper function to urlencode strings + */ +jQuery.urlencode = encodeURIComponent; + +/** + * This function returns the parsed url parameters of the + * current request. Multiple values per key are supported, + * it will always return arrays of strings for the value parts. + */ +jQuery.getQueryParameters = function(s) { + if (typeof s === 'undefined') + s = document.location.search; + var parts = s.substr(s.indexOf('?') + 1).split('&'); + var result = {}; + for (var i = 0; i < parts.length; i++) { + var tmp = parts[i].split('=', 2); + var key = jQuery.urldecode(tmp[0]); + var value = jQuery.urldecode(tmp[1]); + if (key in result) + result[key].push(value); + else + result[key] = [value]; + } + return result; +}; + +/** + * highlight a given string on a jquery object by wrapping it in + * span elements with the given class name. + */ +jQuery.fn.highlightText = function(text, className) { + function highlight(node, addItems) { + if (node.nodeType === 3) { + var val = node.nodeValue; + var pos = val.toLowerCase().indexOf(text); + if (pos >= 0 && + !jQuery(node.parentNode).hasClass(className) && + !jQuery(node.parentNode).hasClass("nohighlight")) { + var span; + var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.className = className; + } + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + node.parentNode.insertBefore(span, node.parentNode.insertBefore( + document.createTextNode(val.substr(pos + text.length)), + node.nextSibling)); + node.nodeValue = val.substr(0, pos); + if (isInSVG) { + var bbox = span.getBBox(); + var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute('class', className); + var parentOfText = node.parentNode.parentNode; + addItems.push({ + "parent": node.parentNode, + "target": rect}); + } + } + } + else if (!jQuery(node).is("button, select, textarea")) { + jQuery.each(node.childNodes, function() { + highlight(this, addItems); + }); + } + } + var addItems = []; + var result = this.each(function() { + highlight(this, addItems); + }); + for (var i = 0; i < addItems.length; ++i) { + jQuery(addItems[i].parent).before(addItems[i].target); + } + return result; +}; + +/* + * backward compatibility for jQuery.browser + * This will be supported until firefox bug is fixed. + */ +if (!jQuery.browser) { + jQuery.uaMatch = function(ua) { + ua = ua.toLowerCase(); + + var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || + /(webkit)[ \/]([\w.]+)/.exec(ua) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || + /(msie) ([\w.]+)/.exec(ua) || + ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || + []; + + return { + browser: match[ 1 ] || "", + version: match[ 2 ] || "0" + }; + }; + jQuery.browser = {}; + jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; +} + +/** + * Small JavaScript module for the documentation. + */ +var Documentation = { + + init : function() { + this.fixFirefoxAnchorBug(); + this.highlightSearchWords(); + this.initIndexTable(); + if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) { + this.initOnKeyListeners(); + } + }, + + /** + * i18n support + */ + TRANSLATIONS : {}, + PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; }, + LOCALE : 'unknown', + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext : function(string) { + var translated = Documentation.TRANSLATIONS[string]; + if (typeof translated === 'undefined') + return string; + return (typeof translated === 'string') ? translated : translated[0]; + }, + + ngettext : function(singular, plural, n) { + var translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated === 'undefined') + return (n == 1) ? singular : plural; + return translated[Documentation.PLURALEXPR(n)]; + }, + + addTranslations : function(catalog) { + for (var key in catalog.messages) + this.TRANSLATIONS[key] = catalog.messages[key]; + this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); + this.LOCALE = catalog.locale; + }, + + /** + * add context elements like header anchor links + */ + addContextElements : function() { + $('div[id] > :header:first').each(function() { + $('\u00B6'). + attr('href', '#' + this.id). + attr('title', _('Permalink to this headline')). + appendTo(this); + }); + $('dt[id]').each(function() { + $('\u00B6'). + attr('href', '#' + this.id). + attr('title', _('Permalink to this definition')). + appendTo(this); + }); + }, + + /** + * workaround a firefox stupidity + * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075 + */ + fixFirefoxAnchorBug : function() { + if (document.location.hash && $.browser.mozilla) + window.setTimeout(function() { + document.location.href += ''; + }, 10); + }, + + /** + * highlight the search words provided in the url in the text + */ + highlightSearchWords : function() { + var params = $.getQueryParameters(); + var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; + if (terms.length) { + var body = $('div.body'); + if (!body.length) { + body = $('body'); + } + window.setTimeout(function() { + $.each(terms, function() { + body.highlightText(this.toLowerCase(), 'highlighted'); + }); + }, 10); + $('') + .appendTo($('#searchbox')); + } + }, + + /** + * init the domain index toggle buttons + */ + initIndexTable : function() { + var togglers = $('img.toggler').click(function() { + var src = $(this).attr('src'); + var idnum = $(this).attr('id').substr(7); + $('tr.cg-' + idnum).toggle(); + if (src.substr(-9) === 'minus.png') + $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); + else + $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); + }).css('display', ''); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { + togglers.click(); + } + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords : function() { + $('#searchbox .highlight-link').fadeOut(300); + $('span.highlighted').removeClass('highlighted'); + }, + + /** + * make the url absolute + */ + makeURL : function(relativeURL) { + return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; + }, + + /** + * get the current relative url + */ + getCurrentURL : function() { + var path = document.location.pathname; + var parts = path.split(/\//); + $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { + if (this === '..') + parts.pop(); + }); + var url = parts.join('/'); + return path.substring(url.lastIndexOf('/') + 1, path.length - 1); + }, + + initOnKeyListeners: function() { + $(document).keyup(function(event) { + var activeElementType = document.activeElement.tagName; + // don't navigate when in search box or textarea + if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT') { + switch (event.keyCode) { + case 37: // left + var prevHref = $('link[rel="prev"]').prop('href'); + if (prevHref) { + window.location.href = prevHref; + return false; + } + case 39: // right + var nextHref = $('link[rel="next"]').prop('href'); + if (nextHref) { + window.location.href = nextHref; + return false; + } + } + } + }); + } +}; + +// quick alias for translations +_ = Documentation.gettext; + +$(document).ready(function() { + Documentation.init(); +}); diff --git a/docs/saml2/_static/documentation_options.js b/docs/saml2/_static/documentation_options.js new file mode 100644 index 00000000..f7bb3d0b --- /dev/null +++ b/docs/saml2/_static/documentation_options.js @@ -0,0 +1,10 @@ +var DOCUMENTATION_OPTIONS = { + URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), + VERSION: '1', + LANGUAGE: 'None', + COLLAPSE_INDEX: false, + FILE_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, +}; \ No newline at end of file diff --git a/docs/saml2/_static/down-pressed.png b/docs/saml2/_static/down-pressed.png new file mode 100644 index 00000000..5756c8ca Binary files /dev/null and b/docs/saml2/_static/down-pressed.png differ diff --git a/docs/saml2/_static/down.png b/docs/saml2/_static/down.png new file mode 100644 index 00000000..1b3bdad2 Binary files /dev/null and b/docs/saml2/_static/down.png differ diff --git a/docs/saml2/_static/file.png b/docs/saml2/_static/file.png new file mode 100644 index 00000000..a858a410 Binary files /dev/null and b/docs/saml2/_static/file.png differ diff --git a/docs/saml2/_static/jquery-3.2.1.js b/docs/saml2/_static/jquery-3.2.1.js new file mode 100644 index 00000000..d2d8ca47 --- /dev/null +++ b/docs/saml2/_static/jquery-3.2.1.js @@ -0,0 +1,10253 @@ +/*! + * jQuery JavaScript Library v3.2.1 + * https://jquery.com/ + * + * Includes Sizzle.js + * https://sizzlejs.com/ + * + * Copyright JS Foundation and other contributors + * Released under the MIT license + * https://jquery.org/license + * + * Date: 2017-03-20T18:59Z + */ +( function( global, factory ) { + + "use strict"; + + if ( typeof module === "object" && typeof module.exports === "object" ) { + + // For CommonJS and CommonJS-like environments where a proper `window` + // is present, execute the factory and get jQuery. + // For environments that do not have a `window` with a `document` + // (such as Node.js), expose a factory as module.exports. + // This accentuates the need for the creation of a real `window`. + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info. + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1 +// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode +// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common +// enough that all such attempts are guarded in a try block. +"use strict"; + +var arr = []; + +var document = window.document; + +var getProto = Object.getPrototypeOf; + +var slice = arr.slice; + +var concat = arr.concat; + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var fnToString = hasOwn.toString; + +var ObjectFunctionString = fnToString.call( Object ); + +var support = {}; + + + + function DOMEval( code, doc ) { + doc = doc || document; + + var script = doc.createElement( "script" ); + + script.text = code; + doc.head.appendChild( script ).parentNode.removeChild( script ); + } +/* global Symbol */ +// Defining this global in .eslintrc.json would create a danger of using the global +// unguarded in another place, it seems safer to define global only for this module + + + +var + version = "3.2.1", + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + + // The jQuery object is actually just the init constructor 'enhanced' + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); + }, + + // Support: Android <=4.0 only + // Make sure we trim BOM and NBSP + rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, + + // Matches dashed string for camelizing + rmsPrefix = /^-ms-/, + rdashAlpha = /-([a-z])/g, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return letter.toUpperCase(); + }; + +jQuery.fn = jQuery.prototype = { + + // The current version of jQuery being used + jquery: version, + + constructor: jQuery, + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + + // Return all the elements in a clean array + if ( num == null ) { + return slice.call( this ); + } + + // Return just the one element from the set + return num < 0 ? this[ num + this.length ] : this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + each: function( callback ) { + return jQuery.each( this, callback ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map( this, function( elem, i ) { + return callback.call( elem, i, elem ); + } ) ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: arr.sort, + splice: arr.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction( target ) ) { + target = {}; + } + + // Extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + // Only deal with non-null/undefined values + if ( ( options = arguments[ i ] ) != null ) { + + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = Array.isArray( copy ) ) ) ) { + + if ( copyIsArray ) { + copyIsArray = false; + clone = src && Array.isArray( src ) ? src : []; + + } else { + clone = src && jQuery.isPlainObject( src ) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend( { + + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + isFunction: function( obj ) { + return jQuery.type( obj ) === "function"; + }, + + isWindow: function( obj ) { + return obj != null && obj === obj.window; + }, + + isNumeric: function( obj ) { + + // As of jQuery 3.0, isNumeric is limited to + // strings and numbers (primitives or objects) + // that can be coerced to finite numbers (gh-2662) + var type = jQuery.type( obj ); + return ( type === "number" || type === "string" ) && + + // parseFloat NaNs numeric-cast false positives ("") + // ...but misinterprets leading-number strings, particularly hex literals ("0x...") + // subtraction forces infinities to NaN + !isNaN( obj - parseFloat( obj ) ); + }, + + isPlainObject: function( obj ) { + var proto, Ctor; + + // Detect obvious negatives + // Use toString instead of jQuery.type to catch host objects + if ( !obj || toString.call( obj ) !== "[object Object]" ) { + return false; + } + + proto = getProto( obj ); + + // Objects with no prototype (e.g., `Object.create( null )`) are plain + if ( !proto ) { + return true; + } + + // Objects with prototype are plain iff they were constructed by a global Object function + Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; + return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; + }, + + isEmptyObject: function( obj ) { + + /* eslint-disable no-unused-vars */ + // See https://github.com/eslint/eslint/issues/6125 + var name; + + for ( name in obj ) { + return false; + } + return true; + }, + + type: function( obj ) { + if ( obj == null ) { + return obj + ""; + } + + // Support: Android <=2.3 only (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; + }, + + // Evaluates a script in a global context + globalEval: function( code ) { + DOMEval( code ); + }, + + // Convert dashed to camelCase; used by the css and data modules + // Support: IE <=9 - 11, Edge 12 - 13 + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + each: function( obj, callback ) { + var length, i = 0; + + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } else { + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } + + return obj; + }, + + // Support: Android <=4.0 only + trim: function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArrayLike( Object( arr ) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + return arr == null ? -1 : indexOf.call( arr, elem, i ); + }, + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var length, value, + i = 0, + ret = []; + + // Go through the array, translating each of the items to their new values + if ( isArrayLike( elems ) ) { + length = elems.length; + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + // Flatten any nested arrays + return concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + var tmp, args, proxy; + + if ( typeof context === "string" ) { + tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + args = slice.call( arguments, 2 ); + proxy = function() { + return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || jQuery.guid++; + + return proxy; + }, + + now: Date.now, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +} ); + +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} + +// Populate the class2type map +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), +function( i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +} ); + +function isArrayLike( obj ) { + + // Support: real iOS 8.2 only (not reproducible in simulator) + // `in` check used to prevent JIT error (gh-2145) + // hasOwn isn't used here due to false negatives + // regarding Nodelist length in IE + var length = !!obj && "length" in obj && obj.length, + type = jQuery.type( obj ); + + if ( type === "function" || jQuery.isWindow( obj ) ) { + return false; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} +var Sizzle = +/*! + * Sizzle CSS Selector Engine v2.3.3 + * https://sizzlejs.com/ + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2016-08-08 + */ +(function( window ) { + +var i, + support, + Expr, + getText, + isXML, + tokenize, + compile, + select, + outermostContext, + sortInput, + hasDuplicate, + + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + + // Instance-specific data + expando = "sizzle" + 1 * new Date(), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + // Instance methods + hasOwn = ({}).hasOwnProperty, + arr = [], + pop = arr.pop, + push_native = arr.push, + push = arr.push, + slice = arr.slice, + // Use a stripped-down indexOf as it's faster than native + // https://jsperf.com/thor-indexof-vs-for/5 + indexOf = function( list, elem ) { + var i = 0, + len = list.length; + for ( ; i < len; i++ ) { + if ( list[i] === elem ) { + return i; + } + } + return -1; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + + // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = "(?:\\\\.|[\\w-]|[^\0-\\xa0])+", + + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + + "*\\]", + + pseudos = ":(" + identifier + ")(?:\\((" + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rwhitespace = new RegExp( whitespace + "+", "g" ), + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), + + rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + identifier + ")" ), + "CLASS": new RegExp( "^\\.(" + identifier + ")" ), + "TAG": new RegExp( "^(" + identifier + "|[*])" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rnative = /^[^{]+\{\s*\[native \w/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + + // CSS escapes + // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), + funescape = function( _, escaped, escapedWhitespace ) { + var high = "0x" + escaped - 0x10000; + // NaN means non-codepoint + // Support: Firefox<24 + // Workaround erroneous numeric interpretation of +"0x" + return high !== high || escapedWhitespace ? + escaped : + high < 0 ? + // BMP codepoint + String.fromCharCode( high + 0x10000 ) : + // Supplemental Plane codepoint (surrogate pair) + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + // CSS string/identifier serialization + // https://drafts.csswg.org/cssom/#common-serializing-idioms + rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g, + fcssescape = function( ch, asCodePoint ) { + if ( asCodePoint ) { + + // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER + if ( ch === "\0" ) { + return "\uFFFD"; + } + + // Control characters and (dependent upon position) numbers get escaped as code points + return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; + } + + // Other potentially-special ASCII characters get backslash-escaped + return "\\" + ch; + }, + + // Used for iframes + // See setDocument() + // Removing the function wrapper causes a "Permission Denied" + // error in IE + unloadHandler = function() { + setDocument(); + }, + + disabledAncestor = addCombinator( + function( elem ) { + return elem.disabled === true && ("form" in elem || "label" in elem); + }, + { dir: "parentNode", next: "legend" } + ); + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + (arr = slice.call( preferredDoc.childNodes )), + preferredDoc.childNodes + ); + // Support: Android<4.0 + // Detect silently failing push.apply + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { apply: arr.length ? + + // Leverage slice if possible + function( target, els ) { + push_native.apply( target, slice.call(els) ); + } : + + // Support: IE<9 + // Otherwise append directly + function( target, els ) { + var j = target.length, + i = 0; + // Can't trust NodeList.length + while ( (target[j++] = els[i++]) ) {} + target.length = j - 1; + } + }; +} + +function Sizzle( selector, context, results, seed ) { + var m, i, elem, nid, match, groups, newSelector, + newContext = context && context.ownerDocument, + + // nodeType defaults to 9, since context defaults to document + nodeType = context ? context.nodeType : 9; + + results = results || []; + + // Return early from calls with invalid selector or context + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { + + return results; + } + + // Try to shortcut find operations (as opposed to filters) in HTML documents + if ( !seed ) { + + if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { + setDocument( context ); + } + context = context || document; + + if ( documentIsHTML ) { + + // If the selector is sufficiently simple, try using a "get*By*" DOM method + // (excepting DocumentFragment context, where the methods don't exist) + if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) { + + // ID selector + if ( (m = match[1]) ) { + + // Document context + if ( nodeType === 9 ) { + if ( (elem = context.getElementById( m )) ) { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + + // Element context + } else { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( newContext && (elem = newContext.getElementById( m )) && + contains( context, elem ) && + elem.id === m ) { + + results.push( elem ); + return results; + } + } + + // Type selector + } else if ( match[2] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Class selector + } else if ( (m = match[3]) && support.getElementsByClassName && + context.getElementsByClassName ) { + + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // Take advantage of querySelectorAll + if ( support.qsa && + !compilerCache[ selector + " " ] && + (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { + + if ( nodeType !== 1 ) { + newContext = context; + newSelector = selector; + + // qSA looks outside Element context, which is not what we want + // Thanks to Andrew Dupont for this workaround technique + // Support: IE <=8 + // Exclude object elements + } else if ( context.nodeName.toLowerCase() !== "object" ) { + + // Capture the context ID, setting it first if necessary + if ( (nid = context.getAttribute( "id" )) ) { + nid = nid.replace( rcssescape, fcssescape ); + } else { + context.setAttribute( "id", (nid = expando) ); + } + + // Prefix every selector in the list + groups = tokenize( selector ); + i = groups.length; + while ( i-- ) { + groups[i] = "#" + nid + " " + toSelector( groups[i] ); + } + newSelector = groups.join( "," ); + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + } + + if ( newSelector ) { + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {function(string, object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key + " " ) > Expr.cacheLength ) { + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return (cache[ key + " " ] = value); + } + return cache; +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created element and returns a boolean result + */ +function assert( fn ) { + var el = document.createElement("fieldset"); + + try { + return !!fn( el ); + } catch (e) { + return false; + } finally { + // Remove from its parent by default + if ( el.parentNode ) { + el.parentNode.removeChild( el ); + } + // release memory in IE + el = null; + } +} + +/** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ +function addHandle( attrs, handler ) { + var arr = attrs.split("|"), + i = arr.length; + + while ( i-- ) { + Expr.attrHandle[ arr[i] ] = handler; + } +} + +/** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + a.sourceIndex - b.sourceIndex; + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( (cur = cur.nextSibling) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for :enabled/:disabled + * @param {Boolean} disabled true for :disabled; false for :enabled + */ +function createDisabledPseudo( disabled ) { + + // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable + return function( elem ) { + + // Only certain elements can match :enabled or :disabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled + if ( "form" in elem ) { + + // Check for inherited disabledness on relevant non-disabled elements: + // * listed form-associated elements in a disabled fieldset + // https://html.spec.whatwg.org/multipage/forms.html#category-listed + // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled + // * option elements in a disabled optgroup + // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled + // All such elements have a "form" property. + if ( elem.parentNode && elem.disabled === false ) { + + // Option elements defer to a parent optgroup if present + if ( "label" in elem ) { + if ( "label" in elem.parentNode ) { + return elem.parentNode.disabled === disabled; + } else { + return elem.disabled === disabled; + } + } + + // Support: IE 6 - 11 + // Use the isDisabled shortcut property to check for disabled fieldset ancestors + return elem.isDisabled === disabled || + + // Where there is no isDisabled, check manually + /* jshint -W018 */ + elem.isDisabled !== !disabled && + disabledAncestor( elem ) === disabled; + } + + return elem.disabled === disabled; + + // Try to winnow out elements that can't be disabled before trusting the disabled property. + // Some victims get caught in our net (label, legend, menu, track), but it shouldn't + // even exist on them, let alone have a boolean value. + } else if ( "label" in elem ) { + return elem.disabled === disabled; + } + + // Remaining elements are neither :enabled nor :disabled + return false; + }; +} + +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction(function( argument ) { + argument = +argument; + return markFunction(function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ (j = matchIndexes[i]) ] ) { + seed[j] = !(matches[j] = seed[j]); + } + } + }); + }); +} + +/** + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Detects XML nodes + * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node + */ +isXML = Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = elem && (elem.ownerDocument || elem).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var hasCompare, subWindow, + doc = node ? node.ownerDocument || node : preferredDoc; + + // Return early if doc is invalid or already selected + if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Update global variables + document = doc; + docElem = document.documentElement; + documentIsHTML = !isXML( document ); + + // Support: IE 9-11, Edge + // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) + if ( preferredDoc !== document && + (subWindow = document.defaultView) && subWindow.top !== subWindow ) { + + // Support: IE 11, Edge + if ( subWindow.addEventListener ) { + subWindow.addEventListener( "unload", unloadHandler, false ); + + // Support: IE 9 - 10 only + } else if ( subWindow.attachEvent ) { + subWindow.attachEvent( "onunload", unloadHandler ); + } + } + + /* Attributes + ---------------------------------------------------------------------- */ + + // Support: IE<8 + // Verify that getAttribute really returns attributes and not properties + // (excepting IE8 booleans) + support.attributes = assert(function( el ) { + el.className = "i"; + return !el.getAttribute("className"); + }); + + /* getElement(s)By* + ---------------------------------------------------------------------- */ + + // Check if getElementsByTagName("*") returns only elements + support.getElementsByTagName = assert(function( el ) { + el.appendChild( document.createComment("") ); + return !el.getElementsByTagName("*").length; + }); + + // Support: IE<9 + support.getElementsByClassName = rnative.test( document.getElementsByClassName ); + + // Support: IE<10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programmatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert(function( el ) { + docElem.appendChild( el ).id = expando; + return !document.getElementsByName || !document.getElementsByName( expando ).length; + }); + + // ID filter and find + if ( support.getById ) { + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute("id") === attrId; + }; + }; + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var elem = context.getElementById( id ); + return elem ? [ elem ] : []; + } + }; + } else { + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== "undefined" && + elem.getAttributeNode("id"); + return node && node.value === attrId; + }; + }; + + // Support: IE 6 - 7 only + // getElementById is not reliable as a find shortcut + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var node, i, elems, + elem = context.getElementById( id ); + + if ( elem ) { + + // Verify the id attribute + node = elem.getAttributeNode("id"); + if ( node && node.value === id ) { + return [ elem ]; + } + + // Fall back on getElementsByName + elems = context.getElementsByName( id ); + i = 0; + while ( (elem = elems[i++]) ) { + node = elem.getAttributeNode("id"); + if ( node && node.value === id ) { + return [ elem ]; + } + } + } + + return []; + } + }; + } + + // Tag + Expr.find["TAG"] = support.getElementsByTagName ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( tag ); + + // DocumentFragment nodes don't have gEBTN + } else if ( support.qsa ) { + return context.querySelectorAll( tag ); + } + } : + + function( tag, context ) { + var elem, + tmp = [], + i = 0, + // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( (elem = results[i++]) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Class + Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21) + // We allow this because of a bug in IE8/9 that throws an error + // whenever `document.activeElement` is accessed on an iframe + // So, we allow :focus to pass through QSA all the time to avoid the IE error + // See https://bugs.jquery.com/ticket/13378 + rbuggyQSA = []; + + if ( (support.qsa = rnative.test( document.querySelectorAll )) ) { + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert(function( el ) { + // Select is set to empty string on purpose + // This is to test IE's treatment of not explicitly + // setting a boolean content attribute, + // since its presence should be enough + // https://bugs.jquery.com/ticket/12359 + docElem.appendChild( el ).innerHTML = "" + + ""; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( el.querySelectorAll("[msallowcapture^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } + + // Support: IE8 + // Boolean attributes and "value" are not treated correctly + if ( !el.querySelectorAll("[selected]").length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ + if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) { + rbuggyQSA.push("~="); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !el.querySelectorAll(":checked").length ) { + rbuggyQSA.push(":checked"); + } + + // Support: Safari 8+, iOS 8+ + // https://bugs.webkit.org/show_bug.cgi?id=136851 + // In-page `selector#id sibling-combinator selector` fails + if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) { + rbuggyQSA.push(".#.+[+~]"); + } + }); + + assert(function( el ) { + el.innerHTML = "" + + ""; + + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + var input = document.createElement("input"); + input.setAttribute( "type", "hidden" ); + el.appendChild( input ).setAttribute( "name", "D" ); + + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( el.querySelectorAll("[name=d]").length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( el.querySelectorAll(":enabled").length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Support: IE9-11+ + // IE's :disabled selector does not pick up the children of disabled fieldsets + docElem.appendChild( el ).disabled = true; + if ( el.querySelectorAll(":disabled").length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Opera 10-11 does not throw on post-comma invalid pseudos + el.querySelectorAll("*,:x"); + rbuggyQSA.push(",.*:"); + }); + } + + if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || + docElem.webkitMatchesSelector || + docElem.mozMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector) )) ) { + + assert(function( el ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( el, "*" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( el, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + }); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); + rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); + + /* Contains + ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); + + // Element contains another + // Purposefully self-exclusive + // As in, an element does not contain itself + contains = hasCompare || rnative.test( docElem.contains ) ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + )); + } : + function( a, b ) { + if ( b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + /* Sorting + ---------------------------------------------------------------------- */ + + // Document order sorting + sortOrder = hasCompare ? + function( a, b ) { + + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + // Calculate position if both inputs belong to the same document + compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 || + (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { + + // Choose the first element that is related to our preferred document + if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { + return -1; + } + if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + } : + function( a, b ) { + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Parentless nodes are either documents or disconnected + if ( !aup || !bup ) { + return a === document ? -1 : + b === document ? 1 : + aup ? -1 : + bup ? 1 : + sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( (cur = cur.parentNode) ) { + ap.unshift( cur ); + } + cur = b; + while ( (cur = cur.parentNode) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[i] === bp[i] ) { + i++; + } + + return i ? + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[i], bp[i] ) : + + // Otherwise nodes in our document sort first + ap[i] === preferredDoc ? -1 : + bp[i] === preferredDoc ? 1 : + 0; + }; + + return document; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + // Make sure that attribute selectors are quoted + expr = expr.replace( rattributeQuotes, "='$1']" ); + + if ( support.matchesSelector && documentIsHTML && + !compilerCache[ expr + " " ] && + ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch (e) {} + } + + return Sizzle( expr, document, null, [ elem ] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + // Set document vars if needed + if ( ( context.ownerDocument || context ) !== document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + // Don't get fooled by Object.prototype properties (jQuery #13807) + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + return val !== undefined ? + val : + support.attributes || !documentIsHTML ? + elem.getAttribute( name ) : + (val = elem.getAttributeNode(name)) && val.specified ? + val.value : + null; +}; + +Sizzle.escape = function( sel ) { + return (sel + "").replace( rcssescape, fcssescape ); +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + sortInput = !support.sortStable && results.slice( 0 ); + results.sort( sortOrder ); + + if ( hasDuplicate ) { + while ( (elem = results[i++]) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + + return results; +}; + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + // If no nodeType, this is expected to be an array + while ( (node = elem[i++]) ) { + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent for elements + // innerText usage removed for consistency of new lines (jQuery #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[1] = match[1].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); + + if ( match[2] === "~=" ) { + match[3] = " " + match[3] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[1] = match[1].toLowerCase(); + + if ( match[1].slice( 0, 3 ) === "nth" ) { + // nth-* requires argument + if ( !match[3] ) { + Sizzle.error( match[0] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); + match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); + + // other types prohibit arguments + } else if ( match[3] ) { + Sizzle.error( match[0] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[6] && match[2]; + + if ( matchExpr["CHILD"].test( match[0] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[3] ) { + match[2] = match[4] || match[5] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + // Get excess from tokenize (recursively) + (excess = tokenize( unquoted, true )) && + // advance to the next closing parenthesis + (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { + + // excess is a negative index + match[0] = match[0].slice( 0, excess ); + match[2] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeNameSelector ) { + var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { return true; } : + function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && + classCache( className, function( elem ) { + return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" ); + }); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + }; + }, + + "CHILD": function( type, what, argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, context, xml ) { + var cache, uniqueCache, outerCache, node, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType, + diff = false; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( (node = node[ dir ]) ) { + if ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) { + + return false; + } + } + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + + // Seek `elem` from a previously-cached index + + // ...in a gzip-friendly way + node = parent; + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex && cache[ 2 ]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( (node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + (diff = nodeIndex = 0) || start.pop()) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + uniqueCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + } else { + // Use previously-cached element index if available + if ( useCache ) { + // ...in a gzip-friendly way + node = elem; + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex; + } + + // xml :nth-child(...) + // or :nth-last-child(...) or :nth(-last)?-of-type(...) + if ( diff === false ) { + // Use the same loop as above to seek `elem` from the start + while ( (node = ++nodeIndex && node && node[ dir ] || + (diff = nodeIndex = 0) || start.pop()) ) { + + if ( ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) && + ++diff ) { + + // Cache the index of each encountered element + if ( useCache ) { + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + uniqueCache[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction(function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf( seed, matched[i] ); + seed[ idx ] = !( matches[ idx ] = matched[i] ); + } + }) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + // Potentially complex pseudos + "not": markFunction(function( selector ) { + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction(function( seed, matches, context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( (elem = unmatched[i]) ) { + seed[i] = !(matches[i] = elem); + } + } + }) : + function( elem, context, xml ) { + input[0] = elem; + matcher( input, null, xml, results ); + // Don't keep the element (issue #299) + input[0] = null; + return !results.pop(); + }; + }), + + "has": markFunction(function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + }), + + "contains": markFunction(function( text ) { + text = text.replace( runescape, funescape ); + return function( elem ) { + return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; + }; + }), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + // lang value must be a valid identifier + if ( !ridentifier.test(lang || "") ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( (elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); + return false; + }; + }), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); + }, + + // Boolean properties + "enabled": createDisabledPseudo( false ), + "disabled": createDisabledPseudo( true ), + + "checked": function( elem ) { + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); + }, + + "selected": function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos["empty"]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); + }, + + // Position-in-collection + "first": createPositionalPseudo(function() { + return [ 0 ]; + }), + + "last": createPositionalPseudo(function( matchIndexes, length ) { + return [ length - 1 ]; + }), + + "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + }), + + "even": createPositionalPseudo(function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "odd": createPositionalPseudo(function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }) + } +}; + +Expr.pseudos["nth"] = Expr.pseudos["eq"]; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +// Easy API for creating new setFilters +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +tokenize = Sizzle.tokenize = function( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || (match = rcomma.exec( soFar )) ) { + if ( match ) { + // Don't consume trailing commas as valid + soFar = soFar.slice( match[0].length ) || soFar; + } + groups.push( (tokens = []) ); + } + + matched = false; + + // Combinators + if ( (match = rcombinators.exec( soFar )) ) { + matched = match.shift(); + tokens.push({ + value: matched, + // Cast descendant combinators to space + type: match[0].replace( rtrim, " " ) + }); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || + (match = preFilters[ type ]( match ))) ) { + matched = match.shift(); + tokens.push({ + value: matched, + type: type, + matches: match + }); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +}; + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[i].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + skip = combinator.next, + key = skip || dir, + checkNonElements = base && key === "parentNode", + doneName = done++; + + return combinator.first ? + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + return false; + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var oldCache, uniqueCache, outerCache, + newCache = [ dirruns, doneName ]; + + // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching + if ( xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || (elem[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {}); + + if ( skip && skip === elem.nodeName.toLowerCase() ) { + elem = elem[ dir ] || elem; + } else if ( (oldCache = uniqueCache[ key ]) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return (newCache[ 2 ] = oldCache[ 2 ]); + } else { + // Reuse newcache so results back-propagate to previous elements + uniqueCache[ key ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { + return true; + } + } + } + } + } + return false; + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[i]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[0]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( (elem = unmatched[i]) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction(function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( (elem = temp[i]) ) { + matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) ) { + // Restore matcherIn since elem is not yet a final match + temp.push( (matcherIn[i] = elem) ); + } + } + postFinder( null, (matcherOut = []), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) && + (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) { + + seed[temp] = !(results[temp] = elem); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + }); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[0].type ], + implicitRelative = leadingRelative || Expr.relative[" "], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + (checkContext = context).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + // Avoid hanging onto element (issue #299) + checkContext = null; + return ret; + } ]; + + for ( ; i < len; i++ ) { + if ( (matcher = Expr.relative[ tokens[i].type ]) ) { + matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; + } else { + matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[j].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) + ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), + len = elems.length; + + if ( outermost ) { + outermostContext = context === document || context || outermost; + } + + // Add elements passing elementMatchers directly to results + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id + for ( ; i !== len && (elem = elems[i]) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + if ( !context && elem.ownerDocument !== document ) { + setDocument( elem ); + xml = !documentIsHTML; + } + while ( (matcher = elementMatchers[j++]) ) { + if ( matcher( elem, context || document, xml) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + // They will have gone through all possible matchers + if ( (elem = !matcher && elem) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // `i` is now the count of elements visited above, and adding it to `matchedCount` + // makes the latter nonnegative. + matchedCount += i; + + // Apply set filters to unmatched elements + // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` + // equals `i`), unless we didn't visit _any_ elements in the above loop because we have + // no element matchers and no seed. + // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that + // case, which will result in a "00" `matchedCount` that differs from `i` but is also + // numerically zero. + if ( bySet && i !== matchedCount ) { + j = 0; + while ( (matcher = setMatchers[j++]) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !(unmatched[i] || setMatched[i]) ) { + setMatched[i] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + // Generate a function of recursive functions that can be used to check each element + if ( !match ) { + match = tokenize( selector ); + } + i = match.length; + while ( i-- ) { + cached = matcherFromTokens( match[i] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + + // Save selector and tokenization + cached.selector = selector; + } + return cached; +}; + +/** + * A low-level selection function that works with Sizzle's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with Sizzle.compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +select = Sizzle.select = function( selector, context, results, seed ) { + var i, tokens, token, type, find, + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( (selector = compiled.selector || selector) ); + + results = results || []; + + // Try to minimize operations if there is only one selector in the list and no seed + // (the latter of which guarantees us context) + if ( match.length === 1 ) { + + // Reduce context if the leading compound selector is an ID + tokens = match[0] = match[0].slice( 0 ); + if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && + context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[1].type ] ) { + + context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[i]; + + // Abort if we hit a combinator + if ( Expr.relative[ (type = token.type) ] ) { + break; + } + if ( (find = Expr.find[ type ]) ) { + // Search, expanding context for leading sibling combinators + if ( (seed = find( + token.matches[0].replace( runescape, funescape ), + rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context + )) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + + // Compile and execute a filtering function if one is not provided + // Provide `match` to avoid retokenization if we modified the selector above + ( compiled || compile( selector, match ) )( + seed, + context, + !documentIsHTML, + results, + !context || rsibling.test( selector ) && testContext( context.parentNode ) || context + ); + return results; +}; + +// One-time assignments + +// Sort stability +support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; + +// Support: Chrome 14-35+ +// Always assume duplicates if they aren't passed to the comparison function +support.detectDuplicates = !!hasDuplicate; + +// Initialize against the default document +setDocument(); + +// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) +// Detached nodes confoundingly follow *each other* +support.sortDetached = assert(function( el ) { + // Should return 1, but returns 4 (following) + return el.compareDocumentPosition( document.createElement("fieldset") ) & 1; +}); + +// Support: IE<8 +// Prevent attribute/property "interpolation" +// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !assert(function( el ) { + el.innerHTML = ""; + return el.firstChild.getAttribute("href") === "#" ; +}) ) { + addHandle( "type|href|height|width", function( elem, name, isXML ) { + if ( !isXML ) { + return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); + } + }); +} + +// Support: IE<9 +// Use defaultValue in place of getAttribute("value") +if ( !support.attributes || !assert(function( el ) { + el.innerHTML = ""; + el.firstChild.setAttribute( "value", "" ); + return el.firstChild.getAttribute( "value" ) === ""; +}) ) { + addHandle( "value", function( elem, name, isXML ) { + if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { + return elem.defaultValue; + } + }); +} + +// Support: IE<9 +// Use getAttributeNode to fetch booleans when getAttribute lies +if ( !assert(function( el ) { + return el.getAttribute("disabled") == null; +}) ) { + addHandle( booleans, function( elem, name, isXML ) { + var val; + if ( !isXML ) { + return elem[ name ] === true ? name.toLowerCase() : + (val = elem.getAttributeNode( name )) && val.specified ? + val.value : + null; + } + }); +} + +return Sizzle; + +})( window ); + + + +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; + +// Deprecated +jQuery.expr[ ":" ] = jQuery.expr.pseudos; +jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; +jQuery.escapeSelector = Sizzle.escape; + + + + +var dir = function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; +}; + + +var siblings = function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; +}; + + +var rneedsContext = jQuery.expr.match.needsContext; + + + +function nodeName( elem, name ) { + + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + +}; +var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i ); + + + +var risSimple = /^.[^:#\[\.,]*$/; + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + return !!qualifier.call( elem, i, elem ) !== not; + } ); + } + + // Single element + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + } ); + } + + // Arraylike of elements (jQuery, arguments, Array) + if ( typeof qualifier !== "string" ) { + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) > -1 ) !== not; + } ); + } + + // Simple selector that can be filtered directly, removing non-Elements + if ( risSimple.test( qualifier ) ) { + return jQuery.filter( qualifier, elements, not ); + } + + // Complex selector, compare the two sets, removing non-Elements + qualifier = jQuery.filter( qualifier, elements ); + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) > -1 ) !== not && elem.nodeType === 1; + } ); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + if ( elems.length === 1 && elem.nodeType === 1 ) { + return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : []; + } + + return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + } ) ); +}; + +jQuery.fn.extend( { + find: function( selector ) { + var i, ret, + len = this.length, + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter( function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + } ) ); + } + + ret = this.pushStack( [] ); + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + return len > 1 ? jQuery.uniqueSort( ret ) : ret; + }, + filter: function( selector ) { + return this.pushStack( winnow( this, selector || [], false ) ); + }, + not: function( selector ) { + return this.pushStack( winnow( this, selector || [], true ) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +} ); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + // Shortcut simple #id case for speed + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/, + + init = jQuery.fn.init = function( selector, context, root ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Method init() accepts an alternate rootjQuery + // so migrate can support jQuery.sub (gh-2101) + root = root || rootjQuery; + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector[ 0 ] === "<" && + selector[ selector.length - 1 ] === ">" && + selector.length >= 3 ) { + + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && ( match[ 1 ] || !context ) ) { + + // HANDLE: $(html) -> $(array) + if ( match[ 1 ] ) { + context = context instanceof jQuery ? context[ 0 ] : context; + + // Option to run scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[ 1 ], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + + // Properties of context are called as methods if possible + if ( jQuery.isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[ 2 ] ); + + if ( elem ) { + + // Inject the element directly into the jQuery object + this[ 0 ] = elem; + this.length = 1; + } + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || root ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this[ 0 ] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return root.ready !== undefined ? + root.ready( selector ) : + + // Execute immediately if ready is not present + selector( jQuery ); + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + + // Methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend( { + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter( function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[ i ] ) ) { + return true; + } + } + } ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + targets = typeof selectors !== "string" && jQuery( selectors ); + + // Positional selectors never match, since there's no _selection_ context + if ( !rneedsContext.test( selectors ) ) { + for ( ; i < l; i++ ) { + for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { + + // Always skip document fragments + if ( cur.nodeType < 11 && ( targets ? + targets.index( cur ) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector( cur, selectors ) ) ) { + + matched.push( cur ); + break; + } + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); + }, + + // Determine the position of an element within the set + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + // Index in selector + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + // Locate the position of the desired element + return indexOf.call( this, + + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.uniqueSort( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + } +} ); + +function sibling( cur, dir ) { + while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each( { + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return siblings( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return siblings( elem.firstChild ); + }, + contents: function( elem ) { + if ( nodeName( elem, "iframe" ) ) { + return elem.contentDocument; + } + + // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only + // Treat the template element as a regular one in browsers that + // don't support it. + if ( nodeName( elem, "template" ) ) { + elem = elem.content || elem; + } + + return jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + jQuery.uniqueSort( matched ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +} ); +var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g ); + + + +// Convert String-formatted options into Object-formatted ones +function createOptions( options ) { + var object = {}; + jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) { + object[ flag ] = true; + } ); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + createOptions( options ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + + // Last fire value for non-forgettable lists + memory, + + // Flag to know if list was already fired + fired, + + // Flag to prevent firing + locked, + + // Actual callback list + list = [], + + // Queue of execution data for repeatable lists + queue = [], + + // Index of currently firing callback (modified by add/remove as needed) + firingIndex = -1, + + // Fire callbacks + fire = function() { + + // Enforce single-firing + locked = locked || options.once; + + // Execute callbacks for all pending executions, + // respecting firingIndex overrides and runtime changes + fired = firing = true; + for ( ; queue.length; firingIndex = -1 ) { + memory = queue.shift(); + while ( ++firingIndex < list.length ) { + + // Run callback and check for early termination + if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && + options.stopOnFalse ) { + + // Jump to end and forget the data so .add doesn't re-fire + firingIndex = list.length; + memory = false; + } + } + } + + // Forget the data if we're done with it + if ( !options.memory ) { + memory = false; + } + + firing = false; + + // Clean up if we're done firing for good + if ( locked ) { + + // Keep an empty list if we have data for future add calls + if ( memory ) { + list = []; + + // Otherwise, this object is spent + } else { + list = ""; + } + } + }, + + // Actual Callbacks object + self = { + + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + + // If we have memory from a past run, we should fire after adding + if ( memory && !firing ) { + firingIndex = list.length - 1; + queue.push( memory ); + } + + ( function add( args ) { + jQuery.each( args, function( _, arg ) { + if ( jQuery.isFunction( arg ) ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) { + + // Inspect recursively + add( arg ); + } + } ); + } )( arguments ); + + if ( memory && !firing ) { + fire(); + } + } + return this; + }, + + // Remove a callback from the list + remove: function() { + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + + // Handle firing indexes + if ( index <= firingIndex ) { + firingIndex--; + } + } + } ); + return this; + }, + + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? + jQuery.inArray( fn, list ) > -1 : + list.length > 0; + }, + + // Remove all callbacks from the list + empty: function() { + if ( list ) { + list = []; + } + return this; + }, + + // Disable .fire and .add + // Abort any current/pending executions + // Clear all callbacks and values + disable: function() { + locked = queue = []; + list = memory = ""; + return this; + }, + disabled: function() { + return !list; + }, + + // Disable .fire + // Also disable .add unless we have memory (since it would have no effect) + // Abort any pending executions + lock: function() { + locked = queue = []; + if ( !memory && !firing ) { + list = memory = ""; + } + return this; + }, + locked: function() { + return !!locked; + }, + + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( !locked ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + queue.push( args ); + if ( !firing ) { + fire(); + } + } + return this; + }, + + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + +function Identity( v ) { + return v; +} +function Thrower( ex ) { + throw ex; +} + +function adoptValue( value, resolve, reject, noValue ) { + var method; + + try { + + // Check for promise aspect first to privilege synchronous behavior + if ( value && jQuery.isFunction( ( method = value.promise ) ) ) { + method.call( value ).done( resolve ).fail( reject ); + + // Other thenables + } else if ( value && jQuery.isFunction( ( method = value.then ) ) ) { + method.call( value, resolve, reject ); + + // Other non-thenables + } else { + + // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer: + // * false: [ value ].slice( 0 ) => resolve( value ) + // * true: [ value ].slice( 1 ) => resolve() + resolve.apply( undefined, [ value ].slice( noValue ) ); + } + + // For Promises/A+, convert exceptions into rejections + // Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in + // Deferred#then to conditionally suppress rejection. + } catch ( value ) { + + // Support: Android 4.0 only + // Strict mode functions invoked without .call/.apply get global-object context + reject.apply( undefined, [ value ] ); + } +} + +jQuery.extend( { + + Deferred: function( func ) { + var tuples = [ + + // action, add listener, callbacks, + // ... .then handlers, argument index, [final state] + [ "notify", "progress", jQuery.Callbacks( "memory" ), + jQuery.Callbacks( "memory" ), 2 ], + [ "resolve", "done", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 0, "resolved" ], + [ "reject", "fail", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 1, "rejected" ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + "catch": function( fn ) { + return promise.then( null, fn ); + }, + + // Keep pipe for back-compat + pipe: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + + return jQuery.Deferred( function( newDefer ) { + jQuery.each( tuples, function( i, tuple ) { + + // Map tuples (progress, done, fail) to arguments (done, fail, progress) + var fn = jQuery.isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; + + // deferred.progress(function() { bind to newDefer or newDefer.notify }) + // deferred.done(function() { bind to newDefer or newDefer.resolve }) + // deferred.fail(function() { bind to newDefer or newDefer.reject }) + deferred[ tuple[ 1 ] ]( function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise() + .progress( newDefer.notify ) + .done( newDefer.resolve ) + .fail( newDefer.reject ); + } else { + newDefer[ tuple[ 0 ] + "With" ]( + this, + fn ? [ returned ] : arguments + ); + } + } ); + } ); + fns = null; + } ).promise(); + }, + then: function( onFulfilled, onRejected, onProgress ) { + var maxDepth = 0; + function resolve( depth, deferred, handler, special ) { + return function() { + var that = this, + args = arguments, + mightThrow = function() { + var returned, then; + + // Support: Promises/A+ section 2.3.3.3.3 + // https://promisesaplus.com/#point-59 + // Ignore double-resolution attempts + if ( depth < maxDepth ) { + return; + } + + returned = handler.apply( that, args ); + + // Support: Promises/A+ section 2.3.1 + // https://promisesaplus.com/#point-48 + if ( returned === deferred.promise() ) { + throw new TypeError( "Thenable self-resolution" ); + } + + // Support: Promises/A+ sections 2.3.3.1, 3.5 + // https://promisesaplus.com/#point-54 + // https://promisesaplus.com/#point-75 + // Retrieve `then` only once + then = returned && + + // Support: Promises/A+ section 2.3.4 + // https://promisesaplus.com/#point-64 + // Only check objects and functions for thenability + ( typeof returned === "object" || + typeof returned === "function" ) && + returned.then; + + // Handle a returned thenable + if ( jQuery.isFunction( then ) ) { + + // Special processors (notify) just wait for resolution + if ( special ) { + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ) + ); + + // Normal processors (resolve) also hook into progress + } else { + + // ...and disregard older resolution values + maxDepth++; + + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ), + resolve( maxDepth, deferred, Identity, + deferred.notifyWith ) + ); + } + + // Handle all other returned values + } else { + + // Only substitute handlers pass on context + // and multiple values (non-spec behavior) + if ( handler !== Identity ) { + that = undefined; + args = [ returned ]; + } + + // Process the value(s) + // Default process is resolve + ( special || deferred.resolveWith )( that, args ); + } + }, + + // Only normal processors (resolve) catch and reject exceptions + process = special ? + mightThrow : + function() { + try { + mightThrow(); + } catch ( e ) { + + if ( jQuery.Deferred.exceptionHook ) { + jQuery.Deferred.exceptionHook( e, + process.stackTrace ); + } + + // Support: Promises/A+ section 2.3.3.3.4.1 + // https://promisesaplus.com/#point-61 + // Ignore post-resolution exceptions + if ( depth + 1 >= maxDepth ) { + + // Only substitute handlers pass on context + // and multiple values (non-spec behavior) + if ( handler !== Thrower ) { + that = undefined; + args = [ e ]; + } + + deferred.rejectWith( that, args ); + } + } + }; + + // Support: Promises/A+ section 2.3.3.3.1 + // https://promisesaplus.com/#point-57 + // Re-resolve promises immediately to dodge false rejection from + // subsequent errors + if ( depth ) { + process(); + } else { + + // Call an optional hook to record the stack, in case of exception + // since it's otherwise lost when execution goes async + if ( jQuery.Deferred.getStackHook ) { + process.stackTrace = jQuery.Deferred.getStackHook(); + } + window.setTimeout( process ); + } + }; + } + + return jQuery.Deferred( function( newDefer ) { + + // progress_handlers.add( ... ) + tuples[ 0 ][ 3 ].add( + resolve( + 0, + newDefer, + jQuery.isFunction( onProgress ) ? + onProgress : + Identity, + newDefer.notifyWith + ) + ); + + // fulfilled_handlers.add( ... ) + tuples[ 1 ][ 3 ].add( + resolve( + 0, + newDefer, + jQuery.isFunction( onFulfilled ) ? + onFulfilled : + Identity + ) + ); + + // rejected_handlers.add( ... ) + tuples[ 2 ][ 3 ].add( + resolve( + 0, + newDefer, + jQuery.isFunction( onRejected ) ? + onRejected : + Thrower + ) + ); + } ).promise(); + }, + + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 5 ]; + + // promise.progress = list.add + // promise.done = list.add + // promise.fail = list.add + promise[ tuple[ 1 ] ] = list.add; + + // Handle state + if ( stateString ) { + list.add( + function() { + + // state = "resolved" (i.e., fulfilled) + // state = "rejected" + state = stateString; + }, + + // rejected_callbacks.disable + // fulfilled_callbacks.disable + tuples[ 3 - i ][ 2 ].disable, + + // progress_callbacks.lock + tuples[ 0 ][ 2 ].lock + ); + } + + // progress_handlers.fire + // fulfilled_handlers.fire + // rejected_handlers.fire + list.add( tuple[ 3 ].fire ); + + // deferred.notify = function() { deferred.notifyWith(...) } + // deferred.resolve = function() { deferred.resolveWith(...) } + // deferred.reject = function() { deferred.rejectWith(...) } + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments ); + return this; + }; + + // deferred.notifyWith = list.fireWith + // deferred.resolveWith = list.fireWith + // deferred.rejectWith = list.fireWith + deferred[ tuple[ 0 ] + "With" ] = list.fireWith; + } ); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( singleValue ) { + var + + // count of uncompleted subordinates + remaining = arguments.length, + + // count of unprocessed arguments + i = remaining, + + // subordinate fulfillment data + resolveContexts = Array( i ), + resolveValues = slice.call( arguments ), + + // the master Deferred + master = jQuery.Deferred(), + + // subordinate callback factory + updateFunc = function( i ) { + return function( value ) { + resolveContexts[ i ] = this; + resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( !( --remaining ) ) { + master.resolveWith( resolveContexts, resolveValues ); + } + }; + }; + + // Single- and empty arguments are adopted like Promise.resolve + if ( remaining <= 1 ) { + adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject, + !remaining ); + + // Use .then() to unwrap secondary thenables (cf. gh-3000) + if ( master.state() === "pending" || + jQuery.isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { + + return master.then(); + } + } + + // Multiple arguments are aggregated like Promise.all array elements + while ( i-- ) { + adoptValue( resolveValues[ i ], updateFunc( i ), master.reject ); + } + + return master.promise(); + } +} ); + + +// These usually indicate a programmer mistake during development, +// warn about them ASAP rather than swallowing them by default. +var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; + +jQuery.Deferred.exceptionHook = function( error, stack ) { + + // Support: IE 8 - 9 only + // Console exists when dev tools are open, which can happen at any time + if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) { + window.console.warn( "jQuery.Deferred exception: " + error.message, error.stack, stack ); + } +}; + + + + +jQuery.readyException = function( error ) { + window.setTimeout( function() { + throw error; + } ); +}; + + + + +// The deferred used on DOM ready +var readyList = jQuery.Deferred(); + +jQuery.fn.ready = function( fn ) { + + readyList + .then( fn ) + + // Wrap jQuery.readyException in a function so that the lookup + // happens at the time of error handling instead of callback + // registration. + .catch( function( error ) { + jQuery.readyException( error ); + } ); + + return this; +}; + +jQuery.extend( { + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + } +} ); + +jQuery.ready.then = readyList.then; + +// The ready event handler and self cleanup method +function completed() { + document.removeEventListener( "DOMContentLoaded", completed ); + window.removeEventListener( "load", completed ); + jQuery.ready(); +} + +// Catch cases where $(document).ready() is called +// after the browser event has already occurred. +// Support: IE <=9 - 10 only +// Older IE sometimes signals "interactive" too soon +if ( document.readyState === "complete" || + ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { + + // Handle it asynchronously to allow scripts the opportunity to delay ready + window.setTimeout( jQuery.ready ); + +} else { + + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed ); +} + + + + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + // Sets many values + if ( jQuery.type( key ) === "object" ) { + chainable = true; + for ( i in key ) { + access( elems, fn, i, key[ i ], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !jQuery.isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < len; i++ ) { + fn( + elems[ i ], key, raw ? + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) + ); + } + } + } + + if ( chainable ) { + return elems; + } + + // Gets + if ( bulk ) { + return fn.call( elems ); + } + + return len ? fn( elems[ 0 ], key ) : emptyGet; +}; +var acceptData = function( owner ) { + + // Accepts only: + // - Node + // - Node.ELEMENT_NODE + // - Node.DOCUMENT_NODE + // - Object + // - Any + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; + + + + +function Data() { + this.expando = jQuery.expando + Data.uid++; +} + +Data.uid = 1; + +Data.prototype = { + + cache: function( owner ) { + + // Check if the owner object already has a cache + var value = owner[ this.expando ]; + + // If not, create one + if ( !value ) { + value = {}; + + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return an empty object. + if ( acceptData( owner ) ) { + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable property + // configurable must be true to allow the property to be + // deleted when data is removed + } else { + Object.defineProperty( owner, this.expando, { + value: value, + configurable: true + } ); + } + } + } + + return value; + }, + set: function( owner, data, value ) { + var prop, + cache = this.cache( owner ); + + // Handle: [ owner, key, value ] args + // Always use camelCase key (gh-2257) + if ( typeof data === "string" ) { + cache[ jQuery.camelCase( data ) ] = value; + + // Handle: [ owner, { properties } ] args + } else { + + // Copy the properties one-by-one to the cache object + for ( prop in data ) { + cache[ jQuery.camelCase( prop ) ] = data[ prop ]; + } + } + return cache; + }, + get: function( owner, key ) { + return key === undefined ? + this.cache( owner ) : + + // Always use camelCase key (gh-2257) + owner[ this.expando ] && owner[ this.expando ][ jQuery.camelCase( key ) ]; + }, + access: function( owner, key, value ) { + + // In cases where either: + // + // 1. No key was specified + // 2. A string key was specified, but no value provided + // + // Take the "read" path and allow the get method to determine + // which value to return, respectively either: + // + // 1. The entire cache object + // 2. The data stored at the key + // + if ( key === undefined || + ( ( key && typeof key === "string" ) && value === undefined ) ) { + + return this.get( owner, key ); + } + + // When the key is not a string, or both a key and value + // are specified, set or extend (existing objects) with either: + // + // 1. An object of properties + // 2. A key and value + // + this.set( owner, key, value ); + + // Since the "set" path can have two possible entry points + // return the expected data based on which path was taken[*] + return value !== undefined ? value : key; + }, + remove: function( owner, key ) { + var i, + cache = owner[ this.expando ]; + + if ( cache === undefined ) { + return; + } + + if ( key !== undefined ) { + + // Support array or space separated string of keys + if ( Array.isArray( key ) ) { + + // If key is an array of keys... + // We always set camelCase keys, so remove that. + key = key.map( jQuery.camelCase ); + } else { + key = jQuery.camelCase( key ); + + // If a key with the spaces exists, use it. + // Otherwise, create an array by matching non-whitespace + key = key in cache ? + [ key ] : + ( key.match( rnothtmlwhite ) || [] ); + } + + i = key.length; + + while ( i-- ) { + delete cache[ key[ i ] ]; + } + } + + // Remove the expando if there's no more data + if ( key === undefined || jQuery.isEmptyObject( cache ) ) { + + // Support: Chrome <=35 - 45 + // Webkit & Blink performance suffers when deleting properties + // from DOM nodes, so set to undefined instead + // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted) + if ( owner.nodeType ) { + owner[ this.expando ] = undefined; + } else { + delete owner[ this.expando ]; + } + } + }, + hasData: function( owner ) { + var cache = owner[ this.expando ]; + return cache !== undefined && !jQuery.isEmptyObject( cache ); + } +}; +var dataPriv = new Data(); + +var dataUser = new Data(); + + + +// Implementation Summary +// +// 1. Enforce API surface and semantic compatibility with 1.9.x branch +// 2. Improve the module's maintainability by reducing the storage +// paths to a single mechanism. +// 3. Use the same single mechanism to support "private" and "user" data. +// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) +// 5. Avoid exposing implementation details on user objects (eg. expando properties) +// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 + +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /[A-Z]/g; + +function getData( data ) { + if ( data === "true" ) { + return true; + } + + if ( data === "false" ) { + return false; + } + + if ( data === "null" ) { + return null; + } + + // Only convert to a number if it doesn't change the string + if ( data === +data + "" ) { + return +data; + } + + if ( rbrace.test( data ) ) { + return JSON.parse( data ); + } + + return data; +} + +function dataAttr( elem, key, data ) { + var name; + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = getData( data ); + } catch ( e ) {} + + // Make sure we set the data so it isn't changed later + dataUser.set( elem, key, data ); + } else { + data = undefined; + } + } + return data; +} + +jQuery.extend( { + hasData: function( elem ) { + return dataUser.hasData( elem ) || dataPriv.hasData( elem ); + }, + + data: function( elem, name, data ) { + return dataUser.access( elem, name, data ); + }, + + removeData: function( elem, name ) { + dataUser.remove( elem, name ); + }, + + // TODO: Now that all calls to _data and _removeData have been replaced + // with direct calls to dataPriv methods, these can be deprecated. + _data: function( elem, name, data ) { + return dataPriv.access( elem, name, data ); + }, + + _removeData: function( elem, name ) { + dataPriv.remove( elem, name ); + } +} ); + +jQuery.fn.extend( { + data: function( key, value ) { + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = dataUser.get( elem ); + + if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + // Support: IE 11 only + // The attrs elements can be null (#14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.slice( 5 ) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + dataPriv.set( elem, "hasDataAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each( function() { + dataUser.set( this, key ); + } ); + } + + return access( this, function( value ) { + var data; + + // The calling jQuery object (element matches) is not empty + // (and therefore has an element appears at this[ 0 ]) and the + // `value` parameter was not undefined. An empty jQuery object + // will result in `undefined` for elem = this[ 0 ] which will + // throw an exception if an attempt to read a data cache is made. + if ( elem && value === undefined ) { + + // Attempt to get data from the cache + // The key will always be camelCased in Data + data = dataUser.get( elem, key ); + if ( data !== undefined ) { + return data; + } + + // Attempt to "discover" the data in + // HTML5 custom data-* attrs + data = dataAttr( elem, key ); + if ( data !== undefined ) { + return data; + } + + // We tried really hard, but the data doesn't exist. + return; + } + + // Set the data... + this.each( function() { + + // We always store the camelCased key + dataUser.set( this, key, value ); + } ); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each( function() { + dataUser.remove( this, key ); + } ); + } +} ); + + +jQuery.extend( { + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = dataPriv.get( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || Array.isArray( data ) ) { + queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // Clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // Not public - generate a queueHooks object, or return the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { + empty: jQuery.Callbacks( "once memory" ).add( function() { + dataPriv.remove( elem, [ type + "queue", key ] ); + } ) + } ); + } +} ); + +jQuery.fn.extend( { + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[ 0 ], type ); + } + + return data === undefined ? + this : + this.each( function() { + var queue = jQuery.queue( this, type, data ); + + // Ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + } ); + }, + dequeue: function( type ) { + return this.each( function() { + jQuery.dequeue( this, type ); + } ); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while ( i-- ) { + tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +} ); +var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; + +var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); + + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var isHiddenWithinTree = function( elem, el ) { + + // isHiddenWithinTree might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + + // Inline style trumps all + return elem.style.display === "none" || + elem.style.display === "" && + + // Otherwise, check computed style + // Support: Firefox <=43 - 45 + // Disconnected elements can have computed display: none, so first confirm that elem is + // in the document. + jQuery.contains( elem.ownerDocument, elem ) && + + jQuery.css( elem, "display" ) === "none"; + }; + +var swap = function( elem, options, callback, args ) { + var ret, name, + old = {}; + + // Remember the old values, and insert the new ones + for ( name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + ret = callback.apply( elem, args || [] ); + + // Revert the old values + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + + return ret; +}; + + + + +function adjustCSS( elem, prop, valueParts, tween ) { + var adjusted, + scale = 1, + maxIterations = 20, + currentValue = tween ? + function() { + return tween.cur(); + } : + function() { + return jQuery.css( elem, prop, "" ); + }, + initial = currentValue(), + unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), + + // Starting value computation is required for potential unit mismatches + initialInUnit = ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && + rcssNum.exec( jQuery.css( elem, prop ) ); + + if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { + + // Trust units reported by jQuery.css + unit = unit || initialInUnit[ 3 ]; + + // Make sure we update the tween properties later on + valueParts = valueParts || []; + + // Iteratively approximate from a nonzero starting point + initialInUnit = +initial || 1; + + do { + + // If previous iteration zeroed out, double until we get *something*. + // Use string for doubling so we don't accidentally see scale as unchanged below + scale = scale || ".5"; + + // Adjust and apply + initialInUnit = initialInUnit / scale; + jQuery.style( elem, prop, initialInUnit + unit ); + + // Update scale, tolerating zero or NaN from tween.cur() + // Break the loop if scale is unchanged or perfect, or if we've just had enough. + } while ( + scale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations + ); + } + + if ( valueParts ) { + initialInUnit = +initialInUnit || +initial || 0; + + // Apply relative offset (+=/-=) if specified + adjusted = valueParts[ 1 ] ? + initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : + +valueParts[ 2 ]; + if ( tween ) { + tween.unit = unit; + tween.start = initialInUnit; + tween.end = adjusted; + } + } + return adjusted; +} + + +var defaultDisplayMap = {}; + +function getDefaultDisplay( elem ) { + var temp, + doc = elem.ownerDocument, + nodeName = elem.nodeName, + display = defaultDisplayMap[ nodeName ]; + + if ( display ) { + return display; + } + + temp = doc.body.appendChild( doc.createElement( nodeName ) ); + display = jQuery.css( temp, "display" ); + + temp.parentNode.removeChild( temp ); + + if ( display === "none" ) { + display = "block"; + } + defaultDisplayMap[ nodeName ] = display; + + return display; +} + +function showHide( elements, show ) { + var display, elem, + values = [], + index = 0, + length = elements.length; + + // Determine new display value for elements that need to change + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + + display = elem.style.display; + if ( show ) { + + // Since we force visibility upon cascade-hidden elements, an immediate (and slow) + // check is required in this first loop unless we have a nonempty display value (either + // inline or about-to-be-restored) + if ( display === "none" ) { + values[ index ] = dataPriv.get( elem, "display" ) || null; + if ( !values[ index ] ) { + elem.style.display = ""; + } + } + if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) { + values[ index ] = getDefaultDisplay( elem ); + } + } else { + if ( display !== "none" ) { + values[ index ] = "none"; + + // Remember what we're overwriting + dataPriv.set( elem, "display", display ); + } + } + } + + // Set the display of the elements in a second loop to avoid constant reflow + for ( index = 0; index < length; index++ ) { + if ( values[ index ] != null ) { + elements[ index ].style.display = values[ index ]; + } + } + + return elements; +} + +jQuery.fn.extend( { + show: function() { + return showHide( this, true ); + }, + hide: function() { + return showHide( this ); + }, + toggle: function( state ) { + if ( typeof state === "boolean" ) { + return state ? this.show() : this.hide(); + } + + return this.each( function() { + if ( isHiddenWithinTree( this ) ) { + jQuery( this ).show(); + } else { + jQuery( this ).hide(); + } + } ); + } +} ); +var rcheckableType = ( /^(?:checkbox|radio)$/i ); + +var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]+)/i ); + +var rscriptType = ( /^$|\/(?:java|ecma)script/i ); + + + +// We have to close these tags to support XHTML (#13200) +var wrapMap = { + + // Support: IE <=9 only + option: [ 1, "" ], + + // XHTML parsers do not magically insert elements in the + // same way that tag soup parsers do. So we cannot shorten + // this by omitting or other required elements. + thead: [ 1, "", "
" ], + col: [ 2, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + + _default: [ 0, "", "" ] +}; + +// Support: IE <=9 only +wrapMap.optgroup = wrapMap.option; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + + +function getAll( context, tag ) { + + // Support: IE <=9 - 11 only + // Use typeof to avoid zero-argument method invocation on host objects (#15151) + var ret; + + if ( typeof context.getElementsByTagName !== "undefined" ) { + ret = context.getElementsByTagName( tag || "*" ); + + } else if ( typeof context.querySelectorAll !== "undefined" ) { + ret = context.querySelectorAll( tag || "*" ); + + } else { + ret = []; + } + + if ( tag === undefined || tag && nodeName( context, tag ) ) { + return jQuery.merge( [ context ], ret ); + } + + return ret; +} + + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + dataPriv.set( + elems[ i ], + "globalEval", + !refElements || dataPriv.get( refElements[ i ], "globalEval" ) + ); + } +} + + +var rhtml = /<|&#?\w+;/; + +function buildFragment( elems, context, scripts, selection, ignored ) { + var elem, tmp, tag, wrap, contains, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( jQuery.type( elem ) === "object" ) { + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; + + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, tmp.childNodes ); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Ensure the created nodes are orphaned (#12392) + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ( ( elem = nodes[ i++ ] ) ) { + + // Skip elements already in the context collection (trac-4087) + if ( selection && jQuery.inArray( elem, selection ) > -1 ) { + if ( ignored ) { + ignored.push( elem ); + } + continue; + } + + contains = jQuery.contains( elem.ownerDocument, elem ); + + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( contains ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( ( elem = tmp[ j++ ] ) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; +} + + +( function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + + // Support: Android 4.0 - 4.3 only + // Check state lost if the name is set (#11217) + // Support: Windows Web Apps (WWA) + // `name` and `type` must use .setAttribute for WWA (#14901) + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + + // Support: Android <=4.1 only + // Older WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE <=11 only + // Make sure textarea (and checkbox) defaultValue is properly cloned + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; +} )(); +var documentElement = document.documentElement; + + + +var + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +// Support: IE <=9 only +// See #13393 for more info +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +function on( elem, types, selector, data, fn, one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + on( elem, type, selector, data, types[ type ], one ); + } + return elem; + } + + if ( data == null && fn == null ) { + + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return elem; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return elem.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + } ); +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.get( elem ); + + // Don't attach events to noData or text/comment nodes (but allow plain objects) + if ( !elemData ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Ensure that invalid selectors throw exceptions at attach time + // Evaluate against documentElement in case elem is a non-element node (e.g., document) + if ( selector ) { + jQuery.find.matchesSelector( documentElement, selector ); + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !( events = elemData.events ) ) { + events = elemData.events = {}; + } + if ( !( eventHandle = elemData.handle ) ) { + eventHandle = elemData.handle = function( e ) { + + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; + }; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend( { + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join( "." ) + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !( handlers = events[ type ] ) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener if the special events handler returns false + if ( !special.setup || + special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); + + if ( !elemData || !( events = elemData.events ) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[ 2 ] && + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || + selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || + special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove data and the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + dataPriv.remove( elem, "handle events" ); + } + }, + + dispatch: function( nativeEvent ) { + + // Make a writable jQuery.Event from the native event object + var event = jQuery.event.fix( nativeEvent ); + + var i, j, ret, matched, handleObj, handlerQueue, + args = new Array( arguments.length ), + handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[ 0 ] = event; + + for ( i = 1; i < arguments.length; i++ ) { + args[ i ] = arguments[ i ]; + } + + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( ( handleObj = matched.handlers[ j++ ] ) && + !event.isImmediatePropagationStopped() ) { + + // Triggered event must either 1) have no namespace, or 2) have namespace(s) + // a subset or equal to those in the bound event (both can have no namespace). + if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || + handleObj.handler ).apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( ( event.result = ret ) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var i, handleObj, sel, matchedHandlers, matchedSelectors, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Find delegate handlers + if ( delegateCount && + + // Support: IE <=9 + // Black-hole SVG instance trees (trac-13180) + cur.nodeType && + + // Support: Firefox <=42 + // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861) + // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click + // Support: IE 11 only + // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343) + !( event.type === "click" && event.button >= 1 ) ) { + + for ( ; cur !== this; cur = cur.parentNode || this ) { + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) { + matchedHandlers = []; + matchedSelectors = {}; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matchedSelectors[ sel ] === undefined ) { + matchedSelectors[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) > -1 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matchedSelectors[ sel ] ) { + matchedHandlers.push( handleObj ); + } + } + if ( matchedHandlers.length ) { + handlerQueue.push( { elem: cur, handlers: matchedHandlers } ); + } + } + } + } + + // Add the remaining (directly-bound) handlers + cur = this; + if ( delegateCount < handlers.length ) { + handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } ); + } + + return handlerQueue; + }, + + addProp: function( name, hook ) { + Object.defineProperty( jQuery.Event.prototype, name, { + enumerable: true, + configurable: true, + + get: jQuery.isFunction( hook ) ? + function() { + if ( this.originalEvent ) { + return hook( this.originalEvent ); + } + } : + function() { + if ( this.originalEvent ) { + return this.originalEvent[ name ]; + } + }, + + set: function( value ) { + Object.defineProperty( this, name, { + enumerable: true, + configurable: true, + writable: true, + value: value + } ); + } + } ); + }, + + fix: function( originalEvent ) { + return originalEvent[ jQuery.expando ] ? + originalEvent : + new jQuery.Event( originalEvent ); + }, + + special: { + load: { + + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + focus: { + + // Fire native event if possible so blur/focus sequence is correct + trigger: function() { + if ( this !== safeActiveElement() && this.focus ) { + this.focus(); + return false; + } + }, + delegateType: "focusin" + }, + blur: { + trigger: function() { + if ( this === safeActiveElement() && this.blur ) { + this.blur(); + return false; + } + }, + delegateType: "focusout" + }, + click: { + + // For checkbox, fire native event so checked state will be right + trigger: function() { + if ( this.type === "checkbox" && this.click && nodeName( this, "input" ) ) { + this.click(); + return false; + } + }, + + // For cross-browser consistency, don't fire native .click() on links + _default: function( event ) { + return nodeName( event.target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + } +}; + +jQuery.removeEvent = function( elem, type, handle ) { + + // This "if" is needed for plain objects + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle ); + } +}; + +jQuery.Event = function( src, props ) { + + // Allow instantiation without the 'new' keyword + if ( !( this instanceof jQuery.Event ) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + + // Support: Android <=2.3 only + src.returnValue === false ? + returnTrue : + returnFalse; + + // Create target properties + // Support: Safari <=6 - 7 only + // Target should not be a text node (#504, #13143) + this.target = ( src.target && src.target.nodeType === 3 ) ? + src.target.parentNode : + src.target; + + this.currentTarget = src.currentTarget; + this.relatedTarget = src.relatedTarget; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + constructor: jQuery.Event, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + isSimulated: false, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if ( e && !this.isSimulated ) { + e.preventDefault(); + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +// Includes all common event props including KeyEvent and MouseEvent specific props +jQuery.each( { + altKey: true, + bubbles: true, + cancelable: true, + changedTouches: true, + ctrlKey: true, + detail: true, + eventPhase: true, + metaKey: true, + pageX: true, + pageY: true, + shiftKey: true, + view: true, + "char": true, + charCode: true, + key: true, + keyCode: true, + button: true, + buttons: true, + clientX: true, + clientY: true, + offsetX: true, + offsetY: true, + pointerId: true, + pointerType: true, + screenX: true, + screenY: true, + targetTouches: true, + toElement: true, + touches: true, + + which: function( event ) { + var button = event.button; + + // Add which for key events + if ( event.which == null && rkeyEvent.test( event.type ) ) { + return event.charCode != null ? event.charCode : event.keyCode; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + if ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) { + if ( button & 1 ) { + return 1; + } + + if ( button & 2 ) { + return 3; + } + + if ( button & 4 ) { + return 2; + } + + return 0; + } + + return event.which; + } +}, jQuery.event.addProp ); + +// Create mouseenter/leave events using mouseover/out and event-time checks +// so that event delegation works in jQuery. +// Do the same for pointerenter/pointerleave and pointerover/pointerout +// +// Support: Safari 7 only +// Safari sends mouseenter too often; see: +// https://bugs.chromium.org/p/chromium/issues/detail?id=470258 +// for the description of the bug (it existed in older Chrome versions as well). +jQuery.each( { + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mouseenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +} ); + +jQuery.fn.extend( { + + on: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn ); + }, + one: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? + handleObj.origType + "." + handleObj.namespace : + handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each( function() { + jQuery.event.remove( this, types, fn, selector ); + } ); + } +} ); + + +var + + /* eslint-disable max-len */ + + // See https://github.com/eslint/eslint/issues/3229 + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi, + + /* eslint-enable */ + + // Support: IE <=10 - 11, Edge 12 - 13 + // In IE/Edge using regex groups here causes severe slowdowns. + // See https://connect.microsoft.com/IE/feedback/details/1736512/ + rnoInnerhtml = /\s*$/g; + +// Prefer a tbody over its parent table for containing new rows +function manipulationTarget( elem, content ) { + if ( nodeName( elem, "table" ) && + nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { + + return jQuery( ">tbody", elem )[ 0 ] || elem; + } + + return elem; +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + var match = rscriptTypeMasked.exec( elem.type ); + + if ( match ) { + elem.type = match[ 1 ]; + } else { + elem.removeAttribute( "type" ); + } + + return elem; +} + +function cloneCopyEvent( src, dest ) { + var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events; + + if ( dest.nodeType !== 1 ) { + return; + } + + // 1. Copy private data: events, handlers, etc. + if ( dataPriv.hasData( src ) ) { + pdataOld = dataPriv.access( src ); + pdataCur = dataPriv.set( dest, pdataOld ); + events = pdataOld.events; + + if ( events ) { + delete pdataCur.handle; + pdataCur.events = {}; + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + } + + // 2. Copy user data + if ( dataUser.hasData( src ) ) { + udataOld = dataUser.access( src ); + udataCur = jQuery.extend( {}, udataOld ); + + dataUser.set( dest, udataCur ); + } +} + +// Fix IE bugs, see support tests +function fixInput( src, dest ) { + var nodeName = dest.nodeName.toLowerCase(); + + // Fails to persist the checked state of a cloned checkbox or radio button. + if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + dest.checked = src.checked; + + // Fails to return the selected option to the default selected state when cloning options + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +function domManip( collection, args, callback, ignored ) { + + // Flatten any nested arrays + args = concat.apply( [], args ); + + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = collection.length, + iNoClone = l - 1, + value = args[ 0 ], + isFunction = jQuery.isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( isFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return collection.each( function( index ) { + var self = collection.eq( index ); + if ( isFunction ) { + args[ 0 ] = value.call( this, index, self.html() ); + } + domManip( self, args, callback, ignored ); + } ); + } + + if ( l ) { + fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + // Require either new content or an interest in ignored elements to invoke the callback + if ( first || ignored ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item + // instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( collection[ i ], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !dataPriv.access( node, "globalEval" ) && + jQuery.contains( doc, node ) ) { + + if ( node.src ) { + + // Optional AJAX dependency, but won't run scripts if not present + if ( jQuery._evalUrl ) { + jQuery._evalUrl( node.src ); + } + } else { + DOMEval( node.textContent.replace( rcleanScript, "" ), doc ); + } + } + } + } + } + } + + return collection; +} + +function remove( elem, selector, keepData ) { + var node, + nodes = selector ? jQuery.filter( selector, elem ) : elem, + i = 0; + + for ( ; ( node = nodes[ i ] ) != null; i++ ) { + if ( !keepData && node.nodeType === 1 ) { + jQuery.cleanData( getAll( node ) ); + } + + if ( node.parentNode ) { + if ( keepData && jQuery.contains( node.ownerDocument, node ) ) { + setGlobalEval( getAll( node, "script" ) ); + } + node.parentNode.removeChild( node ); + } + } + + return elem; +} + +jQuery.extend( { + htmlPrefilter: function( html ) { + return html.replace( rxhtmlTag, "<$1>" ); + }, + + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var i, l, srcElements, destElements, + clone = elem.cloneNode( true ), + inPage = jQuery.contains( elem.ownerDocument, elem ); + + // Fix IE cloning issues + if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && + !jQuery.isXMLDoc( elem ) ) { + + // We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + // Return the cloned set + return clone; + }, + + cleanData: function( elems ) { + var data, elem, type, + special = jQuery.event.special, + i = 0; + + for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { + if ( acceptData( elem ) ) { + if ( ( data = elem[ dataPriv.expando ] ) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Support: Chrome <=35 - 45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataPriv.expando ] = undefined; + } + if ( elem[ dataUser.expando ] ) { + + // Support: Chrome <=35 - 45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataUser.expando ] = undefined; + } + } + } + } +} ); + +jQuery.fn.extend( { + detach: function( selector ) { + return remove( this, selector, true ); + }, + + remove: function( selector ) { + return remove( this, selector ); + }, + + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().each( function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + } ); + }, null, value, arguments.length ); + }, + + append: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + } ); + }, + + prepend: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + } ); + }, + + before: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + } ); + }, + + after: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + } ); + }, + + empty: function() { + var elem, + i = 0; + + for ( ; ( elem = this[ i ] ) != null; i++ ) { + if ( elem.nodeType === 1 ) { + + // Prevent memory leaks + jQuery.cleanData( getAll( elem, false ) ); + + // Remove any remaining nodes + elem.textContent = ""; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + } ); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { + + value = jQuery.htmlPrefilter( value ); + + try { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; + + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch ( e ) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var ignored = []; + + // Make the changes, replacing each non-ignored context element with the new content + return domManip( this, arguments, function( elem ) { + var parent = this.parentNode; + + if ( jQuery.inArray( this, ignored ) < 0 ) { + jQuery.cleanData( getAll( this ) ); + if ( parent ) { + parent.replaceChild( elem, this ); + } + } + + // Force callback invocation + }, ignored ); + } +} ); + +jQuery.each( { + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1, + i = 0; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); + + // Support: Android <=4.0 only, PhantomJS 1 only + // .get() because push.apply(_, arraylike) throws on ancient WebKit + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +} ); +var rmargin = ( /^margin/ ); + +var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); + +var getStyles = function( elem ) { + + // Support: IE <=11 only, Firefox <=30 (#15098, #14150) + // IE throws on elements created in popups + // FF meanwhile throws on frame elements through "defaultView.getComputedStyle" + var view = elem.ownerDocument.defaultView; + + if ( !view || !view.opener ) { + view = window; + } + + return view.getComputedStyle( elem ); + }; + + + +( function() { + + // Executing both pixelPosition & boxSizingReliable tests require only one layout + // so they're executed at the same time to save the second computation. + function computeStyleTests() { + + // This is a singleton, we need to execute it only once + if ( !div ) { + return; + } + + div.style.cssText = + "box-sizing:border-box;" + + "position:relative;display:block;" + + "margin:auto;border:1px;padding:1px;" + + "top:1%;width:50%"; + div.innerHTML = ""; + documentElement.appendChild( container ); + + var divStyle = window.getComputedStyle( div ); + pixelPositionVal = divStyle.top !== "1%"; + + // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44 + reliableMarginLeftVal = divStyle.marginLeft === "2px"; + boxSizingReliableVal = divStyle.width === "4px"; + + // Support: Android 4.0 - 4.3 only + // Some styles come back with percentage values, even though they shouldn't + div.style.marginRight = "50%"; + pixelMarginRightVal = divStyle.marginRight === "4px"; + + documentElement.removeChild( container ); + + // Nullify the div so it wouldn't be stored in the memory and + // it will also be a sign that checks already performed + div = null; + } + + var pixelPositionVal, boxSizingReliableVal, pixelMarginRightVal, reliableMarginLeftVal, + container = document.createElement( "div" ), + div = document.createElement( "div" ); + + // Finish early in limited (non-browser) environments + if ( !div.style ) { + return; + } + + // Support: IE <=9 - 11 only + // Style of cloned element affects source element cloned (#8908) + div.style.backgroundClip = "content-box"; + div.cloneNode( true ).style.backgroundClip = ""; + support.clearCloneStyle = div.style.backgroundClip === "content-box"; + + container.style.cssText = "border:0;width:8px;height:0;top:0;left:-9999px;" + + "padding:0;margin-top:1px;position:absolute"; + container.appendChild( div ); + + jQuery.extend( support, { + pixelPosition: function() { + computeStyleTests(); + return pixelPositionVal; + }, + boxSizingReliable: function() { + computeStyleTests(); + return boxSizingReliableVal; + }, + pixelMarginRight: function() { + computeStyleTests(); + return pixelMarginRightVal; + }, + reliableMarginLeft: function() { + computeStyleTests(); + return reliableMarginLeftVal; + } + } ); +} )(); + + +function curCSS( elem, name, computed ) { + var width, minWidth, maxWidth, ret, + + // Support: Firefox 51+ + // Retrieving style before computed somehow + // fixes an issue with getting wrong values + // on detached elements + style = elem.style; + + computed = computed || getStyles( elem ); + + // getPropertyValue is needed for: + // .css('filter') (IE 9 only, #12537) + // .css('--customProperty) (#3144) + if ( computed ) { + ret = computed.getPropertyValue( name ) || computed[ name ]; + + if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { + ret = jQuery.style( elem, name ); + } + + // A tribute to the "awesome hack by Dean Edwards" + // Android Browser returns percentage for some values, + // but width seems to be reliably pixels. + // This is against the CSSOM draft spec: + // https://drafts.csswg.org/cssom/#resolved-values + if ( !support.pixelMarginRight() && rnumnonpx.test( ret ) && rmargin.test( name ) ) { + + // Remember the original values + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; + + // Put in the new values to get a computed value out + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; + + // Revert the changed values + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; + } + } + + return ret !== undefined ? + + // Support: IE <=9 - 11 only + // IE returns zIndex value as an integer. + ret + "" : + ret; +} + + +function addGetHookIf( conditionFn, hookFn ) { + + // Define the hook, we'll check on the first run if it's really needed. + return { + get: function() { + if ( conditionFn() ) { + + // Hook not needed (or it's not possible to use it due + // to missing dependency), remove it. + delete this.get; + return; + } + + // Hook needed; redefine it so that the support test is not executed again. + return ( this.get = hookFn ).apply( this, arguments ); + } + }; +} + + +var + + // Swappable if display is none or starts with table + // except "table", "table-cell", or "table-caption" + // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + rcustomProp = /^--/, + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: "0", + fontWeight: "400" + }, + + cssPrefixes = [ "Webkit", "Moz", "ms" ], + emptyStyle = document.createElement( "div" ).style; + +// Return a css property mapped to a potentially vendor prefixed property +function vendorPropName( name ) { + + // Shortcut for names that are not vendor prefixed + if ( name in emptyStyle ) { + return name; + } + + // Check for vendor prefixed names + var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), + i = cssPrefixes.length; + + while ( i-- ) { + name = cssPrefixes[ i ] + capName; + if ( name in emptyStyle ) { + return name; + } + } +} + +// Return a property mapped along what jQuery.cssProps suggests or to +// a vendor prefixed property. +function finalPropName( name ) { + var ret = jQuery.cssProps[ name ]; + if ( !ret ) { + ret = jQuery.cssProps[ name ] = vendorPropName( name ) || name; + } + return ret; +} + +function setPositiveNumber( elem, value, subtract ) { + + // Any relative (+/-) values have already been + // normalized at this point + var matches = rcssNum.exec( value ); + return matches ? + + // Guard against undefined "subtract", e.g., when used as in cssHooks + Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) : + value; +} + +function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { + var i, + val = 0; + + // If we already have the right measurement, avoid augmentation + if ( extra === ( isBorderBox ? "border" : "content" ) ) { + i = 4; + + // Otherwise initialize for horizontal or vertical properties + } else { + i = name === "width" ? 1 : 0; + } + + for ( ; i < 4; i += 2 ) { + + // Both box models exclude margin, so add it if we want it + if ( extra === "margin" ) { + val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); + } + + if ( isBorderBox ) { + + // border-box includes padding, so remove it if we want content + if ( extra === "content" ) { + val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } + + // At this point, extra isn't border nor margin, so remove border + if ( extra !== "margin" ) { + val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } else { + + // At this point, extra isn't content, so add padding + val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + // At this point, extra isn't content nor padding, so add border + if ( extra !== "padding" ) { + val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } + } + + return val; +} + +function getWidthOrHeight( elem, name, extra ) { + + // Start with computed style + var valueIsBorderBox, + styles = getStyles( elem ), + val = curCSS( elem, name, styles ), + isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + + // Computed unit is not pixels. Stop here and return. + if ( rnumnonpx.test( val ) ) { + return val; + } + + // Check for style in case a browser which returns unreliable values + // for getComputedStyle silently falls back to the reliable elem.style + valueIsBorderBox = isBorderBox && + ( support.boxSizingReliable() || val === elem.style[ name ] ); + + // Fall back to offsetWidth/Height when value is "auto" + // This happens for inline elements with no explicit setting (gh-3571) + if ( val === "auto" ) { + val = elem[ "offset" + name[ 0 ].toUpperCase() + name.slice( 1 ) ]; + } + + // Normalize "", auto, and prepare for extra + val = parseFloat( val ) || 0; + + // Use the active box-sizing model to add/subtract irrelevant styles + return ( val + + augmentWidthOrHeight( + elem, + name, + extra || ( isBorderBox ? "border" : "content" ), + valueIsBorderBox, + styles + ) + ) + "px"; +} + +jQuery.extend( { + + // Add in style property hooks for overriding the default + // behavior of getting and setting a style property + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + + // We should always get a number back from opacity + var ret = curCSS( elem, "opacity" ); + return ret === "" ? "1" : ret; + } + } + } + }, + + // Don't automatically add "px" to these possibly-unitless properties + cssNumber: { + "animationIterationCount": true, + "columnCount": true, + "fillOpacity": true, + "flexGrow": true, + "flexShrink": true, + "fontWeight": true, + "lineHeight": true, + "opacity": true, + "order": true, + "orphans": true, + "widows": true, + "zIndex": true, + "zoom": true + }, + + // Add in properties whose names you wish to fix before + // setting or getting the value + cssProps: { + "float": "cssFloat" + }, + + // Get and set the style property on a DOM Node + style: function( elem, name, value, extra ) { + + // Don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + // Make sure that we're working with the right name + var ret, type, hooks, + origName = jQuery.camelCase( name ), + isCustomProp = rcustomProp.test( name ), + style = elem.style; + + // Make sure that we're working with the right name. We don't + // want to query the value if it is a CSS custom property + // since they are user-defined. + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + // Gets hook for the prefixed version, then unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // Check if we're setting a value + if ( value !== undefined ) { + type = typeof value; + + // Convert "+=" or "-=" to relative numbers (#7345) + if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) { + value = adjustCSS( elem, name, ret ); + + // Fixes bug #9237 + type = "number"; + } + + // Make sure that null and NaN values aren't set (#7116) + if ( value == null || value !== value ) { + return; + } + + // If a number was passed in, add the unit (except for certain CSS properties) + if ( type === "number" ) { + value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); + } + + // background-* props affect original clone's values + if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { + style[ name ] = "inherit"; + } + + // If a hook was provided, use that value, otherwise just set the specified value + if ( !hooks || !( "set" in hooks ) || + ( value = hooks.set( elem, value, extra ) ) !== undefined ) { + + if ( isCustomProp ) { + style.setProperty( name, value ); + } else { + style[ name ] = value; + } + } + + } else { + + // If a hook was provided get the non-computed value from there + if ( hooks && "get" in hooks && + ( ret = hooks.get( elem, false, extra ) ) !== undefined ) { + + return ret; + } + + // Otherwise just get the value from the style object + return style[ name ]; + } + }, + + css: function( elem, name, extra, styles ) { + var val, num, hooks, + origName = jQuery.camelCase( name ), + isCustomProp = rcustomProp.test( name ); + + // Make sure that we're working with the right name. We don't + // want to modify the value if it is a CSS custom property + // since they are user-defined. + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + // Try prefixed name followed by the unprefixed name + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // If a hook was provided get the computed value from there + if ( hooks && "get" in hooks ) { + val = hooks.get( elem, true, extra ); + } + + // Otherwise, if a way to get the computed value exists, use that + if ( val === undefined ) { + val = curCSS( elem, name, styles ); + } + + // Convert "normal" to computed value + if ( val === "normal" && name in cssNormalTransform ) { + val = cssNormalTransform[ name ]; + } + + // Make numeric if forced or a qualifier was provided and val looks numeric + if ( extra === "" || extra ) { + num = parseFloat( val ); + return extra === true || isFinite( num ) ? num || 0 : val; + } + + return val; + } +} ); + +jQuery.each( [ "height", "width" ], function( i, name ) { + jQuery.cssHooks[ name ] = { + get: function( elem, computed, extra ) { + if ( computed ) { + + // Certain elements can have dimension info if we invisibly show them + // but it must have a current display style that would benefit + return rdisplayswap.test( jQuery.css( elem, "display" ) ) && + + // Support: Safari 8+ + // Table columns in Safari have non-zero offsetWidth & zero + // getBoundingClientRect().width unless display is changed. + // Support: IE <=11 only + // Running getBoundingClientRect on a disconnected node + // in IE throws an error. + ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? + swap( elem, cssShow, function() { + return getWidthOrHeight( elem, name, extra ); + } ) : + getWidthOrHeight( elem, name, extra ); + } + }, + + set: function( elem, value, extra ) { + var matches, + styles = extra && getStyles( elem ), + subtract = extra && augmentWidthOrHeight( + elem, + name, + extra, + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + styles + ); + + // Convert to pixels if value adjustment is needed + if ( subtract && ( matches = rcssNum.exec( value ) ) && + ( matches[ 3 ] || "px" ) !== "px" ) { + + elem.style[ name ] = value; + value = jQuery.css( elem, name ); + } + + return setPositiveNumber( elem, value, subtract ); + } + }; +} ); + +jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, + function( elem, computed ) { + if ( computed ) { + return ( parseFloat( curCSS( elem, "marginLeft" ) ) || + elem.getBoundingClientRect().left - + swap( elem, { marginLeft: 0 }, function() { + return elem.getBoundingClientRect().left; + } ) + ) + "px"; + } + } +); + +// These hooks are used by animate to expand properties +jQuery.each( { + margin: "", + padding: "", + border: "Width" +}, function( prefix, suffix ) { + jQuery.cssHooks[ prefix + suffix ] = { + expand: function( value ) { + var i = 0, + expanded = {}, + + // Assumes a single number if not a string + parts = typeof value === "string" ? value.split( " " ) : [ value ]; + + for ( ; i < 4; i++ ) { + expanded[ prefix + cssExpand[ i ] + suffix ] = + parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; + } + + return expanded; + } + }; + + if ( !rmargin.test( prefix ) ) { + jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; + } +} ); + +jQuery.fn.extend( { + css: function( name, value ) { + return access( this, function( elem, name, value ) { + var styles, len, + map = {}, + i = 0; + + if ( Array.isArray( name ) ) { + styles = getStyles( elem ); + len = name.length; + + for ( ; i < len; i++ ) { + map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); + } + + return map; + } + + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + } +} ); + + +function Tween( elem, options, prop, end, easing ) { + return new Tween.prototype.init( elem, options, prop, end, easing ); +} +jQuery.Tween = Tween; + +Tween.prototype = { + constructor: Tween, + init: function( elem, options, prop, end, easing, unit ) { + this.elem = elem; + this.prop = prop; + this.easing = easing || jQuery.easing._default; + this.options = options; + this.start = this.now = this.cur(); + this.end = end; + this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); + }, + cur: function() { + var hooks = Tween.propHooks[ this.prop ]; + + return hooks && hooks.get ? + hooks.get( this ) : + Tween.propHooks._default.get( this ); + }, + run: function( percent ) { + var eased, + hooks = Tween.propHooks[ this.prop ]; + + if ( this.options.duration ) { + this.pos = eased = jQuery.easing[ this.easing ]( + percent, this.options.duration * percent, 0, 1, this.options.duration + ); + } else { + this.pos = eased = percent; + } + this.now = ( this.end - this.start ) * eased + this.start; + + if ( this.options.step ) { + this.options.step.call( this.elem, this.now, this ); + } + + if ( hooks && hooks.set ) { + hooks.set( this ); + } else { + Tween.propHooks._default.set( this ); + } + return this; + } +}; + +Tween.prototype.init.prototype = Tween.prototype; + +Tween.propHooks = { + _default: { + get: function( tween ) { + var result; + + // Use a property on the element directly when it is not a DOM element, + // or when there is no matching style property that exists. + if ( tween.elem.nodeType !== 1 || + tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) { + return tween.elem[ tween.prop ]; + } + + // Passing an empty string as a 3rd parameter to .css will automatically + // attempt a parseFloat and fallback to a string if the parse fails. + // Simple values such as "10px" are parsed to Float; + // complex values such as "rotate(1rad)" are returned as-is. + result = jQuery.css( tween.elem, tween.prop, "" ); + + // Empty strings, null, undefined and "auto" are converted to 0. + return !result || result === "auto" ? 0 : result; + }, + set: function( tween ) { + + // Use step hook for back compat. + // Use cssHook if its there. + // Use .style if available and use plain properties where available. + if ( jQuery.fx.step[ tween.prop ] ) { + jQuery.fx.step[ tween.prop ]( tween ); + } else if ( tween.elem.nodeType === 1 && + ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || + jQuery.cssHooks[ tween.prop ] ) ) { + jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); + } else { + tween.elem[ tween.prop ] = tween.now; + } + } + } +}; + +// Support: IE <=9 only +// Panic based approach to setting things on disconnected nodes +Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { + set: function( tween ) { + if ( tween.elem.nodeType && tween.elem.parentNode ) { + tween.elem[ tween.prop ] = tween.now; + } + } +}; + +jQuery.easing = { + linear: function( p ) { + return p; + }, + swing: function( p ) { + return 0.5 - Math.cos( p * Math.PI ) / 2; + }, + _default: "swing" +}; + +jQuery.fx = Tween.prototype.init; + +// Back compat <1.8 extension point +jQuery.fx.step = {}; + + + + +var + fxNow, inProgress, + rfxtypes = /^(?:toggle|show|hide)$/, + rrun = /queueHooks$/; + +function schedule() { + if ( inProgress ) { + if ( document.hidden === false && window.requestAnimationFrame ) { + window.requestAnimationFrame( schedule ); + } else { + window.setTimeout( schedule, jQuery.fx.interval ); + } + + jQuery.fx.tick(); + } +} + +// Animations created synchronously will run synchronously +function createFxNow() { + window.setTimeout( function() { + fxNow = undefined; + } ); + return ( fxNow = jQuery.now() ); +} + +// Generate parameters to create a standard animation +function genFx( type, includeWidth ) { + var which, + i = 0, + attrs = { height: type }; + + // If we include width, step value is 1 to do all cssExpand values, + // otherwise step value is 2 to skip over Left and Right + includeWidth = includeWidth ? 1 : 0; + for ( ; i < 4; i += 2 - includeWidth ) { + which = cssExpand[ i ]; + attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; + } + + if ( includeWidth ) { + attrs.opacity = attrs.width = type; + } + + return attrs; +} + +function createTween( value, prop, animation ) { + var tween, + collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ), + index = 0, + length = collection.length; + for ( ; index < length; index++ ) { + if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) { + + // We're done with this property + return tween; + } + } +} + +function defaultPrefilter( elem, props, opts ) { + var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display, + isBox = "width" in props || "height" in props, + anim = this, + orig = {}, + style = elem.style, + hidden = elem.nodeType && isHiddenWithinTree( elem ), + dataShow = dataPriv.get( elem, "fxshow" ); + + // Queue-skipping animations hijack the fx hooks + if ( !opts.queue ) { + hooks = jQuery._queueHooks( elem, "fx" ); + if ( hooks.unqueued == null ) { + hooks.unqueued = 0; + oldfire = hooks.empty.fire; + hooks.empty.fire = function() { + if ( !hooks.unqueued ) { + oldfire(); + } + }; + } + hooks.unqueued++; + + anim.always( function() { + + // Ensure the complete handler is called before this completes + anim.always( function() { + hooks.unqueued--; + if ( !jQuery.queue( elem, "fx" ).length ) { + hooks.empty.fire(); + } + } ); + } ); + } + + // Detect show/hide animations + for ( prop in props ) { + value = props[ prop ]; + if ( rfxtypes.test( value ) ) { + delete props[ prop ]; + toggle = toggle || value === "toggle"; + if ( value === ( hidden ? "hide" : "show" ) ) { + + // Pretend to be hidden if this is a "show" and + // there is still data from a stopped show/hide + if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { + hidden = true; + + // Ignore all other no-op show/hide data + } else { + continue; + } + } + orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); + } + } + + // Bail out if this is a no-op like .hide().hide() + propTween = !jQuery.isEmptyObject( props ); + if ( !propTween && jQuery.isEmptyObject( orig ) ) { + return; + } + + // Restrict "overflow" and "display" styles during box animations + if ( isBox && elem.nodeType === 1 ) { + + // Support: IE <=9 - 11, Edge 12 - 13 + // Record all 3 overflow attributes because IE does not infer the shorthand + // from identically-valued overflowX and overflowY + opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; + + // Identify a display type, preferring old show/hide data over the CSS cascade + restoreDisplay = dataShow && dataShow.display; + if ( restoreDisplay == null ) { + restoreDisplay = dataPriv.get( elem, "display" ); + } + display = jQuery.css( elem, "display" ); + if ( display === "none" ) { + if ( restoreDisplay ) { + display = restoreDisplay; + } else { + + // Get nonempty value(s) by temporarily forcing visibility + showHide( [ elem ], true ); + restoreDisplay = elem.style.display || restoreDisplay; + display = jQuery.css( elem, "display" ); + showHide( [ elem ] ); + } + } + + // Animate inline elements as inline-block + if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) { + if ( jQuery.css( elem, "float" ) === "none" ) { + + // Restore the original display value at the end of pure show/hide animations + if ( !propTween ) { + anim.done( function() { + style.display = restoreDisplay; + } ); + if ( restoreDisplay == null ) { + display = style.display; + restoreDisplay = display === "none" ? "" : display; + } + } + style.display = "inline-block"; + } + } + } + + if ( opts.overflow ) { + style.overflow = "hidden"; + anim.always( function() { + style.overflow = opts.overflow[ 0 ]; + style.overflowX = opts.overflow[ 1 ]; + style.overflowY = opts.overflow[ 2 ]; + } ); + } + + // Implement show/hide animations + propTween = false; + for ( prop in orig ) { + + // General show/hide setup for this element animation + if ( !propTween ) { + if ( dataShow ) { + if ( "hidden" in dataShow ) { + hidden = dataShow.hidden; + } + } else { + dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } ); + } + + // Store hidden/visible for toggle so `.stop().toggle()` "reverses" + if ( toggle ) { + dataShow.hidden = !hidden; + } + + // Show elements before animating them + if ( hidden ) { + showHide( [ elem ], true ); + } + + /* eslint-disable no-loop-func */ + + anim.done( function() { + + /* eslint-enable no-loop-func */ + + // The final step of a "hide" animation is actually hiding the element + if ( !hidden ) { + showHide( [ elem ] ); + } + dataPriv.remove( elem, "fxshow" ); + for ( prop in orig ) { + jQuery.style( elem, prop, orig[ prop ] ); + } + } ); + } + + // Per-property setup + propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); + if ( !( prop in dataShow ) ) { + dataShow[ prop ] = propTween.start; + if ( hidden ) { + propTween.end = propTween.start; + propTween.start = 0; + } + } + } +} + +function propFilter( props, specialEasing ) { + var index, name, easing, value, hooks; + + // camelCase, specialEasing and expand cssHook pass + for ( index in props ) { + name = jQuery.camelCase( index ); + easing = specialEasing[ name ]; + value = props[ index ]; + if ( Array.isArray( value ) ) { + easing = value[ 1 ]; + value = props[ index ] = value[ 0 ]; + } + + if ( index !== name ) { + props[ name ] = value; + delete props[ index ]; + } + + hooks = jQuery.cssHooks[ name ]; + if ( hooks && "expand" in hooks ) { + value = hooks.expand( value ); + delete props[ name ]; + + // Not quite $.extend, this won't overwrite existing keys. + // Reusing 'index' because we have the correct "name" + for ( index in value ) { + if ( !( index in props ) ) { + props[ index ] = value[ index ]; + specialEasing[ index ] = easing; + } + } + } else { + specialEasing[ name ] = easing; + } + } +} + +function Animation( elem, properties, options ) { + var result, + stopped, + index = 0, + length = Animation.prefilters.length, + deferred = jQuery.Deferred().always( function() { + + // Don't match elem in the :animated selector + delete tick.elem; + } ), + tick = function() { + if ( stopped ) { + return false; + } + var currentTime = fxNow || createFxNow(), + remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), + + // Support: Android 2.3 only + // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497) + temp = remaining / animation.duration || 0, + percent = 1 - temp, + index = 0, + length = animation.tweens.length; + + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( percent ); + } + + deferred.notifyWith( elem, [ animation, percent, remaining ] ); + + // If there's more to do, yield + if ( percent < 1 && length ) { + return remaining; + } + + // If this was an empty animation, synthesize a final progress notification + if ( !length ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + } + + // Resolve the animation and report its conclusion + deferred.resolveWith( elem, [ animation ] ); + return false; + }, + animation = deferred.promise( { + elem: elem, + props: jQuery.extend( {}, properties ), + opts: jQuery.extend( true, { + specialEasing: {}, + easing: jQuery.easing._default + }, options ), + originalProperties: properties, + originalOptions: options, + startTime: fxNow || createFxNow(), + duration: options.duration, + tweens: [], + createTween: function( prop, end ) { + var tween = jQuery.Tween( elem, animation.opts, prop, end, + animation.opts.specialEasing[ prop ] || animation.opts.easing ); + animation.tweens.push( tween ); + return tween; + }, + stop: function( gotoEnd ) { + var index = 0, + + // If we are going to the end, we want to run all the tweens + // otherwise we skip this part + length = gotoEnd ? animation.tweens.length : 0; + if ( stopped ) { + return this; + } + stopped = true; + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( 1 ); + } + + // Resolve when we played the last frame; otherwise, reject + if ( gotoEnd ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + deferred.resolveWith( elem, [ animation, gotoEnd ] ); + } else { + deferred.rejectWith( elem, [ animation, gotoEnd ] ); + } + return this; + } + } ), + props = animation.props; + + propFilter( props, animation.opts.specialEasing ); + + for ( ; index < length; index++ ) { + result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts ); + if ( result ) { + if ( jQuery.isFunction( result.stop ) ) { + jQuery._queueHooks( animation.elem, animation.opts.queue ).stop = + jQuery.proxy( result.stop, result ); + } + return result; + } + } + + jQuery.map( props, createTween, animation ); + + if ( jQuery.isFunction( animation.opts.start ) ) { + animation.opts.start.call( elem, animation ); + } + + // Attach callbacks from options + animation + .progress( animation.opts.progress ) + .done( animation.opts.done, animation.opts.complete ) + .fail( animation.opts.fail ) + .always( animation.opts.always ); + + jQuery.fx.timer( + jQuery.extend( tick, { + elem: elem, + anim: animation, + queue: animation.opts.queue + } ) + ); + + return animation; +} + +jQuery.Animation = jQuery.extend( Animation, { + + tweeners: { + "*": [ function( prop, value ) { + var tween = this.createTween( prop, value ); + adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween ); + return tween; + } ] + }, + + tweener: function( props, callback ) { + if ( jQuery.isFunction( props ) ) { + callback = props; + props = [ "*" ]; + } else { + props = props.match( rnothtmlwhite ); + } + + var prop, + index = 0, + length = props.length; + + for ( ; index < length; index++ ) { + prop = props[ index ]; + Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || []; + Animation.tweeners[ prop ].unshift( callback ); + } + }, + + prefilters: [ defaultPrefilter ], + + prefilter: function( callback, prepend ) { + if ( prepend ) { + Animation.prefilters.unshift( callback ); + } else { + Animation.prefilters.push( callback ); + } + } +} ); + +jQuery.speed = function( speed, easing, fn ) { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { + complete: fn || !fn && easing || + jQuery.isFunction( speed ) && speed, + duration: speed, + easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing + }; + + // Go to the end state if fx are off + if ( jQuery.fx.off ) { + opt.duration = 0; + + } else { + if ( typeof opt.duration !== "number" ) { + if ( opt.duration in jQuery.fx.speeds ) { + opt.duration = jQuery.fx.speeds[ opt.duration ]; + + } else { + opt.duration = jQuery.fx.speeds._default; + } + } + } + + // Normalize opt.queue - true/undefined/null -> "fx" + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; + } + + // Queueing + opt.old = opt.complete; + + opt.complete = function() { + if ( jQuery.isFunction( opt.old ) ) { + opt.old.call( this ); + } + + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); + } + }; + + return opt; +}; + +jQuery.fn.extend( { + fadeTo: function( speed, to, easing, callback ) { + + // Show any hidden elements after setting opacity to 0 + return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show() + + // Animate to the value specified + .end().animate( { opacity: to }, speed, easing, callback ); + }, + animate: function( prop, speed, easing, callback ) { + var empty = jQuery.isEmptyObject( prop ), + optall = jQuery.speed( speed, easing, callback ), + doAnimation = function() { + + // Operate on a copy of prop so per-property easing won't be lost + var anim = Animation( this, jQuery.extend( {}, prop ), optall ); + + // Empty animations, or finishing resolves immediately + if ( empty || dataPriv.get( this, "finish" ) ) { + anim.stop( true ); + } + }; + doAnimation.finish = doAnimation; + + return empty || optall.queue === false ? + this.each( doAnimation ) : + this.queue( optall.queue, doAnimation ); + }, + stop: function( type, clearQueue, gotoEnd ) { + var stopQueue = function( hooks ) { + var stop = hooks.stop; + delete hooks.stop; + stop( gotoEnd ); + }; + + if ( typeof type !== "string" ) { + gotoEnd = clearQueue; + clearQueue = type; + type = undefined; + } + if ( clearQueue && type !== false ) { + this.queue( type || "fx", [] ); + } + + return this.each( function() { + var dequeue = true, + index = type != null && type + "queueHooks", + timers = jQuery.timers, + data = dataPriv.get( this ); + + if ( index ) { + if ( data[ index ] && data[ index ].stop ) { + stopQueue( data[ index ] ); + } + } else { + for ( index in data ) { + if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { + stopQueue( data[ index ] ); + } + } + } + + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && + ( type == null || timers[ index ].queue === type ) ) { + + timers[ index ].anim.stop( gotoEnd ); + dequeue = false; + timers.splice( index, 1 ); + } + } + + // Start the next in the queue if the last step wasn't forced. + // Timers currently will call their complete callbacks, which + // will dequeue but only if they were gotoEnd. + if ( dequeue || !gotoEnd ) { + jQuery.dequeue( this, type ); + } + } ); + }, + finish: function( type ) { + if ( type !== false ) { + type = type || "fx"; + } + return this.each( function() { + var index, + data = dataPriv.get( this ), + queue = data[ type + "queue" ], + hooks = data[ type + "queueHooks" ], + timers = jQuery.timers, + length = queue ? queue.length : 0; + + // Enable finishing flag on private data + data.finish = true; + + // Empty the queue first + jQuery.queue( this, type, [] ); + + if ( hooks && hooks.stop ) { + hooks.stop.call( this, true ); + } + + // Look for any active animations, and finish them + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && timers[ index ].queue === type ) { + timers[ index ].anim.stop( true ); + timers.splice( index, 1 ); + } + } + + // Look for any animations in the old queue and finish them + for ( index = 0; index < length; index++ ) { + if ( queue[ index ] && queue[ index ].finish ) { + queue[ index ].finish.call( this ); + } + } + + // Turn off finishing flag + delete data.finish; + } ); + } +} ); + +jQuery.each( [ "toggle", "show", "hide" ], function( i, name ) { + var cssFn = jQuery.fn[ name ]; + jQuery.fn[ name ] = function( speed, easing, callback ) { + return speed == null || typeof speed === "boolean" ? + cssFn.apply( this, arguments ) : + this.animate( genFx( name, true ), speed, easing, callback ); + }; +} ); + +// Generate shortcuts for custom animations +jQuery.each( { + slideDown: genFx( "show" ), + slideUp: genFx( "hide" ), + slideToggle: genFx( "toggle" ), + fadeIn: { opacity: "show" }, + fadeOut: { opacity: "hide" }, + fadeToggle: { opacity: "toggle" } +}, function( name, props ) { + jQuery.fn[ name ] = function( speed, easing, callback ) { + return this.animate( props, speed, easing, callback ); + }; +} ); + +jQuery.timers = []; +jQuery.fx.tick = function() { + var timer, + i = 0, + timers = jQuery.timers; + + fxNow = jQuery.now(); + + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + + // Run the timer and safely remove it when done (allowing for external removal) + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); + } + } + + if ( !timers.length ) { + jQuery.fx.stop(); + } + fxNow = undefined; +}; + +jQuery.fx.timer = function( timer ) { + jQuery.timers.push( timer ); + jQuery.fx.start(); +}; + +jQuery.fx.interval = 13; +jQuery.fx.start = function() { + if ( inProgress ) { + return; + } + + inProgress = true; + schedule(); +}; + +jQuery.fx.stop = function() { + inProgress = null; +}; + +jQuery.fx.speeds = { + slow: 600, + fast: 200, + + // Default speed + _default: 400 +}; + + +// Based off of the plugin by Clint Helfers, with permission. +// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/ +jQuery.fn.delay = function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = window.setTimeout( next, time ); + hooks.stop = function() { + window.clearTimeout( timeout ); + }; + } ); +}; + + +( function() { + var input = document.createElement( "input" ), + select = document.createElement( "select" ), + opt = select.appendChild( document.createElement( "option" ) ); + + input.type = "checkbox"; + + // Support: Android <=4.3 only + // Default value for a checkbox should be "on" + support.checkOn = input.value !== ""; + + // Support: IE <=11 only + // Must access selectedIndex to make default options select + support.optSelected = opt.selected; + + // Support: IE <=11 only + // An input loses its value after becoming a radio + input = document.createElement( "input" ); + input.value = "t"; + input.type = "radio"; + support.radioValue = input.value === "t"; +} )(); + + +var boolHook, + attrHandle = jQuery.expr.attrHandle; + +jQuery.fn.extend( { + attr: function( name, value ) { + return access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each( function() { + jQuery.removeAttr( this, name ); + } ); + } +} ); + +jQuery.extend( { + attr: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set attributes on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + // Attribute hooks are determined by the lowercase version + // Grab necessary hook if one is defined + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + hooks = jQuery.attrHooks[ name.toLowerCase() ] || + ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined ); + } + + if ( value !== undefined ) { + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + } + + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + elem.setAttribute( name, value + "" ); + return value; + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + ret = jQuery.find.attr( elem, name ); + + // Non-existent attributes return null, we normalize to undefined + return ret == null ? undefined : ret; + }, + + attrHooks: { + type: { + set: function( elem, value ) { + if ( !support.radioValue && value === "radio" && + nodeName( elem, "input" ) ) { + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + } + }, + + removeAttr: function( elem, value ) { + var name, + i = 0, + + // Attribute names can contain non-HTML whitespace characters + // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 + attrNames = value && value.match( rnothtmlwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( ( name = attrNames[ i++ ] ) ) { + elem.removeAttribute( name ); + } + } + } +} ); + +// Hooks for boolean attributes +boolHook = { + set: function( elem, value, name ) { + if ( value === false ) { + + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + elem.setAttribute( name, name ); + } + return name; + } +}; + +jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) { + var getter = attrHandle[ name ] || jQuery.find.attr; + + attrHandle[ name ] = function( elem, name, isXML ) { + var ret, handle, + lowercaseName = name.toLowerCase(); + + if ( !isXML ) { + + // Avoid an infinite loop by temporarily removing this function from the getter + handle = attrHandle[ lowercaseName ]; + attrHandle[ lowercaseName ] = ret; + ret = getter( elem, name, isXML ) != null ? + lowercaseName : + null; + attrHandle[ lowercaseName ] = handle; + } + return ret; + }; +} ); + + + + +var rfocusable = /^(?:input|select|textarea|button)$/i, + rclickable = /^(?:a|area)$/i; + +jQuery.fn.extend( { + prop: function( name, value ) { + return access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + return this.each( function() { + delete this[ jQuery.propFix[ name ] || name ]; + } ); + } +} ); + +jQuery.extend( { + prop: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set properties on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + return ( elem[ name ] = value ); + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + return elem[ name ]; + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + + // Support: IE <=9 - 11 only + // elem.tabIndex doesn't always return the + // correct value when it hasn't been explicitly set + // https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + // Use proper attribute retrieval(#12072) + var tabindex = jQuery.find.attr( elem, "tabindex" ); + + if ( tabindex ) { + return parseInt( tabindex, 10 ); + } + + if ( + rfocusable.test( elem.nodeName ) || + rclickable.test( elem.nodeName ) && + elem.href + ) { + return 0; + } + + return -1; + } + } + }, + + propFix: { + "for": "htmlFor", + "class": "className" + } +} ); + +// Support: IE <=11 only +// Accessing the selectedIndex property +// forces the browser to respect setting selected +// on the option +// The getter ensures a default option is selected +// when in an optgroup +// eslint rule "no-unused-expressions" is disabled for this code +// since it considers such accessions noop +if ( !support.optSelected ) { + jQuery.propHooks.selected = { + get: function( elem ) { + + /* eslint no-unused-expressions: "off" */ + + var parent = elem.parentNode; + if ( parent && parent.parentNode ) { + parent.parentNode.selectedIndex; + } + return null; + }, + set: function( elem ) { + + /* eslint no-unused-expressions: "off" */ + + var parent = elem.parentNode; + if ( parent ) { + parent.selectedIndex; + + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + } + }; +} + +jQuery.each( [ + "tabIndex", + "readOnly", + "maxLength", + "cellSpacing", + "cellPadding", + "rowSpan", + "colSpan", + "useMap", + "frameBorder", + "contentEditable" +], function() { + jQuery.propFix[ this.toLowerCase() ] = this; +} ); + + + + + // Strip and collapse whitespace according to HTML spec + // https://html.spec.whatwg.org/multipage/infrastructure.html#strip-and-collapse-whitespace + function stripAndCollapse( value ) { + var tokens = value.match( rnothtmlwhite ) || []; + return tokens.join( " " ); + } + + +function getClass( elem ) { + return elem.getAttribute && elem.getAttribute( "class" ) || ""; +} + +jQuery.fn.extend( { + addClass: function( value ) { + var classes, elem, cur, curValue, clazz, j, finalValue, + i = 0; + + if ( jQuery.isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + if ( typeof value === "string" && value ) { + classes = value.match( rnothtmlwhite ) || []; + + while ( ( elem = this[ i++ ] ) ) { + curValue = getClass( elem ); + cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + j = 0; + while ( ( clazz = classes[ j++ ] ) ) { + if ( cur.indexOf( " " + clazz + " " ) < 0 ) { + cur += clazz + " "; + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + elem.setAttribute( "class", finalValue ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classes, elem, cur, curValue, clazz, j, finalValue, + i = 0; + + if ( jQuery.isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + if ( !arguments.length ) { + return this.attr( "class", "" ); + } + + if ( typeof value === "string" && value ) { + classes = value.match( rnothtmlwhite ) || []; + + while ( ( elem = this[ i++ ] ) ) { + curValue = getClass( elem ); + + // This expression is here for better compressibility (see addClass) + cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + j = 0; + while ( ( clazz = classes[ j++ ] ) ) { + + // Remove *all* instances + while ( cur.indexOf( " " + clazz + " " ) > -1 ) { + cur = cur.replace( " " + clazz + " ", " " ); + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + elem.setAttribute( "class", finalValue ); + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value; + + if ( typeof stateVal === "boolean" && type === "string" ) { + return stateVal ? this.addClass( value ) : this.removeClass( value ); + } + + if ( jQuery.isFunction( value ) ) { + return this.each( function( i ) { + jQuery( this ).toggleClass( + value.call( this, i, getClass( this ), stateVal ), + stateVal + ); + } ); + } + + return this.each( function() { + var className, i, self, classNames; + + if ( type === "string" ) { + + // Toggle individual class names + i = 0; + self = jQuery( this ); + classNames = value.match( rnothtmlwhite ) || []; + + while ( ( className = classNames[ i++ ] ) ) { + + // Check each className given, space separated list + if ( self.hasClass( className ) ) { + self.removeClass( className ); + } else { + self.addClass( className ); + } + } + + // Toggle whole class name + } else if ( value === undefined || type === "boolean" ) { + className = getClass( this ); + if ( className ) { + + // Store className if set + dataPriv.set( this, "__className__", className ); + } + + // If the element has a class name or if we're passed `false`, + // then remove the whole classname (if there was one, the above saved it). + // Otherwise bring back whatever was previously saved (if anything), + // falling back to the empty string if nothing was stored. + if ( this.setAttribute ) { + this.setAttribute( "class", + className || value === false ? + "" : + dataPriv.get( this, "__className__" ) || "" + ); + } + } + } ); + }, + + hasClass: function( selector ) { + var className, elem, + i = 0; + + className = " " + selector + " "; + while ( ( elem = this[ i++ ] ) ) { + if ( elem.nodeType === 1 && + ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { + return true; + } + } + + return false; + } +} ); + + + + +var rreturn = /\r/g; + +jQuery.fn.extend( { + val: function( value ) { + var hooks, ret, isFunction, + elem = this[ 0 ]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || + jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && + "get" in hooks && + ( ret = hooks.get( elem, "value" ) ) !== undefined + ) { + return ret; + } + + ret = elem.value; + + // Handle most common string cases + if ( typeof ret === "string" ) { + return ret.replace( rreturn, "" ); + } + + // Handle cases where value is null/undef or number + return ret == null ? "" : ret; + } + + return; + } + + isFunction = jQuery.isFunction( value ); + + return this.each( function( i ) { + var val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call( this, i, jQuery( this ).val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + + } else if ( typeof val === "number" ) { + val += ""; + + } else if ( Array.isArray( val ) ) { + val = jQuery.map( val, function( value ) { + return value == null ? "" : value + ""; + } ); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + } ); + } +} ); + +jQuery.extend( { + valHooks: { + option: { + get: function( elem ) { + + var val = jQuery.find.attr( elem, "value" ); + return val != null ? + val : + + // Support: IE <=10 - 11 only + // option.text throws exceptions (#14686, #14858) + // Strip and collapse whitespace + // https://html.spec.whatwg.org/#strip-and-collapse-whitespace + stripAndCollapse( jQuery.text( elem ) ); + } + }, + select: { + get: function( elem ) { + var value, option, i, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one", + values = one ? null : [], + max = one ? index + 1 : options.length; + + if ( index < 0 ) { + i = max; + + } else { + i = one ? index : 0; + } + + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Support: IE <=9 only + // IE8-9 doesn't update selected after form reset (#2551) + if ( ( option.selected || i === index ) && + + // Don't return options that are disabled or in a disabled optgroup + !option.disabled && + ( !option.parentNode.disabled || + !nodeName( option.parentNode, "optgroup" ) ) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var optionSet, option, + options = elem.options, + values = jQuery.makeArray( value ), + i = options.length; + + while ( i-- ) { + option = options[ i ]; + + /* eslint-disable no-cond-assign */ + + if ( option.selected = + jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 + ) { + optionSet = true; + } + + /* eslint-enable no-cond-assign */ + } + + // Force browsers to behave consistently when non-matching value is set + if ( !optionSet ) { + elem.selectedIndex = -1; + } + return values; + } + } + } +} ); + +// Radios and checkboxes getter/setter +jQuery.each( [ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + set: function( elem, value ) { + if ( Array.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); + } + } + }; + if ( !support.checkOn ) { + jQuery.valHooks[ this ].get = function( elem ) { + return elem.getAttribute( "value" ) === null ? "on" : elem.value; + }; + } +} ); + + + + +// Return jQuery for attributes-only inclusion + + +var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/; + +jQuery.extend( jQuery.event, { + + trigger: function( event, data, elem, onlyHandlers ) { + + var i, cur, tmp, bubbleType, ontype, handle, special, + eventPath = [ elem || document ], + type = hasOwn.call( event, "type" ) ? event.type : event, + namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; + + cur = tmp = elem = elem || document; + + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "." ) > -1 ) { + + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split( "." ); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf( ":" ) < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join( "." ); + event.rnamespace = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === ( elem.ownerDocument || document ) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + // Fire handlers on the event path + i = 0; + while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { + + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( dataPriv.get( cur, "events" ) || {} )[ event.type ] && + dataPriv.get( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && handle.apply && acceptData( cur ) ) { + event.result = handle.apply( cur, data ); + if ( event.result === false ) { + event.preventDefault(); + } + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( ( !special._default || + special._default.apply( eventPath.pop(), data ) === false ) && + acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name as the event. + // Don't do default actions on window, that's where global variables be (#6170) + if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + elem[ type ](); + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + // Piggyback on a donor event to simulate a different one + // Used only for `focus(in | out)` events + simulate: function( type, elem, event ) { + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true + } + ); + + jQuery.event.trigger( e, null, elem ); + } + +} ); + +jQuery.fn.extend( { + + trigger: function( type, data ) { + return this.each( function() { + jQuery.event.trigger( type, data, this ); + } ); + }, + triggerHandler: function( type, data ) { + var elem = this[ 0 ]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +} ); + + +jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup contextmenu" ).split( " " ), + function( i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( data, fn ) { + return arguments.length > 0 ? + this.on( name, null, data, fn ) : + this.trigger( name ); + }; +} ); + +jQuery.fn.extend( { + hover: function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + } +} ); + + + + +support.focusin = "onfocusin" in window; + + +// Support: Firefox <=44 +// Firefox doesn't have focus(in | out) events +// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 +// +// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1 +// focus(in | out) events fire after focus & blur events, +// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order +// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857 +if ( !support.focusin ) { + jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler on the document while someone wants focusin/focusout + var handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + var doc = this.ownerDocument || this, + attaches = dataPriv.access( doc, fix ); + + if ( !attaches ) { + doc.addEventListener( orig, handler, true ); + } + dataPriv.access( doc, fix, ( attaches || 0 ) + 1 ); + }, + teardown: function() { + var doc = this.ownerDocument || this, + attaches = dataPriv.access( doc, fix ) - 1; + + if ( !attaches ) { + doc.removeEventListener( orig, handler, true ); + dataPriv.remove( doc, fix ); + + } else { + dataPriv.access( doc, fix, attaches ); + } + } + }; + } ); +} +var location = window.location; + +var nonce = jQuery.now(); + +var rquery = ( /\?/ ); + + + +// Cross-browser xml parsing +jQuery.parseXML = function( data ) { + var xml; + if ( !data || typeof data !== "string" ) { + return null; + } + + // Support: IE 9 - 11 only + // IE throws on parseFromString with invalid input. + try { + xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); + } catch ( e ) { + xml = undefined; + } + + if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; +}; + + +var + rbracket = /\[\]$/, + rCRLF = /\r?\n/g, + rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, + rsubmittable = /^(?:input|select|textarea|keygen)/i; + +function buildParams( prefix, obj, traditional, add ) { + var name; + + if ( Array.isArray( obj ) ) { + + // Serialize array item. + jQuery.each( obj, function( i, v ) { + if ( traditional || rbracket.test( prefix ) ) { + + // Treat each array item as a scalar. + add( prefix, v ); + + } else { + + // Item is non-scalar (array or object), encode its numeric index. + buildParams( + prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]", + v, + traditional, + add + ); + } + } ); + + } else if ( !traditional && jQuery.type( obj ) === "object" ) { + + // Serialize object item. + for ( name in obj ) { + buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); + } + + } else { + + // Serialize scalar item. + add( prefix, obj ); + } +} + +// Serialize an array of form elements or a set of +// key/values into a query string +jQuery.param = function( a, traditional ) { + var prefix, + s = [], + add = function( key, valueOrFunction ) { + + // If value is a function, invoke it and use its return value + var value = jQuery.isFunction( valueOrFunction ) ? + valueOrFunction() : + valueOrFunction; + + s[ s.length ] = encodeURIComponent( key ) + "=" + + encodeURIComponent( value == null ? "" : value ); + }; + + // If an array was passed in, assume that it is an array of form elements. + if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { + + // Serialize the form elements + jQuery.each( a, function() { + add( this.name, this.value ); + } ); + + } else { + + // If traditional, encode the "old" way (the way 1.3.2 or older + // did it), otherwise encode params recursively. + for ( prefix in a ) { + buildParams( prefix, a[ prefix ], traditional, add ); + } + } + + // Return the resulting serialization + return s.join( "&" ); +}; + +jQuery.fn.extend( { + serialize: function() { + return jQuery.param( this.serializeArray() ); + }, + serializeArray: function() { + return this.map( function() { + + // Can add propHook for "elements" to filter or add form elements + var elements = jQuery.prop( this, "elements" ); + return elements ? jQuery.makeArray( elements ) : this; + } ) + .filter( function() { + var type = this.type; + + // Use .is( ":disabled" ) so that fieldset[disabled] works + return this.name && !jQuery( this ).is( ":disabled" ) && + rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && + ( this.checked || !rcheckableType.test( type ) ); + } ) + .map( function( i, elem ) { + var val = jQuery( this ).val(); + + if ( val == null ) { + return null; + } + + if ( Array.isArray( val ) ) { + return jQuery.map( val, function( val ) { + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ); + } + + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ).get(); + } +} ); + + +var + r20 = /%20/g, + rhash = /#.*$/, + rantiCache = /([?&])_=[^&]*/, + rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg, + + // #7653, #8125, #8152: local protocol detection + rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, + rnoContent = /^(?:GET|HEAD)$/, + rprotocol = /^\/\//, + + /* Prefilters + * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) + * 2) These are called: + * - BEFORE asking for a transport + * - AFTER param serialization (s.data is a string if s.processData is true) + * 3) key is the dataType + * 4) the catchall symbol "*" can be used + * 5) execution will start with transport dataType and THEN continue down to "*" if needed + */ + prefilters = {}, + + /* Transports bindings + * 1) key is the dataType + * 2) the catchall symbol "*" can be used + * 3) selection will start with transport dataType and THEN go to "*" if needed + */ + transports = {}, + + // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression + allTypes = "*/".concat( "*" ), + + // Anchor tag for parsing the document origin + originAnchor = document.createElement( "a" ); + originAnchor.href = location.href; + +// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport +function addToPrefiltersOrTransports( structure ) { + + // dataTypeExpression is optional and defaults to "*" + return function( dataTypeExpression, func ) { + + if ( typeof dataTypeExpression !== "string" ) { + func = dataTypeExpression; + dataTypeExpression = "*"; + } + + var dataType, + i = 0, + dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || []; + + if ( jQuery.isFunction( func ) ) { + + // For each dataType in the dataTypeExpression + while ( ( dataType = dataTypes[ i++ ] ) ) { + + // Prepend if requested + if ( dataType[ 0 ] === "+" ) { + dataType = dataType.slice( 1 ) || "*"; + ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func ); + + // Otherwise append + } else { + ( structure[ dataType ] = structure[ dataType ] || [] ).push( func ); + } + } + } + }; +} + +// Base inspection function for prefilters and transports +function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { + + var inspected = {}, + seekingTransport = ( structure === transports ); + + function inspect( dataType ) { + var selected; + inspected[ dataType ] = true; + jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { + var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); + if ( typeof dataTypeOrTransport === "string" && + !seekingTransport && !inspected[ dataTypeOrTransport ] ) { + + options.dataTypes.unshift( dataTypeOrTransport ); + inspect( dataTypeOrTransport ); + return false; + } else if ( seekingTransport ) { + return !( selected = dataTypeOrTransport ); + } + } ); + return selected; + } + + return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); +} + +// A special extend for ajax options +// that takes "flat" options (not to be deep extended) +// Fixes #9887 +function ajaxExtend( target, src ) { + var key, deep, + flatOptions = jQuery.ajaxSettings.flatOptions || {}; + + for ( key in src ) { + if ( src[ key ] !== undefined ) { + ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; + } + } + if ( deep ) { + jQuery.extend( true, target, deep ); + } + + return target; +} + +/* Handles responses to an ajax request: + * - finds the right dataType (mediates between content-type and expected dataType) + * - returns the corresponding response + */ +function ajaxHandleResponses( s, jqXHR, responses ) { + + var ct, type, finalDataType, firstDataType, + contents = s.contents, + dataTypes = s.dataTypes; + + // Remove auto dataType and get content-type in the process + while ( dataTypes[ 0 ] === "*" ) { + dataTypes.shift(); + if ( ct === undefined ) { + ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" ); + } + } + + // Check if we're dealing with a known content-type + if ( ct ) { + for ( type in contents ) { + if ( contents[ type ] && contents[ type ].test( ct ) ) { + dataTypes.unshift( type ); + break; + } + } + } + + // Check to see if we have a response for the expected dataType + if ( dataTypes[ 0 ] in responses ) { + finalDataType = dataTypes[ 0 ]; + } else { + + // Try convertible dataTypes + for ( type in responses ) { + if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) { + finalDataType = type; + break; + } + if ( !firstDataType ) { + firstDataType = type; + } + } + + // Or just use first one + finalDataType = finalDataType || firstDataType; + } + + // If we found a dataType + // We add the dataType to the list if needed + // and return the corresponding response + if ( finalDataType ) { + if ( finalDataType !== dataTypes[ 0 ] ) { + dataTypes.unshift( finalDataType ); + } + return responses[ finalDataType ]; + } +} + +/* Chain conversions given the request and the original response + * Also sets the responseXXX fields on the jqXHR instance + */ +function ajaxConvert( s, response, jqXHR, isSuccess ) { + var conv2, current, conv, tmp, prev, + converters = {}, + + // Work with a copy of dataTypes in case we need to modify it for conversion + dataTypes = s.dataTypes.slice(); + + // Create converters map with lowercased keys + if ( dataTypes[ 1 ] ) { + for ( conv in s.converters ) { + converters[ conv.toLowerCase() ] = s.converters[ conv ]; + } + } + + current = dataTypes.shift(); + + // Convert to each sequential dataType + while ( current ) { + + if ( s.responseFields[ current ] ) { + jqXHR[ s.responseFields[ current ] ] = response; + } + + // Apply the dataFilter if provided + if ( !prev && isSuccess && s.dataFilter ) { + response = s.dataFilter( response, s.dataType ); + } + + prev = current; + current = dataTypes.shift(); + + if ( current ) { + + // There's only work to do if current dataType is non-auto + if ( current === "*" ) { + + current = prev; + + // Convert response if prev dataType is non-auto and differs from current + } else if ( prev !== "*" && prev !== current ) { + + // Seek a direct converter + conv = converters[ prev + " " + current ] || converters[ "* " + current ]; + + // If none found, seek a pair + if ( !conv ) { + for ( conv2 in converters ) { + + // If conv2 outputs current + tmp = conv2.split( " " ); + if ( tmp[ 1 ] === current ) { + + // If prev can be converted to accepted input + conv = converters[ prev + " " + tmp[ 0 ] ] || + converters[ "* " + tmp[ 0 ] ]; + if ( conv ) { + + // Condense equivalence converters + if ( conv === true ) { + conv = converters[ conv2 ]; + + // Otherwise, insert the intermediate dataType + } else if ( converters[ conv2 ] !== true ) { + current = tmp[ 0 ]; + dataTypes.unshift( tmp[ 1 ] ); + } + break; + } + } + } + } + + // Apply converter (if not an equivalence) + if ( conv !== true ) { + + // Unless errors are allowed to bubble, catch and return them + if ( conv && s.throws ) { + response = conv( response ); + } else { + try { + response = conv( response ); + } catch ( e ) { + return { + state: "parsererror", + error: conv ? e : "No conversion from " + prev + " to " + current + }; + } + } + } + } + } + } + + return { state: "success", data: response }; +} + +jQuery.extend( { + + // Counter for holding the number of active queries + active: 0, + + // Last-Modified header cache for next request + lastModified: {}, + etag: {}, + + ajaxSettings: { + url: location.href, + type: "GET", + isLocal: rlocalProtocol.test( location.protocol ), + global: true, + processData: true, + async: true, + contentType: "application/x-www-form-urlencoded; charset=UTF-8", + + /* + timeout: 0, + data: null, + dataType: null, + username: null, + password: null, + cache: null, + throws: false, + traditional: false, + headers: {}, + */ + + accepts: { + "*": allTypes, + text: "text/plain", + html: "text/html", + xml: "application/xml, text/xml", + json: "application/json, text/javascript" + }, + + contents: { + xml: /\bxml\b/, + html: /\bhtml/, + json: /\bjson\b/ + }, + + responseFields: { + xml: "responseXML", + text: "responseText", + json: "responseJSON" + }, + + // Data converters + // Keys separate source (or catchall "*") and destination types with a single space + converters: { + + // Convert anything to text + "* text": String, + + // Text to html (true = no transformation) + "text html": true, + + // Evaluate text as a json expression + "text json": JSON.parse, + + // Parse text as xml + "text xml": jQuery.parseXML + }, + + // For options that shouldn't be deep extended: + // you can add your own custom options here if + // and when you create one that shouldn't be + // deep extended (see ajaxExtend) + flatOptions: { + url: true, + context: true + } + }, + + // Creates a full fledged settings object into target + // with both ajaxSettings and settings fields. + // If target is omitted, writes into ajaxSettings. + ajaxSetup: function( target, settings ) { + return settings ? + + // Building a settings object + ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : + + // Extending ajaxSettings + ajaxExtend( jQuery.ajaxSettings, target ); + }, + + ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), + ajaxTransport: addToPrefiltersOrTransports( transports ), + + // Main method + ajax: function( url, options ) { + + // If url is an object, simulate pre-1.5 signature + if ( typeof url === "object" ) { + options = url; + url = undefined; + } + + // Force options to be an object + options = options || {}; + + var transport, + + // URL without anti-cache param + cacheURL, + + // Response headers + responseHeadersString, + responseHeaders, + + // timeout handle + timeoutTimer, + + // Url cleanup var + urlAnchor, + + // Request state (becomes false upon send and true upon completion) + completed, + + // To know if global events are to be dispatched + fireGlobals, + + // Loop variable + i, + + // uncached part of the url + uncached, + + // Create the final options object + s = jQuery.ajaxSetup( {}, options ), + + // Callbacks context + callbackContext = s.context || s, + + // Context for global events is callbackContext if it is a DOM node or jQuery collection + globalEventContext = s.context && + ( callbackContext.nodeType || callbackContext.jquery ) ? + jQuery( callbackContext ) : + jQuery.event, + + // Deferreds + deferred = jQuery.Deferred(), + completeDeferred = jQuery.Callbacks( "once memory" ), + + // Status-dependent callbacks + statusCode = s.statusCode || {}, + + // Headers (they are sent all at once) + requestHeaders = {}, + requestHeadersNames = {}, + + // Default abort message + strAbort = "canceled", + + // Fake xhr + jqXHR = { + readyState: 0, + + // Builds headers hashtable if needed + getResponseHeader: function( key ) { + var match; + if ( completed ) { + if ( !responseHeaders ) { + responseHeaders = {}; + while ( ( match = rheaders.exec( responseHeadersString ) ) ) { + responseHeaders[ match[ 1 ].toLowerCase() ] = match[ 2 ]; + } + } + match = responseHeaders[ key.toLowerCase() ]; + } + return match == null ? null : match; + }, + + // Raw string + getAllResponseHeaders: function() { + return completed ? responseHeadersString : null; + }, + + // Caches the header + setRequestHeader: function( name, value ) { + if ( completed == null ) { + name = requestHeadersNames[ name.toLowerCase() ] = + requestHeadersNames[ name.toLowerCase() ] || name; + requestHeaders[ name ] = value; + } + return this; + }, + + // Overrides response content-type header + overrideMimeType: function( type ) { + if ( completed == null ) { + s.mimeType = type; + } + return this; + }, + + // Status-dependent callbacks + statusCode: function( map ) { + var code; + if ( map ) { + if ( completed ) { + + // Execute the appropriate callbacks + jqXHR.always( map[ jqXHR.status ] ); + } else { + + // Lazy-add the new callbacks in a way that preserves old ones + for ( code in map ) { + statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; + } + } + } + return this; + }, + + // Cancel the request + abort: function( statusText ) { + var finalText = statusText || strAbort; + if ( transport ) { + transport.abort( finalText ); + } + done( 0, finalText ); + return this; + } + }; + + // Attach deferreds + deferred.promise( jqXHR ); + + // Add protocol if not provided (prefilters might expect it) + // Handle falsy url in the settings object (#10093: consistency with old signature) + // We also use the url parameter if available + s.url = ( ( url || s.url || location.href ) + "" ) + .replace( rprotocol, location.protocol + "//" ); + + // Alias method option to type as per ticket #12004 + s.type = options.method || options.type || s.method || s.type; + + // Extract dataTypes list + s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ]; + + // A cross-domain request is in order when the origin doesn't match the current origin. + if ( s.crossDomain == null ) { + urlAnchor = document.createElement( "a" ); + + // Support: IE <=8 - 11, Edge 12 - 13 + // IE throws exception on accessing the href property if url is malformed, + // e.g. http://example.com:80x/ + try { + urlAnchor.href = s.url; + + // Support: IE <=8 - 11 only + // Anchor's host property isn't correctly set when s.url is relative + urlAnchor.href = urlAnchor.href; + s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !== + urlAnchor.protocol + "//" + urlAnchor.host; + } catch ( e ) { + + // If there is an error parsing the URL, assume it is crossDomain, + // it can be rejected by the transport if it is invalid + s.crossDomain = true; + } + } + + // Convert data if not already a string + if ( s.data && s.processData && typeof s.data !== "string" ) { + s.data = jQuery.param( s.data, s.traditional ); + } + + // Apply prefilters + inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); + + // If request was aborted inside a prefilter, stop there + if ( completed ) { + return jqXHR; + } + + // We can fire global events as of now if asked to + // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118) + fireGlobals = jQuery.event && s.global; + + // Watch for a new set of requests + if ( fireGlobals && jQuery.active++ === 0 ) { + jQuery.event.trigger( "ajaxStart" ); + } + + // Uppercase the type + s.type = s.type.toUpperCase(); + + // Determine if request has content + s.hasContent = !rnoContent.test( s.type ); + + // Save the URL in case we're toying with the If-Modified-Since + // and/or If-None-Match header later on + // Remove hash to simplify url manipulation + cacheURL = s.url.replace( rhash, "" ); + + // More options handling for requests with no content + if ( !s.hasContent ) { + + // Remember the hash so we can put it back + uncached = s.url.slice( cacheURL.length ); + + // If data is available, append data to url + if ( s.data ) { + cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data; + + // #9682: remove data so that it's not used in an eventual retry + delete s.data; + } + + // Add or update anti-cache param if needed + if ( s.cache === false ) { + cacheURL = cacheURL.replace( rantiCache, "$1" ); + uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce++ ) + uncached; + } + + // Put hash and anti-cache on the URL that will be requested (gh-1732) + s.url = cacheURL + uncached; + + // Change '%20' to '+' if this is encoded form body content (gh-2658) + } else if ( s.data && s.processData && + ( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) { + s.data = s.data.replace( r20, "+" ); + } + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + if ( jQuery.lastModified[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); + } + if ( jQuery.etag[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); + } + } + + // Set the correct header, if data is being sent + if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { + jqXHR.setRequestHeader( "Content-Type", s.contentType ); + } + + // Set the Accepts header for the server, depending on the dataType + jqXHR.setRequestHeader( + "Accept", + s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ? + s.accepts[ s.dataTypes[ 0 ] ] + + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : + s.accepts[ "*" ] + ); + + // Check for headers option + for ( i in s.headers ) { + jqXHR.setRequestHeader( i, s.headers[ i ] ); + } + + // Allow custom headers/mimetypes and early abort + if ( s.beforeSend && + ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) { + + // Abort if not done already and return + return jqXHR.abort(); + } + + // Aborting is no longer a cancellation + strAbort = "abort"; + + // Install callbacks on deferreds + completeDeferred.add( s.complete ); + jqXHR.done( s.success ); + jqXHR.fail( s.error ); + + // Get transport + transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); + + // If no transport, we auto-abort + if ( !transport ) { + done( -1, "No Transport" ); + } else { + jqXHR.readyState = 1; + + // Send global event + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); + } + + // If request was aborted inside ajaxSend, stop there + if ( completed ) { + return jqXHR; + } + + // Timeout + if ( s.async && s.timeout > 0 ) { + timeoutTimer = window.setTimeout( function() { + jqXHR.abort( "timeout" ); + }, s.timeout ); + } + + try { + completed = false; + transport.send( requestHeaders, done ); + } catch ( e ) { + + // Rethrow post-completion exceptions + if ( completed ) { + throw e; + } + + // Propagate others as results + done( -1, e ); + } + } + + // Callback for when everything is done + function done( status, nativeStatusText, responses, headers ) { + var isSuccess, success, error, response, modified, + statusText = nativeStatusText; + + // Ignore repeat invocations + if ( completed ) { + return; + } + + completed = true; + + // Clear timeout if it exists + if ( timeoutTimer ) { + window.clearTimeout( timeoutTimer ); + } + + // Dereference transport for early garbage collection + // (no matter how long the jqXHR object will be used) + transport = undefined; + + // Cache response headers + responseHeadersString = headers || ""; + + // Set readyState + jqXHR.readyState = status > 0 ? 4 : 0; + + // Determine if successful + isSuccess = status >= 200 && status < 300 || status === 304; + + // Get response data + if ( responses ) { + response = ajaxHandleResponses( s, jqXHR, responses ); + } + + // Convert no matter what (that way responseXXX fields are always set) + response = ajaxConvert( s, response, jqXHR, isSuccess ); + + // If successful, handle type chaining + if ( isSuccess ) { + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + modified = jqXHR.getResponseHeader( "Last-Modified" ); + if ( modified ) { + jQuery.lastModified[ cacheURL ] = modified; + } + modified = jqXHR.getResponseHeader( "etag" ); + if ( modified ) { + jQuery.etag[ cacheURL ] = modified; + } + } + + // if no content + if ( status === 204 || s.type === "HEAD" ) { + statusText = "nocontent"; + + // if not modified + } else if ( status === 304 ) { + statusText = "notmodified"; + + // If we have data, let's convert it + } else { + statusText = response.state; + success = response.data; + error = response.error; + isSuccess = !error; + } + } else { + + // Extract error from statusText and normalize for non-aborts + error = statusText; + if ( status || !statusText ) { + statusText = "error"; + if ( status < 0 ) { + status = 0; + } + } + } + + // Set data for the fake xhr object + jqXHR.status = status; + jqXHR.statusText = ( nativeStatusText || statusText ) + ""; + + // Success/Error + if ( isSuccess ) { + deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); + } else { + deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); + } + + // Status-dependent callbacks + jqXHR.statusCode( statusCode ); + statusCode = undefined; + + if ( fireGlobals ) { + globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", + [ jqXHR, s, isSuccess ? success : error ] ); + } + + // Complete + completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); + + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); + + // Handle the global AJAX counter + if ( !( --jQuery.active ) ) { + jQuery.event.trigger( "ajaxStop" ); + } + } + } + + return jqXHR; + }, + + getJSON: function( url, data, callback ) { + return jQuery.get( url, data, callback, "json" ); + }, + + getScript: function( url, callback ) { + return jQuery.get( url, undefined, callback, "script" ); + } +} ); + +jQuery.each( [ "get", "post" ], function( i, method ) { + jQuery[ method ] = function( url, data, callback, type ) { + + // Shift arguments if data argument was omitted + if ( jQuery.isFunction( data ) ) { + type = type || callback; + callback = data; + data = undefined; + } + + // The url can be an options object (which then must have .url) + return jQuery.ajax( jQuery.extend( { + url: url, + type: method, + dataType: type, + data: data, + success: callback + }, jQuery.isPlainObject( url ) && url ) ); + }; +} ); + + +jQuery._evalUrl = function( url ) { + return jQuery.ajax( { + url: url, + + // Make this explicit, since user can override this through ajaxSetup (#11264) + type: "GET", + dataType: "script", + cache: true, + async: false, + global: false, + "throws": true + } ); +}; + + +jQuery.fn.extend( { + wrapAll: function( html ) { + var wrap; + + if ( this[ 0 ] ) { + if ( jQuery.isFunction( html ) ) { + html = html.call( this[ 0 ] ); + } + + // The elements to wrap the target around + wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); + + if ( this[ 0 ].parentNode ) { + wrap.insertBefore( this[ 0 ] ); + } + + wrap.map( function() { + var elem = this; + + while ( elem.firstElementChild ) { + elem = elem.firstElementChild; + } + + return elem; + } ).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each( function( i ) { + jQuery( this ).wrapInner( html.call( this, i ) ); + } ); + } + + return this.each( function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + } ); + }, + + wrap: function( html ) { + var isFunction = jQuery.isFunction( html ); + + return this.each( function( i ) { + jQuery( this ).wrapAll( isFunction ? html.call( this, i ) : html ); + } ); + }, + + unwrap: function( selector ) { + this.parent( selector ).not( "body" ).each( function() { + jQuery( this ).replaceWith( this.childNodes ); + } ); + return this; + } +} ); + + +jQuery.expr.pseudos.hidden = function( elem ) { + return !jQuery.expr.pseudos.visible( elem ); +}; +jQuery.expr.pseudos.visible = function( elem ) { + return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); +}; + + + + +jQuery.ajaxSettings.xhr = function() { + try { + return new window.XMLHttpRequest(); + } catch ( e ) {} +}; + +var xhrSuccessStatus = { + + // File protocol always yields status code 0, assume 200 + 0: 200, + + // Support: IE <=9 only + // #1450: sometimes IE returns 1223 when it should be 204 + 1223: 204 + }, + xhrSupported = jQuery.ajaxSettings.xhr(); + +support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); +support.ajax = xhrSupported = !!xhrSupported; + +jQuery.ajaxTransport( function( options ) { + var callback, errorCallback; + + // Cross domain only allowed if supported through XMLHttpRequest + if ( support.cors || xhrSupported && !options.crossDomain ) { + return { + send: function( headers, complete ) { + var i, + xhr = options.xhr(); + + xhr.open( + options.type, + options.url, + options.async, + options.username, + options.password + ); + + // Apply custom fields if provided + if ( options.xhrFields ) { + for ( i in options.xhrFields ) { + xhr[ i ] = options.xhrFields[ i ]; + } + } + + // Override mime type if needed + if ( options.mimeType && xhr.overrideMimeType ) { + xhr.overrideMimeType( options.mimeType ); + } + + // X-Requested-With header + // For cross-domain requests, seeing as conditions for a preflight are + // akin to a jigsaw puzzle, we simply never set it to be sure. + // (it can always be set on a per-request basis or even using ajaxSetup) + // For same-domain requests, won't change header if already provided. + if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) { + headers[ "X-Requested-With" ] = "XMLHttpRequest"; + } + + // Set headers + for ( i in headers ) { + xhr.setRequestHeader( i, headers[ i ] ); + } + + // Callback + callback = function( type ) { + return function() { + if ( callback ) { + callback = errorCallback = xhr.onload = + xhr.onerror = xhr.onabort = xhr.onreadystatechange = null; + + if ( type === "abort" ) { + xhr.abort(); + } else if ( type === "error" ) { + + // Support: IE <=9 only + // On a manual native abort, IE9 throws + // errors on any property access that is not readyState + if ( typeof xhr.status !== "number" ) { + complete( 0, "error" ); + } else { + complete( + + // File: protocol always yields status 0; see #8605, #14207 + xhr.status, + xhr.statusText + ); + } + } else { + complete( + xhrSuccessStatus[ xhr.status ] || xhr.status, + xhr.statusText, + + // Support: IE <=9 only + // IE9 has no XHR2 but throws on binary (trac-11426) + // For XHR2 non-text, let the caller handle it (gh-2498) + ( xhr.responseType || "text" ) !== "text" || + typeof xhr.responseText !== "string" ? + { binary: xhr.response } : + { text: xhr.responseText }, + xhr.getAllResponseHeaders() + ); + } + } + }; + }; + + // Listen to events + xhr.onload = callback(); + errorCallback = xhr.onerror = callback( "error" ); + + // Support: IE 9 only + // Use onreadystatechange to replace onabort + // to handle uncaught aborts + if ( xhr.onabort !== undefined ) { + xhr.onabort = errorCallback; + } else { + xhr.onreadystatechange = function() { + + // Check readyState before timeout as it changes + if ( xhr.readyState === 4 ) { + + // Allow onerror to be called first, + // but that will not handle a native abort + // Also, save errorCallback to a variable + // as xhr.onerror cannot be accessed + window.setTimeout( function() { + if ( callback ) { + errorCallback(); + } + } ); + } + }; + } + + // Create the abort callback + callback = callback( "abort" ); + + try { + + // Do send the request (this may raise an exception) + xhr.send( options.hasContent && options.data || null ); + } catch ( e ) { + + // #14683: Only rethrow if this hasn't been notified as an error yet + if ( callback ) { + throw e; + } + } + }, + + abort: function() { + if ( callback ) { + callback(); + } + } + }; + } +} ); + + + + +// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432) +jQuery.ajaxPrefilter( function( s ) { + if ( s.crossDomain ) { + s.contents.script = false; + } +} ); + +// Install script dataType +jQuery.ajaxSetup( { + accepts: { + script: "text/javascript, application/javascript, " + + "application/ecmascript, application/x-ecmascript" + }, + contents: { + script: /\b(?:java|ecma)script\b/ + }, + converters: { + "text script": function( text ) { + jQuery.globalEval( text ); + return text; + } + } +} ); + +// Handle cache's special case and crossDomain +jQuery.ajaxPrefilter( "script", function( s ) { + if ( s.cache === undefined ) { + s.cache = false; + } + if ( s.crossDomain ) { + s.type = "GET"; + } +} ); + +// Bind script tag hack transport +jQuery.ajaxTransport( "script", function( s ) { + + // This transport only deals with cross domain requests + if ( s.crossDomain ) { + var script, callback; + return { + send: function( _, complete ) { + script = jQuery( " + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + +

Index

+ +
+ A + | B + | C + | D + | E + | F + | G + | I + | K + | L + | M + | N + | O + | P + | Q + | R + | S + | T + | U + | V + | W + | X + +
+

A

+ + + +
+ +

B

+ + + +
+ +

C

+ + + +
+ +

D

+ + + +
+ +

E

+ + + +
+ +

F

+ + + +
+ +

G

+ + + +
+ +

I

+ + + +
+ +

K

+ + +
+ +

L

+ + + +
+ +

M

+ + + +
+ +

N

+ + + +
+ +

O

+ + + +
+ +

P

+ + + +
+ +

Q

+ + +
+ +

R

+ + + +
+ +

S

+ + + +
+ +

T

+ + + +
+ +

U

+ + + +
+ +

V

+ + + +
+ +

W

+ + + +
+ +

X

+ + + +
+ + + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/saml2/index.html b/docs/saml2/index.html new file mode 100644 index 00000000..4af91967 --- /dev/null +++ b/docs/saml2/index.html @@ -0,0 +1,133 @@ + + + + + + Welcome to SAML Python Toolkit’s documentation! — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/docs/saml2/modules.html b/docs/saml2/modules.html new file mode 100644 index 00000000..eb2a1aad --- /dev/null +++ b/docs/saml2/modules.html @@ -0,0 +1,129 @@ + + + + + + onelogin — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/saml2/objects.inv b/docs/saml2/objects.inv new file mode 100644 index 00000000..351e1a3c Binary files /dev/null and b/docs/saml2/objects.inv differ diff --git a/docs/saml2/onelogin.html b/docs/saml2/onelogin.html new file mode 100644 index 00000000..fda6e167 --- /dev/null +++ b/docs/saml2/onelogin.html @@ -0,0 +1,158 @@ + + + + + + onelogin package — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

onelogin package

+ +
+

Module contents

+

Copyright (c) 2010-2022 OneLogin, Inc. +Copyright (c) 2023 IAM DIgital Services, SL

+

MIT License

+

Add SAML support to your Python softwares using this library. +Forget those complicated libraries and use that open source +library.

+

SAML Python toolkit let you build a SP (Service Provider) +over your Python application and connect it to any IdP (Identity Provider).

+

Supports:

+
    +
  • 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).
  • +
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/saml2/onelogin.saml2.html b/docs/saml2/onelogin.saml2.html new file mode 100644 index 00000000..18374479 --- /dev/null +++ b/docs/saml2/onelogin.saml2.html @@ -0,0 +1,3811 @@ + + + + + + onelogin.saml2 package — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

onelogin.saml2 package

+
+

Submodules

+
+
+

onelogin.saml2.auth module

+

OneLogin_Saml2_Auth class

+

MIT License

+

Main class of Python Toolkit.

+

Initializes the SP SAML instance

+
+
+class onelogin.saml2.auth.OneLogin_Saml2_Auth(request_data, old_settings=None, custom_base_path=None)[source]
+

Bases: 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 +SAML Response, a Logout Request or a Logout Response).

+
+
+build_request_signature(saml_request, relay_state, sign_algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1')[source]
+

Builds the Signature of the SAML Request.

+ +++ + + + +
Parameters:
    +
  • saml_request (string) – The SAML Request
  • +
  • relay_state (string) – The target URL the user should be redirected to
  • +
  • sign_algorithm (string) – Signature algorithm method
  • +
+
+
+ +
+
+build_response_signature(saml_response, relay_state, sign_algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1')[source]
+

Builds the Signature of the SAML Response. +:param saml_request: The SAML Response +:type saml_request: string

+ +++ + + + +
Parameters:
    +
  • relay_state (string) – The target URL the user should be redirected to
  • +
  • sign_algorithm (string) – Signature algorithm method
  • +
+
+
+ +
+
+get_attribute(name)[source]
+

Returns the requested SAML attribute.

+ +++ + + + + + + + +
Parameters:name (string) – Name of the attribute
Returns:Attribute value(s) if exists or None
Return type:list
+
+ +
+
+get_attributes()[source]
+

Returns the set of SAML attributes.

+ +++ + + + + + +
Returns:SAML attributes
Return type:dict
+
+ +
+
+get_errors()[source]
+

Returns a list with code errors if something went wrong

+ +++ + + + + + +
Returns:List of errors
Return type:list
+
+ +
+
+get_friendlyname_attribute(friendlyname)[source]
+

Returns the requested SAML attribute searched by FriendlyName.

+ +++ + + + + + + + +
Parameters:friendlyname (string) – FriendlyName of the attribute
Returns:Attribute value(s) if exists or None
Return type:list
+
+ +
+
+get_friendlyname_attributes()[source]
+

Returns the set of SAML attributes indexed by FiendlyName.

+ +++ + + + + + +
Returns:SAML attributes
Return type:dict
+
+ +
+
+get_last_assertion_id()[source]
+
+++ + + + + + +
Returns:The ID of the last assertion processed.
Return type:string
+
+ +
+
+get_last_assertion_not_on_or_after()[source]
+

The NotOnOrAfter value of the valid SubjectConfirmationData node +(if any) of the last assertion processed

+
+ +
+
+get_last_authn_contexts()[source]
+
+++ + + + + + +
Returns:The list of authentication contexts sent in the last SAML Response.
Return type:list
+
+ +
+
+get_last_error_reason()[source]
+

Returns the reason for the last error

+ +++ + + + + + +
Returns:Reason of the last error
Return type:None | string
+
+ +
+
+get_last_message_id()[source]
+
+++ + + + + + +
Returns:The ID of the last Response SAML message processed.
Return type:string
+
+ +
+
+get_last_request_id()[source]
+
+++ + + + + + +
Returns:The ID of the last Request SAML message generated.
Return type:string
+
+ +
+
+get_last_request_xml()[source]
+

Retrieves the raw XML sent in the last SAML request

+ +++ + + + + + +
Returns:SAML request XML
Return type:string|None
+
+ +
+
+get_last_response_xml(pretty_print_if_possible=False)[source]
+

Retrieves the raw XML (decrypted) of the last SAML response, +or the last Logout Response generated or processed

+ +++ + + + + + +
Returns:SAML response XML
Return type:string|None
+
+ +
+
+get_nameid()[source]
+

Returns the nameID.

+ +++ + + + + + +
Returns:NameID
Return type:string|None
+
+ +
+
+get_nameid_format()[source]
+

Returns the nameID Format.

+ +++ + + + + + +
Returns:NameID Format
Return type:string|None
+
+ +
+
+get_nameid_nq()[source]
+

Returns the nameID NameQualifier of the Assertion.

+ +++ + + + + + +
Returns:NameID NameQualifier
Return type:string|None
+
+ +
+
+get_nameid_spnq()[source]
+

Returns the nameID SP NameQualifier of the Assertion.

+ +++ + + + + + +
Returns:NameID SP NameQualifier
Return type:string|None
+
+ +
+
+get_session_expiration()[source]
+

Returns the SessionNotOnOrAfter from the AuthnStatement. +:returns: The SessionNotOnOrAfter of the assertion +:rtype: unix/posix timestamp|None

+
+ +
+
+get_session_index()[source]
+

Returns the SessionIndex from the AuthnStatement. +:returns: The SessionIndex of the assertion +:rtype: string

+
+ +
+
+get_settings()[source]
+

Returns the settings info +:return: Setting info +:rtype: OneLogin_Saml2_Setting object

+
+ +
+
+get_slo_response_url()[source]
+

Gets the SLO return URL for IdP-initiated logout.

+ +++ + + + + + +
Returns:an URL, the SLO return endpoint of the IdP
Return type:string
+
+ +
+
+get_slo_url()[source]
+

Gets the IdP SLO URL.

+ +++ + + + + + +
Returns:An URL, the SLO endpoint of the IdP
Return type:string
+
+ +
+
+get_sso_url()[source]
+

Gets the IdP SSO URL.

+ +++ + + + + + +
Returns:An URL, the SSO endpoint of the IdP
Return type:string
+
+ +
+
+is_authenticated()[source]
+

Checks if the user is authenticated or not.

+ +++ + + + + + +
Returns:True if is authenticated, False if not
Return type:bool
+
+ +
+
+login(return_to=None, force_authn=False, is_passive=False, set_nameid_policy=True, name_id_value_req=None)[source]
+

Initiates the SSO process.

+ +++ + + + + + + + +
Parameters:
    +
  • return_to (string) – Optional argument. The target URL the user should be redirected to after login.
  • +
  • force_authn (bool) – Optional argument. When true the AuthNRequest will set the ForceAuthn=’true’.
  • +
  • is_passive (bool) – Optional argument. When true the AuthNRequest will set the Ispassive=’true’.
  • +
  • set_nameid_policy (bool) – Optional argument. When true the AuthNRequest will set a nameIdPolicy element.
  • +
  • name_id_value_req (string) – Optional argument. Indicates to the IdP the subject that should be authenticated
  • +
+
Returns:

Redirection URL

+
Return type:

string

+
+
+ +
+
+logout(return_to=None, name_id=None, session_index=None, nq=None, name_id_format=None, spnq=None)[source]
+

Initiates the SLO process.

+ +++ + + + + + + + + + + + +
Parameters:
    +
  • return_to (string) – Optional argument. The target URL the user should be redirected to after logout.
  • +
  • name_id (string) – The NameID that will be set in the LogoutRequest.
  • +
  • session_index (string) – 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 – SP Name Qualifier
  • +
+
Type:

string

+
Type:

string

+
Type:

string

+
Returns:

Redirection url

+
+
+ +
+
+process_response(request_id=None)[source]
+

Process the SAML Response sent by the IdP.

+ +++ + + + + + +
Parameters:request_id (string) – Is an optional argument. Is the ID of the AuthNRequest sent by this SP to the IdP.
Raises:OneLogin_Saml2_Error.SAML_RESPONSE_NOT_FOUND, when a POST with a SAMLResponse is not found
+
+ +
+
+process_slo(keep_local_session=False, request_id=None, delete_session_cb=None)[source]
+

Process the SAML Logout Response / Logout Request sent by the IdP.

+ +++ + + + + + +
Parameters:
    +
  • keep_local_session (bool) – When false will destroy the local session, otherwise will destroy it
  • +
  • request_id (string) – The ID of the LogoutRequest sent by this SP to the IdP
  • +
+
Returns:

Redirection URL

+
+
+ +
+
+redirect_to(url=None, parameters={})[source]
+

Redirects the user to the URL passed by parameter or to the URL that we defined in our SSO Request.

+ +++ + + + + + +
Parameters:
    +
  • url (string) – The target URL to redirect the user
  • +
  • parameters (dict) – Extra parameters to be passed as part of the URL
  • +
+
Returns:

Redirection URL

+
+
+ +
+
+set_strict(value)[source]
+

Set the strict mode active/disable

+ +++ + + + +
Parameters:value (bool) –
+
+ +
+ +
+
+

onelogin.saml2.authn_request module

+

OneLogin_Saml2_Authn_Request class

+

MIT License

+

AuthNRequest class of Python Toolkit.

+
+
+class onelogin.saml2.authn_request.OneLogin_Saml2_Authn_Request(settings, force_authn=False, is_passive=False, set_nameid_policy=True, name_id_value_req=None)[source]
+

Bases: object

+

This class handles an AuthNRequest. It builds an +AuthNRequest object.

+
+
+get_id()[source]
+

Returns the AuthNRequest ID. +:return: AuthNRequest ID +:rtype: string

+
+ +
+
+get_request(deflate=True)[source]
+

Returns unsigned AuthnRequest. +:param deflate: It makes the deflate process optional +:type: bool +:return: AuthnRequest maybe deflated and base64 encoded +:rtype: str object

+
+ +
+
+get_xml()[source]
+

Returns the XML that will be sent as part of the request +:return: XML request body +:rtype: string

+
+ +
+ +
+
+

onelogin.saml2.constants module

+

OneLogin_Saml2_Constants class

+

MIT License

+

Constants class of Python Toolkit.

+
+
+class onelogin.saml2.constants.OneLogin_Saml2_Constants[source]
+

Bases: object

+

This class defines all the constants that will be used +in the Python Toolkit.

+
+
+AC_KERBEROS = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Kerberos'
+
+ +
+
+AC_PASSWORD = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password'
+
+ +
+
+AC_PASSWORD_PROTECTED = 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'
+
+ +
+
+AC_SMARTCARD = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Smartcard'
+
+ +
+
+AC_UNSPECIFIED = 'urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified'
+
+ +
+
+AC_X509 = 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509'
+
+ +
+
+AES128_CBC = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'
+
+ +
+
+AES192_CBC = 'http://www.w3.org/2001/04/xmlenc#aes192-cbc'
+
+ +
+
+AES256_CBC = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc'
+
+ +
+
+ALLOWED_CLOCK_DRIFT = 300
+
+ +
+
+ATTRNAME_FORMAT_BASIC = 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic'
+
+ +
+
+ATTRNAME_FORMAT_UNSPECIFIED = 'urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified'
+
+ +
+
+ATTRNAME_FORMAT_URI = 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri'
+
+ +
+
+BINDING_DEFLATE = 'urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE'
+
+ +
+
+BINDING_HTTP_ARTIFACT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact'
+
+ +
+
+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'
+
+ +
+
+BINDING_SOAP = 'urn:oasis:names:tc:SAML:2.0:bindings:SOAP'
+
+ +
+
+CM_BEARER = 'urn:oasis:names:tc:SAML:2.0:cm:bearer'
+
+ +
+
+CM_HOLDER_KEY = 'urn:oasis:names:tc:SAML:2.0:cm:holder-of-key'
+
+ +
+
+CM_SENDER_VOUCHES = 'urn:oasis:names:tc:SAML:2.0:cm:sender-vouches'
+
+ +
+
+DEPRECATED_ALGORITHMS = ['http://www.w3.org/2000/09/xmldsig#dsa-sha1', 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', 'http://www.w3.org/2000/09/xmldsig#sha1']
+
+ +
+
+DSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1'
+
+ +
+
+NAMEID_EMAIL_ADDRESS = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
+
+ +
+
+NAMEID_ENCRYPTED = 'urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted'
+
+ +
+
+NAMEID_ENTITY = 'urn:oasis:names:tc:SAML:2.0:nameid-format:entity'
+
+ +
+
+NAMEID_KERBEROS = 'urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos'
+
+ +
+
+NAMEID_PERSISTENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
+
+ +
+
+NAMEID_TRANSIENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+
+ +
+
+NAMEID_UNSPECIFIED = 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified'
+
+ +
+
+NAMEID_WINDOWS_DOMAIN_QUALIFIED_NAME = 'urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName'
+
+ +
+
+NAMEID_X509_SUBJECT_NAME = 'urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName'
+
+ +
+
+NSMAP = {'ds': 'http://www.w3.org/2000/09/xmldsig#', 'md': 'urn:oasis:names:tc:SAML:2.0:metadata', 'saml': 'urn:oasis:names:tc:SAML:2.0:assertion', 'samlp': 'urn:oasis:names:tc:SAML:2.0:protocol', 'xenc': 'http://www.w3.org/2001/04/xmlenc#'}
+
+ +
+
+NS_DS = 'http://www.w3.org/2000/09/xmldsig#'
+
+ +
+
+NS_MD = 'urn:oasis:names:tc:SAML:2.0:metadata'
+
+ +
+
+NS_PREFIX_DS = 'ds'
+
+ +
+
+NS_PREFIX_MD = 'md'
+
+ +
+
+NS_PREFIX_SAML = 'saml'
+
+ +
+
+NS_PREFIX_SAMLP = 'samlp'
+
+ +
+
+NS_PREFIX_XENC = 'xenc'
+
+ +
+
+NS_PREFIX_XS = 'xs'
+
+ +
+
+NS_PREFIX_XSI = 'xsi'
+
+ +
+
+NS_SAML = 'urn:oasis:names:tc:SAML:2.0:assertion'
+
+ +
+
+NS_SAMLP = 'urn:oasis:names:tc:SAML:2.0:protocol'
+
+ +
+
+NS_SOAP = 'http://schemas.xmlsoap.org/soap/envelope/'
+
+ +
+
+NS_XENC = 'http://www.w3.org/2001/04/xmlenc#'
+
+ +
+
+NS_XS = 'http://www.w3.org/2001/XMLSchema'
+
+ +
+
+NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
+
+ +
+
+RSA_1_5 = 'http://www.w3.org/2001/04/xmlenc#rsa-1_5'
+
+ +
+
+RSA_OAEP_MGF1P = 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p'
+
+ +
+
+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'
+
+ +
+
+RSA_SHA512 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512'
+
+ +
+
+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/xmldsig-more#sha384'
+
+ +
+
+SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512'
+
+ +
+
+STATUS_NO_PASSIVE = 'urn:oasis:names:tc:SAML:2.0:status:NoPassive'
+
+ +
+
+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'
+
+ +
+
+STATUS_REQUESTER = 'urn:oasis:names:tc:SAML:2.0:status:Requester'
+
+ +
+
+STATUS_RESPONDER = 'urn:oasis:names:tc:SAML:2.0:status:Responder'
+
+ +
+
+STATUS_SUCCESS = 'urn:oasis:names:tc:SAML:2.0:status:Success'
+
+ +
+
+STATUS_VERSION_MISMATCH = 'urn:oasis:names:tc:SAML:2.0:status:VersionMismatch'
+
+ +
+
+TRIPLEDES_CBC = 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc'
+
+ +
+
+XML = 'http://www.w3.org/XML/1998/namespace'
+
+ +
+
+XSI = 'http://www.w3.org/2001/XMLSchema-instance'
+
+ +
+ +
+
+

onelogin.saml2.errors module

+

OneLogin_Saml2_Error class

+

MIT License

+

Error class of Python Toolkit.

+

Defines common Error codes and has a custom initializator.

+
+
+exception onelogin.saml2.errors.OneLogin_Saml2_Error(message, code=0, errors=None)[source]
+

Bases: exceptions.Exception

+

This class implements a custom Exception handler. +Defines custom error codes.

+
+
+CERT_NOT_FOUND = 4
+
+ +
+
+METADATA_SP_INVALID = 3
+
+ +
+
+PRIVATE_KEY_FILE_NOT_FOUND = 7
+
+ +
+
+PRIVATE_KEY_NOT_FOUND = 13
+
+ +
+
+PUBLIC_CERT_FILE_NOT_FOUND = 6
+
+ +
+
+REDIRECT_INVALID_URL = 5
+
+ +
+
+SAML_LOGOUTMESSAGE_NOT_FOUND = 9
+
+ +
+
+SAML_LOGOUTREQUEST_INVALID = 10
+
+ +
+
+SAML_LOGOUTRESPONSE_INVALID = 11
+
+ +
+
+SAML_RESPONSE_NOT_FOUND = 8
+
+ +
+
+SAML_SINGLE_LOGOUT_NOT_SUPPORTED = 12
+
+ +
+
+SETTINGS_FILE_NOT_FOUND = 0
+
+ +
+
+SETTINGS_INVALID = 2
+
+ +
+
+SETTINGS_INVALID_SYNTAX = 1
+
+ +
+
+SP_CERTS_NOT_FOUND = 4
+
+ +
+
+UNSUPPORTED_SETTINGS_OBJECT = 14
+
+ +
+ +
+
+exception onelogin.saml2.errors.OneLogin_Saml2_ValidationError(message, code=0, errors=None)[source]
+

Bases: exceptions.Exception

+

This class implements another custom Exception handler, related +to exceptions that happens during validation process. +Defines custom error codes .

+
+
+ASSERTION_EXPIRED = 20
+
+ +
+
+ASSERTION_TOO_EARLY = 19
+
+ +
+
+AUTHN_CONTEXT_MISMATCH = 45
+
+ +
+
+CHILDREN_NODE_NOT_FOUND_IN_KEYINFO = 36
+
+ +
+
+DEPRECATED_DIGEST_METHOD = 47
+
+ +
+
+DEPRECATED_SIGNATURE_METHOD = 46
+
+ +
+
+DUPLICATED_ATTRIBUTE_NAME_FOUND = 41
+
+ +
+
+DUPLICATED_ID_IN_SIGNED_ELEMENTS = 8
+
+ +
+
+DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS = 10
+
+ +
+
+EMPTY_DESTINATION = 25
+
+ +
+
+EMPTY_NAMEID = 39
+
+ +
+
+ENCRYPTED_ATTRIBUTES = 23
+
+ +
+
+ID_NOT_FOUND_IN_SIGNED_ELEMENT = 7
+
+ +
+
+INVALID_SIGNATURE = 42
+
+ +
+
+INVALID_SIGNED_ELEMENT = 9
+
+ +
+
+INVALID_XML_FORMAT = 14
+
+ +
+
+ISSUER_MULTIPLE_IN_RESPONSE = 27
+
+ +
+
+ISSUER_NOT_FOUND_IN_ASSERTION = 28
+
+ +
+
+KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA = 35
+
+ +
+
+MISSING_CONDITIONS = 18
+
+ +
+
+MISSING_ID = 1
+
+ +
+
+MISSING_STATUS = 3
+
+ +
+
+MISSING_STATUS_CODE = 4
+
+ +
+
+NO_ATTRIBUTESTATEMENT = 22
+
+ +
+
+NO_ENCRYPTED_ASSERTION = 16
+
+ +
+
+NO_ENCRYPTED_NAMEID = 17
+
+ +
+
+NO_NAMEID = 38
+
+ +
+
+NO_SIGNATURE_FOUND = 34
+
+ +
+
+NO_SIGNED_ASSERTION = 33
+
+ +
+
+NO_SIGNED_MESSAGE = 32
+
+ +
+
+RESPONSE_EXPIRED = 44
+
+ +
+
+SESSION_EXPIRED = 30
+
+ +
+
+SP_NAME_QUALIFIER_NAME_MISMATCH = 40
+
+ +
+
+STATUS_CODE_IS_NOT_SUCCESS = 5
+
+ +
+
+UNEXPECTED_SIGNED_ELEMENTS = 11
+
+ +
+
+UNSUPPORTED_RETRIEVAL_METHOD = 37
+
+ +
+
+UNSUPPORTED_SAML_VERSION = 0
+
+ +
+
+WRONG_AUDIENCE = 26
+
+ +
+
+WRONG_DESTINATION = 24
+
+ +
+
+WRONG_INRESPONSETO = 15
+
+ +
+
+WRONG_ISSUER = 29
+
+ +
+
+WRONG_NUMBER_OF_ASSERTIONS = 2
+
+ +
+
+WRONG_NUMBER_OF_AUTHSTATEMENTS = 21
+
+ +
+
+WRONG_NUMBER_OF_SIGNATURES = 43
+
+ +
+
+WRONG_NUMBER_OF_SIGNATURES_IN_ASSERTION = 13
+
+ +
+
+WRONG_NUMBER_OF_SIGNATURES_IN_RESPONSE = 12
+
+ +
+
+WRONG_SIGNED_ELEMENT = 6
+
+ +
+
+WRONG_SUBJECTCONFIRMATION = 31
+
+ +
+ +
+
+

onelogin.saml2.idp_metadata_parser module

+

OneLogin_Saml2_IdPMetadataParser class

+

MIT License

+

Metadata class of Python Toolkit.

+
+
+class onelogin.saml2.idp_metadata_parser.OneLogin_Saml2_IdPMetadataParser[source]
+

Bases: object

+

A class that contain methods related to obtaining and parsing metadata from IdP

+

This class does not validate in any way the URL that is introduced, +make sure to validate it properly before use it in a get_metadata method.

+
+
+static get_metadata(url, validate_cert=True)[source]
+

Gets the metadata XML from the provided URL

+ +++ + + + + + + + +
Parameters:
    +
  • url (string) – Url where the XML of the Identity Provider Metadata is published.
  • +
  • validate_cert (bool) – If the url uses https schema, that flag enables or not the verification of the associated certificate.
  • +
+
Returns:

metadata XML

+
Return type:

string

+
+
+ +
+
+static merge_settings(settings, new_metadata_settings)[source]
+

Will update the settings with the provided new settings data extracted from the IdP metadata

+ +++ + + + + + + + +
Parameters:
    +
  • settings (dict) – Current settings dict data
  • +
  • new_metadata_settings (dict) – Settings to be merged (extracted from IdP metadata after parsing)
  • +
+
Returns:

merged settings

+
Return type:

dict

+
+
+ +
+
+static parse(idp_metadata, required_sso_binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', required_slo_binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', entity_id=None)[source]
+

Parse the Identity Provider metadata and return a dict with extracted data.

+

If there are multiple <IDPSSODescriptor> 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).

+ +++ + + + + + + + +
Parameters:
    +
  • idp_metadata (string) – XML of the Identity Provider Metadata.
  • +
  • required_sso_binding (one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT +or OneLogin_Saml2_Constants.BINDING_HTTP_POST) – Parse only POST or REDIRECT SSO endpoints.
  • +
  • required_slo_binding (one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT +or OneLogin_Saml2_Constants.BINDING_HTTP_POST) – Parse only POST or REDIRECT SLO endpoints.
  • +
  • entity_id (string) – Specify the entity_id of the EntityDescriptor that you want to parse a XML +that contains multiple EntityDescriptor.
  • +
+
Returns:

settings dict with extracted data

+
Return type:

dict

+
+
+ +
+
+static parse_remote(url, validate_cert=True, entity_id=None, **kwargs)[source]
+

Gets the metadata XML from the provided URL and parse it, returning a dict with extracted data

+ +++ + + + + + + + +
Parameters:
    +
  • url (string) – Url where the XML of the Identity Provider Metadata is published.
  • +
  • validate_cert (bool) – If the url uses https schema, that flag enables or not the verification of the associated certificate.
  • +
  • entity_id (string) – Specify the entity_id of the EntityDescriptor that you want to parse a XML +that contains multiple EntityDescriptor.
  • +
+
Returns:

settings dict with extracted data

+
Return type:

dict

+
+
+ +
+ +
+
+onelogin.saml2.idp_metadata_parser.dict_deep_merge(lhs, rhs)[source]
+

Deep-merge dictionary rhs into dictionary lhs.

+
+ +
+
+

onelogin.saml2.logout_request module

+

OneLogin_Saml2_Logout_Request class

+

MIT License

+

Logout Request class of Python Toolkit.

+
+
+class onelogin.saml2.logout_request.OneLogin_Saml2_Logout_Request(settings, request=None, name_id=None, session_index=None, nq=None, name_id_format=None, spnq=None)[source]
+

Bases: object

+

This class handles a Logout Request.

+

Builds a Logout Response object and validates it.

+
+
+get_error()[source]
+

After executing a validation process, if it fails this method returns the cause

+
+ +
+
+static get_id(request)[source]
+

Returns the ID of the Logout Request +:param request: Logout Request Message +:type request: string|DOMDocument +:return: string ID +:rtype: str object

+
+ +
+
+static get_issuer(request)[source]
+

Gets the Issuer of the Logout Request Message +:param request: Logout Request Message +:type request: string|DOMDocument +:return: The Issuer +:rtype: string

+
+ +
+
+static get_nameid(request, key=None)[source]
+

Gets the NameID 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

+
+ +
+
+static get_nameid_data(request, key=None)[source]
+

Gets the NameID Data of the the Logout Request +:param request: Logout Request Message +:type request: string|DOMDocument +:param key: The SP key +:type key: string +:return: Name ID Data (Value, Format, NameQualifier, SPNameQualifier) +:rtype: dict

+
+ +
+
+static get_nameid_format(request, key=None)[source]
+

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

+
+ +
+
+get_request(deflate=True)[source]
+

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

+
+ +
+
+static get_session_indexes(request)[source]
+

Gets the SessionIndexes from the Logout Request +:param request: Logout Request Message +:type request: string|DOMDocument +:return: The SessionIndex value +:rtype: list

+
+ +
+
+get_xml()[source]
+

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

+
+ +
+
+is_valid(request_data, raise_exceptions=False)[source]
+

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

+
+ +
+ +
+
+

onelogin.saml2.logout_response module

+

OneLogin_Saml2_Logout_Response class

+

MIT License

+

Logout Response class of Python Toolkit.

+
+
+class onelogin.saml2.logout_response.OneLogin_Saml2_Logout_Response(settings, response=None)[source]
+

Bases: object

+

This class handles a Logout Response. It Builds or parses a Logout Response object +and validates it.

+
+
+build(in_response_to)[source]
+

Creates a Logout Response object. +:param in_response_to: InResponseTo value for the Logout Response. +:type in_response_to: string

+
+ +
+
+get_error()[source]
+

After executing a validation process, if it fails this method returns the cause

+
+ +
+
+get_in_response_to()[source]
+

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

+
+ +
+
+get_issuer()[source]
+

Gets the Issuer of the Logout Response Message +:return: The Issuer +:rtype: string

+
+ +
+
+get_response(deflate=True)[source]
+

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

+
+ +
+
+get_status()[source]
+

Gets the Status +:return: The Status +:rtype: string

+
+ +
+
+get_xml()[source]
+

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

+
+ +
+
+is_valid(request_data, request_id=None, raise_exceptions=False)[source]
+

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

+
+ +
+ +
+
+

onelogin.saml2.metadata module

+

OneLogin_Saml2_Metadata class

+

MIT License

+

Metadata class of Python Toolkit.

+
+
+class onelogin.saml2.metadata.OneLogin_Saml2_Metadata[source]
+

Bases: object

+

A class that contains methods related to the metadata of the SP

+
+
+TIME_CACHED = 604800
+
+ +
+
+TIME_VALID = 172800
+
+ +
+
+static add_x509_key_descriptors(metadata, cert=None, add_encryption=True)[source]
+

Adds the x509 descriptors (sign/encryption) to the metadata +The same cert will be used for sign/encrypt

+ +++ + + + + + + + +
Parameters:
    +
  • metadata (string) – SAML Metadata XML
  • +
  • cert (string) – x509 cert
  • +
  • add_encryption (boolean) – Determines if the KeyDescriptor[use=”encryption”] should be added.
  • +
+
Returns:

Metadata with KeyDescriptors

+
Return type:

string

+
+
+ +
+
+static builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=None, contacts=None, organization=None)[source]
+

Builds the metadata of the SP

+ +++ + + + +
Parameters:
    +
  • sp (string) – The SP data
  • +
  • authnsign (string) – authnRequestsSigned attribute
  • +
  • wsign (string) – wantAssertionsSigned attribute
  • +
  • valid_until (string|DateTime|Timestamp) – Metadata’s expiry date
  • +
  • cache_duration (int|string) – Duration of the cache in seconds
  • +
  • contacts (dict) – Contacts info
  • +
  • organization (dict) – Organization info
  • +
+
+
+ +
+
+static sign_metadata(metadata, key, cert, sign_algorithm='http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', digest_algorithm='http://www.w3.org/2001/04/xmlenc#sha256')[source]
+

Signs the metadata with the key/cert provided

+ +++ + + + + + + + +
Parameters:
    +
  • metadata (string) – SAML Metadata XML
  • +
  • key (string) – x509 key
  • +
  • cert (string) – x509 cert
  • +
  • sign_algorithm (string) – Signature algorithm method
  • +
  • digest_algorithm (string) – Digest algorithm method
  • +
+
Returns:

Signed Metadata

+
Return type:

string

+
+
+ +
+ +
+
+

onelogin.saml2.response module

+

OneLogin_Saml2_Response class

+

MIT License

+

SAML Response class of Python Toolkit.

+
+
+class onelogin.saml2.response.OneLogin_Saml2_Response(settings, response)[source]
+

Bases: object

+

This class handles a SAML Response. It parses or validates +a Logout Response object.

+
+
+check_one_authnstatement()[source]
+

Checks that the samlp:Response/saml:Assertion/saml:AuthnStatement element exists and is unique.

+
+ +
+
+check_one_condition()[source]
+

Checks that the samlp:Response/saml:Assertion/saml:Conditions element exists and is unique.

+
+ +
+
+check_status()[source]
+

Check if the status of the response is success or not

+ +++ + + + +
Raises:Exception. If the status is not success
+
+ +
+
+get_assertion_id()[source]
+
+++ + + + + + +
Returns:the ID of the assertion in the response
Return type:string
+
+ +
+
+get_assertion_not_on_or_after()[source]
+

Returns the NotOnOrAfter value of the valid SubjectConfirmationData node if any

+
+ +
+
+get_attributes()[source]
+

Gets the Attributes from the AttributeStatement element. +EncryptedAttributes are not supported

+
+ +
+
+get_audiences()[source]
+

Gets the audiences

+ +++ + + + + + +
Returns:The valid audiences for the SAML Response
Return type:list
+
+ +
+
+get_authn_contexts()[source]
+
+
Gets the authentication contexts
+
+++ + + + +
returns:The authentication classes for the SAML Response
+
+
+ +++ + + + +
Return type:list
+
+ +
+
+get_error()[source]
+

After executing a validation process, if it fails this method returns the cause

+
+ +
+
+get_friendlyname_attributes()[source]
+

Gets the Attributes from the AttributeStatement element indexed by FiendlyName. +EncryptedAttributes are not supported

+
+ +
+
+get_id()[source]
+
+++ + + + + + +
Returns:the ID of the response
Return type:string
+
+ +
+
+get_in_response_to()[source]
+

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

+
+ +
+
+get_issuers()[source]
+

Gets the issuers (from message and from assertion)

+ +++ + + + + + +
Returns:The issuers
Return type:list
+
+ +
+
+get_nameid()[source]
+

Gets the NameID provided by the SAML Response from the IdP

+ +++ + + + + + +
Returns:NameID (value)
Return type:string|None
+
+ +
+
+get_nameid_data()[source]
+

Gets the NameID Data provided by the SAML Response from the IdP

+ +++ + + + + + +
Returns:Name ID Data (Value, Format, NameQualifier, SPNameQualifier)
Return type:dict
+
+ +
+
+get_nameid_format()[source]
+

Gets the NameID Format provided by the SAML Response from the IdP

+ +++ + + + + + +
Returns:NameID Format
Return type:string|None
+
+ +
+
+get_nameid_nq()[source]
+

Gets the NameID NameQualifier provided by the SAML Response from the IdP

+ +++ + + + + + +
Returns:NameID NameQualifier
Return type:string|None
+
+ +
+
+get_nameid_spnq()[source]
+

Gets the NameID SP NameQualifier provided by the SAML response from the IdP.

+ +++ + + + + + +
Returns:NameID SP NameQualifier
Return type:string|None
+
+ +
+
+get_session_index()[source]
+

Gets the SessionIndex from the AuthnStatement +Could be used to be stored in the local session in order +to be used in a future Logout Request that the SP could +send to the SP, to set what specific session must be deleted

+ +++ + + + + + +
Returns:The SessionIndex value
Return type:string|None
+
+ +
+
+get_session_not_on_or_after()[source]
+

Gets the SessionNotOnOrAfter from the AuthnStatement +Could be used to set the local session expiration

+ +++ + + + + + +
Returns:The SessionNotOnOrAfter value
Return type:time|None
+
+ +
+
+get_xml_document()[source]
+

Returns the SAML Response document (If contains an encrypted assertion, decrypts it)

+ +++ + + + + + +
Returns:Decrypted XML response document
Return type:DOMDocument
+
+ +
+
+is_valid(request_data, request_id=None, raise_exceptions=False)[source]
+

Validates the response object.

+ +++ + + + + + + + +
Parameters:
    +
  • request_data (dict) – Request Data
  • +
  • request_id (string) – Optional argument. The ID of the AuthNRequest sent by this SP to the IdP
  • +
  • raise_exceptions (Boolean) – Whether to return false on failure or raise an exception
  • +
+
Returns:

True if the SAML Response is valid, False if not

+
Return type:

bool

+
+
+ +
+
+process_signed_elements()[source]
+
+
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
Return type:list
+
+ +
+
+validate_num_assertions()[source]
+

Verifies that the document only contains a single Assertion (encrypted or not)

+ +++ + + + + + +
Returns:True if only 1 assertion encrypted or not
Return type:bool
+
+ +
+
+validate_signed_elements(**kwargs)[source]
+

Verifies that the document has the expected signed nodes.

+ +++ + + + +
Parameters:
    +
  • signed_elements (list) – The signed elements to be checked
  • +
  • raise_exceptions (Boolean) – Whether to return false on failure or raise an exception
  • +
+
+
+ +
+
+validate_timestamps(**kwargs)[source]
+

Verifies that the document is valid according to Conditions Element

+ +++ + + + + + + + +
Parameters:raise_exceptions (Boolean) – Whether to return false on failure or raise an exception
Returns:True if the condition is valid, False otherwise
Return type:bool
+
+ +
+ +
+
+

onelogin.saml2.settings module

+

OneLogin_Saml2_Settings class

+

MIT License

+

Setting class of Python Toolkit.

+
+
+class onelogin.saml2.settings.OneLogin_Saml2_Settings(settings=None, custom_base_path=None, sp_validation_only=False)[source]
+

Bases: object

+

Handles the settings of the Python toolkits.

+
+
+check_idp_settings(settings)[source]
+

Checks the IdP settings info.

+ +++ + + + + + + + +
Parameters:settings (dict) – Dict with settings data
Returns:Errors found on the IdP settings data
Return type:list
+
+ +
+
+check_settings(settings)[source]
+

Checks the settings info.

+ +++ + + + + + + + +
Parameters:settings (dict) – Dict with settings data
Returns:Errors found on the settings data
Return type:list
+
+ +
+
+check_sp_certs()[source]
+

Checks if the x509 certs of the SP exists and are valid.

+ +++ + + + + + +
Returns:If the x509 certs of the SP exists and are valid
Return type:boolean
+
+ +
+
+check_sp_settings(settings)[source]
+

Checks the SP settings info.

+ +++ + + + + + + + +
Parameters:settings (dict) – Dict with settings data
Returns:Errors found on the SP settings data
Return type:list
+
+ +
+
+format_idp_cert()[source]
+

Formats the IdP cert.

+
+ +
+
+format_idp_cert_multi()[source]
+

Formats the Multple IdP certs.

+
+ +
+
+format_sp_cert()[source]
+

Formats the SP cert.

+
+ +
+
+format_sp_cert_new()[source]
+

Formats the SP cert.

+
+ +
+
+format_sp_key()[source]
+

Formats the private key.

+
+ +
+
+get_base_path()[source]
+

Returns base path

+ +++ + + + + + +
Returns:The base toolkit folder path
Return type:string
+
+ +
+
+get_cert_path()[source]
+

Returns cert path

+ +++ + + + + + +
Returns:The cert folder path
Return type:string
+
+ +
+
+get_contacts()[source]
+

Gets contact data.

+ +++ + + + + + +
Returns:Contacts info
Return type:dict
+
+ +
+
+get_errors()[source]
+

Returns an array with the errors, the array is empty when the settings is ok.

+ +++ + + + + + +
Returns:Errors
Return type:list
+
+ +
+
+get_idp_cert()[source]
+

Returns the x509 public cert of the IdP.

+ +++ + + + + + +
Returns:IdP public cert
Return type:string
+
+ +
+
+get_idp_data()[source]
+

Gets the IdP data.

+ +++ + + + + + +
Returns:IdP info
Return type:dict
+
+ +
+
+get_idp_slo_response_url()[source]
+

Gets the IdP SLO return URL for IdP-initiated logout.

+ +++ + + + + + +
Returns:an URL, the SLO return endpoint of the IdP
Return type:string
+
+ +
+
+get_idp_slo_url()[source]
+

Gets the IdP SLO URL.

+ +++ + + + + + +
Returns:An URL, the SLO endpoint of the IdP
Return type:string
+
+ +
+
+get_idp_sso_url()[source]
+

Gets the IdP SSO URL.

+ +++ + + + + + +
Returns:An URL, the SSO endpoint of the IdP
Return type:string
+
+ +
+
+get_lib_path()[source]
+

Returns lib path

+ +++ + + + + + +
Returns:The library folder path
Return type:string
+
+ +
+
+get_organization()[source]
+

Gets organization data.

+ +++ + + + + + +
Returns:Organization info
Return type:dict
+
+ +
+
+get_schemas_path()[source]
+

Returns schema path

+ +++ + + + + + +
Returns:The schema folder path
Return type:string
+
+ +
+
+get_security_data()[source]
+

Gets security data.

+ +++ + + + + + +
Returns:Security info
Return type:dict
+
+ +
+
+get_sp_cert()[source]
+

Returns the x509 public cert of the SP.

+ +++ + + + + + +
Returns:SP public cert
Return type:string or None
+
+ +
+
+get_sp_cert_new()[source]
+

Returns the x509 public of the SP planned +to be used soon instead the other public cert

+ +++ + + + + + +
Returns:SP public cert new
Return type:string or None
+
+ +
+
+get_sp_data()[source]
+

Gets the SP data.

+ +++ + + + + + +
Returns:SP info
Return type:dict
+
+ +
+
+get_sp_key()[source]
+

Returns the x509 private key of the SP.

+ +++ + + + + + +
Returns:SP private key
Return type:string or None
+
+ +
+
+get_sp_metadata()[source]
+

Gets the SP metadata. The XML representation.

+ +++ + + + + + +
Returns:SP metadata (xml)
Return type:string
+
+ +
+
+is_debug_active()[source]
+

Returns if the debug is active.

+ +++ + + + + + +
Returns:Debug parameter
Return type:boolean
+
+ +
+
+is_strict()[source]
+

Returns if the ‘strict’ mode is active.

+ +++ + + + + + +
Returns:Strict parameter
Return type:boolean
+
+ +
+
+set_cert_path(path)[source]
+

Set a new cert path

+
+ +
+
+set_strict(value)[source]
+

Activates or deactivates the strict mode.

+ +++ + + + +
Parameters:value – Strict parameter
+
+ +
+
+validate_metadata(xml)[source]
+

Validates an XML SP Metadata.

+ +++ + + + + + + + +
Parameters:xml (string) – Metadata’s XML that will be validate
Returns:The list of found errors
Return type:list
+
+ +
+ +
+
+onelogin.saml2.settings.validate_url(url, allow_single_label_domain=False)[source]
+

Auxiliary method to validate an urllib +:param url: An url to be validated +:type url: string +:param allow_single_label_domain: In order to allow or not single label domain +:type url: bool +:returns: True if the url is valid +:rtype: bool

+
+ +
+
+

onelogin.saml2.utils module

+

OneLogin_Saml2_Utils class

+

MIT License

+

Auxiliary class of Python Toolkit.

+
+
+class onelogin.saml2.utils.OneLogin_Saml2_Utils[source]
+

Bases: object

+

Auxiliary class that contains several utility methods to parse time, +urls, add sign, encrypt, decrypt, sign validation, handle xml …

+
+
+ASSERTION_SIGNATURE_XPATH = '/samlp:Response/saml:Assertion/ds:Signature'
+
+ +
+
+RESPONSE_SIGNATURE_XPATH = '/samlp:Response/ds:Signature'
+
+ +
+
+TIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
+
+ +
+
+TIME_FORMAT_2 = '%Y-%m-%dT%H:%M:%S.%fZ'
+
+ +
+
+TIME_FORMAT_WITH_FRAGMENT = <_sre.SRE_Pattern object>
+
+ +
+
+static add_sign(xml, key, cert, debug=False, sign_algorithm='http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', digest_algorithm='http://www.w3.org/2001/04/xmlenc#sha256')[source]
+

Adds signature key and senders certificate to an element (Message or +Assertion).

+ +++ + + + + + + + + + + + + + + + +
Parameters:
    +
  • xml – The element we should sign
  • +
  • key – The private key
  • +
  • cert – The public
  • +
  • debug – Activate the xmlsec debug
  • +
  • sign_algorithm (string) – Signature algorithm method
  • +
  • digest_algorithm (string) – Digest algorithm method
  • +
+
Type:

string | Document

+
Type:

string

+
Type:

string

+
Type:

bool

+
Returns:

Signed XML

+
Return type:

string

+
+
+ +
+
+static calculate_x509_fingerprint(x509_cert, alg='sha1')[source]
+

Calculates the fingerprint of a formatted x509cert.

+ +++ + + + + + + + + + + + +
Parameters:
    +
  • x509_cert – x509 cert formatted
  • +
  • alg – The algorithm to build the fingerprint
  • +
+
Type:

string

+
Type:

string

+
Returns:

fingerprint

+
Return type:

string

+
+
+ +
+
+static case_sensitive_urlencode(to_encode, lowercase=False)[source]
+
+ +
+
+static decode_base64_and_inflate(value)[source]
+

base64 decodes and then inflates according to RFC1951 +:param value: a deflated and encoded string +:type value: string +:returns: the string after decoding and inflating +:rtype: string

+
+ +
+
+static decrypt_element(encrypted_data, key, debug=False, inplace=False)[source]
+

Decrypts an encrypted element.

+ +++ + + + + + + + + + + + + + + + +
Parameters:
    +
  • encrypted_data – The encrypted data.
  • +
  • key – The key.
  • +
  • debug – Activate the xmlsec debug
  • +
  • inplace – update passed data with decrypted result
  • +
+
Type:

lxml.etree.Element | DOMElement | basestring

+
Type:

string

+
Type:

bool

+
Type:

bool

+
Returns:

The decrypted element.

+
Return type:

lxml.etree.Element

+
+
+ +
+
+static deflate_and_base64_encode(value)[source]
+

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

+
+ +
+
+static delete_local_session(callback=None)[source]
+

Deletes the local session.

+
+ +
+
+static element_text(node)[source]
+
+ +
+
+static extract_raw_query_parameter(query_string, parameter, default='')[source]
+
+ +
+
+static format_cert(cert, heads=True)[source]
+

Returns a x509 cert (adding header & footer if required).

+ +++ + + + + + + + + + + + +
Parameters:
    +
  • cert – A x509 unformatted cert
  • +
  • heads – True if we want to include head and footer
  • +
+
Type:

string

+
Type:

boolean

+
Returns:

Formatted cert

+
Return type:

string

+
+
+ +
+
+static format_finger_print(fingerprint)[source]
+

Formats a fingerprint.

+ +++ + + + + + + + + + +
Parameters:fingerprint – fingerprint
Type:string
Returns:Formatted fingerprint
Return type:string
+
+ +
+
+static format_private_key(key, heads=True)[source]
+

Returns a private key (adding header & footer if required).

+

:param key A private key +:type: string

+ +++ + + + + + + + + + +
Parameters:heads – True if we want to include head and footer
Type:boolean
Returns:Formatted private key
Return type:string
+
+ +
+
+static generate_name_id(value, sp_nq, sp_format=None, cert=None, debug=False, nq=None)[source]
+

Generates a nameID.

+ +++ + + + + + + + + + + + + + + + + + + + +
Parameters:
    +
  • value – fingerprint
  • +
  • sp_nq – SP Name Qualifier
  • +
  • sp_format – SP Format
  • +
  • cert – IdP Public Cert to encrypt the nameID
  • +
  • debug – Activate the xmlsec debug
  • +
  • nq – IDP Name Qualifier
  • +
+
Type:

string

+
Type:

string

+
Type:

string

+
Type:

string

+
Type:

bool

+
Type:

string

+
Returns:

DOMElement | XMLSec nameID

+
Return type:

string

+
+
+ +
+
+static generate_unique_id()[source]
+

Generates an unique string (used for example as ID for assertions).

+ +++ + + + + + +
Returns:A unique string
Return type:string
+
+ +
+
+static get_encoded_parameter(get_data, name, default=None, lowercase_urlencoding=False)[source]
+

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)

+
+ +
+
+static get_expire_time(cache_duration=None, valid_until=None)[source]
+

Compares 2 dates and returns the earliest.

+ +++ + + + + + + + + + + + +
Parameters:
    +
  • cache_duration – The duration, as a string.
  • +
  • valid_until – The valid until date, as a string or as a timestamp
  • +
+
Type:

string

+
Type:

string

+
Returns:

The expiration time.

+
Return type:

int

+
+
+ +
+
+static get_self_host(request_data)[source]
+

Returns the current host.

+ +++ + + + + + + + + + +
Parameters:request_data – The request as a dict
Type:dict
Returns:The current host
Return type:string
+
+ +
+
+static get_self_routed_url_no_query(request_data)[source]
+

Returns the routed URL of the current host + current view.

+ +++ + + + + + + + + + +
Parameters:request_data – The request as a dict
Type:dict
Returns:The url of current host + current view
Return type:string
+
+ +
+
+static get_self_url(request_data)[source]
+

Returns the URL of the current host + current view + query.

+ +++ + + + + + + + + + +
Parameters:request_data – The request as a dict
Type:dict
Returns:The url of current host + current view + query
Return type:string
+
+ +
+
+static get_self_url_host(request_data)[source]
+

Returns the protocol + the current host + the port (if different than +common ports).

+ +++ + + + + + + + + + +
Parameters:request_data – The request as a dict
Type:dict
Returns:Url
Return type:string
+
+ +
+
+static get_self_url_no_query(request_data)[source]
+

Returns the URL of the current host + current view.

+ +++ + + + + + + + + + +
Parameters:request_data – The request as a dict
Type:dict
Returns:The url of current host + current view
Return type:string
+
+ +
+
+static get_status(dom)[source]
+

Gets Status from a Response.

+ +++ + + + + + + + + + +
Parameters:dom – The Response as XML
Type:Document
Returns:The Status, an array with the code and a message.
Return type:dict
+
+ +
+
+static is_https(request_data)[source]
+

Checks if https or http.

+ +++ + + + + + + + + + +
Parameters:request_data – The request as a dict
Type:dict
Returns:False if https is not active
Return type:boolean
+
+ +
+
+static normalize_url(url)[source]
+

Returns normalized URL for comparison. +This method converts the netloc to lowercase, as it should be case-insensitive (per RFC 4343, RFC 7617) +If standardization fails, the original URL is returned +Python documentation indicates that URL split also normalizes query strings if empty query fields are present

+ +++ + + + + + + + +
Parameters:url (String) – URL
Returns:A normalized URL, or the given URL string if parsing fails
Return type:String
+
+ +
+
+static now()[source]
+
+++ + + + + + +
Returns:unix timestamp of actual time.
Return type:int
+
+ +
+
+static parse_SAML_to_time(timestr)[source]
+

Converts a SAML2 timestamp on the form yyyy-mm-ddThh:mm:ss(.s+)?Z +to a UNIX timestamp. The sub-second part is ignored.

+ +++ + + + + + + + + + +
Parameters:time – The time we should convert (SAML Timestamp).
Type:string
Returns:Converted to a unix timestamp.
Return type:int
+
+ +
+
+static parse_duration(duration, timestamp=None)[source]
+

Interprets a ISO8601 duration value relative to a given timestamp.

+ +++ + + + + + + + + + + + +
Parameters:
    +
  • duration – The duration, as a string.
  • +
  • timestamp – The unix timestamp we should apply the duration to. +Optional, default to the current time.
  • +
+
Type:

string

+
Type:

string

+
Returns:

The new timestamp, after the duration is applied.

+
Return type:

int

+
+
+ +
+
+static parse_time_to_SAML(time)[source]
+

Converts a UNIX timestamp to SAML2 timestamp on the form +yyyy-mm-ddThh:mm:ss(.s+)?Z.

+ +++ + + + + + + + + + +
Parameters:time – The time we should convert (DateTime).
Type:string
Returns:SAML2 timestamp.
Return type:string
+
+ +
+
+static query(dom, query, context=None, tagid=None)[source]
+

Extracts nodes that match the query from the Element

+ +++ + + + + + + + + + + + + + + + +
Parameters:
    +
  • dom – The root of the lxml objet
  • +
  • query – Xpath Expresion
  • +
  • context – Context Node
  • +
  • tagid – Tag ID
  • +
+
Type:

Element

+
Type:

string

+
Type:

DOMElement

+
Type:

string

+
Returns:

The queried nodes

+
Return type:

list

+
+
+ +
+
+static redirect(url, parameters={}, request_data={})[source]
+

Executes a redirection to the provided url (or return the target url).

+ +++ + + + + + + + + + + + + + +
Parameters:
    +
  • url – The target url
  • +
  • parameters – Extra parameters to be passed as part of the url
  • +
  • request_data – The request as a dict
  • +
+
Type:

string

+
Type:

dict

+
Type:

dict

+
Returns:

Url

+
Return type:

string

+
+
+ +
+
+static validate_binary_sign(*args, **kwargs)[source]
+

Validates signed binary data (Used to validate GET Signature).

+ +++ + + + + + + + + + + + + + +
Parameters:
    +
  • signed_query – The element we should validate
  • +
  • signature – The signature that will be validate
  • +
  • cert – The public cert
  • +
  • algorithm – Signature algorithm
  • +
  • debug – Activate the xmlsec debug
  • +
  • raise_exceptions (Boolean) – Whether to return false on failure or raise an exception
  • +
+
Type:

string

+
Type:

string

+
Type:

string

+
Type:

string

+
Type:

bool

+
+
+ +
+
+static validate_metadata_sign(*args, **kwargs)[source]
+

Validates a signature of a EntityDescriptor.

+ +++ + + + + + + + + + + + + + + + +
Parameters:
    +
  • xml – The element we should validate
  • +
  • cert – The pubic cert
  • +
  • fingerprint – The fingerprint of the public cert
  • +
  • fingerprintalg – The algorithm used to build the fingerprint
  • +
  • validatecert – If true, will verify the signature and if the cert is valid.
  • +
  • debug – Activate the xmlsec debug
  • +
  • raise_exceptions (Boolean) – Whether to return false on failure or raise an exception
  • +
+
Type:

string | Document

+
Type:

string

+
Type:

string

+
Type:

string

+
Type:

bool

+
Type:

bool

+
+
+ +
+
+static validate_node_sign(*args, **kwargs)[source]
+

Validates a signature node.

+ +++ + + + + + + + + + + + + + + + + + +
Parameters:
    +
  • signature_node – The signature node
  • +
  • xml – The element we should validate
  • +
  • cert – The public cert
  • +
  • fingerprint – The fingerprint of the public cert
  • +
  • fingerprintalg – The algorithm used to build the fingerprint
  • +
  • validatecert – If true, will verify the signature and if the cert is valid.
  • +
  • debug – Activate the xmlsec debug
  • +
  • raise_exceptions (Boolean) – Whether to return false on failure or raise an exception
  • +
+
Type:

Node

+
Type:

Document

+
Type:

string

+
Type:

string

+
Type:

string

+
Type:

bool

+
Type:

bool

+
+
+ +
+
+static validate_sign(*args, **kwargs)[source]
+

Validates a signature (Message or Assertion).

+ +++ + + + + + + + + + + + + + + + + + + + +
Parameters:
    +
  • xml – The element we should validate
  • +
  • cert – The pubic cert
  • +
  • fingerprint – The fingerprint of the public cert
  • +
  • fingerprintalg – The algorithm used to build the fingerprint
  • +
  • validatecert – If true, will verify the signature and if the cert is valid.
  • +
  • debug – Activate the xmlsec debug
  • +
  • xpath – The xpath of the signed element
  • +
  • multicerts – Multiple public certs
  • +
  • raise_exceptions (Boolean) – Whether to return false on failure or raise an exception
  • +
+
Type:

string | Document

+
Type:

string

+
Type:

string

+
Type:

string

+
Type:

bool

+
Type:

bool

+
Type:

string

+
Type:

list

+
+
+ +
+
+static validate_xml(xml, schema, debug=False)[source]
+

Validates a xml against a schema +:param xml: The xml that will be validated +:type: string|DomDocument +:param schema: The schema +:type: string +:param debug: If debug is active, the parse-errors will be showed +:type: bool +:returns: Error code or the DomDocument of the xml +:rtype: string

+
+ +
+
+static write_temp_file(content)[source]
+

Writes some content into a temporary file and returns it.

+ +++ + + + + + + + + + +
Parameters:content – The file content
Type:string
Returns:The temporary file
Return type:file-like object
+
+ +
+ +
+
+onelogin.saml2.utils.print_xmlsec_errors(filename, line, func, error_object, error_subject, reason, msg)[source]
+

Auxiliary method. It overrides the default xmlsec debug message.

+
+ +
+
+onelogin.saml2.utils.return_false_on_exception(func)[source]
+

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.

+
+ +
+
+

onelogin.saml2.xmlparser module

+

lxml.etree protection

+
+
+class onelogin.saml2.xmlparser.GlobalParserTLS[source]
+

Bases: thread._local

+

Thread local context for custom parser instances

+
+
+createDefaultParser()[source]
+
+ +
+
+element_class
+

alias of RestrictedElement

+
+ +
+
+getDefaultParser()[source]
+
+ +
+
+parser_config = {'huge_tree': False, 'no_network': True, 'remove_comments': True, 'remove_pis': True, 'resolve_entities': False}
+
+ +
+
+setDefaultParser(parser)[source]
+
+ +
+ +
+
+class onelogin.saml2.xmlparser.RestrictedElement[source]
+

Bases: lxml.etree.ElementBase

+

A restricted Element class that filters out instances of some classes

+
+
+blacklist = (<type 'lxml.etree._Entity'>, <type 'lxml.etree._ProcessingInstruction'>, <type 'lxml.etree._Comment'>)
+
+ +
+
+getchildren(self)[source]
+

Returns all direct children. The elements are returned in document +order.

+ +++ + + + +
Deprecated:Note that this method has been deprecated as of +ElementTree 1.3 and lxml 2.0. New code should use +list(element) or simply iterate over elements.
+
+ +
+
+getiterator(self, tag=None, *tags)[source]
+

Returns a sequence or iterator of all elements in the subtree in +document order (depth first pre-order), starting with this +element.

+

Can be restricted to find only elements with specific tags, +see iter.

+ +++ + + + +
Deprecated:Note that this method is deprecated as of +ElementTree 1.3 and lxml 2.0. It returns an iterator in +lxml, which diverges from the original ElementTree +behaviour. If you want an efficient iterator, use the +element.iter() method instead. You should only use this +method in new code if you require backwards compatibility +with older versions of lxml or ElementTree.
+
+ +
+
+iter(self, tag=None, *tags)[source]
+

Iterate over all elements in the subtree in document order (depth +first pre-order), starting with this element.

+

Can be restricted to find only elements with specific tags: +pass "{ns}localname" as tag. Either or both of ns and +localname can be * for a wildcard; ns can be empty +for no namespace. "localname" is equivalent to "{}localname" +(i.e. no namespace) but "*" is "{*}*" (any or no namespace), +not "{}*".

+

You can also pass the Element, Comment, ProcessingInstruction and +Entity factory functions to look only for the specific element type.

+

Passing multiple tags (or a sequence of tags) instead of a single tag +will let the iterator return all elements matching any of these tags, +in document order.

+
+ +
+
+iterchildren(self, tag=None, *tags, reversed=False)[source]
+

Iterate over the children of this element.

+

As opposed to using normal iteration on this element, the returned +elements can be reversed with the ‘reversed’ keyword and restricted +to find only elements with specific tags, see iter.

+
+ +
+
+iterdescendants(self, tag=None, *tags)[source]
+

Iterate over the descendants of this element in document order.

+

As opposed to el.iter(), this iterator does not yield the element +itself. The returned elements can be restricted to find only elements +with specific tags, see iter.

+
+ +
+
+itersiblings(self, tag=None, *tags, preceding=False)[source]
+

Iterate over the following or preceding siblings of this element.

+

The direction is determined by the ‘preceding’ keyword which +defaults to False, i.e. forward iteration over the following +siblings. When True, the iterator yields the preceding +siblings in reverse document order, i.e. starting right before +the current element and going backwards.

+

Can be restricted to find only elements with specific tags, +see iter.

+
+ +
+ +
+
+onelogin.saml2.xmlparser.XML(text, parser=None, base_url=None, forbid_dtd=True, forbid_entities=True)
+
+ +
+
+onelogin.saml2.xmlparser.check_docinfo(elementtree, forbid_dtd=False, forbid_entities=True)[source]
+

Check docinfo of an element tree for DTD and entity declarations +The check for entity declarations needs lxml 3 or newer. lxml 2.x does +not support dtd.iterentities().

+
+ +
+
+onelogin.saml2.xmlparser.fromstring(text, parser=None, base_url=None, forbid_dtd=True, forbid_entities=True)[source]
+
+ +
+
+onelogin.saml2.xmlparser.iterparse(*args, **kwargs)[source]
+
+ +
+
+onelogin.saml2.xmlparser.parse(source, parser=None, base_url=None, forbid_dtd=True, forbid_entities=True)[source]
+
+ +
+
+

Module contents

+

Copyright (c) 2010-2022 OneLogin, Inc. +Copyright (c) 2023 IAM Digital Services, SL.

+

MIT License

+

Add SAML support to your Python softwares using this library. +Forget those complicated libraries and use that open source +library.

+

SAML Python toolkit let you build a SP (Service Provider) +over your Python application and connect it to any IdP (Identity Provider).

+

Supports:

+
    +
  • 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).
  • +
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/saml2/py-modindex.html b/docs/saml2/py-modindex.html new file mode 100644 index 00000000..e9deeec2 --- /dev/null +++ b/docs/saml2/py-modindex.html @@ -0,0 +1,183 @@ + + + + + + Python Module Index — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + +

Python Module Index

+ +
+ o +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+ o
+ onelogin +
    + onelogin.saml2 +
    + onelogin.saml2.auth +
    + onelogin.saml2.authn_request +
    + onelogin.saml2.constants +
    + onelogin.saml2.errors +
    + onelogin.saml2.idp_metadata_parser +
    + onelogin.saml2.logout_request +
    + onelogin.saml2.logout_response +
    + onelogin.saml2.metadata +
    + onelogin.saml2.response +
    + onelogin.saml2.settings +
    + onelogin.saml2.utils +
    + onelogin.saml2.xmlparser +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/saml2/search.html b/docs/saml2/search.html new file mode 100644 index 00000000..41d3fc8e --- /dev/null +++ b/docs/saml2/search.html @@ -0,0 +1,118 @@ + + + + + + Search — SAML Python Toolkit 1 documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + + + +
+ +
+ +
+
+ +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/docs/saml2/searchindex.js b/docs/saml2/searchindex.js new file mode 100644 index 00000000..80282983 --- /dev/null +++ b/docs/saml2/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({docnames:["index","modules","onelogin","onelogin.saml2"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.cpp":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,"sphinx.ext.viewcode":1,sphinx:54},filenames:["index.rst","modules.rst","onelogin.rst","onelogin.saml2.rst"],objects:{"":{onelogin:[2,0,0,"-"]},"onelogin.saml2":{auth:[3,0,0,"-"],authn_request:[3,0,0,"-"],constants:[3,0,0,"-"],errors:[3,0,0,"-"],idp_metadata_parser:[3,0,0,"-"],logout_request:[3,0,0,"-"],logout_response:[3,0,0,"-"],metadata:[3,0,0,"-"],response:[3,0,0,"-"],settings:[3,0,0,"-"],utils:[3,0,0,"-"],xmlparser:[3,0,0,"-"]},"onelogin.saml2.auth":{OneLogin_Saml2_Auth:[3,1,1,""]},"onelogin.saml2.auth.OneLogin_Saml2_Auth":{build_request_signature:[3,2,1,""],build_response_signature:[3,2,1,""],get_attribute:[3,2,1,""],get_attributes:[3,2,1,""],get_errors:[3,2,1,""],get_friendlyname_attribute:[3,2,1,""],get_friendlyname_attributes:[3,2,1,""],get_last_assertion_id:[3,2,1,""],get_last_assertion_not_on_or_after:[3,2,1,""],get_last_authn_contexts:[3,2,1,""],get_last_error_reason:[3,2,1,""],get_last_message_id:[3,2,1,""],get_last_request_id:[3,2,1,""],get_last_request_xml:[3,2,1,""],get_last_response_xml:[3,2,1,""],get_nameid:[3,2,1,""],get_nameid_format:[3,2,1,""],get_nameid_nq:[3,2,1,""],get_nameid_spnq:[3,2,1,""],get_session_expiration:[3,2,1,""],get_session_index:[3,2,1,""],get_settings:[3,2,1,""],get_slo_response_url:[3,2,1,""],get_slo_url:[3,2,1,""],get_sso_url:[3,2,1,""],is_authenticated:[3,2,1,""],login:[3,2,1,""],logout:[3,2,1,""],process_response:[3,2,1,""],process_slo:[3,2,1,""],redirect_to:[3,2,1,""],set_strict:[3,2,1,""]},"onelogin.saml2.authn_request":{OneLogin_Saml2_Authn_Request:[3,1,1,""]},"onelogin.saml2.authn_request.OneLogin_Saml2_Authn_Request":{get_id:[3,2,1,""],get_request:[3,2,1,""],get_xml:[3,2,1,""]},"onelogin.saml2.constants":{OneLogin_Saml2_Constants:[3,1,1,""]},"onelogin.saml2.constants.OneLogin_Saml2_Constants":{AC_KERBEROS:[3,3,1,""],AC_PASSWORD:[3,3,1,""],AC_PASSWORD_PROTECTED:[3,3,1,""],AC_SMARTCARD:[3,3,1,""],AC_UNSPECIFIED:[3,3,1,""],AC_X509:[3,3,1,""],AES128_CBC:[3,3,1,""],AES192_CBC:[3,3,1,""],AES256_CBC:[3,3,1,""],ALLOWED_CLOCK_DRIFT:[3,3,1,""],ATTRNAME_FORMAT_BASIC:[3,3,1,""],ATTRNAME_FORMAT_UNSPECIFIED:[3,3,1,""],ATTRNAME_FORMAT_URI:[3,3,1,""],BINDING_DEFLATE:[3,3,1,""],BINDING_HTTP_ARTIFACT:[3,3,1,""],BINDING_HTTP_POST:[3,3,1,""],BINDING_HTTP_REDIRECT:[3,3,1,""],BINDING_SOAP:[3,3,1,""],CM_BEARER:[3,3,1,""],CM_HOLDER_KEY:[3,3,1,""],CM_SENDER_VOUCHES:[3,3,1,""],DEPRECATED_ALGORITHMS:[3,3,1,""],DSA_SHA1:[3,3,1,""],NAMEID_EMAIL_ADDRESS:[3,3,1,""],NAMEID_ENCRYPTED:[3,3,1,""],NAMEID_ENTITY:[3,3,1,""],NAMEID_KERBEROS:[3,3,1,""],NAMEID_PERSISTENT:[3,3,1,""],NAMEID_TRANSIENT:[3,3,1,""],NAMEID_UNSPECIFIED:[3,3,1,""],NAMEID_WINDOWS_DOMAIN_QUALIFIED_NAME:[3,3,1,""],NAMEID_X509_SUBJECT_NAME:[3,3,1,""],NSMAP:[3,3,1,""],NS_DS:[3,3,1,""],NS_MD:[3,3,1,""],NS_PREFIX_DS:[3,3,1,""],NS_PREFIX_MD:[3,3,1,""],NS_PREFIX_SAML:[3,3,1,""],NS_PREFIX_SAMLP:[3,3,1,""],NS_PREFIX_XENC:[3,3,1,""],NS_PREFIX_XS:[3,3,1,""],NS_PREFIX_XSI:[3,3,1,""],NS_SAML:[3,3,1,""],NS_SAMLP:[3,3,1,""],NS_SOAP:[3,3,1,""],NS_XENC:[3,3,1,""],NS_XS:[3,3,1,""],NS_XSI:[3,3,1,""],RSA_1_5:[3,3,1,""],RSA_OAEP_MGF1P:[3,3,1,""],RSA_SHA1:[3,3,1,""],RSA_SHA256:[3,3,1,""],RSA_SHA384:[3,3,1,""],RSA_SHA512:[3,3,1,""],SHA1:[3,3,1,""],SHA256:[3,3,1,""],SHA384:[3,3,1,""],SHA512:[3,3,1,""],STATUS_NO_PASSIVE:[3,3,1,""],STATUS_PARTIAL_LOGOUT:[3,3,1,""],STATUS_PROXY_COUNT_EXCEEDED:[3,3,1,""],STATUS_REQUESTER:[3,3,1,""],STATUS_RESPONDER:[3,3,1,""],STATUS_SUCCESS:[3,3,1,""],STATUS_VERSION_MISMATCH:[3,3,1,""],TRIPLEDES_CBC:[3,3,1,""],XML:[3,3,1,""],XSI:[3,3,1,""]},"onelogin.saml2.errors":{OneLogin_Saml2_Error:[3,4,1,""],OneLogin_Saml2_ValidationError:[3,4,1,""]},"onelogin.saml2.errors.OneLogin_Saml2_Error":{CERT_NOT_FOUND:[3,3,1,""],METADATA_SP_INVALID:[3,3,1,""],PRIVATE_KEY_FILE_NOT_FOUND:[3,3,1,""],PRIVATE_KEY_NOT_FOUND:[3,3,1,""],PUBLIC_CERT_FILE_NOT_FOUND:[3,3,1,""],REDIRECT_INVALID_URL:[3,3,1,""],SAML_LOGOUTMESSAGE_NOT_FOUND:[3,3,1,""],SAML_LOGOUTREQUEST_INVALID:[3,3,1,""],SAML_LOGOUTRESPONSE_INVALID:[3,3,1,""],SAML_RESPONSE_NOT_FOUND:[3,3,1,""],SAML_SINGLE_LOGOUT_NOT_SUPPORTED:[3,3,1,""],SETTINGS_FILE_NOT_FOUND:[3,3,1,""],SETTINGS_INVALID:[3,3,1,""],SETTINGS_INVALID_SYNTAX:[3,3,1,""],SP_CERTS_NOT_FOUND:[3,3,1,""],UNSUPPORTED_SETTINGS_OBJECT:[3,3,1,""]},"onelogin.saml2.errors.OneLogin_Saml2_ValidationError":{ASSERTION_EXPIRED:[3,3,1,""],ASSERTION_TOO_EARLY:[3,3,1,""],AUTHN_CONTEXT_MISMATCH:[3,3,1,""],CHILDREN_NODE_NOT_FOUND_IN_KEYINFO:[3,3,1,""],DEPRECATED_DIGEST_METHOD:[3,3,1,""],DEPRECATED_SIGNATURE_METHOD:[3,3,1,""],DUPLICATED_ATTRIBUTE_NAME_FOUND:[3,3,1,""],DUPLICATED_ID_IN_SIGNED_ELEMENTS:[3,3,1,""],DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS:[3,3,1,""],EMPTY_DESTINATION:[3,3,1,""],EMPTY_NAMEID:[3,3,1,""],ENCRYPTED_ATTRIBUTES:[3,3,1,""],ID_NOT_FOUND_IN_SIGNED_ELEMENT:[3,3,1,""],INVALID_SIGNATURE:[3,3,1,""],INVALID_SIGNED_ELEMENT:[3,3,1,""],INVALID_XML_FORMAT:[3,3,1,""],ISSUER_MULTIPLE_IN_RESPONSE:[3,3,1,""],ISSUER_NOT_FOUND_IN_ASSERTION:[3,3,1,""],KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA:[3,3,1,""],MISSING_CONDITIONS:[3,3,1,""],MISSING_ID:[3,3,1,""],MISSING_STATUS:[3,3,1,""],MISSING_STATUS_CODE:[3,3,1,""],NO_ATTRIBUTESTATEMENT:[3,3,1,""],NO_ENCRYPTED_ASSERTION:[3,3,1,""],NO_ENCRYPTED_NAMEID:[3,3,1,""],NO_NAMEID:[3,3,1,""],NO_SIGNATURE_FOUND:[3,3,1,""],NO_SIGNED_ASSERTION:[3,3,1,""],NO_SIGNED_MESSAGE:[3,3,1,""],RESPONSE_EXPIRED:[3,3,1,""],SESSION_EXPIRED:[3,3,1,""],SP_NAME_QUALIFIER_NAME_MISMATCH:[3,3,1,""],STATUS_CODE_IS_NOT_SUCCESS:[3,3,1,""],UNEXPECTED_SIGNED_ELEMENTS:[3,3,1,""],UNSUPPORTED_RETRIEVAL_METHOD:[3,3,1,""],UNSUPPORTED_SAML_VERSION:[3,3,1,""],WRONG_AUDIENCE:[3,3,1,""],WRONG_DESTINATION:[3,3,1,""],WRONG_INRESPONSETO:[3,3,1,""],WRONG_ISSUER:[3,3,1,""],WRONG_NUMBER_OF_ASSERTIONS:[3,3,1,""],WRONG_NUMBER_OF_AUTHSTATEMENTS:[3,3,1,""],WRONG_NUMBER_OF_SIGNATURES:[3,3,1,""],WRONG_NUMBER_OF_SIGNATURES_IN_ASSERTION:[3,3,1,""],WRONG_NUMBER_OF_SIGNATURES_IN_RESPONSE:[3,3,1,""],WRONG_SIGNED_ELEMENT:[3,3,1,""],WRONG_SUBJECTCONFIRMATION:[3,3,1,""]},"onelogin.saml2.idp_metadata_parser":{OneLogin_Saml2_IdPMetadataParser:[3,1,1,""],dict_deep_merge:[3,6,1,""]},"onelogin.saml2.idp_metadata_parser.OneLogin_Saml2_IdPMetadataParser":{get_metadata:[3,5,1,""],merge_settings:[3,5,1,""],parse:[3,5,1,""],parse_remote:[3,5,1,""]},"onelogin.saml2.logout_request":{OneLogin_Saml2_Logout_Request:[3,1,1,""]},"onelogin.saml2.logout_request.OneLogin_Saml2_Logout_Request":{get_error:[3,2,1,""],get_id:[3,5,1,""],get_issuer:[3,5,1,""],get_nameid:[3,5,1,""],get_nameid_data:[3,5,1,""],get_nameid_format:[3,5,1,""],get_request:[3,2,1,""],get_session_indexes:[3,5,1,""],get_xml:[3,2,1,""],is_valid:[3,2,1,""]},"onelogin.saml2.logout_response":{OneLogin_Saml2_Logout_Response:[3,1,1,""]},"onelogin.saml2.logout_response.OneLogin_Saml2_Logout_Response":{build:[3,2,1,""],get_error:[3,2,1,""],get_in_response_to:[3,2,1,""],get_issuer:[3,2,1,""],get_response:[3,2,1,""],get_status:[3,2,1,""],get_xml:[3,2,1,""],is_valid:[3,2,1,""]},"onelogin.saml2.metadata":{OneLogin_Saml2_Metadata:[3,1,1,""]},"onelogin.saml2.metadata.OneLogin_Saml2_Metadata":{TIME_CACHED:[3,3,1,""],TIME_VALID:[3,3,1,""],add_x509_key_descriptors:[3,5,1,""],builder:[3,5,1,""],sign_metadata:[3,5,1,""]},"onelogin.saml2.response":{OneLogin_Saml2_Response:[3,1,1,""]},"onelogin.saml2.response.OneLogin_Saml2_Response":{check_one_authnstatement:[3,2,1,""],check_one_condition:[3,2,1,""],check_status:[3,2,1,""],get_assertion_id:[3,2,1,""],get_assertion_not_on_or_after:[3,2,1,""],get_attributes:[3,2,1,""],get_audiences:[3,2,1,""],get_authn_contexts:[3,2,1,""],get_error:[3,2,1,""],get_friendlyname_attributes:[3,2,1,""],get_id:[3,2,1,""],get_in_response_to:[3,2,1,""],get_issuers:[3,2,1,""],get_nameid:[3,2,1,""],get_nameid_data:[3,2,1,""],get_nameid_format:[3,2,1,""],get_nameid_nq:[3,2,1,""],get_nameid_spnq:[3,2,1,""],get_session_index:[3,2,1,""],get_session_not_on_or_after:[3,2,1,""],get_xml_document:[3,2,1,""],is_valid:[3,2,1,""],process_signed_elements:[3,2,1,""],validate_num_assertions:[3,2,1,""],validate_signed_elements:[3,2,1,""],validate_timestamps:[3,2,1,""]},"onelogin.saml2.settings":{OneLogin_Saml2_Settings:[3,1,1,""],validate_url:[3,6,1,""]},"onelogin.saml2.settings.OneLogin_Saml2_Settings":{check_idp_settings:[3,2,1,""],check_settings:[3,2,1,""],check_sp_certs:[3,2,1,""],check_sp_settings:[3,2,1,""],format_idp_cert:[3,2,1,""],format_idp_cert_multi:[3,2,1,""],format_sp_cert:[3,2,1,""],format_sp_cert_new:[3,2,1,""],format_sp_key:[3,2,1,""],get_base_path:[3,2,1,""],get_cert_path:[3,2,1,""],get_contacts:[3,2,1,""],get_errors:[3,2,1,""],get_idp_cert:[3,2,1,""],get_idp_data:[3,2,1,""],get_idp_slo_response_url:[3,2,1,""],get_idp_slo_url:[3,2,1,""],get_idp_sso_url:[3,2,1,""],get_lib_path:[3,2,1,""],get_organization:[3,2,1,""],get_schemas_path:[3,2,1,""],get_security_data:[3,2,1,""],get_sp_cert:[3,2,1,""],get_sp_cert_new:[3,2,1,""],get_sp_data:[3,2,1,""],get_sp_key:[3,2,1,""],get_sp_metadata:[3,2,1,""],is_debug_active:[3,2,1,""],is_strict:[3,2,1,""],set_cert_path:[3,2,1,""],set_strict:[3,2,1,""],validate_metadata:[3,2,1,""]},"onelogin.saml2.utils":{OneLogin_Saml2_Utils:[3,1,1,""],print_xmlsec_errors:[3,6,1,""],return_false_on_exception:[3,6,1,""]},"onelogin.saml2.utils.OneLogin_Saml2_Utils":{ASSERTION_SIGNATURE_XPATH:[3,3,1,""],RESPONSE_SIGNATURE_XPATH:[3,3,1,""],TIME_FORMAT:[3,3,1,""],TIME_FORMAT_2:[3,3,1,""],TIME_FORMAT_WITH_FRAGMENT:[3,3,1,""],add_sign:[3,5,1,""],calculate_x509_fingerprint:[3,5,1,""],case_sensitive_urlencode:[3,5,1,""],decode_base64_and_inflate:[3,5,1,""],decrypt_element:[3,5,1,""],deflate_and_base64_encode:[3,5,1,""],delete_local_session:[3,5,1,""],element_text:[3,5,1,""],extract_raw_query_parameter:[3,5,1,""],format_cert:[3,5,1,""],format_finger_print:[3,5,1,""],format_private_key:[3,5,1,""],generate_name_id:[3,5,1,""],generate_unique_id:[3,5,1,""],get_encoded_parameter:[3,5,1,""],get_expire_time:[3,5,1,""],get_self_host:[3,5,1,""],get_self_routed_url_no_query:[3,5,1,""],get_self_url:[3,5,1,""],get_self_url_host:[3,5,1,""],get_self_url_no_query:[3,5,1,""],get_status:[3,5,1,""],is_https:[3,5,1,""],normalize_url:[3,5,1,""],now:[3,5,1,""],parse_SAML_to_time:[3,5,1,""],parse_duration:[3,5,1,""],parse_time_to_SAML:[3,5,1,""],query:[3,5,1,""],redirect:[3,5,1,""],validate_binary_sign:[3,5,1,""],validate_metadata_sign:[3,5,1,""],validate_node_sign:[3,5,1,""],validate_sign:[3,5,1,""],validate_xml:[3,5,1,""],write_temp_file:[3,5,1,""]},"onelogin.saml2.xmlparser":{GlobalParserTLS:[3,1,1,""],RestrictedElement:[3,1,1,""],XML:[3,6,1,""],check_docinfo:[3,6,1,""],fromstring:[3,6,1,""],iterparse:[3,6,1,""],parse:[3,6,1,""]},"onelogin.saml2.xmlparser.GlobalParserTLS":{createDefaultParser:[3,2,1,""],element_class:[3,3,1,""],getDefaultParser:[3,2,1,""],parser_config:[3,3,1,""],setDefaultParser:[3,2,1,""]},"onelogin.saml2.xmlparser.RestrictedElement":{blacklist:[3,3,1,""],getchildren:[3,2,1,""],getiterator:[3,2,1,""],iter:[3,2,1,""],iterchildren:[3,2,1,""],iterdescendants:[3,2,1,""],itersiblings:[3,2,1,""]},onelogin:{saml2:[3,0,0,"-"]}},objnames:{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","attribute","Python attribute"],"4":["py","exception","Python exception"],"5":["py","staticmethod","Python static method"],"6":["py","function","Python function"]},objtypes:{"0":"py:module","1":"py:class","2":"py:method","3":"py:attribute","4":"py:exception","5":"py:staticmethod","6":"py:function"},terms:{"1_5":3,"boolean":3,"case":3,"class":3,"default":3,"function":3,"int":3,"new":3,"public":3,"return":3,"static":3,"transient":3,"true":3,IDs:3,The:3,Used:3,Will:3,_comment:3,_entiti:3,_local:3,_processinginstruct:3,_sre:3,ac_kerbero:3,ac_password:3,ac_password_protect:3,ac_smartcard:3,ac_unspecifi:3,ac_x509:3,accord:3,activ:3,actual:3,add:[2,3],add_encrypt:3,add_sign:3,add_x509_key_descriptor:3,added:3,adding:3,adf:3,aes128:3,aes128_cbc:3,aes192:3,aes192_cbc:3,aes256:3,aes256_cbc:3,after:3,against:3,alg:3,algorithm:3,alia:3,all:3,allow:3,allow_single_label_domain:3,allowed_clock_drift:3,also:3,ani:[2,3],anoth:3,appli:3,applic:[2,3],arg:3,argument:3,arrai:3,artifact:3,assert:[2,3],assertion_expir:3,assertion_signature_xpath:3,assertion_too_earli:3,associ:3,attribut:3,attributestat:3,attrnam:3,attrname_format_bas:3,attrname_format_unspecifi:3,attrname_format_uri:3,audienc:3,auth:[0,1,2],authent:3,authn_context_mismatch:3,authn_request:[0,1,2],authnrequest:[2,3],authnrequestssign:3,authnsign:3,authnstat:3,auxiliari:3,backward:3,base64:3,base64encod:3,base:3,base_url:3,basestr:3,basic:3,bearer:3,been:3,befor:3,behaviour:3,binari:3,bind:3,binding_defl:3,binding_http_artifact:3,binding_http_post:3,binding_http_redirect:3,binding_soap:3,blacklist:3,bodi:3,bool:3,both:3,build:[2,3],build_request_signatur:3,build_response_signatur:3,builder:3,cach:3,cache_dur:3,calcul:3,calculate_x509_fingerprint:3,call:3,callback:3,can:[2,3],canon:3,case_sensitive_urlencod:3,caus:3,cbc:3,cert:3,cert_not_found:3,certif:3,check:3,check_docinfo:3,check_idp_set:3,check_one_authnstat:3,check_one_condit:3,check_set:3,check_sp_cert:3,check_sp_set:3,check_statu:3,children:3,children_node_not_found_in_keyinfo:3,cm_bearer:3,cm_holder_kei:3,cm_sender_vouch:3,code:3,comment:3,common:3,compar:3,comparison:3,compat:3,complic:[2,3],condit:3,connect:[2,3],consist:3,constant:[0,1,2],consum:[2,3],contact:3,contain:3,content:[0,1],context:3,convert:3,copyright:[2,3],could:3,creat:3,createdefaultpars:3,current:3,custom:3,custom_base_path:3,data:3,date:3,datetim:3,ddthh:3,deactiv:3,debug:3,declar:3,decod:3,decode_base64_and_infl:3,decor:3,decrypt:3,decrypt_el:3,deep:3,defat:3,defin:3,deflat:3,deflate_and_base64_encod:3,delet:3,delete_local_sess:3,delete_session_cb:3,deprec:3,deprecated_algorithm:3,deprecated_digest_method:3,deprecated_signature_method:3,depth:3,descend:3,descriptor:3,destroi:3,determin:3,dict:3,dict_deep_merg:3,dictionari:3,differ:3,digest:3,digest_algorithm:3,digit:[2,3],direct:3,directli:3,disabl:3,diverg:3,docinfo:3,document:3,doe:3,dom:3,domain:3,domdocu:3,domel:3,dsa:3,dsa_sha1:3,dtd:3,duplicated_attribute_name_found:3,duplicated_id_in_signed_el:3,duplicated_reference_in_signed_el:3,durat:3,dure:3,earliest:3,effici:3,either:3,element:3,element_class:3,element_text:3,elementbas:3,elementtre:3,emailaddress:3,empti:3,empty_destin:3,empty_nameid:3,enabl:[2,3],encod:3,encrypt:[2,3],encrypted_attribut:3,encrypted_data:3,encryptedattribut:3,endpoint:[2,3],entiti:3,entity_id:3,entitydescriptor:3,envelop:3,equival:3,error:[0,1,2],error_object:3,error_subject:3,etre:3,exampl:3,except:3,execut:3,exist:3,expect:3,expir:3,expiri:3,expres:3,extra:3,extract:3,extract_raw_query_paramet:3,factori:3,fail:3,failur:3,fals:3,field:3,fiendlynam:3,file:3,filenam:3,filter:3,find:3,fingerprint:3,fingerprintalg:3,first:3,flag:3,folder:3,follow:3,footer:3,forbid_dtd:3,forbid_ent:3,force_authn:3,forceauthn:3,forget:[2,3],form:3,format:3,format_cert:3,format_finger_print:3,format_idp_cert:3,format_idp_cert_multi:3,format_private_kei:3,format_sp_cert:3,format_sp_cert_new:3,format_sp_kei:3,forward:3,found:3,friendlynam:3,from:3,fromstr:3,func:3,futur:3,gener:3,generate_name_id:3,generate_unique_id:3,get:3,get_assertion_id:3,get_assertion_not_on_or_aft:3,get_attribut:3,get_audi:3,get_authn_context:3,get_base_path:3,get_cert_path:3,get_contact:3,get_data:3,get_encoded_paramet:3,get_error:3,get_expire_tim:3,get_friendlyname_attribut:3,get_id:3,get_idp_cert:3,get_idp_data:3,get_idp_slo_response_url:3,get_idp_slo_url:3,get_idp_sso_url:3,get_in_response_to:3,get_issu:3,get_last_assertion_id:3,get_last_assertion_not_on_or_aft:3,get_last_authn_context:3,get_last_error_reason:3,get_last_message_id:3,get_last_request_id:3,get_last_request_xml:3,get_last_response_xml:3,get_lib_path:3,get_metadata:3,get_nameid:3,get_nameid_data:3,get_nameid_format:3,get_nameid_nq:3,get_nameid_spnq:3,get_organ:3,get_request:3,get_respons:3,get_schemas_path:3,get_security_data:3,get_self_host:3,get_self_routed_url_no_queri:3,get_self_url:3,get_self_url_host:3,get_self_url_no_queri:3,get_session_expir:3,get_session_index:3,get_session_not_on_or_aft:3,get_set:3,get_slo_response_url:3,get_slo_url:3,get_sp_cert:3,get_sp_cert_new:3,get_sp_data:3,get_sp_kei:3,get_sp_metadata:3,get_sso_url:3,get_statu:3,get_xml:3,get_xml_docu:3,getchildren:3,getdefaultpars:3,getiter:3,given:3,globalparsertl:3,going:3,handl:3,handler:3,happen:3,has:3,head:3,header:3,hex:3,hold:3,holder:3,host:3,http:3,huge_tre:3,iam:[2,3],id_not_found_in_signed_el:3,ident:[2,3],identifi:3,idp:[2,3],idp_metadata:3,idp_metadata_pars:[0,1,2],idpssodescriptor:3,ignor:3,implement:3,in_response_to:3,inc:[2,3],includ:3,index:3,indic:3,inflat:3,info:3,initi:[2,3],initializ:3,inplac:3,inresponseto:3,insensit:3,instanc:3,instead:3,interpret:3,introduc:3,invalid_signatur:3,invalid_signed_el:3,invalid_xml_format:3,invok:3,is_authent:3,is_debug_act:3,is_http:3,is_pass:3,is_strict:3,is_valid:3,iso8601:3,ispass:3,issuer:3,issuer_multiple_in_respons:3,issuer_not_found_in_assert:3,iter:3,iterchildren:3,iterdescend:3,iterent:3,iterpars:3,iters:3,itself:3,keep_local_sess:3,kei:3,kerbero:3,keydescriptor:3,keyinfo_not_found_in_encrypted_data:3,keyword:3,kwarg:3,label:3,last:3,let:[2,3],lhs:3,lib:3,librari:[2,3],licens:[2,3],like:3,line:3,list:3,local:3,localnam:3,login:3,logout:[2,3],logout_request:[0,1,2],logout_respons:[0,1,2],logoutrequest:[2,3],logoutrespons:[2,3],look:3,lower:3,lowercas:3,lowercase_urlencod:3,lxml:3,mai:3,main:3,make:3,match:3,mayb:3,merg:3,merge_set:3,messag:[2,3],metadata:[0,1,2],metadata_sp_invalid:3,method:3,mgf1p:3,missing_condit:3,missing_id:3,missing_statu:3,missing_status_cod:3,mit:[2,3],mode:3,modul:[0,1],more:3,msg:3,multicert:3,multipl:3,multpl:3,must:3,name:3,name_id:3,name_id_format:3,name_id_value_req:3,nameid:[2,3],nameid_email_address:3,nameid_encrypt:3,nameid_ent:3,nameid_kerbero:3,nameid_persist:3,nameid_transi:3,nameid_unspecifi:3,nameid_windows_domain_qualified_nam:3,nameid_x509_subject_nam:3,nameidpolici:3,namequalifi:3,namespac:3,need:3,netloc:3,new_metadata_set:3,newer:3,no_attributestat:3,no_encrypted_assert:3,no_encrypted_nameid:3,no_nameid:3,no_network:3,no_signature_found:3,no_signed_assert:3,no_signed_messag:3,node:3,none:3,nopass:3,normal:3,normalize_url:3,note:3,notonoraft:3,now:3,ns_d:3,ns_md:3,ns_prefix_d:3,ns_prefix_md:3,ns_prefix_saml:3,ns_prefix_samlp:3,ns_prefix_x:3,ns_prefix_xenc:3,ns_prefix_xsi:3,ns_saml:3,ns_samlp:3,ns_soap:3,ns_x:3,ns_xenc:3,ns_xsi:3,nsmap:3,number:3,oaep:3,oasi:3,object:3,objet:3,obtain:3,old_set:3,older:3,one:3,onelogin:0,onelogin_saml2_auth:3,onelogin_saml2_authn_request:3,onelogin_saml2_const:3,onelogin_saml2_error:3,onelogin_saml2_idpmetadatapars:3,onelogin_saml2_logout_request:3,onelogin_saml2_logout_respons:3,onelogin_saml2_metadata:3,onelogin_saml2_respons:3,onelogin_saml2_set:3,onelogin_saml2_util:3,onelogin_saml2_validationerror:3,onli:3,open:[2,3],oppos:3,option:3,order:3,org:3,organ:3,origin:3,other:3,otherwis:3,our:3,out:3,over:[2,3],overrid:3,overridden:3,packag:[0,1],param:3,paramet:3,pars:3,parse_dur:3,parse_remot:3,parse_saml_to_tim:3,parse_time_to_saml:3,parser:3,parser_config:3,part:3,partiallogout:3,pass:3,password:3,passwordprotectedtransport:3,path:3,per:3,persist:3,plan:3,port:3,posix:3,post:3,pre:3,preced:3,prefer:3,present:3,pretty_print_if_poss:3,print_xmlsec_error:3,privat:3,private_key_file_not_found:3,private_key_not_found:3,process:3,process_respons:3,process_signed_el:3,process_slo:3,processinginstruct:3,produc:3,properli:3,protect:3,protocol:3,provid:[2,3],proxycountexceed:3,pubic:3,public_cert_file_not_found:3,publish:[2,3],python:[2,3],qualifi:3,queri:3,query_str:3,quote_plu:3,rais:3,raise_except:3,raw:3,reason:3,receiv:3,redirect:3,redirect_invalid_url:3,redirect_to:3,refer:3,rel:3,relat:3,relay_st:3,remove_com:3,remove_pi:3,represent:3,request:3,request_data:3,request_id:3,requir:3,required_slo_bind:3,required_sso_bind:3,resolve_ent:3,respond:3,respons:[0,1,2],response_expir:3,response_signature_xpath:3,restrict:3,restrictedel:3,result:3,retriev:3,return_false_on_except:3,return_to:3,revers:3,rfc1951:3,rfc:3,rhs:3,right:3,root:3,rout:3,rsa:3,rsa_1_5:3,rsa_oaep_mgf1p:3,rsa_sha1:3,rsa_sha256:3,rsa_sha384:3,rsa_sha512:3,rtype:3,same:3,saml2:[0,1,2],saml:[2,3],saml_logoutmessage_not_found:3,saml_logoutrequest_invalid:3,saml_logoutresponse_invalid:3,saml_request:3,saml_respons:3,saml_response_not_found:3,saml_single_logout_not_support:3,samlp:3,samlrespons:3,schema:3,search:3,second:3,secur:3,see:3,self:3,send:3,sender:3,sent:3,sequenc:3,servic:[2,3],session:3,session_expir:3,session_index:3,sessionindex:3,sessionnotonoraft:3,set:[0,1,2],set_cert_path:3,set_nameid_polici:3,set_strict:3,setdefaultpars:3,settings_file_not_found:3,settings_invalid:3,settings_invalid_syntax:3,sever:3,sha1:3,sha256:3,sha384:3,sha512:3,should:3,show:3,sibl:3,sign:[2,3],sign_algorithm:3,sign_metadata:3,signatur:[2,3],signature_nod:3,signed_el:3,signed_queri:3,simpli:3,sinc:3,singl:[2,3],slo:[2,3],smartcard:3,soap:3,softwar:[2,3],some:3,someth:3,soon:3,sourc:[2,3],sp_certs_not_found:3,sp_format:3,sp_name_qualifier_name_mismatch:3,sp_nq:3,sp_validation_onli:3,specif:3,specifi:3,split:3,spnamequalifi:3,spnq:3,sre_pattern:3,sso:[2,3],standard:3,start:3,statu:3,status_code_is_not_success:3,status_no_pass:3,status_partial_logout:3,status_proxy_count_exceed:3,status_request:3,status_respond:3,status_success:3,status_version_mismatch:3,store:3,str:3,strict:3,string:3,sub:3,subject:3,subjectconfirmationdata:3,submodul:[0,1,2],subpackag:[0,1],subtre:3,success:3,support:[2,3],suppress:3,sure:3,tag:3,tagid:3,target:3,temporari:3,text:3,than:3,thi:[2,3],those:[2,3],thread:3,time:3,time_cach:3,time_format:3,time_format_2:3,time_format_with_frag:3,time_valid:3,timestamp:3,timestr:3,to_encod:3,toolkit:[2,3],tree:3,tripled:3,tripledes_cbc:3,type:3,unexpected_signed_el:3,unformat:3,uniqu:3,unix:3,unsign:3,unspecifi:3,unsupported_retrieval_method:3,unsupported_saml_vers:3,unsupported_settings_object:3,until:3,updat:3,upper:3,uri:3,url:3,urllib:3,urn:3,use:[2,3],used:3,user:3,uses:3,using:[2,3],util:[0,1,2],valid:3,valid_until:3,validate_binary_sign:3,validate_cert:3,validate_metadata:3,validate_metadata_sign:3,validate_node_sign:3,validate_num_assert:3,validate_sign:3,validate_signed_el:3,validate_timestamp:3,validate_url:3,validate_xml:3,validatecert:3,valu:3,verif:3,verifi:3,version:3,versionmismatch:3,view:3,vouch:3,wai:3,want:3,wantassertionssign:3,went:3,what:3,when:3,where:3,whether:3,which:[2,3],wildcard:3,windowsdomainqualifiednam:3,wrap:3,write:3,write_temp_fil:3,wrong:3,wrong_audi:3,wrong_destin:3,wrong_inresponseto:3,wrong_issu:3,wrong_number_of_assert:3,wrong_number_of_authstat:3,wrong_number_of_signatur:3,wrong_number_of_signatures_in_assert:3,wrong_number_of_signatures_in_respons:3,wrong_signed_el:3,wrong_subjectconfirm:3,wsign:3,www:3,x509:3,x509_cert:3,x509cert:3,x509subjectnam:3,xenc:3,xml:3,xmldsig:3,xmlenc:3,xmlparser:[0,1,2],xmlschema:3,xmlsec:3,xmlsoap:3,xpath:3,xsi:3,yield:3,you:[2,3],your:[2,3],yyyi:3},titles:["Welcome to SAML Python Toolkit\u2019s documentation!","onelogin","onelogin package","onelogin.saml2 package"],titleterms:{auth:3,authn_request:3,constant:3,content:[2,3],document:0,error:3,idp_metadata_pars:3,logout_request:3,logout_respons:3,metadata:3,modul:[2,3],onelogin:[1,2,3],packag:[2,3],python:0,respons:3,saml2:3,saml:0,set:3,submodul:3,subpackag:2,toolkit:0,util:3,welcom:0,xmlparser:3}}) \ No newline at end of file diff --git a/example.cfg b/example.cfg deleted file mode 100644 index 1e906a8c..00000000 --- a/example.cfg +++ /dev/null @@ -1,22 +0,0 @@ -[app] -host = localhost -port = 7070 - -[saml] -issuer = python-saml -# Email address is a common format to use when verifying identity -name_identifier_format = urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress -# The SAML Consumer URL from your OneLogin app -assertion_consumer_service_url = -# The SAML Login URL from your OneLogin app -idp_sso_target_url = -# The x.509 certificate fingerprint from OneLogin, found under Security->SAML, -# as a file path. Only one of idp_cert_file and idp_cert_fingerprint will be -# used by the example app, with idp_cert_file having priority if both are -# defined. -idp_cert_file = -# The x.509 certificate fingerprint from OneLogin, found under Security->SAML, -# as a string. Only one of idp_cert_file and idp_cert_fingerprint will be -# used by the example app, with idp_cert_file having priority if both are -# defined. -idp_cert_fingerprint = diff --git a/example.py b/example.py deleted file mode 100644 index f244bef1..00000000 --- a/example.py +++ /dev/null @@ -1,182 +0,0 @@ -import os -import ConfigParser -import optparse -import logging -import shutil -import urlparse - -from StringIO import StringIO -from BaseHTTPServer import BaseHTTPRequestHandler -from BaseHTTPServer import HTTPServer - -from onelogin.saml import AuthRequest, Response - -__version__ = '0.1' - -log = logging.getLogger(__name__) - -class SampleAppHTTPRequestHandler(BaseHTTPRequestHandler): - server_version = 'SampleAppHTTPRequestHandler/%s' % __version__ - - def _serve_msg(self, code, msg): - f = StringIO() - f.write('') - f.write("\n\n%s\n\n" % msg) - length = f.tell() - f.seek(0) - - if code == 200: - self.send_response(code) - else: - self.send_error(code, message=msg) - - self.send_header('Content-type', 'text/html') - self.send_header('Content-Length', str(length)) - self.end_headers() - - shutil.copyfileobj(f, self.wfile) - - def _bad_request(self): - """Serve Bad Request (400).""" - self._serve_msg(400, 'Bad Request') - - def log_message(self, format, *args): - log.info(format % args) - - def do_HEAD(self): - """Serve a HEAD request.""" - self._bad_request() - - def do_DEL(self): - """Serve a DEL request.""" - self._bad_request() - - def do_GET(self): - """Serve a GET request.""" - if not self.path == '/': - self._bad_request() - return - - url = AuthnRequest.create(**self.settings) - self.send_response(301) - self.send_header("Location", url) - self.end_headers() - - def do_POST(self): - """Serve a POST request.""" - if not self.path == self.saml_post_path: - self._bad_request() - return - - length = int(self.headers['Content-Length']) - data = self.rfile.read(length) - query = urlparse.parse_qs(data) - res = Response( - query['SAMLResponse'].pop(), - self.settings['idp_cert_fingerprint'], - ) - valid = res.is_valid() - name_id = res.name_id - if valid: - msg = 'The identity of {name_id} has been verified'.format( - name_id=name_id, - ) - self._serve_msg(200, msg) - else: - msg = '{name_id} is not authorized to use this resource'.format( - name_id=name_id, - ) - self._serve_msg(401, msg) - -def main(config_file): - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s.%(msecs)03d example: %(levelname)s: %(message)s', - datefmt='%Y-%m-%dT%H:%M:%S', - ) - - config = ConfigParser.RawConfigParser() - config_path = os.path.expanduser(config_file) - config_path = os.path.abspath(config_path) - with open(config_path) as f: - config.readfp(f) - - host = config.get('app', 'host') - port = config.get('app', 'port') - port = int(port) - - settings = dict() - settings['assertion_consumer_service_url'] = config.get( - 'saml', - 'assertion_consumer_service_url' - ) - settings['issuer'] = config.get( - 'saml', - 'issuer' - ) - settings['name_identifier_format'] = config.get( - 'saml', - 'name_identifier_format' - ) - settings['idp_sso_target_url'] = config.get( - 'saml', - 'idp_sso_target_url' - ) - settings['idp_cert_file'] = config.get( - 'saml', - 'idp_cert_file' - ) - settings['idp_cert_fingerprint'] = config.get( - 'saml', - 'idp_cert_fingerprint' - ) - - cert_file = settings.pop('idp_cert_file', None) - - # idp_cert_file has priority over idp_cert_fingerprint - if cert_file: - cert_path = os.path.expanduser(cert_file) - cert_path = os.path.abspath(cert_path) - - with open(cert_path) as f: - settings['idp_cert_fingerprint'] = f.read() - - parts = urlparse.urlparse(settings['assertion_consumer_service_url']) - SampleAppHTTPRequestHandler.protocol_version = 'HTTP/1.0' - SampleAppHTTPRequestHandler.settings = settings - SampleAppHTTPRequestHandler.saml_post_path = parts.path - httpd = HTTPServer( - (host, port), - SampleAppHTTPRequestHandler, - ) - - socket_name = httpd.socket.getsockname() - - log.info( - 'Serving HTTP on {host} port {port} ...'.format( - host=socket_name[0], - port=socket_name[1], - ) - ) - - httpd.serve_forever() - -if __name__ == '__main__': - parser = optparse.OptionParser( - usage='%prog [OPTS]', - ) - parser.add_option( - '--config-file', - metavar='PATH', - help='The configuration file containing the app and SAML settings', - ) - - parser.set_defaults( - config_file='example.cfg' - ) - - options, args = parser.parse_args() - if args: - parser.error('Wrong number of arguments') - - main(options.config_file) diff --git a/onelogin/__init__.py b/onelogin/__init__.py deleted file mode 100644 index de40ea7c..00000000 --- a/onelogin/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__import__('pkg_resources').declare_namespace(__name__) diff --git a/onelogin/saml/AuthRequest.py b/onelogin/saml/AuthRequest.py deleted file mode 100644 index ae5387c0..00000000 --- a/onelogin/saml/AuthRequest.py +++ /dev/null @@ -1,104 +0,0 @@ -import zlib -import base64 -import uuid -import urllib - -from datetime import datetime -from lxml import etree -from lxml.builder import ElementMaker - -def create( - _clock=None, - _uuid=None, - _zlib=None, - _base64=None, - _urllib=None, - **kwargs - ): - """Create a URL string which can be used to redirect a samlp:AuthnRequest to the identity provider. - Return a URL string containing the idp_sso_target_url and a deflated, base64-encoded, url-encoded (in that order) samlp:AuthnRequest XML element as the value of the SAMLRequest parameter. - - Keyword arguments: - assertion_consumer_service_url -- The URL at which the SAML assertion should be received. - issuer -- The name of your application. Some identity providers might need this to establish the identity of the service provider requesting the login. - name_identifier_format -- The format of the username required by this application. If you need the email address, use "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress". See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf section 8.3 for other options. Note that the identity provider might not support all options. - idp_sso_target_url -- The URL to which the authentication request should be sent. This would be on the identity -""" - if _clock is None: - _clock = datetime.utcnow - if _uuid is None: - _uuid = uuid.uuid4 - if _zlib is None: - _zlib = zlib - if _base64 is None: - _base64 = base64 - if _urllib is None: - _urllib = urllib - - assertion_consumer_service_url = kwargs.pop( - 'assertion_consumer_service_url', - ) - issuer = kwargs.pop('issuer') - name_identifier_format = kwargs.pop('name_identifier_format') - idp_sso_target_url = kwargs.pop('idp_sso_target_url') - - now = _clock() - # Resolution finer than milliseconds not allowed - # http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf Section - # 1.3.3 - now = now.replace(microsecond=0) - now_iso = now.isoformat() - - unique_id = _uuid() - unique_id = unique_id.hex - - samlp_maker = ElementMaker( - namespace='urn:oasis:names:tc:SAML:2.0:protocol', - nsmap=dict(samlp='urn:oasis:names:tc:SAML:2.0:protocol'), - ) - saml_maker = ElementMaker( - namespace='urn:oasis:names:tc:SAML:2.0:assertion', - nsmap=dict(saml='urn:oasis:names:tc:SAML:2.0:assertion'), - ) - - authn_request = samlp_maker.AuthnRequest( - ProtocolBinding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', - Version='2.0', - IssueInstant=now_iso, - ID=unique_id, - AssertionConsumerServiceURL=assertion_consumer_service_url, - ) - - saml_issuer = saml_maker.Issuer() - saml_issuer.text = issuer - authn_request.append(saml_issuer) - - name_id_policy = samlp_maker.NameIDPolicy( - Format=name_identifier_format, - AllowCreate='true', - ) - authn_request.append(name_id_policy) - - request_authn_context = samlp_maker.RequestedAuthnContext( - Comparison='exact', - ) - authn_request.append(request_authn_context) - - authn_context_class_ref = saml_maker.AuthnContextClassRef() - authn_context_class_ref.text = ('urn:oasis:names:tc:SAML:2.0:ac:classes:' - + 'PasswordProtectedTransport' - ) - request_authn_context.append(authn_context_class_ref) - - compressed_request = _zlib.compress(etree.tostring(authn_request)) - # Strip the first 2 bytes (header) and the last 4 bytes (checksum) to get the raw deflate - deflated_request = compressed_request[2:-4] - encoded_request = _base64.b64encode(deflated_request) - urlencoded_request = _urllib.urlencode( - [('SAMLRequest', encoded_request)], - ) - - return '{url}?{query}'.format( - url=idp_sso_target_url, - query=urlencoded_request, - ) diff --git a/onelogin/saml/Response.py b/onelogin/saml/Response.py deleted file mode 100644 index 2bffc4e4..00000000 --- a/onelogin/saml/Response.py +++ /dev/null @@ -1,142 +0,0 @@ -import base64 - -from lxml import etree -from datetime import datetime, timedelta - -from onelogin.saml import SignatureVerifier - -namespaces=dict( - samlp='urn:oasis:names:tc:SAML:2.0:protocol', - saml='urn:oasis:names:tc:SAML:2.0:assertion', - ) - -class ResponseValidationError(Exception): - """There was a problem validating the response""" - def __init__(self, msg): - self._msg = msg - - def __str__(self): - return '%s: %s' % (self.__doc__, self._msg) - -class ResponseNameIDError(Exception): - """There was a problem getting the name ID""" - def __init__(self, msg): - self._msg = msg - - def __str__(self): - return '%s: %s' % (self.__doc__, self._msg) - -class ResponseConditionError(Exception): - """There was a problem validating a condition""" - def __init__(self, msg): - self._msg = msg - - def __str__(self): - return '%s: %s' % (self.__doc__, self._msg) - -class Response(object): - def __init__( - self, - response, - signature, - _base64=None, - _etree=None, - ): - """ - Extract information from an samlp:Response - Arguments: - response -- The base64 encoded, XML string containing the samlp:Response - signature -- The fingerprint to check the samlp:Response against - """ - if _base64 is None: - _base64 = base64 - if _etree is None: - _etree = etree - - decoded_response = _base64.b64decode(response) - self._document = _etree.fromstring(decoded_response) - self._signature = signature - - def _parse_datetime(self, dt): - return datetime.strptime(dt, '%Y-%m-%dT%H:%M:%SZ') - - def _get_name_id(self): - result = self._document.xpath( - '/samlp:Response/saml:Assertion/saml:Subject/saml:NameID', - namespaces=namespaces, - ) - length = len(result) - if length > 1: - raise ResponseNameIDError( - 'Found more than one name ID' - ) - if length == 0: - raise ResponseNameIDError( - 'Did not find a name ID' - ) - - node = result.pop() - - return node.text.strip() - - name_id = property( - fget=_get_name_id, - doc="The value requested in the name_identifier_format, e.g., the user's email address", - ) - - def get_assertion_attribute_value(self,attribute_name): - """ - Get the value of an AssertionAttribute, located in an Assertion/AttributeStatement/Attribute[@Name=attribute_name/AttributeValue tag - """ - result = self._document.xpath('/samlp:Response/saml:Assertion/saml:AttributeStatement/saml:Attribute[@Name="%s"]/saml:AttributeValue'%attribute_name,namespaces=namespaces) - return [n.text.strip() for n in result] - - def is_valid( - self, - _clock=None, - _verifier=None, - ): - """ - Verify that the samlp:Response is valid. - Return True if valid, otherwise False. - """ - if _clock is None: - _clock = datetime.utcnow - if _verifier is None: - _verifier = SignatureVerifier.verify - - conditions = self._document.xpath( - '/samlp:Response/saml:Assertion/saml:Conditions', - namespaces=namespaces, - ) - - now = _clock() - - not_before = None - not_on_or_after = None - for condition in conditions: - not_on_or_after = condition.attrib.get('NotOnOrAfter', None) - not_before = condition.attrib.get('NotBefore', None) - - if not_before is None: - #notbefore condition is not mandatory. If it is not specified, use yesterday as not_before condition - not_before = (now-timedelta(1,0,0)).strftime('%Y-%m-%dT%H:%M:%SZ') - if not_on_or_after is None: - raise ResponseConditionError('Did not find NotOnOrAfter condition') - - not_before = self._parse_datetime(not_before) - not_on_or_after = self._parse_datetime(not_on_or_after) - - if now < not_before: - raise ResponseValidationError( - 'Current time is earlier than NotBefore condition' - ) - if now >= not_on_or_after: - raise ResponseValidationError( - 'Current time is on or after NotOnOrAfter condition' - ) - - return _verifier( - self._document, - self._signature, - ) diff --git a/onelogin/saml/SignatureVerifier.py b/onelogin/saml/SignatureVerifier.py deleted file mode 100644 index 30731f6b..00000000 --- a/onelogin/saml/SignatureVerifier.py +++ /dev/null @@ -1,139 +0,0 @@ -import os -import subprocess -import platform -import tempfile -import logging - -from lxml import etree - -log = logging.getLogger(__name__) - -class SignatureVerifierError(Exception): - """There was a problem validating the response""" - def __init__(self, msg): - self._msg = msg - - def __str__(self): - return '%s: %s' % (self.__doc__, self._msg) - -def _parse_stderr(proc): - output = proc.stderr.read() - for line in output.split('\n'): - line = line.strip() - if line == 'OK': - return True - elif line == 'FAIL': - [log.info('XMLSec: %s' % line) - for line in output.split('\n') - if line - ] - return False - - # If neither success nor failure - if proc.returncode is not 0: - msg = ('XMLSec returned error code %s. Please check your ' - + 'certficate.' - ) - raise SignatureVerifierError(msg % proc.returncode) - - # Should not happen - raise SignatureVerifierError( - ('XMLSec exited with code 0 but did not return OK when verifying the ' - + ' SAML response.' - ) - ) - -def _get_xmlsec_bin(_platform=None): - if _platform is None: - _platform = platform - - xmlsec_bin = 'xmlsec1' - if _platform.system() == 'Windows': - xmlsec_bin = 'xmlsec.exe' - - return xmlsec_bin - -def verify( - document, - signature, - _etree=None, - _tempfile=None, - _subprocess=None, - _os=None, - ): - """ - Verify that signature contained in the samlp:Response is valid when checked against the provided signature. - Return True if valid, otherwise False - Arguments: - document -- lxml.etree.XML object containing the samlp:Response - signature -- The fingerprint to check the samlp:Response against - """ - if _etree is None: - _etree = etree - if _tempfile is None: - _tempfile = tempfile - if _subprocess is None: - _subprocess = subprocess - if _os is None: - _os = os - - xmlsec_bin = _get_xmlsec_bin() - - verified = False - cert_filename = None - xml_filename = None - # Windows hack: Without the delete=False parameter in NamedTemporaryFile - # xmlsec.exe will get an IO Permission Denied error. - try: - with _tempfile.NamedTemporaryFile(delete=False) as xml_fp: - doc_str = _etree.tostring(document) - xml_fp.write(doc_str) - xml_fp.seek(0) - with _tempfile.NamedTemporaryFile(delete=False) as cert_fp: - if signature.startswith( - '-----BEGIN CERTIFICATE-----' - ): - # If there's no matching 'END CERTIFICATE' - # cryptpAppKeyLoad will fail - cert_fp.write(signature) - else: - cert_fp.write( - '{begin}\n{signature}\n{end}'.format( - begin='-----BEGIN CERTIFICATE-----', - signature=signature, - end='-----END CERTIFICATE-----', - ) - ) - cert_fp.seek(0) - - cert_filename = cert_fp.name - xml_filename = xml_fp.name - - # We cannot use xmlsec python bindings to verify here because - # that would require a call to libxml2.xmlAddID. The libxml2 python - # bindings do not yet provide this function. - # http://www.aleksey.com/xmlsec/faq.html Section 3.2 - cmds = [ - xmlsec_bin, - '--verify', - '--pubkey-cert-pem', - cert_filename, - '--id-attr:ID', - 'urn:oasis:names:tc:SAML:2.0:assertion:Assertion', - xml_filename, - ] - - proc = _subprocess.Popen( - cmds, - stderr=_subprocess.PIPE, - stdout=_subprocess.PIPE, - ) - proc.wait() - verified = _parse_stderr(proc) - finally: - if cert_filename is not None: - _os.remove(cert_filename) - if xml_filename is not None: - _os.remove(xml_filename) - - return verified diff --git a/onelogin/saml/__init__.py b/onelogin/saml/__init__.py deleted file mode 100644 index 94d244a4..00000000 --- a/onelogin/saml/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from Response import ( - Response, - ResponseValidationError, - ResponseNameIDError, - ResponseConditionError, - ) -import AuthRequest -import SignatureVerifier diff --git a/onelogin/saml/test/TestAuthRequest.py b/onelogin/saml/test/TestAuthRequest.py deleted file mode 100644 index a3ede75d..00000000 --- a/onelogin/saml/test/TestAuthRequest.py +++ /dev/null @@ -1,59 +0,0 @@ -import fudge - -from datetime import datetime -from nose.tools import eq_ as eq - -from onelogin.saml import AuthnRequest - -class TestAuthnRequest(object): - def setUp(self): - fudge.clear_expectations() - - @fudge.with_fakes - def test_create(self): - fake_uuid_func = fudge.Fake('uuid', callable=True) - fake_uuid_func.with_arg_count(0) - fake_uuid = fudge.Fake('foo_uuid') - fake_uuid.has_attr(hex='hex_uuid') - fake_uuid = fake_uuid_func.returns(fake_uuid) - - def fake_clock(): - return datetime(2011, 7, 9, 19, 24, 52, 325405) - - fake_zlib = fudge.Fake('zlib') - fake_zlib.remember_order() - fake_compress = fake_zlib.expects('compress') - fake_compress.with_args( -"""foo_issuerurn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport""" -) - fake_compress.returns('HDfoo_compressedCHCK') - - fake_base64 = fudge.Fake('base64') - fake_base64.remember_order() - fake_encode = fake_base64.expects('b64encode') - fake_encode.with_args('foo_compressed') - fake_encode.returns('foo_encoded') - - fake_urllib = fudge.Fake('urllib') - fake_urllib.remember_order() - fake_urlencode = fake_urllib.expects('urlencode') - fake_urlencode.with_args( - [('SAMLRequest', 'foo_encoded')], - ) - fake_urlencode.returns('foo_urlencoded') - - req = AuthnRequest.create( - _clock=fake_clock, - _uuid=fake_uuid_func, - _zlib=fake_zlib, - _base64=fake_base64, - _urllib=fake_urllib, - assertion_consumer_service_url='http://foo.bar/consume', - issuer='foo_issuer', - name_identifier_format=('urn:oasis:names:tc:SAML:1.1:nameid-format:' - + 'emailAddress' - ), - idp_sso_target_url='http://foo.idp.bar', - ) - - eq(req, 'http://foo.idp.bar?foo_urlencoded') diff --git a/onelogin/saml/test/TestResponse.py b/onelogin/saml/test/TestResponse.py deleted file mode 100644 index 52a3bbf4..00000000 --- a/onelogin/saml/test/TestResponse.py +++ /dev/null @@ -1,462 +0,0 @@ -from datetime import datetime - -import base64 -import fudge - -from nose.tools import eq_ as eq - -from onelogin.saml.test.util import assert_raises -from onelogin.saml import ( - Response, - ResponseValidationError, - ResponseNameIDError, - ResponseConditionError, - ) - -test_response = """ - https://idp.example.org/SAML2 - - - - - https://idp.example.org/SAML2 - foo signature - - - 3f7b3dcf-1674-4ecd-92c8-1544f346baf8 - - - - - - - - https://sp.example.com/SAML2 - - - - - - urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport - - - - - -""" - -class TestResponse(object): - def setUp(self): - fudge.clear_expectations() - - @fudge.with_fakes - def test__init__(self): - fake_base64 = fudge.Fake('base64') - fake_base64.remember_order() - decode = fake_base64.expects('b64decode') - decode.with_args('foo response') - decode.returns('foo decoded response') - - fake_etree = fudge.Fake('etree') - fake_etree.remember_order() - from_string = fake_etree.expects('fromstring') - from_string.with_args('foo decoded response') - from_string.returns('foo document') - - res = Response( - response='foo response', - signature='foo signature', - _base64=fake_base64, - _etree=fake_etree, - ) - - eq(res._document, 'foo document') - eq(res._signature, 'foo signature') - - @fudge.with_fakes - def test_get_name_id_simple(self): - encoded_response = base64.b64encode(test_response) - res = Response( - response=encoded_response, - signature=None, - ) - name_id = res.name_id - - eq('3f7b3dcf-1674-4ecd-92c8-1544f346baf8', name_id) - - @fudge.with_fakes - def test_get_name_id_multiple(self): - response = """ - https://idp.example.org/SAML2 - - - - - https://idp.example.org/SAML2 - ... - - - 3f7b3dcf-1674-4ecd-92c8-1544f346baf8 - - - foo-copy - - - - - - - - https://sp.example.com/SAML2 - - - - - - urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport - - - - - -""" - encoded_response = base64.b64encode(response) - res = Response( - response=encoded_response, - signature=None, - ) - msg = assert_raises( - ResponseNameIDError, - res._get_name_id, - ) - - eq( - str(msg), - ('There was a problem getting the name ID: Found more than one ' - + 'name ID' - ), - ) - - @fudge.with_fakes - def test_get_name_id_none(self): - response = """ - https://idp.example.org/SAML2 - - - - - https://idp.example.org/SAML2 - ... - - - - - - - - https://sp.example.com/SAML2 - - - - - - urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport - - - - - -""" - encoded_response = base64.b64encode(response) - res = Response( - response=encoded_response, - signature=None, - ) - msg = assert_raises( - ResponseNameIDError, - res._get_name_id, - ) - - eq( - str(msg), - ('There was a problem getting the name ID: Did not find a name ' - + 'ID' - ), - ) - - @fudge.with_fakes - def test_is_valid_not_before_missing(self): - response = """ - https://idp.example.org/SAML2 - - - - - https://idp.example.org/SAML2 - foo signature - - - 3f7b3dcf-1674-4ecd-92c8-1544f346baf8 - - - - - - - - https://sp.example.com/SAML2 - - - - - - urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport - - - - - -""" - encoded_response = base64.b64encode(response) - res = Response( - response=encoded_response, - signature=None, - ) - msg = assert_raises( - ResponseConditionError, - res.is_valid, - ) - - eq(str(msg), - ('There was a problem validating a condition: Did not find NotBefore ' - + 'condition' - ), - ) - - @fudge.with_fakes - def test_is_valid_not_on_or_after_missing(self): - response = """ - https://idp.example.org/SAML2 - - - - - https://idp.example.org/SAML2 - foo signature - - - 3f7b3dcf-1674-4ecd-92c8-1544f346baf8 - - - - - - - - https://sp.example.com/SAML2 - - - - - - urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport - - - - - -""" - encoded_response = base64.b64encode(response) - res = Response( - response=encoded_response, - signature=None, - ) - msg = assert_raises( - ResponseConditionError, - res.is_valid, - ) - - eq(str(msg), - ('There was a problem validating a condition: Did not find ' - + 'NotOnOrAfter condition' - ), - ) - - @fudge.with_fakes - def test_is_valid_current_time_earlier(self): - encoded_response = base64.b64encode(test_response) - res = Response( - response=encoded_response, - signature=None, - ) - - def fake_clock(): - return datetime(2004, 12, 05, 9, 16, 45, 462796) - msg = assert_raises( - ResponseValidationError, - res.is_valid, - _clock=fake_clock, - ) - - eq(str(msg), - ('There was a problem validating the response: Current time is ' - + 'earlier than NotBefore condition' - ), - ) - - @fudge.with_fakes - def test_is_valid_current_time_on_or_after(self): - encoded_response = base64.b64encode(test_response) - res = Response( - response=encoded_response, - signature=None, - ) - - def fake_clock(): - return datetime(2004, 12, 05, 9, 30, 45, 462796) - msg = assert_raises( - ResponseValidationError, - res.is_valid, - _clock=fake_clock, - ) - - eq(str(msg), - ('There was a problem validating the response: Current time is ' - + 'on or after NotOnOrAfter condition' - ), - ) - - @fudge.with_fakes - def test_is_valid_simple(self): - encoded_response = base64.b64encode(test_response) - res = Response( - response=encoded_response, - signature='foo signature', - ) - - def fake_clock(): - return datetime(2004, 12, 05, 9, 18, 45, 462796) - - fake_verifier = fudge.Fake( - 'verifier', - callable=True, - ) - fake_verifier.times_called(1) - fake_verifier.with_args(res._document, 'foo signature') - - fake_verifier.returns(True) - - msg = res.is_valid( - _clock=fake_clock, - _verifier=fake_verifier, - ) - - eq(msg, True) diff --git a/onelogin/saml/test/TestSignatureVerifier.py b/onelogin/saml/test/TestSignatureVerifier.py deleted file mode 100644 index 6c6cd46a..00000000 --- a/onelogin/saml/test/TestSignatureVerifier.py +++ /dev/null @@ -1,230 +0,0 @@ -import fudge - -from lxml import etree -from nose.tools import eq_ as eq -from StringIO import StringIO - -from onelogin.saml import SignatureVerifier - -from onelogin.saml.test.util import assert_raises - -class TestSignatureVerifier(object): - def setUp(self): - fudge.clear_expectations() - - @fudge.with_fakes - def test_verify_simple(self): - document = etree.XML('foo doc') - - fake_etree = fudge.Fake('etree') - fake_etree.remember_order() - to_string = fake_etree.expects('tostring') - to_string.with_args(document) - to_string.returns('foo doc') - - fake_tempfile = fudge.Fake('tempfile') - fake_tempfile.remember_order() - named_xmlfile = fake_tempfile.expects( - 'NamedTemporaryFile' - ) - named_xmlfile.with_args(delete=False) - xmlfile = named_xmlfile.returns_fake() - xmlfile.remember_order() - - enter = xmlfile.expects('__enter__') - enter.with_arg_count(0) - enter.returns(xmlfile) - - write = xmlfile.expects('write') - write.with_args('foo doc') - seek = xmlfile.expects('seek') - seek.with_args(0) - - exit = xmlfile.expects('__exit__') - exit.with_args(None, None,None) - - xmlfile.has_attr(name='xmlfile') - - named_certfile = fake_tempfile.next_call( - 'NamedTemporaryFile' - ) - named_certfile.with_args(delete=False) - certfile = named_certfile.returns_fake() - certfile.remember_order() - - enter = certfile.expects('__enter__') - enter.with_arg_count(0) - enter.returns(certfile) - - write = certfile.expects('write') - write.with_args( - ('-----BEGIN CERTIFICATE-----\nfoo signature\n' - + '-----END CERTIFICATE-----' - ) - ) - seek = certfile.expects('seek') - seek.with_args(0) - - exit = certfile.expects('__exit__') - exit.with_args(None, None,None) - - certfile.has_attr(name='certfile') - - - fake_subprocess = fudge.Fake('subprocess') - fake_subprocess.remember_order() - popen = fake_subprocess.expects('Popen') - fake_subprocess.has_attr(PIPE=1) - popen.with_args( - [ - 'xmlsec1', - '--verify', - '--pubkey-cert-pem', - 'certfile', - '--id-attr:ID', - 'urn:oasis:names:tc:SAML:2.0:assertion:Assertion', - 'xmlfile', - ], - stderr=1, - stdout=1, - ) - proc = popen.returns_fake() - proc.remember_order() - wait = proc.expects('wait') - wait.with_arg_count(0) - stderr = StringIO('OK') - proc.has_attr(stderr=stderr) - - fake_os = fudge.Fake('os') - fake_os.remember_order() - remove = fake_os.expects('remove') - remove.with_args('certfile') - remove = fake_os.next_call('remove') - remove.with_args('xmlfile') - - SignatureVerifier.verify( - document, - 'foo signature', - _etree=fake_etree, - _tempfile=fake_tempfile, - _subprocess=fake_subprocess, - _os=fake_os, - ) - - @fudge.with_fakes - def test_get_xmlsec_bin_default(self): - fake_platform = fudge.Fake('platform') - fake_platform.remember_order() - system_fn = fake_platform.expects('system') - system_fn.with_arg_count(0) - system_fn.returns('Linux') - - xmlsec_bin = SignatureVerifier._get_xmlsec_bin(_platform=fake_platform) - - eq('xmlsec1', xmlsec_bin) - - @fudge.with_fakes - def test_get_xmlsec_bin_windows(self): - fake_platform = fudge.Fake('platform') - fake_platform.remember_order() - system_fn = fake_platform.expects('system') - system_fn.with_arg_count(0) - system_fn.returns('Windows') - - xmlsec_bin = SignatureVerifier._get_xmlsec_bin(_platform=fake_platform) - - eq('xmlsec.exe', xmlsec_bin) - - @fudge.with_fakes - def test_get_parse_stderr_fail(self): - msg = """func=xmlSecOpenSSLX509StoreVerify:file=x509vfy.c:line=360:obj=x509-store:subj=X509_verify_cert:error=4:crypto library function failed:subj=/C=US/ST=California/L=Santa Monica/O=OneLogin/CN=app.onelogin.com;err=18;msg=self signed certificate -func=xmlSecOpenSSLX509StoreVerify:file=x509vfy.c:line=408:obj=x509-store:subj=unknown:error=71:certificate verification failed:err=18;msg=self signed certificate -func=xmlSecOpenSSLEvpSignatureVerify:file=signatures.c:line=346:obj=rsa-sha1:subj=EVP_VerifyFinal:error=18:data do not match:signature do not match -FAIL -SignedInfo References (ok/all): 1/1 -Manifests References (ok/all): 0/0 -Error: failed to verify file "/tmp/tmpYjEjq5" -""" - fake_proc = fudge.Fake('proc') - stderr = StringIO(msg) - fake_proc.has_attr(stderr=stderr) - - res = SignatureVerifier._parse_stderr(fake_proc) - - eq(res, False) - - @fudge.with_fakes - def test_get_parse_stderr_ok(self): - msg = """func=xmlSecOpenSSLX509StoreVerify:file=x509vfy.c:line=360:obj=x509-store:subj=X509_verify_cert:error=4:crypto library function failed:subj=/C=US/ST=California/L=Santa Monica/O=OneLogin/CN=app.onelogin.com;err=18;msg=self signed certificate -func=xmlSecOpenSSLX509StoreVerify:file=x509vfy.c:line=408:obj=x509-store:subj=unknown:error=71:certificate verification failed:err=18;msg=self signed certificate -func=xmlSecOpenSSLEvpSignatureVerify:file=signatures.c:line=346:obj=rsa-sha1:subj=EVP_VerifyFinal:error=18:data do not match:signature do not match -OK -SignedInfo References (ok/all): 1/1 -Manifests References (ok/all): 0/0 -Error: failed to verify file "/tmp/tmpYjEjq5" -""" - fake_proc = fudge.Fake('proc') - stderr = StringIO(msg) - fake_proc.has_attr(stderr=stderr) - - res = SignatureVerifier._parse_stderr(fake_proc) - - eq(res, True) - - @fudge.with_fakes - def test_get_parse_stderr_error(self): - msg = 'FAILURE' - fake_proc = fudge.Fake('proc') - stderr = StringIO(msg) - fake_proc.has_attr(stderr=stderr) - fake_proc.has_attr(returncode=1) - - msg = assert_raises( - SignatureVerifier.SignatureVerifierError, - SignatureVerifier._parse_stderr, - fake_proc - ) - - eq(str(msg), - ('There was a problem validating the response: XMLSec returned error ' - + 'code 1. Please check your certficate.' - ), - ) - - @fudge.with_fakes - def test_get_parse_stderr_error_should_not_happen(self): - msg = 'FAILURE' - fake_proc = fudge.Fake('proc') - stderr = StringIO(msg) - fake_proc.has_attr(stderr=stderr) - fake_proc.has_attr(returncode=0) - - msg = assert_raises( - SignatureVerifier.SignatureVerifierError, - SignatureVerifier._parse_stderr, - fake_proc - ) - - eq(str(msg), - ('There was a problem validating the response: XMLSec exited with ' - + 'code 0 but did not return OK when verifying the SAML response.' - ) - ) - - @fudge.with_fakes - def test_get_parse_stderr_ok_windows(self): - msg = """func=xmlSecOpenSSLX509StoreVerify:file=x509vfy.c:line=360:obj=x509-store:subj=X509_verify_cert:error=4:crypto library function failed:subj=/C=US/ST=California/L=Santa Monica/O=OneLogin/CN=app.onelogin.com;err=18;msg=self signed certificate\r -func=xmlSecOpenSSLX509StoreVerify:file=x509vfy.c:line=408:obj=x509-store:subj=unknown:error=71:certificate verification failed:err=18;msg=self signed certificate\r -func=xmlSecOpenSSLEvpSignatureVerify:file=signatures.c:line=346:obj=rsa-sha1:subj=EVP_VerifyFinal:error=18:data do not match:signature do not match\r -OK\r -SignedInfo References (ok/all): 1/1\r -Manifests References (ok/all): 0/0\r -Error: failed to verify file "/tmp/tmpYjEjq5"\r -""" - fake_proc = fudge.Fake('proc') - stderr = StringIO(msg) - fake_proc.has_attr(stderr=stderr) - - res = SignatureVerifier._parse_stderr(fake_proc) - - eq(res, True) diff --git a/onelogin/saml/test/util.py b/onelogin/saml/test/util.py deleted file mode 100644 index f693d408..00000000 --- a/onelogin/saml/test/util.py +++ /dev/null @@ -1,12 +0,0 @@ -def assert_raises(excClass, callableObj, *args, **kwargs): - """ - Like unittest.TestCase.assertRaises, but returns the exception. - """ - try: - callableObj(*args, **kwargs) - except excClass, e: - return e - else: - if hasattr(excClass,'__name__'): excName = excClass.__name__ - else: excName = str(excClass) - raise AssertionError("%s not raised" % excName) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..ea397531 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,187 @@ +[tool.poetry] +name = "python-saml" +version = "2.13.0" +description = "Saml Python Toolkit. Add SAML support to your Python software using this library" +license = "Apache-2.0" +authors = ["SAML-Toolkits "] +maintainers = ["Sixto Martin "] +readme = "README.md" +homepage = "https://saml.info" +repository = "https://github.com/SAML-Toolkits/python-saml" +keywords = [ + "saml", + "saml2", + "sso", + "xmlsec", + "federation", + "identity", +] +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules", +] +packages = [ + { include = "onelogin", from = "src" }, + { include = "onelogin/saml2", from = "src" }, +] + +include = [ + { path = "src/onelogin/saml2/schemas"}, + { path = "tests", format = "sdist" } +] + +[tool.poetry.urls] +"Bug Tracker" = "https://github.com/SAML-Toolkits/python-saml/issues" + +[tool.poetry.dependencies] +python = "2.7" +lxml = ">=4.6.5, !=4.7.0" +"dm.xmlsec.binding" = "1.3.7" +isodate = ">=0.6.1" +defusedxml = ">=0.7.1" + +#[tool.poetry.group.dev] +#optional = true + +#[tool.poetry.group.dev.dependencies] +#black = "*" +#isort = {version = "^5.10.1", extras = ["pyproject"]} +flake8 = { version = ">=3.6.0, <=4.0", optional = true} +#Flake8-pyproject = "^1.1.0.post0" +#flake8-bugbear = "^22.8.23" +#flake8-logging-format = "^0.7.5" +#ipdb = "^0.13.9" + +#[tool.poetry.group.test.dependencies] +freezegun= { version = ">=0.3.11, <=0.4", optional = true} +pytest = { version = ">=4.6.11", optional = true} +coverage = { version = ">=4.5.0, <5.0", optional = true} +#pylint = ">=1.9.4" + +[tool.poetry.extras] +test = ["flake8", "freezegun", "pytest", "coverage"] + +#[tool.poetry.group.test] +#optional = true + +#[tool.poetry.group.coverage] +#optional = true + +#[tool.poetry.group.coverage.dependencies] +#coverage = ">=4.5.2" +#pytest-cov = "*" + +#[tool.poetry.group.docs] +#optional = true + +#[tool.poetry.group.docs.dependencies] +#sphinx = "*" + +[build-system] +requires = [ + "poetry>=1.1.15", + "setuptools >= 40.1.0", + "wheel" +] +build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +minversion = "4.6.11" +addopts = "-ra -vvv" +testpaths = [ + "tests", +] +pythonpath = [ + "tests", +] + +[tool.coverage.run] +branch = true +source = ["src/onelogin/saml2"] + +[tool.coverage.report] +ignore_errors = true +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "def __str__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", + "if typing.TYPE_CHECKING:", +] + +[tool.coverage.html] +directory = "cov_html" + +[tool.flake8] +max-line-length = 210 +max-complexity = 22 +count = true +show-source = true +statistics = true +disable-noqa = false +# 'ignore' defaults to: E121,E123,E126,E226,E24,E704,W503,W504 +extend-ignore = [ + 'B904', + 'B006', + 'B950', + 'B017', + 'C901', + 'E501', + 'E731', +] +per-file-ignores = [ + '__init__.py:F401', +] +# 'select' defaults to: E,F,W,C90 +extend-select = [ + # * Default warnings reported by flake8-bugbear (B) - + # https://github.com/PyCQA/flake8-bugbear#list-of-warnings + 'B', + # * The B950 flake8-bugbear opinionated warnings - + # https://github.com/PyCQA/flake8-bugbear#opinionated-warnings + 'B9', +] +extend-exclude = [ + '.github', '.gitlab', + '.Python', '.*.pyc', '.*.pyo', '.*.pyd', '.*.py.class', '*.egg-info', + 'venv*', '.venv*', '.*_cache', + 'lib', 'lib64', '.*.so', + 'build', 'dist', 'sdist', 'wheels', +] + +[tool.black] +line-length = 200 +extend-exclude = ''' +# A regex preceded with ^/ will apply only to files and directories +# in the root of the project. +( + \.pytest_cache +) +''' + +[tool.isort] +profile = 'black' +# The 'black' profile means: +# multi_line_output = 3 +# include_trailing_comma = true +# force_grid_wrap = 0 +# use_parentheses = true +# ensure_newline_before_comments = true +# line_length = 88 +line_length = 200 # override black provile line_length +force_single_line = true # override black profile multi_line_output +star_first = true +group_by_package = true +force_sort_within_sections = true +lines_after_imports = 2 +honor_noqa = true +atomic = true +ignore_comments = true +skip_gitignore = true +src_paths = [ + 'src', + 'tests', +] diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..e66de0fb --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[wheel] +python-tag = py27 diff --git a/setup.py b/setup.py index bc02394d..c63e35e8 100644 --- a/setup.py +++ b/setup.py @@ -1,59 +1,49 @@ -#!/usr/bin/python -from setuptools import setup, find_packages -from setuptools.command import install as _install +#! /usr/bin/env python +# -*- coding: utf-8 -*- -class ExampleCommand(_install.install): - description = "Runs the example application" +# MIT License - _install.install.user_options.append( - ( - 'config-file=', - 'c', - 'The configuration file containing the app and SAML settings', - ), - ) +from setuptools import setup - def initialize_options(self): - self.config_file = None - _install.install.initialize_options(self) - - def finalize_options(self): - if self.config_file is None: - self.config_file = 'example.cfg' - - _install.install.finalize_options(self) - - def run(self): - if self.distribution.install_requires: - self.distribution.fetch_build_eggs( - self.distribution.install_requires, - ) - - import example - example.main(self.config_file) - -install_requires = [ - 'lxml>=2.3', - ] -tests_require = [ - 'fudge >=0.9.5', - 'nose >= 0.10.4', - ] setup( - name='onelogin.saml', - version='0.0.1', - description="Python client library for SAML Version 2.0", - packages = find_packages(), - namespace_packages = ['onelogin'], - install_requires=install_requires, - tests_require=tests_require, - extras_require={'tests': tests_require}, - test_suite='nose.collector', - cmdclass={ - 'example': ExampleCommand - }, - author='Andres Buritica', - author_email='andres@thelinuxkid.com', - url='https://github.com/onelogin/python-saml', - ) + name='python-saml', + version='2.13.0', + description='Saml Python Toolkit. Add SAML support to your Python software using this library', + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2.7', + ], + author='SAML-Toolkits', + author_email='contact@iamdigitalservices.com', + maintainer='Sixto Martin', + maintainer_email='sixto.martin.garcia@gmail.com', + license='MIT', + url='https://github.com/SAML-Toolkits/python-saml', + packages=['onelogin', 'onelogin/saml2'], + include_package_data=True, + package_data={ + 'onelogin/saml2/schemas': ['*.xsd'], + }, + package_dir={ + '': 'src', + }, + test_suite='tests', + install_requires=[ + 'lxml>=4.6.5, !=4.7.0', + 'dm.xmlsec.binding==1.3.7', + 'isodate>=0.6.1', + 'defusedxml>=0.7.1', + ], + extras_require={ + 'test': ( + 'coverage>=4.5, <5.0', + 'freezegun>=0.3.5, <0.4', + 'flake8>=3.6.0, < 4.0', + ), + }, + keywords='saml saml2 xmlsec django flask', +) diff --git a/src/onelogin/__init__.py b/src/onelogin/__init__.py new file mode 100644 index 00000000..3ceff6ba --- /dev/null +++ b/src/onelogin/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +""" +Copyright (c) 2010-2022 OneLogin, Inc. +Copyright (c) 2023 IAM DIgital Services, SL + +MIT License + +Add SAML support to your Python softwares using this library. +Forget those complicated libraries and use that open source +library. + +SAML Python toolkit let you build a SP (Service Provider) +over your Python application and connect it to any IdP (Identity Provider). + +Supports: + +* 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). +""" diff --git a/src/onelogin/saml2/__init__.py b/src/onelogin/saml2/__init__.py new file mode 100644 index 00000000..7ddcbd2e --- /dev/null +++ b/src/onelogin/saml2/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +""" +Copyright (c) 2010-2022 OneLogin, Inc. +Copyright (c) 2023 IAM Digital Services, SL. + +MIT License + +Add SAML support to your Python softwares using this library. +Forget those complicated libraries and use that open source +library. + +SAML Python toolkit let you build a SP (Service Provider) +over your Python application and connect it to any IdP (Identity Provider). + +Supports: + +* 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). +""" diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py new file mode 100644 index 00000000..24d3b2b8 --- /dev/null +++ b/src/onelogin/saml2/auth.py @@ -0,0 +1,597 @@ +# -*- coding: utf-8 -*- + +""" OneLogin_Saml2_Auth class + +MIT License + +Main class of Python Toolkit. + +Initializes the SP SAML instance + +""" + +from base64 import b64encode +from urllib import quote_plus + +from onelogin.saml2.authn_request import OneLogin_Saml2_Authn_Request +from onelogin.saml2.constants import OneLogin_Saml2_Constants +from onelogin.saml2.errors import OneLogin_Saml2_Error +from onelogin.saml2.logout_response import OneLogin_Saml2_Logout_Response +from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request +from onelogin.saml2.response import OneLogin_Saml2_Response +from onelogin.saml2.settings import OneLogin_Saml2_Settings +from onelogin.saml2.utils import OneLogin_Saml2_Utils, xmlsec +from onelogin.saml2.xmlparser import tostring + + +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 + SAML Response, a Logout Request or a Logout Response). + """ + + def __init__(self, request_data, old_settings=None, custom_base_path=None): + """ + Initializes the SP SAML instance. + + :param request_data: Request Data + :type request_data: dict + + :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 + """ + self.__request_data = request_data + self.__settings = OneLogin_Saml2_Settings(old_settings, custom_base_path) + self.__attributes = [] + self.__friendlyname_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): + """ + Returns the settings info + :return: Setting info + :rtype: OneLogin_Saml2_Setting object + """ + return self.__settings + + def set_strict(self, value): + """ + Set the strict mode active/disable + + :param value: + :type value: bool + """ + assert isinstance(value, bool) + self.__settings.set_strict(value) + + def process_response(self, request_id=None): + """ + Process the SAML Response sent by 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.__friendlyname_attributes = response.get_friendlyname_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: + self.__errors.append('invalid_response') + self.__error_reason = response.get_error() + else: + self.__errors.append('invalid_binding') + raise OneLogin_Saml2_Error( + 'SAML Response not found, Only supported HTTP_POST Binding', + OneLogin_Saml2_Error.SAML_RESPONSE_NOT_FOUND + ) + + def process_slo(self, keep_local_session=False, request_id=None, delete_session_cb=None): + """ + Process the SAML Logout Response / Logout Request sent by the IdP. + + :param keep_local_session: When false will destroy the local session, otherwise will destroy it + :type keep_local_session: bool + + :param request_id: The ID of the LogoutRequest sent by this SP to the IdP + :type request_id: string + + :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') + 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() + else: + if not keep_local_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']: + parameters['SigAlg'] = security['signatureAlgorithm'] + parameters['Signature'] = self.build_response_signature(logout_response, parameters.get('RelayState', None), security['signatureAlgorithm']) + + return self.redirect_to(self.get_slo_url(), parameters) + else: + self.__errors.append('invalid_binding') + raise OneLogin_Saml2_Error( + 'SAML LogoutRequest/LogoutResponse not found. Only supported HTTP_REDIRECT Binding', + OneLogin_Saml2_Error.SAML_LOGOUTMESSAGE_NOT_FOUND + ) + + def redirect_to(self, url=None, parameters={}): + """ + 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 + :type parameters: dict + + :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. + + :returns: True if is authenticated, False if not + :rtype: bool + """ + return self.__authenticated + + def get_attributes(self): + """ + Returns the set of SAML attributes. + + :returns: SAML attributes + :rtype: dict + """ + return self.__attributes + + def get_friendlyname_attributes(self): + """ + Returns the set of SAML attributes indexed by FiendlyName. + + :returns: SAML attributes + :rtype: dict + """ + return self.__friendlyname_attributes + + def get_nameid(self): + """ + Returns the nameID. + + :returns: NameID + :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. + :returns: The SessionIndex of the assertion + :rtype: string + """ + return self.__session_index + + def get_session_expiration(self): + """ + Returns the SessionNotOnOrAfter from the AuthnStatement. + :returns: The SessionNotOnOrAfter of the assertion + :rtype: unix/posix timestamp|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 + + :returns: List of errors + :rtype: list + """ + return self.__errors + + def get_last_error_reason(self): + """ + Returns the reason for the last error + + :returns: Reason of the last error + :rtype: None | string + """ + return self.__error_reason + + def get_attribute(self, name): + """ + Returns the requested SAML attribute. + + :param name: Name of the attribute + :type name: string + + :returns: Attribute value(s) if exists or None + :rtype: list + """ + assert isinstance(name, basestring) + value = None + if self.__attributes and name in self.__attributes.keys(): + value = self.__attributes[name] + return value + + def get_friendlyname_attribute(self, friendlyname): + """ + Returns the requested SAML attribute searched by FriendlyName. + + :param friendlyname: FriendlyName of the attribute + :type friendlyname: string + + :returns: Attribute value(s) if exists or None + :rtype: list + """ + assert isinstance(friendlyname, basestring) + value = None + if self.__friendlyname_attributes and friendlyname in self.__friendlyname_attributes.keys(): + value = self.__friendlyname_attributes[friendlyname] + return value + + 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 AuthNRequest will set the ForceAuthn='true'. + :type force_authn: bool + + :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 + + :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} + if return_to is not None: + parameters['RelayState'] = return_to + else: + parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data) + + security = self.__settings.get_security_data() + if security.get('authnRequestsSigned', False): + parameters['SigAlg'] = security['signatureAlgorithm'] + 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, nq=None, name_id_format=None, spnq=None): + """ + Initiates the SLO process. + + :param return_to: Optional argument. The target URL the user should be redirected to after logout. + :type return_to: string + + :param name_id: The NameID that will be set in the LogoutRequest. + :type name_id: string + + :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() + if slo_url is None: + raise OneLogin_Saml2_Error( + 'The IdP does not support Single Log Out', + OneLogin_Saml2_Error.SAML_SINGLE_LOGOUT_NOT_SUPPORTED + ) + + if name_id is None and self.__nameid is not None: + name_id = self.__nameid + 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()} + if return_to is not None: + parameters['RelayState'] = return_to + else: + parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data) + + security = self.__settings.get_security_data() + if security.get('logoutRequestSigned', False): + parameters['SigAlg'] = security['signatureAlgorithm'] + parameters['Signature'] = self.build_request_signature(saml_request, parameters['RelayState'], security['signatureAlgorithm']) + return self.redirect_to(slo_url, parameters) + + def get_sso_url(self): + """ + Gets the IdP SSO URL. + + :returns: An URL, the SSO endpoint of the IdP + :rtype: string + """ + return self.__settings.get_idp_sso_url() + + def get_slo_url(self): + """ + Gets the IdP SLO URL. + + :returns: An URL, the SLO endpoint of the IdP + :rtype: string + """ + return self.__settings.get_idp_slo_url() + + def get_slo_response_url(self): + """ + Gets the SLO return URL for IdP-initiated logout. + + :returns: an URL, the SLO return endpoint of the IdP + :rtype: string + """ + return self.__settings.get_idp_slo_response_url() + + def build_request_signature(self, saml_request, relay_state, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): + """ + Builds the Signature of the SAML Request. + + :param saml_request: The SAML Request + :type saml_request: string + + :param relay_state: The target URL the user should be redirected to + :type relay_state: string + + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string + """ + return self.__build_signature(saml_request, relay_state, 'SAMLRequest', sign_algorithm) + + def build_response_signature(self, saml_response, relay_state, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): + """ + Builds the Signature of the SAML Response. + :param saml_request: The SAML Response + :type saml_request: string + + :param relay_state: The target URL the user should be redirected to + :type relay_state: string + + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string + """ + return self.__build_signature(saml_response, relay_state, 'SAMLResponse', sign_algorithm) + + def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1): + """ + Builds the Signature + :param saml_data: The SAML Data + :type saml_data: string + + :param relay_state: The target URL the user should be redirected to + :type relay_state: string + + :param saml_type: The target URL the user should be redirected to + :type saml_type: string SAMLRequest | SAMLResponse + + :param sign_algorithm: Signature algorithm method + :type sign_algorithm: string + """ + assert saml_type in ['SAMLRequest', 'SAMLResponse'] + + # Load the key into the xmlsec context + key = self.__settings.get_sp_key() + + 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.PRIVATE_KEY_NOT_FOUND + ) + + dsig_ctx = xmlsec.DSigCtx() + dsig_ctx.signKey = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None) + + 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 = { + 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(sign_algorithm, xmlsec.TransformRsaSha1) + + 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 new file mode 100644 index 00000000..5e2c5355 --- /dev/null +++ b/src/onelogin/saml2/authn_request.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- + +""" OneLogin_Saml2_Authn_Request class + +MIT License + +AuthNRequest class of Python Toolkit. + +""" +from base64 import b64encode + +from onelogin.saml2.constants import OneLogin_Saml2_Constants +from onelogin.saml2.utils import OneLogin_Saml2_Utils + + +class OneLogin_Saml2_Authn_Request(object): + """ + + This class handles an AuthNRequest. It builds an + AuthNRequest object. + + """ + + 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 settings: OneLogin_Saml2_Settings + + :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 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 + + sp_data = self.__settings.get_sp_data() + idp_data = self.__settings.get_idp_data() + security = self.__settings.get_security_data() + + uid = OneLogin_Saml2_Utils.generate_unique_id() + self.__id = uid + issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) + + destination = idp_data['singleSignOnService']['url'] + + provider_name_str = '' + organization_data = settings.get_organization() + if isinstance(organization_data, dict) and organization_data: + langs = organization_data.keys() + if 'en-US' in langs: + lang = 'en-US' + else: + lang = langs[0] + if 'displayname' in organization_data[lang] and organization_data[lang]['displayname'] is not None: + provider_name_str = "\n" + ' ProviderName="%s"' % organization_data[lang]['displayname'] + + force_authn_str = '' + if force_authn is True: + force_authn_str = "\n" + ' ForceAuthn="true"' + + is_passive_str = '' + if is_passive is 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 = security['requestedAuthnContextComparison'] + + if security['requestedAuthnContext'] is True: + requested_authn_context_str = "\n" + """ + urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + """ % authn_comparison + else: + 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%(subject_str)s%(nameid_policy_str)s%(requested_authn_context_str)s +""" % \ + { + 'id': uid, + 'provider_name': provider_name_str, + 'force_authn_str': force_authn_str, + 'is_passive_str': is_passive_str, + 'issue_instant': issue_instant, + 'destination': destination, + 'assertion_url': sp_data['assertionConsumerService']['url'], + 'entity_id': sp_data['entityId'], + '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, deflate=True): + """ + Returns unsigned AuthnRequest. + :param deflate: It makes the deflate process optional + :type: bool + :return: AuthnRequest maybe deflated and base64 encoded + :rtype: str object + """ + 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): + """ + Returns the AuthNRequest ID. + :return: AuthNRequest ID + :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 new file mode 100644 index 00000000..0e4844d0 --- /dev/null +++ b/src/onelogin/saml2/constants.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- + +""" OneLogin_Saml2_Constants class + +MIT License + +Constants class of Python Toolkit. + +""" + + +class OneLogin_Saml2_Constants(object): + """ + + This class defines all the constants that will be used + in the Python Toolkit. + + """ + + # Value added to the current time in time condition validations + 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' + NAMEID_X509_SUBJECT_NAME = 'urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName' + NAMEID_WINDOWS_DOMAIN_QUALIFIED_NAME = 'urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName' + NAMEID_UNSPECIFIED = 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified' + NAMEID_KERBEROS = 'urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos' + NAMEID_ENTITY = 'urn:oasis:names:tc:SAML:2.0:nameid-format:entity' + NAMEID_TRANSIENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + NAMEID_PERSISTENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent' + NAMEID_ENCRYPTED = 'urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted' + + # Attribute Name Formats + ATTRNAME_FORMAT_UNSPECIFIED = 'urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified' + ATTRNAME_FORMAT_URI = 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri' + ATTRNAME_FORMAT_BASIC = 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic' + + # Namespaces + NS_SAML = 'urn:oasis:names:tc:SAML:2.0:assertion' + NS_SAMLP = 'urn:oasis:names:tc:SAML:2.0:protocol' + NS_SOAP = 'http://schemas.xmlsoap.org/soap/envelope/' + NS_MD = 'urn:oasis:names:tc:SAML:2.0:metadata' + NS_XS = 'http://www.w3.org/2001/XMLSchema' + NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance' + 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' + BINDING_HTTP_ARTIFACT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact' + BINDING_SOAP = 'urn:oasis:names:tc:SAML:2.0:bindings:SOAP' + BINDING_DEFLATE = 'urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE' + + # Auth Context Class + AC_UNSPECIFIED = 'urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified' + AC_PASSWORD = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password' + AC_PASSWORD_PROTECTED = 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' + AC_X509 = 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509' + AC_SMARTCARD = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Smartcard' + AC_KERBEROS = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Kerberos' + + # Subject Confirmation + CM_BEARER = 'urn:oasis:names:tc:SAML:2.0:cm:bearer' + CM_HOLDER_KEY = 'urn:oasis:names:tc:SAML:2.0:cm:holder-of-key' + CM_SENDER_VOUCHES = 'urn:oasis:names:tc:SAML:2.0:cm:sender-vouches' + + # Status Codes + STATUS_SUCCESS = 'urn:oasis:names:tc:SAML:2.0:status:Success' + STATUS_REQUESTER = 'urn:oasis:names:tc:SAML:2.0:status:Requester' + STATUS_RESPONDER = 'urn:oasis:names:tc:SAML:2.0:status:Responder' + STATUS_VERSION_MISMATCH = 'urn:oasis:names:tc:SAML:2.0:status:VersionMismatch' + STATUS_NO_PASSIVE = 'urn:oasis:names:tc:SAML:2.0:status:NoPassive' + 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' + + # 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/xmldsig-more#sha384' + SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512' + + 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' + RSA_SHA512 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512' + + # Enc + TRIPLEDES_CBC = 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' + AES128_CBC = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' + AES192_CBC = 'http://www.w3.org/2001/04/xmlenc#aes192-cbc' + AES256_CBC = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' + RSA_1_5 = 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' + RSA_OAEP_MGF1P = 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' + + # Define here the deprecated algorithms + DEPRECATED_ALGORITHMS = [DSA_SHA1, RSA_SHA1, SHA1] diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py new file mode 100644 index 00000000..a4ffd7e2 --- /dev/null +++ b/src/onelogin/saml2/errors.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- + +""" OneLogin_Saml2_Error class + +MIT License + +Error class of Python Toolkit. + +Defines common Error codes and has a custom initializator. + +""" + + +class OneLogin_Saml2_Error(Exception): + """ + + This class implements a custom Exception handler. + Defines custom error codes. + + """ + + # Errors + SETTINGS_FILE_NOT_FOUND = 0 + 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 + SAML_RESPONSE_NOT_FOUND = 8 + SAML_LOGOUTMESSAGE_NOT_FOUND = 9 + 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 + DEPRECATED_SIGNATURE_METHOD = 46 + DEPRECATED_DIGEST_METHOD = 47 + + 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 diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py new file mode 100644 index 00000000..8981211f --- /dev/null +++ b/src/onelogin/saml2/idp_metadata_parser.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- + +""" OneLogin_Saml2_IdPMetadataParser class + +MIT License + +Metadata class of Python Toolkit. + +""" + +import urllib2 +import ssl + +from copy import deepcopy + +from onelogin.saml2.constants import OneLogin_Saml2_Constants +from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.xmlparser import fromstring + + +class OneLogin_Saml2_IdPMetadataParser(object): + """ + A class that contain methods related to obtaining and parsing metadata from IdP + + This class does not validate in any way the URL that is introduced, + make sure to validate it properly before use it in a get_metadata method. + """ + + @staticmethod + def get_metadata(url, validate_cert=True, timeout=None, headers=None): + """ + 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 + + :param timeout: Timeout in seconds to wait for metadata response + :type timeout: int + + :param headers: Extra headers to send in the request + :type headers: dict + + :returns: metadata XML + :rtype: string + """ + valid = False + + request = urllib2.Request(url, headers=headers or {}) + + if validate_cert: + response = urllib2.urlopen(request, timeout=timeout) + else: + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + response = urllib2.urlopen(request, context=ctx, timeout=timeout) + 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, timeout=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 + + :param timeout: Timeout in seconds to wait for metadata response + :type timeout: int + + :returns: settings dict with extracted data + :rtype: dict + """ + idp_metadata = OneLogin_Saml2_IdPMetadataParser.get_metadata(url, validate_cert, timeout, headers=kwargs.pop('headers', None)) + 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: dict + + :param new_metadata_settings: Settings to be merged (extracted from IdP metadata after parsing) + :type new_metadata_settings: dict + + :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(lhs, rhs): + """Deep-merge dictionary `rhs` into dictionary `lhs`.""" + updated_rhs = {} + for key in rhs: + if key in lhs and isinstance(lhs[key], dict) and isinstance(rhs[key], dict): + updated_rhs[key] = dict_deep_merge(lhs[key], rhs[key]) + else: + updated_rhs[key] = rhs[key] + lhs.update(updated_rhs) + return lhs diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py new file mode 100644 index 00000000..9426d5d9 --- /dev/null +++ b/src/onelogin/saml2/logout_request.py @@ -0,0 +1,450 @@ +# -*- coding: utf-8 -*- + +""" OneLogin_Saml2_Logout_Request class + +MIT License + +Logout Request class of Python Toolkit. + +""" + +from zlib import decompress +from base64 import b64encode, b64decode +from lxml import etree +from xml.dom.minidom import Document + +from onelogin.saml2.constants import OneLogin_Saml2_Constants +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError +from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.xmlparser import fromstring + + +class OneLogin_Saml2_Logout_Request(object): + """ + + This class handles a Logout Request. + + Builds a Logout Response object and validates it. + + """ + + 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. + + :param settings: Setting data + :type request_data: OneLogin_Saml2_Settings + + :param request: Optional. A LogoutRequest to be loaded instead build one. + :type request: string + + :param name_id: The NameID that will be set in the LogoutRequest. + :type name_id: string + + :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 + self.id = None + + if request is None: + sp_data = self.__settings.get_sp_data() + idp_data = self.__settings.get_idp_data() + security = self.__settings.get_security_data() + + uid = OneLogin_Saml2_Utils.generate_unique_id() + self.id = uid + + issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) + + cert = None + if 'nameIdEncrypted' in security and security['nameIdEncrypted']: + 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: + 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'] + 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, + spnq, + name_id_format, + cert, + False, + nq + ) + + if session_index: + session_index_str = '%s' % session_index + else: + session_index_str = '' + + logout_request = """ + %(entity_id)s + %(name_id)s + %(session_index)s + """ % \ + { + 'id': uid, + 'issue_instant': issue_instant, + 'single_logout_url': self.__settings.get_idp_slo_url(), + 'entity_id': sp_data['entityId'], + 'name_id': name_id_obj, + 'session_index': session_index_str, + } + else: + decoded = b64decode(request) + # We try to inflate + try: + inflated = decompress(decoded, -15) + logout_request = inflated + except Exception: + logout_request = decoded + self.id = self.get_id(logout_request) + + self.__logout_request = logout_request + + def get_request(self, deflate=True): + """ + 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 + """ + 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): + """ + Returns the ID of the Logout Request + :param request: Logout Request Message + :type request: string|DOMDocument + :return: string ID + :rtype: str object + """ + if isinstance(request, etree._Element): + elem = request + else: + if isinstance(request, Document): + request = request.toxml() + elem = fromstring(request, forbid_dtd=True) + return elem.get('ID', None) + + @staticmethod + def get_nameid_data(request, key=None): + """ + Gets the NameID Data of the the Logout Request + :param request: Logout Request Message + :type request: string|DOMDocument + :param key: The SP key + :type key: string + :return: Name ID Data (Value, Format, NameQualifier, SPNameQualifier) + :rtype: dict + """ + if isinstance(request, etree._Element): + elem = request + else: + if isinstance(request, Document): + request = request.toxml() + 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 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: + encrypted_data = encrypted_data_nodes[0] + name_id = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) + else: + entries = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:NameID') + if len(entries) == 1: + name_id = entries[0] + + if name_id is None: + raise OneLogin_Saml2_ValidationError( + 'NameID not found in the Logout Request', + OneLogin_Saml2_ValidationError.NO_NAMEID + ) + + name_id_data = { + 'Value': OneLogin_Saml2_Utils.element_text(name_id) + } + for attr in ['Format', 'SPNameQualifier', 'NameQualifier']: + if attr in name_id.attrib.keys(): + name_id_data[attr] = name_id.attrib[attr] + + return name_id_data + + @staticmethod + def get_nameid(request, key=None): + """ + Gets the NameID 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 = 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): + """ + Gets the Issuer of the Logout Request Message + :param request: Logout Request Message + :type request: string|DOMDocument + :return: The Issuer + :rtype: string + """ + if isinstance(request, etree._Element): + elem = request + else: + if isinstance(request, Document): + request = request.toxml() + 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 = OneLogin_Saml2_Utils.element_text(issuer_nodes[0]) + return issuer + + @staticmethod + def get_session_indexes(request): + """ + Gets the SessionIndexes from the Logout Request + :param request: Logout Request Message + :type request: string|DOMDocument + :return: The SessionIndex value + :rtype: list + """ + if isinstance(request, etree._Element): + elem = request + else: + if isinstance(request, Document): + request = request.toxml() + 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(OneLogin_Saml2_Utils.element_text(session_index_node)) + return session_indexes + + def is_valid(self, request_data, raise_exceptions=False): + """ + 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, forbid_dtd=True) + + idp_data = self.__settings.get_idp_data() + idp_entity_id = idp_data['entityId'] + + if 'get_data' in request_data.keys(): + get_data = request_data['get_data'] + else: + get_data = {} + + if 'lowercase_urlencoding' in request_data.keys(): + lowercase_urlencoding = request_data['lowercase_urlencoding'] + + security = self.__settings.get_security_data() + 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 OneLogin_Saml2_ValidationError( + 'Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd', + OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT + ) + + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + + # Check NotOnOrAfter + if dom.get('NotOnOrAfter', None): + na = OneLogin_Saml2_Utils.parse_SAML_to_time(dom.get('NotOnOrAfter')) + if na <= OneLogin_Saml2_Utils.now(): + raise OneLogin_Saml2_ValidationError( + 'Could not validate timestamp: expired. Check system clock.', + OneLogin_Saml2_ValidationError.RESPONSE_EXPIRED + ) + + # Check destination + destination = dom.get('Destination') + if destination: + if not OneLogin_Saml2_Utils.normalize_url(url=destination).startswith(OneLogin_Saml2_Utils.normalize_url(url=current_url)): + raise Exception( + 'The LogoutRequest was received at ' + '%(currentURL)s instead of %(destination)s' % + { + '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 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 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: + sign_alg = OneLogin_Saml2_Constants.RSA_SHA1 + else: + sign_alg = get_data['SigAlg'] + + reject_deprecated_alg = security.get('rejectDeprecatedAlgorithm', False) + if reject_deprecated_alg: + if sign_alg in OneLogin_Saml2_Constants.DEPRECATED_ALGORITHMS: + raise OneLogin_Saml2_ValidationError( + 'Deprecated signature algorithm found: %s' % sign_alg, + OneLogin_Saml2_ValidationError.DEPRECATED_SIGNATURE_METHOD + ) + + 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, 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', sign_alg, 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__()) + if raise_exceptions: + raise err + return False + + def get_error(self): + """ + 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 new file mode 100644 index 00000000..66191caf --- /dev/null +++ b/src/onelogin/saml2/logout_response.py @@ -0,0 +1,278 @@ +# -*- coding: utf-8 -*- + +""" OneLogin_Saml2_Logout_Response class + +MIT License + +Logout Response class of Python Toolkit. + +""" + +from base64 import b64encode, b64decode +from xml.dom.minidom import Document +from defusedxml.minidom import parseString + +from onelogin.saml2.constants import OneLogin_Saml2_Constants +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError +from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.xmlparser import fromstring + + +class OneLogin_Saml2_Logout_Response(object): + """ + + This class handles a Logout Response. It Builds or parses a Logout Response object + and validates it. + + """ + + def __init__(self, settings, response=None): + """ + Constructs a Logout Response object (Initialize params from settings + and if provided load the Logout Response. + + Arguments are: + * (OneLogin_Saml2_Settings) settings. Setting data + * (string) response. An UUEncoded SAML Logout + response from the IdP. + """ + 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, forbid_dtd=True) + self.id = self.document.documentElement.getAttribute('ID') + + def get_issuer(self): + """ + Gets the Issuer of the Logout Response Message + :return: The Issuer + :rtype: string + """ + issuer = None + issuer_nodes = self.__query('/samlp:LogoutResponse/saml:Issuer') + if len(issuer_nodes) == 1: + issuer = OneLogin_Saml2_Utils.element_text(issuer_nodes[0]) + return issuer + + def get_status(self): + """ + Gets the Status + :return: The Status + :rtype: string + """ + entries = self.__query('/samlp:LogoutResponse/samlp:Status/samlp:StatusCode') + if len(entries) == 0: + return None + status = entries[0].attrib['Value'] + return status + + 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'] + + security = self.__settings.get_security_data() + 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 OneLogin_Saml2_ValidationError( + 'Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd', + OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT + ) + + 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 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) + + # Check destination + if self.document.documentElement.hasAttribute('Destination'): + destination = self.document.documentElement.getAttribute('Destination') + if destination: + if not OneLogin_Saml2_Utils.normalize_url(url=destination).startswith(OneLogin_Saml2_Utils.normalize_url(url=current_url)): + 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 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: + sign_alg = OneLogin_Saml2_Constants.RSA_SHA1 + else: + sign_alg = get_data['SigAlg'] + + reject_deprecated_alg = security.get('rejectDeprecatedAlgorithm', False) + if reject_deprecated_alg: + if sign_alg in OneLogin_Saml2_Constants.DEPRECATED_ALGORITHMS: + raise OneLogin_Saml2_ValidationError( + 'Deprecated signature algorithm found: %s' % sign_alg, + OneLogin_Saml2_ValidationError.DEPRECATED_SIGNATURE_METHOD + ) + + 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, 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', sign_alg, 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 OneLogin_Saml2_ValidationError( + 'Signature validation failed. Logout Response rejected', + OneLogin_Saml2_ValidationError.INVALID_SIGNATURE + ) + + return True + # pylint: disable=R0801 + except Exception as err: + self.__error = err.__str__() + debug = self.__settings.is_debug_active() + if debug: + print(err.__str__()) + if raise_exceptions: + raise err + return False + + def __query(self, query): + """ + Extracts a node from the DOMDocument (Logout Response Menssage) + :param query: Xpath Expresion + :type query: string + :return: The queried node + :rtype: DOMNodeList + """ + # Switch to lxml for querying + xml = self.document.toxml() + return OneLogin_Saml2_Utils.query(fromstring(xml, forbid_dtd=True), query) + + def build(self, in_response_to): + """ + Creates a Logout Response object. + :param in_response_to: InResponseTo value for the Logout Response. + :type in_response_to: string + """ + sp_data = self.__settings.get_sp_data() + + uid = OneLogin_Saml2_Utils.generate_unique_id() + issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now()) + + logout_response = """ + %(entity_id)s + + + +""" % \ + { + 'id': uid, + 'issue_instant': issue_instant, + 'destination': self.__settings.get_idp_slo_response_url(), + 'in_response_to': in_response_to, + 'entity_id': sp_data['entityId'], + } + + self.__logout_response = logout_response + + 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 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 self.__logout_response + + def get_error(self): + """ + 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 new file mode 100644 index 00000000..ae7c5ffb --- /dev/null +++ b/src/onelogin/saml2/metadata.py @@ -0,0 +1,284 @@ +# -*- coding: utf-8 -*- + +""" OneLogin_Saml2_Metadata class + +MIT License + +Metadata class of Python Toolkit. + +""" + +from time import gmtime, strftime, time +from datetime import datetime +from defusedxml.minidom import parseString + +from onelogin.saml2.constants import OneLogin_Saml2_Constants +from onelogin.saml2.utils import OneLogin_Saml2_Utils + + +class OneLogin_Saml2_Metadata(object): + """ + + A class that contains methods related to the metadata of the SP + + """ + + TIME_VALID = 172800 # 2 days + TIME_CACHED = 604800 # 1 week + + @staticmethod + def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=None, contacts=None, organization=None): + """ + Builds the metadata of the SP + + :param sp: The SP data + :type sp: string + + :param authnsign: authnRequestsSigned attribute + :type authnsign: string + + :param wsign: wantAssertionsSigned attribute + :type wsign: string + + :param valid_until: Metadata's expiry date + :type valid_until: string|DateTime|Timestamp + + :param cache_duration: Duration of the cache in seconds + :type cache_duration: int|string + + :param contacts: Contacts info + :type contacts: dict + + :param organization: Organization info + :type organization: dict + """ + if valid_until is None: + 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() + else: + valid_until_time = gmtime(valid_until) + valid_until_str = strftime(r'%Y-%m-%dT%H:%M:%SZ', valid_until_time) + else: + valid_until_str = valid_until + + if cache_duration is None: + cache_duration = OneLogin_Saml2_Metadata.TIME_CACHED + if not isinstance(cache_duration, basestring): + cache_duration_str = 'PT%sS' % cache_duration # 'P'eriod of 'T'ime x 'S'econds + else: + cache_duration_str = cache_duration + + if contacts is None: + contacts = {} + 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 = """ \n""" % \ + { + 'binding': sp['singleLogoutService']['binding'], + 'location': sp['singleLogoutService']['url'], + } + + str_authnsign = 'true' if authnsign else 'false' + str_wsign = 'true' if wsign else 'false' + + str_organization = '' + if len(organization) > 0: + organization_names = [] + organization_displaynames = [] + organization_urls = [] + for (lang, info) in organization.items(): + 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: + contacts_info = [] + for (ctype, info) in contacts.items(): + contact = """ + %(name)s + %(email)s + """ % \ + { + 'type': ctype, + 'name': info['givenName'], + 'email': info['emailAddress'], + } + contacts_info.append(contact) + str_contacts = '\n'.join(contacts_info) + '\n' + + metadata = u""" + + +%(sls)s %(name_id_format)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 '', + 'entity_id': sp['entityId'], + 'authnsign': str_authnsign, + 'wsign': str_wsign, + 'name_id_format': sp['NameIDFormat'], + 'binding': sp['assertionConsumerService']['binding'], + 'location': sp['assertionConsumerService']['url'], + '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_SHA256, digest_algorithm=OneLogin_Saml2_Constants.SHA256): + """ + Signs the metadata with the key/cert provided + + :param metadata: SAML Metadata XML + :type metadata: string + + :param key: x509 key + :type key: string + + :param cert: x509 cert + :type cert: string + + :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, digest_algorithm) + + @staticmethod + def add_x509_key_descriptors(metadata, cert=None, add_encryption=True): + """ + Adds the x509 descriptors (sign/encryption) to the metadata + The same cert will be used for sign/encrypt + + :param metadata: SAML Metadata XML + :type metadata: string + + :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.encode('utf-8'), forbid_dtd=True, forbid_entities=True, forbid_external=True) + except Exception as e: + raise Exception('Error parsing metadata. ' + e.message) + + formated_cert = OneLogin_Saml2_Utils.format_cert(cert, False) + x509_certificate = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'ds:X509Certificate') + content = xml.createTextNode(formated_cert) + x509_certificate.appendChild(content) + + key_data = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'ds:X509Data') + key_data.appendChild(x509_certificate) + + key_info = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'ds:KeyInfo') + key_info.appendChild(key_data) + + key_descriptor = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'md:KeyDescriptor') + + entity_descriptor = xml.getElementsByTagName('md:EntityDescriptor')[0] + + sp_sso_descriptor = entity_descriptor.getElementsByTagName('md:SPSSODescriptor')[0] + 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') + signing.appendChild(key_info) + signing.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 new file mode 100644 index 00000000..0da8c06c --- /dev/null +++ b/src/onelogin/saml2/response.py @@ -0,0 +1,988 @@ +# -*- coding: utf-8 -*- + +""" OneLogin_Saml2_Response class + +MIT License + +SAML Response class of Python Toolkit. + +""" + +from base64 import b64decode +from copy import deepcopy +from xml.dom.minidom import Document + +from onelogin.saml2.constants import OneLogin_Saml2_Constants +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError +from onelogin.saml2.utils import OneLogin_Saml2_Utils, return_false_on_exception +from onelogin.saml2.xmlparser import tostring, fromstring + + +class OneLogin_Saml2_Response(object): + """ + + This class handles a SAML Response. It parses or validates + a Logout Response object. + + """ + + def __init__(self, settings, response): + """ + Constructs the response object. + + :param settings: The setting info + :type settings: OneLogin_Saml2_Setting object + + :param response: The base64 encoded, XML string containing the samlp:Response + :type response: string + """ + self.__settings = settings + self.__error = None + self.response = b64decode(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') + if encrypted_assertion_nodes: + decrypted_document = deepcopy(self.document) + self.encrypted = True + self.decrypted_document = self.__decrypt_assertion(decrypted_document) + + def is_valid(self, request_data, request_id=None, raise_exceptions=False): + """ + Validates the response object. + + :param request_data: Request Data + :type request_data: dict + + :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 + """ + self.__error = None + try: + # Checks SAML version + if self.document.get('Version', None) != '2.0': + 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 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', '') + + signed_elements = self.process_signed_elements() + + 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 + + security = self.__settings.get_security_data() + if self.__settings.is_strict(): + 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 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 + ) + + 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 + 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 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) != 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(authn_contexts).difference(requested_authn_contexts) + if unmatched_contexts: + raise OneLogin_Saml2_ValidationError( + 'The AuthnContext "%s" was not a requested context "%s"' % (', '.join(unmatched_contexts), ', '.join(requested_authn_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 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 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', None) + if destination: + if not OneLogin_Saml2_Utils.normalize_url(url=destination).startswith(OneLogin_Saml2_Utils.normalize_url(url=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 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 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 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 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 + subject_confirmation_nodes = self.__query_assertion('/saml:Subject/saml:SubjectConfirmation') + + for scn in subject_confirmation_nodes: + method = scn.get('Method', None) + if method and method != OneLogin_Saml2_Constants.CM_BEARER: + continue + sc_data = scn.find('saml:SubjectConfirmationData', namespaces=OneLogin_Saml2_Constants.NSMAP) + if sc_data is None: + continue + else: + irt = sc_data.get('InResponseTo', None) + 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: + continue + nooa = sc_data.get('NotOnOrAfter', None) + if nooa: + parsed_nooa = OneLogin_Saml2_Utils.parse_SAML_to_time(nooa) + if parsed_nooa <= OneLogin_Saml2_Utils.now(): + continue + nb = sc_data.get('NotBefore', None) + if nb: + 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 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) + + 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__()) + if raise_exceptions: + raise err + return False + + def check_status(self): + """ + Check if the status of the response is success or not + + :raises: Exception. If the status is not success + """ + status = OneLogin_Saml2_Utils.get_status(self.document) + code = status.get('code', None) + if code and code != OneLogin_Saml2_Constants.STATUS_SUCCESS: + splited_code = code.split(':') + printable_code = splited_code.pop() + status_exception_msg = 'The status code of the Response was not Success, was %s' % printable_code + status_msg = status.get('msg', None) + if status_msg: + status_exception_msg += ' -> ' + status_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): + """ + Gets the audiences + + :returns: The valid audiences for the SAML Response + :rtype: list + """ + audience_nodes = self.__query_assertion('/saml:Conditions/saml:AudienceRestriction/saml:Audience') + 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): + """ + Gets the issuers (from message and from assertion) + + :returns: The issuers + :rtype: list + """ + issuers = [] + + 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 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)) + + def get_nameid_data(self): + """ + Gets the NameID Data provided by the SAML Response from the IdP + + :returns: Name ID Data (Value, Format, NameQualifier, SPNameQualifier) + :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] + key = self.__settings.get_sp_key() + nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) + else: + nameid_nodes = self.__query_assertion('/saml:Subject/saml:NameID') + if nameid_nodes: + nameid = nameid_nodes[0] + + 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): + """ + Gets the NameID provided by the SAML Response from the IdP + + :returns: NameID (value) + :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() + 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): + """ + Gets the SessionNotOnOrAfter from the AuthnStatement + Could be used to set the local session expiration + + :returns: The SessionNotOnOrAfter value + :rtype: time|None + """ + not_on_or_after = None + authn_statement_nodes = self.__query_assertion('/saml:AuthnStatement[@SessionNotOnOrAfter]') + if authn_statement_nodes: + 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 + Could be used to be stored in the local session in order + to be used in a future Logout Request that the SP could + send to the SP, to set what specific session must be deleted + + :returns: The SessionIndex value + :rtype: string|None + """ + session_index = None + authn_statement_nodes = self.__query_assertion('/saml:AuthnStatement[@SessionIndex]') + if authn_statement_nodes: + session_index = authn_statement_nodes[0].get('SessionIndex') + return session_index + + def get_attributes(self): + """ + Gets the Attributes from the AttributeStatement element. + EncryptedAttributes are not supported + """ + attributes = {} + 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[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 + + def get_friendlyname_attributes(self): + """ + Gets the Attributes from the AttributeStatement element indexed by FiendlyName. + EncryptedAttributes are not supported + """ + attributes = {} + attribute_nodes = self.__query_assertion('/saml:AttributeStatement/saml:Attribute') + for attribute_node in attribute_nodes: + attr_friendlyname = attribute_node.get('FriendlyName') + if attr_friendlyname: + if attr_friendlyname in attributes.keys(): + raise OneLogin_Saml2_ValidationError( + 'Found an Attribute element with duplicated FriendlyName', + OneLogin_Saml2_ValidationError.DUPLICATED_ATTRIBUTE_NAME_FOUND + ) + + values = [] + 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_friendlyname] = values + return attributes + + def validate_num_assertions(self): + """ + Verifies that the document only contains a single Assertion (encrypted or not) + + :returns: True if only 1 assertion encrypted or not + :rtype: bool + """ + 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') + + security = self.__settings.get_security_data() + reject_deprecated_alg = security.get('rejectDeprecatedAlgorithm', False) + + 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) + + # Check the signature and digest algorithm + if reject_deprecated_alg: + sig_method_node = OneLogin_Saml2_Utils.query(sign_node, './/ds:SignatureMethod') + if sig_method_node: + sig_method = sig_method_node[0].get("Algorithm") + if sig_method in OneLogin_Saml2_Constants.DEPRECATED_ALGORITHMS: + raise OneLogin_Saml2_ValidationError( + 'Deprecated signature algorithm found: %s' % sig_method, + OneLogin_Saml2_ValidationError.DEPRECATED_SIGNATURE_METHOD + ) + + dig_method_node = OneLogin_Saml2_Utils.query(sign_node, './/ds:DigestMethod') + if dig_method_node: + dig_method = dig_method_node[0].get("Algorithm") + if dig_method in OneLogin_Saml2_Constants.DEPRECATED_ALGORITHMS: + raise OneLogin_Saml2_ValidationError( + 'Deprecated digest algorithm found: %s' % dig_method, + OneLogin_Saml2_ValidationError.DEPRECATED_DIGEST_METHOD + ) + + 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 + """ + conditions_nodes = self.__query_assertion('/saml:Conditions') + + 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.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): + """ + Extracts nodes that match the query from the Assertion + + :param query: Xpath Expresion + :type query: String + + :returns: The queried nodes + :rtype: list + """ + 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 + signed_message_query = '/samlp:Response' + signature_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=$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=$tagid]" + tagid = assertion_id[1:] + final_query += xpath_expr + return self.__query(final_query, tagid) + + 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 + """ + if self.encrypted: + document = self.decrypted_document + else: + document = self.document + 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 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, '/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 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 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 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=$tagid]', None, uri) + if encrypted_key: + keyinfo.append(encrypted_key[0]) + + encrypted_data = encrypted_data_nodes[0] + 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 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/saml-schema-assertion-2.0.xsd b/src/onelogin/saml2/schemas/saml-schema-assertion-2.0.xsd new file mode 100644 index 00000000..2b2f7b80 --- /dev/null +++ b/src/onelogin/saml2/schemas/saml-schema-assertion-2.0.xsd @@ -0,0 +1,283 @@ + + + + + + + Document identifier: saml-schema-assertion-2.0 + Location: http://docs.oasis-open.org/security/saml/v2.0/ + Revision history: + V1.0 (November, 2002): + Initial Standard Schema. + V1.1 (September, 2003): + Updates within the same V1.0 namespace. + V2.0 (March, 2005): + New assertion schema for SAML V2.0 namespace. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/onelogin/saml2/schemas/saml-schema-authn-context-2.0.xsd b/src/onelogin/saml2/schemas/saml-schema-authn-context-2.0.xsd new file mode 100644 index 00000000..e4754faf --- /dev/null +++ b/src/onelogin/saml2/schemas/saml-schema-authn-context-2.0.xsd @@ -0,0 +1,23 @@ + + + + + + Document identifier: saml-schema-authn-context-2.0 + Location: http://docs.oasis-open.org/security/saml/v2.0/ + Revision history: + V2.0 (March, 2005): + New core authentication context schema for SAML V2.0. + This is just an include of all types from the schema + referred to in the include statement below. + + + + + + \ No newline at end of file diff --git a/src/onelogin/saml2/schemas/saml-schema-authn-context-types-2.0.xsd b/src/onelogin/saml2/schemas/saml-schema-authn-context-types-2.0.xsd new file mode 100644 index 00000000..8513959a --- /dev/null +++ b/src/onelogin/saml2/schemas/saml-schema-authn-context-types-2.0.xsd @@ -0,0 +1,821 @@ + + + + + + Document identifier: saml-schema-authn-context-types-2.0 + Location: http://docs.oasis-open.org/security/saml/v2.0/ + Revision history: + V2.0 (March, 2005): + New core authentication context schema types for SAML V2.0. + + + + + + + A particular assertion on an identity + provider's part with respect to the authentication + context associated with an authentication assertion. + + + + + + + + Refers to those characteristics that describe the + processes and mechanisms + the Authentication Authority uses to initially create + an association between a Principal + and the identity (or name) by which the Principal will + be known + + + + + + + + This element indicates that identification has been + performed in a physical + face-to-face meeting with the principal and not in an + online manner. + + + + + + + + + + + + + + + + + + + + Refers to those characterstics that describe how the + 'secret' (the knowledge or possession + of which allows the Principal to authenticate to the + Authentication Authority) is kept secure + + + + + + + + This element indicates the types and strengths of + facilities + of a UA used to protect a shared secret key from + unauthorized access and/or use. + + + + + + + + This element indicates the types and strengths of + facilities + of a UA used to protect a private key from + unauthorized access and/or use. + + + + + + + The actions that must be performed + before the private key can be used. + + + + + + Whether or not the private key is shared + with the certificate authority. + + + + + + + In which medium is the key stored. + memory - the key is stored in memory. + smartcard - the key is stored in a smartcard. + token - the key is stored in a hardware token. + MobileDevice - the key is stored in a mobile device. + MobileAuthCard - the key is stored in a mobile + authentication card. + + + + + + + + + + + This element indicates that a password (or passphrase) + has been used to + authenticate the Principal to a remote system. + + + + + + + + This element indicates that a Pin (Personal + Identification Number) has been used to authenticate the Principal to + some local system in order to activate a key. + + + + + + + + This element indicates that a hardware or software + token is used + as a method of identifying the Principal. + + + + + + + + This element indicates that a time synchronization + token is used to identify the Principal. hardware - + the time synchonization + token has been implemented in hardware. software - the + time synchronization + token has been implemented in software. SeedLength - + the length, in bits, of the + random seed used in the time synchronization token. + + + + + + + + This element indicates that a smartcard is used to + identity the Principal. + + + + + + + + This element indicates the minimum and/or maximum + ASCII length of the password which is enforced (by the UA or the + IdP). In other words, this is the minimum and/or maximum number of + ASCII characters required to represent a valid password. + min - the minimum number of ASCII characters required + in a valid password, as enforced by the UA or the IdP. + max - the maximum number of ASCII characters required + in a valid password, as enforced by the UA or the IdP. + + + + + + + + This element indicates the length of time for which an + PIN-based authentication is valid. + + + + + + + + Indicates whether the password was chosen by the + Principal or auto-supplied by the Authentication Authority. + principalchosen - the Principal is allowed to choose + the value of the password. This is true even if + the initial password is chosen at random by the UA or + the IdP and the Principal is then free to change + the password. + automatic - the password is chosen by the UA or the + IdP to be cryptographically strong in some sense, + or to satisfy certain password rules, and that the + Principal is not free to change it or to choose a new password. + + + + + + + + + + + + + + + + + + + Refers to those characteristics that define the + mechanisms by which the Principal authenticates to the Authentication + Authority. + + + + + + + + The method that a Principal employs to perform + authentication to local system components. + + + + + + + + The method applied to validate a principal's + authentication across a network + + + + + + + + Supports Authenticators with nested combinations of + additional complexity. + + + + + + + + Indicates that the Principal has been strongly + authenticated in a previous session during which the IdP has set a + cookie in the UA. During the present session the Principal has only + been authenticated by the UA returning the cookie to the IdP. + + + + + + + + Rather like PreviousSession but using stronger + security. A secret that was established in a previous session with + the Authentication Authority has been cached by the local system and + is now re-used (e.g. a Master Secret is used to derive new session + keys in TLS, SSL, WTLS). + + + + + + + + This element indicates that the Principal has been + authenticated by a zero knowledge technique as specified in ISO/IEC + 9798-5. + + + + + + + + + + This element indicates that the Principal has been + authenticated by a challenge-response protocol utilizing shared secret + keys and symmetric cryptography. + + + + + + + + + + + + This element indicates that the Principal has been + authenticated by a mechanism which involves the Principal computing a + digital signature over at least challenge data provided by the IdP. + + + + + + + + The local system has a private key but it is used + in decryption mode, rather than signature mode. For example, the + Authentication Authority generates a secret and encrypts it using the + local system's public key: the local system then proves it has + decrypted the secret. + + + + + + + + The local system has a private key and uses it for + shared secret key agreement with the Authentication Authority (e.g. + via Diffie Helman). + + + + + + + + + + + + + + + This element indicates that the Principal has been + authenticated through connection from a particular IP address. + + + + + + + + The local system and Authentication Authority + share a secret key. The local system uses this to encrypt a + randomised string to pass to the Authentication Authority. + + + + + + + + The protocol across which Authenticator information is + transferred to an Authentication Authority verifier. + + + + + + + + This element indicates that the Authenticator has been + transmitted using bare HTTP utilizing no additional security + protocols. + + + + + + + + This element indicates that the Authenticator has been + transmitted using a transport mechanism protected by an IPSEC session. + + + + + + + + This element indicates that the Authenticator has been + transmitted using a transport mechanism protected by a WTLS session. + + + + + + + + This element indicates that the Authenticator has been + transmitted solely across a mobile network using no additional + security mechanism. + + + + + + + + + + + This element indicates that the Authenticator has been + transmitted using a transport mechnanism protected by an SSL or TLS + session. + + + + + + + + + + + + Refers to those characteristics that describe + procedural security controls employed by the Authentication Authority. + + + + + + + + + + + + Provides a mechanism for linking to external (likely + human readable) documents in which additional business agreements, + (e.g. liability constraints, obligations, etc) can be placed. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This attribute indicates whether or not the + Identification mechanisms allow the actions of the Principal to be + linked to an actual end user. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This element indicates that the Key Activation Limit is + defined as a specific duration of time. + + + + + + + + This element indicates that the Key Activation Limit is + defined as a number of usages. + + + + + + + + This element indicates that the Key Activation Limit is + the session. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/onelogin/saml2/schemas/saml-schema-metadata-2.0.xsd b/src/onelogin/saml2/schemas/saml-schema-metadata-2.0.xsd new file mode 100644 index 00000000..86e58f9b --- /dev/null +++ b/src/onelogin/saml2/schemas/saml-schema-metadata-2.0.xsd @@ -0,0 +1,336 @@ + + + + + + + + + Document identifier: saml-schema-metadata-2.0 + Location: http://docs.oasis-open.org/security/saml/v2.0/ + Revision history: + V2.0 (March, 2005): + Schema for SAML metadata, first published in SAML 2.0. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/onelogin/saml2/schemas/saml-schema-protocol-2.0.xsd b/src/onelogin/saml2/schemas/saml-schema-protocol-2.0.xsd new file mode 100644 index 00000000..7fa6f489 --- /dev/null +++ b/src/onelogin/saml2/schemas/saml-schema-protocol-2.0.xsd @@ -0,0 +1,302 @@ + + + + + + + Document identifier: saml-schema-protocol-2.0 + Location: http://docs.oasis-open.org/security/saml/v2.0/ + Revision history: + V1.0 (November, 2002): + Initial Standard Schema. + V1.1 (September, 2003): + Updates within the same V1.0 namespace. + V2.0 (March, 2005): + New protocol schema based in a SAML V2.0 namespace. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/onelogin/saml2/schemas/sstc-metadata-attr.xsd b/src/onelogin/saml2/schemas/sstc-metadata-attr.xsd new file mode 100644 index 00000000..f23e462a --- /dev/null +++ b/src/onelogin/saml2/schemas/sstc-metadata-attr.xsd @@ -0,0 +1,35 @@ + + + + + + Document title: SAML V2.0 Metadata Extention for Entity Attributes Schema + Document identifier: sstc-metadata-attr.xsd + Location: http://www.oasis-open.org/committees/documents.php?wg_abbrev=security + Revision history: + V1.0 (November 2008): + Initial version. + + + + + + + + + + + + + + + diff --git a/src/onelogin/saml2/schemas/sstc-saml-attribute-ext.xsd b/src/onelogin/saml2/schemas/sstc-saml-attribute-ext.xsd new file mode 100644 index 00000000..ad309c14 --- /dev/null +++ b/src/onelogin/saml2/schemas/sstc-saml-attribute-ext.xsd @@ -0,0 +1,25 @@ + + + + + + Document title: SAML V2.0 Attribute Extension Schema + Document identifier: sstc-saml-attribute-ext.xsd + Location: http://www.oasis-open.org/committees/documents.php?wg_abbrev=security + Revision history: + V1.0 (October 2008): + Initial version. + + + + + + + + diff --git a/src/onelogin/saml2/schemas/sstc-saml-metadata-algsupport-v1.0.xsd b/src/onelogin/saml2/schemas/sstc-saml-metadata-algsupport-v1.0.xsd new file mode 100644 index 00000000..3236ffcd --- /dev/null +++ b/src/onelogin/saml2/schemas/sstc-saml-metadata-algsupport-v1.0.xsd @@ -0,0 +1,41 @@ + + + + + + Document title: Metadata Extension Schema for SAML V2.0 Metadata Profile for Algorithm Support Version 1.0 + Document identifier: sstc-saml-metadata-algsupport.xsd + Location: http://docs.oasis-open.org/security/saml/Post2.0/ + Revision history: + V1.0 (June 2010): + Initial version. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/onelogin/saml2/schemas/sstc-saml-metadata-ui-v1.0.xsd b/src/onelogin/saml2/schemas/sstc-saml-metadata-ui-v1.0.xsd new file mode 100644 index 00000000..de0b754a --- /dev/null +++ b/src/onelogin/saml2/schemas/sstc-saml-metadata-ui-v1.0.xsd @@ -0,0 +1,89 @@ + + + + + + Document title: Metadata Extension Schema for SAML V2.0 Metadata Extensions for Login and Discovery User Interface Version 1.0 + Document identifier: sstc-saml-metadata-ui-v1.0.xsd + Location: http://docs.oasis-open.org/security/saml/Post2.0/ + Revision history: + 16 November 2010: + Added Keywords element/type. + 01 November 2010 + Changed filename. + September 2010: + Initial version. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/onelogin/saml2/schemas/xenc-schema.xsd b/src/onelogin/saml2/schemas/xenc-schema.xsd new file mode 100644 index 00000000..d6d79103 --- /dev/null +++ b/src/onelogin/saml2/schemas/xenc-schema.xsd @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/onelogin/saml2/schemas/xml.xsd b/src/onelogin/saml2/schemas/xml.xsd new file mode 100644 index 00000000..aea7d0db --- /dev/null +++ b/src/onelogin/saml2/schemas/xml.xsd @@ -0,0 +1,287 @@ + + + + + + +
+

About the XML namespace

+ +
+

+ This schema document describes the XML namespace, in a form + suitable for import by other schema documents. +

+

+ See + http://www.w3.org/XML/1998/namespace.html and + + http://www.w3.org/TR/REC-xml for information + about this namespace. +

+

+ Note that local names in this namespace are intended to be + defined only by the World Wide Web Consortium or its subgroups. + The names currently defined in this namespace are listed below. + They should not be used with conflicting semantics by any Working + Group, specification, or document instance. +

+

+ See further below in this document for more information about how to refer to this schema document from your own + XSD schema documents and about the + namespace-versioning policy governing this schema document. +

+
+
+
+
+ + + + +
+ +

lang (as an attribute name)

+

+ denotes an attribute whose value + is a language code for the natural language of the content of + any element; its value is inherited. This name is reserved + by virtue of its definition in the XML specification.

+ +
+
+

Notes

+

+ Attempting to install the relevant ISO 2- and 3-letter + codes as the enumerated possible values is probably never + going to be a realistic possibility. +

+

+ See BCP 47 at + http://www.rfc-editor.org/rfc/bcp/bcp47.txt + and the IANA language subtag registry at + + http://www.iana.org/assignments/language-subtag-registry + for further information. +

+

+ The union allows for the 'un-declaration' of xml:lang with + the empty string. +

+
+
+
+ + + + + + + + + +
+ + + + +
+ +

space (as an attribute name)

+

+ denotes an attribute whose + value is a keyword indicating what whitespace processing + discipline is intended for the content of the element; its + value is inherited. This name is reserved by virtue of its + definition in the XML specification.

+ +
+
+
+ + + + + + +
+ + + +
+ +

base (as an attribute name)

+

+ denotes an attribute whose value + provides a URI to be used as the base for interpreting any + relative URIs in the scope of the element on which it + appears; its value is inherited. This name is reserved + by virtue of its definition in the XML Base specification.

+ +

+ See http://www.w3.org/TR/xmlbase/ + for information about this attribute. +

+
+
+
+
+ + + + +
+ +

id (as an attribute name)

+

+ denotes an attribute whose value + should be interpreted as if declared to be of type ID. + This name is reserved by virtue of its definition in the + xml:id specification.

+ +

+ See http://www.w3.org/TR/xml-id/ + for information about this attribute. +

+
+
+
+
+ + + + + + + + + + +
+ +

Father (in any context at all)

+ +
+

+ denotes Jon Bosak, the chair of + the original XML Working Group. This name is reserved by + the following decision of the W3C XML Plenary and + XML Coordination groups: +

+
+

+ In appreciation for his vision, leadership and + dedication the W3C XML Plenary on this 10th day of + February, 2000, reserves for Jon Bosak in perpetuity + the XML name "xml:Father". +

+
+
+
+
+
+ + + +
+

About this schema document

+ +
+

+ This schema defines attributes and an attribute group suitable + for use by schemas wishing to allow xml:base, + xml:lang, xml:space or + xml:id attributes on elements they define. +

+

+ To enable this, such a schema must import this schema for + the XML namespace, e.g. as follows: +

+
+          <schema . . .>
+           . . .
+           <import namespace="http://www.w3.org/XML/1998/namespace"
+                      schemaLocation="http://www.w3.org/2001/xml.xsd"/>
+     
+

+ or +

+
+           <import namespace="http://www.w3.org/XML/1998/namespace"
+                      schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
+     
+

+ Subsequently, qualified reference to any of the attributes or the + group defined below will have the desired effect, e.g. +

+
+          <type . . .>
+           . . .
+           <attributeGroup ref="xml:specialAttrs"/>
+     
+

+ will define a type which will schema-validate an instance element + with any of those attributes. +

+
+
+
+
+ + + +
+

Versioning policy for this schema document

+
+

+ In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + + http://www.w3.org/2009/01/xml.xsd. +

+

+ At the date of issue it can also be found at + + http://www.w3.org/2001/xml.xsd. +

+

+ The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML + Schema itself, or with the XML namespace itself. In other words, + if the XML Schema or XML namespaces change, the version of this + document at + http://www.w3.org/2001/xml.xsd + + will change accordingly; the version at + + http://www.w3.org/2009/01/xml.xsd + + will not change. +

+

+ Previous dated (and unchanging) versions of this schema + document are at: +

+ +
+
+
+
+ +
+ diff --git a/src/onelogin/saml2/schemas/xmldsig-core-schema.xsd b/src/onelogin/saml2/schemas/xmldsig-core-schema.xsd new file mode 100644 index 00000000..6f5acc75 --- /dev/null +++ b/src/onelogin/saml2/schemas/xmldsig-core-schema.xsd @@ -0,0 +1,309 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py new file mode 100644 index 00000000..fb194546 --- /dev/null +++ b/src/onelogin/saml2/settings.py @@ -0,0 +1,861 @@ +# -*- coding: utf-8 -*- + +""" OneLogin_Saml2_Settings class + +MIT License + +Setting class of Python Toolkit. + +""" + +import json +import re +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 +from onelogin.saml2.errors import OneLogin_Saml2_Error +from onelogin.saml2.metadata import OneLogin_Saml2_Metadata +from onelogin.saml2.utils import OneLogin_Saml2_Utils + + +# Regex from Django Software Foundation and individual contributors. +# 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'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 + r'(?::\d+)?' # optional port + r'(?:/?|[/?]\S+)$', re.IGNORECASE) +url_regex_single_label_domain = 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_]))|' # single-label-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 + r'(?::\d+)?' # optional port + r'(?:/?|[/?]\S+)$', re.IGNORECASE) +url_schemes = ['http', 'https', 'ftp', 'ftps'] + + +def validate_url(url, allow_single_label_domain=False): + """ + Auxiliary method to validate an urllib + :param url: An url to be validated + :type url: string + :param allow_single_label_domain: In order to allow or not single label domain + :type url: bool + :returns: True if the url is valid + :rtype: bool + """ + + scheme = url.split('://')[0].lower() + if scheme not in url_schemes: + return False + if allow_single_label_domain: + if not bool(url_regex_single_label_domain.search(url)): + return False + else: + if not bool(url_regex.search(url)): + return False + return True + + +class OneLogin_Saml2_Settings(object): + """ + + Handles the settings of the Python toolkits. + + """ + + def __init__(self, settings=None, custom_base_path=None, sp_validation_only=False): + """ + Initializes the settings: + - Sets the paths of the different folders + - Loads settings info from settings file or array/object provided + + :param settings: SAML Toolkit Settings + :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 = True + self.__debug = False + self.__sp = {} + self.__idp = {} + self.__security = {} + self.__contacts = {} + self.__organization = {} + self.__errors = [] + + self.__load_paths(base_path=custom_base_path) + self.__update_paths(settings) + + if settings is None: + try: + valid = self.__load_settings_from_file() + except Exception as e: + raise e + if not valid: + raise OneLogin_Saml2_Error( + 'Invalid dict settings at the file: %s', + OneLogin_Saml2_Error.SETTINGS_INVALID, + ','.join(self.__errors) + ) + self.__add_default_values() + elif isinstance(settings, dict): + if not self.__load_settings_from_dict(settings): + raise OneLogin_Saml2_Error( + 'Invalid dict settings: %s', + OneLogin_Saml2_Error.SETTINGS_INVALID, + ','.join(self.__errors) + ) + else: + 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): + """ + Set the paths of the different folders + """ + if base_path is None: + base_path = dirname(dirname(dirname(abspath(__file__)))) + if not base_path.endswith(sep): + base_path += sep + self.__paths = { + 'base': base_path, + 'cert': base_path + 'certs' + sep, + 'lib': dirname(__file__) + sep + } + + def __update_paths(self, settings): + """ + Set custom paths if necessary + """ + if not isinstance(settings, dict): + return + + if 'custom_base_path' in settings: + base_path = settings['custom_base_path'] + base_path = join(dirname(abspath(__file__)), base_path) + self.__load_paths(base_path) + + def get_base_path(self): + """ + Returns base path + + :return: The base toolkit folder path + :rtype: string + """ + return self.__paths['base'] + + def set_cert_path(self, path): + """ + Set a new cert path + """ + self.__paths['cert'] = path + + def get_cert_path(self): + """ + Returns cert path + + :return: The cert folder path + :rtype: string + """ + return self.__paths['cert'] + + def get_lib_path(self): + """ + Returns lib path + + :return: The library folder path + :rtype: string + """ + return self.__paths['lib'] + + def get_schemas_path(self): + """ + Returns schema path + + :return: The schema folder path + :rtype: string + """ + return self.__paths['lib'] + 'schemas/' + + def __load_settings_from_dict(self, settings): + """ + Loads settings info from a settings Dict + + :param settings: SAML Toolkit Settings + :type settings: dict + + :returns: True if the settings info is valid + :rtype: boolean + """ + errors = self.check_settings(settings) + if len(errors) == 0: + self.__errors = [] + self.__sp = settings['sp'] + + 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 + + self.__errors = errors + return False + + def __load_settings_from_file(self): + """ + Loads settings info from the settings json file + + :returns: True if the settings info is valid + :rtype: boolean + """ + filename = self.get_base_path() + 'settings.json' + + if not exists(filename): + raise OneLogin_Saml2_Error( + 'Settings file not found: %s', + OneLogin_Saml2_Error.SETTINGS_FILE_NOT_FOUND, + filename + ) + + # In the php toolkit instead of being a json file it is a php file and + # it is directly included + json_data = open(filename, 'r') + settings = json.load(json_data) + json_data.close() + + advanced_filename = self.get_base_path() + 'advanced_settings.json' + if exists(advanced_filename): + json_data = open(advanced_filename, 'r') + settings.update(json.load(json_data)) # Merge settings + json_data.close() + + return self.__load_settings_from_dict(settings) + + def __add_default_values(self): + """ + Add default values if the settings info is not complete + """ + self.__sp.setdefault('assertionConsumerService', {}) + self.__sp['assertionConsumerService'].setdefault('binding', OneLogin_Saml2_Constants.BINDING_HTTP_POST) + + self.__sp.setdefault('attributeConsumingService', {}) + + self.__sp.setdefault('singleLogoutService', {}) + self.__sp['singleLogoutService'].setdefault('binding', OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT) + + self.__idp.setdefault('singleLogoutService', {}) + + # Related to nameID + self.__sp.setdefault('NameIDFormat', OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED) + self.__security.setdefault('nameIdEncrypted', False) + + # Metadata format + self.__security.setdefault('metadataValidUntil', None) # None means use default + self.__security.setdefault('metadataCacheDuration', None) # None means use default + + # Sign provided + self.__security.setdefault('authnRequestsSigned', False) + self.__security.setdefault('logoutRequestSigned', False) + self.__security.setdefault('logoutResponseSigned', False) + self.__security.setdefault('signMetadata', False) + + # Sign expected + 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 + self.__security.setdefault('wantAssertionsEncrypted', False) + self.__security.setdefault('wantNameIdEncrypted', False) + + # Signature Algorithm + self.__security.setdefault('signatureAlgorithm', OneLogin_Saml2_Constants.RSA_SHA256) + + # Digest Algorithm + self.__security.setdefault('digestAlgorithm', OneLogin_Saml2_Constants.SHA256) + + # Reject Deprecated Algorithms + self.__security.setdefault('rejectDeprecatedAlgorithm', False) + + # AttributeStatement required by default + self.__security.setdefault('wantAttributeStatement', 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('requestedAuthnContextComparison', 'exact') + self.__security.setdefault('failOnAuthnContextMismatch', False) + + def check_settings(self, settings): + """ + Checks the settings info. + + :param settings: Dict with settings data + :type settings: dict + + :returns: Errors found on the settings data + :rtype: list + """ + assert isinstance(settings, dict) + + errors = [] + if not isinstance(settings, dict) or len(settings) == 0: + errors.append('invalid_syntax') + else: + if not self.__sp_validation_only: + errors += self.check_idp_settings(settings) + sp_errors = self.check_sp_settings(settings) + errors += sp_errors + + return errors + + def check_idp_settings(self, settings): + """ + Checks the IdP settings info. + + :param settings: Dict with settings data + :type settings: dict + + :returns: Errors found on the IdP settings data + :rtype: list + """ + assert isinstance(settings, dict) + + errors = [] + if not isinstance(settings, dict) or len(settings) == 0: + errors.append('invalid_syntax') + else: + if not settings.get('idp'): + errors.append('idp_not_found') + else: + allow_single_domain_urls = self._get_allow_single_label_domain(settings) + idp = settings['idp'] + if not idp.get('entityId'): + errors.append('idp_entityId_not_found') + + if not idp.get('singleSignOnService', {}).get('url'): + errors.append('idp_sso_not_found') + elif not validate_url(idp['singleSignOnService']['url'], allow_single_domain_urls): + errors.append('idp_sso_url_invalid') + + slo_url = idp.get('singleLogoutService', {}).get('url') + if slo_url and not validate_url(slo_url, allow_single_domain_urls): + errors.append('idp_slo_url_invalid') + + if 'security' in settings: + security = settings['security'] + + 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 = 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 or exists_multix509sign): + errors.append('idp_cert_or_fingerprint_not_found_and_required') + if nameid_enc and not (exists_x509 or exists_multix509enc): + errors.append('idp_cert_not_found_and_required') + + return errors + + def check_sp_settings(self, settings): + """ + Checks the SP settings info. + + :param settings: Dict with settings data + :type settings: dict + + :returns: Errors found on the SP settings data + :rtype: list + """ + assert isinstance(settings, dict) + + errors = [] + if not isinstance(settings, dict) or not settings: + errors.append('invalid_syntax') + else: + if not settings.get('sp'): + errors.append('sp_not_found') + else: + allow_single_domain_urls = self._get_allow_single_label_domain(settings) + # check_sp_certs uses self.__sp so I add it + old_sp = self.__sp + self.__sp = settings['sp'] + + sp = settings['sp'] + security = settings.get('security', {}) + + if not sp.get('entityId'): + errors.append('sp_entityId_not_found') + + if not sp.get('assertionConsumerService', {}).get('url'): + errors.append('sp_acs_not_found') + elif not validate_url(sp['assertionConsumerService']['url'], allow_single_domain_urls): + errors.append('sp_acs_url_invalid') + + 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, allow_single_domain_urls): + errors.append('sp_sls_url_invalid') + + if 'signMetadata' in security and isinstance(security['signMetadata'], dict): + if 'keyFileName' not in security['signMetadata'] or \ + 'certFileName' not in security['signMetadata']: + errors.append('sp_signMetadata_invalid') + + 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 \ + want_assert_enc or want_nameid_enc: + errors.append('sp_cert_not_found_and_required') + + if 'contactPerson' in settings: + types = settings['contactPerson'].keys() + valid_types = ['technical', 'support', 'administrative', 'billing', 'other'] + for c_type in types: + if c_type not in valid_types: + errors.append('contact_type_invalid') + break + + for c_type in settings['contactPerson']: + contact = settings['contactPerson'][c_type] + if ('givenName' not in contact or len(contact['givenName']) == 0) or \ + ('emailAddress' not in contact or len(contact['emailAddress']) == 0): + errors.append('contact_not_enough_data') + break + + if 'organization' in settings: + for org in settings['organization']: + organization = settings['organization'][org] + if ('name' not in organization or len(organization['name']) == 0) or \ + ('displayname' not in organization or len(organization['displayname']) == 0) or \ + ('url' not in organization or len(organization['url']) == 0): + errors.append('organization_not_enough_data') + break + # Restores the value that had the self.__sp + if 'old_sp' in locals(): + self.__sp = old_sp + + return errors + + def check_sp_certs(self): + """ + Checks if the x509 certs of the SP exists and are valid. + + :returns: If the x509 certs of the SP exists and are valid + :rtype: boolean + """ + key = self.get_sp_key() + cert = self.get_sp_cert() + return key is not None and cert is not None + + def get_idp_sso_url(self): + """ + Gets the IdP SSO URL. + + :returns: An URL, the SSO endpoint of the IdP + :rtype: string + """ + idp_data = self.get_idp_data() + return idp_data['singleSignOnService']['url'] + + def get_idp_slo_url(self): + """ + Gets the IdP SLO URL. + + :returns: An URL, the SLO endpoint of the IdP + :rtype: string + """ + idp_data = self.get_idp_data() + if 'url' in idp_data['singleLogoutService']: + return idp_data['singleLogoutService']['url'] + + def get_idp_slo_response_url(self): + """ + Gets the IdP SLO return URL for IdP-initiated logout. + + :returns: an URL, the SLO return endpoint of the IdP + :rtype: string + """ + idp_data = self.get_idp_data() + if 'url' in idp_data['singleLogoutService']: + return idp_data['singleLogoutService'].get('responseUrl', self.get_idp_slo_url()) + + def get_sp_key(self): + """ + Returns the x509 private key of the SP. + + :returns: SP private key + :rtype: string or None + """ + key = self.__sp.get('privateKey') + 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() + + return key or None + + def get_sp_cert(self): + """ + Returns the x509 public cert of the SP. + + :returns: SP public cert + :rtype: string or None + """ + cert = self.__sp.get('x509cert') + cert_file_name = self.__paths['cert'] + 'sp.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_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): + """ + Returns the x509 public cert of the IdP. + + :returns: IdP public cert + :rtype: string + """ + return self.__idp.get('x509cert') + + def get_idp_data(self): + """ + Gets the IdP data. + + :returns: IdP info + :rtype: dict + """ + return self.__idp + + def get_sp_data(self): + """ + Gets the SP data. + + :returns: SP info + :rtype: dict + """ + return self.__sp + + def get_security_data(self): + """ + Gets security data. + + :returns: Security info + :rtype: dict + """ + return self.__security + + def get_contacts(self): + """ + Gets contact data. + + :returns: Contacts info + :rtype: dict + """ + return self.__contacts + + def get_organization(self): + """ + Gets organization data. + + :returns: Organization info + :rtype: dict + """ + return self.__organization + + def get_sp_metadata(self): + """ + Gets the SP metadata. The XML representation. + + :returns: SP metadata (xml) + :rtype: string + """ + metadata = OneLogin_Saml2_Metadata.builder( + self.__sp, self.__security['authnRequestsSigned'], + self.__security['wantAssertionsSigned'], + self.__security['metadataValidUntil'], + 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, add_encryption) + + # Sign metadata + if 'signMetadata' in self.__security and self.__security['signMetadata'] is not False: + if self.__security['signMetadata'] is True: + # Use the SP's normal key to sign the metadata: + if not cert: + raise OneLogin_Saml2_Error( + 'Cannot sign metadata: missing SP public key certificate.', + OneLogin_Saml2_Error.PUBLIC_CERT_FILE_NOT_FOUND + ) + cert_metadata = cert + key_metadata = self.get_sp_key() + if not key_metadata: + raise OneLogin_Saml2_Error( + 'Cannot sign metadata: missing SP private key.', + OneLogin_Saml2_Error.PRIVATE_KEY_FILE_NOT_FOUND + ) + else: + # Use a custom key to sign the metadata: + if ('keyFileName' not in self.__security['signMetadata'] or + 'certFileName' not in self.__security['signMetadata']): + raise OneLogin_Saml2_Error( + 'Invalid Setting: signMetadata value of the sp is not valid', + OneLogin_Saml2_Error.SETTINGS_INVALID_SYNTAX + ) + key_file_name = self.__security['signMetadata']['keyFileName'] + cert_file_name = self.__security['signMetadata']['certFileName'] + key_metadata_file = self.__paths['cert'] + key_file_name + cert_metadata_file = self.__paths['cert'] + cert_file_name + + try: + with open(key_metadata_file, 'r') as f_metadata_key: + key_metadata = f_metadata_key.read() + except IOError: + raise OneLogin_Saml2_Error( + 'Private key file not readable: %s', + OneLogin_Saml2_Error.PRIVATE_KEY_FILE_NOT_FOUND, + key_metadata_file + ) + + try: + with open(cert_metadata_file, 'r') as f_metadata_cert: + cert_metadata = f_metadata_cert.read() + except IOError: + raise OneLogin_Saml2_Error( + 'Public cert file not readable: %s', + OneLogin_Saml2_Error.PUBLIC_CERT_FILE_NOT_FOUND, + cert_metadata_file + ) + + 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 + + def validate_metadata(self, xml): + """ + Validates an XML SP Metadata. + + :param xml: Metadata's XML that will be validate + :type xml: string + + :returns: The list of found errors + :rtype: list + """ + + assert isinstance(xml, basestring) + + if len(xml) == 0: + raise Exception('Empty string supplied as input') + + errors = [] + res = OneLogin_Saml2_Utils.validate_xml(xml, 'saml-schema-metadata-2.0.xsd', self.__debug) + if not isinstance(res, Document): + errors.append(res) + else: + dom = res + element = dom.documentElement + if element.tagName not in 'md:EntityDescriptor': + errors.append('noEntityDescriptor_xml') + else: + if len(element.getElementsByTagName('md:SPSSODescriptor')) != 1: + errors.append('onlySPSSODescriptor_allowed_xml') + else: + valid_until = cache_duration = expire_time = None + + if element.hasAttribute('validUntil'): + valid_until = OneLogin_Saml2_Utils.parse_SAML_to_time(element.getAttribute('validUntil')) + if element.hasAttribute('cacheDuration'): + 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(time()) > int(expire_time): + errors.append('expired_xml') + + # TODO: Validate Sign + + return errors + + def format_idp_cert(self): + """ + Formats the IdP cert. + """ + 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. + """ + self.__sp['privateKey'] = OneLogin_Saml2_Utils.format_private_key(self.__sp['privateKey']) + + def get_errors(self): + """ + Returns an array with the errors, the array is empty when the settings is ok. + + :returns: Errors + :rtype: list + """ + return self.__errors + + def set_strict(self, value): + """ + Activates or deactivates the strict mode. + + :param value: Strict parameter + :type xml: boolean + """ + assert isinstance(value, bool) + + self.__strict = value + + def is_strict(self): + """ + Returns if the 'strict' mode is active. + + :returns: Strict parameter + :rtype: boolean + """ + return self.__strict + + def is_debug_active(self): + """ + Returns if the debug is active. + + :returns: Debug parameter + :rtype: boolean + """ + return self.__debug + + def _get_allow_single_label_domain(self, settings): + security = settings.get('security', {}) + return 'allowSingleLabelDomains' in security.keys() and security['allowSingleLabelDomains'] diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py new file mode 100644 index 00000000..bfdb10a8 --- /dev/null +++ b/src/onelogin/saml2/utils.py @@ -0,0 +1,1303 @@ +# -*- coding: utf-8 -*- + +""" OneLogin_Saml2_Utils class + +MIT License + +Auxiliary class of Python Toolkit. + +""" + +import base64 +from copy import deepcopy +from datetime import datetime +import calendar +from hashlib import sha1, sha256, sha384, sha512 +from isodate import parse_duration as duration_parser +from lxml import etree +from os.path import basename, dirname, join +import re +from sys import stderr +from tempfile import NamedTemporaryFile +from textwrap import wrap +from urllib import quote_plus +from urlparse import urlsplit, urlunsplit +from uuid import uuid4 +from xml.dom.minidom import Document, Element +from defusedxml.minidom import parseString +from functools import wraps + +import zlib + +import dm.xmlsec.binding as xmlsec +from dm.xmlsec.binding.tmpl import EncData, Signature + +from onelogin.saml2.constants import OneLogin_Saml2_Constants +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError +from onelogin.saml2.xmlparser import tostring, fromstring + + +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 overrides the default xmlsec debug message. + """ + + info = [] + if error_object != "unknown": + info.append("obj=" + error_object) + if error_subject != "unknown": + info.append("subject=" + error_subject) + if msg.strip(): + info.append("msg=" + msg) + if reason != 1: + info.append("errno=%d" % reason) + if info: + print("%s:%d(%s)" % (filename, line, func), " ".join(info)) + + +class OneLogin_Saml2_Utils(object): + """ + + Auxiliary class that contains several utility methods to parse time, + urls, add sign, encrypt, decrypt, sign validation, handle xml ... + + """ + + RESPONSE_SIGNATURE_XPATH = '/samlp:Response/ds:Signature' + ASSERTION_SIGNATURE_XPATH = '/samlp:Response/saml:Assertion/ds:Signature' + + TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" + TIME_FORMAT_2 = "%Y-%m-%dT%H:%M:%S.%fZ" + TIME_FORMAT_WITH_FRAGMENT = re.compile(r'^(\d{4,4}-\d{2,2}-\d{2,2}T\d{2,2}:\d{2,2}:\d{2,2})(\.\d*)?Z?$') + + @staticmethod + def decode_base64_and_inflate(value): + """ + base64 decodes and then inflates according to RFC1951 + :param value: a deflated and encoded string + :type value: string + :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 result.decode('utf-8') + + @staticmethod + def deflate_and_base64_encode(value): + """ + 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.encode('utf-8'))[2:-4]) + + @staticmethod + def validate_xml(xml, schema, debug=False): + """ + Validates a xml against a schema + :param xml: The xml that will be validated + :type: string|DomDocument + :param schema: The schema + :type: string + :param debug: If debug is active, the parse-errors will be showed + :type: bool + :returns: Error code or the DomDocument of the xml + :rtype: string + """ + assert isinstance(xml, basestring) or isinstance(xml, Document) or isinstance(xml, etree._Element) + assert isinstance(schema, basestring) + + if isinstance(xml, Document): + xml = xml.toxml() + elif isinstance(xml, etree._Element): + xml = tostring(xml, encoding='unicode') + + # Switch to lxml for schema validation + try: + dom = fromstring(xml.encode('utf-8'), forbid_dtd=True) + except Exception: + return 'unloaded_xml' + + schema_file = join(dirname(__file__), 'schemas', schema) + f_schema = open(schema_file, 'r') + schema_doc = etree.parse(f_schema) + f_schema.close() + xmlschema = etree.XMLSchema(schema_doc) + + if not xmlschema.validate(dom): + if debug: + stderr.write('Errors validating the metadata') + stderr.write(':\n\n') + for error in xmlschema.error_log: + stderr.write('%s\n' % error.message) + + return 'invalid_xml' + + return parseString(tostring(dom, encoding='unicode').encode('utf-8'), forbid_dtd=True, forbid_entities=True, forbid_external=True) + + @staticmethod + def element_text(node): + # Double check, the LXML Parser already removes comments + 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 unformatted cert + :type: string + + :param heads: True if we want to include head and footer + :type: boolean + + :returns: Formatted cert + :rtype: string + """ + x509_cert = cert.replace('\x0D', '') + x509_cert = x509_cert.replace('\r', '') + x509_cert = x509_cert.replace('\n', '') + if len(x509_cert) > 0: + x509_cert = x509_cert.replace('-----BEGIN CERTIFICATE-----', '') + x509_cert = x509_cert.replace('-----END CERTIFICATE-----', '') + x509_cert = x509_cert.replace(' ', '') + + if heads: + x509_cert = "-----BEGIN CERTIFICATE-----\n" + "\n".join(wrap(x509_cert, 64)) + "\n-----END CERTIFICATE-----\n" + + return x509_cert + + @staticmethod + def format_private_key(key, heads=True): + """ + Returns a private key (adding header & footer if required). + + :param key A private key + :type: string + + :param heads: True if we want to include head and footer + :type: boolean + + :returns: Formatted private key + :rtype: string + """ + private_key = key.replace('\x0D', '') + private_key = private_key.replace('\r', '') + private_key = private_key.replace('\n', '') + if len(private_key) > 0: + if private_key.find('-----BEGIN PRIVATE KEY-----') != -1: + private_key = private_key.replace('-----BEGIN PRIVATE KEY-----', '') + private_key = private_key.replace('-----END PRIVATE KEY-----', '') + private_key = private_key.replace(' ', '') + if heads: + private_key = "-----BEGIN PRIVATE KEY-----\n" + "\n".join(wrap(private_key, 64)) + "\n-----END PRIVATE KEY-----\n" + else: + private_key = private_key.replace('-----BEGIN RSA PRIVATE KEY-----', '') + private_key = private_key.replace('-----END RSA PRIVATE KEY-----', '') + private_key = private_key.replace(' ', '') + if heads: + private_key = "-----BEGIN RSA PRIVATE KEY-----\n" + "\n".join(wrap(private_key, 64)) + "\n-----END RSA PRIVATE KEY-----\n" + return private_key + + @staticmethod + def redirect(url, parameters={}, request_data={}): + """ + Executes a redirection to the provided url (or return the target url). + + :param url: The target url + :type: string + + :param parameters: Extra parameters to be passed as part of the url + :type: dict + + :param request_data: The request as a dict + :type: dict + + :returns: Url + :rtype: string + """ + assert isinstance(url, basestring) + assert isinstance(parameters, dict) + + if url.startswith('/'): + url = '%s%s' % (OneLogin_Saml2_Utils.get_self_url_host(request_data), url) + + # Verify that the URL is to a http or https site. + if re.search('^https?://', url) is None: + raise OneLogin_Saml2_Error( + 'Redirect to invalid URL: ' + url, + OneLogin_Saml2_Error.REDIRECT_INVALID_URL + ) + + # Add encoded parameters + if url.find('?') < 0: + param_prefix = '?' + else: + param_prefix = '&' + + for name, value in parameters.items(): + + if value is None: + param = quote_plus(name) + elif isinstance(value, list): + param = '' + for val in value: + param += quote_plus(name) + '[]=' + quote_plus(val) + '&' + if len(param) > 0: + param = param[0:-1] + else: + param = quote_plus(name) + '=' + quote_plus(value) + + if param: + url += param_prefix + param + param_prefix = '&' + + return url + + @staticmethod + def get_self_url_host(request_data): + """ + Returns the protocol + the current host + the port (if different than + common ports). + + :param request_data: The request as a dict + :type: dict + + :return: Url + :rtype: string + """ + current_host = OneLogin_Saml2_Utils.get_self_host(request_data) + port = '' + if OneLogin_Saml2_Utils.is_https(request_data): + protocol = 'https' + else: + protocol = 'http' + + if 'server_port' in request_data and request_data['server_port'] is not None: + port_number = str(request_data['server_port']) + port = ':' + port_number + + if protocol == 'http' and port_number == '80': + port = '' + elif protocol == 'https' and port_number == '443': + port = '' + + return '%s://%s%s' % (protocol, current_host, port) + + @staticmethod + def get_self_host(request_data): + """ + Returns the current host. + + :param request_data: The request as a dict + :type: dict + + :return: The current host + :rtype: string + """ + if 'http_host' in request_data: + current_host = request_data['http_host'] + elif 'server_name' in request_data: + current_host = request_data['server_name'] + else: + raise Exception('No hostname defined') + + if ':' in current_host: + current_host_data = current_host.split(':') + possible_port = current_host_data[-1] + try: + possible_port = float(possible_port) + current_host = current_host_data[0] + except ValueError: + current_host = ':'.join(current_host_data) + + return current_host + + @staticmethod + def is_https(request_data): + """ + Checks if https or http. + + :param request_data: The request as a dict + :type: dict + + :return: False if https is not active + :rtype: boolean + """ + is_https = 'https' in request_data and request_data['https'] != 'off' + is_https = is_https or ('server_port' in request_data and str(request_data['server_port']) == '443') + return is_https + + @staticmethod + def get_self_url_no_query(request_data): + """ + Returns the URL of the current host + current view. + + :param request_data: The request as a dict + :type: dict + + :return: The url of current host + current view + :rtype: string + """ + self_url_host = OneLogin_Saml2_Utils.get_self_url_host(request_data) + script_name = request_data['script_name'] + if script_name: + if script_name[0] != '/': + script_name = '/' + script_name + else: + script_name = '' + self_url_no_query = self_url_host + script_name + if 'path_info' in request_data: + self_url_no_query += request_data['path_info'] + + return self_url_no_query + + @staticmethod + def get_self_routed_url_no_query(request_data): + """ + Returns the routed URL of the current host + current view. + + :param request_data: The request as a dict + :type: dict + + :return: The url of current host + current view + :rtype: string + """ + self_url_host = OneLogin_Saml2_Utils.get_self_url_host(request_data) + route = '' + if 'request_uri' in request_data.keys() and request_data['request_uri']: + route = request_data['request_uri'] + if 'query_string' in request_data.keys() and request_data['query_string']: + route = route.replace(request_data['query_string'], '') + + return self_url_host + route + + @staticmethod + def get_self_url(request_data): + """ + Returns the URL of the current host + current view + query. + + :param request_data: The request as a dict + :type: dict + + :return: The url of current host + current view + query + :rtype: string + """ + self_url_host = OneLogin_Saml2_Utils.get_self_url_host(request_data) + + request_uri = '' + if 'request_uri' in request_data: + request_uri = request_data['request_uri'] + if not request_uri.startswith('/'): + match = re.search('^https?://[^/]*(/.*)', request_uri) + if match is not None: + request_uri = match.groups()[0] + + return self_url_host + request_uri + + @staticmethod + def generate_unique_id(): + """ + Generates an unique string (used for example as ID for assertions). + + :return: A unique string + :rtype: string + """ + return 'ONELOGIN_%s' % sha1(uuid4().hex).hexdigest() + + @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. + + :param time: The time we should convert (DateTime). + :type: string + + :return: SAML2 timestamp. + :rtype: string + """ + data = datetime.utcfromtimestamp(float(time)) + return data.strftime(OneLogin_Saml2_Utils.TIME_FORMAT) + + @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. + + :param time: The time we should convert (SAML Timestamp). + :type: string + + :return: Converted to a unix timestamp. + :rtype: int + """ + try: + data = datetime.strptime(timestr, OneLogin_Saml2_Utils.TIME_FORMAT) + except ValueError: + try: + data = datetime.strptime(timestr, OneLogin_Saml2_Utils.TIME_FORMAT_2) + except ValueError: + elem = OneLogin_Saml2_Utils.TIME_FORMAT_WITH_FRAGMENT.match(timestr) + if not elem: + raise Exception("time data %s does not match format %s" % (timestr, r'yyyy-mm-ddThh:mm:ss(\.s+)?Z')) + data = datetime.strptime(elem.groups()[0] + "Z", OneLogin_Saml2_Utils.TIME_FORMAT) + + return calendar.timegm(data.utctimetuple()) + + @staticmethod + def now(): + """ + :return: unix timestamp of actual time. + :rtype: int + """ + return calendar.timegm(datetime.utcnow().utctimetuple()) + + @staticmethod + def parse_duration(duration, timestamp=None): + """ + Interprets a ISO8601 duration value relative to a given timestamp. + + :param duration: The duration, as a string. + :type: string + + :param timestamp: The unix timestamp we should apply the duration to. + Optional, default to the current time. + :type: string + + :return: The new timestamp, after the duration is applied. + :rtype: int + """ + assert isinstance(duration, basestring) + assert timestamp is None or isinstance(timestamp, int) + + timedelta = duration_parser(duration) + if timestamp is None: + data = datetime.utcnow() + timedelta + else: + data = datetime.utcfromtimestamp(timestamp) + timedelta + return calendar.timegm(data.utctimetuple()) + + @staticmethod + def get_expire_time(cache_duration=None, valid_until=None): + """ + Compares 2 dates and returns the earliest. + + :param cache_duration: The duration, as a string. + :type: string + + :param valid_until: The valid until date, as a string or as a timestamp + :type: string + + :return: The expiration time. + :rtype: int + """ + expire_time = None + + if cache_duration is not None: + expire_time = OneLogin_Saml2_Utils.parse_duration(cache_duration) + + if valid_until is not None: + if isinstance(valid_until, int): + valid_until_time = valid_until + else: + valid_until_time = OneLogin_Saml2_Utils.parse_SAML_to_time(valid_until) + if expire_time is None or expire_time > valid_until_time: + expire_time = valid_until_time + + if expire_time is not None: + return '%d' % expire_time + return None + + @staticmethod + def query(dom, query, context=None, tagid=None): + """ + Extracts nodes that match the query from the Element + + :param dom: The root of the lxml objet + :type: Element + + :param query: Xpath Expresion + :type: string + + :param context: Context Node + :type: DOMElement + + :param tagid: Tag ID + :type: string + + :returns: The queried nodes + :rtype: list + """ + if context is None: + source = dom + else: + source = context + + if tagid is None: + return source.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP) + else: + return source.xpath(query, tagid=tagid, namespaces=OneLogin_Saml2_Constants.NSMAP) + + @staticmethod + def delete_local_session(callback=None): + """ + Deletes the local session. + """ + + if callback is not None: + callback() + + @staticmethod + def calculate_x509_fingerprint(x509_cert, alg='sha1'): + """ + Calculates the fingerprint of a formatted x509cert. + + :param x509_cert: x509 cert formatted + :type: string + + :param alg: The algorithm to build the fingerprint + :type: string + + :returns: fingerprint + :rtype: string + """ + assert isinstance(x509_cert, basestring) + + lines = x509_cert.split('\n') + data = '' + inData = False + + for line in lines: + # Remove '\r' from end of line if present. + line = line.rstrip() + 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': + fingerprint = sha512(decoded_data) + elif alg == 'sha384': + fingerprint = sha384(decoded_data) + elif alg == 'sha256': + fingerprint = sha256(decoded_data) + else: + fingerprint = sha1(decoded_data) + + return fingerprint.hexdigest().lower() + + @staticmethod + def format_finger_print(fingerprint): + """ + Formats a fingerprint. + + :param fingerprint: fingerprint + :type: string + + :returns: Formatted fingerprint + :rtype: string + """ + formated_fingerprint = fingerprint.replace(':', '') + return formated_fingerprint.lower() + + @staticmethod + def generate_name_id(value, sp_nq, sp_format=None, cert=None, debug=False, nq=None): + """ + Generates a nameID. + + :param value: fingerprint + :type: string + + :param sp_nq: SP Name Qualifier + :type: string + + :param sp_format: SP Format + :type: string + + :param cert: IdP Public Cert to encrypt the nameID + :type: string + + :param debug: Activate the xmlsec debug + :type: bool + + :param nq: IDP Name Qualifier + :type: string + + :returns: DOMElement | XMLSec nameID + :rtype: string + """ + doc = Document() + name_id_container = doc.createElementNS(OneLogin_Saml2_Constants.NS_SAML, 'container') + name_id_container.setAttribute("xmlns:saml", OneLogin_Saml2_Constants.NS_SAML) + + name_id = doc.createElement('saml:NameID') + 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, forbid_dtd=True) + + error_callback_method = None + if debug: + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) + + # Load the public cert + mngr = xmlsec.KeysMngr() + file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) + key_data = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None) + key_data.name = basename(file_cert.name) + mngr.addKey(key_data) + file_cert.close() + + # Prepare for encryption + enc_data = EncData(xmlsec.TransformAes128Cbc, type=xmlsec.TypeEncElement) + enc_data.ensureCipherValue() + key_info = enc_data.ensureKeyInfo() + # enc_key = key_info.addEncryptedKey(xmlsec.TransformRsaPkcs1) + enc_key = key_info.addEncryptedKey(xmlsec.TransformRsaOaep) + enc_key.ensureCipherValue() + + # Encrypt! + enc_ctx = xmlsec.EncCtx(mngr) + enc_ctx.encKey = xmlsec.Key.generate(xmlsec.KeyDataAes, 128, xmlsec.KeyDataTypeSession) + + edata = enc_ctx.encryptXml(enc_data, elem[0]) + + newdoc = parseString(tostring(edata, encoding='unicode').encode('utf-8'), forbid_dtd=True, forbid_entities=True, forbid_external=True) + + if newdoc.hasChildNodes(): + child = newdoc.firstChild + child.removeAttribute('xmlns') + child.removeAttribute('xmlns:saml') + child.setAttribute('xmlns:xenc', OneLogin_Saml2_Constants.NS_XENC) + child.setAttribute('xmlns:dsig', OneLogin_Saml2_Constants.NS_DS) + + nodes = newdoc.getElementsByTagName("*") + for node in nodes: + if node.tagName == 'ns0:KeyInfo': + node.tagName = 'dsig:KeyInfo' + node.removeAttribute('xmlns:ns0') + node.setAttribute('xmlns:dsig', OneLogin_Saml2_Constants.NS_DS) + else: + node.tagName = 'xenc:' + node.tagName + + encrypted_id = newdoc.createElement('saml:EncryptedID') + encrypted_data = newdoc.replaceChild(encrypted_id, newdoc.firstChild) + encrypted_id.appendChild(encrypted_data) + return newdoc.saveXML(encrypted_id) + else: + return doc.saveXML(name_id) + + @staticmethod + def get_status(dom): + """ + Gets Status from a Response. + + :param dom: The Response as XML + :type: Document + + :returns: The Status, an array with the code and a message. + :rtype: dict + """ + status = {} + + status_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status') + 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) != 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) == 1: + status['msg'] = subcode_entry[0].values()[0] + 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, inplace=False): + """ + Decrypts an encrypted element. + + :param encrypted_data: The encrypted data. + :type: lxml.etree.Element | DOMElement | basestring + + :param key: The key. + :type: string + + :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()), forbid_dtd=True) + elif isinstance(encrypted_data, basestring): + 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: + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) + + mngr = xmlsec.KeysMngr() + + key = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None) + mngr.addKey(key) + enc_ctx = xmlsec.EncCtx(mngr) + + return enc_ctx.decrypt(encrypted_data) + + @staticmethod + def write_temp_file(content): + """ + Writes some content into a temporary file and returns it. + + :param content: The file content + :type: string + + :returns: The temporary file + :rtype: file-like object + """ + f_temp = NamedTemporaryFile(delete=True) + f_temp.file.write(content) + f_temp.file.flush() + return f_temp + + @staticmethod + def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA256, digest_algorithm=OneLogin_Saml2_Constants.SHA256): + """ + Adds signature key and senders certificate to an element (Message or + Assertion). + + :param xml: The element we should sign + :type: string | Document + + :param key: The private key + :type: string + + :param cert: The public + :type: string + + :param debug: Activate the xmlsec debug + :type: bool + + :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') + elif isinstance(xml, etree._Element): + elem = xml + elif isinstance(xml, Document): + xml = xml.toxml() + elem = fromstring(xml.encode('utf-8'), 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(xml.encode('utf-8'), forbid_dtd=True) + elif isinstance(xml, basestring): + elem = fromstring(xml.encode('utf-8'), 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) + + 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(sign_algorithm, xmlsec.TransformRsaSha1) + + 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: + 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.addTransform(xmlsec.TransformEnveloped) + ref.addTransform(xmlsec.TransformExclC14N) + + key_info = signature.ensureKeyInfo() + key_info.addX509Data() + + dsig_ctx = xmlsec.DSigCtx() + sign_key = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None) + + file_cert = OneLogin_Saml2_Utils.write_temp_file(cert) + sign_key.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem) + file_cert.close() + + dsig_ctx.signKey = sign_key + dsig_ctx.sign(signature) + + return tostring(elem, encoding='unicode').encode('utf-8') + + @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). + + :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) + + 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 + @return_false_on_exception + def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False): + """ + Validates a signature of a EntityDescriptor. + + :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 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_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') + + error_callback_method = None + if debug: + error_callback_method = print_xmlsec_errors + xmlsec.set_error_callback(error_callback_method) + + xmlsec.addIDs(elem, ["ID"]) + + signature_nodes = OneLogin_Saml2_Utils.query(elem, '/md:EntitiesDescriptor/ds:Signature') + + if len(signature_nodes) == 0: + signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/ds:Signature') + + 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 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.') + + @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. + + :param signature_node: The signature node + :type: Node + + :param xml: The element we should validate + :type: Document + + :param cert: The public 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 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 binary data (Used to validate GET Signature). + + :param signed_query: The element we should validate + :type: string + + :param signature: The signature that will be validate + :type: string + + :param cert: The public cert + :type: string + + :param algorithm: Signature algorithm + :type: string + + :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) + + 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() + + # 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 + + @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 + + @staticmethod + def normalize_url(url): + """ + Returns normalized URL for comparison. + This method converts the netloc to lowercase, as it should be case-insensitive (per RFC 4343, RFC 7617) + If standardization fails, the original URL is returned + Python documentation indicates that URL split also normalizes query strings if empty query fields are present + + :param url: URL + :type url: String + + :returns: A normalized URL, or the given URL string if parsing fails + :rtype: String + """ + try: + scheme, netloc, path, query, fragment = urlsplit(url) + normalized_url = urlunsplit((scheme.lower(), netloc.lower(), path, query, fragment)) + return normalized_url + except Exception: + return url diff --git a/src/onelogin/saml2/xmlparser.py b/src/onelogin/saml2/xmlparser.py new file mode 100644 index 00000000..ce51cd04 --- /dev/null +++ b/src/onelogin/saml2/xmlparser.py @@ -0,0 +1,145 @@ +# Based on the lxml example from defusedxml +# +# Copyright (c) 2013 by Christian Heimes +# Licensed to PSF under a Contributor Agreement. +# See https://www.python.org/psf/license for licensing details. +"""lxml.etree protection""" + +from __future__ import print_function, absolute_import + +import threading + +from lxml import etree as _etree + +from defusedxml.lxml import DTDForbidden, EntitiesForbidden, NotSupportedError + +LXML3 = _etree.LXML_VERSION[0] >= 3 + +__origin__ = "lxml.etree" + +tostring = _etree.tostring + + +class RestrictedElement(_etree.ElementBase): + """A restricted Element class that filters out instances of some classes + """ + + __slots__ = () + blacklist = (_etree._Entity, _etree._ProcessingInstruction, _etree._Comment) + + def _filter(self, iterator): + blacklist = self.blacklist + for child in iterator: + if isinstance(child, blacklist): + continue + yield child + + def __iter__(self): + iterator = super(RestrictedElement, self).__iter__() + return self._filter(iterator) + + def iterchildren(self, tag=None, reversed=False): + iterator = super(RestrictedElement, self).iterchildren(tag=tag, reversed=reversed) + return self._filter(iterator) + + def iter(self, tag=None, *tags): + iterator = super(RestrictedElement, self).iter(tag=tag, *tags) + return self._filter(iterator) + + def iterdescendants(self, tag=None, *tags): + iterator = super(RestrictedElement, self).iterdescendants(tag=tag, *tags) + return self._filter(iterator) + + def itersiblings(self, tag=None, preceding=False): + iterator = super(RestrictedElement, self).itersiblings(tag=tag, preceding=preceding) + return self._filter(iterator) + + def getchildren(self): + iterator = super(RestrictedElement, self).__iter__() + return list(self._filter(iterator)) + + def getiterator(self, tag=None): + iterator = super(RestrictedElement, self).getiterator(tag) + return self._filter(iterator) + + +class GlobalParserTLS(threading.local): + """Thread local context for custom parser instances + """ + + parser_config = { + "resolve_entities": False, + 'remove_comments': True, + 'no_network': True, + 'remove_pis': True, + 'huge_tree': False + } + + element_class = RestrictedElement + + def createDefaultParser(self): + parser = _etree.XMLParser(**self.parser_config) + element_class = self.element_class + if self.element_class is not None: + lookup = _etree.ElementDefaultClassLookup(element=element_class) + parser.set_element_class_lookup(lookup) + return parser + + def setDefaultParser(self, parser): + self._default_parser = parser + + def getDefaultParser(self): + parser = getattr(self, "_default_parser", None) + if parser is None: + parser = self.createDefaultParser() + self.setDefaultParser(parser) + return parser + + +_parser_tls = GlobalParserTLS() +getDefaultParser = _parser_tls.getDefaultParser + + +def check_docinfo(elementtree, forbid_dtd=False, forbid_entities=True): + """Check docinfo of an element tree for DTD and entity declarations + The check for entity declarations needs lxml 3 or newer. lxml 2.x does + not support dtd.iterentities(). + """ + docinfo = elementtree.docinfo + if docinfo.doctype: + if forbid_dtd: + raise DTDForbidden(docinfo.doctype, docinfo.system_url, docinfo.public_id) + if forbid_entities and not LXML3: + # lxml < 3 has no iterentities() + raise NotSupportedError("Unable to check for entity declarations " "in lxml 2.x") + + if forbid_entities: + for dtd in docinfo.internalDTD, docinfo.externalDTD: + if dtd is None: + continue + for entity in dtd.iterentities(): + raise EntitiesForbidden(entity.name, entity.content, None, None, None, None) + + +def parse(source, parser=None, base_url=None, forbid_dtd=True, forbid_entities=True): + if parser is None: + parser = getDefaultParser() + elementtree = _etree.parse(source, parser, base_url=base_url) + check_docinfo(elementtree, forbid_dtd, forbid_entities) + return elementtree + + +def fromstring(text, parser=None, base_url=None, forbid_dtd=True, forbid_entities=True): + if parser is None: + parser = getDefaultParser() + rootelement = _etree.fromstring(text, parser, base_url=base_url) + elementtree = rootelement.getroottree() + check_docinfo(elementtree, forbid_dtd, forbid_entities) + return rootelement + + +XML = fromstring + + +def iterparse(*args, **kwargs): + raise NotSupportedError("iterparse not available") diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/certs/certificate1 b/tests/certs/certificate1 new file mode 100644 index 00000000..df72def3 --- /dev/null +++ b/tests/certs/certificate1 @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBrTCCAaGgAwIBAgIBATADBgEAMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApD +YWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9uZUxv +Z2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMB4XDTEwMTAxMTIxMTUxMloX +DTE1MTAxMTIxMTUxMlowZzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3Ju +aWExFTATBgNVBAcMDFNhbnRhIE1vbmljYTERMA8GA1UECgwIT25lTG9naW4xGTAX +BgNVBAMMEGFwcC5vbmVsb2dpbi5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ +AoGBAMPmjfjy7L35oDpeBXBoRVCgktPkLno9DOEWB7MgYMMVKs2B6ymWQLEWrDug +MK1hkzWFhIb5fqWLGbWy0J0veGR9/gHOQG+rD/I36xAXnkdiXXhzoiAG/zQxM0ed +MOUf40n314FC8moErcUg6QabttzesO59HFz6shPuxcWaVAgxAgMBAAEwAwYBAAMB +AA== +-----END CERTIFICATE----- diff --git a/tests/coverage.rc b/tests/coverage.rc new file mode 100644 index 00000000..855ec7f9 --- /dev/null +++ b/tests/coverage.rc @@ -0,0 +1,30 @@ +[run] +branch = True + +omit = + +[paths] +source = src/onelogin/saml2 + +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + + + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + if self\.debug + if debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: + +ignore_errors = True diff --git a/tests/data/customPath/advanced_settings.json b/tests/data/customPath/advanced_settings.json new file mode 100644 index 00000000..335a797b --- /dev/null +++ b/tests/data/customPath/advanced_settings.json @@ -0,0 +1,24 @@ +{ + "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/data/customPath/certs/metadata.crt b/tests/data/customPath/certs/metadata.crt new file mode 100755 index 00000000..b4147e51 --- /dev/null +++ b/tests/data/customPath/certs/metadata.crt @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMC +Tk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYD +VQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG +9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4 +MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xi +ZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2Zl +aWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5v +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LO +NoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHIS +KOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d +1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8 +BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7n +bK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2Qar +Q4/67OZfHd7R+POBXhophSMv1ZOo +-----END CERTIFICATE----- diff --git a/tests/data/customPath/certs/metadata.key b/tests/data/customPath/certs/metadata.key new file mode 100755 index 00000000..673196b1 --- /dev/null +++ b/tests/data/customPath/certs/metadata.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9 +IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+ +PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQAB +AoGAD4/Z4LWVWV6D1qMIp1Gzr0ZmdWTE1SPdZ7Ej8glGnCzPdguCPuzbhGXmIg0V +J5D+02wsqws1zd48JSMXXM8zkYZVwQYIPUsNn5FetQpwxDIMPmhHg+QNBgwOnk8J +K2sIjjLPL7qY7Itv7LT7Gvm5qSOkZ33RCgXcgz+okEIQMYkCQQDzbTOyDL0c5WQV +6A2k06T/azdhUdGXF9C0+WkWSfNaovmTgRXh1G+jMlr82Snz4p4/STt7P/XtyWzF +3pkVgZr3AkEA7nPjXwHlttNEMo6AtxHd47nizK2NUN803ElIUT8P9KSCoERmSXq6 +6PDekGNic4ldpsSvOeYCk8MAYoDBy9kvVwJBAMLgX4xg6lzhv7hR5+pWjTb1rIY6 +rCHbrPfU264+UZXz9v2BT/VUznLF81WMvStD9xAPHpFS6R0OLghSZhdzhI0CQQDL +8Duvfxzrn4b9QlmduV8wLERoT6rEVxKLsPVz316TGrxJvBZLk/cV0SRZE1cZf4uk +XSWMfEcJ/0Zt+LdG1CqjAkEAqwLSglJ9Dy3HpgMz4vAAyZWzAxvyA1zW0no9GOLc +PQnYaNUN/Fy2SYtETXTb0CQ9X1rt8ffkFP7ya+5TC83aMg== +-----END RSA PRIVATE KEY----- diff --git a/tests/data/customPath/certs/sp.crt b/tests/data/customPath/certs/sp.crt new file mode 100755 index 00000000..b4147e51 --- /dev/null +++ b/tests/data/customPath/certs/sp.crt @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMC +Tk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYD +VQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG +9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4 +MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xi +ZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2Zl +aWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5v +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LO +NoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHIS +KOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d +1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8 +BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7n +bK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2Qar +Q4/67OZfHd7R+POBXhophSMv1ZOo +-----END CERTIFICATE----- diff --git a/tests/data/customPath/certs/sp.key b/tests/data/customPath/certs/sp.key new file mode 100755 index 00000000..673196b1 --- /dev/null +++ b/tests/data/customPath/certs/sp.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9 +IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+ +PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQAB +AoGAD4/Z4LWVWV6D1qMIp1Gzr0ZmdWTE1SPdZ7Ej8glGnCzPdguCPuzbhGXmIg0V +J5D+02wsqws1zd48JSMXXM8zkYZVwQYIPUsNn5FetQpwxDIMPmhHg+QNBgwOnk8J +K2sIjjLPL7qY7Itv7LT7Gvm5qSOkZ33RCgXcgz+okEIQMYkCQQDzbTOyDL0c5WQV +6A2k06T/azdhUdGXF9C0+WkWSfNaovmTgRXh1G+jMlr82Snz4p4/STt7P/XtyWzF +3pkVgZr3AkEA7nPjXwHlttNEMo6AtxHd47nizK2NUN803ElIUT8P9KSCoERmSXq6 +6PDekGNic4ldpsSvOeYCk8MAYoDBy9kvVwJBAMLgX4xg6lzhv7hR5+pWjTb1rIY6 +rCHbrPfU264+UZXz9v2BT/VUznLF81WMvStD9xAPHpFS6R0OLghSZhdzhI0CQQDL +8Duvfxzrn4b9QlmduV8wLERoT6rEVxKLsPVz316TGrxJvBZLk/cV0SRZE1cZf4uk +XSWMfEcJ/0Zt+LdG1CqjAkEAqwLSglJ9Dy3HpgMz4vAAyZWzAxvyA1zW0no9GOLc +PQnYaNUN/Fy2SYtETXTb0CQ9X1rt8ffkFP7ya+5TC83aMg== +-----END RSA PRIVATE KEY----- diff --git a/tests/data/customPath/settings.json b/tests/data/customPath/settings.json new file mode 120000 index 00000000..0d813456 --- /dev/null +++ b/tests/data/customPath/settings.json @@ -0,0 +1 @@ +../../settings/settings1.json \ No newline at end of file diff --git a/tests/data/logout_requests/invalids/invalid_issuer.xml b/tests/data/logout_requests/invalids/invalid_issuer.xml new file mode 100644 index 00000000..4b7714d1 --- /dev/null +++ b/tests/data/logout_requests/invalids/invalid_issuer.xml @@ -0,0 +1,14 @@ + + + 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 new file mode 100644 index 00000000..8af7cbb1 --- /dev/null +++ b/tests/data/logout_requests/invalids/invalid_issuer.xml.base64 @@ -0,0 +1 @@ +jZPLboMwEEX3kfIPiH3A5pEGKyGKlLZCSkPbVF10U7lmaJDApoyp8vk1pI8sQhWvrPH43jP2zHx5qErrExoslFzY1CH2Mh6P5sirsmYb9a5a/QgfLaC2TKZE1p8s7LaRTHEskEleATIt2G51t2GeQ1jdKK2EKu3xyDq3/oT+1+GI0GgDNiSUrBd2ur3epLfJ9tWj4SwQIsu54P6U0yDkEEUkCLxMZNH0LYcpoSGFIbHnn0cw1oOGiC0kEjWX2iQS6k+oN6HkiQTMj5hPX4Zurs0LFpLr3mGvdc1cF3Wb545QlQsyq1UhNZ7ssESn3tdDglulU5k2q1xD06GE/oSEF6HEx3j/x6yvqIk7IjRIcOBVXYKzh7JUPRoXAtDQmOS5e3rlVGVr/i5ZW7v7bvPQ8rLIiw7rItlznDeqqbgebg/q0D5SZJO8T2WtxBpE55udE4x/+4SCaQlBvQhoPvM8MSPRlMxMw1DiixDAE1eCAxXf1R5L62bCPTMU8Rc= diff --git a/tests/data/logout_requests/invalids/no_nameId.xml b/tests/data/logout_requests/invalids/no_nameId.xml new file mode 100644 index 00000000..f1d02c83 --- /dev/null +++ b/tests/data/logout_requests/invalids/no_nameId.xml @@ -0,0 +1,11 @@ + + + https://example.hello.com/access/saml + diff --git a/tests/data/logout_requests/invalids/not_after_failed.xml b/tests/data/logout_requests/invalids/not_after_failed.xml new file mode 100644 index 00000000..1f825036 --- /dev/null +++ b/tests/data/logout_requests/invalids/not_after_failed.xml @@ -0,0 +1,14 @@ + + + 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 new file mode 100644 index 00000000..8ba510d0 --- /dev/null +++ b/tests/data/logout_requests/invalids/not_after_failed.xml.base64 @@ -0,0 +1 @@ +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 new file mode 100644 index 00000000..10904a28 --- /dev/null +++ b/tests/data/logout_requests/logout_request.xml @@ -0,0 +1,13 @@ + + + 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 new file mode 100644 index 00000000..870a6383 --- /dev/null +++ b/tests/data/logout_requests/logout_request.xml.base64 @@ -0,0 +1 @@ +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 new file mode 100644 index 00000000..894de1ec --- /dev/null +++ b/tests/data/logout_requests/logout_request_deflated.xml.base64 @@ -0,0 +1 @@ +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_encrypted_nameid.xml b/tests/data/logout_requests/logout_request_encrypted_nameid.xml new file mode 100644 index 00000000..72951344 --- /dev/null +++ b/tests/data/logout_requests/logout_request_encrypted_nameid.xml @@ -0,0 +1,28 @@ + + http://stuff.com/endpoints/metadata.php + + + + + + + + Dqhp3WPaoV7i2ry6ZywsfTa0ctWf9a37+BTPLlIycvpRqSObwvBoEGCezXpxUaMTNcFp6p7qSzyGpAbf/h3Qowt1gQqhTn04ofGOwXoMD21Pr5DCbjs9roU005DExjLCFKbf5P3de8QZh5Lcz3dVBU3y1Kc6wku/RuD/HfYBy7g= + + + + + h1PDxhhKkO/IyIb4G5JFoHnBhaNj7n8fh8jqjHhFkRZtwpS/9HXENF3R+1F6XeHs3YfhbLJDHJroQPJwh4PjRUQnPYHdH5YbriR59VI0H9DoMWaJvd9wEMnH6FniXak9npP8BFPhV0uW1r3ynZ8mfn6DkBzC/IORRsmDrQv3IAcp+3J/rRui51ccH60xkff4dWsBYHBIHspuDNz8EU02Ho+hSayN3x1e4WuSdIOF0SRkh4r1mQ00/q2PHExdxmHr4wLQe8DkBFmdt+FxuEpjOIzyWgdJ1TbvgfjBe7a10PiiPO3inMy29ObLo0l4fkHH + + + + diff --git a/tests/data/logout_requests/logout_request_with_sessionindex.xml b/tests/data/logout_requests/logout_request_with_sessionindex.xml new file mode 100644 index 00000000..5dbc3c32 --- /dev/null +++ b/tests/data/logout_requests/logout_request_with_sessionindex.xml @@ -0,0 +1,14 @@ + + + http://idp.example.com/ + ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c + _ac72a76526cb6ca19f8438e73879a0e6c8ae5131 + diff --git a/tests/data/logout_responses/invalids/no_status.xml.base64 b/tests/data/logout_responses/invalids/no_status.xml.base64 new file mode 100644 index 00000000..a5342762 --- /dev/null +++ b/tests/data/logout_responses/invalids/no_status.xml.base64 @@ -0,0 +1 @@ +fZLBasMwDIbvfYqSexM7TkxtmsKgYxS6Fraywy7FseU1kNgmcqCPvy7bWBnLdLJk+ft/Ia9QdW2QO//mh/gEGLxDmF+61qEcr6pk6J30ChuUTnWAMmr5fPe4k3lKZOh99Nq3yWz+Z/yA/ucoROhj490UaLupkpMVAJzWRpjacsYJt0oJBaympCyWpqwZL7mtl6WYwrxAj1eVKrmKTkohDrB1GJWL10ZC2YLmC0qOpJBMSEZfp15uAGPjVBwVzjEGmWUYB2tT7bsMnAm+cRFvTthiGs5h0or7XsnRV8lhf787PGz3p5yWy0JrY5VWjCtalAqEIEWRG20Ery1wQksKU9j1WF99rEWO0/brL7eNCSlcVBdaGD2vstum2Wf6+7us3wE= diff --git a/tests/data/logout_responses/invalids/status_code_responder.xml.base64 b/tests/data/logout_responses/invalids/status_code_responder.xml.base64 new file mode 100644 index 00000000..48f3c51c --- /dev/null +++ b/tests/data/logout_responses/invalids/status_code_responder.xml.base64 @@ -0,0 +1 @@ +fZJda8IwFIbv9yuk99qkH8EEWxhzDMEpTPFiN5I2J7PQJqEnBX/+XKdTYTVXyfl4n/dwMkPZ1E4s7Zft/AegswZhdGxqg6JPZUHXGmElViiMbACFL8Xm+X0pogkRrrXelrYOnkb/nqvQYx2JCK2vrBkSWsyzYK85AKOF4qrQLGaEaSm5hLigJE2mKi1iljJdTFM+JLODFk+ULDhBB1GIHSwMemn8qZDQeEyjMSVbkoiYi5h+DnXOAX1lpO8JB++dCEP0ndaT0jYhGOVsZTze3LDGiTu4QSvmspKtzYL16nW5flus9hFNp0lZKi1LGTNJk1QC5yRJIlUqzgoNjNCUwpBs3sdnP2sR/bRtfnZbKTeBo2xcDb3nWXhbdG1zYuOl7zD/I9yFX6yC0U7WHTxeO/bV4ndIBW0QnhnhPeTyvv+m+Tc= diff --git a/tests/data/logout_responses/logout_response.xml b/tests/data/logout_responses/logout_response.xml new file mode 100644 index 00000000..388a7f38 --- /dev/null +++ b/tests/data/logout_responses/logout_response.xml @@ -0,0 +1,13 @@ + + http://idp.example.com/ + + + + diff --git a/tests/data/logout_responses/logout_response.xml.base64 b/tests/data/logout_responses/logout_response.xml.base64 new file mode 100644 index 00000000..89bb74da --- /dev/null +++ b/tests/data/logout_responses/logout_response.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOkxvZ291dFJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiDQogICAgICAgICAgICAgICAgICAgICAgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiINCiAgICAgICAgICAgICAgICAgICAgICBJRD0iX2Y5ZWU2MWJkOWRiZjYzNjA2ZmFhOWFlM2IxMDU0OGQ1YjM2NTZmYjg1OSINCiAgICAgICAgICAgICAgICAgICAgICBWZXJzaW9uPSIyLjAiDQogICAgICAgICAgICAgICAgICAgICAgSXNzdWVJbnN0YW50PSIyMDEzLTEyLTEwVDA0OjM5OjMxWiINCiAgICAgICAgICAgICAgICAgICAgICBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL3Nscy5waHAiDQogICAgICAgICAgICAgICAgICAgICAgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl8yMTU4NGNjZGZhY2EzNmExNDVhZTk5MDQ0MmRjZDk2YmZlNjAxNTFlIg0KICAgICAgICAgICAgICAgICAgICAgID4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPg0KICAgIDxzYW1scDpTdGF0dXM+DQogICAgICAgIDxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiIC8+DQogICAgPC9zYW1scDpTdGF0dXM+DQo8L3NhbWxwOkxvZ291dFJlc3BvbnNlPg== diff --git a/tests/data/logout_responses/logout_response_deflated.xml.base64 b/tests/data/logout_responses/logout_response_deflated.xml.base64 new file mode 100644 index 00000000..e874f02b --- /dev/null +++ b/tests/data/logout_responses/logout_response_deflated.xml.base64 @@ -0,0 +1 @@ +fZJfa8IwFMXf/RSl77ZJ/wQbbGHMMQSnMMWHvUia3M5Cm4TeFPz4c0Wnwmqekpt7z++EkzmKtrF8Zb5N7z4BrdEI3qltNPLhKvf7TnMjsEauRQvIneTbl48VjwLCbWeckabxJ96/6yb0XEcgQudqo8eElovcP1QZAKOlylRZsZgRVgmRCYhLStJkptIyZimrylmajcnsocMzJffP0FEUYg9LjU5od24kNJ7SaErJjiQ8znhMv8YmF4Cu1sINhKNzlochur6qAmnaELSyptYO73bYYGCPdtSKvkayM7m/Wb+tNu/L9SGi6SyRUlVCipgJmqQCsowkSaSkylhZASM0pTAmWwz1+W8sfHhtV1zc1soGcBKtbWDwPA/vm25jlm+dcD0Wf4SH8qtR4O1F08Pz2HHo5tteSkD0vfCCCB8Z1/PjLy0mPw== diff --git a/tests/data/metadata/entities_metadata.xml b/tests/data/metadata/entities_metadata.xml new file mode 100644 index 00000000..4d5da11d --- /dev/null +++ b/tests/data/metadata/entities_metadata.xml @@ -0,0 +1,44 @@ + + + + + + + MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m + + + + + + + MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m + + + + + + + + + + + + + + MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m + + + + + + + MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m + + + + + + + + + \ No newline at end of file diff --git a/tests/data/metadata/expired_metadata_settings1.xml b/tests/data/metadata/expired_metadata_settings1.xml new file mode 100644 index 00000000..9af134d8 --- /dev/null +++ b/tests/data/metadata/expired_metadata_settings1.xml @@ -0,0 +1,9 @@ + + + + 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:1.1:nameid-format:unspecified + + + diff --git a/tests/data/metadata/idp_metadata.xml b/tests/data/metadata/idp_metadata.xml new file mode 100644 index 00000000..146428c1 --- /dev/null +++ b/tests/data/metadata/idp_metadata.xml @@ -0,0 +1,42 @@ + + + + + + + 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_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 new file mode 100644 index 00000000..9d100de4 --- /dev/null +++ b/tests/data/metadata/metadata_bad_order_settings1.xml @@ -0,0 +1,14 @@ + + + + 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 new file mode 100644 index 00000000..e470c778 --- /dev/null +++ b/tests/data/metadata/metadata_settings1.xml @@ -0,0 +1,14 @@ + + + + + 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 new file mode 100644 index 00000000..92c42ba8 --- /dev/null +++ b/tests/data/metadata/no_expiration_mark_metadata.xml @@ -0,0 +1,12 @@ + + + + + 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 new file mode 100644 index 00000000..b773de92 --- /dev/null +++ b/tests/data/metadata/noentity_metadata_settings1.xml @@ -0,0 +1,9 @@ + + + + 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:1.1:nameid-format:unspecified + + + diff --git a/tests/data/metadata/signed_metadata_settings1.xml b/tests/data/metadata/signed_metadata_settings1.xml new file mode 100644 index 00000000..9a37fe4e --- /dev/null +++ b/tests/data/metadata/signed_metadata_settings1.xml @@ -0,0 +1,39 @@ + + + + + +FoWTQxwj75/mQK600oN7ZobfqU=lm/ZJWEoAOeBD+bqimMLJEECySqYSRkcJ5KVU8mKORh044go2YSN5MLyJe9772506FsWf9UxCMV+EhBl7wj4k1E1/SAXQ3GhdWr8qTTBZ4QiVSeB/ReqFQaD/W0vlrYLwu1f+hMoEGdalqsFOetjBSsuiRi3n6qHY2x3ePIXCXY= +MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo + + + + + MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo + + + + + + + MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo + + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress + + + + sp_test + SP test + http://sp.example.com + + + technical_name + technical@example.com + + + support_name + support@example.com + + diff --git a/tests/data/metadata/signed_metadata_settings2.xml b/tests/data/metadata/signed_metadata_settings2.xml new file mode 100644 index 00000000..b3e60fc1 --- /dev/null +++ b/tests/data/metadata/signed_metadata_settings2.xml @@ -0,0 +1,36 @@ + + q+ufWp5Xleex2jt5NJHoLUORbyc=WIL36kstHjnFnHRc78dVDVL2htsi4GVOOUDXby7ht77aJbe4nknoES6eqvMgiznL +QX3nIeJBkfLtnl2i3dh7mk1RBsFS7G3R00u7ADW+8GqkLMn/Tl7+qaRi6/iiCScs +l/TrnoiY/emDNREboGVgts99NHSF53COW5tSnUwqNAg= +MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czET +MBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYD +VQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIy +NDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UE +CgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJ +KoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BF +rSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PP +J2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHC +pS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAf +BgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0G +CSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZ +E+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kY +vrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m +MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8mMIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m + urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified + + + + + sp_test + SP test + http://sp.example.com + + + technical_name + technical@example.com + + + support_name + support@example.com + + \ No newline at end of file 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 new file mode 100644 index 00000000..691824da --- /dev/null +++ b/tests/data/metadata/unparsed_metadata.xml @@ -0,0 +1,14 @@ + + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + + + diff --git a/tests/data/misc/sp2.key b/tests/data/misc/sp2.key new file mode 100644 index 00000000..f8fbfa9e --- /dev/null +++ b/tests/data/misc/sp2.key @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAOK9uFHs/nXrH9Lc +GorG6lB7Qs42iWK6mIE56wI7dIdsOuXf6r0ht+d+YTTis24xw+wjEHXrVN0Okh6w +sKftzxo8chIo60+UB5NlKdvxAC7tpGNmrf49us/m5bdNx8IY+0pPK0c6B786Uluj +Tvx1WFdDXh3UQPBclbWtFe5S3gLxAgMBAAECgYAPj9ngtZVZXoPWowinUbOvRmZ1 +ZMTVI91nsSPyCUacLM92C4I+7NuEZeYiDRUnkP7TbCyrCzXN3jwlIxdczzORhlXB +Bgg9Sw2fkV61CnDEMgw+aEeD5A0GDA6eTwkrawiOMs8vupjsi2/stPsa+bmpI6Rn +fdEKBdyDP6iQQhAxiQJBAPNtM7IMvRzlZBXoDaTTpP9rN2FR0ZcX0LT5aRZJ81qi ++ZOBFeHUb6MyWvzZKfPinj9JO3s/9e3JbMXemRWBmvcCQQDuc+NfAeW200QyjoC3 +Ed3jueLMrY1Q3zTcSUhRPw/0pIKgRGZJerro8N6QY2JziV2mxK855gKTwwBigMHL +2S9XAkEAwuBfjGDqXOG/uFHn6laNNvWshjqsIdus99Tbrj5RlfP2/YFP9VTOcsXz +VYy9K0P3EA8ekVLpHQ4uCFJmF3OEjQJBAMvwO69/HOufhv1CWZ25XzAsRGhPqsRX +Eouw9XPfXpMavEm8FkuT9xXRJFkTVxl/i6RdJYx8Rwn/Rm34t0bUKqMCQQCrAtKC +Un0PLcemAzPi8ADJlbMDG/IDXNbSej0Y4tw9Cdho1Q38XLZJi0RNdNvQJD1fWu3x +9+QU/vJr7lMLzdoy +-----END PRIVATE KEY----- diff --git a/tests/data/misc/sp3.key b/tests/data/misc/sp3.key new file mode 100644 index 00000000..bd4d3a19 --- /dev/null +++ b/tests/data/misc/sp3.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9 +IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+ +PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQAB +AoGAD4/Z4LWVWV6D1qMIp1Gzr0ZmdWTE1SPdZ7Ej8glGnCzPdguCPuzbhGXmIg0V +J5D+02wsqws1zd48JSMXXM8zkYZVwQYIPUsNn5FetQpwxDIMPmhHg+QNBgwOnk8J +K2sIjjLPL7qY7Itv7LT7Gvm5qSOkZ33RCgXcgz+okEIQMYkCQQDzbTOyDL0c5WQV +6A2k06T/azdhUdGXF9C0+WkWSfNaovmTgRXh1G+jMlr82Snz4p4/STt7P/XtyWzF +3pkVgZr3AkEA7nPjXwHlttNEMo6AtxHd47nizK2NUN803ElIUT8P9KSCoERmSXq6 +6PDekGNic4ldpsSvOeYCk8MAYoDBy9kvVwJBAMLgX4xg6lzhv7hR5+pWjTb1rIY6 +rCHbrPfU264+UZXz9v2BT/VUznLF81WMvStD9xAPHpFS6R0OLghSZhdzhI0CQQDL +8Duvfxzrn4b9QlmduV8wLERoT6rEVxKLsPVz316TGrxJvBZLk/cV0SRZE1cZf4uk +XSWMfEcJ/0Zt+LdG1CqjAkEAqwLSglJ9Dy3HpgMz4vAAyZWzAxvyA1zW0no9GOLc +PQnYaNUN/Fy2SYtETXTb0CQ9X1rt8ffkFP7ya+5TC83aCg== +-----END RSA PRIVATE KEY----- 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/requests/authn_request.xml b/tests/data/requests/authn_request.xml new file mode 100644 index 00000000..ab9e1225 --- /dev/null +++ b/tests/data/requests/authn_request.xml @@ -0,0 +1,11 @@ + + + http://idp.example.com/metadata + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:Password + + + diff --git a/tests/data/requests/authn_request.xml.base64 b/tests/data/requests/authn_request.xml.base64 new file mode 100644 index 00000000..73f07ba5 --- /dev/null +++ b/tests/data/requests/authn_request.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOkF1dGhuUmVxdWVzdCB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBJRD0iX09ORUxPR0lOMTAzNDI4OTA5YWJlYzQyNGZhNTgzMjdmNzk0NzQ5ODQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTExLTEzVDExOjM5OjM0WiIgRm9yY2VBdXRobj0iZmFsc2UiIElzUGFzc2l2ZT0iZmFsc2UiIFByb3RvY29sQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUE9TVCIgQXNzZXJ0aW9uQ29uc3VtZXJTZXJ2aWNlVVJMPSJodHRwOi8vZXhhbXBsZS5jb20vYWNzIj4NCiAgICA8c2FtbDpJc3N1ZXIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+DQogICAgICAgIGh0dHA6Ly9pZHAuZXhhbXBsZS5jb20vbWV0YWRhdGENCiAgICA8L3NhbWw6SXNzdWVyPg0KICAgIDxzYW1scDpOYW1lSURQb2xpY3kgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDpwZXJzaXN0ZW50IiBTUE5hbWVRdWFsaWZpZXI9ImV4YW1wbGUuY29tIiBBbGxvd0NyZWF0ZT0idHJ1ZSIgLz4NCiAgICA8c2FtbHA6UmVxdWVzdGVkQXV0aG5Db250ZXh0IHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIENvbXBhcmlzb249ImV4YWN0Ij4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWYgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+DQogICAgICAgICAgICB1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZA0KICAgICAgICA8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgPC9zYW1scDpSZXF1ZXN0ZWRBdXRobkNvbnRleHQ+DQo8L3NhbWxwOkF1dGhuUmVxdWVzdD4= diff --git a/tests/data/responses/adfs_response.xml.base64 b/tests/data/responses/adfs_response.xml.base64 new file mode 100644 index 00000000..4a2df5a1 --- /dev/null +++ b/tests/data/responses/adfs_response.xml.base64 @@ -0,0 +1,91 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzYW1scDpSZXNwb25zZSB4bWxuczpz +YW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBJ +RD0iXzAyNjNhMDdiLTIwNWYtNDc5Yy05MGZjLTc0OTU3MTVlY2JiZiIgVmVy +c2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMjJUMTI6NDk6MzAu +MzQ4WiIgRGVzdGluYXRpb249Imh0dHBzOi8vc29tZW9uZS5leGFtcGxlLmNv +bS9lbmRwb2ludCIgQ29uc2VudD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6 +Mi4wOmNvbnNlbnQ6dW5zcGVjaWZpZWQiIEluUmVzcG9uc2VUbz0iX2ZjNGEz +NGIwLTdlZmItMDEyZS1jYWFlLTc4MmJjYjEzYmIzOCI+CiAgPElzc3VlciB4 +bWxucz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+ +aHR0cDovL2xvZ2luLmV4YW1wbGUuY29tL2lzc3VlcjwvSXNzdWVyPgogIDxz +YW1scDpTdGF0dXM+CiAgICA8c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJu +Om9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+CiAg +PC9zYW1scDpTdGF0dXM+CiAgPEFzc2VydGlvbiB4bWxucz0idXJuOm9hc2lz +Om5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9Il83MjFiNGE1YS1k +N2UxLTQ4NjEtOTc1NC1hOWIxOTdiNmY5YWIiIElzc3VlSW5zdGFudD0iMjAx +MS0wNi0yMlQxMjo0OTozMC4zNDhaIiBWZXJzaW9uPSIyLjAiPgogICAgPElz +c3Vlcj5odHRwOi8vbG9naW4uZXhhbXBsZS5jb20vaXNzdWVyPC9Jc3N1ZXI+ +CiAgICA8ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9y +Zy8yMDAwLzA5L3htbGRzaWcjIj4KICAgICAgPGRzOlNpZ25lZEluZm8+CiAg +ICAgICAgPGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJo +dHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAg +ICAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3 +dy53My5vcmcvMjAwMS8wNC94bWxkc2lnLW1vcmUjcnNhLXNoYTI1NiIvPgog +ICAgICAgIDxkczpSZWZlcmVuY2UgVVJJPSIjXzcyMWI0YTVhLWQ3ZTEtNDg2 +MS05NzU0LWE5YjE5N2I2ZjlhYiI+CiAgICAgICAgICA8ZHM6VHJhbnNmb3Jt +cz4KICAgICAgICAgICAgPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6 +Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0 +dXJlIi8+CiAgICAgICAgICAgIDxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJo +dHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAg +ICAgICAgIDwvZHM6VHJhbnNmb3Jtcz4KICAgICAgICAgIDxkczpEaWdlc3RN +ZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3ht +bGVuYyNzaGEyNTYiLz4KICAgICAgICAgIDxkczpEaWdlc3RWYWx1ZT52NTN3 +cW80ZllESzhVY3JPVWNPV2cyemxKL2NIVnVtWVMwS2pycm5WdUprPTwvZHM6 +RGlnZXN0VmFsdWU+CiAgICAgICAgPC9kczpSZWZlcmVuY2U+CiAgICAgIDwv +ZHM6U2lnbmVkSW5mbz4KICAgICAgPGRzOlNpZ25hdHVyZVZhbHVlPlowOXBl +d1k3ekZ2OTFobjkwbHgwRUVubE1HTkw5elVKWk14SVI2cW9mTFpPWk1sVG5Y +TjV6RnNmKzFYUFBJWVpMdzVsQ0dqanRtZE5seGR2NzJ6TkZsTVROUnFaN0lp +SXd2azVHUk0zenZBV3NOT1k2ZEI0YzVxamU0UkhxL2ZySkdCZ04vZ2VWeFZt +bjNMWmQ1WmNrdWh1UzFzN0ZKQW9MVWNaRUxKL25jZ1JEZGdqQUUrcjhHdGFO +a3U0VVRCUkdBZnRsMFBXbUFTMDdsbGU2bGFTVVBSQmRCRE5sVlN6R0FQT3lY +UDE2ZUkxOWJvbllMaGpiOHVoY0N0bWdicnJhbkpVVGxZc1htcnhvaGNGdW4r +eWZxVFdXd2l4OW1SUXRBdEFFOW5nSUUwVkRkTC9reFR0NktOb1B6d2tlajVW +eFNMRkFncTJ1M3JaTWN1WUdadTFIUT09PC9kczpTaWduYXR1cmVWYWx1ZT4K +ICAgICAgPEtleUluZm8geG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAv +MDkveG1sZHNpZyMiPgogICAgICAgIDxkczpYNTA5RGF0YT4KICAgICAgICAg +IDxkczpYNTA5Q2VydGlmaWNhdGU+TUlJQzVEQ0NBY3lnQXdJQkFnSVFOQlRr +dDdxaWNhcEtOc0lYTWNrOHhUQU5CZ2txaGtpRzl3MEJBUXNGQURBdU1Td3dL +Z1lEVlFRREV5TkJSRVpUSUZOcFoyNXBibWNnTFNCc2IyZHBiaTVrY21WemIz +VnlZMlZ6TG1OdmJUQWVGdzB4TVRBMk1UQXhPRFUyTURGYUZ3MHhNakEyTURr +eE9EVTJNREZhTUM0eExEQXFCZ05WQkFNVEkwRkVSbE1nVTJsbmJtbHVaeUF0 +SUd4dloybHVMbVJ5WlhOdmRYSmpaWE11WTI5dE1JSUJJakFOQmdrcWhraUc5 +dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcTdBTURHbkJISUd3dDlLUHRM +RDBNMEVYR3VabldHQW1iNXAyRkRjRnp0SkhPSThXWVBxZVJwaHpWU0VrZ1h0 +UEloNUp4M2VsUzZoVm43SFZqMld2eklENmpwQjQ1bzhpRGs4UFdnaTE0ZnhH +V0U1bzFQaUI4WHJlMWM1dnMySUc1YVBXSUQ1dUM2YkQwWGduTDk1TWdPOUhH +UFBTUVJGbnVqS05xekZRZHRvQkpJSmF3QWVEL2kveHM3RmpGazl4MWZBMEV5 +TENuaCtlYWZmSXBvcmIrMXh4VzJENkQzbVJUZ2ZIeFhyV1I4VzRqSG5pZ2da +aHFkRGhVeHZFYWlRRlRiSU4yRCt6eUI3YVF3UUNIU0ZwZXJCYytSNUZsbGdu +R0FhK3NqYjZnMUZYYmVobUVHd1NheHdSWklEQWhqSVFtYTV3WDV5V0pEeEZ6 +UjRwc1RlRlJRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFCekFQ +QzJRUStVdHZrcVFZMm8vam9IR3RudUx5Zmt3ZDc2NERjR0RsY1lLVktFYURD +dm5KeDNneXdSVU9ERVJoRGh1Zkpid3I3T29YVmRodzcwTnRURU11Z0pGcjI5 +U2d4bjNDaVRpeVBGU0RHang5MTFhYkt4dEpTQkludkkwMEFqWCtWbElaaG95 +ODNZWU9SWEZjeWIrVXZoMnIyU1pVM0FDTnA4TTNjWlI2SjFFREJoUEtZd0VF +VWs4TlRNbVpMM3ZXanFMWldUeVRUaFRyUUYvbEg5UENsdzlPMjl1d2lmaXEy +WHpTeVNyMy9QSHh6cE1Sa0w5YzRFaTQ1UURtYWdlckFVUndlcTVwVVc4QzNV +QVVqTExWY1hrLzJwZXZaRU43MFlndDVwMmZBZ3M4NE9KaERSS2lIR3BhcmlF +bWo0THNKR1pzcDdxRkpwbjErTWlqUmU8L2RzOlg1MDlDZXJ0aWZpY2F0ZT4K +ICAgICAgICA8L2RzOlg1MDlEYXRhPgogICAgICA8L0tleUluZm8+CiAgICA8 +L2RzOlNpZ25hdHVyZT4KICAgIDxTdWJqZWN0PgogICAgICA8TmFtZUlEIEZv +cm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3Jt +YXQ6ZW1haWxBZGRyZXNzIj5oZWxsb0BleGFtcGxlLmNvbTwvTmFtZUlEPgog +ICAgICA8U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpu +YW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPgogICAgICAgIDxTdWJqZWN0 +Q29uZmlybWF0aW9uRGF0YSBJblJlc3BvbnNlVG89Il9mYzRhMzRiMC03ZWZi +LTAxMmUtY2FhZS03ODJiY2IxM2JiMzgiIE5vdE9uT3JBZnRlcj0iMjAxMS0w +Ni0yMlQxMjo1NDozMC4zNDhaIiBSZWNpcGllbnQ9Imh0dHBzOi8vc29tZW9u +ZS5leGFtcGxlLmNvbS9lbmRwb2ludCIvPgogICAgICA8L1N1YmplY3RDb25m +aXJtYXRpb24+CiAgICA8L1N1YmplY3Q+CiAgICA8Q29uZGl0aW9ucyBOb3RC +ZWZvcmU9IjIwMTEtMDYtMjJUMTI6NDk6MzAuMzMyWiIgTm90T25PckFmdGVy +PSIyMDExLTA2LTIyVDEzOjQ5OjMwLjMzMloiPgogICAgICA8QXVkaWVuY2VS +ZXN0cmljdGlvbj4KICAgICAgICA8QXVkaWVuY2U+ZXhhbXBsZS5jb208L0F1 +ZGllbmNlPgogICAgICA8L0F1ZGllbmNlUmVzdHJpY3Rpb24+CiAgICA8L0Nv +bmRpdGlvbnM+CiAgICA8QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIy +MDExLTA2LTIyVDEyOjQ5OjMwLjExMloiIFNlc3Npb25JbmRleD0iXzcyMWI0 +YTVhLWQ3ZTEtNDg2MS05NzU0LWE5YjE5N2I2ZjlhYiI+CiAgICAgIDxBdXRo +bkNvbnRleHQ+CiAgICAgICAgPEF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpv +YXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkUHJv +dGVjdGVkVHJhbnNwb3J0PC9BdXRobkNvbnRleHRDbGFzc1JlZj4KICAgICAg +PC9BdXRobkNvbnRleHQ+CiAgICA8L0F1dGhuU3RhdGVtZW50PgogIDwvQXNz +ZXJ0aW9uPgo8L3NhbWxwOlJlc3BvbnNlPgo= \ No newline at end of file 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/double_signed_encrypted_assertion.xml.base64 b/tests/data/responses/double_signed_encrypted_assertion.xml.base64 new file mode 100644 index 00000000..b292d274 --- /dev/null +++ b/tests/data/responses/double_signed_encrypted_assertion.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZng2ZmE1NDcwMi0wMDc4LTE0ODUtMjgxMS0zMTRlNTg2NWFhYzAiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTMwVDIwOjQ4OjQ0WiIgRGVzdGluYXRpb249Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl82YmZmYjRlZjgzNzYwMGUwNjc2Y2IyMWY0ZjBhYTRiYWQ5NGRkOTYyIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng2ZmE1NDcwMi0wMDc4LTE0ODUtMjgxMS0zMTRlNTg2NWFhYzAiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPktYMEl1ZnRJNnVqKzhiQ1c0R25HRmJkK2Fodz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+Wmw3TjJ2Y0UzTjYrS2VUWGZFY3NaM3FRL0FRWGlId290RWJJZGduNUNlNlNjL0ZaTFhpVTFDOERRbWlhWk1HWDJCbW4ycmNDWGtuQ2dQWjlrVlNUQzFQbUZ3UkJRbGozSExGb2FrTHorUkJUbkJJMmxhSTQrd2U1ZVQ4ZWU3YVpneG1udjFTN1lXbEtyVjZLZDZ1eU1YU1dQQzYxaU9YdUNSbjJnNWR5aGdVPTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6RW5jcnlwdGVkQXNzZXJ0aW9uPjx4ZW5jOkVuY3J5cHRlZERhdGEgeG1sbnM6eGVuYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjIiB4bWxuczpkc2lnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIiBUeXBlPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNFbGVtZW50Ij48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjYWVzMTI4LWNiYyIvPjxkc2lnOktleUluZm8geG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PHhlbmM6RW5jcnlwdGVkS2V5Pjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNyc2EtMV81Ii8+PHhlbmM6Q2lwaGVyRGF0YT48eGVuYzpDaXBoZXJWYWx1ZT5zQUNhWHN6OUpHM2RUektZNitHRWFwS2ZkV0k4THJNcXl1d1FIcDNuK2ZUYmdmanZGUGtnMkhNSVgwQTBvdEFWS25rRGlMRFJGUERoQWpxRk5GMXNyMUdRanFXdi94d2N3THZNQ1orMjVMWFBjdk5ra05rZmZ3V0xVdVA4QUU3ZG84WHludllqMHBBNnh4di9pa3NldXFSbWoyeTNsVEpuZ3UrREx3aUtKdUk9PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWRLZXk+PC9kc2lnOktleUluZm8+CiAgIDx4ZW5jOkNpcGhlckRhdGE+CiAgICAgIDx4ZW5jOkNpcGhlclZhbHVlPnJUUlQ2YzA3QjVKZ3dYdnZPYUQ5L2NQZ0lhYUpyZ0I5QjZLR1lpK3ZFWExHaTZBWXI0ejVmWTVxMXR2dXA3eUZHTStwZVdGdzRvQ1N2L1Jxdy9HS3pVZ003UjNNZnBzVU1QaERxS2xTT3o1SDhsSSt2ejVSWi9TZVB6eGUxK0N5NGdkUlVRR3dMM1R1c1VLRnhxZUlJVnNjdVVjajVvZ0Z0YmtIS25GbkxnZllNbWVCbFhIN3NnbFZIcFhldHM0TWk0QlAwb2xIZVhXaDc3OWNrSkxsVG15cFBpdThVTGFtYlQrMlhia0xWaHZvVEVEZ1F4TlpUQlZPOW5SdzZwQTUzZkNYZjNTQk43NW5FanFOVDl6UXlEMVBMQU9UVU93U1JNM2d4WFNrTHAzQ0ZaZUVPS3VpZmp4VUZsQWk3WENlUFM4dFdDcUdSbjZ5R3JodlRtYVp6TnJQREFPQmUrTVpTNjBqUTlyMGxLaVk1Z3lxMkkzSUZNYjFMRUQ2N2twRHc1Z3ZjR0xuclZRdkNaVTBLaHc3RWZ2QUc1Y0JlQ1ZMY0RxODBmSWtCVFk1Y285R3UvMjVxVFI4NkxXR2QvMThYK2QxWHo3SkI1OWp6eGlEY05wRllKU3NjQUV2WmZPbHRPTzdNN3RSZ2lsWktta25GWGNMaEpvbUluV3poNE9aSElqcDRla2xlWVV2WHVxRGhDVFE3MjdDZVZJZ0ZNRThUalZCbFpqUW4rOC8vYklud2ZPcDFQQjQrc1dFRzk5dFZhNFB3TGoxTXRVc1dxM2xoL0Y0eUlmSWFRUXFmcGZvRDFWcnRyT3BpSG8yM0NBSGdDajVwVW5wbGxHTDIvU3laRzMybEtja2pWMm5KbjFzaEpXQ1RZNmtnaGdvWkw5QlA3eFh4Nk8rUkRqR3VCZm5idk5lRk5UMi9ROWFPVHZVMW93ckphazJKbWtlWW8rS2Y1SDNvSng1VkUxTG15V3ZYUFZwcVNmMEd6RlFteXg5UDA3WVZOTGdFSDFhM1NaNDlUNzdXSUpXSFdYbWIwUGZacytBMWdhZVZEcis0bHdOTVg4UCs0REF0WDlPTlRUSk1IZ1BiMThaTDE4cGZRUDZxMHdDWDZhdmtJdzh2b1loZFBRSGxhbmlkL3dyN0IxOG8zVEZtT3pTblNHcEFYUHpQM3FodTE0L1NVQlRxWGFPem9JRDZtdW80NElHcWxVbUp4Q1FQOWQ0VDYyU2lZZWVXYlRuOTZIYVNsN1FoYkIyU2hRajFONGVuNEhNQTZkR2FLa0d2cG9tamhTbGF2T1NoVFlKWlBkTnZCeWJUNGFUdHBPVGFJWG9ML1lqdmUrTk9aMGZoZjkxZnZ0NTJNbWttNkxnN3VSK3dJckdQUjl0bzJNQkgwY1k4QjV6Rkt5ei95MXRuTXhGQkZOaTFvNXlQbWZyS3EySUNpVHRPWFFKMzBTMkNEeDR1aU9pcHpGVEFJeC9sTGgxUEhIdC9PVlhVSklMcmdRR3FCVmM5b1BZQXRGMDJJWHB1bU1SdVlUa0VQMGpxeU1SVHlxRDNBTkhIcHlzaUNQYnViSU5YK2VZWUJBb0FqekZXU0YxTDd1K0JxS0grVzdIaXRpYXVSbS9xa09RY1dPUTlsY1lPaWI5UC9RRC9RYWFZQzhoMG9rZ1pPb0EyS09wZ3E0cHNudGt5clMzMEo1OXRxU1hORXVuQXVweURLSkVtZ3poK2JWTTRQVXo5U3ppTDJ3OHdKVzcrb3h5c2ZpRXpNZHhFRDhMa3FkTjBJVEx4VVFBM3V4elMxUHR6ajZtcmZYZ3pGVXJUZFZIUUw0Z2dTejliYTAzZEFIZDZEWFFWelBaS0svTnBaNUY5UzRxUGxSRHo5ZmM4ZDhMSlZIbDFvbUMwUDVlbUlkU0V5NGxLcHl6TE9IMG82K1g5cXpxdGhmTUFHVzhnSk9CbDVJQzkwMjZjOEE1TlJFUnQxcjJ0NTNBWmNSejJIWjEwdGZ6cWZ4UjRBc0FiM0dFdkxpa1N2VDRIZWpRcVlqZDd3dTVSK2V3Ym43aFBIYzNPUG00TEJyTEg3Q013K2U4MmM2bCtUaWdrL0ZJS3Ryc1U3YUhENkNqZ0VRZnNkWHNrRmREOVY2RTgwODU3WjBVWHFveDZuKzdCTkRsN1pzci9PZXJ5QmZlVTk0OXVSY1JKc2xuZVorRlZqL1R5OEs4TXh3LzYrcG5tcUlwUUhYR3FkK0dKZVZvZDZpK3hodDdzTUw0RUhKbnc1RU4zbURLd29OY09RVE55OHM3REcxOHp5NXhEUGkvK1ZxVEw5bTFtZTFsSllLSVhwWXB3UStSNHVPeUlOWDJuM2NhN0RRY3lkcitXQ2hjTEc2RjFFRWhBK0U4NmhKTFU4VUxSbitRNU1kQ3VZOGt1V0graTlOT3R0YU5JRjdnTG4vYndXQzlyNmNud2pTd3RyYXBYQlRWSGxXNXNYWmVaUjdCMjZTZk9wSFhwRVJQUklkMmthU2RzTlZVeGdUMGl6L2h4cFo5MWgrR2huYjg1dW1uOTRSakpVNDRUSFVtdmJ2U1psZklGL1U3RlBhakx0TFN2TzNvUlFIUzdRaVhRelhtMFZoWEdYeVF3cUs1MXhXWWx5LzFmaExXTTB1bkMrK1J2dlhRUFQvVVhoeGQvVVdUV1paVXg4UHFNZVk4Ni80bCt2N0s4RlVtN0FNcTBLRTN2bC9JSWw1d1kzS3NORDZjSUJJSXhDQ3EwS2p3ODN3ZDZCMHl2QndYOTFnZ0s3dHgzL1p1NFNST3YwdjN4eEJZeEtPQ2dSSWRSWG9oU1ZSOEY2VFNGejVCNmw4VjBzMklxUDJnQ056ZStSeXkvWEIzc3pmbi9tMW53Rzg2MHRpWHgwK3V3ODJGdnVsNzY5TSt4c09INnlXRHIvWk0rZ2VRdzk1ODQ0cUFKNUU2UWVoQUU1WHpGUG1lTGRRN2trSUx2VXNnTFVDTTM2bWhobjF6SGg5QnUvZzhkZWFGSE0rajZTbmVhTkJBN1Yvc3hhNEV1U3VBckMyMzlSeUhsT244aFFXa2NUUThUWTEra2JCMmJOOXg2Qi9FNjdxWHNEenU1NW9vSzhVY1ZSa1J0Z2s5a2R3Ukt4djR6SHZZTE9VeE1BZnJCV0xna3VUWFhGUitZbWNwc0s4UmkrSkhZUUd6U0EwY0JzZ2VvS0xqQ3hoWmhoMlF6QmZxNTFWOWE0Rm5ObkJYdFIxT25JTTdrVVRXMVZZQUt4S2c0UWs5M2dQck4vQ2E1M0ZiM1czRUdhc1hha3JXNFE5bXBmNjFxVlJQN3k1WWgzZmRrU2tsU2hhRitmNGxreVB6VTlKTTZSRCtJaEc5OWZjVHJSeWEyNm5QcUdEQTdxT1Y2YWpiT25VRjRDK2FuU2lvS1JXQWIyUDBnTmhGc2J5eXpCQXdpT2lyOFN0eUxBQXNrcE5UWVRGWEx4UDgvMjNtbDlBYVU4U1hGeGtsM0RqR2hBY2F0Mi8yZnFBSERqWThGaG1INVVtZ0t1dENYTW5yNlJzUFlXaEF5YU1vdjhIWHl6NmhUK28rSWthL0ljRU9DKzAzWVovczlWdUNTVjM2a2tLWWI1Sk54QlZpN1lZSzBFZTlZcDFqWVRrWWQrWjhhZm15ZVNkbHA1aitBUUJQcWxtUFFFY0Nub0d2b2ZhdGhjU1NYWHQ2ZVVBTWdxQmI3R01kdTczVjRCbFlHOXhPZmtHbW8rOE9DVHBhK3JhMnlLQlNVK2hNUFZjQU9BRjlUYXpYYUwzRVRwRk5mU2pURllUSmVPSHZFeXZDdnBESkVXT3p1bjdjc3lkaWdUNkxVNnBvUHR4cGpBMlVZZkEzSUpuQ3grZ0JHeVAweG5Na2lBRVUwWWg0c0ROcFZlR2h4NzlpSGNKOU1MQnZKRkNVaUQ0anhnMXE2TldZNVQ4ck1HaGt5N2NJdUpTaWRxaDBXM3VTMGFBanNjd3lMdE82UFZuVjdlblVGbHZKQURIVXNGRHpycEtrdm5Fd3VJZjd0cHNqRW5uc21tVk84d2J1ZWRQL0ZNUlZwbzJnUXlZYURWckFnUmlMSEJkTnpaMi9lUnN4OTN4czlBREE3Yk53M0F3cTVtZXVPZXZDTTRNWHZZWU5xczYrVzNXa2ordWF0cVE0K1E3alYza0UrTEFPeWNwdnV2RXVxWGRsSURFVDR0OU9FWGJiRG8vVXFwZkVhTHdyb1JyNXM1UmR0aVZ0NU5YNnBxeUQ3bUJMRHBUTmlkRVRwdkY1SVNaM2Z5Zkk1aHJYa3dXSU94UVZjL2lTN0tHYTRzYW1UQ1dXM1pPSTI1QWVyUFdUSlhmeU0veXJNSnA0YVRSVFFITFIrRjM1bzhqeHZjVXFCZ1R0Z1VIdlM3eTFZS1lhRTlLYmxUcGVhUVBYcW1oL0FSY0UrL1NxWlNIY2hlakN0ZHRnNXl5RkZlaDAzaEd0U080UVgrM2h5REd5UVAveVZRNjNJM2syejQ2djlDMzRkYzdBUjVBd0gzOWIzK2t4MERSeDJocDFoeGZZTkJYK1c3cWN1bDY1dTJMV2JTMzhOMXgzSkVJY0dSbXlQNFhQV2FUTHBkcmM1ei9YSnVoZHoxYXMxMGd2NWVLQ3AxczJCWGdWUlQ3d2dUclFzaDZxeEl4UkdBYXBFYWY5QVFjRFltYlFEOUtrbU5lREthaFdVdm9vZTJvb29iSGFRdFVleWhJT0I2T053VEp5RkYvdU1sNzZIV2F3dzBYTGV0Sm90NldQR2J6U3JVeCtYSXcxY215NEFQOW80bXYrWkY5OXA0QmUrc2N5aHNLeXEyMkQ4SnFwc0pWL3NiYmZkYjU3MUY0YThRb3ZLSnBoMTQvMXVndWVHT2ZTRU1BWnZRZmI3Q2h4czVDdXBLRm14bFFuMXoxRjBIMzRKQUh0cVRqek0vc210U0lOeVprUGtXUHpVZXpid3F2UndjMHo0N1g3YUtYRVZyQjI5Qkh1b2dadnJqSldLdUNjL3BoTUdBQ05BQTkyT2YwaVNTbUlLSWxaRkVET2FsYWc2d0xvMHc2UlVHKzR6bnV6eGVRbmQ2bDRsZjJYalNBUXZ6N1ZNaGxrZDBLZ0pMc1JQc3FjTkl3dXRRekxvcCtrOG9MeWNvV3ZOaEtlTEIxY2ZGNW1KWENwRDZGRjFqRk91S0dEOHdLenltRVRGcTd0RkJYMWVjMENnMG5iVEFQclVYb3MwZkRLaDRudHZiL3UzN2RXY2pwc3dSWjY3dkM0L3FSby9Ed1gram5XdDVFdzRjQjQ0eWowK2x1WEp3K2lSWk5JYXB1YWtBenR4QzlhaFpheHk0b093cWxyUzB4dS9GMzcyTU5uQThQRmIrZ3pzbFZrMXBZa2g1blo3RXNsUHphMUJIREpQaXQzamFENjVVZzUxb1RQZnhkSWJkK3ZJdHd0cStQUUNOV0dJQnRCTUoxblU1aldMUFpTVEFoenBQNnp6SFU4OTE1Zll6YkZST2tmWEdjOEVkc0dGYmdMdnhCWUJBVWRveElSR0pCTWZyTlV1YWQvRkhGRjdhWkdXTVlKVUJaY1M5aXNNVTJ3M0NXSnl0NWphRW9KaUVwTjRHUG91THNnUGc5ME43eU1md1h6bjE1d3JLclFzWlFnQ2tabDFyUzZBTUdOYlFJTkxUelluT2F3ZGhzN2sxUDVxeGFqdWl1VDlnd3RDamxHNURmbVg5N2xqZXBYY2JabkVNM29XUG9sSHhkeTlUL3p0VExiZFh6OFZ5azY3UlFUOEljZnZSZXdGcDF4QlIxeThzWE9nWklSZTZvUGRQN3ArdnhlVmRHYllTNUtMYVFwRFJ5SExuRGw3NWQxeWRKTHpCdUdtM28raSsyRSt1V29kVXdPNnhJSTg5Vm1YaFVBQzA3SmlNQTFKemJYYXFLWlFkQS82OHl0Y05tYUpjYWsxVGJOZUwvc3l3WUVRa0ZSTkZaYlJkTFdCWVN5Z1FhWU5DVXJIdDFTMExaMDV4aVM3bGxOVmZFNUh2cWtyWFpLekVrS1YwWjE3cGNYMDZ2Tjd5TU1ZT2Z1REdSR3BwZlBzNXVBV2pyN0lKR3IxM3VNRnJ6MTFtQVNOQjVhcTVYNXhUdGp0VmlGZXRDN1FVd2NJaEM0K1lvQkpYQlhST1lyTzVpWkhuY3ZpS2lpb2ZsWWRkcFVIRXIrT0hVQzBjTEdRTlBoQ3BsUjZhRXQ4ZTEySUVZYjBoTURzdmVhbDg4YkwvU2lSUGg5WjFZbXQ1VnZkV0NxMW9YRW51RlNmOTk1SnM5QkVoVitRaktiSjhCNS94Q1NkeWMvSk0zZVpoeThmTFEvN2t2YUxHNWJGa2VKMnM0UXExVmNWRkVzZEVGeGRxWGJkalBBMFY5ZGFmeExhZlpXR3I3UURhTm55THQvUFNWR0NEeGYrZ2g5Zlo0SEl1djlrV2N2d3J5L1BtRHc5ckN5eVI1RGg2dlgxOFBITzB3TG1GWUhSK25zY2ROQlMzNGxualFTQ0d3ZGZmYWUxTXlwYTJhL3ZhaGtsMG42UHViNy8ycFB5c1dOWlhoQ2x2ekNJQzR4UktFRHhiYnhqKy92Wit2Uk9mNndQaXRaUjYySnBveXVQaHpJdDhadVQybE9sNEVhSGZiRWxlVmEvQ1NWbzdKZE5qRVk0NTJ0WnJXa3B2N0dnK2tqZk5RNnQzQTVYb0JJV2hhUUozeTJjYStpeDRZcFcxWVcvdnZ5WlE1TVFPdm9OZHBFY1hqY1d4MEtMVlRUdnY0SUVPRUtQMzZEL3dxeWtEamc1akpWcnkva1RiTDlaSWRtbFJML1FCWDNqR3FMYmp4cG1UN3VnMkVOZFFlbUd6L29XVGpQem0vZVUwcTZlbHliNitDenRGbldkbWU0YmE4T1ZYdVd4TWJsdDVUd3NxOXpCZzc2RGxDaWYrS1h2SlUzRUdXMFQ5WnUrRTJsaEQwQyszOGlJMmtoYm1BWW5qZXlHTzE0VkpIUGFGeS94amZKODM4YjJKaVQ1QnJMZEx4V1NiNXprc2tqSFRBUm5vTzh0SSt1VS9tSzhDSVZGMEhiZEhHYitwbmFoTnVyTXpqeTdiUm0zcUYvdkdVUmdhN0wycVk1SGU0a2U2RStkUDNpc1RTcGVlRGluV2Q4eWo3RmtVZDlwMk5WS1c2bGZEa2paRTR3clNPdGlKYnBrV2RRNjd3VUsxN01iZ2VneFZxT3ZndE9nd1JHQWNTNVhSRVFTNHpUK3lTZ1ZzY29jR2pyMGYrWXBDWXh4eGJ4OVN1Wk5qMGdZbDZVcFdOb3hQd2Q2S3hpYWJYZ3c2MCtIMy9GbitoOVg5cm9wZDJCMXVLelVIVUY2YUZrdlRIN3ZMZThoY1JubE5zNWpyMHozT25iQWpocENtTmt6aDhtRTJsTWR1QW9vTXBZZEF4dzd0Zm14cVZRMlVoajlUMHpheGNYYkhqK1M4MFhoTHNqYnZSL2dxcGJnMUVPMjhuR0I3NUNLZTVtNEJDWnE0VU0vKzFIUTVnN2FWZnlPb1czYklzRUJRPT08L3hlbmM6Q2lwaGVyVmFsdWU+CiAgIDwveGVuYzpDaXBoZXJEYXRhPgo8L3hlbmM6RW5jcnlwdGVkRGF0YT48L3NhbWw6RW5jcnlwdGVkQXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+ diff --git a/tests/data/responses/double_signed_encrypted_assertion2.xml.base64 b/tests/data/responses/double_signed_encrypted_assertion2.xml.base64 new file mode 100644 index 00000000..a39003a3 --- /dev/null +++ b/tests/data/responses/double_signed_encrypted_assertion2.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfYzdmNjk1NTk1MTFhNTU1YWMwM2E5YTM1ZTk1NTI3YTlhMmE3MjZmYWQwIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wOS0yNFQwMDo0OTo0OVoiIERlc3RpbmF0aW9uPSJodHRwOi8vcHl0b29sa2l0LmNvbTo4MDAwLz9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fZmQ3OGQ4YTYyMmQwMDEwNzcxOTMyODcwNzI0ZjM4NDM4MTZkMzk4YSI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vaWRwLmV4YW1wbGUuY29tL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4KICA8ZHM6UmVmZXJlbmNlIFVSST0iI19jN2Y2OTU1OTUxMWE1NTVhYzAzYTlhMzVlOTU1MjdhOWEyYTcyNmZhZDAiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPlRoeHRtcHY1QUF2SGY2bXl0K0RMWHg1SW9DST08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+MTNRaFp3VDgzbk5tVVMyNmZHVkptQ29NZUw1SzNrcXNaS2xKMWJxWWd2cTBOTHZyRUo0YVh6TTVHTElrN21idjR3UysxRkQ1MTgxdVVJVWRURjNJWHJNeW04R215NXhYcVk2MVYrYWR6YlJIYnIzamZqdlFnaXlJdWcvUE9rWkxpSWhTTHVxLy9tb2FXYXRBQXB0dWVCYU5lOVFQb3N0N3pXSlpJc2EweXRRPTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNiRENDQWRXZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRMEZBREJUTVFzd0NRWURWUVFHRXdKMWN6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVWTUJNR0ExVUVDZ3dNVDI1bGJHOW5hVzRnU1c1ak1SZ3dGZ1lEVlFRRERBOXBaSEF1WlhoaGJYQnNaUzVqYjIwd0hoY05NVFF3T1RJek1USXlOREE0V2hjTk5ESXdNakE0TVRJeU5EQTRXakJUTVFzd0NRWURWUVFHRXdKMWN6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVWTUJNR0ExVUVDZ3dNVDI1bGJHOW5hVzRnU1c1ak1SZ3dGZ1lEVlFRRERBOXBaSEF1WlhoaGJYQnNaUzVqYjIwd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFPV0ErWUhVN2N2UE9yQk9meENzY3NZVEpCK2tIM01hQTlCRnJTSEZTK0tjUjZjdzdvUFNrdElKeFVndkRwUWJ0Zk5jT2tFL3R1T1BCRG9lY2g3QVhmdkg2ZDdCdzd4dFc4UFBKMm1CNUhuL0hHVzJyb1loeG1maDN0UjVTZHdONmk0RVJWRjhlTGt2d0NIc05ReUsyUmVmMERBSnZwQk5aTUhDcFMyNDkxNi9BZ01CQUFHalVEQk9NQjBHQTFVZERnUVdCQlE3Ny9xVmVpaWdmaFlESVRwbENOdEpLWlRNOERBZkJnTlZIU01FR0RBV2dCUTc3L3FWZWlpZ2ZoWURJVHBsQ050SktaVE04REFNQmdOVkhSTUVCVEFEQVFIL01BMEdDU3FHU0liM0RRRUJEUVVBQTRHQkFKTzJqLzF1TzgwRTVDMlBNNkZrOW16ZXJyYmt4bDdBWi9tdmxiT24rc05aRStWWjFBbnRZdUc4ZWtiSnBKdEcxWWZSZmM3RUE5bUV0cXZ2NGRodjd6Qnk0bks0OU9SK0twSUJqSXRXQjVrWXZycU1MS0JhMzJzTWJncXFVcWVGMUVOWEtqcHZMU3VQZGZHSlpBM2ROYS8rRHliOEdHcVdlNzA3ekx5YzVGOG08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpFbmNyeXB0ZWRBc3NlcnRpb24+PHhlbmM6RW5jcnlwdGVkRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIHhtbG5zOmRzaWc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNhZXMxMjgtY2JjIi8+PGRzaWc6S2V5SW5mbyB4bWxuczpkc2lnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48eGVuYzpFbmNyeXB0ZWRLZXk+PHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIi8+PHhlbmM6Q2lwaGVyRGF0YT48eGVuYzpDaXBoZXJWYWx1ZT56SFNWUW9LdktScGg4Q0x5VVNxeWhBMkVPanB3UU93dDkwNlA4bUFVeE5pRlZ3WjBiMWgwcGR4RnpMdkJsT05wSDlZMWFFOVZ3aGg4UnRSNDdYc3ptMXBUbjJBbFZ2R3g2SDZtVE1aS2NYbmppeDYrZTA4WmREY1ZHUHFFd09KNWpqQVpvKzFjTHowLzV3VG9MRzZpZ0FlWDEvZi9WVDhLdG9PQXVZSWFCbTA9PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWRLZXk+PC9kc2lnOktleUluZm8+CiAgIDx4ZW5jOkNpcGhlckRhdGE+CiAgICAgIDx4ZW5jOkNpcGhlclZhbHVlPkVOdGFJbDBVVndBcmpvNXo4Zm81d1d0MFExeWV5ekFZKzE3K0RzZktiM1NqTERuVjJiTXVOZHM2dFRRcGF2WnBEYU0rZW0xOVo1Q2JRbUlzVENON2xsVUxOUzJuTEFhckF4U2dJNGxkTitpZ1RIWTMxWDVzNUNMN1NacFBzVG11dWs2OUJlaHF2WGZrclBhRDlDMHpaTVlYcjllcEozbm9reXk2NnBrS3JlZ2dsZEpOem1leWc1SWdSWjdkbG1JQ2duUE1yUmRwZnNLMC9jUjkzbGw5d2hJVGNYV2l6VG9MWWtZMUkrSUptTWRORVk5a1F6L0hDVEQ0SUNsYmhEUzJwUXAxWG51YzM4Q05lTzU4Rk4vSHhVUnhRckRMUHFNUWV2Tlo0alBObktZdUQwcFlzbHl0V1U4aGd4dGtYWWZ6T3R5Nll2VFArUFhjdmhybUMzRHNZUXRDSURBU09nTENkQUtNVmxVc3owemxnbTZGalkwQitsc3ZTWE9sVko0OU83Ymx6Z3ZYKzNSZVVsSHB3RFZ4Nm5welVNWW5iWFJ3Ky9SSlNZeW01b2U5M1krMGttaEpkSzBiWlVMY1lOZWVnTWVtVXJyUjN3Q1dyeTZPZTZ1YUVxWVNFdXJrcytYS2hQdS9PS1dHOHZ5TWhPZ0FrTVdMbzQ0N1NGM2lPQTAydldYZUNVbSt0dEpxdmVwamVxOTJVZ1FoSDVjZWo2dW1Gb2lnSTlFOHJsZ0xnem1jYnd4aG14UWprOG5jaDc0d0FKbWFhT1JjNjYvRWtBK1hwUnN5SXhKWG51VmlLWWt3bjMzNjZJSTJaenpnUUU5UWhOM0lYbE1COHp5a2pFczl5aW43V0JqZy9Id0xwT1d5VG1NY2FMcDhMVmdTNlpXR29TY21Gc29jeDluQkNvSmJuU3NRanV2a3VPbVRtMHlORHFGRzNmVWRsWUlSdzh1QWlTM2ZweWttclJpd081V3BnWFRIMnBYUFFzWFpDdlltQXorbDJUUU5YendOejY2Y2FGck9wdm5MUW52bTJWRDg2a3JaaXhKRGRWZDZaOEhoRjJRSHpNamdLN1hCOG9wcjhwK2ZqN3BrY2JwenRaV2Y3RVNFUnNuWlBHT3VwWTlTLy9ZQjc0c0pDaTNhamczYmdsbEhIN0MrbGlnQWFkd0NDRjViWjRsT1RxY1M3VFVJMjlLNDRKMmZrS2VWeThBVnVZVXNxZ3NWSjd2WGtzNUljMnFoc1FSdStzY2NzOFR0Y0dYSFowalRhUkU4ei9JT1VmdXdCNUFLNC8vV1hNLzVZekEwWEhnSmpIUVpueHhieTdvdlNaYnNvYXVxRFMxTHJhOWtIUFhpZTlvZHlQbmpobExXb1d0WTBodW9EUUR0K3N1aHpyYTF4RzJ6MUh4UmEvRFZVREhKR2FCR29sMFRLQ0FmbXVkMUlqb05VVzdYVVN1d21VSVdVb0drbVNRTy9EYTVYQXhEV1lHVHVuMUdBcmdyTEwzVkJPNGJ6WEMybG1pNUl1OWhqQTJlZ3JRWTBQVit0WkJkTnpCdkxRYTY3RlJqR0hjd1lneDh6ZW9TSGVGRkVUcHBUeHIxWGdicHBVV1lLUjF2V1NvMjdhcXFjeVhTbnVHMUpCSmJOeEdDbjR4SU4yR2RMOXQ5VzNKa2NXUGRSeGE2K1ZuSyszbHpKNGVNNzBtekhaY2ZMQTJLaURYUmhjVG1oU1lGKzZJWThxREdYQXBWMXdld1BSRXZPNWozL21lOTdPVUlIY0cwaFRZTnhhbjhucG1XMmVMVU53ZnpjZE1JNzNhaUsrWk5FTXNkY1hRMnFMSittc0I0dzVYRHBNaHFxMGhYZ3NsZmF6eFZ0Q3E3VWJqS2tENlVRc1pWTHZZSTduYjJUOG9QMlVabUZFUlNDdU5sZys2M3JDTU1lOXd2NnZWMkdUM0RqZHU4Vi9IQzgrMUlXRUtaZEJOM3UvaVNUdEhZTnFtTUdCMVJ4Q0tRWVZrY3hDUzJRV0VrL2hoaGJndEJNOU5KY1BScHJqVS9oSHR6ZFB5UTNNckFLR05sRXBJeGpJNjRkbEdmall5T1JpWDZiTGs5aU5tdmJ4SXl6YnQxSzg0WlNsbTZJTDlXZDVUSmlMdGR5d2lqTlFQZkNVOUxSdkw4NUZYL1RtS3lqOENxenJXcGFZSEp6S0p6RUVUeFMwRldzMnJvcDJURXhrclFjWHlzUjQzNFhMUmE2SHRyVUNjMkNEV2g1dmkxMUF0ZUpCOHhzSVJFYXp1blQ3blBkb0I5NjNaMWFFa1VscTFjb0NieHZnTThxSS9nemZLUDVORmJPRHNJVFdMcVlvTHZaQ2pYSU9jYWdTWGt0Z240YkRGL3hlUG1vMHgxdXJlY0oyMHBJZ2tXM0J5RFFxOWc2YWk5NWdLY1ZZU3FhYXYxLzl1ZXoxK0VzWEZHYVJMU0o3ZTVWZTIxOW1mTGc5T1hrSldWdkZSeFFTVkROWkJWeWRqcUlZd013YjdaQXB4Zkp1OFJGSnQyNTdVT3VDeTBTZWFuRFBFa2M3NlZRekh6MmlBWkhvbitRQ2l4OUVXdXg1dWZEOWRmUmwzc05tOWljMmxUUFZRbm5Sb2NGTjRpdWFyZjQvVkp2dW1uQnlhdmV2Z3g3aXJwdFZmT3lZQWF2YkMzUzByREVaRms2VTBMZUNGd1pPalJjZEVhZDRjeEZYMEI4NndtYXVQSkF2ZTVmaXRJd29GNkpuUkZEK2x4c2N2RUxKYVlFVEF0RkFwN2ljS0JPTi9rNGdzeUs2TGd3NFEvcFNWWXducnBua2t3byszQXJSbUlwdTE5ZVowcHN1QjV6dnFJbkFTV09naGRsZHo4YWdQaXA0N3lSZisrcnE2NHp5RE5TZGVXSTM3ZUVWYkVpd1h5eHRSR3RFSWtRa2drdG05VEU5MCt5eHIrTVo0RmxQU0NKTFkwUDN6WGNFZ2hMZzJUUC84ZXBwUVljcUlJd0pCUWZ6S21CN3pta3FOd0ZZQkhVck05VVRCRlVMVkdhbXpEQnQ4UzNVc0g5OHV0MHlOeUxLSThueEh3V3lnSGFzaGUybVFpUlRDV1l4MGdUc2l5NTN0aVMySHpmRFZmTDM4U0VkNXFqUWUyR1ZVUTlJakt0N05mMUlSQ2dZblRYTVk3KzQ3eUpaYkVJcithZGRIRXFadk9mQWpKY2ZpcUc1eEp1MGhWV01vTThvemFkWkxCN2pUR2dGTERCTHdXTjBvVEVHNVo5Wm81SXlNYmVlWHYxZFc2ZWNLSjJkd2dnb1VxMytmWmN0NVcwbldERHhWSjZRdEQzbHFrZjdzNm9XdmFRcHhHL1ZnRXA4L1NKZnNSekNnbm1XWVFqUWtDclUxbldUREVHWkhuUGtpbUh5RHRaNUJ3REJGZVhVdHQ5R3Nwdk1CNHUwdjYzcmF6bVAwMVdiT2hBVDd0Y0dBQVZzM2ZhTGpvaEd0NTlraVljQkR0cEllZjAzdjBhT2VtOS9wTjNPYW1nM0dUTzc3MG1LS1dDTTNEM2JQejAzNng3cHpWZCtrSUtCZjFtMkF5WDFmMTRKS2VHQmJWUlAxcmIxaXh0OGVKdGVxSHdLRXd2NFRUKzhPeU5GR1F5eVNCQ2d0R1lzdjkrRm1KN2pNeWpNOHJHb2lUTmxQb0pCMlVDcERKT2M5SlVaOVVYa3k4THFCRE95d2pyTzBBL0h1UFdXWnc1RWEyUktkenFNQkxON2JXRktvREtsK3VIblJCcTRzcU5EWUhmeHEvUml5ejRsVlRXYlUvMVV6akpGSzF6UisyYlJzN1piOUtxbzU4ZHVHekV5V1Q5Tk94M0wvSEowUVNYQ0hRSk41YXRDZ2JNVEJTMEtGQmdLSGpTS1FiYWVYS0l6a3FJSkQ5US85ZkJaMGZuOVMwd2tTNlJ6Nk84SWpxWGpQMXdpT0RoT1VGVjdrRlJjSVdQM1ZIVTFtN0ZGcGJpY1RuREdvczBPV01pRWt3VW8vcWZTRFhOQ0FKNFY0b2x5M244bUZSL2JTaDZHRmJpc1VUTmJmQnBDcmZUeFRlaCtJNGFkVlU2dUIyaTl5ZmtWSVFaSjR5QnZ2M1c5NnlaV2dmcXh4M1BKN25GTW0wODdqb2FYMTcwM2tSL3NzbllEVHZFOXlCL1ZGS0J1VnkzQk5oREl2cVRXcDVlNFhJV0lUK0RYL1Uxb3FEb2lDNnVtcHZKUVJVY1R4UUx5RGp6ZEtmNW8yMzRGUkQ3d1NVQVpYa1B5UVR2SmdtVnJzbCtJdDNaazQ4MVhOd01wdG9iZWRHR0ZlUFJ4T3dyNWtQTlRYSHFxL2xkZmlCcENtQzJZQUxkZEV1NDExdk9TYXhUc2JqSGNaRDVlblE0cmR4K1RrelIzbGdkL2lVd0RHSUZpYnU5ZVdkWkx2YWZEV3gxOXJhUGxJRUl1TkJ4dGZsU0IyOWlVaEljeUxFQUtQUy90eTBvWGQ5VTQ2UkZscDA2UlNyY1ArVU1FQWxqN2dqamZuM3MxZmlRc1d1cnhqb2YxZDFad1EwVUFxdzRGSWxpaFpZZU40UVRXYVd6c2FJWXlNcWdmZjNqTVFTSFNMYVJLUHF1S1RFTEJJT0txaU9iVk1hVUdGRndWeGgvNC9rblVHa25LZFFzZTl5c0dJZzBlR2s0OXhRZy9RVkFDVU13N1lqQ2hVdnNqa3U5dkhQUks0Z0RwZWtzM0d3SDlDclVrRGV2cHk2NTZiZi96bUF4dlZnS3NVUU5HUVA1eXJ5dDd4cDB0OEpIbHQ5K054ZHYyL2tVbFpuQlQyUnh6Rm04bDBhWVFRR3ZFSGY4K25TNHl4ejRVMFMyY0dRb2xKYWRyM0l2em9DdndNNVNiNXV6MFJVNnU0cjg0TkV4cllxRDRORWRSeHhRelF4Tm91T2ZvMlRKRGFBUktUMXZndi9lS2hzeFo3bnRaRWx5K2pKTlN0ZHc5cXdmSG0wZVJwR2QxRTlFZytOZEF1cTRCR2ErYzBseEp6ZnU3R0VOVndsd0R2MkUrSjhUb2JpbktkaWlWdzg2V1VYVFFNZnp1Qkg4NmZhdzYyYTV5TXJSZnhEMnViQlQ4Y2FJMGM1aDRqc2hvNW9yZmpGb1RCOVQybldMc3puUTBqdktPREpuUjVQSHR2WkdiZGRaTHd3SHQ5N1RwWEVESUFiNEE3d3NvVlBjMGM1NEg1cDVFMFNqUFhJdEpEcHdSR1piSFN5TnduSm1WZU95YVhWaVdOZSt4NDd3eWttR24rUUViT2h6YUNJNERtRE1CRThDU2xJZzRCQWNwTVE5VjB0NW1FcDVWZDRaRXJwL0FXV1lTakFkVDg0WGFLR0R6cUJsYjBQc1J3YnhkL2M5NnEvTUtORHpLNU5DcHJnTEZHZTluSUkrOWVZeUg0L01mRnFZOWpuWVc2M3MyOTlQY3pWV00yRmFDS1JJL2dyRy9MWlZOaVdjdC9KT0JVQkpzaDYvbkZvc0pWbDdCdllZc3dLM0gzNHgvcnQxcFljV0M0WlhxSHZyRWNMVGFlcjRXZzFTZTZxN0V4UUZYZ21BTGtWNFNiTjQ4QUJHTGZhY2ZHUWRzTkQrWFlVaTdTY1YvYU9EL2tSNGZNeWhrN3RBTGNENENIQUNDRzJ1MW1lRmxvNngzcC83cmEvZm5UcVJoRXNwRjJtV2k3QjR2YUIrNkc1enVBQURtYmxlalNycE5UR1FoaFNyRHozcnlvYkpYb0ZiY2Q5SHZ6dVNYWU04dU1sUVdYOURWSzNrMkZvcFlyQytjTGE2K1VIdlZwdG14S2lpSGM4MFBhdUVXUUZxRnhPb3VwUGtIS0J4R0F2VnFnR2NGYW9Zb2ZUeld4QU5CNGN3ZVRNOWhyWTlieGdzRGZXUklUOEdOWCtNK2RQWWdwVjVLdm92M0s5dFJJZHVMbWVVMW5WVFI0NU11ZkJ6VG03d2ZZWmdoTndEdjExbUszYkZzcll0dmVWZFFQT1cwSkt0dHI2RFo3TW5GRkMra1FJZEJ4RXNxUGlXUEFsY21EZW5oVUxidTVxb2JUc2xiSkk5THlhUDZHNTQ3WkVmanVNNGlzemJHVHIrN0J6ZDdwR3FDa0lraHdWU1hjVmVweDVkUWNwNHdZSjVEZmRYUUZnaU9vMXd2UDBFNzhWVjhjc245NlNhaTBMWHErdHVET1U5UEFDb0hFaXdLNjVYenl6MSt6YWVNajkwWkRNN2pkQWx1MzJadmVRYWZQY3RVcWx6cUNPUXltRUZRUDhHczczV1J5aWhFRS84aEN3UW5rcm5VTktzUi9BendPcUFRaTJ4UzZsUWEvalBwTUFqcnVGNkdYaHpJR2l2RDMxVVRiOXVOQXdlUWpBT29UaEZINkxNc2hETC9WLzRpWEszeFl3cjVWQWUvb0FzVFJzS3VoZFBab0gxcW9ueE5yRlNxR29TclVEWVorUG45dmQ0cXB3R0RuQTRLWnpMYTFQdTYwVkxqWGR3M3ErZEdsV0RFVmVlRUljMjQyUnhxbVRHSWp1QnAwUGl3UGxpSC9VVC9xRWNUQmM4enlTYzVndXVMU1NsZHowMERubzhlMXZGdEJrMzlSbHZ2MmhJeUVaeFMzcGFNckRkRnRYczh5cWRIUTNWU3pyTGNHWTd3L01UNkc5ZUt4N1l2OVlrUGtaaWEzNGkyNmtwcEx3MDMvazdGUWt0U1QyUk1BWkEwa3BVNjBZNkJzNkE0TWFNcU5kN1VZSlpFNXlqek1aTU9iQWpFSW9UMXl2VFlNWVI5N0ZhK2d5Mzd6SDlzVHc1ckVna0xscmtJS2gzbThBK1ZIVi8vTGw0MHp0ZTNPN2dtbkFubldSRC8rbHBwSlJnMHFyR0xQMTRZcUN3NmpaamtpTjdJYmF4UTlsU0l0WTZKdDFDekU4NFhhWk54VXNDVk1JNGgzN21wN0FTK3ZCZWRDdEZ4QkJMeGxoMWt3R01PUW1qQnV6OVhwN3MxU2t3WkZyWlluOFN5T1VLdzZSekF5bUhqUDVGeVJ2MXhiUmVOODB6SFlpd2Y3RndlZEhEdG0yQlg4SVRIL1kxVnJjRXJqQVhack1iWXM2VWhxbXBKRTdJSkFFSktJdWVsMW1PNitoRE5uVXpnL29ScHB6YXBtdFdxamIxcHRVc0t5YWxycm8xd2ptb3lURjRvRFpWN1FnYThxSEthNURFcjRKVi9nMzNaK3VVbURkamRTYXZIa0FKYVY4RDV3NHFjRUhoVm4xMmpQZUtyVjlYMEg4alFuSS9TNEt5UWlhRzFvR0dNbFN6NGEzczdrcytianVKTFFzZ1BHK3ZaRFhHbEtxSnFIQ09UbXR3WENhcWdVQ1YxU2lDU0JmWVdya2trNjd4TFFQUkZxNDU2WVgvTS9RUlg4b3J6RE1POTd6V1loUW5BUHllVGx1VXQrWWUzNXdyencybmM3M0hNK0dHVEJHSW1NaWhCYzZFS2hUUjBYNFJTR1JsVllIUUp0S1FHV2Z4V1VGa3F4ZnFtTGNXUGhYTzM0UHVYekhzZTRrenlLRGJXaTgyWVhranRZbUo4YWpEWm9wL0NYelVmeGZkQ1diamkvUkZPQkhpb0Q0Y3prRjlTTlI5TWpJbjJtQkkyekxBRGI1NGhsN0pLWFg0Rmd2SVNVZWhNZXUzT1FOUmduV2h5eExLWFJsRnJIN1QwVnZlVFJuRWtTcWxOeEI2TXlwbDNDVUNndTF6Q1ZWL2ZlTWd3R2VzdjJXSi9tUytQV2ViOGRKTlMzL3lzRUFDc25FTFJtL05xUE1VQ2xNUGVCSS9rek9GS1pRZkNyQnlweEFSbHRodVFZPTwveGVuYzpDaXBoZXJWYWx1ZT4KICAgPC94ZW5jOkNpcGhlckRhdGE+CjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDpFbmNyeXB0ZWRBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4= \ No newline at end of file diff --git a/tests/data/responses/double_signed_response.xml.base64 b/tests/data/responses/double_signed_response.xml.base64 new file mode 100644 index 00000000..9e43ce67 --- /dev/null +++ b/tests/data/responses/double_signed_response.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZngxYmRkMzhjMS04OTljLWMyNTktZjU4Ni1hM2QzNjU3MWViZWYiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTIxVDEzOjQyOjMxWiIgRGVzdGluYXRpb249Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl8xOTFjMDNlNjhkNzFkOTc5NmY1ZTA3ZTYyNjJjYTRhZDg4M2E3NGIxIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZngxYmRkMzhjMS04OTljLWMyNTktZjU4Ni1hM2QzNjU3MWViZWYiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPnZqVjZNT1VsaWpXVEU1M3dac2N1Z0dZN05oRT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+RWJnWDZHenRYME9GNkpWZ0IyMElRMVBhUDYvMlpzanh6SHNFdHEzK0hMeG1ZbG5lYkVhalFIMzJWbTJ0UERwclRMdkRtaGZWd1NaZEtLaEFoNGtoc3YxODN1Q3pVdU5GTmZneWx0WVFmem91UlhGZ0ZOdUJKU1IwOTRkWFc2THVqRm5TbHk0VGNScThzSG5YZ0p6V3h1V0ltKzc3TjVOemtYajRxRXFSQXdVPTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGQzNGZiMGMzLTFkZmItY2EzZS1iMjYzLWEyYWFhMGJlZWRlNyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMjFUMTM6NDI6MzFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZnhkMzRmYjBjMy0xZGZiLWNhM2UtYjI2My1hMmFhYTBiZWVkZTciPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPmlUem5CamF3U09EUFZVRVAwVWpvMTdoM1RNWT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+TXB4Ny9LL29OUVlIbm15aVdDdEdod3FoVzZtOG5zcnhMYSs5LzQ2dTJuenU5NDMxdm1TQWZPQmF2RURjL3JnNGRYeTRWZmZyejNINFVvUEQyeFNpODFJOU1kVDl1WHRYQkJGUytVN0xIb3VyVjB4VFU0clhDajd3ZmdkbkpMVmprSnVUeWZRMXhsd2VJNzRLSUdLOGZvMVlaYmY5TGREZUE4TURCWDZXN3VBPTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWw6U3ViamVjdD48c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocCIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDp0cmFuc2llbnQiPl8yMTI2ZGQxOWI4YTlhMjgyMzhkODhmZGM3Mzg1ZTYwOTk1MDA0YTc3ODI8L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDktMjJUMTk6MDI6MzFaIiBSZWNpcGllbnQ9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl8xOTFjMDNlNjhkNzFkOTc5NmY1ZTA3ZTYyNjJjYTRhZDg4M2E3NGIxIi8+PC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+PC9zYW1sOlN1YmplY3Q+PHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTQtMDMtMjFUMTM6NDI6MDFaIiBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDktMjJUMTk6MDI6MzFaIj48c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjxzYW1sOkF1ZGllbmNlPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMy0yMVQxMzo0MTowOVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDMtMjFUMjE6NDI6MzFaIiBTZXNzaW9uSW5kZXg9Il9lNjU3OGQ2YWY5N2I5ZjdmMDY3MmQ4NTBkMjlkYjRhZGQxYTI4NmRjMjQiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0QGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj53YWEyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+ diff --git a/tests/data/responses/double_signed_response2.xml.base64 b/tests/data/responses/double_signed_response2.xml.base64 new file mode 100644 index 00000000..35c046d4 --- /dev/null +++ b/tests/data/responses/double_signed_response2.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfZTZkMzIxZGM1OGMyYTZkNjEzMTFhNTNkYTFkMjhiMzZkMjdiOWRhZGEzIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wOS0yM1QxMjo0NjozMVoiIERlc3RpbmF0aW9uPSJodHRwOi8vcHl0b29sa2l0LmNvbTo4MDAwLz9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNTJlOGNiZGM0OGNkNzdmZmM3MGI4ZWI2MTgxYmEwYTVjN2U1YTRiYyI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vaWRwLmV4YW1wbGUuY29tL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4KICA8ZHM6UmVmZXJlbmNlIFVSST0iI19lNmQzMjFkYzU4YzJhNmQ2MTMxMWE1M2RhMWQyOGIzNmQyN2I5ZGFkYTMiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPnBUTEcxYXliMTFvRjRJanorOC9OdW43NDdpND08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+dlM2NnB0L3QvMzlwWG9jd0ZneWRUWmg1eC9YN29tMFBnL2xoYXFEOWN3NzByWE5CNU1zbVZMR3BWSXZwQnd5WlkzS0t2ZUZNdDA5cTJCSkl1V0RsblVFOGhXR0k2STlFOGZpVXUvZ0VoRU4yRmx3NWdtK05BYXhWVHZuR1IrMkh5UmtYUjBCa0R3TngyVFM3bnZweFFEbzI1b25KUnhtenpPekxlZWlwQ2o0PTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNiRENDQWRXZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRMEZBREJUTVFzd0NRWURWUVFHRXdKMWN6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVWTUJNR0ExVUVDZ3dNVDI1bGJHOW5hVzRnU1c1ak1SZ3dGZ1lEVlFRRERBOXBaSEF1WlhoaGJYQnNaUzVqYjIwd0hoY05NVFF3T1RJek1USXlOREE0V2hjTk5ESXdNakE0TVRJeU5EQTRXakJUTVFzd0NRWURWUVFHRXdKMWN6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVWTUJNR0ExVUVDZ3dNVDI1bGJHOW5hVzRnU1c1ak1SZ3dGZ1lEVlFRRERBOXBaSEF1WlhoaGJYQnNaUzVqYjIwd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFPV0ErWUhVN2N2UE9yQk9meENzY3NZVEpCK2tIM01hQTlCRnJTSEZTK0tjUjZjdzdvUFNrdElKeFVndkRwUWJ0Zk5jT2tFL3R1T1BCRG9lY2g3QVhmdkg2ZDdCdzd4dFc4UFBKMm1CNUhuL0hHVzJyb1loeG1maDN0UjVTZHdONmk0RVJWRjhlTGt2d0NIc05ReUsyUmVmMERBSnZwQk5aTUhDcFMyNDkxNi9BZ01CQUFHalVEQk9NQjBHQTFVZERnUVdCQlE3Ny9xVmVpaWdmaFlESVRwbENOdEpLWlRNOERBZkJnTlZIU01FR0RBV2dCUTc3L3FWZWlpZ2ZoWURJVHBsQ050SktaVE04REFNQmdOVkhSTUVCVEFEQVFIL01BMEdDU3FHU0liM0RRRUJEUVVBQTRHQkFKTzJqLzF1TzgwRTVDMlBNNkZrOW16ZXJyYmt4bDdBWi9tdmxiT24rc05aRStWWjFBbnRZdUc4ZWtiSnBKdEcxWWZSZmM3RUE5bUV0cXZ2NGRodjd6Qnk0bks0OU9SK0twSUJqSXRXQjVrWXZycU1MS0JhMzJzTWJncXFVcWVGMUVOWEtqcHZMU3VQZGZHSlpBM2ROYS8rRHliOEdHcVdlNzA3ekx5YzVGOG08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpBc3NlcnRpb24geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiBJRD0iXzc2ZDEwMTAyOGY3MDRjNjJhOTkyNjg5MWE0YTFjOWNjM2QzMzJkMTI5YiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDktMjNUMTI6NDY6MzFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9pZHAuZXhhbXBsZS5jb20vc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPgogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPgogIDxkczpSZWZlcmVuY2UgVVJJPSIjXzc2ZDEwMTAyOGY3MDRjNjJhOTkyNjg5MWE0YTFjOWNjM2QzMzJkMTI5YiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+L25YbW1VenY3M0hlK1FUMS9nejdiM3JCRk5NPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5keG1SNWVMdTdya3kyZWo3MVVZbkpodUJSeXAxMENadGdSOTJFRjU5SzFlY1dzcHI0RlVHbFhON0hlcGN1RWUzcFN5NEs1Qno3MlV4bFhpR2JvKzlhSmVXTGZVNlVQMVBJYlR2cFBLT3ljdzZ0dE5zOWtDVUh1UTdGUGI3L2pTRzZHZE1wRkpoQmZoVjhXRS92ODBJSWwwNk4vMFpXK2E4QzRtQUxTMy9HRVU9PC9kczpTaWduYXR1cmVWYWx1ZT4KPGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJQ2JEQ0NBZFdnQXdJQkFnSUJBREFOQmdrcWhraUc5dzBCQVEwRkFEQlRNUXN3Q1FZRFZRUUdFd0oxY3pFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVZNQk1HQTFVRUNnd01UMjVsYkc5bmFXNGdTVzVqTVJnd0ZnWURWUVFEREE5cFpIQXVaWGhoYlhCc1pTNWpiMjB3SGhjTk1UUXdPVEl6TVRJeU5EQTRXaGNOTkRJd01qQTRNVEl5TkRBNFdqQlRNUXN3Q1FZRFZRUUdFd0oxY3pFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVZNQk1HQTFVRUNnd01UMjVsYkc5bmFXNGdTVzVqTVJnd0ZnWURWUVFEREE5cFpIQXVaWGhoYlhCc1pTNWpiMjB3Z1o4d0RRWUpLb1pJaHZjTkFRRUJCUUFEZ1kwQU1JR0pBb0dCQU9XQStZSFU3Y3ZQT3JCT2Z4Q3Njc1lUSkIra0gzTWFBOUJGclNIRlMrS2NSNmN3N29QU2t0SUp4VWd2RHBRYnRmTmNPa0UvdHVPUEJEb2VjaDdBWGZ2SDZkN0J3N3h0VzhQUEoybUI1SG4vSEdXMnJvWWh4bWZoM3RSNVNkd042aTRFUlZGOGVMa3Z3Q0hzTlF5SzJSZWYwREFKdnBCTlpNSENwUzI0OTE2L0FnTUJBQUdqVURCT01CMEdBMVVkRGdRV0JCUTc3L3FWZWlpZ2ZoWURJVHBsQ050SktaVE04REFmQmdOVkhTTUVHREFXZ0JRNzcvcVZlaWlnZmhZRElUcGxDTnRKS1pUTThEQU1CZ05WSFJNRUJUQURBUUgvTUEwR0NTcUdTSWIzRFFFQkRRVUFBNEdCQUpPMmovMXVPODBFNUMyUE02Rms5bXplcnJia3hsN0FaL212bGJPbitzTlpFK1ZaMUFudFl1Rzhla2JKcEp0RzFZZlJmYzdFQTltRXRxdnY0ZGh2N3pCeTRuSzQ5T1IrS3BJQmpJdFdCNWtZdnJxTUxLQmEzMnNNYmdxcVVxZUYxRU5YS2pwdkxTdVBkZkdKWkEzZE5hLytEeWI4R0dxV2U3MDd6THljNUY4bTwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL3B5dG9vbGtpdC5jb206ODAwMC9tZXRhZGF0YS8iIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6dW5zcGVjaWZpZWQiPjI1ZGRkN2QzNGE3ZDc5ZGI2OTE2NzYyNWNkYTU2YTMyMGFkZjI4NzY8L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMjQtMDMtMjZUMTg6MDY6MzFaIiBSZWNpcGllbnQ9Imh0dHA6Ly9weXRvb2xraXQuY29tOjgwMDAvP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl81MmU4Y2JkYzQ4Y2Q3N2ZmYzcwYjhlYjYxODFiYTBhNWM3ZTVhNGJjIi8+PC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+PC9zYW1sOlN1YmplY3Q+PHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTQtMDktMjNUMTI6NDY6MDFaIiBOb3RPbk9yQWZ0ZXI9IjIwMjQtMDMtMjZUMTg6MDY6MzFaIj48c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9weXRvb2xraXQuY29tOjgwMDAvbWV0YWRhdGEvPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wOS0yM1QxMjo0NjozMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDktMjNUMjA6NDY6MzFaIiBTZXNzaW9uSW5kZXg9Il9jZWYzYjIwNTViYTZhMTI1MmMyMjQ2YWJhN2UwNmJmMzQ1MDkwODBmYTMiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJwaG9uZSIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyIvPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/tests/data/responses/expired_response.xml.base64 b/tests/data/responses/expired_response.xml.base64 new file mode 100644 index 00000000..664ee302 --- /dev/null +++ b/tests/data/responses/expired_response.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZnhmYTk3ZWVkNS03NTg4LTBkMjMtMmFkNy1mYTY2ZjI4OTM3ODgiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjA1OjQ5WiIgRGVzdGluYXRpb249Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl9hZjNkNGE3MTBmYzhiMzA1ODg0Yjk2ZDAwOTRhYjYyODgwMmY1NjkyIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1scDpTdGF0dXM+PHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbHA6U3RhdHVzPjxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZng3ZTQ4ZmQ3NS1jZTJiLTYwZWQtMjllZS1lNzk4NDZjOTU5YzYiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjA1OjQ5WiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAxNC0wMi0xOVQwMToxMDo0OVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOX2FmM2Q0YTcxMGZjOGIzMDU4ODRiOTZkMDA5NGFiNjI4ODAyZjU2OTIiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTowNToxOVoiIE5vdE9uT3JBZnRlcj0iMjAxNC0wMi0xOVQwMToxMDo0OVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAyLTE4VDE5OjQyOjIwWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxNC0wMi0xOVQwOTowNTo0OVoiIFNlc3Npb25JbmRleD0iXzBmNGYxODhkYzFiZmQzYmZlYTM2YTE2MzRhNzQ0MTU4M2FmYzNiMzc4MSI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNtYXJ0aW5AeWFjby5lczwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+U2l4dG8zPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5NYXJ0aW4yPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file diff --git a/tests/data/responses/hmac1_response.xml.base64 b/tests/data/responses/hmac1_response.xml.base64 new file mode 100644 index 00000000..7be67630 --- /dev/null +++ b/tests/data/responses/hmac1_response.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDQyYmU0MGJmLTM5YzMtNzdmMC1jNmFlLThiZjJlMjNhMWEyZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+PFNpZ25hdHVyZSB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxTaWduZWRJbmZvPg0KICAgIDxDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPFNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNobWFjLXNoYTEiLz4NCiAgICA8UmVmZXJlbmNlIFVSST0iIj4NCiAgICAgIDxUcmFuc2Zvcm1zPg0KICAgICAgICA8VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz4NCiAgICAgICAgPFRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgICAgPC9UcmFuc2Zvcm1zPg0KICAgICAgPERpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+DQogICAgICA8RGlnZXN0VmFsdWU+eGFSZDN2WWxLaVB5TlBJTVhWYlVQVEhjQ1JzPTwvRGlnZXN0VmFsdWU+DQogICAgPC9SZWZlcmVuY2U+DQogIDwvU2lnbmVkSW5mbz4NCiAgPFNpZ25hdHVyZVZhbHVlPlhrM2lCRHoxN0QzM05MMS9qUm9lR2M5enhtcz08L1NpZ25hdHVyZVZhbHVlPg0KPEtleUluZm8+PFg1MDlEYXRhPjxYNTA5Q2VydGlmaWNhdGU+TUlJQ2dUQ0NBZW9DQ1FDYk9scldEZFg3RlRBTkJna3Foa2lHOXcwQkFRVUZBRENCaERFTE1Ba0dBMVVFQmhNQ1RrOHhHREFXQmdOVkJBZ1REMEZ1WkhKbFlYTWdVMjlzWW1WeVp6RU1NQW9HQTFVRUJ4TURSbTl2TVJBd0RnWURWUVFLRXdkVlRrbE9SVlJVTVJnd0ZnWURWUVFERXc5bVpXbGtaUzVsY214aGJtY3VibTh4SVRBZkJna3Foa2lHOXcwQkNRRVdFbUZ1WkhKbFlYTkFkVzVwYm1WMGRDNXViekFlRncwd056QTJNVFV4TWpBeE16VmFGdzB3TnpBNE1UUXhNakF4TXpWYU1JR0VNUXN3Q1FZRFZRUUdFd0pPVHpFWU1CWUdBMVVFQ0JNUFFXNWtjbVZoY3lCVGIyeGlaWEpuTVF3d0NnWURWUVFIRXdOR2IyOHhFREFPQmdOVkJBb1RCMVZPU1U1RlZGUXhHREFXQmdOVkJBTVREMlpsYVdSbExtVnliR0Z1Wnk1dWJ6RWhNQjhHQ1NxR1NJYjNEUUVKQVJZU1lXNWtjbVZoYzBCMWJtbHVaWFIwTG01dk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRRGl2YmhSN1A1MTZ4L1MzQnFLeHVwUWUwTE9Ob2xpdXBpQk9lc0NPM1NIYkRybDMrcTlJYmZuZm1FMDRyTnVNY1BzSXhCMTYxVGREcEllc0xDbjdjOGFQSElTS090UGxBZVRaU25iOFFBdTdhUmpacTMrUGJyUDV1VzNUY2ZDR1B0S1R5dEhPZ2UvT2xKYm8wNzhkVmhYUTE0ZDFFRHdYSlcxclJYdVV0NEM4UUlEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRHQkFDRFZmcDg2SE9icVkrZThCVW9XUTkrVk1ReDFBU0RvaEJqd09zZzJXeWtVcVJYRitkTGZjVUg5ZFdSNjNDdFpJS0ZEYlN0Tm9tUG5RejduYksrb255Z3dCc3BWRWJuSHVVaWhacTNaVWRtdW1RcUN3NFV2cy8xVXZxM29yT28vV0pWaFR5dkxnRlZLMlFhclE0LzY3T1pmSGQ3UitQT0JYaG9waFNNdjFaT288L1g1MDlDZXJ0aWZpY2F0ZT48L1g1MDlEYXRhPjwvS2V5SW5mbz48L1NpZ25hdHVyZT48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpBc3NlcnRpb24geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiBJRD0icGZ4NTdkZmRhNjAtYjIxMS00Y2RhLTBmNjMtNmQ1ZGViNjllNWJiIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiPjxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+PHNhbWw6U3ViamVjdD48c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJodHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj5hdHRhY2tlckBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjA1NC0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjA1NC0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwNTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hdHRhY2tlckBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+QXR0YWNrZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4= diff --git a/tests/data/responses/invalids/bad_reference.xml.base64 b/tests/data/responses/invalids/bad_reference.xml.base64 new file mode 100644 index 00000000..c8246810 --- /dev/null +++ b/tests/data/responses/invalids/bad_reference.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZngwNWYzY2UxMC0xNjE1LWYzZWEtYTk4OC02MGUzODBiMzI5OWYiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgRGVzdGluYXRpb249Imh0dHBzOi8vZXhhbXBsZS5jb20vbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl81ZmU5ZDZlNDk5YjJmMDkxMzIwNmFhYjNmNzE5MTcyOTA0OWJiODA3Ij48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9leGFtcGxlLmNvbS9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZngwNWYzY2UxMC0xNjE1LWYzZWEtYTk4OC02MGUzODBiMzI5OWYiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPmpBZ290RjBKK1JLMS9LODd3MjRNTUMyK1pScz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+cFhmM3Z3T1p2dGZtZjdNTWNPU0cwMzkyU213bnBJb0FqZ2dzVmErUlNJRE1Td0tTckwzcWw3SnlZQjVTaXZxL0xYODlUYXF5WTJ4MFBnTWl4YXY0bjFHMTFDM3NtbFJBTXJEZTZ2UnRJbUpVc2xTR2s5N3pQaHlvUStKNW9nUVBkNlZsTVR6OEtXemZxdE9QRGY1ZGwyWXlKcG9rZU9OVE0xemM0SkdNM0dBPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1scDpTdGF0dXM+PHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbHA6U3RhdHVzPjxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZnhiNGVjOWM4YS00OGViLWZkYTItN2Y3NC1mYTFhMTA1YTk5ZmUiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vZXhhbXBsZS5jb20vc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4NCiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4NCiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+DQogIDxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4YjRlYzljOGEtNDhlYi1mZGEyLTdmNzQtZmExYTEwNWE5OWZlIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPjxkczpEaWdlc3RWYWx1ZT5obnBUOWN1ZTZ0Qks0SVpPajUwZU1tM21DZ0k9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPnJBMGZMWjdLa2wzSndzTEtYRHdhRmNnNkxuS0hSeklVcng2bU5YMVRvOXpNQ3ZDVUQvb1BZVmFFZU10bnhZWTQ2ZnNtYU52c0tpdWdpeGRWekxMMzhLbUFZOHVNVGV6aEd2ejJWZUhVb3I0dTRoeGZRbHNPdDJmdXpBRTZEYnF2SVZLMHdqeDNSWTA4WC91T0NSOXVQY3B5NEJCc1EzeFBsNFF2aXBKalNOTT08L2RzOlNpZ25hdHVyZVZhbHVlPg0KPGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJQ2dUQ0NBZW9DQ1FDYk9scldEZFg3RlRBTkJna3Foa2lHOXcwQkFRVUZBRENCaERFTE1Ba0dBMVVFQmhNQ1RrOHhHREFXQmdOVkJBZ1REMEZ1WkhKbFlYTWdVMjlzWW1WeVp6RU1NQW9HQTFVRUJ4TURSbTl2TVJBd0RnWURWUVFLRXdkVlRrbE9SVlJVTVJnd0ZnWURWUVFERXc5bVpXbGtaUzVsY214aGJtY3VibTh4SVRBZkJna3Foa2lHOXcwQkNRRVdFbUZ1WkhKbFlYTkFkVzVwYm1WMGRDNXViekFlRncwd056QTJNVFV4TWpBeE16VmFGdzB3TnpBNE1UUXhNakF4TXpWYU1JR0VNUXN3Q1FZRFZRUUdFd0pPVHpFWU1CWUdBMVVFQ0JNUFFXNWtjbVZoY3lCVGIyeGlaWEpuTVF3d0NnWURWUVFIRXdOR2IyOHhFREFPQmdOVkJBb1RCMVZPU1U1RlZGUXhHREFXQmdOVkJBTVREMlpsYVdSbExtVnliR0Z1Wnk1dWJ6RWhNQjhHQ1NxR1NJYjNEUUVKQVJZU1lXNWtjbVZoYzBCMWJtbHVaWFIwTG01dk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRRGl2YmhSN1A1MTZ4L1MzQnFLeHVwUWUwTE9Ob2xpdXBpQk9lc0NPM1NIYkRybDMrcTlJYmZuZm1FMDRyTnVNY1BzSXhCMTYxVGREcEllc0xDbjdjOGFQSElTS090UGxBZVRaU25iOFFBdTdhUmpacTMrUGJyUDV1VzNUY2ZDR1B0S1R5dEhPZ2UvT2xKYm8wNzhkVmhYUTE0ZDFFRHdYSlcxclJYdVV0NEM4UUlEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRHQkFDRFZmcDg2SE9icVkrZThCVW9XUTkrVk1ReDFBU0RvaEJqd09zZzJXeWtVcVJYRitkTGZjVUg5ZFdSNjNDdFpJS0ZEYlN0Tm9tUG5RejduYksrb255Z3dCc3BWRWJuSHVVaWhacTNaVWRtdW1RcUN3NFV2cy8xVXZxM29yT28vV0pWaFR5dkxnRlZLMlFhclE0LzY3T1pmSGQ3UitQT0JYaG9waFNNdjFaT288L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vZXhhbXBsZS5jb20vbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9leGFtcGxlLmNvbS9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9leGFtcGxlLmNvbS9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxNC0wMi0xOVQwOTozNzowMVoiIFNlc3Npb25JbmRleD0iXzYyNzNkNzdiOGNkZTBjMzMzZWM3OWQyMmE5ZmEwMDAzYjlmZTJkNzVjYiI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5leGFtcGxldXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyQGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5UZXN0PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5Vc2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+ 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..d4359ec9 --- /dev/null +++ b/tests/data/responses/invalids/duplicated_attributes.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGQzMGUzYTJlLTllMTAtODg2My04Y2MyLWM0N2M1ZTlmMTkyYiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMjFUMTM6NDI6MzFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzE5MWMwM2U2OGQ3MWQ5Nzk2ZjVlMDdlNjI2MmNhNGFkODgzYTc0YjEiPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeGQzMGUzYTJlLTllMTAtODg2My04Y2MyLWM0N2M1ZTlmMTkyYiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+NkkreTZCVnVjTWo1SjVRbm45bzNnTHU0dVMwPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5vTWIzemJYZS9HdlFuampDTmk0Y0x2cEtGdmZEeEc2REgyT05GMUVuVTV0eU1zTXFyTDAzbVNKN0czTXcyaWtmWmRpRUN6SjJXSGZ6K013TGQzK1JGTnBGWGwvYW5aR1pDcnkzNTZ5eW1WVllkWkp1YldFdFBRaXRPdGRndlBxZGI3UGM2Q1BJYXJ1T01nRDk1TXpLdVZSYVkxc1NKUlNOM3BYeExhWml3MTQ9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDExZjAwNWQ3LTlhMjMtZDI5Yi0xN2Y1LTQwNjAxMDdiYzEwZCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMjFUMTM6NDI6MzFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZngxMWYwMDVkNy05YTIzLWQyOWItMTdmNS00MDYwMTA3YmMxMGQiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPjdoMlNLb1JhMG1QdHYvTnFHWGFXc3Y1SEIvdz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+b1ZscFpBc1g5cituUzBUek4zTFhzNkhDNVFWWTJNaFNaNE1CYkRIblNwbGNGU3VNZW9lcU94MGZxNUFQK1pFYzhVVG9VTkVZYUZmQ2N4NzhpZ042SDdGQzFvM0JQS1FkbnZwWmpkSGlMS3QyUno1Q3hVbHpIZzJCSGd1aGR5RW5RSUZEa00wK2huQkNuRG5oWWhnVG52YmI5VEx4TmFaUU5nQS96QU90TW9jPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6dHJhbnNpZW50Ij5fMjEyNmRkMTliOGE5YTI4MjM4ZDg4ZmRjNzM4NWU2MDk5NTAwNGE3NzgyPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyOTkzLTA5LTIyVDE5OjAyOjMxWiIgUmVjaXBpZW50PSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fMTkxYzAzZTY4ZDcxZDk3OTZmNWUwN2U2MjYyY2E0YWQ4ODNhNzRiMSIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAzLTIxVDEzOjQyOjAxWiIgTm90T25PckFmdGVyPSIyOTkzLTA5LTIyVDE5OjAyOjMxWiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDMtMjFUMTM6NDE6MDlaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyOTkzLTAzLTIxVDIxOjQyOjMxWiIgU2Vzc2lvbkluZGV4PSJfZTY1NzhkNmFmOTdiOWY3ZjA2NzJkODUwZDI5ZGI0YWRkMWEyODZkYzI0Ij48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3Q8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0Mjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0QGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj53YWEyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+ 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..74a9f5e5 --- /dev/null +++ b/tests/data/responses/invalids/empty_destination.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDE2MGI1ODI1LWUwNmItZWM0Yy04MGYyLTM3YTRjY2ZiNmY5OSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDE2MGI1ODI1LWUwNmItZWM0Yy04MGYyLTM3YTRjY2ZiNmY5OSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+WUlJVmNQU2VwbUVvVGVxYlcyeldWbnBtanhnPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5wR0p1dDJPMGZPRktJam5BQWpSbDgxUlpWMHFRVm1jUzRyNnY0TEpNQnlTY25rNjJBQ0oxT3k1eUkyblU4cnJhRERmOWZBaTBEazFscWE5Y3JVMFNScGZMeFQwalYvWHFzejBNblFRTi9FdS9KL0Z1Zi9MbWNaTDN0VjMrZkZXc0w1ZEg1RTNtcXZiK25tYXBzN3MrVGo0ZzBiV0x6TkN2Z01SdWdkV3JKRWs9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDdiZTU1OTliLTc3ZTEtNTE1OS1lNWFkLTc3NzEyMTBmN2M2NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng3YmU1NTk5Yi03N2UxLTUxNTktZTVhZC03NzcxMjEwZjdjNjciPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPjlXK0lTTU52Nk4xdWJNemVCZDhtd3dQTVEzMD08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+TVhPMEFPbWFxL1owZnhTOTNZMThObkdidWhIbms0bk82MHFEaWNIN3kwQzFlUU9BdkJSYTIzZjVMSjRBVVc4OEZ1UFZ6bHNkb2g1alhsa2g4RUxmL1RvWEIzR01WS1VYSk13ek1iR1ZPcEl3S2tXWE5aKzdaT0J1ZzZlS0tmNGFMK1FZMENiamxDdGxRRTRIQXVhZ1RWaEtXZGh4Tmw5ZVVkUklCM24xVXhnPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjk5My0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjk5My0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjI5OTMtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== 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..35115f56 --- /dev/null +++ b/tests/data/responses/invalids/empty_nameid.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDlhMWFkNWU0LWMyZTQtMzYxZS04Y2ZlLWVjZWUwNTQwOGUwOCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDlhMWFkNWU0LWMyZTQtMzYxZS04Y2ZlLWVjZWUwNTQwOGUwOCI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+M2ZsdVpsMWdxY01WSWVsamtqYllWS2k2MDFrPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT54cmhEYS95Z25kSzRFY1VVSCtTVXdUT0xlU3ZWbE1OSUI2MXY3bm1zcVJld1NmSkxHWVU3UVdidFFvVWUzSGdlS1BqK1FlUktKbHZ1ZG1qSWY5TFp5VlBvazYvTEpsT01HemEzbHZPUGVBSzJ1bmtFbW8wY1VCVHZNUGJ1V3gxdUIzOVJhazFLSE5WYzB5Z1FOU1h2dFZ6ZU4vQVNOZFNmeTd0Si9jZEM0OWc9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDgxYWNhMTc4LTUxYjEtMGRlMS00MDM1LTg5NmI5ZDNhYzFmZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng4MWFjYTE3OC01MWIxLTBkZTEtNDAzNS04OTZiOWQzYWMxZmUiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPkpqRllNVmkwNTMrSWR6czBnU0NXelg1R0w0MD08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+MVVxOUVNMTRUak5yQWxGaUhQTzdoRU5BZjNIejV4UUNra255TjZGMjdacHBmUnE5VzAzbDVKRnIrdk56WC9KZnBjdUxnNzdXZHhoVXdaeGtESzk0djlMbE1yK1lBSmtUeUhxUFAzYXZNWG5BcW0vaG9CeG11Skw4aXpTS2xITWU1NFA1Y1R3QVlrZ1Y0dW1XNGxpaHE1bUI1bU1DTWI2b0theU1pT0Niekw0PTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIi8+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjk5My0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjk5My0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjk5My0wMi0xOVQwOTozNzowMVoiIFNlc3Npb25JbmRleD0iXzYyNzNkNzdiOGNkZTBjMzMzZWM3OWQyMmE5ZmEwMDAzYjlmZTJkNzVjYiI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNtYXJ0aW5AeWFjby5lczwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+U2l4dG8zPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5NYXJ0aW4yPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+ diff --git a/tests/data/responses/invalids/encrypted_attrs.xml.base64 b/tests/data/responses/invalids/encrypted_attrs.xml.base64 new file mode 100644 index 00000000..a170970a --- /dev/null +++ b/tests/data/responses/invalids/encrypted_attrs.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaGh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocCIgSW5SZXNwb25zZVRvPSJfNTdiY2JmNzAtN2IxZi0wMTJlLWM4MjEtNzgyYmNiMTNiYjM4Ii8+DQogICAgICA8L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0wNi0xN1QxNDo1Mzo0NFoiIE5vdE9uT3JBZnRlcj0iMjA5OS0wNi0xN1QxNDo1OToxNFoiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPg0KICAgICAgPC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgPC9zYW1sOkNvbmRpdGlvbnM+DQogICAgPHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDExLTA2LTE3VDE0OjU0OjA3WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QyMjo1NDoxNFoiIFNlc3Npb25JbmRleD0iXzUxYmUzNzk2NWZlYjU1NzlkODAzMTQxMDc2OTM2ZGMyZTlkMWQ5OGViZiI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KIDxzYW1sOkVuY3J5cHRlZEF0dHJpYnV0ZSB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj4NCiAgICAgICAgICA8eGVuYzpFbmNyeXB0ZWREYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyIgVHlwZT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjRWxlbWVudCIgSWQ9Il9GMzk2MjVBRjY4QjRGQzA3OENDNzU4MkQyOEQwNUQ5QyI+DQogICAgICAgICAgICA8eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjYWVzMjU2LWNiYyIvPg0KICAgICAgICAgICAgPGRzOktleUluZm8geG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICAgICAgICAgICAgICA8eGVuYzpFbmNyeXB0ZWRLZXk+DQogICAgICAgICAgICAgICAgPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIi8+DQogICAgICAgICAgICAgICAgPGRzOktleUluZm8geG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogICAgICAgICAgICAgICAgICA8ZHM6S2V5TmFtZT42MjM1NWZiZDFmNjI0NTAzYzVjOTY3NzQwMmVjY2EwMGVmMWY2Mjc3PC9kczpLZXlOYW1lPg0KICAgICAgICAgICAgICAgIDwvZHM6S2V5SW5mbz4NCiAgICAgICAgICAgICAgICA8eGVuYzpDaXBoZXJEYXRhPg0KICAgICAgICAgICAgICAgICAgPHhlbmM6Q2lwaGVyVmFsdWU+SzBtQkx4Zkx6aUtWVUtFQU9ZZTdENnVWU0NQeTh2eVdWaDNSZWNuUEVTKzhRa0FoT3VSU3VFL0xRcEZyMGh1SS9pQ0V5OXBkZTFRZ2pZREx0akhjdWpLaTJ4R3FXNmprWFcvRXVLb21xV1BQQTJ4WXMxZnBCMXN1NGFYVU9RQjZPSjcwL29EY09zeTgzNGdoRmFCV2lsRThmcXlEQlVCdlcrMkl2YU1VWmFid04vczltVmtXek0zcjMwdGxraExLN2lPcmJHQWxkSUh3RlU1ejdQUFI2Uk8zWTNmSXhqSFU0ME9uTHNKYzN4SXFkTEgzZlhwQzBrZ2k1VXNwTGRxMTRlNU9vWGpMb1BHM0JPM3p3T0FJSjhYTkJXWTV1UW9mNktyS2JjdnRaU1kwZk12UFloWWZOanRSRnk4eTQ5b3ZMOWZ3akNSVERsVDUrYUhxc0NUQnJ3PT08L3hlbmM6Q2lwaGVyVmFsdWU+DQogICAgICAgICAgICAgICAgPC94ZW5jOkNpcGhlckRhdGE+DQogICAgICAgICAgICAgIDwveGVuYzpFbmNyeXB0ZWRLZXk+DQogICAgICAgICAgICA8L2RzOktleUluZm8+DQogICAgICAgICAgICA8eGVuYzpDaXBoZXJEYXRhPg0KICAgICAgICAgICAgICA8eGVuYzpDaXBoZXJWYWx1ZT5aekN1NmF4R2dBWVpIVmY3N05YOGFwWktCL0dKRGV1VjZiRkJ5QlMwQUlnaVhrdkRVQW1MQ3BhYlRBV0JNK3l6MTlvbEE2cnJ5dU9mcjgyZXYyYnpQTlVSdm00U1l4YWh2dUw0UGlibjV3Smt5MEJsNTRWcW1jVStBcWowZEF2T2dxRzF5M1g0d085bjliUnNUdjY5MjFtMGVxUkFGcGg4a0s4TDloaXJLMUJ4WUJZajJSeUZDb0ZEUHhWWjV3eXJhM3E0cW1FNC9FTFFwRlA2bWZVOExYYjB1b1dKVWpHVWVsUzJBYTdiWmlzOHpFcHdvdjRDd3RsTmpsdFFpaDRtdjd0dENBZllxY1FJRnpCVEIrREFhMCtYZ2d4Q0xjZEIzK21RaVJjRUNCZndISEo3Z1JtbnVCRWdlV1QzQ0dLYTNOYjdHTVhPZnV4RktGNXBJZWhXZ28za2ROUUxhbG9yOFJWVzZJOFAvSThmUTMzRmUrTnNIVm5KM3p3U0EvL2E8L3hlbmM6Q2lwaGVyVmFsdWU+DQogICAgICAgICAgICA8L3hlbmM6Q2lwaGVyRGF0YT4NCiAgICAgICAgICA8L3hlbmM6RW5jcnlwdGVkRGF0YT4NCiAgICAgICAgPC9zYW1sOkVuY3J5cHRlZEF0dHJpYnV0ZT4NCiAgICA8L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICA8L3NhbWw6QXNzZXJ0aW9uPg0KPC9zYW1scDpSZXNwb25zZT4= \ No newline at end of file diff --git a/tests/data/responses/invalids/encrypted_nameID_without_EncMethod.xml.base64 b/tests/data/responses/invalids/encrypted_nameID_without_EncMethod.xml.base64 new file mode 100644 index 00000000..b845dbc2 --- /dev/null +++ b/tests/data/responses/invalids/encrypted_nameID_without_EncMethod.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZng4NDMwYjFkZS1mNTU0LTQ5M2YtNjMyZS00YTIxZTBhMGRkZDQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTA5VDEyOjIzOjM3WiIgRGVzdGluYXRpb249Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl9iZjM3MmI5ZDY3ZDBjODlkMGNmMWFmM2ZmNjI1ZWE3YzA1MWM5ODg1Ij48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng4NDMwYjFkZS1mNTU0LTQ5M2YtNjMyZS00YTIxZTBhMGRkZDQiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPkhENDgxVEsxVmtPMGVaNGUxL2Vma1VJTVVCaz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+bWZrcnh5SlU4MG1hMWwwQ2xUU2tObTgzajVZQ0Y3YUt2bDJqRmVsVU04M3RuaitwNDB1RXRITjkrbHBJQVgrZEZkNnNCR3JEdUk0Q21lOTZuQ2lDK05mb2Y5MnpYaUcrWExCbFR2K0Z0cWxaZUgxc0ZNTFhuOWwxTHNLT0dPbjM5alA4bklUSGtYL0VkYjRucENNZlpQeWZ2b3M1dkMwbUtnWlFKZ1JtcG5vPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1scDpTdGF0dXM+PHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbHA6U3RhdHVzPjxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZnhkMDhjOTU2Ny1lNzUxLTY2ODItMGEwYS1hNDUwZDZmMTUxZTAiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTA5VDEyOjIzOjM3WiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4NCiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4NCiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+DQogIDxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4ZDA4Yzk1NjctZTc1MS02NjgyLTBhMGEtYTQ1MGQ2ZjE1MWUwIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPjxkczpEaWdlc3RWYWx1ZT42alZGMVZEMGJwVW1BVm1ERzlLeGxqMmFOUTA9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPlU3YTdFVWdxeDFiRC9ZaW9uTldPVWVMMHRJR0dRSU5ad1V6N1FBODNhYmxlVnE5dGtKbUsvWm8wak10MnluWHgxelA4K0M1bjNDRTY4TnZ4and5S2RFQUExUjAwblpneDRMR0JnZWl2WVBrazRBb0p6Q0RBbE11M1VLelZ0SU90dTV1VjFNMmVGc0NJY2RUcXBRMGt2b2pVQUVZWVhObSt6Y2xkcnVZWGQ1dz08L2RzOlNpZ25hdHVyZVZhbHVlPg0KPGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJQ2dUQ0NBZW9DQ1FDYk9scldEZFg3RlRBTkJna3Foa2lHOXcwQkFRVUZBRENCaERFTE1Ba0dBMVVFQmhNQ1RrOHhHREFXQmdOVkJBZ1REMEZ1WkhKbFlYTWdVMjlzWW1WeVp6RU1NQW9HQTFVRUJ4TURSbTl2TVJBd0RnWURWUVFLRXdkVlRrbE9SVlJVTVJnd0ZnWURWUVFERXc5bVpXbGtaUzVsY214aGJtY3VibTh4SVRBZkJna3Foa2lHOXcwQkNRRVdFbUZ1WkhKbFlYTkFkVzVwYm1WMGRDNXViekFlRncwd056QTJNVFV4TWpBeE16VmFGdzB3TnpBNE1UUXhNakF4TXpWYU1JR0VNUXN3Q1FZRFZRUUdFd0pPVHpFWU1CWUdBMVVFQ0JNUFFXNWtjbVZoY3lCVGIyeGlaWEpuTVF3d0NnWURWUVFIRXdOR2IyOHhFREFPQmdOVkJBb1RCMVZPU1U1RlZGUXhHREFXQmdOVkJBTVREMlpsYVdSbExtVnliR0Z1Wnk1dWJ6RWhNQjhHQ1NxR1NJYjNEUUVKQVJZU1lXNWtjbVZoYzBCMWJtbHVaWFIwTG01dk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRRGl2YmhSN1A1MTZ4L1MzQnFLeHVwUWUwTE9Ob2xpdXBpQk9lc0NPM1NIYkRybDMrcTlJYmZuZm1FMDRyTnVNY1BzSXhCMTYxVGREcEllc0xDbjdjOGFQSElTS090UGxBZVRaU25iOFFBdTdhUmpacTMrUGJyUDV1VzNUY2ZDR1B0S1R5dEhPZ2UvT2xKYm8wNzhkVmhYUTE0ZDFFRHdYSlcxclJYdVV0NEM4UUlEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRHQkFDRFZmcDg2SE9icVkrZThCVW9XUTkrVk1ReDFBU0RvaEJqd09zZzJXeWtVcVJYRitkTGZjVUg5ZFdSNjNDdFpJS0ZEYlN0Tm9tUG5RejduYksrb255Z3dCc3BWRWJuSHVVaWhacTNaVWRtdW1RcUN3NFV2cy8xVXZxM29yT28vV0pWaFR5dkxnRlZLMlFhclE0LzY3T1pmSGQ3UitQT0JYaG9waFNNdjFaT288L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbDpTdWJqZWN0PjxzYW1sOkVuY3J5cHRlZElEPjx4ZW5jOkVuY3J5cHRlZERhdGEgeG1sbnM6eGVuYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjIiB4bWxuczpkc2lnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIiBUeXBlPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNFbGVtZW50Ij48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjYWVzMTI4LWNiYyIvPjxkc2lnOktleUluZm8geG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PHhlbmM6RW5jcnlwdGVkS2V5Pjx4ZW5jOkNpcGhlckRhdGE+PHhlbmM6Q2lwaGVyVmFsdWU+RjJQVjV4TjVFM3EzODFJV2Qya25CdjAvUEpPN1ExR01oMGhkUm1USUxSYnJGZ0toNlFtN2VoTFlBS1B5QW52b0lFRDZSK0g4VkhweEtsZ2lybm9xNWYvSUxCajhOV1FpOEFKV21qZkMyWTBxQ3duOHJxVFRWQzVlZFhFZUMzU094ZHBTbzNzWGlOakhKUms3VndRUEV5RXNNeWcvSkN1cytGeUJYbXRickMwPTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHNpZzpLZXlJbmZvPg0KICAgPHhlbmM6Q2lwaGVyRGF0YT4NCiAgICAgIDx4ZW5jOkNpcGhlclZhbHVlPmxuMytVMVZNMmNDQjVOMXdIcmczaWVMMzRUSDdFdC9EZVhvK0FOUkdETnY5eG1GT3N1YUpuZzB1L1dseXpGNE10cWxpZWJEZUNCNVNZVE9FRHE0VGxVUno2QVhIcEg1UTNZa2ErbmttZXlhYytWc2lyNHB6eXBuUmgvb0x5ZFFXd01xSWZNQjAzeGtzeXhJUzVCTmRLNGNielprdnRsa1gvRmtGdlRCcktTMGxTUDgzZWVXaXhJNE16WXZBNW0rVHVJemNFdCtRR29zQVBnSkJ0REhMR1hpSWF6dFJYYUJLNXR5TEtwN0IydUdXK1ZjYlpRNWZlV2VRNW1DSnhYUFhiMnQ5c0xDUTFFRUFDZ05CK0kwYWJ6MWFLWXhOK0Vqd1B3akNVUVpmcWdiOVVjTFBFcENqcXNyR0QwWk9JbFBiMlFyYmM0cVZzY0xpSVZENm9pbEZYUzlDOHNtMDd2YklPMDdJSGhTWi9Gb2MrcDNsKy9BVFVvQjh5NzVISnZPMTwveGVuYzpDaXBoZXJWYWx1ZT4NCiAgIDwveGVuYzpDaXBoZXJEYXRhPg0KPC94ZW5jOkVuY3J5cHRlZERhdGE+PC9zYW1sOkVuY3J5cHRlZElEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDktMTBUMTc6NDM6MzdaIiBSZWNpcGllbnQ9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl9iZjM3MmI5ZDY3ZDBjODlkMGNmMWFmM2ZmNjI1ZWE3YzA1MWM5ODg1Ii8+PC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+PC9zYW1sOlN1YmplY3Q+PHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTQtMDMtMDlUMTI6MjM6MDdaIiBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDktMTBUMTc6NDM6MzdaIj48c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjxzYW1sOkF1ZGllbmNlPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMy0wOVQxMjoyMzozN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDMtMDlUMjA6MjM6MzdaIiBTZXNzaW9uSW5kZXg9Il85NDRiZmNhY2IwZDgzMmIxMmU0YmNmNzc0ZTAyYmJlNWY2NDU1YzY4MDMiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0QGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj53YWEyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+ diff --git a/tests/data/responses/invalids/encrypted_nameID_without_keyinfo.xml.base64 b/tests/data/responses/invalids/encrypted_nameID_without_keyinfo.xml.base64 new file mode 100644 index 00000000..bb020beb --- /dev/null +++ b/tests/data/responses/invalids/encrypted_nameID_without_keyinfo.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZng4NDMwYjFkZS1mNTU0LTQ5M2YtNjMyZS00YTIxZTBhMGRkZDQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTA5VDEyOjIzOjM3WiIgRGVzdGluYXRpb249Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl9iZjM3MmI5ZDY3ZDBjODlkMGNmMWFmM2ZmNjI1ZWE3YzA1MWM5ODg1Ij48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng4NDMwYjFkZS1mNTU0LTQ5M2YtNjMyZS00YTIxZTBhMGRkZDQiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPkhENDgxVEsxVmtPMGVaNGUxL2Vma1VJTVVCaz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+bWZrcnh5SlU4MG1hMWwwQ2xUU2tObTgzajVZQ0Y3YUt2bDJqRmVsVU04M3RuaitwNDB1RXRITjkrbHBJQVgrZEZkNnNCR3JEdUk0Q21lOTZuQ2lDK05mb2Y5MnpYaUcrWExCbFR2K0Z0cWxaZUgxc0ZNTFhuOWwxTHNLT0dPbjM5alA4bklUSGtYL0VkYjRucENNZlpQeWZ2b3M1dkMwbUtnWlFKZ1JtcG5vPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1scDpTdGF0dXM+PHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbHA6U3RhdHVzPjxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZnhkMDhjOTU2Ny1lNzUxLTY2ODItMGEwYS1hNDUwZDZmMTUxZTAiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTA5VDEyOjIzOjM3WiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4NCiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4NCiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+DQogIDxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4ZDA4Yzk1NjctZTc1MS02NjgyLTBhMGEtYTQ1MGQ2ZjE1MWUwIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPjxkczpEaWdlc3RWYWx1ZT42alZGMVZEMGJwVW1BVm1ERzlLeGxqMmFOUTA9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPlU3YTdFVWdxeDFiRC9ZaW9uTldPVWVMMHRJR0dRSU5ad1V6N1FBODNhYmxlVnE5dGtKbUsvWm8wak10MnluWHgxelA4K0M1bjNDRTY4TnZ4and5S2RFQUExUjAwblpneDRMR0JnZWl2WVBrazRBb0p6Q0RBbE11M1VLelZ0SU90dTV1VjFNMmVGc0NJY2RUcXBRMGt2b2pVQUVZWVhObSt6Y2xkcnVZWGQ1dz08L2RzOlNpZ25hdHVyZVZhbHVlPg0KPGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJQ2dUQ0NBZW9DQ1FDYk9scldEZFg3RlRBTkJna3Foa2lHOXcwQkFRVUZBRENCaERFTE1Ba0dBMVVFQmhNQ1RrOHhHREFXQmdOVkJBZ1REMEZ1WkhKbFlYTWdVMjlzWW1WeVp6RU1NQW9HQTFVRUJ4TURSbTl2TVJBd0RnWURWUVFLRXdkVlRrbE9SVlJVTVJnd0ZnWURWUVFERXc5bVpXbGtaUzVsY214aGJtY3VibTh4SVRBZkJna3Foa2lHOXcwQkNRRVdFbUZ1WkhKbFlYTkFkVzVwYm1WMGRDNXViekFlRncwd056QTJNVFV4TWpBeE16VmFGdzB3TnpBNE1UUXhNakF4TXpWYU1JR0VNUXN3Q1FZRFZRUUdFd0pPVHpFWU1CWUdBMVVFQ0JNUFFXNWtjbVZoY3lCVGIyeGlaWEpuTVF3d0NnWURWUVFIRXdOR2IyOHhFREFPQmdOVkJBb1RCMVZPU1U1RlZGUXhHREFXQmdOVkJBTVREMlpsYVdSbExtVnliR0Z1Wnk1dWJ6RWhNQjhHQ1NxR1NJYjNEUUVKQVJZU1lXNWtjbVZoYzBCMWJtbHVaWFIwTG01dk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRRGl2YmhSN1A1MTZ4L1MzQnFLeHVwUWUwTE9Ob2xpdXBpQk9lc0NPM1NIYkRybDMrcTlJYmZuZm1FMDRyTnVNY1BzSXhCMTYxVGREcEllc0xDbjdjOGFQSElTS090UGxBZVRaU25iOFFBdTdhUmpacTMrUGJyUDV1VzNUY2ZDR1B0S1R5dEhPZ2UvT2xKYm8wNzhkVmhYUTE0ZDFFRHdYSlcxclJYdVV0NEM4UUlEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRHQkFDRFZmcDg2SE9icVkrZThCVW9XUTkrVk1ReDFBU0RvaEJqd09zZzJXeWtVcVJYRitkTGZjVUg5ZFdSNjNDdFpJS0ZEYlN0Tm9tUG5RejduYksrb255Z3dCc3BWRWJuSHVVaWhacTNaVWRtdW1RcUN3NFV2cy8xVXZxM29yT28vV0pWaFR5dkxnRlZLMlFhclE0LzY3T1pmSGQ3UitQT0JYaG9waFNNdjFaT288L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbDpTdWJqZWN0PjxzYW1sOkVuY3J5cHRlZElEPjx4ZW5jOkVuY3J5cHRlZERhdGEgeG1sbnM6eGVuYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjIiB4bWxuczpkc2lnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIiBUeXBlPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNFbGVtZW50Ij48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjYWVzMTI4LWNiYyIvPjx4ZW5jOkNpcGhlckRhdGE+DQogICAgICA8eGVuYzpDaXBoZXJWYWx1ZT5sbjMrVTFWTTJjQ0I1TjF3SHJnM2llTDM0VEg3RXQvRGVYbytBTlJHRE52OXhtRk9zdWFKbmcwdS9XbHl6RjRNdHFsaWViRGVDQjVTWVRPRURxNFRsVVJ6NkFYSHBINVEzWWthK25rbWV5YWMrVnNpcjRwenlwblJoL29MeWRRV3dNcUlmTUIwM3hrc3l4SVM1Qk5kSzRjYnpaa3Z0bGtYL0ZrRnZUQnJLUzBsU1A4M2VlV2l4STRNell2QTVtK1R1SXpjRXQrUUdvc0FQZ0pCdERITEdYaUlhenRSWGFCSzV0eUxLcDdCMnVHVytWY2JaUTVmZVdlUTVtQ0p4WFBYYjJ0OXNMQ1ExRUVBQ2dOQitJMGFiejFhS1l4TitFandQd2pDVVFaZnFnYjlVY0xQRXBDanFzckdEMFpPSWxQYjJRcmJjNHFWc2NMaUlWRDZvaWxGWFM5QzhzbTA3dmJJTzA3SUhoU1ovRm9jK3AzbCsvQVRVb0I4eTc1SEp2TzE8L3hlbmM6Q2lwaGVyVmFsdWU+DQogICA8L3hlbmM6Q2lwaGVyRGF0YT4NCjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDpFbmNyeXB0ZWRJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIzLTA5LTEwVDE3OjQzOjM3WiIgUmVjaXBpZW50PSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fYmYzNzJiOWQ2N2QwYzg5ZDBjZjFhZjNmZjYyNWVhN2MwNTFjOTg4NSIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAzLTA5VDEyOjIzOjA3WiIgTm90T25PckFmdGVyPSIyMDIzLTA5LTEwVDE3OjQzOjM3WiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTAzLTA5VDIwOjIzOjM3WiIgU2Vzc2lvbkluZGV4PSJfOTQ0YmZjYWNiMGQ4MzJiMTJlNGJjZjc3NGUwMmJiZTVmNjQ1NWM2ODAzIj48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3Q8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdEBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+d2FhMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== diff --git a/tests/data/responses/invalids/invalid_audience.xml.base64 b/tests/data/responses/invalids/invalid_audience.xml.base64 new file mode 100644 index 00000000..80f30e51 --- /dev/null +++ b/tests/data/responses/invalids/invalid_audience.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjk5My0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjI5OTMtMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgICAgIDxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9pbnZhbGlkLmF1ZGllbmNlLmNvbTwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDowN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjI5OTMtMDYtMTdUMjI6NTQ6MTRaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ diff --git a/tests/data/responses/invalids/invalid_issuer_assertion.xml.base64 b/tests/data/responses/invalids/invalid_issuer_assertion.xml.base64 new file mode 100644 index 00000000..426ea5c2 --- /dev/null +++ b/tests/data/responses/invalids/invalid_issuer_assertion.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2ludmFsaWQuaXNzdWVyLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+ICAgIA0KICAgIDxzYW1sOlN1YmplY3Q+DQogICAgICA8c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJoZWxsby5jb20iIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj5zb21lb25lQGV4YW1wbGUuY29tPC9zYW1sOk5hbWVJRD4NCiAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj4NCiAgICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIwLTA2LTE3VDE0OjU5OjE0WiIgUmVjaXBpZW50PSJodHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9lbmRwb2ludHMvYWNzLnBocCIgSW5SZXNwb25zZVRvPSJfNTdiY2JmNzAtN2IxZi0wMTJlLWM4MjEtNzgyYmNiMTNiYjM4Ii8+DQogICAgICA8L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0wNi0xN1QxNDo1Mzo0NFoiIE5vdE9uT3JBZnRlcj0iMjA5OS0wNi0xN1QxNDo1OToxNFoiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPg0KICAgICAgPC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgPC9zYW1sOkNvbmRpdGlvbnM+DQogICAgPHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDExLTA2LTE3VDE0OjU0OjA3WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjA5OS0wNi0xN1QyMjo1NDoxNFoiIFNlc3Npb25JbmRleD0iXzUxYmUzNzk2NWZlYjU1NzlkODAzMTQxMDc2OTM2ZGMyZTlkMWQ5OGViZiI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPg0KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zb21lb25lQGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPg0KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4NCiAgICA8L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICA8L3NhbWw6QXNzZXJ0aW9uPg0KPC9zYW1scDpSZXNwb25zZT4= \ No newline at end of file diff --git a/tests/data/responses/invalids/invalid_issuer_message.xml.base64 b/tests/data/responses/invalids/invalid_issuer_message.xml.base64 new file mode 100644 index 00000000..0c06a25e --- /dev/null +++ b/tests/data/responses/invalids/invalid_issuer_message.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaW52YWxpZC5pc3Nlci5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPg0KICA8c2FtbHA6U3RhdHVzPg0KICAgIDxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz4NCiAgPC9zYW1scDpTdGF0dXM+DQogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZng3ODQxOTkxYy1jNzNmLTQwMzUtZTJlZS1jMTcwYzBlMWQzZTQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDExLTA2LTE3VDE0OjU0OjE0WiI+DQogICAgPHNhbWw6SXNzdWVyPmh0dHA6Ly9pZHAuZXhhbXBsZS5jb20vPC9zYW1sOklzc3Vlcj4gICAgDQogICAgPHNhbWw6U3ViamVjdD4NCiAgICAgIDxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9ImhlbGxvLmNvbSIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPnNvbWVvbmVAZXhhbXBsZS5jb208L3NhbWw6TmFtZUlEPg0KICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPg0KICAgICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMjAtMDYtMTdUMTQ6NTk6MTRaIiBSZWNpcGllbnQ9Imh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL2VuZHBvaW50cy9hY3MucGhwIiBJblJlc3BvbnNlVG89Il81N2JjYmY3MC03YjFmLTAxMmUtYzgyMS03ODJiY2IxM2JiMzgiLz4NCiAgICAgIDwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPg0KICAgIDwvc2FtbDpTdWJqZWN0Pg0KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDEwLTA2LTE3VDE0OjUzOjQ0WiIgTm90T25PckFmdGVyPSIyMDk5LTA2LTE3VDE0OjU5OjE0WiI+DQogICAgICA8c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICA8c2FtbDpBdWRpZW5jZT5odHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+DQogICAgICA8L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICA8L3NhbWw6Q29uZGl0aW9ucz4NCiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MDdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDk5LTA2LTE3VDIyOjU0OjE0WiIgU2Vzc2lvbkluZGV4PSJfNTFiZTM3OTY1ZmViNTU3OWQ4MDMxNDEwNzY5MzZkYzJlOWQxZDk4ZWJmIj4NCiAgICAgIDxzYW1sOkF1dGhuQ29udGV4dD4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0Pg0KICAgIDwvc2FtbDpBdXRoblN0YXRlbWVudD4NCiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNvbWVvbmVAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg0KICA= \ No newline at end of file diff --git a/tests/data/responses/invalids/invalid_sessionindex.xml.base64 b/tests/data/responses/invalids/invalid_sessionindex.xml.base64 new file mode 100644 index 00000000..f2e4c4c6 --- /dev/null +++ b/tests/data/responses/invalids/invalid_sessionindex.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgICAgIDxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDowN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTMtMDYtMTdUMjI6NTQ6MTRaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file diff --git a/tests/data/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 b/tests/data/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 new file mode 100644 index 00000000..b6a4e2eb --- /dev/null +++ b/tests/data/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iaW52YWxpZF9pbnJlc3BvbnNlIi8+DQogICAgICA8L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0wNi0xN1QxNDo1Mzo0NFoiIE5vdE9uT3JBZnRlcj0iMjA5OS0wNi0xN1QxNDo1OToxNFoiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPg0KICAgICAgPC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgPC9zYW1sOkNvbmRpdGlvbnM+DQogICAgPHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDExLTA2LTE3VDE0OjU0OjA3WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjA5OS0wNi0xN1QyMjo1NDoxNFoiIFNlc3Npb25JbmRleD0iXzUxYmUzNzk2NWZlYjU1NzlkODAzMTQxMDc2OTM2ZGMyZTlkMWQ5OGViZiI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPg0KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zb21lb25lQGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPg0KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4NCiAgICA8L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICA8L3NhbWw6QXNzZXJ0aW9uPg0KPC9zYW1scDpSZXNwb25zZT4= \ No newline at end of file diff --git a/tests/data/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 b/tests/data/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 new file mode 100644 index 00000000..5d1f8bc1 --- /dev/null +++ b/tests/data/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdEJlZm9yZT0iMjk5OS0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjI5OTktMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgICAgIDxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDowN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjI5OTktMDYtMTdUMjI6NTQ6MTRaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ diff --git a/tests/data/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 b/tests/data/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 new file mode 100644 index 00000000..4f922132 --- /dev/null +++ b/tests/data/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAxMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgICAgIDxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDowN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMjI6NTQ6MTRaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file diff --git a/tests/data/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 b/tests/data/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 new file mode 100644 index 00000000..30c55eb2 --- /dev/null +++ b/tests/data/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL2ludmFsaWQucmVjaXBlbnQuZXhhbXBsZS5jb20iIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgICAgIDxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDowN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMjI6NTQ6MTRaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file diff --git a/tests/data/responses/invalids/multiple_assertions.xml.base64 b/tests/data/responses/invalids/multiple_assertions.xml.base64 new file mode 100644 index 00000000..228906e9 --- /dev/null +++ b/tests/data/responses/invalids/multiple_assertions.xml.base64 @@ -0,0 +1,2 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIElEPSJHT1NBTUxSMTI5MDExNzQ1NzE3OTQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiIgRGVzdGluYXRpb249IntyZWNpcGllbnR9Ij4NCiAgPHNhbWxwOlN0YXR1cz4NCiAgICA8c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+DQogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIFZlcnNpb249IjIuMCIgSUQ9InBmeGE0NjU3NGRmLWIzYjAtYTA2YS0yM2M4LTYzNjQxMzE5ODc3MiIgSXNzdWVJbnN0YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiI+DQogICAgPHNhbWw6SXNzdWVyPmh0dHBzOi8vYXBwLm9uZWxvZ2luLmNvbS9zYW1sL21ldGFkYXRhLzEzNTkwPC9zYW1sOklzc3Vlcj4NCiAgICA8ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4NCiAgICAgIDxkczpTaWduZWRJbmZvPg0KICAgICAgICA8ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgICAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+DQogICAgICAgIDxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4YTQ2NTc0ZGYtYjNiMC1hMDZhLTIzYzgtNjM2NDEzMTk4NzcyIj4NCiAgICAgICAgICA8ZHM6VHJhbnNmb3Jtcz4NCiAgICAgICAgICAgIDxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPg0KICAgICAgICAgICAgPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgICAgICAgIDwvZHM6VHJhbnNmb3Jtcz4NCiAgICAgICAgICA8ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz4NCiAgICAgICAgICA8ZHM6RGlnZXN0VmFsdWU+cEpRN01TL2VrNEtSUldHbXYvSDQzUmVIWU1zPTwvZHM6RGlnZXN0VmFsdWU+DQogICAgICAgIDwvZHM6UmVmZXJlbmNlPg0KICAgICAgPC9kczpTaWduZWRJbmZvPg0KICAgICAgPGRzOlNpZ25hdHVyZVZhbHVlPnlpdmVLY1BkRHB1RE5qNnNoclEzQUJ3ci9jQTNDcnlEMnBoRy94TFpzektXeFU1L21sYUt0OGV3YlpPZEtLdnRPczJwSEJ5NUR1YTNrOTRBRit6eEd5ZWw1Z09vd21veVhKcitBT3Ira1BPMHZsaTFWOG8zaFBQVVp3UmdTWDZROXBTMUNxUWdoS2lFYXNSeXlscXFKVWFQWXptT3pPRTgvWGxNa3dpV21PMD08L2RzOlNpZ25hdHVyZVZhbHVlPg0KICAgICAgPGRzOktleUluZm8+DQogICAgICAgIDxkczpYNTA5RGF0YT4NCiAgICAgICAgICA8ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUJyVENDQWFHZ0F3SUJBZ0lCQVRBREJnRUFNR2N4Q3pBSkJnTlZCQVlUQWxWVE1STXdFUVlEVlFRSURBcERZV3hwWm05eWJtbGhNUlV3RXdZRFZRUUhEQXhUWVc1MFlTQk5iMjVwWTJFeEVUQVBCZ05WQkFvTUNFOXVaVXh2WjJsdU1Sa3dGd1lEVlFRRERCQmhjSEF1YjI1bGJHOW5hVzR1WTI5dE1CNFhEVEV3TURNd09UQTVOVGcwTlZvWERURTFNRE13T1RBNU5UZzBOVm93WnpFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ01Da05oYkdsbWIzSnVhV0V4RlRBVEJnTlZCQWNNREZOaGJuUmhJRTF2Ym1sallURVJNQThHQTFVRUNnd0lUMjVsVEc5bmFXNHhHVEFYQmdOVkJBTU1FR0Z3Y0M1dmJtVnNiMmRwYmk1amIyMHdnWjh3RFFZSktvWklodmNOQVFFQkJRQURnWTBBTUlHSkFvR0JBT2pTdTFmalB5OGQ1dzRReUwxK3pkNGhJdzFNa2tmZjRXWS9UTEc4T1prVTVZVFNXbW1IUEQ1a3ZZSDV1b1hTLzZxUTgxcVhwUjJ3VjhDVG93WkpVTGcwOWRkUmRSbjhRc3FqMUZ5T0M1c2xFM3kyYloyb0Z1YTcyb2YvNDlmcHVqbkZUNktuUTYxQ0JNcWxEb1RRcU9UNjJ2R0o4blA2TVpXdkE2c3hxdWQ1QWdNQkFBRXdBd1lCQUFNQkFBPT08L2RzOlg1MDlDZXJ0aWZpY2F0ZT4NCiAgICAgICAgPC9kczpYNTA5RGF0YT4NCiAgICAgIDwvZHM6S2V5SW5mbz4NCiAgICA8L2RzOlNpZ25hdHVyZT4NCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj5zdXBwb3J0QG9uZWxvZ2luLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOFQyMjowMjozN1oiIFJlY2lwaWVudD0ie3JlY2lwaWVudH0iLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0xMS0xOFQyMTo1MjozN1oiIE5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOFQyMjowMjozN1oiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+e2F1ZGllbmNlfTwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMC0xMS0xOFQyMTo1NzozN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTAtMTEtMTlUMjE6NTc6MzdaIiBTZXNzaW9uSW5kZXg9Il81MzFjMzJkMjgzYmRmZjdlMDRlNDg3YmNkYmM0ZGQ4ZCI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+ZGVtbzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0iYW5vdGhlcl92YWx1ZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+dmFsdWU8L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIFZlcnNpb249IjIuMCIgSUQ9InBmeGE0NjU3NGRmLWIzYjAtYTA2YS0yM2M4LTYzNjQxMzE5ODc3MyIgSXNzdWVJbnN0YW50PSIyMDEwLTExLTE4VDIxOjU3OjQ1WiI+DQogICAgPHNhbWw6SXNzdWVyPmh0dHBzOi8vYXBwLm9uZWxvZ2luLmNvbS9zYW1sL21ldGFkYXRhLzEzNTkwPC9zYW1sOklzc3Vlcj4NCiAgICA8ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4NCiAgICAgIDxkczpTaWduZWRJbmZvPg0KICAgICAgICA8ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgICAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+DQogICAgICAgIDxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4YTQ2NTc0ZGYtYjNiMC1hMDZhLTIzYzgtNjM2NDEzMTk4NzcyIj4NCiAgICAgICAgICA8ZHM6VHJhbnNmb3Jtcz4NCiAgICAgICAgICAgIDxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPg0KICAgICAgICAgICAgPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgICAgICAgIDwvZHM6VHJhbnNmb3Jtcz4NCiAgICAgICAgICA8ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz4NCiAgICAgICAgICA8ZHM6RGlnZXN0VmFsdWU+cEpRN01TL2VrNEtSUldHbXYvSDQzUmVIWU1zPTwvZHM6RGlnZXN0VmFsdWU+DQogICAgICAgIDwvZHM6UmVmZXJlbmNlPg0KICAgICAgPC9kczpTaWduZWRJbmZvPg0KICAgICAgPGRzOlNpZ25hdHVyZVZhbHVlPnlpdmVLY1BkRHB1RE5qNnNoclEzQUJ3ci9jQTNDcnlEMnBoRy94TFpzektXeFU1L21sYUt0OGV3YlpPZEtLdnRPczJwSEJ5NUR1YTNrOTRBRit6eEd5ZWw1Z09vd21veVhKcitBT3Ira1BPMHZsaTFWOG8zaFBQVVp3UmdTWDZROXBTMUNxUWdoS2lFYXNSeXlscXFKVWFQWXptT3pPRTgvWGxNa3dpV21PMD08L2RzOlNpZ25hdHVyZVZhbHVlPg0KICAgICAgPGRzOktleUluZm8+DQogICAgICAgIDxkczpYNTA5RGF0YT4NCiAgICAgICAgICA8ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUJyVENDQWFHZ0F3SUJBZ0lCQVRBREJnRUFNR2N4Q3pBSkJnTlZCQVlUQWxWVE1STXdFUVlEVlFRSURBcERZV3hwWm05eWJtbGhNUlV3RXdZRFZRUUhEQXhUWVc1MFlTQk5iMjVwWTJFeEVUQVBCZ05WQkFvTUNFOXVaVXh2WjJsdU1Sa3dGd1lEVlFRRERCQmhjSEF1YjI1bGJHOW5hVzR1WTI5dE1CNFhEVEV3TURNd09UQTVOVGcwTlZvWERURTFNRE13T1RBNU5UZzBOVm93WnpFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ01Da05oYkdsbWIzSnVhV0V4RlRBVEJnTlZCQWNNREZOaGJuUmhJRTF2Ym1sallURVJNQThHQTFVRUNnd0lUMjVsVEc5bmFXNHhHVEFYQmdOVkJBTU1FR0Z3Y0M1dmJtVnNiMmRwYmk1amIyMHdnWjh3RFFZSktvWklodmNOQVFFQkJRQURnWTBBTUlHSkFvR0JBT2pTdTFmalB5OGQ1dzRReUwxK3pkNGhJdzFNa2tmZjRXWS9UTEc4T1prVTVZVFNXbW1IUEQ1a3ZZSDV1b1hTLzZxUTgxcVhwUjJ3VjhDVG93WkpVTGcwOWRkUmRSbjhRc3FqMUZ5T0M1c2xFM3kyYloyb0Z1YTcyb2YvNDlmcHVqbkZUNktuUTYxQ0JNcWxEb1RRcU9UNjJ2R0o4blA2TVpXdkE2c3hxdWQ1QWdNQkFBRXdBd1lCQUFNQkFBPT08L2RzOlg1MDlDZXJ0aWZpY2F0ZT4NCiAgICAgICAgPC9kczpYNTA5RGF0YT4NCiAgICAgIDwvZHM6S2V5SW5mbz4NCiAgICA8L2RzOlNpZ25hdHVyZT4NCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj5zdXBwb3J0QG9uZWxvZ2luLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOFQyMjowMjo0NVoiIFJlY2lwaWVudD0ie3JlY2lwaWVudH0iLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0xMS0xOFQyMTo1MjozN1oiIE5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOFQyMjowMjo0NVoiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+e2F1ZGllbmNlfTwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMC0xMS0xOFQyMTo1Nzo0NVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTAtMTEtMTlUMjE6NTc6NDVaIiBTZXNzaW9uSW5kZXg9Il81MzFjMzJkMjgzYmRmZjdlMDRlNDg3YmNkYmM0ZGQ4ZCI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+ZGVtbzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0iYW5vdGhlcl92YWx1ZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+dmFsdWU8L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg== + 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..64239d42 --- /dev/null +++ b/tests/data/responses/invalids/no_authnstatement.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDI2MjgzZmMzLWQ4MDQtMTNlYS04OGUxLWM2ZTlhNjUzY2I5NiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDI2MjgzZmMzLWQ4MDQtMTNlYS04OGUxLWM2ZTlhNjUzY2I5NiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+VmExNWZUdkNXeDMwOWNzQkEvN3lZYjMrOVlVPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5kRkx0SGYvcnpQMUZFQlNJM3NFTEwrTEVLYmt3cENpZW1FT2M2SVFiNi9wV1pIYmJ5VXdNSHYwTVFkZW1yNFZYK3E2QzFSMVp2bnp5MTdCWHcrL1Z4ckdMWVdydVpqa2RCQVI3aHBTMzRab2QyQ0hGMkZ4QktwR3RIL2RadncxRmE1Q1Z4eitnczJYcm96aGNlblVkNU5YOVNtZ0RnQ001TFZXaFpHc09NSTQ9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGI0ZWM5YzhhLTQ4ZWItZmRhMi03Zjc0LWZhMWExMDVhOTlmZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj40OTI4ODI2MTVhY2YzMWM4MDk2YjYyNzI0NWQ3NmFlNTMwMzZjMDkwPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyOTkzLTA4LTIzVDA2OjU3OjAxWiIgUmVjaXBpZW50PSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNWZlOWQ2ZTQ5OWIyZjA5MTMyMDZhYWIzZjcxOTE3MjkwNDliYjgwNyIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAyLTE5VDAxOjM2OjMxWiIgTm90T25PckFmdGVyPSIyOTkzLTA4LTIzVDA2OjU3OjAxWiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== 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_id.xml.base64 b/tests/data/responses/invalids/no_id.xml.base64 new file mode 100644 index 00000000..11db2653 --- /dev/null +++ b/tests/data/responses/invalids/no_id.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjA1OjQ5WiIgRGVzdGluYXRpb249Imh0dHBzOi8vZXhhbXBsZS5jb20vbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl9hZjNkNGE3MTBmYzhiMzA1ODg0Yjk2ZDAwOTRhYjYyODgwMmY1NjkyIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9leGFtcGxlLmNvbS9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZng3ZTQ4ZmQ3NS1jZTJiLTYwZWQtMjllZS1lNzk4NDZjOTU5YzYiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjA1OjQ5WiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vZXhhbXBsZS5jb20vc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj4NCjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9leGFtcGxlLmNvbS9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj40OTI4ODI2MTVhY2YzMWM4MDk2YjYyNzI0NWQ3NmFlNTMwMzZjMDkwPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDE0LTAyLTE5VDAxOjEwOjQ5WiIgUmVjaXBpZW50PSJodHRwczovL2V4YW1wbGUuY29tL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fYWYzZDRhNzEwZmM4YjMwNTg4NGI5NmQwMDk0YWI2Mjg4MDJmNTY5MiIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAyLTE5VDAxOjA1OjE5WiIgTm90T25PckFmdGVyPSIyMDE0LTAyLTE5VDAxOjEwOjQ5WiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL2V4YW1wbGUuY29tL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDItMThUMTk6NDI6MjBaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTAyLTE5VDA5OjA1OjQ5WiIgU2Vzc2lvbkluZGV4PSJfMGY0ZjE4OGRjMWJmZDNiZmVhMzZhMTYzNGE3NDQxNTgzYWZjM2IzNzgxIj48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlckBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== 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/no_key.xml.base64 b/tests/data/responses/invalids/no_key.xml.base64 new file mode 100644 index 00000000..ffd98ed3 --- /dev/null +++ b/tests/data/responses/invalids/no_key.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZngwNWYzY2UxMC0xNjE1LWYzZWEtYTk4OC02MGUzODBiMzI5OWYiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgRGVzdGluYXRpb249Imh0dHBzOi8vZXhhbXBsZS5jb20vbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl81ZmU5ZDZlNDk5YjJmMDkxMzIwNmFhYjNmNzE5MTcyOTA0OWJiODA3Ij48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9leGFtcGxlLmNvbS9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1scDpTdGF0dXM+PHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbHA6U3RhdHVzPjxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZnhiNGVjOWM4YS00OGViLWZkYTItN2Y3NC1mYTFhMTA1YTk5ZmUiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vZXhhbXBsZS5jb20vc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4NCiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4gICAgDQogIDxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4YjRlYzljOGEtNDhlYi1mZGEyLTdmNzQtZmExYTEwNWE5OWZlIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPjxkczpEaWdlc3RWYWx1ZT5obnBUOWN1ZTZ0Qks0SVpPajUwZU1tM21DZ0k9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+DQo8L2RzOlNpZ25hdHVyZT48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vZXhhbXBsZS5jb20vbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9leGFtcGxlLmNvbS9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9leGFtcGxlLmNvbS9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxNC0wMi0xOVQwOTozNzowMVoiIFNlc3Npb25JbmRleD0iXzYyNzNkNzdiOGNkZTBjMzMzZWM3OWQyMmE5ZmEwMDAzYjlmZTJkNzVjYiI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4= diff --git a/tests/data/responses/invalids/no_nameid.xml.base64 b/tests/data/responses/invalids/no_nameid.xml.base64 new file mode 100644 index 00000000..db8879b2 --- /dev/null +++ b/tests/data/responses/invalids/no_nameid.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPg0KICAgICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMjAtMDYtMTdUMTQ6NTk6MTRaIiBSZWNpcGllbnQ9Imh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL2VuZHBvaW50cy9hY3MucGhwIiBJblJlc3BvbnNlVG89Il81N2JjYmY3MC03YjFmLTAxMmUtYzgyMS03ODJiY2IxM2JiMzgiLz4NCiAgICAgIDwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPg0KICAgIDwvc2FtbDpTdWJqZWN0Pg0KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDEwLTA2LTE3VDE0OjUzOjQ0WiIgTm90T25PckFmdGVyPSIyMDk5LTA2LTE3VDE0OjU5OjE0WiI+DQogICAgICA8c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICA8c2FtbDpBdWRpZW5jZT5odHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+DQogICAgICA8L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICA8L3NhbWw6Q29uZGl0aW9ucz4NCiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MDdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDk5LTA2LTE3VDIyOjU0OjE0WiIgU2Vzc2lvbkluZGV4PSJfNTFiZTM3OTY1ZmViNTU3OWQ4MDMxNDEwNzY5MzZkYzJlOWQxZDk4ZWJmIj4NCiAgICAgIDxzYW1sOkF1dGhuQ29udGV4dD4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0Pg0KICAgIDwvc2FtbDpBdXRoblN0YXRlbWVudD4NCiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNvbWVvbmVAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/tests/data/responses/invalids/no_saml2.xml.base64 b/tests/data/responses/invalids/no_saml2.xml.base64 new file mode 100644 index 00000000..383d0d40 --- /dev/null +++ b/tests/data/responses/invalids/no_saml2.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8UmVzcG9uc2UgeG1sbnM9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4wOmFzc2VydGlvbiINCiAgICB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4wOnByb3RvY29sIiB4bWxuczp4c2Q9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIg0KICAgIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIElzc3VlSW5zdGFudD0iMjAwOC0xMi0xMFQxNDoxMjoxNC44MTdaIg0KICAgIE1ham9yVmVyc2lvbj0iMSIgTWlub3JWZXJzaW9uPSIxIiBSZWNpcGllbnQ9Imh0dHBzOi8vZWlnZXIuaWFkLnZ0LmVkdS9kYXQvaG9tZS5kbyINCiAgICBSZXNwb25zZUlEPSJfNWM5NGI1NDMxYzU0MDM2NWU1YTcwYjI4NzRiNzU5OTYiPg0KICAgICAgPFN0YXR1cz4NCiAgICAgICAgPFN0YXR1c0NvZGUgVmFsdWU9InNhbWxwOlN1Y2Nlc3MiPg0KICAgICAgICA8L1N0YXR1c0NvZGU+DQogICAgICA8L1N0YXR1cz4NCiAgICAgIDxBc3NlcnRpb24geG1sbnM9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMDphc3NlcnRpb24iIEFzc2VydGlvbklEPSJfZTVjMjNmZjdhMzg4OWUxMmZhMDE4MDJhNDczMzE2NTMiDQogICAgICBJc3N1ZUluc3RhbnQ9IjIwMDgtMTItMTBUMTQ6MTI6MTQuODE3WiIgSXNzdWVyPSJsb2NhbGhvc3QiIE1ham9yVmVyc2lvbj0iMSINCiAgICAgIE1pbm9yVmVyc2lvbj0iMSI+DQogICAgICAgIDxDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAwOC0xMi0xMFQxNDoxMjoxNC44MTdaIiBOb3RPbk9yQWZ0ZXI9IjI5OTMtMTItMTBUMTQ6MTI6NDQuODE3WiI+DQogICAgICAgICAgPEF1ZGllbmNlUmVzdHJpY3Rpb25Db25kaXRpb24+DQogICAgICAgICAgICA8QXVkaWVuY2U+DQogICAgICAgICAgICAgIGh0dHBzOi8vc29tZS1zZXJ2aWNlLmV4YW1wbGUuY29tL2FwcC8NCiAgICAgICAgICAgIDwvQXVkaWVuY2U+DQogICAgICAgICAgPC9BdWRpZW5jZVJlc3RyaWN0aW9uQ29uZGl0aW9uPg0KICAgICAgICA8L0NvbmRpdGlvbnM+DQogICAgICAgIDxBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICAgICAgPFN1YmplY3Q+DQogICAgICAgICAgICA8TmFtZUlkZW50aWZpZXI+am9obnE8L05hbWVJZGVudGlmaWVyPg0KICAgICAgICAgICAgPFN1YmplY3RDb25maXJtYXRpb24+DQogICAgICAgICAgICAgIDxDb25maXJtYXRpb25NZXRob2Q+DQogICAgICAgICAgICAgICAgdXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4wOmNtOmFydGlmYWN0DQogICAgICAgICAgICAgIDwvQ29uZmlybWF0aW9uTWV0aG9kPg0KICAgICAgICAgICAgPC9TdWJqZWN0Q29uZmlybWF0aW9uPg0KICAgICAgICAgIDwvU3ViamVjdD4NCiAgICAgICAgICA8QXR0cmlidXRlIEF0dHJpYnV0ZU5hbWU9InVpZCIgQXR0cmlidXRlTmFtZXNwYWNlPSJodHRwOi8vd3d3LmphLXNpZy5vcmcvcHJvZHVjdHMvY2FzLyI+DQogICAgICAgICAgICA8QXR0cmlidXRlVmFsdWU+MTIzNDU8L0F0dHJpYnV0ZVZhbHVlPg0KICAgICAgICAgIDwvQXR0cmlidXRlPg0KICAgICAgICAgIDxBdHRyaWJ1dGUgQXR0cmlidXRlTmFtZT0iZ3JvdXBNZW1iZXJzaGlwIiBBdHRyaWJ1dGVOYW1lc3BhY2U9Imh0dHA6Ly93d3cuamEtc2lnLm9yZy9wcm9kdWN0cy9jYXMvIj4NCiAgICAgICAgICAgIDxBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgICAgICAgICAgdXVnaWQ9bWlkZGxld2FyZS5zdGFmZixvdT1Hcm91cHMsZGM9dnQsZGM9ZWR1DQogICAgICAgICAgICA8L0F0dHJpYnV0ZVZhbHVlPg0KICAgICAgICAgIDwvQXR0cmlidXRlPg0KICAgICAgICAgIDxBdHRyaWJ1dGUgQXR0cmlidXRlTmFtZT0iZWR1UGVyc29uQWZmaWxpYXRpb24iIEF0dHJpYnV0ZU5hbWVzcGFjZT0iaHR0cDovL3d3dy5qYS1zaWcub3JnL3Byb2R1Y3RzL2Nhcy8iPg0KICAgICAgICAgICAgPEF0dHJpYnV0ZVZhbHVlPnN0YWZmPC9BdHRyaWJ1dGVWYWx1ZT4NCiAgICAgICAgICA8L0F0dHJpYnV0ZT4NCiAgICAgICAgICA8QXR0cmlidXRlIEF0dHJpYnV0ZU5hbWU9ImFjY291bnRTdGF0ZSIgQXR0cmlidXRlTmFtZXNwYWNlPSJodHRwOi8vd3d3LmphLXNpZy5vcmcvcHJvZHVjdHMvY2FzLyI+DQogICAgICAgICAgICA8QXR0cmlidXRlVmFsdWU+QUNUSVZFPC9BdHRyaWJ1dGVWYWx1ZT4NCiAgICAgICAgICA8L0F0dHJpYnV0ZT4NCiAgICAgICAgPC9BdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICAgIDxBdXRoZW50aWNhdGlvblN0YXRlbWVudCBBdXRoZW50aWNhdGlvbkluc3RhbnQ9IjIwMDgtMTItMTBUMTQ6MTI6MTQuNzQxWiINCiAgICAgICAgQXV0aGVudGljYXRpb25NZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMDphbTpwYXNzd29yZCI+DQogICAgICAgICAgPFN1YmplY3Q+DQogICAgICAgICAgICA8TmFtZUlkZW50aWZpZXI+am9obnE8L05hbWVJZGVudGlmaWVyPg0KICAgICAgICAgICAgPFN1YmplY3RDb25maXJtYXRpb24+DQogICAgICAgICAgICAgIDxDb25maXJtYXRpb25NZXRob2Q+DQogICAgICAgICAgICAgICAgdXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4wOmNtOmFydGlmYWN0DQogICAgICAgICAgICAgIDwvQ29uZmlybWF0aW9uTWV0aG9kPg0KICAgICAgICAgICAgPC9TdWJqZWN0Q29uZmlybWF0aW9uPg0KICAgICAgICAgIDwvU3ViamVjdD4NCiAgICAgICAgPC9BdXRoZW50aWNhdGlvblN0YXRlbWVudD4NCiAgICAgIDwvQXNzZXJ0aW9uPg0KICAgIDwvUmVzcG9uc2U+ diff --git a/tests/data/responses/invalids/no_signature.xml.base64 b/tests/data/responses/invalids/no_signature.xml.base64 new file mode 100644 index 00000000..76dfc435 --- /dev/null +++ b/tests/data/responses/invalids/no_signature.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZngwNWYzY2UxMC0xNjE1LWYzZWEtYTk4OC02MGUzODBiMzI5OWYiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgRGVzdGluYXRpb249Imh0dHBzOi8vZXhhbXBsZS5jb20vbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl81ZmU5ZDZlNDk5YjJmMDkxMzIwNmFhYjNmNzE5MTcyOTA0OWJiODA3Ij48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9leGFtcGxlLmNvbS9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1scDpTdGF0dXM+PHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbHA6U3RhdHVzPjxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZnhiNGVjOWM4YS00OGViLWZkYTItN2Y3NC1mYTFhMTA1YTk5ZmUiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vZXhhbXBsZS5jb20vc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vZXhhbXBsZS5jb20vbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjk5My0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9leGFtcGxlLmNvbS9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjk5My0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9leGFtcGxlLmNvbS9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjk5My0wMi0xOVQwOTozNzowMVoiIFNlc3Npb25JbmRleD0iXzYyNzNkNzdiOGNkZTBjMzMzZWM3OWQyMmE5ZmEwMDAzYjlmZTJkNzVjYiI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4= diff --git a/tests/data/responses/invalids/no_status.xml.base64 b/tests/data/responses/invalids/no_status.xml.base64 new file mode 100644 index 00000000..fffb8592 --- /dev/null +++ b/tests/data/responses/invalids/no_status.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgSUQ9InBmeGY2MjIzYWE2LTkxZWEtMmE3NS1iYWUzLWU1YThkZmI0NzZmMSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTAtMTEtMThUMjE6NTc6MzdaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4NCiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4NCiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+DQogIDxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4ZjYyMjNhYTYtOTFlYS0yYTc1LWJhZTMtZTVhOGRmYjQ3NmYxIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPjxkczpEaWdlc3RWYWx1ZT41NFFDT3cxenIwYjdnWmlmZVJGUjhMVjI3NXc9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPjJzdjJ4R0RIdVdxcXY1MEhVQm5vQmdoN0pJUzlQSmxzc1BVV00rRG56SUZuNk8vaDZQN1NDdUVJcXJWRnFrN2NaYWg2Q1o4KzVUMUd1emx5Vzkwem84dGRGU3liTzFvbHNCcVVHSmprVUZZS0hLdGNRSTFSb2YxTHowT2ZRbnFSekNlc0NDODlzNnU4RCtLdlFLSWdIcEpzbWVjM2w5VDFYdlRPcmVmNmI4dz08L2RzOlNpZ25hdHVyZVZhbHVlPg0KPGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJQ2dUQ0NBZW9DQ1FDYk9scldEZFg3RlRBTkJna3Foa2lHOXcwQkFRVUZBRENCaERFTE1Ba0dBMVVFQmhNQ1RrOHhHREFXQmdOVkJBZ1REMEZ1WkhKbFlYTWdVMjlzWW1WeVp6RU1NQW9HQTFVRUJ4TURSbTl2TVJBd0RnWURWUVFLRXdkVlRrbE9SVlJVTVJnd0ZnWURWUVFERXc5bVpXbGtaUzVsY214aGJtY3VibTh4SVRBZkJna3Foa2lHOXcwQkNRRVdFbUZ1WkhKbFlYTkFkVzVwYm1WMGRDNXViekFlRncwd056QTJNVFV4TWpBeE16VmFGdzB3TnpBNE1UUXhNakF4TXpWYU1JR0VNUXN3Q1FZRFZRUUdFd0pPVHpFWU1CWUdBMVVFQ0JNUFFXNWtjbVZoY3lCVGIyeGlaWEpuTVF3d0NnWURWUVFIRXdOR2IyOHhFREFPQmdOVkJBb1RCMVZPU1U1RlZGUXhHREFXQmdOVkJBTVREMlpsYVdSbExtVnliR0Z1Wnk1dWJ6RWhNQjhHQ1NxR1NJYjNEUUVKQVJZU1lXNWtjbVZoYzBCMWJtbHVaWFIwTG01dk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRRGl2YmhSN1A1MTZ4L1MzQnFLeHVwUWUwTE9Ob2xpdXBpQk9lc0NPM1NIYkRybDMrcTlJYmZuZm1FMDRyTnVNY1BzSXhCMTYxVGREcEllc0xDbjdjOGFQSElTS090UGxBZVRaU25iOFFBdTdhUmpacTMrUGJyUDV1VzNUY2ZDR1B0S1R5dEhPZ2UvT2xKYm8wNzhkVmhYUTE0ZDFFRHdYSlcxclJYdVV0NEM4UUlEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRHQkFDRFZmcDg2SE9icVkrZThCVW9XUTkrVk1ReDFBU0RvaEJqd09zZzJXeWtVcVJYRitkTGZjVUg5ZFdSNjNDdFpJS0ZEYlN0Tm9tUG5RejduYksrb255Z3dCc3BWRWJuSHVVaWhacTNaVWRtdW1RcUN3NFV2cy8xVXZxM29yT28vV0pWaFR5dkxnRlZLMlFhclE0LzY3T1pmSGQ3UitQT0JYaG9waFNNdjFaT288L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgVmVyc2lvbj0iMi4wIiBJRD0icGZ4YTQ2NTc0ZGYtYjNiMC1hMDZhLTIzYzgtNjM2NDEzMTk4NzcyIiBJc3N1ZUluc3RhbnQ9IjIwMTAtMTEtMThUMjE6NTc6MzdaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cHM6Ly9hcHAub25lbG9naW4uY29tL3NhbWwvbWV0YWRhdGEvMTM1OTA8L3NhbWw6SXNzdWVyPg0KICAgIDxzYW1sOlN1YmplY3Q+DQogICAgICA8c2FtbDpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPnN1cHBvcnRAb25lbG9naW4uY29tPC9zYW1sOk5hbWVJRD4NCiAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj4NCiAgICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyOTkzLTExLTE4VDIyOjAyOjM3WiIgUmVjaXBpZW50PSJ7cmVjaXBpZW50fSIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPg0KICAgIDwvc2FtbDpTdWJqZWN0Pg0KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDEwLTExLTE4VDIxOjUyOjM3WiIgTm90T25PckFmdGVyPSIyOTkzLTExLTE4VDIyOjAyOjM3WiI+DQogICAgICA8c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICA8c2FtbDpBdWRpZW5jZT57YXVkaWVuY2V9PC9zYW1sOkF1ZGllbmNlPg0KICAgICAgPC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgPC9zYW1sOkNvbmRpdGlvbnM+DQogICAgPHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjk5My0xMS0xOVQyMTo1NzozN1oiIFNlc3Npb25JbmRleD0iXzUzMWMzMmQyODNiZGZmN2UwNGU0ODdiY2RiYzRkZDhkIj4NCiAgICAgIDxzYW1sOkF1dGhuQ29udGV4dD4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0Pg0KICAgIDwvc2FtbDpBdXRoblN0YXRlbWVudD4NCiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5kZW1vPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPg0KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJhbm90aGVyX3ZhbHVlIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj52YWx1ZTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ diff --git a/tests/data/responses/invalids/no_status_code.xml.base64 b/tests/data/responses/invalids/no_status_code.xml.base64 new file mode 100644 index 00000000..e62333cd --- /dev/null +++ b/tests/data/responses/invalids/no_status_code.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgSUQ9InBmeDg5MGIzMTU1LTQ3NWItNWE2NC1lNzllLTc2NTQ5YjY5M2IyNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTAtMTEtMThUMjE6NTc6MzdaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9leGFtcGxlLmNvbS9kZW1vMS9pbmRleC5waHA/YWNzIj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4NCiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4NCiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+DQogIDxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4ODkwYjMxNTUtNDc1Yi01YTY0LWU3OWUtNzY1NDliNjkzYjI0Ij48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPjxkczpEaWdlc3RWYWx1ZT5OVGhVY0JMWm55T1RqdjhDQzlPRU81UWU0alk9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPnpWZ3ZWVGVmWGpTNmFIN0I0UjNINUYrTFhaTzBRTjdTZ0RJSXNRUTdlY0xLbTVhVHIxYU9TNGNIY0dRVjZzcVh6YS96cUxOc2hhTTlRZWZnTng0NnA1OHpqdHc5VDRNNExWUFFkVDdub0RxVWdYcWFRWU5hdlUzUlhpVGpPeVBrUjMrakdTN1ltWnBXUFFEdThpa2RFZUJHNDVYMGhjL0dhTXFtczVLeW12bz08L2RzOlNpZ25hdHVyZVZhbHVlPg0KPGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJQ2dUQ0NBZW9DQ1FDYk9scldEZFg3RlRBTkJna3Foa2lHOXcwQkFRVUZBRENCaERFTE1Ba0dBMVVFQmhNQ1RrOHhHREFXQmdOVkJBZ1REMEZ1WkhKbFlYTWdVMjlzWW1WeVp6RU1NQW9HQTFVRUJ4TURSbTl2TVJBd0RnWURWUVFLRXdkVlRrbE9SVlJVTVJnd0ZnWURWUVFERXc5bVpXbGtaUzVsY214aGJtY3VibTh4SVRBZkJna3Foa2lHOXcwQkNRRVdFbUZ1WkhKbFlYTkFkVzVwYm1WMGRDNXViekFlRncwd056QTJNVFV4TWpBeE16VmFGdzB3TnpBNE1UUXhNakF4TXpWYU1JR0VNUXN3Q1FZRFZRUUdFd0pPVHpFWU1CWUdBMVVFQ0JNUFFXNWtjbVZoY3lCVGIyeGlaWEpuTVF3d0NnWURWUVFIRXdOR2IyOHhFREFPQmdOVkJBb1RCMVZPU1U1RlZGUXhHREFXQmdOVkJBTVREMlpsYVdSbExtVnliR0Z1Wnk1dWJ6RWhNQjhHQ1NxR1NJYjNEUUVKQVJZU1lXNWtjbVZoYzBCMWJtbHVaWFIwTG01dk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRRGl2YmhSN1A1MTZ4L1MzQnFLeHVwUWUwTE9Ob2xpdXBpQk9lc0NPM1NIYkRybDMrcTlJYmZuZm1FMDRyTnVNY1BzSXhCMTYxVGREcEllc0xDbjdjOGFQSElTS090UGxBZVRaU25iOFFBdTdhUmpacTMrUGJyUDV1VzNUY2ZDR1B0S1R5dEhPZ2UvT2xKYm8wNzhkVmhYUTE0ZDFFRHdYSlcxclJYdVV0NEM4UUlEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRHQkFDRFZmcDg2SE9icVkrZThCVW9XUTkrVk1ReDFBU0RvaEJqd09zZzJXeWtVcVJYRitkTGZjVUg5ZFdSNjNDdFpJS0ZEYlN0Tm9tUG5RejduYksrb255Z3dCc3BWRWJuSHVVaWhacTNaVWRtdW1RcUN3NFV2cy8xVXZxM29yT28vV0pWaFR5dkxnRlZLMlFhclE0LzY3T1pmSGQ3UitQT0JYaG9waFNNdjFaT288L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT4NCiAgPHNhbWxwOlN0YXR1cy8+DQogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIFZlcnNpb249IjIuMCIgSUQ9InBmeGE0NjU3NGRmLWIzYjAtYTA2YS0yM2M4LTYzNjQxMzE5ODc3MiIgSXNzdWVJbnN0YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiI+DQogICAgPHNhbWw6SXNzdWVyPmh0dHBzOi8vYXBwLm9uZWxvZ2luLmNvbS9zYW1sL21ldGFkYXRhLzEzNTkwPC9zYW1sOklzc3Vlcj4NCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj5zdXBwb3J0QG9uZWxvZ2luLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOFQyMjowMjozN1oiIFJlY2lwaWVudD0ie3JlY2lwaWVudH0iLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0xMS0xOFQyMTo1MjozN1oiIE5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOFQyMjowMjozN1oiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+e2F1ZGllbmNlfTwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMC0xMS0xOFQyMTo1NzozN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTAtMTEtMTlUMjE6NTc6MzdaIiBTZXNzaW9uSW5kZXg9Il81MzFjMzJkMjgzYmRmZjdlMDRlNDg3YmNkYmM0ZGQ4ZCI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+ZGVtbzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0iYW5vdGhlcl92YWx1ZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+dmFsdWU8L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg== diff --git a/tests/data/responses/invalids/no_subjectconfirmation_data.xml.base64 b/tests/data/responses/invalids/no_subjectconfirmation_data.xml.base64 new file mode 100644 index 00000000..bc28d907 --- /dev/null +++ b/tests/data/responses/invalids/no_subjectconfirmation_data.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+ICAgICAgICANCiAgICAgIDwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPg0KICAgIDwvc2FtbDpTdWJqZWN0Pg0KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDEwLTA2LTE3VDE0OjUzOjQ0WiIgTm90T25PckFmdGVyPSIyMDk5LTA2LTE3VDE0OjU5OjE0WiI+DQogICAgICA8c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICA8c2FtbDpBdWRpZW5jZT5odHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+DQogICAgICA8L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICA8L3NhbWw6Q29uZGl0aW9ucz4NCiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MDdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDk5LTA2LTE3VDIyOjU0OjE0WiIgU2Vzc2lvbkluZGV4PSJfNTFiZTM3OTY1ZmViNTU3OWQ4MDMxNDEwNzY5MzZkYzJlOWQxZDk4ZWJmIj4NCiAgICAgIDxzYW1sOkF1dGhuQ29udGV4dD4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0Pg0KICAgIDwvc2FtbDpBdXRoblN0YXRlbWVudD4NCiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNvbWVvbmVAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/tests/data/responses/invalids/no_subjectconfirmation_method.xml.base64 b/tests/data/responses/invalids/no_subjectconfirmation_method.xml.base64 new file mode 100644 index 00000000..37b69370 --- /dev/null +++ b/tests/data/responses/invalids/no_subjectconfirmation_method.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmhvbGRlci1vZi1rZXkiPg0KICAgICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMjAtMDYtMTdUMTQ6NTk6MTRaIiBSZWNpcGllbnQ9Imh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL2VuZHBvaW50cy9hY3MucGhwIiBJblJlc3BvbnNlVG89Il81N2JjYmY3MC03YjFmLTAxMmUtYzgyMS03ODJiY2IxM2JiMzgiLz4NCiAgICAgIDwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPg0KICAgIDwvc2FtbDpTdWJqZWN0Pg0KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDEwLTA2LTE3VDE0OjUzOjQ0WiIgTm90T25PckFmdGVyPSIyMDk5LTA2LTE3VDE0OjU5OjE0WiI+DQogICAgICA8c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICA8c2FtbDpBdWRpZW5jZT5odHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+DQogICAgICA8L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICA8L3NhbWw6Q29uZGl0aW9ucz4NCiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MDdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDk5LTA2LTE3VDIyOjU0OjE0WiIgU2Vzc2lvbkluZGV4PSJfNTFiZTM3OTY1ZmViNTU3OWQ4MDMxNDEwNzY5MzZkYzJlOWQxZDk4ZWJmIj4NCiAgICAgIDxzYW1sOkF1dGhuQ29udGV4dD4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0Pg0KICAgIDwvc2FtbDpBdXRoblN0YXRlbWVudD4NCiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNvbWVvbmVAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/tests/data/responses/invalids/not_after_failed.xml.base64 b/tests/data/responses/invalids/not_after_failed.xml.base64 new file mode 100644 index 00000000..5efc0c30 --- /dev/null +++ b/tests/data/responses/invalids/not_after_failed.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZnhmYTk3ZWVkNS03NTg4LTBkMjMtMmFkNy1mYTY2ZjI4OTM3ODgiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjA1OjQ5WiIgRGVzdGluYXRpb249Imh0dHBzOi8vZXhhbXBsZS5jb20vbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl9hZjNkNGE3MTBmYzhiMzA1ODg0Yjk2ZDAwOTRhYjYyODgwMmY1NjkyIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9leGFtcGxlLmNvbS9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZng3ZTQ4ZmQ3NS1jZTJiLTYwZWQtMjllZS1lNzk4NDZjOTU5YzYiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjA1OjQ5WiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vZXhhbXBsZS5jb20vc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj4NCjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9leGFtcGxlLmNvbS9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj40OTI4ODI2MTVhY2YzMWM4MDk2YjYyNzI0NWQ3NmFlNTMwMzZjMDkwPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDE0LTAyLTE5VDAxOjEwOjQ5WiIgUmVjaXBpZW50PSJodHRwczovL2V4YW1wbGUuY29tL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fYWYzZDRhNzEwZmM4YjMwNTg4NGI5NmQwMDk0YWI2Mjg4MDJmNTY5MiIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAyLTE5VDAxOjA1OjE5WiIgTm90T25PckFmdGVyPSIyMDE0LTAyLTE5VDAxOjEwOjQ5WiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL2V4YW1wbGUuY29tL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDItMThUMTk6NDI6MjBaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTAyLTE5VDA5OjA1OjQ5WiIgU2Vzc2lvbkluZGV4PSJfMGY0ZjE4OGRjMWJmZDNiZmVhMzZhMTYzNGE3NDQxNTgzYWZjM2IzNzgxIj48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlckBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== diff --git a/tests/data/responses/invalids/not_before_failed.xml.base64 b/tests/data/responses/invalids/not_before_failed.xml.base64 new file mode 100644 index 00000000..6c15a076 --- /dev/null +++ b/tests/data/responses/invalids/not_before_failed.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZnhmYTk3ZWVkNS03NTg4LTBkMjMtMmFkNy1mYTY2ZjI4OTM3ODgiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjA1OjQ5WiIgRGVzdGluYXRpb249Imh0dHBzOi8vZXhhbXBsZS5jb20vbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl9hZjNkNGE3MTBmYzhiMzA1ODg0Yjk2ZDAwOTRhYjYyODgwMmY1NjkyIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9leGFtcGxlLmNvbS9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZng3ZTQ4ZmQ3NS1jZTJiLTYwZWQtMjllZS1lNzk4NDZjOTU5YzYiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjA1OjQ5WiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vZXhhbXBsZS5jb20vc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj4NCjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9leGFtcGxlLmNvbS9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj40OTI4ODI2MTVhY2YzMWM4MDk2YjYyNzI0NWQ3NmFlNTMwMzZjMDkwPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDE0LTAyLTE5VDAxOjEwOjQ5WiIgUmVjaXBpZW50PSJodHRwczovL2V4YW1wbGUuY29tL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fYWYzZDRhNzEwZmM4YjMwNTg4NGI5NmQwMDk0YWI2Mjg4MDJmNTY5MiIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDI0LTAyLTE5VDAxOjA1OjE5WiIgTm90T25PckFmdGVyPSIyMDE0LTAyLTE5VDAxOjEwOjQ5WiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL2V4YW1wbGUuY29tL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDItMThUMTk6NDI6MjBaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTAyLTE5VDA5OjA1OjQ5WiIgU2Vzc2lvbkluZGV4PSJfMGY0ZjE4OGRjMWJmZDNiZmVhMzZhMTYzNGE3NDQxNTgzYWZjM2IzNzgxIj48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlckBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== diff --git a/tests/data/responses/invalids/response_encrypted_attrs.xml.base64 b/tests/data/responses/invalids/response_encrypted_attrs.xml.base64 new file mode 100644 index 00000000..ad6392c5 --- /dev/null +++ b/tests/data/responses/invalids/response_encrypted_attrs.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaGh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocCIgSW5SZXNwb25zZVRvPSJfNTdiY2JmNzAtN2IxZi0wMTJlLWM4MjEtNzgyYmNiMTNiYjM4Ii8+DQogICAgICA8L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0wNi0xN1QxNDo1Mzo0NFoiIE5vdE9uT3JBZnRlcj0iMjA5OS0wNi0xN1QxNDo1OToxNFoiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPg0KICAgICAgPC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgPC9zYW1sOkNvbmRpdGlvbnM+DQogICAgPHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDExLTA2LTE3VDE0OjU0OjA3WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjA5OS0wNi0xN1QyMjo1NDoxNFoiIFNlc3Npb25JbmRleD0iXzUxYmUzNzk2NWZlYjU1NzlkODAzMTQxMDc2OTM2ZGMyZTlkMWQ5OGViZiI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KIDxzYW1sOkVuY3J5cHRlZEF0dHJpYnV0ZSB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj4NCiAgICAgICAgICA8eGVuYzpFbmNyeXB0ZWREYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyIgVHlwZT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjRWxlbWVudCIgSWQ9Il9GMzk2MjVBRjY4QjRGQzA3OENDNzU4MkQyOEQwNUQ5QyI+DQogICAgICAgICAgICA8eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjYWVzMjU2LWNiYyIvPg0KICAgICAgICAgICAgPGRzOktleUluZm8geG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICAgICAgICAgICAgICA8eGVuYzpFbmNyeXB0ZWRLZXk+DQogICAgICAgICAgICAgICAgPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIi8+DQogICAgICAgICAgICAgICAgPGRzOktleUluZm8geG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogICAgICAgICAgICAgICAgICA8ZHM6S2V5TmFtZT42MjM1NWZiZDFmNjI0NTAzYzVjOTY3NzQwMmVjY2EwMGVmMWY2Mjc3PC9kczpLZXlOYW1lPg0KICAgICAgICAgICAgICAgIDwvZHM6S2V5SW5mbz4NCiAgICAgICAgICAgICAgICA8eGVuYzpDaXBoZXJEYXRhPg0KICAgICAgICAgICAgICAgICAgPHhlbmM6Q2lwaGVyVmFsdWU+SzBtQkx4Zkx6aUtWVUtFQU9ZZTdENnVWU0NQeTh2eVdWaDNSZWNuUEVTKzhRa0FoT3VSU3VFL0xRcEZyMGh1SS9pQ0V5OXBkZTFRZ2pZREx0akhjdWpLaTJ4R3FXNmprWFcvRXVLb21xV1BQQTJ4WXMxZnBCMXN1NGFYVU9RQjZPSjcwL29EY09zeTgzNGdoRmFCV2lsRThmcXlEQlVCdlcrMkl2YU1VWmFid04vczltVmtXek0zcjMwdGxraExLN2lPcmJHQWxkSUh3RlU1ejdQUFI2Uk8zWTNmSXhqSFU0ME9uTHNKYzN4SXFkTEgzZlhwQzBrZ2k1VXNwTGRxMTRlNU9vWGpMb1BHM0JPM3p3T0FJSjhYTkJXWTV1UW9mNktyS2JjdnRaU1kwZk12UFloWWZOanRSRnk4eTQ5b3ZMOWZ3akNSVERsVDUrYUhxc0NUQnJ3PT08L3hlbmM6Q2lwaGVyVmFsdWU+DQogICAgICAgICAgICAgICAgPC94ZW5jOkNpcGhlckRhdGE+DQogICAgICAgICAgICAgIDwveGVuYzpFbmNyeXB0ZWRLZXk+DQogICAgICAgICAgICA8L2RzOktleUluZm8+DQogICAgICAgICAgICA8eGVuYzpDaXBoZXJEYXRhPg0KICAgICAgICAgICAgICA8eGVuYzpDaXBoZXJWYWx1ZT5aekN1NmF4R2dBWVpIVmY3N05YOGFwWktCL0dKRGV1VjZiRkJ5QlMwQUlnaVhrdkRVQW1MQ3BhYlRBV0JNK3l6MTlvbEE2cnJ5dU9mcjgyZXYyYnpQTlVSdm00U1l4YWh2dUw0UGlibjV3Smt5MEJsNTRWcW1jVStBcWowZEF2T2dxRzF5M1g0d085bjliUnNUdjY5MjFtMGVxUkFGcGg4a0s4TDloaXJLMUJ4WUJZajJSeUZDb0ZEUHhWWjV3eXJhM3E0cW1FNC9FTFFwRlA2bWZVOExYYjB1b1dKVWpHVWVsUzJBYTdiWmlzOHpFcHdvdjRDd3RsTmpsdFFpaDRtdjd0dENBZllxY1FJRnpCVEIrREFhMCtYZ2d4Q0xjZEIzK21RaVJjRUNCZndISEo3Z1JtbnVCRWdlV1QzQ0dLYTNOYjdHTVhPZnV4RktGNXBJZWhXZ28za2ROUUxhbG9yOFJWVzZJOFAvSThmUTMzRmUrTnNIVm5KM3p3U0EvL2E8L3hlbmM6Q2lwaGVyVmFsdWU+DQogICAgICAgICAgICA8L3hlbmM6Q2lwaGVyRGF0YT4NCiAgICAgICAgICA8L3hlbmM6RW5jcnlwdGVkRGF0YT4NCiAgICAgICAgPC9zYW1sOkVuY3J5cHRlZEF0dHJpYnV0ZT4NCiAgICA8L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICA8L3NhbWw6QXNzZXJ0aW9uPg0KPC9zYW1scDpSZXNwb25zZT4= \ 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/signature_wrapping_attack2.xml.base64 b/tests/data/responses/invalids/signature_wrapping_attack2.xml.base64 new file mode 100644 index 00000000..408b2f77 --- /dev/null +++ b/tests/data/responses/invalids/signature_wrapping_attack2.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxuczA6UmVzcG9uc2UgeG1sbnM6bnMwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOm5zMT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgeG1sbnM6bnMyPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiBEZXN0aW5hdGlvbj0iaHR0cDovL2xpbmdvbi5jYXRhbG9naXguc2U6ODA4Ny8iIElEPSJpZC12cU9RNzJKQ3BwWGFCV25CRSIgSW5SZXNwb25zZVRvPSJpZDEyIiBJc3N1ZUluc3RhbnQ9IjIwMTktMTItMjBUMTI6MTU6MTZaIiBWZXJzaW9uPSIyLjAiPjxuczE6SXNzdWVyIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6ZW50aXR5Ij51cm46bWFjZTpleGFtcGxlLmNvbTpzYW1sOnJvbGFuZDppZHA8L25zMTpJc3N1ZXI+PG5zMDpTdGF0dXM+PG5zMDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L25zMDpTdGF0dXM+PG5zMTpBc3NlcnRpb24gSUQ9ImlkLVNQT09GRURfQVNTRVJUSU9OIiBJc3N1ZUluc3RhbnQ9IjIwMTktMTItMjBUMTI6MTU6MTZaIiBWZXJzaW9uPSIyLjAiPjxuczE6SXNzdWVyIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6ZW50aXR5Ij51cm46bWFjZTpleGFtcGxlLmNvbTpzYW1sOnJvbGFuZDppZHA8L25zMTpJc3N1ZXI+PG5zMjpTaWduYXR1cmUgSWQ9IlNpZ25hdHVyZTIiPjxuczI6U2lnbmVkSW5mbz48bnMyOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48bnMyOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPjxuczI6UmVmZXJlbmNlIFVSST0iI2lkLUFhOUlXZkR4SlZJWDZHUXllIj48bnMyOlRyYW5zZm9ybXM+PG5zMjpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxuczI6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9uczI6VHJhbnNmb3Jtcz48bnMyOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PG5zMjpEaWdlc3RWYWx1ZT5FV0J2UVVscndRYnRyQWp1VVhrU0JBVnNaNTA9PC9uczI6RGlnZXN0VmFsdWU+PC9uczI6UmVmZXJlbmNlPjwvbnMyOlNpZ25lZEluZm8+PG5zMjpTaWduYXR1cmVWYWx1ZT5tNHpSZ1RXbGVNY3gxZEZib2VpWWxiaURpZ0hXQVZoSFZhK0dMTisrRUxOTUZEdXR1ekJ4YzN0dTZva3lhTlFHVzNsZXUzMnd6YmZkcGI1KzNSbHBHb0tqMndQWDU3MC9FTUpqNHV3OTFYZlhzWmZwTlArNUdsZ05UOHcvZWxEbUJYaEcvS3dtU080NzdJbWswc3pLb3ZUQk1WSG1vM1FPZCtiYS8vZFZzSkU9PC9uczI6U2lnbmF0dXJlVmFsdWU+PG5zMjpLZXlJbmZvPjxuczI6WDUwOURhdGE+PG5zMjpYNTA5Q2VydGlmaWNhdGU+TUlJQ3NEQ0NBaG1nQXdJQkFnSUpBSnJ6cVNTd21EWTlNQTBHQ1NxR1NJYjNEUUVCQlFVQU1FVXhDekFKQmdOVkJBWVRBa0ZWTVJNd0VRWURWUVFJRXdwVGIyMWxMVk4wWVhSbE1TRXdId1lEVlFRS0V4aEpiblJsY201bGRDQlhhV1JuYVhSeklGQjBlU0JNZEdRd0hoY05NRGt4TURBMk1UazBPVFF4V2hjTk1Ea3hNVEExTVRrME9UUXhXakJGTVFzd0NRWURWUVFHRXdKQlZURVRNQkVHQTFVRUNCTUtVMjl0WlMxVGRHRjBaVEVoTUI4R0ExVUVDaE1ZU1c1MFpYSnVaWFFnVjJsa1oybDBjeUJRZEhrZ1RIUmtNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURKZzJjbXM3TXFqbmlUOEZpL1hrTkhaTlBiTlZReU1VTVhFOXRYT2Rxd1lDQTFjYzh2UWR6a2loc2NRTVh5M2lQdzJjTWdnQnU2Z2pNVE9TT3hFQ2t1dlg1WkNjbEtyOHBYQUpNNWNZNmdWT2FWTzJQZFRaY3ZEQktHYmlhTmVmaUV3NWhub1pvbXFaR3A4d0hOTEFVa3d0SDl2anFxdnh5Uy92Y2xjNmsyZXdJREFRQUJvNEduTUlHa01CMEdBMVVkRGdRV0JCUmVQc0tIS1lKc2lvakU3OFpXWGNjSzlLNGFKVEIxQmdOVkhTTUViakJzZ0JSZVBzS0hLWUpzaW9qRTc4WldYY2NLOUs0YUphRkpwRWN3UlRFTE1Ba0dBMVVFQmhNQ1FWVXhFekFSQmdOVkJBZ1RDbE52YldVdFUzUmhkR1V4SVRBZkJnTlZCQW9UR0VsdWRHVnlibVYwSUZkcFpHZHBkSE1nVUhSNUlFeDBaSUlKQUpyenFTU3dtRFk5TUF3R0ExVWRFd1FGTUFNQkFmOHdEUVlKS29aSWh2Y05BUUVGQlFBRGdZRUFKU3JLT0V6SE83VEw1Y3k2aDNxaCszK0pBazhIYkdCVytjYlg2S0JDQXcvbXpVOGZsSzI1dm5Xd1hTM2R2MkZGM0FvZDAvUzdBV05mS2liNVUvU0E5bkphei9tV2VGOVMwZmFyejlBUUZjOC9OU3pBemFWcTdZYk00RjZmNk4yRlJsN0dpa2RYUkNlZDQ1ajZtclB6R3prM0VDYnVwRm5xeVJFSDMrWlBTZGs9PC9uczI6WDUwOUNlcnRpZmljYXRlPjwvbnMyOlg1MDlEYXRhPjwvbnMyOktleUluZm8+PC9uczI6U2lnbmF0dXJlPjxuczE6U3ViamVjdD48bnMxOk5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnRyYW5zaWVudCIgTmFtZVF1YWxpZmllcj0iIiBTUE5hbWVRdWFsaWZpZXI9ImlkMTIiPkFOT1RIRVJfSUQ8L25zMTpOYW1lSUQ+PG5zMTpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PG5zMTpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBJblJlc3BvbnNlVG89ImlkMTIiIE5vdE9uT3JBZnRlcj0iMjAxOS0xMi0yMFQxMjoyMDoxNloiIFJlY2lwaWVudD0iaHR0cDovL2xpbmdvbi5jYXRhbG9naXguc2U6ODA4Ny8iLz48L25zMTpTdWJqZWN0Q29uZmlybWF0aW9uPjwvbnMxOlN1YmplY3Q+PG5zMTpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxOS0xMi0yMFQxMjoxNToxNloiIE5vdE9uT3JBZnRlcj0iMjAxOS0xMi0yMFQxMjoyMDoxNloiPjxuczE6QXVkaWVuY2VSZXN0cmljdGlvbj48bnMxOkF1ZGllbmNlPnVybjptYWNlOmV4YW1wbGUuY29tOnNhbWw6cm9sYW5kOnNwPC9uczE6QXVkaWVuY2U+PC9uczE6QXVkaWVuY2VSZXN0cmljdGlvbj48L25zMTpDb25kaXRpb25zPjxuczE6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE5LTEyLTIwVDEyOjE1OjE2WiIgU2Vzc2lvbkluZGV4PSJpZC1lRWhOQ2M1QlNpZXNWT2w4QiI+PG5zMTpBdXRobkNvbnRleHQ+PG5zMTpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpJbnRlcm5ldFByb3RvY29sUGFzc3dvcmQ8L25zMTpBdXRobkNvbnRleHRDbGFzc1JlZj48bnMxOkF1dGhlbnRpY2F0aW5nQXV0aG9yaXR5Pmh0dHA6Ly93d3cuZXhhbXBsZS5jb20vbG9naW48L25zMTpBdXRoZW50aWNhdGluZ0F1dGhvcml0eT48L25zMTpBdXRobkNvbnRleHQ+PC9uczE6QXV0aG5TdGF0ZW1lbnQ+PG5zMTpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PG5zMTpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZT0idXJuOm9pZDoxLjMuNi4xLjQuMS41OTIzLjEuMS4xLjEiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48bnMxOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeHNpOnR5cGU9InhzOnN0cmluZyI+c3RhZmY8L25zMTpBdHRyaWJ1dGVWYWx1ZT48bnMxOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeHNpOnR5cGU9InhzOnN0cmluZyI+QURNSU48L25zMTpBdHRyaWJ1dGVWYWx1ZT48L25zMTpBdHRyaWJ1dGU+PG5zMTpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJtYWlsIiBOYW1lPSJ1cm46b2lkOjAuOS4yMzQyLjE5MjAwMzAwLjEwMC4xLjMiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48bnMxOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeHNpOnR5cGU9InhzOnN0cmluZyI+SEFDS0VSQGdtYWlsLmNvbTwvbnMxOkF0dHJpYnV0ZVZhbHVlPjwvbnMxOkF0dHJpYnV0ZT48bnMxOkF0dHJpYnV0ZSBGcmllbmRseU5hbWU9ImdpdmVuTmFtZSIgTmFtZT0idXJuOm9pZDoyLjUuNC40MiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1cmkiPjxuczE6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5EZXJlazwvbnMxOkF0dHJpYnV0ZVZhbHVlPjwvbnMxOkF0dHJpYnV0ZT48bnMxOkF0dHJpYnV0ZSBGcmllbmRseU5hbWU9InN1ck5hbWUiIE5hbWU9InVybjpvaWQ6Mi41LjQuNCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1cmkiPjxuczE6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5KZXRlcjwvbnMxOkF0dHJpYnV0ZVZhbHVlPjwvbnMxOkF0dHJpYnV0ZT48bnMxOkF0dHJpYnV0ZSBGcmllbmRseU5hbWU9InRpdGxlIiBOYW1lPSJ1cm46b2lkOjIuNS40LjEyIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+PG5zMTpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNob3J0c3RvcDwvbnMxOkF0dHJpYnV0ZVZhbHVlPjwvbnMxOkF0dHJpYnV0ZT48L25zMTpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9uczE6QXNzZXJ0aW9uPg0KPFhTV19BVFRBQ0s+DQo8bnMxOkFzc2VydGlvbiBJRD0iaWQtQWE5SVdmRHhKVklYNkdReWUiIElzc3VlSW5zdGFudD0iMjAxOS0xMi0yMFQxMjoxNToxNloiIFZlcnNpb249IjIuMCI+PG5zMTpJc3N1ZXIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDplbnRpdHkiPnVybjptYWNlOmV4YW1wbGUuY29tOnNhbWw6cm9sYW5kOmlkcDwvbnMxOklzc3Vlcj48bnMxOlN1YmplY3Q+PG5zMTpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDp0cmFuc2llbnQiIE5hbWVRdWFsaWZpZXI9IiIgU1BOYW1lUXVhbGlmaWVyPSJpZDEyIj5hYzViMjJiYjhlYWM0YTI2ZWQwN2E1NTQzMmEwZmUwZGEyNDNmNmU5MTFhYTYxNGNmZjQwMmM0NGQ3Y2RlYzM2PC9uczE6TmFtZUlEPjxuczE6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxuczE6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgSW5SZXNwb25zZVRvPSJpZDEyIiBOb3RPbk9yQWZ0ZXI9IjIwMTktMTItMjBUMTI6MjA6MTZaIiBSZWNpcGllbnQ9Imh0dHA6Ly9saW5nb24uY2F0YWxvZ2l4LnNlOjgwODcvIi8+PC9uczE6U3ViamVjdENvbmZpcm1hdGlvbj48L25zMTpTdWJqZWN0PjxuczE6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTktMTItMjBUMTI6MTU6MTZaIiBOb3RPbk9yQWZ0ZXI9IjIwMTktMTItMjBUMTI6MjA6MTZaIj48bnMxOkF1ZGllbmNlUmVzdHJpY3Rpb24+PG5zMTpBdWRpZW5jZT51cm46bWFjZTpleGFtcGxlLmNvbTpzYW1sOnJvbGFuZDpzcDwvbnMxOkF1ZGllbmNlPjwvbnMxOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9uczE6Q29uZGl0aW9ucz48bnMxOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxOS0xMi0yMFQxMjoxNToxNloiIFNlc3Npb25JbmRleD0iaWQtZUVoTkNjNUJTaWVzVk9sOEIiPjxuczE6QXV0aG5Db250ZXh0PjxuczE6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6SW50ZXJuZXRQcm90b2NvbFBhc3N3b3JkPC9uczE6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PG5zMTpBdXRoZW50aWNhdGluZ0F1dGhvcml0eT5odHRwOi8vd3d3LmV4YW1wbGUuY29tL2xvZ2luPC9uczE6QXV0aGVudGljYXRpbmdBdXRob3JpdHk+PC9uczE6QXV0aG5Db250ZXh0PjwvbnMxOkF1dGhuU3RhdGVtZW50PjxuczE6QXR0cmlidXRlU3RhdGVtZW50PjxuczE6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0iZWR1UGVyc29uQWZmaWxpYXRpb24iIE5hbWU9InVybjpvaWQ6MS4zLjYuMS40LjEuNTkyMy4xLjEuMS4xIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+PG5zMTpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhzaTp0eXBlPSJ4czpzdHJpbmciPnN0YWZmPC9uczE6QXR0cmlidXRlVmFsdWU+PG5zMTpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhzaTp0eXBlPSJ4czpzdHJpbmciPm1lbWJlcjwvbnMxOkF0dHJpYnV0ZVZhbHVlPjwvbnMxOkF0dHJpYnV0ZT48bnMxOkF0dHJpYnV0ZSBGcmllbmRseU5hbWU9Im1haWwiIE5hbWU9InVybjpvaWQ6MC45LjIzNDIuMTkyMDAzMDAuMTAwLjEuMyIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1cmkiPjxuczE6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5mb29AZ21haWwuY29tPC9uczE6QXR0cmlidXRlVmFsdWU+PC9uczE6QXR0cmlidXRlPjxuczE6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0iZ2l2ZW5OYW1lIiBOYW1lPSJ1cm46b2lkOjIuNS40LjQyIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+PG5zMTpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhzaTp0eXBlPSJ4czpzdHJpbmciPkRlcmVrPC9uczE6QXR0cmlidXRlVmFsdWU+PC9uczE6QXR0cmlidXRlPjxuczE6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0ic3VyTmFtZSIgTmFtZT0idXJuOm9pZDoyLjUuNC40IiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+PG5zMTpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhzaTp0eXBlPSJ4czpzdHJpbmciPkpldGVyPC9uczE6QXR0cmlidXRlVmFsdWU+PC9uczE6QXR0cmlidXRlPjxuczE6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0idGl0bGUiIE5hbWU9InVybjpvaWQ6Mi41LjQuMTIiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48bnMxOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeHNpOnR5cGU9InhzOnN0cmluZyI+c2hvcnRzdG9wPC9uczE6QXR0cmlidXRlVmFsdWU+PC9uczE6QXR0cmlidXRlPjwvbnMxOkF0dHJpYnV0ZVN0YXRlbWVudD48L25zMTpBc3NlcnRpb24+DQo8L1hTV19BVFRBQ0s+DQo8L25zMDpSZXNwb25zZT4= \ 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..736ea793 --- /dev/null +++ b/tests/data/responses/invalids/signed_assertion_response.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIERlc3RpbmF0aW9uPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIElEPSJwZnhkYjRkOWVmZS1kMGFkLTAwODYtY2U4OC1jMjg4Njg3Y2FjNjEiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNjEyYmJmOWIxNjQ1Mjk0YWEwYjQ2MzdiMWJjNWYzOWRlOGI3OWNlYiIgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTMxVDAwOjM3OjE2WiIgVmVyc2lvbj0iMi4wIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZnhkYjRkOWVmZS1kMGFkLTAwODYtY2U4OC1jMjg4Njg3Y2FjNjEiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPmpjMklRWFNoc3dzTG85TkdJSHp2cGtBaXY4ND08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+aUVqR2QrdWFqSVArYU9ucGo4MjYxUzRBaWdMeXJqc0pheTJzdVFKakhhVHlETlh4TFhWQ3AxZG1PR0JhZGhmRUtnWVJsaTFBZDA1QktBejlpd3NBME14OGZ6SmFhSlBUbHM2NS93ODZTSEN4NTdrNXhteDBSUjhuR09MOU1vb2lidnZWeTVRODl2Z2lnVWN5cWJUY0dxaU5uSVNCWGZuYVR2dnpQYS9QbWJ3PTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDVnpDQ0FjQUNDUURJVkhhTlNCWUw2VEFOQmdrcWhraUc5dzBCQVFzRkFEQndNUXN3Q1FZRFZRUUdFd0pHVWpFT01Bd0dBMVVFQ0F3RlVHRnlhWE14RGpBTUJnTlZCQWNNQlZCaGNtbHpNUll3RkFZRFZRUUtEQTFPYjNaaGNHOXpkQ0JVUlZOVU1Ta3dKd1lKS29aSWh2Y05BUWtCRmhwbWJHOXlaVzUwTG5CcFoyOTFkRUJ1YjNaaGNHOXpkQzVtY2pBZUZ3MHhOREF5TVRNeE16VXpOREJhRncweE5UQXlNVE14TXpVek5EQmFNSEF4Q3pBSkJnTlZCQVlUQWtaU01RNHdEQVlEVlFRSURBVlFZWEpwY3pFT01Bd0dBMVVFQnd3RlVHRnlhWE14RmpBVUJnTlZCQW9NRFU1dmRtRndiM04wSUZSRlUxUXhLVEFuQmdrcWhraUc5dzBCQ1FFV0dtWnNiM0psYm5RdWNHbG5iM1YwUUc1dmRtRndiM04wTG1aeU1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRQ2hMRkhuM0xuTjRKUS83V0NkWXVweGtVZ2NOT1FuUEYreWxsKy9EUHB1eDlucGZZMDU5UElVYXRCOFg3a0NuNWk4dFJ3SXkvaWtISlI2TXI4K01QdmM2Vk9aRHhQTmRadk1vLzhsaHhyYk4zSmRydzN3aFptVS9LUFI5RjNCZEZkdStTTHpyTWwxVERVWmxQdFk5WHpVRlhjcU44SVhjeThUSnpDQmVOZXkzUUlEQVFBQk1BMEdDU3FHU0liM0RRRUJDd1VBQTRHQkFDdEo4ZmVHemUxTkhCNVZ3MThqTVVQdkhvN0gzR3dtajZaREFYUWxhaUFYTXVOQnhOWFZXVndpZmw2VituVzN3OVFhN0Zlby9uWi9PNFRVT0gxbnorYWRrbGNDRDRRcFphRUlibUFicmlQV0pLZ2I0TFdHaHFRcnV3WVI3SXRUUjFNTlg5Z0xiUDB6MHp2REVRbm50L1ZVV0ZFQkxTSnE0WjROcmU4TEZtUzI8L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpBc3NlcnRpb24geG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiBJRD0icGZ4N2UzZjFmMTEtM2QzOC03ZGE1LTU1ZWQtOWI0ZDZjMGE0NGViIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMzFUMDA6Mzc6MTZaIiBWZXJzaW9uPSIyLjAiPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDdlM2YxZjExLTNkMzgtN2RhNS01NWVkLTliNGQ2YzBhNDRlYiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+NndXc3psZkZZYnRkczZ0eStuK090REp2S0VBPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5lVUU5MWhQNm02d1ZVbXdJYlZKU2Z4VnZKaTlRcHd0MGRqSFA0aXFucjJONmNmVlZld3hEVTNHV0E5bDlaVmp5bVdvdkZJbS9ZNHRkd1UzNEdkYmkvMmlobzJndDhlVkd3OGozUnVRaE1VSHNWZitoSGgyZUg4bjB4amRBanRkaE5ISE96TDJ0RFd4WGs4Nk9lWW5sOEpadVU3QkVFU2VLZUM5YngwT1FuWU09PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNWekNDQWNBQ0NRRElWSGFOU0JZTDZUQU5CZ2txaGtpRzl3MEJBUXNGQURCd01Rc3dDUVlEVlFRR0V3SkdVakVPTUF3R0ExVUVDQXdGVUdGeWFYTXhEakFNQmdOVkJBY01CVkJoY21sek1SWXdGQVlEVlFRS0RBMU9iM1poY0c5emRDQlVSVk5VTVNrd0p3WUpLb1pJaHZjTkFRa0JGaHBtYkc5eVpXNTBMbkJwWjI5MWRFQnViM1poY0c5emRDNW1jakFlRncweE5EQXlNVE14TXpVek5EQmFGdzB4TlRBeU1UTXhNelV6TkRCYU1IQXhDekFKQmdOVkJBWVRBa1pTTVE0d0RBWURWUVFJREFWUVlYSnBjekVPTUF3R0ExVUVCd3dGVUdGeWFYTXhGakFVQmdOVkJBb01EVTV2ZG1Gd2IzTjBJRlJGVTFReEtUQW5CZ2txaGtpRzl3MEJDUUVXR21ac2IzSmxiblF1Y0dsbmIzVjBRRzV2ZG1Gd2IzTjBMbVp5TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FDaExGSG4zTG5ONEpRLzdXQ2RZdXB4a1VnY05PUW5QRit5bGwrL0RQcHV4OW5wZlkwNTlQSVVhdEI4WDdrQ241aTh0UndJeS9pa0hKUjZNcjgrTVB2YzZWT1pEeFBOZFp2TW8vOGxoeHJiTjNKZHJ3M3doWm1VL0tQUjlGM0JkRmR1K1NMenJNbDFURFVabFB0WTlYelVGWGNxTjhJWGN5OFRKekNCZU5leTNRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkN3VUFBNEdCQUN0SjhmZUd6ZTFOSEI1VncxOGpNVVB2SG83SDNHd21qNlpEQVhRbGFpQVhNdU5CeE5YVldWd2lmbDZWK25XM3c5UWE3RmVvL25aL080VFVPSDFueithZGtsY0NENFFwWmFFSWJtQWJyaVBXSktnYjRMV0docVFydXdZUjdJdFRSMU1OWDlnTGJQMHowenZERVFubnQvVlVXRkVCTFNKcTRaNE5yZThMRm1TMjwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6dHJhbnNpZW50IiBTUE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwIj5fM2FmNjJmMWQwMzUxM2JkZDYxZGQ1YmYwNGQzZGViN2FhNjE3NDgwZTIyPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl82MTJiYmY5YjE2NDUyOTRhYTBiNDYzN2IxYmM1ZjM5ZGU4Yjc5Y2ViIiBOb3RPbk9yQWZ0ZXI9IjI5OTMtMTAtMDJUMDU6NTc6MTZaIiBSZWNpcGllbnQ9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAzLTMxVDAwOjM2OjQ2WiIgTm90T25PckFmdGVyPSIyOTkzLTEwLTAyVDA1OjU3OjE2WiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDMtMzFUMDA6Mzc6MTZaIiBTZXNzaW9uSW5kZXg9Il84NWU3Y2ZlMTZkNmU3ZTYwMGJkOThiYmMyYjQzNzFlMWM2OTU4OGE0ZGEiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjI5OTMtMDMtMzFUMDg6Mzc6MTZaIj48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== diff --git a/tests/data/responses/invalids/status_code_responder.xml.base64 b/tests/data/responses/invalids/status_code_responder.xml.base64 new file mode 100644 index 00000000..56996dd2 --- /dev/null +++ b/tests/data/responses/invalids/status_code_responder.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlDQp4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIg0KQ29uc2VudD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNvbnNlbnQ6dW5zcGVjaWZpZWQiIERlc3RpbmF0aW9uPSINCmh0dHBzOi8vZXhhbXBsZS5jb20vb3BlbnNzby9Db25zdW1lci9tZXRhQWxpYXMvc3AiDQpJRD0iX2E3MWJiZjIyLTkwYTktNGE5Ni1iOWNlLWVhNWJhMzBhZWU2NSINCkluUmVzcG9uc2VUbz0iczIxMjAzYjI3ZDM4ZDBhMDdlYTJjNzEzZTdhMDA0NWNmOGQxZTMyODExIg0KSXNzdWVJbnN0YW50PSIyMDExLTA4LTI0VDE2OjM2OjMwLjM2NVoiIFZlcnNpb249IjIuMCI+PElzc3Vlcg0KeG1sbnM9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPg0KaHR0cDovL2lkcC5leGFtcGxlLmNvbS9hZGZzL3NlcnZpY2VzL3RydXN0PC9Jc3N1ZXI+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZQ0KVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6UmVzcG9uZGVyIi8+PC9zYW1scDpTdGF0dXM+PC9zYW1scDpSZXNwb25zZT4NCg== diff --git a/tests/data/responses/invalids/status_code_responer_and_msg.xml.base64 b/tests/data/responses/invalids/status_code_responer_and_msg.xml.base64 new file mode 100644 index 00000000..d2697a69 --- /dev/null +++ b/tests/data/responses/invalids/status_code_responer_and_msg.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlDQp4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIg0KQ29uc2VudD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNvbnNlbnQ6dW5zcGVjaWZpZWQiIERlc3RpbmF0aW9uPSINCmh0dHBzOi8vZXhhbXBsZS5jb20vb3BlbnNzby9Db25zdW1lci9tZXRhQWxpYXMvc3AiDQpJRD0iX2E3MWJiZjIyLTkwYTktNGE5Ni1iOWNlLWVhNWJhMzBhZWU2NSINCkluUmVzcG9uc2VUbz0iczIxMjAzYjI3ZDM4ZDBhMDdlYTJjNzEzZTdhMDA0NWNmOGQxZTMyODExIg0KSXNzdWVJbnN0YW50PSIyMDExLTA4LTI0VDE2OjM2OjMwLjM2NVoiIFZlcnNpb249IjIuMCI+PElzc3Vlcg0KeG1sbnM9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPg0KaHR0cDovL2lkcC5leGFtcGxlLmNvbS9hZGZzL3NlcnZpY2VzL3RydXN0PC9Jc3N1ZXI+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpSZXNwb25kZXIiLz48c2FtbHA6U3RhdHVzTWVzc2FnZT5zb21ldGhpbmdfaXNfd3Jvbmc8L3NhbWxwOlN0YXR1c01lc3NhZ2U+PC9zYW1scDpTdGF0dXM+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/no_audience.xml.base64 b/tests/data/responses/no_audience.xml.base64 new file mode 100644 index 00000000..25550e3b --- /dev/null +++ b/tests/data/responses/no_audience.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICA8L3NhbWw6Q29uZGl0aW9ucz4NCiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MDdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDk5LTA2LTE3VDIyOjU0OjE0WiIgU2Vzc2lvbkluZGV4PSJfNTFiZTM3OTY1ZmViNTU3OWQ4MDMxNDEwNzY5MzZkYzJlOWQxZDk4ZWJmIj4NCiAgICAgIDxzYW1sOkF1dGhuQ29udGV4dD4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0Pg0KICAgIDwvc2FtbDpBdXRoblN0YXRlbWVudD4NCiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNvbWVvbmVAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/tests/data/responses/open_saml_response.xml b/tests/data/responses/open_saml_response.xml new file mode 100644 index 00000000..67dea720 --- /dev/null +++ b/tests/data/responses/open_saml_response.xml @@ -0,0 +1,56 @@ + + + https://idm.orademo.com + + + + + + + + + + + + + uHuSry39P16Yh7srS32xESmj4Lw= + + + fdghdfggfd= + + + dfghjkl + + + + + + + + https://idm.orademo.com + + someone@example.org + + + + + + + hello.com + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + + + + + Someone + + + Special + + + + 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..8ac3541e --- /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 + + + + mv5lfRE63rPIrb29tQ6Qbfe/yvY=yQvrNsoSwWXL3rSW3sH6iyfG0ukrq1tyvsydSrs63wz1ayaVY/uXBQuldn1VcQmOZpQgDwnZwjmb5fU+CZejQw5dTtj2mJR50TO1Wj81upVEtQByF2RyOP1GsB27dSeYRTMYsK64ywzIxfltw02BBQUqpz10i3EtlVh46hc+LWY= +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/response1.xml.base64 b/tests/data/responses/response1.xml.base64 new file mode 100644 index 00000000..201c5246 --- /dev/null +++ b/tests/data/responses/response1.xml.base64 @@ -0,0 +1 @@ +DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgSUQ9IkdPU0FNTFIxMjkwMTE3NDU3MTc5NCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTAtMTEtMThUMjE6NTc6MzdaIiBEZXN0aW5hdGlvbj0ie3JlY2lwaWVudH0iPg0KICA8c2FtbHA6U3RhdHVzPg0KICAgIDxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgVmVyc2lvbj0iMi4wIiBJRD0icGZ4YTQ2NTc0ZGYtYjNiMC1hMDZhLTIzYzgtNjM2NDEzMTk4NzcyIiBJc3N1ZUluc3RhbnQ9IjIwMTAtMTEtMThUMjE6NTc6MzdaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cHM6Ly9hcHAub25lbG9naW4uY29tL3NhbWwvbWV0YWRhdGEvMTM1OTA8L3NhbWw6SXNzdWVyPg0KICAgIDxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICAgICAgPGRzOlNpZ25lZEluZm8+DQogICAgICAgIDxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgICAgICAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZnhhNDY1NzRkZi1iM2IwLWEwNmEtMjNjOC02MzY0MTMxOTg3NzIiPg0KICAgICAgICAgIDxkczpUcmFuc2Zvcm1zPg0KICAgICAgICAgICAgPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+DQogICAgICAgICAgICA8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgICAgICAgPC9kczpUcmFuc2Zvcm1zPg0KICAgICAgICAgIDxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPg0KICAgICAgICAgIDxkczpEaWdlc3RWYWx1ZT5wSlE3TVMvZWs0S1JSV0dtdi9INDNSZUhZTXM9PC9kczpEaWdlc3RWYWx1ZT4NCiAgICAgICAgPC9kczpSZWZlcmVuY2U+DQogICAgICA8L2RzOlNpZ25lZEluZm8+DQogICAgICA8ZHM6U2lnbmF0dXJlVmFsdWU+eWl2ZUtjUGREcHVETmo2c2hyUTNBQndyL2NBM0NyeUQycGhHL3hMWnN6S1d4VTUvbWxhS3Q4ZXdiWk9kS0t2dE9zMnBIQnk1RHVhM2s5NEFGK3p4R3llbDVnT293bW95WEpyK0FPcitrUE8wdmxpMVY4bzNoUFBVWndSZ1NYNlE5cFMxQ3FRZ2hLaUVhc1J5eWxxcUpVYVBZem1Pek9FOC9YbE1rd2lXbU8wPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQogICAgICA8ZHM6S2V5SW5mbz4NCiAgICAgICAgPGRzOlg1MDlEYXRhPg0KICAgICAgICAgIDxkczpYNTA5Q2VydGlmaWNhdGU+TUlJQnJUQ0NBYUdnQXdJQkFnSUJBVEFEQmdFQU1HY3hDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SVXdFd1lEVlFRSERBeFRZVzUwWVNCTmIyNXBZMkV4RVRBUEJnTlZCQW9NQ0U5dVpVeHZaMmx1TVJrd0Z3WURWUVFEREJCaGNIQXViMjVsYkc5bmFXNHVZMjl0TUI0WERURXdNRE13T1RBNU5UZzBOVm9YRFRFMU1ETXdPVEE1TlRnME5Wb3daekVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhGVEFUQmdOVkJBY01ERk5oYm5SaElFMXZibWxqWVRFUk1BOEdBMVVFQ2d3SVQyNWxURzluYVc0eEdUQVhCZ05WQkFNTUVHRndjQzV2Ym1Wc2IyZHBiaTVqYjIwd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFPalN1MWZqUHk4ZDV3NFF5TDEremQ0aEl3MU1ra2ZmNFdZL1RMRzhPWmtVNVlUU1dtbUhQRDVrdllINXVvWFMvNnFRODFxWHBSMndWOENUb3daSlVMZzA5ZGRSZFJuOFFzcWoxRnlPQzVzbEUzeTJiWjJvRnVhNzJvZi80OWZwdWpuRlQ2S25RNjFDQk1xbERvVFFxT1Q2MnZHSjhuUDZNWld2QTZzeHF1ZDVBZ01CQUFFd0F3WUJBQU1CQUE9PTwvZHM6WDUwOUNlcnRpZmljYXRlPg0KICAgICAgICA8L2RzOlg1MDlEYXRhPg0KICAgICAgPC9kczpLZXlJbmZvPg0KICAgIDwvZHM6U2lnbmF0dXJlPg0KICAgIDxzYW1sOlN1YmplY3Q+DQogICAgICA8c2FtbDpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPnN1cHBvcnRAb25lbG9naW4uY29tPC9zYW1sOk5hbWVJRD4NCiAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj4NCiAgICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDEwLTExLTE4VDIyOjAyOjM3WiIgUmVjaXBpZW50PSJ7cmVjaXBpZW50fSIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPg0KICAgIDwvc2FtbDpTdWJqZWN0Pg0KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDEwLTExLTE4VDIxOjUyOjM3WiIgTm90T25PckFmdGVyPSIyMDEwLTExLTE4VDIyOjAyOjM3WiI+DQogICAgICA8c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICA8c2FtbDpBdWRpZW5jZT57YXVkaWVuY2V9PC9zYW1sOkF1ZGllbmNlPg0KICAgICAgPC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgPC9zYW1sOkNvbmRpdGlvbnM+DQogICAgPHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOVQyMTo1NzozN1oiIFNlc3Npb25JbmRleD0iXzUzMWMzMmQyODNiZGZmN2UwNGU0ODdiY2RiYzRkZDhkIj4NCiAgICAgIDxzYW1sOkF1dGhuQ29udGV4dD4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0Pg0KICAgIDwvc2FtbDpBdXRoblN0YXRlbWVudD4NCiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5kZW1vPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPg0KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJhbm90aGVyX3ZhbHVlIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj52YWx1ZTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+DQo= \ No newline at end of file diff --git a/tests/data/responses/response1_with_friendlyname.xml.base64 b/tests/data/responses/response1_with_friendlyname.xml.base64 new file mode 100644 index 00000000..a17c3941 --- /dev/null +++ b/tests/data/responses/response1_with_friendlyname.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIElEPSJHT1NBTUxSMTI5MDExNzQ1NzE3OTQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiIgRGVzdGluYXRpb249IntyZWNpcGllbnR9Ij4NCiAgPHNhbWxwOlN0YXR1cz4NCiAgICA8c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+DQogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIFZlcnNpb249IjIuMCIgSUQ9InBmeDhmZmIzOTgzLWNiZjYtOTJhMS1mMmM0LTYxOWFlMWJlMWM4NiIgSXNzdWVJbnN0YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiI+DQogICAgPHNhbWw6SXNzdWVyPmh0dHBzOi8vYXBwLm9uZWxvZ2luLmNvbS9zYW1sL21ldGFkYXRhLzEzNTkwPC9zYW1sOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4NCiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4NCiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+DQogIDxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4OGZmYjM5ODMtY2JmNi05MmExLWYyYzQtNjE5YWUxYmUxYzg2Ij48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPjxkczpEaWdlc3RWYWx1ZT5oZ3VRYkNIYW5pYmJEQzdxM1p6eHpIY1Bvbkk9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPkdhbmNEOXZSb2g5TWJUMDAyRHk3OXQxbTZJNllmaFVLUGZibGttcDJ1ZG9sWHVqdjZlMU1XdnNWbXhOenRzSUdseEFhMHFLRGlTTXpDTkRac2szanN5c1VsMW5BS25BZzE4NWpmWGpzemhzZG1SK005MWR4azZrZmNMVW9zT29sb3ZhZFdMUFdxbjdQM2o4LzV4enA5THBSQTNndkI0MTgyUlNpcldDQlhQUT08L2RzOlNpZ25hdHVyZVZhbHVlPg0KPGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJQ2dUQ0NBZW9DQ1FDYk9scldEZFg3RlRBTkJna3Foa2lHOXcwQkFRVUZBRENCaERFTE1Ba0dBMVVFQmhNQ1RrOHhHREFXQmdOVkJBZ1REMEZ1WkhKbFlYTWdVMjlzWW1WeVp6RU1NQW9HQTFVRUJ4TURSbTl2TVJBd0RnWURWUVFLRXdkVlRrbE9SVlJVTVJnd0ZnWURWUVFERXc5bVpXbGtaUzVsY214aGJtY3VibTh4SVRBZkJna3Foa2lHOXcwQkNRRVdFbUZ1WkhKbFlYTkFkVzVwYm1WMGRDNXViekFlRncwd056QTJNVFV4TWpBeE16VmFGdzB3TnpBNE1UUXhNakF4TXpWYU1JR0VNUXN3Q1FZRFZRUUdFd0pPVHpFWU1CWUdBMVVFQ0JNUFFXNWtjbVZoY3lCVGIyeGlaWEpuTVF3d0NnWURWUVFIRXdOR2IyOHhFREFPQmdOVkJBb1RCMVZPU1U1RlZGUXhHREFXQmdOVkJBTVREMlpsYVdSbExtVnliR0Z1Wnk1dWJ6RWhNQjhHQ1NxR1NJYjNEUUVKQVJZU1lXNWtjbVZoYzBCMWJtbHVaWFIwTG01dk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRRGl2YmhSN1A1MTZ4L1MzQnFLeHVwUWUwTE9Ob2xpdXBpQk9lc0NPM1NIYkRybDMrcTlJYmZuZm1FMDRyTnVNY1BzSXhCMTYxVGREcEllc0xDbjdjOGFQSElTS090UGxBZVRaU25iOFFBdTdhUmpacTMrUGJyUDV1VzNUY2ZDR1B0S1R5dEhPZ2UvT2xKYm8wNzhkVmhYUTE0ZDFFRHdYSlcxclJYdVV0NEM4UUlEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRHQkFDRFZmcDg2SE9icVkrZThCVW9XUTkrVk1ReDFBU0RvaEJqd09zZzJXeWtVcVJYRitkTGZjVUg5ZFdSNjNDdFpJS0ZEYlN0Tm9tUG5RejduYksrb255Z3dCc3BWRWJuSHVVaWhacTNaVWRtdW1RcUN3NFV2cy8xVXZxM29yT28vV0pWaFR5dkxnRlZLMlFhclE0LzY3T1pmSGQ3UitQT0JYaG9waFNNdjFaT288L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT4NCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj5zdXBwb3J0QG9uZWxvZ2luLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOFQyMjowMjozN1oiIFJlY2lwaWVudD0ie3JlY2lwaWVudH0iLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0xMS0xOFQyMTo1MjozN1oiIE5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOFQyMjowMjozN1oiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+e2F1ZGllbmNlfTwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMC0xMS0xOFQyMTo1NzozN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTAtMTEtMTlUMjE6NTc6MzdaIiBTZXNzaW9uSW5kZXg9Il81MzFjMzJkMjgzYmRmZjdlMDRlNDg3YmNkYmM0ZGQ4ZCI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgRnJpZW5kbHlOYW1lPSJ1c2VybmFtZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+ZGVtbzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0iYW5vdGhlcl92YWx1ZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+dmFsdWU8L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/tests/data/responses/response2.xml.base64 b/tests/data/responses/response2.xml.base64 new file mode 100644 index 00000000..b4ef73af --- /dev/null +++ b/tests/data/responses/response2.xml.base64 @@ -0,0 +1,79 @@ +PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHNhbWwy +cDpSZXNwb25zZSB4bWxuczpzYW1sMnA9InVybjpvYXNpczpuYW1lczp0YzpT +QU1MOjIuMDpwcm90b2NvbCIgRGVzdGluYXRpb249Imh0dHBzOi8vd2liYmxl +MTI5OTY5MzIwOC5leGFtcGxlLmNvbS9hY2Nlc3Mvc2FtbCIgSUQ9ImlkMTMw +MzEzOTUyNjA0ODY2MjAwMDE1OTIzNzMwNDciIElzc3VlSW5zdGFudD0iMjAx +MS0wNC0xOFQxNToxMjowNS45OTRaIiBWZXJzaW9uPSIyLjAiPgogIDxzYW1s +MjpJc3N1ZXIgeG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1M +OjIuMDphc3NlcnRpb24iIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNB +TUw6Mi4wOm5hbWVpZC1mb3JtYXQ6ZW50aXR5Ij53aWJibGU8L3NhbWwyOklz +c3Vlcj4KICA8c2FtbDJwOlN0YXR1cyB4bWxuczpzYW1sMnA9InVybjpvYXNp +czpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCI+CiAgICA8c2FtbDJwOlN0 +YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpz +dGF0dXM6U3VjY2VzcyIvPgogIDwvc2FtbDJwOlN0YXR1cz4KICA8c2FtbDI6 +QXNzZXJ0aW9uIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FN +TDoyLjA6YXNzZXJ0aW9uIiBJRD0iaWQxMzAzMTM5NTI2MDU2NjkzMDAwOTI4 +Mzg2OTY2IiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDQtMThUMTU6MTI6MDUuOTk0 +WiIgVmVyc2lvbj0iMi4wIj4KICAgIDxzYW1sMjpJc3N1ZXIgeG1sbnM6c2Ft +bDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIEZv +cm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3Jt +YXQ6ZW50aXR5Ij53aWJibGU8L3NhbWwyOklzc3Vlcj4KICAgIDxkczpTaWdu +YXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1s +ZHNpZyMiPgogICAgICA8ZHM6U2lnbmVkSW5mbyB4bWxuczpkcz0iaHR0cDov +L3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAgICAgICAgPGRzOkNh +bm9uaWNhbGl6YXRpb25NZXRob2QgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMu +b3JnLzIwMDAvMDkveG1sZHNpZyMiIEFsZ29yaXRobT0iaHR0cDovL3d3dy53 +My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICAgICAgPGRzOlNp +Z25hdHVyZU1ldGhvZCB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAw +MC8wOS94bWxkc2lnIyIgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8y +MDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4KICAgICAgICA8ZHM6UmVmZXJl +bmNlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRz +aWcjIiBVUkk9IiNpZDEzMDMxMzk1MjYwNTY2OTMwMDA5MjgzODY5NjYiPgog +ICAgICAgICAgPGRzOlRyYW5zZm9ybXMgeG1sbnM6ZHM9Imh0dHA6Ly93d3cu +dzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogICAgICAgICAgICA8ZHM6VHJh +bnNmb3JtIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3ht +bGRzaWcjIiBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkv +eG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+CiAgICAgICAgICAgIDxk +czpUcmFuc2Zvcm0geG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAv +MDkveG1sZHNpZyMiIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAw +MS8xMC94bWwtZXhjLWMxNG4jIj4KICAgICAgICAgICAgICA8ZWM6SW5jbHVz +aXZlTmFtZXNwYWNlcyB4bWxuczplYz0iaHR0cDovL3d3dy53My5vcmcvMjAw +MS8xMC94bWwtZXhjLWMxNG4jIiBQcmVmaXhMaXN0PSJkcyBzYW1sMiIvPgog +ICAgICAgICAgICA8L2RzOlRyYW5zZm9ybT4KICAgICAgICAgIDwvZHM6VHJh +bnNmb3Jtcz4KICAgICAgICAgIDxkczpEaWdlc3RNZXRob2QgeG1sbnM6ZHM9 +Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiIEFsZ29yaXRo +bT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz4K +ICAgICAgICAgIDxkczpEaWdlc3RWYWx1ZSB4bWxuczpkcz0iaHR0cDovL3d3 +dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+RGlnZXN0IFN0dWZmPC9kczpE +aWdlc3RWYWx1ZT4KICAgICAgICA8L2RzOlJlZmVyZW5jZT4KICAgICAgPC9k +czpTaWduZWRJbmZvPgogICAgICA8ZHM6U2lnbmF0dXJlVmFsdWUgeG1sbnM6 +ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPlNpZ25h +dHVyZSBTdHVmZjwvZHM6U2lnbmF0dXJlVmFsdWU+CiAgICAgIDxkczpLZXlJ +bmZvPgogICAgICAgIDxkczpYNTA5RGF0YT4KICAgICAgICAgIDxkczpYNTA5 +Q2VydGlmaWNhdGU+Q2VydGlmaWNhdGUgU3R1ZmY8L2RzOlg1MDlDZXJ0aWZp +Y2F0ZT4KICAgICAgICA8L2RzOlg1MDlEYXRhPgogICAgICA8L2RzOktleUlu +Zm8+CiAgICA8L2RzOlNpZ25hdHVyZT4KICAgIDxzYW1sMjpTdWJqZWN0IHht +bG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0 +aW9uIj4KICAgICAgPHNhbWwyOk5hbWVJRD53aWJibGVAd2liYmxlLmNvbTwv +c2FtbDI6TmFtZUlEPgogICAgICA8c2FtbDI6U3ViamVjdENvbmZpcm1hdGlv +biBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFy +ZXIiPgogICAgICAgIDxzYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBO +b3RPbk9yQWZ0ZXI9IjIwMTEtMDQtMThUMTU6MTc6MDYuMDY0WiIgUmVjaXBp +ZW50PSJodHRwczovL3dpYmJsZTEyOTk2OTMyMDguZXhhbXBsZS5jb20vYWNj +ZXNzL3NhbWwvIi8+CiAgICAgIDwvc2FtbDI6U3ViamVjdENvbmZpcm1hdGlv +bj4KICAgIDwvc2FtbDI6U3ViamVjdD4KICAgIDxzYW1sMjpDb25kaXRpb25z +IHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNz +ZXJ0aW9uIiBOb3RCZWZvcmU9IjIwMTEtMDQtMThUMTU6MDc6MDYuMDY3WiIg +Tm90T25PckFmdGVyPSIyMDExLTA0LTE4VDE1OjE3OjA2LjA2NFoiPgogICAg +ICA8c2FtbDI6QXVkaWVuY2VSZXN0cmljdGlvbj4KICAgICAgICA8c2FtbDI6 +QXVkaWVuY2U+ZXhhbXBsZS5jb208L3NhbWwyOkF1ZGllbmNlPgogICAgICA8 +L3NhbWwyOkF1ZGllbmNlUmVzdHJpY3Rpb24+CiAgICA8L3NhbWwyOkNvbmRp +dGlvbnM+CiAgICA8c2FtbDI6QXV0aG5TdGF0ZW1lbnQgeG1sbnM6c2FtbDI9 +InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIEF1dGhu +SW5zdGFudD0iMjAxMS0wNC0xOFQxNToxMjowNS45OTRaIj4KICAgICAgPHNh +bWwyOkF1dGhuQ29udGV4dD4KICAgICAgICA8c2FtbDI6QXV0aG5Db250ZXh0 +Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNz +ZXM6UGFzc3dvcmRQcm90ZWN0ZWRUcmFuc3BvcnQ8L3NhbWwyOkF1dGhuQ29u +dGV4dENsYXNzUmVmPgogICAgICA8L3NhbWwyOkF1dGhuQ29udGV4dD4KICAg +IDwvc2FtbDI6QXV0aG5TdGF0ZW1lbnQ+CiAgPC9zYW1sMjpBc3NlcnRpb24+ +Cjwvc2FtbDJwOlJlc3BvbnNlPgo= \ No newline at end of file diff --git a/tests/data/responses/response3.xml.base64 b/tests/data/responses/response3.xml.base64 new file mode 100644 index 00000000..d0e55f7f --- /dev/null +++ b/tests/data/responses/response3.xml.base64 @@ -0,0 +1,66 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzYW1scDpSZXNwb25zZSB4bWxuczpz +YW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBJ +RD0iXzZiODVkMGRkLWJmYTgtNGRlZi04MmMyLTg2MjFlMDQ1MjQ3NyIgVmVy +c2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDUtMDJUMTk6NDM6NTQu +NjkyWiIgRGVzdGluYXRpb249Imh0dHBzOi8vZXhhbXBsZS5jb20vYWNjZXNz +L3NhbWwiIENvbnNlbnQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpj +b25zZW50OnVuc3BlY2lmaWVkIiBJblJlc3BvbnNlVG89Il9mYjg0MThkMC01 +NzFlLTAxMmUtZWVlMC0wMDUwNTY5MjAwZDAiPgogIDxJc3N1ZXIgeG1sbnM9 +InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPmh0dHA6 +Ly9leGFtcGxlLmNvbS9zZXJ2aWNlcy90cnVzdDwvSXNzdWVyPgogIDxzYW1s +cDpTdGF0dXM+CiAgICA8c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9h +c2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+CiAgPC9z +YW1scDpTdGF0dXM+CiAgPEFzc2VydGlvbiB4bWxucz0idXJuOm9hc2lzOm5h +bWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9Il9kYmU2YTM2NS05NTgy +LTQ2MGYtYjRiMS0xZjc5YmY3MGY3NmIiIElzc3VlSW5zdGFudD0iMjAxMS0w +NS0wMlQxOTo0Mzo1NC42NDVaIiBWZXJzaW9uPSIyLjAiPgogICAgPElzc3Vl +cj5odHRwOi8vZXhhbXBsZS5jb20vc2VydmljZXMvdHJ1c3Q8L0lzc3Vlcj4K +ICAgIDxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3Jn +LzIwMDAvMDkveG1sZHNpZyMiPgogICAgICA8ZHM6U2lnbmVkSW5mbz4KICAg +ICAgICA8ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0 +dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPgogICAg +ICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3 +LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4KICAgICAgICA8 +ZHM6UmVmZXJlbmNlIFVSST0iI19kYmU2YTM2NS05NTgyLTQ2MGYtYjRiMS0x +Zjc5YmY3MGY3NmIiPgogICAgICAgICAgPGRzOlRyYW5zZm9ybXM+CiAgICAg +ICAgICAgIDxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3Lncz +Lm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPgog +ICAgICAgICAgICA8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3 +dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICAgICAgICA8 +L2RzOlRyYW5zZm9ybXM+CiAgICAgICAgICA8ZHM6RGlnZXN0TWV0aG9kIEFs +Z29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3No +YTEiLz4KICAgICAgICAgIDxkczpEaWdlc3RWYWx1ZT5EaWdlc3Q8L2RzOkRp +Z2VzdFZhbHVlPgogICAgICAgIDwvZHM6UmVmZXJlbmNlPgogICAgICA8L2Rz +OlNpZ25lZEluZm8+CiAgICAgIDxkczpTaWduYXR1cmVWYWx1ZT5TaWduYXR1 +cmU8L2RzOlNpZ25hdHVyZVZhbHVlPgogICAgICA8S2V5SW5mbyB4bWxucz0i +aHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAgICAgICAg +PGRzOlg1MDlEYXRhPgogICAgICAgICAgPGRzOlg1MDlDZXJ0aWZpY2F0ZT5T +dHVmZjwvZHM6WDUwOUNlcnRpZmljYXRlPgogICAgICAgIDwvZHM6WDUwOURh +dGE+CiAgICAgIDwvS2V5SW5mbz4KICAgIDwvZHM6U2lnbmF0dXJlPgogICAg +PFN1YmplY3Q+CiAgICAgIDxOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFt +ZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPnNv +bWVvbmVAZXhhbXBsZS5jb208L05hbWVJRD4KICAgICAgPFN1YmplY3RDb25m +aXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6 +Y206YmVhcmVyIj4KICAgICAgICA8U3ViamVjdENvbmZpcm1hdGlvbkRhdGEg +SW5SZXNwb25zZVRvPSJfZmI4NDE4ZDAtNTcxZS0wMTJlLWVlZTAtMDA1MDU2 +OTIwMGQwIiBOb3RPbk9yQWZ0ZXI9IjIwMTEtMDUtMDJUMTk6NDg6NTQuNzA3 +WiIgUmVjaXBpZW50PSJodHRwczovL2V4YW1wbGUuY29tL2FjY2Vzcy9zYW1s +Ii8+CiAgICAgIDwvU3ViamVjdENvbmZpcm1hdGlvbj4KICAgIDwvU3ViamVj +dD4KICAgIDxDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMS0wNS0wMlQxOTo0 +Mzo1NC42NDVaIiBOb3RPbk9yQWZ0ZXI9IjIwMTEtMDUtMDJUMjA6NDM6NTQu +NjQ1WiI+CiAgICAgIDxBdWRpZW5jZVJlc3RyaWN0aW9uPgogICAgICAgIDxB +dWRpZW5jZT5jb25zdW1lci5leGFtcGxlLmNvbTwvQXVkaWVuY2U+CiAgICAg +IDwvQXVkaWVuY2VSZXN0cmljdGlvbj4KICAgIDwvQ29uZGl0aW9ucz4KICAg +IDxBdHRyaWJ1dGVTdGF0ZW1lbnQ+CiAgICAgIDxBdHRyaWJ1dGUgTmFtZT0i +aHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0 +eS9jbGFpbXMvZW1haWxhZGRyZXNzIj4KICAgICAgICA8QXR0cmlidXRlVmFs +dWU+c29tZW9uZUBleGFtcGxlLmNvbTwvQXR0cmlidXRlVmFsdWU+CiAgICAg +IDwvQXR0cmlidXRlPgogICAgPC9BdHRyaWJ1dGVTdGF0ZW1lbnQ+CiAgICA8 +QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDExLTA1LTAyVDE5OjQz +OjU0LjI4NVoiIFNlc3Npb25JbmRleD0iX2RiZTZhMzY1LTk1ODItNDYwZi1i +NGIxLTFmNzliZjcwZjc2YiI+CiAgICAgIDxBdXRobkNvbnRleHQ+CiAgICAg +ICAgPEF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpT +QU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkUHJvdGVjdGVkVHJhbnNwb3J0 +PC9BdXRobkNvbnRleHRDbGFzc1JlZj4KICAgICAgPC9BdXRobkNvbnRleHQ+ +CiAgICA8L0F1dGhuU3RhdGVtZW50PgogIDwvQXNzZXJ0aW9uPgo8L3NhbWxw +OlJlc3BvbnNlPgo= diff --git a/tests/data/responses/response4.xml.base64 b/tests/data/responses/response4.xml.base64 new file mode 100644 index 00000000..a185427b --- /dev/null +++ b/tests/data/responses/response4.xml.base64 @@ -0,0 +1,93 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0 +YzpTQU1MOjIuMDphc3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6 +bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIElEPSJHT1NBTUxSMTMwNzE1 +NDEyMjM0MDkiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDExLTA2 +LTA0VDAyOjIyOjAyWiIgRGVzdGluYXRpb249InJlY2lwaWVudCI+PHNhbWw6 +SXNzdWVyPmh0dHBzOi8vYXBwLm9uZWxvZ2luLmNvbS9zYW1sMjwvc2FtbDpJ +c3N1ZXI+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0i +dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+ +PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzPSJodHRw +Oi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRw +Oi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgVmVyc2lv +bj0iMi4wIiBJRD0iZmFrZV9hc3NlcnRpb24iIElzc3VlSW5zdGFudD0iMjAx +MS0wNi0wNFQwMjoyMjowMloiPjxzYW1sOklzc3Vlcj5odHRwczovL2FwcC5v +bmVsb2dpbi5jb20vc2FtbDI8L3NhbWw6SXNzdWVyPjxzYW1sOlN1YmplY3Q+ +PHNhbWw6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6 +MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj5ib2d1c0BvbmVsb2dp +bi5jb208L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24g +TWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVy +Ij48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9 +IjIwMTEtMDYtMDRUMDI6Mjc6MDJaIiBSZWNpcGllbnQ9InJlY2lwaWVudCIv +Pjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0Pjxz +YW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDExLTA2LTA0VDAyOjE3OjAy +WiIgTm90T25PckFmdGVyPSIyMDExLTA2LTA0VDAyOjI3OjAyWiI+PHNhbWw6 +QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5hdWRpZW5jZTwv +c2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3Nh +bWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3Rh +bnQ9IjIwMTEtMDYtMDRUMDI6MjI6MDJaIiBTZXNzaW9uTm90T25PckFmdGVy +PSIyMDExLTA2LTA1VDAyOjIyOjAyWiIgU2Vzc2lvbkluZGV4PSJfMTZmNTcw +ZmJjMDMxNTAwN2EwMzU1ZGZlYTZiM2M0NmMiPjxzYW1sOkF1dGhuQ29udGV4 +dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6 +dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZFByb3RlY3RlZFRyYW5z +cG9ydDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5D +b250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9u +PjxzYW1sOkFzc2VydGlvbiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcv +MjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcv +MjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIFZlcnNpb249IjIuMCIgSUQ9InBm +eDk1MTZiMGYzLTQ1MzYtMTBmNi1jNmZhLTlkZDUyM2UxNDk4YyIgSXNzdWVJ +bnN0YW50PSIyMDExLTA2LTA0VDAyOjIyOjAyWiI+PHNhbWw6SXNzdWVyPmh0 +dHBzOi8vYXBwLm9uZWxvZ2luLmNvbS9zYW1sMjwvc2FtbDpJc3N1ZXI+PGRz +OlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8w +OS94bWxkc2lnIyI+CiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6 +YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAx +LzEwL3htbC1leGMtYzE0biMiLz4KICAgIDxkczpTaWduYXR1cmVNZXRob2Qg +QWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcj +cnNhLXNoYTEiLz4KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDk1MTZiMGYz +LTQ1MzYtMTBmNi1jNmZhLTlkZDUyM2UxNDk4YyI+PGRzOlRyYW5zZm9ybXM+ +PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIw +MDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5z +Zm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1s +LWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9k +IEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2ln +I3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+L0ZFUHRwVlhKTDJHNzJ1UDZMd2Vv +R1ltYnpVPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpT +aWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5rSEhrMFFOekMwTFlvVkVQ +UnE0MGRNT1puckpSdzdlT1dkWWdJY240MXQ2N25xOHJwVjhpSEF2WWQva0RS +MVJKcEc2UjU4K25LRVhtTUFLcFNDY29GeC9tQU1UNjNodzQ5RlhOYWZ4WWhj +MGc1bjFSVGhaQURFNlJZSFZpV2pvRHdxZzVCT3FnNnNLNUxlZk5ZM080dVhn +QXFyMUw4R3pVNXNtMGlMWm9xSUU9PC9kczpTaWduYXR1cmVWYWx1ZT4KPGRz +OktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJ +QnJUQ0NBYUdnQXdJQkFnSUJBVEFEQmdFQU1HY3hDekFKQmdOVkJBWVRBbFZU +TVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SVXdFd1lEVlFRSERBeFRZ +VzUwWVNCTmIyNXBZMkV4RVRBUEJnTlZCQW9NQ0U5dVpVeHZaMmx1TVJrd0Z3 +WURWUVFEREJCaGNIQXViMjVsYkc5bmFXNHVZMjl0TUI0WERURXdNVEF4TVRJ +eE1UVXhNbG9YRFRFMU1UQXhNVEl4TVRVeE1sb3daekVMTUFrR0ExVUVCaE1D +VlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhGVEFUQmdOVkJBY01E +Rk5oYm5SaElFMXZibWxqWVRFUk1BOEdBMVVFQ2d3SVQyNWxURzluYVc0eEdU +QVhCZ05WQkFNTUVHRndjQzV2Ym1Wc2IyZHBiaTVqYjIwd2daOHdEUVlKS29a +SWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFNUG1qZmp5N0wzNW9EcGVCWEJv +UlZDZ2t0UGtMbm85RE9FV0I3TWdZTU1WS3MyQjZ5bVdRTEVXckR1Z01LMWhr +eldGaEliNWZxV0xHYld5MEowdmVHUjkvZ0hPUUcrckQvSTM2eEFYbmtkaVhY +aHpvaUFHL3pReE0wZWRNT1VmNDBuMzE0RkM4bW9FcmNVZzZRYWJ0dHplc081 +OUhGejZzaFB1eGNXYVZBZ3hBZ01CQUFFd0F3WUJBQU1CQUE9PTwvZHM6WDUw +OUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6 +U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIEZvcm1hdD0i +dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1h +aWxBZGRyZXNzIj50ZXN0QG9uZWxvZ2luLmNvbTwvc2FtbDpOYW1lSUQ+PHNh +bWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1l +czp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJt +YXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAxMS0wNi0wNFQwMjoyNzowMloi +IFJlY2lwaWVudD0icmVjaXBpZW50Ii8+PC9zYW1sOlN1YmplY3RDb25maXJt +YXRpb24+PC9zYW1sOlN1YmplY3Q+PHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZv +cmU9IjIwMTEtMDYtMDRUMDI6MTc6MDJaIiBOb3RPbk9yQWZ0ZXI9IjIwMTEt +MDYtMDRUMDI6Mjc6MDJaIj48c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjxz +YW1sOkF1ZGllbmNlPmF1ZGllbmNlPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpB +dWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1 +dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0wNFQwMjoyMjow +MloiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTEtMDYtMDVUMDI6MjI6MDJa +IiBTZXNzaW9uSW5kZXg9Il8xNmY1NzBmYmMwMzE1MDA3YTAzNTVkZmVhNmIz +YzQ2YyI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENs +YXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2Vz +OlBhc3N3b3JkUHJvdGVjdGVkVHJhbnNwb3J0PC9zYW1sOkF1dGhuQ29udGV4 +dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3Rh +dGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4= \ No newline at end of file diff --git a/tests/data/responses/response5.xml.base64 b/tests/data/responses/response5.xml.base64 new file mode 100644 index 00000000..c1b54d97 --- /dev/null +++ b/tests/data/responses/response5.xml.base64 @@ -0,0 +1,102 @@ +PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHNhbWwy +cDpSZXNwb25zZSB4bWxuczpzYW1sMnA9InVybjpvYXNpczpuYW1lczp0YzpT +QU1MOjIuMDpwcm90b2NvbCIKICAgICAgICAgICAgICAgICBEZXN0aW5hdGlv +bj0iaHR0cHM6Ly9leGFtcGxlc2FtbC5mb29ibGUuY29tL2FjY2Vzcy9zYW1s +IiBJRD0iaWQ0MzUwMDE5MDE0NzE3NzIxMDMwODIzNjI0IgogICAgICAgICAg +ICAgICAgIElzc3VlSW5zdGFudD0iMjAxMS0wNi0xNFQxODoyNjowMS41MTVa +IiBWZXJzaW9uPSIyLjAiPgogICAgPHNhbWwyOklzc3VlciB4bWxuczpzYW1s +Mj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIKICAg +ICAgICAgICAgICAgICAgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FN +TDoyLjA6bmFtZWlkLWZvcm1hdDplbnRpdHkiPmV4YW1wbGUKICAgIDwvc2Ft +bDI6SXNzdWVyPgogICAgPHNhbWwycDpTdGF0dXMgeG1sbnM6c2FtbDJwPSJ1 +cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiPgogICAgICAg +IDxzYW1sMnA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRj +OlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+CiAgICA8L3NhbWwycDpTdGF0 +dXM+CiAgICA8c2FtbDI6QXNzZXJ0aW9uIHhtbG5zOnNhbWwyPSJ1cm46b2Fz +aXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0iaWQ0MzUwMDE5 +MDE2NzIzNzcxNTg0NjA0NzQiCiAgICAgICAgICAgICAgICAgICAgIElzc3Vl +SW5zdGFudD0iMjAxMS0wNi0xNFQxODoyNjowMS41MTVaIiBWZXJzaW9uPSIy +LjAiPgogICAgICAgIDxzYW1sMjpJc3N1ZXIgRm9ybWF0PSJ1cm46b2FzaXM6 +bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDplbnRpdHkiCiAgICAg +ICAgICAgICAgICAgICAgICB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVz +OnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+ZXhhbXBsZQogICAgICAgIDwvc2Ft +bDI6SXNzdWVyPgogICAgICAgIDxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0 +dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogICAgICAgICAg +ICA8ZHM6U2lnbmVkSW5mbyB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcv +MjAwMC8wOS94bWxkc2lnIyI+CiAgICAgICAgICAgICAgICA8ZHM6Q2Fub25p +Y2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3Jn +LzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIKICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgIHhtbG5zOmRzPSJodHRwOi8vd3d3Lncz +Lm9yZy8yMDAwLzA5L3htbGRzaWcjIi8+CiAgICAgICAgICAgICAgICA8ZHM6 +U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcv +MjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIgogICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcv +MjAwMC8wOS94bWxkc2lnIyIvPgogICAgICAgICAgICAgICAgPGRzOlJlZmVy +ZW5jZSBVUkk9IiNpZDQzNTAwMTkwMTY3MjM3NzE1ODQ2MDQ3NCIgeG1sbnM6 +ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogICAg +ICAgICAgICAgICAgICAgIDxkczpUcmFuc2Zvcm1zIHhtbG5zOmRzPSJodHRw +Oi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KICAgICAgICAgICAg +ICAgICAgICAgICAgPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93 +d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJl +IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHhtbG5z +OmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIi8+CiAg +ICAgICAgICAgICAgICAgICAgICAgIDxkczpUcmFuc2Zvcm0gQWxnb3JpdGht +PSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiCiAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgeG1sbnM6ZHM9 +Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogICAgICAg +ICAgICAgICAgICAgICAgICAgICAgPGVjOkluY2x1c2l2ZU5hbWVzcGFjZXMg +eG1sbnM6ZWM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1j +MTRuIyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgIFByZWZpeExpc3Q9ImRzIHNhbWwyIi8+CiAgICAgICAg +ICAgICAgICAgICAgICAgIDwvZHM6VHJhbnNmb3JtPgogICAgICAgICAgICAg +ICAgICAgIDwvZHM6VHJhbnNmb3Jtcz4KICAgICAgICAgICAgICAgICAgICA8 +ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcv +MjAwMC8wOS94bWxkc2lnI3NoYTEiCiAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAw +MC8wOS94bWxkc2lnIyIvPgogICAgICAgICAgICAgICAgICAgIDxkczpEaWdl +c3RWYWx1ZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94 +bWxkc2lnIyI+c3R1ZmZpZ0wyeTAybVBXUWxHSU9sbz0KICAgICAgICAgICAg +ICAgICAgICA8L2RzOkRpZ2VzdFZhbHVlPgogICAgICAgICAgICAgICAgPC9k +czpSZWZlcmVuY2U+CiAgICAgICAgICAgIDwvZHM6U2lnbmVkSW5mbz4KICAg +ICAgICAgICAgPGRzOlNpZ25hdHVyZVZhbHVlIHhtbG5zOmRzPSJodHRwOi8v +d3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KICAgICAgICAgICAgICAg +IE85REFPSWVFazFYbFgyWVZkaHFVemd4amJBam9XQjdnam1VQmtWdlRFb1dO +akYrK2QvcnVFWkFicnVHMXhtblR2VHJ4TXN0S08wRmYKICAgICAgICAgICAg +ICAgIDk0VVAyN05yeWM1OExmZlF0dzhEVXQ0WVNtR3k5OFM3aFVqS28yeTcy +VTdPQVp1TnJoV0JYcTZodEc0ZzBJNDk4MTNkSWROS2xkS2QKICAgICAgICAg +ICAgICAgIGJMSitRMTJIbEM1aUtUWG9pQjQ9CiAgICAgICAgICAgIDwvZHM6 +U2lnbmF0dXJlVmFsdWU+CiAgICAgICAgICAgIDxkczpLZXlJbmZvPgogICAg +ICAgICAgICAgICAgPGRzOlg1MDlEYXRhPgogICAgICAgICAgICAgICAgICAg +IDxkczpYNTA5Q2VydGlmaWNhdGU+CiAgICAgICAgICAgICAgICAgICAgICAg +IHN0dWZmLzZwTW9Mc2NaaDJHTUsrVkE9PQogICAgICAgICAgICAgICAgICAg +IDwvZHM6WDUwOUNlcnRpZmljYXRlPgogICAgICAgICAgICAgICAgPC9kczpY +NTA5RGF0YT4KICAgICAgICAgICAgPC9kczpLZXlJbmZvPgogICAgICAgIDwv +ZHM6U2lnbmF0dXJlPgogICAgICAgIDxzYW1sMjpTdWJqZWN0IHhtbG5zOnNh +bWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj4K +ICAgICAgICAgICAgPHNhbWwyOk5hbWVJRD5zb21lb25lLmV4YW1wbGVAZ21h +aWwuY29tPC9zYW1sMjpOYW1lSUQ+CiAgICAgICAgICAgIDxzYW1sMjpTdWJq +ZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNB +TUw6Mi4wOmNtOmJlYXJlciI+CiAgICAgICAgICAgICAgICA8c2FtbDI6U3Vi +amVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDExLTA2LTE0 +VDE4OjMxOjAxLjUxNloiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgUmVjaXBpZW50PSJodHRwczovL2V4YW1wbGVz +YW1sLmZvb2JsZS5jb20vYWNjZXNzL3NhbWwvIi8+CiAgICAgICAgICAgIDwv +c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbj4KICAgICAgICA8L3NhbWwyOlN1 +YmplY3Q+CiAgICAgICAgPHNhbWwyOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIy +MDExLTA2LTE0VDE4OjIxOjAxLjUxNloiIE5vdE9uT3JBZnRlcj0iMjAxMS0w +Ni0xNFQxODozMTowMS41MTZaIgogICAgICAgICAgICAgICAgICAgICAgICAg +IHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNz +ZXJ0aW9uIj4KICAgICAgICAgICAgPHNhbWwyOkF1ZGllbmNlUmVzdHJpY3Rp +b24+CiAgICAgICAgICAgICAgICA8c2FtbDI6QXVkaWVuY2U+Zm9vYmxlLmNv +bTwvc2FtbDI6QXVkaWVuY2U+CiAgICAgICAgICAgIDwvc2FtbDI6QXVkaWVu +Y2VSZXN0cmljdGlvbj4KICAgICAgICA8L3NhbWwyOkNvbmRpdGlvbnM+CiAg +ICAgICAgPHNhbWwyOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAx +MS0wNi0xNFQxODoyNjowMS41MTVaIgogICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6 +Mi4wOmFzc2VydGlvbiI+CiAgICAgICAgICAgIDxzYW1sMjpBdXRobkNvbnRl +eHQ+CiAgICAgICAgICAgICAgICA8c2FtbDI6QXV0aG5Db250ZXh0Q2xhc3NS +ZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFz +c3dvcmRQcm90ZWN0ZWRUcmFuc3BvcnQKICAgICAgICAgICAgICAgIDwvc2Ft +bDI6QXV0aG5Db250ZXh0Q2xhc3NSZWY+CiAgICAgICAgICAgIDwvc2FtbDI6 +QXV0aG5Db250ZXh0PgogICAgICAgIDwvc2FtbDI6QXV0aG5TdGF0ZW1lbnQ+ +CiAgICA8L3NhbWwyOkFzc2VydGlvbj4KPC9zYW1sMnA6UmVzcG9uc2U+ diff --git a/tests/data/responses/response_encrypted_nameid.xml.base64 b/tests/data/responses/response_encrypted_nameid.xml.base64 new file mode 100644 index 00000000..d3bcbb6e --- /dev/null +++ b/tests/data/responses/response_encrypted_nameid.xml.base64 @@ -0,0 +1 @@ +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_ampersands.xml b/tests/data/responses/response_with_ampersands.xml new file mode 100644 index 00000000..90f87d8f --- /dev/null +++ b/tests/data/responses/response_with_ampersands.xml @@ -0,0 +1,139 @@ +PFJlc3BvbnNlIElzc3VlSW5zdGFudD0iMjAxMS0xMi0wOVQxMToyMzozNC4w +NzlaIiBJRD0iWW9FUmhTRkFYYXhWUjFqNWpYMlp1TDhCSkJRIiBWZXJzaW9u +PSIyLjAiIHhtbG5zPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJv +dG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIu +MDphc3NlcnRpb24iIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAw +LzA5L3htbGRzaWcjIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIw +MDEvWE1MU2NoZW1hLWluc3RhbmNlIj4KICA8c2FtbDpJc3N1ZXI+UEYtREVN +Tzwvc2FtbDpJc3N1ZXI+CiAgPFN0YXR1cz4KICAgIDxTdGF0dXNDb2RlIFZh +bHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nl +c3MiLz4KICA8L1N0YXR1cz4KICA8c2FtbDpBc3NlcnRpb24gVmVyc2lvbj0i +Mi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMTItMDlUMTE6MjM6MzQuMDgxWiIg +SUQ9IlpMZGloS1VSV0JSd3RmOWNnXzBCLmxuZUxMUCI+CiAgICA8c2FtbDpJ +c3N1ZXI+UEYtREVNTzwvc2FtbDpJc3N1ZXI+CiAgICA8ZHM6U2lnbmF0dXJl +PgogICAgICA8ZHM6U2lnbmVkSW5mbz4KICAgICAgICA8ZHM6Q2Fub25pY2Fs +aXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIw +MDEvMTAveG1sLWV4Yy1jMTRuIyIvPgogICAgICAgIDxkczpTaWduYXR1cmVN +ZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3ht +bGRzaWcjcnNhLXNoYTEiLz4KICAgICAgICA8ZHM6UmVmZXJlbmNlIFVSST0i +I1pMZGloS1VSV0JSd3RmOWNnXzBCLmxuZUxMUCI+CiAgICAgICAgICA8ZHM6 +VHJhbnNmb3Jtcz4KICAgICAgICAgICAgPGRzOlRyYW5zZm9ybSBBbGdvcml0 +aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9w +ZWQtc2lnbmF0dXJlIi8+CiAgICAgICAgICAgIDxkczpUcmFuc2Zvcm0gQWxn +b3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0 +biMiLz4KICAgICAgICAgIDwvZHM6VHJhbnNmb3Jtcz4KICAgICAgICAgIDxk +czpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8y +MDAwLzA5L3htbGRzaWcjc2hhMSIvPgogICAgICAgICAgPGRzOkRpZ2VzdFZh +bHVlPlBNWmVkc3ZISS80STNiWjNJNnFtTXA0bCtaRT08L2RzOkRpZ2VzdFZh +bHVlPgogICAgICAgIDwvZHM6UmVmZXJlbmNlPgogICAgICA8L2RzOlNpZ25l +ZEluZm8+CiAgICAgIDxkczpTaWduYXR1cmVWYWx1ZT5ncUYyM2NYSGErUW54 +eElnamx3eU5vVnhoZ3p4WGlGMXRiUnUyOXY4OWFsYm1SMnkvREo1UU5XeW5t +OWlPeEZUb3AwU0lzTDlBRFI1Cm41MGNkWlF6UkJaZldORFVWaFdHYmc2QTdq +UVZuKzdXd216Z2ZZdFMzMjNwTjV5N1A4YkhEYTROMjlMQzNaT29odkdOTDFw +d1RQUEUKemd5OTc4cFJNYXBRd0twNmFNMD08L2RzOlNpZ25hdHVyZVZhbHVl +PgogICAgICA8ZHM6S2V5SW5mbz4KICAgICAgICA8ZHM6WDUwOURhdGE+CiAg +ICAgICAgICA8ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUI3ekNDQVZpZ0F3SUJB +Z0lHQVJicWVwZU9NQTBHQ1NxR1NJYjNEUUVCQlFVQU1Ec3hDekFKQmdOVkJB +WVRBbFZUTVJnd0ZnWUQKVlFRS0V3OVJkV2xqYXlCVGRHRnlkQ0JCY0hBeEVq +QVFCZ05WQkFNVENXeHZZMkZzYUc5emREQWVGdzB3TnpFeU1UY3lNelF3TVRG +YQpGdzB4TWpFeU1UVXlNelF3TVRGYU1Ec3hDekFKQmdOVkJBWVRBbFZUTVJn +d0ZnWURWUVFLRXc5UmRXbGpheUJUZEdGeWRDQkJjSEF4CkVqQVFCZ05WQkFN +VENXeHZZMkZzYUc5emREQ0JuekFOQmdrcWhraUc5dzBCQVFFRkFBT0JqUUF3 +Z1lrQ2dZRUFqZWhoMTZqWGE1ekgKTFhpaDhyTkxLTzJ1dlFVb0NXSVd3VFhX +ZkZGd1krVXNQUjBDVm44WUlubkppS1hwOFlDZVhDa2hIRXFnU1dVYjJLS0J0 +RituSm9NdwpETTNSbkoxeUNYMFoxa1ptTitubXVsR2Z0a1FJNmVrUDV2QnJY +OGgvNXp1WThkTmp2TGJjenhCNzVpaDcwMHpoWVRoZUNzYVQ3STFZCmFBVDNB +R01DQXdFQUFUQU5CZ2txaGtpRzl3MEJBUVVGQUFPQmdRQnJTOWtNVFR3QTVU +M0VKbWpCMkd1RElnRUl1NFE3d3ZyWm9KeG0KWERNUm5IaTBMaHBLbGVUWGIv +VlJ6dytRY0dIcVJzSjErUHRKRUhLdzRnNU10ZmtFVzNyYnJtWHVzdmNobXpa +SUIxak9sRXRFdVp6TwpxZ2U0Z3JueFpBRzc2MDM2YmZQZmo2aVpLOFlOTWJz +TEF5dzVnaGhvOStkUUI4ZWtoR0ROSGxmSjlBPT08L2RzOlg1MDlDZXJ0aWZp +Y2F0ZT4KICAgICAgICA8L2RzOlg1MDlEYXRhPgogICAgICAgIDxkczpLZXlW +YWx1ZT4KICAgICAgICAgIDxkczpSU0FLZXlWYWx1ZT4KICAgICAgICAgICAg +PGRzOk1vZHVsdXM+amVoaDE2alhhNXpITFhpaDhyTkxLTzJ1dlFVb0NXSVd3 +VFhXZkZGd1krVXNQUjBDVm44WUlubkppS1hwOFlDZVhDa2hIRXFnU1dVYgoy +S0tCdEYrbkpvTXdETTNSbkoxeUNYMFoxa1ptTitubXVsR2Z0a1FJNmVrUDV2 +QnJYOGgvNXp1WThkTmp2TGJjenhCNzVpaDcwMHpoCllUaGVDc2FUN0kxWWFB +VDNBR009PC9kczpNb2R1bHVzPgogICAgICAgICAgICA8ZHM6RXhwb25lbnQ+ +QVFBQjwvZHM6RXhwb25lbnQ+CiAgICAgICAgICA8L2RzOlJTQUtleVZhbHVl +PgogICAgICAgIDwvZHM6S2V5VmFsdWU+CiAgICAgIDwvZHM6S2V5SW5mbz4K +ICAgIDwvZHM6U2lnbmF0dXJlPgogICAgPHNhbWw6U3ViamVjdD4KICAgICAg +PHNhbWw6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6 +MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj5qb2huPC9zYW1sOk5h +bWVJRD4KICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9 +InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPgogICAg +ICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRl +cj0iMjAxMS0xMi0wOVQxMToyODozNC4wODFaIiBSZWNpcGllbnQ9Imh0dHA6 +Ly9mYWMuc21hY2thaG8uc3Q6MzAwMC9hdXRoL3NhbWwvY2FsbGJhY2siLz4K +ICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+CiAgICA8L3NhbWw6 +U3ViamVjdD4KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90T25PckFmdGVyPSIy +MDExLTEyLTA5VDExOjI4OjM0LjA4MVoiIE5vdEJlZm9yZT0iMjAxMS0xMi0w +OVQxMToxODozNC4wODFaIj4KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmlj +dGlvbj4KICAgICAgICA8c2FtbDpBdWRpZW5jZT5mYWM8L3NhbWw6QXVkaWVu +Y2U+CiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPgogICAgPC9z +YW1sOkNvbmRpdGlvbnM+CiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRo +bkluc3RhbnQ9IjIwMTEtMTItMDlUMTE6MjM6MzQuMDgxWiIgU2Vzc2lvbklu +ZGV4PSJaTGRpaEtVUldCUnd0ZjljZ18wQi5sbmVMTFAiPgogICAgICA8c2Ft +bDpBdXRobkNvbnRleHQ+CiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xh +c3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6 +dW5zcGVjaWZpZWQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+CiAgICAg +IDwvc2FtbDpBdXRobkNvbnRleHQ+CiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1l +bnQ+CiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQgeG1sbnM6eHM9Imh0 +dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIj4KICAgICAgPHNhbWw6 +QXR0cmlidXRlIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1M +OjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiIE5hbWU9IkNvbXBhbnlUeXBl +Ij4KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6 +c3RyaW5nIj5Va0xpbWl0ZWRDb21wYW55PC9zYW1sOkF0dHJpYnV0ZVZhbHVl +PgogICAgICA8L3NhbWw6QXR0cmlidXRlPgogICAgICA8c2FtbDpBdHRyaWJ1 +dGUgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0 +dHJuYW1lLWZvcm1hdDpiYXNpYyIgTmFtZT0iZm9yZW5hbWUiPgogICAgICAg +IDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPkpv +aG4mYW1wOzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4KICAgICAgPC9zYW1sOkF0 +dHJpYnV0ZT4KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWVGb3JtYXQ9InVy +bjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFz +aWMiIE5hbWU9InN1cm5hbWUiPgogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZh +bHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPkRvZTwvc2FtbDpBdHRyaWJ1dGVW +YWx1ZT4KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4KICAgICAgPHNhbWw6QXR0 +cmlidXRlIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIu +MDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiIE5hbWU9ImFkZHJlc3NfMSI+CiAg +ICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmlu +ZyI+YWRkcmVzc18xPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPgogICAgICA8L3Nh +bWw6QXR0cmlidXRlPgogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZUZvcm1h +dD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1h +dDpiYXNpYyIgTmFtZT0iYWRkcmVzc18yIj4KICAgICAgICA8c2FtbDpBdHRy +aWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZGRyZXNzXzI8L3Nh +bWw6QXR0cmlidXRlVmFsdWU+CiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+CiAg +ICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFt +ZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIiBOYW1lPSJj +b21wYW55TmFtZSI+CiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNp +OnR5cGU9InhzOnN0cmluZyI+QiAmYW1wOyBHPC9zYW1sOkF0dHJpYnV0ZVZh +bHVlPgogICAgICA8L3NhbWw6QXR0cmlidXRlPgogICAgICA8c2FtbDpBdHRy +aWJ1dGUgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4w +OmF0dHJuYW1lLWZvcm1hdDpiYXNpYyIgTmFtZT0icG9zdGNvZGUiPgogICAg +ICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmci +PkVIMSAxQUI8L3NhbWw6QXR0cmlidXRlVmFsdWU+CiAgICAgIDwvc2FtbDpB +dHRyaWJ1dGU+CiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lRm9ybWF0PSJ1 +cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJh +c2ljIiBOYW1lPSJjaXR5Ij4KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1 +ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5jaXR5PC9zYW1sOkF0dHJpYnV0ZVZh +bHVlPgogICAgICA8L3NhbWw6QXR0cmlidXRlPgogICAgICA8c2FtbDpBdHRy +aWJ1dGUgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4w +OmF0dHJuYW1lLWZvcm1hdDpiYXNpYyIgTmFtZT0iY291bnRyeSI+CiAgICAg +ICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+ +VW5pdGVkIEtpbmdkb208L3NhbWw6QXR0cmlidXRlVmFsdWU+CiAgICAgIDwv +c2FtbDpBdHRyaWJ1dGU+CiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lRm9y +bWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9y +bWF0OmJhc2ljIiBOYW1lPSJ1c2VyRW1haWxJRCI+CiAgICAgICAgPHNhbWw6 +QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+am9obkBleGFt +cGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4KICAgICAgPC9zYW1sOkF0 +dHJpYnV0ZT4KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWVGb3JtYXQ9InVy +bjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFz +aWMiIE5hbWU9ImNvdW50eSI+CiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFs +dWUgeHNpOnR5cGU9InhzOnN0cmluZyI+Y291bnR5PC9zYW1sOkF0dHJpYnV0 +ZVZhbHVlPgogICAgICA8L3NhbWw6QXR0cmlidXRlPgogICAgICA8c2FtbDpB +dHRyaWJ1dGUgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6 +Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyIgTmFtZT0ibW9iaWxlTnVtYmVy +Ij4KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6 +c3RyaW5nIj4xMjM0NTY3PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPgogICAgICA8 +L3NhbWw6QXR0cmlidXRlPgogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVu +dD4KICA8L3NhbWw6QXNzZXJ0aW9uPgo8L1Jlc3BvbnNlPgo= diff --git a/tests/data/responses/response_with_ampersands.xml.base64 b/tests/data/responses/response_with_ampersands.xml.base64 new file mode 100644 index 00000000..a185427b --- /dev/null +++ b/tests/data/responses/response_with_ampersands.xml.base64 @@ -0,0 +1,93 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0 +YzpTQU1MOjIuMDphc3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6 +bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIElEPSJHT1NBTUxSMTMwNzE1 +NDEyMjM0MDkiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDExLTA2 +LTA0VDAyOjIyOjAyWiIgRGVzdGluYXRpb249InJlY2lwaWVudCI+PHNhbWw6 +SXNzdWVyPmh0dHBzOi8vYXBwLm9uZWxvZ2luLmNvbS9zYW1sMjwvc2FtbDpJ +c3N1ZXI+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0i +dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+ +PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzPSJodHRw +Oi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRw +Oi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgVmVyc2lv +bj0iMi4wIiBJRD0iZmFrZV9hc3NlcnRpb24iIElzc3VlSW5zdGFudD0iMjAx +MS0wNi0wNFQwMjoyMjowMloiPjxzYW1sOklzc3Vlcj5odHRwczovL2FwcC5v +bmVsb2dpbi5jb20vc2FtbDI8L3NhbWw6SXNzdWVyPjxzYW1sOlN1YmplY3Q+ +PHNhbWw6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6 +MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj5ib2d1c0BvbmVsb2dp +bi5jb208L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24g +TWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVy +Ij48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9 +IjIwMTEtMDYtMDRUMDI6Mjc6MDJaIiBSZWNpcGllbnQ9InJlY2lwaWVudCIv +Pjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0Pjxz +YW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDExLTA2LTA0VDAyOjE3OjAy +WiIgTm90T25PckFmdGVyPSIyMDExLTA2LTA0VDAyOjI3OjAyWiI+PHNhbWw6 +QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5hdWRpZW5jZTwv +c2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3Nh +bWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3Rh +bnQ9IjIwMTEtMDYtMDRUMDI6MjI6MDJaIiBTZXNzaW9uTm90T25PckFmdGVy +PSIyMDExLTA2LTA1VDAyOjIyOjAyWiIgU2Vzc2lvbkluZGV4PSJfMTZmNTcw +ZmJjMDMxNTAwN2EwMzU1ZGZlYTZiM2M0NmMiPjxzYW1sOkF1dGhuQ29udGV4 +dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6 +dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZFByb3RlY3RlZFRyYW5z +cG9ydDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5D +b250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9u +PjxzYW1sOkFzc2VydGlvbiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcv +MjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcv +MjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIFZlcnNpb249IjIuMCIgSUQ9InBm +eDk1MTZiMGYzLTQ1MzYtMTBmNi1jNmZhLTlkZDUyM2UxNDk4YyIgSXNzdWVJ +bnN0YW50PSIyMDExLTA2LTA0VDAyOjIyOjAyWiI+PHNhbWw6SXNzdWVyPmh0 +dHBzOi8vYXBwLm9uZWxvZ2luLmNvbS9zYW1sMjwvc2FtbDpJc3N1ZXI+PGRz +OlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8w +OS94bWxkc2lnIyI+CiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6 +YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAx +LzEwL3htbC1leGMtYzE0biMiLz4KICAgIDxkczpTaWduYXR1cmVNZXRob2Qg +QWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcj +cnNhLXNoYTEiLz4KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDk1MTZiMGYz +LTQ1MzYtMTBmNi1jNmZhLTlkZDUyM2UxNDk4YyI+PGRzOlRyYW5zZm9ybXM+ +PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIw +MDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5z +Zm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1s +LWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9k +IEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2ln +I3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+L0ZFUHRwVlhKTDJHNzJ1UDZMd2Vv +R1ltYnpVPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpT +aWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5rSEhrMFFOekMwTFlvVkVQ +UnE0MGRNT1puckpSdzdlT1dkWWdJY240MXQ2N25xOHJwVjhpSEF2WWQva0RS +MVJKcEc2UjU4K25LRVhtTUFLcFNDY29GeC9tQU1UNjNodzQ5RlhOYWZ4WWhj +MGc1bjFSVGhaQURFNlJZSFZpV2pvRHdxZzVCT3FnNnNLNUxlZk5ZM080dVhn +QXFyMUw4R3pVNXNtMGlMWm9xSUU9PC9kczpTaWduYXR1cmVWYWx1ZT4KPGRz +OktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJ +QnJUQ0NBYUdnQXdJQkFnSUJBVEFEQmdFQU1HY3hDekFKQmdOVkJBWVRBbFZU +TVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SVXdFd1lEVlFRSERBeFRZ +VzUwWVNCTmIyNXBZMkV4RVRBUEJnTlZCQW9NQ0U5dVpVeHZaMmx1TVJrd0Z3 +WURWUVFEREJCaGNIQXViMjVsYkc5bmFXNHVZMjl0TUI0WERURXdNVEF4TVRJ +eE1UVXhNbG9YRFRFMU1UQXhNVEl4TVRVeE1sb3daekVMTUFrR0ExVUVCaE1D +VlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhGVEFUQmdOVkJBY01E +Rk5oYm5SaElFMXZibWxqWVRFUk1BOEdBMVVFQ2d3SVQyNWxURzluYVc0eEdU +QVhCZ05WQkFNTUVHRndjQzV2Ym1Wc2IyZHBiaTVqYjIwd2daOHdEUVlKS29a +SWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFNUG1qZmp5N0wzNW9EcGVCWEJv +UlZDZ2t0UGtMbm85RE9FV0I3TWdZTU1WS3MyQjZ5bVdRTEVXckR1Z01LMWhr +eldGaEliNWZxV0xHYld5MEowdmVHUjkvZ0hPUUcrckQvSTM2eEFYbmtkaVhY +aHpvaUFHL3pReE0wZWRNT1VmNDBuMzE0RkM4bW9FcmNVZzZRYWJ0dHplc081 +OUhGejZzaFB1eGNXYVZBZ3hBZ01CQUFFd0F3WUJBQU1CQUE9PTwvZHM6WDUw +OUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6 +U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIEZvcm1hdD0i +dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1h +aWxBZGRyZXNzIj50ZXN0QG9uZWxvZ2luLmNvbTwvc2FtbDpOYW1lSUQ+PHNh +bWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1l +czp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJt +YXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAxMS0wNi0wNFQwMjoyNzowMloi +IFJlY2lwaWVudD0icmVjaXBpZW50Ii8+PC9zYW1sOlN1YmplY3RDb25maXJt +YXRpb24+PC9zYW1sOlN1YmplY3Q+PHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZv +cmU9IjIwMTEtMDYtMDRUMDI6MTc6MDJaIiBOb3RPbk9yQWZ0ZXI9IjIwMTEt +MDYtMDRUMDI6Mjc6MDJaIj48c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjxz +YW1sOkF1ZGllbmNlPmF1ZGllbmNlPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpB +dWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1 +dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0wNFQwMjoyMjow +MloiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTEtMDYtMDVUMDI6MjI6MDJa +IiBTZXNzaW9uSW5kZXg9Il8xNmY1NzBmYmMwMzE1MDA3YTAzNTVkZmVhNmIz +YzQ2YyI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENs +YXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2Vz +OlBhc3N3b3JkUHJvdGVjdGVkVHJhbnNwb3J0PC9zYW1sOkF1dGhuQ29udGV4 +dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3Rh +dGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4= \ 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/response_without_assertion_reference_uri.xml.base64 b/tests/data/responses/response_without_assertion_reference_uri.xml.base64 new file mode 100644 index 00000000..8c4dab5d --- /dev/null +++ b/tests/data/responses/response_without_assertion_reference_uri.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzYW1scDpSZXNwb25zZSB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBJRD0icGZ4ZDU5NDM0N2QtNDk1Zi1iOGQxLTBlZTItNDFjZmRhMTRkZDM1IiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNS0wMS0wMlQyMjo0ODo0OFoiIERlc3RpbmF0aW9uPSJodHRwOi8vbG9jYWxob3N0OjkwMDEvdjEvdXNlcnMvYXV0aG9yaXplL3NhbWwiIENvbnNlbnQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjb25zZW50OnVuc3BlY2lmaWVkIiBJblJlc3BvbnNlVG89Il9lZDkxNWE0MC03NGZiLTAxMzItNWIxNi00OGUwZWIxNGExYzciPgogIDxJc3N1ZXIgeG1sbnM9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPmh0dHA6Ly9leGFtcGxlLmNvbTwvSXNzdWVyPgogIDxzYW1scDpTdGF0dXM+CiAgICA8c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+CiAgPC9zYW1scDpTdGF0dXM+CgogIDxBc3NlcnRpb24geG1sbnM9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfNzAwYWMzMjAtNzRmZi0wMTMyLTViMTQtNDhlMGViMTRhMWM3IiBJc3N1ZUluc3RhbnQ9IjIwMTUtMDEtMDJUMjI6NDg6NDhaIiBWZXJzaW9uPSIyLjAiPgogICAgPElzc3Vlcj5odHRwOi8vZXhhbXBsZS5jb208L0lzc3Vlcj4KICAgIDxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPgogICAgPGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4KICAgIDxkczpSZWZlcmVuY2UgVVJJPSIiPgogICAgICA8ZHM6VHJhbnNmb3Jtcz4KICAgICAgICA8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz4KICAgICAgICA8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICAgIDwvZHM6VHJhbnNmb3Jtcz4KICAgICAgPGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+CiAgICAgIDxkczpEaWdlc3RWYWx1ZT5qQ2dlWENQREZsd2pUZ3FnUHAwbVUyVHF3OWc9PC9kczpEaWdlc3RWYWx1ZT4KICAgIDwvZHM6UmVmZXJlbmNlPgogIDwvZHM6U2lnbmVkSW5mbz4KICA8ZHM6U2lnbmF0dXJlVmFsdWU+bG9SN21DRmlNSURIUHBLeVgzRUd2dzJYeTZycEtFZWZVMDhYS1lWRXJ6MXB3a1BUUFFlYU5iK2RGMHZLai9rNQoyUmJ2Z3ZFUFN2ZGI3RDJOMTY5QjJMTGVmbXpaWTBDY0RKcThkK3lNbnZSNER3YitSUFl6bWJoS29XQ1ZyY3VPCnNvbEUxQTg3WFZjenNpd2JYRWllM2p4RHdDSk5vWi9GRFJRZy80RHRQVmc9PC9kczpTaWduYXR1cmVWYWx1ZT4KPGRzOktleUluZm8+CiAgPGRzOlg1MDlEYXRhPgogICAgPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDVnpDQ0FjQUNDUURJVkhhTlNCWUw2VEFOQmdrcWhraUc5dzBCQVFzRkFEQndNUXN3Q1FZRFZRUUdFd0pHVWpFT01Bd0dBMVVFQ0F3RlVHRnlhWE14RGpBTUJnTlZCQWNNQlZCaGNtbHpNUll3RkFZRFZRUUtEQTFPYjNaaGNHOXpkQ0JVUlZOVU1Ta3dKd1lKS29aSWh2Y05BUWtCRmhwbWJHOXlaVzUwTG5CcFoyOTFkRUJ1YjNaaGNHOXpkQzVtY2pBZUZ3MHhOREF5TVRNeE16VXpOREJhRncweE5UQXlNVE14TXpVek5EQmFNSEF4Q3pBSkJnTlZCQVlUQWtaU01RNHdEQVlEVlFRSURBVlFZWEpwY3pFT01Bd0dBMVVFQnd3RlVHRnlhWE14RmpBVUJnTlZCQW9NRFU1dmRtRndiM04wSUZSRlUxUXhLVEFuQmdrcWhraUc5dzBCQ1FFV0dtWnNiM0psYm5RdWNHbG5iM1YwUUc1dmRtRndiM04wTG1aeU1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRQ2hMRkhuM0xuTjRKUS83V0NkWXVweGtVZ2NOT1FuUEYreWxsKy9EUHB1eDlucGZZMDU5UElVYXRCOFg3a0NuNWk4dFJ3SXkvaWtISlI2TXI4K01QdmM2Vk9aRHhQTmRadk1vLzhsaHhyYk4zSmRydzN3aFptVS9LUFI5RjNCZEZkdStTTHpyTWwxVERVWmxQdFk5WHpVRlhjcU44SVhjeThUSnpDQmVOZXkzUUlEQVFBQk1BMEdDU3FHU0liM0RRRUJDd1VBQTRHQkFDdEo4ZmVHemUxTkhCNVZ3MThqTVVQdkhvN0gzR3dtajZaREFYUWxhaUFYTXVOQnhOWFZXVndpZmw2VituVzN3OVFhN0Zlby9uWi9PNFRVT0gxbnorYWRrbGNDRDRRcFphRUlibUFicmlQV0pLZ2I0TFdHaHFRcnV3WVI3SXRUUjFNTlg5Z0xiUDB6MHp2REVRbm50L1ZVV0ZFQkxTSnE0WjROcmU4TEZtUzI8L2RzOlg1MDlDZXJ0aWZpY2F0ZT4KICA8L2RzOlg1MDlEYXRhPgo8L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PFN1YmplY3Q+CiAgICAgIDxOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPnNhbWxAdXNlci5jb208L05hbWVJRD4KICAgICAgPFN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj4KICAgICAgICA8U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgSW5SZXNwb25zZVRvPSJfZWQ5MTVhNDAtNzRmYi0wMTMyLTViMTYtNDhlMGViMTRhMWM3IiBOb3RPbk9yQWZ0ZXI9IjIwMzgtMDEtMDJUMjI6NTE6NDhaIiBSZWNpcGllbnQ9Imh0dHA6Ly9sb2NhbGhvc3Q6OTAwMS92MS91c2Vycy9hdXRob3JpemUvc2FtbCIvPgogICAgICA8L1N1YmplY3RDb25maXJtYXRpb24+CiAgICA8L1N1YmplY3Q+CiAgICA8Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTUtMDEtMDJUMjI6NDg6NDNaIiBOb3RPbk9yQWZ0ZXI9IjIwMzgtMDEtMDJUMjM6NDg6NDhaIj4KICAgICAgPEF1ZGllbmNlUmVzdHJpY3Rpb24+CiAgICAgICAgPEF1ZGllbmNlPmh0dHA6Ly9sb2NhbGhvc3Q6OTAwMS88L0F1ZGllbmNlPgogICAgICAgIDxBdWRpZW5jZT5mbGF0X3dvcmxkPC9BdWRpZW5jZT4KICAgICAgPC9BdWRpZW5jZVJlc3RyaWN0aW9uPgogICAgPC9Db25kaXRpb25zPgogICAgPEF0dHJpYnV0ZVN0YXRlbWVudD4KICAgICAgPEF0dHJpYnV0ZSBOYW1lPSJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiPgogICAgICAgIDxBdHRyaWJ1dGVWYWx1ZT5zYW1sQHVzZXIuY29tPC9BdHRyaWJ1dGVWYWx1ZT4KICAgICAgPC9BdHRyaWJ1dGU+CiAgICA8L0F0dHJpYnV0ZVN0YXRlbWVudD4KICAgIDxBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTUtMDEtMDJUMjI6NDg6NDhaIiBTZXNzaW9uSW5kZXg9Il83MDBhYzMyMC03NGZmLTAxMzItNWIxNC00OGUwZWIxNGExYzciPgogICAgICA8QXV0aG5Db250ZXh0PgogICAgICAgIDxBdXRobkNvbnRleHRDbGFzc1JlZj51cm46ZmVkZXJhdGlvbjphdXRoZW50aWNhdGlvbjp3aW5kb3dzPC9BdXRobkNvbnRleHRDbGFzc1JlZj4KICAgICAgPC9BdXRobkNvbnRleHQ+CiAgICA8L0F1dGhuU3RhdGVtZW50PgogIDwvQXNzZXJ0aW9uPgo8L3NhbWxwOlJlc3BvbnNlPgo= diff --git a/tests/data/responses/response_without_reference_uri.xml.base64 b/tests/data/responses/response_without_reference_uri.xml.base64 new file mode 100644 index 00000000..d830db01 --- /dev/null +++ b/tests/data/responses/response_without_reference_uri.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzYW1scDpSZXNwb25zZSB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBJRD0icGZ4ZDU5NDM0N2QtNDk1Zi1iOGQxLTBlZTItNDFjZmRhMTRkZDM1IiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNS0wMS0wMlQyMjo0ODo0OFoiIERlc3RpbmF0aW9uPSJodHRwOi8vbG9jYWxob3N0OjkwMDEvdjEvdXNlcnMvYXV0aG9yaXplL3NhbWwiIENvbnNlbnQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjb25zZW50OnVuc3BlY2lmaWVkIiBJblJlc3BvbnNlVG89Il9lZDkxNWE0MC03NGZiLTAxMzItNWIxNi00OGUwZWIxNGExYzciPgogIDxJc3N1ZXIgeG1sbnM9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPmh0dHA6Ly9leGFtcGxlLmNvbTwvSXNzdWVyPgogIDxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPgogICAgPGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4KICAgIDxkczpSZWZlcmVuY2UgVVJJPSIiPgogICAgICA8ZHM6VHJhbnNmb3Jtcz4KICAgICAgICA8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz4KICAgICAgICA8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICAgIDwvZHM6VHJhbnNmb3Jtcz4KICAgICAgPGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+CiAgICAgIDxkczpEaWdlc3RWYWx1ZT5qQ2dlWENQREZsd2pUZ3FnUHAwbVUyVHF3OWc9PC9kczpEaWdlc3RWYWx1ZT4KICAgIDwvZHM6UmVmZXJlbmNlPgogIDwvZHM6U2lnbmVkSW5mbz4KICA8ZHM6U2lnbmF0dXJlVmFsdWU+bG9SN21DRmlNSURIUHBLeVgzRUd2dzJYeTZycEtFZWZVMDhYS1lWRXJ6MXB3a1BUUFFlYU5iK2RGMHZLai9rNQoyUmJ2Z3ZFUFN2ZGI3RDJOMTY5QjJMTGVmbXpaWTBDY0RKcThkK3lNbnZSNER3YitSUFl6bWJoS29XQ1ZyY3VPCnNvbEUxQTg3WFZjenNpd2JYRWllM2p4RHdDSk5vWi9GRFJRZy80RHRQVmc9PC9kczpTaWduYXR1cmVWYWx1ZT4KPGRzOktleUluZm8+CiAgPGRzOlg1MDlEYXRhPgogICAgPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDVnpDQ0FjQUNDUURJVkhhTlNCWUw2VEFOQmdrcWhraUc5dzBCQVFzRkFEQndNUXN3Q1FZRFZRUUdFd0pHVWpFT01Bd0dBMVVFQ0F3RlVHRnlhWE14RGpBTUJnTlZCQWNNQlZCaGNtbHpNUll3RkFZRFZRUUtEQTFPYjNaaGNHOXpkQ0JVUlZOVU1Ta3dKd1lKS29aSWh2Y05BUWtCRmhwbWJHOXlaVzUwTG5CcFoyOTFkRUJ1YjNaaGNHOXpkQzVtY2pBZUZ3MHhOREF5TVRNeE16VXpOREJhRncweE5UQXlNVE14TXpVek5EQmFNSEF4Q3pBSkJnTlZCQVlUQWtaU01RNHdEQVlEVlFRSURBVlFZWEpwY3pFT01Bd0dBMVVFQnd3RlVHRnlhWE14RmpBVUJnTlZCQW9NRFU1dmRtRndiM04wSUZSRlUxUXhLVEFuQmdrcWhraUc5dzBCQ1FFV0dtWnNiM0psYm5RdWNHbG5iM1YwUUc1dmRtRndiM04wTG1aeU1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRQ2hMRkhuM0xuTjRKUS83V0NkWXVweGtVZ2NOT1FuUEYreWxsKy9EUHB1eDlucGZZMDU5UElVYXRCOFg3a0NuNWk4dFJ3SXkvaWtISlI2TXI4K01QdmM2Vk9aRHhQTmRadk1vLzhsaHhyYk4zSmRydzN3aFptVS9LUFI5RjNCZEZkdStTTHpyTWwxVERVWmxQdFk5WHpVRlhjcU44SVhjeThUSnpDQmVOZXkzUUlEQVFBQk1BMEdDU3FHU0liM0RRRUJDd1VBQTRHQkFDdEo4ZmVHemUxTkhCNVZ3MThqTVVQdkhvN0gzR3dtajZaREFYUWxhaUFYTXVOQnhOWFZXVndpZmw2VituVzN3OVFhN0Zlby9uWi9PNFRVT0gxbnorYWRrbGNDRDRRcFphRUlibUFicmlQV0pLZ2I0TFdHaHFRcnV3WVI3SXRUUjFNTlg5Z0xiUDB6MHp2REVRbm50L1ZVV0ZFQkxTSnE0WjROcmU4TEZtUzI8L2RzOlg1MDlDZXJ0aWZpY2F0ZT4KICA8L2RzOlg1MDlEYXRhPgo8L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz4KICAgIDxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz4KICA8L3NhbWxwOlN0YXR1cz4KCiAgPEFzc2VydGlvbiB4bWxucz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9Il83MDBhYzMyMC03NGZmLTAxMzItNWIxNC00OGUwZWIxNGExYzciIElzc3VlSW5zdGFudD0iMjAxNS0wMS0wMlQyMjo0ODo0OFoiIFZlcnNpb249IjIuMCI+CiAgICA8SXNzdWVyPmh0dHA6Ly9leGFtcGxlLmNvbTwvSXNzdWVyPgogICAgPFN1YmplY3Q+CiAgICAgIDxOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPnNhbWxAdXNlci5jb208L05hbWVJRD4KICAgICAgPFN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj4KICAgICAgICA8U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgSW5SZXNwb25zZVRvPSJfZWQ5MTVhNDAtNzRmYi0wMTMyLTViMTYtNDhlMGViMTRhMWM3IiBOb3RPbk9yQWZ0ZXI9IjIwMzgtMDEtMDJUMjI6NTE6NDhaIiBSZWNpcGllbnQ9Imh0dHA6Ly9sb2NhbGhvc3Q6OTAwMS92MS91c2Vycy9hdXRob3JpemUvc2FtbCIvPgogICAgICA8L1N1YmplY3RDb25maXJtYXRpb24+CiAgICA8L1N1YmplY3Q+CiAgICA8Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTUtMDEtMDJUMjI6NDg6NDNaIiBOb3RPbk9yQWZ0ZXI9IjIwMzgtMDEtMDJUMjM6NDg6NDhaIj4KICAgICAgPEF1ZGllbmNlUmVzdHJpY3Rpb24+CiAgICAgICAgPEF1ZGllbmNlPmh0dHA6Ly9sb2NhbGhvc3Q6OTAwMS88L0F1ZGllbmNlPgogICAgICAgIDxBdWRpZW5jZT5mbGF0X3dvcmxkPC9BdWRpZW5jZT4KICAgICAgPC9BdWRpZW5jZVJlc3RyaWN0aW9uPgogICAgPC9Db25kaXRpb25zPgogICAgPEF0dHJpYnV0ZVN0YXRlbWVudD4KICAgICAgPEF0dHJpYnV0ZSBOYW1lPSJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiPgogICAgICAgIDxBdHRyaWJ1dGVWYWx1ZT5zYW1sQHVzZXIuY29tPC9BdHRyaWJ1dGVWYWx1ZT4KICAgICAgPC9BdHRyaWJ1dGU+CiAgICA8L0F0dHJpYnV0ZVN0YXRlbWVudD4KICAgIDxBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTUtMDEtMDJUMjI6NDg6NDhaIiBTZXNzaW9uSW5kZXg9Il83MDBhYzMyMC03NGZmLTAxMzItNWIxNC00OGUwZWIxNGExYzciPgogICAgICA8QXV0aG5Db250ZXh0PgogICAgICAgIDxBdXRobkNvbnRleHRDbGFzc1JlZj51cm46ZmVkZXJhdGlvbjphdXRoZW50aWNhdGlvbjp3aW5kb3dzPC9BdXRobkNvbnRleHRDbGFzc1JlZj4KICAgICAgPC9BdXRobkNvbnRleHQ+CiAgICA8L0F1dGhuU3RhdGVtZW50PgogIDwvQXNzZXJ0aW9uPgo8L3NhbWxwOlJlc3BvbnNlPgo= diff --git a/tests/data/responses/signed_assertion_response.xml.base64 b/tests/data/responses/signed_assertion_response.xml.base64 new file mode 100644 index 00000000..ff563b49 --- /dev/null +++ b/tests/data/responses/signed_assertion_response.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfMmUwZjNlOGE3YzUxZGUyNjcxNjczNDE0YWE3ZDVhNjkyNDdmNmQ2NjI1IiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wMy0zMVQwMDozNzoxNloiIERlc3RpbmF0aW9uPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNjEyYmJmOWIxNjQ1Mjk0YWEwYjQ2MzdiMWJjNWYzOWRlOGI3OWNlYiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpBc3NlcnRpb24geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiBJRD0icGZ4ZDNkZDIzYjEtYWZiYy1jNWQxLTVmOTgtMjFjNmJhYzVkYjRjIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wMy0zMVQwMDozNzoxNloiPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeGQzZGQyM2IxLWFmYmMtYzVkMS01Zjk4LTIxYzZiYWM1ZGI0YyI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+d2dCMnYvaE9hU29PQzd6S0tFLzhpdmhsQnRVPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5QbUwrRTllcWxXVDdobExxeHM2QVlWZTJTR2VmT0lTbWZQTjBMUlhaa2VmSks4d3hpM01wZUVaU25XTC91SEI5MmRYcSsrWStBd1dUQnFySHZqL3g5ckMzRE5FUmZXcFB1QUZUejcrNXBCWDVEb2lpeDNnZmlHUXQ0WDM0YThwRWY2WG90bGUzTmpYcEVWRXVQWmRlRzB1WG5kWVZKUmJpSUV6WnNMMnB4QXM9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWw6U3ViamVjdD48c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocCIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDp0cmFuc2llbnQiPl8zYWY2MmYxZDAzNTEzYmRkNjFkZDViZjA0ZDNkZWI3YWE2MTc0ODBlMjI8L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjI5OTMtMTAtMDJUMDU6NTc6MTZaIiBSZWNpcGllbnQ9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl82MTJiYmY5YjE2NDUyOTRhYTBiNDYzN2IxYmM1ZjM5ZGU4Yjc5Y2ViIi8+PC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+PC9zYW1sOlN1YmplY3Q+PHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTQtMDMtMzFUMDA6MzY6NDZaIiBOb3RPbk9yQWZ0ZXI9IjI5OTMtMTAtMDJUMDU6NTc6MTZaIj48c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjxzYW1sOkF1ZGllbmNlPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMy0zMVQwMDozNzoxNloiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjI5OTMtMDMtMzFUMDg6Mzc6MTZaIiBTZXNzaW9uSW5kZXg9Il84NWU3Y2ZlMTZkNmU3ZTYwMGJkOThiYmMyYjQzNzFlMWM2OTU4OGE0ZGEiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0QGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj53YWEyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+ diff --git a/tests/data/responses/signed_assertion_response2.xml.base64 b/tests/data/responses/signed_assertion_response2.xml.base64 new file mode 100644 index 00000000..762f86af --- /dev/null +++ b/tests/data/responses/signed_assertion_response2.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfZTNmNzIwOThmYzU5MDcwMDE5YTc2YWQzMDU4NDcyMTNiMThjYmQ5YWRiIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wOS0yM1QxMjo0NToyMFoiIERlc3RpbmF0aW9uPSJodHRwOi8vcHl0b29sa2l0LmNvbTo4MDAwLz9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fMDEzMzVlZTE1YjIyNzZlNTUwZTMzM2E1MDNiMzM3NDQyMzY2YzA2YyI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vaWRwLmV4YW1wbGUuY29tL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9Il8yY2JlNjk2YzUxMTE0YzFiY2RiZGE4YjcxNWU1NmZhOTM1ZGMzMjZiOWYiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTA5LTIzVDEyOjQ1OjIwWiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vaWRwLmV4YW1wbGUuY29tL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4KICA8ZHM6UmVmZXJlbmNlIFVSST0iI18yY2JlNjk2YzUxMTE0YzFiY2RiZGE4YjcxNWU1NmZhOTM1ZGMzMjZiOWYiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPk82SkJPdGxIczJNL2hDR205V2kzdHd2Y3lhZz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+TGJNdXNEMHFUczh2YUVyTjZ4bGtacnBjTEpmSmsvRkV0czdtSGU0YXVNK2hkeXQwaHdCL3NnUVBnUFBic3hCQ2NPNU5mM3ZsUHNRYTl4bGQ2ODhDcmljWTV6VnFmeFBMLytKSnU5bWdJL05NWmFaQ1B5YUorem0vTXQrUFJuVW56QWhTem1IQlIzK0JtU0VuVk9IenFtL0VVY0lOQy9HUFN0OTFWZnQ5eHRJPTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNiRENDQWRXZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRMEZBREJUTVFzd0NRWURWUVFHRXdKMWN6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVWTUJNR0ExVUVDZ3dNVDI1bGJHOW5hVzRnU1c1ak1SZ3dGZ1lEVlFRRERBOXBaSEF1WlhoaGJYQnNaUzVqYjIwd0hoY05NVFF3T1RJek1USXlOREE0V2hjTk5ESXdNakE0TVRJeU5EQTRXakJUTVFzd0NRWURWUVFHRXdKMWN6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVWTUJNR0ExVUVDZ3dNVDI1bGJHOW5hVzRnU1c1ak1SZ3dGZ1lEVlFRRERBOXBaSEF1WlhoaGJYQnNaUzVqYjIwd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFPV0ErWUhVN2N2UE9yQk9meENzY3NZVEpCK2tIM01hQTlCRnJTSEZTK0tjUjZjdzdvUFNrdElKeFVndkRwUWJ0Zk5jT2tFL3R1T1BCRG9lY2g3QVhmdkg2ZDdCdzd4dFc4UFBKMm1CNUhuL0hHVzJyb1loeG1maDN0UjVTZHdONmk0RVJWRjhlTGt2d0NIc05ReUsyUmVmMERBSnZwQk5aTUhDcFMyNDkxNi9BZ01CQUFHalVEQk9NQjBHQTFVZERnUVdCQlE3Ny9xVmVpaWdmaFlESVRwbENOdEpLWlRNOERBZkJnTlZIU01FR0RBV2dCUTc3L3FWZWlpZ2ZoWURJVHBsQ050SktaVE04REFNQmdOVkhSTUVCVEFEQVFIL01BMEdDU3FHU0liM0RRRUJEUVVBQTRHQkFKTzJqLzF1TzgwRTVDMlBNNkZrOW16ZXJyYmt4bDdBWi9tdmxiT24rc05aRStWWjFBbnRZdUc4ZWtiSnBKdEcxWWZSZmM3RUE5bUV0cXZ2NGRodjd6Qnk0bks0OU9SK0twSUJqSXRXQjVrWXZycU1MS0JhMzJzTWJncXFVcWVGMUVOWEtqcHZMU3VQZGZHSlpBM2ROYS8rRHliOEdHcVdlNzA3ekx5YzVGOG08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHA6Ly9weXRvb2xraXQuY29tOjgwMDAvbWV0YWRhdGEvIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnVuc3BlY2lmaWVkIj4yNWRkZDdkMzRhN2Q3OWRiNjkxNjc2MjVjZGE1NmEzMjBhZGYyODc2PC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDI0LTAzLTI2VDE4OjA1OjIwWiIgUmVjaXBpZW50PSJodHRwOi8vcHl0b29sa2l0LmNvbTo4MDAwLz9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fMDEzMzVlZTE1YjIyNzZlNTUwZTMzM2E1MDNiMzM3NDQyMzY2YzA2YyIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTA5LTIzVDEyOjQ0OjUwWiIgTm90T25PckFmdGVyPSIyMDI0LTAzLTI2VDE4OjA1OjIwWiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwOi8vcHl0b29sa2l0LmNvbTo4MDAwL21ldGFkYXRhLzwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDktMjNUMTI6NDU6MjBaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTA5LTIzVDIwOjQ1OjIwWiIgU2Vzc2lvbkluZGV4PSJfYWVkNjA5MTJmODkzOWYwNzIzOWFiYjc3ZDhiMDI5ODI3YTMwY2NiMDNiIj48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNtYXJ0aW48L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbkB5YWNvLmVzPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5TaXh0bzM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ic24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPk1hcnRpbjI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0icGhvbmUiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiLz48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iZWR1UGVyc29uQWZmaWxpYXRpb24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+YWRtaW48L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4= \ No newline at end of file diff --git a/tests/data/responses/signed_encrypted_assertion.xml.base64 b/tests/data/responses/signed_encrypted_assertion.xml.base64 new file mode 100644 index 00000000..3a363fa1 --- /dev/null +++ b/tests/data/responses/signed_encrypted_assertion.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiDQogICAgICAgICAgICAgICAgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiINCiAgICAgICAgICAgICAgICBJRD0iXzljMmFmOTQzNGUyOTRiMGNiMmQ1YzQ0ZWQ0OWIzYTcyNWYwNGExZDI2ZCINCiAgICAgICAgICAgICAgICBWZXJzaW9uPSIyLjAiDQogICAgICAgICAgICAgICAgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTMwVDIwOjM5OjE5WiINCiAgICAgICAgICAgICAgICBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiDQogICAgICAgICAgICAgICAgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl9mMjkyMzRlNzJhY2E2ZWQ1Y2MwMjExMmQyMWEzODIxOTFjYzc5ZjZmIg0KICAgICAgICAgICAgICAgID4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPg0KICAgIDxzYW1scDpTdGF0dXM+DQogICAgICAgIDxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiIC8+DQogICAgPC9zYW1scDpTdGF0dXM+DQogICAgPHNhbWw6RW5jcnlwdGVkQXNzZXJ0aW9uPg0KICAgICAgICA8eGVuYzpFbmNyeXB0ZWREYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyINCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB4bWxuczpkc2lnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgPg0KICAgICAgICAgICAgPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI2FlczEyOC1jYmMiIC8+DQogICAgICAgICAgICA8ZHNpZzpLZXlJbmZvIHhtbG5zOmRzaWc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICAgICAgICAgICAgICAgIDx4ZW5jOkVuY3J5cHRlZEtleT4NCiAgICAgICAgICAgICAgICAgICAgPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS0xXzUiIC8+DQogICAgICAgICAgICAgICAgICAgIDx4ZW5jOkNpcGhlckRhdGE+DQogICAgICAgICAgICAgICAgICAgICAgICA8eGVuYzpDaXBoZXJWYWx1ZT5VYmdZN1VKU0V3ZnZlTWhNVkx3QTFpZGt4OFRad25ONVB6ZWNZdFo5bjdVbGd0aEtudGYydk1XYUNDdlpnbHEvOS9PRWgvQmFxVXFacFNRTkh2bU5TWDRsbFU5VzhDWTJMRGZFZW5ZQVZSelE1R0J3Q1c0b3JsYUw5TWpzQkJxeUQ0anBMVXkrdWxJaVpzK0ludlNUNmF6MThoSFBoZHdSUDFZek92Tzl0ajA9PC94ZW5jOkNpcGhlclZhbHVlPg0KICAgICAgICAgICAgICAgICAgICA8L3hlbmM6Q2lwaGVyRGF0YT4NCiAgICAgICAgICAgICAgICA8L3hlbmM6RW5jcnlwdGVkS2V5Pg0KICAgICAgICAgICAgPC9kc2lnOktleUluZm8+DQogICAgICAgICAgICA8eGVuYzpDaXBoZXJEYXRhPg0KICAgICAgICAgICAgICAgIDx4ZW5jOkNpcGhlclZhbHVlPjJoeWZhOUJTWXNUN090eUwvQTY0TlZsZDc0L2tNUlZxeVc2R0VyMmxHN2w1emhRNEptNEVURHN0VFZtb01ubVZ2RDk0SXpwcW8vaXNQaTNReHgwRkt3YmEwYys2OVZHY0xjODFEWnFHZTA1N2dmcG12dmRubEl6dDBwYUVjQUhNSjZrZmYrWmRHN0RXTlFwcHMrRTBkSW81d0wzNUVKWU5MVUZIR091S2JCQXhXbjN4UWlGQk85QnNZWFB5ZkNYejFJVXdKZ09aK2pZVlJZSjhMdlRsZDVNNG5FMFRORm5rRjZQMHRrS2U4SkYwMDRBbDd5bTRVdFY1S3o1YmhUNlI4bGo1NTBtcGxPWVlHd1RPbG1ZdStnZFJZL3NFdXJYSXc0bk5DQ1lHZE1qcEJZakg4MVJvUGJVYXBRc20vcXhqSEdyZHB1aUFmM2RlaFlNUGtNYzhoSHhVL2l6RXVsdEdZUzJIcE5aeWcvWkYvUm10eWN0TXFHczFJU21DVmYxcGN2QU5YcFVidmE3dlpkQ2NWajVMVFF4SktqQWN1bmlzZGxqYjdFdHpzeDZiS3lWcSt1czUvVzRoN014anBMaFZZNmhtcWRpdk0wdTVmd3NHRkJaZFI3T0FEQzIzUUhuTlViaDhhNlcvTmVwM0dwMzcxZHdQOHhDVVZ4dllsR21qZkppSVI5M0x2elA1RC9nWGIxbm5KemZXOHorNGt4ekNDZTNuOEFvd0YrYVd4cXlQdXplVXlEME92a3dKcXdCcGd2bGhaSzVBVUo2d1oxVllNY1FLYmRQeWpMbytuWFVxSEZyQ2cxejdaMEtKTFluNTJXZnJKSnNpUno2akxtSzJLa2RSUDkwR255UEdyalMzaFBjYkhHKzNvai9RTDZyRmtmelZZM1hOTlVNWWZnVzAzdW1PQXVHb2tDK3hmd2t4KzBwZkFxcUlQaHdDZEVrdU1sZWJLSHZkV1FWNktjK1UxWlFweWswVDdZaElIdGVjMmdUYVM1cGg2VlcyV2lLeWRuVGJEWmE0cHdKdVVyMTZQUW16Z1FvRFVaaGVVd3F2M2h4cHkrZ0VaUXdsMThyc1lidDhIVGh2UW84WXFkUUdORWprZzRVSE5uNUw4L1N3d3dESitTRitBRktOZWNzdENSNGRMd3N6U0R1QzcrbnFmRm9XamtILzVtOTd3RTNrUmtJY3BGQ01qVFlDZkNmVDAvSGFLMnFYZzRTSkZxVHpvcGFESFFUWTRJelR2N3FRaHQ4UEd5cDFxTTNPWnpmQ0x2SStIckc2SFhTY2dyNVd2SkFMZzFHeno3VDg0WGJ3OWFPSFZRdEZIL2MzQ0NiTE9nUVNsTmJ1bWZGYmcxRGJyWTdDVFgyOG5iMUc5bEtnZXZWQy9Idmo0NEJtREFiQ3VlSFArd1BsVms0MDdJV2lSSTJIZi9QSmh5K2ppU1E0bmp0d2pyV00yNEZiMUI4ZHJpVXpMeWhrOXpsYnVUdHJWRmtsRjI4cUQrUUVWY29Tb1gycWxoa1Q5K0hsUnlxYThucXV3RDZMdW5HdktreTduMVY2RW5FMUdKdU5SK2h2Ri9yN04yMTRnS1QweFVVN3Z5VzVlSWFqTEdLNjRKbFZjaE5YaVp6U244ckpwRitmVGZMTGdmVU5Kc0xBTTh2aXh5cjZGeGRBU3k1Rm9lbkZFNy9WVDdSb0l1QmZFSDNRUnoxYyt1V2ZhNWpjTnROQmZsZ1hNdVVzYVY2dXR1L2RPTnVjWk1KR0MweTN6NCs2aEFZbWdMVXJabEM2NlBlTm9XMmZTM2VJNzVvNlBScm9YaUJ5R0hCZUtsMTF4UVp4YTE4ZHRsV1AyK21ta2FRblAyMUZkR084Y1V6c2JwanZWSGlLMFZlamdHcHB1bzIvVHNOZ0tHdHBMb0hoeGhmOVR3eE05R1hZeUI5VVZSUm9pazdWUSt6MmFHN1cyK3RscWl1TzFxR2kvSmZnamN2UzUxRmthN3pJUFZQYXltL2pCNlNvT1R1LzlJNTQyd1RTZ3h3ZG8vTWlCaHd1SklFUzd2WlZDQ0s2M2tjalhBNzdDSlNDTkRDRDhvdEdPVzlMb0JNTjBzeGsyS1lDY1UvVGQ4MisvajZHQ09mb3JXUS9UbGFQZ3FWa3BDNzNIU3FObDZydGJKWUtkVlpJRjkxU2QrcGhzTTB0bTZwSldSV2pyc1U4YnA5cHZMNnRXbkNpVVhVVFdNVi9hdmpJdjd6clhIS1BsTXRwbkxIbjc4TjlTMG0wYlJjaE1nWmlEVXFMaW5yUlU1N1phUUZhUERCS0cza0NaNFlla0Nhc2RQU0dwd1B2Y21Ib1dvNVJzYnVaeWFXN0tCU2grQ0lCeDdCMlFmdWdLbXduTEVBSlNobUZYZ2JCcTBCVlJmdm8zM1AyRXo2VmVvamhPdzJaOTZ1R0IyLzJNRmkxMEZiNllzQ21zU2Q1eFNaVVhFcllVMG1nUkpXTlNCRFV2aTZ6ZVV3LzFtZWF4Y2RacmFuY0x1M1pDa3VoSmh1NlpVblFiOUtweVNRdTBZNldzUUp4RkdkU1ZJRzRuU3lsZUtrT0tPQXVacjMzempoNEVqNi8vNTRpMlJuOHJVSVFHRWxvc3BSUFRwdVN2aXNVc21uYncrNlhXRU1sNU5VbmU0ZlJFWkhBc0h1NXhoQmtYcXUzc2wvZFMzclBXdmxxM1A5ZktvUjJPVG81VlJyK2Z6cUxiUTdJdi9vK2pTMnlwWXBhZGVtcGh1L2swUFpTTGdyVS9ub0Q4eVNXWURhbG90MEdJOW5DSHkycVJQNWN2SE1xSDZUUWhQME1ZelVtL0NLaXZ3aTUwUXlBbUFoeFVrK2hIRk5iU0lBV3ZHdGhuZU9jT2QyVFAvR08xQXhlSFhGS2VIWlZzazhtMHdoYXZvZFgvcjRuWGNkVkVSenhOVnBKeXhJQ2lrQ1NzZjlYWDBSdzR3djlFRzB0WVI1dkQ1Qkw4TEJVaG9VWE8ydFRuMmFhNEJJeWRHWEcrRU5MTEhLL3VybUtYSDdMUFlHRmZPaXhKeGR2TWJjWHgwM0ZvcjBMc2xKcVZrNVFRZHFoWUlFUTVRdUsvT1phZXdISVFlT1lndGhhMkphTFlRMHltM2dTRklZdHE2eXJWMDRjMnZVNVdyTHJTOUZkeXBwaVhBUkNCT0FTZFU3cXFUdloxZ2xyd0VLMUU5ZDQ1dWFKT3N2cFdhWXc1aWdWYzlENnNLRENLeFFFdVdBeThVbithTkJMbHdxOSt3V2RvMDQ3alcvU2NsYm03K1NRVzE4K1BrbjM4YmpMNldWRUg0ZkduQVc3aFBLVDVzOXJUL3FhRW9yd05Zd3FYdm8rZjdySFBQUnNWd2kxNDNmUHI5U3FMTXdmb3pwb0hDcjdrTjcvcGRPdmVybXNPTy9xWENhUHF1anRyWEdTOHFNdzFSb200RXEydG9zc2NjSUdaUW02bGVlakh4bUtqK2ZSYjMvMG9USkhtVmJMWDZKL1RJMml6Y2F2SWRFNkc2RnpiaTE2QmRkU3pzVjlyMlhiMFc5TWdsY0ladks3NFNGMG1aSVpPVmtBUDVZYktCaU84N1BDQ0E3aXdtazdzc3JaNG9lUFBhbVVyQ1JmcTAwT0NPbHZWYURFRHZJWVdXekl0R01xYStadWUzdFYrRFNxUDR1bXdSWTJMSms2MzZJUkJjNnZSekI0cS9kdmx0VzV2YTlEbnAxWVAyQzJuTGwvSTgyTDduRDBoVU1WYjhVcVNjdWpISGRiSm5hSzUwUDdwamNQYlVOUXVXWmQ0MVh2dElXdHBTQkJxMTI1TkpjblZwaFI0VTNFb3N5S3hJMTAyMXFXMmU4bFVGS3gzQjNXSmVvYitsRG90bEZmcEJsaVRneWhxbitFV2diWHlQUEFDWm1TM1J0c2VMemxSTzZzcjUvQUQ2YmlBcVkwVldQbW04T2RaTm1kVHVQZ3JVLzFscy9IR2R1eTc2b0VQUTJpTmU0TzErOEN6a0d4Ykowbzcrd1AyejFPTVZUWFZtY3N1ejVoKzNzMU1veld0OXI4QVFCRnRuR3Z6Q2VVVGdCNmNONTM0akJCQnFlS3Zna1pTK2Y5NFQzaDVSSWRodldGa2JRZ29zcktkdGQvUEkvcjBXUTJHRHhlb2U0aDFvV09ldzBHbndHdzVyRzNxYnlremdsTFNZQmFHNmh3c1pTM2ZHU2JoQURvZXZMUUlNTlF4Tlk5MEk2ZWxMYllYTVowTmpzU05QWnlWa3JLU1RNUHphNGpJWS9lRnVhNWU2ZXZtUXh4TUxsdzFFbUp4SElaMSt4OVJLZy9Lb1cwOUdadDhyaytleFdMUGpBdXpzL043bnBObUd6Z2FNR1ZxSEI2TUFXY1hXODNjMG9mVXp0UlRBWStBR0hMWUE5ZzZHVmdCb3E0UHBXenZ4Z2dOQmRUUnlIdVk3V3ZyeEVSY0h1aWZHdjJ5QjVlaHZQR1U2bWgxNTNGcWJLeFozbC9KSlExUHUvd1o4ODJZL0tIc0dUQ0RDNGwwRXZ4ZVFCcVQ3TzFTZkRVUXVSUUFKNUIzOURDRjlvVHBqT3hQSkdSSW5RZE1oUU5tT1hNZGlNZTZCZDBZUlJPbUJaeG1xWWxwUGo2Ukhta3BiU3N0bGVxMDg5RDRmNTBKTU05S09SREV4R3B3SVFIMlp0YjFRdElwY2ZoNThsWWRGazd3N09HMzRZdGVlSFJUTDJxK0dhMVhIS2xQSnNlQzVDN2FoSzV4Y1BKNVJ4SW9kK2IrSnREejdJZk1zZUM4b2RTcmpqUnUrQUVNUmdLT3ZOVThHVHUzR09YRUpIekVkbklIRXM3R0V0enZyblFOVFNycmg2cWlFNUY1U09mT1JBbWdHVzJpV3dGRUUrL1hjSUYrUDJ1dVhtSWtwbmxqa0czbDJIU3ZiTW5EQ0VTdWo2NkRSNGVSMVE0bUxsUDdWMU80cEo0QTVXZmNOK1NmdGNEN2M3RERVRHNkOHBwZWF2Z1ZETndJbU5WazEzNWNIMzNmSmxUMUJsSWZWN3VxNnNEYTZJSEk2SHZwaTJPMTNsMDNmMHFaTEFFUkExN2NZaTRDTFBPQnlDSTFUekw0OFAwa0xBMDV1c0xFYW56Uzc0TW9QRFl1Qzh2NUZLK1VqZ1VGU3UrTTZkRDVXdm5pU3g3UkswVFJQNGNWSFlWblVyNFROcHVsSHFKZkhKYVJxcElvQzRsMmxHbS9RSnd2eEFkMkVPNlhNMk5KT1UyeFJPSGVuT2llWFJzSTBDUGJEZ3RQbWt6d3cyQWh6UHlUNWJjb3c5Ui96OVhhUDE2OHlDRllBNlRaME5aWmNiVG5zT2ZSYTdjOHhXS0Zic3RVU1BHQnE1V05MWnMxbTVBQlN6SCs2QUtEOFRpVjRBRG1hU2s5ZC8ySnFMbEZ3Ylk5NkxseU1iVVQ3OVY0OFg5UEpVWGFYZVJJVlE5VVZLdzJvT3d4VFgxZnV6M0Y0ckxkSUh2NnAxbWw2Rk9udUVteml5UElOeHJEVFErZlpjckZwRmxSK1llWVM3enYxVVVtZVRHZ0xhUk5zQ1h4ejViUnluelJtWFZKcE9EWm9VWnpiVC8vUWUwNlNYSncwOUhldXhvY3ZRZzAweTZLR2toNU45TXNJKzNKUE1WNnRNOCtYc3V3Q3Z5a0xYUDBoNGV1ZVFmU3F6bWxRMnZweVlHRmNEU3NBeHRaMmVsb3F6ak1tamczUmdGeW5tejBmcGZ1aitKU3JVRnY1c3JiTGFjNFJ5aXhqSStiM0krKzNOdGo0U1RLK2t6blRxbUlubmVaMzVlNmxyTlNubzJmZjZRaWpvUE40RVRIQkk5aThsMGNKNWJRM2dDd2J6Zjh0SEwvZXVTQ1hJa0xEZVVHaTVtbGxzRlBadlVLRkRkNjlTQU5oZXZCMXlLVld2azN5dlNhZmlYc3RsZ1FjVHVJT3o0Z3BZeVdOVWE1Q2dlMDhRZHZWVVZtUm83bWFHNjhNN3RsQ2VLSGU4ZDFyRlVBUytiTWpFWVJDSitSUTFiaElRUWhyUExYSG5kZnRJYTlmeGN6d1ZkdFg3ZEpPaWJyb2ZlZk84M2ZiMzlDSzFERXprcWk3eFR1VVlTRm9pRWtIclVKNFJnc1hFL0ZsWXVMS0xoQ29qMDJLd2Z3UE9ha0lzeWRQcndSVkp0MW02U1hFbE16ell1K25kZ2xyNHIyejJIOVRocXBTbWtuZjZOaS9YdDBvWFM1bjRrYmZ3SGl5ME5YbnplMk1vcStUQjJTa0QvM1JHbm5kNy9DRndiWGxjaTE3RE94T2NraTNaYjdrb3NZVjhBbythSnIvY3lOaW9YK01qUFdWRnd2eXJpS3Z3Sm5tK2Nra3AzU2lpUHMydnVUV1hDUG5Wa215SXpXWElKdWJ1U0g3OXdPQW1lMnI5T3l3eFVzUkVxTE04THZWVlQ1OGxvbDQ3ejhORlJVUU1wQzRJdEYzVk11OU04N2lVZ0xqMGVhaFZIdG14TXNNcDNwcmNMeWN2dnJnb3Nid1NJby9HOXNSdFcxQkc5Q25JYlVFUDAyRDlJM29MTkFEeFY4STd3RGVXcnduOGhRRG5mdExVOEpZRjVVLzBWRnlJSE1nK3lTdjZZS0VKUXk3M1lYMmtZSmlFREtnRDI2UnVnVUJRdk5VT1AzMDJjK1M4aWpNeTNMdDk0RHQvK3dOcFJiL1JlNzQvY1BnUTRzdGFmUTFIVmIzZFJ0UStURCtsRXJSaUdaWlQ1Q2RGb1ZMUmpXSXF4cG5oRVc0a0xIL2VZL3FPOTFKeERxdit1TDYyVnNud1R2YlRqNzUyd2N3Vm9kTzh6d0pTcG52YVVveXhuVjhwbTFvczVxbFA4cFg0MzhST211REpGdzVlRGJjdGlxbkt6RVNFZWc2NGEveUFPZ1dyeVZGVkY4RG5XZlgyV0xoUVhYTVRYM09ZaWdINE9iRDZtQnB2UVloU1J3SHZmMWJWcmptVFpySTR6MW81UnVPYnlsSDl2ZmhYSTdXTDRueHFqZm5lV3BUd0FsbWtXTWk3c1BpWFUrRG0yUTdSanc2NEV2MzdyZnVpWmRhbC9Ednh4MFg1eSs1Z1VyZEM1MjNMSE9OZWN3Q2xXRzBsZzI5NTNBK3NiOGFsWXcyYnl4RDI4enk1UXRzeTN1bkFCeWl4V1dBSGlPM2JpL3NCS01pVytnT0RWMWJ4NWFHSDIzaVR5dkp6S3dvbEc0M0hOVFFST0c4b2t0WVZWN2RBaGpLRXFBY1pTT2dFMWZmSDRhY0xGQWdVeDJvK1ZYQ0NqTUpVNVN2RWlCTUZlaGJQdXJSR1lPamFmOWRxZkRvNjdJMzVsazIvRTlJamJ3UEwxZEx1SXJ6c1hKcmgrcmNoNEUwSHBRZkNGbVRHQzhvd3dTNmMzYktGNkJLdStaZE9nQThDeWFueVVBajdmb0lhcU40NjdoS2c3dDMxK2JYZUJjNDRMNjNxd1dQbWNQbFEvUnh1bTRPcDdiaWFtdS8vd1pUMkNHNEViUGpHNllnPT08L3hlbmM6Q2lwaGVyVmFsdWU+DQogICAgICAgICAgICA8L3hlbmM6Q2lwaGVyRGF0YT4NCiAgICAgICAgPC94ZW5jOkVuY3J5cHRlZERhdGE+DQogICAgPC9zYW1sOkVuY3J5cHRlZEFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ diff --git a/tests/data/responses/signed_encrypted_assertion2.xml.base64 b/tests/data/responses/signed_encrypted_assertion2.xml.base64 new file mode 100644 index 00000000..17d213fe --- /dev/null +++ b/tests/data/responses/signed_encrypted_assertion2.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfNGIyYmQwYTk0ZmIzMDVjOGU2NGZkZmFkMTZhNzM1YjdiOGVhODIzOWJiIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wOS0yNFQwMDo0MTowOFoiIERlc3RpbmF0aW9uPSJodHRwOi8vcHl0b29sa2l0LmNvbTo4MDAwLz9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNTU4MGI4YTU2NzFjYTRiNjM2NTI3NTY0YjhhZWU3ZTdhYTZkZDM4NSI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vaWRwLmV4YW1wbGUuY29tL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6RW5jcnlwdGVkQXNzZXJ0aW9uPjx4ZW5jOkVuY3J5cHRlZERhdGEgeG1sbnM6eGVuYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjIiB4bWxuczpkc2lnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIiBUeXBlPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNFbGVtZW50Ij48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjYWVzMTI4LWNiYyIvPjxkc2lnOktleUluZm8geG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PHhlbmM6RW5jcnlwdGVkS2V5Pjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNyc2Etb2FlcC1tZ2YxcCIvPjx4ZW5jOkNpcGhlckRhdGE+PHhlbmM6Q2lwaGVyVmFsdWU+VzUvSzI0Z0dFZ1piZEp6SWNsM1ZITW5IQWc5UkRKOENQMmJDNVFnNUtDejBHUVhSQ2FMVi9OWUs4bHArZE00cjhsdDh4RGo1c2xkNTlRZGNmUm1oZHhtaUh2L1A5ZXg3R1J4ZjZTTVMramIxQ3VSR2VoV0haL2c3dEFFd0w5M1dhcUdHZk0rZkZjS21pa2tkNVNxVG5XczE1a3QyWnpXSFkycmp2bzgva3lJPTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHNpZzpLZXlJbmZvPgogICA8eGVuYzpDaXBoZXJEYXRhPgogICAgICA8eGVuYzpDaXBoZXJWYWx1ZT5GNW5aV3VzSTM4d2NpUmpUVnF5SDlGcVJhVXFqZDloRDJqYjB3S1NZY01xWDN0L28zamdrUmJVTlU3aU1hcDA2NTFpWkx0L0t6YnBMenJUQXBhdE1jMHdMOFp5NkllSWwwVkxDUWFTbjlBV08wV0MycEJjN2tJMUU0THloNmtQbmVJdGpDOTVFeFhZTXNiZ3Jic2VicGlNSUpFbDc5V1A2SGlrTUl3bWVmb3ljbWgyU3g2UytLR0haRVFUaHBDTzdBeDRjc0l3dWJSaWJSczBGaWxNODZDc3U3a1dxR1lGK1JSWjRaaWlnQ0h2TXlRc1JsaWgwSk9VYlZ2MjY1dnBWamJuNytJLzlnNGFVR2xsUFVMUERLVVBpZVhjSkx3UkJoU25vT21FY1k1R013ZU1tRlVmMjJKVkFIWm9McTJlMUttdi9nTk5kRUFHZmdQUkp2bnovTXpTampFZWhtZ3ExUXNSN092VEJiMFlUQ2VJL1h4V1crYXFZOVdITERCU3VzRFFuTlNDQWJ6b2ZPQngyTDg0Q3VLdGdDc0JtNjNoUm50YVdoMjRydzRXTzNqdUR3a2RZS2JCVmhjUGlKZHJxclo4bnEyNGV1a0pDU3BTQkRORG85MUsyQmR5RGVNOW5aeU5iQ3VFbk1COFczNUNQRHcreFJobXM5bnNxbVNBdW5JODFQY2NEM29BcjlrcEpoaHNRM01EVXVGaUJtU3Y0eU1HaDY1bU5Ta1lMWHhzTXB5ekpnZWRqdENTbWNTdEZtNERnVXFocnlUUXFkNnhLalJ2MVRIcHZBOXB2cDlib0NwK3JhSjlHaitYQ1A0cUhsQVdoU1dxVTQ3NXFyM25NUDF5cCtkNmpyMTFEZUNLa0hZN2lXY0kwRi9DVnVrRVVkWXAwUTJXMTlvdzlvWDRqc1F2cTJocDFaVWw0ZlhaUVNBM3BlUUdoMWh1TlRCOExtclM4QlhUVTFJSm5VT2xCd2hHNkZ3RE9VazlTZHMvS0VqV2xUVkhXemVrQ1FIcW5nNWVnWlJONWJST3NXVFVQUFBuaEIyTXFZWkg4SWJkdmh2UisxRTlGd1hkZ0lNUThCT1ZHdHpHZ1l0aXBIV1ZzbVpkcGlpSTVsWTdyMlFCQTBuOFFXVTdIQlcza3FVMEhSWSsxNkh2OEFyUlVFYlVOdHd5R0szSFlEaDhRclUzWWtqdHBobkk0K2R5eFR5SDY1MWhTK0pMSXRuYUVLY0UwY2dTZ21SbERxWnBLZk9zdUx4RnViWmZRNEFSS2J5TFVZN0t1V29MVzZjUUZNQStJYy9YNmRHVTZzUzEzblVWSzNzMXJqUFdOWi9zMHBsendETFQ4NkR3aFQway84dmtEQ3BicEc2WGZEeG9wUElUYlBHenRwMDd4RVQzUjQvSmNYSDdZWWtXd0RpNk9ITHpkWWw3SStlRmUwNTd3UlBWZmxlNDNES3ZLSi9tdHRHZ05qeGdwTkFnN2tlN0luZGJ6ZytBQlZUTjZMS2FpdUxmdmZZTFpCZ3hFeEJOeDZpZkJXSDY0UFI2bWVGWWZxbEJaeURacHNwUEpEQkl3MlNqc2pOUWZmcEhJTExqenZBQVM0anZUZHVaZ3JnYzZFMyt1YW81SS9QSUtvc0ZSL0VscjFrV3JVTzAzNThmTWpSaXZBSmNHcloxaTBHaEhWK1JEZk5zNGtkSEtTUVgvZERFTHExZ29NMFpmbkkwcmRINTVKWlNtR3RFZVdaemdVY2Vja1UrVUJwcUJtUVBmRlJ4QlJaaHhKTTNDTjUvdzVLMkVYaThpVUgwOGhhbmV4U1ZkR29VcmVQMWcva2Fwc1NueENaREFyaTFsNWNyaFhaMitYcjI0OWtHcTZYWTd4b0Y3MFlRY0lGUGFzOUk4eWVaTHpwNFI4YU9TbXJEQnRXc0NKYUVQWkVVa2ZRMUxqY0JzZTRlWVpyWDZVdm8wbjdNVXRheThQekNJSVJpQW9TWDdNM0VnZkZiYkZwcnJqMUtjSDhpRXdTaFlzQ3A4TU1qS09rbitXOER0ZkhoYnM2QlZkdVN1SVlNQUlucUc0QnpDMTNYdDZRWGg2cytOclhNRmlNUVUrdUFFK2Y0ZHNqaXJGRUpRK2VNV0lZOWJ2aHZyUFNBTmtqTHRFeWJFNUxIdUE1NGJQMDloMElwYm1weUpwRUhlaUVZdGhzNUJZMzBmWW5MaSs5NjB0a01zTkExMTlsUmpsZHA4ZGxvYXFsaWFUU1hSL0VaQVVqMUtQbkIyaWw0R1o4aWoySnQwbnRMYzFRcjFxSHBoU0dNL3hDcFRUR0ZWcGQ0OXkxVG9vZVR4VVFoVk1CUmwvMjFtTXYvaWVUZ3E3ZTdrd3JRY25rOEFNZkRYaVgzNzgwYmUrSlJ2NVE1NkZYR21UL0VOeUlhK25iTVdDdC82a3pvYjJjL1k3R0xhUkxqUEw1a0lYYnc3SWhNN1RKTU9FcnFndTJYcG5EN3VIQUlMZCtTVk1YazZaOUFmY21NUnVhN01MS01QeG1NNTZxSjVHT2Z1QnRFeVUvVnA1NUk0V1psK01qc1Y2Y0prNkV6dXdvZXk1U0V3MlVsUnVBWlBzTTlUZ1hLbDR5SG4vVnFIdWREKzNwbm13NTJSbUVFOEVxQ0dEYWVNektxRFgzbGsrVnd3WDBTRzVuNnNMR0Z4U2FKR2lQUEx0Y2ZNNDN4eFFwV2svOUdyaFU5VGw0SU43dTRjN0RLSHZQTWQ3VVF3Z3dwdzFqZkZ1ajVvSkNNZlhuOXRabkNaMkdRVjlEaSt0TXdqTDBBaDFtRWtHRjF4emFWbVhnVnpRWDgrWERGdkpudG40MzNoaHJhU1E4aTFuUXdhU1o1SWJMUEQxaFNPSEVodlVoekNjSW1WQ3ExazhTYjVWbmQrejViOVVoVHhQam1peTRRTkNlUGVveERZemNLMHY3U0xydVhyN3pmd0pqWUdmdkswWUl6Z1Q5elVieWxLVUFTdmx2bVU5RDlXTUVKU3I4SzVJanJFd0ZkbVRQdDlNOG5UR21LakxhenFyTGlaZ2FWMmhqY2w5ckhjWloxQjRoTWN1d015dS9qWUlMK1UzL3RxR1AwdGQyZFFrbENzMnp2YnJkZGRva092OTJXd1hVUFVzQXNGMHZJNTlrYmJFUklFRndSY0I0dkV5WFZkZXVIU0xOamJnNDJPK0x0OFR5Ly9sNmt3OW14ZVJoUklic1BEUGlQS1AxdnBxb2lYeThqR1JwOVFvd1lLWm82b3Zlak5lQUd0d0RWSGFFS2tPTXZDQ2hvTk5UNTNscStRTzVMTk9PRnoxTkxidzYvU2g5dG5ZcXNFR0lvbC91NTJGK1E1SFJnUGtNc1RiczBVSDl4RVBMdTlML1dCcXJxVCszajJtcHVmbzI2Q1NPcmt3Wmo0V3JNMzM2ZkRKMkV1Y2o0ako0cUFUYXNERGpvV1Y0SkZURFBnaDRkcm9HSkE3NnplSk5HcklOL1VzTGNHY1BFSEVwYnN4aU54QThTbTZkZWVSWjc1akFBNUVUaEFadHdscU9jUUZVV0hWSE9xVmVGTHpab2NNV0lVRGsrT2JkaEJTMit5MFptSnA3djRNV1NJeEZnWTU2Y24yamhwazZ0MzlobFBFTnNKY2wzeVphWU43ZDI3UENnVm5Rb1VkaHFoSXNQTDJBeDN2RUo5VVY1WHcrR0tWYk1JYS9rREFsTjY5SUxTdExQNnZmQ0JVV3VXdEtKZ0xaVHZERU5YR2dCdGVCcnZOc2oxTzBtY0d2Und5Y09yWkNnMDBNbmdNdVhKdE1ha2RTMTNPOVFlaFo1by9wNXB6RzFGSENtbzAvNmFVdmh4YjhFUWF5c1RIdU1rdmYxTmlFajRqQnB1MHZKWkFya2NmTElEUU1uakhnd1l3RDJZQk9uSkliWjZkTk9ZazNOZWpRRmNHbUFwUjhITUYrajdWcUlDREx5a2IvT2c2cEh0QktlMU1uYlQ5dVVOQ3J3QVd4LzYzK0NVMk9COXdTMmxmNWc5YjNvbm9oRmhOUTdrbytzZXNpUGZ3bFB2S08rU0ozbUpWQmxLWkRDSzM5eDhveitKWTZERTR4VjZReXBGQ3NBNHo4cGRxRWFKekcybmN6RXh6c1pEV0ZUTmx5Sk03SzFHMkN1UTR6MVZPOGl4NHdqYlJGRk5rbTJYbWRMZjhxdEJuQVhuWkdhMlR2bi9TZlVEZ2ZWQ1hoUGFkNnIzV28wdzVLZWtleGdUa2ZnQ0phaVlaZjZ2WmM4dHMwSkZ1ZkE0SWtoek5OZ25nR1ZVNEpEbS92MEI0TnJRcm14ekx3M1hUdGhCNjdOcDlDVmJjTVpIWll4eXNiMmRwbUJ2SFQzS01LbzJKNHdzbUZUQUlNcWlzMm5iVnBMb2tIWWlHNFRCdGhmUXRJVS9LVmJ5Ti9FV1pVL3RzOTlMREw5NngzNkZuRTQ1V0NESFM1ZTBNRysvT2RpVWRzc2F4eDJKc3BPTEFxWmxZQjFnM2dVZjJscFUyR1YrczAzT1B0SHBsdjBmaEV0YTJMKzJLTm9NNGNKZjU4LytLMUk3aWhBNXlYN0pvWVFpa2kxV3VxZEpmUWl3ZXF3VUJjUWgrbHNJemFlbFpZaTZ4TGpCSTRWQ2dkOGdjdDJHS3RrWnVWQ3l0aWcrQk0yMTJVc2l2ejBNYVhtaVdaZ0V5OVNLK3FBM1JPZWtJSU9sRmdJQXZpNUFvL2RrNXZuNjhzN0pXL2g2dERZbjBhT2JFQ2lPcmFGNmhHd0J5YWJDTFAvTnhEWDdpMDNlZGgvRU42ZC9BLzIyR1RkUzNUZWtkS0pyZVhwVExwLzBwcUxQQ29BYmFIUjJjaGRPQVUrSzR1dWQyeDQyK1hsUXJ6M3lKbWx0UDd4NXZ4YU1zV2wrKzlFQmdUcWsweHVJWVhBM0JNQnptbUkzSGFqUjl0b3dBNzRiNGFqaWJ4Nkw1eVJSTkEyT1Q4bzkveGs1Zk8vRDBJQ1U1UVpDU1VWS2ZWdWcxT2FwQytPVDkxa3RnV2ZEdTg2d2o1aVoxdUtuTXphSkJCRVZMQXFEYjA1clpxV2tlNFRrUmh4ZEpHQ2pUcERKSFBHYnNaeXJQMVI4T1BHTGVBUHJOaTJoMEhlUDBZcW5QYXVwRzVUSE0wVk02b2FNTE04eVlYRmRZbytsTjNnaEErZUMybkdZU3pMQjZ6NDdDdFBlUGhJb3JwQ1lUNDJubjlJN0MrTXh0UnNLZ3poRDZpaUlBL2plYVVMSmVybmZ1WUtTa0EySHlZWmhvQXVwb2ZqNWQ0cDVvdUVDdTQvRUNmdXdPdnJKYktQTTRhcW9xOTYwdWlzTXkzeGVIWkFNcFdrcXNoNzZmNjNQTkpibENaOURIUEhSSER0NGNTbDhXek9YSkhjVHk5enVZRVJ5L1M2OVp3dVlOMU5rM2RFN2F5Z1Z4MkxENG15Y0FVWHMxV1NNNFVVbXNGTkdTb0ZOZUE3eXhJM0JSZGg5d1NqNFpGdmNjYTVVRmUyWE9PUVlJMENIM3EvZEdHRjdockVKK1dDNms4U2puK05aM2xpMExnMkZIQ1RscFR5ZXZqQzBuWWJFMEExbjZaVHJ0aXVqWEpwRVZlVENJUGJoeEdHZXJIWmtMYjArNU5BcFhsZWRIVEZwdHZ5UWdLS0lGTEZQSHN5VTlBTlRKM3luTVBWWlQ5Q1VnTXpseUxBeEhON3Q5SVd5dWRiTzVaU2owa2Vrb01JaDNxMlBjQmNLbUlhbE1takEwTWRXOGZSaTliaVE1M1RwN1dtVDZFKzhwdHVsMTZWamhRVUpIYWE4aXBXSnl1dVVHWGVubTFNaXJQN3hSZk1Zc2dzRnFPMVFBUk8xZ2xjcmJIeTJpWGs2OVNnYWYxTmFkQWh1Rm03SE5FemZIN05MdVFqSTRsV0ptMXBCc2kxcnYwcDFBdWlieVVrcGVheE9QSnJaUVg3UnpacWtiWHRZV1BvZDhsMW9adEZFcWhMRFVKQXRFTU14ZC9LMnVPRThLV0EwRXpJcm9aNVFyVUdFeFN3bE14QndCMVhUaWZzVEU5SzVQQktVV2ZLNGNvNitwaHJnZ05CNDJta3ZkVitvZ21IZUc3Y3g1b25OK1EyZTl6Z3FHa2tEZndpN2N6clh4bFRpM3NpdFErakwzRHdqb2hINU9YREJEczNBbkFkTjIwQllyaGg5bnhtcHUxSXlRT3JVZmFhMzRLMlkzTWtGaDlwMWxEZmRSVEJIRmhnVFBTUlBWekp2Ty9TRy9aN2pFd1kzYytWSERiVU4raXZCbE5XeW0vZEl0RXY3elJ3UDRRVXRRd3J6dGFoZUJDSzBoaHc3TmRqWmxIeTJ3bmdVNFhLT0hDREpUWVIwRWlLdHJVRXl4RXZReVZhMm5MdlpBdHNhakwvWkdEVnZmbW05dmw0UjAyQ2hGMmtrbEJiMEQ0RkIxTmx4ZVV4cHMvRU5jbFhiRmxMMnh0WTN0K0c3d1FiWUNNRDdUOGFjREc0RWpqMmd6YThKRGV5K3dGTnVvL2RVSlowWDh6MStmMlpFSXVHNnlPK0g2ZkVnYWd6WTFOWURlMGp0b0FCc0ZGbVFJK3lqcCtwVDZZZlpWMXZIVW1vd1ZsNGQrZE1XNHVQVTBvdGJYR2l4bmZ2NHpLMnV2SEN6Y2d4OUNmNmJaajlud3lCYzYrVTA5N05uY1hGcndTb0VpSnJ6QWhsOFhxQTJSVDBCdGlDTVhERVhZdEpLRE03Sy9taVl6TXVFb1ZrY3lQRWQ5MkJ6Nkp0SjI4MUxzVUF1S2dKcmlhQWxKL1dBZXljM0hhWWxKei9CQ0UrV2I3R3BSQTZYS0o1U1ozaVV1SEdLczd5T2N4d1ArSjAzcmhob3Z5bkNSazRUR2hWRUhacThtVXpudFFJV1NOTTQ5L21mNWRqYmFJMGxJOFJka2xGYjNlOXVxWFpGbWxWcGllMDUvN1hZeXdPbXRTVmsyaU9sZm1aRVZmcE43eVAyQ0daMHF4VnhETGY0QkEzdTFXajVoMVdtcmt2R0Z0Y2s4bTVSdHdCek56SzdoNzdrRm5HUFgxaTgwSGpDblp6bWY0MnRGcStOWUdmRFVKWUJUeUhyVnZQbjFCUHJCS2FWa1gzMW9tcFFkcmYyZnVleVU5QWQ1R2lxbm1CTDFLa0lJMUFOTkdjeFIzN0hwdlQ5Y2Q4OXlsTTVHYSt2UWt2MHZLbGtNYXozbVFlRUdsWDI2MDh5Sy81VXVkZm1JM0dldzJTRnZyR2ZycWJCNkozWFhmTzc0SzR5N3N2Ym1QWU9EeklkdW4wQTFDQzNvMWh6d0NtUGRkVC9SUkFXaElTVGVYOUhaY1ZVNVNLd3QrSVlPaFpYa3RnUk1IRm9kczUyKzNZbVY4a0g0WDFUTlNNS2lhcWo3SXZJVEppWmxnYVAydnAzT3hWUU1UeHNPQzIxTnRGTENLQWZHdkFlQXBIcFlqaEU5SGJFSFl0dkpmeVM2OVR6cTBXSFk3NTNxclRvZHZZTDBud2Z6TGRIU2NVZkM4THl2Yyt3VGt3TFk2eERMV0wrNGx0UG5GaWk3TlVEdnN6TWxZdnQ5THdBRlA5YjZseG9NWmdncWhYU0VhMDVYSzJHSERkSVNrUUwzTCtVN004SWw3TWlxMi9CdTlvaG1hSGFkWWxqZzdTNHFBRnZEYkExTnlCbGJydlRwdVhQRXFCM3RDVEsya1JHc2tOY3BxT1VpUTYwU3dydm83ZTZObGp2SnV1eXNXRERoa1V2cG12aUcvQytSK2hMWFFWRkcxV0xpbXdqclp6alFieGdqT0dDZHRLamlmUT08L3hlbmM6Q2lwaGVyVmFsdWU+CiAgIDwveGVuYzpDaXBoZXJEYXRhPgo8L3hlbmM6RW5jcnlwdGVkRGF0YT48L3NhbWw6RW5jcnlwdGVkQXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file diff --git a/tests/data/responses/signed_message_encrypted_assertion.xml.base64 b/tests/data/responses/signed_message_encrypted_assertion.xml.base64 new file mode 100644 index 00000000..76624342 --- /dev/null +++ b/tests/data/responses/signed_message_encrypted_assertion.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZnhjZGM0OTJmMy02Mzk0LTYyNzAtNmRlMS0zZDg2YjA3YTkwNWEiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTMwVDIwOjQ3OjMxWiIgRGVzdGluYXRpb249Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl9mY2UxY2JmZmNkZWEwMzQ5YmU3ODBhOTdhMzdlZjA2ODYyNjg4YzBjIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZnhjZGM0OTJmMy02Mzk0LTYyNzAtNmRlMS0zZDg2YjA3YTkwNWEiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPit1RXNQM3JaQ0ZuOUNDVnNoeFk2SnJVTHNKST08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+T1RYS0ZySDI2YTRQRkgyU2pSWFU0ZHpOVEg4a21Cclg4WXdJUUZkYUdiMVFZc3UvWEE0Uk9YbEt0YzZEblp4akQwUzZ0K2RoKzB1VCtCZVJyZkpxZHM5cWdKdWtkVUVteG9kSEwwSVh1TDN5Q3FHU0hraXpYdUJhMVFhQW5HU1RlV2dRdW9QVmw4cXNDRGltcmZEcTU3amRoNmxiZkR5VW1DV3V4a0dmRzV3PTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6RW5jcnlwdGVkQXNzZXJ0aW9uPjx4ZW5jOkVuY3J5cHRlZERhdGEgeG1sbnM6eGVuYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjIiB4bWxuczpkc2lnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIiBUeXBlPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNFbGVtZW50Ij48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjYWVzMTI4LWNiYyIvPjxkc2lnOktleUluZm8geG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PHhlbmM6RW5jcnlwdGVkS2V5Pjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNyc2EtMV81Ii8+PHhlbmM6Q2lwaGVyRGF0YT48eGVuYzpDaXBoZXJWYWx1ZT5xV3U2NnlqLzJtMDErZHJQUjdybHZhVVlnamlhQVdtZVFLdzV2ZW1HNjJUUmRtZEJpV3g2R3daNGtGcWNNSDFFKzM0Z3JuMXRkaU82REVpTmYza3Y3eVZBWmJvQzNRcmIyV3d2QnpvZEUyUkJJV2RieTY0SHdYa2g4cjM4RlYxQUFXdEJEa3BOSENhMnNHNmc3c1MxMVlOc0UvZ1g2a2FQRXpVRXlUT3ZQcDA9PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWRLZXk+PC9kc2lnOktleUluZm8+CiAgIDx4ZW5jOkNpcGhlckRhdGE+CiAgICAgIDx4ZW5jOkNpcGhlclZhbHVlPnNNMjFIRzQrMEc4empibWJxT1NDWGorOWVueEMwcmNlS1JTdWlKamF5OG9TSlA3WDJsc1Z4NDFRdGh4U3B6MUJmRzIxOW81M3RzWXlFbCt1Q0hEdkVKR1EwVUJsaHcrc3hmdm5yejliK2hLVldwOHZNQlkzYkduNGhnNEVpQmk4N0lpRzlQRHpFMjM5T2lrcjgwWFlkaFY3TG1GbDdWSkIrUlozSUpsbHJCTmFuUlMzU01YTnBOVUNGaTZoWXoxYVFGL3p3VllsYzNVbEk1QjVQQW1LaHlMQVlhSUQwOXlqdDE1MUVnWDJ6TktyOHJ6SHpPWko2RExlLzVxV0NTaWgydFJBKytyY2I2WWRabE5GbTdrMkVVT1J4MUFaZ2tqZGY2UjcvWm54REFwQmhxU0tFbkN5UFhKd1IxSUk5S3A3dW85MzFnR3F5WU1QS0twbmpuRlZBZTNoTUNXNzBBeGtodkd0anFXM3NVaWpRQ2wxbHJkK2hrZlh2bXF3UUk1Tm9ZNHdVTlFwR2ZvVFhVKzlRT25jM1JCOGp4eTlQM0srY1hiVkZZNFlpSTBTOUU2MWU4eWdUL1BIMUtLOGlROFFIQUd1YWtBN3lMeHd2dnpGTjk5ZGZaVjYxUzhtNHcrOHY2SE9FamFYNHRtM1FEZ0VJZ3BHSFhRUDFVcU5MN0dKZ3VrTFJ4WWJ6c3NxbDRMZDZhOElma0JBUllTK0VYZ29GTWc2TDhSYU9mQ3dUZjhIN2RUY1hZTEhWMk5tN1RtelcxcjZENGs0R3I3d2h4eER6RG5TS2JlRUIvN2Zkay9sTHZ0QlAvQmVlZlkrV0lFL3BnVHVMYk1idEZKSU42QUdWQ0tSNHhUUDJPOVU2bWYrbUZDS282Y1lXMmdNVDJoWjJLRWljaW92THFTbUttTS9LaWgxcUtnaGRERTRVdmRIQkRsUmlxMFRHYWMrNXJJa0NiMG45VEtwWmNaT2FORkFjZjhmZk9obVpYRDlSSkZGYUg0QlJWWk9DdVBra3B2NERQakcrUzhNbE00K29MWnc2T2NPdTJTdTA2RklhOXc4RjM3N0psbE9MN1h0cC92Q1VGWlRYdUNjR1ltSExaYThWWHIvSjBzZExkVTNWK2xNVTZHMzNFaVlvMVV4MWNWNTN4TVhaMExSRmdnL3ZZT25HNjNqSHFkdk92dW5jNC82M2s3a2N0bjlwVGw3NlN1dzZkOXhHVzF6aUViTS9GYkxqdTZpb0s1Z3l3NThGRWZtdlgxN2xidnJGd3k0OVRYbHErVVdudmpOcmc0NldSYWhoYk9zdUlLeU9KVFhZNUJGOVc2Zzd0RFdDOG94a3BmSDcxRkU1QVhCNWlBRE1RTExMaEZQNzlGWDNWMEkycHhqREFMazQyazVwQVArcXdFZmwvY016NzVkQU9qL3EzMXp5a0xiODZqRE5jUmJzc3BQeFI1eDFTN1A5TjEvWjRZR2oya2xnRmNiT0U2SCtBTlo0RlYrMDcwQXpTL2h0M2UyUncyRXJKZVFXWHlXeTJaMlBZR3hzNjlDdktyREFqOTU1S2RqMnpkVDRESnhiNVltMDdpcXlwWnA3d3JWQUZTdENtazdEUW0raGhyTTFjOEM5aFE5aGRZdVV3Ky91cnpaZWVvd09pWXJJZnVDVktKTFZNZXZQY2VmLytPSXdXSHIzOFVkb3BNQ1FwZG93UjViZkI2TTRVTldzMmZKVmRqaFBKQXd5V1dWWjlUbS9RMDJOcWJRYWFpZGhkTlZOMEZqbGFQdStDRFg4ZGxLQzNPbjNXWUVQUFFQT3Y1OUlKWmYxd05aTjlES2xLdngyaDdHYlJBNjJqakRsWGo5NlVKcEFlTkJZY3J1Yng0OWFtZW44a2pMeWpTMkwwR3E5SGVHTGNCQmR2eVhHSjJhRlprOTh1L2RXYUdZd2x4Wjcra0Z3QURRcnNjQno1dk9aLytaZHgvYUpxYzNPOUs0T2s2TjNEZ3VHeW9aWlBoMDJuS1A2Y3I3ZlJZaUY2d0I4MTR1dUhkS0pRQURiK1Z6OUVVRU55OTZleFhFTWxFZTZhWUpTU25RVFVQNmE2TXJCM3IwSmZwZHhzNHY1djdlWnVacE9mY29BWUN0WmxHTUhLRWk0QU9jQXI4VXUwbkdPS25iOVMwZkpwQXJhZjVWejRzUUdCWlNuOTNSdWxiNnNlNkVWUlQ1M0NBak91SCsraDJNTUwzbWt3aVY1Z1A5NklYNHpYRGs1eHNtWlFoZGt4L296NnNDMXcxejRBaE4zcGs3TXVucUx5Z3UxUUI4MEx1T3k5K0swNG16dlFJVUtCK0t3K0ZncjczSDY1aW5WRk5vTEplUHJySEMwcGVrbjlKMEgwbE4wS1FwSGNKWG96QngvV1p5bG9uU092K0s5RzNrNmVEelhLeTFpME1HMmpWUzFEV0QrUStSTFF3dXVQZ3lZZkNnU3kzR01aNWQvN1o5Qit3Ui9jN3F2czVXYzFOeWxpL3FUU093VUZNSDErUmVJdEQ2V0J6bEtWbkdUbVFCSzFvWGtsZkZJYVZUSjBvK1pCeUFMaThBQmlLVlFGQW5EdExGaUMvY09XRFo0TFprOHo1YkFrc0duV2pLS1hQbFFNbWFBeUUvQkZCd1pRakFVNjlHUGxjRjJ6a1BNTkZHcUUwYWhHY09TZ3hZMStoUlozNFJhbEhXcmlFdGxjMGJXT1crQWt6UEFoazgzbG0vWFM4dVRieS9IeFh5RTVxd3JUTkg5eGFvYTBBV0hLT2o0bFJSajIxR3BhdWZVQkxhZTF0anhCUzRDdkJ5ODNUeVYvVzFGV1plRXY2MDh2NHV3VWxZTU5raWZZNmlISXhxd1l1RXRtc3lJVDRuOTVXNitnWm1henAwVTd0RkxQTFF0N01jQ1Z1Vmx5QmNEeSt2eStSRHJTZmVVVDhYc1M0VW1zWXdEbnNnQTZNcENsSWI1bDlITVU5UTQyTnJzMWowUlhiWi9CNVpCN2VzM3NQQkpuVTNzYWRncmNoVlU1MnFKS0Q1cDJVM3k4WVFoUFJPWHZscnBEbERsUjZINmJrQTdPMjR0WE9qSUtpR250Y1lXcTFSa29zcUsydjVtVFBpUkhuNDBlM2QzOUE4ZFFDWEx3TThvNWQrZVVuSVBVUW9HNnYzaWhsY05reVgwUUJXYTduNDdmcXVZeW4rZk8wWTlxRjNxcGdmekt5QTVaczlpUWNBMzBaU0E4VzdBaW1IMUxUNWVaRXdKNjJvcUxWa3ppWkU4dC9jVDZFV3NqTnNJWmkxNDRsRUd3TFM5UXRSVjF3Q1p3M2hYTVRpbi9FVlYxemN3bmtuMDJDVEt5UnB6SStjbnQ4V0JNMkRlN0M4Y000N0lvK0c4dmpqK3VmNmFnMFNzUHlYVzhqZEFrV3UvaERxSENrcGpHMWQyZTBUUFo1QXNBQWtNaUNiZXlpT1NpSzdtQkFmUEhZRENuUVdJQ0NMN3JOUlJSWWRiRTZ4TGlsWEdVazAzNXR1Z0pCYThQZnljTDNneWtmWVl3US92YStyZEMwdURHdEdkM0loaFNLRDN1dmxLUWtRT1k2RFZWZ2NGUk1LK2x4SkRkWU1La0NXRU8wSWVQQzdKUzZlZ3N4NzZwTkF5NCtjQzZUZGx1TkNVQmNyMVhZNWdxZXVOL3V4NFVkemwwUWdFeW9RNGRCYWYvSDF6SG15aDRFclNPbHhzWlExbnpkaUFuRy9nbmIvVkE1UkJtc1hiUUMrWjUvQ1hjRGxTNFVnOFJPaHZNYjh3eGVRSmNnV2lKSXcvaHdxeFFvUWhVR09weEJwTUlnWERXQUNyU0MyS2JpTE1ZM3FWYlFvdmhqSGpEelZHRk1FZE1ta09WeUw4bkIxYXQ5QWxkdEhGYnZqNFNQN0R4L042T0JTakNxNHZYOFo3Qng5ajBHTkt6QVlWRDl2QXRmNTRtaFVDSThKTWMwMGlOZXFvVDZwQlFyUWc5dzYzaWhjV1JZUWJxK3lXa1R6UUlYb0g4MG5ZU1d6ZE1HVks5TzkwZ2ViN1hHRm9rVGxzWDhqNzJ3empoa0RGbjBpNi9kTVF4ZnQ4bWM2ak0zZGcyWlAvMUJISXJsS0QreDBYS0hkR2xHSDU3OU9jQzcxYVorV0t2cUlHTk9ZZFhQck1maFlxbG5iRVF3Z0tFN01EdUswTWdzRnVuTW5zSTdTUFc2OUplN3A4dndlSHJZY1VoREUwTm43Rzd2SDFQbGw4UyttMEVyOG5JK3lBaEhkMHgyV0NYazY5UmxJVml2SVlBZFpRTkxNUERSTGNCMU9FRWhOMEg0aUFlRT08L3hlbmM6Q2lwaGVyVmFsdWU+CiAgIDwveGVuYzpDaXBoZXJEYXRhPgo8L3hlbmM6RW5jcnlwdGVkRGF0YT48L3NhbWw6RW5jcnlwdGVkQXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+ diff --git a/tests/data/responses/signed_message_encrypted_assertion2.xml.base64 b/tests/data/responses/signed_message_encrypted_assertion2.xml.base64 new file mode 100644 index 00000000..97687d01 --- /dev/null +++ b/tests/data/responses/signed_message_encrypted_assertion2.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfNGEzNjdmY2M4ZTI0MTEyYTY4Y2Y1ODZlMmY0ZGNiYmY2MTgyZTBiZTRkIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wOS0yNFQwMDozOTo1OFoiIERlc3RpbmF0aW9uPSJodHRwOi8vcHl0b29sa2l0LmNvbTo4MDAwLz9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fODFhNjhjYWIxNGU3MWFjM2I3NzFjYjA3YmExMjU2ODJlODkzOWQ4MCI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vaWRwLmV4YW1wbGUuY29tL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4KICA8ZHM6UmVmZXJlbmNlIFVSST0iI180YTM2N2ZjYzhlMjQxMTJhNjhjZjU4NmUyZjRkY2JiZjYxODJlMGJlNGQiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPnFTOWtJYjhlVEpPamRrbzlyLzZVZ0FmejJnTT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+akVESk8wdmcvTG9nN1BVUkRvSWRpQ2pSRXZFQVU1VjA3STB6U3hZaFFlT1FkaXFzTERQYUZjNDZ3UGlNUDZHcVFXZ053VlB3WTR1ekhZazhJWG56RElDQ2p6ak16SHJmWjRycERZRExiOC8wWlhwL3U2a1ZhSGtzUzhWT3A2RFlPdmFXWm02TU1kNjd5c3o3cDB1S0JJSWVVWEZxVUd0S0Z4cnVKVFJ3QktVPTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNiRENDQWRXZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRMEZBREJUTVFzd0NRWURWUVFHRXdKMWN6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVWTUJNR0ExVUVDZ3dNVDI1bGJHOW5hVzRnU1c1ak1SZ3dGZ1lEVlFRRERBOXBaSEF1WlhoaGJYQnNaUzVqYjIwd0hoY05NVFF3T1RJek1USXlOREE0V2hjTk5ESXdNakE0TVRJeU5EQTRXakJUTVFzd0NRWURWUVFHRXdKMWN6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVWTUJNR0ExVUVDZ3dNVDI1bGJHOW5hVzRnU1c1ak1SZ3dGZ1lEVlFRRERBOXBaSEF1WlhoaGJYQnNaUzVqYjIwd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFPV0ErWUhVN2N2UE9yQk9meENzY3NZVEpCK2tIM01hQTlCRnJTSEZTK0tjUjZjdzdvUFNrdElKeFVndkRwUWJ0Zk5jT2tFL3R1T1BCRG9lY2g3QVhmdkg2ZDdCdzd4dFc4UFBKMm1CNUhuL0hHVzJyb1loeG1maDN0UjVTZHdONmk0RVJWRjhlTGt2d0NIc05ReUsyUmVmMERBSnZwQk5aTUhDcFMyNDkxNi9BZ01CQUFHalVEQk9NQjBHQTFVZERnUVdCQlE3Ny9xVmVpaWdmaFlESVRwbENOdEpLWlRNOERBZkJnTlZIU01FR0RBV2dCUTc3L3FWZWlpZ2ZoWURJVHBsQ050SktaVE04REFNQmdOVkhSTUVCVEFEQVFIL01BMEdDU3FHU0liM0RRRUJEUVVBQTRHQkFKTzJqLzF1TzgwRTVDMlBNNkZrOW16ZXJyYmt4bDdBWi9tdmxiT24rc05aRStWWjFBbnRZdUc4ZWtiSnBKdEcxWWZSZmM3RUE5bUV0cXZ2NGRodjd6Qnk0bks0OU9SK0twSUJqSXRXQjVrWXZycU1MS0JhMzJzTWJncXFVcWVGMUVOWEtqcHZMU3VQZGZHSlpBM2ROYS8rRHliOEdHcVdlNzA3ekx5YzVGOG08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpFbmNyeXB0ZWRBc3NlcnRpb24+PHhlbmM6RW5jcnlwdGVkRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIHhtbG5zOmRzaWc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNhZXMxMjgtY2JjIi8+PGRzaWc6S2V5SW5mbyB4bWxuczpkc2lnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48eGVuYzpFbmNyeXB0ZWRLZXk+PHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIi8+PHhlbmM6Q2lwaGVyRGF0YT48eGVuYzpDaXBoZXJWYWx1ZT5aMXhkeDFmNWV0NDFiM0MwbmZiWmNMWTdWcHZsQ3I4R3habFQxMGc0WDdWU2RBZC9nekFUQUpVbGlUajZyS2JQZ0NMZnBVZmdKSmYyY3hyeGs0bHNXbmROVERjelM2eExFZVp0WEJkREdtZjE4N0tLZjJGQnB5bHhGQlR6NmErYjFjb3RGY0ZQNjBSUGNIRXJTRys1WjdmU0EwUGozZXdtaGxhcTJ4Zmx4M0k9PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWRLZXk+PC9kc2lnOktleUluZm8+CiAgIDx4ZW5jOkNpcGhlckRhdGE+CiAgICAgIDx4ZW5jOkNpcGhlclZhbHVlPnJ0ejFmMS93SDEzcGJ5ckZhVzVMT2svWlNYVDQ1Vi83c2VHbXpSUHlmK0c1ZzN0UGlKRWJoNnVWdVJHa1lRN2F6czdHYkZkNUY2NGd5WjJFTXlQYVkza1NuVEwvRG1tbndlWEgzU3l2d3NVekdLcWxiTWtWN2tiNG9WaFpXOWhFVzZhOG1odVU0TGx6RWdyK3hXUmI0U3N3SmxRb094bnBhVVlMaE9CNVlKUzRHcCtXcjFhZzRGcWM2K0tnSGhCZmtPcFNlN0NON3RSRGJLS1JCOWwrYkJlOEF1aWUzWDFEOHdCTWYvVHZXYlM3eXhuajVrVjYyRExpYXcrYWh4dndQQXJRU1ptVUdLbGhuYmpFNEs4U3pHMjdoTXJWUzdla1ZMU2RDaDhOZlQybDNEWjRVcFZhOExBNTBrNSs5TmxtQittMldsRjNDZmdmMG5JWHVBMlJqaWZmRWFJeVRBODFhOUVPOHdhR2p1VWZPT1ZGUjlIWVhnMHdWb3doSTBxZEd1L3p0MldBWUVZelNUTlNKcUxKVFhwUUpldUc3OWVMN0M4dGFsRllic3pyNjZWcEVFRk5TUnJWYVRaNjVNK2N1aktGemJidC9Jc3M1cEZzZ3pLbnorTmc4ZXR4N0hKSHdGMkFQRjhpS3J2VEJNZEEzZDBCS1hzZE5wd0RPb3luNEtrbVB3a3MvcDhCajkvbjd5Q3hnbzlwOUJ0Mnd6ZG5jbXlPTVZENXE4VUJYS25VQlp1MjE3T213bkNNZlo3QmN1TGJGenRKZ2krelBWa3F5ZGRDUytMUnBRRkl0SnF1RkRyblJ1eE9xaGJnaFVRS1RvT3hRVEZtZ0FoekxjMytrRm0zREM5MlAwR2tpTGZPMWxmNmozQTZsa0RNTmlXZFkvY0I2bFVyVFQ1ZVRVWWpOaXErUlNzOHptUUVnbWFJbG43NzQyYWJQY3FycHdBemFGajhpU25qckhuMmtualorUVZPdXNTODlpRHBzOVdZWlJEbjhkYm1SOGRjbjB5RlByQVZuR1NiV2dnb2oxM3pMM05VVzNXMUxhUFlTOVVjT210c2RadEY4YnVkK3g5MFR6QzkrT3R1ZjRDeGRnb1BrOWtUNlBvbDVSc0ZTN3FxWWNmRXlDUkpDSExKOXhsc2wzV0VFUHE0QzFUWFFvcGR0ZUMyM1MwZk40V2RWNWpiZWRyRmM0SFBDYlEwdHR0SHc2bklmQU9wY0dPWmxJZW5rVnEyenQzNndmaGh1SFJXRHlrYXNXOXczeWhoQzhhYldEYmRDbnZHZllVTmMwclBPaUdPemREQ0tpNmVRTUN2NU5WQUFPRlloa3ROOExwVlJvRldLc0dRNllCL3pPSjFvM29VMXQ5bi9manBVRnFTMXpBN1NRN09qdmovWXVYTExCWHZWNEpjUnFuamZ3R1BFNG9DT28zVkE4R1pBS2kvekYxS3Z1Z0lOb0N1dE84cTZ2QU5aa3FKMWUzNjlBaTVnYzJqd3FlV205WW5naHFJR3R4U0Z4MXN5bHI3QUlRQUVnbHc0Qk5TeWY5Rllxalo0ZTdRK3QwVm85bEIvVGhuemFoRDEzMXZQb2wyM2tVMk5iTWZTTHVUVWVSUGtPVCtLUFEzTHBIN2dPWERPdDFnNWtWWnBkTHVjbko0QXlXYTlvVGZyRkgrSWkvalE1NFdtUDFkM3N3TTFCckRYa0Nwd3BTVVJEdHZpVWpQZU0xYVhqMXhPL1ZqdFQrb1ZHd2pFK0lQclVLZjlndGloSFpRd1M2Mi9NN1lNVGxmMHdBUzdRWXdmcFJ5b1JDdk03bURjSEVpVVYwQURJcnVGbldTYTMwNHlpVWE1dkJFKzJ0amx1ZEVad3RTK1Mwa1lXMURzVHQ4RDdhUGxncWRwdWsrZjI0SlE3OHhzcWkra1dPVkRCNVJQOCtOOU1zcXl6enhiUG8vQXZUUUU5WGc4N01WcFcxcll3YTlJR0tmVzNMVlRPVFIwWFBZT0lwKy8yNU9zUUZxNy80VzFsclFPUWR1UUs3MzE0ZW9MV1hBejJ4K3FPbngrQi9tTG1iRlB0L3FpWkd5bHkyTk5oSWZHdy9rY2ZTSVBPUEx4WTg3RTlvN1JjUGVJQnJ1NUNoTnplTk9ObTBOaDdudGxJS0Y5R3ZqMTlaUlY3RXpCNU9CME8wU0V4bDhxVkpPUlM1UDBaNENDSGpZcWdkci81TVdsV0taVGJCa2RrSjhZc1lZYmVpaWRDRVNpTnJRYlFBa1h0MDluNmN6SWt1d3FlTkRNa1hLTW1VczlCVGNRN3ExTC9KTUowY0dkZ3ZQZmp3VFBHYlVRMldOZS9nOEJpQVk2OWVsZ0c3YjdBbC9GcC9lb0QwUGVlMy85MldnNzlxOGI2MzFJbTcwYWpMM2treEJuWFFVdFR5YnRWblF4b1hRU2FwMUFtNnM1NXM0Q3grV3Z4QWRKUXlVVWdhYmg2TG9TdTRFTVE1cXBCZmhjNFdLSnFHaS9wTktXaUdUUG8yWEVSa3FYWlYwMG9mZllzWExKNUlrdjZKTU16U3U2QnNINXZiRnpaQWRhTWlVWWhCdXFnVjNUYzZYNk1Jc0FJQ1gwV0E5eURrTEFhRE5WaHNJYk5aeHIrMmhKc2FkMnJFV09jY0M2aDlZbjV3Y3BXWTlheXdmN0hjd0lnRG9VblplWlRzV00xU3lFQWRzaWNTLytoa2xwOEx2V1FsZVdDYk9uZnpCOFZaUkNQakk5aXZFZGRwK2N3QTRPWXk5UUhhSjU5NW9Sdll1M2l6dlBMZVhMUmhhTUdOSkhvdk5LbFNEbHdQKzNBWUEzUmhXSTlITGV1cE5WdmxidDJsbEowSmZWVHlaM2dYQm04QjhlcXpabGprUkxGTytjZlNVY3hDbUtCL2pJMHJBQ2FuclN3azBPZWEyeng0TXpGU2JWWWVUQTRQQTFjSHo3NmxSK2QwcHdmNjdGTWdNNUpzcHFpVWsxNmYvQmxYM3BralBsZzZnalFwV0phQ3p0WFZvR3dtMHRLTDFBdjl2SFJ2MmVFVEQvUUhZMG5Md1lVTHgvQ0dselNMMzh4Z01mSVFSOHo1dUZHbmNJQ1pOUFpzNXIrR28xRjQvT0ZrK0ZyVnY2ckNsTWt4UEVJZk1JYVhFdXFBbFZyUlMwUWtUd1Z2Ti9tVmMzK2ora1ZOT1c5eVFaaHdpU3RTOUNSdEhSaFpKaUdjcHNFUmREWHkyTjZaMjBCcGZYaVNNeWFuSzVWV1lXZVIvV2JkVGVFV1JSNmtWVUVyNEFkU0dnekEvbUFVdGorUjNwSTI2b2k2VWZ1LzRRempLVGF2MmxZVjc2b01rVmVxLzRvTzhEYWhCWnd2aDYxVW9BUFcyWlF4UCtPYXB3QVlhTERrbjB4MnBWZTlTQk0xL1VOMHIwUzV5cDBFUU5va1pNTkw4cC9jTVQvRWxXWSt3VFJpc0FkZTJmUnk2c2dzaEZwdFZzdlNsWjdYZm1QQjcrNC9kcjhWZlJJUkNsRGd3d0YrakY2ZExEdnE5aytXMWtxQXJYdHFIcUFSS0kzSytCN01ITkV0Kyt1VEROSThIaEJmaEZac2loNUluRmNnckxvUEZxMldHamgyb2VoOWhSNWhJd1Bkbm1TZUtJNnc2S2cyd1p2NzBUUDBSdDVyTFRXSUNqVXZRY3hjSys5TU56bG5wN0YwWW5Nby93NTN6bnpvNkUyY3BFOFJIUFh6b3hFUG1XVzR5SU9hR1dsRVQ4Sno4RXRtMWNhQU1uWklxOUpMOGE0UlA5NDJVU3MyZ1JDQXk0M2hyRGw5UjhRaFdnOXhNdExnaHFwZ3RnZWRpWmNxRmMyZWJpK2dxMGFxNVNHVzJXUTEyeDN6SmJ3TTF0VkxlWmtUbkl3RG9BREJrYTRsZkYxVHNjOTU1dlVFbzhGMEdWMHZVSzg2akVXTis1czl1cTFydW1qSFEwdmZmTzRwbHVaQUtHb0g4eXNJZ3dpbWwybUNKVzJYaE8zSWNNWXJ4M2ZWcW5Ka05jOWNiTjhpd01VUnoySEdKSFA4bmtDdGpQaG1VdkczRlJQdW4yZnpyU1dUR29qQ1lBbDMzVk13dC8vSHowQWRzYXovUmxqSDh1bVMrRWJBOVdBb0VNUjdNYWxoaXZRaWNrRHlRck4wNzV4K1dJVm52VjlUKzBMNzN2TW1SbzUzWkNjZTJLdmhUMmpkZTFWbzQzZ2c2NnJsNWFiMVhJY3JmNHRZcUk1WXNISU93WStCbXQyRmtPamtJcGVLT1ZyYzRhVHp6SDhVN2ZnejF6MDduK0hLOEc4cTEzOUFOdGpucWJDbGR6dE84SEFZQkoweG9oSXVsMTYxMnhCR2F1M1NTd1NzQ01nK1lia0dmNVVEdmFJNW5qUDY1UzYreE1LelovSENvanBtMnNBUGY1R0g2bU53dWp0RTRzS3lGRXMvelFYYnVwS1Q3RVRCcUw4c1doMVErTDBLUzZwOFZqSWQ2T0U5eVZaVk1pY1hjYW40NmIvM0IxdGVMRWFudmZZcU9zZUttMVNXOWxPOGk2bitvOE55OEJGYnk3RnRmTy9LUEhwNXlYaEZhL05iL0RTaDFxeFdWMW94bzwveGVuYzpDaXBoZXJWYWx1ZT4KICAgPC94ZW5jOkNpcGhlckRhdGE+CjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDpFbmNyeXB0ZWRBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4= \ No newline at end of file diff --git a/tests/data/responses/signed_message_response.xml.base64 b/tests/data/responses/signed_message_response.xml.base64 new file mode 100644 index 00000000..09951908 --- /dev/null +++ b/tests/data/responses/signed_message_response.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGYyMDljZDYwLWYwNjAtNzIyYi0wMmU5LTQ4NTBhYzVhMmU0MSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMjFUMTM6NDE6MDlaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVkOWUzMTljMWI4YTY3ZGE0ODIyNzk2NGMyOGQyODBlNzg2MGY4MDQiPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeGYyMDljZDYwLWYwNjAtNzIyYi0wMmU5LTQ4NTBhYzVhMmU0MSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+bXY1bGZSRTYzclBJcmIyOXRRNlFiZmUveXZZPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT55UXZyTnNvU3dXWEwzclNXM3NINml5ZkcwdWtycTF0eXZzeWRTcnM2M3d6MWF5YVZZL3VYQlF1bGRuMVZjUW1PWnBRZ0R3blp3am1iNWZVK0NaZWpRdzVkVHRqMm1KUjUwVE8xV2o4MXVwVkV0UUJ5RjJSeU9QMUdzQjI3ZFNlWVJUTVlzSzY0eXd6SXhmbHR3MDJCQlFVcXB6MTBpM0V0bFZoNDZoYytMV1k9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9Il9jY2NkNjAyNDExNjY0MWZlNDhlMGFlMmM1MTIyMGQwMjc1NWY5NmM5OGQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTIxVDEzOjQxOjA5WiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnRyYW5zaWVudCI+X2I5OGY5OGJiMWFiNTEyY2VkNjUzYjU4YmFhZmY1NDM0NDhkYWVkNTM1ZDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjk5My0wOS0yMlQxOTowMTowOVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVkOWUzMTljMWI4YTY3ZGE0ODIyNzk2NGMyOGQyODBlNzg2MGY4MDQiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMy0yMVQxMzo0MDozOVoiIE5vdE9uT3JBZnRlcj0iMjk5My0wOS0yMlQxOTowMTowOVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAzLTIxVDEzOjQxOjA5WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjk5My0wMy0yMVQyMTo0MTowOVoiIFNlc3Npb25JbmRleD0iXzlmZTBjOGRjZDMzMDJlNzM2NGZjYWIyMmE1Mjc0OGViZjIyMjRkZjBhYSI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3RAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3Q8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ic24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPndhYTI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iZWR1UGVyc29uQWZmaWxpYXRpb24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+YWRtaW48L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4= diff --git a/tests/data/responses/signed_message_response2.xml.base64 b/tests/data/responses/signed_message_response2.xml.base64 new file mode 100644 index 00000000..d5b0b4f2 --- /dev/null +++ b/tests/data/responses/signed_message_response2.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfNjRmYzM3ZjY0OWQ2M2YxZjkyMzg0NTMyNDQ3NTgwMmFjMGZjMjQ1MzBiIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wOS0yM1QxMjozOToyN1oiIERlc3RpbmF0aW9uPSJodHRwOi8vcHl0b29sa2l0LmNvbTo4MDAwLz9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fZTQwYjUwMTM4MDA2YTgzZjc4ODQwYzRiYjQ1MjY3MDQ5NDhkNmRjZCI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vaWRwLmV4YW1wbGUuY29tL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4KICA8ZHM6UmVmZXJlbmNlIFVSST0iI182NGZjMzdmNjQ5ZDYzZjFmOTIzODQ1MzI0NDc1ODAyYWMwZmMyNDUzMGIiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPmZGZ1hlbE8zdTBSU01vLzNFWkplNXhaNk5Fcz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+cDFsNVdETFlNNlQxa0plZDJJaE40Z0ZKRFhWeDdWaWlkM3N0Y3VKNHdJRXVRb2R4UXppcDhvNjZzcWJ3RW1haHhKbUx5NWxSUmE4UzBtMmRKZXB6NWVUTkZsdCtCbU94YUNlbVhPbTFzcFR6OFRTeEw4UmJRMUZPczJlaG4rN3E2a0F1S0wzY09yaUFuMHRxVzFEMzQzZDBicTdkTkJsYlhlNk9UUnFaTCtZPTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNiRENDQWRXZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRMEZBREJUTVFzd0NRWURWUVFHRXdKMWN6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVWTUJNR0ExVUVDZ3dNVDI1bGJHOW5hVzRnU1c1ak1SZ3dGZ1lEVlFRRERBOXBaSEF1WlhoaGJYQnNaUzVqYjIwd0hoY05NVFF3T1RJek1USXlOREE0V2hjTk5ESXdNakE0TVRJeU5EQTRXakJUTVFzd0NRWURWUVFHRXdKMWN6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVWTUJNR0ExVUVDZ3dNVDI1bGJHOW5hVzRnU1c1ak1SZ3dGZ1lEVlFRRERBOXBaSEF1WlhoaGJYQnNaUzVqYjIwd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFPV0ErWUhVN2N2UE9yQk9meENzY3NZVEpCK2tIM01hQTlCRnJTSEZTK0tjUjZjdzdvUFNrdElKeFVndkRwUWJ0Zk5jT2tFL3R1T1BCRG9lY2g3QVhmdkg2ZDdCdzd4dFc4UFBKMm1CNUhuL0hHVzJyb1loeG1maDN0UjVTZHdONmk0RVJWRjhlTGt2d0NIc05ReUsyUmVmMERBSnZwQk5aTUhDcFMyNDkxNi9BZ01CQUFHalVEQk9NQjBHQTFVZERnUVdCQlE3Ny9xVmVpaWdmaFlESVRwbENOdEpLWlRNOERBZkJnTlZIU01FR0RBV2dCUTc3L3FWZWlpZ2ZoWURJVHBsQ050SktaVE04REFNQmdOVkhSTUVCVEFEQVFIL01BMEdDU3FHU0liM0RRRUJEUVVBQTRHQkFKTzJqLzF1TzgwRTVDMlBNNkZrOW16ZXJyYmt4bDdBWi9tdmxiT24rc05aRStWWjFBbnRZdUc4ZWtiSnBKdEcxWWZSZmM3RUE5bUV0cXZ2NGRodjd6Qnk0bks0OU9SK0twSUJqSXRXQjVrWXZycU1MS0JhMzJzTWJncXFVcWVGMUVOWEtqcHZMU3VQZGZHSlpBM2ROYS8rRHliOEdHcVdlNzA3ekx5YzVGOG08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpFbmNyeXB0ZWRBc3NlcnRpb24+PHhlbmM6RW5jcnlwdGVkRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIHhtbG5zOmRzaWc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNhZXMxMjgtY2JjIi8+PGRzaWc6S2V5SW5mbyB4bWxuczpkc2lnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48eGVuYzpFbmNyeXB0ZWRLZXk+PHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIi8+PHhlbmM6Q2lwaGVyRGF0YT48eGVuYzpDaXBoZXJWYWx1ZT5CK1pRQ2x2dFdMZTdGelZvQlNFZWcrZDllZ2Y0Ly9YMnRSSnlkWXMzbzF2OXY1YnE0dVB6Wng5U1I5aXl1OURmMm1CYS9JWlBUR0JZQ2loL1ZrUXZpejVHajZSZEYyV2hSRTVmLzkreFhsa1hRVU1qU0tReHhzblAzSUNOb25POGZlS216aWZHKzVnS3Fab0w2aUNzMkNNR3BJaURLTGFXdkxDWW13ZXZVb3c9PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWRLZXk+PC9kc2lnOktleUluZm8+CiAgIDx4ZW5jOkNpcGhlckRhdGE+CiAgICAgIDx4ZW5jOkNpcGhlclZhbHVlPlBOUjNKd3VER0VxQjExb0tqVmJwQzJ5SDJTVXFvR3Zoa2NPNTAzeVVDK3g0M1NUdWlxWGpqVmFSeVV4cFI3eXFBQ2V5bWNSK0NLSVVXTzRVckFGd0RxSUlSY1Raa0FSeVJGVkl0M3ByUk9TVFRzZXNiMkdmTFA2Uk1JMGp1eUN4SmZKemx0SThTL01HSkhZaWRldlRiV1ErU254NlFTakE3SFJZdW1pWXpNUWZwMUp3SWU2cHdldGZxUDcxWVUvTUlWWnFEQzJLSU4xeWxUVnVrbzlvSHlGa25ERURqRlN0ZDRWQUZFQ0dJbnc4TUhnQkNFcGdZMFMrMzNPblRya1Q4QVFOWGdoVWxTajl3V2JQYXRnVExMb0swVG0xNEVKcC9WcGJyWWJkTUo0U2t5cGgxc2pDTXgzMGQ0U0I3a0JaQ3pEclhSRUFRbFRtVjJ3OVpuVFBnOXVDR1FKNDNvWUk0U2VGZENOd1llWFlhZ3l4dmRrbSt4NVNyc1BsNWg2Zm9QRWFLczBwMEFycndvNzNoaEJ6cU94TGk3a3JkRjNZVVEwMldtY0ZYOWlCZDRXWEdLSCtIYzFtbUtmSEZEekRqSWRUYVozVEhhRDVROCtLY2dndURrT0x6bXhwVnVJVkE3anVEWENKVXNBSGxLblVHSloreDFzZ1lKcTdGUFRjUjd2U1VIMG5BRHhMdHBCbmc2MXpXS3dNREFRcmw2azlodmREM2dkRXFGTzdFSFRDbTBJU0Y2K0hTODVHd2lDLzJwcDhqYlpEUFJLWndHZkZ1UlBnVmpMSTNHd1I0MDNHTThHMUZFWUsvb2RPVHZPWGd5aTNUQzRYc3VMVW1XWk9OVk54TEU2TlZ3cnRoeHZKMmtkY0VxUTVYSmhhME85MVRlU2dHV1U3MFZRc1VjdjZyakdRNitRV2F1cmxOVnltSk02YURIeEZLOHIxR2R2QU5VaTl5YzAzVHl2WmVtTGZpbGlQYmJ4WU0zV2FDUHRtZFA0bE55d0dya2djeEZDb2M4MWpNNnFDTHJRQ0dGenVRRGhhOUN5aUZIRFVHQWI3ekRMamRDQjFqR0NJTndXWHJyeWorWUdEeHhvYWZ2cFJvMGxZNFhDU3QxVkxyNlM3RU52bmxMdklESDZXNnR3ZE1jN2I2QkluVGRxQzdIT1I2NitLQVFKY2FVbDZWaHZzSEJmaitNeDdoTW5EenVGbWYzQjN6V05FcytyTjFnRDNSQTEwdWh3SGZpcjVBTVZCdFpuUk1xYlNEVGlpKzgwckVBWVplSlc3a2puYmhUbk9pZVZhMGErL0lkaFo3cDNSTEplcjZiUFhHdUgyS3VZT1hsU0srMHlZWUd0TXUvMjNSNVpEMi9PUmNJc3FvWENyREN4TnZmbzFpMkRKZmhvUStwTU9rK2doeGFwQjNnVTFEaW5FNzZnYUU4U2Z3QSsyc2lsbEdGVDFIU0FOQjlvS3pMV0JRR0lpSlZQTklQM2h5akhZUHgva0ZibU1QaFEvRVF2cjZMSEdKNUN1ZTJmUkZNRDdobTVTYlhTOFJST0wvYjh2Y3RqNTNhbG1IR3VJWVRveFBzcVd1aTdCQWpjZ1o2RnY2V2hKbkhkSW9DRWQxV0RCVTFSTm5yNVBPSW5QM2JZbXdXbEppeVNjYVFBZ1QxMHNRUkd5aTZ3UmJqdHFheUxUMlNJTGNteTBlYytjVWtvOU5KY0xXOVRQTEY0R3ZncFV0dEdoUlFHbEdINC9DVWNjRTF1d2pQR1htY05nN1QrZGNPK2U5eWQwYnlLWko0dHVPRy94ZzN5SFRzU3FSTFhxcXM3aU50cDAzZEJsVmdiNEJJM0hCMUFiNWsyMEN3MGluVTlMc3MveXZuS0hzVnJmY2poNlBIeUo3a3YyYnJjTXhhSjJhNHpSRDR3TmR5M0ptQk1icW94NU9namtoOXJ4Yy9mZFhuK2kxTXV5Yy9YcFgrS2p3M3o3aFdHMnNQOC9lUlNiLzlnWGVBdEJnRDkzZkNWTEtEWnQwOUh6WWhzN1ovaGc4YVBwOXNIR1hMcTI1c2JxdzdmQm5Kc2JDOU00Q0JUZW0vYUpLSktnd28rS3VtZlhERlI0SllzVlQrZGpFem1tTE8ya3I5R0pGNE9KaXF1aUZWV3pFZTA0YzA0NmJ1UGlSTlN2dzVkRGo3dmJwZzJaQ3MxalF0YTNzK0VKaEU0dDBFSFZBNzlYbXMrSnU2QURCTUtBY1ZDL1ZuS2FKdmJndVNva3NPcGExRHRmUE4rUy9pYytueVpNaEF1K1FZakdtTVJocFhSY3BlUXB5WUgyNmVTeWlmOEd2VkhJMkZhbUF3SXd6aHRnbDVkZkVlL1lzbHFBdjVNUHZFaXB2TzdlbUZpSmFpL0hrZ2VkbThpT3FPOENySldZcjFvanlvbDlnM013Tmo4Mm0rMTJ2Y3hoTU5pRFV2N3JTcENNZ2p0dVNIMXYrWkFTNjcwdVRiSlJEdEx3SU13NTU3SHBBTkx4VGNXaUZuRzdjZ05vaGsvOE05bVpwQTREVVdCZG0rNld2MTBTdGwxT21xVURIMkRCTnl6a0V0MjBXTFUxZEhpWUVWOTNEV29oK1J2WDRJcHhuaFBadk1rSWRTaUhDNnk3MnRGU1IrdlhzdWVJTmFBYWZLeWIrR1ZpR0tISXdUclAxZklqcXZaMlgvUzFYSkZSNHZoVHNITUJKTXA5dzNNZmxwMVlnd2xkWExOUTkvMzJhWXgwWks0WWJCL014bDJubUErNTRzT3BOZ0l3Sm5oZUVyajZJVXA1OU16NWM1d2lQOUZPd09MbXFtNG1ZaFZ0VTZWK1d5TzJ3QVNNK2tDKzNQUnltSlo3eEMzUXYzc0dhbktrQkptZEhja211ZkI0L0R1OVZ5MURlOUR3SmN2VXZ5b3paZnUxZGJUZWhOY0dXNjV4L0dubjNMb3JETGpsdDBTVnhFeEIzK29EK2djTjVSQ3JvM3VKTEtXQlppSCtSbG93allaZWxpakpadmZRb1JjQ29SbjF1ZWw0R0l3ZFViTlR3Z3RHYW45Sk9VdlJTK0dhcmFXck9LNURjTEQydlZaVmkyUUF1Q1dza2ZzQVBFV090ZHhrOWFOM2tDbGZsbWtyVjRIK1VGbHoyamZsNkxWNmVRTUdOQkxNVEZJNDEzZ0EwakNPQklRamtyMzVXcHpCdWw1RFN5ZHBvckxhdXFET1Nqa0lyWnZKMVdLZVRXWW1WSG5uMW5vMW02Yi8wYllLYW1ta3RIZ0swaGdkZzltcXFvTEUyT3VpaEJvbDJwS1VHMGJPak0ydDBKOEJjb2kxTzErRlFUVVNVeGJuMmwrVXpLb1RvQkxjNllnSFZrcUNHOTZxbU8yVkpjZVY0RW9RMHEwaEVDQTl3RUJHUWsrTGZpOUprVVNYdjRZZHZ0MEhvZHpRcGsrODUzSEdoVmhqRFFVYVh2SmQ4OEZROENuZzFXVS9XdTZycTJlOFBDRG92VFM2bStKN0tsY3BoanE4akU0Um1ySU1CcVF1bGl4NFNHZStOK2NFTmxjY0JrNlpHUEJSUTAyc1BCTXVFS04yWG5hL1BhbUcxTUhsN0w4cEthNU9BSWhYcHNFQXJUTHMvUnQ4aloyVnFHZndoVEJxVTJaL1g0d21zZzRUTllLQklJMExMVmJHYVZmQkZsOVNqcmJqMk12cmJJZlNtMERBeUUyTVc0Z2NuL1hzRFJ3ekI2T0hhdFh1YVZ5MENJb3ZvWGFuMHJadlRlTkF1WmNKUnE4ZGdGT0N3a1lqMXlONE1lZW00anRodFJ3d1VxK1ZLeW94YS92RStGTExieE5OakJnNyt5eW80R1BKcHE2d29ubFI0NUQ5ZU1uUUNXWjRBYSsrUmpRQWNsWjV0RWFEV1BMVlNoYWJxOVFxSjRHQnZxeCtDMTlDdExUaHpTOFZ0YlpvcHhkcytKRzdqOVdZOVNIY09MMys4T2k3YnJuZm5ySlp4VVV4NDFMS0ZORzdlT293eDR4L0w2Nm9UUm1iUHY5YUMrdm9hVmFCbm5EUjM5ZWhxMDZHeVNaQ29oQk9zN2JtMlQvdk0vZ1ViMVZpWTZURTg2Q2pZb3AxL1VnYnQwUXptNzNBRENEODZtVldReHhzMFkyeGFnTDlQWlQzTFhHdWFsRUlJZmxLUFV1QjViV0lmNG1ETmxtVEZHN1hTUmNCUHB0ekRhaUoweHU0Ym01aHJLZHViVlk1VU55N2FXRGpjRkJCSDRFRVFqd3B3QkcvUlNwMFlFRHEzeDNDVG8zbGZvYi9iaER3dUF4WVo4eFZGdk9OZkNLM216ZWtBRGlNQlBwVnJVNU9FUGl3c0V4alVyY21pUWNxdG1UOWdsamJ3bTF3ZEM2VFpxV1Y1ZjVFMEdMUGpGcWVERHJqV0xSeWZHODdPNEFkcHBSbTZqUC9MODR5WEE5N2Z2aGhCSkNvUVhGZTFiZi9na0gxSkZQcS9Lb0wxYUFYWUJiNE16OW8yN1VZM0NjdTlucTBNak94cG90MGNoWjJvalZObkpNQTE2Vjh5RlFJQkJnSzVRK1llWDNPak9BWEpDVEhDZDFqUEZEUmt6SHFuMWNMdFpyaFhiUlRjM0Y5MFlWekY2akZkdUZlU2tkTTJxRW90K0xvajhBQjBvTkNvb1dzVm4xTmltNzl6MTVEd2xuWVBrcmh0TzBWRzJvN3p0bDN2VmZZSFpMWWlqeS91QzI0YVVyWDJ0LzBUTzcvRlIwZTQ5UXQ5QzBRVWRYckFwSjRGc0l3TDBFUDJCeGJMU2F0WTNVZ3VLK1IzaXFGOEVUak45UmVacXBzcDcrcmFHMXdzb004YUdiVS9ZNkVhOEttcXlOUEpNNkV6ZC96VXd6N09FbWp5Z0ZDUTlmemdjTkxuTU5VYkJWVjRmVnlGbzVtU0hHaC9KMUswNkd1UHBhSTYxMGRubFY1aTNRcHk1bVFTNzZ5Ukp1SDVFaXl4b0k0d1ZmdFFGMnRFQ0R6bXNlbUJ2eXhnQlJFbEx0TnM5QUJGUXN2UTVidGRFOUh2MDBpaHdVWno3bmJjVVN1VlJhR1E3emtDSWxrQU5NVFd4aHQ1V0JwU1lURXhhV1Btc21leC9zaFZ5bjNHWEtMbWppb2NSUHFrajNVVG5PbkNIQ282eFBmaGp4TitEUHRLR3YwMXU0alNIWU9LVGtnSzVvSVRLbUIyeW84dkc3dDRmVlFRVTA5bDZQYXV5OG5yS1BRUjZTdGF0ZnlqRklneFhHMENCY3pTcWVCV1QxZkU5dHdUNmZjZ0s0aXIwbWhGejFKRENSSkdlY083UTJDVUJDVDN6TkxtTjRSSTVIblJ5T0V1bFlrdXQ1SmdTMjZ0Nnl5UnpLSHdidGE5aXdVdjdIZnN4NVJxbXVpME9EY3BFQ2JiNUQ2dkQ3YTNWU21DWFJSQ0ozZXZDTCt1cGlZQ2wxZXFMdzBtNStrU1dtMndXNkg2Qi8yYkVOZDVLTjNYd1pHSTB1MXZzNVozd2hOY3ArZWc2NXpFcXlQM1RCS2xtMDRlc2tVZVBWWHlaQVozQVRsV1EzZ3dtRTBkeGJqQmNVMXRYcGRyWDhIU3lhMDI0TUYwSjRwSlhtYnJwSmZzdmRIOEZOWGNpVzd4NjUvOGp4R2FmbzVaVVJ6NTVpbUo4WTF3U1RWa0ZBQ3d6SE5BWHRMbFdVRWtBcGxGUmRwTUFWVW02YWdGSi8xWU10V25aYW5OMlQxcjhGZXdnaW85czJNS1QzTVFoYlRXZDJKekg3QkhmL0xadlBRZjB3dVFxcG5RajJ1MzZ4dXV3KzFjMEgwMXJkdHZSaThwY21TaVJlRVBsdVpRc1VyZVZTc3JBbk1wWUdlNHBwR24yZE1UcFVGSDlZLzZIZG0zQUNUc1hvZDdjKy83bVNDeUp3MHJ1UkRRQkFQdUc1ZVE0RTA1R0dlY3UvR0JDK2lkbzNuNXpZYStNUmlNS0ZQV1FIK3F5VVhJRkc1Zm5HMWtJSjBUK0pxc3ErOUJ0T1V0TGZVWmQwQmNrYmVRWUtzV0RNTTl1OXI5cXJYYUhzRkxEOXJNdDFHTm1MNm5iZzBMeWpYdmM2MHNLOEFkUXlNcUpiQ2drN2M5UzdVWEZEYVBvays0eGtDU2tydGx0MkoyaGZuLzhjUDJMa2FNazZYQk1lRkp3TnJVblZtMFFyMkYzK0hGVVVMSkRETW9PajU0TkE0WkYwZ044NDFqajNyNmZkRy96NVdrL2lGWURoa3pnTHlqZE5RWEM0bVdhMlFKNkkvbnB5RlhkWG1RSHI0UGNLZlJSbHBHOVpjS2tDZE5MTHN3YmRuU0pwNkZCSTlmVktFWWFrdS9pbmw2V2pMRU5Cd3hCMD08L3hlbmM6Q2lwaGVyVmFsdWU+CiAgIDwveGVuYzpDaXBoZXJEYXRhPgo8L3hlbmM6RW5jcnlwdGVkRGF0YT48L3NhbWw6RW5jcnlwdGVkQXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+ diff --git a/tests/data/responses/simple_saml_php.xml b/tests/data/responses/simple_saml_php.xml new file mode 100644 index 00000000..3dddefd3 --- /dev/null +++ b/tests/data/responses/simple_saml_php.xml @@ -0,0 +1,71 @@ + + + https://federate.example.net/saml/saml2/idp/metadata.php + + + + + + + + + + + dVJ592k5xPjCHBCMiJ8eZkPUiT8= + + + LHNK1FJfcOIUuWVKJmGABQ+W98+pQ== + + + MIIQmS6WmmIht3k= + + + + + + + + https://federate.example.net/saml/saml2/idp/metadata.php + + + + + + + + + + + mi0IAultZkpsZa1XxGx9X4iAPQg= + + + LqkW39SOYbttYxlGhIBw== + + + MIIGmmIht3k= + + + + + someone@example.com + + + + + + + hello.com + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:Password + + + + + someone@example.com + + + + diff --git a/tests/data/responses/unsigned_assertion.xml.base64 b/tests/data/responses/unsigned_assertion.xml.base64 new file mode 100644 index 00000000..86294a1c --- /dev/null +++ b/tests/data/responses/unsigned_assertion.xml.base64 @@ -0,0 +1 @@ +PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCjxzYW1sOklzc3Vlcj5odHRwczovL2ZlZGVyYXRlLmV4YW1wbGUubmV0L3NhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+ICAgIA0KPHNhbWw6U3ViamVjdD4NCiAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj4NCiAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMTEtMDYtMTdUMTQ6NTk6MTRaIiBSZWNpcGllbnQ9Imh0dHBzOi8vZXhhbXBsZS5oZWxsby5jb20vYWNjZXNzL3NhbWwiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICA8L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCjwvc2FtbDpTdWJqZWN0Pg0KPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTEtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwMTEtMDYtMTdUMTQ6NTk6MTRaIj4NCiAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICA8c2FtbDpBdWRpZW5jZT5oZWxsby5jb208L3NhbWw6QXVkaWVuY2U+DQogIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KPC9zYW1sOkNvbmRpdGlvbnM+DQo8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MDdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDExLTA2LTE3VDIyOjU0OjE0WiIgU2Vzc2lvbkluZGV4PSJfNTFiZTM3OTY1ZmViNTU3OWQ4MDMxNDEwNzY5MzZkYzJlOWQxZDk4ZWJmIj4NCiAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICA8L3NhbWw6QXV0aG5Db250ZXh0Pg0KPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+DQogICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgPC9zYW1sOkF0dHJpYnV0ZT4NCjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQo8L3NhbWw6QXNzZXJ0aW9uPg== diff --git a/tests/data/responses/unsigned_response.xml.base64 b/tests/data/responses/unsigned_response.xml.base64 new file mode 100644 index 00000000..64815b02 --- /dev/null +++ b/tests/data/responses/unsigned_response.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogIDxzYW1scDpTdGF0dXM+DQogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc4NDE5OTFjLWM3M2YtNDAzNS1lMmVlLWMxNzBjMGUxZDNlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIj4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPiAgICANCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjA5OS0wNi0xN1QxNDo1OToxNFoiIFJlY2lwaWVudD0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiIEluUmVzcG9uc2VUbz0iXzU3YmNiZjcwLTdiMWYtMDEyZS1jODIxLTc4MmJjYjEzYmIzOCIvPg0KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+DQogICAgPC9zYW1sOlN1YmplY3Q+DQogICAgPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTAtMDYtMTdUMTQ6NTM6NDRaIiBOb3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMTQ6NTk6MTRaIj4NCiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgICAgIDxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDowN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMjI6NTQ6MTRaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file diff --git a/tests/data/responses/unsigned_response_with_miliseconds.xm.base64 b/tests/data/responses/unsigned_response_with_miliseconds.xm.base64 new file mode 100644 index 00000000..1722cbf9 --- /dev/null +++ b/tests/data/responses/unsigned_response_with_miliseconds.xm.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGMzMmFlZDY3LTgyMGYtNDI5Ni0wYzIwLTIwNWExMGRkNTc4NyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTQuMTIwWiIgRGVzdGluYXRpb249Imh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL2VuZHBvaW50cy9hY3MucGhwIiBJblJlc3BvbnNlVG89Il81N2JjYmY3MC03YjFmLTAxMmUtYzgyMS03ODJiY2IxM2JiMzgiPg0KICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPg0KICA8c2FtbHA6U3RhdHVzPg0KICAgIDxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz4NCiAgPC9zYW1scDpTdGF0dXM+DQogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZng3ODQxOTkxYy1jNzNmLTQwMzUtZTJlZS1jMTcwYzBlMWQzZTQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDExLTA2LTE3VDE0OjU0OjE0LjEyMFoiPg0KICAgIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+ICAgIA0KICAgIDxzYW1sOlN1YmplY3Q+DQogICAgICA8c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJoZWxsby5jb20iIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj5zb21lb25lQGV4YW1wbGUuY29tPC9zYW1sOk5hbWVJRD4NCiAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj4NCiAgICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIwLTA2LTE3VDE0OjU5OjE0WiIgUmVjaXBpZW50PSJodHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9lbmRwb2ludHMvYWNzLnBocCIgSW5SZXNwb25zZVRvPSJfNTdiY2JmNzAtN2IxZi0wMTJlLWM4MjEtNzgyYmNiMTNiYjM4Ii8+DQogICAgICA8L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0wNi0xN1QxNDo1Mzo0NC4xNzNaIiBOb3RPbk9yQWZ0ZXI9IjIwOTktMDYtMTdUMTQ6NTk6MTQuMjM1WiI+DQogICAgICA8c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICA8c2FtbDpBdWRpZW5jZT5odHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+DQogICAgICA8L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICA8L3NhbWw6Q29uZGl0aW9ucz4NCiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MDcuMTIwWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjA5OS0wNi0xN1QyMjo1NDoxNC4xMjBaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiPg0KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Pg0KICAgICAgICA8c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgIDwvc2FtbDpBdXRobkNvbnRleHQ+DQogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50Pg0KICAgIDxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c29tZW9uZUBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ \ No newline at end of file diff --git a/tests/data/responses/valid_encrypted_assertion.xml.base64 b/tests/data/responses/valid_encrypted_assertion.xml.base64 new file mode 100644 index 00000000..a3a24b2a --- /dev/null +++ b/tests/data/responses/valid_encrypted_assertion.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiDQogICAgICAgICAgICAgICAgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiINCiAgICAgICAgICAgICAgICBJRD0iXzVmNDY4MjQ5NjA5MDQwYzZhMzUxYWMxYmUwZTlmYzYwNTMzZmYwOWQzZCINCiAgICAgICAgICAgICAgICBWZXJzaW9uPSIyLjAiDQogICAgICAgICAgICAgICAgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTI5VDEyOjAxOjU3WiINCiAgICAgICAgICAgICAgICBEZXN0aW5hdGlvbj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvZW5kcG9pbnRzL2Fjcy5waHAiDQogICAgICAgICAgICAgICAgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl9iZTYwYjhjYWY4ZTlkMTliN2EzNTUxYjI0NGYxMTZjOTQ3ZmYyNDdkIg0KICAgICAgICAgICAgICAgID4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPg0KICAgIDxzYW1scDpTdGF0dXM+DQogICAgICAgIDxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiIC8+DQogICAgPC9zYW1scDpTdGF0dXM+DQogICAgPHNhbWw6RW5jcnlwdGVkQXNzZXJ0aW9uPg0KICAgICAgICA8eGVuYzpFbmNyeXB0ZWREYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyINCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB4bWxuczpkc2lnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgPg0KICAgICAgICAgICAgPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI2FlczEyOC1jYmMiIC8+DQogICAgICAgICAgICA8ZHNpZzpLZXlJbmZvIHhtbG5zOmRzaWc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICAgICAgICAgICAgICAgIDx4ZW5jOkVuY3J5cHRlZEtleT4NCiAgICAgICAgICAgICAgICAgICAgPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS0xXzUiIC8+DQogICAgICAgICAgICAgICAgICAgIDx4ZW5jOkNpcGhlckRhdGE+DQogICAgICAgICAgICAgICAgICAgICAgICA8eGVuYzpDaXBoZXJWYWx1ZT5JSDQwY3lrYkMzOUJYNDdLMmtpOC9vMjJXR2svbTRZZm9BVlNaRTBUcWwxZTJBdWxxYXlPWklIREZoL25XRzVSWXpWYzVyVjJZU0lXd2JhUmFrTUl0YTMzSDg2KzhaOCtPTXhPNFpRa3RIV2VUbHEvbFFnUm5TZ2gzZ3EwS3BnaUtBbFVBaTJ4Z1Q3ZEw5THlUWUwwejJrVXVLMzdSUDJnTGJjUEVjeVZXcTQ9PC94ZW5jOkNpcGhlclZhbHVlPg0KICAgICAgICAgICAgICAgICAgICA8L3hlbmM6Q2lwaGVyRGF0YT4NCiAgICAgICAgICAgICAgICA8L3hlbmM6RW5jcnlwdGVkS2V5Pg0KICAgICAgICAgICAgPC9kc2lnOktleUluZm8+DQogICAgICAgICAgICA8eGVuYzpDaXBoZXJEYXRhPg0KICAgICAgICAgICAgICAgIDx4ZW5jOkNpcGhlclZhbHVlPmFJWThnMVhJRVNOM1JhejkwMXA2QUNMaEpMNG4vcWp2Z1VIdG90WHk0enBrUG0wNmZxMnpHVlQvcDU4ejdwRGRtZEdtZy9jUTlWSTZiUGNjc0d5allaTnZwUXRVc042bVcyb2VkbTFXV3JsUUhVOFA0VlJkS0M5STJOQXUzc0doNHEyMDlMcE5scmwyRUI2eWw1eUw3N0ZSU2lCOGtDWGdHaks3SU1sUXo4WU1qWE93Z2tNa3BEVUNuSTlWbVhCTmJUNkp4NDdRU3Y1YThFU081RVdnRkdVc3NPZGtVVTA2a1J5YWRxQkl6eXhFM1NnbTR6Vi82ekdzL0xHYlhjRElkM0tyS3A1NlREOGJlQ1Zhei82VjI0WjJRSjFCbjNwZGJndk5XeS9CeFNuczg2bjRncnAvSUE2aU1NNUVlS3IvRWFlaHp1TU5XRlhqdEhWRWloQ3cxaHpVeHpTZ1ZBVmIxRHE0MktIWm5YNDdMUWRxaDdRVXNWamdBU3hrYisxM0hiRnV4eGVOWkkzdWZkcG1SNmtaNGlRZXdYMk10bnJrYUIyeU1VMytWblYzempvc2lYaCtDbEtZemcvMStCMUMvekw0V3RvUEZBUktwQ2ozd1FvQ3E4QWdFWllndkhMWGFObjAxSTN3QTl3dnlsNklEKzExajBQU1JzTzN6SXl5aE9xaDNlYm1TUk1Ya1Z4a2ZFUVg3MXoyMGtNOHovU2tJUjFSVWxvVzFyajJHaUNvQ0dodUY4YmVxeS9QWFZ4VFIxYUl1OUNvbCsxTUIvZldGNXN4c09lUGNSWjE5S21kVVRPZ3paS2R0MkNjcWo3TTVlVDhCTERRbVRQYWtsRWFuUDVYSFBhSGRGYnFldHBnQk5HMDVYd1A1TWczbDNZSmhqd010WWpGejdqSGw0ZzNpV09jaVVlaXNyS21DT0F3ZUFXT3JMYWNLZzh6QThJRjRyTWY5U2IrOWlvamtRQzBIR2FvUWxHKzZuODBRT0pOQkZ5TGRCSFNzZktacmVmZDdGazZFWjlkMDU5cEw1Yks5Q2IyLzFoUGZJRU1EVkpqSkVpeWh5VHllVllmdEJhcVZWa1AvYzRwdE5TSnN6a0lpdlV2YUExeXVHNktMYzcwanlFRUloTVBKVTN1aEtrSGthRnowQWF6a1ZjbG9wY3J3NWNyYVlSMUYweUpBWnBoWnZ1QXBlMGU5TG1iakcwaGNVRUJrOFFjclNZbXp6elgrc3Q4bFdNRjU1blZybFpsTmVBcmtXMXZER3FBT2RmRnlSSjQ2MnRVT3RYQkNVKy9KSzNPVTZmRjQxR2dONmdaNzJuWU5KMWU4Unl6RWxPdzlqNzdaRkxHK0lKMkZnVk1FWlV0a1RFaUwvYWtoc0RDUUI5Vk1rOTNPSFgvL1lVOTUwZXNGQ3RicTcyejloeFVEKzZ3Um5IUWlnMkU4VXo0Mk9WT3VEaEJxQi9sbXZEUmpMYk92NnJCTUVqOFE1VjFmTGpBckdEbWsrbDJKTFhzZm4xeVBLUHdmZ2VtR013RkdRaXM4OU9wY3NoNy9JS1c3VXBhTGFaTFJOeDI2ME9LMXR0RVV6bHkzWG5ZdGM1MjhCWUZtQ0ZybDhveGV0RVZkRVlrWWJEYzNpK1p5Y2t3MjRWeGEyd2ltdk5HY0JCTVVVT1djN3lHM1R6eklPVUpZZFlJUGs0WCtuM2dJS1Q2SGxjdGNvbmpwM1QxMzBSMUFYSGZWeFJyQ2ZsMzJnNDkrL2dsZmtlcnFoOXczeEFwTWVvNWlrNEFzU0lIY0ZKVGdZb2I4elNZbHcwKzd6Q2pIcSt5bkRxOXZ1R1ZGdmt2ODhHSkR3U0ZqQnR2R1pBRnh5Tk9wZVM2UGdsR3o0MjJCRXd3MlA0QUhwR05DaTBEc2pvVW5GKzNmTG1PYzZnZFpvRGpzMHRoN2dLRmE0THd1RWg4RzAxTzc3TUJUVWFqd3ZxMVlldE1CQnBmakY5S1NqYjhtb05yWCttYnY1L2FuT2xzVzJ6cUhwb3Z6QTJWSkVGdmpJcUVqaGtxejFDR25TMUg0aHg5Ny9jS2VqMHZwbmphSjdoL0ZJZVhHUnNRd3h3b3JUNDQ0UW4xSThQUWpBdUN1cjdsWmlvMnRtSDZMakxneDB2TExXdFNWQWx0U0ZydWZlMHoxNE82a1NpZndGelA3cTM0VjQzVmlpelVmZllpQ0FYSDhtRnN3MnBQQ2MvcGdUVXBQMCtJY1pESWY2WGE3M3k2cnd5RkxZZTVuVTJKN0I4YnpIUlh0Ym83Szg2N3IzMzRicWoxbjU0RGFzbDlYQkFQdlpxWU1ac1lEMFJJS2gyZFFwQ3pLeDc4RVdMRHozSndkYy9OZEMyOHRTVDRUVDFVWTBVSTAvNDNPRWlxeHlUUndyMXZrMDNuTEdza00zQlVNUXJRMk9VY3c0RUlCcWpNRW9pQUNWM3QyMmZXcDE2dDAwV2h3U3JPVW9mR25MWEdGcmQ1Q2ZwbE5OQjJqcloxYW1yMlE0QkNaazFRYkpMZkdGTFhDT2ptR24yZlNEY3M0bDNSSTNBdkk4aGx0TWw3NHdDaS9lTjNaVjM3MlZHOG1JendTNzhrc3ViT05lWHJwVUNCNkI1VUdCVzlKS0xBRnVXd2p0STFtOHROTjJaVDZzM2tKb3l5bjZ6S2REWnVLS09ndmIwbURtV0dHZ1VCWnNrOGpUNGNoUUR6aWlCUTZoRjQrZS95MDdVem14QnBENXlNM0VXKzlTVUY5N1g4V3pwamFUNGUycWVwSnlvekkvanA2NitzNkJlV1Fha3hEM05rL0NleHFib0pNNk1jYWdxa3lVeUVBd0JjNWx2djl0RThsRWQxbmlyTlZaa0QwVmxsNm1WcngrYzNxdHNIc1lUdC9YWkF0Q0x2NXpoL2ZDSzF5aXZWRGVmVUdtMXhFbGZMdzVVNnlScEdRWmpETDkxL2kyMUkzS2pJRTc3VVhqdm54OE56Q2RqK2V6blNVNTRZRU5iczdTaWZzWUpoVjB1U3VJeC9MUVZ4ZWtJSjlwMWFzNktnNEJtTDlBdFZ6d25OUi9uVHREcVZRcGlobzBZTUdmVnM0a3dVaGpubjY4ZzFMcEtlL0wwUTZoT3R3d3lRZVhBTkw5cHRzUzBOQWMrQWVrWWNJWHphTFhrblB1VEFFY2lRQnJUR0JEbk83aThoOU9PblVqekZhVnFKY01Pd1A1cnRnMFcxck9PclpoNTNpOWxNY0JwNnhkeWEzWkJ4aGVYL0hkRFdNZ0NUQklmS09vN1hHZFNPWUhhNkpDVmNWb0R1a3dYS1FZTkI1VUhKMHFLNmFyUmR6VTBTaUxEKzhLbFVmb2c5azZTanQzSFFvMkxybEI3Y3dGTFJybW1nWk5wd0RTeTBvTDBKQ1YwUXd6djlKalg2bDVFcnVwWU9QQ3RuWmYvYzUwc0NHUTlBMGkwYUJsdzhsZGRtTFhaa3VJQXdxSUErUlE2czBpWldkRm1TY2RmWncybCsvU0xEMzl6ekxDbTdQdE5CT3MybWVpbjJEMjlPZHdiZ05GYitzc1JRLzNPaW8yYmx4V01rSCtLcXFpU1RHZmlvMUlkdW9QYzVHanhTVEFPbXNmalBkeHJpY1EwdWw1RCt0MXNyaHU2Vm9rYm1JN0xGbURVWjRaRWhsTVZMTHhDWGsvV0pWWTVYK0Fkcmc1MGRKLzJjbW42VmFiWW5FczF5TXFEeDZZUXFObGhvR0l6MHpRRmtuM1g2T0JsRWkrVzBQeFpiVmxJT1hjRmMva0x4VUwxY0V6WkwxV3haUldFanpIdVZPcnFCVEMxNjluWUZUdVdLdWVSN2VxQitXbEl3ekpHZ2RnYUYwVWR3dzNGdnJxYURzSFZhTVFPWDBQSVdsb0xhOWNFSmRVS1dWOW9DeU4wWHpNcERMVC8xYkVkdmVRUXEyNGhSM1FyUitVMGJWdkJNdFptc0g3MTRMdVNtUFovR1ZSaXcwSHErRjhRV2R4bDZmdWJud1ZsV0JtcUlJRVE4VTQ4cXd4WWJaZW1uMVFtS2UvNnRucWU1ZkVVSTlxOXNPSXFDdlFMOGVuUGNLOTBwa3dEYk1EajBxMXRqSFhQeXBzR1VLTkFEUERDOUl1TTFjZ2JLWEUrUWtJOGppOS9EdjUwNnFyam9vb21kVGgvWEs5eXB3ZmorMTY1b0hIOXBrcXE3RHBYbzR5b0RpNVlXeVluOUhObkYzRkc3NXQrRXRUSGtiWjdhUE13R2RISHVob0NERXhtUVJwc1BkQmJDOFBiL3Q5U3ZmSjZ6S05sT2dON2pFYTYvMzVoVXlhMD08L3hlbmM6Q2lwaGVyVmFsdWU+DQogICAgICAgICAgICA8L3hlbmM6Q2lwaGVyRGF0YT4NCiAgICAgICAgPC94ZW5jOkVuY3J5cHRlZERhdGE+DQogICAgPC9zYW1sOkVuY3J5cHRlZEFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+ diff --git a/tests/data/responses/valid_encrypted_assertion_encrypted_nameid.xml.base64 b/tests/data/responses/valid_encrypted_assertion_encrypted_nameid.xml.base64 new file mode 100644 index 00000000..63854b65 --- /dev/null +++ b/tests/data/responses/valid_encrypted_assertion_encrypted_nameid.xml.base64 @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDFjYWMxMjlhLWFhZjItYzBiYi01YmQyLTdjNjY5Yjg1MDlhMSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDktMjJUMTY6MzM6MTVaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3B5dG9vbGtpdC5jb206ODAwMC8/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzk5ZmI3YjlkODk2MzQ3ZmNlMTIxMmQxOTI0ZDQ0NzNiNTQ3MmYyYTkiPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDFjYWMxMjlhLWFhZjItYzBiYi01YmQyLTdjNjY5Yjg1MDlhMSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+TUg2NUJ4SSt4QkFXNUpmUS9KdVdMVmpEL3lzPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT53bG1MdXZ0dUc4bjEyRm9MOWlJRWFiekxxTUxJNEtSYU04VlM1bzU5NmtHdjhoclpvc25BQVNUYS9vak0xdkhlMnNMcmF0Q2NNYkhwVFJFaXNZcnpmUkhaVkk5bVNBaTU3SHVLZitsSnJla24rL1VNdndJWFVDUGtXcUIzT3orWWdxM3ZKOHNsaGM3THlVTG5haXZORmVkVXBKMFBPVkt4dU41TEdvRmN2SU09PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6RW5jcnlwdGVkQXNzZXJ0aW9uPjx4ZW5jOkVuY3J5cHRlZERhdGEgeG1sbnM6eGVuYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjIiB4bWxuczpkc2lnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIiBUeXBlPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNFbGVtZW50Ij48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjYWVzMTI4LWNiYyIvPjxkc2lnOktleUluZm8geG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PHhlbmM6RW5jcnlwdGVkS2V5Pjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNyc2EtMV81Ii8+PHhlbmM6Q2lwaGVyRGF0YT48eGVuYzpDaXBoZXJWYWx1ZT5VeWcxMW42d0E3MGFoMGxFb2xJL0VWelpjVzFuM3RpUjluRFNnR1FXY3RwK2l2ektBRTg4ekl0UHFWdnlaMjJFbmFjU2tIbjUwRlNWUk1ZYm91bzhYTFZtSzVndzdQdW9sZGVVRmsxTmNuNnpiaUowV1hBSXNUNG9vOTFDcExXR29hN3FaRjNSL20xVjZmcjBKYTBZemhVV2lvZUhtMUQ0TXRncUJSbG4zTlk9PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWRLZXk+PC9kc2lnOktleUluZm8+DQogICA8eGVuYzpDaXBoZXJEYXRhPg0KICAgICAgPHhlbmM6Q2lwaGVyVmFsdWU+clhyUGQ3WUt5dk9VZThpZ2NPeE1tdHl5aFUyYndNbU5GTEJSOUtvdm9yT1BEb1pBWGxtcGVyNUFRT2FjM0NkTzlBbDNKcWl0Uk1kYW44cU51M1RibCtsQ1YrY1ljTG5sdUlpYngra20yT0NyMTBsbWZpRWlhUGdWbzJ5djh6ZFZuK1RxZkJINUVCTG1BS09vUXIycmdCeTFvMVQwYXRrRW5MUzZnMGFLRUtJOXFzSGNaRVVkSkNYTU03NUlYQzY5dm5BMis5MEljQ01GY0JRT3B3aCtBRWhrWVY2ZmhpMHZKRDN0YzIxOGJiQ2xla25NeEZjYUdvUWRJZHd0U2tpUW1NTHRLY2RKSncrakVreDJOQ1d3N1BxM3Q3WkJBL0NKOHNVRERZNHFnWjJXRVh1TFRXZDVNb0xoZ1hwczUvR1lKZUtnVG9VNURVK2M2R3I0bllhVzZ0bkdsNHVQeHpFeDQ1czYxL1pjUkU5T1UwSHVseU9uL1ZyRmw5b1dxMXpERjFzQkMxdFI2dnhSS3lqYWkvazRsMnRCZit2ZGJmc21hYml3YS9MdzdUMUcxbVFVdWtZVk42SkU3YmNxM0N0Vm02VmxUYmxNTTNqSFNQUWVTTy9KbUZEOU9QRUNhZ3N1akZQRmNDLzVKa2VyK0YvRHI0M09IYy9odFBNTzlLMjRTUThzMi9ycWJGWXlIcFVrME0zN0RXWW9YRDVjMDBEY05qUjRQM1NDdlhibC9XSVpWYnd5MksxMnN0ZmpvcHdCT2taNWJLQVNXNkFYeWI4OHhHc3JreWFBOTI3ekQ0Nnp6bUNRWDMrZWNmQzFQU3FmVjRPRXQwSStiMzY5OS9FZm9ETXFzd08wWkpGb1JJSmR2U0RicnBwVmZ4WGFqZWhVSllDZUhNdVFZeFZmK202WW5VUHJaKzBLRzVzRmtZR2V5SEdYc25sUTVFcS9GQzFNRXRNS2IzemJHUllGTTZ1RDhMZ0JJZk1jNjlsL3VMKzhLcjZ0OFVvUmJwTm4raDJDOURzQ3A2WE5LdE5JYWhydE0xYWlkK1dtK2d4TW0vK1dNdGlzajVBRXA2TjlQUjE5WVNMdndjMjZhdnFWTjRRLzBMN1QrSEtqWlNLSG4vMCtmQWJMYUo3bGZLZVJYazJVTzhMUDFJei8xQVp5ekovRU5GMDdUK2xCci8xQlp5OHpLVFdlM1dkenF4MXhlN29JbU53RjJZbHpwc29WL29QU211M1VoRTNrVFJuV20rUlVpM0R3UVZqVDNSbWRoR1Vmb01FMDVrQWhlQUJhbVNWVVJVd2xLSC80b04vbDBNZEVWOElXdE9lOGQyVTdCQ3N1WWlTQ3UxQ1kwcmZwWEQ2U2RyNURseFNtWk9zTUM1NUVubE44MlowU25mUitUNXQrRmRpa2VmM3IwYzlQSG51dlpLeVd6OXZlV01Pa012SVFlQ1duN2ZTYXFpdmQydUxDaVUyYnpCWG1vdVBaNTdQMko5TFF6RjJpQ3hvY0lLOFhiRTJ5QmsyczRuUitjckt4L2lLTzJzR1FBSmtSMkNJRVhDSGdwUWVwSmdzbml0Qk9FVUhnSGlEVUE4enhuTGEzQkNtSVdyNjA4cnpZOEt5SG1ZZFFuVjhiajYyVnhFRmZvN0hMMVpRQ2xPNFpWdHdZVGhlUE1DQmtWcU5iTDhnVm8wUmM4cHp4b2ZQUG9hVXJhbmMwUGtabm1hbG5RR2lvQnErbEhRTktxR3BxRXZuNCtCTEZUQ3U1MkcrUnJTZXkvK25CdG9SVXdCMjJnOGZDRjl2ZWs1N3MzZ0gvQnQxUkJVb1VXUXpPeDdiK2pGSmYvTHMyYkVacVFHT09hWVp5ZVFaN1pxREFRV1VEdXV0NHpqYklrejFnWlFUSnlFMllCdE0vL2hYYi83MmZyWjkycnV3N1Z0dXhOY2NIekZDVzBmbzZWV0U2Q0NEdFlQaThnTlFnbHBlM3FJM0dyaG5zRytiRzd6dmRWUXJteXdTblh6aHNMOGFteDRkZytETGZQS3FXZVplSm5ocnY5VXl5TWVnYkM2K1cwWGROSm15dGRJcEhYTWs0ajd5QTBmOWtLbGRjeEpmOHh6b3I4MkF1UTlsWjQvdXB2T1BRdUk1L2IvTjRzdFFDK0kvZTJvUnlIWENlNUE0ZlpENjJ3UFphNTRsQmNCRWxUU2NFYWpoTEVaZ1dEY25rYmRaNFk4L3ZVQ29UUWp5QUdJS1QrdlI4cGtUbXVxd2g5WXZyZDltdGwxS2FTQ3ZNRUc1R0tlZXVMdWNocURUNlJteUs2YW93TFAyWFNFSk1WczY5WFBWQTBiNWxNWlIzWmQ4YlhvT1NkWElsbHhSbnNPNHNtSlJiOCtyTFhqaXpaSmd5TWYzVVNidkJQalFqTjVDS3ptNi9BSDRObHMrelRYWmVpekNYc3NCOE5kZXBTZmxvT1hxdTl1UU54cTI3RlpNaFZ1UG9CVTM1MG9lZzBTUHhVL2RGWUpEcEFGclZhVmtKRkE0d3VUYTJTYW5zaFc0MG4xM1p1ZGRsdkZMTkZaNGxYd1pFY3NhMm9DRWJ1aERscWFzYThLVnUrVVM4Z0pibWhERisyQlR1RjNOcTJrWlQrWTZxanFNTWRsVytpbHlsUXFVdG5vWWNMdklwUTBkTzdtM0NwLzB4eGdXeGJGVm9BMjB6MGVKZEk2T2N2a2VIYkxGeW5ydWV0ak5IdUROODNxMm9mcUVvNjZKV2tPRXdGYlBsL1BhZzFxc0VURGJVaCt4RmQ5cUdoUERqd0hCcCs3TS84a0hlbDhKTUZRM2t3b0ZZdFVDMjBGcmJ5REc1SkRiV1FhTzBPM1M0UGRBRkRrRDk2VDVYN3MrbThEN01GYkl0NWI3di9iMVdWZG9JVkJYSjRtZmsvelJ4NjBWUHFpSFVoUkM1Q3BoWXFGTVN3aWZXMFkyN1RuM25ucWxNdGFFcUV5RFlMRlBkdm1sWFJqc1MwQW5zSm03ejliK1VTTUM4ZXZMdnVMMVc4V0ZkdHpielZJUGRwYmJrQm9BMFlSZldOeTNVMUhqd3ZmcXFxQytDOFpXcUY4RVJSNFBpM3VVT2NsOUl5dFVNcDdqTVNDd2wxQXB2aElWRTdSQm1kbVdMSTlMbkhocjFxcGJOWE5remtiNS9aM2YyQnZkbEZ6eU1rZkg3UVdVMmRuaHN4QjRURThIb2xYSHoxQ2xUSXRlU3BqRG5ITHlTbTVjM0h6ODAvNkIvSWxvYTE5c0hJN0F1V2VHdVFFYUVkSWhOMUt5YlVoUWs3Tk50UHJXQVBkbng3NC83QjFIR3Q2N2dPb05Zb0VWWEZvUTJaVTUvQ1N1ZUFRbTdDV01td0tORHFXSGd1cHNwZWgvTkdHekYrTUtPOXVSWmJWbktLL3l1NXJCVDdYNGE4QnRCbGRuQTJ2UGt2Y2pROEpUWkpES00xNmxxR09ITGJXVUpiNDZJTVg2Y0RnQTd6blF2Z0hyeU5CRnl4U2orQXBoSitQRHEzblhKeFFNc1d0dlVWaFBiZEswZkpSbUNTd01UWHloYXEwMHpaMGtCL2o0NmFJN2I2RXFvR01Ka2JrdzJaUlpkelE2Ty9SZWUyWDRtSWkyNllVQUk3bnNNbk1HNnpwNG1UMFdCNFhGcFNUanZtYm9SRithaEhHR1VLc2JEQjEweUdCSVdCV2xLV1RzMyt3Sm5ZT0VadXpaMU1OTzVOWW5abDllczc3eXp6QWFIUy9xeVNDNW1oQmg4WjYzYklPUmFIdmUyaVJiY2Qvcm9laUJPajhXTnNkdnBWOXFGbU9WcEdkUWdlaGtsd2s3alRIZHBxRC94NjI1OFA4KzhxQnEzK0g1OHpnV0dLcm5OaERhbkRhNWFyblBqR1RNQ296TE5ZU2M2eHJZTWhlcGdUaUhPaTlFYnBwUS9UYnNSOWppU2VYZWtRTytjV2pDVFFvcEFoaU9qc2x5Z0R3V0sxajUvclVUM1BaVVlrUm5rNGYwODB1b3poc2JybkxydCtUME9WZDYwcnFGYW93SFBXLzZqcGg2OE5Jait2Z21Ba0pvNDNqbTJwR2JwcGNERWpIa05FYjdiNmZoL2RZSEZjdmVBM3lsTFc3T0VKSVl6Tm1GWEtrdklDVmNMZmw2WDZjRWtGTHUyVmFIekRCVllDMTdFWFJyTXYyaEN2VUhmaDFLMXBNMXVOWmtNVmpyblU1ZU9jNDRJZEtEOEg5N1FjZm5rRGhadGxpbHZGR2JVTGNPWTRqcHkya3dkSSt6TTFxMktHazN0S0Z5aURHdndBQXhXVC9zZHl1YTFkaHAwV1lybGpwSmpEVzVMSmRYYWN2bk1heWRlallGd3E5a083OTB1eit0YjlNMTQxQktPYnhRSE1VZENyWktNazYzbWtGeUlTMzhHRVhjSGlKWHkyR0RpZjFQa0dYVzVyUHpiRHc2YlBDbS9oUGJaWmI0d3hiT3ZiWmUyeHVxaDAyNmY5RW13TzZyaGFhOGVITytJRVJ5WmZwNGd0S2cycElqS3JTQjlvSU52UU4xMVpKQ1ZsdnpURGd1Z25RL1lGVVlNeWZpTmR6QURVRVg4d0VhT0VYS1loSHFaTzdXSEV0ak10N3lXdERqaVBhbGQ1WGw4Tnl4RVBldzJHdEZrNkI5VzhuMmhwUGlaWTBpK1BGaEpEallBQ2hPYVlSM1ZPVHpSYTB4ZFNtMHBDcTVVellRQzhoakdRR3I4SER2UEt4TW5Tb2tvamlsYitxZlZ1cW94WENhWVFLOVZuUktTUERsYzVPRmJhbkNKZWQ4V2ZaRXhpWXBqZUxEMXV2NXBpZVU0QndZaWZENGw1OWdWQ2hCczh0UUJEayswZmw2TEY4RlBCcEtuNStySndYRENvdjMxZ1U3eit4VjNZVkppclh6VFZrTzk0S2lnWFpCYlZ2Q0w3Q280M2toUjVrL1I4eWl2Ym9EcHY4TThSQlNGSUg3ZFA0RkFBQjZrdHRuVWl4Zjcvc28zMGNuMVJ5a3djVFdMTzRnRlZuTWRtTDM1R1M1ZXFMYjNvMmNtdU5tSXV3ZGM4Ymx4REx0TGhjQ0FMYmkvZktXYjhtSmdwdEczcThHNm13bkwydXJVcVJQM2ZNVFdJNW1DZUR5dkZEMENFbHBSeHRuN3ZFd0FZTlI1cGVlSkY5UzE4WWJuL3p0a3lTL2xaNEU4OVUzTGNyQ0hROGJaOW9MTmQxUVc4MVVSMm9aVE52NkZHZlZxenYrbyticFd4UytpT1RST0huNEdEdmtsVlNBNytubUxhSHFBWnREeFZ1ZEh1eVdyYmtuNUU4d2dSckRKTy9qYkxvRGYxRy9RcXVlOXhNRFdIV0J0QjVFZzFRRUVsejhFRVZ2K1A5aFhVWXF2SnRBcHVxK1oxT0NCWDBPeFlybCtBcENObVUybXRqc3hwNGprYWtpSzVVb3FWaDIzb0tkeFczNkI4dEVFY05TdDV5UnVBc3hsZmxjVUNGK0U1QU5vdVV6UnRtYkpDZS94ZUxPVEdXUHk3bVpRVG5PcjN4K1RWN0RkNU5vVUU0TlN5d1JTS1hieTN5TTVlV0l0blRxQTdiNEVjNjBxeVhmeE4xT1cvaDdUL3RLR3JrRGJoRllDQktrQzdwcGlpc3ArSkhxWExzTXk2bnNHY1U3aDlzQWhaQWFIWllla3lSLzBQYU5MTjVJTTk1bWVVZkpRNDh2OTlaRWpYMTN6enJtY2pNZG1WOXZCWk5yd1l1NXRHU2NkMWdtbUlOR1pIcGlnTUJ4YnRMUy9Mb2MzTlVzemozYlo0QmpqY0NPQmsvdlEvTE9CMGFmOWY1bzV4SSt1Nk10RklmWDE3MS9Zbjc3clY5Y0dZek1xZGdHTzVoZGV3dG5FTG9uSldGTmQwNlRlZlRMRlV6WVNwY3NQRHpoWWxJNTNDQ1E3cFF5Q2JiTDVLQ1ZKQWZEVHRyTmlieXFPZFlDQ1dzYXloczVqY01jS05PQk1ITmdzalFqQzVGZUtGc0JJdk9DbEM4MUZ2VU5TdXI4aUh0bUVzYnVoRW1XeTFTaXlDMWkyRkRudThWbHlkVmJlV2ZHRFdYK284c1lGanZpaEFSeWtqR3JSUlAvc1ZsWDBSK0pnb0VTWmpyRjk2QzVhNkxxWmVITTVkUDdqWmtWeDRaZnVmbis0Qnp4dlVHczJuZkk1aXdESGtyZz08L3hlbmM6Q2lwaGVyVmFsdWU+DQogICA8L3hlbmM6Q2lwaGVyRGF0YT4NCjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDpFbmNyeXB0ZWRBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4= diff --git a/tests/data/responses/valid_response.xml.base64 b/tests/data/responses/valid_response.xml.base64 new file mode 100644 index 00000000..5a917f2f --- /dev/null +++ b/tests/data/responses/valid_response.xml.base64 @@ -0,0 +1 @@ +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_response2.xml.base64 b/tests/data/responses/valid_response2.xml.base64 new file mode 100644 index 00000000..dc11134d --- /dev/null +++ b/tests/data/responses/valid_response2.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfNTkxYzA3OGRhY2MwYzE4MWM1MTIzY2RmMzA2NmQwNmY1ZjRlNjRmMmRjIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wOS0yNFQwMDoxNjo1OVoiIERlc3RpbmF0aW9uPSJodHRwOi8vcHl0b29sa2l0LmNvbTo4MDAwLz9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fMDMwZDViMWNlNmQ1OTM4NDQ0ZDI0ZDQyY2U5MWVjNDkwZTUwMDFjNyI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vaWRwLmV4YW1wbGUuY29tL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4KICA8ZHM6UmVmZXJlbmNlIFVSST0iI181OTFjMDc4ZGFjYzBjMTgxYzUxMjNjZGYzMDY2ZDA2ZjVmNGU2NGYyZGMiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPnZMN3BFb3dZS1pHM0FYRWdKUjZzQVQ0NTF3Yz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+ZnZOQ3QrcngzaFdjM3VUeXRHd09qYk9taTQ3ektRTFRXLzdlY0RwS2xRNFZhZVc2YnRzNWNqb2wxR2hOZnIxdW5taFF0NXhVMkJsbXRTSzlQekVaNzFYWVlyMWRrNDlaam5EWUtQeFhFQXlBWDlEMWljbjF4cjltL2JiNzcvYkhUZ25PNjBSbk5yMGlrUktQYVAzNFlNSjUzRUcyWTJDZlVhRHRmb2lKSno4PTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNiRENDQWRXZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRMEZBREJUTVFzd0NRWURWUVFHRXdKMWN6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVWTUJNR0ExVUVDZ3dNVDI1bGJHOW5hVzRnU1c1ak1SZ3dGZ1lEVlFRRERBOXBaSEF1WlhoaGJYQnNaUzVqYjIwd0hoY05NVFF3T1RJek1USXlOREE0V2hjTk5ESXdNakE0TVRJeU5EQTRXakJUTVFzd0NRWURWUVFHRXdKMWN6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVWTUJNR0ExVUVDZ3dNVDI1bGJHOW5hVzRnU1c1ak1SZ3dGZ1lEVlFRRERBOXBaSEF1WlhoaGJYQnNaUzVqYjIwd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFPV0ErWUhVN2N2UE9yQk9meENzY3NZVEpCK2tIM01hQTlCRnJTSEZTK0tjUjZjdzdvUFNrdElKeFVndkRwUWJ0Zk5jT2tFL3R1T1BCRG9lY2g3QVhmdkg2ZDdCdzd4dFc4UFBKMm1CNUhuL0hHVzJyb1loeG1maDN0UjVTZHdONmk0RVJWRjhlTGt2d0NIc05ReUsyUmVmMERBSnZwQk5aTUhDcFMyNDkxNi9BZ01CQUFHalVEQk9NQjBHQTFVZERnUVdCQlE3Ny9xVmVpaWdmaFlESVRwbENOdEpLWlRNOERBZkJnTlZIU01FR0RBV2dCUTc3L3FWZWlpZ2ZoWURJVHBsQ050SktaVE04REFNQmdOVkhSTUVCVEFEQVFIL01BMEdDU3FHU0liM0RRRUJEUVVBQTRHQkFKTzJqLzF1TzgwRTVDMlBNNkZrOW16ZXJyYmt4bDdBWi9tdmxiT24rc05aRStWWjFBbnRZdUc4ZWtiSnBKdEcxWWZSZmM3RUE5bUV0cXZ2NGRodjd6Qnk0bks0OU9SK0twSUJqSXRXQjVrWXZycU1MS0JhMzJzTWJncXFVcWVGMUVOWEtqcHZMU3VQZGZHSlpBM2ROYS8rRHliOEdHcVdlNzA3ekx5YzVGOG08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpBc3NlcnRpb24geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiBJRD0iX2VlMDIxYjg5N2U5NjgyM2ZiOWI3MjFkZDgxYTU4MjI4ZGUxZDE1ODNmMiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDktMjRUMDA6MTY6NTlaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9pZHAuZXhhbXBsZS5jb20vc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPgogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPgogIDxkczpSZWZlcmVuY2UgVVJJPSIjX2VlMDIxYjg5N2U5NjgyM2ZiOWI3MjFkZDgxYTU4MjI4ZGUxZDE1ODNmMiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+TmFsL2J4MTBsc1NJUHZvcmo0VjV5dFpqdXRBPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5iMnVkaDAvT3NZcU5yREJEWi8wVlNKdm5XZmVqU2FtZU1pdC9EN3FoRmtsLzRjSUNjTm9jMTlmTUE0aFdIWHU4TDNnYXRyc0YvWE9mOGJlMmlyUmxZbm5CTithMmFyUVRJQUwyNzlhSlJ5MDVtYVJZV2lvUzBaemdpV2tLSEw1YVovaWFlT2Exbkt5eDZpTWxhbW9KdG14b3hxRmQxUWEzUEdzc2JScmZFZlE9PC9kczpTaWduYXR1cmVWYWx1ZT4KPGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJQ2JEQ0NBZFdnQXdJQkFnSUJBREFOQmdrcWhraUc5dzBCQVEwRkFEQlRNUXN3Q1FZRFZRUUdFd0oxY3pFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVZNQk1HQTFVRUNnd01UMjVsYkc5bmFXNGdTVzVqTVJnd0ZnWURWUVFEREE5cFpIQXVaWGhoYlhCc1pTNWpiMjB3SGhjTk1UUXdPVEl6TVRJeU5EQTRXaGNOTkRJd01qQTRNVEl5TkRBNFdqQlRNUXN3Q1FZRFZRUUdFd0oxY3pFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVZNQk1HQTFVRUNnd01UMjVsYkc5bmFXNGdTVzVqTVJnd0ZnWURWUVFEREE5cFpIQXVaWGhoYlhCc1pTNWpiMjB3Z1o4d0RRWUpLb1pJaHZjTkFRRUJCUUFEZ1kwQU1JR0pBb0dCQU9XQStZSFU3Y3ZQT3JCT2Z4Q3Njc1lUSkIra0gzTWFBOUJGclNIRlMrS2NSNmN3N29QU2t0SUp4VWd2RHBRYnRmTmNPa0UvdHVPUEJEb2VjaDdBWGZ2SDZkN0J3N3h0VzhQUEoybUI1SG4vSEdXMnJvWWh4bWZoM3RSNVNkd042aTRFUlZGOGVMa3Z3Q0hzTlF5SzJSZWYwREFKdnBCTlpNSENwUzI0OTE2L0FnTUJBQUdqVURCT01CMEdBMVVkRGdRV0JCUTc3L3FWZWlpZ2ZoWURJVHBsQ050SktaVE04REFmQmdOVkhTTUVHREFXZ0JRNzcvcVZlaWlnZmhZRElUcGxDTnRKS1pUTThEQU1CZ05WSFJNRUJUQURBUUgvTUEwR0NTcUdTSWIzRFFFQkRRVUFBNEdCQUpPMmovMXVPODBFNUMyUE02Rms5bXplcnJia3hsN0FaL212bGJPbitzTlpFK1ZaMUFudFl1Rzhla2JKcEp0RzFZZlJmYzdFQTltRXRxdnY0ZGh2N3pCeTRuSzQ5T1IrS3BJQmpJdFdCNWtZdnJxTUxLQmEzMnNNYmdxcVVxZUYxRU5YS2pwdkxTdVBkZkdKWkEzZE5hLytEeWI4R0dxV2U3MDd6THljNUY4bTwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL3B5dG9vbGtpdC5jb206ODAwMC9tZXRhZGF0YS8iIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6dW5zcGVjaWZpZWQiPjI1ZGRkN2QzNGE3ZDc5ZGI2OTE2NzYyNWNkYTU2YTMyMGFkZjI4NzY8L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMjQtMDMtMjdUMDU6MzY6NTlaIiBSZWNpcGllbnQ9Imh0dHA6Ly9weXRvb2xraXQuY29tOjgwMDAvP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl8wMzBkNWIxY2U2ZDU5Mzg0NDRkMjRkNDJjZTkxZWM0OTBlNTAwMWM3Ii8+PC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+PC9zYW1sOlN1YmplY3Q+PHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTQtMDktMjRUMDA6MTY6MjlaIiBOb3RPbk9yQWZ0ZXI9IjIwMjQtMDMtMjdUMDU6MzY6NTlaIj48c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjxzYW1sOkF1ZGllbmNlPmh0dHA6Ly9weXRvb2xraXQuY29tOjgwMDAvbWV0YWRhdGEvPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wOS0yNFQwMDoxNjo1OVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDktMjRUMDg6MTY6NTlaIiBTZXNzaW9uSW5kZXg9Il9hMzNkYzlmNTkwYjdiNDVmMGE2ZDZiMzIwOTBjYzRiNDY4YzYwN2Q0N2YiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJwaG9uZSIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyIvPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== 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..120a6c3c --- /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 diff --git a/tests/data/responses/valid_unsigned_response.xml b/tests/data/responses/valid_unsigned_response.xml new file mode 100644 index 00000000..a3b88c3e --- /dev/null +++ b/tests/data/responses/valid_unsigned_response.xml @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZngwNWYzY2UxMC0xNjE1LWYzZWEtYTk4OC02MGUzODBiMzI5OWYiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgRGVzdGluYXRpb249Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl81ZmU5ZDZlNDk5YjJmMDkxMzIwNmFhYjNmNzE5MTcyOTA0OWJiODA3Ij48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1scDpTdGF0dXM+PHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbHA6U3RhdHVzPjxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZnhiNGVjOWM4YS00OGViLWZkYTItN2Y3NC1mYTFhMTA1YTk5ZmUiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxNC0wMi0xOVQwOTozNzowMVoiIFNlc3Npb25JbmRleD0iXzYyNzNkNzdiOGNkZTBjMzMzZWM3OWQyMmE5ZmEwMDAzYjlmZTJkNzVjYiI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNtYXJ0aW5AeWFjby5lczwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+U2l4dG8zPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5NYXJ0aW4yPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+ diff --git a/tests/data/responses/valid_unsigned_response.xml.base64 b/tests/data/responses/valid_unsigned_response.xml.base64 new file mode 100644 index 00000000..a3b88c3e --- /dev/null +++ b/tests/data/responses/valid_unsigned_response.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZngwNWYzY2UxMC0xNjE1LWYzZWEtYTk4OC02MGUzODBiMzI5OWYiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgRGVzdGluYXRpb249Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl81ZmU5ZDZlNDk5YjJmMDkxMzIwNmFhYjNmNzE5MTcyOTA0OWJiODA3Ij48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1scDpTdGF0dXM+PHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbHA6U3RhdHVzPjxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZnhiNGVjOWM4YS00OGViLWZkYTItN2Y3NC1mYTFhMTA1YTk5ZmUiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxNC0wMi0xOVQwOTozNzowMVoiIFNlc3Npb25JbmRleD0iXzYyNzNkNzdiOGNkZTBjMzMzZWM3OWQyMmE5ZmEwMDAzYjlmZTJkNzVjYiI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNtYXJ0aW5AeWFjby5lczwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+U2l4dG8zPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5NYXJ0aW4yPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+ diff --git a/tests/data/responses/wrapped_response_2.xml.base64 b/tests/data/responses/wrapped_response_2.xml.base64 new file mode 100644 index 00000000..fb2f8f41 --- /dev/null +++ b/tests/data/responses/wrapped_response_2.xml.base64 @@ -0,0 +1,150 @@ +77u/PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjxzYW1s +cDpSZXNwb25zZSB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6 +Mi4wOnByb3RvY29sIiB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FN +TDoyLjA6YXNzZXJ0aW9uIiBJRD0iXzI2NTAyNGI0NjAyZmM2OGMwMTQ1YzZlOWM1 +NzFkOGY2MjE5ZTZjZmVlMCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIw +MTEtMDYtMTNUMTY6MDI6MjVaIiBEZXN0aW5hdGlvbj0iaHR0cDovL2xvY2FsaG9z +dC9waHAtc2FtbC1maXhlZC9jb25zdW1lLnBocCIgSW5SZXNwb25zZVRvPSJfMzI0 +NDJhOGMzZDFiYThlYTEzNmMiPg0KICA8c2FtbDpJc3N1ZXI+aHR0cHM6Ly9pZHAv +c2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj4N +CiAgPHNhbWxwOlN0YXR1cz4NCiAgICA8c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0i +dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIiAvPg0K +ICA8L3NhbWxwOlN0YXR1cz4NCiAgPHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0i +aHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5z +OnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9Il82M2Iw +YWVhZWMyYmJiNDU4ZjcxMTUzZjIxODBjNzJjNDM5MzFkM2M5MjAiIFZlcnNpb249 +IjIuMCIgSXNzdWVJbnN0YW50PSIyMDExLTA2LTEzVDE2OjAyOjI1WiI+DQogICAg +PHNhbWw6SXNzdWVyPmh0dHBzOi8vaWRwL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21l +dGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+DQogICAgPHNhbWw6U3ViamVjdD4NCiAg +ICAgIDxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9InBocC1zYW1sLWZpeGVk +IiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9y +bWF0OmVtYWlsQWRkcmVzcyI+cm9vdEBleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+ +DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9h +c2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1s +OlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAxMS0wNi0x +M1QxNjowNzoyNVoiIFJlY2lwaWVudD0iaHR0cDovL2xvY2FsaG9zdC9waHAtc2Ft +bC1maXhlZC9jb25zdW1lLnBocCIgSW5SZXNwb25zZVRvPSJfMzI0NDJhOGMzZDFi +YThlYTEzNmMiIC8+DQogICAgICA8L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4N +CiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJl +Zm9yZT0iMjAxMS0wNi0xM1QxNjowMTo1NVoiIE5vdE9uT3JBZnRlcj0iMjAxMS0w +Ni0xM1QxNjowNzoyNVoiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlv +bj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+cGhwLXNhbWwtZml4ZWQ8L3NhbWw6 +QXVkaWVuY2U+DQogICAgICA8L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAg +ICA8L3NhbWw6Q29uZGl0aW9ucz4NCiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBB +dXRobkluc3RhbnQ9IjIwMTEtMDYtMTNUMTI6NDc6MzNaIiBTZXNzaW9uTm90T25P +ckFmdGVyPSIyMDExLTA2LTE0VDAwOjAyOjI1WiIgU2Vzc2lvbkluZGV4PSJfNTk5 +NGFjYjUyODc4MTc4ZjAyYjY2ZTY5M2RlYmUzNDA3MjU3OTZjZDJjIj4NCiAgICAg +IDxzYW1sOkF1dGhuQ29udGV4dD4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0 +Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6 +UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICA8L3Nh +bWw6QXV0aG5Db250ZXh0Pg0KICAgIDwvc2FtbDpBdXRoblN0YXRlbWVudD4NCiAg +PC9zYW1sOkFzc2VydGlvbj4NCiAgPG1kOkVudGl0eURlc2NyaXB0b3IgeG1sbnM6 +bWQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDptZXRhZGF0YSIgeG1sbnM6 +ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiIGVudGl0eUlE +PSJodHRwczovL2lkcC9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHAi +IElEPSJwZng4YjhmZTFkMC0wZjhmLTJlMDAtYTAwOC1iOThiYmM1ZGExZDAiPg0K +ICAgIDxkczpTaWduYXR1cmU+DQogICAgICA8ZHM6U2lnbmVkSW5mbz4NCiAgICAg +ICAgPGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8v +d3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiIC8+DQogICAgICAgIDxk +czpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8y +MDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiIC8+DQogICAgICAgIDxkczpSZWZlcmVu +Y2UgVVJJPSIjcGZ4OGI4ZmUxZDAtMGY4Zi0yZTAwLWEwMDgtYjk4YmJjNWRhMWQw +Ij4NCiAgICAgICAgICA8ZHM6VHJhbnNmb3Jtcz4NCiAgICAgICAgICAgIDxkczpU +cmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3ht +bGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIgLz4NCiAgICAgICAgICAgIDxkczpU +cmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3ht +bC1leGMtYzE0biMiIC8+DQogICAgICAgICAgPC9kczpUcmFuc2Zvcm1zPg0KICAg +ICAgICAgIDxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3Lncz +Lm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIgLz4NCiAgICAgICAgICA8ZHM6RGln +ZXN0VmFsdWU+NVVmdy9lUlMwVHpIbC9vc2pMVCtJOGxlUDZVPTwvZHM6RGlnZXN0 +VmFsdWU+DQogICAgICAgIDwvZHM6UmVmZXJlbmNlPg0KICAgICAgPC9kczpTaWdu +ZWRJbmZvPg0KICAgICAgPGRzOlNpZ25hdHVyZVZhbHVlPkM0OFpLQ2FwQVdsNHBx +WlM1ZFhMTmVmdjZSYS9hMXZGSDlGWDZsd3c3RS94VmxtZXFTbHh1WGEra0JicE4r +RWFzWmJaMGE4blYxTE1oNGN5TER2ajVnVURyYkhvMG1aOVNhRDBZaFhxcnBQY21H +djVmSGZxZFRtRTVJUUs2MjQ0UkFPdk05MklyYU0vU0hRQ0dROE1hdkhTNSs4Nm11 +MGdkbjVuNWJrcUU1ND08L2RzOlNpZ25hdHVyZVZhbHVlPg0KICAgICAgPGRzOktl +eUluZm8+DQogICAgICAgIDxkczpYNTA5RGF0YT4NCiAgICAgICAgICA8ZHM6WDUw +OUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtp +Rzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBG +dVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFR +S0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04 +eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpB +ZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFz +d0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhK +bk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RB +V0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFF +SkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFF +QkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9s +aXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJ +ZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNm +Q0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJ +REFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVv +V1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtG +RGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRV +dnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhv +cGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+DQogICAgICAgIDwvZHM6WDUw +OURhdGE+DQogICAgICA8L2RzOktleUluZm8+DQogICAgPC9kczpTaWduYXR1cmU+ +DQogICAgPG1kOklEUFNTT0Rlc2NyaXB0b3IgcHJvdG9jb2xTdXBwb3J0RW51bWVy +YXRpb249InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCI+DQog +ICAgICA8bWQ6S2V5RGVzY3JpcHRvciB1c2U9InNpZ25pbmciPg0KICAgICAgICA8 +ZHM6S2V5SW5mbyB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94 +bWxkc2lnIyI+DQogICAgICAgICAgPGRzOlg1MDlEYXRhPg0KICAgICAgICAgICAg +PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFO +QmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05W +QkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3 +RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXho +Ym1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYw +ZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZh +TUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJU +YjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVG +VkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FH +U0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FH +U0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBR +ZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIx +NjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQ +NXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1 +VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2Jx +WStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2 +M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11 +bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdS +K1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPg0KICAgICAgICAg +IDwvZHM6WDUwOURhdGE+DQogICAgICAgIDwvZHM6S2V5SW5mbz4NCiAgICAgIDwv +bWQ6S2V5RGVzY3JpcHRvcj4NCiAgICAgIDxtZDpLZXlEZXNjcmlwdG9yIHVzZT0i +ZW5jcnlwdGlvbiI+DQogICAgICAgIDxkczpLZXlJbmZvIHhtbG5zOmRzPSJodHRw +Oi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4NCiAgICAgICAgICA8ZHM6 +WDUwOURhdGE+DQogICAgICAgICAgICA8ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNn +VENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxN +QWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnla +ekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dG +Z1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJD +UUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhN +elZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlN +QllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3Tkdi +Mjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxM +bVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFi +bWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0Jn +UURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwz +K3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBs +QWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJv +MDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNE +UUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandP +c2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29u +eWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhU +eXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5 +Q2VydGlmaWNhdGU+DQogICAgICAgICAgPC9kczpYNTA5RGF0YT4NCiAgICAgICAg +PC9kczpLZXlJbmZvPg0KICAgICAgPC9tZDpLZXlEZXNjcmlwdG9yPg0KICAgICAg +PG1kOlNpbmdsZUxvZ291dFNlcnZpY2UgQmluZGluZz0idXJuOm9hc2lzOm5hbWVz +OnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUmVkaXJlY3QiIExvY2F0aW9uPSJo +dHRwczovL2lkcC9zaW1wbGVzYW1sL3NhbWwyL2lkcC9TaW5nbGVMb2dvdXRTZXJ2 +aWNlLnBocCIgLz4NCiAgICAgIDxtZDpOYW1lSURGb3JtYXQ+dXJuOm9hc2lzOm5h +bWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6dHJhbnNpZW50PC9tZDpOYW1l +SURGb3JtYXQ+DQogICAgICA8bWQ6U2luZ2xlU2lnbk9uU2VydmljZSBCaW5kaW5n +PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YmluZGluZ3M6SFRUUC1SZWRp +cmVjdCIgTG9jYXRpb249Imh0dHBzOi8vaWRwL3NpbXBsZXNhbWwvc2FtbDIvaWRw +L1NTT1NlcnZpY2UucGhwIiAvPg0KICAgIDwvbWQ6SURQU1NPRGVzY3JpcHRvcj4N +CiAgICA8bWQ6Q29udGFjdFBlcnNvbiBjb250YWN0VHlwZT0idGVjaG5pY2FsIj4N +CiAgICAgIDxtZDpHaXZlbk5hbWU+QW5kcmVhczwvbWQ6R2l2ZW5OYW1lPg0KICAg +ICAgPG1kOlN1ck5hbWU+TWF5ZXI8L21kOlN1ck5hbWU+DQogICAgICA8bWQ6RW1h +aWxBZGRyZXNzPmFuZHJlYXMubWF5ZXJAd3VlcnRoLmNvbTwvbWQ6RW1haWxBZGRy +ZXNzPg0KICAgIDwvbWQ6Q29udGFjdFBlcnNvbj4NCiAgPC9tZDpFbnRpdHlEZXNj +cmlwdG9yPg0KPC9zYW1scDpSZXNwb25zZT4= diff --git a/tests/pep8.rc b/tests/pep8.rc new file mode 100644 index 00000000..ad7c254b --- /dev/null +++ b/tests/pep8.rc @@ -0,0 +1,3 @@ +[pycodestyle] +ignore = E501, E731, W504 +max-line-length = 160 diff --git a/tests/pylint.rc b/tests/pylint.rc new file mode 100644 index 00000000..e5dd9bb9 --- /dev/null +++ b/tests/pylint.rc @@ -0,0 +1,75 @@ +[MASTER] +profile=no +persistent=yes +ignore= +cache-size=500 + +[REPORTS] +output-format=text +files-output=no +reports=yes + +[BASIC] +no-docstring-rgx=__.*__|_.* +class-rgx=[A-Z_][a-zA-Z0-9_]+$ +function-rgx=[a-zA_][a-zA-Z0-9_]{2,70}$ +method-rgx=[a-z_][a-zA-Z0-9_]{2,70}$ +const-rgx=(([A-Z_][A-Z0-9_]*)|([a-z_][a-z0-9_]*)|(__.*__)|register|urlpatterns)$ +good-names=_,i,j,k,e,qs,pk,setUp,tearDown,el,ns,fd,js,nb,na,sp,SAML_SINGLE_LOGOUT_NOT_SUPPORTED,SAML_SINGLE_LOGOUT_NOT_SUPPORTED,NAMEID_WINDOWS_DOMAIN_QUALIFIED_NAME +docstring-min-length=1 + +disable=E0611,W0703,W0511,W1401,F0401,W0102,E1103,W0212,I0011 + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus extisting member attributes cannot be deduced by static analysis +ignored-modules= + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). +ignored-classes=SQLObject,WSGIRequest + +# When zope mode is activated, add a predefined set of Zope acquired attributes +# to generated-members. +zope=no + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E0201 when accessed. +generated-members=objects,DoesNotExist,id,pk,_meta,base_fields,context,views,save + +# List of method names used to declare (i.e. assign) instance attributes +defining-attr-methods=__init__,__new__,setUp + +[VARIABLES] +init-import=no +dummy-variables-rgx=_|dummy + +[SIMILARITIES] +min-similarity-lines=6 +ignore-comments=yes +ignore-docstrings=yes +[MISCELLANEOUS] +notes=FIXME,XXX,TODO + +[FORMAT] +max-line-length=200 +max-module-lines=1200 +indent-string=' ' +indent-after-paren=4 + +[DESIGN] +max-args=10 +max-locals=40 +max-returns=6 +max-branches=50 +max-statements=120 +max-parents=10 +max-attributes=10 +min-public-methods=0 +max-public-methods=100 diff --git a/tests/settings/settings.json b/tests/settings/settings.json new file mode 120000 index 00000000..eb2af51e --- /dev/null +++ b/tests/settings/settings.json @@ -0,0 +1 @@ +settings1.json \ No newline at end of file diff --git a/tests/settings/settings1.json b/tests/settings/settings1.json new file mode 100644 index 00000000..69d7d25e --- /dev/null +++ b/tests/settings/settings1.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": "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/settings2.json b/tests/settings/settings2.json new file mode 100644 index 00000000..22f92dc1 --- /dev/null +++ b/tests/settings/settings2.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": "https://idp.example.com/simplesaml/saml2/idp/metadata.php", + "singleSignOnService": { + "url": "http://idp.example.com/SSOService.php" + }, + "singleLogoutService": { + "url": "http://idp.example.com/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/settings3.json b/tests/settings/settings3.json new file mode 100644 index 00000000..de72e50d --- /dev/null +++ b/tests/settings/settings3.json @@ -0,0 +1,47 @@ +{ + "strict": false, + "debug": false, + "custom_base_path": "../../../tests/data/customPath/", + "sp": { + "entityId": "http://pytoolkit.com:8000/metadata/", + "assertionConsumerService": { + "url": "http://pytoolkit.com:8000/?acs" + }, + "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/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/__init__.py b/tests/src/OneLogin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/src/OneLogin/saml2_tests/__init__.py b/tests/src/OneLogin/saml2_tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py new file mode 100644 index 00000000..e1f4b062 --- /dev/null +++ b/tests/src/OneLogin/saml2_tests/auth_test.py @@ -0,0 +1,1305 @@ +# -*- coding: utf-8 -*- + +# MIT License + +from base64 import b64decode, b64encode +import json +from os.path import dirname, join, exists +import unittest +from urlparse import urlparse, parse_qs + +from onelogin.saml2.auth import OneLogin_Saml2_Auth +from onelogin.saml2.constants import OneLogin_Saml2_Constants +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(dirname(dirname(dirname(__file__)))), 'data') + settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings') + + 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') + content = f.read() + f.close() + return content + + def get_request(self): + return { + 'http_host': 'example.com', + 'script_name': '/index.html', + 'get_data': {} + } + + def testGetSettings(self): + """ + Tests the get_settings method of the OneLogin_Saml2_Auth class + Build a OneLogin_Saml2_Settings object with a setting array + and compare the value returned from the method of the + auth object + """ + settings_info = self.loadSettingsJSON() + settings = OneLogin_Saml2_Settings(settings_info) + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + + auth_settings = auth.get_settings() + self.assertEqual(settings.get_sp_data(), auth_settings.get_sp_data()) + + def testGetSSOurl(self): + """ + Tests the get_sso_url method of the OneLogin_Saml2_Auth class + """ + settings_info = self.loadSettingsJSON() + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + + sso_url = settings_info['idp']['singleSignOnService']['url'] + self.assertEqual(auth.get_sso_url(), sso_url) + + def testGetSLOurl(self): + """ + Tests the get_slo_url method of the OneLogin_Saml2_Auth class + """ + settings_info = self.loadSettingsJSON() + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + + slo_url = settings_info['idp']['singleLogoutService']['url'] + self.assertEqual(auth.get_slo_url(), slo_url) + + def testGetSLOresponseUrl(self): + """ + Tests the get_slo_response_url method of the OneLogin_Saml2_Auth class + """ + settings_info = self.loadSettingsJSON() + settings_info['idp']['singleLogoutService']['responseUrl'] = "http://idp.example.com/SingleLogoutReturn.php" + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + slo_url = settings_info['idp']['singleLogoutService']['responseUrl'] + self.assertEqual(auth.get_slo_response_url(), slo_url) + # test that the function falls back to the url setting if responseUrl is not set + settings_info['idp']['singleLogoutService'].pop('responseUrl') + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + slo_url = settings_info['idp']['singleLogoutService']['url'] + self.assertEqual(auth.get_slo_response_url(), slo_url) + + def testGetSessionIndex(self): + """ + Tests the get_session_index 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_index()) + + 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_index()) + + 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 + Case Invalid Response + """ + request_data = self.get_request() + message = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + del request_data['get_data'] + request_data['post_data'] = { + 'SAMLResponse': message + } + auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) + auth.process_response() + + self.assertEqual(auth.get_last_error_reason(), 'Signature validation failed. SAML Response rejected') + + def testProcessNoResponse(self): + """ + Tests the process_response method of the OneLogin_Saml2_Auth class + Case No Response, An exception is throw + """ + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=self.loadSettingsJSON()) + + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'SAML Response not found'): + auth.process_response() + + self.assertEqual(auth.get_errors(), ['invalid_binding']) + + def testProcessResponseInvalid(self): + """ + Tests the process_response method of the OneLogin_Saml2_Auth class + Case Invalid Response, After processing the response the user + is not authenticated, attributes are notreturned, no nameID and + the error array is not empty, contains 'invalid_response + """ + request_data = self.get_request() + message = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + del request_data['get_data'] + request_data['post_data'] = { + 'SAMLResponse': message + } + auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) + auth.process_response() + + self.assertFalse(auth.is_authenticated()) + self.assertEqual(len(auth.get_attributes()), 0) + self.assertEqual(auth.get_nameid(), None) + self.assertEqual(auth.get_attribute('uid'), None) + self.assertEqual(auth.get_errors(), ['invalid_response']) + + def testProcessResponseInvalidRequestId(self): + """ + Tests the process_response method of the OneLogin_Saml2_Auth class + Case Invalid Response, Invalid requestID + """ + request_data = self.get_request() + message = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) + plain_message = b64decode(message) + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) + del request_data['get_data'] + request_data['post_data'] = { + 'SAMLResponse': b64encode(plain_message) + } + auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) + request_id = 'invalid' + auth.process_response(request_id) + self.assertEqual('No Signature found. SAML Response rejected', auth.get_last_error_reason()) + + auth.set_strict(True) + auth.process_response(request_id) + self.assertEqual(auth.get_errors(), ['invalid_response']) + self.assertEqual('The InResponseTo of the Response: _57bcbf70-7b1f-012e-c821-782bcb13bb38, does not match the ID of the AuthNRequest sent by the SP: invalid', auth.get_last_error_reason()) + + valid_request_id = '_57bcbf70-7b1f-012e-c821-782bcb13bb38' + auth.process_response(valid_request_id) + self.assertEqual('No Signature found. SAML Response rejected', auth.get_last_error_reason()) + + def testProcessResponseValid(self): + """ + Tests the process_response method of the OneLogin_Saml2_Auth class + Case Valid Response, After processing the response the user + is authenticated, attributes are returned, also has a nameID and + the error array is empty + """ + 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=self.loadSettingsJSON()) + + auth.process_response() + self.assertTrue(auth.is_authenticated()) + self.assertEqual(len(auth.get_errors()), 0) + self.assertEqual('492882615acf31c8096b627245d76ae53036c090', auth.get_nameid()) + attributes = auth.get_attributes() + self.assertNotEqual(len(attributes), 0) + self.assertEqual(auth.get_attribute('mail'), attributes['mail']) + friendlyname_attributes = auth.get_friendlyname_attributes() + self.assertEqual(len(friendlyname_attributes), 0) + 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): + """ + Tests the redirect_to method of the OneLogin_Saml2_Auth class + (phpunit raises an exception when a redirect is executed, the + exception is catched and we check that the targetURL is correct) + Case redirect without url parameter + """ + request_data = self.get_request() + relay_state = 'http://sp.example.com' + request_data['get_data']['RelayState'] = relay_state + auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) + target_url = auth.redirect_to() + self.assertEqual(target_url, relay_state) + + def testRedirectTowithUrl(self): + """ + Tests the redirect_to method of the OneLogin_Saml2_Auth class + (phpunit raises an exception when a redirect is executed, the + exception is catched and we check that the targetURL is correct) + Case redirect with url parameter + """ + request_data = self.get_request() + relay_state = 'http://sp.example.com' + url_2 = 'http://sp2.example.com' + request_data['get_data']['RelayState'] = relay_state + auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) + target_url = auth.redirect_to(url_2) + self.assertEqual(target_url, url_2) + + def testProcessNoSLO(self): + """ + Tests the process_slo method of the OneLogin_Saml2_Auth class + Case No Message, An exception is throw + """ + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=self.loadSettingsJSON()) + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'SAML LogoutRequest/LogoutResponse not found'): + auth.process_slo(True) + + def testProcessSLOResponseInvalid(self): + """ + Tests the process_slo method of the OneLogin_Saml2_Auth class + Case Invalid Logout Response + """ + request_data = self.get_request() + message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) + request_data['get_data']['SAMLResponse'] = message + auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) + + auth.process_slo(True) + self.assertEqual(len(auth.get_errors()), 0) + + auth.set_strict(True) + auth.process_slo(True) + # The Destination fails + self.assertEqual(auth.get_errors(), ['invalid_logout_response']) + + auth.set_strict(False) + auth.process_slo(True) + self.assertEqual(len(auth.get_errors()), 0) + + def testProcessSLOResponseNoSucess(self): + """ + Tests the process_slo method of the OneLogin_Saml2_Auth class + Case Logout Response not sucess + """ + request_data = self.get_request() + message = self.file_contents(join(self.data_path, 'logout_responses', 'invalids', 'status_code_responder.xml.base64')) + # In order to avoid the destination problem + plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) + message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) + request_data['get_data']['SAMLResponse'] = message + auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) + + auth.set_strict(True) + auth.process_slo(True) + self.assertEqual(auth.get_errors(), ['logout_not_success']) + + def testProcessSLOResponseRequestId(self): + """ + Tests the process_slo method of the OneLogin_Saml2_Auth class + Case Logout Response with valid and invalid Request ID + """ + request_data = self.get_request() + message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) + # In order to avoid the destination problem + plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) + message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) + request_data['get_data']['SAMLResponse'] = message + auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) + + request_id = 'wrongID' + auth.set_strict(True) + auth.process_slo(True, request_id) + self.assertEqual(auth.get_errors(), ['invalid_logout_response']) + + request_id = 'ONELOGIN_21584ccdfaca36a145ae990442dcd96bfe60151e' + auth.process_slo(True, request_id) + self.assertEqual(len(auth.get_errors()), 0) + + def testProcessSLOResponseValid(self): + """ + Tests the process_slo method of the OneLogin_Saml2_Auth class + Case Valid Logout Response + """ + request_data = self.get_request() + message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) + # In order to avoid the destination problem + plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) + message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) + request_data['get_data']['SAMLResponse'] = message + auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) + + # FIXME + # if (!isset($_SESSION)) { + # $_SESSION = array(); + # } + # $_SESSION['samltest'] = true; + + auth.set_strict(True) + auth.process_slo(True) + self.assertEqual(len(auth.get_errors()), 0) + + # FIXME + # // Session keep alive + # $this->assertTrue(isset($_SESSION['samltest'])); + # $this->assertTrue($_SESSION['samltest']); + + def testProcessSLOResponseValidDeletingSession(self): + """ + Tests the process_slo method of the OneLogin_Saml2_Auth class + Case Valid Logout Response, validating deleting the local session + """ + request_data = self.get_request() + message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) + + # FIXME + # if (!isset($_SESSION)) { + # $_SESSION = array(); + # } + # $_SESSION['samltest'] = true; + + # In order to avoid the destination problem + plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) + message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) + request_data['get_data']['SAMLResponse'] = message + auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) + + auth.set_strict(True) + auth.process_slo(False) + + self.assertEqual(len(auth.get_errors()), 0) + + # FIXME + # $this->assertFalse(isset($_SESSION['samltest'])); + + def testProcessSLORequestInvalidValid(self): + """ + Tests the process_slo method of the OneLogin_Saml2_Auth class + Case Invalid Logout Request + """ + settings_info = self.loadSettingsJSON() + request_data = self.get_request() + message = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_deflated.xml.base64')) + request_data['get_data']['SAMLRequest'] = message + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_info) + target_url = auth.process_slo(True) + parsed_query = parse_qs(urlparse(target_url)[4]) + self.assertEqual(len(auth.get_errors()), 0) + slo_url = settings_info['idp']['singleLogoutService']['url'] + self.assertIn(slo_url, target_url) + self.assertIn('SAMLResponse', parsed_query) + # self.assertNotIn('RelayState', parsed_query) + + auth.set_strict(True) + auth.process_slo(True) + # Fail due destination missmatch + self.assertEqual(auth.get_errors(), ['invalid_logout_request']) + + auth.set_strict(False) + target_url_2 = auth.process_slo(True) + parsed_query_2 = parse_qs(urlparse(target_url_2)[4]) + self.assertEqual(len(auth.get_errors()), 0) + slo_url = settings_info['idp']['singleLogoutService']['url'] + self.assertIn(slo_url, target_url_2) + self.assertIn('SAMLResponse', parsed_query_2) + # self.assertNotIn('RelayState', parsed_query_2) + + def testProcessSLORequestNotOnOrAfterFailed(self): + """ + Tests the process_slo method of the OneLogin_Saml2_Auth class + Case Logout Request NotOnOrAfter failed + """ + request_data = self.get_request() + message = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'not_after_failed.xml.base64')) + # In order to avoid the destination problem + plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) + message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) + request_data['get_data']['SAMLRequest'] = message + auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON()) + + auth.set_strict(True) + auth.process_slo(True) + self.assertEqual(auth.get_errors(), ['invalid_logout_request']) + + def testProcessSLORequestDeletingSession(self): + """ + Tests the process_slo method of the OneLogin_Saml2_Auth class + Case Valid Logout Request, validating that the local session is deleted, + a LogoutResponse is created and a redirection executed + """ + settings_info = self.loadSettingsJSON() + request_data = self.get_request() + message = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_deflated.xml.base64')) + # In order to avoid the destination problem + plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) + message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) + request_data['get_data']['SAMLRequest'] = message + # FIXME + # if (!isset($_SESSION)) { + # $_SESSION = array(); + # } + # $_SESSION['samltest'] = true; + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_info) + + auth.set_strict(True) + target_url = auth.process_slo(True) + parsed_query = parse_qs(urlparse(target_url)[4]) + slo_url = settings_info['idp']['singleLogoutService']['url'] + self.assertIn(slo_url, target_url) + self.assertIn('SAMLResponse', parsed_query) + # self.assertNotIn('RelayState', parsed_query) + + # FIXME // Session is not alive + # $this->assertFalse(isset($_SESSION['samltest'])); + + # $_SESSION['samltest'] = true; + + auth.set_strict(True) + target_url_2 = auth.process_slo(True) + target_url_2 = auth.process_slo(True) + parsed_query_2 = parse_qs(urlparse(target_url_2)[4]) + slo_url = settings_info['idp']['singleLogoutService']['url'] + self.assertIn(slo_url, target_url_2) + self.assertIn('SAMLResponse', parsed_query_2) + # self.assertNotIn('RelayState', parsed_query_2) + + # FIXME // Session is alive + # $this->assertTrue(isset($_SESSION['samltest'])); + # $this->assertTrue($_SESSION['samltest']); + + def testProcessSLORequestRelayState(self): + """ + Tests the process_slo method of the OneLogin_Saml2_Auth class + Case Valid Logout Request, validating the relayState, + a LogoutResponse is created and a redirection executed + """ + settings_info = self.loadSettingsJSON() + request_data = self.get_request() + message = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_deflated.xml.base64')) + # In order to avoid the destination problem + plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) + message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) + request_data['get_data']['SAMLRequest'] = message + request_data['get_data']['RelayState'] = 'http://relaystate.com' + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_info) + + auth.set_strict(True) + target_url = auth.process_slo(False) + parsed_query = parse_qs(urlparse(target_url)[4]) + slo_url = settings_info['idp']['singleLogoutService']['url'] + self.assertIn(slo_url, target_url) + self.assertIn('SAMLResponse', parsed_query) + self.assertIn('RelayState', parsed_query) + self.assertIn('http://relaystate.com', parsed_query['RelayState']) + + def testProcessSLORequestSignedResponse(self): + """ + Tests the process_slo method of the OneLogin_Saml2_Auth class + Case Valid Logout Request, validating the relayState, + a signed LogoutResponse is created and a redirection executed + """ + settings_info = self.loadSettingsJSON() + settings_info['security']['logoutResponseSigned'] = True + request_data = self.get_request() + message = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_deflated.xml.base64')) + # In order to avoid the destination problem + plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) + message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) + request_data['get_data']['SAMLRequest'] = message + request_data['get_data']['RelayState'] = 'http://relaystate.com' + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_info) + + auth.set_strict(True) + target_url = auth.process_slo(False) + parsed_query = parse_qs(urlparse(target_url)[4]) + slo_url = settings_info['idp']['singleLogoutService']['url'] + self.assertIn(slo_url, target_url) + self.assertIn('SAMLResponse', parsed_query) + self.assertIn('RelayState', parsed_query) + self.assertIn('SigAlg', parsed_query) + self.assertIn('Signature', parsed_query) + self.assertIn('http://relaystate.com', parsed_query['RelayState']) + self.assertIn(OneLogin_Saml2_Constants.RSA_SHA256, parsed_query['SigAlg']) + + def testLogin(self): + """ + Tests the login method of the OneLogin_Saml2_Auth class + Case Login with no parameters. An AuthnRequest is built an redirect executed + """ + settings_info = self.loadSettingsJSON() + 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]) + sso_url = settings_info['idp']['singleSignOnService']['url'] + self.assertIn(sso_url, target_url) + self.assertIn('SAMLRequest', parsed_query) + self.assertIn('RelayState', parsed_query) + 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 + Case Login with relayState. An AuthnRequest is built with a the + RelayState in the assertion is built and redirect executed + """ + settings_info = self.loadSettingsJSON() + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + relay_state = 'http://sp.example.com' + + target_url = auth.login(relay_state) + 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) + self.assertIn('RelayState', parsed_query) + self.assertIn(relay_state, parsed_query['RelayState']) + + def testLoginSigned(self): + """ + Tests the login method of the OneLogin_Saml2_Auth class + Case Login signed. An AuthnRequest signed is built an redirect executed + """ + settings_info = self.loadSettingsJSON() + settings_info['security']['authnRequestsSigned'] = True + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + return_to = u'http://example.com/returnto' + + 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) + self.assertIn('RelayState', parsed_query) + self.assertIn('SigAlg', parsed_query) + self.assertIn('Signature', parsed_query) + self.assertIn(return_to, parsed_query['RelayState']) + self.assertIn(OneLogin_Saml2_Constants.RSA_SHA256, parsed_query['SigAlg']) + + def testLoginForceAuthN(self): + """ + Tests the login method of the OneLogin_Saml2_Auth class + Case AuthN Request is built with ForceAuthn and redirect executed + """ + 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.assertNotIn('ForceAuthn="true"', request) + + auth_2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + target_url_2 = auth_2.login(return_to, False, False) + parsed_query_2 = parse_qs(urlparse(target_url_2)[4]) + self.assertIn(sso_url, target_url_2) + self.assertIn('SAMLRequest', parsed_query_2) + request_2 = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_2['SAMLRequest'][0]) + self.assertNotIn('ForceAuthn="true"', request_2) + + auth_3 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + target_url_3 = auth_3.login(return_to, True, False) + 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('ForceAuthn="true"', request_3) + + def testLoginIsPassive(self): + """ + Tests the login method of the OneLogin_Saml2_Auth class + Case AuthN Request is built with IsPassive and redirect executed + """ + 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.assertNotIn('IsPassive="true"', request) + + auth_2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + target_url_2 = auth_2.login(return_to, False, False) + parsed_query_2 = parse_qs(urlparse(target_url_2)[4]) + self.assertIn(sso_url, target_url_2) + self.assertIn('SAMLRequest', parsed_query_2) + request_2 = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_2['SAMLRequest'][0]) + self.assertNotIn('IsPassive="true"', request_2) + + auth_3 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + target_url_3 = auth_3.login(return_to, False, True) + 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('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 + Case Logout with no parameters. A logout Request is built and redirect + executed + """ + settings_info = self.loadSettingsJSON() + request_data = self.get_request() + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_info) + + target_url = auth.logout() + parsed_query = parse_qs(urlparse(target_url)[4]) + slo_url = settings_info['idp']['singleLogoutService']['url'] + self.assertIn(slo_url, target_url) + self.assertIn('SAMLRequest', parsed_query) + self.assertIn('RelayState', parsed_query) + hostname = OneLogin_Saml2_Utils.get_self_host(request_data) + self.assertIn(u'http://%s/index.html' % hostname, parsed_query['RelayState']) + + def testLogoutWithRelayState(self): + """ + Tests the logout method of the OneLogin_Saml2_Auth class + Case Logout with relayState. A logout Request with a the RelayState in + the assertion is built and redirect executed + """ + settings_info = self.loadSettingsJSON() + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + relay_state = 'http://sp.example.com' + + target_url = auth.logout(relay_state) + parsed_query = parse_qs(urlparse(target_url)[4]) + slo_url = settings_info['idp']['singleLogoutService']['url'] + self.assertIn(slo_url, target_url) + self.assertIn('SAMLRequest', parsed_query) + self.assertIn('RelayState', parsed_query) + self.assertIn(relay_state, parsed_query['RelayState']) + + def testLogoutSigned(self): + """ + Tests the logout method of the OneLogin_Saml2_Auth class + Case Logout signed. A logout Request signed in + the assertion is built and redirect executed + """ + settings_info = self.loadSettingsJSON() + settings_info['security']['logoutRequestSigned'] = True + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + return_to = u'http://example.com/returnto' + + target_url = auth.logout(return_to) + parsed_query = parse_qs(urlparse(target_url)[4]) + slo_url = settings_info['idp']['singleLogoutService']['url'] + self.assertIn(slo_url, target_url) + self.assertIn('SAMLRequest', parsed_query) + self.assertIn('RelayState', parsed_query) + self.assertIn('SigAlg', parsed_query) + self.assertIn('Signature', parsed_query) + self.assertIn(return_to, parsed_query['RelayState']) + self.assertIn(OneLogin_Saml2_Constants.RSA_SHA256, parsed_query['SigAlg']) + + def testLogoutNoSLO(self): + """ + Tests the logout method of the OneLogin_Saml2_Auth class + Case IdP no SLO endpoint. + """ + settings_info = self.loadSettingsJSON() + del settings_info['idp']['singleLogoutService'] + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + + 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') + + def testLogoutNameIDandSessionIndex(self): + """ + Tests the logout method of the OneLogin_Saml2_Auth class + Case nameID and sessionIndex as parameters. + """ + settings_info = self.loadSettingsJSON() + request_data = self.get_request() + auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_info) + + name_id = 'name_id_example' + session_index = 'session_index_example' + target_url = auth.logout(name_id=name_id, session_index=session_index) + parsed_query = parse_qs(urlparse(target_url)[4]) + slo_url = settings_info['idp']['singleLogoutService']['url'] + self.assertIn(slo_url, target_url) + 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) + sessions_index_in_request = OneLogin_Saml2_Logout_Request.get_session_indexes(logout_request) + self.assertIn(session_index, sessions_index_in_request) + self.assertEqual(name_id, name_id_from_request) + + def testLogoutNameID(self): + """ + Tests the logout method of the OneLogin_Saml2_Auth class + Case nameID loaded after process SAML Response + """ + 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=self.loadSettingsJSON()) + 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]) + 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(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): + """ + Tests the set_strict method of the OneLogin_Saml2_Auth + """ + settings_info = self.loadSettingsJSON() + settings_info['strict'] = False + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info) + + settings = auth.get_settings() + self.assertFalse(settings.is_strict()) + + auth.set_strict(True) + settings = auth.get_settings() + self.assertTrue(settings.is_strict()) + + auth.set_strict(False) + settings = auth.get_settings() + self.assertFalse(settings.is_strict()) + + with self.assertRaises(AssertionError): + auth.set_strict('42') + + 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): + """ + Tests the build_request_signature method of the OneLogin_Saml2_Auth + """ + settings = self.loadSettingsJSON() + message = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_deflated.xml.base64')) + relay_state = 'http://relaystate.com' + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings) + + signature = auth.build_request_signature(message, relay_state) + 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) + 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) + + def testBuildResponseSignature(self): + """ + Tests the build_response_signature method of the OneLogin_Saml2_Auth + """ + settings = self.loadSettingsJSON() + message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) + relay_state = 'http://relaystate.com' + auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings) + + signature = auth.build_response_signature(message, relay_state) + valid_signature = 'IcyWLRX6Dz3wHBfpcUaNLVDMGM3uo6z2Z11Gjq0/APPJaHboKGljffsgMVAGBml497yckq+eYKmmz+jpURV9yTj2sF9qfD6CwX2dEzSzMdRzB40X7pWyHgEJGIhs6BhaOt5oXEk4T+h3AczERqpVYFpL00yo7FNtyQkhZFpHFhM=' + 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) + 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) + + 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 new file mode 100644 index 00000000..edbad6f4 --- /dev/null +++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py @@ -0,0 +1,375 @@ +# -*- coding: utf-8 -*- + +# MIT License + +from base64 import b64decode +import json +from os.path import dirname, join, exists +import unittest +from urlparse import urlparse, parse_qs +from zlib import decompress + +from onelogin.saml2.authn_request import OneLogin_Saml2_Authn_Request +from onelogin.saml2.constants import OneLogin_Saml2_Constants +from onelogin.saml2.settings import OneLogin_Saml2_Settings +from onelogin.saml2.utils import OneLogin_Saml2_Utils + + +class OneLogin_Saml2_Authn_Request_Test(unittest.TestCase): + 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) + stream.close() + return settings + else: + raise Exception('Settings json file does not exist') + + def setUp(self): + self.__settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + def testCreateRequest(self): + """ + Tests the OneLogin_Saml2_Authn_Request Constructor. + The creation of a deflated SAML Request + """ + + saml_settings = self.loadSettingsJSON() + settings = OneLogin_Saml2_Settings(saml_settings) + settings._OneLogin_Saml2_Settings__organization = { + u'en-US': { + u'url': u'http://sp.example.com', + u'name': u'sp_test' + } + } + + 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, '^', 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. + The creation of a deflated SAML Request + """ + authn_request = OneLogin_Saml2_Authn_Request(self.__settings) + parameters = { + 'SAMLRequest': authn_request.get_request() + } + auth_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SSOService.php', parameters, True) + 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] + decoded = b64decode(payload) + inflated = decompress(decoded, -15) + 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 new file mode 100644 index 00000000..07edfcff --- /dev/null +++ b/tests/src/OneLogin/saml2_tests/error_test.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +# MIT License + +import unittest +from onelogin.saml2.errors import OneLogin_Saml2_Error + + +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..c88e9d03 --- /dev/null +++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py @@ -0,0 +1,646 @@ +# -*- coding: utf-8 -*- + +# 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 new file mode 100644 index 00000000..966f11b8 --- /dev/null +++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py @@ -0,0 +1,629 @@ +# -*- coding: utf-8 -*- + +# MIT License + +from base64 import b64encode +import json +from os.path import dirname, join, exists +import unittest +from urlparse import urlparse, parse_qs +from xml.dom.minidom import parseString + +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(dirname(dirname(dirname(__file__)))), 'data') + settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings') + + 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 + + def file_contents(self, filename): + f = open(filename, 'r') + content = f.read() + f.close() + return content + + def testConstructor(self): + """ + Tests the OneLogin_Saml2_LogoutRequest Constructor. + """ + settings_info = self.loadSettingsJSON() + settings_info['security']['nameIdEncrypted'] = True + settings = OneLogin_Saml2_Settings(settings_info) + + logout_request = OneLogin_Saml2_Logout_Request(settings) + + parameters = {'SAMLRequest': logout_request.get_request()} + logout_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SingleLogoutService.php', parameters, True) + 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 + """ + logout_request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml')) + id = OneLogin_Saml2_Logout_Request.get_id(logout_request) + self.assertEqual('ONELOGIN_21584ccdfaca36a145ae990442dcd96bfe60151e', id) + + dom = parseString(logout_request) + id2 = OneLogin_Saml2_Logout_Request.get_id(dom) + self.assertEqual('ONELOGIN_21584ccdfaca36a145ae990442dcd96bfe60151e', id2) + + def testGetIDFromDeflatedSAMLLogoutRequest(self): + """ + Tests the get_id method of the OneLogin_Saml2_LogoutRequest + """ + deflated_logout_request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_deflated.xml.base64')) + logout_request = OneLogin_Saml2_Utils.decode_base64_and_inflate(deflated_logout_request) + id = OneLogin_Saml2_Logout_Request.get_id(logout_request) + self.assertEqual('ONELOGIN_21584ccdfaca36a145ae990442dcd96bfe60151e', id) + + def testGetNameIdData(self): + """ + Tests the get_nameid_data method of the OneLogin_Saml2_LogoutRequest + """ + expected_name_id_data = { + 'Value': 'ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c', + 'Format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', + 'SPNameQualifier': 'http://idp.example.com/' + } + + request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml')) + name_id_data = OneLogin_Saml2_Logout_Request.get_nameid_data(request) + self.assertEqual(expected_name_id_data, name_id_data) + + dom = parseString(request) + name_id_data_2 = OneLogin_Saml2_Logout_Request.get_nameid_data(dom) + 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')) + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'Key is required in order to decrypt the NameID'): + OneLogin_Saml2_Logout_Request.get_nameid_data(request_2) + + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + key = settings.get_sp_key() + name_id_data_4 = OneLogin_Saml2_Logout_Request.get_nameid_data(request_2, key) + expected_name_id_data = { + 'Value': 'ONELOGIN_9c86c4542ab9d6fce07f2f7fd335287b9b3cdf69', + 'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress', + 'SPNameQualifier': 'https://pitbulk.no-ip.org/newonelogin/demo1/metadata.php' + } + self.assertEqual(expected_name_id_data, name_id_data_4) + + dom_2 = parseString(request_2) + encrypted_id_nodes = dom_2.getElementsByTagName('saml:EncryptedID') + encrypted_data = encrypted_id_nodes[0].firstChild.nextSibling + encrypted_id_nodes[0].removeChild(encrypted_data) + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the Logout Request'): + OneLogin_Saml2_Logout_Request.get_nameid_data(dom_2.toxml(), key) + + 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')) + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the Logout Request'): + OneLogin_Saml2_Logout_Request.get_nameid_data(inv_request) + + 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): + """ + Tests the get_nameid of the OneLogin_Saml2_LogoutRequest + """ + request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml')) + name_id = OneLogin_Saml2_Logout_Request.get_nameid(request) + self.assertEqual(name_id, 'ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c') + + request_2 = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_encrypted_nameid.xml')) + with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'Key is required in order to decrypt the NameID'): + OneLogin_Saml2_Logout_Request.get_nameid(request_2) + + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + key = settings.get_sp_key() + name_id_3 = OneLogin_Saml2_Logout_Request.get_nameid(request_2, key) + self.assertEqual('ONELOGIN_9c86c4542ab9d6fce07f2f7fd335287b9b3cdf69', name_id_3) + + def testGetIssuer(self): + """ + Tests the get_issuer of the OneLogin_Saml2_LogoutRequest + """ + request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml')) + + issuer = OneLogin_Saml2_Logout_Request.get_issuer(request) + self.assertEqual('http://idp.example.com/', issuer) + + dom = parseString(request) + issuer_2 = OneLogin_Saml2_Logout_Request.get_issuer(dom) + self.assertEqual('http://idp.example.com/', issuer_2) + + issuer_node = dom.getElementsByTagName('saml:Issuer')[0] + issuer_node.parentNode.removeChild(issuer_node) + issuer_3 = OneLogin_Saml2_Logout_Request.get_issuer(dom) + self.assertIsNone(issuer_3) + + def testGetSessionIndexes(self): + """ + Tests the get_session_indexes of the OneLogin_Saml2_LogoutRequest + """ + request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml')) + + session_indexes = OneLogin_Saml2_Logout_Request.get_session_indexes(request) + self.assertEqual(len(session_indexes), 0) + + dom = parseString(request) + session_indexes_2 = OneLogin_Saml2_Logout_Request.get_session_indexes(dom) + self.assertEqual(len(session_indexes_2), 0) + + request_2 = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_with_sessionindex.xml')) + session_indexes_3 = OneLogin_Saml2_Logout_Request.get_session_indexes(request_2) + self.assertEqual(['_ac72a76526cb6ca19f8438e73879a0e6c8ae5131'], session_indexes_3) + + def testIsInvalidXML(self): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutRequest + Case Invalid XML + """ + request = b64encode('invalid') + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html', + } + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + logout_request = OneLogin_Saml2_Logout_Request(settings, request) + + self.assertTrue(logout_request.is_valid(request_data)) + + settings.set_strict(True) + logout_request2 = OneLogin_Saml2_Logout_Request(settings, request) + self.assertFalse(logout_request2.is_valid(request_data)) + + def testIsInvalidIssuer(self): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutRequest + Case Invalid Issuer + """ + request = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'invalid_issuer.xml')) + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html' + } + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + request = request.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + self.assertTrue(logout_request.is_valid(request_data)) + + settings.set_strict(True) + 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): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutRequest + Case Invalid Destination + """ + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html' + } + request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml')) + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + self.assertTrue(logout_request.is_valid(request_data)) + + settings.set_strict(True) + 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) + logout_request3 = OneLogin_Saml2_Logout_Request(settings, b64encode(dom.toxml())) + self.assertTrue(logout_request3.is_valid(request_data)) + + dom.documentElement.removeAttribute('Destination') + logout_request4 = OneLogin_Saml2_Logout_Request(settings, b64encode(dom.toxml())) + self.assertTrue(logout_request4.is_valid(request_data)) + + def testIsValidWithCapitalization(self): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutRequest + """ + request_data = { + 'http_host': 'exaMPLe.com', + 'script_name': 'index.html' + } + request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml')) + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + self.assertTrue(logout_request.is_valid(request_data)) + + settings.set_strict(True) + logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + self.assertFalse(logout_request2.is_valid(request_data)) + + settings.set_strict(False) + dom = parseString(request) + logout_request3 = OneLogin_Saml2_Logout_Request(settings, b64encode(dom.toxml())) + self.assertTrue(logout_request3.is_valid(request_data)) + + settings.set_strict(True) + logout_request4 = OneLogin_Saml2_Logout_Request(settings, b64encode(dom.toxml())) + self.assertFalse(logout_request4.is_valid(request_data)) + + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + request = request.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url.lower()) + logout_request5 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + self.assertTrue(logout_request5.is_valid(request_data)) + + def testIsInValidWithCapitalization(self): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutRequest + """ + request_data = { + 'http_host': 'example.com', + 'script_name': 'INdex.html' + } + request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml')) + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + self.assertTrue(logout_request.is_valid(request_data)) + + settings.set_strict(True) + logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + self.assertFalse(logout_request2.is_valid(request_data)) + + settings.set_strict(False) + dom = parseString(request) + logout_request3 = OneLogin_Saml2_Logout_Request(settings, b64encode(dom.toxml())) + self.assertTrue(logout_request3.is_valid(request_data)) + + settings.set_strict(True) + logout_request4 = OneLogin_Saml2_Logout_Request(settings, b64encode(dom.toxml())) + self.assertFalse(logout_request4.is_valid(request_data)) + + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + request = request.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url.lower()) + logout_request5 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + self.assertFalse(logout_request5.is_valid(request_data)) + + def testIsInvalidNotOnOrAfter(self): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutRequest + Case Invalid NotOnOrAfter + """ + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html' + } + request = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'not_after_failed.xml')) + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + request = request.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + self.assertTrue(logout_request.is_valid(request_data)) + + settings.set_strict(True) + 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): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutRequest + """ + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html' + } + request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml')) + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + self.assertTrue(logout_request.is_valid(request_data)) + + settings.set_strict(True) + logout_request2 = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + self.assertFalse(logout_request2.is_valid(request_data)) + + settings.set_strict(False) + dom = parseString(request) + logout_request3 = OneLogin_Saml2_Logout_Request(settings, b64encode(dom.toxml())) + self.assertTrue(logout_request3.is_valid(request_data)) + + settings.set_strict(True) + logout_request4 = OneLogin_Saml2_Logout_Request(settings, b64encode(dom.toxml())) + self.assertFalse(logout_request4.is_valid(request_data)) + + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + request = request.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) + 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 + """ + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html', + 'get_data': { + 'SAMLRequest': 'lVLBitswEP0Vo7tjWbJkSyReFkIhsN1tm6WHvQTZHmdFbUmVZLqfXzlpIS10oZdhGM17b96MtkHNk5MP9myX+AW+LxBi9jZPJsjLyw4t3kirgg7SqBmCjL083n98kGSDpfM22t5O6AbyPkKFAD5qa1B22O/QSWA+EFWPjCtaM6gBugrXHCo6Ut6UgvTV2DSkBoKyr+BDQu5QIkrwEBY4mBCViamEyyrHNCf4ueSScMnIC8r2yY02Kl5QrzG6IIvC6dgt07eNsbl2G+vPhYEf1sBkz9oUA8y2LLQZ4G3jXt1dmALKHm18Mk/+fozgk5YQNMciJ+UzKWV11Wq3q3l5mcq3/9YKenYTrL3FGkihB1fMENWgoloVt8Ut0ZX1Me3xsM+On9bk86ImPep1kv+xdKuBsg/Wzyq+f6u1ood8vLTK6JUJGkxE7WnsSDcQRirOKMc97TtWCgqU1ZyJBvM+RZbSrv/l5mrg6sbJI4T1kId1ye0JhoaQgYg+XT1dnilMSZO4uko1jPSYVF0luqQjrmR/4X8X//jC7U8=', + 'RelayState': '_1037fbc88ec82ce8e770b2bed1119747bb812a07e6', + 'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', + 'Signature': 'j/qDRTzgQw3cMDkkSkBOShqxi3t9qJxYnrADqwAECnJ3Y+iYgT33C0l/Vy3+ooQkFRyObYJqg9o7iIcMdgV6CXxpa6itVIUAI2VJewsMjzvJ4OdpePeSx7+/umVPKCfMvffsELlqo/UgxsyRZh8NMLej0ojCB7bUfIMKsiU7e0c=' + } + } + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + + request = OneLogin_Saml2_Utils.decode_base64_and_inflate(request_data['get_data']['SAMLRequest']) + + settings.set_strict(False) + logout_request = OneLogin_Saml2_Logout_Request(settings, b64encode(request)) + self.assertTrue(logout_request.is_valid(request_data)) + + relayState = request_data['get_data']['RelayState'] + del request_data['get_data']['RelayState'] + self.assertFalse(logout_request.is_valid(request_data)) + request_data['get_data']['RelayState'] = relayState + + settings.set_strict(True) + 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=' + 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'] + del request_data['get_data']['SigAlg'] + self.assertTrue(logout_request3.is_valid(request_data)) + + request_data['get_data']['RelayState'] = 'http://example.com/relaystate' + 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) + 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) + 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' + 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 + settings_info['security']['wantMessagesSigned'] = True + settings = OneLogin_Saml2_Settings(settings_info) + request_data['get_data']['SigAlg'] = old_signature_algorithm + old_signature = request_data['get_data']['Signature'] + del request_data['get_data']['Signature'] + 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) + 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/ivHdiTyyZEskhkJYCPQDtmUPvQRZHm8NtqRKMuTnr2J3IbuHXsQwM887My86BDVPTj7a33aJP/FzwRCz6zyZINfKMV+8kVaFMUijZgwyavn68PQoYUek8zZabaf8DvmeUCGgj6M1eXY+HfOLILwHVQ+MK1ozrBG7itQcKzpQ3pQCdDU0DdQIefYLfUjkMU9CCQ9hwbMJUZmYUqSsCkILIG8ll8Alg/c8O6VrRqPiSn3E6OR+H+IyDDtt5z2a3tnRxHAXhSns3IfLs2cbX8yLfxgi+iQvBC2IKKB8g1JWm3x7uN0r10V8+yU/9m6HVzW7Cdchh/1900Y8J1vOp+yH9bOK3/t1y4x9MaytMnplwogm5u1l6KDrgUHFGeVEU92xUlCkrOZMNITr9LIUdvprhW3qtoKTrxhuZp5Nj9f2gn0D0IPQyfnkPlOEQpO0uko1DDSBqqtEl+aITew//m/yn2/U/gE=', + 'RelayState': '_1037fbc88ec82ce8e770b2bed1119747bb812a07e6', + 'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', + 'Signature': 'L2YrP7Ngms1ew8va4drALt9bjK4ZInIS8V6W3HUSlvW/Hw2VD93vy1jPdDBsrRt8cLIuAkkHatemiq1bbgWyrGqlbX5VA/klRYJvHVowfUh2vuf8s17bdFWUOlsTWXxKaA2lJl93MnzJQsZrfVeCqJrcTsSFlYYbcqr/g5Kdcgg=' + } + } + 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 testIsInValidRejectingDeprecatedSignatureAlgorithm(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['security']['rejectDeprecatedAlgorithm'] = True + settings = OneLogin_Saml2_Settings(settings_info) + logout_request = OneLogin_Saml2_Logout_Request(settings, request_data['get_data']['SAMLRequest']) + self.assertFalse(logout_request.is_valid(request_data)) + self.assertEqual('Deprecated signature algorithm found: http://www.w3.org/2000/09/xmldsig#rsa-sha1', logout_request.get_error()) + + 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 new file mode 100644 index 00000000..ba950be9 --- /dev/null +++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py @@ -0,0 +1,493 @@ +# -*- coding: utf-8 -*- + +# MIT License + +import json +from os.path import dirname, join, exists +import unittest +from urlparse import urlparse, parse_qs +from xml.dom.minidom import parseString + +from onelogin.saml2.constants import OneLogin_Saml2_Constants +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(dirname(dirname(dirname(__file__)))), 'data') + settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings') + + 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 + + def file_contents(self, filename): + f = open(filename, 'r') + content = f.read() + f.close() + return content + + def testConstructor(self): + """ + Tests the OneLogin_Saml2_LogoutResponse Constructor. + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) + response = OneLogin_Saml2_Logout_Response(settings, message) + self.assertRegexpMatches(response.document.toxml(), 'invalid') + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html', + 'get_data': {} + } + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + response = OneLogin_Saml2_Logout_Response(settings, message) + self.assertTrue(response.is_valid(request_data)) + + settings.set_strict(True) + response_2 = OneLogin_Saml2_Logout_Response(settings, message) + self.assertFalse(response_2.is_valid(request_data)) + + def testIsInValidRequestId(self): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutResponse + Case invalid request Id + """ + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html', + 'get_data': {} + } + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) + + plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) + message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) + + request_id = 'invalid_request_id' + + settings.set_strict(False) + response = OneLogin_Saml2_Logout_Response(settings, message) + self.assertTrue(response.is_valid(request_data, request_id)) + + settings.set_strict(True) + response_2 = OneLogin_Saml2_Logout_Response(settings, 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): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutResponse + Case invalid Issuer + """ + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html', + 'get_data': {} + } + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) + + plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) + plain_message = plain_message.replace('http://idp.example.com/', 'http://invalid.issuer.example.com') + message = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) + + settings.set_strict(False) + response = OneLogin_Saml2_Logout_Response(settings, message) + self.assertTrue(response.is_valid(request_data)) + + settings.set_strict(True) + response_2 = OneLogin_Saml2_Logout_Response(settings, message) + self.assertFalse(response_2.is_valid(request_data)) + self.assertIn('Invalid issuer in the Logout Response', response_2.get_error()) + + def testIsInValidDestination(self): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutResponse + Case invalid Destination + """ + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html', + 'get_data': {} + } + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) + + settings.set_strict(False) + response = OneLogin_Saml2_Logout_Response(settings, message) + self.assertTrue(response.is_valid(request_data)) + + settings.set_strict(True) + response_2 = OneLogin_Saml2_Logout_Response(settings, 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)) + dom.firstChild.setAttribute('Destination', '') + xml = dom.toxml() + message_3 = OneLogin_Saml2_Utils.deflate_and_base64_encode(xml) + response_3 = OneLogin_Saml2_Logout_Response(settings, message_3) + self.assertTrue(response_3.is_valid(request_data)) + + # No destination + dom.firstChild.removeAttribute('Destination') + xml = dom.toxml() + message_4 = OneLogin_Saml2_Utils.deflate_and_base64_encode(xml) + response_4 = OneLogin_Saml2_Logout_Response(settings, message_4) + self.assertTrue(response_4.is_valid(request_data)) + + def testIsValidWithCapitalization(self): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutResponse + """ + request_data = { + 'http_host': 'exaMPLe.com', + 'script_name': 'index.html', + 'get_data': {} + } + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) + + response = OneLogin_Saml2_Logout_Response(settings, message) + self.assertTrue(response.is_valid(request_data)) + + settings.set_strict(True) + response_2 = OneLogin_Saml2_Logout_Response(settings, message) + with self.assertRaisesRegexp(Exception, 'The LogoutResponse was received at'): + response_2.is_valid(request_data, raise_exceptions=True) + + plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data).lower() + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) + message_3 = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) + + response_3 = OneLogin_Saml2_Logout_Response(settings, message_3) + self.assertTrue(response_3.is_valid(request_data)) + + def testIsInValidWithCapitalization(self): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutResponse + """ + request_data = { + 'http_host': 'example.com', + 'script_name': 'INdex.html', + 'get_data': {} + } + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) + + response = OneLogin_Saml2_Logout_Response(settings, message) + self.assertTrue(response.is_valid(request_data)) + + settings.set_strict(True) + response_2 = OneLogin_Saml2_Logout_Response(settings, message) + with self.assertRaisesRegexp(Exception, 'The LogoutResponse was received at'): + response_2.is_valid(request_data, raise_exceptions=True) + + plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message) + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data).lower() + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) + message_3 = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) + + response_3 = OneLogin_Saml2_Logout_Response(settings, message_3) + self.assertFalse(response_3.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 + """ + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html', + 'get_data': {} + } + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + settings.set_strict(False) + request_data['get_data'] = { + 'SAMLResponse': 'fZJva8IwEMa/Ssl7TZrW/gnqGHMMwSlM8cXeyLU9NaxNQi9lfvxVZczB5ptwSe733MPdjQma2qmFPdjOvyE5awiDU1MbUpevCetaoyyQJmWgQVK+VOvH14WSQ6Fca70tbc1ukPsEEGHrtTUsmM8mbDfKUhnFci8gliGINI/yXIAAiYnsw6JIRgWWAKlkwRZb6skJ64V6nKjDuSEPxvdPIowHIhpIsQkTFaYqSt9ZMEPy2oC/UEfvHSnOnfZFV38MjR1oN7TtgRv8tAZre9CGV9jYkGtT4Wnoju6Bauprme/ebOyErZbPi9XLfLnDoohwhHGc5WVSVhjCKM6rBMpYQpWJrIizfZ4IZNPxuTPqYrmd/m+EdONqPOfy8yG5rhxv0EMFHs52xvxWaHyd3tqD7+j37clWGGyh7vD+POiSrdZdWSIR49NrhR9R/teGTL8A', + 'RelayState': 'https://pitbulk.no-ip.org/newonelogin/demo1/index.php', + 'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', + 'Signature': 'vfWbbc47PkP3ejx4bjKsRX7lo9Ml1WRoE5J5owF/0mnyKHfSY6XbhO1wwjBV5vWdrUVX+xp6slHyAf4YoAsXFS0qhan6txDiZY4Oec6yE+l10iZbzvie06I4GPak4QrQ4gAyXOSzwCrRmJu4gnpeUxZ6IqKtdrKfAYRAcVfNKGA=' + } + response = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) + self.assertTrue(response.is_valid(request_data)) + + relayState = request_data['get_data']['RelayState'] + del request_data['get_data']['RelayState'] + inv_response = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) + self.assertFalse(inv_response.is_valid(request_data)) + request_data['get_data']['RelayState'] = relayState + + settings.set_strict(True) + response_2 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) + 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']) + 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'] + del request_data['get_data']['SigAlg'] + response_4 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) + self.assertTrue(response_4.is_valid(request_data)) + + request_data['get_data']['RelayState'] = 'http://example.com/relaystate' + response_5 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) + 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) + plain_message_6 = OneLogin_Saml2_Utils.decode_base64_and_inflate(request_data['get_data']['SAMLResponse']) + plain_message_6 = plain_message_6.replace('https://pitbulk.no-ip.org/newonelogin/demo1/index.php?sls', current_url) + plain_message_6 = plain_message_6.replace('https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php', 'http://idp.example.com/') + 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']) + 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']) + 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']) + 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 + settings_info['security']['wantMessagesSigned'] = True + settings = OneLogin_Saml2_Settings(settings_info) + + request_data['get_data']['SigAlg'] = old_signature_algorithm + old_signature = request_data['get_data']['Signature'] + 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']) + 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' + del settings_info['idp']['x509cert'] + settings_2 = OneLogin_Saml2_Settings(settings_info) + + response_10 = OneLogin_Saml2_Logout_Response(settings_2, request_data['get_data']['SAMLResponse']) + 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): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutResponse + """ + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html', + 'get_data': {} + } + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + message = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response_deflated.xml.base64')) + + response = OneLogin_Saml2_Logout_Response(settings, message) + self.assertTrue(response.is_valid(request_data)) + + settings.set_strict(True) + response_2 = OneLogin_Saml2_Logout_Response(settings, 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) + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/sls.php', current_url) + message_3 = OneLogin_Saml2_Utils.deflate_and_base64_encode(plain_message) + + 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 testIsInValidRejectingDeprecatedSignatureAlgorithm(self): + """ + Tests the is_valid method of the OneLogin_Saml2_LogoutResponse + """ + """ + 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['security']['rejectDeprecatedAlgorithm'] = True + settings = OneLogin_Saml2_Settings(settings_info) + logout_response = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse']) + self.assertFalse(logout_response.is_valid(request_data)) + self.assertEqual('Deprecated signature algorithm found: http://www.w3.org/2000/09/xmldsig#rsa-sha1', logout_response.get_error()) + + 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 new file mode 100644 index 00000000..b578a73b --- /dev/null +++ b/tests/src/OneLogin/saml2_tests/metadata_test.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8 -*- + +# MIT License + + +import json +from os.path import dirname, join, exists +from time import strftime +from datetime import datetime +import unittest + +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): + 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 testBuilder(self): + """ + Tests the builder method of the OneLogin_Saml2_Metadata + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + 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.assertIsNotNone(metadata) + + self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', metadata) + + self.assertIn('sp_test', metadata) + self.assertIn('', metadata) + self.assertIn('technical_name', metadata) + + security['authnRequestsSigned'] = True + security['wantAssertionsSigned'] = True + del sp_data['singleLogoutService']['url'] + + metadata2 = OneLogin_Saml2_Metadata.builder( + sp_data, security['authnRequestsSigned'], + security['wantAssertionsSigned'] + ) + + self.assertIsNotNone(metadata2) + self.assertIn('', metadata2) + + metadata3 = OneLogin_Saml2_Metadata.builder( + sp_data, security['authnRequestsSigned'], + security['wantAssertionsSigned'], + '2014-10-01T11:04:29Z', + 'P1Y', + contacts, + organization + ) + self.assertIsNotNone(metadata3) + 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 + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + sp_data = settings.get_sp_data() + security = settings.get_security_data() + + metadata = OneLogin_Saml2_Metadata.builder( + sp_data, security['authnRequestsSigned'], + security['wantAssertionsSigned'] + ) + + self.assertIsNotNone(metadata) + + cert_path = settings.get_cert_path() + key = self.file_contents(join(cert_path, 'sp.key')) + 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:1.1:nameid-format:unspecified', signed_metadata) + + self.assertIn('', signed_metadata) + self.assertIn('', signed_metadata) + self.assertIn('', signed_metadata) + self.assertIn('\n', signed_metadata) + + with self.assertRaisesRegexp(Exception, 'Empty string supplied as input'): + OneLogin_Saml2_Metadata.sign_metadata('', key, cert) + + 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): + """ + Tests the addX509KeyDescriptors method of the OneLogin_Saml2_Metadata + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + sp_data = settings.get_sp_data() + + metadata = OneLogin_Saml2_Metadata.builder(sp_data) + self.assertNotIn(' something_is_wrong'): + response_3.check_status() + + 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): + """ + Tests the get_audiences method of the OneLogin_Saml2_Response + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + xml = self.file_contents(join(self.data_path, 'responses', 'no_audience.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertEqual([], response.get_audiences()) + + xml_2 = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + response_2 = OneLogin_Saml2_Response(settings, xml_2) + self.assertEqual(['{audience}'], response_2.get_audiences()) + + 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(['http://stuff.com/endpoints/metadata.php'], response_3.get_audiences()) + + def testQueryAssertions(self): + """ + Tests the __query_assertion and __query methods of the + OneLogin_Saml2_Response using the get_issuers call + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + xml = self.file_contents(join(self.data_path, 'responses', 'adfs_response.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + 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) + self.assertEqual(['http://idp.example.com/'], response_2.get_issuers()) + + xml_3 = self.file_contents(join(self.data_path, 'responses', 'double_signed_encrypted_assertion.xml.base64')) + 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', 'double_signed_response.xml.base64')) + response_4 = OneLogin_Saml2_Response(settings, xml_4) + 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', 'signed_message_encrypted_assertion.xml.base64')) + response_5 = OneLogin_Saml2_Response(settings, xml_5) + self.assertEqual(['http://idp.example.com/', 'https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'], response_5.get_issuers()) + + xml_6 = self.file_contents(join(self.data_path, 'responses', 'signed_assertion_response.xml.base64')) + response_6 = OneLogin_Saml2_Response(settings, xml_6) + self.assertEqual(['https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'], response_6.get_issuers()) + + xml_7 = self.file_contents(join(self.data_path, 'responses', 'signed_encrypted_assertion.xml.base64')) + response_7 = OneLogin_Saml2_Response(settings, xml_7) + self.assertEqual(['http://idp.example.com/'], response_7.get_issuers()) + + 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', 'adfs_response.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + 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) + self.assertEqual(['http://idp.example.com/'], response_2.get_issuers()) + + xml_3 = self.file_contents(join(self.data_path, 'responses', 'double_signed_encrypted_assertion.xml.base64')) + 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 + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertEqual('_531c32d283bdff7e04e487bcdbc4dd8d', response.get_session_index()) + + xml_2 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64')) + response_2 = OneLogin_Saml2_Response(settings, xml_2) + self.assertEqual('_7164a9a9f97828bfdb8d0ebc004a05d2e7d873f70c', response_2.get_session_index()) + + def testGetAttributes(self): + """ + Tests the get_attributes method of the OneLogin_Saml2_Response + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + expected_attributes = { + 'uid': ['demo'], + 'another_value': ['value'] + } + self.assertEqual(expected_attributes, response.get_attributes()) + + # An assertion that has no attributes should return an empty + # array when asked for the attributes + xml_2 = self.file_contents(join(self.data_path, 'responses', 'response2.xml.base64')) + response_2 = OneLogin_Saml2_Response(settings, xml_2) + self.assertEqual({}, response_2.get_attributes()) + + # Encrypted Attributes are not supported + xml_3 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'encrypted_attrs.xml.base64')) + response_3 = OneLogin_Saml2_Response(settings, xml_3) + self.assertEqual({}, response_3.get_attributes()) + + def testGetFriendlyAttributes(self): + """ + Tests the get_friendlyname_attributes method of the OneLogin_Saml2_Response + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertEqual({}, response.get_friendlyname_attributes()) + + expected_attributes = { + 'username': ['demo'] + } + xml_2 = self.file_contents(join(self.data_path, 'responses', 'response1_with_friendlyname.xml.base64')) + response_2 = OneLogin_Saml2_Response(settings, xml_2) + self.assertEqual(expected_attributes, response_2.get_friendlyname_attributes()) + + xml_3 = self.file_contents(join(self.data_path, 'responses', 'response2.xml.base64')) + response_3 = OneLogin_Saml2_Response(settings, xml_3) + self.assertEqual({}, response_3.get_friendlyname_attributes()) + + xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'encrypted_attrs.xml.base64')) + response_4 = OneLogin_Saml2_Response(settings, xml_4) + self.assertEqual({}, response_4.get_friendlyname_attributes()) + + def testGetNestedNameIDAttributes(self): + """ + Tests the get_attributes 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 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) + 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): + """ + Tests the get_nameid method of the OneLogin_Saml2_Response + Test that the SignatureWrappingAttack is not allowed + """ + 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.assertFalse(response.is_valid(self.get_request_data())) + self.assertEqual('test@onelogin.com', response.get_nameid()) + + def testDoesNotAllowSignatureWrappingAttack2(self): + # Signature Wraping attack 2 + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + settings.set_strict(False) + xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'signature_wrapping_attack2.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertFalse(response.is_valid(self.get_request_data())) + self.assertEquals("SAML Response must contain 1 assertion", response.get_error()) + + 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): + """ + Tests the get_session_not_on_or_after method of the OneLogin_Saml2_Response + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertEqual(1290203857, response.get_session_not_on_or_after()) + + # An assertion that do not specified Session timeout should return NULL + xml_2 = self.file_contents(join(self.data_path, 'responses', 'response2.xml.base64')) + response_2 = OneLogin_Saml2_Response(settings, xml_2) + self.assertEqual(None, response_2.get_session_not_on_or_after()) + + 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(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 + Case Invalid XML + """ + message = b64encode('invalid') + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html', + 'get_data': {} + } + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + response = OneLogin_Saml2_Response(settings, message) + response.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) + + settings.set_strict(True) + response_2 = OneLogin_Saml2_Response(settings, message) + self.assertFalse(response_2.is_valid(request_data)) + self.assertEqual('Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd', response_2.get_error()) + + def testValidateNumAssertions(self): + """ + Tests the validate_num_assertions method of the OneLogin_Saml2_Response + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertTrue(response.validate_num_assertions()) + + xml_multi_assertion = self.file_contents(join(self.data_path, 'responses', 'invalids', 'multiple_assertions.xml.base64')) + response_2 = OneLogin_Saml2_Response(settings, xml_multi_assertion) + self.assertFalse(response_2.validate_num_assertions()) + + def testValidateTimestamps(self): + """ + Tests the validate_timestamps method of the OneLogin_Saml2_Response + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + xml = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertTrue(response.validate_timestamps()) + + xml_2 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64')) + response_2 = OneLogin_Saml2_Response(settings, xml_2) + self.assertTrue(response_2.validate_timestamps()) + + xml_3 = self.file_contents(join(self.data_path, 'responses', 'expired_response.xml.base64')) + response_3 = OneLogin_Saml2_Response(settings, xml_3) + self.assertFalse(response_3.validate_timestamps()) + + xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'not_after_failed.xml.base64')) + response_4 = OneLogin_Saml2_Response(settings, xml_4) + self.assertFalse(response_4.validate_timestamps()) + + xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'not_before_failed.xml.base64')) + response_5 = OneLogin_Saml2_Response(settings, xml_5) + self.assertFalse(response_5.validate_timestamps()) + + def testValidateVersion(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response + Case invalid version + """ + 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) + self.assertFalse(response.is_valid(self.get_request_data())) + self.assertEqual('Unsupported SAML version', response.get_error()) + + def testValidateID(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response + Case invalid no ID + """ + 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) + self.assertFalse(response.is_valid(self.get_request_data())) + self.assertEqual('Missing ID attribute on SAML Response', response.get_error()) + + def testIsInValidReference(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response + Case invalid reference + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertFalse(response.is_valid(self.get_request_data())) + self.assertEqual('Signature validation failed. SAML Response rejected', response.get_error()) + + def testIsInValidExpired(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response + Case expired response + """ + 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) + 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) + 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): + """ + Tests the is_valid method of the OneLogin_Saml2_Response + Case no statement + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_signature.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + 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) + 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 + """ + json_settings = self.loadSettingsJSON() + 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' + + 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')) + + response = OneLogin_Saml2_Response(settings, xml) + 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) + + # check response + self.assertFalse(response.is_valid({ + 'https': 'on', + 'http_host': 'pitbulk.no-ip.org', + 'script_name': 'newonelogin/demo1/index.php?acs' + })) + self.assertNotEqual('There is no AttributeStatement on the Response', response.get_error()) + self.assertEqual('Signature validation failed. SAML Response rejected', response.get_error()) + + def testIsInValidNoKey(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response + Case no key + """ + 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) + self.assertFalse(response.is_valid(self.get_request_data())) + self.assertEqual('Signature validation failed. SAML Response rejected', response.get_error()) + + def testIsInValidDeprecatedAlgorithm(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response + Case Deprecated algorithm used + """ + settings_dict = self.loadSettingsJSON() + settings_dict['security']['rejectDeprecatedAlgorithm'] = True + settings = OneLogin_Saml2_Settings(settings_dict) + xml = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertFalse(response.is_valid(self.get_request_data())) + self.assertEqual('Deprecated signature algorithm found: http://www.w3.org/2000/09/xmldsig#rsa-sha1', response.get_error()) + + def testIsInValidMultipleAssertions(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response + Case invalid multiple assertions + """ + + 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) + self.assertFalse(response.is_valid(self.get_request_data())) + self.assertEqual('SAML Response must contain 1 assertion', response.get_error()) + + def testIsInValidEncAttrs(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response + Case invalid Encrypted Attrs + """ + 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) + 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) + 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): + """ + Tests the is_valid method of the OneLogin_Saml2_Response class + Case Invalid Response, Invalid Destination + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + message = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) + response = OneLogin_Saml2_Response(settings, message) + 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, message) + 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('The response has an empty Destination value', response_3.get_error()) + + 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.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()) + + settings.set_strict(True) + response_2 = OneLogin_Saml2_Response(settings, message) + self.assertFalse(response_2.is_valid(self.get_request_data())) + self.assertIn('The response was received at', response_2.get_error()) + + def testIsInValidDestinationCapitalizationOfElements(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response class + Case Invalid Response due to differences in capitalization of path + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + message = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) + + # Test path capitalized + settings.set_strict(True) + response = OneLogin_Saml2_Response(settings, message) + self.assertFalse(response.is_valid(self.get_request_data_path_capitalized())) + self.assertIn('The response was received at', response.get_error()) + + # Test both domain and path capitalized + response_2 = OneLogin_Saml2_Response(settings, message) + self.assertFalse(response_2.is_valid(self.get_request_data_both_capitalized())) + self.assertIn('The response was received at', response_2.get_error()) + + def testIsValidDestinationCapitalizationOfHost(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response class + Case Valid Response, even if host is differently capitalized (per RFC) + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + message = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) + # Test domain capitalized + settings.set_strict(True) + response = OneLogin_Saml2_Response(settings, message) + self.assertFalse(response.is_valid(self.get_request_data_domain_capitalized())) + self.assertNotIn('The response was received at', response.get_error()) + + # Assert we got past the destination check, which appears later + self.assertIn('A valid SubjectConfirmation was not found', response.get_error()) + + def testIsInValidAudience(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response class + Case Invalid Response, Invalid Audience + """ + request_data = { + 'http_host': 'stuff.com', + 'script_name': '/endpoints/endpoints/acs.php', + } + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + message = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_audience.xml.base64')) + + response = OneLogin_Saml2_Response(settings, message) + response.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) + + settings.set_strict(True) + response_2 = OneLogin_Saml2_Response(settings, message) + + 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" was not a 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 + Case Invalid Response, Invalid Issuer + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html' + } + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_issuer_assertion.xml.base64')) + plain_message = b64decode(xml) + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) + message = b64encode(plain_message) + + xml_2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_issuer_message.xml.base64')) + plain_message_2 = b64decode(xml_2) + plain_message_2 = plain_message_2.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) + message_2 = b64encode(plain_message_2) + + response = OneLogin_Saml2_Response(settings, message) + response.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) + + response_2 = OneLogin_Saml2_Response(settings, message_2) + response_2.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_2.get_error()) + + settings.set_strict(True) + response_3 = OneLogin_Saml2_Response(settings, 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) + 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): + """ + Tests the is_valid method of the OneLogin_Saml2_Response class + Case Invalid Response, Invalid SessionIndex + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html' + } + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_sessionindex.xml.base64')) + plain_message = b64decode(xml) + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) + message = b64encode(plain_message) + + response = OneLogin_Saml2_Response(settings, message) + response.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) + + settings.set_strict(True) + response_2 = OneLogin_Saml2_Response(settings, 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): + """ + Tests the is_valid method of the OneLogin_Saml2_Response class + Somtimes IdPs uses datetimes with miliseconds, this + test is to verify that the toolkit supports them + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html' + } + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + + xml = self.file_contents(join(self.data_path, 'responses', 'unsigned_response_with_miliseconds.xm.base64')) + plain_message = b64decode(xml) + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) + message = b64encode(plain_message) + response = OneLogin_Saml2_Response(settings, message) + response.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) + + def testIsInValidSubjectConfirmation(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response class + Case Invalid Response, Invalid SubjectConfirmation + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html' + } + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_subjectconfirmation_method.xml.base64')) + plain_message = b64decode(xml) + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) + message = b64encode(plain_message) + + xml_2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_subjectconfirmation_data.xml.base64')) + plain_message_2 = b64decode(xml_2) + plain_message_2 = plain_message_2.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) + message_2 = b64encode(plain_message_2) + + xml_3 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_subjectconfirmation_inresponse.xml.base64')) + plain_message_3 = b64decode(xml_3) + plain_message_3 = plain_message_3.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) + message_3 = b64encode(plain_message_3) + + xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_subjectconfirmation_recipient.xml.base64')) + plain_message_4 = b64decode(xml_4) + plain_message_4 = plain_message_4.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) + message_4 = b64encode(plain_message_4) + + xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_subjectconfirmation_noa.xml.base64')) + plain_message_5 = b64decode(xml_5) + plain_message_5 = plain_message_5.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) + message_5 = b64encode(plain_message_5) + + xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'invalid_subjectconfirmation_nb.xml.base64')) + plain_message_6 = b64decode(xml_6) + plain_message_6 = plain_message_6.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) + message_6 = b64encode(plain_message_6) + + response = OneLogin_Saml2_Response(settings, message) + response.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) + + response_2 = OneLogin_Saml2_Response(settings, message_2) + response_2.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_2.get_error()) + + response_3 = OneLogin_Saml2_Response(settings, message_3) + response_3.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_3.get_error()) + + response_4 = OneLogin_Saml2_Response(settings, message_4) + response_4.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_4.get_error()) + + response_5 = OneLogin_Saml2_Response(settings, message_5) + response_5.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_5.get_error()) + + response_6 = OneLogin_Saml2_Response(settings, message_6) + response_6.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_6.get_error()) + + settings.set_strict(True) + response = OneLogin_Saml2_Response(settings, 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) + 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) + 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) + 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) + 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) + 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): + """ + Tests the is_valid method of the OneLogin_Saml2_Response class + Case Invalid Response, Invalid requestID + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html' + } + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + xml = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) + plain_message = b64decode(xml) + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) + message = b64encode(plain_message) + + response = OneLogin_Saml2_Response(settings, message) + request_id = 'invalid' + response.is_valid(request_data, request_id) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) + + settings.set_strict(True) + response = OneLogin_Saml2_Response(settings, 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 + Case Invalid Response, Invalid signing issues + """ + settings_info = self.loadSettingsJSON() + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html' + } + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + xml = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) + plain_message = b64decode(xml) + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) + message = b64encode(plain_message) + + settings_info['security']['wantAssertionsSigned'] = False + settings = OneLogin_Saml2_Settings(settings_info) + response = OneLogin_Saml2_Response(settings, message) + response.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) + + settings_info['security']['wantAssertionsSigned'] = True + settings_2 = OneLogin_Saml2_Settings(settings_info) + response_2 = OneLogin_Saml2_Response(settings_2, message) + response_2.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_2.get_error()) + + settings_info['strict'] = True + settings_info['security']['wantAssertionsSigned'] = False + settings_3 = OneLogin_Saml2_Settings(settings_info) + response_3 = OneLogin_Saml2_Response(settings_3, message) + response_3.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_3.get_error()) + + settings_info['security']['wantAssertionsSigned'] = True + settings_4 = OneLogin_Saml2_Settings(settings_info) + response_4 = OneLogin_Saml2_Response(settings_4, 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 + + settings_info['security']['wantMessagesSigned'] = False + settings_5 = OneLogin_Saml2_Settings(settings_info) + response_5 = OneLogin_Saml2_Response(settings_5, message) + response_5.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_5.get_error()) + + settings_info['security']['wantMessagesSigned'] = True + settings_6 = OneLogin_Saml2_Settings(settings_info) + response_6 = OneLogin_Saml2_Response(settings_6, message) + response_6.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_6.get_error()) + + settings_info['strict'] = True + settings_info['security']['wantMessagesSigned'] = False + settings_7 = OneLogin_Saml2_Settings(settings_info) + response_7 = OneLogin_Saml2_Response(settings_7, message) + 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) + 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): + """ + Tests the is_valid method of the OneLogin_Saml2_Response class + Case Invalid Response, Invalid encryptation issues + """ + settings_info = self.loadSettingsJSON() + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html' + } + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + xml = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64')) + plain_message = b64decode(xml) + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) + message = b64encode(plain_message) + + settings_info['security']['wantAssertionsEncrypted'] = True + settings = OneLogin_Saml2_Settings(settings_info) + response = OneLogin_Saml2_Response(settings, message) + response.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) + + settings_info['strict'] = True + settings_info['security']['wantAssertionsEncrypted'] = False + settings = OneLogin_Saml2_Settings(settings_info) + response_2 = OneLogin_Saml2_Response(settings, message) + response_2.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_2.get_error()) + + settings_info['security']['wantAssertionsEncrypted'] = True + settings = OneLogin_Saml2_Settings(settings_info) + response_3 = OneLogin_Saml2_Response(settings, message) + + self.assertFalse(response_3.is_valid(request_data)) + self.assertEqual('The assertion of the Response is not encrypted and the SP require it', response_3.get_error()) + + settings_info['security']['wantAssertionsEncrypted'] = False + settings_info['security']['wantNameIdEncrypted'] = True + settings_info['strict'] = False + settings = OneLogin_Saml2_Settings(settings_info) + response_4 = OneLogin_Saml2_Response(settings, message) + response_4.is_valid(request_data) + self.assertEqual('No Signature found. SAML Response rejected', response_4.get_error()) + + settings_info['strict'] = True + settings = OneLogin_Saml2_Settings(settings_info) + response_5 = OneLogin_Saml2_Response(settings, message) + + self.assertFalse(response_5.is_valid(request_data)) + self.assertEqual('The NameID of the Response is not encrypted and the SP require it', response_5.get_error()) + + settings_info_2 = self.loadSettingsJSON('settings3.json') + settings_info_2['strict'] = True + settings_info_2['security']['wantNameIdEncrypted'] = True + settings_2 = OneLogin_Saml2_Settings(settings_info_2) + + request_data = { + 'http_host': 'pytoolkit.com', + 'server_port': 8000, + 'script_name': '', + 'request_uri': '?acs', + } + + message_2 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion_encrypted_nameid.xml.base64')) + response_6 = OneLogin_Saml2_Response(settings_2, message_2) + self.assertFalse(response_6.is_valid(request_data)) + self.assertEqual('The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response', response_6.get_error()) + + def testIsInValidCert(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response + 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) + self.assertFalse(response.is_valid(self.get_request_data())) + self.assertIn('Signature validation failed. SAML Response rejected', response.get_error()) + + def testIsInValidCert2(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response + Case invalid cert2 + """ + settings_info = self.loadSettingsJSON() + settings_info['idp']['x509cert'] = 'MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=' + 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) + self.assertFalse(response.is_valid(self.get_request_data())) + + def testIsValid(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response + Case valid unsigned response + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + xml = self.file_contents(join(self.data_path, 'responses', 'valid_unsigned_response.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + response.is_valid(self.get_request_data()) + self.assertEqual('No Signature found. SAML Response rejected', response.get_error()) + + def testIsValid2(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response + Case valid response2 + """ + settings_info = self.loadSettingsJSON() + settings = OneLogin_Saml2_Settings(settings_info) + + # expired cert + xml = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + + self.assertTrue(response.is_valid(self.get_request_data())) + + settings_info_2 = self.loadSettingsJSON('settings2.json') + settings_2 = OneLogin_Saml2_Settings(settings_info_2) + xml_2 = self.file_contents(join(self.data_path, 'responses', 'valid_response2.xml.base64')) + response_2 = OneLogin_Saml2_Response(settings_2, xml_2) + self.assertTrue(response_2.is_valid(self.get_request_data())) + + settings_info_3 = self.loadSettingsJSON('settings2.json') + 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) + response_3 = OneLogin_Saml2_Response(settings_3, xml_2) + self.assertTrue(response_3.is_valid(self.get_request_data())) + + settings_info_3['idp']['certFingerprintAlgorithm'] = 'sha1' + settings_4 = OneLogin_Saml2_Settings(settings_info_3) + response_4 = OneLogin_Saml2_Response(settings_4, xml_2) + self.assertTrue(response_4.is_valid(self.get_request_data())) + + settings_info_3['idp']['certFingerprintAlgorithm'] = 'sha256' + settings_5 = OneLogin_Saml2_Settings(settings_info_3) + response_5 = OneLogin_Saml2_Response(settings_5, xml_2) + self.assertFalse(response_5.is_valid(self.get_request_data())) + + settings_info_3['idp']['certFingerprint'] = OneLogin_Saml2_Utils.calculate_x509_fingerprint(idp_cert, 'sha256') + settings_6 = OneLogin_Saml2_Settings(settings_info_3) + response_6 = OneLogin_Saml2_Response(settings_6, xml_2) + self.assertTrue(response_6.is_valid(self.get_request_data())) + + def testIsValidEnc(self): + """ + Tests the is_valid method of the OneLogin_Saml2_Response + Case valid encrypted assertion + + Signed data can't be modified, so Destination will always fail in + strict mode + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + # expired cert + xml = self.file_contents(join(self.data_path, 'responses', 'double_signed_encrypted_assertion.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertTrue(response.is_valid(self.get_request_data())) + + xml_2 = self.file_contents(join(self.data_path, 'responses', 'signed_encrypted_assertion.xml.base64')) + response_2 = OneLogin_Saml2_Response(settings, xml_2) + self.assertTrue(response_2.is_valid(self.get_request_data())) + + xml_3 = self.file_contents(join(self.data_path, 'responses', 'signed_message_encrypted_assertion.xml.base64')) + response_3 = OneLogin_Saml2_Response(settings, xml_3) + self.assertTrue(response_3.is_valid(self.get_request_data())) + + settings_2 = OneLogin_Saml2_Settings(self.loadSettingsJSON('settings2.json')) + xml_4 = self.file_contents(join(self.data_path, 'responses', 'double_signed_encrypted_assertion2.xml.base64')) + response_4 = OneLogin_Saml2_Response(settings_2, xml_4) + self.assertTrue(response_4.is_valid(self.get_request_data())) + + xml_5 = self.file_contents(join(self.data_path, 'responses', 'signed_encrypted_assertion2.xml.base64')) + response_5 = OneLogin_Saml2_Response(settings_2, xml_5) + self.assertTrue(response_5.is_valid(self.get_request_data())) + + xml_6 = self.file_contents(join(self.data_path, 'responses', 'signed_message_encrypted_assertion2.xml.base64')) + response_6 = OneLogin_Saml2_Response(settings_2, xml_6) + self.assertTrue(response_6.is_valid(self.get_request_data())) + + settings.set_strict(True) + xml_7 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64')) + # In order to avoid the destination problem + plain_message = b64decode(xml_7) + request_data = { + 'http_host': 'example.com', + 'script_name': 'index.html' + } + current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data) + plain_message = plain_message.replace('http://stuff.com/endpoints/endpoints/acs.php', current_url) + message = b64encode(plain_message) + response_7 = OneLogin_Saml2_Response(settings, message) + 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 + Case valid sign response / sign assertion / both signed + + Strict mode will always fail due destination problem, if we manipulate + it the sign will fail. + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + # expired cert + 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())) + + xml_2 = self.file_contents(join(self.data_path, 'responses', 'signed_assertion_response.xml.base64')) + response_2 = OneLogin_Saml2_Response(settings, xml_2) + self.assertTrue(response_2.is_valid(self.get_request_data())) + + xml_3 = self.file_contents(join(self.data_path, 'responses', 'double_signed_response.xml.base64')) + response_3 = OneLogin_Saml2_Response(settings, xml_3) + self.assertTrue(response_3.is_valid(self.get_request_data())) + + settings_2 = OneLogin_Saml2_Settings(self.loadSettingsJSON('settings2.json')) + xml_4 = self.file_contents(join(self.data_path, 'responses', 'signed_message_response2.xml.base64')) + response_4 = OneLogin_Saml2_Response(settings_2, xml_4) + self.assertTrue(response_4.is_valid(self.get_request_data())) + + xml_5 = self.file_contents(join(self.data_path, 'responses', 'signed_assertion_response2.xml.base64')) + response_5 = OneLogin_Saml2_Response(settings_2, xml_5) + self.assertTrue(response_5.is_valid(self.get_request_data())) + + xml_6 = self.file_contents(join(self.data_path, 'responses', 'double_signed_response2.xml.base64')) + response_6 = OneLogin_Saml2_Response(settings_2, xml_6) + self.assertTrue(response_6.is_valid(self.get_request_data())) + + dom = parseString(b64decode(xml_4)) + dom.firstChild.firstChild.firstChild.nodeValue = 'https://example.com/other-idp' + xml_7 = b64encode(dom.toxml()) + response_7 = OneLogin_Saml2_Response(settings, xml_7) + # Modified message + self.assertFalse(response_7.is_valid(self.get_request_data())) + + dom_2 = parseString(b64decode(xml_5)) + dom_2.firstChild.firstChild.firstChild.nodeValue = 'https://example.com/other-idp' + xml_8 = b64encode(dom_2.toxml()) + response_8 = OneLogin_Saml2_Response(settings, xml_8) + # Modified message + self.assertFalse(response_8.is_valid(self.get_request_data())) + + dom_3 = parseString(b64decode(xml_6)) + dom_3.firstChild.firstChild.firstChild.nodeValue = 'https://example.com/other-idp' + xml_9 = b64encode(dom_3.toxml()) + response_9 = OneLogin_Saml2_Response(settings, xml_9) + # 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 testMessageSignedIsValidSignWithEmptyReferenceURI(self): + settings_info = self.loadSettingsJSON() + del settings_info['idp']['x509cert'] + settings_info['idp']['certFingerprint'] = "657302a5e11a4794a1e50a705988d66c9377575d" + 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())) + + def testAssertionSignedIsValidSignWithEmptyReferenceURI(self): + settings_info = self.loadSettingsJSON() + del settings_info['idp']['x509cert'] + settings_info['idp']['certFingerprint'] = "657302a5e11a4794a1e50a705988d66c9377575d" + settings = OneLogin_Saml2_Settings(settings_info) + xml = self.file_contents(join(self.data_path, 'responses', 'response_without_assertion_reference_uri.xml.base64')) + response = OneLogin_Saml2_Response(settings, xml) + self.assertTrue(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(), 'pfxf209cd60-f060-722b-02e9-4850ac5a2e41') + + 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 new file mode 100644 index 00000000..4da3363e --- /dev/null +++ b/tests/src/OneLogin/saml2_tests/settings_test.py @@ -0,0 +1,821 @@ +# -*- coding: utf-8 -*- + +# MIT License + +import json +from os.path import dirname, join, exists, sep +import unittest + +from onelogin.saml2.errors import OneLogin_Saml2_Error +from onelogin.saml2.settings import OneLogin_Saml2_Settings +from onelogin.saml2.utils import OneLogin_Saml2_Utils + + +class OneLogin_Saml2_Settings_Test(unittest.TestCase): + data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data') + settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings') + + 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') + content = f.read() + f.close() + return content + + def testLoadSettingsFromDict(self): + """ + Tests the OneLogin_Saml2_Settings Constructor. + Case load setting from dict + """ + settings_info = self.loadSettingsJSON() + settings = OneLogin_Saml2_Settings(settings_info) + self.assertEqual(len(settings.get_errors()), 0) + + del settings_info['contactPerson'] + del settings_info['organization'] + settings = OneLogin_Saml2_Settings(settings_info) + self.assertEqual(len(settings.get_errors()), 0) + + del settings_info['security'] + settings = OneLogin_Saml2_Settings(settings_info) + self.assertEqual(len(settings.get_errors()), 0) + + del settings_info['sp']['NameIDFormat'] + del settings_info['idp']['x509cert'] + settings_info['idp']['certFingerprint'] = 'afe71c28ef740bc87425be13a2263d37971daA1f9' + settings = OneLogin_Saml2_Settings(settings_info) + self.assertEqual(len(settings.get_errors()), 0) + + settings_info['idp']['singleSignOnService']['url'] = 'invalid_url' + with self.assertRaisesRegexp(Exception, 'Invalid dict settings: idp_sso_url_invalid'): + OneLogin_Saml2_Settings(settings_info) + + settings_info['idp']['singleSignOnService']['url'] = 'http://invalid_domain' + with self.assertRaisesRegexp(Exception, 'Invalid dict settings: idp_sso_url_invalid'): + OneLogin_Saml2_Settings(settings_info) + + settings_info['idp']['singleSignOnService']['url'] = 'http://single-label-domain' + settings_info['security'] = {} + settings_info['security']['allowSingleLabelDomains'] = True + settings = OneLogin_Saml2_Settings(settings_info) + self.assertEqual(len(settings.get_errors()), 0) + + del settings_info['security'] + del settings_info['sp'] + del settings_info['idp'] + 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__) + 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'] + with self.assertRaisesRegexp(Exception, 'Invalid dict settings: idp_cert_not_found_and_required'): + OneLogin_Saml2_Settings(settings_info) + + def testLoadSettingsFromInvalidData(self): + """ + Tests the OneLogin_Saml2_Settings Constructor. + Case load setting + """ + invalid_settings = ('param1', 'param2') + + with self.assertRaisesRegexp(Exception, 'Unsupported settings object'): + settings = OneLogin_Saml2_Settings(invalid_settings) + + settings = OneLogin_Saml2_Settings(custom_base_path=self.settings_path) + self.assertEqual(len(settings.get_errors()), 0) + + def testLoadSettingsFromFile(self): + """ + Tests the OneLogin_Saml2_Settings Constructor. + Case load setting from file + """ + 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__) + with self.assertRaisesRegexp(Exception, 'Settings file not found'): + OneLogin_Saml2_Settings(custom_base_path=custom_base_path) + + 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) + + def testGetCertPath(self): + """ + Tests getCertPath method of the OneLogin_Saml2_Settings + """ + settings = OneLogin_Saml2_Settings(custom_base_path=self.settings_path) + self.assertEqual(self.settings_path + sep + 'certs' + sep, settings.get_cert_path()) + + def testSetCertPath(self): + """ + Tests setCertPath method of the OneLogin_Saml2_Settings + """ + settings = OneLogin_Saml2_Settings(custom_base_path=self.settings_path) + self.assertEqual(self.settings_path + sep + 'certs' + sep, settings.get_cert_path()) + + settings.set_cert_path('/tmp') + self.assertEqual('/tmp', settings.get_cert_path()) + + def testGetLibPath(self): + """ + Tests getLibPath method of the OneLogin_Saml2_Settings + """ + settingsInfo = self.loadSettingsJSON() + settings = OneLogin_Saml2_Settings(settingsInfo) + path = settings.get_base_path() + self.assertEqual(settings.get_lib_path(), join(dirname(dirname(dirname(dirname(dirname(__file__))))), 'src/onelogin/saml2/')) + self.assertEqual(path, join(dirname(dirname(dirname(dirname(dirname(__file__))))), 'src/onelogin/saml2/../../../tests/data/customPath/')) + + del settingsInfo['custom_base_path'] + settings = OneLogin_Saml2_Settings(settingsInfo) + path = settings.get_base_path() + self.assertEqual(settings.get_lib_path(), join(dirname(dirname(dirname(dirname(dirname(__file__))))), 'src/onelogin/saml2/')) + self.assertEqual(path, join(dirname(dirname(dirname(dirname(dirname(__file__))))), 'src/')) + + settings = OneLogin_Saml2_Settings(custom_base_path=self.settings_path) + path = settings.get_base_path() + self.assertEqual(settings.get_lib_path(), join(dirname(dirname(dirname(dirname(dirname(__file__))))), 'src/onelogin/saml2/')) + self.assertEqual(path, join(dirname(dirname(dirname(dirname(__file__)))), 'settings/')) + + def testGetSchemasPath(self): + """ + Tests getSchemasPath method of the OneLogin_Saml2_Settings + """ + settingsInfo = self.loadSettingsJSON() + settings = OneLogin_Saml2_Settings(settingsInfo) + path = settings.get_base_path() + self.assertEqual(settings.get_schemas_path(), join(dirname(dirname(dirname(dirname(dirname(__file__))))), 'src/onelogin/saml2/schemas/')) + self.assertEqual(path, join(dirname(dirname(dirname(dirname(dirname(__file__))))), 'src/onelogin/saml2/../../../tests/data/customPath/')) + + del settingsInfo['custom_base_path'] + settings = OneLogin_Saml2_Settings(settingsInfo) + path = settings.get_base_path() + self.assertEqual(settings.get_schemas_path(), join(dirname(dirname(dirname(dirname(dirname(__file__))))), 'src/onelogin/saml2/schemas/')) + self.assertEqual(path, join(dirname(dirname(dirname(dirname(dirname(__file__))))), 'src/')) + + settings = OneLogin_Saml2_Settings(custom_base_path=self.settings_path) + path = settings.get_base_path() + self.assertEqual(settings.get_schemas_path(), join(dirname(dirname(dirname(dirname(dirname(__file__))))), 'src/onelogin/saml2/schemas/')) + self.assertEqual(path, join(dirname(dirname(dirname(dirname(__file__)))), 'settings/')) + + def testGetIdPSSOurl(self): + """ + Tests the get_idp_sso_url method of the OneLogin_Saml2_Settings class + """ + settings_info = self.loadSettingsJSON() + settings = OneLogin_Saml2_Settings(settings_info) + + sso_url = settings_info['idp']['singleSignOnService']['url'] + self.assertEqual(settings.get_idp_sso_url(), sso_url) + + def testGetIdPSLOurl(self): + """ + Tests the get_idp_slo_url method of the OneLogin_Saml2_Settings class + """ + settings_info = self.loadSettingsJSON() + settings = OneLogin_Saml2_Settings(settings_info) + + slo_url = settings_info['idp']['singleLogoutService']['url'] + self.assertEqual(settings.get_idp_slo_url(), slo_url) + + def testGetIdPSLOresponseUrl(self): + """ + Tests the get_idp_slo_response_url method of the OneLogin_Saml2_Settings class + """ + settings_info = self.loadSettingsJSON() + settings_info['idp']['singleLogoutService']['responseUrl'] = "http://idp.example.com/SingleLogoutReturn.php" + settings = OneLogin_Saml2_Settings(settings_info) + slo_url = settings_info['idp']['singleLogoutService']['responseUrl'] + self.assertEqual(settings.get_idp_slo_response_url(), slo_url) + # test that the function falls back to the url setting if responseUrl is not set + settings_info['idp']['singleLogoutService'].pop('responseUrl') + settings = OneLogin_Saml2_Settings(settings_info) + slo_url = settings_info['idp']['singleLogoutService']['url'] + self.assertEqual(settings.get_idp_slo_response_url(), slo_url) + + def testGetSPCert(self): + """ + Tests the get_sp_cert 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()) + + cert_2 = "-----BEGIN CERTIFICATE-----\nMIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czET\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYD\nVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIy\nNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UE\nCgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJ\nKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BF\nrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PP\nJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHC\npS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAf\nBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0G\nCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZ\nE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kY\nvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m\n-----END CERTIFICATE-----\n" + settings_data['sp']['x509cert'] = cert_2 + settings = OneLogin_Saml2_Settings(settings_data) + self.assertEqual(cert_2, settings.get_sp_cert()) + + del settings_data['sp']['x509cert'] + del settings_data['custom_base_path'] + custom_base_path = dirname(__file__) + + 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 + """ + settings_data = self.loadSettingsJSON() + key = "-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9\nIbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+\nPbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQAB\nAoGAD4/Z4LWVWV6D1qMIp1Gzr0ZmdWTE1SPdZ7Ej8glGnCzPdguCPuzbhGXmIg0V\nJ5D+02wsqws1zd48JSMXXM8zkYZVwQYIPUsNn5FetQpwxDIMPmhHg+QNBgwOnk8J\nK2sIjjLPL7qY7Itv7LT7Gvm5qSOkZ33RCgXcgz+okEIQMYkCQQDzbTOyDL0c5WQV\n6A2k06T/azdhUdGXF9C0+WkWSfNaovmTgRXh1G+jMlr82Snz4p4/STt7P/XtyWzF\n3pkVgZr3AkEA7nPjXwHlttNEMo6AtxHd47nizK2NUN803ElIUT8P9KSCoERmSXq6\n6PDekGNic4ldpsSvOeYCk8MAYoDBy9kvVwJBAMLgX4xg6lzhv7hR5+pWjTb1rIY6\nrCHbrPfU264+UZXz9v2BT/VUznLF81WMvStD9xAPHpFS6R0OLghSZhdzhI0CQQDL\n8Duvfxzrn4b9QlmduV8wLERoT6rEVxKLsPVz316TGrxJvBZLk/cV0SRZE1cZf4uk\nXSWMfEcJ/0Zt+LdG1CqjAkEAqwLSglJ9Dy3HpgMz4vAAyZWzAxvyA1zW0no9GOLc\nPQnYaNUN/Fy2SYtETXTb0CQ9X1rt8ffkFP7ya+5TC83aMg==\n-----END RSA PRIVATE KEY-----\n" + settings = OneLogin_Saml2_Settings(settings_data) + self.assertEqual(key, settings.get_sp_key()) + + key_2 = "-----BEGIN PRIVATE KEY-----\nMIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOWA+YHU7cvPOrBO\nfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoe\nch7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHs\nNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAECgYEA0wDXZPS9hKqMTNh+nnfONioX\nBjhA6fQ7GVtWKDxa3ofMoPyt7ejGL/Hnvcv13Vn02UAsFx1bKrCstDqVtYwrWrnm\nywXyH+o9paJnTmd+cRIjWU8mRvCrxzH5I/Bcvbp1qZoASuqZEaGwNjM6JpW2o3QT\nmHGMALcLUPfEvhApssECQQDy2e65E86HcFhi/Ta8TQ0odDCNbiWA0bI1Iu8B7z+N\nAy1D1+WnCd7w2u9U6CF/k2nFHCsvxEoeANM0z7h5T/XvAkEA8e4JqKmDrfdiakQT\n7nf9svU2jXZtxSbPiIRMafNikDvzZ1vJCZkvdmaWYL70GlDZIwc9ad67rHZ/n/fq\nX1d0MQJAbRpRsJ5gY+KqItbFt3UaWzlP8sowWR5cZJjsLb9RmsV5mYguKYw6t5R0\nf33GRu1wUFimYlBaR/5w5MIJi57LywJATO1a5uWX+G5MPewNxmsjIY91XEAHIYR4\nwzkGLz5z3dciS4BVCZdLD0QJlxPA/MkuckPwFET9uhYn+M7VGKHvUQJBANSDwsY+\nBdCGpi/WRV37HUfwLl07damaFbW3h08PQx8G8SuF7DpN+FPBcI6VhzrIWNRBxWpr\nkgeGioKNfFWzSaM=\n-----END PRIVATE KEY-----\n" + settings_data['sp']['privateKey'] = key_2 + settings_2 = OneLogin_Saml2_Settings(settings_data) + self.assertEqual(key_2, settings_2.get_sp_key()) + + del settings_data['sp']['privateKey'] + del settings_data['custom_base_path'] + custom_base_path = dirname(__file__) + + settings_3 = OneLogin_Saml2_Settings(settings_data, custom_base_path=custom_base_path) + self.assertIsNone(settings_3.get_sp_key()) + + def testFormatIdPCert(self): + """ + Tests the format_idp_cert 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_idp_cert()) + + cert_2 = "-----BEGIN CERTIFICATE-----\nMIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czET\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYD\nVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIy\nNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UE\nCgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJ\nKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BF\nrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PP\nJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHC\npS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAf\nBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0G\nCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZ\nE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kY\nvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m\n-----END CERTIFICATE-----\n" + settings_data['idp']['x509cert'] = cert_2 + settings = OneLogin_Saml2_Settings(settings_data) + self.assertEqual(cert_2, settings.get_idp_cert()) + + def testFormatSPCert(self): + """ + Tests the format_sp_cert 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()) + + settings_data['sp']['x509cert'] = cert + settings = OneLogin_Saml2_Settings(settings_data) + self.assertEqual(cert, settings.get_sp_cert()) + + cert_2 = "-----BEGIN CERTIFICATE-----\nMIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czET\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYD\nVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIy\nNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UE\nCgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJ\nKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BF\nrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PP\nJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHC\npS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAf\nBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0G\nCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZ\nE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kY\nvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m\n-----END CERTIFICATE-----\n" + settings_data['sp']['x509cert'] = cert_2 + settings = OneLogin_Saml2_Settings(settings_data) + self.assertEqual(cert_2, settings.get_sp_cert()) + + def testFormatSPKey(self): + """ + Tests the format_sp_key method of the OneLogin_Saml2_Settings + """ + settings_data = self.loadSettingsJSON() + key = "-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9\nIbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+\nPbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQAB\nAoGAD4/Z4LWVWV6D1qMIp1Gzr0ZmdWTE1SPdZ7Ej8glGnCzPdguCPuzbhGXmIg0V\nJ5D+02wsqws1zd48JSMXXM8zkYZVwQYIPUsNn5FetQpwxDIMPmhHg+QNBgwOnk8J\nK2sIjjLPL7qY7Itv7LT7Gvm5qSOkZ33RCgXcgz+okEIQMYkCQQDzbTOyDL0c5WQV\n6A2k06T/azdhUdGXF9C0+WkWSfNaovmTgRXh1G+jMlr82Snz4p4/STt7P/XtyWzF\n3pkVgZr3AkEA7nPjXwHlttNEMo6AtxHd47nizK2NUN803ElIUT8P9KSCoERmSXq6\n6PDekGNic4ldpsSvOeYCk8MAYoDBy9kvVwJBAMLgX4xg6lzhv7hR5+pWjTb1rIY6\nrCHbrPfU264+UZXz9v2BT/VUznLF81WMvStD9xAPHpFS6R0OLghSZhdzhI0CQQDL\n8Duvfxzrn4b9QlmduV8wLERoT6rEVxKLsPVz316TGrxJvBZLk/cV0SRZE1cZf4uk\nXSWMfEcJ/0Zt+LdG1CqjAkEAqwLSglJ9Dy3HpgMz4vAAyZWzAxvyA1zW0no9GOLc\nPQnYaNUN/Fy2SYtETXTb0CQ9X1rt8ffkFP7ya+5TC83aMg==\n-----END RSA PRIVATE KEY-----\n" + settings_data['sp']['privateKey'] = key + settings = OneLogin_Saml2_Settings(settings_data) + self.assertEqual(key, settings.get_sp_key()) + + key_2 = "-----BEGIN PRIVATE KEY-----\nMIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOWA+YHU7cvPOrBO\nfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoe\nch7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHs\nNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAECgYEA0wDXZPS9hKqMTNh+nnfONioX\nBjhA6fQ7GVtWKDxa3ofMoPyt7ejGL/Hnvcv13Vn02UAsFx1bKrCstDqVtYwrWrnm\nywXyH+o9paJnTmd+cRIjWU8mRvCrxzH5I/Bcvbp1qZoASuqZEaGwNjM6JpW2o3QT\nmHGMALcLUPfEvhApssECQQDy2e65E86HcFhi/Ta8TQ0odDCNbiWA0bI1Iu8B7z+N\nAy1D1+WnCd7w2u9U6CF/k2nFHCsvxEoeANM0z7h5T/XvAkEA8e4JqKmDrfdiakQT\n7nf9svU2jXZtxSbPiIRMafNikDvzZ1vJCZkvdmaWYL70GlDZIwc9ad67rHZ/n/fq\nX1d0MQJAbRpRsJ5gY+KqItbFt3UaWzlP8sowWR5cZJjsLb9RmsV5mYguKYw6t5R0\nf33GRu1wUFimYlBaR/5w5MIJi57LywJATO1a5uWX+G5MPewNxmsjIY91XEAHIYR4\nwzkGLz5z3dciS4BVCZdLD0QJlxPA/MkuckPwFET9uhYn+M7VGKHvUQJBANSDwsY+\nBdCGpi/WRV37HUfwLl07damaFbW3h08PQx8G8SuF7DpN+FPBcI6VhzrIWNRBxWpr\nkgeGioKNfFWzSaM=\n-----END PRIVATE KEY-----\n" + settings_data['sp']['privateKey'] = key_2 + settings_2 = OneLogin_Saml2_Settings(settings_data) + self.assertEqual(key_2, settings_2.get_sp_key()) + + def testCheckSPCerts(self): + """ + Tests the checkSPCerts method of the OneLogin_Saml2_Settings + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + self.assertTrue(settings.check_sp_certs()) + + def testCheckSettings(self): + """ + Tests the checkSettings method of the OneLogin_Saml2_Settings + The checkSettings method is private and is used at the constructor + """ + settings_info = {} + with self.assertRaisesRegexp(Exception, 'Invalid dict settings: invalid_syntax'): + OneLogin_Saml2_Settings(settings_info) + + settings_info['strict'] = True + with self.assertRaisesRegexp(Exception, 'Invalid dict settings: idp_not_found,sp_not_found'): + OneLogin_Saml2_Settings(settings_info) + + settings_info['idp'] = {} + settings_info['idp']['x509cert'] = '' + settings_info['sp'] = {} + settings_info['sp']['entityID'] = 'SPentityId' + settings_info['security'] = {} + settings_info['security']['signMetadata'] = False + 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) + + settings_info['idp']['entityID'] = 'entityId' + settings_info['idp']['singleSignOnService'] = {} + settings_info['idp']['singleSignOnService']['url'] = 'invalid_value' + settings_info['idp']['singleLogoutService'] = {} + settings_info['idp']['singleLogoutService']['url'] = 'invalid_value' + settings_info['sp']['assertionConsumerService'] = {} + settings_info['sp']['assertionConsumerService']['url'] = 'invalid_value' + settings_info['sp']['singleLogoutService'] = {} + settings_info['sp']['singleLogoutService']['url'] = 'invalid_value' + 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) + + settings_info['security']['wantAssertionsSigned'] = True + 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) + + settings_info = self.loadSettingsJSON() + settings_info['security']['signMetadata'] = {} + settings_info['security']['signMetadata']['keyFileName'] = 'metadata.key' + settings_info['organization'] = { + 'en-US': { + 'name': 'miss_information' + } + } + settings_info['contactPerson'] = { + 'support': { + 'givenName': 'support_name' + }, + 'auxiliar': { + 'givenName': 'auxiliar_name', + 'emailAddress': 'auxiliar@example.com' + } + } + with self.assertRaisesRegexp(Exception, 'Invalid dict settings: sp_signMetadata_invalid,contact_type_invalid,contact_not_enough_data,organization_not_enough_data'): + OneLogin_Saml2_Settings(settings_info) + + def testGetSPMetadata(self): + """ + Tests the getSPMetadata method of the OneLogin_Saml2_Settings + Case unsigned metadata + """ + 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) + self.assertIn('', metadata) + self.assertIn('', 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): + """ + Tests the getSPMetadata method of the OneLogin_Saml2_Settings + Case signed metadata + """ + settings_info = self.loadSettingsJSON() + if 'security' not in settings_info: + settings_info['security'] = {} + + # Use custom cert/key + settings_info['security']['signMetadata'] = { + "keyFileName": "sp.key", + "certFileName": "sp.crt" + } + self.generateAndCheckMetadata(settings_info) + + # Default cert/key + settings_info['security']['signMetadata'] = True + self.generateAndCheckMetadata(settings_info) + + # Now try again with SP keys set directly from files that no exists: + settings_info['custom_base_path'] = '../path/not/exists/' + with self.assertRaises(OneLogin_Saml2_Error): + OneLogin_Saml2_Settings(settings_info).get_sp_metadata() + + # Now try again with SP keys set directly in settings and not from files: + del settings_info['custom_base_path'] + # Now the keys should not be found, so metadata generation won't work: + with self.assertRaises(OneLogin_Saml2_Error): + OneLogin_Saml2_Settings(settings_info).get_sp_metadata() + # Set the keys in the settings: + settings_info['sp']['x509cert'] = self.file_contents(join(self.data_path, 'customPath', 'certs', 'sp.crt')) + settings_info['sp']['privateKey'] = self.file_contents(join(self.data_path, 'customPath', 'certs', 'sp.key')) + self.generateAndCheckMetadata(settings_info) + + # Now fails due no privateKey + del settings_info['sp']['privateKey'] + with self.assertRaises(OneLogin_Saml2_Error): + OneLogin_Saml2_Settings(settings_info).get_sp_metadata() + + def generateAndCheckMetadata(self, settings): + """ + Helper method: Given some settings, generate metadata and validate it + """ + if not isinstance(settings, OneLogin_Saml2_Settings): + settings = OneLogin_Saml2_Settings(settings) + metadata = settings.get_sp_metadata() + self.assertIn('', metadata) + self.assertIn('', metadata) + self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', metadata) + self.assertIn('', metadata) + self.assertIn('', metadata) + self.assertIn('', metadata) + return metadata + + def testGetSPMetadataSignedNoMetadataCert(self): + """ + Tests the getSPMetadata method of the OneLogin_Saml2_Settings + Case signed metadata with specific certs + """ + settings_info = self.loadSettingsJSON() + if 'security' not in settings_info: + settings_info['security'] = {} + settings_info['security']['signMetadata'] = {} + + with self.assertRaisesRegexp(Exception, 'Invalid dict settings: sp_signMetadata_invalid'): + OneLogin_Saml2_Settings(settings_info) + + settings_info['security']['signMetadata'] = { + 'keyFileName': 'noexist.key', + 'certFileName': 'sp.crt' + } + settings = OneLogin_Saml2_Settings(settings_info) + with self.assertRaisesRegexp(Exception, 'Private key file not readable'): + settings.get_sp_metadata() + + settings_info['security']['signMetadata'] = { + 'keyFileName': 'sp.key', + 'certFileName': 'noexist.crt' + } + settings = OneLogin_Saml2_Settings(settings_info) + with self.assertRaisesRegexp(Exception, 'Public cert file not readable'): + settings.get_sp_metadata() + + settings_info['security']['signMetadata'] = 'invalid_value' + settings = OneLogin_Saml2_Settings(settings_info) + with self.assertRaisesRegexp(Exception, 'Invalid Setting: signMetadata value of the sp is not valid'): + settings.get_sp_metadata() + + def testValidateMetadata(self): + """ + Tests the validateMetadata method of the OneLogin_Saml2_Settings + Case valid metadata + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + metadata = settings.get_sp_metadata() + self.assertEqual(len(settings.validate_metadata(metadata)), 0) + + xml = self.file_contents(join(self.data_path, 'metadata', 'metadata_settings1.xml')) + self.assertEqual(len(settings.validate_metadata(xml)), 0) + + xml_2 = 'invalid' + self.assertIn('invalid_xml', settings.validate_metadata(xml_2)) + + xml_3 = self.file_contents(join(self.data_path, 'metadata', 'entities_metadata.xml')) + self.assertIn('noEntityDescriptor_xml', settings.validate_metadata(xml_3)) + + xml_4 = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata.xml')) + self.assertIn('onlySPSSODescriptor_allowed_xml', settings.validate_metadata(xml_4)) + + xml_5 = self.file_contents(join(self.data_path, 'metadata', 'no_expiration_mark_metadata.xml')) + self.assertEqual(len(settings.validate_metadata(xml_5)), 0) + + def testValidateMetadataExpired(self): + """ + Tests the validateMetadata method of the OneLogin_Saml2_Settings + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + metadata = self.file_contents(join(self.data_path, 'metadata', 'expired_metadata_settings1.xml')) + errors = settings.validate_metadata(metadata) + self.assertNotEqual(len(metadata), 0) + self.assertIn('expired_xml', errors) + + def testValidateMetadataNoXML(self): + """ + Tests the validateMetadata method of the OneLogin_Saml2_Settings + Case no metadata + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + metadata = '' + with self.assertRaisesRegexp(Exception, 'Empty string supplied as input'): + errors = settings.validate_metadata(metadata) + + metadata = '' + errors = settings.validate_metadata(metadata) + + self.assertNotEqual(len(errors), 0) + self.assertIn('unloaded_xml', errors) + + def testValidateMetadataNoEntity(self): + """ + Tests the validateMetadata method of the OneLogin_Saml2_Settings + Case invalid xml metadata + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + metadata = self.file_contents(join(self.data_path, 'metadata', 'noentity_metadata_settings1.xml')) + errors = settings.validate_metadata(metadata) + self.assertNotEqual(len(metadata), 0) + self.assertIn('invalid_xml', errors) + + def testGetIdPData(self): + """ + Tests the getIdPData method of the OneLogin_Saml2_Settings + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + idp_data = settings.get_idp_data() + self.assertNotEqual(len(idp_data), 0) + self.assertIn('entityId', idp_data) + self.assertIn('singleSignOnService', idp_data) + self.assertIn('singleLogoutService', idp_data) + self.assertIn('x509cert', idp_data) + + self.assertEqual('http://idp.example.com/', idp_data['entityId']) + self.assertEqual('http://idp.example.com/SSOService.php', idp_data['singleSignOnService']['url']) + self.assertEqual('http://idp.example.com/SingleLogoutService.php', idp_data['singleLogoutService']['url']) + + x509cert = 'MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo' + formated_x509_cert = OneLogin_Saml2_Utils.format_cert(x509cert) + self.assertEqual(formated_x509_cert, idp_data['x509cert']) + + settings2 = OneLogin_Saml2_Settings(self.loadSettingsJSON('settings8.json')) + idp_data2 = settings2.get_idp_data() + self.assertNotEqual(len(idp_data2), 0) + self.assertIn('x509certMulti', idp_data2) + self.assertIn('signing', idp_data2['x509certMulti']) + self.assertIn('encryption', idp_data2['x509certMulti']) + x509cert2 = 'MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m' + formated_x509_cert2 = OneLogin_Saml2_Utils.format_cert(x509cert2) + self.assertEqual(formated_x509_cert2, idp_data2['x509certMulti']['signing'][0]) + self.assertEqual(formated_x509_cert, idp_data2['x509certMulti']['signing'][1]) + self.assertEqual(formated_x509_cert, idp_data2['x509certMulti']['encryption'][0]) + + def testGetSPData(self): + """ + Tests the getSPData method of the OneLogin_Saml2_Settings + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + sp_data = settings.get_sp_data() + self.assertNotEqual(len(sp_data), 0) + self.assertIn('entityId', sp_data) + self.assertIn('assertionConsumerService', sp_data) + self.assertIn('singleLogoutService', sp_data) + self.assertIn('NameIDFormat', sp_data) + + self.assertEqual('http://stuff.com/endpoints/metadata.php', sp_data['entityId']) + self.assertEqual('http://stuff.com/endpoints/endpoints/acs.php', sp_data['assertionConsumerService']['url']) + self.assertEqual('http://stuff.com/endpoints/endpoints/sls.php', sp_data['singleLogoutService']['url']) + self.assertEqual('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', sp_data['NameIDFormat']) + + def testGetSecurityData(self): + """ + Tests the getSecurityData method of the OneLogin_Saml2_Settings + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + security = settings.get_security_data() + self.assertNotEqual(len(security), 0) + self.assertIn('nameIdEncrypted', security) + self.assertIn('authnRequestsSigned', security) + self.assertIn('logoutRequestSigned', security) + self.assertIn('logoutResponseSigned', security) + self.assertIn('signMetadata', security) + self.assertIn('wantMessagesSigned', security) + self.assertIn('wantAssertionsSigned', security) + self.assertIn('requestedAuthnContext', security) + self.assertIn('wantNameId', security) + self.assertIn('wantNameIdEncrypted', security) + + def testGetDefaultSecurityValues(self): + """ + Tests default values of Security advanced sesettings + """ + settings_json = self.loadSettingsJSON() + del settings_json['security'] + settings = OneLogin_Saml2_Settings(settings_json) + security = settings.get_security_data() + + self.assertIn('nameIdEncrypted', security) + self.assertFalse(security.get('nameIdEncrypted')) + + self.assertIn('authnRequestsSigned', security) + self.assertFalse(security.get('authnRequestsSigned')) + + self.assertIn('logoutRequestSigned', security) + self.assertFalse(security.get('logoutRequestSigned')) + + self.assertIn('logoutResponseSigned', security) + self.assertFalse(security.get('logoutResponseSigned')) + + self.assertIn('signMetadata', security) + self.assertFalse(security.get('signMetadata')) + + self.assertIn('wantMessagesSigned', security) + self.assertFalse(security.get('wantMessagesSigned')) + + self.assertIn('wantAssertionsSigned', security) + self.assertFalse(security.get('wantAssertionsSigned')) + + self.assertIn('requestedAuthnContext', security) + self.assertTrue(security.get('requestedAuthnContext')) + + self.assertIn('wantNameId', security) + self.assertTrue(security.get('wantNameId')) + + self.assertIn('wantNameIdEncrypted', security) + self.assertFalse(security.get('wantNameIdEncrypted')) + + def testGetContacts(self): + """ + Tests the getContacts method of the OneLogin_Saml2_Settings + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + contacts = settings.get_contacts() + self.assertNotEqual(len(contacts), 0) + self.assertEqual('technical_name', contacts['technical']['givenName']) + self.assertEqual('technical@example.com', contacts['technical']['emailAddress']) + self.assertEqual('support_name', contacts['support']['givenName']) + self.assertEqual('support@example.com', contacts['support']['emailAddress']) + + def testGetOrganization(self): + """ + Tests the getOrganization method of the OneLogin_Saml2_Settings + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + organization = settings.get_organization() + self.assertNotEqual(len(organization), 0) + self.assertEqual('sp_test', organization['en-US']['name']) + self.assertEqual('SP test', organization['en-US']['displayname']) + self.assertEqual('http://sp.example.com', organization['en-US']['url']) + + def testSetStrict(self): + """ + Tests the setStrict method of the OneLogin_Saml2_Settings + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + self.assertFalse(settings.is_strict()) + + settings.set_strict(True) + self.assertTrue(settings.is_strict()) + + settings.set_strict(False) + self.assertFalse(settings.is_strict()) + + with self.assertRaises(AssertionError): + settings.set_strict('a') + + def testIsStrict(self): + """ + Tests the isStrict method of the OneLogin_Saml2_Settings + """ + settings_info = self.loadSettingsJSON() + del settings_info['strict'] + + settings = OneLogin_Saml2_Settings(settings_info) + self.assertTrue(settings.is_strict()) + + settings_info['strict'] = False + settings_2 = OneLogin_Saml2_Settings(settings_info) + self.assertFalse(settings_2.is_strict()) + + settings_info['strict'] = True + settings_3 = OneLogin_Saml2_Settings(settings_info) + self.assertTrue(settings_3.is_strict()) + + def testIsDebugActive(self): + """ + Tests the isDebugActive method of the OneLogin_Saml2_Settings + """ + settings_info = self.loadSettingsJSON() + del settings_info['debug'] + + settings = OneLogin_Saml2_Settings(settings_info) + self.assertFalse(settings.is_debug_active()) + + settings_info['debug'] = False + settings_2 = OneLogin_Saml2_Settings(settings_info) + self.assertFalse(settings_2.is_debug_active()) + + settings_info['debug'] = True + settings_3 = OneLogin_Saml2_Settings(settings_info) + self.assertTrue(settings_3.is_debug_active()) + + +if __name__ == '__main__': + runner = unittest.TextTestRunner() + unittest.main(testRunner=runner) diff --git a/tests/src/OneLogin/saml2_tests/signed_response_test.py b/tests/src/OneLogin/saml2_tests/signed_response_test.py new file mode 100644 index 00000000..29a61006 --- /dev/null +++ b/tests/src/OneLogin/saml2_tests/signed_response_test.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +# MIT License + +from base64 import b64encode +import json +from os.path import dirname, join, exists +import unittest + +from onelogin.saml2.response import OneLogin_Saml2_Response +from onelogin.saml2.settings import OneLogin_Saml2_Settings + + +class OneLogin_Saml2_SignedResponse_Test(unittest.TestCase): + 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') + 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 testResponseSignedAssertionNot(self): + """ + Tests the getNameId method of the OneLogin_Saml2_Response + Case valid signed response, unsigned assertion + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + message = self.file_contents(join(self.data_path, 'responses', 'open_saml_response.xml')) + response = OneLogin_Saml2_Response(settings, b64encode(message)) + + self.assertEquals('someone@example.org', response.get_nameid()) + + def testResponseAndAssertionSigned(self): + """ + Tests the getNameId method of the OneLogin_Saml2_Response + Case valid signed response, signed assertion + """ + settings_info = self.loadSettingsJSON() + settings_info['idp']['entityId'] = "https://federate.example.net/saml/saml2/idp/metadata.php" + settings_info['sp']['entityId'] = "hello.com" + + settings = OneLogin_Saml2_Settings(settings_info) + message = self.file_contents(join(self.data_path, 'responses', 'simple_saml_php.xml')) + response = OneLogin_Saml2_Response(settings, b64encode(message)) + + self.assertEquals('someone@example.com', response.get_nameid()) + + +if __name__ == '__main__': + runner = unittest.TextTestRunner() + unittest.main(testRunner=runner) diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py new file mode 100644 index 00000000..295e9db7 --- /dev/null +++ b/tests/src/OneLogin/saml2_tests/utils_test.py @@ -0,0 +1,1018 @@ +# -*- coding: utf-8 -*- + +# MIT License + +from base64 import b64decode +import json +from lxml import etree +from os.path import dirname, join, exists +import unittest +from xml.dom.minidom import Document, parseString + +from onelogin.saml2.constants import OneLogin_Saml2_Constants +from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError +from onelogin.saml2.settings import OneLogin_Saml2_Settings +from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.xmlparser import fromstring + + +class OneLogin_Saml2_Utils_Test(unittest.TestCase): + data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data') + + def loadSettingsJSON(self, filename=None): + if filename: + filename = join(dirname(dirname(dirname(dirname(__file__)))), 'settings', filename) + else: + filename = join(dirname(dirname(dirname(dirname(__file__)))), 'settings', 'settings1.json') + 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 testDeflateBase64Roundtrip(self): + """ + Tests deflate_and_base64_encode and decode_base64_and_inflate methods of OneLogin_Saml2_Utils + """ + body = 'Some random string.' + encoded = OneLogin_Saml2_Utils.deflate_and_base64_encode(body) + self.assertEqual(OneLogin_Saml2_Utils.decode_base64_and_inflate(encoded), body) + + unicode_body = u'Sömé rändöm nön-äsçïï strïng.' + unicode_encoded = OneLogin_Saml2_Utils.deflate_and_base64_encode(unicode_body) + self.assertEqual(OneLogin_Saml2_Utils.decode_base64_and_inflate(unicode_encoded), unicode_body) + + def testValidateXML(self): + """ + Tests the validate_xml method of the OneLogin_Saml2_Utils + """ + metadata_unloaded = '' + self.assertEqual(OneLogin_Saml2_Utils.validate_xml(metadata_unloaded, 'saml-schema-metadata-2.0.xsd'), 'unloaded_xml') + + metadata_invalid = self.file_contents(join(self.data_path, 'metadata', 'noentity_metadata_settings1.xml')) + self.assertEqual(OneLogin_Saml2_Utils.validate_xml(metadata_invalid, 'saml-schema-metadata-2.0.xsd'), 'invalid_xml') + + metadata_expired = self.file_contents(join(self.data_path, 'metadata', 'expired_metadata_settings1.xml')) + res = OneLogin_Saml2_Utils.validate_xml(metadata_expired, 'saml-schema-metadata-2.0.xsd') + self.assertTrue(isinstance(res, Document)) + + metadata_ok = self.file_contents(join(self.data_path, 'metadata', 'metadata_settings1.xml')) + res = OneLogin_Saml2_Utils.validate_xml(metadata_ok, 'saml-schema-metadata-2.0.xsd') + self.assertTrue(isinstance(res, Document)) + + metadata_bad_order = self.file_contents(join(self.data_path, 'metadata', 'metadata_bad_order_settings1.xml')) + res = OneLogin_Saml2_Utils.validate_xml(metadata_bad_order, 'saml-schema-metadata-2.0.xsd') + self.assertFalse(isinstance(res, Document)) + + metadata_signed = self.file_contents(join(self.data_path, 'metadata', 'signed_metadata_settings1.xml')) + res = OneLogin_Saml2_Utils.validate_xml(metadata_signed, 'saml-schema-metadata-2.0.xsd') + self.assertTrue(isinstance(res, Document)) + + dom = parseString(metadata_ok) + res = OneLogin_Saml2_Utils.validate_xml(dom, 'saml-schema-metadata-2.0.xsd') + self.assertTrue(isinstance(res, Document)) + + def testFormatCert(self): + """ + Tests the format_cert method of the OneLogin_Saml2_Utils + """ + settings_info = self.loadSettingsJSON() + cert = settings_info['idp']['x509cert'] + self.assertNotIn('-----BEGIN CERTIFICATE-----', cert) + self.assertNotIn('-----END CERTIFICATE-----', cert) + self.assertEqual(len(cert), 860) + + formated_cert1 = OneLogin_Saml2_Utils.format_cert(cert) + self.assertIn('-----BEGIN CERTIFICATE-----', formated_cert1) + self.assertIn('-----END CERTIFICATE-----', formated_cert1) + + formated_cert2 = OneLogin_Saml2_Utils.format_cert(cert, True) + self.assertEqual(formated_cert1, formated_cert2) + + formated_cert3 = OneLogin_Saml2_Utils.format_cert(cert, False) + self.assertNotIn('-----BEGIN CERTIFICATE-----', formated_cert3) + self.assertNotIn('-----END CERTIFICATE-----', formated_cert3) + self.assertEqual(len(formated_cert3), 860) + + def testFormatPrivateKey(self): + """ + Tests the format_private_key method of the OneLogin_Saml2_Utils + """ + key = "-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9\nIbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+\nPbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQAB\nAoGAD4/Z4LWVWV6D1qMIp1Gzr0ZmdWTE1SPdZ7Ej8glGnCzPdguCPuzbhGXmIg0V\nJ5D+02wsqws1zd48JSMXXM8zkYZVwQYIPUsNn5FetQpwxDIMPmhHg+QNBgwOnk8J\nK2sIjjLPL7qY7Itv7LT7Gvm5qSOkZ33RCgXcgz+okEIQMYkCQQDzbTOyDL0c5WQV\n6A2k06T/azdhUdGXF9C0+WkWSfNaovmTgRXh1G+jMlr82Snz4p4/STt7P/XtyWzF\n3pkVgZr3AkEA7nPjXwHlttNEMo6AtxHd47nizK2NUN803ElIUT8P9KSCoERmSXq6\n6PDekGNic4ldpsSvOeYCk8MAYoDBy9kvVwJBAMLgX4xg6lzhv7hR5+pWjTb1rIY6\nrCHbrPfU264+UZXz9v2BT/VUznLF81WMvStD9xAPHpFS6R0OLghSZhdzhI0CQQDL\n8Duvfxzrn4b9QlmduV8wLERoT6rEVxKLsPVz316TGrxJvBZLk/cV0SRZE1cZf4uk\nXSWMfEcJ/0Zt+LdG1CqjAkEAqwLSglJ9Dy3HpgMz4vAAyZWzAxvyA1zW0no9GOLc\nPQnYaNUN/Fy2SYtETXTb0CQ9X1rt8ffkFP7ya+5TC83aMg==\n-----END RSA PRIVATE KEY-----\n" + formated_key = OneLogin_Saml2_Utils.format_private_key(key, True) + self.assertIn('-----BEGIN RSA PRIVATE KEY-----', formated_key) + self.assertIn('-----END RSA PRIVATE KEY-----', formated_key) + self.assertEqual(len(formated_key), 891) + + formated_key = OneLogin_Saml2_Utils.format_private_key(key, False) + self.assertNotIn('-----BEGIN RSA PRIVATE KEY-----', formated_key) + self.assertNotIn('-----END RSA PRIVATE KEY-----', formated_key) + self.assertEqual(len(formated_key), 816) + + key_2 = "-----BEGIN PRIVATE KEY-----\nMIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAM62buSW9Zgh7CmZ\nouJekK0ac9sgEZkspemjv7SyE6Hbdz+KmUr3C7MI6JuPfVyJbxvMDf3FbgBBK7r5\nyfGgehXwplLMZj8glvV3NkdLMLPWmaw9U5sOzRoym46pVvsEo1PUL2qDK5Wrsm1g\nuY1KIDSHL59NQ7PzDKgm1dxioeXFAgMBAAECgYA/fvRzTReloo3rfWD2Tfv84EpE\nPgaJ2ZghO4Zwl97F8icgIo/R4i760Lq6xgnI+gJiNHz7vcB7XYl0RrRMf3HgbA7z\npJxREmOVltESDHy6lH0TmCdv9xMmHltB+pbGOhqBvuGgFbEOR73lDDV0ln2rEITJ\nA2zjYF+hWe8b0JFeQQJBAOsIIIlHAMngjhCQDD6kla/vce972gCFU7ZeFw16ZMmb\n8W4rGRfQoQWYxSLAFIFsYewSBTccanyYbBNe3njki3ECQQDhJ4cgV6VpTwez4dkp\nU/xCHKoReedAEJhXucTNGpiIqu+TDgIz9aRbrgnUKkS1s06UJhcDRTl/+pCSRRt/\nCA2VAkBkPw4pn1hNwvK1S8t9OJQD+5xcKjZcvIFtKoqonAi7GUGL3OQSDVFw4q1K\n2iSk40aM+06wJ/WfeR+3z2ISrGBxAkAJ20YiF1QpcQlASbHNCl0vs7uKOlDyUAer\nR3mjFPf6e6kzQdi815MTZGIPxK3vWmMlPymgvgYPYTO1A4t5myulAkEA1QioAWcJ\noO26qhUlFRBCR8BMJoVPImV7ndVHE7usHdJvP7V2P9RyuRcMCTVul8RRmyoh/+yG\n4ghMaHo/v0YY5Q==\n-----END PRIVATE KEY-----\n" + formated_key_2 = OneLogin_Saml2_Utils.format_private_key(key_2, True) + self.assertIn('-----BEGIN PRIVATE KEY-----', formated_key_2) + self.assertIn('-----END PRIVATE KEY-----', formated_key_2) + self.assertEqual(len(formated_key_2), 916) + + formated_key_2 = OneLogin_Saml2_Utils.format_private_key(key_2, False) + self.assertNotIn('-----BEGIN PRIVATE KEY-----', formated_key_2) + self.assertNotIn('-----END PRIVATE KEY-----', formated_key_2) + self.assertEqual(len(formated_key_2), 848) + + key_3 = 'MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAM62buSW9Zgh7CmZouJekK0ac9sgEZkspemjv7SyE6Hbdz+KmUr3C7MI6JuPfVyJbxvMDf3FbgBBK7r5yfGgehXwplLMZj8glvV3NkdLMLPWmaw9U5sOzRoym46pVvsEo1PUL2qDK5Wrsm1guY1KIDSHL59NQ7PzDKgm1dxioeXFAgMBAAECgYA/fvRzTReloo3rfWD2Tfv84EpEPgaJ2ZghO4Zwl97F8icgIo/R4i760Lq6xgnI+gJiNHz7vcB7XYl0RrRMf3HgbA7zpJxREmOVltESDHy6lH0TmCdv9xMmHltB+pbGOhqBvuGgFbEOR73lDDV0ln2rEITJA2zjYF+hWe8b0JFeQQJBAOsIIIlHAMngjhCQDD6kla/vce972gCFU7ZeFw16ZMmb8W4rGRfQoQWYxSLAFIFsYewSBTccanyYbBNe3njki3ECQQDhJ4cgV6VpTwez4dkpU/xCHKoReedAEJhXucTNGpiIqu+TDgIz9aRbrgnUKkS1s06UJhcDRTl/+pCSRRt/CA2VAkBkPw4pn1hNwvK1S8t9OJQD+5xcKjZcvIFtKoqonAi7GUGL3OQSDVFw4q1K2iSk40aM+06wJ/WfeR+3z2ISrGBxAkAJ20YiF1QpcQlASbHNCl0vs7uKOlDyUAerR3mjFPf6e6kzQdi815MTZGIPxK3vWmMlPymgvgYPYTO1A4t5myulAkEA1QioAWcJoO26qhUlFRBCR8BMJoVPImV7ndVHE7usHdJvP7V2P9RyuRcMCTVul8RRmyoh/+yG4ghMaHo/v0YY5Q==' + formated_key_3 = OneLogin_Saml2_Utils.format_private_key(key_3, True) + self.assertIn('-----BEGIN RSA PRIVATE KEY-----', formated_key_3) + self.assertIn('-----END RSA PRIVATE KEY-----', formated_key_3) + self.assertEqual(len(formated_key_3), 924) + + formated_key_3 = OneLogin_Saml2_Utils.format_private_key(key_3, False) + self.assertNotIn('-----BEGIN PRIVATE KEY-----', formated_key_3) + self.assertNotIn('-----END PRIVATE KEY-----', formated_key_3) + self.assertNotIn('-----BEGIN RSA PRIVATE KEY-----', formated_key_3) + self.assertNotIn('-----END RSA PRIVATE KEY-----', formated_key_3) + self.assertEqual(len(formated_key_3), 848) + + def testRedirect(self): + """ + Tests the redirect method of the OneLogin_Saml2_Utils + """ + request_data = { + 'http_host': 'example.com' + } + + # Check relative and absolute + hostname = OneLogin_Saml2_Utils.get_self_host(request_data) + url = 'http://%s/example' % hostname + url2 = '/example' + + target_url = OneLogin_Saml2_Utils.redirect(url, {}, request_data) + target_url2 = OneLogin_Saml2_Utils.redirect(url2, {}, request_data) + + self.assertEqual(target_url, target_url2) + + # Check that accept http/https and reject other protocols + url3 = 'https://%s/example?test=true' % hostname + url4 = 'ftp://%s/example' % hostname + + target_url3 = OneLogin_Saml2_Utils.redirect(url3, {}, request_data) + self.assertIn('test=true', target_url3) + + with self.assertRaisesRegexp(Exception, 'Redirect to invalid URL'): + target_url4 = OneLogin_Saml2_Utils.redirect(url4, {}, request_data) + self.assertTrue(target_url4 == 42) + + # Review parameter prefix + parameters1 = { + 'value1': 'a' + } + + target_url5 = OneLogin_Saml2_Utils.redirect(url, parameters1, request_data) + self.assertEqual('http://%s/example?value1=a' % hostname, target_url5) + + target_url6 = OneLogin_Saml2_Utils.redirect(url3, parameters1, request_data) + self.assertEqual('https://%s/example?test=true&value1=a' % hostname, target_url6) + + # Review parameters + parameters2 = { + 'alphavalue': 'a', + 'numvaluelist': ['1', '2'], + 'testing': None + } + + target_url7 = OneLogin_Saml2_Utils.redirect(url, parameters2, request_data) + self.assertEqual('http://%s/example?numvaluelist[]=1&numvaluelist[]=2&testing&alphavalue=a' % hostname, target_url7) + + parameters3 = { + 'alphavalue': 'a', + 'emptynumvaluelist': [], + 'numvaluelist': [''], + } + target_url8 = OneLogin_Saml2_Utils.redirect(url, parameters3, request_data) + self.assertEqual('http://%s/example?numvaluelist[]=&alphavalue=a' % hostname, target_url8) + + def testGetselfhost(self): + """ + Tests the get_self_host method of the OneLogin_Saml2_Utils + """ + request_data = {} + with self.assertRaisesRegexp(Exception, 'No hostname defined'): + OneLogin_Saml2_Utils.get_self_host(request_data) + + request_data = { + 'server_name': 'example.com' + } + self.assertEqual('example.com', OneLogin_Saml2_Utils.get_self_host(request_data)) + + request_data = { + 'http_host': 'example.com' + } + self.assertEqual('example.com', OneLogin_Saml2_Utils.get_self_host(request_data)) + + request_data = { + 'http_host': 'example.com:443' + } + self.assertEqual('example.com', OneLogin_Saml2_Utils.get_self_host(request_data)) + + request_data = { + 'http_host': 'example.com:ok' + } + self.assertEqual('example.com:ok', OneLogin_Saml2_Utils.get_self_host(request_data)) + + def testisHTTPS(self): + """ + Tests the is_https method of the OneLogin_Saml2_Utils + """ + request_data = { + 'https': 'off' + } + self.assertFalse(OneLogin_Saml2_Utils.is_https(request_data)) + + request_data = { + 'https': 'on' + } + self.assertTrue(OneLogin_Saml2_Utils.is_https(request_data)) + + request_data = { + 'server_port': '80' + } + self.assertFalse(OneLogin_Saml2_Utils.is_https(request_data)) + + request_data = { + 'server_port': '443' + } + self.assertTrue(OneLogin_Saml2_Utils.is_https(request_data)) + + def testGetSelfURLhost(self): + """ + Tests the get_self_url_host method of the OneLogin_Saml2_Utils + """ + request_data = { + 'http_host': 'example.com' + } + self.assertEqual('http://example.com', OneLogin_Saml2_Utils.get_self_url_host(request_data)) + + request_data['server_port'] = '80' + self.assertEqual('http://example.com', OneLogin_Saml2_Utils.get_self_url_host(request_data)) + + request_data['server_port'] = '81' + self.assertEqual('http://example.com:81', OneLogin_Saml2_Utils.get_self_url_host(request_data)) + + request_data['server_port'] = '443' + self.assertEqual('https://example.com', OneLogin_Saml2_Utils.get_self_url_host(request_data)) + + del request_data['server_port'] + request_data['https'] = 'on' + self.assertEqual('https://example.com', OneLogin_Saml2_Utils.get_self_url_host(request_data)) + + request_data['server_port'] = '444' + self.assertEqual('https://example.com:444', OneLogin_Saml2_Utils.get_self_url_host(request_data)) + + request_data['server_port'] = '443' + request_data['request_uri'] = '' + self.assertEqual('https://example.com', OneLogin_Saml2_Utils.get_self_url_host(request_data)) + + request_data['request_uri'] = '/' + self.assertEqual('https://example.com', OneLogin_Saml2_Utils.get_self_url_host(request_data)) + + request_data['request_uri'] = 'onelogin/' + self.assertEqual('https://example.com', OneLogin_Saml2_Utils.get_self_url_host(request_data)) + + request_data['request_uri'] = '/onelogin' + self.assertEqual('https://example.com', OneLogin_Saml2_Utils.get_self_url_host(request_data)) + + request_data['request_uri'] = 'https://example.com/onelogin/sso' + self.assertEqual('https://example.com', OneLogin_Saml2_Utils.get_self_url_host(request_data)) + + request_data2 = { + 'request_uri': 'example.com/onelogin/sso' + } + with self.assertRaisesRegexp(Exception, 'No hostname defined'): + OneLogin_Saml2_Utils.get_self_url_host(request_data2) + + def testGetSelfURL(self): + """ + Tests the get_self_url method of the OneLogin_Saml2_Utils + """ + request_data = { + 'http_host': 'example.com' + } + url = OneLogin_Saml2_Utils.get_self_url_host(request_data) + self.assertEqual(url, OneLogin_Saml2_Utils.get_self_url(request_data)) + + request_data['request_uri'] = '' + self.assertEqual(url, OneLogin_Saml2_Utils.get_self_url(request_data)) + + request_data['request_uri'] = '/' + self.assertEqual(url + '/', OneLogin_Saml2_Utils.get_self_url(request_data)) + + request_data['request_uri'] = 'index.html' + self.assertEqual(url + 'index.html', OneLogin_Saml2_Utils.get_self_url(request_data)) + + request_data['request_uri'] = '?index.html' + self.assertEqual(url + '?index.html', OneLogin_Saml2_Utils.get_self_url(request_data)) + + request_data['request_uri'] = '/index.html' + self.assertEqual(url + '/index.html', OneLogin_Saml2_Utils.get_self_url(request_data)) + + request_data['request_uri'] = '/index.html?testing' + self.assertEqual(url + '/index.html?testing', OneLogin_Saml2_Utils.get_self_url(request_data)) + + request_data['request_uri'] = '/test/index.html?testing' + self.assertEqual(url + '/test/index.html?testing', OneLogin_Saml2_Utils.get_self_url(request_data)) + + request_data['request_uri'] = 'https://example.com/testing' + self.assertEqual(url + '/testing', OneLogin_Saml2_Utils.get_self_url(request_data)) + + def testGetSelfURLNoQuery(self): + """ + Tests the get_self_url_no_query method of the OneLogin_Saml2_Utils + """ + request_data = { + 'http_host': 'example.com', + 'script_name': '/index.html' + } + url = OneLogin_Saml2_Utils.get_self_url_host(request_data) + request_data['script_name'] + self.assertEqual(url, OneLogin_Saml2_Utils.get_self_url_no_query(request_data)) + + request_data['path_info'] = '/test' + self.assertEqual(url + '/test', OneLogin_Saml2_Utils.get_self_url_no_query(request_data)) + + def testGetSelfRoutedURLNoQuery(self): + """ + Tests the get_self_routed_url_no_query method of the OneLogin_Saml2_Utils + """ + request_data = { + 'http_host': 'example.com', + 'request_uri': '/example1/route?x=test', + 'query_string': '?x=test' + } + url = OneLogin_Saml2_Utils.get_self_url_host(request_data) + '/example1/route' + self.assertEqual(url, OneLogin_Saml2_Utils.get_self_routed_url_no_query(request_data)) + + request_data_2 = { + 'http_host': 'example.com', + 'request_uri': '', + } + url_2 = OneLogin_Saml2_Utils.get_self_url_host(request_data_2) + self.assertEqual(url_2, OneLogin_Saml2_Utils.get_self_routed_url_no_query(request_data_2)) + + request_data_3 = { + 'http_host': 'example.com', + } + url_3 = OneLogin_Saml2_Utils.get_self_url_host(request_data_3) + self.assertEqual(url_3, OneLogin_Saml2_Utils.get_self_routed_url_no_query(request_data_3)) + + request_data_4 = { + 'http_host': 'example.com', + 'request_uri': '/example1/route/test/', + 'query_string': '?invalid=1' + } + url_4 = OneLogin_Saml2_Utils.get_self_url_host(request_data_4) + '/example1/route/test/' + self.assertEqual(url_4, OneLogin_Saml2_Utils.get_self_routed_url_no_query(request_data_4)) + + request_data_5 = { + 'http_host': 'example.com', + 'request_uri': '/example1/route/test/', + 'query_string': '' + } + url_5 = OneLogin_Saml2_Utils.get_self_url_host(request_data_5) + '/example1/route/test/' + self.assertEqual(url_5, OneLogin_Saml2_Utils.get_self_routed_url_no_query(request_data_5)) + + request_data_6 = { + 'http_host': 'example.com', + 'request_uri': '/example1/route/test/', + } + url_6 = OneLogin_Saml2_Utils.get_self_url_host(request_data_6) + '/example1/route/test/' + self.assertEqual(url_6, OneLogin_Saml2_Utils.get_self_routed_url_no_query(request_data_6)) + + def testGetStatus(self): + """ + Gets the status of a message + """ + xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64')) + xml = b64decode(xml) + dom = etree.fromstring(xml) + + status = OneLogin_Saml2_Utils.get_status(dom) + self.assertEqual(OneLogin_Saml2_Constants.STATUS_SUCCESS, status['code']) + + xml2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'status_code_responder.xml.base64')) + xml2 = b64decode(xml2) + dom2 = etree.fromstring(xml2) + + status2 = OneLogin_Saml2_Utils.get_status(dom2) + self.assertEqual(OneLogin_Saml2_Constants.STATUS_RESPONDER, status2['code']) + self.assertEqual('', status2['msg']) + + xml3 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'status_code_responer_and_msg.xml.base64')) + xml3 = b64decode(xml3) + dom3 = etree.fromstring(xml3) + + status3 = OneLogin_Saml2_Utils.get_status(dom3) + self.assertEqual(OneLogin_Saml2_Constants.STATUS_RESPONDER, status3['code']) + self.assertEqual('something_is_wrong', status3['msg']) + + xml_inv = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_status.xml.base64')) + xml_inv = b64decode(xml_inv) + dom_inv = etree.fromstring(xml_inv) + + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Missing Status on response'): + OneLogin_Saml2_Utils.get_status(dom_inv) + + xml_inv2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_status_code.xml.base64')) + xml_inv2 = b64decode(xml_inv2) + dom_inv2 = etree.fromstring(xml_inv2) + + with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Missing Status Code on response'): + OneLogin_Saml2_Utils.get_status(dom_inv2) + + def testParseDuration(self): + """ + Tests the parse_duration method of the OneLogin_Saml2_Utils + """ + duration = 'PT1393462294S' + timestamp = 1393876825 + + parsed_duration = OneLogin_Saml2_Utils.parse_duration(duration, timestamp) + self.assertEqual(2787339119, parsed_duration) + + parsed_duration_2 = OneLogin_Saml2_Utils.parse_duration(duration) + self.assertTrue(parsed_duration_2 > parsed_duration) + + invalid_duration = 'PT1Y' + 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) + self.assertEqual(1428091225, parsed_duration_4) + + neg_duration = '-P14M' + parsed_duration_5 = OneLogin_Saml2_Utils.parse_duration(neg_duration, timestamp) + self.assertEqual(1357243225, parsed_duration_5) + + def testParseSAML2Time(self): + """ + Tests the parse_SAML_to_time method of the OneLogin_Saml2_Utils + """ + time = 1386650371 + saml_time = '2013-12-10T04:39:31Z' + self.assertEqual(time, OneLogin_Saml2_Utils.parse_SAML_to_time(saml_time)) + + with self.assertRaisesRegexp(Exception, 'does not match format'): + OneLogin_Saml2_Utils.parse_SAML_to_time('invalidSAMLTime') + + # Now test if toolkit supports miliseconds + saml_time2 = '2013-12-10T04:39:31.120Z' + self.assertEqual(time, OneLogin_Saml2_Utils.parse_SAML_to_time(saml_time2)) + + # Now test if toolkit supports microseconds + saml_time3 = '2013-12-10T04:39:31.120240Z' + self.assertEqual(time, OneLogin_Saml2_Utils.parse_SAML_to_time(saml_time3)) + + # Now test if toolkit supports nanoseconds + saml_time4 = '2013-12-10T04:39:31.120240360Z' + self.assertEqual(time, OneLogin_Saml2_Utils.parse_SAML_to_time(saml_time4)) + + def testParseTime2SAML(self): + """ + Tests the parse_time_to_SAML method of the OneLogin_Saml2_Utils + """ + time = 1386650371 + saml_time = '2013-12-10T04:39:31Z' + self.assertEqual(saml_time, OneLogin_Saml2_Utils.parse_time_to_SAML(time)) + + with self.assertRaisesRegexp(Exception, 'could not convert string to float'): + OneLogin_Saml2_Utils.parse_time_to_SAML('invalidtime') + + def testGetExpireTime(self): + """ + Tests the get_expire_time method of the OneLogin_Saml2_Utils + """ + self.assertEqual(None, OneLogin_Saml2_Utils.get_expire_time()) + self.assertNotEqual(None, OneLogin_Saml2_Utils.get_expire_time('PT360000S')) + + self.assertEqual('1291955971', OneLogin_Saml2_Utils.get_expire_time('PT360000S', '2010-12-10T04:39:31Z')) + self.assertEqual('1291955971', OneLogin_Saml2_Utils.get_expire_time('PT360000S', 1291955971)) + + self.assertNotEqual('3311642371', OneLogin_Saml2_Utils.get_expire_time('PT360000S', '2074-12-10T04:39:31Z')) + self.assertNotEqual('3311642371', OneLogin_Saml2_Utils.get_expire_time('PT360000S', 1418186371)) + + def testQuery(self): + """ + Tests the query method of the OneLogin_Saml2_Utils + """ + xml = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64')) + xml = b64decode(xml) + dom = etree.fromstring(xml) + + assertion_nodes = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/saml:Assertion') + self.assertEqual(1, len(assertion_nodes)) + assertion = assertion_nodes[0] + self.assertIn('Assertion', assertion.tag) + + attribute_statement_nodes = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/saml:Assertion/saml:AttributeStatement') + self.assertEqual(1, len(assertion_nodes)) + attribute_statement = attribute_statement_nodes[0] + self.assertIn('AttributeStatement', attribute_statement.tag) + + attribute_statement_nodes_2 = OneLogin_Saml2_Utils.query(dom, './saml:AttributeStatement', assertion) + self.assertEqual(1, len(attribute_statement_nodes_2)) + attribute_statement_2 = attribute_statement_nodes_2[0] + self.assertEqual(attribute_statement, attribute_statement_2) + + signature_res_nodes = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/ds:Signature') + self.assertEqual(1, len(signature_res_nodes)) + signature_res = signature_res_nodes[0] + self.assertIn('Signature', signature_res.tag) + + signature_nodes = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/saml:Assertion/ds:Signature') + self.assertEqual(1, len(signature_nodes)) + signature = signature_nodes[0] + self.assertIn('Signature', signature.tag) + + signature_nodes_2 = OneLogin_Saml2_Utils.query(dom, './ds:Signature', assertion) + self.assertEqual(1, len(signature_nodes_2)) + signature2 = signature_nodes_2[0] + self.assertNotEqual(signature_res, signature2) + self.assertEqual(signature, signature2) + + signature_nodes_3 = OneLogin_Saml2_Utils.query(dom, './ds:SignatureValue', assertion) + self.assertEqual(0, len(signature_nodes_3)) + + signature_nodes_4 = OneLogin_Saml2_Utils.query(dom, './ds:Signature/ds:SignatureValue', assertion) + self.assertEqual(1, len(signature_nodes_4)) + + signature_nodes_5 = OneLogin_Saml2_Utils.query(dom, './/ds:SignatureValue', assertion) + self.assertEqual(1, len(signature_nodes_5)) + + 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: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' + 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, entity_id, name_id_format, key) + 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 + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + cert_path = settings.get_cert_path() + + key = self.file_contents(cert_path + 'sp.key') + cert = self.file_contents(cert_path + 'sp.crt') + + self.assertEqual(None, OneLogin_Saml2_Utils.calculate_x509_fingerprint(key)) + self.assertEqual('afe71c28ef740bc87425be13a2263d37971da1f9', OneLogin_Saml2_Utils.calculate_x509_fingerprint(cert)) + self.assertEqual('afe71c28ef740bc87425be13a2263d37971da1f9', OneLogin_Saml2_Utils.calculate_x509_fingerprint(cert, 'sha1')) + + self.assertEqual('c51cfa06c7a49767f6eab18238eae1c56708e29264da3d11f538a12cd2c357ba', OneLogin_Saml2_Utils.calculate_x509_fingerprint(cert, 'sha256')) + + self.assertEqual('bc5826e6f9429247254bae5e3c650e6968a36a62d23075eb168134978d88600559c10830c28711b2c29c7947c0c2eb1d', OneLogin_Saml2_Utils.calculate_x509_fingerprint(cert, 'sha384')) + + 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 + """ + global local_session_test + local_session_test = 1 + + OneLogin_Saml2_Utils.delete_local_session() + self.assertEqual(1, local_session_test) + + OneLogin_Saml2_Utils.delete_local_session(self.dscb) + self.assertEqual(0, local_session_test) + + def session_clear(self): + """ + Auxiliar method to test the delete_local_session method of the OneLogin_Saml2_Utils + """ + global local_session_test + local_session_test = 0 + + def testFormatFingerPrint(self): + """ + Tests the format_finger_print method of the OneLogin_Saml2_Utils + """ + finger_print_1 = 'AF:E7:1C:28:EF:74:0B:C8:74:25:BE:13:A2:26:3D:37:97:1D:A1:F9' + self.assertEqual('afe71c28ef740bc87425be13a2263d37971da1f9', OneLogin_Saml2_Utils.format_finger_print(finger_print_1)) + + finger_print_2 = 'afe71c28ef740bc87425be13a2263d37971da1f9' + self.assertEqual('afe71c28ef740bc87425be13a2263d37971da1f9', OneLogin_Saml2_Utils.format_finger_print(finger_print_2)) + + def testDecryptElement(self): + """ + Tests the decrypt_element method of the OneLogin_Saml2_Utils + """ + 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_nameid_enc = parseString(xml_nameid_enc) + encrypted_nameid_nodes = dom_nameid_enc.getElementsByTagName('saml:EncryptedID') + 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.assertIn('NameID', decrypted_nameid.tag) + self.assertEqual('2de11defd199f8d5bb63f9b7deb265ba5c675c10', decrypted_nameid.text) + + decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data_str, key) + 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'))) + dom_assertion_enc = parseString(xml_assertion_enc) + encrypted_assertion_enc_nodes = dom_assertion_enc.getElementsByTagName('saml:EncryptedAssertion') + encrypted_data_assert = encrypted_assertion_enc_nodes[0].firstChild + + decrypted_assertion = OneLogin_Saml2_Utils.decrypt_element(encrypted_data_assert, key) + self.assertEqual('{%s}Assertion' % (OneLogin_Saml2_Constants.NS_SAML), decrypted_assertion.tag) + self.assertEqual('_6fe189b1c241827773902f2b1d3a843418206a5c97', decrypted_assertion.get('ID')) + + decrypted_assertion.xpath('/saml:Assertion/saml:EncryptedID', namespaces=OneLogin_Saml2_Constants.NSMAP) + 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.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') + f = open(key_2_file_name, 'r') + key2 = f.read() + f.close() + + # 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', 'sp3.key') + f = open(key_3_file_name, 'r') + key3 = f.read() + f.close() + + # 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 + + with self.assertRaisesRegexp(Exception, "('failed to decrypt', -1)"): + OneLogin_Saml2_Utils.decrypt_element(encrypted_data_2, key) + + 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 + + with self.assertRaisesRegexp(Exception, "('failed to decrypt', -1)"): + OneLogin_Saml2_Utils.decrypt_element(encrypted_data_3, key) + + 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): + """ + Tests the add_sign method of the OneLogin_Saml2_Utils + """ + 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) + + xml_authn_dom = parseString(xml_authn) + xml_authn_signed_2 = OneLogin_Saml2_Utils.add_sign(xml_authn_dom, key, cert) + xml_authn_signed_3 = OneLogin_Saml2_Utils.add_sign(xml_authn_dom.firstChild, key, cert) + + xml_authn_etree = etree.fromstring(xml_authn) + xml_authn_signed_4 = OneLogin_Saml2_Utils.add_sign(xml_authn_etree, key, cert) + + 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) + + 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) + + xml_signed_saml_messages = [xml_authn_signed, xml_authn_signed_2, xml_authn_signed_3, xml_authn_signed_4, xml_logout_req_signed, xml_logout_res_signed] + + for xml_signed_saml_message in xml_signed_saml_messages: + self.assertIn('', xml_signed_saml_message) + signed_saml_message = parseString(xml_signed_saml_message) + ds_signature = signed_saml_message.firstChild.firstChild.nextSibling.nextSibling + if ds_signature.nodeType == signed_saml_message.TEXT_NODE: + ds_signature = ds_signature.nextSibling + self.assertEqual('ds:Signature', ds_signature.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 + self.assertEqual('ds:Signature', ds_signature_8.tagName) + + with self.assertRaisesRegexp(Exception, 'Error parsing xml string'): + OneLogin_Saml2_Utils.add_sign(1, key, cert) + + 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): + """ + Tests the validate_sign method of the OneLogin_Saml2_Utils + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + idp_data = settings.get_idp_data() + cert = idp_data['x509cert'] + + settings_2 = OneLogin_Saml2_Settings(self.loadSettingsJSON('settings2.json')) + idp_data2 = settings_2.get_idp_data() + cert_2 = idp_data2['x509cert'] + fingerprint_2 = OneLogin_Saml2_Utils.calculate_x509_fingerprint(cert_2) + fingerprint_2_256 = OneLogin_Saml2_Utils.calculate_x509_fingerprint(cert_2, 'sha256') + + self.assertFalse(OneLogin_Saml2_Utils.validate_sign('', cert, raise_exceptions=False)) + self.assertFalse(OneLogin_Saml2_Utils.validate_sign(1, cert, raise_exceptions=False)) + + 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_metadata_sign(xml_metadata_signed, cert)) + # expired cert, verified it + 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_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'))) + + # expired cert + 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') + f = open(other_cert_path + '/certificate1', 'r') + cert_x = f.read() + 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)) + self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed_2, None, fingerprint_2)) + self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed_2, None, fingerprint_2, 'sha1')) + self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed_2, None, fingerprint_2_256, 'sha256')) + + xml_response_assert_signed = b64decode(self.file_contents(join(self.data_path, 'responses', 'signed_assertion_response.xml.base64'))) + + # expired cert + 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)) + self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed_2, None, fingerprint_2)) + + xml_response_double_signed = b64decode(self.file_contents(join(self.data_path, 'responses', 'double_signed_response.xml.base64'))) + + # expired cert + 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)) + self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed_2, None, fingerprint_2)) + + dom = parseString(xml_response_msg_signed_2) + self.assertTrue(OneLogin_Saml2_Utils.validate_sign(dom, cert_2)) + + dom.firstChild.firstChild.firstChild.nodeValue = 'https://idp.example.com/simplesaml/saml2/idp/metadata.php' + + 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_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.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) diff --git a/tests/src/__init__.py b/tests/src/__init__.py new file mode 100644 index 00000000..e69de29b