Przeglądaj źródła

Hue [py3] Adding djangosaml2 python module

Prakash Ranade 3 lat temu
rodzic
commit
5204cd63f9
45 zmienionych plików z 4507 dodań i 0 usunięć
  1. 256 0
      desktop/core/ext-py3/djangosaml2-0.18.0/CHANGES
  2. 202 0
      desktop/core/ext-py3/djangosaml2-0.18.0/COPYING
  3. 6 0
      desktop/core/ext-py3/djangosaml2-0.18.0/MANIFEST.in
  4. 584 0
      desktop/core/ext-py3/djangosaml2-0.18.0/PKG-INFO
  5. 545 0
      desktop/core/ext-py3/djangosaml2-0.18.0/README.rst
  6. 0 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/__init__.py
  7. 22 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/acs_failures.py
  8. 278 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/backends.py
  9. 89 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/cache.py
  10. 70 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/conf.py
  11. 2 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/exceptions.py
  12. 1 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/models.py
  13. 24 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/overrides.py
  14. 20 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/signals.py
  15. 16 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/templates/djangosaml2/auth_error.html
  16. 18 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/templates/djangosaml2/echo_attributes.html
  17. 15 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/templates/djangosaml2/example_post_binding_form.html
  18. 13 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/templates/djangosaml2/login_error.html
  19. 17 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/templates/djangosaml2/logout_error.html
  20. 16 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/templates/djangosaml2/wayf.html
  21. 0 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/templatetags/__init__.py
  22. 45 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/templatetags/idplist.py
  23. 567 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/__init__.py
  24. 243 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/attribute-maps/saml_uri.py
  25. 103 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/auth_response.py
  26. 104 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/conf.py
  27. 17 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/idpcert.csr
  28. 27 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/idpcert.key
  29. 20 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/idpcert.pem
  30. 17 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/mycert.csr
  31. 27 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/mycert.key
  32. 20 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/mycert.pem
  33. 123 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/remote_metadata.xml
  34. 33 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/remote_metadata_one_idp.xml
  35. 93 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/remote_metadata_three_idps.xml
  36. 42 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/sp_metadata.xml
  37. 16 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/spcert.csr
  38. 27 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/spcert.key
  39. 19 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/spcert.pem
  40. 31 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/urls.py
  41. 27 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/urls.py
  42. 90 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/utils.py
  43. 535 0
      desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/views.py
  44. 7 0
      desktop/core/ext-py3/djangosaml2-0.18.0/setup.cfg
  45. 80 0
      desktop/core/ext-py3/djangosaml2-0.18.0/setup.py

+ 256 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/CHANGES

@@ -0,0 +1,256 @@
+Changes
+=======
+0.18.0 (2020-02-14)
+----------
+- Django 3.0 support. Thanks to OskarPersson
+- forceauthn and allowcreate support. Thanks to peppelinux
+- Dropped support for Python 3.4
+- Also thanks to WebSpider, mhindery, DylannCordel, habi3000 for various fixes and improvements
+
+Thanks to plumdog
+
+0.17.2 (2018-08-29)
+----------
+- Upgraded pysaml2 dependency to version 4.6.0 which fixes security issue.
+
+Thanks to plumdog
+
+0.17.1 (2018-07-16)
+----------
+- A 403 (permission denied) is now raised if a SAMLResponse is replayed, instead of 500.
+- Dropped support for Python 3.3
+- Upgraded pysaml2 dependency to version 4.5.0
+
+Thanks to francoisfreitag, mhindery, vkurup, peppelinux
+
+0.16.11 (2017-12-25)
+----------
+- Dropped compatibility for Python < 2.7 and Django < 1.8.
+- Added a clean_attributes hook allowing backends to restructure attributes extracted from SAML response.
+- Log when fields are missing in a SAML response.
+- Log when attribute_mapping maps to nonexistent User fields.
+- Multiple compatibility fixes and other minor improvements and code cleanups
+
+Thanks to francoisfreitag, mhindery, charn, jdufresne
+
+0.16.10 (2017-10-02)
+-------------------
+- Bugfixes and internal refactorings.
+- Added support for custom USERNAME_FIELD on custom User models. Many thanks to francoisfreitag.
+
+0.16.9 (2017-09-19)
+-------------------
+- Bugfixes and minor improvements. Thanks to goetzk and AmbientLighter.
+- Added option SAML_LOGOUT_REQUEST_PREFERRED_BINDING
+- Added Django 1.11 to tox.
+
+0.16.4 (2017-09-11)
+-------------------
+- Added support for SHA-256 signing. Thanks to WebSpider.
+- Bugfixes. Thanks to justinsg and charn.
+- Error handling made more extensible. This will be further improved in next versions.
+
+0.16.1 (2017-07-15)
+-------------------
+- Bugfixes. Thanks to canni, AmbientLighter, cranti and logston.
+- request is now passed to authentication backend (introduced in Django 1.11). Thanks to terite.
+
+0.16.0 (2017-04-14)
+-------------------
+- Upgrade pysaml2 dependency to version 4.4.0 which fixes some serialization issues. Thanks to nakato for the report.
+- Added support for HTTP Redirect binding with signed authentication requests. Many thanks to liquidpele for this feature and other related refactorings.
+- The custom permission_denied.html template was removed in favor of standard PermissionDenied exception. Thanks to mhindery.
+
+0.15.0 (2016-12-18)
+-------------------
+- Python 3.5 support. Thanks to timheap.
+- Added support for callable user attributes. Thanks to andy-miracl and joetsoi.
+- Security improvement: "next" URL is now checked. thanks to flupzor.
+- Improved testability. Thanks to flupzor.
+- Other bugfixes and minor improvements. Thanks to jamaalscarlett, ws0w, jaywink and liquidpele.
+
+0.14.5 (2016-09-19)
+-------------------
+- Django 1.10 support. Thanks to inducer.
+- Various fixes and minor improvements. Thanks to ajsmilutin, ganiserb, inducer, grunichev, liquidpele and darbula
+
+0.14.4 (2016-03-29)
+-------------------
+- Fix compatibility issue with pysaml2-4.0.3+. Thanks to jimr and astoltz.
+- Fix Django 1.9 compatibility issue in templates. Thanks to nikoskal.
+
+0.14.3 (2016-03-18)
+-------------------
+- Upgraded to pysaml2-4.0.5.
+- Added 'ACS_DEFAULT_REDIRECT_URL' setting for default redirection after successful authentication. Thanks to ganiserb.
+
+0.14.2 (2016-03-11)
+-------------------
+- Released under the original 'djangosaml2' package name; abandoning the djangosaml2-knaperek fork.
+
+0.14.1 (2016-03-09)
+-------------------
+- Upgraded to pysaml2-4.0.4.
+
+0.14.0 (2016-01-28)
+-------------------
+- Upgrade to pysaml2-4.0.2. Thanks to kviktor
+- Django 1.9 support. Thanks to Jordi Gutiérrez Hermoso
+
+0.13.2 (2015-06-24)
+-------------------
+- Improved usage of standard Python logging.
+
+0.13.1 (2015-06-05)
+-------------------
+- Added support for djangosaml2 specific user model defined by SAML_USER_MODEL setting
+
+0.13.0 (2015-02-12)
+-------------------
+- Django 1.7 support. Thanks to Kamei Toshimitsu
+
+0.12.0 (2014-11-18)
+-------------------
+- Pysaml2 2.2.0 support. Thanks to Erick Tryzelaar
+
+0.11.0 (2014-06-15)
+-------------------
+- Django 1.5 custom user model support. Thanks to Jos van Velzen
+- Django 1.5 compatibility url template tag. Thanks to bula
+- Support Django 1.5 and 1.6. Thanks to David Evans and Justin Quick
+
+0.10.0 (2013-05-05)
+-------------------
+- Check that RelayState is not empty before redirecting into a loop. Thanks
+  to Sam Bull for reporting this issue.
+- In the global logout process, when the session is lost, report an error
+  message to the user and perform a local logout.
+
+0.9.2 (2013-04-19)
+------------------
+- Upgrade to pysaml2-0.4.3.
+
+0.9.1 (2013-01-29)
+------------------
+- Add a method to the authentication backend so it is possible
+  to customize the authorization based on SAML attributes.
+
+0.9.0 (2012-10-30)
+------------------
+- Add a signal for modifying the user just before saving it on
+  the update_user method of the authentication backend.
+
+0.8.1 (2012-10-29)
+------------------
+- Trim the SAML attributes before setting them to the Django objects
+  if they are too long. This fixes a crash with MySQL.
+
+0.8.0 (2012-10-25)
+------------------
+- Allow to use different attributes besides 'username' to look for
+  existing users.
+
+0.7.0 (2012-10-19)
+------------------
+- Add a setting to decide if the user should be redirected to the
+  next view or shown an authorization error when the user tries to
+  login twice.
+
+0.6.1 (2012-09-03)
+------------------
+- Remove Django from our dependencies
+- Restore support for Django 1.3
+
+0.6.0 (2012-08-29)
+------------------
+- Add tox support configured to run the tests with Python 2.6 and 2.7
+- Fix some dependencies and sdist generation. Lorenzo Gil
+- Allow defining a logout redirect url in the settings. Lorenzo Gil
+- Add some logging calls to improve debugging. Lorenzo Gil
+- Add support for custom conf loading function. Sam Bull.
+- Make the tests more robust and easier to run when djangosaml2 is
+  included in a Django project. Sam Bull.
+- Make sure the profile is not None before saving it. Bug reported by
+  Leif Johansson
+
+0.5.0 (2012-05-22)
+------------------
+- Allow defining custom config loaders. They can be dynamic depending on
+  the request.
+- Do not automatically add the authentication backend. This way
+  we allow other people to add their own backends.
+- Support for additional attributes other than the ones that get mapped
+  into the User model. Those attributes get stored in the UserProfile model.
+
+0.4.2 (2012-03-23)
+------------------
+- Fix a crash in the idplist templatetag about using an old pysaml2 function
+- Added a test for the previous crash
+
+0.4.1 (2012-03-19)
+------------------
+- Upgrade pysaml2 dependency to version 0.4.1
+
+0.4.0 (2012-03-18)
+------------------
+- Upgrade pysaml2 dependency to version 0.4.0 (update our tests as a result
+  of this)
+- Add logging calls to make debugging easier
+- Use the Django configured logger in pysaml2
+
+0.3.3 (2012-02-14)
+------------------
+- Freeze the version of pysaml2 since we are not (yet!) compatible with
+  version 0.4.0
+
+0.3.2 (2011-12-13)
+------------------
+- Avoid a crash when reading the SAML attribute that maps to the Django
+  username
+
+0.3.1 (2011-12-01)
+------------------
+- Load the config in the render method of the idplist templatetag to
+  make it more flexible and reentrant.
+
+0.3.0 (2011-11-30)
+------------------
+- Templatetag to get the list of available idps.
+- Allow to map the same SAML attribute into several Django field.
+
+0.2.4 (2011-11-29)
+------------------
+- Fix restructured text bugs that made pypi page looks bad.
+
+0.2.3 (2011-06-14)
+------------------
+- Set a unusable password when the user is created for the first time
+
+0.2.2 (2011-06-07)
+------------------
+- Prevent infinite loop when going to the /saml2/login/ endpoint and the user
+  is already logged in and the settings.LOGIN_REDIRECT_URL is (badly) pointing
+  to /saml2/login.
+
+0.2.1 (2011-05-09)
+------------------
+- If no next parameter is supplied to the login view, use the
+  settings.LOGIN_REDIRECT_URL as default
+
+0.2.0 (2011-04-26)
+------------------
+- Python 2.4 compatible if the elementtree library is installed
+- Allow post processing after the authentication phase by using
+  Django signals.
+
+0.1.1 (2011-04-18)
+------------------
+- Simple view to echo SAML attributes
+- Improve documentation
+- Change default behaviour when a new user is created. Now their attributes
+  are filled this first time
+- Allow to set a next page after the logout
+
+0.1.0 (2011-03-16)
+------------------
+- Emancipation from the pysaml package

+ 202 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/COPYING

@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 6 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/MANIFEST.in

@@ -0,0 +1,6 @@
+include README
+include CHANGES
+include COPYING
+global-include *.html *.csr *.key *.pem *.xml
+include djangosaml2/tests/attribute-maps/*.py
+global-exclude *.pyc

+ 584 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/PKG-INFO

@@ -0,0 +1,584 @@
+Metadata-Version: 2.1
+Name: djangosaml2
+Version: 0.18.0
+Summary: pysaml2 integration for Django
+Home-page: https://github.com/knaperek/djangosaml2
+Author: Yaco Sistemas and independent contributors
+Author-email: lgs@yaco.es
+Maintainer: Jozef Knaperek
+License: Apache 2.0
+Download-URL: https://pypi.org/project/djangosaml2/
+Description: ===========
+        djangosaml2
+        ===========
+        
+        .. image:: https://travis-ci.org/knaperek/djangosaml2.svg?branch=master
+            :target: https://travis-ci.org/knaperek/djangosaml2
+            :align: left
+        
+        
+        djangosaml2 is a Django application that integrates the PySAML2 library
+        into your project. This mean that you can protect your Django based project
+        with a service provider based on PySAML. This way it will talk SAML2 with
+        your Identity Provider allowing you to use this authentication mechanism.
+        This document will guide you through a few simple steps to accomplish
+        such goal.
+        
+        .. contents::
+        
+        Installation
+        ============
+        
+        PySAML2 uses xmlsec1_ binary to sign SAML assertions so you need to install
+        it either through your operating system package or by compiling the source
+        code. It doesn't matter where the final executable is installed because
+        you will need to set the full path to it in the configuration stage.
+        
+        .. _xmlsec1: http://www.aleksey.com/xmlsec/
+        
+        Now you can install the djangosaml2 package using easy_install or pip. This
+        will also install PySAML2 and its dependencies automatically.
+        
+        
+        Configuration
+        =============
+        
+        There are three things you need to setup to make djangosaml2 work in your
+        Django project:
+        
+        1. **settings.py** as you may already know, it is the main Django
+           configuration file.
+        2. **urls.py** is the file where you will include djangosaml2 urls.
+        3. **pysaml2** specific files such as an attribute map directory and a
+           certificate.
+        
+        
+        Changes in the settings.py file
+        -------------------------------
+        The first thing you need to do is add ``djangosaml2`` to the list of
+        installed apps::
+        
+          INSTALLED_APPS = (
+              'django.contrib.auth',
+              'django.contrib.contenttypes',
+              'django.contrib.sessions',
+              'django.contrib.sites',
+              'django.contrib.messages',
+              'django.contrib.admin',
+              'djangosaml2',  # new application
+          )
+        
+        Actually this is not really required since djangosaml2 does not include
+        any data model. The only reason we include it is to be able to run
+        djangosaml2 test suite from our project, something you should always
+        do to make sure it is compatible with your Django version and environment.
+        
+        .. note::
+        
+          When you finish the configuration you can run the djangosaml2 test suite as
+          you run any other Django application test suite. Just type ``python manage.py
+          test djangosaml2``.
+        
+          Python 2 users need to ``pip install djangosaml2[test]`` in order to run the
+          tests.
+        
+        Then you have to add the ``djangosaml2.backends.Saml2Backend``
+        authentication backend to the list of authentications backends.
+        By default only the ModelBackend included in Django is configured.
+        A typical configuration would look like this::
+        
+          AUTHENTICATION_BACKENDS = (
+              'django.contrib.auth.backends.ModelBackend',
+              'djangosaml2.backends.Saml2Backend',
+          )
+        
+        .. note::
+        
+          Before djangosaml2 0.5.0 this authentication backend was
+          automatically added by djangosaml2. This turned out to be
+          a bad idea since some applications want to use their own
+          custom policies for authorization and the authentication
+          backend is a good place to define that. Starting from
+          djangosaml2 0.5.0 it is now possible to define such
+          backends.
+        
+        Finally we have to tell Django what the new login url we want to use is::
+        
+          LOGIN_URL = '/saml2/login/'
+          SESSION_EXPIRE_AT_BROWSER_CLOSE = True
+        
+        Here we are telling Django that any view that requires an authenticated
+        user should redirect the user browser to that url if the user has not
+        been authenticated before. We are also telling that when the user closes
+        his browser, the session should be terminated. This is useful in SAML2
+        federations where the logout protocol is not always available.
+        
+        .. note::
+        
+          The login url starts with ``/saml2/`` as an example but you can change that
+          if you want. Check the section about changes in the ``urls.py``
+          file for more information.
+        
+        If you want to allow several authentication mechanisms in your project
+        you should set the LOGIN_URL option to another view and put a link in such
+        view to the ``/saml2/login/`` view.
+        
+        Preferred Logout binding
+        ------------------------
+        Use the following setting to choose your preferred binding for SP initiated logout requests::
+        
+          SAML_LOGOUT_REQUEST_PREFERRED_BINDING
+        
+        For example::
+        
+          import saml2
+          SAML_LOGOUT_REQUEST_PREFERRED_BINDING = saml2.BINDING_HTTP_POST
+        
+        Changes in the urls.py file
+        ---------------------------
+        
+        The next thing you need to do is to include ``djangosaml2.urls`` module in your
+        main ``urls.py`` module::
+        
+          urlpatterns = patterns(
+              '',
+              #  lots of url definitions here
+        
+              (r'^saml2/', include('djangosaml2.urls')),
+        
+              #  more url definitions
+          )
+        
+        As you can see we are including ``djangosaml2.urls`` under the *saml2*
+        prefix. Feel free to use your own prefix but be consistent with what
+        you have put in the ``settings.py`` file in the LOGIN_URL parameter.
+        
+        
+        PySAML2 specific files and configuration
+        ----------------------------------------
+        Once you have finished configuring your Django project you have to
+        start configuring PySAML. If you use just that library you have to
+        put your configuration options in a file and initialize PySAML2 with
+        the path to that file.
+        
+        In djangosaml2 you just put the same information in the Django
+        settings.py file under the SAML_CONFIG option.
+        
+        We will see a typical configuration for protecting a Django project::
+        
+          from os import path
+          import saml2
+          import saml2.saml
+          BASEDIR = path.dirname(path.abspath(__file__))
+          SAML_CONFIG = {
+            # full path to the xmlsec1 binary programm
+            'xmlsec_binary': '/usr/bin/xmlsec1',
+        
+            # your entity id, usually your subdomain plus the url to the metadata view
+            'entityid': 'http://localhost:8000/saml2/metadata/',
+        
+            # directory with attribute mapping
+            'attribute_map_dir': path.join(BASEDIR, 'attribute-maps'),
+        
+            # this block states what services we provide
+            'service': {
+                # we are just a lonely SP
+                'sp' : {
+                    'name': 'Federated Django sample SP',
+                    'name_id_format': saml2.saml.NAMEID_FORMAT_PERSISTENT,
+                    'endpoints': {
+                        # url and binding to the assetion consumer service view
+                        # do not change the binding or service name
+                        'assertion_consumer_service': [
+                            ('http://localhost:8000/saml2/acs/',
+                             saml2.BINDING_HTTP_POST),
+                            ],
+                        # url and binding to the single logout service view
+                        # do not change the binding or service name
+                        'single_logout_service': [
+                            ('http://localhost:8000/saml2/ls/',
+                             saml2.BINDING_HTTP_REDIRECT),
+                            ('http://localhost:8000/saml2/ls/post',
+                             saml2.BINDING_HTTP_POST),
+                            ],
+                        },
+                     # Mandates that the identity provider MUST authenticate the
+                     # presenter directly rather than rely on a previous security context.
+                    'force_authn': False, 
+                    
+                     # Enable AllowCreate in NameIDPolicy.
+                    'name_id_format_allow_create': False,
+                    
+                     # attributes that this project need to identify a user
+                    'required_attributes': ['uid'],
+        
+                     # attributes that may be useful to have but not required
+                    'optional_attributes': ['eduPersonAffiliation'],
+        
+                    # in this section the list of IdPs we talk to are defined
+                    'idp': {
+                        # we do not need a WAYF service since there is
+                        # only an IdP defined here. This IdP should be
+                        # present in our metadata
+        
+                        # the keys of this dictionary are entity ids
+                        'https://localhost/simplesaml/saml2/idp/metadata.php': {
+                            'single_sign_on_service': {
+                                saml2.BINDING_HTTP_REDIRECT: 'https://localhost/simplesaml/saml2/idp/SSOService.php',
+                                },
+                            'single_logout_service': {
+                                saml2.BINDING_HTTP_REDIRECT: 'https://localhost/simplesaml/saml2/idp/SingleLogoutService.php',
+                                },
+                            },
+                        },
+                    },
+                },
+        
+            # where the remote metadata is stored
+            'metadata': {
+                'local': [path.join(BASEDIR, 'remote_metadata.xml')],
+                },
+        
+            # set to 1 to output debugging information
+            'debug': 1,
+        
+            # Signing
+            'key_file': path.join(BASEDIR, 'mycert.key'),  # private part
+            'cert_file': path.join(BASEDIR, 'mycert.pem'),  # public part
+        
+            # Encryption
+            'encryption_keypairs': [{
+                'key_file': path.join(BASEDIR, 'my_encryption_key.key'),  # private part
+                'cert_file': path.join(BASEDIR, 'my_encryption_cert.pem'),  # public part
+            }],
+        
+            # own metadata settings
+            'contact_person': [
+                {'given_name': 'Lorenzo',
+                 'sur_name': 'Gil',
+                 'company': 'Yaco Sistemas',
+                 'email_address': 'lgs@yaco.es',
+                 'contact_type': 'technical'},
+                {'given_name': 'Angel',
+                 'sur_name': 'Fernandez',
+                 'company': 'Yaco Sistemas',
+                 'email_address': 'angel@yaco.es',
+                 'contact_type': 'administrative'},
+                ],
+            # you can set multilanguage information here
+            'organization': {
+                'name': [('Yaco Sistemas', 'es'), ('Yaco Systems', 'en')],
+                'display_name': [('Yaco', 'es'), ('Yaco', 'en')],
+                'url': [('http://www.yaco.es', 'es'), ('http://www.yaco.com', 'en')],
+                },
+            'valid_for': 24,  # how long is our metadata valid
+            }
+        
+        .. note::
+        
+          Please check the `PySAML2 documentation`_ for more information about
+          these and other configuration options.
+        
+        .. _`PySAML2 documentation`: http://pysaml2.readthedocs.io/en/latest/
+        
+        There are several external files and directories you have to create according
+        to this configuration.
+        
+        The xmlsec1 binary was mentioned in the installation section. Here, in the
+        configuration part you just need to put the full path to xmlsec1 so PySAML2
+        can call it as it needs.
+        
+        The ``attribute_map_dir`` points to a directory with attribute mappings that
+        are used to translate user attribute names from several standards. It's usually
+        safe to just copy the default PySAML2 attribute maps that you can find in the
+        ``tests/attributemaps`` directory of the source distribution.
+        
+        The ``metadata`` option is a dictionary where you can define several types of
+        metadata for remote entities. Usually the easiest type is the ``local`` where
+        you just put the name of a local XML file with the contents of the remote
+        entities metadata. This XML file should be in the SAML2 metadata format.
+        
+        The ``key_file`` and ``cert_file`` options reference the two parts of a
+        standard x509 certificate. You need it to sign your metadata. For assertion
+        encryption/decryption support please configure another set of ``key_file`` and
+        ``cert_file``, but as inner attributes of ``encryption_keypairs`` option.
+        
+        .. note::
+        
+          Check your openssl documentation to generate a test certificate but don't
+          forget to order a real one when you go into production.
+        
+        
+        Custom and dynamic configuration loading
+        ........................................
+        
+        By default, djangosaml2 reads the pysaml2 configuration options from the
+        SAML_CONFIG setting but sometimes you want to read this information from
+        another place, like a file or a database. Sometimes you even want this
+        configuration to be different depending on the request.
+        
+        Starting from djangosaml2 0.5.0 you can define your own configuration
+        loader which is a callable that accepts a request parameter and returns
+        a saml2.config.SPConfig object. In order to do so you set the following
+        setting::
+        
+          SAML_CONFIG_LOADER = 'python.path.to.your.callable'
+        
+        
+        User attributes
+        ---------------
+        
+        In the SAML 2.0 authentication process the Identity Provider (IdP) will
+        send a security assertion to the Service Provider (SP) upon a successful
+        authentication. This assertion contains attributes about the user that
+        was authenticated. It depends on the IdP configuration what exact
+        attributes are sent to each SP it can talk to.
+        
+        When such assertion is received on the Django side it is used to find a Django
+        user and create a session for it. By default djangosaml2 will do a query on the
+        User model with the USERNAME_FIELD_ attribute but you can change it to any
+        other attribute of the User model. For example, you can do this lookup using
+        the 'email' attribute. In order to do so you should set the following setting::
+        
+          SAML_DJANGO_USER_MAIN_ATTRIBUTE = 'email'
+        
+        .. _USERNAME_FIELD: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#django.contrib.auth.models.CustomUser.USERNAME_FIELD
+        
+        Please, use an unique attribute when setting this option. Otherwise
+        the authentication process may fail because djangosaml2 will not know
+        which Django user it should pick.
+        
+        If your main attribute is something inherently case-insensitive (such as
+        an email address), you may set::
+        
+          SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP = '__iexact'
+        
+        (This is simply appended to the main attribute name to form a Django
+        query. Your main attribute must be unique even given this lookup.)
+        
+        Another option is to use the SAML2 name id as the username by setting::
+        
+          SAML_USE_NAME_ID_AS_USERNAME = True
+        
+        You can configure djangosaml2 to create such user if it is not already in
+        the Django database or maybe you don't want to allow users that are not
+        in your database already. For this purpose there is another option you
+        can set in the settings.py file::
+        
+          SAML_CREATE_UNKNOWN_USER = True
+        
+        This setting is True by default.
+        
+          ACS_DEFAULT_REDIRECT_URL = reverse_lazy('some_url_name')
+        
+        This setting lets you specify a URL for redirection after a successful
+        authentication. Particularly useful when you only plan to use
+        IdP initiated login and the IdP does not have a configured RelayState
+        parameter. The default is ``/``.
+        
+        The other thing you will probably want to configure is the mapping of
+        SAML2 user attributes to Django user attributes. By default only the
+        User.username attribute is mapped but you can add more attributes or
+        change that one. In order to do so you need to change the
+        SAML_ATTRIBUTE_MAPPING option in your settings.py::
+        
+          SAML_ATTRIBUTE_MAPPING = {
+              'uid': ('username', ),
+              'mail': ('email', ),
+              'cn': ('first_name', ),
+              'sn': ('last_name', ),
+          }
+        
+        where the keys of this dictionary are SAML user attributes and the values
+        are Django User attributes.
+        
+        If you are using Django user profile objects to store extra attributes
+        about your user you can add those attributes to the SAML_ATTRIBUTE_MAPPING
+        dictionary. For each (key, value) pair, djangosaml2 will try to store the
+        attribute in the User model if there is a matching field in that model.
+        Otherwise it will try to do the same with your profile custom model. For
+        multi-valued attributes only the first value is assigned to the destination field.
+        
+        Alternatively, custom processing of attributes can be achieved by setting the
+        value(s) in the SAML_ATTRIBUTE_MAPPING, to name(s) of method(s) defined on a
+        custom django User object. In this case, each method is called by djangosaml2,
+        passing the full list of attribute values extracted from the <saml:AttributeValue>
+        elements of the <saml:Attribute>. Among other uses, this is a useful way to process
+        multi-valued attributes such as lists of user group names.
+        
+        For example:
+        
+        Saml assertion snippet::
+        
+          <saml:Attribute Name="groups" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+                <saml:AttributeValue>group1</saml:AttributeValue>
+                <saml:AttributeValue>group2</saml:AttributeValue>
+                <saml:AttributeValue>group3</saml:AttributeValue>
+          </saml:Attribute>
+        
+        Custom User object::
+        
+          from django.contrib.auth.models import AbstractUser
+        
+          class User(AbstractUser):
+        
+            def process_groups(self, groups):
+              // process list of group names in argument 'groups'
+              pass;
+        
+        settings.py::
+        
+          SAML_ATTRIBUTE_MAPPING = {
+              'groups': ('process_groups', ),
+          }
+        
+        
+        Learn more about Django profile models at:
+        
+        https://docs.djangoproject.com/en/dev/topics/auth/customizing/#substituting-a-custom-user-model
+        
+        
+        Sometimes you need to use special logic to update the user object
+        depending on the SAML2 attributes and the mapping described above
+        is simply not enough. For these cases djangosaml2 provides a Django
+        signal that you can listen to. In order to do so you can add the
+        following code to your app::
+        
+          from djangosaml2.signals import pre_user_save
+        
+          def custom_update_user(sender=User, instance, attributes, user_modified, **kargs)
+             ...
+             return True  # I modified the user object
+        
+        
+        Your handler will receive the user object, the list of SAML attributes
+        and a flag telling you if the user is already modified and need
+        to be saved after your handler is executed. If your handler
+        modifies the user object it should return True. Otherwise it should
+        return False. This way djangosaml2 will know if it should save
+        the user object so you don't need to do it and no more calls to
+        the save method are issued.
+        
+        
+        IdP setup
+        =========
+        Congratulations, you have finished configuring the SP side of the federation.
+        Now you need to send the entity id and the metadata of this new SP to the
+        IdP administrators so they can add it to their list of trusted services.
+        
+        You can get this information starting your Django development server and
+        going to the http://localhost:8000/saml2/metadata url. If you have included
+        the djangosaml2 urls under a different url prefix you need to correct this
+        url.
+        
+        SimpleSAMLphp issues
+        --------------------
+        As of SimpleSAMLphp 1.8.2 there is a problem if you specify attributes in
+        the SP configuration. When the SimpleSAMLphp metadata parser converts the
+        XML into its custom php format it puts the following option::
+        
+          'attributes.NameFormat' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri'
+        
+        But it need to be replaced by this one::
+        
+          'AttributeNameFormat' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri'
+        
+        Otherwise the Assertions sent from the IdP to the SP will have a wrong
+        Attribute Name Format and pysaml2 will be confused.
+        
+        Furthermore if you have a AttributeLimit filter in your SimpleSAMLphp
+        configuration  you will need to enable another attribute filter just
+        before to make sure that the AttributeLimit does not remove the attributes
+        from the authentication source. The filter you need to add is an AttributeMap
+        filter like this::
+        
+          10 => array(
+                     'class' => 'core:AttributeMap', 'name2oid'
+                ),
+        
+        Testing
+        =======
+        
+        One way to check if everything is working as expected is to enable the
+        following url::
+        
+          urlpatterns = patterns(
+              '',
+              #  lots of url definitions here
+        
+              (r'^saml2/', include('djangosaml2.urls')),
+              (r'^test/', 'djangosaml2.views.echo_attributes'),
+        
+              #  more url definitions
+          )
+        
+        
+        Now if you go to the /test/ url you will see your SAML attributes and also
+        a link to do a global logout.
+        
+        You can also run the unit tests with the following command::
+        
+          python tests/run_tests.py
+        
+        If you have `tox`_ installed you can simply call tox inside the root directory
+        and it will run the tests in multiple versions of Python.
+        
+        .. _`tox`: http://pypi.python.org/pypi/tox
+        
+        FAQ
+        ===
+        
+        **Why can't SAML be implemented as an Django Authentication Backend?**
+        
+        well SAML authentication is not that simple as a set of credentials you can
+        put on a login form and get a response back. Actually the user password is
+        not given to the service provider at all. This is by design. You have to
+        delegate the task of authentication to the IdP and then get an asynchronous
+        response from it.
+        
+        Given said that, djangosaml2 does use a Django Authentication Backend to
+        transform the SAML assertion about the user into a Django user object.
+        
+        **Why not put everything in a Django middleware class and make our lifes
+        easier?**
+        
+        Yes, that was an option I did evaluate but at the end the current design
+        won. In my opinion putting this logic into a middleware has the advantage
+        of making it easier to configure but has a couple of disadvantages: first,
+        the middleware would need to check if the request path is one of the
+        SAML endpoints for every request. Second, it would be too magical and in
+        case of a problem, much harder to debug.
+        
+        **Why not call this package django-saml as many other Django applications?**
+        
+        Following that pattern then I should import the application with
+        import saml but unfortunately that module name is already used in pysaml2.
+        
+Keywords: django,pysaml2,sso,saml2,federated authentication,authentication
+Platform: UNKNOWN
+Classifier: Development Status :: 4 - Beta
+Classifier: Environment :: Web Environment
+Classifier: Framework :: Django
+Classifier: Framework :: Django :: 1.8
+Classifier: Framework :: Django :: 1.9
+Classifier: Framework :: Django :: 1.10
+Classifier: Framework :: Django :: 1.11
+Classifier: Framework :: Django :: 2.0
+Classifier: Framework :: Django :: 2.1
+Classifier: Framework :: Django :: 2.2
+Classifier: Framework :: Django :: 3.0
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Topic :: Internet :: WWW/HTTP
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
+Classifier: Topic :: Security
+Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
+Provides-Extra: test

+ 545 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/README.rst

@@ -0,0 +1,545 @@
+===========
+djangosaml2
+===========
+
+.. image:: https://travis-ci.org/knaperek/djangosaml2.svg?branch=master
+    :target: https://travis-ci.org/knaperek/djangosaml2
+    :align: left
+
+
+djangosaml2 is a Django application that integrates the PySAML2 library
+into your project. This mean that you can protect your Django based project
+with a service provider based on PySAML. This way it will talk SAML2 with
+your Identity Provider allowing you to use this authentication mechanism.
+This document will guide you through a few simple steps to accomplish
+such goal.
+
+.. contents::
+
+Installation
+============
+
+PySAML2 uses xmlsec1_ binary to sign SAML assertions so you need to install
+it either through your operating system package or by compiling the source
+code. It doesn't matter where the final executable is installed because
+you will need to set the full path to it in the configuration stage.
+
+.. _xmlsec1: http://www.aleksey.com/xmlsec/
+
+Now you can install the djangosaml2 package using easy_install or pip. This
+will also install PySAML2 and its dependencies automatically.
+
+
+Configuration
+=============
+
+There are three things you need to setup to make djangosaml2 work in your
+Django project:
+
+1. **settings.py** as you may already know, it is the main Django
+   configuration file.
+2. **urls.py** is the file where you will include djangosaml2 urls.
+3. **pysaml2** specific files such as an attribute map directory and a
+   certificate.
+
+
+Changes in the settings.py file
+-------------------------------
+The first thing you need to do is add ``djangosaml2`` to the list of
+installed apps::
+
+  INSTALLED_APPS = (
+      'django.contrib.auth',
+      'django.contrib.contenttypes',
+      'django.contrib.sessions',
+      'django.contrib.sites',
+      'django.contrib.messages',
+      'django.contrib.admin',
+      'djangosaml2',  # new application
+  )
+
+Actually this is not really required since djangosaml2 does not include
+any data model. The only reason we include it is to be able to run
+djangosaml2 test suite from our project, something you should always
+do to make sure it is compatible with your Django version and environment.
+
+.. note::
+
+  When you finish the configuration you can run the djangosaml2 test suite as
+  you run any other Django application test suite. Just type ``python manage.py
+  test djangosaml2``.
+
+  Python 2 users need to ``pip install djangosaml2[test]`` in order to run the
+  tests.
+
+Then you have to add the ``djangosaml2.backends.Saml2Backend``
+authentication backend to the list of authentications backends.
+By default only the ModelBackend included in Django is configured.
+A typical configuration would look like this::
+
+  AUTHENTICATION_BACKENDS = (
+      'django.contrib.auth.backends.ModelBackend',
+      'djangosaml2.backends.Saml2Backend',
+  )
+
+.. note::
+
+  Before djangosaml2 0.5.0 this authentication backend was
+  automatically added by djangosaml2. This turned out to be
+  a bad idea since some applications want to use their own
+  custom policies for authorization and the authentication
+  backend is a good place to define that. Starting from
+  djangosaml2 0.5.0 it is now possible to define such
+  backends.
+
+Finally we have to tell Django what the new login url we want to use is::
+
+  LOGIN_URL = '/saml2/login/'
+  SESSION_EXPIRE_AT_BROWSER_CLOSE = True
+
+Here we are telling Django that any view that requires an authenticated
+user should redirect the user browser to that url if the user has not
+been authenticated before. We are also telling that when the user closes
+his browser, the session should be terminated. This is useful in SAML2
+federations where the logout protocol is not always available.
+
+.. note::
+
+  The login url starts with ``/saml2/`` as an example but you can change that
+  if you want. Check the section about changes in the ``urls.py``
+  file for more information.
+
+If you want to allow several authentication mechanisms in your project
+you should set the LOGIN_URL option to another view and put a link in such
+view to the ``/saml2/login/`` view.
+
+Preferred Logout binding
+------------------------
+Use the following setting to choose your preferred binding for SP initiated logout requests::
+
+  SAML_LOGOUT_REQUEST_PREFERRED_BINDING
+
+For example::
+
+  import saml2
+  SAML_LOGOUT_REQUEST_PREFERRED_BINDING = saml2.BINDING_HTTP_POST
+
+Changes in the urls.py file
+---------------------------
+
+The next thing you need to do is to include ``djangosaml2.urls`` module in your
+main ``urls.py`` module::
+
+  urlpatterns = patterns(
+      '',
+      #  lots of url definitions here
+
+      (r'^saml2/', include('djangosaml2.urls')),
+
+      #  more url definitions
+  )
+
+As you can see we are including ``djangosaml2.urls`` under the *saml2*
+prefix. Feel free to use your own prefix but be consistent with what
+you have put in the ``settings.py`` file in the LOGIN_URL parameter.
+
+
+PySAML2 specific files and configuration
+----------------------------------------
+Once you have finished configuring your Django project you have to
+start configuring PySAML. If you use just that library you have to
+put your configuration options in a file and initialize PySAML2 with
+the path to that file.
+
+In djangosaml2 you just put the same information in the Django
+settings.py file under the SAML_CONFIG option.
+
+We will see a typical configuration for protecting a Django project::
+
+  from os import path
+  import saml2
+  import saml2.saml
+  BASEDIR = path.dirname(path.abspath(__file__))
+  SAML_CONFIG = {
+    # full path to the xmlsec1 binary programm
+    'xmlsec_binary': '/usr/bin/xmlsec1',
+
+    # your entity id, usually your subdomain plus the url to the metadata view
+    'entityid': 'http://localhost:8000/saml2/metadata/',
+
+    # directory with attribute mapping
+    'attribute_map_dir': path.join(BASEDIR, 'attribute-maps'),
+
+    # this block states what services we provide
+    'service': {
+        # we are just a lonely SP
+        'sp' : {
+            'name': 'Federated Django sample SP',
+            'name_id_format': saml2.saml.NAMEID_FORMAT_PERSISTENT,
+            'endpoints': {
+                # url and binding to the assetion consumer service view
+                # do not change the binding or service name
+                'assertion_consumer_service': [
+                    ('http://localhost:8000/saml2/acs/',
+                     saml2.BINDING_HTTP_POST),
+                    ],
+                # url and binding to the single logout service view
+                # do not change the binding or service name
+                'single_logout_service': [
+                    ('http://localhost:8000/saml2/ls/',
+                     saml2.BINDING_HTTP_REDIRECT),
+                    ('http://localhost:8000/saml2/ls/post',
+                     saml2.BINDING_HTTP_POST),
+                    ],
+                },
+             # Mandates that the identity provider MUST authenticate the
+             # presenter directly rather than rely on a previous security context.
+            'force_authn': False, 
+            
+             # Enable AllowCreate in NameIDPolicy.
+            'name_id_format_allow_create': False,
+            
+             # attributes that this project need to identify a user
+            'required_attributes': ['uid'],
+
+             # attributes that may be useful to have but not required
+            'optional_attributes': ['eduPersonAffiliation'],
+
+            # in this section the list of IdPs we talk to are defined
+            'idp': {
+                # we do not need a WAYF service since there is
+                # only an IdP defined here. This IdP should be
+                # present in our metadata
+
+                # the keys of this dictionary are entity ids
+                'https://localhost/simplesaml/saml2/idp/metadata.php': {
+                    'single_sign_on_service': {
+                        saml2.BINDING_HTTP_REDIRECT: 'https://localhost/simplesaml/saml2/idp/SSOService.php',
+                        },
+                    'single_logout_service': {
+                        saml2.BINDING_HTTP_REDIRECT: 'https://localhost/simplesaml/saml2/idp/SingleLogoutService.php',
+                        },
+                    },
+                },
+            },
+        },
+
+    # where the remote metadata is stored
+    'metadata': {
+        'local': [path.join(BASEDIR, 'remote_metadata.xml')],
+        },
+
+    # set to 1 to output debugging information
+    'debug': 1,
+
+    # Signing
+    'key_file': path.join(BASEDIR, 'mycert.key'),  # private part
+    'cert_file': path.join(BASEDIR, 'mycert.pem'),  # public part
+
+    # Encryption
+    'encryption_keypairs': [{
+        'key_file': path.join(BASEDIR, 'my_encryption_key.key'),  # private part
+        'cert_file': path.join(BASEDIR, 'my_encryption_cert.pem'),  # public part
+    }],
+
+    # own metadata settings
+    'contact_person': [
+        {'given_name': 'Lorenzo',
+         'sur_name': 'Gil',
+         'company': 'Yaco Sistemas',
+         'email_address': 'lgs@yaco.es',
+         'contact_type': 'technical'},
+        {'given_name': 'Angel',
+         'sur_name': 'Fernandez',
+         'company': 'Yaco Sistemas',
+         'email_address': 'angel@yaco.es',
+         'contact_type': 'administrative'},
+        ],
+    # you can set multilanguage information here
+    'organization': {
+        'name': [('Yaco Sistemas', 'es'), ('Yaco Systems', 'en')],
+        'display_name': [('Yaco', 'es'), ('Yaco', 'en')],
+        'url': [('http://www.yaco.es', 'es'), ('http://www.yaco.com', 'en')],
+        },
+    'valid_for': 24,  # how long is our metadata valid
+    }
+
+.. note::
+
+  Please check the `PySAML2 documentation`_ for more information about
+  these and other configuration options.
+
+.. _`PySAML2 documentation`: http://pysaml2.readthedocs.io/en/latest/
+
+There are several external files and directories you have to create according
+to this configuration.
+
+The xmlsec1 binary was mentioned in the installation section. Here, in the
+configuration part you just need to put the full path to xmlsec1 so PySAML2
+can call it as it needs.
+
+The ``attribute_map_dir`` points to a directory with attribute mappings that
+are used to translate user attribute names from several standards. It's usually
+safe to just copy the default PySAML2 attribute maps that you can find in the
+``tests/attributemaps`` directory of the source distribution.
+
+The ``metadata`` option is a dictionary where you can define several types of
+metadata for remote entities. Usually the easiest type is the ``local`` where
+you just put the name of a local XML file with the contents of the remote
+entities metadata. This XML file should be in the SAML2 metadata format.
+
+The ``key_file`` and ``cert_file`` options reference the two parts of a
+standard x509 certificate. You need it to sign your metadata. For assertion
+encryption/decryption support please configure another set of ``key_file`` and
+``cert_file``, but as inner attributes of ``encryption_keypairs`` option.
+
+.. note::
+
+  Check your openssl documentation to generate a test certificate but don't
+  forget to order a real one when you go into production.
+
+
+Custom and dynamic configuration loading
+........................................
+
+By default, djangosaml2 reads the pysaml2 configuration options from the
+SAML_CONFIG setting but sometimes you want to read this information from
+another place, like a file or a database. Sometimes you even want this
+configuration to be different depending on the request.
+
+Starting from djangosaml2 0.5.0 you can define your own configuration
+loader which is a callable that accepts a request parameter and returns
+a saml2.config.SPConfig object. In order to do so you set the following
+setting::
+
+  SAML_CONFIG_LOADER = 'python.path.to.your.callable'
+
+
+User attributes
+---------------
+
+In the SAML 2.0 authentication process the Identity Provider (IdP) will
+send a security assertion to the Service Provider (SP) upon a successful
+authentication. This assertion contains attributes about the user that
+was authenticated. It depends on the IdP configuration what exact
+attributes are sent to each SP it can talk to.
+
+When such assertion is received on the Django side it is used to find a Django
+user and create a session for it. By default djangosaml2 will do a query on the
+User model with the USERNAME_FIELD_ attribute but you can change it to any
+other attribute of the User model. For example, you can do this lookup using
+the 'email' attribute. In order to do so you should set the following setting::
+
+  SAML_DJANGO_USER_MAIN_ATTRIBUTE = 'email'
+
+.. _USERNAME_FIELD: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#django.contrib.auth.models.CustomUser.USERNAME_FIELD
+
+Please, use an unique attribute when setting this option. Otherwise
+the authentication process may fail because djangosaml2 will not know
+which Django user it should pick.
+
+If your main attribute is something inherently case-insensitive (such as
+an email address), you may set::
+
+  SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP = '__iexact'
+
+(This is simply appended to the main attribute name to form a Django
+query. Your main attribute must be unique even given this lookup.)
+
+Another option is to use the SAML2 name id as the username by setting::
+
+  SAML_USE_NAME_ID_AS_USERNAME = True
+
+You can configure djangosaml2 to create such user if it is not already in
+the Django database or maybe you don't want to allow users that are not
+in your database already. For this purpose there is another option you
+can set in the settings.py file::
+
+  SAML_CREATE_UNKNOWN_USER = True
+
+This setting is True by default.
+
+  ACS_DEFAULT_REDIRECT_URL = reverse_lazy('some_url_name')
+
+This setting lets you specify a URL for redirection after a successful
+authentication. Particularly useful when you only plan to use
+IdP initiated login and the IdP does not have a configured RelayState
+parameter. The default is ``/``.
+
+The other thing you will probably want to configure is the mapping of
+SAML2 user attributes to Django user attributes. By default only the
+User.username attribute is mapped but you can add more attributes or
+change that one. In order to do so you need to change the
+SAML_ATTRIBUTE_MAPPING option in your settings.py::
+
+  SAML_ATTRIBUTE_MAPPING = {
+      'uid': ('username', ),
+      'mail': ('email', ),
+      'cn': ('first_name', ),
+      'sn': ('last_name', ),
+  }
+
+where the keys of this dictionary are SAML user attributes and the values
+are Django User attributes.
+
+If you are using Django user profile objects to store extra attributes
+about your user you can add those attributes to the SAML_ATTRIBUTE_MAPPING
+dictionary. For each (key, value) pair, djangosaml2 will try to store the
+attribute in the User model if there is a matching field in that model.
+Otherwise it will try to do the same with your profile custom model. For
+multi-valued attributes only the first value is assigned to the destination field.
+
+Alternatively, custom processing of attributes can be achieved by setting the
+value(s) in the SAML_ATTRIBUTE_MAPPING, to name(s) of method(s) defined on a
+custom django User object. In this case, each method is called by djangosaml2,
+passing the full list of attribute values extracted from the <saml:AttributeValue>
+elements of the <saml:Attribute>. Among other uses, this is a useful way to process
+multi-valued attributes such as lists of user group names.
+
+For example:
+
+Saml assertion snippet::
+
+  <saml:Attribute Name="groups" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+        <saml:AttributeValue>group1</saml:AttributeValue>
+        <saml:AttributeValue>group2</saml:AttributeValue>
+        <saml:AttributeValue>group3</saml:AttributeValue>
+  </saml:Attribute>
+
+Custom User object::
+
+  from django.contrib.auth.models import AbstractUser
+
+  class User(AbstractUser):
+
+    def process_groups(self, groups):
+      // process list of group names in argument 'groups'
+      pass;
+
+settings.py::
+
+  SAML_ATTRIBUTE_MAPPING = {
+      'groups': ('process_groups', ),
+  }
+
+
+Learn more about Django profile models at:
+
+https://docs.djangoproject.com/en/dev/topics/auth/customizing/#substituting-a-custom-user-model
+
+
+Sometimes you need to use special logic to update the user object
+depending on the SAML2 attributes and the mapping described above
+is simply not enough. For these cases djangosaml2 provides a Django
+signal that you can listen to. In order to do so you can add the
+following code to your app::
+
+  from djangosaml2.signals import pre_user_save
+
+  def custom_update_user(sender=User, instance, attributes, user_modified, **kargs)
+     ...
+     return True  # I modified the user object
+
+
+Your handler will receive the user object, the list of SAML attributes
+and a flag telling you if the user is already modified and need
+to be saved after your handler is executed. If your handler
+modifies the user object it should return True. Otherwise it should
+return False. This way djangosaml2 will know if it should save
+the user object so you don't need to do it and no more calls to
+the save method are issued.
+
+
+IdP setup
+=========
+Congratulations, you have finished configuring the SP side of the federation.
+Now you need to send the entity id and the metadata of this new SP to the
+IdP administrators so they can add it to their list of trusted services.
+
+You can get this information starting your Django development server and
+going to the http://localhost:8000/saml2/metadata url. If you have included
+the djangosaml2 urls under a different url prefix you need to correct this
+url.
+
+SimpleSAMLphp issues
+--------------------
+As of SimpleSAMLphp 1.8.2 there is a problem if you specify attributes in
+the SP configuration. When the SimpleSAMLphp metadata parser converts the
+XML into its custom php format it puts the following option::
+
+  'attributes.NameFormat' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri'
+
+But it need to be replaced by this one::
+
+  'AttributeNameFormat' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri'
+
+Otherwise the Assertions sent from the IdP to the SP will have a wrong
+Attribute Name Format and pysaml2 will be confused.
+
+Furthermore if you have a AttributeLimit filter in your SimpleSAMLphp
+configuration  you will need to enable another attribute filter just
+before to make sure that the AttributeLimit does not remove the attributes
+from the authentication source. The filter you need to add is an AttributeMap
+filter like this::
+
+  10 => array(
+             'class' => 'core:AttributeMap', 'name2oid'
+        ),
+
+Testing
+=======
+
+One way to check if everything is working as expected is to enable the
+following url::
+
+  urlpatterns = patterns(
+      '',
+      #  lots of url definitions here
+
+      (r'^saml2/', include('djangosaml2.urls')),
+      (r'^test/', 'djangosaml2.views.echo_attributes'),
+
+      #  more url definitions
+  )
+
+
+Now if you go to the /test/ url you will see your SAML attributes and also
+a link to do a global logout.
+
+You can also run the unit tests with the following command::
+
+  python tests/run_tests.py
+
+If you have `tox`_ installed you can simply call tox inside the root directory
+and it will run the tests in multiple versions of Python.
+
+.. _`tox`: http://pypi.python.org/pypi/tox
+
+FAQ
+===
+
+**Why can't SAML be implemented as an Django Authentication Backend?**
+
+well SAML authentication is not that simple as a set of credentials you can
+put on a login form and get a response back. Actually the user password is
+not given to the service provider at all. This is by design. You have to
+delegate the task of authentication to the IdP and then get an asynchronous
+response from it.
+
+Given said that, djangosaml2 does use a Django Authentication Backend to
+transform the SAML assertion about the user into a Django user object.
+
+**Why not put everything in a Django middleware class and make our lifes
+easier?**
+
+Yes, that was an option I did evaluate but at the end the current design
+won. In my opinion putting this logic into a middleware has the advantage
+of making it easier to configure but has a couple of disadvantages: first,
+the middleware would need to check if the request path is one of the
+SAML endpoints for every request. Second, it would be too magical and in
+case of a problem, much harder to debug.
+
+**Why not call this package django-saml as many other Django applications?**
+
+Following that pattern then I should import the application with
+import saml but unfortunately that module name is already used in pysaml2.

+ 0 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/__init__.py


+ 22 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/acs_failures.py

@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+#
+# This module defines a set of useful ACS failure functions that are used to
+# produce an output suitable for end user in case of SAML failure.
+#
+from __future__ import unicode_literals
+
+from django.core.exceptions import PermissionDenied
+from django.shortcuts import render
+
+
+def template_failure(request, status=403, **kwargs):
+    """ Renders a SAML-specific template with general authentication error description. """
+    return render(request, 'djangosaml2/login_error.html', status=status)
+
+
+def exception_failure(request, exc_class=PermissionDenied, **kwargs):
+    """ Rather than using a custom SAML specific template that is rendered on failure,
+    this makes use of a standard exception handling machinery present in Django
+    and thus ends up rendering a project-wide error page for Permission Denied exceptions.
+    """
+    raise exc_class

+ 278 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/backends.py

@@ -0,0 +1,278 @@
+# Copyright (C) 2010-2012 Yaco Sistemas (http://www.yaco.es)
+# Copyright (C) 2009 Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#            http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+
+from django.conf import settings
+from django.contrib import auth
+from django.contrib.auth.backends import ModelBackend
+from django.core.exceptions import (
+    MultipleObjectsReturned, ImproperlyConfigured,
+)
+
+from djangosaml2.signals import pre_user_save
+
+
+logger = logging.getLogger('djangosaml2')
+
+
+def get_model(model_path):
+    try:
+        from django.apps import apps
+        return apps.get_model(model_path)
+    except ImportError:
+        # Django < 1.7 (cannot use the new app loader)
+        from django.db.models import get_model as django_get_model
+        try:
+            app_label, model_name = model_path.split('.')
+        except ValueError:
+            raise ImproperlyConfigured("SAML_USER_MODEL must be of the form "
+                "'app_label.model_name'")
+        user_model = django_get_model(app_label, model_name)
+        if user_model is None:
+            raise ImproperlyConfigured("SAML_USER_MODEL refers to model '%s' "
+                "that has not been installed" % model_path)
+        return user_model
+
+
+def get_saml_user_model():
+    try:
+        # djangosaml2 custom user model
+        return get_model(settings.SAML_USER_MODEL)
+    except AttributeError:
+        try:
+            # Django 1.5 Custom user model
+            return auth.get_user_model()
+        except AttributeError:
+            return auth.models.User
+
+
+class Saml2Backend(ModelBackend):
+
+    def authenticate(self, request, session_info=None, attribute_mapping=None,
+                     create_unknown_user=True, **kwargs):
+        if session_info is None or attribute_mapping is None:
+            logger.info('Session info or attribute mapping are None')
+            return None
+
+        if 'ava' not in session_info:
+            logger.error('"ava" key not found in session_info')
+            return None
+
+        attributes = self.clean_attributes(session_info['ava'])
+        if not attributes:
+            logger.error('The attributes dictionary is empty')
+
+        use_name_id_as_username = getattr(
+            settings, 'SAML_USE_NAME_ID_AS_USERNAME', False)
+
+        django_user_main_attribute = self.get_django_user_main_attribute()
+
+        logger.debug('attributes: %s', attributes)
+        saml_user = None
+        if use_name_id_as_username:
+            if 'name_id' in session_info:
+                logger.debug('name_id: %s', session_info['name_id'])
+                saml_user = session_info['name_id'].text
+            else:
+                logger.error('The nameid is not available. Cannot find user without a nameid.')
+        else:
+            saml_user = self.get_attribute_value(django_user_main_attribute,
+                                                 attributes,
+                                                 attribute_mapping)
+
+        if saml_user is None:
+            logger.error('Could not find saml_user value')
+            return None
+
+        if not self.is_authorized(attributes, attribute_mapping):
+            return None
+
+        main_attribute = self.clean_user_main_attribute(saml_user)
+
+        # Note that this could be accomplished in one try-except clause, but
+        # instead we use get_or_create when creating unknown users since it has
+        # built-in safeguards for multiple threads.
+        return self.get_saml2_user(
+            create_unknown_user, main_attribute, attributes, attribute_mapping)
+
+    def get_attribute_value(self, django_field, attributes, attribute_mapping):
+        saml_user = None
+        logger.debug('attribute_mapping: %s', attribute_mapping)
+        for saml_attr, django_fields in attribute_mapping.items():
+            if django_field in django_fields and saml_attr in attributes:
+                saml_user = attributes.get('saml_attr', [None])[0]
+                if not saml_user:
+                    logger.error('attributes[saml_attr] attribute '
+                                 'value is missing. Probably the user '
+                                 'session is expired.')
+        return saml_user
+
+    def is_authorized(self, attributes, attribute_mapping):
+        """Hook to allow custom authorization policies based on
+        SAML attributes.
+        """
+        return True
+
+    def clean_attributes(self, attributes):
+        """Hook to clean attributes from the SAML response."""
+        return attributes
+
+    def clean_user_main_attribute(self, main_attribute):
+        """Performs any cleaning on the user main attribute (which
+        usually is "username") prior to using it to get or
+        create the user object.  Returns the cleaned attribute.
+
+        By default, returns the attribute unchanged.
+        """
+        return main_attribute
+
+    def get_django_user_main_attribute(self):
+        return getattr(
+            settings,
+            'SAML_DJANGO_USER_MAIN_ATTRIBUTE',
+            getattr(get_saml_user_model(), 'USERNAME_FIELD', 'username'))
+
+    def get_django_user_main_attribute_lookup(self):
+        return getattr(settings, 'SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP', '')
+
+    def get_user_query_args(self, main_attribute):
+        lookup = (self.get_django_user_main_attribute() +
+                  self.get_django_user_main_attribute_lookup())
+        return {lookup: main_attribute}
+
+    def get_saml2_user(self, create, main_attribute, attributes, attribute_mapping):
+        if create:
+            return self._get_or_create_saml2_user(main_attribute, attributes, attribute_mapping)
+
+        return self._get_saml2_user(main_attribute, attributes, attribute_mapping)
+
+    def _get_or_create_saml2_user(self, main_attribute, attributes, attribute_mapping):
+        logger.debug('Check if the user "%s" exists or create otherwise',
+                     main_attribute)
+        django_user_main_attribute = self.get_django_user_main_attribute()
+        user_query_args = self.get_user_query_args(main_attribute)
+        user_create_defaults = {django_user_main_attribute: main_attribute}
+
+        User = get_saml_user_model()
+        try:
+            user, created = User.objects.get_or_create(
+                defaults=user_create_defaults, **user_query_args)
+        except MultipleObjectsReturned:
+            logger.error("There are more than one user with %s = %s",
+                         django_user_main_attribute, main_attribute)
+            return None
+
+        if created:
+            logger.debug('New user created')
+            user = self.configure_user(user, attributes, attribute_mapping)
+        else:
+            logger.debug('User updated')
+            user = self.update_user(user, attributes, attribute_mapping)
+        return user
+
+    def _get_saml2_user(self, main_attribute, attributes, attribute_mapping):
+        User = get_saml_user_model()
+        django_user_main_attribute = self.get_django_user_main_attribute()
+        user_query_args = self.get_user_query_args(main_attribute)
+
+        logger.debug('Retrieving existing user "%s"', main_attribute)
+        try:
+            user = User.objects.get(**user_query_args)
+            user = self.update_user(user, attributes, attribute_mapping)
+        except User.DoesNotExist:
+            logger.error('The user "%s" does not exist, searched %s', main_attribute, django_user_main_attribute)
+            return None
+        except MultipleObjectsReturned:
+            logger.error("There are more than one user with %s = %s",
+                         django_user_main_attribute, main_attribute)
+            return None
+        return user
+
+    def configure_user(self, user, attributes, attribute_mapping):
+        """Configures a user after creation and returns the updated user.
+
+        By default, returns the user with his attributes updated.
+        """
+        user.set_unusable_password()
+        return self.update_user(user, attributes, attribute_mapping,
+                                force_save=True)
+
+    def update_user(self, user, attributes, attribute_mapping,
+                    force_save=False):
+        """Update a user with a set of attributes and returns the updated user.
+
+        By default it uses a mapping defined in the settings constant
+        SAML_ATTRIBUTE_MAPPING. For each attribute, if the user object has
+        that field defined it will be set.
+        """
+        if not attribute_mapping:
+            return user
+
+        user_modified = False
+        for saml_attr, django_attrs in attribute_mapping.items():
+            attr_value_list = attributes.get(saml_attr)
+            if not attr_value_list:
+                logger.debug(
+                    'Could not find value for "%s", not updating fields "%s"',
+                    saml_attr, django_attrs)
+                continue
+
+            for attr in django_attrs:
+                if hasattr(user, attr):
+                    user_attr = getattr(user, attr)
+                    if callable(user_attr):
+                        modified = user_attr(attr_value_list)
+                    else:
+                        modified = self._set_attribute(user, attr, attr_value_list[0])
+
+                    user_modified = user_modified or modified
+                else:
+                    logger.debug(
+                        'Could not find attribute "%s" on user "%s"', attr, user)
+
+        logger.debug('Sending the pre_save signal')
+        signal_modified = any(
+            [response for receiver, response
+             in pre_user_save.send_robust(sender=user.__class__,
+                                          instance=user,
+                                          attributes=attributes,
+                                          user_modified=user_modified)]
+            )
+
+        if user_modified or signal_modified or force_save:
+            user.save()
+
+        return user
+
+    def _set_attribute(self, obj, attr, value):
+        """Set an attribute of an object to a specific value.
+
+        Return True if the attribute was changed and False otherwise.
+        """
+        field = obj._meta.get_field(attr)
+        if field.max_length is not None and len(value) > field.max_length:
+            cleaned_value = value[:field.max_length]
+            logger.warn('The attribute "%s" was trimmed from "%s" to "%s"',
+                        attr, value, cleaned_value)
+        else:
+            cleaned_value = value
+
+        old_value = getattr(obj, attr)
+        if cleaned_value != old_value:
+            setattr(obj, attr, cleaned_value)
+            return True
+
+        return False

+ 89 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/cache.py

@@ -0,0 +1,89 @@
+# Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es)
+# Copyright (C) 2010 Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#            http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from saml2.cache import Cache
+
+
+class DjangoSessionCacheAdapter(dict):
+    """A cache of things that are stored in the Django Session"""
+
+    key_prefix = '_saml2'
+
+    def __init__(self, django_session, key_suffix):
+        self.session = django_session
+        self.key = self.key_prefix + key_suffix
+
+        super(DjangoSessionCacheAdapter, self).__init__(self._get_objects())
+
+    def _get_objects(self):
+        return self.session.get(self.key, {})
+
+    def _set_objects(self, objects):
+        self.session[self.key] = objects
+
+    def sync(self):
+        # Changes in inner objects do not cause session invalidation
+        # https://docs.djangoproject.com/en/1.9/topics/http/sessions/#when-sessions-are-saved
+
+        #add objects to session
+        self._set_objects(dict(self))
+        #invalidate session
+        self.session.modified = True
+
+
+class OutstandingQueriesCache(object):
+    """Handles the queries that have been sent to the IdP and have not
+    been replied yet.
+    """
+
+    def __init__(self, django_session):
+        self._db = DjangoSessionCacheAdapter(django_session,
+                                             '_outstanding_queries')
+
+    def outstanding_queries(self):
+        return self._db._get_objects()
+
+    def set(self, saml2_session_id, came_from):
+        self._db[saml2_session_id] = came_from
+        self._db.sync()
+
+    def delete(self, saml2_session_id):
+        if saml2_session_id in self._db:
+            del self._db[saml2_session_id]
+            self._db.sync()
+
+
+class IdentityCache(Cache):
+    """Handles information about the users that have been succesfully
+    logged in.
+
+    This information is useful because when the user logs out we must
+    know where does he come from in order to notify such IdP/AA.
+
+    The current implementation stores this information in the Django session.
+    """
+
+    def __init__(self, django_session):
+        self._db = DjangoSessionCacheAdapter(django_session, '_identities')
+        self._sync = True
+
+
+class StateCache(DjangoSessionCacheAdapter):
+    """Store state information that is needed to associate a logout
+    request with its response.
+    """
+
+    def __init__(self, django_session):
+        super(StateCache, self).__init__(django_session, '_state')

+ 70 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/conf.py

@@ -0,0 +1,70 @@
+# Copyright (C) 2010-2012 Yaco Sistemas (http://www.yaco.es)
+# Copyright (C) 2009 Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#            http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import copy
+from importlib import import_module
+
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+
+from saml2.config import SPConfig
+
+from djangosaml2.utils import get_custom_setting
+
+
+def get_config_loader(path, request=None):
+    i = path.rfind('.')
+    module, attr = path[:i], path[i + 1:]
+    try:
+        mod = import_module(module)
+    except ImportError as e:
+        raise ImproperlyConfigured(
+            'Error importing SAML config loader %s: "%s"' % (path, e))
+    except ValueError as e:
+        raise ImproperlyConfigured(
+            'Error importing SAML config loader. Is SAML_CONFIG_LOADER '
+            'a correctly string with a callable path?'
+            )
+    try:
+        config_loader = getattr(mod, attr)
+    except AttributeError:
+        raise ImproperlyConfigured(
+            'Module "%s" does not define a "%s" config loader' %
+            (module, attr)
+            )
+
+    if not hasattr(config_loader, '__call__'):
+        raise ImproperlyConfigured(
+            "SAML config loader must be a callable object.")
+
+    return config_loader
+
+
+def config_settings_loader(request=None):
+    """Utility function to load the pysaml2 configuration.
+
+    This is also the default config loader.
+    """
+    conf = SPConfig()
+    conf.load(copy.deepcopy(settings.SAML_CONFIG))
+    return conf
+
+
+def get_config(config_loader_path=None, request=None):
+    config_loader_path = config_loader_path or get_custom_setting(
+        'SAML_CONFIG_LOADER', 'djangosaml2.conf.config_settings_loader')
+
+    config_loader = get_config_loader(config_loader_path)
+    return config_loader(request)

+ 2 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/exceptions.py

@@ -0,0 +1,2 @@
+class IdPConfigurationMissing(Exception):
+    pass

+ 1 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/models.py

@@ -0,0 +1 @@
+# nothing here, just an empty file to avoid Django complaints

+ 24 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/overrides.py

@@ -0,0 +1,24 @@
+import logging
+
+import saml2.client
+from django.conf import settings
+
+logger = logging.getLogger('djangosaml2')
+
+
+class Saml2Client(saml2.client.Saml2Client):
+    """
+    Custom Saml2Client that adds a choice of preference for binding used with
+    SAML Logout Requests. The preferred binding can be configured via
+    SAML_LOGOUT_REQUEST_PREFERRED_BINDING settings variable.
+    (Original Saml2Client always prefers SOAP, so it is always used if declared
+    in remote metadata); but doesn't actually work and causes crashes.
+    """
+    def do_logout(self, *args, **kwargs):
+        if not kwargs.get('expected_binding'):
+            try:
+                kwargs['expected_binding'] = settings.SAML_LOGOUT_REQUEST_PREFERRED_BINDING
+            except AttributeError:
+                logger.warning('SAML_LOGOUT_REQUEST_PREFERRED_BINDING setting is'
+                    ' not defined. Default binding will be used.')
+        return super(Saml2Client, self).do_logout(*args, **kwargs)

+ 20 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/signals.py

@@ -0,0 +1,20 @@
+# Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#            http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import django.dispatch
+
+
+pre_user_save = django.dispatch.Signal(providing_args=['attributes',
+                                                       'user_modified'])
+post_authenticated = django.dispatch.Signal(providing_args=['session_info'])

+ 16 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/templates/djangosaml2/auth_error.html

@@ -0,0 +1,16 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+  </head>
+  <body>
+    <h1>Authorization error</h1>
+    <p>You are already logged in and you are trying to go to the login page again.</p>
+
+    <p>You may have been redirected here when trying to access some content
+      that required extra privileges that you do not have.</p>
+
+    <p>Please <a href="{% url 'saml2_logout' %}">logout</a> and login as a different user</p>
+  </body>
+</html>

+ 18 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/templates/djangosaml2/echo_attributes.html

@@ -0,0 +1,18 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+  </head>
+  <body>
+    <h1>SAML attributes</h1>
+    <dl>
+      {% for attribute, value in attributes.items %}
+      <dt>{{ attribute }}:</dt>
+      <dd>{{ value|join:", " }}</dd>
+      {% endfor %}
+    </dl>
+
+    <p><a href="{% url 'saml2_logout' %}">Log out</a></p>
+  </body>
+</html>

+ 15 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/templates/djangosaml2/example_post_binding_form.html

@@ -0,0 +1,15 @@
+<script type="text/javascript">
+	window.onload = function(){
+		document.SSO_Login.submit();
+	};
+</script>
+<p>
+You're being redirected to a SSO login page.
+Please click the button below if you're not redirected automatically within a few seconds.
+</p>
+<form method="post" action="{{ target_url }}" name="SSO_Login">
+	{% for key, value in params.items %}
+	    <input type="hidden" name="{{ key|safe }}" value="{{ value|safe }}" />
+	{% endfor %}
+    <input type="submit" value="Log in" />
+</form>

+ 13 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/templates/djangosaml2/login_error.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+  </head>
+  <body>
+    <h1>Authentication Error.</h1>
+
+    <h2>Access Denied.</h2>
+
+  </body>
+</html>

+ 17 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/templates/djangosaml2/logout_error.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+  </head>
+  <body>
+    <h1>Logout error</h1>
+    <p>Your Identity Provider ask this system to do a global logout but your federated session is lost.</p>
+
+    <p>Even if your local session in this system has been closed, you have probably open sessions in other systems.</p>
+
+    <p>In order to prevent illicit use of your personal information, please close your browser window and/or remove your cookies from your browser.</p>
+
+    <p>Sorry for this inconvenience.</p>
+  </body>
+</html>

+ 16 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/templates/djangosaml2/wayf.html

@@ -0,0 +1,16 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+  </head>
+  <body>
+    <h1>Where are you from?</h1>
+    <p>Please select your <strong>Identity Provider</strong> from the following list:</p>
+    <ul>
+      {% for url, name in available_idps %}
+      <li><a href="{% url 'saml2_login' %}?idp={{ url }}{% if came_from %}&next={{ came_from }}{% endif %}">{{ name }}</a></li>
+      {% endfor %}
+    </ul>
+  </body>
+</html>

+ 0 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/templatetags/__init__.py


+ 45 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/templatetags/idplist.py

@@ -0,0 +1,45 @@
+# Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#            http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from django import template
+
+from djangosaml2.conf import config_settings_loader
+from djangosaml2.utils import available_idps
+
+register = template.Library()
+
+
+class IdPListNode(template.Node):
+
+    def __init__(self, variable_name):
+        self.variable_name = variable_name
+
+    def render(self, context):
+        conf = config_settings_loader()
+        context[self.variable_name] = available_idps(conf)
+        return ''
+
+
+@register.tag
+def idplist(parser, token):
+    try:
+        tag_name, as_part, variable = token.split_contents()
+    except ValueError:
+        raise template.TemplateSyntaxError(
+            '%r tag requires two arguments' % token.contents.split()[0])
+    if not as_part == 'as':
+        raise template.TemplateSyntaxError(
+            '%r tag first argument must be the literal "as"' % tag_name)
+
+    return IdPListNode(variable)

+ 567 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/__init__.py

@@ -0,0 +1,567 @@
+# Copyright (C) 2012 Sam Bull (lsb@pocketuniverse.ca)
+# Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es)
+# Copyright (C) 2010 Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#            http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import datetime
+import base64
+import re
+from unittest import skip
+import sys
+
+from django.conf import settings
+from django.contrib.auth import SESSION_KEY, get_user_model
+from django.contrib.auth.models import AnonymousUser
+from django.contrib.sessions.middleware import SessionMiddleware
+try:
+    from django.urls import reverse
+except ImportError:
+    from django.core.urlresolvers import reverse
+from django.template import Template, Context
+from django.test import TestCase
+from django.test.client import RequestFactory
+try:
+    from django.utils.encoding import force_text
+except ImportError:
+    from django.utils.text import force_text
+try:
+    from django.utils.six.moves.urllib.parse import urlparse, parse_qs
+except ImportError:
+    from urllib.parse import urlparse, parse_qs
+
+from saml2.config import SPConfig
+from saml2.s_utils import decode_base64_and_inflate, deflate_and_base64_encode
+
+from djangosaml2 import views
+from djangosaml2.cache import OutstandingQueriesCache
+from djangosaml2.conf import get_config
+from djangosaml2.tests import conf
+from djangosaml2.tests.auth_response import auth_response
+from djangosaml2.signals import post_authenticated
+from djangosaml2.views import finish_logout
+
+User = get_user_model()
+
+PY_VERSION = sys.version_info[:2]
+
+
+class SAML2Tests(TestCase):
+
+    urls = 'djangosaml2.tests.urls'
+
+    def setUp(self):
+        if hasattr(settings, 'SAML_ATTRIBUTE_MAPPING'):
+            self.actual_attribute_mapping = settings.SAML_ATTRIBUTE_MAPPING
+            del settings.SAML_ATTRIBUTE_MAPPING
+        if hasattr(settings, 'SAML_CONFIG_LOADER'):
+            self.actual_conf_loader = settings.SAML_CONFIG_LOADER
+            del settings.SAML_CONFIG_LOADER
+
+    def tearDown(self):
+        if hasattr(self, 'actual_attribute_mapping'):
+            settings.SAML_ATTRIBUTE_MAPPING = self.actual_attribute_mapping
+        if hasattr(self, 'actual_conf_loader'):
+            settings.SAML_CONFIG_LOADER = self.actual_conf_loader
+
+    def assertSAMLRequestsEquals(self, real_xml, expected_xmls):
+
+        def remove_variable_attributes(xml_string):
+            xml_string = re.sub(r' ID=".*?" ', ' ', xml_string)
+            xml_string = re.sub(r' IssueInstant=".*?" ', ' ', xml_string)
+            xml_string = re.sub(
+                r'<saml:NameID(.*)>.*</saml:NameID>',
+                r'<saml:NameID\1></saml:NameID>',
+                xml_string)
+
+            return xml_string
+
+        self.assertEqual(remove_variable_attributes(real_xml),
+                         remove_variable_attributes(expected_xmls))
+
+    def init_cookies(self):
+        self.client.cookies[settings.SESSION_COOKIE_NAME] = 'testing'
+
+    def add_outstanding_query(self, session_id, came_from):
+        session = self.client.session
+        oq_cache = OutstandingQueriesCache(session)
+        oq_cache.set(session_id, came_from)
+        session.save()
+        self.client.cookies[settings.SESSION_COOKIE_NAME] = session.session_key
+
+    def render_template(self, text):
+        return Template(text).render(Context())
+
+    def b64_for_post(self, xml_text, encoding='utf-8'):
+        return base64.b64encode(xml_text.encode(encoding)).decode('ascii')
+
+    def test_login_evil_redirect(self):
+        """
+        Make sure that if we give an URL other than our own host as the next
+        parameter, it is replaced with the default LOGIN_REDIRECT_URL.
+        """
+
+        # monkey patch SAML configuration
+        settings.SAML_CONFIG = conf.create_conf(
+            sp_host='sp.example.com',
+            idp_hosts=['idp.example.com'],
+            metadata_file='remote_metadata_one_idp.xml',
+        )
+        response = self.client.get(reverse('saml2_login') + '?next=http://evil.com')
+        url = urlparse(response['Location'])
+        params = parse_qs(url.query)
+
+        self.assertEqual(params['RelayState'], [settings.LOGIN_REDIRECT_URL, ])
+
+    def test_login_one_idp(self):
+        # monkey patch SAML configuration
+        settings.SAML_CONFIG = conf.create_conf(
+            sp_host='sp.example.com',
+            idp_hosts=['idp.example.com'],
+            metadata_file='remote_metadata_one_idp.xml',
+        )
+
+        response = self.client.get(reverse('saml2_login'))
+        self.assertEqual(response.status_code, 302)
+        location = response['Location']
+
+        url = urlparse(location)
+        self.assertEqual(url.hostname, 'idp.example.com')
+        self.assertEqual(url.path, '/simplesaml/saml2/idp/SSOService.php')
+
+        params = parse_qs(url.query)
+        self.assertIn('SAMLRequest', params)
+        self.assertIn('RelayState', params)
+
+        saml_request = params['SAMLRequest'][0]
+        if PY_VERSION < (3,):
+            expected_request = """<?xml version='1.0' encoding='UTF-8'?>
+<samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>"""
+        else:
+            expected_request = """<samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>"""
+
+        self.assertSAMLRequestsEquals(
+            decode_base64_and_inflate(saml_request).decode('utf-8'),
+            expected_request)
+
+        # if we set a next arg in the login view, it is preserverd
+        # in the RelayState argument
+        next = '/another-view/'
+        response = self.client.get(reverse('saml2_login'), {'next': next})
+        self.assertEqual(response.status_code, 302)
+        location = response['Location']
+
+        url = urlparse(location)
+        self.assertEqual(url.hostname, 'idp.example.com')
+        self.assertEqual(url.path, '/simplesaml/saml2/idp/SSOService.php')
+
+        params = parse_qs(url.query)
+        self.assertIn('SAMLRequest', params)
+        self.assertIn('RelayState', params)
+        self.assertEqual(params['RelayState'][0], next)
+
+    def test_login_several_idps(self):
+        settings.SAML_CONFIG = conf.create_conf(
+            sp_host='sp.example.com',
+            idp_hosts=['idp1.example.com',
+                       'idp2.example.com',
+                       'idp3.example.com'],
+            metadata_file='remote_metadata_three_idps.xml',
+        )
+        response = self.client.get(reverse('saml2_login'))
+        # a WAYF page should be displayed
+        self.assertContains(response, 'Where are you from?', status_code=200)
+        for i in range(1, 4):
+            link = '/login/?idp=https://idp%d.example.com/simplesaml/saml2/idp/metadata.php&next=/'
+            self.assertContains(response, link % i)
+
+        # click on the second idp
+        response = self.client.get(reverse('saml2_login'), {
+                'idp': 'https://idp2.example.com/simplesaml/saml2/idp/metadata.php',
+                'next': '/',
+                })
+        self.assertEqual(response.status_code, 302)
+        location = response['Location']
+
+        url = urlparse(location)
+        self.assertEqual(url.hostname, 'idp2.example.com')
+        self.assertEqual(url.path, '/simplesaml/saml2/idp/SSOService.php')
+
+        params = parse_qs(url.query)
+        self.assertIn('SAMLRequest', params)
+        self.assertIn('RelayState', params)
+
+        saml_request = params['SAMLRequest'][0]
+        if PY_VERSION < (3,):
+            expected_request = """<?xml version='1.0' encoding='UTF-8'?>
+<samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp2.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>"""
+        else:
+            expected_request = """<samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp2.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>"""
+
+        self.assertSAMLRequestsEquals(decode_base64_and_inflate(saml_request).decode('utf-8'),
+                                      expected_request)
+
+    def test_assertion_consumer_service(self):
+        # Get initial number of users
+        initial_user_count = User.objects.count()
+        settings.SAML_CONFIG = conf.create_conf(
+            sp_host='sp.example.com',
+            idp_hosts=['idp.example.com'],
+            metadata_file='remote_metadata_one_idp.xml',
+        )
+
+        # session_id should start with a letter since it is a NCName
+        session_id = "a0123456789abcdef0123456789abcdef"
+        came_from = '/another-view/'
+        self.add_outstanding_query(session_id, came_from)
+
+        # this will create a user
+        saml_response = auth_response(session_id, 'student')
+        response = self.client.post(reverse('saml2_acs'), {
+                'SAMLResponse': self.b64_for_post(saml_response),
+                'RelayState': came_from,
+                })
+        self.assertEqual(response.status_code, 302)
+        location = response['Location']
+        url = urlparse(location)
+        self.assertEqual(url.path, came_from)
+
+        self.assertEqual(User.objects.count(), initial_user_count + 1)
+        user_id = self.client.session[SESSION_KEY]
+        user = User.objects.get(id=user_id)
+        self.assertEqual(user.username, 'student')
+
+        # let's create another user and log in with that one
+        new_user = User.objects.create(username='teacher', password='not-used')
+
+        session_id = "a1111111111111111111111111111111"
+        came_from = ''  # bad, let's see if we can deal with this
+        saml_response = auth_response(session_id, 'teacher')
+        self.add_outstanding_query(session_id, '/')
+        response = self.client.post(reverse('saml2_acs'), {
+                'SAMLResponse': self.b64_for_post(saml_response),
+                'RelayState': came_from,
+                })
+        self.assertEqual(response.status_code, 302)
+        location = response['Location']
+
+        url = urlparse(location)
+        # as the RelayState is empty we have redirect to LOGIN_REDIRECT_URL
+        self.assertEqual(url.path, settings.LOGIN_REDIRECT_URL)
+        self.assertEqual(force_text(new_user.id), self.client.session[SESSION_KEY])
+
+    def test_assertion_consumer_service_no_session(self):
+        settings.SAML_CONFIG = conf.create_conf(
+            sp_host='sp.example.com',
+            idp_hosts=['idp.example.com'],
+            metadata_file='remote_metadata_one_idp.xml',
+        )
+
+        # session_id should start with a letter since it is a NCName
+        session_id = "a0123456789abcdef0123456789abcdef"
+        came_from = '/another-view/'
+        self.add_outstanding_query(session_id, came_from)
+
+        # Authentication is confirmed.
+        saml_response = auth_response(session_id, 'student')
+        response = self.client.post(reverse('saml2_acs'), {
+            'SAMLResponse': self.b64_for_post(saml_response),
+            'RelayState': came_from,
+        })
+        self.assertEqual(response.status_code, 302)
+        location = response['Location']
+        url = urlparse(location)
+        self.assertEqual(url.path, came_from)
+
+        # Session should no longer be in outstanding queries.
+        saml_response = auth_response(session_id, 'student')
+        response = self.client.post(reverse('saml2_acs'), {
+            'SAMLResponse': self.b64_for_post(saml_response),
+            'RelayState': came_from,
+        })
+        self.assertEqual(response.status_code, 403)
+
+    def test_missing_param_to_assertion_consumer_service_request(self):
+        # Send request without SAML2Response parameter
+        response = self.client.post(reverse('saml2_acs'))
+        # Assert that view responded with "Bad Request" error
+        self.assertEqual(response.status_code, 400)
+
+    def test_bad_request_method_to_assertion_consumer_service(self):
+        # Send request with non-POST method.
+        response = self.client.get(reverse('saml2_acs'))
+        # Assert that view responded with method not allowed status
+        self.assertEqual(response.status_code, 405)
+
+    def do_login(self):
+        """Auxiliary method used in several tests (mainly logout tests)"""
+        self.init_cookies()
+
+        session_id = "a0123456789abcdef0123456789abcdef"
+        came_from = '/another-view/'
+        self.add_outstanding_query(session_id, came_from)
+
+        saml_response = auth_response(session_id, 'student')
+
+        # this will create a user
+        response = self.client.post(reverse('saml2_acs'), {
+                'SAMLResponse': self.b64_for_post(saml_response),
+                'RelayState': came_from,
+                })
+        self.assertEqual(response.status_code, 302)
+
+    @skip("This is a known issue caused by pysaml2. Needs more investigation. Fixes are welcome.")
+    def test_logout(self):
+        settings.SAML_CONFIG = conf.create_conf(
+            sp_host='sp.example.com',
+            idp_hosts=['idp.example.com'],
+            metadata_file='remote_metadata_one_idp.xml',
+        )
+        self.do_login()
+
+        response = self.client.get(reverse('saml2_logout'))
+        self.assertEqual(response.status_code, 302)
+        location = response['Location']
+
+        url = urlparse(location)
+        self.assertEqual(url.hostname, 'idp.example.com')
+        self.assertEqual(url.path,
+                         '/simplesaml/saml2/idp/SingleLogoutService.php')
+
+        params = parse_qs(url.query)
+        self.assertIn('SAMLRequest', params)
+
+        saml_request = params['SAMLRequest'][0]
+        if PY_VERSION < (3,):
+            expected_request = """<?xml version='1.0' encoding='UTF-8'?>
+<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/">58bcc81ea14700f66aeb707a0eff1360</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>"""
+        else:
+            expected_request = """<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/">58bcc81ea14700f66aeb707a0eff1360</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>"""
+        self.assertSAMLRequestsEquals(decode_base64_and_inflate(saml_request).decode('utf-8'),
+                                      expected_request)
+
+    def test_logout_service_local(self):
+        settings.SAML_CONFIG = conf.create_conf(
+            sp_host='sp.example.com',
+            idp_hosts=['idp.example.com'],
+            metadata_file='remote_metadata_one_idp.xml',
+        )
+
+        self.do_login()
+
+        response = self.client.get(reverse('saml2_logout'))
+        self.assertEqual(response.status_code, 302)
+        location = response['Location']
+
+        url = urlparse(location)
+        self.assertEqual(url.hostname, 'idp.example.com')
+        self.assertEqual(url.path,
+                         '/simplesaml/saml2/idp/SingleLogoutService.php')
+
+        params = parse_qs(url.query)
+        self.assertIn('SAMLRequest', params)
+
+        saml_request = params['SAMLRequest'][0]
+        if PY_VERSION < (3,):
+            expected_request = """<?xml version='1.0' encoding='UTF-8'?>
+<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/">58bcc81ea14700f66aeb707a0eff1360</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>"""
+        else:
+            expected_request = """<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/">58bcc81ea14700f66aeb707a0eff1360</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>"""
+        self.assertSAMLRequestsEquals(decode_base64_and_inflate(saml_request).decode('utf-8'),
+                                      expected_request)
+
+        # now simulate a logout response sent by the idp
+        request_id = re.findall(r' ID="(.*?)" ', expected_request)[0]
+        instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
+
+        saml_response = """<?xml version='1.0' encoding='UTF-8'?>
+<samlp:LogoutResponse xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="http://sp.example.com/saml2/ls/" ID="a140848e7ce2bce834d7264ecdde0151" InResponseTo="%s" IssueInstant="%s" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://idp.example.com/simplesaml/saml2/idp/metadata.php</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status></samlp:LogoutResponse>""" % (
+            request_id, instant)
+
+        response = self.client.get(reverse('saml2_ls'), {
+                'SAMLResponse': deflate_and_base64_encode(saml_response),
+                })
+        self.assertContains(response, "Logged out", status_code=200)
+        self.assertListEqual(list(self.client.session.keys()), [])
+
+    def test_logout_service_global(self):
+        settings.SAML_CONFIG = conf.create_conf(
+            sp_host='sp.example.com',
+            idp_hosts=['idp.example.com'],
+            metadata_file='remote_metadata_one_idp.xml',
+        )
+
+        self.do_login()
+
+        # now simulate a global logout process initiated by another SP
+        subject_id = views._get_subject_id(self.client.session)
+        instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
+        saml_request = """<?xml version='1.0' encoding='UTF-8'?>
+<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_9961abbaae6d06d251226cb25e38bf8f468036e57e" Version="2.0" IssueInstant="%s" Destination="http://sp.example.com/saml2/ls/"><saml:Issuer>https://idp.example.com/simplesaml/saml2/idp/metadata.php</saml:Issuer><saml:NameID SPNameQualifier="http://sp.example.com/saml2/metadata/" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">%s</saml:NameID><samlp:SessionIndex>_1837687b7bc9faad85839dbeb319627889f3021757</samlp:SessionIndex></samlp:LogoutRequest>""" % (instant, subject_id.text)
+
+        response = self.client.get(reverse('saml2_ls'), {
+                'SAMLRequest': deflate_and_base64_encode(saml_request),
+                })
+        self.assertEqual(response.status_code, 302)
+        location = response['Location']
+
+        url = urlparse(location)
+        self.assertEqual(url.hostname, 'idp.example.com')
+        self.assertEqual(url.path,
+                         '/simplesaml/saml2/idp/SingleLogoutService.php')
+
+        params = parse_qs(url.query)
+        self.assertIn('SAMLResponse', params)
+
+        saml_response = params['SAMLResponse'][0]
+        if PY_VERSION < (3,):
+            expected_response = """<?xml version='1.0' encoding='UTF-8'?>
+<samlp:LogoutResponse xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="a140848e7ce2bce834d7264ecdde0151" InResponseTo="_9961abbaae6d06d251226cb25e38bf8f468036e57e" IssueInstant="2010-09-05T09:10:12Z" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status></samlp:LogoutResponse>"""
+        else:
+            expected_response = """<samlp:LogoutResponse xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="a140848e7ce2bce834d7264ecdde0151" InResponseTo="_9961abbaae6d06d251226cb25e38bf8f468036e57e" IssueInstant="2010-09-05T09:10:12Z" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status></samlp:LogoutResponse>"""
+        self.assertSAMLRequestsEquals(decode_base64_and_inflate(saml_response).decode('utf-8'),
+                                      expected_response)
+
+    def test_incomplete_logout(self):
+        settings.SAML_CONFIG = conf.create_conf(sp_host='sp.example.com',
+                                                idp_hosts=['idp.example.com'])
+
+        # don't do a login
+
+        # now simulate a global logout process initiated by another SP
+        instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
+        saml_request = '<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_9961abbaae6d06d251226cb25e38bf8f468036e57e" Version="2.0" IssueInstant="%s" Destination="http://sp.example.com/saml2/ls/"><saml:Issuer>https://idp.example.com/simplesaml/saml2/idp/metadata.php</saml:Issuer><saml:NameID SPNameQualifier="http://sp.example.com/saml2/metadata/" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">%s</saml:NameID><samlp:SessionIndex>_1837687b7bc9faad85839dbeb319627889f3021757</samlp:SessionIndex></samlp:LogoutRequest>' % (
+            instant, 'invalid-subject-id')
+
+        response = self.client.get(reverse('saml2_ls'), {
+                'SAMLRequest': deflate_and_base64_encode(saml_request),
+                })
+        self.assertContains(response, 'Logout error', status_code=403)
+
+    def test_finish_logout_renders_error_template(self):
+        request = RequestFactory().get('/bar/foo')
+        response = finish_logout(request, None)
+        self.assertContains(response, "<h1>Logout error</h1>", status_code=200)
+
+    def _test_metadata(self):
+        settings.SAML_CONFIG = conf.create_conf(
+            sp_host='sp.example.com',
+            idp_hosts=['idp.example.com'],
+            metadata_file='remote_metadata_one_idp.xml',
+        )
+        valid_until = datetime.datetime.utcnow() + datetime.timedelta(hours=24)
+        valid_until = valid_until.strftime("%Y-%m-%dT%H:%M:%SZ")
+        expected_metadata = """<?xml version='1.0' encoding='UTF-8'?>
+<md:EntityDescriptor entityID="http://sp.example.com/saml2/metadata/" validUntil="%s" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"><md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><md:KeyDescriptor><ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:X509Data><ds:X509Certificate>MIIDPjCCAiYCCQCkHjPQlll+mzANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQGEwJF
+UzEQMA4GA1UECBMHU2V2aWxsYTEbMBkGA1UEChMSWWFjbyBTaXN0ZW1hcyBTLkwu
+MRAwDgYDVQQHEwdTZXZpbGxhMREwDwYDVQQDEwh0aWNvdGljbzAeFw0wOTEyMDQx
+OTQzNTJaFw0xMDEyMDQxOTQzNTJaMGExCzAJBgNVBAYTAkVTMRAwDgYDVQQIEwdT
+ZXZpbGxhMRswGQYDVQQKExJZYWNvIFNpc3RlbWFzIFMuTC4xEDAOBgNVBAcTB1Nl
+dmlsbGExETAPBgNVBAMTCHRpY290aWNvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEA7rMOMOaIZ/YYD5hYS6Hpjpovcu4k8gaIY+om9zCxLV5F8BLEfkxo
+Pk9IA3cRQNRxf7AXCFxEOH3nKy56AIi1gU7X6fCT30JBT8NQlYdgOVMLlR+tjy1b
+YV07tDa9U8gzjTyKQHgVwH0436+rmSPnacGj3fMwfySTMhtmrJmax0bIa8EB+gY1
+77DBtvf8dIZIXLlGMQFloZeUspvHOrgNoEA9xU4E9AanGnV9HeV37zv3mLDUOQLx
+4tk9sMQmylCpij7WZmcOV07DyJ/cEmnvHSalBTcyIgkcwlhmjtSgfCy6o5zuWxYd
+T9ia80SZbWzn8N6B0q+nq23+Oee9H0lvcwIDAQABMA0GCSqGSIb3DQEBBQUAA4IB
+AQCQBhKOqucJZAqGHx4ybDXNzpPethszonLNVg5deISSpWagy55KlGCi5laio/xq
+hHRx18eTzeCeLHQYvTQxw0IjZOezJ1X30DD9lEqPr6C+IrmZc6bn/pF76xsvdaRS
+gduNQPT1B25SV2HrEmbf8wafSlRARmBsyUHh860TqX7yFVjhYIAUF/El9rLca51j
+ljCIqqvT+klPdjQoZwODWPFHgute2oNRmoIcMjSnoy1+mxOC2Q/j7kcD8/etulg2
+XDxB3zD81gfdtT8VBFP+G4UrBa+5zFk6fT6U8a7ZqVsyH+rCXAdCyVlEC4Y5fZri
+ID4zT0FcZASGuthM56rRJJSx
+</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://sp.example.com/saml2/ls/" /><md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://sp.example.com/saml2/acs/" index="1" /><md:AttributeConsumingService index="1"><md:ServiceName xml:lang="en">Test SP</md:ServiceName><md:RequestedAttribute FriendlyName="uid" Name="urn:oid:0.9.2342.19200300.100.1.1" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="true" /><md:RequestedAttribute FriendlyName="eduPersonAffiliation" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.1" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="false" /></md:AttributeConsumingService></md:SPSSODescriptor><md:Organization><md:OrganizationName xml:lang="es">Ejemplo S.A.</md:OrganizationName><md:OrganizationName xml:lang="en">Example Inc.</md:OrganizationName><md:OrganizationDisplayName xml:lang="es">Ejemplo</md:OrganizationDisplayName><md:OrganizationDisplayName xml:lang="en">Example</md:OrganizationDisplayName><md:OrganizationURL xml:lang="es">http://www.example.es</md:OrganizationURL><md:OrganizationURL xml:lang="en">http://www.example.com</md:OrganizationURL></md:Organization><md:ContactPerson contactType="technical"><md:Company>Example Inc.</md:Company><md:GivenName>Technical givenname</md:GivenName><md:SurName>Technical surname</md:SurName><md:EmailAddress>technical@sp.example.com</md:EmailAddress></md:ContactPerson><md:ContactPerson contactType="administrative"><md:Company>Example Inc.</md:Company><md:GivenName>Administrative givenname</md:GivenName><md:SurName>Administrative surname</md:SurName><md:EmailAddress>administrative@sp.example.ccom</md:EmailAddress></md:ContactPerson></md:EntityDescriptor>"""
+
+        expected_metadata = expected_metadata % valid_until
+
+        response = self.client.get('/metadata/')
+        self.assertEqual(response['Content-type'], 'text/xml; charset=utf8')
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.content, expected_metadata)
+
+    def test_post_authenticated_signal(self):
+
+        def signal_handler(signal, user, session_info):
+            self.assertEqual(isinstance(user, User), True)
+
+        post_authenticated.connect(signal_handler, dispatch_uid='test_signal')
+
+        self.do_login()
+
+        post_authenticated.disconnect(dispatch_uid='test_signal')
+
+    def test_idplist_templatetag(self):
+        settings.SAML_CONFIG = conf.create_conf(
+            sp_host='sp.example.com',
+            idp_hosts=['idp1.example.com',
+                       'idp2.example.com',
+                       'idp3.example.com'],
+            metadata_file='remote_metadata_three_idps.xml',
+        )
+        rendered = self.render_template(
+            '{% load idplist %}'
+            '{% idplist as idps %}'
+            '{% for url, name in idps.items %}'
+            '{{ url }} - {{ name }}; '
+            '{% endfor %}'
+            )
+
+        # the idplist is unordered, so convert the result into a set.
+        rendered = set(rendered.split('; '))
+        expected = set([
+            u'https://idp1.example.com/simplesaml/saml2/idp/metadata.php - idp1.example.com IdP',
+            u'https://idp2.example.com/simplesaml/saml2/idp/metadata.php - idp2.example.com IdP',
+            u'https://idp3.example.com/simplesaml/saml2/idp/metadata.php - idp3.example.com IdP',
+            u'',
+        ])
+
+        self.assertEqual(rendered, expected)
+
+
+def test_config_loader(request):
+    config = SPConfig()
+    config.load({'entityid': 'testentity'})
+    return config
+
+
+def test_config_loader_with_real_conf(request):
+    config = SPConfig()
+    config.load(conf.create_conf(sp_host='sp.example.com',
+                                 idp_hosts=['idp.example.com'],
+                                 metadata_file='remote_metadata_one_idp.xml'))
+    return config
+
+
+class ConfTests(TestCase):
+
+    def test_custom_conf_loader(self):
+        config_loader_path = 'djangosaml2.tests.test_config_loader'
+        request = RequestFactory().get('/bar/foo')
+        conf = get_config(config_loader_path, request)
+
+        self.assertEqual(conf.entityid, 'testentity')
+
+    def test_custom_conf_loader_from_view(self):
+        config_loader_path = 'djangosaml2.tests.test_config_loader_with_real_conf'
+        request = RequestFactory().get('/login/')
+        request.user = AnonymousUser()
+        middleware = SessionMiddleware()
+        middleware.process_request(request)
+        request.session.save()
+        response = views.login(request, config_loader_path)
+        self.assertEqual(response.status_code, 302)
+        location = response['Location']
+
+        url = urlparse(location)
+        self.assertEqual(url.hostname, 'idp.example.com')
+        self.assertEqual(url.path, '/simplesaml/saml2/idp/SSOService.php')

+ 243 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/attribute-maps/saml_uri.py

@@ -0,0 +1,243 @@
+__author__ = 'rolandh'
+
+EDUPERSON_OID = "urn:oid:1.3.6.1.4.1.5923.1.1.1."
+X500ATTR_OID = "urn:oid:2.5.4."
+NOREDUPERSON_OID = "urn:oid:1.3.6.1.4.1.2428.90.1."
+NETSCAPE_LDAP = "urn:oid:2.16.840.1.113730.3.1."
+UCL_DIR_PILOT = 'urn:oid:0.9.2342.19200300.100.1.'
+PKCS_9 = "urn:oid:1.2.840.113549.1.9.1."
+UMICH = "urn:oid:1.3.6.1.4.1.250.1.57."
+SCHAC = "urn:oid:1.3.6.1.4.1.25178.1.2."
+
+MAP = {
+    "identifier": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
+    "fro": {
+        EDUPERSON_OID+'2': 'eduPersonNickname',
+        EDUPERSON_OID+'9': 'eduPersonScopedAffiliation',
+        EDUPERSON_OID+'11': 'eduPersonAssurance',
+        EDUPERSON_OID+'10': 'eduPersonTargetedID',
+        EDUPERSON_OID+'4': 'eduPersonOrgUnitDN',
+        NOREDUPERSON_OID+'6': 'norEduOrgAcronym',
+        NOREDUPERSON_OID+'7': 'norEduOrgUniqueIdentifier',
+        NOREDUPERSON_OID+'4': 'norEduPersonLIN',
+        EDUPERSON_OID+'1': 'eduPersonAffiliation',
+        NOREDUPERSON_OID+'2': 'norEduOrgUnitUniqueNumber',
+        NETSCAPE_LDAP+'40': 'userSMIMECertificate',
+        NOREDUPERSON_OID+'1': 'norEduOrgUniqueNumber',
+        NETSCAPE_LDAP+'241': 'displayName',
+        UCL_DIR_PILOT+'37': 'associatedDomain',
+        EDUPERSON_OID+'6': 'eduPersonPrincipalName',
+        NOREDUPERSON_OID+'8': 'norEduOrgUnitUniqueIdentifier',
+        NOREDUPERSON_OID+'9': 'federationFeideSchemaVersion',
+        X500ATTR_OID+'53': 'deltaRevocationList',
+        X500ATTR_OID+'52': 'supportedAlgorithms',
+        X500ATTR_OID+'51': 'houseIdentifier',
+        X500ATTR_OID+'50': 'uniqueMember',
+        X500ATTR_OID+'19': 'physicalDeliveryOfficeName',
+        X500ATTR_OID+'18': 'postOfficeBox',
+        X500ATTR_OID+'17': 'postalCode',
+        X500ATTR_OID+'16': 'postalAddress',
+        X500ATTR_OID+'15': 'businessCategory',
+        X500ATTR_OID+'14': 'searchGuide',
+        EDUPERSON_OID+'5': 'eduPersonPrimaryAffiliation',
+        X500ATTR_OID+'12': 'title',
+        X500ATTR_OID+'11': 'ou',
+        X500ATTR_OID+'10': 'o',
+        X500ATTR_OID+'37': 'cACertificate',
+        X500ATTR_OID+'36': 'userCertificate',
+        X500ATTR_OID+'31': 'member',
+        X500ATTR_OID+'30': 'supportedApplicationContext',
+        X500ATTR_OID+'33': 'roleOccupant',
+        X500ATTR_OID+'32': 'owner',
+        NETSCAPE_LDAP+'1': 'carLicense',
+        PKCS_9+'1': 'email',
+        NETSCAPE_LDAP+'3': 'employeeNumber',
+        NETSCAPE_LDAP+'2': 'departmentNumber',
+        X500ATTR_OID+'39': 'certificateRevocationList',
+        X500ATTR_OID+'38': 'authorityRevocationList',
+        NETSCAPE_LDAP+'216': 'userPKCS12',
+        EDUPERSON_OID+'8': 'eduPersonPrimaryOrgUnitDN',
+        X500ATTR_OID+'9': 'street',
+        X500ATTR_OID+'8': 'st',
+        NETSCAPE_LDAP+'39': 'preferredLanguage',
+        EDUPERSON_OID+'7': 'eduPersonEntitlement',
+        X500ATTR_OID+'2': 'knowledgeInformation',
+        X500ATTR_OID+'7': 'l',
+        X500ATTR_OID+'6': 'c',
+        X500ATTR_OID+'5': 'serialNumber',
+        X500ATTR_OID+'4': 'sn',
+        X500ATTR_OID+'3': 'cn',
+        UCL_DIR_PILOT+'60': 'jpegPhoto',
+        X500ATTR_OID+'65': 'pseudonym',
+        NOREDUPERSON_OID+'5': 'norEduPersonNIN',
+        UCL_DIR_PILOT+'3': 'mail',
+        UCL_DIR_PILOT+'25': 'dc',
+        X500ATTR_OID+'40': 'crossCertificatePair',
+        X500ATTR_OID+'42': 'givenName',
+        X500ATTR_OID+'43': 'initials',
+        X500ATTR_OID+'44': 'generationQualifier',
+        X500ATTR_OID+'45': 'x500UniqueIdentifier',
+        X500ATTR_OID+'46': 'dnQualifier',
+        X500ATTR_OID+'47': 'enhancedSearchGuide',
+        X500ATTR_OID+'48': 'protocolInformation',
+        X500ATTR_OID+'54': 'dmdName',
+        NETSCAPE_LDAP+'4': 'employeeType',
+        X500ATTR_OID+'22': 'teletexTerminalIdentifier',
+        X500ATTR_OID+'23': 'facsimileTelephoneNumber',
+        X500ATTR_OID+'20': 'telephoneNumber',
+        X500ATTR_OID+'21': 'telexNumber',
+        X500ATTR_OID+'26': 'registeredAddress',
+        X500ATTR_OID+'27': 'destinationIndicator',
+        X500ATTR_OID+'24': 'x121Address',
+        X500ATTR_OID+'25': 'internationaliSDNNumber',
+        X500ATTR_OID+'28': 'preferredDeliveryMethod',
+        X500ATTR_OID+'29': 'presentationAddress',
+        EDUPERSON_OID+'3': 'eduPersonOrgDN',
+        NOREDUPERSON_OID+'3': 'norEduPersonBirthDate',
+        UMICH+'57': 'labeledURI',
+        UCL_DIR_PILOT+'1': 'uid',
+        SCHAC+'1': 'schacMotherTongue',
+        SCHAC+'2': 'schacGender',
+        SCHAC+'3': 'schacDateOfBirth',
+        SCHAC+'4': 'schacPlaceOfBirth',
+        SCHAC+'5': 'schacCountryOfCitizenship',
+        SCHAC+'6': 'schacSn1',
+        SCHAC+'7': 'schacSn2',
+        SCHAC+'8': 'schacPersonalTitle',
+        SCHAC+'9': 'schacHomeOrganization',
+        SCHAC+'10': 'schacHomeOrganizationType',
+        SCHAC+'11': 'schacCountryOfResidence',
+        SCHAC+'12': 'schacUserPresenceID',
+        SCHAC+'13': 'schacPersonalPosition',
+        SCHAC+'14': 'schacPersonalUniqueCode',
+        SCHAC+'15': 'schacPersonalUniqueID',
+        SCHAC+'17': 'schacExpiryDate',
+        SCHAC+'18': 'schacUserPrivateAttribute',
+        SCHAC+'19': 'schacUserStatus',
+        SCHAC+'20': 'schacProjectMembership',
+        SCHAC+'21': 'schacProjectSpecificRole',
+    },
+    "to": {
+        'roleOccupant': X500ATTR_OID+'33',
+        'gn': X500ATTR_OID+'42',
+        'norEduPersonNIN': NOREDUPERSON_OID+'5',
+        'title': X500ATTR_OID+'12',
+        'facsimileTelephoneNumber': X500ATTR_OID+'23',
+        'mail': UCL_DIR_PILOT+'3',
+        'postOfficeBox': X500ATTR_OID+'18',
+        'fax': X500ATTR_OID+'23',
+        'telephoneNumber': X500ATTR_OID+'20',
+        'norEduPersonBirthDate': NOREDUPERSON_OID+'3',
+        'rfc822Mailbox': UCL_DIR_PILOT+'3',
+        'dc': UCL_DIR_PILOT+'25',
+        'countryName': X500ATTR_OID+'6',
+        'emailAddress': PKCS_9+'1',
+        'employeeNumber': NETSCAPE_LDAP+'3',
+        'organizationName': X500ATTR_OID+'10',
+        'eduPersonAssurance': EDUPERSON_OID+'11',
+        'norEduOrgAcronym': NOREDUPERSON_OID+'6',
+        'registeredAddress': X500ATTR_OID+'26',
+        'physicalDeliveryOfficeName': X500ATTR_OID+'19',
+        'associatedDomain': UCL_DIR_PILOT+'37',
+        'l': X500ATTR_OID+'7',
+        'stateOrProvinceName': X500ATTR_OID+'8',
+        'federationFeideSchemaVersion': NOREDUPERSON_OID+'9',
+        'pkcs9email': PKCS_9+'1',
+        'givenName': X500ATTR_OID+'42',
+        'givenname': X500ATTR_OID+'42',
+        'x500UniqueIdentifier': X500ATTR_OID+'45',
+        'eduPersonNickname': EDUPERSON_OID+'2',
+        'houseIdentifier': X500ATTR_OID+'51',
+        'street': X500ATTR_OID+'9',
+        'supportedAlgorithms': X500ATTR_OID+'52',
+        'preferredLanguage': NETSCAPE_LDAP+'39',
+        'postalAddress': X500ATTR_OID+'16',
+        'email': PKCS_9+'1',
+        'norEduOrgUnitUniqueIdentifier': NOREDUPERSON_OID+'8',
+        'eduPersonPrimaryOrgUnitDN': EDUPERSON_OID+'8',
+        'c': X500ATTR_OID+'6',
+        'teletexTerminalIdentifier': X500ATTR_OID+'22',
+        'o': X500ATTR_OID+'10',
+        'cACertificate': X500ATTR_OID+'37',
+        'telexNumber': X500ATTR_OID+'21',
+        'ou': X500ATTR_OID+'11',
+        'initials': X500ATTR_OID+'43',
+        'eduPersonOrgUnitDN': EDUPERSON_OID+'4',
+        'deltaRevocationList': X500ATTR_OID+'53',
+        'norEduPersonLIN': NOREDUPERSON_OID+'4',
+        'supportedApplicationContext': X500ATTR_OID+'30',
+        'eduPersonEntitlement': EDUPERSON_OID+'7',
+        'generationQualifier': X500ATTR_OID+'44',
+        'eduPersonAffiliation': EDUPERSON_OID+'1',
+        'edupersonaffiliation': EDUPERSON_OID+'1',
+        'eduPersonPrincipalName': EDUPERSON_OID+'6',
+        'edupersonprincipalname': EDUPERSON_OID+'6',
+        'localityName': X500ATTR_OID+'7',
+        'owner': X500ATTR_OID+'32',
+        'norEduOrgUnitUniqueNumber': NOREDUPERSON_OID+'2',
+        'searchGuide': X500ATTR_OID+'14',
+        'certificateRevocationList': X500ATTR_OID+'39',
+        'organizationalUnitName': X500ATTR_OID+'11',
+        'userCertificate': X500ATTR_OID+'36',
+        'preferredDeliveryMethod': X500ATTR_OID+'28',
+        'internationaliSDNNumber': X500ATTR_OID+'25',
+        'uniqueMember': X500ATTR_OID+'50',
+        'departmentNumber': NETSCAPE_LDAP+'2',
+        'enhancedSearchGuide': X500ATTR_OID+'47',
+        'userPKCS12': NETSCAPE_LDAP+'216',
+        'eduPersonTargetedID': EDUPERSON_OID+'10',
+        'norEduOrgUniqueNumber': NOREDUPERSON_OID+'1',
+        'x121Address': X500ATTR_OID+'24',
+        'destinationIndicator': X500ATTR_OID+'27',
+        'eduPersonPrimaryAffiliation': EDUPERSON_OID+'5',
+        'surname': X500ATTR_OID+'4',
+        'jpegPhoto': UCL_DIR_PILOT+'60',
+        'eduPersonScopedAffiliation': EDUPERSON_OID+'9',
+        'edupersonscopedaffiliation': EDUPERSON_OID+'9',
+        'protocolInformation': X500ATTR_OID+'48',
+        'knowledgeInformation': X500ATTR_OID+'2',
+        'employeeType': NETSCAPE_LDAP+'4',
+        'userSMIMECertificate': NETSCAPE_LDAP+'40',
+        'member': X500ATTR_OID+'31',
+        'streetAddress': X500ATTR_OID+'9',
+        'dmdName': X500ATTR_OID+'54',
+        'postalCode': X500ATTR_OID+'17',
+        'pseudonym': X500ATTR_OID+'65',
+        'dnQualifier': X500ATTR_OID+'46',
+        'crossCertificatePair': X500ATTR_OID+'40',
+        'eduPersonOrgDN': EDUPERSON_OID+'3',
+        'authorityRevocationList': X500ATTR_OID+'38',
+        'displayName': NETSCAPE_LDAP+'241',
+        'businessCategory': X500ATTR_OID+'15',
+        'serialNumber': X500ATTR_OID+'5',
+        'norEduOrgUniqueIdentifier': NOREDUPERSON_OID+'7',
+        'st': X500ATTR_OID+'8',
+        'carLicense': NETSCAPE_LDAP+'1',
+        'presentationAddress': X500ATTR_OID+'29',
+        'sn': X500ATTR_OID+'4',
+        'cn': X500ATTR_OID+'3',
+        'domainComponent': UCL_DIR_PILOT+'25',
+        'labeledURI': UMICH+'57',
+        'uid': UCL_DIR_PILOT+'1',
+        'schacMotherTongue':SCHAC+'1',
+        'schacGender': SCHAC+'2',
+        'schacDateOfBirth':SCHAC+'3',
+        'schacPlaceOfBirth': SCHAC+'4',
+        'schacCountryOfCitizenship':SCHAC+'5',
+        'schacSn1': SCHAC+'6',
+        'schacSn2': SCHAC+'7',
+        'schacPersonalTitle':SCHAC+'8',
+        'schacHomeOrganization': SCHAC+'9',
+        'schacHomeOrganizationType': SCHAC+'10',
+        'schacCountryOfResidence': SCHAC+'11',
+        'schacUserPresenceID': SCHAC+'12',
+        'schacPersonalPosition': SCHAC+'13',
+        'schacPersonalUniqueCode': SCHAC+'14',
+        'schacPersonalUniqueID': SCHAC+'15',
+        'schacExpiryDate': SCHAC+'17',
+        'schacUserPrivateAttribute': SCHAC+'18',
+        'schacUserStatus': SCHAC+'19',
+        'schacProjectMembership': SCHAC+'20',
+        'schacProjectSpecificRole': SCHAC+'21',
+    }
+}

+ 103 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/auth_response.py

@@ -0,0 +1,103 @@
+# Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es)
+# Copyright (C) 2010 Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#            http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import datetime
+
+
+def auth_response(session_id,
+                  uid,
+                  audience='http://sp.example.com/saml2/metadata/',
+                  acs_url='http://sp.example.com/saml2/acs/',
+                  metadata_url='http://sp.example.com/saml2/metadata/',
+                  attribute_statements=None):
+    """Generates a fresh signed authentication response
+
+    Params:
+        session_id: The session ID to generate the reponse for. Login set an
+            outstanding session ID, i.e. djangosaml2 waits for a response for
+            that session.
+        uid: Unique identifier for a User (will be present as an attribute in
+            the answer). Ignored when attribute_statements is not ``None``.
+        audience: SP entityid (used when PySAML validates the response
+            audience).
+        acs_url: URL where the response has been posted back.
+        metadata_url: URL where the SP metadata can be queried.
+        attribute_statements: An alternative XML AttributeStatement to use in
+            lieu of the default (uid). The uid argument is ignored when
+            attribute_statements is not ``None``.
+    """
+    timestamp = datetime.datetime.now() - datetime.timedelta(seconds=10)
+    tomorrow = datetime.datetime.now() + datetime.timedelta(days=1)
+    yesterday = datetime.datetime.now() - datetime.timedelta(days=1)
+
+    if attribute_statements is None:
+        attribute_statements = (
+            '<saml:AttributeStatement>'
+                '<saml:Attribute FriendlyName="uid" Name="urn:oid:0.9.2342.19200300.100.1.1" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">'
+                    '<saml:AttributeValue xsi:nil="true" xsi:type="xs:string">'
+                        '%(uid)s'
+                    '</saml:AttributeValue>'
+                '</saml:Attribute>'
+            '</saml:AttributeStatement>'
+        ) % {'uid': uid}
+
+    saml_response_tpl = (
+        "<?xml version='1.0' encoding='UTF-8'?>"
+        '<samlp:Response xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Destination="%(acs_url)s" ID="id-88b9f586a2a3a639f9327485cc37c40a" InResponseTo="%(session_id)s" IssueInstant="%(timestamp)s" Version="2.0">'
+            '<saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">'
+                'https://idp.example.com/simplesaml/saml2/idp/metadata.php'
+            '</saml:Issuer>'
+            '<samlp:Status>'
+                '<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />'
+            '</samlp:Status>'
+            '<saml:Assertion ID="id-093952102ceb73436e49cb91c58b0578" IssueInstant="%(timestamp)s" Version="2.0">'
+                '<saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">'
+                    'https://idp.example.com/simplesaml/saml2/idp/metadata.php'
+                '</saml:Issuer>'
+                '<saml:Subject>'
+                    '<saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" NameQualifier="" SPNameQualifier="%(metadata_url)s">'
+                        '1f87035b4c1325b296a53d92097e6b3fa36d7e30ee82e3fcb0680d60243c1f03'
+                    '</saml:NameID>'
+                    '<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">'
+                        '<saml:SubjectConfirmationData InResponseTo="%(session_id)s" NotOnOrAfter="%(tomorrow)s" Recipient="%(acs_url)s" />'
+                    '</saml:SubjectConfirmation>'
+                '</saml:Subject>'
+                '<saml:Conditions NotBefore="%(yesterday)s" NotOnOrAfter="%(tomorrow)s">'
+                    '<saml:AudienceRestriction>'
+                        '<saml:Audience>'
+                            '%(audience)s'
+                        '</saml:Audience>'
+                    '</saml:AudienceRestriction>'
+                '</saml:Conditions>'
+                '<saml:AuthnStatement AuthnInstant="%(timestamp)s" SessionIndex="%(session_id)s">'
+                    '<saml:AuthnContext>'
+                        '<saml:AuthnContextClassRef>'
+                            'urn:oasis:names:tc:SAML:2.0:ac:classes:Password'
+                        '</saml:AuthnContextClassRef>'
+                    '</saml:AuthnContext>'
+                '</saml:AuthnStatement>'
+                '%(attribute_statements)s'
+            '</saml:Assertion>'
+        '</samlp:Response>')
+    return saml_response_tpl % {
+        'session_id': session_id,
+        'audience': audience,
+        'acs_url': acs_url,
+        'metadata_url': metadata_url,
+        'attribute_statements': attribute_statements,
+        'timestamp': timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'),
+        'tomorrow': tomorrow.strftime('%Y-%m-%dT%H:%M:%SZ'),
+        'yesterday': yesterday.strftime('%Y-%m-%dT%H:%M:%SZ'),
+    }

+ 104 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/conf.py

@@ -0,0 +1,104 @@
+# Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es)
+# Copyright (C) 2010 Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#            http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os.path
+
+import saml2
+
+
+def create_conf(sp_host='sp.example.com', idp_hosts=['idp.example.com'],
+                metadata_file='remote_metadata.xml'):
+
+    try:
+        from saml2.sigver import get_xmlsec_binary
+    except ImportError:
+        get_xmlsec_binary = None
+
+    if get_xmlsec_binary:
+        xmlsec_path = get_xmlsec_binary(["/opt/local/bin"])
+    else:
+        xmlsec_path = '/usr/bin/xmlsec1'
+
+    BASEDIR = os.path.dirname(os.path.abspath(__file__))
+    config = {
+        'xmlsec_binary': xmlsec_path,
+        'entityid': 'http://%s/saml2/metadata/' % sp_host,
+        'attribute_map_dir': os.path.join(BASEDIR, 'attribute-maps'),
+
+        'service': {
+            'sp': {
+                'name': 'Test SP',
+                'name_id_format': saml2.saml.NAMEID_FORMAT_PERSISTENT,
+                'endpoints': {
+                    'assertion_consumer_service': [
+                        ('http://%s/saml2/acs/' % sp_host,
+                         saml2.BINDING_HTTP_POST),
+                        ],
+                    'single_logout_service': [
+                        ('http://%s/saml2/ls/' % sp_host,
+                         saml2.BINDING_HTTP_REDIRECT),
+                        ],
+                    },
+                'required_attributes': ['uid'],
+                'optional_attributes': ['eduPersonAffiliation'],
+                'idp': {},  # this is filled later
+                'want_response_signed': False,
+                },
+            },
+
+        'metadata': {
+            'local': [os.path.join(BASEDIR, metadata_file)],
+            },
+
+        'debug': 1,
+
+        # certificates
+        'key_file': os.path.join(BASEDIR, 'mycert.key'),
+        'cert_file': os.path.join(BASEDIR, 'mycert.pem'),
+
+        # These fields are only used when generating the metadata
+        'contact_person': [
+            {'given_name': 'Technical givenname',
+             'sur_name': 'Technical surname',
+             'company': 'Example Inc.',
+             'email_address': 'technical@sp.example.com',
+             'contact_type': 'technical'},
+            {'given_name': 'Administrative givenname',
+             'sur_name': 'Administrative surname',
+             'company': 'Example Inc.',
+             'email_address': 'administrative@sp.example.ccom',
+             'contact_type': 'administrative'},
+            ],
+        'organization': {
+            'name': [('Ejemplo S.A.', 'es'), ('Example Inc.', 'en')],
+            'display_name': [('Ejemplo', 'es'), ('Example', 'en')],
+            'url': [('http://www.example.es', 'es'),
+                    ('http://www.example.com', 'en')],
+            },
+        'valid_for': 24,
+        }
+
+    for idp in idp_hosts:
+        entity_id = 'https://%s/simplesaml/saml2/idp/metadata.php' % idp
+        config['service']['sp']['idp'][entity_id] = {
+            'single_sign_on_service': {
+                saml2.BINDING_HTTP_REDIRECT: 'https://%s/simplesaml/saml2/idp/SSOService.php' % idp,
+                },
+            'single_logout_service': {
+                saml2.BINDING_HTTP_REDIRECT: 'https://%s/simplesaml/saml2/idp/SingleLogoutService.php' % idp,
+                },
+            }
+
+    return config

+ 17 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/idpcert.csr

@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICrTCCAZUCAQAwaDELMAkGA1UEBhMCRVMxEDAOBgNVBAgMB1NldmlsbGExGzAZ
+BgNVBAoMEllhY28gU2lzdGVtYXMgUy5MLjEQMA4GA1UEBwwHU2V2aWxsYTEYMBYG
+A1UEAwwPaWRwLmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEAsUBPANpwZb1MUmRsEKMsb+v5eHNih8J/fU7g6iUtvBHacjuQeZEVye/K
+qxMOGC+wKW53xUDeITSs91w79eztm+QwdpZPzfjuKH4q5LNeMj6E8YwGw9vymF4b
+gsZfZ7iKY+RkqubH7bzYtPSeTtqDkNPlJy6qjpuFMaEkbjAaSAm0KW84/NpMnZn6
+HRATWs0noqNDo7yHafTRvtCbJbFp6cCbkTd4h0WeolQBmRisg7pMAmC8uuA06CX2
+hU8Ej5/unGw/hCMsF5ysPDYUzLwI18m+kZSgE+Yw2pkVdJcEtmJjw/HqbzJJ+rH4
+E8TdWHK7mMi13EwbyEav5d1shN7erwIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEB
+AJIrkVa22Yoi5TBIq5grZhDkyCFkxLn8xIGbr3eS4VSq6osgqsALCuxGAioXGoR7
+QkczLXz4rWVGCZBF8yGZ3/zujSW8ajqjLqnwgu4hK8TlgtBiIWG7kq+1/yTWD0zl
+kSts9WGKWKdSYHGAX8vTAFpYGVnw76H6Fd+cJwnuk0Zym6I9Vr4lTWtBQeVLrMxM
+8AlBYMAgJS3JGgsqAhcxv4oNdMKec6nJmJPSggWUmdNQN8Cluq30kJj3GGtuRd0c
+Z0qgTvBQzlSty63nqS76EFNdQiaIKfwvracqoDFIFkqvZgznQih40jzqhRpQ6E/Y
+jwz0DPWrG/7E5c/ga9yUt3U=
+-----END CERTIFICATE REQUEST-----

+ 27 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/idpcert.key

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAsUBPANpwZb1MUmRsEKMsb+v5eHNih8J/fU7g6iUtvBHacjuQ
+eZEVye/KqxMOGC+wKW53xUDeITSs91w79eztm+QwdpZPzfjuKH4q5LNeMj6E8YwG
+w9vymF4bgsZfZ7iKY+RkqubH7bzYtPSeTtqDkNPlJy6qjpuFMaEkbjAaSAm0KW84
+/NpMnZn6HRATWs0noqNDo7yHafTRvtCbJbFp6cCbkTd4h0WeolQBmRisg7pMAmC8
+uuA06CX2hU8Ej5/unGw/hCMsF5ysPDYUzLwI18m+kZSgE+Yw2pkVdJcEtmJjw/Hq
+bzJJ+rH4E8TdWHK7mMi13EwbyEav5d1shN7erwIDAQABAoIBAQCFkQk3gmOaNvhR
+Sf0o2Fz/Bdnai1BfLxB088CGkHeTNfzfgcUP5mV94yVcnqJLVXww7F5ylLwOV6xT
+RfylB+HRTDW81u3SL1f/yXs3FXbQ882oWzUp2A9KA/hFJoj0FtqqBYxaQEe9/UVr
+rr2we/cSZqpSSVca2VSYHm7eXX8gcnZa42jKrLLKFfybq4Pw/917tvKnWnES7MV1
+2jt2+ZeS/Q5GqqJR7hhyVwim10ifO2Lh9c0TB9e/U7ddyTIt6VfKusW7YiOrkRwx
+ZM1NpZly1IAxK55ceV6w7IX9y1FM6A8ZvV7bqfVn13S5Zdyjr5XkiU10WXBsHodx
+ZndU2SFRAoGBANqvzFjEcWkaDIYnV/vSPmDMrgMA5FL1cUN3if5+LRatiXeQKbnv
+nVA5edGnB8LlkMDD4oDjCqx0xVbxaWqS4X8WEPa1/xss34y1MRg6yMZgu60mkzPM
+pj+i5mk1Kvsjm6uYz6XPtm7//GWl2y+zgAl8+bgzeUFD5HKV4B+8iZPnAoGBAM9+
+nSZDTFrbA5z5vGLXJlOdAx38ffQTfC1isb+M5I8NhE4CctaYcXcgMbhl+avfEsK9
+Wgpivmd7KTnhTujnf4WaIoe655TpnXJJ6dCdXDWktWZTErfqAXTTl945BLrQuGbe
+EIAN+1bYqGmk73U/Uw28c8hbTmY6GHTEyQSWMYX5AoGBAKxcfw0/17tlApYCEIC0
+VuHosQZA/7S7KwhoAWWKgXMsV/rar2iTiUQf6PnrUly0n4CvY6j+Sf1fE+LQ56tO
+FVkbRUeOboE2vwOiFA3q1zA0MffpPYBIPohNlpk5hKTojduT16XyrvGR5ZcgQD+6
+lKHl1NTwDRP5tObzZfDdovnlAoGBAIXpeRKQrF6WqqZMpsBDioC7/J8FrWQwjxvb
+bkvpajjIyHJwMh09FT2EkZIofhHmTf1QpyO8xpWSbvDj8EFv5mUbLN3cSklY3Dw+
+Z6AzbqdQPaJkSthXNcloJcNNmTfYLKp29r8uRt+txEMqJ0DMNZXP4gmUo+xl4hK6
+TeGf7SZBAoGAdTfeG5EmOoTVITw3imvoyETtbXO309YeUhk14p0OELK25w8Dnh70
+OLTaY5o4zfUh0noKQSTM9sgxkBCFhwYPxw2CoIWikc1+izAiFesWpgVp8rkFPWuI
+B7+5OACkAYK2DGzdlwwVti3LkrvW8gOZ7CUS5W7XzhmpcH6/+mHaKEY=
+-----END RSA PRIVATE KEY-----

+ 20 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/idpcert.pem

@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDTDCCAjQCCQDs8RuyGDEFWTANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJF
+UzEQMA4GA1UECAwHU2V2aWxsYTEbMBkGA1UECgwSWWFjbyBTaXN0ZW1hcyBTLkwu
+MRAwDgYDVQQHDAdTZXZpbGxhMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcN
+MTAwODI4MTAyNjQ1WhcNMTEwODI4MTAyNjQ1WjBoMQswCQYDVQQGEwJFUzEQMA4G
+A1UECAwHU2V2aWxsYTEbMBkGA1UECgwSWWFjbyBTaXN0ZW1hcyBTLkwuMRAwDgYD
+VQQHDAdTZXZpbGxhMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxQE8A2nBlvUxSZGwQoyxv6/l4c2KHwn99
+TuDqJS28EdpyO5B5kRXJ78qrEw4YL7ApbnfFQN4hNKz3XDv17O2b5DB2lk/N+O4o
+firks14yPoTxjAbD2/KYXhuCxl9nuIpj5GSq5sftvNi09J5O2oOQ0+UnLqqOm4Ux
+oSRuMBpICbQpbzj82kydmfodEBNazSeio0OjvIdp9NG+0JslsWnpwJuRN3iHRZ6i
+VAGZGKyDukwCYLy64DToJfaFTwSPn+6cbD+EIywXnKw8NhTMvAjXyb6RlKAT5jDa
+mRV0lwS2YmPD8epvMkn6sfgTxN1YcruYyLXcTBvIRq/l3WyE3t6vAgMBAAEwDQYJ
+KoZIhvcNAQEFBQADggEBAHLT+SirLvjzGb1kPJZq5hDhYAMIrUFSgU/ghNRd3tDw
+ryOHh9nHgjDq4siy9cRL19LRgly1wspErUTmL/cD6A6L7t6CFUXgXEzshJ+RsZz7
+Nbg+61pfK+4+OyO2I3pzGXAHsqLuUpUQFpwHBLu9YiHzY+uiKLgODZl5B3A8nqLN
+2NJ9uH9+YWgquxB6KQLW8cx9kC3AWAsEWihYFb22Uc6I8qFngmDldeHPgVFbt6nV
+74F28qlbWr69NvGMGHZdfL2Ts+KC/yer88+tNrUrJ1tV1jmaMglfWoVTIVMI0Agl
+/jPhsxhx0+HHOuIfcD6b334mi/UZz91y/d7poiiiMtY=
+-----END CERTIFICATE-----

+ 17 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/mycert.csr

@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICpjCCAY4CAQAwYTELMAkGA1UEBhMCRVMxEDAOBgNVBAgTB1NldmlsbGExGzAZ
+BgNVBAoTEllhY28gU2lzdGVtYXMgUy5MLjEQMA4GA1UEBxMHU2V2aWxsYTERMA8G
+A1UEAxMIdGljb3RpY28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDu
+sw4w5ohn9hgPmFhLoemOmi9y7iTyBohj6ib3MLEtXkXwEsR+TGg+T0gDdxFA1HF/
+sBcIXEQ4fecrLnoAiLWBTtfp8JPfQkFPw1CVh2A5UwuVH62PLVthXTu0Nr1TyDON
+PIpAeBXAfTjfr6uZI+dpwaPd8zB/JJMyG2asmZrHRshrwQH6BjXvsMG29/x0hkhc
+uUYxAWWhl5Sym8c6uA2gQD3FTgT0BqcadX0d5XfvO/eYsNQ5AvHi2T2wxCbKUKmK
+PtZmZw5XTsPIn9wSae8dJqUFNzIiCRzCWGaO1KB8LLqjnO5bFh1P2JrzRJltbOfw
+3oHSr6erbf45570fSW9zAgMBAAGgADANBgkqhkiG9w0BAQUFAAOCAQEAoK9viSij
+kqOwofEUvoJTMdcONm/Ext1yHIilsC3h5ZU451u0kurg4uuwpAOoZDOXtmZHfGOE
+/WQ0Juojpwco/SF1I+QGr7coq26xNQldsHlKBuO/wIrgVdtVfjOc+TxS/szMTZv5
+whZoZe6HEdxFBvVVedtOMiXONZzzzK3cycSgaQz7BglYgfbFuwB3hdV1Y+iMfQfq
+PoIxWrjeDJa1LgFBDkklpgWYLFiaMVvhhdZVFXs/E66OdFwZg+OEplb97bLVba8T
+BxDqu6yhNlDlnhaLBd5lIfDCqy5OXa65MqS3vIR9k5CCAwwtaPPxQ2ChUXYhwj0n
+aW9RyTsESl7Z+Q==
+-----END CERTIFICATE REQUEST-----

+ 27 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/mycert.key

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA7rMOMOaIZ/YYD5hYS6Hpjpovcu4k8gaIY+om9zCxLV5F8BLE
+fkxoPk9IA3cRQNRxf7AXCFxEOH3nKy56AIi1gU7X6fCT30JBT8NQlYdgOVMLlR+t
+jy1bYV07tDa9U8gzjTyKQHgVwH0436+rmSPnacGj3fMwfySTMhtmrJmax0bIa8EB
++gY177DBtvf8dIZIXLlGMQFloZeUspvHOrgNoEA9xU4E9AanGnV9HeV37zv3mLDU
+OQLx4tk9sMQmylCpij7WZmcOV07DyJ/cEmnvHSalBTcyIgkcwlhmjtSgfCy6o5zu
+WxYdT9ia80SZbWzn8N6B0q+nq23+Oee9H0lvcwIDAQABAoIBAEXolw1nVyfrgVx/
+58wu3XJwYdktOhDQLP3mRAc9cYayB5WqSXYb9qPZIGQzaRAtqBgXgIdoTmqlJSEW
+eZDSeSYn60COvyAyDWLI9z7z6RCg69F+95vpUswPPD8pkQWKqt6AjpUXFnfLtO5+
+SqmNRGdK2S1V3iw+kAWq1MVUL2qRFaC6izY3eln/C27mqVDLwb/SWGfl90S+/cNB
+4AyI5LaTT6fwTxjkxlJFIWF9qWjzXM2skRzb/V2QzCF+RiS27maNDnVJMGHWacmo
+2idxOyU+Q0VXmY0ycKkIOrSqoq+F168VxfZNMqUN9fyLu+AjEJmIZWC3jRX9P2NX
+ILQjMkECgYEA/sXSyjj05+oFCr/XSvBDy1/JnB2tOTTk/lFXe2IQpJpEb0RqNmYf
+vk/5OteMsDcxf13c4Gk7xJXkkgZutXa66PSL4h8DzBAwvl9R0YuWHhDyj4oeYCXl
+VsmIjtaUoPS297jckpQMXsuT7YSZpuycJmUGJ0WVq4wuuATIPIxIA9ECgYEA79lp
+N46rWGfel2+ruHxt/OZ5fbYzj6VnuwcwrDe2CcYmZ07SRH7AlnwRdPLyY7TGt+p2
+7RjhJ8jpjReJK5yNrnq2D82KgJOwysCGCNDHWf2llEW8Pv66MhDqCn86RYyFbiGn
+4jEb7IcWdmUfMPpOu3TWhxVl/AxUhA8asz1oJAMCgYEAtdoIgrWzAhLFdI3Io8Hp
+8jG2G4wHSC0cQvdWpUgzLvq6XF2OHrQ4dkRpVnni/yj2WL5r2Xbj5YdEdoLG5RoR
+ghSEAGw47qCj2k75fMPQ7DcWnCRvWBvUnmUN5z79KgJi02GNd8bbKZLQTRp3/nEn
+aDR19vQxSBiwhENNlgJfqPECgYAMdTJt3E8yDFMXcolsz6m21RHCYdBTybeVk04H
+4+zknRIpk4KAZEUEi/UsKeJFI4Ke0uLSddRcCKd42JwbU8pYIa+LKpXjD8jC/zT3
+CEESf4Y2KVkZvIlXSGGfofQY4K+dhMn/iaV1p56XD7GLDbVBL1RlN8tQSCOrqE0u
+uiXKmQKBgQCrBHEns8QMjWsMRfh+5ebSQNRY1XVtIWV10MMsA3zZqRLUwC0XcU2Z
+FEF0FvXgWGC/MQOxXA5ACnToEC7PTfg5WPHxvf0dFi8sGr6nE6yWIzvNJKdp4rz3
+q+99h7P2j25C2CmXJ2uS4zhXzaNbZ+UpyDDIzi7Ndw40A3wuh9U7Qw==
+-----END RSA PRIVATE KEY-----

+ 20 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/mycert.pem

@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDPjCCAiYCCQCkHjPQlll+mzANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQGEwJF
+UzEQMA4GA1UECBMHU2V2aWxsYTEbMBkGA1UEChMSWWFjbyBTaXN0ZW1hcyBTLkwu
+MRAwDgYDVQQHEwdTZXZpbGxhMREwDwYDVQQDEwh0aWNvdGljbzAeFw0wOTEyMDQx
+OTQzNTJaFw0xMDEyMDQxOTQzNTJaMGExCzAJBgNVBAYTAkVTMRAwDgYDVQQIEwdT
+ZXZpbGxhMRswGQYDVQQKExJZYWNvIFNpc3RlbWFzIFMuTC4xEDAOBgNVBAcTB1Nl
+dmlsbGExETAPBgNVBAMTCHRpY290aWNvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEA7rMOMOaIZ/YYD5hYS6Hpjpovcu4k8gaIY+om9zCxLV5F8BLEfkxo
+Pk9IA3cRQNRxf7AXCFxEOH3nKy56AIi1gU7X6fCT30JBT8NQlYdgOVMLlR+tjy1b
+YV07tDa9U8gzjTyKQHgVwH0436+rmSPnacGj3fMwfySTMhtmrJmax0bIa8EB+gY1
+77DBtvf8dIZIXLlGMQFloZeUspvHOrgNoEA9xU4E9AanGnV9HeV37zv3mLDUOQLx
+4tk9sMQmylCpij7WZmcOV07DyJ/cEmnvHSalBTcyIgkcwlhmjtSgfCy6o5zuWxYd
+T9ia80SZbWzn8N6B0q+nq23+Oee9H0lvcwIDAQABMA0GCSqGSIb3DQEBBQUAA4IB
+AQCQBhKOqucJZAqGHx4ybDXNzpPethszonLNVg5deISSpWagy55KlGCi5laio/xq
+hHRx18eTzeCeLHQYvTQxw0IjZOezJ1X30DD9lEqPr6C+IrmZc6bn/pF76xsvdaRS
+gduNQPT1B25SV2HrEmbf8wafSlRARmBsyUHh860TqX7yFVjhYIAUF/El9rLca51j
+ljCIqqvT+klPdjQoZwODWPFHgute2oNRmoIcMjSnoy1+mxOC2Q/j7kcD8/etulg2
+XDxB3zD81gfdtT8VBFP+G4UrBa+5zFk6fT6U8a7ZqVsyH+rCXAdCyVlEC4Y5fZri
+ID4zT0FcZASGuthM56rRJJSx
+-----END CERTIFICATE-----

+ 123 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/remote_metadata.xml

@@ -0,0 +1,123 @@
+<?xml version="1.0"?>
+<md:EntitiesDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+  <md:EntityDescriptor entityID="https://idp.example.com/simplesaml/saml2/idp/metadata.php">
+    <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+      <md:KeyDescriptor use="signing">
+        <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+          <ds:X509Data>
+            <ds:X509Certificate>MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo</ds:X509Certificate>
+          </ds:X509Data>
+        </ds:KeyInfo>
+      </md:KeyDescriptor>
+      <md:KeyDescriptor use="encryption">
+        <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+          <ds:X509Data>
+            <ds:X509Certificate>MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo</ds:X509Certificate>
+          </ds:X509Data>
+        </ds:KeyInfo>
+      </md:KeyDescriptor>
+      <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php"/>
+      <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
+      <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp.example.com/simplesaml/saml2/idp/SSOService.php"/>
+    </md:IDPSSODescriptor>
+    <md:Organization>
+      <md:OrganizationName xml:lang="en">Lorenzo's test IdP</md:OrganizationName>
+      <md:OrganizationDisplayName xml:lang="en">idp.example.com IdP</md:OrganizationDisplayName>
+      <md:OrganizationURL xml:lang="en">http://idp.example.com/</md:OrganizationURL>
+    </md:Organization>
+    <md:ContactPerson contactType="technical">
+      <md:SurName>Administrator</md:SurName>
+      <md:EmailAddress>lgs@yaco.es</md:EmailAddress>
+    </md:ContactPerson>
+  </md:EntityDescriptor>
+  <md:EntityDescriptor entityID="https://idp1.example.com/simplesaml/saml2/idp/metadata.php">
+    <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+      <md:KeyDescriptor use="signing">
+        <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+          <ds:X509Data>
+            <ds:X509Certificate>MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo</ds:X509Certificate>
+          </ds:X509Data>
+        </ds:KeyInfo>
+      </md:KeyDescriptor>
+      <md:KeyDescriptor use="encryption">
+        <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+          <ds:X509Data>
+            <ds:X509Certificate>MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo</ds:X509Certificate>
+          </ds:X509Data>
+        </ds:KeyInfo>
+      </md:KeyDescriptor>
+      <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp1.example.com/simplesaml/saml2/idp/SingleLogoutService.php"/>
+      <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
+      <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp1.example.com/simplesaml/saml2/idp/SSOService.php"/>
+    </md:IDPSSODescriptor>
+    <md:Organization>
+      <md:OrganizationName xml:lang="en">Lorenzo's test IdP</md:OrganizationName>
+      <md:OrganizationDisplayName xml:lang="en">idp1.example.com IdP</md:OrganizationDisplayName>
+      <md:OrganizationURL xml:lang="en">http://idp1.example.com/</md:OrganizationURL>
+    </md:Organization>
+    <md:ContactPerson contactType="technical">
+      <md:SurName>Administrator</md:SurName>
+      <md:EmailAddress>lgs@yaco.es</md:EmailAddress>
+    </md:ContactPerson>
+  </md:EntityDescriptor>
+  <md:EntityDescriptor entityID="https://idp2.example.com/simplesaml/saml2/idp/metadata.php">
+    <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+      <md:KeyDescriptor use="signing">
+        <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+          <ds:X509Data>
+            <ds:X509Certificate>MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo</ds:X509Certificate>
+          </ds:X509Data>
+        </ds:KeyInfo>
+      </md:KeyDescriptor>
+      <md:KeyDescriptor use="encryption">
+        <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+          <ds:X509Data>
+            <ds:X509Certificate>MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo</ds:X509Certificate>
+          </ds:X509Data>
+        </ds:KeyInfo>
+      </md:KeyDescriptor>
+      <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp2.example.com/simplesaml/saml2/idp/SingleLogoutService.php"/>
+      <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
+      <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp2.example.com/simplesaml/saml2/idp/SSOService.php"/>
+    </md:IDPSSODescriptor>
+    <md:Organization>
+      <md:OrganizationName xml:lang="en">Lorenzo's test IdP</md:OrganizationName>
+      <md:OrganizationDisplayName xml:lang="en">idp2.example.com IdP</md:OrganizationDisplayName>
+      <md:OrganizationURL xml:lang="en">http://idp2.example.com/</md:OrganizationURL>
+    </md:Organization>
+    <md:ContactPerson contactType="technical">
+      <md:SurName>Administrator</md:SurName>
+      <md:EmailAddress>lgs@yaco.es</md:EmailAddress>
+    </md:ContactPerson>
+  </md:EntityDescriptor>
+  <md:EntityDescriptor entityID="https://idp3.example.com/simplesaml/saml2/idp/metadata.php">
+    <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+      <md:KeyDescriptor use="signing">
+        <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+          <ds:X509Data>
+            <ds:X509Certificate>MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo</ds:X509Certificate>
+          </ds:X509Data>
+        </ds:KeyInfo>
+      </md:KeyDescriptor>
+      <md:KeyDescriptor use="encryption">
+        <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+          <ds:X509Data>
+            <ds:X509Certificate>MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo</ds:X509Certificate>
+          </ds:X509Data>
+        </ds:KeyInfo>
+      </md:KeyDescriptor>
+      <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp3.example.com/simplesaml/saml2/idp/SingleLogoutService.php"/>
+      <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
+      <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp3.example.com/simplesaml/saml2/idp/SSOService.php"/>
+    </md:IDPSSODescriptor>
+    <md:Organization>
+      <md:OrganizationName xml:lang="en">Lorenzo's test IdP</md:OrganizationName>
+      <md:OrganizationDisplayName xml:lang="en">idp3.example.com IdP</md:OrganizationDisplayName>
+      <md:OrganizationURL xml:lang="en">http://idp3.example.com/</md:OrganizationURL>
+    </md:Organization>
+    <md:ContactPerson contactType="technical">
+      <md:SurName>Administrator</md:SurName>
+      <md:EmailAddress>lgs@yaco.es</md:EmailAddress>
+    </md:ContactPerson>
+  </md:EntityDescriptor>
+</md:EntitiesDescriptor>

+ 33 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/remote_metadata_one_idp.xml

@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<md:EntitiesDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+  <md:EntityDescriptor entityID="https://idp.example.com/simplesaml/saml2/idp/metadata.php">
+    <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+      <md:KeyDescriptor use="signing">
+        <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+          <ds:X509Data>
+            <ds:X509Certificate>MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo</ds:X509Certificate>
+          </ds:X509Data>
+        </ds:KeyInfo>
+      </md:KeyDescriptor>
+      <md:KeyDescriptor use="encryption">
+        <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+          <ds:X509Data>
+            <ds:X509Certificate>MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo</ds:X509Certificate>
+          </ds:X509Data>
+        </ds:KeyInfo>
+      </md:KeyDescriptor>
+      <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php"/>
+      <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
+      <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp.example.com/simplesaml/saml2/idp/SSOService.php"/>
+    </md:IDPSSODescriptor>
+    <md:Organization>
+      <md:OrganizationName xml:lang="en">Lorenzo's test IdP</md:OrganizationName>
+      <md:OrganizationDisplayName xml:lang="en">idp.example.com IdP</md:OrganizationDisplayName>
+      <md:OrganizationURL xml:lang="en">http://idp.example.com/</md:OrganizationURL>
+    </md:Organization>
+    <md:ContactPerson contactType="technical">
+      <md:SurName>Administrator</md:SurName>
+      <md:EmailAddress>lgs@yaco.es</md:EmailAddress>
+    </md:ContactPerson>
+  </md:EntityDescriptor>
+</md:EntitiesDescriptor>

+ 93 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/remote_metadata_three_idps.xml

@@ -0,0 +1,93 @@
+<?xml version="1.0"?>
+<md:EntitiesDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+  <md:EntityDescriptor entityID="https://idp1.example.com/simplesaml/saml2/idp/metadata.php">
+    <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+      <md:KeyDescriptor use="signing">
+        <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+          <ds:X509Data>
+            <ds:X509Certificate>MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo</ds:X509Certificate>
+          </ds:X509Data>
+        </ds:KeyInfo>
+      </md:KeyDescriptor>
+      <md:KeyDescriptor use="encryption">
+        <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+          <ds:X509Data>
+            <ds:X509Certificate>MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo</ds:X509Certificate>
+          </ds:X509Data>
+        </ds:KeyInfo>
+      </md:KeyDescriptor>
+      <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp1.example.com/simplesaml/saml2/idp/SingleLogoutService.php"/>
+      <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
+      <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp1.example.com/simplesaml/saml2/idp/SSOService.php"/>
+    </md:IDPSSODescriptor>
+    <md:Organization>
+      <md:OrganizationName xml:lang="en">Lorenzo's test IdP</md:OrganizationName>
+      <md:OrganizationDisplayName xml:lang="en">idp1.example.com IdP</md:OrganizationDisplayName>
+      <md:OrganizationURL xml:lang="en">http://idp1.example.com/</md:OrganizationURL>
+    </md:Organization>
+    <md:ContactPerson contactType="technical">
+      <md:SurName>Administrator</md:SurName>
+      <md:EmailAddress>lgs@yaco.es</md:EmailAddress>
+    </md:ContactPerson>
+  </md:EntityDescriptor>
+  <md:EntityDescriptor entityID="https://idp2.example.com/simplesaml/saml2/idp/metadata.php">
+    <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+      <md:KeyDescriptor use="signing">
+        <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+          <ds:X509Data>
+            <ds:X509Certificate>MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo</ds:X509Certificate>
+          </ds:X509Data>
+        </ds:KeyInfo>
+      </md:KeyDescriptor>
+      <md:KeyDescriptor use="encryption">
+        <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+          <ds:X509Data>
+            <ds:X509Certificate>MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo</ds:X509Certificate>
+          </ds:X509Data>
+        </ds:KeyInfo>
+      </md:KeyDescriptor>
+      <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp2.example.com/simplesaml/saml2/idp/SingleLogoutService.php"/>
+      <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
+      <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp2.example.com/simplesaml/saml2/idp/SSOService.php"/>
+    </md:IDPSSODescriptor>
+    <md:Organization>
+      <md:OrganizationName xml:lang="en">Lorenzo's test IdP</md:OrganizationName>
+      <md:OrganizationDisplayName xml:lang="en">idp2.example.com IdP</md:OrganizationDisplayName>
+      <md:OrganizationURL xml:lang="en">http://idp2.example.com/</md:OrganizationURL>
+    </md:Organization>
+    <md:ContactPerson contactType="technical">
+      <md:SurName>Administrator</md:SurName>
+      <md:EmailAddress>lgs@yaco.es</md:EmailAddress>
+    </md:ContactPerson>
+  </md:EntityDescriptor>
+  <md:EntityDescriptor entityID="https://idp3.example.com/simplesaml/saml2/idp/metadata.php">
+    <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+      <md:KeyDescriptor use="signing">
+        <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+          <ds:X509Data>
+            <ds:X509Certificate>MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo</ds:X509Certificate>
+          </ds:X509Data>
+        </ds:KeyInfo>
+      </md:KeyDescriptor>
+      <md:KeyDescriptor use="encryption">
+        <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+          <ds:X509Data>
+            <ds:X509Certificate>MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo</ds:X509Certificate>
+          </ds:X509Data>
+        </ds:KeyInfo>
+      </md:KeyDescriptor>
+      <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp3.example.com/simplesaml/saml2/idp/SingleLogoutService.php"/>
+      <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
+      <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp3.example.com/simplesaml/saml2/idp/SSOService.php"/>
+    </md:IDPSSODescriptor>
+    <md:Organization>
+      <md:OrganizationName xml:lang="en">Lorenzo's test IdP</md:OrganizationName>
+      <md:OrganizationDisplayName xml:lang="en">idp3.example.com IdP</md:OrganizationDisplayName>
+      <md:OrganizationURL xml:lang="en">http://idp3.example.com/</md:OrganizationURL>
+    </md:Organization>
+    <md:ContactPerson contactType="technical">
+      <md:SurName>Administrator</md:SurName>
+      <md:EmailAddress>lgs@yaco.es</md:EmailAddress>
+    </md:ContactPerson>
+  </md:EntityDescriptor>
+</md:EntitiesDescriptor>

+ 42 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/sp_metadata.xml

@@ -0,0 +1,42 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<md:EntitiesDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata">
+  <md:EntityDescriptor entityID="http://sp.example.com/saml2/metadata/">
+    <md:SPSSODescriptor AuthnRequestsSigned="False" WantAssertionsSigned="True" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+      <md:KeyDescriptor>
+	<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+	  <ds:X509Data>
+	    <ds:X509Certificate>MIIDFjCCAf4CCQCzHO9MprkomDANBgkqhkiG9w0BAQUFADBNMQswCQYDVQQGEwJl
+czEQMA4GA1UECAwHU2V2aWxsYTENMAsGA1UECgwEWWFjbzEQMA4GA1UEBwwHU2V2
+aWxsYTELMAkGA1UEAwwCU1AwHhcNMTIwMzE1MjA1MjA1WhcNMTMwMzE1MjA1MjA1
+WjBNMQswCQYDVQQGEwJlczEQMA4GA1UECAwHU2V2aWxsYTENMAsGA1UECgwEWWFj
+bzEQMA4GA1UEBwwHU2V2aWxsYTELMAkGA1UEAwwCU1AwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQCeOJEVPpGZMDm3nZsJkl/jH8lEmhA4OWgILP3XzdYL
+rc/fuKR66XYapGied+Pe+PldqyfLpkojUuRDwAXHTprr1HKlUkKvt4Lk0mqH9Z3/
+mZgj1NkKQBkGRLU0miFK93+m1B/Zlg4K1ycRV7111l5NvT9EVDAnyRU0RVjTrifp
+duy85vz9BnRusaR1YKc3NfwC1BiRUAAqhbuSYa0ALwVVri7mNob+/lYmbqrWScpA
+QFHy4VjSricjR8WvFjC3eBJbV7LIzdtd19cD+yDX2cDgXXR+QFxLUhHEhVrF+wvT
+QGcaZPYFiujcY/3FveRoRwdp6e03sUH/eqJksgR5ylJfAgMBAAEwDQYJKoZIhvcN
+AQEFBQADggEBAAu0rKFYHr6pi3yqIIc8EE6NnngqyZEnnDRPWuUK3WKcDI5rOmy9
+8pPE+6sj2NBJyyPu/bsiaCOZBOPywh/AZymO6q3iJRB3pmllH7zYp0LW10HI3NRw
+0T5BJ1ecudM5oitzE7EeMAT6+PogB9i+wxbf/p2YUYsbyiD/JPcrbt5h22sLGxTZ
+ovbOVacQF9es/YenvgmFGY42yea6fO33jZyBTiY69Tmjt+sv1nQJTUIyGeb1bVvF
+JMCJ3g73lNb3DTDS0UO+zLTlTHb1M/uJJnGY/CCb4kmoPRpxgMbybOh2TVfx9RHm
+45W7GtDf4fZ+LqdZC0JVAZQ7a28L5df0TwQ=
+</ds:X509Certificate>
+	  </ds:X509Data>
+	</ds:KeyInfo>
+      </md:KeyDescriptor>
+      <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://sp.example.com/saml2/acs/" index="0" />
+      <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://sp.example.com/saml2/ls/"/>
+    </md:SPSSODescriptor>
+    <md:Organization>
+      <md:OrganizationURL xml:lang="en">http://sp.example.com/</md:OrganizationURL>
+      <md:OrganizationName xml:lang="en">Lorenzo's test SP</md:OrganizationName>
+      <md:OrganizationDisplayName xml:lang="en">sp.example.com</md:OrganizationDisplayName>
+    </md:Organization>
+    <md:ContactPerson contactType="technical">
+      <md:SurName>Administrator</md:SurName>
+      <md:EmailAddress>lgs@yaco.es</md:EmailAddress>
+    </md:ContactPerson>
+  </md:EntityDescriptor>
+</md:EntitiesDescriptor>

+ 16 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/spcert.csr

@@ -0,0 +1,16 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICkjCCAXoCAQAwTTELMAkGA1UEBhMCZXMxEDAOBgNVBAgMB1NldmlsbGExDTAL
+BgNVBAoMBFlhY28xEDAOBgNVBAcMB1NldmlsbGExCzAJBgNVBAMMAlNQMIIBIjAN
+BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnjiRFT6RmTA5t52bCZJf4x/JRJoQ
+ODloCCz9183WC63P37ikeul2GqRonnfj3vj5Xasny6ZKI1LkQ8AFx06a69RypVJC
+r7eC5NJqh/Wd/5mYI9TZCkAZBkS1NJohSvd/ptQf2ZYOCtcnEVe9ddZeTb0/RFQw
+J8kVNEVY064n6XbsvOb8/QZ0brGkdWCnNzX8AtQYkVAAKoW7kmGtAC8FVa4u5jaG
+/v5WJm6q1knKQEBR8uFY0q4nI0fFrxYwt3gSW1eyyM3bXdfXA/sg19nA4F10fkBc
+S1IRxIVaxfsL00BnGmT2BYro3GP9xb3kaEcHaentN7FB/3qiZLIEecpSXwIDAQAB
+oAAwDQYJKoZIhvcNAQEFBQADggEBAIKJqu8OspbEUBizU9XJBUsdIgFaSurC2QxX
+Z/E1bVsg5NLlWYk3Hq8Vec6jCRluasOtqyTqCt9KH+RP+6Q4PXKf0OM5AQ/wLS4R
+4tj2wISEUeuIawwZ64hu8ICEHEoQrRpFos0MWGNXFG5uCxApy7wtpoZsaG8/Lrlw
+5+NVqR3PfC64e4LMVWO60g4OqLzda/XwIrkQszPL5q8zvlTc8ra4d0XEklrmgj2f
+I0U0CaImxEVjBXphRUK/RKOFo97mrzw/I9J2oNgJXr0M079m4tnC7kMZgBao39l5
+buzpclosi0oznSJWwyV9i/KiIBDqr9iF8l9qCyyVv/CwHGVQW3k=
+-----END CERTIFICATE REQUEST-----

+ 27 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/spcert.key

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEAnjiRFT6RmTA5t52bCZJf4x/JRJoQODloCCz9183WC63P37ik
+eul2GqRonnfj3vj5Xasny6ZKI1LkQ8AFx06a69RypVJCr7eC5NJqh/Wd/5mYI9TZ
+CkAZBkS1NJohSvd/ptQf2ZYOCtcnEVe9ddZeTb0/RFQwJ8kVNEVY064n6XbsvOb8
+/QZ0brGkdWCnNzX8AtQYkVAAKoW7kmGtAC8FVa4u5jaG/v5WJm6q1knKQEBR8uFY
+0q4nI0fFrxYwt3gSW1eyyM3bXdfXA/sg19nA4F10fkBcS1IRxIVaxfsL00BnGmT2
+BYro3GP9xb3kaEcHaentN7FB/3qiZLIEecpSXwIDAQABAoIBAFtwT5Cah2SjtUeD
+gx0mBdpp/VRzQRptOs02y0ETyTcYrUEbIZuTHtlI2Nl0ajHra5oRlz8fjEsb1aW9
+7NkBeZD/R355quaIRNJfNIf8j+Iu7vkOQpyk7JFt1ddfmAwOOyy7/Ogvy0/CheaE
+8Y6PZBLDYzPm/6mOkX2S8kHrrU9DrdOWNzcJNhOV1UbPpo/e4S2rHHQzx71GU/50
+HKdKVv5WX+A9vqIzugvXlN0BpGtW4vAOwnXLg5rTg2RLAeCNdBsKUbFPqVaA6xSC
++bgCpR+UC2MWmDBGlIMMTr4Yytuv0n+EkF590N/VqlF5coWTbBjAMogs1t8WxEuQ
+d3Caj5ECgYEAz4qum5tkPMZEvLIXABWb26ZBhTSqsyWt4uLuEJb66Q0PRE7yYtxh
+8XL0b2WFfia5dQJGFBTeQATa5VFejMXbxtKWjbdhX8d5mefWZ6xMsgxssx4kBwmp
+fsvd1wdFRZEJZE6VLnm/WGRd1SETYFUuzpyLwLypnqkjTdw8Gl9htqcCgYEAwyna
+Kr7+b/8F9y0Ka09WdgJIagqi4aoFX63ZV1LfIiqPiAFu/N9BGMr2BRfy+PoKfMdk
+R6oVIiFKVE5KWgy++K9TXZ3zBkhRwDFKzyTbaIF8P9mxbEBxhB/G8GF/sEnVo2/R
++F3TUIRO8/ZIk7HU9uubIcdIgSuuJ5pthsO5NYkCgYBfOnwJzFA/Dp6FkpW5JTEh
+pPSVYWgd0WErJQMlO5Gfk614o1zWfda3Cg8cehG5o50fEk8DcdvUtiWWaTKgFz1T
+ylboacdVQlsKgnU/lrCOVeMegOr5C7bpBjQhMSXY2Mbdbq1G6PgiX9MqMwYIAq36
+gZwicK7HrUYUuMQfObrFKwKBgFBZnM7Yj5zAnE4lpxKDOY+gZPvzoRfTjh7UTpUb
+M263oxxVqsJFkGGKvjtentRO7Z5t4SV4KvdASX/oM8hbUwzD8kiqzPGbOL0uDiS2
+gfbGyMbo85kj9xh0lM1G9vE3lNOTKBlfV67gqjja/wp/vrRiUB5aE8nKmAsKE2nW
+jxwxAoGAXBVVaHMA5vVmCZLwuKQxfFfCfd8K7GDeEwXVh0dAv1aglfHk4wSKiKJB
+K2OEyYusUQX2hFKzAPWY3nReH+nrgXOBzFXlk6S8pJlXLiDFaDac/DCi0u2dQa4w
+vYKXdYMo+cuVYtf1zuUlzlkWprL6Tk04T3AFbcf4nX8dvtf6Iwc=
+-----END RSA PRIVATE KEY-----

+ 19 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/spcert.pem

@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDFjCCAf4CCQCzHO9MprkomDANBgkqhkiG9w0BAQUFADBNMQswCQYDVQQGEwJl
+czEQMA4GA1UECAwHU2V2aWxsYTENMAsGA1UECgwEWWFjbzEQMA4GA1UEBwwHU2V2
+aWxsYTELMAkGA1UEAwwCU1AwHhcNMTIwMzE1MjA1MjA1WhcNMTMwMzE1MjA1MjA1
+WjBNMQswCQYDVQQGEwJlczEQMA4GA1UECAwHU2V2aWxsYTENMAsGA1UECgwEWWFj
+bzEQMA4GA1UEBwwHU2V2aWxsYTELMAkGA1UEAwwCU1AwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQCeOJEVPpGZMDm3nZsJkl/jH8lEmhA4OWgILP3XzdYL
+rc/fuKR66XYapGied+Pe+PldqyfLpkojUuRDwAXHTprr1HKlUkKvt4Lk0mqH9Z3/
+mZgj1NkKQBkGRLU0miFK93+m1B/Zlg4K1ycRV7111l5NvT9EVDAnyRU0RVjTrifp
+duy85vz9BnRusaR1YKc3NfwC1BiRUAAqhbuSYa0ALwVVri7mNob+/lYmbqrWScpA
+QFHy4VjSricjR8WvFjC3eBJbV7LIzdtd19cD+yDX2cDgXXR+QFxLUhHEhVrF+wvT
+QGcaZPYFiujcY/3FveRoRwdp6e03sUH/eqJksgR5ylJfAgMBAAEwDQYJKoZIhvcN
+AQEFBQADggEBAAu0rKFYHr6pi3yqIIc8EE6NnngqyZEnnDRPWuUK3WKcDI5rOmy9
+8pPE+6sj2NBJyyPu/bsiaCOZBOPywh/AZymO6q3iJRB3pmllH7zYp0LW10HI3NRw
+0T5BJ1ecudM5oitzE7EeMAT6+PogB9i+wxbf/p2YUYsbyiD/JPcrbt5h22sLGxTZ
+ovbOVacQF9es/YenvgmFGY42yea6fO33jZyBTiY69Tmjt+sv1nQJTUIyGeb1bVvF
+JMCJ3g73lNb3DTDS0UO+zLTlTHb1M/uJJnGY/CCb4kmoPRpxgMbybOh2TVfx9RHm
+45W7GtDf4fZ+LqdZC0JVAZQ7a28L5df0TwQ=
+-----END CERTIFICATE-----

+ 31 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/tests/urls.py

@@ -0,0 +1,31 @@
+# Copyright (C) 2012 Yaco Sistemas (http://www.yaco.es)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#            http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from django.conf.urls import include, url
+from django.contrib import admin
+
+from djangosaml2 import views
+
+
+urlpatterns = [
+    url(r'^login/$', views.login, name='saml2_login'),
+    url(r'^acs/$', views.assertion_consumer_service, name='saml2_acs'),
+    url(r'^logout/$', views.logout, name='saml2_logout'),
+    url(r'^ls/$', views.logout_service, name='saml2_ls'),
+    url(r'^ls/post/$', views.logout_service_post, name='saml2_ls_post'),
+    url(r'^metadata/$', views.metadata, name='saml2_metadata'),
+
+    # this is needed for the tests
+    url(r'^admin/', include(admin.site.urls)),
+]

+ 27 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/urls.py

@@ -0,0 +1,27 @@
+# Copyright (C) 2010-2012 Yaco Sistemas (http://www.yaco.es)
+# Copyright (C) 2009 Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#            http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from django.conf.urls import url
+from djangosaml2 import views
+
+
+urlpatterns = [
+    url(r'^login/$', views.login, name='saml2_login'),
+    url(r'^acs/$', views.assertion_consumer_service, name='saml2_acs'),
+    url(r'^logout/$', views.logout, name='saml2_logout'),
+    url(r'^ls/$', views.logout_service, name='saml2_ls'),
+    url(r'^ls/post/$', views.logout_service_post, name='saml2_ls_post'),
+    url(r'^metadata/$', views.metadata, name='saml2_metadata'),
+]

+ 90 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/utils.py

@@ -0,0 +1,90 @@
+# Copyright (C) 2012 Yaco Sistemas (http://www.yaco.es)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#            http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import django
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.http import is_safe_url
+from django.utils.module_loading import import_string
+from saml2.s_utils import UnknownSystemEntity
+
+
+def get_custom_setting(name, default=None):
+    return getattr(settings, name, default)
+
+
+def available_idps(config, langpref=None):
+    if langpref is None:
+        langpref = "en"
+
+    idps = set()
+
+    for metadata_name, metadata in config.metadata.metadata.items():
+        result = metadata.any('idpsso_descriptor', 'single_sign_on_service')
+        if result:
+            idps = idps.union(set(result.keys()))
+
+    return dict([(idp, config.metadata.name(idp, langpref)) for idp in idps])
+
+
+def get_idp_sso_supported_bindings(idp_entity_id=None, config=None):
+    """Returns the list of bindings supported by an IDP
+    This is not clear in the pysaml2 code, so wrapping it in a util"""
+    if config is None:
+        # avoid circular import
+        from djangosaml2.conf import get_config
+        config = get_config()
+    # load metadata store from config
+    meta = getattr(config, 'metadata', {})
+    # if idp is None, assume only one exists so just use that
+    if idp_entity_id is None:
+        # .keys() returns dict_keys in python3.5+
+        try:
+            idp_entity_id = list(available_idps(config).keys())[0]
+        except IndexError:
+            raise ImproperlyConfigured("No IdP configured!")
+    try:
+        return meta.service(idp_entity_id, 'idpsso_descriptor', 'single_sign_on_service').keys()
+    except UnknownSystemEntity:
+        return []
+
+
+def get_location(http_info):
+    """Extract the redirect URL from a pysaml2 http_info object"""
+    try:
+        headers = dict(http_info['headers'])
+        return headers['Location']
+    except KeyError:
+        return http_info['url']
+
+
+def fail_acs_response(request, *args, **kwargs):
+    """ Serves as a common mechanism for ending ACS in case of any SAML related failure.
+    Handling can be configured by setting the SAML_ACS_FAILURE_RESPONSE_FUNCTION as
+    suitable for the project.
+
+    The default behavior uses SAML specific template that is rendered on any ACS error,
+    but this can be simply changed so that PermissionDenied exception is raised instead.
+    """
+    failure_function = import_string(get_custom_setting('SAML_ACS_FAILURE_RESPONSE_FUNCTION',
+                                                        'djangosaml2.acs_failures.template_failure'))
+    return failure_function(request, *args, **kwargs)
+
+
+def is_safe_url_compat(url, allowed_hosts=None, require_https=False):
+    if django.VERSION >= (1, 11):
+        return is_safe_url(url, allowed_hosts=allowed_hosts, require_https=require_https)
+    assert len(allowed_hosts) == 1
+    host = allowed_hosts.pop()
+    return is_safe_url(url, host=host)

+ 535 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/djangosaml2/views.py

@@ -0,0 +1,535 @@
+# Copyright (C) 2010-2013 Yaco Sistemas (http://www.yaco.es)
+# Copyright (C) 2009 Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#            http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+import logging
+
+from django.conf import settings
+from django.contrib import auth
+from django.contrib.auth.decorators import login_required
+try:
+    from django.contrib.auth.views import LogoutView
+    django_logout = LogoutView.as_view()
+except ImportError:
+    from django.contrib.auth.views import logout as django_logout
+from django.core.exceptions import PermissionDenied, SuspiciousOperation
+from django.http import Http404, HttpResponse
+from django.http import HttpResponseRedirect  # 30x
+from django.http import HttpResponseBadRequest  # 40x
+from django.http import HttpResponseServerError  # 50x
+from django.views.decorators.http import require_POST
+from django.shortcuts import render
+from django.template import TemplateDoesNotExist
+
+try:
+    from django.utils.six import text_type, binary_type, PY3
+except ImportError:
+    import sys
+    PY3 = sys.version_info[0] == 3
+    text_type = str
+    binary_type = bytes
+
+from django.views.decorators.csrf import csrf_exempt
+
+from saml2 import BINDING_HTTP_REDIRECT, BINDING_HTTP_POST
+from saml2.metadata import entity_descriptor
+from saml2.ident import code, decode
+from saml2.sigver import MissingKey
+from saml2.s_utils import UnsupportedBinding
+from saml2.response import (
+    StatusError, StatusAuthnFailed, SignatureError, StatusRequestDenied,
+    UnsolicitedResponse, StatusNoAuthnContext,
+)
+from saml2.validate import ResponseLifetimeExceed, ToEarly
+from saml2.xmldsig import SIG_RSA_SHA1, SIG_RSA_SHA256  # support for SHA1 is required by spec
+
+from djangosaml2.cache import IdentityCache, OutstandingQueriesCache
+from djangosaml2.cache import StateCache
+from djangosaml2.exceptions import IdPConfigurationMissing
+from djangosaml2.conf import get_config
+from djangosaml2.overrides import Saml2Client
+from djangosaml2.signals import post_authenticated
+from djangosaml2.utils import (
+    available_idps, fail_acs_response, get_custom_setting,
+    get_idp_sso_supported_bindings, get_location, is_safe_url_compat,
+)
+
+
+logger = logging.getLogger('djangosaml2')
+
+
+def _set_subject_id(session, subject_id):
+    session['_saml2_subject_id'] = code(subject_id)
+
+
+def _get_subject_id(session):
+    try:
+        return decode(session['_saml2_subject_id'])
+    except KeyError:
+        return None
+
+
+def callable_bool(value):
+    """ A compatibility wrapper for pre Django 1.10 User model API that used
+    is_authenticated() and is_anonymous() methods instead of attributes
+    """
+    if callable(value):
+        return value()
+    else:
+        return value
+
+
+def login(request,
+          config_loader_path=None,
+          wayf_template='djangosaml2/wayf.html',
+          authorization_error_template='djangosaml2/auth_error.html',
+          post_binding_form_template='djangosaml2/post_binding_form.html'):
+    """SAML Authorization Request initiator
+
+    This view initiates the SAML2 Authorization handshake
+    using the pysaml2 library to create the AuthnRequest.
+    It uses the SAML 2.0 Http Redirect protocol binding.
+
+    * post_binding_form_template - path to a template containing HTML form with
+    hidden input elements, used to send the SAML message data when HTTP POST
+    binding is being used. You can customize this template to include custom
+    branding and/or text explaining the automatic redirection process. Please
+    see the example template in
+    templates/djangosaml2/example_post_binding_form.html
+    If set to None or nonexistent template, default form from the saml2 library
+    will be rendered.
+    """
+    logger.debug('Login process started')
+
+    came_from = request.GET.get('next', settings.LOGIN_REDIRECT_URL)
+    if not came_from:
+        logger.warning('The next parameter exists but is empty')
+        came_from = settings.LOGIN_REDIRECT_URL
+
+    # Ensure the user-originating redirection url is safe.
+    if not is_safe_url_compat(url=came_from, allowed_hosts={request.get_host()}):
+        came_from = settings.LOGIN_REDIRECT_URL
+
+    # if the user is already authenticated that maybe because of two reasons:
+    # A) He has this URL in two browser windows and in the other one he
+    #    has already initiated the authenticated session.
+    # B) He comes from a view that (incorrectly) send him here because
+    #    he does not have enough permissions. That view should have shown
+    #    an authorization error in the first place.
+    # We can only make one thing here and that is configurable with the
+    # SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN setting. If that setting
+    # is True (default value) we will redirect him to the came_from view.
+    # Otherwise, we will show an (configurable) authorization error.
+    if callable_bool(request.user.is_authenticated):
+        redirect_authenticated_user = getattr(settings, 'SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN', True)
+        if redirect_authenticated_user:
+            return HttpResponseRedirect(came_from)
+        else:
+            logger.debug('User is already logged in')
+            return render(request, authorization_error_template, {
+                    'came_from': came_from,
+                    })
+
+    selected_idp = request.GET.get('idp', None)
+    conf = get_config(config_loader_path, request)
+
+    kwargs = {}
+    # pysaml needs a string otherwise: "cannot serialize True (type bool)"
+    if getattr(conf, '_sp_force_authn', False):
+        kwargs['force_authn'] = "true"
+    if getattr(conf, '_sp_allow_create', False):
+        kwargs['allow_create'] = "true"
+
+    # is a embedded wayf needed?
+    idps = available_idps(conf)
+    if selected_idp is None and len(idps) > 1:
+        logger.debug('A discovery process is needed')
+        return render(request, wayf_template, {
+                'available_idps': idps.items(),
+                'came_from': came_from,
+                })
+    else:
+        # is the first one, otherwise next logger message will print None
+        if not idps:
+            raise IdPConfigurationMissing(('IdP configuration is missing or '
+                                           'its metadata is expired.'))
+        if selected_idp is None:
+            selected_idp = list(idps.keys())[0]
+    
+    # choose a binding to try first
+    sign_requests = getattr(conf, '_sp_authn_requests_signed', False)
+    binding = BINDING_HTTP_POST if sign_requests else BINDING_HTTP_REDIRECT
+    logger.debug('Trying binding %s for IDP %s', binding, selected_idp)
+
+    # ensure our selected binding is supported by the IDP
+    supported_bindings = get_idp_sso_supported_bindings(selected_idp, config=conf)
+    if binding not in supported_bindings:
+        logger.debug('Binding %s not in IDP %s supported bindings: %s',
+                     binding, selected_idp, supported_bindings)
+        if binding == BINDING_HTTP_POST:
+            logger.warning('IDP %s does not support %s,  trying %s',
+                           selected_idp, binding, BINDING_HTTP_REDIRECT)
+            binding = BINDING_HTTP_REDIRECT
+        else:
+            logger.warning('IDP %s does not support %s,  trying %s',
+                           selected_idp, binding, BINDING_HTTP_POST)
+            binding = BINDING_HTTP_POST
+        # if switched binding still not supported, give up
+        if binding not in supported_bindings:
+            raise UnsupportedBinding('IDP %s does not support %s or %s',
+                                     selected_idp, BINDING_HTTP_POST, BINDING_HTTP_REDIRECT)
+
+    client = Saml2Client(conf)
+    http_response = None
+
+    logger.debug('Redirecting user to the IdP via %s binding.', binding)
+    if binding == BINDING_HTTP_REDIRECT:
+        try:
+            # do not sign the xml itself, instead use the sigalg to
+            # generate the signature as a URL param
+            sig_alg_option_map = {'sha1': SIG_RSA_SHA1,
+                                  'sha256': SIG_RSA_SHA256}
+            sig_alg_option = getattr(conf, '_sp_authn_requests_signed_alg', 'sha1')
+            sigalg = sig_alg_option_map[sig_alg_option] if sign_requests else None
+            nsprefix = get_namespace_prefixes()
+            session_id, result = client.prepare_for_authenticate(
+                entityid=selected_idp, relay_state=came_from,
+                binding=binding, sign=False, sigalg=sigalg,
+                nsprefix=nsprefix, **kwargs)
+        except TypeError as e:
+            logger.error('Unable to know which IdP to use')
+            return HttpResponse(text_type(e))
+        else:
+            http_response = HttpResponseRedirect(get_location(result))
+    elif binding == BINDING_HTTP_POST:
+        if post_binding_form_template:
+            # get request XML to build our own html based on the template
+            try:
+                location = client.sso_location(selected_idp, binding)
+            except TypeError as e:
+                logger.error('Unable to know which IdP to use')
+                return HttpResponse(text_type(e))
+            session_id, request_xml = client.create_authn_request(
+                location,
+                binding=binding,
+                **kwargs)
+            try:
+                if PY3:
+                    saml_request = base64.b64encode(binary_type(request_xml, 'UTF-8')).decode('utf-8')
+                else:
+                    saml_request = base64.b64encode(binary_type(request_xml))
+
+                http_response = render(request, post_binding_form_template, {
+                    'target_url': location,
+                    'params': {
+                        'SAMLRequest': saml_request,
+                        'RelayState': came_from,
+                        },
+                    })
+            except TemplateDoesNotExist:
+                pass
+
+        if not http_response:
+            # use the html provided by pysaml2 if no template was specified or it didn't exist
+            try:
+                session_id, result = client.prepare_for_authenticate(
+                    entityid=selected_idp, relay_state=came_from,
+                    binding=binding)
+            except TypeError as e:
+                logger.error('Unable to know which IdP to use')
+                return HttpResponse(text_type(e))
+            else:
+                http_response = HttpResponse(result['data'])
+    else:
+        raise UnsupportedBinding('Unsupported binding: %s', binding)
+
+    # success, so save the session ID and return our response
+    logger.debug('Saving the session_id in the OutstandingQueries cache')
+    oq_cache = OutstandingQueriesCache(request.session)
+    oq_cache.set(session_id, came_from)
+    return http_response
+
+
+@require_POST
+@csrf_exempt
+def assertion_consumer_service(request,
+                               config_loader_path=None,
+                               attribute_mapping=None,
+                               create_unknown_user=None):
+    """SAML Authorization Response endpoint
+
+    The IdP will send its response to this view, which
+    will process it with pysaml2 help and log the user
+    in using the custom Authorization backend
+    djangosaml2.backends.Saml2Backend that should be
+    enabled in the settings.py
+    """
+    attribute_mapping = attribute_mapping or get_custom_setting('SAML_ATTRIBUTE_MAPPING', {'uid': ('username', )})
+    create_unknown_user = create_unknown_user if create_unknown_user is not None else \
+                          get_custom_setting('SAML_CREATE_UNKNOWN_USER', True)
+    conf = get_config(config_loader_path, request)
+    try:
+        xmlstr = request.POST['SAMLResponse']
+    except KeyError:
+        logger.warning('Missing "SAMLResponse" parameter in POST data.')
+        raise SuspiciousOperation
+
+    client = Saml2Client(conf, identity_cache=IdentityCache(request.session))
+
+    oq_cache = OutstandingQueriesCache(request.session)
+    outstanding_queries = oq_cache.outstanding_queries()
+
+    try:
+        response = client.parse_authn_request_response(xmlstr, BINDING_HTTP_POST, outstanding_queries)
+    except (StatusError, ToEarly):
+        logger.exception("Error processing SAML Assertion.")
+        return fail_acs_response(request)
+    except ResponseLifetimeExceed:
+        logger.info("SAML Assertion is no longer valid. Possibly caused by network delay or replay attack.", exc_info=True)
+        return fail_acs_response(request)
+    except SignatureError:
+        logger.info("Invalid or malformed SAML Assertion.", exc_info=True)
+        return fail_acs_response(request)
+    except StatusAuthnFailed:
+        logger.info("Authentication denied for user by IdP.", exc_info=True)
+        return fail_acs_response(request)
+    except StatusRequestDenied:
+        logger.warning("Authentication interrupted at IdP.", exc_info=True)
+        return fail_acs_response(request)
+    except StatusNoAuthnContext:
+        logger.warning("Missing Authentication Context from IdP.", exc_info=True)
+        return fail_acs_response(request)
+    except MissingKey:
+        logger.exception("SAML Identity Provider is not configured correctly: certificate key is missing!")
+        return fail_acs_response(request)
+    except UnsolicitedResponse:
+        logger.exception("Received SAMLResponse when no request has been made.")
+        return fail_acs_response(request)
+
+    if response is None:
+        logger.warning("Invalid SAML Assertion received (unknown error).")
+        return fail_acs_response(request, status=400, exc_class=SuspiciousOperation)
+
+    session_id = response.session_id()
+    oq_cache.delete(session_id)
+
+    # authenticate the remote user
+    session_info = response.session_info()
+
+    if callable(attribute_mapping):
+        attribute_mapping = attribute_mapping()
+    if callable(create_unknown_user):
+        create_unknown_user = create_unknown_user()
+
+    logger.debug('Trying to authenticate the user. Session info: %s', session_info)
+    user = auth.authenticate(request=request,
+                             session_info=session_info,
+                             attribute_mapping=attribute_mapping,
+                             create_unknown_user=create_unknown_user)
+    if user is None:
+        logger.warning("Could not authenticate user received in SAML Assertion. Session info: %s", session_info)
+        raise PermissionDenied
+
+    auth.login(request, user)
+    _set_subject_id(request.session, session_info['name_id'])
+    logger.debug("User %s authenticated via SSO.", user)
+
+    logger.debug('Sending the post_authenticated signal')
+    post_authenticated.send_robust(sender=user, session_info=session_info)
+
+    # redirect the user to the view where he came from
+    default_relay_state = get_custom_setting('ACS_DEFAULT_REDIRECT_URL',
+                                             settings.LOGIN_REDIRECT_URL)
+    relay_state = request.POST.get('RelayState', default_relay_state)
+    if not relay_state:
+        logger.warning('The RelayState parameter exists but is empty')
+        relay_state = default_relay_state
+    if not is_safe_url_compat(url=relay_state, allowed_hosts={request.get_host()}):
+        relay_state = settings.LOGIN_REDIRECT_URL
+    logger.debug('Redirecting to the RelayState: %s', relay_state)
+    return HttpResponseRedirect(relay_state)
+
+
+@login_required
+def echo_attributes(request,
+                    config_loader_path=None,
+                    template='djangosaml2/echo_attributes.html'):
+    """Example view that echo the SAML attributes of an user"""
+    state = StateCache(request.session)
+    conf = get_config(config_loader_path, request)
+
+    client = Saml2Client(conf, state_cache=state,
+                         identity_cache=IdentityCache(request.session))
+    subject_id = _get_subject_id(request.session)
+    try:
+        identity = client.users.get_identity(subject_id,
+                                             check_not_on_or_after=False)
+    except AttributeError:
+        return HttpResponse("No active SAML identity found. Are you sure you have logged in via SAML?")
+
+    return render(request, template, {'attributes': identity[0]})
+
+
+@login_required
+def logout(request, config_loader_path=None):
+    """SAML Logout Request initiator
+
+    This view initiates the SAML2 Logout request
+    using the pysaml2 library to create the LogoutRequest.
+    """
+    state = StateCache(request.session)
+    conf = get_config(config_loader_path, request)
+
+    client = Saml2Client(conf, state_cache=state,
+                         identity_cache=IdentityCache(request.session))
+    subject_id = _get_subject_id(request.session)
+    if subject_id is None:
+        logger.warning(
+            'The session does not contain the subject id for user %s',
+            request.user)
+
+    result = client.global_logout(subject_id)
+
+    state.sync()
+
+    if not result:
+        logger.error("Looks like the user %s is not logged in any IdP/AA", subject_id)
+        return HttpResponseBadRequest("You are not logged in any IdP/AA")
+
+    if len(result) > 1:
+        logger.error('Sorry, I do not know how to logout from several sources. I will logout just from the first one')
+
+    for entityid, logout_info in result.items():
+        if isinstance(logout_info, tuple):
+            binding, http_info = logout_info
+            if binding == BINDING_HTTP_POST:
+                logger.debug('Returning form to the IdP to continue the logout process')
+                body = ''.join(http_info['data'])
+                return HttpResponse(body)
+            elif binding == BINDING_HTTP_REDIRECT:
+                logger.debug('Redirecting to the IdP to continue the logout process')
+                return HttpResponseRedirect(get_location(http_info))
+            else:
+                logger.error('Unknown binding: %s', binding)
+                return HttpResponseServerError('Failed to log out')
+        else:
+            # We must have had a soap logout
+            return finish_logout(request, logout_info)
+
+    logger.error('Could not logout because there only the HTTP_REDIRECT is supported')
+    return HttpResponseServerError('Logout Binding not supported')
+
+
+def logout_service(request, *args, **kwargs):
+    return do_logout_service(request, request.GET, BINDING_HTTP_REDIRECT, *args, **kwargs)
+
+
+@csrf_exempt
+def logout_service_post(request, *args, **kwargs):
+    return do_logout_service(request, request.POST, BINDING_HTTP_POST, *args, **kwargs)
+
+
+def do_logout_service(request, data, binding, config_loader_path=None, next_page=None,
+                   logout_error_template='djangosaml2/logout_error.html'):
+    """SAML Logout Response endpoint
+
+    The IdP will send the logout response to this view,
+    which will process it with pysaml2 help and log the user
+    out.
+    Note that the IdP can request a logout even when
+    we didn't initiate the process as a single logout
+    request started by another SP.
+    """
+    logger.debug('Logout service started')
+    conf = get_config(config_loader_path, request)
+
+    state = StateCache(request.session)
+    client = Saml2Client(conf, state_cache=state,
+                         identity_cache=IdentityCache(request.session))
+
+    if 'SAMLResponse' in data:  # we started the logout
+        logger.debug('Receiving a logout response from the IdP')
+        response = client.parse_logout_request_response(data['SAMLResponse'], binding)
+        state.sync()
+        return finish_logout(request, response, next_page=next_page)
+
+    elif 'SAMLRequest' in data:  # logout started by the IdP
+        logger.debug('Receiving a logout request from the IdP')
+        subject_id = _get_subject_id(request.session)
+        if subject_id is None:
+            logger.warning(
+                'The session does not contain the subject id for user %s. Performing local logout',
+                request.user)
+            auth.logout(request)
+            return render(request, logout_error_template, status=403)
+        else:
+            http_info = client.handle_logout_request(
+                data['SAMLRequest'],
+                subject_id,
+                binding,
+                relay_state=data.get('RelayState', ''))
+            state.sync()
+            auth.logout(request)
+            if (
+                http_info.get('method', 'GET') == 'POST' and
+                'data' in http_info and
+                ('Content-type', 'text/html') in http_info.get('headers', [])
+            ):
+                # need to send back to the IDP a signed POST response with user session
+                # return HTML form content to browser with auto form validation
+                # to finally send request to the IDP
+                return HttpResponse(http_info['data'])
+            else:
+                return HttpResponseRedirect(get_location(http_info))
+    else:
+        logger.error('No SAMLResponse or SAMLRequest parameter found')
+        raise Http404('No SAMLResponse or SAMLRequest parameter found')
+
+
+def finish_logout(request, response, next_page=None):
+    if response and response.status_ok():
+        if next_page is None and hasattr(settings, 'LOGOUT_REDIRECT_URL'):
+            next_page = settings.LOGOUT_REDIRECT_URL
+        logger.debug('Performing django logout with a next_page of %s',
+                     next_page)
+        return django_logout(request, next_page=next_page)
+    else:
+        logger.error('Unknown error during the logout')
+        return render(request, "djangosaml2/logout_error.html", {})
+
+
+def metadata(request, config_loader_path=None, valid_for=None):
+    """Returns an XML with the SAML 2.0 metadata for this
+    SP as configured in the settings.py file.
+    """
+    conf = get_config(config_loader_path, request)
+    metadata = entity_descriptor(conf)
+    return HttpResponse(content=text_type(metadata).encode('utf-8'),
+                        content_type="text/xml; charset=utf8")
+
+
+def get_namespace_prefixes():
+    from saml2 import md, saml, samlp
+    try:
+        from saml2 import xmlenc
+        from saml2 import xmldsig
+    except ImportError:
+        import xmlenc
+        import xmldsig
+    return {'saml': saml.NAMESPACE,
+            'samlp': samlp.NAMESPACE,
+            'md': md.NAMESPACE,
+            'ds': xmldsig.NAMESPACE,
+            'xenc': xmlenc.NAMESPACE}

+ 7 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/setup.cfg

@@ -0,0 +1,7 @@
+[bdist_wheel]
+universal = 1
+
+[egg_info]
+tag_build = 
+tag_date = 0
+

+ 80 - 0
desktop/core/ext-py3/djangosaml2-0.18.0/setup.py

@@ -0,0 +1,80 @@
+# Copyright (C) 2011-2012 Yaco Sistemas <lgs@yaco.es>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#            http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import codecs
+import os
+import sys
+from setuptools import setup, find_packages
+
+
+def read(*rnames):
+    return codecs.open(os.path.join(os.path.dirname(__file__), *rnames), encoding='utf-8').read()
+
+
+extra = {'test': []}
+if sys.version_info < (3, 4):
+    # Necessary to use assertLogs in tests
+    extra['test'].append('unittest2')
+
+
+setup(
+    name='djangosaml2',
+    version='0.18.0',
+    description='pysaml2 integration for Django',
+    long_description=read('README.rst'),
+    classifiers=[
+        "Development Status :: 4 - Beta",
+        "Environment :: Web Environment",
+        "Framework :: Django",
+        "Framework :: Django :: 1.8",
+        "Framework :: Django :: 1.9",
+        "Framework :: Django :: 1.10",
+        "Framework :: Django :: 1.11",
+        "Framework :: Django :: 2.0",
+        "Framework :: Django :: 2.1",
+        "Framework :: Django :: 2.2",
+        "Framework :: Django :: 3.0",
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: Apache Software License",
+        "Operating System :: OS Independent",
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 2",
+        "Programming Language :: Python :: 2.7",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.5",
+        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.7",
+        "Topic :: Internet :: WWW/HTTP",
+        "Topic :: Internet :: WWW/HTTP :: WSGI",
+        "Topic :: Security",
+        "Topic :: Software Development :: Libraries :: Application Frameworks",
+        ],
+    keywords="django,pysaml2,sso,saml2,federated authentication,authentication",
+    author="Yaco Sistemas and independent contributors",
+    author_email="lgs@yaco.es",
+    maintainer="Jozef Knaperek",
+    url="https://github.com/knaperek/djangosaml2",
+    download_url="https://pypi.org/project/djangosaml2/",
+    license='Apache 2.0',
+    packages=find_packages(exclude=["tests", "tests.*"]),
+    include_package_data=True,
+    zip_safe=False,
+    install_requires=[
+        'defusedxml>=0.4.1',
+        'Django>=1.8',
+        'pysaml2>=4.6.0',
+        ],
+    extras_require=extra,
+    )