├── .github ├── release-drafter.yml └── workflows │ ├── pypi.yml │ ├── python-package.yml │ └── release-drafter.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── CHANGES ├── COPYING ├── MANIFEST.in ├── README.md ├── build_pypi.sh ├── djangosaml2 ├── __init__.py ├── apps.py ├── backends.py ├── cache.py ├── conf.py ├── exceptions.py ├── middleware.py ├── overrides.py ├── signals.py ├── templates │ └── djangosaml2 │ │ ├── auth_error.html │ │ ├── echo_attributes.html │ │ ├── example_post_binding_form.html │ │ ├── login_error.html │ │ ├── logout_error.html │ │ ├── post_binding_form.html │ │ └── wayf.html ├── templatetags │ ├── __init__.py │ └── idplist.py ├── tests │ ├── __init__.py │ ├── attribute-maps │ │ ├── django_saml_uri.py │ │ └── saml_uri.py │ ├── auth_response.py │ ├── conf.py │ ├── idpcert.csr │ ├── idpcert.key │ ├── idpcert.pem │ ├── mycert.csr │ ├── mycert.key │ ├── mycert.pem │ ├── remote_metadata.xml │ ├── remote_metadata_no_idp.xml │ ├── remote_metadata_one_idp.xml │ ├── remote_metadata_post_binding.xml │ ├── remote_metadata_three_idps.xml │ ├── sp_metadata.xml │ ├── spcert.csr │ ├── spcert.key │ ├── spcert.pem │ └── utils.py ├── urls.py ├── utils.py └── views.py ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ ├── custom.css │ └── logo.jpg │ ├── _templates │ └── pplnx_template │ │ ├── footer.html │ │ └── layout.html │ ├── conf.py │ ├── contents │ ├── developer.rst │ ├── faq.md │ ├── miscellanea.rst │ ├── security.md │ ├── setup.rst │ └── usage.md │ └── index.rst ├── pyproject.toml ├── requirements-dev.txt ├── requirements-docs.txt ├── setup.cfg ├── setup.py ├── tests ├── .coveragerc ├── __init__.py ├── manage.py ├── run_tests.py ├── settings.py └── testprofiles │ ├── __init__.py │ ├── app.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ ├── models.py │ ├── tests.py │ ├── urls.py │ └── utils.py └── tox.ini /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | categories: 4 | - 5 | title: 'Features' 6 | labels: 7 | - 'enhancement' 8 | - 'feat' 9 | - 'feature' 10 | - 11 | title: 'Bug Fixes' 12 | labels: 13 | - 'bug' 14 | - 'bugfix' 15 | - 'fix' 16 | - 17 | title: 'Maintenance' 18 | labels: 19 | - 'chore' 20 | - 'style' 21 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 22 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 23 | version-resolver: 24 | major: 25 | labels: ['major'] 26 | minor: 27 | labels: ['minor'] 28 | patch: 29 | labels: ['patch'] 30 | default: patch 31 | exclude-labels: ['skip'] 32 | autolabeler: 33 | - 34 | label: 'bug' 35 | branch: 36 | - '/bug\/.+/' 37 | - '/bugfix\/.+/' 38 | - '/fix\/.+/' 39 | - 40 | label: 'enhancement' 41 | branch: 42 | - '/dependabot\/.+/' 43 | - '/enhancement\/.+/' 44 | - '/feat\/.+/' 45 | - '/feature\/.+/' 46 | - 47 | label: 'chore' 48 | branch: 49 | - '/chore\/.+/' 50 | - '/style\/.+/' 51 | template: | 52 | ## Release notes 53 | 54 | $CHANGES 55 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python distribution to PyPI 2 | on: 3 | release: 4 | types: 5 | - published 6 | 7 | jobs: 8 | build-n-publish: 9 | name: Publish Python distribution to PyPI 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@master 13 | - name: Setup Python 3.10.16 14 | uses: actions/setup-python@v1 15 | with: 16 | python-version: 3.10.16 17 | - name: Install pypa/build 18 | run: >- 19 | python -m 20 | pip install 21 | build 22 | --user 23 | - name: Build a binary wheel and a source tarball 24 | run: >- 25 | python -m 26 | build 27 | --sdist 28 | --wheel 29 | --outdir dist/ 30 | . 31 | - name: Publish distribution to PyPI 32 | uses: pypa/gh-action-pypi-publish@master 33 | with: 34 | user: __token__ 35 | password: ${{ secrets.PYPI_API_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: djangosaml2 5 | 6 | on: 7 | push: 8 | branches: '*' 9 | pull_request: 10 | branches: '*' 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: ["3.10", "3.11", "3.12"] 19 | django-version: ["4.2", "5.0", "5.1"] 20 | include: 21 | - python-version: "3.9" 22 | django-version: "4.2" 23 | - python-version: "3.13" 24 | django-version: "5.1" 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v4 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | allow-prereleases: true 33 | - name: Install dependencies and testing utilities 34 | run: | 35 | sudo apt-get update && sudo apt-get install xmlsec1 36 | python -m pip install --upgrade pip 37 | python -m pip install --upgrade tox rstcheck setuptools codecov 38 | #- name: Readme check 39 | #if: ${{ matrix.python-version }} == 3.8 && ${{ matrix.django-version }} == "3.0" 40 | #run: rstcheck README.rst 41 | - name: Tests 42 | run: tox -e py${{ matrix.python-version }}-django${{ matrix.django-version }} 43 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release drafter 2 | 3 | on: 4 | push: 5 | branches: [main, master, dev] 6 | pull_request: 7 | types: [opened, reopened, synchronize] 8 | 9 | jobs: 10 | update_release_draft: 11 | name: Update draft release 12 | runs-on: ubuntu-latest 13 | steps: 14 | - 15 | uses: release-drafter/release-drafter@v5 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | db.sqlite3 2 | .tox/ 3 | *.pyc 4 | *.egg-info 5 | *.sqp 6 | build/ 7 | dist/ 8 | _build/ 9 | .pytest_cache 10 | .env 11 | env/ 12 | venv 13 | tags 14 | .idea/ 15 | .vscode/ 16 | build/ 17 | dist/ 18 | *__pycache__* 19 | *.coverage 20 | docs/build/* 21 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: 'docs|migrations' 2 | 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v5.0.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-toml 11 | - id: check-case-conflict 12 | - id: check-merge-conflict 13 | - id: debug-statements 14 | 15 | - repo: https://github.com/asottile/pyupgrade 16 | rev: v3.19.0 17 | hooks: 18 | - id: pyupgrade 19 | args: [--py39-plus] 20 | 21 | - repo: https://github.com/myint/autoflake 22 | rev: 'v2.3.1' 23 | hooks: 24 | - id: autoflake 25 | args: ['--in-place', '--remove-all-unused-imports', '--ignore-init-module-imports'] 26 | 27 | - repo: https://github.com/pycqa/isort 28 | rev: 5.13.2 29 | hooks: 30 | - id: isort 31 | name: isort (python) 32 | args: ['--settings-path=pyproject.toml'] 33 | 34 | - repo: https://github.com/psf/black 35 | rev: 24.10.0 36 | hooks: 37 | - id: black 38 | 39 | - repo: https://github.com/adamchainz/django-upgrade 40 | rev: 1.22.2 41 | hooks: 42 | - id: django-upgrade 43 | args: [--target-version, "4.2"] 44 | 45 | - repo: https://github.com/pycqa/flake8 46 | rev: 7.1.1 47 | hooks: 48 | - id: flake8 49 | args: ['--config=setup.cfg'] 50 | additional_dependencies: [flake8-bugbear, flake8-isort] 51 | verbose: true 52 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/source/conf.py 11 | 12 | # Build documentation with MkDocs 13 | #mkdocs: 14 | # configuration: mkdocs.yml 15 | 16 | # Optionally build your docs in additional formats such as PDF and ePub 17 | formats: all 18 | 19 | # Optionally set the version of Python and requirements required to build your docs 20 | python: 21 | version: 3.8 22 | install: 23 | - requirements: requirements-docs.txt 24 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | 4 | v1.2.2 (2021-05-27) 5 | ------------------- 6 | 7 | - Fix #245: Don't update user_main_attribute @jaap3 (#283) 8 | - Fix #278: Allow ACS_DEFAULT_REDIRECT_URL to override LOGIN_REDIRECT_URL in more places @jaap3 (#285) 9 | - Add release notes for previous and upcoming releases @jaap3 (#284) 10 | - Add default_auto_field setting, strongly recommended in Django 3.2 @jaap3 (#282) 11 | - SPConfig loader instead of global django settings @peppelinux (#281) 12 | - Fix #277: Resolve LOGIN_REDIRECT_URL @jaap3 (#279) 13 | - Resolve LOGIN_REDIRECT_URL @jaap3 (#277) 14 | 15 | v1.2.1 (2021-05-23) 16 | ------------------- 17 | 18 | - Documentation improved 19 | - unit tests and coverage improved 20 | - better handling of unknown idps 21 | 22 | 23 | v1.2.0 (2021-05-14) 24 | ------------------- 25 | 26 | - Implement IdP Scoping parameter for SPs suggesting an entityID to a proxy @pauldekkers (#272) 27 | 28 | 29 | v1.1.5 (2021-04-29) 30 | ------------------- 31 | 32 | - Cast major/minor django VERSION number into float before comparing @lgarvey (#269) 33 | - Add note to SameSite cookie docs section recommending upgrade to Django >= 3.1 @m6312 (#267) 34 | 35 | 36 | v1.1.4 (2021-04-28) 37 | ------------------- 38 | 39 | - fix: samesite cookie configuration fix for django version <3.1 40 | 41 | 42 | v1.1.3 (2021-04-28) 43 | -------------------- 44 | 45 | - Add assertion param to backed.authenticate and backend.is_authorized @lucyeun-alation (#128) 46 | - feat: CI - added django 3.1 and 3.2 47 | - fix: Samesite cookie value - fixed #266 48 | - fix: Docs small changes in setup - pysaml2 example conf improved 49 | 50 | 51 | v.1.1.2 (2021-04-11) 52 | -------------------- 53 | 54 | - fix: idp hinting invalid import 55 | 56 | 57 | v.1.1.1 (2021-04-05) 58 | -------------------- 59 | 60 | - Read the docs 61 | - Information exposure mitigation on SSO login view 62 | 63 | 64 | v.1.1.0 (2021-04-01) 65 | -------------------- 66 | 67 | - feature: Idp Hinting 68 | - params: SAML_DEFAULT_BINDING for SSO 69 | - code cleanup in SSO 70 | 71 | 72 | v.1.0.5 (2021-03-05) 73 | -------------------- 74 | - code linting, cleanup. Not enough but better than before 75 | - Documentation: Replace signal with hooks (#251) 76 | - Better saml_attribute handling in backend - more resilient 77 | - Add session_info to user auth failed template (#248) 78 | - Fix SAML_ACS_FAILURE_RESPONSE_FUNCTION override 79 | - Update Custom Error Handler docs 80 | 81 | 82 | v.1.0.4 (2021-02-16) 83 | -------------------- 84 | - fixed saml_attributes of zero length 85 | - removed unused code 86 | 87 | 88 | v1.0.3 (2020-02-04) 89 | ------------------- 90 | - Django Logout behaviour improved 91 | 92 | 93 | v1.0.2 (2020-01-24) 94 | ------------------- 95 | - RequestVersionTooLow exception handled in ACS 96 | - Better exception handling for Malformed SAML Response 97 | - pySAML2 dep up to v6.5.1 98 | 99 | 100 | v1.0.1 (2020-01-20) 101 | ------------------- 102 | - PySAML2 dependency, security update 103 | https://github.com/IdentityPython/pysaml2/commit/12ec4a70c5aaf4c144f6b30a158193ca99bc76cd 104 | 105 | v1.0.0 (2020-10-15) 106 | ------------------- 107 | - General refactor with Django ClassViews 108 | 109 | 110 | 0.50.0 (2020-10-15) 111 | ------------------- 112 | - Discovery Service support 113 | 114 | 0.40.1 (2020-09-08) 115 | ------------------- 116 | - [BugFix] HTTP-REDIRECT Authn Requests with optional signature now works. 117 | - [BugFix] SameSite - SuspiciousOperation issue in middleware (Issue #220) 118 | 119 | 0.40.0 (2020-08-07) 120 | ------------------- 121 | - Allow a SSO request without any attributes besides the NameID info. Backwards-incompatible changes to allow easier behaviour differentiation, two methods now receive the idp identifier (+ **kwargs were added to introduce possible similar changes in the future with less breaking effect): 122 | - Method signature changed on Saml2Backend.clean_attributes: from `clean_attributes(self, attributes: dict)` to `clean_attributes(self, attributes: dict, idp_entityid: str, **kwargs)` 123 | - Methodignature changed on Saml2Backend.is_authorized: from `is_authorized(self, attributes: dict, attribute_mapping: dict)` to `is_authorized(self, attributes: dict, attribute_mapping: dict, idp_entityid: str, **kwargs)` 124 | - SAML session refactor and minor changes in README file 125 | - local Logout - independent by IdP SLO Response 126 | 127 | 0.30.0 (2020-07-30) 128 | ------------------- 129 | - SameSite workaround with a specialized cookie, decoupled from django default 130 | 131 | 0.20.0 (2020-06-29) 132 | ------------------- 133 | - Bugfix: Always save newly created users when ATTRIBUTE_MAPPING is missing in the config 134 | - pySAML2 v5.3.0 135 | 136 | 0.19.1 (2020-06-15) 137 | ------------------ 138 | - Fixes creating new user with iexact lookup 139 | 140 | 0.19.0 (2020-06-03) 141 | ------------------ 142 | 143 | - Support several required fields during User creation 144 | - Don't pass sigalg parameter when not signing login request 145 | - ALLOW_SAML_HOSTNAMES validation for redirect 146 | - Custom attribute mapping for Django user model (example) 147 | - Slo absence workaround 148 | - Metadata EntityID exception handling 149 | - Fix unsigned authentication request to POST endpoint 150 | - py38 Test fixes 151 | - CI with Github actions 152 | - Backend restructuring for easier subclassing 153 | - Assertion consumer service now more extensible as a class-based view 154 | with hooks that can be overridden by subclass implementations. 155 | 156 | 0.18.1 (2020-02-15) 157 | ---------- 158 | - Fixed regression from 0.18.0. Thanks to OskarPersson 159 | 160 | 0.18.0 (2020-02-14) 161 | ---------- 162 | - Django 3.0 support. Thanks to OskarPersson 163 | - forceauthn and allowcreate support. Thanks to peppelinux 164 | - Dropped support for Python 3.4 165 | - Also thanks to WebSpider, mhindery, DylannCordel, habi3000 for various fixes and improvements 166 | 167 | Thanks to plumdog 168 | 169 | 0.17.2 (2018-08-29) 170 | ---------- 171 | - Upgraded pysaml2 dependency to version 4.6.0 which fixes security issue. 172 | 173 | Thanks to plumdog 174 | 175 | UNRELEASED 176 | ---------- 177 | - Allowed creating Users with multiple required fields. 178 | 179 | 0.17.1 (2018-07-16) 180 | ---------- 181 | - A 403 (permission denied) is now raised if a SAMLResponse is replayed, instead of 500. 182 | - Dropped support for Python 3.3 183 | - Upgraded pysaml2 dependency to version 4.5.0 184 | 185 | Thanks to francoisfreitag, mhindery, vkurup, peppelinux 186 | 187 | 0.16.11 (2017-12-25) 188 | ---------- 189 | - Dropped compatibility for Python < 2.7 and Django < 1.8. 190 | - Added a clean_attributes hook allowing backends to restructure attributes extracted from SAML response. 191 | - Log when fields are missing in a SAML response. 192 | - Log when attribute_mapping maps to nonexistent User fields. 193 | - Multiple compatibility fixes and other minor improvements and code cleanups 194 | 195 | Thanks to francoisfreitag, mhindery, charn, jdufresne 196 | 197 | 0.16.10 (2017-10-02) 198 | ------------------- 199 | - Bugfixes and internal refactorings. 200 | - Added support for custom USERNAME_FIELD on custom User models. Many thanks to francoisfreitag. 201 | 202 | 0.16.9 (2017-09-19) 203 | ------------------- 204 | - Bugfixes and minor improvements. Thanks to goetzk and AmbientLighter. 205 | - Added option SAML_LOGOUT_REQUEST_PREFERRED_BINDING 206 | - Added Django 1.11 to tox. 207 | 208 | 0.16.4 (2017-09-11) 209 | ------------------- 210 | - Added support for SHA-256 signing. Thanks to WebSpider. 211 | - Bugfixes. Thanks to justinsg and charn. 212 | - Error handling made more extensible. This will be further improved in next versions. 213 | 214 | 0.16.1 (2017-07-15) 215 | ------------------- 216 | - Bugfixes. Thanks to canni, AmbientLighter, cranti and logston. 217 | - request is now passed to authentication backend (introduced in Django 1.11). Thanks to terite. 218 | 219 | 0.16.0 (2017-04-14) 220 | ------------------- 221 | - Upgrade pysaml2 dependency to version 4.4.0 which fixes some serialization issues. Thanks to nakato for the report. 222 | - Added support for HTTP Redirect binding with signed authentication requests. Many thanks to liquidpele for this feature and other related refactorings. 223 | - The custom permission_denied.html template was removed in favor of standard PermissionDenied exception. Thanks to mhindery. 224 | 225 | 0.15.0 (2016-12-18) 226 | ------------------- 227 | - Python 3.5 support. Thanks to timheap. 228 | - Added support for callable user attributes. Thanks to andy-miracl and joetsoi. 229 | - Security improvement: "next" URL is now checked. thanks to flupzor. 230 | - Improved testability. Thanks to flupzor. 231 | - Other bugfixes and minor improvements. Thanks to jamaalscarlett, ws0w, jaywink and liquidpele. 232 | 233 | 0.14.5 (2016-09-19) 234 | ------------------- 235 | - Django 1.10 support. Thanks to inducer. 236 | - Various fixes and minor improvements. Thanks to ajsmilutin, ganiserb, inducer, grunichev, liquidpele and darbula 237 | 238 | 0.14.4 (2016-03-29) 239 | ------------------- 240 | - Fix compatibility issue with pysaml2-4.0.3+. Thanks to jimr and astoltz. 241 | - Fix Django 1.9 compatibility issue in templates. Thanks to nikoskal. 242 | 243 | 0.14.3 (2016-03-18) 244 | ------------------- 245 | - Upgraded to pysaml2-4.0.5. 246 | - Added 'ACS_DEFAULT_REDIRECT_URL' setting for default redirection after successful authentication. Thanks to ganiserb. 247 | 248 | 0.14.2 (2016-03-11) 249 | ------------------- 250 | - Released under the original 'djangosaml2' package name; abandoning the djangosaml2-knaperek fork. 251 | 252 | 0.14.1 (2016-03-09) 253 | ------------------- 254 | - Upgraded to pysaml2-4.0.4. 255 | 256 | 0.14.0 (2016-01-28) 257 | ------------------- 258 | - Upgrade to pysaml2-4.0.2. Thanks to kviktor 259 | - Django 1.9 support. Thanks to Jordi Gutiérrez Hermoso 260 | 261 | 0.13.2 (2015-06-24) 262 | ------------------- 263 | - Improved usage of standard Python logging. 264 | 265 | 0.13.1 (2015-06-05) 266 | ------------------- 267 | - Added support for djangosaml2 specific user model defined by SAML_USER_MODEL setting 268 | 269 | 0.13.0 (2015-02-12) 270 | ------------------- 271 | - Django 1.7 support. Thanks to Kamei Toshimitsu 272 | 273 | 0.12.0 (2014-11-18) 274 | ------------------- 275 | - Pysaml2 2.2.0 support. Thanks to Erick Tryzelaar 276 | 277 | 0.11.0 (2014-06-15) 278 | ------------------- 279 | - Django 1.5 custom user model support. Thanks to Jos van Velzen 280 | - Django 1.5 compatibility url template tag. Thanks to bula 281 | - Support Django 1.5 and 1.6. Thanks to David Evans and Justin Quick 282 | 283 | 0.10.0 (2013-05-05) 284 | ------------------- 285 | - Check that RelayState is not empty before redirecting into a loop. Thanks 286 | to Sam Bull for reporting this issue. 287 | - In the global logout process, when the session is lost, report an error 288 | message to the user and perform a local logout. 289 | 290 | 0.9.2 (2013-04-19) 291 | ------------------ 292 | - Upgrade to pysaml2-0.4.3. 293 | 294 | 0.9.1 (2013-01-29) 295 | ------------------ 296 | - Add a method to the authentication backend so it is possible 297 | to customize the authorization based on SAML attributes. 298 | 299 | 0.9.0 (2012-10-30) 300 | ------------------ 301 | - Add a signal for modifying the user just before saving it on 302 | the update_user method of the authentication backend. 303 | 304 | 0.8.1 (2012-10-29) 305 | ------------------ 306 | - Trim the SAML attributes before setting them to the Django objects 307 | if they are too long. This fixes a crash with MySQL. 308 | 309 | 0.8.0 (2012-10-25) 310 | ------------------ 311 | - Allow to use different attributes besides 'username' to look for 312 | existing users. 313 | 314 | 0.7.0 (2012-10-19) 315 | ------------------ 316 | - Add a setting to decide if the user should be redirected to the 317 | next view or shown an authorization error when the user tries to 318 | login twice. 319 | 320 | 0.6.1 (2012-09-03) 321 | ------------------ 322 | - Remove Django from our dependencies 323 | - Restore support for Django 1.3 324 | 325 | 0.6.0 (2012-08-29) 326 | ------------------ 327 | - Add tox support configured to run the tests with Python 2.6 and 2.7 328 | - Fix some dependencies and sdist generation. Lorenzo Gil 329 | - Allow defining a logout redirect url in the settings. Lorenzo Gil 330 | - Add some logging calls to improve debugging. Lorenzo Gil 331 | - Add support for custom conf loading function. Sam Bull. 332 | - Make the tests more robust and easier to run when djangosaml2 is 333 | included in a Django project. Sam Bull. 334 | - Make sure the profile is not None before saving it. Bug reported by 335 | Leif Johansson 336 | 337 | 0.5.0 (2012-05-22) 338 | ------------------ 339 | - Allow defining custom config loaders. They can be dynamic depending on 340 | the request. 341 | - Do not automatically add the authentication backend. This way 342 | we allow other people to add their own backends. 343 | - Support for additional attributes other than the ones that get mapped 344 | into the User model. Those attributes get stored in the UserProfile model. 345 | 346 | 0.4.2 (2012-03-23) 347 | ------------------ 348 | - Fix a crash in the idplist templatetag about using an old pysaml2 function 349 | - Added a test for the previous crash 350 | 351 | 0.4.1 (2012-03-19) 352 | ------------------ 353 | - Upgrade pysaml2 dependency to version 0.4.1 354 | 355 | 0.4.0 (2012-03-18) 356 | ------------------ 357 | - Upgrade pysaml2 dependency to version 0.4.0 (update our tests as a result 358 | of this) 359 | - Add logging calls to make debugging easier 360 | - Use the Django configured logger in pysaml2 361 | 362 | 0.3.3 (2012-02-14) 363 | ------------------ 364 | - Freeze the version of pysaml2 since we are not (yet!) compatible with 365 | version 0.4.0 366 | 367 | 0.3.2 (2011-12-13) 368 | ------------------ 369 | - Avoid a crash when reading the SAML attribute that maps to the Django 370 | username 371 | 372 | 0.3.1 (2011-12-01) 373 | ------------------ 374 | - Load the config in the render method of the idplist templatetag to 375 | make it more flexible and reentrant. 376 | 377 | 0.3.0 (2011-11-30) 378 | ------------------ 379 | - Templatetag to get the list of available idps. 380 | - Allow to map the same SAML attribute into several Django field. 381 | 382 | 0.2.4 (2011-11-29) 383 | ------------------ 384 | - Fix restructured text bugs that made pypi page looks bad. 385 | 386 | 0.2.3 (2011-06-14) 387 | ------------------ 388 | - Set a unusable password when the user is created for the first time 389 | 390 | 0.2.2 (2011-06-07) 391 | ------------------ 392 | - Prevent infinite loop when going to the /saml2/login/ endpoint and the user 393 | is already logged in and the settings.LOGIN_REDIRECT_URL is (badly) pointing 394 | to /saml2/login. 395 | 396 | 0.2.1 (2011-05-09) 397 | ------------------ 398 | - If no next parameter is supplied to the login view, use the 399 | settings.LOGIN_REDIRECT_URL as default 400 | 401 | 0.2.0 (2011-04-26) 402 | ------------------ 403 | - Python 2.4 compatible if the elementtree library is installed 404 | - Allow post processing after the authentication phase by using 405 | Django signals. 406 | 407 | 0.1.1 (2011-04-18) 408 | ------------------ 409 | - Simple view to echo SAML attributes 410 | - Improve documentation 411 | - Change default behaviour when a new user is created. Now their attributes 412 | are filled this first time 413 | - Allow to set a next page after the logout 414 | 415 | 0.1.0 (2011-03-16) 416 | ------------------ 417 | - Emancipation from the pysaml package 418 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include CHANGES 3 | include COPYING 4 | global-include *.html *.csr *.key *.pem *.xml 5 | include djangosaml2/tests/attribute-maps/*.py 6 | global-exclude *.pyc 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | djangosaml2 2 | =========== 3 | 4 | ![CI build](https://github.com/peppelinux/djangosaml2/workflows/djangosaml2/badge.svg) 5 | ![pypi](https://img.shields.io/pypi/v/djangosaml2.svg) 6 | [![Downloads](https://pepy.tech/badge/djangosaml2/month)](https://pepy.tech/project/djangosaml2) 7 | ![Documentation Status](https://readthedocs.org/projects/djangosaml2/badge/?version=latest) 8 | ![License](https://img.shields.io/badge/license-Apache%202-blue.svg) 9 | ![Python versions](https://img.shields.io/pypi/pyversions/djangosaml2) 10 | ![Django versions](https://img.shields.io/pypi/djversions/djangosaml2) 11 | 12 | 13 | A Django application that builds a Fully Compliant SAML2 Service Provider on top of PySAML2 library. 14 | Djangosaml2 protects your project with a SAML2 SSO Authentication. 15 | 16 | 17 | Features: 18 | 19 | - HTTP-REDIRECT SSO Binding 20 | - HTTP-POST SSO Binding 21 | - SLO POST and HTTP-REDIRECT Binding 22 | - Discovery Service 23 | - embedded Wayf page with customizable html template 24 | - IdP Hinting 25 | - IdP Scoping 26 | - Samesite cookie 27 | 28 | 29 | Please consult the [official Documentation of djangosaml2](https://djangosaml2.readthedocs.io) to get started. 30 | 31 | 32 | Contributing 33 | ============ 34 | 35 | Please open Issues to start debate regarding the requested 36 | features, or the patch that you would apply. We do not use 37 | a strict submission format, please try to be more concise as possible. 38 | 39 | The Pull Request MUST be done on the dev branch, please don't 40 | push code directly on the master branch. 41 | 42 | 43 | Special thanks 44 | ============== 45 | 46 | This is a community-driven project, born as a 47 | fork and maintained by different authors at different times, such as: 48 | 49 | - [Lorenzo Gil Sanchez](https://github.com/lorenzogil) 50 | - [Jozef knaperek](https://github.com/knaperek) 51 | -------------------------------------------------------------------------------- /build_pypi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PROJ_NAME=$(ls | grep *.egg-info | sed -e 's/.egg-info//g') ; rm -R build/ dist/* *.egg-info ; pip uninstall $PROJ_NAME ; python setup.py build sdist 4 | twine upload dist/* 5 | -------------------------------------------------------------------------------- /djangosaml2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdentityPython/djangosaml2/ceb48c28805ae79cb54c74c57b337365373e4171/djangosaml2/__init__.py -------------------------------------------------------------------------------- /djangosaml2/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DjangoSaml2Config(AppConfig): 5 | name = "djangosaml2" 6 | verbose_name = "DjangoSAML2" 7 | 8 | def ready(self): 9 | from . import signals # noqa 10 | -------------------------------------------------------------------------------- /djangosaml2/backends.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2012 Yaco Sistemas (http://www.yaco.es) 2 | # Copyright (C) 2009 Lorenzo Gil Sanchez 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import logging 17 | import warnings 18 | from typing import Any, Optional 19 | 20 | from django.apps import apps 21 | from django.conf import settings 22 | from django.core.exceptions import ImproperlyConfigured, MultipleObjectsReturned 23 | 24 | from django.contrib import auth 25 | from django.contrib.auth.backends import ModelBackend 26 | 27 | logger = logging.getLogger("djangosaml2") 28 | 29 | 30 | def set_attribute(obj: Any, attr: str, new_value: Any) -> bool: 31 | """Set an attribute of an object to a specific value, if it wasn't that already. 32 | Return True if the attribute was changed and False otherwise. 33 | """ 34 | if not hasattr(obj, attr): 35 | setattr(obj, attr, new_value) 36 | return True 37 | if new_value != getattr(obj, attr): 38 | setattr(obj, attr, new_value) 39 | return True 40 | return False 41 | 42 | 43 | class Saml2Backend(ModelBackend): 44 | 45 | # ############################################ 46 | # Internal logic, not meant to be overwritten 47 | # ############################################ 48 | 49 | @property 50 | def _user_model(self): 51 | """Returns the user model specified in the settings, or the default one from this Django installation""" 52 | if hasattr(settings, "SAML_USER_MODEL"): 53 | try: 54 | return apps.get_model(settings.SAML_USER_MODEL) 55 | except LookupError: 56 | raise ImproperlyConfigured( 57 | f"Model '{settings.SAML_USER_MODEL}' could not be loaded" 58 | ) 59 | except ValueError: 60 | raise ImproperlyConfigured( 61 | f"Model was specified as '{settings.SAML_USER_MODEL}', but it must be of the form 'app_label.model_name'" 62 | ) 63 | 64 | return auth.get_user_model() 65 | 66 | @property 67 | def _user_lookup_attribute(self) -> str: 68 | """Returns the attribute on which to match the identifier with when performing a user lookup""" 69 | if hasattr(settings, "SAML_DJANGO_USER_MAIN_ATTRIBUTE"): 70 | return settings.SAML_DJANGO_USER_MAIN_ATTRIBUTE 71 | return getattr(self._user_model, "USERNAME_FIELD", "username") 72 | 73 | def _extract_user_identifier_params( 74 | self, session_info: dict, attributes: dict, attribute_mapping: dict 75 | ) -> tuple[str, Optional[Any]]: 76 | """Returns the attribute to perform a user lookup on, and the value to use for it. 77 | The value could be the name_id, or any other saml attribute from the request. 78 | """ 79 | # Lookup key 80 | user_lookup_key = self._user_lookup_attribute 81 | 82 | # Lookup value 83 | if getattr(settings, "SAML_USE_NAME_ID_AS_USERNAME", False): 84 | if session_info.get("name_id"): 85 | logger.debug(f"name_id: {session_info['name_id']}") 86 | user_lookup_value = session_info["name_id"].text 87 | else: 88 | logger.error( 89 | "The nameid is not available. Cannot find user without a nameid." 90 | ) 91 | user_lookup_value = None 92 | else: 93 | # Obtain the value of the custom attribute to use 94 | user_lookup_value = self._get_attribute_value( 95 | user_lookup_key, attributes, attribute_mapping 96 | ) 97 | 98 | return user_lookup_key, self.clean_user_main_attribute(user_lookup_value) 99 | 100 | def _get_attribute_value( 101 | self, django_field: str, attributes: dict, attribute_mapping: dict 102 | ): 103 | saml_attribute = None 104 | logger.debug("attribute_mapping: %s", attribute_mapping) 105 | for saml_attr, django_fields in attribute_mapping.items(): 106 | if django_field in django_fields and saml_attr in attributes: 107 | saml_attribute = attributes.get(saml_attr, [None]) 108 | 109 | if saml_attribute: 110 | return saml_attribute[0] 111 | else: 112 | logger.error( 113 | "attributes[saml_attr] attribute value is missing. " 114 | f"Either the user session is expired or your mapping is invalid.\n" 115 | f"django_field: {django_field}\n" 116 | f"attributes: {attributes}\n" 117 | f"attribute_mapping: {attribute_mapping}" 118 | ) 119 | 120 | def authenticate( 121 | self, 122 | request, 123 | session_info=None, 124 | attribute_mapping=None, 125 | create_unknown_user=True, 126 | assertion_info=None, 127 | **kwargs, 128 | ): 129 | if session_info is None or attribute_mapping is None: 130 | logger.info("Session info or attribute mapping are None") 131 | return None 132 | 133 | if "ava" not in session_info: 134 | logger.error('"ava" key not found in session_info') 135 | return None 136 | 137 | idp_entityid = session_info["issuer"] 138 | 139 | attributes = self.clean_attributes(session_info["ava"], idp_entityid) 140 | 141 | logger.debug(f"attributes: {attributes}") 142 | 143 | if not self.is_authorized( 144 | attributes, attribute_mapping, idp_entityid, assertion_info 145 | ): 146 | logger.error("Request not authorized") 147 | return None 148 | 149 | user_lookup_key, user_lookup_value = self._extract_user_identifier_params( 150 | session_info, attributes, attribute_mapping 151 | ) 152 | if not user_lookup_value: 153 | logger.error("Could not determine user identifier") 154 | return None 155 | 156 | user, created = self.get_or_create_user( 157 | user_lookup_key, 158 | user_lookup_value, 159 | create_unknown_user, 160 | idp_entityid=idp_entityid, 161 | attributes=attributes, 162 | attribute_mapping=attribute_mapping, 163 | request=request, 164 | ) 165 | 166 | # Update user with new attributes from incoming request 167 | if user is not None: 168 | user = self._update_user( 169 | user, attributes, attribute_mapping, force_save=created 170 | ) 171 | 172 | if self.user_can_authenticate(user): 173 | return user 174 | 175 | def _update_user( 176 | self, user, attributes: dict, attribute_mapping: dict, force_save: bool = False 177 | ): 178 | """Update a user with a set of attributes and returns the updated user. 179 | 180 | By default it uses a mapping defined in the settings constant 181 | SAML_ATTRIBUTE_MAPPING. For each attribute, if the user object has 182 | that field defined it will be set. 183 | """ 184 | 185 | # No attributes to set on the user instance, nothing to update 186 | if not attribute_mapping: 187 | # Always save a brand new user instance 188 | if user.pk is None: 189 | user = self.save_user(user) 190 | return user 191 | 192 | # Lookup key 193 | user_lookup_key = self._user_lookup_attribute 194 | has_updated_fields = False 195 | for saml_attr, django_attrs in attribute_mapping.items(): 196 | attr_value_list = attributes.get(saml_attr) 197 | if not attr_value_list: 198 | logger.debug( 199 | f'Could not find value for "{saml_attr}", not updating fields "{django_attrs}"' 200 | ) 201 | continue 202 | 203 | for attr in django_attrs: 204 | if attr == user_lookup_key: 205 | # Don't update user_lookup_key (e.g. username) (issue #245) 206 | # It was just used to find/create this user and might have 207 | # been changed by `clean_user_main_attribute` 208 | continue 209 | elif hasattr(user, attr): 210 | user_attr = getattr(user, attr) 211 | if callable(user_attr): 212 | modified = user_attr(attr_value_list) 213 | else: 214 | modified = set_attribute(user, attr, attr_value_list[0]) 215 | 216 | has_updated_fields = has_updated_fields or modified 217 | else: 218 | logger.debug(f'Could not find attribute "{attr}" on user "{user}"') 219 | 220 | if has_updated_fields or force_save: 221 | user = self.save_user(user) 222 | 223 | return user 224 | 225 | # ############################################ 226 | # Hooks to override by end-users in subclasses 227 | # ############################################ 228 | 229 | def clean_attributes(self, attributes: dict, idp_entityid: str, **kwargs) -> dict: 230 | """Hook to clean or filter attributes from the SAML response. No-op by default.""" 231 | return attributes 232 | 233 | def is_authorized( 234 | self, 235 | attributes: dict, 236 | attribute_mapping: dict, 237 | idp_entityid: str, 238 | assertion_info: dict, 239 | **kwargs, 240 | ) -> bool: 241 | """Hook to allow custom authorization policies based on SAML attributes. True by default.""" 242 | return True 243 | 244 | def user_can_authenticate(self, user) -> bool: 245 | """ 246 | Reject users with is_active=False. Custom user models that don't have 247 | that attribute are allowed. 248 | """ 249 | is_active = getattr(user, "is_active", None) 250 | return is_active or is_active is None 251 | 252 | def clean_user_main_attribute(self, main_attribute: Any) -> Any: 253 | """Hook to clean the extracted user-identifying value. No-op by default.""" 254 | return main_attribute 255 | 256 | def get_or_create_user( 257 | self, 258 | user_lookup_key: str, 259 | user_lookup_value: Any, 260 | create_unknown_user: bool, 261 | idp_entityid: str, 262 | attributes: dict, 263 | attribute_mapping: dict, 264 | request, 265 | ) -> tuple[Optional[settings.AUTH_USER_MODEL], bool]: 266 | """Look up the user to authenticate. If he doesn't exist, this method creates him (if so desired). 267 | The default implementation looks only at the user_identifier. Override this method in order to do more complex behaviour, 268 | e.g. customize this per IdP. 269 | """ 270 | UserModel = self._user_model 271 | 272 | # Construct query parameters to query the userModel with. An additional lookup modifier could be specified in the settings. 273 | user_query_args = { 274 | user_lookup_key 275 | + getattr( 276 | settings, "SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP", "" 277 | ): user_lookup_value 278 | } 279 | 280 | # Lookup existing user 281 | # Lookup existing user 282 | user, created = None, False 283 | try: 284 | user = UserModel.objects.get(**user_query_args) 285 | except MultipleObjectsReturned: 286 | logger.exception( 287 | f"Multiple users match, model: {UserModel._meta}, lookup: {user_query_args}", 288 | ) 289 | except UserModel.DoesNotExist: 290 | # Create new one if desired by settings 291 | if create_unknown_user: 292 | user = UserModel(**{user_lookup_key: user_lookup_value}) 293 | user.set_unusable_password() 294 | created = True 295 | logger.debug(f"New user created: {user}", exc_info=True) 296 | else: 297 | logger.exception( 298 | f"The user does not exist, model: {UserModel._meta}, lookup: {user_query_args}" 299 | ) 300 | 301 | return user, created 302 | 303 | def save_user( 304 | self, user: settings.AUTH_USER_MODEL, *args, **kwargs 305 | ) -> settings.AUTH_USER_MODEL: 306 | """Hook to add custom logic around saving a user. Return the saved user instance.""" 307 | is_new_instance = user.pk is None 308 | user.save() 309 | 310 | if is_new_instance: 311 | logger.debug("New user created") 312 | else: 313 | logger.debug(f"User {user} updated with incoming attributes") 314 | 315 | return user 316 | 317 | # ############################################ 318 | # Backwards-compatibility stubs 319 | # ############################################ 320 | 321 | def get_attribute_value(self, django_field, attributes, attribute_mapping): 322 | warnings.warn( 323 | "get_attribute_value() is deprecated, look at the Saml2Backend on how to subclass it", 324 | DeprecationWarning, 325 | stacklevel=2, 326 | ) 327 | return self._get_attribute_value(django_field, attributes, attribute_mapping) 328 | 329 | def get_django_user_main_attribute(self): 330 | warnings.warn( 331 | "get_django_user_main_attribute() is deprecated, look at the Saml2Backend on how to subclass it", 332 | DeprecationWarning, 333 | stacklevel=2, 334 | ) 335 | return self._user_lookup_attribute 336 | 337 | def get_django_user_main_attribute_lookup(self): 338 | warnings.warn( 339 | "get_django_user_main_attribute_lookup() is deprecated, look at the Saml2Backend on how to subclass it", 340 | DeprecationWarning, 341 | stacklevel=2, 342 | ) 343 | return getattr(settings, "SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP", "") 344 | 345 | def get_user_query_args(self, main_attribute): 346 | warnings.warn( 347 | "get_user_query_args() is deprecated, look at the Saml2Backend on how to subclass it", 348 | DeprecationWarning, 349 | stacklevel=2, 350 | ) 351 | return { 352 | self.get_django_user_main_attribute() 353 | + self.get_django_user_main_attribute_lookup() 354 | } 355 | 356 | def configure_user(self, user, attributes, attribute_mapping): 357 | warnings.warn( 358 | "configure_user() is deprecated, look at the Saml2Backend on how to subclass it", 359 | DeprecationWarning, 360 | stacklevel=2, 361 | ) 362 | return self._update_user(user, attributes, attribute_mapping) 363 | 364 | def update_user(self, user, attributes, attribute_mapping, force_save=False): 365 | warnings.warn( 366 | "update_user() is deprecated, look at the Saml2Backend on how to subclass it", 367 | DeprecationWarning, 368 | stacklevel=2, 369 | ) 370 | return self._update_user(user, attributes, attribute_mapping) 371 | 372 | def _set_attribute(self, obj, attr, value): 373 | warnings.warn( 374 | "_set_attribute() is deprecated, look at the Saml2Backend on how to subclass it", 375 | DeprecationWarning, 376 | stacklevel=2, 377 | ) 378 | return set_attribute(obj, attr, value) 379 | 380 | 381 | def get_saml_user_model(): 382 | warnings.warn( 383 | "_set_attribute() is deprecated, look at the Saml2Backend on how to subclass it", 384 | DeprecationWarning, 385 | stacklevel=2, 386 | ) 387 | return Saml2Backend()._user_model 388 | -------------------------------------------------------------------------------- /djangosaml2/cache.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es) 2 | # Copyright (C) 2010 Lorenzo Gil Sanchez 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from saml2.cache import Cache 17 | 18 | 19 | class DjangoSessionCacheAdapter(dict): 20 | """A cache of things that are stored in the Django Session""" 21 | 22 | key_prefix = "_saml2" 23 | 24 | def __init__(self, django_session, key_suffix): 25 | self.session = django_session 26 | self.key = self.key_prefix + key_suffix 27 | 28 | super().__init__(self._get_objects()) 29 | 30 | def _get_objects(self): 31 | return self.session.get(self.key, {}) 32 | 33 | def _set_objects(self, objects): 34 | self.session[self.key] = objects 35 | 36 | def sync(self): 37 | # Changes in inner objects do not cause session invalidation 38 | # https://docs.djangoproject.com/en/1.9/topics/http/sessions/#when-sessions-are-saved 39 | 40 | # add objects to session 41 | self._set_objects(dict(self)) 42 | # invalidate session 43 | self.session.modified = True 44 | 45 | 46 | class OutstandingQueriesCache: 47 | """Handles the queries that have been sent to the IdP and have not 48 | been replied yet. 49 | """ 50 | 51 | def __init__(self, django_session): 52 | self._db = DjangoSessionCacheAdapter(django_session, "_outstanding_queries") 53 | 54 | def outstanding_queries(self): 55 | return self._db._get_objects() 56 | 57 | def set(self, saml2_session_id, came_from): 58 | self._db[saml2_session_id] = came_from 59 | self._db.sync() 60 | 61 | def delete(self, saml2_session_id): 62 | if saml2_session_id in self._db: 63 | del self._db[saml2_session_id] 64 | self._db.sync() 65 | 66 | def sync(self): 67 | self._db.sync() 68 | 69 | 70 | class IdentityCache(Cache): 71 | """Handles information about the users that have been succesfully 72 | logged in. 73 | 74 | This information is useful because when the user logs out we must 75 | know where does he come from in order to notify such IdP/AA. 76 | 77 | The current implementation stores this information in the Django session. 78 | """ 79 | 80 | def __init__(self, django_session): 81 | self._db = DjangoSessionCacheAdapter(django_session, "_identities") 82 | self._sync = True 83 | 84 | 85 | class StateCache(DjangoSessionCacheAdapter): 86 | """Store state information that is needed to associate a logout 87 | request with its response. 88 | """ 89 | 90 | def __init__(self, django_session): 91 | super().__init__(django_session, "_state") 92 | -------------------------------------------------------------------------------- /djangosaml2/conf.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2012 Yaco Sistemas (http://www.yaco.es) 2 | # Copyright (C) 2009 Lorenzo Gil Sanchez 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import copy 17 | from typing import Callable, Optional, Union 18 | 19 | from django.conf import settings 20 | from django.core.exceptions import ImproperlyConfigured 21 | from django.http import HttpRequest 22 | from django.utils.module_loading import import_string 23 | 24 | from saml2.config import SPConfig 25 | 26 | from .utils import get_custom_setting 27 | 28 | 29 | def get_config_loader(path: str) -> Callable: 30 | """Import the function at a given path and return it""" 31 | try: 32 | config_loader = import_string(path) 33 | except ImportError as e: 34 | raise ImproperlyConfigured(f'Error importing SAML config loader {path}: "{e}"') 35 | 36 | if not callable(config_loader): 37 | raise ImproperlyConfigured("SAML config loader must be a callable object.") 38 | 39 | return config_loader 40 | 41 | 42 | def config_settings_loader(request: Optional[HttpRequest] = None) -> SPConfig: 43 | """Utility function to load the pysaml2 configuration. 44 | The configuration can be modified based on the request being passed. 45 | This is the default config loader, which just loads the config from the settings. 46 | """ 47 | conf = SPConfig() 48 | conf.load(copy.deepcopy(settings.SAML_CONFIG)) 49 | return conf 50 | 51 | 52 | def get_config( 53 | config_loader_path: Optional[Union[Callable, str]] = None, 54 | request: Optional[HttpRequest] = None, 55 | ) -> SPConfig: 56 | """Load a config_loader function if necessary, and call that 57 | function with the request as argument. 58 | If the config_loader_path is a callable instead of a string, 59 | no importing is necessary and it will be used directly. 60 | Return the resulting SPConfig. 61 | """ 62 | config_loader_path = config_loader_path or get_custom_setting( 63 | "SAML_CONFIG_LOADER", "djangosaml2.conf.config_settings_loader" 64 | ) 65 | 66 | if callable(config_loader_path): 67 | config_loader = config_loader_path 68 | else: 69 | config_loader = get_config_loader(config_loader_path) 70 | 71 | return config_loader(request) 72 | -------------------------------------------------------------------------------- /djangosaml2/exceptions.py: -------------------------------------------------------------------------------- 1 | class IdPConfigurationMissing(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /djangosaml2/middleware.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from django import VERSION 4 | from django.conf import settings 5 | from django.core.exceptions import SuspiciousOperation 6 | from django.utils.cache import patch_vary_headers 7 | from django.utils.http import http_date 8 | 9 | from django.contrib.sessions.backends.base import UpdateError 10 | from django.contrib.sessions.middleware import SessionMiddleware 11 | 12 | django_version = float("{}.{}".format(*VERSION[:2])) 13 | SAMESITE_NONE = None if django_version < 3.1 else "None" 14 | 15 | 16 | class SamlSessionMiddleware(SessionMiddleware): 17 | cookie_name = getattr(settings, "SAML_SESSION_COOKIE_NAME", "saml_session") 18 | 19 | def process_request(self, request): 20 | session_key = request.COOKIES.get(self.cookie_name, None) 21 | request.saml_session = self.SessionStore(session_key) 22 | 23 | def process_response(self, request, response): 24 | """ 25 | If request.saml_session was modified, or if the configuration is to save the 26 | session every time, save the changes and set a session cookie or delete 27 | the session cookie if the session has been emptied. 28 | """ 29 | SAMESITE = getattr(settings, "SAML_SESSION_COOKIE_SAMESITE", SAMESITE_NONE) 30 | 31 | try: 32 | accessed = request.saml_session.accessed 33 | modified = request.saml_session.modified 34 | empty = request.saml_session.is_empty() 35 | except AttributeError: 36 | return response 37 | # First check if we need to delete this cookie. 38 | # The session should be deleted only if the session is entirely empty. 39 | if self.cookie_name in request.COOKIES and empty: 40 | response.delete_cookie( 41 | self.cookie_name, 42 | path=settings.SESSION_COOKIE_PATH, 43 | domain=settings.SESSION_COOKIE_DOMAIN, 44 | samesite=SAMESITE, 45 | ) 46 | patch_vary_headers(response, ("Cookie",)) 47 | else: 48 | if accessed: 49 | patch_vary_headers(response, ("Cookie",)) 50 | # relies and the global one 51 | if (modified or settings.SESSION_SAVE_EVERY_REQUEST) and not empty: 52 | if request.saml_session.get_expire_at_browser_close(): 53 | max_age = None 54 | expires = None 55 | else: 56 | max_age = request.saml_session.get_expiry_age() 57 | expires_time = time.time() + max_age 58 | expires = http_date(expires_time) 59 | # Save the session data and refresh the client cookie. 60 | # Skip session save for 500 responses, refs #3881. 61 | if response.status_code != 500: 62 | try: 63 | request.saml_session.save() 64 | except UpdateError: 65 | raise SuspiciousOperation( 66 | "The request's session was deleted before the " 67 | "request completed. The user may have logged " 68 | "out in a concurrent request, for example." 69 | ) 70 | response.set_cookie( 71 | self.cookie_name, 72 | request.saml_session.session_key, 73 | max_age=max_age, 74 | expires=expires, 75 | domain=settings.SESSION_COOKIE_DOMAIN, 76 | path=settings.SESSION_COOKIE_PATH, 77 | secure=settings.SESSION_COOKIE_SECURE or None, 78 | httponly=settings.SESSION_COOKIE_HTTPONLY or None, 79 | samesite=SAMESITE, 80 | ) 81 | return response 82 | -------------------------------------------------------------------------------- /djangosaml2/overrides.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf import settings 4 | 5 | import saml2.client 6 | 7 | logger = logging.getLogger("djangosaml2") 8 | 9 | 10 | class Saml2Client(saml2.client.Saml2Client): 11 | """ 12 | Custom Saml2Client that adds a choice of preference for binding used with 13 | SAML Logout Requests. The preferred binding can be configured via 14 | SAML_LOGOUT_REQUEST_PREFERRED_BINDING settings variable. 15 | (Original Saml2Client always prefers SOAP, so it is always used if declared 16 | in remote metadata); but doesn't actually work and causes crashes. 17 | """ 18 | 19 | def do_logout(self, *args, **kwargs): 20 | if not kwargs.get("expected_binding"): 21 | try: 22 | kwargs["expected_binding"] = ( 23 | settings.SAML_LOGOUT_REQUEST_PREFERRED_BINDING 24 | ) 25 | except AttributeError: 26 | logger.warning( 27 | "SAML_LOGOUT_REQUEST_PREFERRED_BINDING setting is" 28 | " not defined. Default binding will be used." 29 | ) 30 | return super().do_logout(*args, **kwargs) 31 | -------------------------------------------------------------------------------- /djangosaml2/signals.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es) 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import django.dispatch 16 | 17 | pre_user_save = django.dispatch.Signal() 18 | post_authenticated = django.dispatch.Signal() 19 | -------------------------------------------------------------------------------- /djangosaml2/templates/djangosaml2/auth_error.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 |

Authorization error

9 |

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

10 | 11 |

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

13 | 14 |

Please logout and login as a different user

15 | 16 | 17 | -------------------------------------------------------------------------------- /djangosaml2/templates/djangosaml2/echo_attributes.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 |

SAML attributes

9 |
10 | {% for attribute, value in attributes.items %} 11 |
{{ attribute }}:
12 |
{{ value|join:", " }}
13 | {% endfor %} 14 |
15 | 16 |

Log out

17 | 18 | 19 | -------------------------------------------------------------------------------- /djangosaml2/templates/djangosaml2/example_post_binding_form.html: -------------------------------------------------------------------------------- 1 | 6 |

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

10 |
11 | {% for key, value in params.items %} 12 | 13 | {% endfor %} 14 | 15 |
16 | -------------------------------------------------------------------------------- /djangosaml2/templates/djangosaml2/login_error.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 |

Authentication Error.

9 | 10 |

Access Denied.

11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /djangosaml2/templates/djangosaml2/logout_error.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 |

Logout error

9 |

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

10 | 11 |

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

12 | 13 |

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

14 | 15 |

Sorry for this inconvenience.

16 | 17 | 18 | -------------------------------------------------------------------------------- /djangosaml2/templates/djangosaml2/post_binding_form.html: -------------------------------------------------------------------------------- 1 | 6 |

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

10 |
11 | {% for key, value in params.items %} 12 | 13 | {% endfor %} 14 | 15 |
16 | -------------------------------------------------------------------------------- /djangosaml2/templates/djangosaml2/wayf.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 |

Where are you from?

9 |

Please select your Identity Provider from the following list:

10 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /djangosaml2/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdentityPython/djangosaml2/ceb48c28805ae79cb54c74c57b337365373e4171/djangosaml2/templatetags/__init__.py -------------------------------------------------------------------------------- /djangosaml2/templatetags/idplist.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es) 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from django import template 16 | 17 | from djangosaml2.conf import config_settings_loader 18 | from djangosaml2.utils import available_idps 19 | 20 | register = template.Library() 21 | 22 | 23 | class IdPListNode(template.Node): 24 | def __init__(self, variable_name): 25 | self.variable_name = variable_name 26 | 27 | def render(self, context): 28 | conf = config_settings_loader() 29 | context[self.variable_name] = available_idps(conf) 30 | return "" 31 | 32 | 33 | @register.tag 34 | def idplist(parser, token): 35 | try: 36 | tag_name, as_part, variable = token.split_contents() 37 | except ValueError: 38 | raise template.TemplateSyntaxError( 39 | "%r tag requires two arguments" % token.contents.split()[0] 40 | ) 41 | if not as_part == "as": 42 | raise template.TemplateSyntaxError( 43 | '%r tag first argument must be the literal "as"' % tag_name 44 | ) 45 | 46 | return IdPListNode(variable) 47 | -------------------------------------------------------------------------------- /djangosaml2/tests/attribute-maps/django_saml_uri.py: -------------------------------------------------------------------------------- 1 | X500ATTR_OID = "urn:oid:2.5.4." 2 | PKCS_9 = "urn:oid:1.2.840.113549.1.9.1." 3 | UCL_DIR_PILOT = "urn:oid:0.9.2342.19200300.100.1." 4 | 5 | MAP = { 6 | "identifier": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", 7 | "fro": { 8 | X500ATTR_OID + "3": "first_name", # cn 9 | X500ATTR_OID + "4": "last_name", # sn 10 | PKCS_9 + "1": "email", 11 | UCL_DIR_PILOT + "1": "uid", 12 | }, 13 | "to": { 14 | "first_name": X500ATTR_OID + "3", 15 | "last_name": X500ATTR_OID + "4", 16 | "email": PKCS_9 + "1", 17 | "uid": UCL_DIR_PILOT + "1", 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /djangosaml2/tests/attribute-maps/saml_uri.py: -------------------------------------------------------------------------------- 1 | __author__ = "rolandh" 2 | 3 | EDUPERSON_OID = "urn:oid:1.3.6.1.4.1.5923.1.1.1." 4 | X500ATTR_OID = "urn:oid:2.5.4." 5 | NOREDUPERSON_OID = "urn:oid:1.3.6.1.4.1.2428.90.1." 6 | NETSCAPE_LDAP = "urn:oid:2.16.840.1.113730.3.1." 7 | UCL_DIR_PILOT = "urn:oid:0.9.2342.19200300.100.1." 8 | PKCS_9 = "urn:oid:1.2.840.113549.1.9.1." 9 | UMICH = "urn:oid:1.3.6.1.4.1.250.1.57." 10 | SCHAC = "urn:oid:1.3.6.1.4.1.25178.1.2." 11 | 12 | MAP = { 13 | "identifier": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", 14 | "fro": { 15 | EDUPERSON_OID + "2": "eduPersonNickname", 16 | EDUPERSON_OID + "9": "eduPersonScopedAffiliation", 17 | EDUPERSON_OID + "11": "eduPersonAssurance", 18 | EDUPERSON_OID + "10": "eduPersonTargetedID", 19 | EDUPERSON_OID + "4": "eduPersonOrgUnitDN", 20 | NOREDUPERSON_OID + "6": "norEduOrgAcronym", 21 | NOREDUPERSON_OID + "7": "norEduOrgUniqueIdentifier", 22 | NOREDUPERSON_OID + "4": "norEduPersonLIN", 23 | EDUPERSON_OID + "1": "eduPersonAffiliation", 24 | NOREDUPERSON_OID + "2": "norEduOrgUnitUniqueNumber", 25 | NETSCAPE_LDAP + "40": "userSMIMECertificate", 26 | NOREDUPERSON_OID + "1": "norEduOrgUniqueNumber", 27 | NETSCAPE_LDAP + "241": "displayName", 28 | UCL_DIR_PILOT + "37": "associatedDomain", 29 | EDUPERSON_OID + "6": "eduPersonPrincipalName", 30 | NOREDUPERSON_OID + "8": "norEduOrgUnitUniqueIdentifier", 31 | NOREDUPERSON_OID + "9": "federationFeideSchemaVersion", 32 | X500ATTR_OID + "53": "deltaRevocationList", 33 | X500ATTR_OID + "52": "supportedAlgorithms", 34 | X500ATTR_OID + "51": "houseIdentifier", 35 | X500ATTR_OID + "50": "uniqueMember", 36 | X500ATTR_OID + "19": "physicalDeliveryOfficeName", 37 | X500ATTR_OID + "18": "postOfficeBox", 38 | X500ATTR_OID + "17": "postalCode", 39 | X500ATTR_OID + "16": "postalAddress", 40 | X500ATTR_OID + "15": "businessCategory", 41 | X500ATTR_OID + "14": "searchGuide", 42 | EDUPERSON_OID + "5": "eduPersonPrimaryAffiliation", 43 | X500ATTR_OID + "12": "title", 44 | X500ATTR_OID + "11": "ou", 45 | X500ATTR_OID + "10": "o", 46 | X500ATTR_OID + "37": "cACertificate", 47 | X500ATTR_OID + "36": "userCertificate", 48 | X500ATTR_OID + "31": "member", 49 | X500ATTR_OID + "30": "supportedApplicationContext", 50 | X500ATTR_OID + "33": "roleOccupant", 51 | X500ATTR_OID + "32": "owner", 52 | NETSCAPE_LDAP + "1": "carLicense", 53 | PKCS_9 + "1": "email", 54 | NETSCAPE_LDAP + "3": "employeeNumber", 55 | NETSCAPE_LDAP + "2": "departmentNumber", 56 | X500ATTR_OID + "39": "certificateRevocationList", 57 | X500ATTR_OID + "38": "authorityRevocationList", 58 | NETSCAPE_LDAP + "216": "userPKCS12", 59 | EDUPERSON_OID + "8": "eduPersonPrimaryOrgUnitDN", 60 | X500ATTR_OID + "9": "street", 61 | X500ATTR_OID + "8": "st", 62 | NETSCAPE_LDAP + "39": "preferredLanguage", 63 | EDUPERSON_OID + "7": "eduPersonEntitlement", 64 | X500ATTR_OID + "2": "knowledgeInformation", 65 | X500ATTR_OID + "7": "l", 66 | X500ATTR_OID + "6": "c", 67 | X500ATTR_OID + "5": "serialNumber", 68 | X500ATTR_OID + "4": "sn", 69 | X500ATTR_OID + "3": "cn", 70 | UCL_DIR_PILOT + "60": "jpegPhoto", 71 | X500ATTR_OID + "65": "pseudonym", 72 | NOREDUPERSON_OID + "5": "norEduPersonNIN", 73 | UCL_DIR_PILOT + "3": "mail", 74 | UCL_DIR_PILOT + "25": "dc", 75 | X500ATTR_OID + "40": "crossCertificatePair", 76 | X500ATTR_OID + "42": "givenName", 77 | X500ATTR_OID + "43": "initials", 78 | X500ATTR_OID + "44": "generationQualifier", 79 | X500ATTR_OID + "45": "x500UniqueIdentifier", 80 | X500ATTR_OID + "46": "dnQualifier", 81 | X500ATTR_OID + "47": "enhancedSearchGuide", 82 | X500ATTR_OID + "48": "protocolInformation", 83 | X500ATTR_OID + "54": "dmdName", 84 | NETSCAPE_LDAP + "4": "employeeType", 85 | X500ATTR_OID + "22": "teletexTerminalIdentifier", 86 | X500ATTR_OID + "23": "facsimileTelephoneNumber", 87 | X500ATTR_OID + "20": "telephoneNumber", 88 | X500ATTR_OID + "21": "telexNumber", 89 | X500ATTR_OID + "26": "registeredAddress", 90 | X500ATTR_OID + "27": "destinationIndicator", 91 | X500ATTR_OID + "24": "x121Address", 92 | X500ATTR_OID + "25": "internationaliSDNNumber", 93 | X500ATTR_OID + "28": "preferredDeliveryMethod", 94 | X500ATTR_OID + "29": "presentationAddress", 95 | EDUPERSON_OID + "3": "eduPersonOrgDN", 96 | NOREDUPERSON_OID + "3": "norEduPersonBirthDate", 97 | UMICH + "57": "labeledURI", 98 | UCL_DIR_PILOT + "1": "uid", 99 | SCHAC + "1": "schacMotherTongue", 100 | SCHAC + "2": "schacGender", 101 | SCHAC + "3": "schacDateOfBirth", 102 | SCHAC + "4": "schacPlaceOfBirth", 103 | SCHAC + "5": "schacCountryOfCitizenship", 104 | SCHAC + "6": "schacSn1", 105 | SCHAC + "7": "schacSn2", 106 | SCHAC + "8": "schacPersonalTitle", 107 | SCHAC + "9": "schacHomeOrganization", 108 | SCHAC + "10": "schacHomeOrganizationType", 109 | SCHAC + "11": "schacCountryOfResidence", 110 | SCHAC + "12": "schacUserPresenceID", 111 | SCHAC + "13": "schacPersonalPosition", 112 | SCHAC + "14": "schacPersonalUniqueCode", 113 | SCHAC + "15": "schacPersonalUniqueID", 114 | SCHAC + "17": "schacExpiryDate", 115 | SCHAC + "18": "schacUserPrivateAttribute", 116 | SCHAC + "19": "schacUserStatus", 117 | SCHAC + "20": "schacProjectMembership", 118 | SCHAC + "21": "schacProjectSpecificRole", 119 | }, 120 | "to": { 121 | "roleOccupant": X500ATTR_OID + "33", 122 | "gn": X500ATTR_OID + "42", 123 | "norEduPersonNIN": NOREDUPERSON_OID + "5", 124 | "title": X500ATTR_OID + "12", 125 | "facsimileTelephoneNumber": X500ATTR_OID + "23", 126 | "mail": UCL_DIR_PILOT + "3", 127 | "postOfficeBox": X500ATTR_OID + "18", 128 | "fax": X500ATTR_OID + "23", 129 | "telephoneNumber": X500ATTR_OID + "20", 130 | "norEduPersonBirthDate": NOREDUPERSON_OID + "3", 131 | "rfc822Mailbox": UCL_DIR_PILOT + "3", 132 | "dc": UCL_DIR_PILOT + "25", 133 | "countryName": X500ATTR_OID + "6", 134 | "emailAddress": PKCS_9 + "1", 135 | "employeeNumber": NETSCAPE_LDAP + "3", 136 | "organizationName": X500ATTR_OID + "10", 137 | "eduPersonAssurance": EDUPERSON_OID + "11", 138 | "norEduOrgAcronym": NOREDUPERSON_OID + "6", 139 | "registeredAddress": X500ATTR_OID + "26", 140 | "physicalDeliveryOfficeName": X500ATTR_OID + "19", 141 | "associatedDomain": UCL_DIR_PILOT + "37", 142 | "l": X500ATTR_OID + "7", 143 | "stateOrProvinceName": X500ATTR_OID + "8", 144 | "federationFeideSchemaVersion": NOREDUPERSON_OID + "9", 145 | "pkcs9email": PKCS_9 + "1", 146 | "givenName": X500ATTR_OID + "42", 147 | "givenname": X500ATTR_OID + "42", 148 | "x500UniqueIdentifier": X500ATTR_OID + "45", 149 | "eduPersonNickname": EDUPERSON_OID + "2", 150 | "houseIdentifier": X500ATTR_OID + "51", 151 | "street": X500ATTR_OID + "9", 152 | "supportedAlgorithms": X500ATTR_OID + "52", 153 | "preferredLanguage": NETSCAPE_LDAP + "39", 154 | "postalAddress": X500ATTR_OID + "16", 155 | "email": PKCS_9 + "1", 156 | "norEduOrgUnitUniqueIdentifier": NOREDUPERSON_OID + "8", 157 | "eduPersonPrimaryOrgUnitDN": EDUPERSON_OID + "8", 158 | "c": X500ATTR_OID + "6", 159 | "teletexTerminalIdentifier": X500ATTR_OID + "22", 160 | "o": X500ATTR_OID + "10", 161 | "cACertificate": X500ATTR_OID + "37", 162 | "telexNumber": X500ATTR_OID + "21", 163 | "ou": X500ATTR_OID + "11", 164 | "initials": X500ATTR_OID + "43", 165 | "eduPersonOrgUnitDN": EDUPERSON_OID + "4", 166 | "deltaRevocationList": X500ATTR_OID + "53", 167 | "norEduPersonLIN": NOREDUPERSON_OID + "4", 168 | "supportedApplicationContext": X500ATTR_OID + "30", 169 | "eduPersonEntitlement": EDUPERSON_OID + "7", 170 | "generationQualifier": X500ATTR_OID + "44", 171 | "eduPersonAffiliation": EDUPERSON_OID + "1", 172 | "edupersonaffiliation": EDUPERSON_OID + "1", 173 | "eduPersonPrincipalName": EDUPERSON_OID + "6", 174 | "edupersonprincipalname": EDUPERSON_OID + "6", 175 | "localityName": X500ATTR_OID + "7", 176 | "owner": X500ATTR_OID + "32", 177 | "norEduOrgUnitUniqueNumber": NOREDUPERSON_OID + "2", 178 | "searchGuide": X500ATTR_OID + "14", 179 | "certificateRevocationList": X500ATTR_OID + "39", 180 | "organizationalUnitName": X500ATTR_OID + "11", 181 | "userCertificate": X500ATTR_OID + "36", 182 | "preferredDeliveryMethod": X500ATTR_OID + "28", 183 | "internationaliSDNNumber": X500ATTR_OID + "25", 184 | "uniqueMember": X500ATTR_OID + "50", 185 | "departmentNumber": NETSCAPE_LDAP + "2", 186 | "enhancedSearchGuide": X500ATTR_OID + "47", 187 | "userPKCS12": NETSCAPE_LDAP + "216", 188 | "eduPersonTargetedID": EDUPERSON_OID + "10", 189 | "norEduOrgUniqueNumber": NOREDUPERSON_OID + "1", 190 | "x121Address": X500ATTR_OID + "24", 191 | "destinationIndicator": X500ATTR_OID + "27", 192 | "eduPersonPrimaryAffiliation": EDUPERSON_OID + "5", 193 | "surname": X500ATTR_OID + "4", 194 | "jpegPhoto": UCL_DIR_PILOT + "60", 195 | "eduPersonScopedAffiliation": EDUPERSON_OID + "9", 196 | "edupersonscopedaffiliation": EDUPERSON_OID + "9", 197 | "protocolInformation": X500ATTR_OID + "48", 198 | "knowledgeInformation": X500ATTR_OID + "2", 199 | "employeeType": NETSCAPE_LDAP + "4", 200 | "userSMIMECertificate": NETSCAPE_LDAP + "40", 201 | "member": X500ATTR_OID + "31", 202 | "streetAddress": X500ATTR_OID + "9", 203 | "dmdName": X500ATTR_OID + "54", 204 | "postalCode": X500ATTR_OID + "17", 205 | "pseudonym": X500ATTR_OID + "65", 206 | "dnQualifier": X500ATTR_OID + "46", 207 | "crossCertificatePair": X500ATTR_OID + "40", 208 | "eduPersonOrgDN": EDUPERSON_OID + "3", 209 | "authorityRevocationList": X500ATTR_OID + "38", 210 | "displayName": NETSCAPE_LDAP + "241", 211 | "businessCategory": X500ATTR_OID + "15", 212 | "serialNumber": X500ATTR_OID + "5", 213 | "norEduOrgUniqueIdentifier": NOREDUPERSON_OID + "7", 214 | "st": X500ATTR_OID + "8", 215 | "carLicense": NETSCAPE_LDAP + "1", 216 | "presentationAddress": X500ATTR_OID + "29", 217 | "sn": X500ATTR_OID + "4", 218 | "cn": X500ATTR_OID + "3", 219 | "domainComponent": UCL_DIR_PILOT + "25", 220 | "labeledURI": UMICH + "57", 221 | "uid": UCL_DIR_PILOT + "1", 222 | "schacMotherTongue": SCHAC + "1", 223 | "schacGender": SCHAC + "2", 224 | "schacDateOfBirth": SCHAC + "3", 225 | "schacPlaceOfBirth": SCHAC + "4", 226 | "schacCountryOfCitizenship": SCHAC + "5", 227 | "schacSn1": SCHAC + "6", 228 | "schacSn2": SCHAC + "7", 229 | "schacPersonalTitle": SCHAC + "8", 230 | "schacHomeOrganization": SCHAC + "9", 231 | "schacHomeOrganizationType": SCHAC + "10", 232 | "schacCountryOfResidence": SCHAC + "11", 233 | "schacUserPresenceID": SCHAC + "12", 234 | "schacPersonalPosition": SCHAC + "13", 235 | "schacPersonalUniqueCode": SCHAC + "14", 236 | "schacPersonalUniqueID": SCHAC + "15", 237 | "schacExpiryDate": SCHAC + "17", 238 | "schacUserPrivateAttribute": SCHAC + "18", 239 | "schacUserStatus": SCHAC + "19", 240 | "schacProjectMembership": SCHAC + "20", 241 | "schacProjectSpecificRole": SCHAC + "21", 242 | }, 243 | } 244 | -------------------------------------------------------------------------------- /djangosaml2/tests/auth_response.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es) 2 | # Copyright (C) 2010 Lorenzo Gil Sanchez 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import datetime 17 | 18 | 19 | def auth_response( 20 | session_id, 21 | uid, 22 | audience="http://sp.example.com/saml2/metadata/", 23 | acs_url="http://sp.example.com/saml2/acs/", 24 | metadata_url="http://sp.example.com/saml2/metadata/", 25 | attribute_statements=None, 26 | ): 27 | """Generates a fresh signed authentication response 28 | 29 | Params: 30 | session_id: The session ID to generate the reponse for. Login set an 31 | outstanding session ID, i.e. djangosaml2 waits for a response for 32 | that session. 33 | uid: Unique identifier for a User (will be present as an attribute in 34 | the answer). Ignored when attribute_statements is not ``None``. 35 | audience: SP entityid (used when PySAML validates the response 36 | audience). 37 | acs_url: URL where the response has been posted back. 38 | metadata_url: URL where the SP metadata can be queried. 39 | attribute_statements: An alternative XML AttributeStatement to use in 40 | lieu of the default (uid). The uid argument is ignored when 41 | attribute_statements is not ``None``. 42 | """ 43 | timestamp = datetime.datetime.now() - datetime.timedelta(seconds=10) 44 | tomorrow = datetime.datetime.now() + datetime.timedelta(days=1) 45 | yesterday = datetime.datetime.now() - datetime.timedelta(days=1) 46 | 47 | if attribute_statements is None: 48 | attribute_statements = ( 49 | "" 50 | '' 51 | '' 52 | "%(uid)s" 53 | "" 54 | "" 55 | "" 56 | ) % {"uid": uid} 57 | 58 | saml_response_tpl = ( 59 | "" 60 | '' 61 | '' 62 | "https://idp.example.com/simplesaml/saml2/idp/metadata.php" 63 | "" 64 | "" 65 | '' 66 | "" 67 | '' 68 | '' 69 | "https://idp.example.com/simplesaml/saml2/idp/metadata.php" 70 | "" 71 | "" 72 | '' 73 | "1f87035b4c1325b296a53d92097e6b3fa36d7e30ee82e3fcb0680d60243c1f03" 74 | "" 75 | '' 76 | '' 77 | "" 78 | "" 79 | '' 80 | "" 81 | "" 82 | "%(audience)s" 83 | "" 84 | "" 85 | "" 86 | '' 87 | "" 88 | "" 89 | "urn:oasis:names:tc:SAML:2.0:ac:classes:Password" 90 | "" 91 | "" 92 | "" 93 | "%(attribute_statements)s" 94 | "" 95 | "" 96 | ) 97 | return saml_response_tpl % { 98 | "session_id": session_id, 99 | "audience": audience, 100 | "acs_url": acs_url, 101 | "metadata_url": metadata_url, 102 | "attribute_statements": attribute_statements, 103 | "timestamp": timestamp.strftime("%Y-%m-%dT%H:%M:%SZ"), 104 | "tomorrow": tomorrow.strftime("%Y-%m-%dT%H:%M:%SZ"), 105 | "yesterday": yesterday.strftime("%Y-%m-%dT%H:%M:%SZ"), 106 | } 107 | -------------------------------------------------------------------------------- /djangosaml2/tests/conf.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es) 2 | # Copyright (C) 2010 Lorenzo Gil Sanchez 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import os.path 17 | 18 | import saml2 19 | 20 | 21 | def create_conf( 22 | sp_host="sp.example.com", 23 | idp_hosts=None, 24 | metadata_file="remote_metadata.xml", 25 | authn_requests_signed=None, 26 | sp_kwargs: dict = None, 27 | ): 28 | if idp_hosts is None: 29 | idp_hosts = ["idp.example.com"] 30 | 31 | try: 32 | from saml2.sigver import get_xmlsec_binary 33 | except ImportError: 34 | get_xmlsec_binary = None 35 | 36 | if get_xmlsec_binary: 37 | xmlsec_path = get_xmlsec_binary(["/opt/local/bin"]) 38 | else: 39 | xmlsec_path = "/usr/bin/xmlsec1" 40 | 41 | BASEDIR = os.path.dirname(os.path.abspath(__file__)) 42 | config = { 43 | "xmlsec_binary": xmlsec_path, 44 | "entityid": "http://%s/saml2/metadata/" % sp_host, 45 | "attribute_map_dir": os.path.join(BASEDIR, "attribute-maps"), 46 | "service": { 47 | "sp": { 48 | "name": "Test SP", 49 | "name_id_format": saml2.saml.NAMEID_FORMAT_PERSISTENT, 50 | "endpoints": { 51 | "assertion_consumer_service": [ 52 | ("http://%s/saml2/acs/" % sp_host, saml2.BINDING_HTTP_POST), 53 | ], 54 | "single_logout_service": [ 55 | ("http://%s/saml2/ls/" % sp_host, saml2.BINDING_HTTP_REDIRECT), 56 | ], 57 | }, 58 | "required_attributes": ["uid"], 59 | "optional_attributes": ["eduPersonAffiliation"], 60 | "idp": {}, # this is filled later 61 | "want_response_signed": False, 62 | }, 63 | }, 64 | "metadata": { 65 | "local": [os.path.join(BASEDIR, metadata_file)], 66 | }, 67 | "debug": 1, 68 | # certificates 69 | "key_file": os.path.join(BASEDIR, "mycert.key"), 70 | "cert_file": os.path.join(BASEDIR, "mycert.pem"), 71 | # These fields are only used when generating the metadata 72 | "contact_person": [ 73 | { 74 | "given_name": "Technical givenname", 75 | "sur_name": "Technical surname", 76 | "company": "Example Inc.", 77 | "email_address": "technical@sp.example.com", 78 | "contact_type": "technical", 79 | }, 80 | { 81 | "given_name": "Administrative givenname", 82 | "sur_name": "Administrative surname", 83 | "company": "Example Inc.", 84 | "email_address": "administrative@sp.example.ccom", 85 | "contact_type": "administrative", 86 | }, 87 | ], 88 | "organization": { 89 | "name": [("Ejemplo S.A.", "es"), ("Example Inc.", "en")], 90 | "display_name": [("Ejemplo", "es"), ("Example", "en")], 91 | "url": [("http://www.example.es", "es"), ("http://www.example.com", "en")], 92 | }, 93 | "valid_for": 24, 94 | } 95 | if sp_kwargs is not None: 96 | config["service"]["sp"].update(**sp_kwargs) 97 | 98 | if authn_requests_signed is not None: 99 | config["service"]["sp"]["authn_requests_signed"] = authn_requests_signed 100 | 101 | for idp in idp_hosts: 102 | entity_id = "https://%s/simplesaml/saml2/idp/metadata.php" % idp 103 | config["service"]["sp"]["idp"][entity_id] = { 104 | "single_sign_on_service": { 105 | saml2.BINDING_HTTP_REDIRECT: "https://%s/simplesaml/saml2/idp/SSOService.php" 106 | % idp, 107 | }, 108 | "single_logout_service": { 109 | saml2.BINDING_HTTP_REDIRECT: "https://%s/simplesaml/saml2/idp/SingleLogoutService.php" 110 | % idp, 111 | }, 112 | } 113 | 114 | return config 115 | -------------------------------------------------------------------------------- /djangosaml2/tests/idpcert.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICrTCCAZUCAQAwaDELMAkGA1UEBhMCRVMxEDAOBgNVBAgMB1NldmlsbGExGzAZ 3 | BgNVBAoMEllhY28gU2lzdGVtYXMgUy5MLjEQMA4GA1UEBwwHU2V2aWxsYTEYMBYG 4 | A1UEAwwPaWRwLmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 5 | CgKCAQEAsUBPANpwZb1MUmRsEKMsb+v5eHNih8J/fU7g6iUtvBHacjuQeZEVye/K 6 | qxMOGC+wKW53xUDeITSs91w79eztm+QwdpZPzfjuKH4q5LNeMj6E8YwGw9vymF4b 7 | gsZfZ7iKY+RkqubH7bzYtPSeTtqDkNPlJy6qjpuFMaEkbjAaSAm0KW84/NpMnZn6 8 | HRATWs0noqNDo7yHafTRvtCbJbFp6cCbkTd4h0WeolQBmRisg7pMAmC8uuA06CX2 9 | hU8Ej5/unGw/hCMsF5ysPDYUzLwI18m+kZSgE+Yw2pkVdJcEtmJjw/HqbzJJ+rH4 10 | E8TdWHK7mMi13EwbyEav5d1shN7erwIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEB 11 | AJIrkVa22Yoi5TBIq5grZhDkyCFkxLn8xIGbr3eS4VSq6osgqsALCuxGAioXGoR7 12 | QkczLXz4rWVGCZBF8yGZ3/zujSW8ajqjLqnwgu4hK8TlgtBiIWG7kq+1/yTWD0zl 13 | kSts9WGKWKdSYHGAX8vTAFpYGVnw76H6Fd+cJwnuk0Zym6I9Vr4lTWtBQeVLrMxM 14 | 8AlBYMAgJS3JGgsqAhcxv4oNdMKec6nJmJPSggWUmdNQN8Cluq30kJj3GGtuRd0c 15 | Z0qgTvBQzlSty63nqS76EFNdQiaIKfwvracqoDFIFkqvZgznQih40jzqhRpQ6E/Y 16 | jwz0DPWrG/7E5c/ga9yUt3U= 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /djangosaml2/tests/idpcert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAsUBPANpwZb1MUmRsEKMsb+v5eHNih8J/fU7g6iUtvBHacjuQ 3 | eZEVye/KqxMOGC+wKW53xUDeITSs91w79eztm+QwdpZPzfjuKH4q5LNeMj6E8YwG 4 | w9vymF4bgsZfZ7iKY+RkqubH7bzYtPSeTtqDkNPlJy6qjpuFMaEkbjAaSAm0KW84 5 | /NpMnZn6HRATWs0noqNDo7yHafTRvtCbJbFp6cCbkTd4h0WeolQBmRisg7pMAmC8 6 | uuA06CX2hU8Ej5/unGw/hCMsF5ysPDYUzLwI18m+kZSgE+Yw2pkVdJcEtmJjw/Hq 7 | bzJJ+rH4E8TdWHK7mMi13EwbyEav5d1shN7erwIDAQABAoIBAQCFkQk3gmOaNvhR 8 | Sf0o2Fz/Bdnai1BfLxB088CGkHeTNfzfgcUP5mV94yVcnqJLVXww7F5ylLwOV6xT 9 | RfylB+HRTDW81u3SL1f/yXs3FXbQ882oWzUp2A9KA/hFJoj0FtqqBYxaQEe9/UVr 10 | rr2we/cSZqpSSVca2VSYHm7eXX8gcnZa42jKrLLKFfybq4Pw/917tvKnWnES7MV1 11 | 2jt2+ZeS/Q5GqqJR7hhyVwim10ifO2Lh9c0TB9e/U7ddyTIt6VfKusW7YiOrkRwx 12 | ZM1NpZly1IAxK55ceV6w7IX9y1FM6A8ZvV7bqfVn13S5Zdyjr5XkiU10WXBsHodx 13 | ZndU2SFRAoGBANqvzFjEcWkaDIYnV/vSPmDMrgMA5FL1cUN3if5+LRatiXeQKbnv 14 | nVA5edGnB8LlkMDD4oDjCqx0xVbxaWqS4X8WEPa1/xss34y1MRg6yMZgu60mkzPM 15 | pj+i5mk1Kvsjm6uYz6XPtm7//GWl2y+zgAl8+bgzeUFD5HKV4B+8iZPnAoGBAM9+ 16 | nSZDTFrbA5z5vGLXJlOdAx38ffQTfC1isb+M5I8NhE4CctaYcXcgMbhl+avfEsK9 17 | Wgpivmd7KTnhTujnf4WaIoe655TpnXJJ6dCdXDWktWZTErfqAXTTl945BLrQuGbe 18 | EIAN+1bYqGmk73U/Uw28c8hbTmY6GHTEyQSWMYX5AoGBAKxcfw0/17tlApYCEIC0 19 | VuHosQZA/7S7KwhoAWWKgXMsV/rar2iTiUQf6PnrUly0n4CvY6j+Sf1fE+LQ56tO 20 | FVkbRUeOboE2vwOiFA3q1zA0MffpPYBIPohNlpk5hKTojduT16XyrvGR5ZcgQD+6 21 | lKHl1NTwDRP5tObzZfDdovnlAoGBAIXpeRKQrF6WqqZMpsBDioC7/J8FrWQwjxvb 22 | bkvpajjIyHJwMh09FT2EkZIofhHmTf1QpyO8xpWSbvDj8EFv5mUbLN3cSklY3Dw+ 23 | Z6AzbqdQPaJkSthXNcloJcNNmTfYLKp29r8uRt+txEMqJ0DMNZXP4gmUo+xl4hK6 24 | TeGf7SZBAoGAdTfeG5EmOoTVITw3imvoyETtbXO309YeUhk14p0OELK25w8Dnh70 25 | OLTaY5o4zfUh0noKQSTM9sgxkBCFhwYPxw2CoIWikc1+izAiFesWpgVp8rkFPWuI 26 | B7+5OACkAYK2DGzdlwwVti3LkrvW8gOZ7CUS5W7XzhmpcH6/+mHaKEY= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /djangosaml2/tests/idpcert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDTDCCAjQCCQDs8RuyGDEFWTANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJF 3 | UzEQMA4GA1UECAwHU2V2aWxsYTEbMBkGA1UECgwSWWFjbyBTaXN0ZW1hcyBTLkwu 4 | MRAwDgYDVQQHDAdTZXZpbGxhMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcN 5 | MTAwODI4MTAyNjQ1WhcNMTEwODI4MTAyNjQ1WjBoMQswCQYDVQQGEwJFUzEQMA4G 6 | A1UECAwHU2V2aWxsYTEbMBkGA1UECgwSWWFjbyBTaXN0ZW1hcyBTLkwuMRAwDgYD 7 | VQQHDAdTZXZpbGxhMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wggEiMA0GCSqG 8 | SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxQE8A2nBlvUxSZGwQoyxv6/l4c2KHwn99 9 | TuDqJS28EdpyO5B5kRXJ78qrEw4YL7ApbnfFQN4hNKz3XDv17O2b5DB2lk/N+O4o 10 | firks14yPoTxjAbD2/KYXhuCxl9nuIpj5GSq5sftvNi09J5O2oOQ0+UnLqqOm4Ux 11 | oSRuMBpICbQpbzj82kydmfodEBNazSeio0OjvIdp9NG+0JslsWnpwJuRN3iHRZ6i 12 | VAGZGKyDukwCYLy64DToJfaFTwSPn+6cbD+EIywXnKw8NhTMvAjXyb6RlKAT5jDa 13 | mRV0lwS2YmPD8epvMkn6sfgTxN1YcruYyLXcTBvIRq/l3WyE3t6vAgMBAAEwDQYJ 14 | KoZIhvcNAQEFBQADggEBAHLT+SirLvjzGb1kPJZq5hDhYAMIrUFSgU/ghNRd3tDw 15 | ryOHh9nHgjDq4siy9cRL19LRgly1wspErUTmL/cD6A6L7t6CFUXgXEzshJ+RsZz7 16 | Nbg+61pfK+4+OyO2I3pzGXAHsqLuUpUQFpwHBLu9YiHzY+uiKLgODZl5B3A8nqLN 17 | 2NJ9uH9+YWgquxB6KQLW8cx9kC3AWAsEWihYFb22Uc6I8qFngmDldeHPgVFbt6nV 18 | 74F28qlbWr69NvGMGHZdfL2Ts+KC/yer88+tNrUrJ1tV1jmaMglfWoVTIVMI0Agl 19 | /jPhsxhx0+HHOuIfcD6b334mi/UZz91y/d7poiiiMtY= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /djangosaml2/tests/mycert.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICpjCCAY4CAQAwYTELMAkGA1UEBhMCRVMxEDAOBgNVBAgTB1NldmlsbGExGzAZ 3 | BgNVBAoTEllhY28gU2lzdGVtYXMgUy5MLjEQMA4GA1UEBxMHU2V2aWxsYTERMA8G 4 | A1UEAxMIdGljb3RpY28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDu 5 | sw4w5ohn9hgPmFhLoemOmi9y7iTyBohj6ib3MLEtXkXwEsR+TGg+T0gDdxFA1HF/ 6 | sBcIXEQ4fecrLnoAiLWBTtfp8JPfQkFPw1CVh2A5UwuVH62PLVthXTu0Nr1TyDON 7 | PIpAeBXAfTjfr6uZI+dpwaPd8zB/JJMyG2asmZrHRshrwQH6BjXvsMG29/x0hkhc 8 | uUYxAWWhl5Sym8c6uA2gQD3FTgT0BqcadX0d5XfvO/eYsNQ5AvHi2T2wxCbKUKmK 9 | PtZmZw5XTsPIn9wSae8dJqUFNzIiCRzCWGaO1KB8LLqjnO5bFh1P2JrzRJltbOfw 10 | 3oHSr6erbf45570fSW9zAgMBAAGgADANBgkqhkiG9w0BAQUFAAOCAQEAoK9viSij 11 | kqOwofEUvoJTMdcONm/Ext1yHIilsC3h5ZU451u0kurg4uuwpAOoZDOXtmZHfGOE 12 | /WQ0Juojpwco/SF1I+QGr7coq26xNQldsHlKBuO/wIrgVdtVfjOc+TxS/szMTZv5 13 | whZoZe6HEdxFBvVVedtOMiXONZzzzK3cycSgaQz7BglYgfbFuwB3hdV1Y+iMfQfq 14 | PoIxWrjeDJa1LgFBDkklpgWYLFiaMVvhhdZVFXs/E66OdFwZg+OEplb97bLVba8T 15 | BxDqu6yhNlDlnhaLBd5lIfDCqy5OXa65MqS3vIR9k5CCAwwtaPPxQ2ChUXYhwj0n 16 | aW9RyTsESl7Z+Q== 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /djangosaml2/tests/mycert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA7rMOMOaIZ/YYD5hYS6Hpjpovcu4k8gaIY+om9zCxLV5F8BLE 3 | fkxoPk9IA3cRQNRxf7AXCFxEOH3nKy56AIi1gU7X6fCT30JBT8NQlYdgOVMLlR+t 4 | jy1bYV07tDa9U8gzjTyKQHgVwH0436+rmSPnacGj3fMwfySTMhtmrJmax0bIa8EB 5 | +gY177DBtvf8dIZIXLlGMQFloZeUspvHOrgNoEA9xU4E9AanGnV9HeV37zv3mLDU 6 | OQLx4tk9sMQmylCpij7WZmcOV07DyJ/cEmnvHSalBTcyIgkcwlhmjtSgfCy6o5zu 7 | WxYdT9ia80SZbWzn8N6B0q+nq23+Oee9H0lvcwIDAQABAoIBAEXolw1nVyfrgVx/ 8 | 58wu3XJwYdktOhDQLP3mRAc9cYayB5WqSXYb9qPZIGQzaRAtqBgXgIdoTmqlJSEW 9 | eZDSeSYn60COvyAyDWLI9z7z6RCg69F+95vpUswPPD8pkQWKqt6AjpUXFnfLtO5+ 10 | SqmNRGdK2S1V3iw+kAWq1MVUL2qRFaC6izY3eln/C27mqVDLwb/SWGfl90S+/cNB 11 | 4AyI5LaTT6fwTxjkxlJFIWF9qWjzXM2skRzb/V2QzCF+RiS27maNDnVJMGHWacmo 12 | 2idxOyU+Q0VXmY0ycKkIOrSqoq+F168VxfZNMqUN9fyLu+AjEJmIZWC3jRX9P2NX 13 | ILQjMkECgYEA/sXSyjj05+oFCr/XSvBDy1/JnB2tOTTk/lFXe2IQpJpEb0RqNmYf 14 | vk/5OteMsDcxf13c4Gk7xJXkkgZutXa66PSL4h8DzBAwvl9R0YuWHhDyj4oeYCXl 15 | VsmIjtaUoPS297jckpQMXsuT7YSZpuycJmUGJ0WVq4wuuATIPIxIA9ECgYEA79lp 16 | N46rWGfel2+ruHxt/OZ5fbYzj6VnuwcwrDe2CcYmZ07SRH7AlnwRdPLyY7TGt+p2 17 | 7RjhJ8jpjReJK5yNrnq2D82KgJOwysCGCNDHWf2llEW8Pv66MhDqCn86RYyFbiGn 18 | 4jEb7IcWdmUfMPpOu3TWhxVl/AxUhA8asz1oJAMCgYEAtdoIgrWzAhLFdI3Io8Hp 19 | 8jG2G4wHSC0cQvdWpUgzLvq6XF2OHrQ4dkRpVnni/yj2WL5r2Xbj5YdEdoLG5RoR 20 | ghSEAGw47qCj2k75fMPQ7DcWnCRvWBvUnmUN5z79KgJi02GNd8bbKZLQTRp3/nEn 21 | aDR19vQxSBiwhENNlgJfqPECgYAMdTJt3E8yDFMXcolsz6m21RHCYdBTybeVk04H 22 | 4+zknRIpk4KAZEUEi/UsKeJFI4Ke0uLSddRcCKd42JwbU8pYIa+LKpXjD8jC/zT3 23 | CEESf4Y2KVkZvIlXSGGfofQY4K+dhMn/iaV1p56XD7GLDbVBL1RlN8tQSCOrqE0u 24 | uiXKmQKBgQCrBHEns8QMjWsMRfh+5ebSQNRY1XVtIWV10MMsA3zZqRLUwC0XcU2Z 25 | FEF0FvXgWGC/MQOxXA5ACnToEC7PTfg5WPHxvf0dFi8sGr6nE6yWIzvNJKdp4rz3 26 | q+99h7P2j25C2CmXJ2uS4zhXzaNbZ+UpyDDIzi7Ndw40A3wuh9U7Qw== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /djangosaml2/tests/mycert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDPjCCAiYCCQCkHjPQlll+mzANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQGEwJF 3 | UzEQMA4GA1UECBMHU2V2aWxsYTEbMBkGA1UEChMSWWFjbyBTaXN0ZW1hcyBTLkwu 4 | MRAwDgYDVQQHEwdTZXZpbGxhMREwDwYDVQQDEwh0aWNvdGljbzAeFw0wOTEyMDQx 5 | OTQzNTJaFw0xMDEyMDQxOTQzNTJaMGExCzAJBgNVBAYTAkVTMRAwDgYDVQQIEwdT 6 | ZXZpbGxhMRswGQYDVQQKExJZYWNvIFNpc3RlbWFzIFMuTC4xEDAOBgNVBAcTB1Nl 7 | dmlsbGExETAPBgNVBAMTCHRpY290aWNvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A 8 | MIIBCgKCAQEA7rMOMOaIZ/YYD5hYS6Hpjpovcu4k8gaIY+om9zCxLV5F8BLEfkxo 9 | Pk9IA3cRQNRxf7AXCFxEOH3nKy56AIi1gU7X6fCT30JBT8NQlYdgOVMLlR+tjy1b 10 | YV07tDa9U8gzjTyKQHgVwH0436+rmSPnacGj3fMwfySTMhtmrJmax0bIa8EB+gY1 11 | 77DBtvf8dIZIXLlGMQFloZeUspvHOrgNoEA9xU4E9AanGnV9HeV37zv3mLDUOQLx 12 | 4tk9sMQmylCpij7WZmcOV07DyJ/cEmnvHSalBTcyIgkcwlhmjtSgfCy6o5zuWxYd 13 | T9ia80SZbWzn8N6B0q+nq23+Oee9H0lvcwIDAQABMA0GCSqGSIb3DQEBBQUAA4IB 14 | AQCQBhKOqucJZAqGHx4ybDXNzpPethszonLNVg5deISSpWagy55KlGCi5laio/xq 15 | hHRx18eTzeCeLHQYvTQxw0IjZOezJ1X30DD9lEqPr6C+IrmZc6bn/pF76xsvdaRS 16 | gduNQPT1B25SV2HrEmbf8wafSlRARmBsyUHh860TqX7yFVjhYIAUF/El9rLca51j 17 | ljCIqqvT+klPdjQoZwODWPFHgute2oNRmoIcMjSnoy1+mxOC2Q/j7kcD8/etulg2 18 | XDxB3zD81gfdtT8VBFP+G4UrBa+5zFk6fT6U8a7ZqVsyH+rCXAdCyVlEC4Y5fZri 19 | ID4zT0FcZASGuthM56rRJJSx 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /djangosaml2/tests/remote_metadata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo 9 | 10 | 11 | 12 | 13 | 14 | 15 | MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo 16 | 17 | 18 | 19 | 20 | urn:oasis:names:tc:SAML:2.0:nameid-format:transient 21 | 22 | 23 | 24 | Lorenzo's test IdP 25 | idp.example.com IdP 26 | http://idp.example.com/ 27 | 28 | 29 | Administrator 30 | lorenzo.gil.sanchez@gmail.com 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo 39 | 40 | 41 | 42 | 43 | 44 | 45 | MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo 46 | 47 | 48 | 49 | 50 | urn:oasis:names:tc:SAML:2.0:nameid-format:transient 51 | 52 | 53 | 54 | Lorenzo's test IdP 55 | idp1.example.com IdP 56 | http://idp1.example.com/ 57 | 58 | 59 | Administrator 60 | lorenzo.gil.sanchez@gmail.com 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo 69 | 70 | 71 | 72 | 73 | 74 | 75 | MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo 76 | 77 | 78 | 79 | 80 | urn:oasis:names:tc:SAML:2.0:nameid-format:transient 81 | 82 | 83 | 84 | Lorenzo's test IdP 85 | idp2.example.com IdP 86 | http://idp2.example.com/ 87 | 88 | 89 | Administrator 90 | lorenzo.gil.sanchez@gmail.com 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo 99 | 100 | 101 | 102 | 103 | 104 | 105 | MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo 106 | 107 | 108 | 109 | 110 | urn:oasis:names:tc:SAML:2.0:nameid-format:transient 111 | 112 | 113 | 114 | Lorenzo's test IdP 115 | idp3.example.com IdP 116 | http://idp3.example.com/ 117 | 118 | 119 | Administrator 120 | lorenzo.gil.sanchez@gmail.com 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /djangosaml2/tests/remote_metadata_no_idp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /djangosaml2/tests/remote_metadata_one_idp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo 9 | 10 | 11 | 12 | 13 | 14 | 15 | MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo 16 | 17 | 18 | 19 | 20 | urn:oasis:names:tc:SAML:2.0:nameid-format:transient 21 | 22 | 23 | 24 | Lorenzo's test IdP 25 | idp.example.com IdP 26 | http://idp.example.com/ 27 | 28 | 29 | Administrator 30 | lorenzo.gil.sanchez@gmail.com 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /djangosaml2/tests/remote_metadata_post_binding.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo 9 | 10 | 11 | 12 | 13 | 14 | 15 | MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo 16 | 17 | 18 | 19 | 20 | urn:oasis:names:tc:SAML:2.0:nameid-format:transient 21 | 22 | 23 | 24 | Lorenzo's test IdP 25 | idp.example.com IdP 26 | http://idp.example.com/ 27 | 28 | 29 | Administrator 30 | lorenzo.gil.sanchez@gmail.com 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /djangosaml2/tests/remote_metadata_three_idps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo 9 | 10 | 11 | 12 | 13 | 14 | 15 | MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo 16 | 17 | 18 | 19 | 20 | urn:oasis:names:tc:SAML:2.0:nameid-format:transient 21 | 22 | 23 | 24 | Lorenzo's test IdP 25 | idp1.example.com IdP 26 | http://idp1.example.com/ 27 | 28 | 29 | Administrator 30 | lorenzo.gil.sanchez@gmail.com 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo 39 | 40 | 41 | 42 | 43 | 44 | 45 | MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo 46 | 47 | 48 | 49 | 50 | urn:oasis:names:tc:SAML:2.0:nameid-format:transient 51 | 52 | 53 | 54 | Lorenzo's test IdP 55 | idp2.example.com IdP 56 | http://idp2.example.com/ 57 | 58 | 59 | Administrator 60 | lorenzo.gil.sanchez@gmail.com 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo 69 | 70 | 71 | 72 | 73 | 74 | 75 | MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo 76 | 77 | 78 | 79 | 80 | urn:oasis:names:tc:SAML:2.0:nameid-format:transient 81 | 82 | 83 | 84 | Lorenzo's test IdP 85 | idp3.example.com IdP 86 | http://idp3.example.com/ 87 | 88 | 89 | Administrator 90 | lorenzo.gil.sanchez@gmail.com 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /djangosaml2/tests/sp_metadata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | MIIDFjCCAf4CCQCzHO9MprkomDANBgkqhkiG9w0BAQUFADBNMQswCQYDVQQGEwJl 9 | czEQMA4GA1UECAwHU2V2aWxsYTENMAsGA1UECgwEWWFjbzEQMA4GA1UEBwwHU2V2 10 | aWxsYTELMAkGA1UEAwwCU1AwHhcNMTIwMzE1MjA1MjA1WhcNMTMwMzE1MjA1MjA1 11 | WjBNMQswCQYDVQQGEwJlczEQMA4GA1UECAwHU2V2aWxsYTENMAsGA1UECgwEWWFj 12 | bzEQMA4GA1UEBwwHU2V2aWxsYTELMAkGA1UEAwwCU1AwggEiMA0GCSqGSIb3DQEB 13 | AQUAA4IBDwAwggEKAoIBAQCeOJEVPpGZMDm3nZsJkl/jH8lEmhA4OWgILP3XzdYL 14 | rc/fuKR66XYapGied+Pe+PldqyfLpkojUuRDwAXHTprr1HKlUkKvt4Lk0mqH9Z3/ 15 | mZgj1NkKQBkGRLU0miFK93+m1B/Zlg4K1ycRV7111l5NvT9EVDAnyRU0RVjTrifp 16 | duy85vz9BnRusaR1YKc3NfwC1BiRUAAqhbuSYa0ALwVVri7mNob+/lYmbqrWScpA 17 | QFHy4VjSricjR8WvFjC3eBJbV7LIzdtd19cD+yDX2cDgXXR+QFxLUhHEhVrF+wvT 18 | QGcaZPYFiujcY/3FveRoRwdp6e03sUH/eqJksgR5ylJfAgMBAAEwDQYJKoZIhvcN 19 | AQEFBQADggEBAAu0rKFYHr6pi3yqIIc8EE6NnngqyZEnnDRPWuUK3WKcDI5rOmy9 20 | 8pPE+6sj2NBJyyPu/bsiaCOZBOPywh/AZymO6q3iJRB3pmllH7zYp0LW10HI3NRw 21 | 0T5BJ1ecudM5oitzE7EeMAT6+PogB9i+wxbf/p2YUYsbyiD/JPcrbt5h22sLGxTZ 22 | ovbOVacQF9es/YenvgmFGY42yea6fO33jZyBTiY69Tmjt+sv1nQJTUIyGeb1bVvF 23 | JMCJ3g73lNb3DTDS0UO+zLTlTHb1M/uJJnGY/CCb4kmoPRpxgMbybOh2TVfx9RHm 24 | 45W7GtDf4fZ+LqdZC0JVAZQ7a28L5df0TwQ= 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | http://sp.example.com/ 34 | Lorenzo's test SP 35 | sp.example.com 36 | 37 | 38 | Administrator 39 | lorenzo.gil.sanchez@gmail.com 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /djangosaml2/tests/spcert.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICkjCCAXoCAQAwTTELMAkGA1UEBhMCZXMxEDAOBgNVBAgMB1NldmlsbGExDTAL 3 | BgNVBAoMBFlhY28xEDAOBgNVBAcMB1NldmlsbGExCzAJBgNVBAMMAlNQMIIBIjAN 4 | BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnjiRFT6RmTA5t52bCZJf4x/JRJoQ 5 | ODloCCz9183WC63P37ikeul2GqRonnfj3vj5Xasny6ZKI1LkQ8AFx06a69RypVJC 6 | r7eC5NJqh/Wd/5mYI9TZCkAZBkS1NJohSvd/ptQf2ZYOCtcnEVe9ddZeTb0/RFQw 7 | J8kVNEVY064n6XbsvOb8/QZ0brGkdWCnNzX8AtQYkVAAKoW7kmGtAC8FVa4u5jaG 8 | /v5WJm6q1knKQEBR8uFY0q4nI0fFrxYwt3gSW1eyyM3bXdfXA/sg19nA4F10fkBc 9 | S1IRxIVaxfsL00BnGmT2BYro3GP9xb3kaEcHaentN7FB/3qiZLIEecpSXwIDAQAB 10 | oAAwDQYJKoZIhvcNAQEFBQADggEBAIKJqu8OspbEUBizU9XJBUsdIgFaSurC2QxX 11 | Z/E1bVsg5NLlWYk3Hq8Vec6jCRluasOtqyTqCt9KH+RP+6Q4PXKf0OM5AQ/wLS4R 12 | 4tj2wISEUeuIawwZ64hu8ICEHEoQrRpFos0MWGNXFG5uCxApy7wtpoZsaG8/Lrlw 13 | 5+NVqR3PfC64e4LMVWO60g4OqLzda/XwIrkQszPL5q8zvlTc8ra4d0XEklrmgj2f 14 | I0U0CaImxEVjBXphRUK/RKOFo97mrzw/I9J2oNgJXr0M079m4tnC7kMZgBao39l5 15 | buzpclosi0oznSJWwyV9i/KiIBDqr9iF8l9qCyyVv/CwHGVQW3k= 16 | -----END CERTIFICATE REQUEST----- 17 | -------------------------------------------------------------------------------- /djangosaml2/tests/spcert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAnjiRFT6RmTA5t52bCZJf4x/JRJoQODloCCz9183WC63P37ik 3 | eul2GqRonnfj3vj5Xasny6ZKI1LkQ8AFx06a69RypVJCr7eC5NJqh/Wd/5mYI9TZ 4 | CkAZBkS1NJohSvd/ptQf2ZYOCtcnEVe9ddZeTb0/RFQwJ8kVNEVY064n6XbsvOb8 5 | /QZ0brGkdWCnNzX8AtQYkVAAKoW7kmGtAC8FVa4u5jaG/v5WJm6q1knKQEBR8uFY 6 | 0q4nI0fFrxYwt3gSW1eyyM3bXdfXA/sg19nA4F10fkBcS1IRxIVaxfsL00BnGmT2 7 | BYro3GP9xb3kaEcHaentN7FB/3qiZLIEecpSXwIDAQABAoIBAFtwT5Cah2SjtUeD 8 | gx0mBdpp/VRzQRptOs02y0ETyTcYrUEbIZuTHtlI2Nl0ajHra5oRlz8fjEsb1aW9 9 | 7NkBeZD/R355quaIRNJfNIf8j+Iu7vkOQpyk7JFt1ddfmAwOOyy7/Ogvy0/CheaE 10 | 8Y6PZBLDYzPm/6mOkX2S8kHrrU9DrdOWNzcJNhOV1UbPpo/e4S2rHHQzx71GU/50 11 | HKdKVv5WX+A9vqIzugvXlN0BpGtW4vAOwnXLg5rTg2RLAeCNdBsKUbFPqVaA6xSC 12 | +bgCpR+UC2MWmDBGlIMMTr4Yytuv0n+EkF590N/VqlF5coWTbBjAMogs1t8WxEuQ 13 | d3Caj5ECgYEAz4qum5tkPMZEvLIXABWb26ZBhTSqsyWt4uLuEJb66Q0PRE7yYtxh 14 | 8XL0b2WFfia5dQJGFBTeQATa5VFejMXbxtKWjbdhX8d5mefWZ6xMsgxssx4kBwmp 15 | fsvd1wdFRZEJZE6VLnm/WGRd1SETYFUuzpyLwLypnqkjTdw8Gl9htqcCgYEAwyna 16 | Kr7+b/8F9y0Ka09WdgJIagqi4aoFX63ZV1LfIiqPiAFu/N9BGMr2BRfy+PoKfMdk 17 | R6oVIiFKVE5KWgy++K9TXZ3zBkhRwDFKzyTbaIF8P9mxbEBxhB/G8GF/sEnVo2/R 18 | +F3TUIRO8/ZIk7HU9uubIcdIgSuuJ5pthsO5NYkCgYBfOnwJzFA/Dp6FkpW5JTEh 19 | pPSVYWgd0WErJQMlO5Gfk614o1zWfda3Cg8cehG5o50fEk8DcdvUtiWWaTKgFz1T 20 | ylboacdVQlsKgnU/lrCOVeMegOr5C7bpBjQhMSXY2Mbdbq1G6PgiX9MqMwYIAq36 21 | gZwicK7HrUYUuMQfObrFKwKBgFBZnM7Yj5zAnE4lpxKDOY+gZPvzoRfTjh7UTpUb 22 | M263oxxVqsJFkGGKvjtentRO7Z5t4SV4KvdASX/oM8hbUwzD8kiqzPGbOL0uDiS2 23 | gfbGyMbo85kj9xh0lM1G9vE3lNOTKBlfV67gqjja/wp/vrRiUB5aE8nKmAsKE2nW 24 | jxwxAoGAXBVVaHMA5vVmCZLwuKQxfFfCfd8K7GDeEwXVh0dAv1aglfHk4wSKiKJB 25 | K2OEyYusUQX2hFKzAPWY3nReH+nrgXOBzFXlk6S8pJlXLiDFaDac/DCi0u2dQa4w 26 | vYKXdYMo+cuVYtf1zuUlzlkWprL6Tk04T3AFbcf4nX8dvtf6Iwc= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /djangosaml2/tests/spcert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDFjCCAf4CCQCzHO9MprkomDANBgkqhkiG9w0BAQUFADBNMQswCQYDVQQGEwJl 3 | czEQMA4GA1UECAwHU2V2aWxsYTENMAsGA1UECgwEWWFjbzEQMA4GA1UEBwwHU2V2 4 | aWxsYTELMAkGA1UEAwwCU1AwHhcNMTIwMzE1MjA1MjA1WhcNMTMwMzE1MjA1MjA1 5 | WjBNMQswCQYDVQQGEwJlczEQMA4GA1UECAwHU2V2aWxsYTENMAsGA1UECgwEWWFj 6 | bzEQMA4GA1UEBwwHU2V2aWxsYTELMAkGA1UEAwwCU1AwggEiMA0GCSqGSIb3DQEB 7 | AQUAA4IBDwAwggEKAoIBAQCeOJEVPpGZMDm3nZsJkl/jH8lEmhA4OWgILP3XzdYL 8 | rc/fuKR66XYapGied+Pe+PldqyfLpkojUuRDwAXHTprr1HKlUkKvt4Lk0mqH9Z3/ 9 | mZgj1NkKQBkGRLU0miFK93+m1B/Zlg4K1ycRV7111l5NvT9EVDAnyRU0RVjTrifp 10 | duy85vz9BnRusaR1YKc3NfwC1BiRUAAqhbuSYa0ALwVVri7mNob+/lYmbqrWScpA 11 | QFHy4VjSricjR8WvFjC3eBJbV7LIzdtd19cD+yDX2cDgXXR+QFxLUhHEhVrF+wvT 12 | QGcaZPYFiujcY/3FveRoRwdp6e03sUH/eqJksgR5ylJfAgMBAAEwDQYJKoZIhvcN 13 | AQEFBQADggEBAAu0rKFYHr6pi3yqIIc8EE6NnngqyZEnnDRPWuUK3WKcDI5rOmy9 14 | 8pPE+6sj2NBJyyPu/bsiaCOZBOPywh/AZymO6q3iJRB3pmllH7zYp0LW10HI3NRw 15 | 0T5BJ1ecudM5oitzE7EeMAT6+PogB9i+wxbf/p2YUYsbyiD/JPcrbt5h22sLGxTZ 16 | ovbOVacQF9es/YenvgmFGY42yea6fO33jZyBTiY69Tmjt+sv1nQJTUIyGeb1bVvF 17 | JMCJ3g73lNb3DTDS0UO+zLTlTHb1M/uJJnGY/CCb4kmoPRpxgMbybOh2TVfx9RHm 18 | 45W7GtDf4fZ+LqdZC0JVAZQ7a28L5df0TwQ= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /djangosaml2/tests/utils.py: -------------------------------------------------------------------------------- 1 | from html.parser import HTMLParser 2 | 3 | 4 | class SAMLPostFormParser(HTMLParser): 5 | """ 6 | Parses the SAML Post binding form page for the SAMLRequest value. 7 | """ 8 | 9 | saml_request_value = None 10 | 11 | def handle_starttag(self, tag, attrs): 12 | attrs_dict = dict(attrs) 13 | 14 | if tag != "input" or attrs_dict.get("name") != "SAMLRequest": 15 | return 16 | self.saml_request_value = attrs_dict.get("value") 17 | -------------------------------------------------------------------------------- /djangosaml2/urls.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2012 Yaco Sistemas (http://www.yaco.es) 2 | # Copyright (C) 2009 Lorenzo Gil Sanchez 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from django.urls import path 17 | 18 | from . import views 19 | 20 | urlpatterns = [ 21 | path("login/", views.LoginView.as_view(), name="saml2_login"), 22 | path("acs/", views.AssertionConsumerServiceView.as_view(), name="saml2_acs"), 23 | path("logout/", views.LogoutInitView.as_view(), name="saml2_logout"), 24 | path("ls/", views.LogoutView.as_view(), name="saml2_ls"), 25 | path("ls/post/", views.LogoutView.as_view(), name="saml2_ls_post"), 26 | path("metadata/", views.MetadataView.as_view(), name="saml2_metadata"), 27 | ] 28 | -------------------------------------------------------------------------------- /djangosaml2/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Yaco Sistemas (http://www.yaco.es) 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import base64 15 | import logging 16 | import re 17 | import urllib 18 | import zlib 19 | from functools import lru_cache, wraps 20 | from typing import Optional 21 | from importlib.metadata import version, PackageNotFoundError 22 | 23 | from django.conf import settings 24 | from django.core.exceptions import ImproperlyConfigured 25 | from django.http import HttpResponse, HttpResponseRedirect 26 | from django.shortcuts import resolve_url 27 | from django.urls import NoReverseMatch 28 | from django.utils.http import url_has_allowed_host_and_scheme 29 | from django.utils.module_loading import import_string 30 | 31 | from saml2.config import SPConfig 32 | from saml2.mdstore import MetaDataMDX 33 | from saml2.s_utils import UnknownSystemEntity 34 | 35 | logger = logging.getLogger(__name__) 36 | 37 | 38 | def get_custom_setting(name: str, default=None): 39 | return getattr(settings, name, default) 40 | 41 | 42 | def available_idps(config: SPConfig, langpref=None, idp_to_check: str = None) -> dict: 43 | if langpref is None: 44 | langpref = "en" 45 | 46 | idps = set() 47 | 48 | for metadata in config.metadata.metadata.values(): 49 | # initiate a fetch to the selected idp when using MDQ, otherwise the MetaDataMDX is an empty database 50 | if isinstance(metadata, MetaDataMDX) and idp_to_check: 51 | m = metadata[idp_to_check] # noqa: F841 52 | result = metadata.any("idpsso_descriptor", "single_sign_on_service") 53 | if result: 54 | idps.update(result.keys()) 55 | 56 | return {idp: config.metadata.name(idp, langpref) for idp in idps} 57 | 58 | 59 | def get_idp_sso_supported_bindings( 60 | idp_entity_id: Optional[str] = None, config: Optional[SPConfig] = None 61 | ) -> list: 62 | """Returns the list of bindings supported by an IDP 63 | This is not clear in the pysaml2 code, so wrapping it in a util""" 64 | if config is None: 65 | # avoid circular import 66 | from .conf import get_config 67 | 68 | config = get_config() 69 | # load metadata store from config 70 | meta = getattr(config, "metadata", {}) 71 | # if idp is None, assume only one exists so just use that 72 | if idp_entity_id is None: 73 | try: 74 | idp_entity_id = list(available_idps(config).keys())[0] 75 | except IndexError: 76 | raise ImproperlyConfigured("No IdP configured!") 77 | try: 78 | return list( 79 | meta.service( 80 | idp_entity_id, "idpsso_descriptor", "single_sign_on_service" 81 | ).keys() 82 | ) 83 | except UnknownSystemEntity: 84 | raise UnknownSystemEntity 85 | except Exception as e: 86 | logger.exception(f"get_idp_sso_supported_bindings failed with: {e}") 87 | 88 | 89 | def get_location(http_info): 90 | """Extract the redirect URL from a pysaml2 http_info object""" 91 | try: 92 | headers = dict(http_info["headers"]) 93 | return headers["Location"] 94 | except KeyError: 95 | return http_info["url"] 96 | 97 | 98 | def get_fallback_login_redirect_url(): 99 | login_redirect_url = get_custom_setting("LOGIN_REDIRECT_URL", "/") 100 | return resolve_url( 101 | get_custom_setting("ACS_DEFAULT_REDIRECT_URL", login_redirect_url) 102 | ) 103 | 104 | 105 | def validate_referral_url(request, url): 106 | # Ensure the url is even a valid URL; sometimes the given url is a 107 | # RelayState containing PySAML data. 108 | # Some technically-valid urls will be fail this check, so the 109 | # SAML_STRICT_URL_VALIDATION setting can be used to turn off this check. 110 | # This should only happen if there is no slash, host and/or protocol in the 111 | # given URL. A better fix would be to add those to the RelayState. 112 | saml_strict_url_validation = getattr(settings, "SAML_STRICT_URL_VALIDATION", True) 113 | try: 114 | if saml_strict_url_validation: 115 | # This will also resolve Django URL pattern names 116 | url = resolve_url(url) 117 | except NoReverseMatch: 118 | logger.debug( 119 | "Could not validate given referral url is a valid URL", exc_info=True 120 | ) 121 | return None 122 | 123 | # Ensure the user-originating redirection url is safe. 124 | # By setting SAML_ALLOWED_HOSTS in settings.py the user may provide a list of "allowed" 125 | # hostnames for post-login redirects, much like one would specify ALLOWED_HOSTS . 126 | # If this setting is absent, the default is to use the hostname that was used for the current 127 | # request. 128 | saml_allowed_hosts = set( 129 | getattr(settings, "SAML_ALLOWED_HOSTS", [request.get_host()]) 130 | ) 131 | 132 | if not url_has_allowed_host_and_scheme(url=url, allowed_hosts=saml_allowed_hosts): 133 | logger.debug("Referral URL not in SAML_ALLOWED_HOSTS or of the origin host.") 134 | return None 135 | 136 | return url 137 | 138 | 139 | def saml2_from_httpredirect_request(url): 140 | urlquery = urllib.parse.urlparse(url).query 141 | b64_inflated_saml2req = urllib.parse.parse_qs(urlquery)["SAMLRequest"][0] 142 | 143 | inflated_saml2req = base64.b64decode(b64_inflated_saml2req) 144 | deflated_saml2req = zlib.decompress(inflated_saml2req, -15) 145 | return deflated_saml2req 146 | 147 | 148 | def get_session_id_from_saml2(saml2_xml): 149 | saml2_xml = saml2_xml.decode() if isinstance(saml2_xml, bytes) else saml2_xml 150 | return re.findall(r'ID="([a-z0-9\-]*)"', saml2_xml, re.I)[0] 151 | 152 | 153 | def get_subject_id_from_saml2(saml2_xml): 154 | saml2_xml = saml2_xml if isinstance(saml2_xml, str) else saml2_xml.decode() 155 | re.findall('">([a-z0-9]+)', saml2_xml)[0] 156 | 157 | 158 | def add_param_in_url(url: str, param_key: str, param_value: str): 159 | params = list(url.split("?")) 160 | params.append(f"{param_key}={param_value}") 161 | new_url = params[0] + "?" + "".join(params[1:]) 162 | return new_url 163 | 164 | 165 | def add_idp_hinting(request, http_response) -> bool: 166 | idphin_param = getattr(settings, "SAML2_IDPHINT_PARAM", "idphint") 167 | urllib.parse.urlencode(request.GET) 168 | 169 | if idphin_param not in request.GET.keys(): 170 | return False 171 | 172 | idphint = request.GET[idphin_param] 173 | # validation : TODO -> improve! 174 | if idphint[0:4] != "http": 175 | logger.warning( 176 | f'Idp hinting: "{idphint}" doesn\'t contain a valid value.' 177 | "idphint paramenter ignored." 178 | ) 179 | return False 180 | 181 | if http_response.status_code in (302, 303): 182 | # redirect binding 183 | # urlp = urllib.parse.urlparse(http_response.url) 184 | new_url = add_param_in_url(http_response.url, idphin_param, idphint) 185 | return HttpResponseRedirect(new_url) 186 | 187 | elif http_response.status_code == 200: 188 | # post binding 189 | res = re.search( 190 | r'action="(?P[a-z0-9\:\/\_\-\.]*)"', 191 | http_response.content.decode(), 192 | re.I, 193 | ) 194 | if not res: 195 | return False 196 | orig_url = res.groupdict()["url"] 197 | # 198 | new_url = add_param_in_url(orig_url, idphin_param, idphint) 199 | content = http_response.content.decode().replace(orig_url, new_url).encode() 200 | return HttpResponse(content) 201 | 202 | else: 203 | logger.warning( 204 | f"Idp hinting: cannot detect request type [{http_response.status_code}]" 205 | ) 206 | return False 207 | 208 | 209 | @lru_cache 210 | def get_csp_handler(): 211 | """Returns a view decorator for CSP.""" 212 | 213 | def empty_view_decorator(view): 214 | return view 215 | 216 | csp_handler_string = get_custom_setting("SAML_CSP_HANDLER", None) 217 | 218 | if csp_handler_string is None: 219 | # No CSP handler configured, attempt to use django-csp 220 | return _django_csp_update_decorator() or empty_view_decorator 221 | 222 | if csp_handler_string.strip() != "": 223 | # Non empty string is configured, attempt to import it 224 | csp_handler = import_string(csp_handler_string) 225 | 226 | def custom_csp_updater(f): 227 | @wraps(f) 228 | def wrapper(*args, **kwargs): 229 | return csp_handler(f(*args, **kwargs)) 230 | 231 | return wrapper 232 | 233 | return custom_csp_updater 234 | 235 | # Fall back to empty decorator when csp_handler_string is empty 236 | return empty_view_decorator 237 | 238 | 239 | def _django_csp_update_decorator(): 240 | """Returns a view CSP decorator if django-csp is available, otherwise None.""" 241 | try: 242 | from csp.decorators import csp_update 243 | import csp 244 | except ModuleNotFoundError: 245 | # If csp is not installed, do not update fields as Content-Security-Policy 246 | # is not used 247 | logger.warning( 248 | "django-csp could not be found, not updating Content-Security-Policy. Please " 249 | "make sure CSP is configured. This can be done by your reverse proxy, " 250 | "django-csp or a custom CSP handler via SAML_CSP_HANDLER. See " 251 | "https://djangosaml2.readthedocs.io/contents/security.html#content-security-policy" 252 | " for more information. " 253 | "This warning can be disabled by setting `SAML_CSP_HANDLER=''` in your settings." 254 | ) 255 | return 256 | else: 257 | # autosubmit of forms uses nonce per default 258 | # form-action https: to send data to IdPs 259 | # Check django-csp version to determine the appropriate format 260 | try: 261 | csp_version = version('django-csp') 262 | major_version = int(csp_version.split('.')[0]) 263 | 264 | # Version detection successful 265 | if major_version >= 4: 266 | # django-csp 4.0+ uses dict format with named 'config' parameter 267 | return csp_update(config={"form-action": ["https:"]}) 268 | # django-csp < 4.0 uses kwargs format 269 | return csp_update(FORM_ACTION=["https:"]) 270 | except (PackageNotFoundError, ValueError, RuntimeError, AttributeError, IndexError): 271 | # Version detection failed, we need to try both formats 272 | # Try v4.0+ style first because: 273 | # 1. It has better error handling with clear messages 274 | # 2. Newer versions are more likely to be supported in the future 275 | # 3. If using kwargs with v4.0, it raises a specific RuntimeError we can catch 276 | try: 277 | return csp_update(config={"form-action": ["https:"]}) 278 | except (TypeError, RuntimeError): 279 | # TypeErrors could happen if config is not a recognized parameter (v3.x) 280 | # RuntimeErrors could happen in v4.0+ if we try the wrong approach 281 | return csp_update(FORM_ACTION=["https:"]) 282 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/_static/custom.css: -------------------------------------------------------------------------------- 1 | body, 2 | h1, h2, 3 | .rst-content .toctree-wrapper p.caption, 4 | h3, h4, h5, h6, 5 | legend{ 6 | font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif; 7 | } 8 | 9 | .wy-side-nav-search{ 10 | background: #ffffff; 11 | } 12 | 13 | .wy-side-nav-search>a, 14 | .wy-side-nav-search .wy-dropdown>a{ 15 | color: #9b9c9e; 16 | font-weight: normal; 17 | } 18 | 19 | .wy-menu-vertical header, 20 | .wy-menu-vertical p.caption{ 21 | color: #fff; 22 | font-size:85%; 23 | } 24 | 25 | .wy-nav-top{ 26 | background: #fff; 27 | border-bottom: 1px solid #f7f5f5; 28 | } 29 | 30 | .wy-nav-top a{ 31 | display: block; 32 | color: #9b9c9e; 33 | font-weight: normal; 34 | } 35 | 36 | .wy-nav-top i{ 37 | color: #BE0417; 38 | } 39 | 40 | .wy-nav-top img{ 41 | border-radius: 0; 42 | background: none; 43 | width: 65%; 44 | } 45 | 46 | img{ 47 | height: auto !important; 48 | } 49 | 50 | .document{ 51 | text-align: justify; 52 | } 53 | 54 | h1{ 55 | text-align: left; 56 | } 57 | 58 | #logo_main{ 59 | margin-bottom: 0; 60 | } 61 | 62 | #title_under_logo{ 63 | margin-bottom: 1em; 64 | } 65 | 66 | .alert-danger { 67 | color: #721c24; 68 | background-color: #f8d7da; 69 | border-color: #f5c6cb; 70 | } 71 | .alert-primary { 72 | color: #004085; 73 | background-color: #cce5ff; 74 | border-color: #b8daff; 75 | } 76 | .alert { 77 | position: relative; 78 | padding: .75rem 1.25rem; 79 | margin-bottom: 1rem; 80 | border: 1px solid transparent; 81 | border-radius: .25rem; 82 | } 83 | -------------------------------------------------------------------------------- /docs/source/_static/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdentityPython/djangosaml2/ceb48c28805ae79cb54c74c57b337365373e4171/docs/source/_static/logo.jpg -------------------------------------------------------------------------------- /docs/source/_templates/pplnx_template/footer.html: -------------------------------------------------------------------------------- 1 |
2 | {% if (theme_prev_next_buttons_location == 'bottom' or theme_prev_next_buttons_location == 'both') and (next or prev) %} 3 | 11 | {% endif %} 12 | 13 |
14 | 15 |
16 |

17 | {%- if show_copyright %} 18 | {%- if hasdoc('copyright') %} 19 | {% trans path=pathto('copyright'), copyright=copyright|e %}{{ copyright }}.{% endtrans %} 20 | {%- else %} 21 | {% trans copyright=copyright|e %}{{ copyright }}.{% endtrans %} 22 | {%- endif %} 23 | {%- endif %} 24 | 25 | {%- if build_id and build_url %} 26 | {% trans build_url=build_url, build_id=build_id %} 27 | 28 | Build 29 | {{ build_id }}. 30 | 31 | {% endtrans %} 32 | {%- elif commit %} 33 | {% trans commit=commit %} 34 | 35 | Revision {{ commit }}. 36 | 37 | {% endtrans %} 38 | {%- elif last_updated %} 39 | {% trans last_updated=last_updated|e %}Last updated on {{ last_updated }}.{% endtrans %} 40 | {%- endif %} 41 | 42 |

43 |
44 | 45 | 46 | {%- if show_sphinx %} 47 | {% trans %}Built with Sphinx{% endtrans %}. 48 | {%- endif %} 49 | 50 | {%- block extrafooter %} 51 | {% endblock %} 52 | 53 |
54 | -------------------------------------------------------------------------------- /docs/source/_templates/pplnx_template/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {% block extrahead %} 4 | 5 | 6 | {% endblock %} 7 | 8 | 9 | 10 | {% block sidebartitle %} 11 | 12 | {% if logo %} 13 | {# Not strictly valid HTML, but it's the only way to display/scale 14 | it properly, without weird scripting or heaps of work 15 | #} 16 | 17 | {% endif %} 18 | 19 | {% if logo and theme_logo_only %} 20 | 25 | 26 | {% if theme_display_version %} 27 | {%- set nav_version = version %} 28 | {% if READTHEDOCS and current_version %} 29 | {%- set nav_version = current_version %} 30 | {% endif %} 31 | {% if nav_version %} 32 |
33 | {{ nav_version }} 34 |
35 | {% endif %} 36 | {% endif %} 37 | 38 | {% include "searchbox.html" %} 39 | 40 | {% endblock %} 41 | 42 | 43 | 44 | 45 | {% block mobile_nav %} 46 | 47 | {% if logo %} 48 | 49 | 50 | 51 | {% endif %} 52 | 53 | {{ project }} 54 | 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | from recommonmark.parser import CommonMarkParser 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'djangosaml2' 21 | copyright = '2020, Giuseppe De Marco' 22 | author = 'Giuseppe De Marco' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '0.2.4' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = ['sphinxcontrib.images', 'recommonmark'] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ['_templates/pplnx_template'] 37 | html_logo = "_static/logo.jpg" 38 | 39 | # List of patterns, relative to source directory, that match files and 40 | # directories to ignore when looking for source files. 41 | # This pattern also affects html_static_path and html_extra_path. 42 | exclude_patterns = [] 43 | 44 | 45 | # -- Options for HTML output ------------------------------------------------- 46 | 47 | # The theme to use for HTML and HTML Help pages. See the documentation for 48 | # a list of builtin themes. 49 | # 50 | html_theme = 'sphinx_rtd_theme' 51 | # Add any paths that contain custom static files (such as style sheets) here, 52 | # relative to this directory. They are copied after the builtin static files, 53 | # so a file named "default.css" will overwrite the builtin "default.css". 54 | html_static_path = ['_static'] 55 | 56 | source_suffix = ['.rst', '.md'] 57 | -------------------------------------------------------------------------------- /docs/source/contents/developer.rst: -------------------------------------------------------------------------------- 1 | Testing 2 | ======= 3 | 4 | One way to check if everything is working as expected is to enable the 5 | following url:: 6 | 7 | urlpatterns = patterns( 8 | '', 9 | # lots of url definitions here 10 | 11 | (r'saml2/', include('djangosaml2.urls')), 12 | (r'test/', 'djangosaml2.views.EchoAttributesView.as_view()'), 13 | 14 | # more url definitions 15 | ) 16 | 17 | 18 | Now if you go to the /test/ url you will see your SAML attributes and also 19 | a link to do a global logout. 20 | 21 | Unit tests 22 | ========== 23 | 24 | Djangosaml2 have a legacy way to do tests, using an example project in `tests` directory. 25 | This means that to run tests you have to clone the repository, then install djangosaml2, then run tests using the example project. 26 | 27 | example:: 28 | 29 | pip install -r requirements-dev.txt 30 | # or 31 | pip install djangosaml2[test] 32 | 33 | 34 | then:: 35 | cd tests 36 | ./manage.py migrate 37 | ./manage.py test djangosaml2 38 | 39 | 40 | If you have `tox`_ installed you can simply call `tox` inside the root directory 41 | and it will run the tests in multiple versions of Python. 42 | 43 | .. _`tox`: http://pypi.python.org/pypi/tox 44 | 45 | 46 | Code Coverage 47 | ============= 48 | 49 | example:: 50 | 51 | cd tests/ 52 | coverage erase 53 | coverage run ./manage.py test djangosaml2 testprofiles 54 | coverage report -m 55 | 56 | 57 | Custom error handler 58 | ==================== 59 | 60 | When an error occurs during the authentication flow, djangosaml2 will render 61 | a simple error page with an error message and status code. You can customize 62 | this behaviour by specifying the path to your own error handler in the settings:: 63 | 64 | SAML_ACS_FAILURE_RESPONSE_FUNCTION = 'python.path.to.your.view' 65 | 66 | This should be a view which takes a request, optional exception which occured 67 | and status code, and returns a response to serve the user. E.g. The default 68 | implementation looks like this:: 69 | 70 | def template_failure(request, exception=None, status=403, **kwargs): 71 | """ Renders a simple template with an error message. """ 72 | return render(request, 'djangosaml2/login_error.html', {'exception': exception}, status=status) 73 | 74 | 75 | Contributing 76 | ============ 77 | 78 | Please open Issues to start debate regarding the requested 79 | features, or the patch that you would apply. We do not use 80 | a strict submission format, please try to be more concise as possibile. 81 | 82 | The Pull Request MUST be done on the dev branch, please don't 83 | push code directly on the master branch. 84 | -------------------------------------------------------------------------------- /docs/source/contents/faq.md: -------------------------------------------------------------------------------- 1 | FAQ 2 | === 3 | 4 | **Why can't SAML be implemented as an Django Authentication Backend?** 5 | 6 | well SAML authentication is not that simple as a set of credentials you can 7 | put on a login form and get a response back. Actually the user password is 8 | not given to the service provider at all. This is by design. You have to 9 | delegate the task of authentication to the IdP and then get an asynchronous 10 | response from it. 11 | 12 | Given said that, djangosaml2 does use a Django Authentication Backend to 13 | transform the SAML assertion about the user into a Django user object. 14 | 15 | **Why not put everything in a Django middleware class and make our lifes 16 | easier?** 17 | 18 | Yes, that was an option I did evaluate but at the end the current design 19 | won. In my opinion putting this logic into a middleware has the advantage 20 | of making it easier to configure but has a couple of disadvantages: first, 21 | the middleware would need to check if the request path is one of the 22 | SAML endpoints for every request. Second, it would be too magical and in 23 | case of a problem, much harder to debug. 24 | 25 | **Why not call this package django-saml as many other Django applications?** 26 | 27 | Following that pattern then I should import the application with 28 | import saml but unfortunately that module name is already used in pysaml2. 29 | 30 | **saml2.response.UnsolicitedResponse: Unsolicited response** 31 | 32 | If you are experiencing issues with unsolicited requests this is due to the fact that 33 | cookies not being sent when using the HTTP-POST binding. You have to configure samesite 34 | djangosaml2 middleware (see setup documentation) and also consider upgrading 35 | to Django 3.1 or higher. 36 | If you can't do that, configure "allow_unsolicited" to True in pySAML2 configuration. 37 | -------------------------------------------------------------------------------- /docs/source/contents/miscellanea.rst: -------------------------------------------------------------------------------- 1 | SimpleSAMLphp issues 2 | -------------------- 3 | As of SimpleSAMLphp 1.8.2 there is a problem if you specify attributes in 4 | the SP configuration. When the SimpleSAMLphp metadata parser converts the 5 | XML into its custom php format it puts the following option:: 6 | 7 | 'attributes.NameFormat' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri' 8 | 9 | But it need to be replaced by this one:: 10 | 11 | 'AttributeNameFormat' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri' 12 | 13 | Otherwise the Assertions sent from the IdP to the SP will have a wrong 14 | Attribute Name Format and pysaml2 will be confused. 15 | 16 | Furthermore if you have a AttributeLimit filter in your SimpleSAMLphp 17 | configuration you will need to enable another attribute filter just 18 | before to make sure that the AttributeLimit does not remove the attributes 19 | from the authentication source. The filter you need to add is an AttributeMap 20 | filter like this:: 21 | 22 | 10 => array( 23 | 'class' => 'core:AttributeMap', 'name2oid' 24 | ), 25 | 26 | Okta federation 27 | --------------- 28 | 29 | Okta settings to configure on your Idp's SAML app advanced settings:: 30 | 31 | Single Logout URL: http://localhost:8000/saml2/ls/post/ 32 | SP Issuer : http://localhost:8000/saml2/metadata/ 33 | 34 | Okta sample configuration for setting up an Okta SSO with Django:: 35 | 36 | 'service': { 37 | # we are just a lonely SP 38 | 'sp': { 39 | 'name': 'XXX', 40 | 'allow_unsolicited': True, 41 | 'want_assertions_signed': True, # assertion signing (default=True) 42 | 'want_response_signed': True, 43 | "want_assertions_or_response_signed": True, # is response signing required 44 | 'name_id_format': NAMEID_FORMAT_UNSPECIFIED, 45 | 46 | # Must for signed logout requests 47 | "logout_requests_signed": True, 48 | 'endpoints': { 49 | # url and binding to the assetion consumer service view 50 | # do not change the binding or service name 51 | 'assertion_consumer_service': [ 52 | ('http://localhost:8000/saml2/acs/', 53 | saml2.BINDING_HTTP_POST), 54 | ], 55 | # url and binding to the single logout service view 56 | # do not change the binding or service name 57 | 'single_logout_service': [ 58 | # ('http://localhost:8000/saml2/ls/', 59 | # saml2.BINDING_HTTP_REDIRECT), 60 | ('http://localhost:8000/saml2/ls/post/', 61 | saml2.BINDING_HTTP_POST), 62 | ], 63 | }, 64 | # Mandates that the identity provider MUST authenticate the 65 | # presenter directly rather than rely on a previous security context. 66 | 'force_authn': False, 67 | 68 | "allow_unsolicited": True, 69 | 70 | # Enable AllowCreate in NameIDPolicy. 71 | 'name_id_format_allow_create': False, 72 | 73 | # attributes that this project need to identify a user 74 | 'required_attributes': ['email'], 75 | 76 | # in this section the list of IdPs we talk to are defined 77 | 'idp': { 78 | # we do not need a WAYF service since there is 79 | # only an IdP defined here. This IdP should be 80 | # present in our metadata 81 | 82 | # the keys of this dictionary are entity ids 83 | 'https://xxx.okta.com/app/XXXXXXXXXX/sso/saml/metadata': { 84 | # Okta only uses HTTP_POST disable this 85 | # 'single_sign_on_service': { 86 | # saml2.BINDING_HTTP_REDIRECT: 'https://xxx.okta.com/app/APPNAME/xxxxxxxxx/sso/saml', 87 | # }, 88 | 'single_logout_service': { 89 | saml2.BINDING_HTTP_POST: 'https://xxx.okta.com/app/APPNAME/xxxxxxxxxx/slo/saml', 90 | }, 91 | }, 92 | }, 93 | 94 | }, 95 | }, 96 | -------------------------------------------------------------------------------- /docs/source/contents/security.md: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | Authentication and Authorization are quite security relevant topics on its own. 5 | Make sure you understand SAML2 and its implications, specifically the 6 | separation of duties between Service Provider (SP) and Identity Provider (IdP): 7 | this library aims to support a Service Provider in getting authenticated with 8 | with one or more Identity Provider. 9 | 10 | Communication between SP and IdP is routed via the Browser, eliminating the 11 | need for direct communication between SP and IdP. However, for security the use 12 | of cryptographic signatures (both while sending and receiving messages) must be 13 | examined and the private keys in use must be kept closely guarded. 14 | 15 | Content Security Policy 16 | ======================= 17 | 18 | When using POST-Bindings, the Browser is presented with a small HTML-Form for 19 | every redirect (both Login and Logout), which is sent using JavaScript and 20 | sends the Data to the selected IdP. If your application uses technices such as 21 | Content Security Policy, this might affect the calls. Since Version 1.9.0 22 | djangosaml2 will detect if django-csp is installed and update the Content 23 | Security Policy accordingly. 24 | 25 | [Content Security Policy](https://content-security-policy.com/) is an important 26 | HTTP-Extension to prevent User Input or other harmful sources from manipulating 27 | application data. Usage is strongly advised, see 28 | [OWASP Control](https://owasp.org/www-community/controls/Content_Security_Policy). 29 | 30 | To enable CSP with [django-csp](https://django-csp.readthedocs.io/), simply 31 | follow their [installation](https://django-csp.readthedocs.io/en/latest/installation.html) 32 | and [configuration](https://django-csp.readthedocs.io/en/latest/configuration.html) 33 | guides: djangosaml2 will automatically blend in and update the headers for 34 | POST-bindings, so you must not include exceptions for djangosaml2 in your 35 | global configuration. 36 | 37 | Note that to enable autosubmit of post-bindings inline-javascript is used. To 38 | allow execution of this autosubmit-code a nonce is included, which works in 39 | default configuration but may not work if you modify `CSP_INCLUDE_NONCE_IN` 40 | to exclude `script-src`. 41 | 42 | You can specify a custom CSP handler via the `SAML_CSP_HANDLER` setting and the 43 | warning can be disabled by setting `SAML_CSP_HANDLER=''`. See the 44 | [djangosaml2](https://djangosaml2.readthedocs.io/) documentation for more 45 | information. 46 | -------------------------------------------------------------------------------- /docs/source/contents/usage.md: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | Prepare Database and Preload example data 5 | ```` 6 | ./manage.py migrate 7 | ./manage.py createsuperuser 8 | ./manage.py runserver 9 | ```` 10 | 11 | Test IdP 12 | ======== 13 | 14 | Congratulations, you have finished configuring the SP side of the federation. 15 | Now you need to send the entity id and the metadata of this new SP to the 16 | IdP administrators so they can add it to their list of trusted services. 17 | 18 | You can get this information starting your Django development server and 19 | going to the **http://localhost:8000/saml2/metadata/** url. If you have included 20 | the djangosaml2 urls under a different url prefix you need to correct this 21 | url. 22 | 23 | There are many saml2 idps suitable for testing, such as [samltest.id](https://samltest.id/). 24 | If you are looking for a django IdP, you can try [uniAuth](https://github.com/UniversitaDellaCalabria/uniAuth) or 25 | [djangosaml2idp](https://github.com/OTA-Insight/djangosaml2idp/). 26 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Djangosaml2's Documentation 2 | ====================================== 3 | 4 | A Django application that builds a fully compliant SAML2 Service Provider on top of 5 | `PySAML2 `_ library. 6 | Djangosaml2 protects your project with a SAML2 SSO Authentication, supporting features like 7 | **HTTP-REDIRECT** and **HTTP-POST SSO Binding**, **Single logout**, 8 | **Discovery Service**, **Wayf page** with customizable html template, 9 | **IdP Hinting**, **IdP Scoping** and **Samesite cookie** SSO workaround. 10 | 11 | The entire project code is open sourced and therefore licensed 12 | under the `Apache 2.0 `_. 13 | 14 | 15 | .. toctree:: 16 | :maxdepth: 2 17 | :caption: Setup 18 | 19 | contents/setup.rst 20 | 21 | .. toctree:: 22 | :maxdepth: 2 23 | :caption: Usage 24 | 25 | contents/usage.md 26 | 27 | .. toctree:: 28 | :maxdepth: 2 29 | :caption: Developer's 30 | 31 | contents/developer.rst 32 | 33 | .. toctree:: 34 | :maxdepth: 2 35 | :caption: Miscellanea 36 | 37 | contents/miscellanea.rst 38 | 39 | .. toctree:: 40 | :maxdepth: 2 41 | :caption: FAQ 42 | 43 | contents/faq.md 44 | 45 | .. toctree:: 46 | :maxdepth: 2 47 | :caption: Security considerations 48 | 49 | contents/security.md -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | force-exclude = '''/(migrations)/''' 3 | target-version = ["py39"] 4 | 5 | [tool.isort] 6 | src_paths = ["djangosaml2", "tests"] 7 | profile = "black" 8 | known_django = ["django"] 9 | known_contrib = ["django.contrib"] 10 | known_saml2 = ["saml2"] 11 | known_first_party = ["djangosaml2"] 12 | known_tests = ["tests"] 13 | sections = ["FUTURE", "STDLIB", "DJANGO", "CONTRIB", "THIRDPARTY", "SAML2", "FIRSTPARTY", "TESTS", "LOCALFOLDER"] 14 | skip_glob = ["**/migrations/*.py"] 15 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-django 3 | pytest-cov 4 | codecov 5 | -------------------------------------------------------------------------------- /requirements-docs.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | recommonmark 3 | sphinx_rtd_theme 4 | sphinxcontrib-images 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | # E203 ignore 6 | # https://github.com/PyCQA/pycodestyle/issues/373 7 | # https://github.com/PyCQA/pycodestyle/pull/914 8 | ignore = D106,E203,W503 9 | select = B0,B901,B902,B903,C,F,I,W 10 | max-line-length = 88 11 | exclude = .tox,.git,*/migrations/*,docs 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2011-2012 Yaco Sistemas 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import codecs 17 | import os 18 | 19 | from setuptools import find_packages, setup 20 | 21 | 22 | def read(*rnames): 23 | return codecs.open( 24 | os.path.join(os.path.dirname(__file__), *rnames), encoding="utf-8" 25 | ).read() 26 | 27 | 28 | setup( 29 | name="djangosaml2", 30 | version="1.11.0", 31 | description="pysaml2 integration for Django", 32 | long_description=read("README.md"), 33 | long_description_content_type="text/markdown", 34 | classifiers=[ 35 | "Development Status :: 5 - Production/Stable", 36 | "Environment :: Web Environment", 37 | "Framework :: Django", 38 | "Framework :: Django :: 4.2", 39 | "Framework :: Django :: 5.0", 40 | "Framework :: Django :: 5.1", 41 | "Intended Audience :: Developers", 42 | "License :: OSI Approved :: Apache Software License", 43 | "Operating System :: OS Independent", 44 | "Programming Language :: Python", 45 | "Programming Language :: Python :: 3.9", 46 | "Programming Language :: Python :: 3.10", 47 | "Programming Language :: Python :: 3.11", 48 | "Programming Language :: Python :: 3.12", 49 | "Programming Language :: Python :: 3.13", 50 | "Topic :: Internet :: WWW/HTTP", 51 | "Topic :: Internet :: WWW/HTTP :: WSGI", 52 | "Topic :: Security", 53 | "Topic :: Software Development :: Libraries :: Application Frameworks", 54 | ], 55 | keywords="django,pysaml2,sso,saml2,federated authentication,authentication", 56 | author="Yaco Sistemas and independent contributors", 57 | author_email="lorenzo.gil.sanchez@gmail.com", 58 | maintainer="Giuseppe De Marco", 59 | url="https://github.com/IdentityPython/djangosaml2", 60 | download_url="https://pypi.org/project/djangosaml2/", 61 | license="Apache 2.0", 62 | packages=find_packages(exclude=["tests", "tests.*"]), 63 | include_package_data=True, 64 | zip_safe=False, 65 | install_requires=["defusedxml>=0.4.1", "Django>=4.2", "pysaml2>=6.5.1"], 66 | python_requires=">=3.9", 67 | ) 68 | -------------------------------------------------------------------------------- /tests/.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | source = djangosaml2 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdentityPython/djangosaml2/ceb48c28805ae79cb54c74c57b337365373e4171/tests/__init__.py -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /tests/run_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (C) 2012 Sam Bull (lsb@pocketuniverse.ca) 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import os 18 | import sys 19 | 20 | from django.core import management 21 | from django.core.wsgi import get_wsgi_application 22 | 23 | PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)) 24 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") 25 | sys.path.append(PROJECT_DIR) 26 | # Load models 27 | application = get_wsgi_application() 28 | 29 | management.call_command("test", "djangosaml2.tests", "testprofiles", "-v", "2") 30 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for xxx project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.10.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.10/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | # Make this unique, and don't share it with anybody. 19 | SECRET_KEY = "xvds$ppv5ha75qg1yx3aax7ugr_2*fmdrc(lrc%x7kdez-63xn" 20 | 21 | DEBUG = True 22 | 23 | ALLOWED_HOSTS = [] 24 | 25 | INSTALLED_APPS = ( 26 | "testprofiles", 27 | "django.contrib.admin", 28 | "django.contrib.auth", 29 | "django.contrib.contenttypes", 30 | "django.contrib.sessions", 31 | "django.contrib.messages", 32 | "django.contrib.staticfiles", 33 | "djangosaml2", 34 | ) 35 | 36 | MIDDLEWARE = ( 37 | "django.middleware.security.SecurityMiddleware", 38 | "django.contrib.sessions.middleware.SessionMiddleware", 39 | "django.middleware.common.CommonMiddleware", 40 | "django.middleware.csrf.CsrfViewMiddleware", 41 | "django.contrib.auth.middleware.AuthenticationMiddleware", 42 | "django.contrib.messages.middleware.MessageMiddleware", 43 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 44 | # SameSite Cookie handler 45 | "djangosaml2.middleware.SamlSessionMiddleware", 46 | ) 47 | 48 | ROOT_URLCONF = "testprofiles.urls" 49 | 50 | TEMPLATES = [ 51 | { 52 | "BACKEND": "django.template.backends.django.DjangoTemplates", 53 | "DIRS": [], 54 | "APP_DIRS": True, 55 | "OPTIONS": { 56 | "context_processors": [ 57 | "django.template.context_processors.debug", 58 | "django.template.context_processors.request", 59 | "django.contrib.auth.context_processors.auth", 60 | "django.contrib.messages.context_processors.messages", 61 | ], 62 | }, 63 | }, 64 | ] 65 | 66 | WSGI_APPLICATION = "testprofiles.wsgi.application" 67 | 68 | # Database 69 | # https://docs.djangoproject.com/en/1.10/ref/settings/#databases 70 | 71 | DATABASES = { 72 | "default": { 73 | "ENGINE": "django.db.backends.sqlite3", 74 | "NAME": os.path.join(BASE_DIR, "tests/db.sqlite3"), 75 | } 76 | } 77 | 78 | # Password validation 79 | # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators 80 | 81 | AUTH_PASSWORD_VALIDATORS = [ 82 | { 83 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 84 | }, 85 | { 86 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 87 | }, 88 | { 89 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 90 | }, 91 | { 92 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 93 | }, 94 | ] 95 | 96 | # Internationalization 97 | # https://docs.djangoproject.com/en/1.10/topics/i18n/ 98 | 99 | LANGUAGE_CODE = "en-us" 100 | 101 | TIME_ZONE = "UTC" 102 | 103 | SITE_ID = 1 104 | 105 | USE_I18N = True 106 | 107 | USE_TZ = True 108 | 109 | 110 | # https://docs.djangoproject.com/en/dev/ref/settings/#default-auto-field 111 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField" 112 | 113 | 114 | # Static files (CSS, JavaScript, Images) 115 | # https://docs.djangoproject.com/en/1.10/howto/static-files/ 116 | 117 | STATIC_URL = "/static/" 118 | 119 | 120 | AUTH_USER_MODEL = "testprofiles.TestUser" 121 | 122 | # A sample logging configuration. The only tangible logging 123 | # performed by this configuration is to send an email to 124 | # the site admins on every HTTP 500 error when DEBUG=False. 125 | # See http://docs.djangoproject.com/en/dev/topics/logging for 126 | # more details on how to customize your logging configuration. 127 | LOGGING = { 128 | "version": 1, 129 | "disable_existing_loggers": False, 130 | "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, 131 | "handlers": { 132 | "mail_admins": { 133 | "level": "ERROR", 134 | "filters": ["require_debug_false"], 135 | "class": "django.utils.log.AdminEmailHandler", 136 | }, 137 | "console": { 138 | "level": "DEBUG", 139 | "class": "logging.StreamHandler", 140 | }, 141 | }, 142 | "loggers": { 143 | "django.request": { 144 | "handlers": ["mail_admins"], 145 | "level": "ERROR", 146 | "propagate": True, 147 | }, 148 | "djangosaml2": { 149 | "handlers": ["console"], 150 | "level": "DEBUG", 151 | }, 152 | }, 153 | } 154 | 155 | 156 | AUTHENTICATION_BACKENDS = ("djangosaml2.backends.Saml2Backend",) 157 | -------------------------------------------------------------------------------- /tests/testprofiles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdentityPython/djangosaml2/ceb48c28805ae79cb54c74c57b337365373e4171/tests/testprofiles/__init__.py -------------------------------------------------------------------------------- /tests/testprofiles/app.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TestProfilesConfig(AppConfig): 5 | name = "testprofiles" 6 | verbose_name = "Test profiles" 7 | default_auto_field = "django.db.models.AutoField" 8 | -------------------------------------------------------------------------------- /tests/testprofiles/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-05-01 14:54 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('auth', '0011_update_proxy_permissions'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='RequiredFieldUser', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('email', models.EmailField(max_length=254, unique=True)), 23 | ('email_verified', models.BooleanField()), 24 | ], 25 | ), 26 | migrations.CreateModel( 27 | name='StandaloneUserModel', 28 | fields=[ 29 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 30 | ('username', models.CharField(max_length=30, unique=True)), 31 | ], 32 | ), 33 | migrations.CreateModel( 34 | name='TestUser', 35 | fields=[ 36 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 37 | ('password', models.CharField(max_length=128, verbose_name='password')), 38 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 39 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 40 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 41 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 42 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 43 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 44 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 45 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 46 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 47 | ('age', models.CharField(blank=True, max_length=100)), 48 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 49 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 50 | ], 51 | options={ 52 | 'verbose_name': 'user', 53 | 'verbose_name_plural': 'users', 54 | 'abstract': False, 55 | }, 56 | managers=[ 57 | ('objects', django.contrib.auth.models.UserManager()), 58 | ], 59 | ), 60 | ] 61 | -------------------------------------------------------------------------------- /tests/testprofiles/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IdentityPython/djangosaml2/ceb48c28805ae79cb54c74c57b337365373e4171/tests/testprofiles/migrations/__init__.py -------------------------------------------------------------------------------- /tests/testprofiles/models.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es) 2 | # Copyright (C) 2010 Lorenzo Gil Sanchez 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from django.db import models 17 | 18 | from django.contrib.auth.models import AbstractUser 19 | 20 | 21 | class TestUser(AbstractUser): 22 | age = models.CharField(max_length=100, blank=True) 23 | 24 | def process_first_name(self, first_name): 25 | self.first_name = first_name[0] 26 | 27 | class Meta: 28 | app_label = "testprofiles" 29 | 30 | 31 | class StandaloneUserModel(models.Model): 32 | """ 33 | Does not inherit from Django's base abstract user and does not define a 34 | USERNAME_FIELD. 35 | """ 36 | 37 | username = models.CharField(max_length=30, unique=True) 38 | 39 | 40 | class RequiredFieldUser(models.Model): 41 | email = models.EmailField(unique=True) 42 | email_verified = models.BooleanField() 43 | 44 | USERNAME_FIELD = "email" 45 | 46 | def __repr__(self): 47 | return self.email 48 | 49 | def set_unusable_password(self): 50 | pass 51 | -------------------------------------------------------------------------------- /tests/testprofiles/tests.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Sam Bull (lsb@pocketuniverse.ca) 2 | # Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es) 3 | # Copyright (C) 2010 Lorenzo Gil Sanchez 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from django.conf import settings 18 | from django.core.exceptions import ImproperlyConfigured 19 | from django.test import Client, TestCase, override_settings 20 | from django.urls import reverse 21 | 22 | from django.contrib.auth import get_user_model 23 | from django.contrib.auth.models import User as DjangoUserModel 24 | 25 | from djangosaml2.backends import Saml2Backend, get_saml_user_model, set_attribute 26 | from djangosaml2.utils import get_csp_handler 27 | from testprofiles.models import TestUser 28 | 29 | 30 | class BackendUtilMethodsTests(TestCase): 31 | def test_set_attribute(self): 32 | u = TestUser() 33 | self.assertFalse(hasattr(u, "custom_attribute")) 34 | 35 | # Set attribute initially 36 | changed = set_attribute(u, "custom_attribute", "value") 37 | self.assertTrue(changed) 38 | self.assertEqual(u.custom_attribute, "value") 39 | 40 | # 'Update' to the same value again 41 | changed_same = set_attribute(u, "custom_attribute", "value") 42 | self.assertFalse(changed_same) 43 | self.assertEqual(u.custom_attribute, "value") 44 | 45 | # Update to a different value 46 | changed_different = set_attribute(u, "custom_attribute", "new_value") 47 | self.assertTrue(changed_different) 48 | self.assertEqual(u.custom_attribute, "new_value") 49 | 50 | 51 | class dummyNameId: 52 | text = "dummyNameId" 53 | 54 | 55 | class Saml2BackendTests(TestCase): 56 | """UnitTests on backend classes""" 57 | 58 | backend_cls = Saml2Backend 59 | 60 | def setUp(self): 61 | self.backend = self.backend_cls() 62 | self.user = TestUser.objects.create(username="john") 63 | 64 | def test_get_model_ok(self): 65 | self.assertEqual(self.backend._user_model, TestUser) 66 | 67 | def test_get_model_nonexisting(self): 68 | with override_settings(SAML_USER_MODEL="testprofiles.NonExisting"): 69 | with self.assertRaisesMessage( 70 | ImproperlyConfigured, 71 | "Model 'testprofiles.NonExisting' could not be loaded", 72 | ): 73 | self.assertEqual(self.backend._user_model, None) 74 | 75 | def test_get_model_invalid_specifier(self): 76 | with override_settings( 77 | SAML_USER_MODEL="random_package.specifier.testprofiles.NonExisting" 78 | ): 79 | with self.assertRaisesMessage( 80 | ImproperlyConfigured, 81 | "Model was specified as 'random_package.specifier.testprofiles.NonExisting', but it must be of the form 'app_label.model_name'", 82 | ): 83 | self.assertEqual(self.backend._user_model, None) 84 | 85 | def test_user_model_specified(self): 86 | with override_settings(AUTH_USER_MODEL="auth.User"): 87 | with override_settings(SAML_USER_MODEL="testprofiles.TestUser"): 88 | self.assertEqual(self.backend._user_model, TestUser) 89 | 90 | def test_user_model_default(self): 91 | with override_settings(AUTH_USER_MODEL="auth.User"): 92 | self.assertEqual(self.backend._user_model, DjangoUserModel) 93 | 94 | def test_user_lookup_attribute_specified(self): 95 | with override_settings(SAML_USER_MODEL="testprofiles.TestUser"): 96 | with override_settings(SAML_DJANGO_USER_MAIN_ATTRIBUTE="age"): 97 | self.assertEqual(self.backend._user_lookup_attribute, "age") 98 | 99 | def test_user_lookup_attribute_default(self): 100 | with override_settings(SAML_USER_MODEL="testprofiles.TestUser"): 101 | self.assertEqual(self.backend._user_lookup_attribute, "username") 102 | 103 | def test_extract_user_identifier_params_use_nameid_present(self): 104 | with override_settings(SAML_USER_MODEL="testprofiles.TestUser"): 105 | with override_settings(SAML_USE_NAME_ID_AS_USERNAME=True): 106 | _, lookup_value = self.backend._extract_user_identifier_params( 107 | {"name_id": dummyNameId()}, {}, {} 108 | ) 109 | self.assertEqual(lookup_value, "dummyNameId") 110 | 111 | def test_extract_user_identifier_params_use_nameid_missing(self): 112 | with override_settings(SAML_USER_MODEL="testprofiles.TestUser"): 113 | with override_settings(SAML_USE_NAME_ID_AS_USERNAME=True): 114 | _, lookup_value = self.backend._extract_user_identifier_params( 115 | {}, {}, {} 116 | ) 117 | self.assertEqual(lookup_value, None) 118 | 119 | def test_is_authorized(self): 120 | self.assertTrue(self.backend.is_authorized({}, {}, "", {})) 121 | 122 | def test_clean_attributes(self): 123 | attributes = {"random": "dummy", "value": 123} 124 | self.assertEqual(self.backend.clean_attributes(attributes, ""), attributes) 125 | 126 | def test_clean_user_main_attribute(self): 127 | self.assertEqual(self.backend.clean_user_main_attribute("value"), "value") 128 | 129 | def test_update_user_simple(self): 130 | u = TestUser(username="johny") 131 | self.assertIsNone(u.pk) 132 | u = self.backend._update_user(u, {}, {}) 133 | self.assertIsNotNone(u.pk) 134 | 135 | def test_update_user(self): 136 | attribute_mapping = { 137 | "uid": ("username",), 138 | "mail": ("email",), 139 | "cn": ("first_name",), 140 | "sn": ("last_name",), 141 | } 142 | attributes = { 143 | "uid": ("john",), 144 | "mail": ("john@example.com",), 145 | "cn": ("John",), 146 | "sn": ("Doe",), 147 | } 148 | self.backend._update_user(self.user, attributes, attribute_mapping) 149 | self.assertEqual(self.user.email, "john@example.com") 150 | self.assertEqual(self.user.first_name, "John") 151 | self.assertEqual(self.user.last_name, "Doe") 152 | 153 | attribute_mapping["saml_age"] = ("age",) 154 | attributes["saml_age"] = ("22",) 155 | self.backend._update_user(self.user, attributes, attribute_mapping) 156 | self.assertEqual(self.user.age, "22") 157 | 158 | def test_update_user_callable_attributes(self): 159 | attribute_mapping = { 160 | "uid": ("username",), 161 | "mail": ("email",), 162 | "cn": ("process_first_name",), 163 | "sn": ("last_name",), 164 | } 165 | attributes = { 166 | "uid": ("john",), 167 | "mail": ("john@example.com",), 168 | "cn": ("John",), 169 | "sn": ("Doe",), 170 | } 171 | self.backend._update_user(self.user, attributes, attribute_mapping) 172 | self.assertEqual(self.user.email, "john@example.com") 173 | self.assertEqual(self.user.first_name, "John") 174 | self.assertEqual(self.user.last_name, "Doe") 175 | 176 | def test_update_user_empty_attribute(self): 177 | self.user.last_name = "Smith" 178 | self.user.save() 179 | 180 | attribute_mapping = { 181 | "uid": ("username",), 182 | "mail": ("email",), 183 | "cn": ("first_name",), 184 | "sn": ("last_name",), 185 | } 186 | attributes = { 187 | "uid": ("john",), 188 | "mail": ("john@example.com",), 189 | "cn": ("John",), 190 | "sn": (), 191 | } 192 | with self.assertLogs("djangosaml2", level="DEBUG") as logs: 193 | self.backend._update_user(self.user, attributes, attribute_mapping) 194 | self.assertEqual(self.user.email, "john@example.com") 195 | self.assertEqual(self.user.first_name, "John") 196 | # empty attribute list: no update 197 | self.assertEqual(self.user.last_name, "Smith") 198 | self.assertIn( 199 | 'DEBUG:djangosaml2:Could not find value for "sn", not updating fields "(\'last_name\',)"', 200 | logs.output, 201 | ) 202 | 203 | def test_invalid_model_attribute_log(self): 204 | attribute_mapping = { 205 | "uid": ["username"], 206 | "cn": ["nonexistent"], 207 | } 208 | attributes = { 209 | "uid": ["john"], 210 | "cn": ["John"], 211 | } 212 | 213 | with self.assertLogs("djangosaml2", level="DEBUG") as logs: 214 | user, _ = self.backend.get_or_create_user( 215 | self.backend._user_lookup_attribute, 216 | "john", 217 | True, 218 | None, 219 | None, 220 | None, 221 | None, 222 | ) 223 | self.backend._update_user(user, attributes, attribute_mapping) 224 | 225 | self.assertIn( 226 | 'DEBUG:djangosaml2:Could not find attribute "nonexistent" on user "john"', 227 | logs.output, 228 | ) 229 | 230 | @override_settings(SAML_USER_MODEL="testprofiles.RequiredFieldUser") 231 | def test_create_user_with_required_fields(self): 232 | attribute_mapping = {"mail": ["email"], "mail_verified": ["email_verified"]} 233 | attributes = { 234 | "mail": ["john@example.org"], 235 | "mail_verified": [True], 236 | } 237 | # User creation does not fail if several fields are required. 238 | user, created = self.backend.get_or_create_user( 239 | self.backend._user_lookup_attribute, 240 | "john@example.org", 241 | True, 242 | None, 243 | None, 244 | None, 245 | None, 246 | ) 247 | 248 | self.assertEqual(user.email, "john@example.org") 249 | self.assertIs(user.email_verified, None) 250 | 251 | user = self.backend._update_user(user, attributes, attribute_mapping, created) 252 | self.assertIs(user.email_verified, True) 253 | 254 | def test_django_user_main_attribute(self): 255 | old_username_field = get_user_model().USERNAME_FIELD 256 | get_user_model().USERNAME_FIELD = "slug" 257 | self.assertEqual(self.backend._user_lookup_attribute, "slug") 258 | get_user_model().USERNAME_FIELD = old_username_field 259 | 260 | with override_settings(AUTH_USER_MODEL="auth.User"): 261 | self.assertEqual( 262 | DjangoUserModel.USERNAME_FIELD, self.backend._user_lookup_attribute 263 | ) 264 | 265 | with override_settings(AUTH_USER_MODEL="testprofiles.StandaloneUserModel"): 266 | self.assertEqual(self.backend._user_lookup_attribute, "username") 267 | 268 | with override_settings(SAML_DJANGO_USER_MAIN_ATTRIBUTE="foo"): 269 | self.assertEqual(self.backend._user_lookup_attribute, "foo") 270 | 271 | def test_get_or_create_user_existing(self): 272 | with override_settings(SAML_USER_MODEL="testprofiles.TestUser"): 273 | user, created = self.backend.get_or_create_user( 274 | self.backend._user_lookup_attribute, 275 | "john", 276 | False, 277 | None, 278 | None, 279 | None, 280 | None, 281 | ) 282 | 283 | self.assertTrue(isinstance(user, TestUser)) 284 | self.assertFalse(created) 285 | 286 | def test_get_or_create_user_duplicates(self): 287 | TestUser.objects.create(username="paul") 288 | 289 | with self.assertLogs("djangosaml2", level="DEBUG") as logs: 290 | with override_settings(SAML_USER_MODEL="testprofiles.TestUser"): 291 | user, created = self.backend.get_or_create_user( 292 | "age", "", False, None, None, None, None 293 | ) 294 | 295 | self.assertTrue(user is None) 296 | self.assertFalse(created) 297 | self.assertIn( 298 | "ERROR:djangosaml2:Multiple users match, model: testprofiles.testuser, lookup: {'age': ''}", 299 | logs.output[0], 300 | ) 301 | 302 | def test_get_or_create_user_no_create(self): 303 | with self.assertLogs("djangosaml2", level="DEBUG") as logs: 304 | with override_settings(SAML_USER_MODEL="testprofiles.TestUser"): 305 | user, created = self.backend.get_or_create_user( 306 | self.backend._user_lookup_attribute, 307 | "paul", 308 | False, 309 | None, 310 | None, 311 | None, 312 | None, 313 | ) 314 | 315 | self.assertTrue(user is None) 316 | self.assertFalse(created) 317 | self.assertIn( 318 | "ERROR:djangosaml2:The user does not exist, model: testprofiles.testuser, lookup: {'username': 'paul'}", 319 | logs.output[0], 320 | ) 321 | 322 | def test_get_or_create_user_create(self): 323 | with self.assertLogs("djangosaml2", level="DEBUG") as logs: 324 | with override_settings(SAML_USER_MODEL="testprofiles.TestUser"): 325 | user, created = self.backend.get_or_create_user( 326 | self.backend._user_lookup_attribute, 327 | "paul", 328 | True, 329 | None, 330 | None, 331 | None, 332 | None, 333 | ) 334 | 335 | self.assertTrue(isinstance(user, TestUser)) 336 | self.assertTrue(created) 337 | self.assertIn( 338 | f"DEBUG:djangosaml2:New user created: {user}", 339 | logs.output[0], 340 | ) 341 | 342 | def test_deprecations(self): 343 | attribute_mapping = {"mail": ["email"], "mail_verified": ["email_verified"]} 344 | attributes = { 345 | "mail": ["john@example.org"], 346 | "mail_verified": [True], 347 | } 348 | 349 | old = self.backend.get_attribute_value( 350 | "email_verified", attributes, attribute_mapping 351 | ) 352 | self.assertEqual(old, True) 353 | 354 | self.assertEqual( 355 | self.backend.get_django_user_main_attribute(), 356 | self.backend._user_lookup_attribute, 357 | ) 358 | 359 | with override_settings(SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP="user_name"): 360 | self.assertEqual( 361 | self.backend.get_django_user_main_attribute_lookup(), 362 | settings.SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP, 363 | ) 364 | 365 | self.assertEqual(self.backend.get_user_query_args(""), {"username"}) 366 | 367 | u = TestUser(username="mathieu") 368 | self.assertEqual(u.email, "") 369 | new_u = self.backend.configure_user(u, attributes, attribute_mapping) 370 | self.assertIsNotNone(new_u.pk) 371 | self.assertEqual(new_u.email, "john@example.org") 372 | 373 | u = TestUser(username="mathieu_2") 374 | self.assertEqual(u.email, "") 375 | new_u = self.backend.update_user(u, attributes, attribute_mapping) 376 | self.assertIsNotNone(new_u.pk) 377 | self.assertEqual(new_u.email, "john@example.org") 378 | 379 | u = TestUser() 380 | self.assertTrue(self.backend._set_attribute(u, "new_attribute", True)) 381 | self.assertFalse(self.backend._set_attribute(u, "new_attribute", True)) 382 | self.assertTrue(self.backend._set_attribute(u, "new_attribute", False)) 383 | 384 | self.assertEqual(get_saml_user_model(), TestUser) 385 | 386 | 387 | class CustomizedBackend(Saml2Backend): 388 | """Override the available methods with some customized implementation to test customization""" 389 | 390 | def is_authorized( 391 | self, attributes, attribute_mapping, idp_entityid: str, assertion_info, **kwargs 392 | ): 393 | """Allow only staff users from the IDP""" 394 | return ( 395 | attributes.get("is_staff", (None,))[0] == True 396 | and assertion_info.get("assertion_id", None) != None 397 | ) 398 | 399 | def clean_attributes(self, attributes: dict, idp_entityid: str, **kwargs) -> dict: 400 | """Keep only certain attribute""" 401 | return { 402 | "age": attributes.get("age", (None,)), 403 | "mail": attributes.get("mail", (None,)), 404 | "is_staff": attributes.get("is_staff", (None,)), 405 | "uid": attributes.get("uid", (None,)), 406 | } 407 | 408 | def clean_user_main_attribute(self, main_attribute): 409 | """Partition string on @ and return the first part""" 410 | if main_attribute: 411 | return main_attribute.partition("@")[0] 412 | return main_attribute 413 | 414 | 415 | class CustomizedSaml2BackendTests(Saml2BackendTests): 416 | backend_cls = CustomizedBackend 417 | 418 | def test_is_authorized(self): 419 | attribute_mapping = { 420 | "uid": ("username",), 421 | "mail": ("email",), 422 | "cn": ("first_name",), 423 | "sn": ("last_name",), 424 | } 425 | attributes = { 426 | "uid": ("john",), 427 | "mail": ("john@example.com",), 428 | "cn": ("John",), 429 | "sn": ("Doe",), 430 | } 431 | assertion_info = { 432 | "assertion_id": None, 433 | "not_on_or_after": None, 434 | } 435 | self.assertFalse( 436 | self.backend.is_authorized( 437 | attributes, attribute_mapping, "", assertion_info 438 | ) 439 | ) 440 | attributes["is_staff"] = (True,) 441 | self.assertFalse( 442 | self.backend.is_authorized( 443 | attributes, attribute_mapping, "", assertion_info 444 | ) 445 | ) 446 | assertion_info["assertion_id"] = "abcdefg12345" 447 | self.assertTrue( 448 | self.backend.is_authorized( 449 | attributes, attribute_mapping, "", assertion_info 450 | ) 451 | ) 452 | 453 | def test_clean_attributes(self): 454 | attributes = {"random": "dummy", "value": 123, "age": "28"} 455 | self.assertEqual( 456 | self.backend.clean_attributes(attributes, ""), 457 | {"age": "28", "mail": (None,), "is_staff": (None,), "uid": (None,)}, 458 | ) 459 | 460 | def test_clean_user_main_attribute(self): 461 | self.assertEqual( 462 | self.backend.clean_user_main_attribute("john@example.com"), "john" 463 | ) 464 | 465 | def test_authenticate(self): 466 | attribute_mapping = { 467 | "uid": ("username",), 468 | "mail": ("email",), 469 | "cn": ("first_name",), 470 | "sn": ("last_name",), 471 | "age": ("age",), 472 | "is_staff": ("is_staff",), 473 | } 474 | attributes = { 475 | "uid": ("john",), 476 | "mail": ("john@example.com",), 477 | "cn": ("John",), 478 | "sn": ("Doe",), 479 | "age": ("28",), 480 | "is_staff": (True,), 481 | } 482 | assertion_info = { 483 | "assertion_id": "abcdefg12345", 484 | "not_on_or_after": "", 485 | } 486 | 487 | self.assertEqual(self.user.age, "") 488 | self.assertEqual(self.user.is_staff, False) 489 | 490 | user = self.backend.authenticate(None) 491 | self.assertIsNone(user) 492 | 493 | user = self.backend.authenticate( 494 | None, 495 | session_info={"random": "content"}, 496 | attribute_mapping=attribute_mapping, 497 | assertion_info=assertion_info, 498 | ) 499 | self.assertIsNone(user) 500 | 501 | with override_settings(SAML_USE_NAME_ID_AS_USERNAME=True): 502 | user = self.backend.authenticate( 503 | None, 504 | session_info={"ava": attributes, "issuer": "dummy_entity_id"}, 505 | attribute_mapping=attribute_mapping, 506 | assertion_info=assertion_info, 507 | ) 508 | self.assertIsNone(user) 509 | 510 | attributes["is_staff"] = (False,) 511 | user = self.backend.authenticate( 512 | None, 513 | session_info={"ava": attributes, "issuer": "dummy_entity_id"}, 514 | attribute_mapping=attribute_mapping, 515 | assertion_info=assertion_info, 516 | ) 517 | self.assertIsNone(user) 518 | 519 | attributes["is_staff"] = (True,) 520 | user = self.backend.authenticate( 521 | None, 522 | session_info={"ava": attributes, "issuer": "dummy_entity_id"}, 523 | attribute_mapping=attribute_mapping, 524 | assertion_info=assertion_info, 525 | ) 526 | 527 | self.assertEqual(user, self.user) 528 | 529 | self.user.refresh_from_db() 530 | self.assertEqual(self.user.age, "28") 531 | self.assertEqual(self.user.is_staff, True) 532 | 533 | def test_user_cleaned_main_attribute(self): 534 | """ 535 | In this test the username is taken from the `mail` attribute, 536 | but cleaned to remove the @domain part. After fetching and 537 | updating the user, the username remains the same. 538 | """ 539 | attribute_mapping = { 540 | "mail": ("username",), 541 | "cn": ("first_name",), 542 | "sn": ("last_name",), 543 | "is_staff": ("is_staff",), 544 | } 545 | attributes = { 546 | "mail": ("john@example.com",), 547 | "cn": ("John",), 548 | "sn": ("Doe",), 549 | "is_staff": (True,), 550 | } 551 | assertion_info = { 552 | "assertion_id": "abcdefg12345", 553 | } 554 | user = self.backend.authenticate( 555 | None, 556 | session_info={"ava": attributes, "issuer": "dummy_entity_id"}, 557 | attribute_mapping=attribute_mapping, 558 | assertion_info=assertion_info, 559 | ) 560 | self.assertEqual(user, self.user) 561 | 562 | self.user.refresh_from_db() 563 | self.assertEqual(user.username, "john") 564 | 565 | 566 | class CSPHandlerTests(TestCase): 567 | def test_get_csp_handler_none(self): 568 | get_csp_handler.cache_clear() 569 | with override_settings(SAML_CSP_HANDLER=None): 570 | csp_handler = get_csp_handler() 571 | self.assertIn( 572 | csp_handler.__module__, ["csp.decorators", "djangosaml2.utils"] 573 | ) 574 | self.assertIn(csp_handler.__name__, ["decorator", "empty_view_decorator"]) 575 | 576 | def test_get_csp_handler_empty(self): 577 | get_csp_handler.cache_clear() 578 | with override_settings(SAML_CSP_HANDLER=""): 579 | csp_handler = get_csp_handler() 580 | self.assertEqual(csp_handler.__name__, "empty_view_decorator") 581 | 582 | def test_get_csp_handler_specified(self): 583 | get_csp_handler.cache_clear() 584 | with override_settings(SAML_CSP_HANDLER="testprofiles.utils.csp_handler"): 585 | client = Client() 586 | response = client.get(reverse("saml2_login")) 587 | self.assertIn("Content-Security-Policy", response.headers) 588 | self.assertEqual( 589 | response.headers["Content-Security-Policy"], "testing CSP value" 590 | ) 591 | 592 | def test_get_csp_handler_specified_missing(self): 593 | get_csp_handler.cache_clear() 594 | with override_settings(SAML_CSP_HANDLER="does.not.exist"): 595 | with self.assertRaises(ImportError): 596 | get_csp_handler() 597 | -------------------------------------------------------------------------------- /tests/testprofiles/urls.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.urls import include, path 3 | 4 | from django.contrib import admin 5 | 6 | testpatterns = ( 7 | [path("dashboard/", lambda request: HttpResponse(""), name="dashboard")], 8 | "testprofiles", # app_name 9 | ) 10 | 11 | urlpatterns = [ 12 | path("saml2/", include("djangosaml2.urls")), 13 | path("admin/", admin.site.urls), 14 | path("", include(testpatterns)), 15 | ] 16 | -------------------------------------------------------------------------------- /tests/testprofiles/utils.py: -------------------------------------------------------------------------------- 1 | def csp_handler(response): 2 | response.headers["Content-Security-Policy"] = "testing CSP value" 3 | return response 4 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{3.9,3.10,3.11,3.12,3.13}-django{4.2,5.0,5.1} 4 | 5 | [testenv] 6 | commands = 7 | pip list 8 | python tests/run_tests.py 9 | 10 | deps = 11 | django4.2: django~=4.2 12 | django5.0: django~=5.0 13 | django5.1: django~=5.1 14 | djangomaster: https://github.com/django/django/archive/master.tar.gz 15 | . 16 | 17 | ignore_outcome = 18 | djangomaster: True 19 | 20 | setenv = 21 | PYTHONWARNINGS=module::DeprecationWarning 22 | --------------------------------------------------------------------------------