pax_global_header00006660000000000000000000000064136220333450014513gustar00rootroot0000000000000052 comment=46f9b836bb060e196d4088c65f63e363e66e6084 djangosaml2-0.18.1/000077500000000000000000000000001362203334500140035ustar00rootroot00000000000000djangosaml2-0.18.1/.gitignore000066400000000000000000000000521362203334500157700ustar00rootroot00000000000000.tox/ *.pyc *.egg-info *.sqp build/ dist/ djangosaml2-0.18.1/.hgignore000066400000000000000000000000721362203334500156050ustar00rootroot00000000000000syntax: glob *.egg-info build dist *.pyc *.swp *.swo .toxdjangosaml2-0.18.1/.travis.yml000066400000000000000000000030271362203334500161160ustar00rootroot00000000000000dist: bionic language: python sudo: false matrix: include: - python: 2.7 env: TOX_ENV=py27-django18 - python: 2.7 env: TOX_ENV=py27-django19 - python: 3.5 env: TOX_ENV=py35-django19 - python: 2.7 env: TOX_ENV=py27-django110 - python: 3.5 env: TOX_ENV=py35-django110 - python: 2.7 env: TOX_ENV=py27-django111 - python: 3.5 env: TOX_ENV=py35-django111 - python: 3.6 env: TOX_ENV=py36-django111 - python: 3.5 env: TOX_ENV=py35-django20 - python: 3.6 env: TOX_ENV=py36-django20 - python: 3.7 env: TOX_ENV=py37-django20 - python: 3.5 env: TOX_ENV=py35-django21 - python: 3.6 env: TOX_ENV=py36-django21 - python: 3.7 env: TOX_ENV=py37-django21 - python: 3.5 env: TOX_ENV=py35-django22 - python: 3.6 env: TOX_ENV=py36-django22 - python: 3.7 env: TOX_ENV=py37-django22 - python: 3.6 env: TOX_ENV=py36-django30 - python: 3.7 env: TOX_ENV=py37-django30 - python: 3.6 env: TOX_ENV=py36-djangomaster - python: 3.7 env: TOX_ENV=py37-djangomaster fast_finish: true allow_failures: - env: TOX_ENV=py36-djangomaster - env: TOX_ENV=py37-djangomaster addons: apt: packages: - xmlsec1 install: - pip install --upgrade pip setuptools tox virtualenv rstcheck script: - tox -e $TOX_ENV - rstcheck README.rst after_success: - pip install codecov - codecov -e TOX_ENV,TRAVIS_PYTHON_VERSION notifications: email: false djangosaml2-0.18.1/CHANGES000066400000000000000000000207261362203334500150050ustar00rootroot00000000000000Changes ======= 0.18.1 (2020-02-15) ---------- - Fixed regression from 0.18.0. Thanks to OskarPersson 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 djangosaml2-0.18.1/COPYING000066400000000000000000000261361362203334500150460ustar00rootroot00000000000000 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. djangosaml2-0.18.1/MANIFEST.in000066400000000000000000000002371362203334500155430ustar00rootroot00000000000000include README include CHANGES include COPYING global-include *.html *.csr *.key *.pem *.xml include djangosaml2/tests/attribute-maps/*.py global-exclude *.pycdjangosaml2-0.18.1/README.rst000066400000000000000000000476601362203334500155070ustar00rootroot00000000000000=========== 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 elements of the . 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:: group1 group2 group3 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. djangosaml2-0.18.1/djangosaml2/000077500000000000000000000000001362203334500162045ustar00rootroot00000000000000djangosaml2-0.18.1/djangosaml2/__init__.py000066400000000000000000000000001362203334500203030ustar00rootroot00000000000000djangosaml2-0.18.1/djangosaml2/acs_failures.py000066400000000000000000000015601362203334500212200ustar00rootroot00000000000000# -*- 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 djangosaml2-0.18.1/djangosaml2/backends.py000066400000000000000000000255501362203334500203370ustar00rootroot00000000000000# Copyright (C) 2010-2012 Yaco Sistemas (http://www.yaco.es) # Copyright (C) 2009 Lorenzo Gil Sanchez # # 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 djangosaml2-0.18.1/djangosaml2/cache.py000066400000000000000000000056031362203334500176250ustar00rootroot00000000000000# Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es) # Copyright (C) 2010 Lorenzo Gil Sanchez # # 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') djangosaml2-0.18.1/djangosaml2/conf.py000066400000000000000000000044401362203334500175050ustar00rootroot00000000000000# Copyright (C) 2010-2012 Yaco Sistemas (http://www.yaco.es) # Copyright (C) 2009 Lorenzo Gil Sanchez # # 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) djangosaml2-0.18.1/djangosaml2/exceptions.py000066400000000000000000000000631362203334500207360ustar00rootroot00000000000000class IdPConfigurationMissing(Exception): pass djangosaml2-0.18.1/djangosaml2/models.py000066400000000000000000000000761362203334500200440ustar00rootroot00000000000000# nothing here, just an empty file to avoid Django complaints djangosaml2-0.18.1/djangosaml2/overrides.py000066400000000000000000000017301362203334500205610ustar00rootroot00000000000000import 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) djangosaml2-0.18.1/djangosaml2/signals.py000066400000000000000000000015331362203334500202200ustar00rootroot00000000000000# 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']) djangosaml2-0.18.1/djangosaml2/templates/000077500000000000000000000000001362203334500202025ustar00rootroot00000000000000djangosaml2-0.18.1/djangosaml2/templates/djangosaml2/000077500000000000000000000000001362203334500224035ustar00rootroot00000000000000djangosaml2-0.18.1/djangosaml2/templates/djangosaml2/auth_error.html000066400000000000000000000012341362203334500254430ustar00rootroot00000000000000

Authorization error

You are already logged in and you are trying to go to the login page again.

You may have been redirected here when trying to access some content that required extra privileges that you do not have.

Please logout and login as a different user

djangosaml2-0.18.1/djangosaml2/templates/djangosaml2/echo_attributes.html000066400000000000000000000010611362203334500264530ustar00rootroot00000000000000

SAML attributes

{% for attribute, value in attributes.items %}
{{ attribute }}:
{{ value|join:", " }}
{% endfor %}

Log out

djangosaml2-0.18.1/djangosaml2/templates/djangosaml2/example_post_binding_form.html000066400000000000000000000007561362203334500305160ustar00rootroot00000000000000

You're being redirected to a SSO login page. Please click the button below if you're not redirected automatically within a few seconds.

{% for key, value in params.items %} {% endfor %}
djangosaml2-0.18.1/djangosaml2/templates/djangosaml2/login_error.html000066400000000000000000000005721362203334500256160ustar00rootroot00000000000000

Authentication Error.

Access Denied.

djangosaml2-0.18.1/djangosaml2/templates/djangosaml2/logout_error.html000066400000000000000000000013731362203334500260170ustar00rootroot00000000000000

Logout error

Your Identity Provider ask this system to do a global logout but your federated session is lost.

Even if your local session in this system has been closed, you have probably open sessions in other systems.

In order to prevent illicit use of your personal information, please close your browser window and/or remove your cookies from your browser.

Sorry for this inconvenience.

djangosaml2-0.18.1/djangosaml2/templates/djangosaml2/wayf.html000066400000000000000000000012061362203334500242360ustar00rootroot00000000000000

Where are you from?

Please select your Identity Provider from the following list:

    {% for url, name in available_idps %}
  • {{ name }}
  • {% endfor %}
djangosaml2-0.18.1/djangosaml2/templatetags/000077500000000000000000000000001362203334500206765ustar00rootroot00000000000000djangosaml2-0.18.1/djangosaml2/templatetags/__init__.py000066400000000000000000000000001362203334500227750ustar00rootroot00000000000000djangosaml2-0.18.1/djangosaml2/templatetags/idplist.py000066400000000000000000000027011362203334500227200ustar00rootroot00000000000000# 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) djangosaml2-0.18.1/djangosaml2/tests/000077500000000000000000000000001362203334500173465ustar00rootroot00000000000000djangosaml2-0.18.1/djangosaml2/tests/__init__.py000066400000000000000000001002211362203334500214530ustar00rootroot00000000000000# Copyright (C) 2012 Sam Bull (lsb@pocketuniverse.ca) # Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es) # Copyright (C) 2010 Lorenzo Gil Sanchez # # 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'.*', r'', 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 = """ http://sp.example.com/saml2/metadata/""" else: expected_request = """http://sp.example.com/saml2/metadata/""" 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 = """ http://sp.example.com/saml2/metadata/""" else: expected_request = """http://sp.example.com/saml2/metadata/""" 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 = """ http://sp.example.com/saml2/metadata/58bcc81ea14700f66aeb707a0eff1360a0123456789abcdef0123456789abcdef""" else: expected_request = """http://sp.example.com/saml2/metadata/58bcc81ea14700f66aeb707a0eff1360a0123456789abcdef0123456789abcdef""" 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 = """ http://sp.example.com/saml2/metadata/58bcc81ea14700f66aeb707a0eff1360a0123456789abcdef0123456789abcdef""" else: expected_request = """http://sp.example.com/saml2/metadata/58bcc81ea14700f66aeb707a0eff1360a0123456789abcdef0123456789abcdef""" 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 = """ https://idp.example.com/simplesaml/saml2/idp/metadata.php""" % ( 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 = """ https://idp.example.com/simplesaml/saml2/idp/metadata.php%s_1837687b7bc9faad85839dbeb319627889f3021757""" % (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 = """ http://sp.example.com/saml2/metadata/""" else: expected_response = """http://sp.example.com/saml2/metadata/""" 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 = 'https://idp.example.com/simplesaml/saml2/idp/metadata.php%s_1837687b7bc9faad85839dbeb319627889f3021757' % ( 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, "

Logout error

", 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 = """ 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 Test SPEjemplo S.A.Example Inc.EjemploExamplehttp://www.example.eshttp://www.example.comExample Inc.Technical givennameTechnical surnametechnical@sp.example.comExample Inc.Administrative givennameAdministrative surnameadministrative@sp.example.ccom""" 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') djangosaml2-0.18.1/djangosaml2/tests/attribute-maps/000077500000000000000000000000001362203334500223075ustar00rootroot00000000000000djangosaml2-0.18.1/djangosaml2/tests/attribute-maps/saml_uri.py000066400000000000000000000246331362203334500245040ustar00rootroot00000000000000__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', } } djangosaml2-0.18.1/djangosaml2/tests/auth_response.py000066400000000000000000000126511362203334500226040ustar00rootroot00000000000000# Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es) # Copyright (C) 2010 Lorenzo Gil Sanchez # # 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 = ( '' '' '' '%(uid)s' '' '' '' ) % {'uid': uid} saml_response_tpl = ( "" '' '' 'https://idp.example.com/simplesaml/saml2/idp/metadata.php' '' '' '' '' '' '' 'https://idp.example.com/simplesaml/saml2/idp/metadata.php' '' '' '' '1f87035b4c1325b296a53d92097e6b3fa36d7e30ee82e3fcb0680d60243c1f03' '' '' '' '' '' '' '' '' '%(audience)s' '' '' '' '' '' '' 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password' '' '' '' '%(attribute_statements)s' '' '') 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'), } djangosaml2-0.18.1/djangosaml2/tests/conf.py000066400000000000000000000074321362203334500206530ustar00rootroot00000000000000# Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es) # Copyright (C) 2010 Lorenzo Gil Sanchez # # 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 djangosaml2-0.18.1/djangosaml2/tests/idpcert.csr000066400000000000000000000017551362203334500215210ustar00rootroot00000000000000-----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----- djangosaml2-0.18.1/djangosaml2/tests/idpcert.key000066400000000000000000000032171362203334500215150ustar00rootroot00000000000000-----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----- djangosaml2-0.18.1/djangosaml2/tests/idpcert.pem000066400000000000000000000022641362203334500215070ustar00rootroot00000000000000-----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----- djangosaml2-0.18.1/djangosaml2/tests/mycert.csr000066400000000000000000000017451362203334500213710ustar00rootroot00000000000000-----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----- djangosaml2-0.18.1/djangosaml2/tests/mycert.key000066400000000000000000000032171362203334500213660ustar00rootroot00000000000000-----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----- djangosaml2-0.18.1/djangosaml2/tests/mycert.pem000066400000000000000000000022401362203334500213520ustar00rootroot00000000000000-----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----- djangosaml2-0.18.1/djangosaml2/tests/remote_metadata.xml000066400000000000000000000326141362203334500232310ustar00rootroot00000000000000 MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo urn:oasis:names:tc:SAML:2.0:nameid-format:transient Lorenzo's test IdP idp.example.com IdP http://idp.example.com/ Administrator lgs@yaco.es MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo urn:oasis:names:tc:SAML:2.0:nameid-format:transient Lorenzo's test IdP idp1.example.com IdP http://idp1.example.com/ Administrator lgs@yaco.es MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo urn:oasis:names:tc:SAML:2.0:nameid-format:transient Lorenzo's test IdP idp2.example.com IdP http://idp2.example.com/ Administrator lgs@yaco.es MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo urn:oasis:names:tc:SAML:2.0:nameid-format:transient Lorenzo's test IdP idp3.example.com IdP http://idp3.example.com/ Administrator lgs@yaco.es djangosaml2-0.18.1/djangosaml2/tests/remote_metadata_one_idp.xml000066400000000000000000000067331362203334500247310ustar00rootroot00000000000000 MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo urn:oasis:names:tc:SAML:2.0:nameid-format:transient Lorenzo's test IdP idp.example.com IdP http://idp.example.com/ Administrator lgs@yaco.es djangosaml2-0.18.1/djangosaml2/tests/remote_metadata_three_idps.xml000066400000000000000000000241261362203334500254360ustar00rootroot00000000000000 MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo urn:oasis:names:tc:SAML:2.0:nameid-format:transient Lorenzo's test IdP idp1.example.com IdP http://idp1.example.com/ Administrator lgs@yaco.es MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo urn:oasis:names:tc:SAML:2.0:nameid-format:transient Lorenzo's test IdP idp2.example.com IdP http://idp2.example.com/ Administrator lgs@yaco.es MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo urn:oasis:names:tc:SAML:2.0:nameid-format:transient Lorenzo's test IdP idp3.example.com IdP http://idp3.example.com/ Administrator lgs@yaco.es djangosaml2-0.18.1/djangosaml2/tests/sp_metadata.xml000066400000000000000000000046201362203334500223540ustar00rootroot00000000000000 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= http://sp.example.com/ Lorenzo's test SP sp.example.com Administrator lgs@yaco.es djangosaml2-0.18.1/djangosaml2/tests/spcert.csr000066400000000000000000000017101362203334500213560ustar00rootroot00000000000000-----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----- djangosaml2-0.18.1/djangosaml2/tests/spcert.key000066400000000000000000000032131362203334500213570ustar00rootroot00000000000000-----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----- djangosaml2-0.18.1/djangosaml2/tests/spcert.pem000066400000000000000000000021531362203334500213520ustar00rootroot00000000000000-----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----- djangosaml2-0.18.1/djangosaml2/tests/urls.py000066400000000000000000000022571362203334500207130ustar00rootroot00000000000000# 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)), ] djangosaml2-0.18.1/djangosaml2/urls.py000066400000000000000000000021771362203334500175520ustar00rootroot00000000000000# Copyright (C) 2010-2012 Yaco Sistemas (http://www.yaco.es) # Copyright (C) 2009 Lorenzo Gil Sanchez # # 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'), ] djangosaml2-0.18.1/djangosaml2/utils.py000066400000000000000000000066051362203334500177250ustar00rootroot00000000000000# 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) djangosaml2-0.18.1/djangosaml2/views.py000066400000000000000000000542611362203334500177230ustar00rootroot00000000000000# Copyright (C) 2010-2013 Yaco Sistemas (http://www.yaco.es) # Copyright (C) 2009 Lorenzo Gil Sanchez # # 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} djangosaml2-0.18.1/setup.cfg000066400000000000000000000000341362203334500156210ustar00rootroot00000000000000[bdist_wheel] universal = 1 djangosaml2-0.18.1/setup.py000066400000000000000000000053721362203334500155240ustar00rootroot00000000000000# Copyright (C) 2011-2012 Yaco Sistemas # # 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.1', 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, ) djangosaml2-0.18.1/tests/000077500000000000000000000000001362203334500151455ustar00rootroot00000000000000djangosaml2-0.18.1/tests/__init__.py000066400000000000000000000000001362203334500172440ustar00rootroot00000000000000djangosaml2-0.18.1/tests/manage.py000066400000000000000000000003621362203334500167500ustar00rootroot00000000000000#!/usr/bin/env python import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) djangosaml2-0.18.1/tests/run_tests.py000077500000000000000000000020221362203334500175440ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) 2012 Sam Bull (lsb@pocketuniverse.ca) # # 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 import sys from django.core.wsgi import get_wsgi_application from django.core import management PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") sys.path.append(PROJECT_DIR) # Load models application = get_wsgi_application() management.call_command('test', 'djangosaml2.tests', 'testprofiles') djangosaml2-0.18.1/tests/settings.py000066400000000000000000000101201362203334500173510ustar00rootroot00000000000000""" Django settings for xxx project. Generated by 'django-admin startproject' using Django 1.10.2. For more information on this file, see https://docs.djangoproject.com/en/1.10/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.10/ref/settings/ """ import os import django # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Make this unique, and don't share it with anybody. SECRET_KEY = 'xvds$ppv5ha75qg1yx3aax7ugr_2*fmdrc(lrc%x7kdez-63xn' DEBUG = True ALLOWED_HOSTS = [] INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'djangosaml2', 'testprofiles', ) MIDDLEWARE = ( 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) if django.VERSION < (1, 10): MIDDLEWARE_CLASSES = MIDDLEWARE ROOT_URLCONF = 'testprofiles.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'testprofiles.wsgi.application' # Database # https://docs.djangoproject.com/en/1.10/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), } } # Password validation # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/1.10/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' SITE_ID = 1 USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.10/howto/static-files/ STATIC_URL = '/static/' AUTH_USER_MODEL = 'testprofiles.TestUser' # A sample logging configuration. The only tangible logging # performed by this configuration is to send an email to # the site admins on every HTTP 500 error when DEBUG=False. # See http://docs.djangoproject.com/en/dev/topics/logging for # more details on how to customize your logging configuration. LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'filters': { 'require_debug_false': { '()': 'django.utils.log.RequireDebugFalse' } }, 'handlers': { 'mail_admins': { 'level': 'ERROR', 'filters': ['require_debug_false'], 'class': 'django.utils.log.AdminEmailHandler' }, 'console': { 'level': 'DEBUG', 'class': 'logging.StreamHandler', }, }, 'loggers': { 'django.request': { 'handlers': ['mail_admins'], 'level': 'ERROR', 'propagate': True, }, 'djangosaml2': { 'handlers': ['console'], 'level': 'DEBUG', }, } } AUTHENTICATION_BACKENDS = ( 'djangosaml2.backends.Saml2Backend', ) djangosaml2-0.18.1/tests/testprofiles/000077500000000000000000000000001362203334500176705ustar00rootroot00000000000000djangosaml2-0.18.1/tests/testprofiles/__init__.py000066400000000000000000000000001362203334500217670ustar00rootroot00000000000000djangosaml2-0.18.1/tests/testprofiles/models.py000066400000000000000000000022061362203334500215250ustar00rootroot00000000000000# Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es) # Copyright (C) 2010 Lorenzo Gil Sanchez # # 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.contrib.auth.models import AbstractUser from django.db import models class TestUser(AbstractUser): age = models.CharField(max_length=100, blank=True) def process_first_name(self, first_name): self.first_name = first_name[0] class StandaloneUserModel(models.Model): """ Does not inherit from Django's base abstract user and does not define a USERNAME_FIELD. """ username = models.CharField(max_length=30, unique=True) djangosaml2-0.18.1/tests/testprofiles/tests.py000066400000000000000000000151111362203334500214030ustar00rootroot00000000000000# Copyright (C) 2012 Sam Bull (lsb@pocketuniverse.ca) # Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es) # Copyright (C) 2010 Lorenzo Gil Sanchez # # 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 sys from django.contrib.auth import get_user_model from django.contrib.auth.models import User as DjangoUserModel from django.test import TestCase, override_settings from djangosaml2.backends import Saml2Backend User = get_user_model() if sys.version_info < (3, 4): # Monkey-patch TestCase to add the assertLogs method introduced in # Python 3.4 from unittest2.case import _AssertLogsContext class LoggerTestCase(TestCase): def assertLogs(self, logger=None, level=None): return _AssertLogsContext(self, logger, level) TestCase = LoggerTestCase class Saml2BackendTests(TestCase): def test_update_user(self): # we need a user user = User.objects.create(username='john') backend = Saml2Backend() attribute_mapping = { 'uid': ('username', ), 'mail': ('email', ), 'cn': ('first_name', ), 'sn': ('last_name', ), } attributes = { 'uid': ('john', ), 'mail': ('john@example.com', ), 'cn': ('John', ), 'sn': ('Doe', ), } backend.update_user(user, attributes, attribute_mapping) self.assertEqual(user.email, 'john@example.com') self.assertEqual(user.first_name, 'John') self.assertEqual(user.last_name, 'Doe') attribute_mapping['saml_age'] = ('age', ) attributes['saml_age'] = ('22', ) backend.update_user(user, attributes, attribute_mapping) self.assertEqual(user.age, '22') def test_update_user_callable_attributes(self): user = User.objects.create(username='john') backend = Saml2Backend() attribute_mapping = { 'uid': ('username', ), 'mail': ('email', ), 'cn': ('process_first_name', ), 'sn': ('last_name', ), } attributes = { 'uid': ('john', ), 'mail': ('john@example.com', ), 'cn': ('John', ), 'sn': ('Doe', ), } backend.update_user(user, attributes, attribute_mapping) self.assertEqual(user.email, 'john@example.com') self.assertEqual(user.first_name, 'John') self.assertEqual(user.last_name, 'Doe') def test_update_user_empty_attribute(self): user = User.objects.create(username='john', last_name='Smith') backend = Saml2Backend() attribute_mapping = { 'uid': ('username', ), 'mail': ('email', ), 'cn': ('first_name', ), 'sn': ('last_name', ), } attributes = { 'uid': ('john', ), 'mail': ('john@example.com', ), 'cn': ('John', ), 'sn': (), } with self.assertLogs('djangosaml2', level='DEBUG') as logs: backend.update_user(user, attributes, attribute_mapping) self.assertEqual(user.email, 'john@example.com') self.assertEqual(user.first_name, 'John') # empty attribute list: no update self.assertEqual(user.last_name, 'Smith') self.assertIn( 'DEBUG:djangosaml2:Could not find value for "sn", not ' 'updating fields "(\'last_name\',)"', logs.output, ) def test_invalid_model_attribute_log(self): backend = Saml2Backend() attribute_mapping = { 'uid': ['username'], 'cn': ['nonexistent'], } attributes = { 'uid': ['john'], 'cn': ['John'], } with self.assertLogs('djangosaml2', level='DEBUG') as logs: backend.get_saml2_user(True, 'john', attributes, attribute_mapping) self.assertIn( 'DEBUG:djangosaml2:Could not find attribute "nonexistent" on user "john"', logs.output, ) def test_django_user_main_attribute(self): backend = Saml2Backend() old_username_field = User.USERNAME_FIELD User.USERNAME_FIELD = 'slug' self.assertEqual(backend.get_django_user_main_attribute(), 'slug') User.USERNAME_FIELD = old_username_field with override_settings(AUTH_USER_MODEL='auth.User'): self.assertEqual( DjangoUserModel.USERNAME_FIELD, backend.get_django_user_main_attribute()) with override_settings( AUTH_USER_MODEL='testprofiles.StandaloneUserModel'): self.assertEqual( backend.get_django_user_main_attribute(), 'username') with override_settings(SAML_DJANGO_USER_MAIN_ATTRIBUTE='foo'): self.assertEqual(backend.get_django_user_main_attribute(), 'foo') def test_django_user_main_attribute_lookup(self): backend = Saml2Backend() self.assertEqual(backend.get_django_user_main_attribute_lookup(), '') with override_settings( SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP='__iexact'): self.assertEqual( backend.get_django_user_main_attribute_lookup(), '__iexact') class LowerCaseSaml2Backend(Saml2Backend): def clean_attributes(self, attributes): return dict([k.lower(), v] for k, v in attributes.items()) class LowerCaseSaml2BackendTest(TestCase): def test_update_user_clean_attributes(self): user = User.objects.create(username='john') attribute_mapping = { 'uid': ('username', ), 'mail': ('email', ), 'cn': ('first_name', ), 'sn': ('last_name', ), } attributes = { 'UID': ['john'], 'MAIL': ['john@example.com'], 'CN': ['John'], 'SN': [], } backend = LowerCaseSaml2Backend() user = backend.authenticate( None, session_info={'ava': attributes}, attribute_mapping=attribute_mapping, ) self.assertIsNotNone(user) djangosaml2-0.18.1/tests/testprofiles/urls.py000066400000000000000000000002661362203334500212330ustar00rootroot00000000000000from django.conf.urls import include, url from django.contrib import admin urlpatterns = [ url(r'^saml2/', include('djangosaml2.urls')), url(r'^admin/', admin.site.urls), ] djangosaml2-0.18.1/tox.ini000066400000000000000000000012511362203334500153150ustar00rootroot00000000000000[tox] envlist = py{27,35}-django18 py{27,35}-django19 py{27,35}-django110 py{27,35,36}-django111 py{35,36,37}-django20 py{35,36,37}-django21 py{35,36,37}-django22 py{36,37}-django30 py{36,37}-djangomaster [testenv] commands = python tests/run_tests.py deps = django18: Django>=1.8,<1.9 django19: Django>=1.9,<1.10 django110: Django>=1.10,<1.11 django111: Django>=1.11,<2.0 django20: Django>=2.0,<2.1 django21: Django>=2.1,<2.2 django22: Django>=2.2,<3.0 django30: Django>=3.0,<3.1 djangomaster: https://github.com/django/django/archive/master.tar.gz .[test] ignore_outcome = djangomaster: True