├── .gitattributes
├── setup.cfg
├── saml
├── schema
│ ├── meta.py
│ ├── utils.py
│ ├── __init__.py
│ ├── types.py
│ ├── base.py
│ ├── saml.py
│ └── samlp.py
├── _version.py
├── __init__.py
├── client.py
└── signature.py
├── docs
├── creating_documents.rst
├── signing_documents.rst
├── index.rst
├── installation.rst
├── contributing.rst
├── conf.py
└── Makefile
├── .gitignore
├── .editorconfig
├── requirements.txt
├── tests
├── test_client.py
├── rsapub.pem
├── artifact-resolve-simple.xml
├── authentication-request-simple.xml
├── logout-response-simple.xml
├── logout-request-simple.xml
├── test_utils.py
├── artifact-response-simple.xml
├── artifact-resolve-signed.xml
├── assertion-simple.xml
├── logout-response-signed.xml
├── authentication-request-signed.xml
├── logout-request-signed.xml
├── rsakey.pem
├── artifact-response-signed.xml
├── assertion-signed.xml
├── response-simple.xml
├── response-signed.xml
├── rsacert.pem
└── test_schema.py
├── .travis.yml
├── LICENSE
├── setup.py
└── README.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [pytest]
2 | addopts = -rx -s
3 |
--------------------------------------------------------------------------------
/saml/schema/meta.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | version = '2.0'
3 |
--------------------------------------------------------------------------------
/docs/creating_documents.rst:
--------------------------------------------------------------------------------
1 | Creating Documents
2 | ==================
3 |
4 | .. automodule:: saml.schema
5 |
--------------------------------------------------------------------------------
/docs/signing_documents.rst:
--------------------------------------------------------------------------------
1 | Signing Documents
2 | =================
3 |
4 | .. automodule:: saml.signature
5 |
--------------------------------------------------------------------------------
/saml/_version.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | __version_info__ = (0, 9, 0)
3 | __version__ = '.'.join(map(str, __version_info__))
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Dot files
2 | .*
3 | !.editorconfig
4 | !.gitattributes
5 | !.gitignore
6 | !.travis*
7 |
8 | # Python
9 | /dist
10 | /build
11 | *.pyc
12 | *.pyo
13 | *.egg-info
14 |
15 | # Sphinx
16 | /docs/_build
17 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 4
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | alabaster==0.7.2
2 | Babel==1.3
3 | docutils==0.12
4 | Jinja2==2.7.3
5 | lxml==3.4.2
6 | MarkupSafe==0.23
7 | Pygments==2.0.2
8 | python-dateutil==2.4.1
9 | pytz==2014.10
10 | six==1.9.0
11 | snowballstemmer==1.2.0
12 | Sphinx==1.3.1
13 | sphinx-rtd-theme==0.1.7
14 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Python-SAML Documentation
2 | =========================
3 |
4 | .. automodule:: saml
5 |
6 | Contents:
7 |
8 | .. toctree::
9 | :maxdepth: 2
10 |
11 | installation
12 | creating_documents
13 | signing_documents
14 | contributing
15 |
16 | Indices and tables
17 | ==================
18 |
19 | * :ref:`genindex`
20 | * :ref:`modindex`
21 | * :ref:`search`
22 |
23 |
--------------------------------------------------------------------------------
/tests/test_client.py:
--------------------------------------------------------------------------------
1 | from saml import client
2 | from test_schema import build_authentication_request_simple
3 | from six.moves.urllib.parse import quote_plus
4 |
5 |
6 | def test_relay_state():
7 | target = build_authentication_request_simple()
8 | state = 'http://localhost:8080/'
9 | uri, _ = client.send('http://localhost', target.serialize(), state)
10 |
11 | relay_state_part = 'RelayState=%s' % quote_plus(state)
12 | assert relay_state_part in uri
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - '2.7'
4 | - '3.3'
5 | - '3.4'
6 |
7 | before_install:
8 | - 'travis_retry sudo apt-get update'
9 | - 'travis_retry sudo apt-get install python-dev libxml2-dev libxmlsec1-dev'
10 | - 'travis_retry pip install Cython --use-mirrors'
11 |
12 | install:
13 | - 'travis_retry pip install -e ".[test]"'
14 | - 'travis_retry pip install coveralls'
15 |
16 | script: 'py.test --pep8 --flakes --cov saml'
17 |
18 | after_success: 'coveralls'
19 |
--------------------------------------------------------------------------------
/tests/rsapub.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PUBLIC KEY-----
2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl7j+tD+DNXgWiQTsK2GM
3 | v8RfAIFKRebZzeniPJc7Ra2q5o0Ld3EHAU98+X3iGardkVn08c89unhGlhGctltG
4 | OXNVI6r3ngBc5elJ7DucP4SZOpCt335khsYmcs4xCHl+ExW45b/WVgKNYCFMJxhk
5 | +/tVcPYzvS9VcNVefpmupOCqRUcTqDDVoIqdzCDs5I5RyVTFfz5mLXS/o3r48+yU
6 | Vzm0rAB1YmFUtNDgUob4XnfsUEOc0rqnjGJavLL+88xifiNga8dRSTd4fiUVMKv6
7 | tK4ljyL8o0h/8gqKbuD+jfAB7cYzzGuh/aaA7waMr/ZAOo5CFCBhEh/j/AWxBdVl
8 | wwIDAQAB
9 | -----END PUBLIC KEY-----
10 |
--------------------------------------------------------------------------------
/tests/artifact-resolve-simple.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 | https://idp.example.org/SAML2
9 | AAQAAMh48/1oXIM+sDo7Dh2qMp1HM4IF5DaRNmDj6RdUmllwn9jJHyEgIi8=
10 |
11 |
--------------------------------------------------------------------------------
/tests/authentication-request-simple.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 | https://sp.example.com/SAML2
11 |
14 |
15 |
--------------------------------------------------------------------------------
/saml/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | A python interface to produce and consume Security Assertion Markup
4 | Language (SAML) v2.0 messages.
5 |
6 | See: https://www.oasis-open.org/standards#samlv2.0
7 | """
8 | # Version of the library.
9 | from ._version import __version__, __version_info__ # noqa
10 |
11 | # Version of the SAML standard supported.
12 | from .schema import VERSION as SAML_VERSION
13 |
14 | from .signature import sign, verify
15 | from . import client
16 |
17 | VERSION = __version__
18 |
19 | __all__ = [
20 | 'VERSION',
21 | 'SAML_VERSION',
22 | 'sign',
23 | 'verify',
24 | 'client'
25 | ]
26 |
--------------------------------------------------------------------------------
/saml/schema/utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import re
3 |
4 |
5 | def _upcase_first_letter(s):
6 | return s[0].upper() + s[1:]
7 |
8 |
9 | def pascalize(name):
10 | name = _upcase_first_letter(name.strip())
11 | pattern = r'[-_\s]+(.)?'
12 | name = re.sub(pattern, lambda m: m.groups()[0].upper() if m else '', name)
13 | return name
14 |
15 |
16 | class classproperty(object):
17 | """Declares a read-only `property` that acts on the class object.
18 | """
19 |
20 | def __init__(self, getter):
21 | self.getter = getter
22 |
23 | def __get__(self, obj, cls):
24 | return self.getter(cls)
25 |
--------------------------------------------------------------------------------
/tests/logout-response-simple.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 | https://idp.example.org/SAML2
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/logout-request-simple.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 | myhost
10 | myemail@mydomain.com
12 | _0628125f-7f95-42cc-ad8e-fde86ae90bbe
13 |
14 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | from saml.schema import utils
2 |
3 |
4 | class TestPascalize:
5 |
6 | def test_normal(self):
7 | text = 'something'
8 |
9 | assert utils.pascalize(text) == 'Something'
10 |
11 | def test_underscore(self):
12 | text = 'some_thing'
13 |
14 | assert utils.pascalize(text) == 'SomeThing'
15 |
16 | def test_already(self):
17 | text = 'someThing'
18 |
19 | assert utils.pascalize(text) == 'SomeThing'
20 |
21 | def test_dash(self):
22 | text = 'some-thing'
23 |
24 | assert utils.pascalize(text) == 'SomeThing'
25 |
26 | def test_null(self):
27 | text = 'SomeThing'
28 |
29 | assert utils.pascalize(text) == 'SomeThing'
30 |
31 | def test_pascal(self):
32 | text = 'Some_thing'
33 |
34 | assert utils.pascalize(text) == 'SomeThing'
35 |
--------------------------------------------------------------------------------
/saml/schema/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Create XML documents in accordance with the SAML 2.0 specification
5 |
6 | AuthnRequest
7 | ------------
8 | .. autoclass:: saml.schema.AuthenticationRequest
9 |
10 | Response
11 | --------
12 | .. autoclass:: saml.schema.Response
13 |
14 | LogoutRequest
15 | -------------
16 | .. autoclass:: saml.schema.LogoutRequest
17 |
18 | LogoutResponse
19 | --------------
20 | .. autoclass:: saml.schema.LogoutResponse
21 | """
22 |
23 | from .meta import version as VERSION # noqa
24 | from .saml import * # noqa
25 | from .samlp import * # noqa
26 |
27 |
28 | def deserialize(xml):
29 | from .base import _element_registry
30 |
31 | # Resolve the xml into an element.
32 | element = _element_registry.get(xml.tag)
33 | if not element:
34 | return None
35 |
36 | # Deserialize the xml and return.
37 | return element.deserialize(xml)
38 |
--------------------------------------------------------------------------------
/docs/installation.rst:
--------------------------------------------------------------------------------
1 | Installation
2 | ============
3 |
4 | Supported platforms
5 | -------------------
6 | - Python 2.7
7 | - Python 3.3
8 | - Python 3.4
9 |
10 | Dependencies
11 | ------------
12 |
13 | In order to sign and verify signatures, `libxml2` and `libxmlsec` are required.
14 |
15 | Linux
16 | ::
17 | apt-get install libxml2-dev libxmlsec1-dev
18 |
19 | Mac
20 | ::
21 | brew install libxml2 libxmlsec1
22 |
23 |
24 | Installing an official release
25 | ------------------------------
26 |
27 | The most recent release is available from PyPI
28 | ::
29 | pip install saml
30 |
31 | Installing the development version
32 | ----------------------------------
33 |
34 | 1. Clone the **python-saml** repository
35 | ::
36 | git clone git://github.com/mehcode/python-saml.git
37 |
38 | 2. Change into the project directory
39 | ::
40 | cd python-saml
41 |
42 | 3. Install the project and all its dependencies using `pip`
43 | ::
44 | pip install .
45 |
--------------------------------------------------------------------------------
/docs/contributing.rst:
--------------------------------------------------------------------------------
1 | Contributing
2 | ============
3 |
4 | Setting up your environment
5 | ---------------------------
6 |
7 | 1. Fork the repository
8 | 2. Clone your fork
9 | 3. `Create a virtual environment `_.
10 | 4. Install **python-saml** in development mode with testing enabled. This will download all dependencies required for running the unit tests.
11 | ::
12 | pip install -e ".[test]"
13 | 5. Make changes with tests and documentation
14 | 6. Open a pull request
15 |
16 | Running the tests
17 | -----------------
18 |
19 | Tests are run with `py.test`.
20 | ::
21 | py.test --pep8 --flakes --cov saml
22 |
23 | Testing documentation changes
24 | -----------------------------
25 |
26 | Documentation is handled with `Sphinx `_. Use the `make html` command in the `docs` directory to build an HTML preview of the documentation.
27 | ::
28 | cd docs
29 | make html
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Ryan Leckey
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is furnished
10 | to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tests/artifact-response-simple.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
20 | https://sp.example.com/SAML2
21 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/tests/artifact-resolve-signed.xml:
--------------------------------------------------------------------------------
1 | https://idp.example.org/SAML2
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | r6jFOa6bsPAjD3IZGidgANuw3p4=
11 |
12 |
13 | HOKsFmgbRWXnj9oB9qNxA7hBMm0edNV3MBldAnkVONrla9ybOvqO1BPFc/1CRcYV
14 | vaX+BrNhDi6A1pFJ/+Wo0HdijqyhM6zRlc3PHslloB5vyxIExrA/4ujzMfDJxqe8
15 | 1P54fqijWRkwT26cjqHjrgvhJir9YmjkDxitVOcnHR8ggupT8gluygVlHHPiRfJI
16 | TWz1Kc0/9kghuneXbxXZbeK9DvGi+pfpf3Tp3ES7TORwT+MF/GRbXecZuuqurD6V
17 | ZT1hJySA5C4surumKCGbFf6yT9ySAM8fuWOSXAp+16mF/p9/bx+xNq4jwdMloXuv
18 | 2z2PumKL/zpR4mNaw3f19A==
19 | AAQAAMh48/1oXIM+sDo7Dh2qMp1HM4IF5DaRNmDj6RdUmllwn9jJHyEgIi8=
20 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import sys
4 | import os
5 |
6 | import sphinx_rtd_theme
7 |
8 | sys.path.insert(0, os.path.abspath('../'))
9 |
10 | from saml import __version__ # noqa
11 |
12 | extensions = [
13 | 'sphinx.ext.autodoc',
14 | 'sphinx.ext.coverage',
15 | 'sphinx.ext.ifconfig',
16 | 'sphinx.ext.viewcode',
17 | ]
18 |
19 | templates_path = ['_templates']
20 | source_suffix = ['.rst']
21 | master_doc = 'index'
22 |
23 | project = u'python-saml'
24 | copyright = u'2015, Ryan Leckey'
25 | author = u'Ryan Leckey'
26 |
27 | version = __version__
28 | release = version
29 |
30 | exclude_patterns = ['_build']
31 | pygments_style = 'sphinx'
32 | todo_include_todos = False
33 | html_theme = "sphinx_rtd_theme"
34 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
35 | html_static_path = ['_static']
36 | htmlhelp_basename = 'python-samldoc'
37 | latex_elements = {}
38 |
39 | latex_documents = [
40 | (master_doc, 'python-saml.tex', u'python-saml Documentation',
41 | u'Ryan Leckey', 'manual'),
42 | ]
43 |
44 | man_pages = [
45 | (master_doc, 'python-saml', u'python-saml Documentation',
46 | [author], 1)
47 | ]
48 |
49 | texinfo_documents = [
50 | (master_doc, 'python-saml', u'python-saml Documentation',
51 | author, 'python-saml', 'One line description of project.',
52 | 'Miscellaneous'),
53 | ]
54 |
--------------------------------------------------------------------------------
/tests/assertion-simple.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://idp.example.org/SAML2
4 |
5 | 3f7b3dcf-1674-4ecd-92c8-1544f346baf8
6 |
7 |
8 |
9 |
10 |
11 |
12 | https://sp.example.com/SAML2
13 |
14 |
15 |
16 |
17 | urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/tests/logout-response-signed.xml:
--------------------------------------------------------------------------------
1 | https://idp.example.org/SAML2
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | 7rz6jS2X2irRen/0qthQdjuPetw=
11 |
12 |
13 | Ra7hDm4vCa655p2TXP5LRXcNfvh0przg1c2pQHFS+b4P8uu5cplzF87dMzxQy6hc
14 | ZJKZTusn1icpczzRmjHh4KupXZJ4Qh4XABXdy9bdIh3e8crdkxq55jRtt9kBQNDb
15 | fOqH4GJpdoRMPQ2MzHU3lZg8D/zWhrO6Nat5MXT0EFfHiaNftXEpzTxdR9sk6lTA
16 | 9A2BfsX0/F1CULwByayAkkfgGpk8pPbPBv5+GFTSNenJJoXRhnLlsaV5EjKNc7JA
17 | lhPa2InB4pvBICNbB0lMeI/ORY0AyxmdZ+VhMSnLHe0mRyonSGrBLycSsotqBxQU
18 | qfNhum9m5fE1Ah3MN15Hnw==
19 |
20 |
--------------------------------------------------------------------------------
/tests/authentication-request-signed.xml:
--------------------------------------------------------------------------------
1 | https://sp.example.com/SAML2
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | CxgHIaLL1Ce4iAMD0ry6i++BFP0=
11 |
12 |
13 | IKi1NOMYzrz6muSboAS+0K3gw8waPT1QqL0BYXTcmOHmbgKFRgp29q1+a6tBmNhN
14 | o0inSa9v8CT5omdwCi+3yN1/gXT1sXBq9BJD1I6bhURprTMdRqhsWXiYC7HOtx+b
15 | ISVq+0OfbEhH0WKnwRo+Wc0JdiTGYty1Vu34FRyg+9rrTCjKk40Brl0FDMExSxmM
16 | PPi3RHkEmL2X9g5M2fLjJg4UOjx+K3RyNOvOObo4GfZJf+9B5M/TxB48mojbwj9R
17 | 8kVKDso0YMrNsZOo9c2ptu1gpHxvNS3Y6c1c43X0883s6cGNJSuICoZk/vHMwtjz
18 | EP9ntIMnZ26905JYwsrbAQ==
19 |
20 |
--------------------------------------------------------------------------------
/tests/logout-request-signed.xml:
--------------------------------------------------------------------------------
1 | myhost
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | cdDEc7e5AgOZIOTAVr5uq/KCneY=
11 |
12 |
13 | d2qTJAJk8bLpq7dpRy4Ne8RLQwlb8g1Wy71r4EiyDBOri9Kh6AHv3mDrMLwCiFx7
14 | jcoYZtLWhQRglA5Icl8Z3WnPZDR/7EyRyVLyEnMFwLF2ZuXCBWlAhIJsJX+BBOd8
15 | 5IKXOMw3QLFshGWwj/npCYpHckDTU9uvOi0Q3Flg4lgXNUFZ94iUD0W0uqxFZmm5
16 | oMJQALvIoi0fsFrXxQ2RTFUFP6TdW4h0KkVHZWSjzN14KGJxpCoh0zxngQ0O7EfV
17 | YIHgBCOPv+Z5IFTOUsdkR4sruOT/fw0FX4cAlFtjt7FSw8e0t/TuzucOU/lTIeqF
18 | yk7Yxka80ncjHmeJlPq7jg==
19 | myemail@mydomain.com_0628125f-7f95-42cc-ad8e-fde86ae90bbe
20 |
--------------------------------------------------------------------------------
/saml/schema/types.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from dateutil.parser import parse as parse_datetime
3 |
4 |
5 | class Base(object):
6 |
7 | def prepare(self, value):
8 | return str(value) if value is not None else None
9 |
10 | def clean(self, text):
11 | if text is not None:
12 | return text
13 |
14 |
15 | class String(Base):
16 | """String values [saml-core § 1.3.1].
17 | """
18 |
19 |
20 | class Integer(Base):
21 | """Integral values.
22 | """
23 |
24 |
25 | class Boolean(Base):
26 | """Boolean values.
27 | """
28 |
29 | def prepare(self, value):
30 | if value is None:
31 | return None
32 |
33 | return 'true' if value else 'false'
34 |
35 | def clean(self, text):
36 | if not text:
37 | return None
38 |
39 | return text == 'true'
40 |
41 |
42 | class DateTime(Base):
43 | """
44 | An ISO 8601 formatted, UTC (eg. 2008-01-23T04:56:22Z)
45 | date-time [saml-core § 1.3.3].
46 | """
47 |
48 | @staticmethod
49 | def to_iso8601(when):
50 | text = when.strftime("%Y-%m-%dT%H:%M:%SZ")
51 | return text
52 |
53 | @staticmethod
54 | def from_iso8601(when):
55 | return parse_datetime(when)
56 |
57 | def prepare(self, value):
58 | return DateTime.to_iso8601(value) if value is not None else None
59 |
60 | def clean(self, text):
61 | if text is not None:
62 | return DateTime.from_iso8601(text)
63 |
--------------------------------------------------------------------------------
/tests/rsakey.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEowIBAAKCAQEAl7j+tD+DNXgWiQTsK2GMv8RfAIFKRebZzeniPJc7Ra2q5o0L
3 | d3EHAU98+X3iGardkVn08c89unhGlhGctltGOXNVI6r3ngBc5elJ7DucP4SZOpCt
4 | 335khsYmcs4xCHl+ExW45b/WVgKNYCFMJxhk+/tVcPYzvS9VcNVefpmupOCqRUcT
5 | qDDVoIqdzCDs5I5RyVTFfz5mLXS/o3r48+yUVzm0rAB1YmFUtNDgUob4XnfsUEOc
6 | 0rqnjGJavLL+88xifiNga8dRSTd4fiUVMKv6tK4ljyL8o0h/8gqKbuD+jfAB7cYz
7 | zGuh/aaA7waMr/ZAOo5CFCBhEh/j/AWxBdVlwwIDAQABAoIBAQCAvt6DnZF9gdW9
8 | l4vAlBqXb88d4phgELCp5tmviLUnP2NSGEWuqR7Eoeru2z9NgIxblvYfazh6Ty22
9 | kmNk6rcAcTnB9oYAcVZjUj8EUuEXlTFhXPvuNpafNu3RZd59znqJP1mSu+LpQWku
10 | NZMlabHnkTLDlGf7FXtvL9/rlgV4qk3QcDVF793JFszWrtK3mnld3KHQ6cuo9iSm
11 | 0rQKtkDjeHsRell8qTQvfBsgG1q2bv8QWT45/eQrra9mMbGTr3DbnXvoeJmTj1VN
12 | XJV7tBNllxxPahlYMByJaf/Tuva5j6HWUEIfYky5ihr2z1P/fNQ2OSCM6SQHpkiG
13 | EXQDueXBAoGBAMfW7KcmToEQEcTiqfey6C1LOLoemcX0/ROUktPq/5JQJRRrT4t7
14 | XevLX0ed8sLyR5T29XQtdnuV0DJfvcJD+6ZwfOcQ+f6ZzCaNXJP97JtEt5kSWY01
15 | Ei+nphZ0RFvPb04V3qDU9dElU26GR36CRBYJyM2WQPx4v+/YyDSZH9kLAoGBAMJc
16 | ZBU8pRbIia/FFOHUlS3v5P18nVmXyOd0fvRq0ZelaQCebTZ4K9wjnCfw//yzkb2Z
17 | 0vZFNB+xVBKB0Pt6nVvnSNzxdQ8EAXVFwHtXa25FUyP2RERQgTvmajqmgWjZsDYp
18 | 6GHcK3ZhmdmscQHF/Q2Uo4scvBcheahm9IXiNskpAoGAXelEgTBhSAmTMCEMmti6
19 | fz6QQ/bJcNu2apMxhOE0hT+gjT34vaWV9481EWTKho5w0TJVGumaem1mz6VqeXaV
20 | Nhw6tiOmN91ysNNRpEJ6BGWAmjCjYNaF21s/k+HDlhmfRuTEIHSzqDuQP6pewrbY
21 | 5Dpo4SQxGfRsznvjacRj0Q0CgYBN247oBvQnDUxCkhNMZ8kersOvW5T4x9neBge5
22 | R3UQZ12Jtu0O7dK8C7PJODyDcTeHmTAuIQjBTVrdUw1xP+v7XcoNX9hBnJws6zUw
23 | 85MAiFrGxCcSqqEqaqHRPtQGOXXiLKV/ViA++tgTn4VhbXtyTkG5P1iFd45xjFSV
24 | sUm7CQKBgDn92tHxzePly1L1mK584TkVryx4cP9RFHpebnmNduGwwjnRuYipoj8y
25 | pPPAkVbbaA3f9OB2go48rN0Ft9nHdlqgh9BpIKCVtkIb1XN0K3Oa/8BW8W/GAiNG
26 | HJcsrOtIrGVRdlyJG6bDaN8T49DnhOcsqMbf+IkIvfh50VeE9L/e
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/tests/artifact-response-signed.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | xnNFU/3XnZYzBCpat60kGSKZRp0=
11 |
12 |
13 | iu3hlDNpHFUJplrfBzUIh6tzRaW4v2iDdB4x05K1iGTW2Ik0WA9ShfOGVSzGVPVb
14 | +jej8Nxw/awnszPxTYMbQbjftDp4s5Wdb4vPSGH9C7IByg7oix5MRsri7Hu4rzs9
15 | D7lucJH+4czID2m1c7c4EBq32qTTAqdXsl0NOWJUxp7TofBwwGSZ5sAYQMiXQObB
16 | Mm97gvOwKQ9tgHRMwLOIZNjECteKeGVlUg6y0wQlfykC6XyaM5rsZQo/N7KytN5v
17 | XA6ybMeIJxzWZ13BAAJ4kV9BgLTvKrI3nXtYSSVqf/Ft3ciZSWFf2sflQ77R0iFv
18 | lUEyNfTEi6j2HtaU12pEuQ==
19 | https://sp.example.com/SAML2
20 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | from setuptools import setup, find_packages
4 | from imp import load_source
5 |
6 | setup(
7 | name='saml',
8 | version=load_source('', 'saml/_version.py').__version__,
9 | description='A python interface to produce and consume Security '
10 | 'Assertion Markup Language (SAML) v2.0 messages.',
11 | classifiers=[
12 | 'Development Status :: 4 - Beta',
13 | 'Intended Audience :: Developers',
14 | 'Intended Audience :: System Administrators',
15 | 'License :: OSI Approved :: MIT License',
16 | 'Operating System :: OS Independent',
17 | 'Programming Language :: Python',
18 | 'Programming Language :: Python :: 2.7',
19 | 'Programming Language :: Python :: 3.3',
20 | 'Programming Language :: Python :: 3.4'
21 | ],
22 | author='Concordus Applications',
23 | author_email='leckey.ryan@gmail.com',
24 | url='http://github.com/mehcode/python-saml',
25 | packages=find_packages('.'),
26 | install_requires=(
27 | # Extensions to the standard Python datetime module.
28 | # Provides ability to easily parse ISO 8601 formatted dates.
29 | 'python-dateutil',
30 |
31 | # lxml is the most feature-rich and easy-to-use library for
32 | # processing XML and HTML in the Python language.
33 | 'lxml',
34 |
35 | # Python bindings for the XML Security Library.
36 | 'xmlsec'
37 | ),
38 | extras_require={
39 | 'test': (
40 | # Test runner.
41 | 'pytest',
42 |
43 | # Ensure PEP8 conformance.
44 | 'pytest-pep8',
45 |
46 | # Ensure PyFlakes conformance.
47 | 'pytest-flakes',
48 |
49 | # Ensure test coverage.
50 | 'pytest-cov',
51 | )
52 | }
53 | )
54 |
--------------------------------------------------------------------------------
/tests/assertion-signed.xml:
--------------------------------------------------------------------------------
1 | https://idp.example.org/SAML2
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | 0KtoALnAv524NZukvr5j3XM7y/k=
11 |
12 |
13 | eEVBMicN0s4pck1nv1/YJwT0yQ2GhLDt3nQktBL2vgMgeGp9OMB8odRIerNnWMKA
14 | IeDh7j7Blp7h8NIJO62VbbNHEyL+DmIuVgbigVDdcwnXtN/rci+1inveCztDV09J
15 | /zD6jOEFPS+bxysNT1T5eK9/F38FfOjEjtdE7ZYLoh+JnIXuFWc5bOgp9mbsRDu/
16 | X72LpTgwascA68yinyvNU5i+vLckskzujOfAQo2H5mC952qk9+d0z9IIjtz2d48G
17 | aUXdiUQPE8lNDlM+IqvyOt8wcGjvMi38bFEXJLfGBfk7fUdee6zcEAAdPW0Rav/m
18 | YFY2LQrnJrJXNPjBI3h2/g==
19 | 3f7b3dcf-1674-4ecd-92c8-1544f346baf8https://sp.example.com/SAML2urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
20 |
--------------------------------------------------------------------------------
/tests/response-simple.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 | https://idp.example.org/SAML2
11 |
12 |
13 |
14 |
19 | https://idp.example.org/SAML2
20 |
21 | 3f7b3dcf-1674-4ecd-92c8-1544f346baf8
22 |
24 |
28 |
29 |
30 |
33 |
34 | https://sp.example.com/SAML2
35 |
36 |
37 |
40 |
41 | urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/tests/response-signed.xml:
--------------------------------------------------------------------------------
1 | https://idp.example.org/SAML2
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | qv9GT+D06Nzr8uymGd60HG1ObUM=
11 |
12 |
13 | fNf3jKJwYHgYTsqZpBBGlEq9Hny1uSQyLsJEWp1cGmfidKnf6jJn9PwB4F6F5F/9
14 | UZ6DjmGRAYivVXU1JmUInsHpqyDllOYeKTfksf0RbQ6RNv6AqDDt7ZGOKeKqP3A+
15 | k0onIqhP8BBcFCRE8/QZPv9RxMqtEtWcQX603khHobGg/JJVniVJBWluwBs6xNOP
16 | xr7ZaP1idmLq+1yiGPhdIcjDyhGujP4Nmmw9jV8iml1YlXT/t+VNRF0y9J+rT8EK
17 | BiOORNDmTLmyUCcf9HnIT11UIPVH/6SDJ4jYvCyzveS9Dp7B9e1R2Dr97kppx3hn
18 | Ag422cWv09Vl9q+YlmoptA==
19 | https://idp.example.org/SAML23f7b3dcf-1674-4ecd-92c8-1544f346baf8https://sp.example.com/SAML2urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
20 |
--------------------------------------------------------------------------------
/saml/client.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import base64
3 | import zlib
4 | import six
5 | from six.moves.urllib.parse import urlencode, parse_qs, unquote_plus
6 | from lxml import etree
7 | from saml import schema
8 | from saml.schema.base import _element_registry
9 |
10 |
11 | def send(uri, message, relay_state=None, protocol='redirect'):
12 |
13 | # Determine the name of parameter.
14 | element = _element_registry.get(message.tag)
15 | if not element:
16 | raise ValueError('unknown element', message)
17 |
18 | if protocol == 'redirect':
19 | # For sending a message through redirection; we need
20 | # to encode the message (depending on what it is) in the URI
21 | # as SAMLRequest or SAMLResponse.
22 | name = 'SAMLRequest'
23 | if isinstance(element, schema.StatusResponse):
24 | name = 'SAMLResponse'
25 |
26 | # Serialize and encode the message.
27 | text = base64.b64encode(zlib.compress(etree.tostring(message))[2:-4])
28 |
29 | # Build the parameters.
30 | parameters = {name: text}
31 | if relay_state:
32 | parameters['RelayState'] = relay_state
33 |
34 | # Append the parameters on the uri and return.
35 | uri = '%s?%s' % (uri, urlencode(parameters))
36 | return uri, None
37 |
38 | raise ValueError('unknown protocol', protocol)
39 |
40 |
41 | def _text(str_or_bytes):
42 | if isinstance(str_or_bytes, six.text_type):
43 | return str_or_bytes
44 |
45 | return str_or_bytes.decode()
46 |
47 |
48 | def receive(method, query_string, body):
49 | # Determine the protocol used and pare the appropriate data.
50 | method = method.upper()
51 | if method == 'GET':
52 | data = parse_qs(_text(query_string))
53 | binding = 'artifact' if 'SAMLArtifact' in data else 'redirect'
54 |
55 | elif method == 'POST':
56 | data = parse_qs(_text(body))
57 | binding = 'post'
58 |
59 | else:
60 | # Unknown method used.
61 | return None
62 |
63 | if binding in ('redirect', 'post'):
64 | # Pull the text out of the query.
65 | encoded = data.get('SAMLResponse', data.get('SAMLRequest'))
66 | if not encoded:
67 | # No SAML message found.
68 | return None
69 |
70 | # Decode the text.
71 | text = base64.b64decode(encoded[0])
72 | if binding == "redirect":
73 | text = zlib.decompress(text, -15)
74 |
75 | # Parse the text into xml.
76 | message = etree.XML(text)
77 |
78 | # Get the relay state if present.
79 | relay_state = data.get('RelayState')
80 | if relay_state:
81 | relay_state = unquote_plus(relay_state[0])
82 |
83 | # Return the message and the relay state.
84 | return message, relay_state
85 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # python-saml
2 | [](https://travis-ci.org/mehcode/python-saml)
3 | [](https://coveralls.io/r/mehcode/python-saml?branch=master)
4 | [](https://pypi.python.org/pypi/saml)
5 | 
6 | > A python interface to produce and consume Security Asserion Markup Language v2.0 (SAML2) messages.
7 |
8 | ## Features
9 |
10 | ##### Python 2.7.x, 3.3.x, 3.4.x support
11 |
12 | python-saml supports both python 2.7.x+ and 3.3.x+.
13 |
14 | ##### SAML conformance
15 |
16 | python-saml conforms to the latest [SAML][] (v2.0) standards.
17 |
18 | [SAML]: https://www.oasis-open.org/standards#samlv2.0
19 |
20 | ##### Environment agnostic
21 |
22 | python-saml may be used to produce and consume SAML messages regardless of the environment (terminal, WSGI, django) used to call it.
23 |
24 | ## Usage
25 |
26 | ###
27 |
28 | Check the [test suite](https://github.com/mehcode/python-saml/blob/master/tests/saml/test_schema.py#L33) for additional examples on using the library.
29 |
30 | ## Install
31 |
32 | ### Pre-Install
33 |
34 | #### Linux
35 |
36 | ```sh
37 | apt-get install libxml2-dev libxmlsec1-dev
38 | ```
39 |
40 | #### Mac
41 |
42 | ```sh
43 | brew install libxml2 libxmlsec1
44 | ```
45 |
46 |
47 | ### Automated
48 |
49 | 1. **saml** can be installed through `easy_install` or `pip`.
50 |
51 | ```sh
52 | pip install saml
53 | ```
54 |
55 | ### Manual
56 |
57 | 1. Clone the **saml** repository to your local computer.
58 |
59 | ```sh
60 | git clone git://github.com/mehcode/python-saml.git
61 | ```
62 |
63 | 2. Change into the **saml** root directory.
64 |
65 | ```sh
66 | cd /path/to/saml
67 | ```
68 |
69 | 3. Install the project and all its dependencies using `pip`.
70 |
71 | ```sh
72 | pip install .
73 | ```
74 |
75 | ## Contributing
76 |
77 | ### Setting up your environment
78 |
79 | 1. Follow steps 1 and 2 of the [manual installation instructions][].
80 |
81 | [manual installation instructions]: #manual
82 |
83 | 2. Initialize a virtual environment to develop in.
84 | This is done so as to ensure every contributor is working with
85 | close-to-identicial versions of packages.
86 |
87 | ```sh
88 | mkvirtualenv saml
89 | ```
90 |
91 | The `mkvirtualenv` command is available from `virtualenvwrapper` which
92 | can be installed by following: http://virtualenvwrapper.readthedocs.org/en/latest/install.html#basic-installation
93 |
94 | 3. Install **saml** in development mode with testing enabled.
95 | This will download all dependencies required for running the unit tests.
96 |
97 | ```sh
98 | pip install -e ".[test]"
99 | ```
100 |
101 | ### Running the test suite
102 |
103 | 1. [Set up your environment](#setting-up-your-environment).
104 |
105 | 2. Run the unit tests.
106 |
107 | ```sh
108 | py.test
109 | ```
110 |
111 | ## License
112 | Unless otherwise noted, all files contained within this project are liensed under the MIT opensource license. See the included file LICENSE or visit [opensource.org][] for more information.
113 |
114 | [opensource.org]: http://opensource.org/licenses/MIT
115 |
--------------------------------------------------------------------------------
/tests/rsacert.pem:
--------------------------------------------------------------------------------
1 | Certificate:
2 | Data:
3 | Version: 3 (0x2)
4 | Serial Number: 5 (0x5)
5 | Signature Algorithm: md5WithRSAEncryption
6 | Issuer: C=US, ST=California, L=Sunnyvale, O=XML Security Library (http://www.aleksey.com/xmlsec), OU=Root Certificate, CN=Aleksey Sanin/emailAddress=xmlsec@aleksey.com
7 | Validity
8 | Not Before: Mar 31 04:02:22 2003 GMT
9 | Not After : Mar 28 04:02:22 2013 GMT
10 | Subject: C=US, ST=California, O=XML Security Library (http://www.aleksey.com/xmlsec), OU=Examples RSA Certificate, CN=Aleksey Sanin/emailAddress=xmlsec@aleksey.com
11 | Subject Public Key Info:
12 | Public Key Algorithm: rsaEncryption
13 | RSA Public Key: (2048 bit)
14 | Modulus (2048 bit):
15 | 00:97:b8:fe:b4:3f:83:35:78:16:89:04:ec:2b:61:
16 | 8c:bf:c4:5f:00:81:4a:45:e6:d9:cd:e9:e2:3c:97:
17 | 3b:45:ad:aa:e6:8d:0b:77:71:07:01:4f:7c:f9:7d:
18 | e2:19:aa:dd:91:59:f4:f1:cf:3d:ba:78:46:96:11:
19 | 9c:b6:5b:46:39:73:55:23:aa:f7:9e:00:5c:e5:e9:
20 | 49:ec:3b:9c:3f:84:99:3a:90:ad:df:7e:64:86:c6:
21 | 26:72:ce:31:08:79:7e:13:15:b8:e5:bf:d6:56:02:
22 | 8d:60:21:4c:27:18:64:fb:fb:55:70:f6:33:bd:2f:
23 | 55:70:d5:5e:7e:99:ae:a4:e0:aa:45:47:13:a8:30:
24 | d5:a0:8a:9d:cc:20:ec:e4:8e:51:c9:54:c5:7f:3e:
25 | 66:2d:74:bf:a3:7a:f8:f3:ec:94:57:39:b4:ac:00:
26 | 75:62:61:54:b4:d0:e0:52:86:f8:5e:77:ec:50:43:
27 | 9c:d2:ba:a7:8c:62:5a:bc:b2:fe:f3:cc:62:7e:23:
28 | 60:6b:c7:51:49:37:78:7e:25:15:30:ab:fa:b4:ae:
29 | 25:8f:22:fc:a3:48:7f:f2:0a:8a:6e:e0:fe:8d:f0:
30 | 01:ed:c6:33:cc:6b:a1:fd:a6:80:ef:06:8c:af:f6:
31 | 40:3a:8e:42:14:20:61:12:1f:e3:fc:05:b1:05:d5:
32 | 65:c3
33 | Exponent: 65537 (0x10001)
34 | X509v3 extensions:
35 | X509v3 Basic Constraints:
36 | CA:FALSE
37 | Netscape Comment:
38 | OpenSSL Generated Certificate
39 | X509v3 Subject Key Identifier:
40 | 24:84:2C:F2:D4:59:20:62:8B:2E:5C:86:90:A3:AA:30:BA:27:1A:9C
41 | X509v3 Authority Key Identifier:
42 | keyid:B4:B9:EF:9A:E6:97:0E:68:65:1E:98:CE:FA:55:0D:89:06:DB:4C:7C
43 | DirName:/C=US/ST=California/L=Sunnyvale/O=XML Security Library (http://www.aleksey.com/xmlsec)/OU=Root Certificate/CN=Aleksey Sanin/emailAddress=xmlsec@aleksey.com
44 | serial:00
45 |
46 | Signature Algorithm: md5WithRSAEncryption
47 | b5:3f:9b:32:31:4a:ff:2f:84:3b:a8:9b:11:5c:a6:5c:f0:76:
48 | 52:d9:6e:f4:90:ad:fa:0d:90:c1:98:d5:4a:12:dd:82:6b:37:
49 | e8:d9:2d:62:92:c9:61:37:98:86:8f:a4:49:6a:5e:25:d0:18:
50 | 69:30:0f:98:8f:43:58:89:31:b2:3b:05:e2:ef:c7:a6:71:5f:
51 | f7:fe:73:c5:a7:b2:cd:2e:73:53:71:7d:a8:4c:68:1a:32:1b:
52 | 5e:48:2f:8f:9b:7a:a3:b5:f3:67:e8:b1:a2:89:4e:b2:4d:1b:
53 | 79:9c:ff:f0:0d:19:4f:4e:b1:03:3d:99:f0:44:b7:8a:0b:34:
54 | 9d:83
55 | -----BEGIN CERTIFICATE-----
56 | MIIE3zCCBEigAwIBAgIBBTANBgkqhkiG9w0BAQQFADCByzELMAkGA1UEBhMCVVMx
57 | EzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVN1bm55dmFsZTE9MDsGA1UE
58 | ChM0WE1MIFNlY3VyaXR5IExpYnJhcnkgKGh0dHA6Ly93d3cuYWxla3NleS5jb20v
59 | eG1sc2VjKTEZMBcGA1UECxMQUm9vdCBDZXJ0aWZpY2F0ZTEWMBQGA1UEAxMNQWxl
60 | a3NleSBTYW5pbjEhMB8GCSqGSIb3DQEJARYSeG1sc2VjQGFsZWtzZXkuY29tMB4X
61 | DTAzMDMzMTA0MDIyMloXDTEzMDMyODA0MDIyMlowgb8xCzAJBgNVBAYTAlVTMRMw
62 | EQYDVQQIEwpDYWxpZm9ybmlhMT0wOwYDVQQKEzRYTUwgU2VjdXJpdHkgTGlicmFy
63 | eSAoaHR0cDovL3d3dy5hbGVrc2V5LmNvbS94bWxzZWMpMSEwHwYDVQQLExhFeGFt
64 | cGxlcyBSU0EgQ2VydGlmaWNhdGUxFjAUBgNVBAMTDUFsZWtzZXkgU2FuaW4xITAf
65 | BgkqhkiG9w0BCQEWEnhtbHNlY0BhbGVrc2V5LmNvbTCCASIwDQYJKoZIhvcNAQEB
66 | BQADggEPADCCAQoCggEBAJe4/rQ/gzV4FokE7CthjL/EXwCBSkXm2c3p4jyXO0Wt
67 | quaNC3dxBwFPfPl94hmq3ZFZ9PHPPbp4RpYRnLZbRjlzVSOq954AXOXpSew7nD+E
68 | mTqQrd9+ZIbGJnLOMQh5fhMVuOW/1lYCjWAhTCcYZPv7VXD2M70vVXDVXn6ZrqTg
69 | qkVHE6gw1aCKncwg7OSOUclUxX8+Zi10v6N6+PPslFc5tKwAdWJhVLTQ4FKG+F53
70 | 7FBDnNK6p4xiWryy/vPMYn4jYGvHUUk3eH4lFTCr+rSuJY8i/KNIf/IKim7g/o3w
71 | Ae3GM8xrof2mgO8GjK/2QDqOQhQgYRIf4/wFsQXVZcMCAwEAAaOCAVcwggFTMAkG
72 | A1UdEwQCMAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRp
73 | ZmljYXRlMB0GA1UdDgQWBBQkhCzy1FkgYosuXIaQo6owuicanDCB+AYDVR0jBIHw
74 | MIHtgBS0ue+a5pcOaGUemM76VQ2JBttMfKGB0aSBzjCByzELMAkGA1UEBhMCVVMx
75 | EzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVN1bm55dmFsZTE9MDsGA1UE
76 | ChM0WE1MIFNlY3VyaXR5IExpYnJhcnkgKGh0dHA6Ly93d3cuYWxla3NleS5jb20v
77 | eG1sc2VjKTEZMBcGA1UECxMQUm9vdCBDZXJ0aWZpY2F0ZTEWMBQGA1UEAxMNQWxl
78 | a3NleSBTYW5pbjEhMB8GCSqGSIb3DQEJARYSeG1sc2VjQGFsZWtzZXkuY29tggEA
79 | MA0GCSqGSIb3DQEBBAUAA4GBALU/mzIxSv8vhDuomxFcplzwdlLZbvSQrfoNkMGY
80 | 1UoS3YJrN+jZLWKSyWE3mIaPpElqXiXQGGkwD5iPQ1iJMbI7BeLvx6ZxX/f+c8Wn
81 | ss0uc1NxfahMaBoyG15IL4+beqO182fosaKJTrJNG3mc//ANGU9OsQM9mfBEt4oL
82 | NJ2D
83 | -----END CERTIFICATE-----
84 |
--------------------------------------------------------------------------------
/saml/signature.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Sign and verify signatures using the `python-xmlsec` library.
5 |
6 | .. autofunction:: sign
7 | .. autofunction:: verify
8 | """
9 |
10 |
11 | def sign(xml, stream, password=None):
12 | """
13 | Sign an XML document with the given private key file. This will add a
14 | element to the document.
15 |
16 | :param lxml.etree._Element xml: The document to sign
17 | :param file stream: The private key to sign the document with
18 | :param str password: The password used to access the private key
19 |
20 | :rtype: None
21 |
22 | Example usage:
23 | ::
24 | from saml import schema
25 | from lxml import etree
26 |
27 | document = schema.AuthenticationRequest()
28 | xml_document = document.serialize()
29 | with open('my_key_file.pem', 'r+') as stream:
30 | sign(xml_document, stream)
31 |
32 | print etree.tostring(xml_document)
33 |
34 | Produces the following XML document:
35 |
36 | .. code-block:: xml
37 |
38 |
43 |
44 |
45 |
47 |
50 |
51 |
52 |
55 |
56 |
59 |
60 | 94O1FOjRE4JQYVDqStkYzne9StQ=
61 |
62 |
63 |
64 |
65 | aFYRRjtB3bDyLLJzLZmsn0K4SXmOpFYJ+8R8D31VojgiF37FOElbE56UFbm8BAjn
66 | l2AixrUGXP4djxoxxnfBD/reYw5yVuIVXlMxKec784nF2V4GyrfwJOKaNmlVPkq5
67 | c8SI+EkKJ02mwiail0Zvjb9FzwvlYD+osMSXvJXVqnGHQDVFlhwbBRRVB6t44/M3
68 | TzC4mLSVhuvcpsm4GTQSpGkHP7HvweKN/OTc0aTy8Kh/YUrImwnUCii+J0EW4nGg
69 | 71eZyq/IiSPnTD09WDHsWe3g29kpicZXqrQCWeLE2zfVKtyxxs7PyEmodH19jXyz
70 | wh9hQ8t6PFO47Ros5aV0bw==
71 |
72 |
73 |
74 | """
75 |
76 | # Import xmlsec here to delay initializing the C library in
77 | # case we don't need it.
78 | import xmlsec
79 |
80 | # Resolve the SAML/2.0 element in question.
81 | from saml.schema.base import _element_registry
82 | element = _element_registry.get(xml.tag)
83 |
84 | # Create a signature template for RSA-SHA1 enveloped signature.
85 | signature_node = xmlsec.template.create(
86 | xml,
87 | xmlsec.Transform.EXCL_C14N,
88 | xmlsec.Transform.RSA_SHA1)
89 |
90 | # Add the node to the document.
91 | xml.insert(element.meta.signature_index, signature_node)
92 |
93 | # Add the node to the signature template.
94 | ref = xmlsec.template.add_reference(
95 | signature_node, xmlsec.Transform.SHA1)
96 |
97 | # Add the enveloped transform descriptor.
98 | xmlsec.template.add_transform(ref, xmlsec.Transform.ENVELOPED)
99 |
100 | # Create a digital signature context (no key manager is needed).
101 | ctx = xmlsec.SignatureContext()
102 |
103 | # Load private key.
104 | key = xmlsec.Key.from_memory(stream, xmlsec.KeyFormat.PEM, password)
105 |
106 | # Set the key on the context.
107 | ctx.key = key
108 |
109 | # Sign the template.
110 | ctx.sign(signature_node)
111 |
112 |
113 | def verify(xml, stream):
114 | """
115 | Verify the signaure of an XML document with the given certificate.
116 | Returns `True` if the document is signed with a valid signature.
117 | Returns `False` if the document is not signed or if the signature is
118 | invalid.
119 |
120 | :param lxml.etree._Element xml: The document to sign
121 | :param file stream: The private key to sign the document with
122 |
123 | :rtype: Boolean
124 | """
125 | # Import xmlsec here to delay initializing the C library in
126 | # case we don't need it.
127 | import xmlsec
128 |
129 | # Find the node.
130 | signature_node = xmlsec.tree.find_node(xml, xmlsec.Node.SIGNATURE)
131 | if signature_node is None:
132 | # No `signature` node found; we cannot verify
133 | return False
134 |
135 | # Create a digital signature context (no key manager is needed).
136 | ctx = xmlsec.SignatureContext()
137 |
138 | # Register and
139 | ctx.register_id(xml)
140 | for assertion in xml.xpath("//*[local-name()='Assertion']"):
141 | ctx.register_id(assertion)
142 |
143 | # Load the public key.
144 | key = None
145 | for fmt in [
146 | xmlsec.KeyFormat.PEM,
147 | xmlsec.KeyFormat.CERT_PEM]:
148 | stream.seek(0)
149 | try:
150 | key = xmlsec.Key.from_memory(stream, fmt)
151 | break
152 | except ValueError:
153 | # xmlsec now throws when it can't load the key
154 | pass
155 |
156 | # Set the key on the context.
157 | ctx.key = key
158 |
159 | # Verify the signature.
160 | try:
161 | ctx.verify(signature_node)
162 |
163 | return True
164 |
165 | except Exception:
166 | return False
167 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # User-friendly check for sphinx-build
11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
13 | endif
14 |
15 | # Internal variables.
16 | PAPEROPT_a4 = -D latex_paper_size=a4
17 | PAPEROPT_letter = -D latex_paper_size=letter
18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
19 | # the i18n builder cannot share the environment and doctrees with the others
20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
21 |
22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
23 |
24 | help:
25 | @echo "Please use \`make ' where is one of"
26 | @echo " html to make standalone HTML files"
27 | @echo " dirhtml to make HTML files named index.html in directories"
28 | @echo " singlehtml to make a single large HTML file"
29 | @echo " pickle to make pickle files"
30 | @echo " json to make JSON files"
31 | @echo " htmlhelp to make HTML files and a HTML help project"
32 | @echo " qthelp to make HTML files and a qthelp project"
33 | @echo " applehelp to make an Apple Help Book"
34 | @echo " devhelp to make HTML files and a Devhelp project"
35 | @echo " epub to make an epub"
36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
37 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
39 | @echo " text to make text files"
40 | @echo " man to make manual pages"
41 | @echo " texinfo to make Texinfo files"
42 | @echo " info to make Texinfo files and run them through makeinfo"
43 | @echo " gettext to make PO message catalogs"
44 | @echo " changes to make an overview of all changed/added/deprecated items"
45 | @echo " xml to make Docutils-native XML files"
46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
47 | @echo " linkcheck to check all external links for integrity"
48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
49 | @echo " coverage to run coverage check of the documentation (if enabled)"
50 |
51 | clean:
52 | rm -rf $(BUILDDIR)/*
53 |
54 | html:
55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
56 | @echo
57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
58 |
59 | dirhtml:
60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
61 | @echo
62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
63 |
64 | singlehtml:
65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
66 | @echo
67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
68 |
69 | pickle:
70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
71 | @echo
72 | @echo "Build finished; now you can process the pickle files."
73 |
74 | json:
75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
76 | @echo
77 | @echo "Build finished; now you can process the JSON files."
78 |
79 | htmlhelp:
80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
81 | @echo
82 | @echo "Build finished; now you can run HTML Help Workshop with the" \
83 | ".hhp project file in $(BUILDDIR)/htmlhelp."
84 |
85 | qthelp:
86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
87 | @echo
88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-saml.qhcp"
91 | @echo "To view the help file:"
92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-saml.qhc"
93 |
94 | applehelp:
95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
96 | @echo
97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
98 | @echo "N.B. You won't be able to view it unless you put it in" \
99 | "~/Library/Documentation/Help or install it in your application" \
100 | "bundle."
101 |
102 | devhelp:
103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
104 | @echo
105 | @echo "Build finished."
106 | @echo "To view the help file:"
107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/python-saml"
108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-saml"
109 | @echo "# devhelp"
110 |
111 | epub:
112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
113 | @echo
114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
115 |
116 | latex:
117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
118 | @echo
119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
121 | "(use \`make latexpdf' here to do that automatically)."
122 |
123 | latexpdf:
124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
125 | @echo "Running LaTeX files through pdflatex..."
126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
128 |
129 | latexpdfja:
130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
131 | @echo "Running LaTeX files through platex and dvipdfmx..."
132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
134 |
135 | text:
136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
137 | @echo
138 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
139 |
140 | man:
141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
142 | @echo
143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
144 |
145 | texinfo:
146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
147 | @echo
148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
149 | @echo "Run \`make' in that directory to run these through makeinfo" \
150 | "(use \`make info' here to do that automatically)."
151 |
152 | info:
153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
154 | @echo "Running Texinfo files through makeinfo..."
155 | make -C $(BUILDDIR)/texinfo info
156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
157 |
158 | gettext:
159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
160 | @echo
161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
162 |
163 | changes:
164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
165 | @echo
166 | @echo "The overview file is in $(BUILDDIR)/changes."
167 |
168 | linkcheck:
169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
170 | @echo
171 | @echo "Link check complete; look for any errors in the above output " \
172 | "or in $(BUILDDIR)/linkcheck/output.txt."
173 |
174 | doctest:
175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
176 | @echo "Testing of doctests in the sources finished, look at the " \
177 | "results in $(BUILDDIR)/doctest/output.txt."
178 |
179 | coverage:
180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
181 | @echo "Testing of coverage in the sources finished, look at the " \
182 | "results in $(BUILDDIR)/coverage/python.txt."
183 |
184 | xml:
185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
186 | @echo
187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
188 |
189 | pseudoxml:
190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
191 | @echo
192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
193 |
--------------------------------------------------------------------------------
/tests/test_schema.py:
--------------------------------------------------------------------------------
1 | import saml
2 | import xmlsec
3 | from saml import schema
4 | from saml.schema import utils
5 | from datetime import datetime
6 | from lxml import etree
7 | from os import path
8 | from pytest import mark
9 |
10 | BASE_DIR = path.abspath(path.dirname(__file__))
11 |
12 |
13 | def strip(text):
14 | if not text:
15 | return None
16 |
17 | text = text.replace('\n', '')
18 | text = text.strip()
19 |
20 | return text if text else None
21 |
22 |
23 | def assert_node(expected, result):
24 | assert expected.tag == result.tag
25 | assert expected.attrib == result.attrib
26 | assert strip(expected.text) == strip(result.text)
27 | assert strip(expected.tail) == strip(result.tail)
28 | assert len(expected) == len(result)
29 |
30 | for expected, result in zip(expected, result):
31 | assert_node(expected, result)
32 |
33 |
34 | def build_assertion_simple():
35 | # Create the assertion object.
36 | target = schema.Assertion()
37 | target.id = 'b07b804c-7c29-ea16-7300-4f3d6f7928ac'
38 | target.issue_instant = datetime(2004, 12, 5, 9, 22, 5)
39 | target.issuer = 'https://idp.example.org/SAML2'
40 |
41 | # Create a subject.
42 | target.subject = subject = schema.Subject()
43 | subject.principal = '3f7b3dcf-1674-4ecd-92c8-1544f346baf8'
44 | subject.principal.format = schema.NameID.Format.TRANSIENT
45 | subject.confirmation = confirmation = schema.SubjectConfirmation()
46 | confirmation.data = data = schema.SubjectConfirmationData()
47 | data.in_response_to = 'aaf23196-1773-2113-474a-fe114412ab72'
48 | data.not_on_or_after = datetime(2004, 12, 5, 9, 27, 5)
49 | data.recipient = 'https://sp.example.com/SAML2/SSO/POST'
50 | del data.recipient
51 | data.recipient = 'https://sp.example.com/SAML2/SSO/POST'
52 |
53 | assert data.recipient == 'https://sp.example.com/SAML2/SSO/POST'
54 |
55 | # Create an authentication statement.
56 | statement = schema.AuthenticationStatement()
57 | target.statements.append(statement)
58 | statement.authn_instant = datetime(2004, 12, 5, 9, 22, 0)
59 | statement.session_index = 'b07b804c-7c29-ea16-7300-4f3d6f7928ac'
60 | ref = schema.AuthenticationContextReference.PASSWORD_PROTECTED_TRANSPORT
61 | statement.context.reference = ref
62 |
63 | # Create a authentication condition.
64 | target.conditions = conditions = schema.Conditions()
65 | conditions.not_before = datetime(2004, 12, 5, 9, 17, 5)
66 | conditions.not_on_or_after = datetime(2004, 12, 5, 9, 27, 5)
67 | condition = schema.AudienceRestriction()
68 | condition.audiences = 'https://sp.example.com/SAML2'
69 | conditions.condition = condition
70 |
71 | # Return the built object.
72 | return target
73 |
74 |
75 | def build_authentication_request_simple():
76 | # Create the authentication request.
77 | target = schema.AuthenticationRequest()
78 | target.id = 'aaf23196-1773-2113-474a-fe114412ab72'
79 | target.issue_instant = datetime(2004, 12, 5, 9, 21, 59)
80 | target.assertion_consumer_service_index = 0
81 | target.attribute_consuming_service_index = 0
82 | target.issuer = 'https://sp.example.com/SAML2'
83 |
84 | # Add a name id policy to the request.
85 | target.policy = policy = schema.NameIDPolicy()
86 | policy.allow_create = True
87 | policy.format = schema.NameID.Format.TRANSIENT
88 |
89 | # Return the built object.
90 | return target
91 |
92 |
93 | def build_artifact_resolve_simple():
94 | # Create the artifact resolution request.
95 | target = schema.ArtifactResolve()
96 | target.id = '_cce4ee769ed970b501d680f697989d14'
97 | target.issue_instant = datetime(2004, 12, 5, 9, 21, 58)
98 | target.issuer = 'https://idp.example.org/SAML2'
99 | target.artifact = '''
100 | AAQAAMh48/1oXIM+sDo7Dh2qMp1HM4IF5DaRNmDj6RdUmllwn9jJHyEgIi8=
101 | '''.strip()
102 |
103 | # Return the built object.
104 | return target
105 |
106 |
107 | def build_artifact_response_simple():
108 | # Create the artifact response.
109 | target = schema.ArtifactResponse()
110 | target.id = '_d84a49e5958803dedcff4c984c2b0d95'
111 | target.in_response_to = '_cce4ee769ed970b501d680f697989d14'
112 | target.issue_instant = datetime(2004, 12, 5, 9, 21, 59)
113 | target.status.code.value = schema.StatusCode.SUCCESS
114 |
115 | # Create an authentication request to stuff inside of the artifact
116 | # response.
117 | target.message = message = schema.AuthenticationRequest()
118 | message.id = '_306f8ec5b618f361c70b6ffb1480eade'
119 | message.issue_instant = datetime(2004, 12, 5, 9, 21, 59)
120 | message.destination = 'https://idp.example.org/SAML2/SSO/Artifact'
121 | message.protocol = schema.Protocol.ARTIFACT
122 | message.issuer = 'https://sp.example.com/SAML2'
123 | message.assertion_consumer_service_url = (
124 | 'https://sp.example.com/SAML2/SSO/Artifact')
125 |
126 | # Add a name id policy to the authentication request.
127 | message.policy = policy = schema.NameIDPolicy()
128 | policy.allow_create = False
129 | policy.format = schema.NameID.Format.EMAIL
130 |
131 | # Return the built object.
132 | return target
133 |
134 |
135 | def build_response_simple():
136 | # Create the response.
137 | target = schema.Response()
138 | target.id = 'identifier_2'
139 | target.in_response_to = 'identifier_1'
140 | target.issue_instant = datetime(2004, 12, 5, 9, 22, 5)
141 | target.issuer = 'https://idp.example.org/SAML2'
142 | target.destination = 'https://sp.example.com/SAML2/SSO/POST'
143 | target.status.code.value = schema.StatusCode.SUCCESS
144 |
145 | # Create an assertion for the response.
146 | target.assertions = assertion = schema.Assertion()
147 | assertion.id = 'identifier_3'
148 | assertion.issue_instant = datetime(2004, 12, 5, 9, 22, 5)
149 | assertion.issuer = 'https://idp.example.org/SAML2'
150 |
151 | # Create a subject.
152 | assertion.subject = subject = schema.Subject()
153 | subject.principal = '3f7b3dcf-1674-4ecd-92c8-1544f346baf8'
154 | subject.principal.format = schema.NameID.Format.TRANSIENT
155 | subject.confirmation = confirmation = schema.SubjectConfirmation()
156 | confirmation.data = data = schema.SubjectConfirmationData()
157 | data.in_response_to = 'identifier_1'
158 | data.not_on_or_after = datetime(2004, 12, 5, 9, 27, 5)
159 | data.recipient = 'https://sp.example.com/SAML2/SSO/POST'
160 |
161 | # Create an authentication statement.
162 | statement = schema.AuthenticationStatement()
163 | assertion.statements.append(statement)
164 | statement.authn_instant = datetime(2004, 12, 5, 9, 22, 0)
165 | statement.session_index = 'identifier_3'
166 | ref = schema.AuthenticationContextReference.PASSWORD_PROTECTED_TRANSPORT
167 | statement.context.reference = ref
168 |
169 | # Create a authentication condition.
170 | assertion.conditions = conditions = schema.Conditions()
171 | conditions.not_before = datetime(2004, 12, 5, 9, 17, 5)
172 | conditions.not_on_or_after = datetime(2004, 12, 5, 9, 27, 5)
173 | condition = schema.AudienceRestriction()
174 | condition.audiences = 'https://sp.example.com/SAML2'
175 | conditions.condition = condition
176 |
177 | # Return the built object.
178 | return target
179 |
180 |
181 | def build_logout_request_simple():
182 | # Create the request.
183 | target = schema.LogoutRequest()
184 | target.id = 'identifier_1'
185 | target.issue_instant = datetime(2008, 6, 3, 12, 59, 57)
186 | target.issuer = 'myhost'
187 | target.destination = 'https://idphost/adfs/ls/'
188 | target.principal = 'myemail@mydomain.com'
189 | target.principal.format = schema.NameID.Format.EMAIL
190 | target.principal.name_qualifier = 'https://idphost/adfs/ls/'
191 | target.session_index = '_0628125f-7f95-42cc-ad8e-fde86ae90bbe'
192 |
193 | # Return the built object.
194 | return target
195 |
196 |
197 | def build_logout_response_simple():
198 | # Create the response.
199 | target = schema.LogoutResponse()
200 | target.id = 'identifier_2'
201 | target.in_response_to = 'identifier_1'
202 | target.issue_instant = datetime(2004, 12, 5, 9, 22, 5)
203 | target.issuer = 'https://idp.example.org/SAML2'
204 | target.destination = 'https://sp.example.com/SAML2/SLO/POST'
205 | target.status.code.value = schema.StatusCode.SUCCESS
206 |
207 | # Return the built object.
208 | return target
209 |
210 |
211 | NAMES = [
212 | 'assertion',
213 | 'authentication-request',
214 | 'response',
215 | 'logout-request',
216 | 'logout-response',
217 | 'artifact-resolve',
218 | 'artifact-response'
219 | ]
220 |
221 |
222 | @mark.parametrize('name', NAMES)
223 | def test_simple_serialize(name):
224 | # Load the expected result.
225 | filename = path.join(BASE_DIR, '%s-simple.xml' % name)
226 | parser = etree.XMLParser(
227 | ns_clean=True, remove_blank_text=True, remove_comments=True)
228 | expected = etree.parse(filename, parser).getroot()
229 |
230 | # Build the result.
231 | build_fn_name = ('build-%s-simple' % name).replace('-', '_')
232 | target = globals()[build_fn_name]()
233 |
234 | # Serialize the result into an XML object.
235 | result = target.serialize()
236 |
237 | # Resolve and compare the result against the expected.
238 | assert_node(expected, result)
239 |
240 |
241 | @mark.parametrize('name', NAMES)
242 | def test_simple_deserialize(name):
243 | # Load the result.
244 | filename = path.join(BASE_DIR, '%s-simple.xml' % name)
245 | parser = etree.XMLParser(
246 | ns_clean=True, remove_blank_text=True, remove_comments=True)
247 | target = etree.parse(filename, parser).getroot()
248 |
249 | # Build the expected result.
250 | build_fn_name = ('build-%s-simple' % name).replace('-', '_')
251 | expected = globals()[build_fn_name]().serialize()
252 |
253 | # Deserialize and subsequently serialize the target.
254 | cls_name = utils.pascalize(name)
255 | result = getattr(schema, cls_name).deserialize(target).serialize()
256 |
257 | # Compare the nodes.
258 | assert_node(expected, result)
259 |
260 |
261 | @mark.parametrize('name', NAMES)
262 | def test_signed_deserialize(name):
263 | # Load the result.
264 | filename = path.join(BASE_DIR, '%s-signed.xml' % name)
265 | parser = etree.XMLParser(
266 | ns_clean=True, remove_blank_text=True, remove_comments=True)
267 | target = etree.parse(filename, parser).getroot()
268 |
269 | # Build the expected result.
270 | build_fn_name = ('build-%s-simple' % name).replace('-', '_')
271 | expected = globals()[build_fn_name]().serialize()
272 |
273 | # Deserialize and subsequently serialize the target.
274 | cls_name = utils.pascalize(name)
275 | result = getattr(schema, cls_name).deserialize(target).serialize()
276 |
277 | # TODO: Remove the signature node.
278 |
279 | # Compare the nodes.
280 | assert_node(expected, result)
281 |
282 |
283 | @mark.parametrize('name', NAMES)
284 | def test_generic_deserialize(name):
285 | filename = path.join(BASE_DIR, '%s-simple.xml' % name)
286 | parser = etree.XMLParser(
287 | ns_clean=True, remove_blank_text=True, remove_comments=True)
288 | target = etree.parse(filename, parser).getroot()
289 |
290 | build_fn_name = ('build-%s-simple' % name).replace('-', '_')
291 | expected = globals()[build_fn_name]().serialize()
292 |
293 | result = schema.deserialize(target).serialize()
294 |
295 | assert_node(expected, result)
296 |
297 |
298 | def test_generic_deserialize_outside_registry():
299 | xml = build_authentication_request_simple().serialize()
300 | xml.tag = 'BadTagName'
301 | result = schema.deserialize(xml)
302 |
303 | assert result is None
304 |
305 |
306 | @mark.parametrize('name', NAMES)
307 | def test_sign(name):
308 | # Load the expected result.
309 | filename = path.join(BASE_DIR, '%s-signed.xml' % name)
310 | expected = etree.parse(filename).getroot()
311 |
312 | # Build the result.
313 | build_fn_name = ('build-%s-simple' % name).replace('-', '_')
314 | target = globals()[build_fn_name]()
315 |
316 | # Serialize the result into an XML object.
317 | # Round-trips the XML to remove weird spacing.
318 | result = target.serialize()
319 |
320 | with open(path.join(BASE_DIR, 'rsakey.pem'), 'r') as stream:
321 | # Sign the result.
322 | saml.sign(result, stream)
323 |
324 | # Compare the nodes.
325 | assert_node(expected, result)
326 |
327 |
328 | @mark.parametrize('name', NAMES)
329 | def test_verify(name):
330 | # Load the SAML XML document to verify.
331 | filename = path.join(BASE_DIR, '%s-signed.xml' % name)
332 | expected = etree.parse(filename).getroot()
333 |
334 | # Sign the result.
335 | with open(path.join(BASE_DIR, 'rsapub.pem'), 'r') as stream:
336 | assert saml.verify(expected, stream)
337 |
338 |
339 | @mark.parametrize('name', NAMES)
340 | def test_verify_with_bad_signature_returns_False(name):
341 | filename = path.join(BASE_DIR, '%s-signed.xml' % name)
342 | expected = etree.parse(filename).getroot()
343 |
344 | signature_node = xmlsec.tree.find_node(expected, xmlsec.Node.SIGNATURE)
345 | signature_node.clear()
346 |
347 | with open(path.join(BASE_DIR, 'rsapub.pem'), 'r') as stream:
348 | assert saml.verify(expected, stream) is False
349 |
350 |
351 | @mark.parametrize('name', NAMES)
352 | def test_verify_with_no_signature_returns_False(name):
353 | filename = path.join(BASE_DIR, '%s-simple.xml' % name)
354 | expected = etree.parse(filename).getroot()
355 |
356 | with open(path.join(BASE_DIR, 'rsapub.pem'), 'r') as stream:
357 | assert saml.verify(expected, stream) is False
358 |
--------------------------------------------------------------------------------
/saml/schema/base.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from collections import OrderedDict
3 | from lxml import etree
4 | import six
5 | from .utils import pascalize, classproperty
6 |
7 |
8 | class Options(object):
9 |
10 | def __init__(self, meta, name, data, bases):
11 | """
12 | Initializes the options object and defaults configuration not
13 | specified.
14 | """
15 |
16 | # Name of the element in its serialized form.
17 | self.name = meta.get('name')
18 | if self.name is None:
19 | # Generate a name if none is provided.
20 | self.name = pascalize(name)
21 |
22 | # The namespace of the element.
23 | self.namespace = meta.get('namespace')
24 |
25 | # Index into the elements of where we stick the signature block.
26 | self.signature_index = meta.get('signature_index', 1)
27 |
28 | # Element registry in order to lookup for deserialize.
29 | _element_registry = {}
30 |
31 |
32 | class Declarative(type):
33 |
34 | @classmethod
35 | def __prepare__(cls, name, bases):
36 | return OrderedDict()
37 |
38 | @classmethod
39 | def _gather_metadata(cls, metadata, bases):
40 | for base in bases:
41 | if isinstance(base, cls) and hasattr(base, 'Meta'):
42 | # Append metadata.
43 | metadata.append(getattr(base, 'Meta'))
44 |
45 | # Recurse.
46 | cls._gather_metadata(metadata, base.__bases__)
47 |
48 | @classmethod
49 | def _is_derived(cls, name, bases):
50 | for base in bases:
51 | if isinstance(base, cls):
52 | # This is some sort of derived resource; good.
53 | return True
54 |
55 | # This is not derived at all from Resource (eg. is base).
56 | return False
57 |
58 | @classmethod
59 | def _get_attributes_dict(cls, obj):
60 | return {n: getattr(obj, n) for n in dir(obj)}
61 |
62 | def __new__(cls, name, bases, attrs):
63 | # Only continue if we are dervied from declarative.
64 | if not cls._is_derived(name, bases):
65 | return super(Declarative, cls).__new__(
66 | cls, name, bases, attrs)
67 |
68 | # Gather the attributes of all options classes.
69 | # Start with the base configuration.
70 | metadata = {}
71 |
72 | # Expand the options class with the gathered metadata.
73 | base_meta = []
74 | cls._gather_metadata(base_meta, bases)
75 |
76 | # Apply the configuration from each class in the chain.
77 | for meta in base_meta:
78 | metadata.update(**cls._get_attributes_dict(meta))
79 |
80 | # Apply the configuration from the current class.
81 | cur_meta = {}
82 | if attrs.get('Meta'):
83 | cur_meta = cls._get_attributes_dict(attrs['Meta'])
84 | metadata.update(**cur_meta)
85 |
86 | # Gather and construct the options object.
87 | meta = attrs['meta'] = Options(metadata, name, cur_meta, base_meta)
88 |
89 | # Collect declared attributes.
90 | attrs['_items'] = OrderedDict()
91 |
92 | # Collect attributes from base classes.
93 | for base in bases:
94 | values = getattr(base, '_items', None)
95 | if values:
96 | attrs['_items'].update(values)
97 |
98 | # Collect attributes from current class.
99 | attrs_l = list(filter(lambda x: issubclass(type(x[1]), Component),
100 | attrs.items()))
101 | attrs_l.sort(key=lambda x: x[1].creation_counter)
102 | for key, attr in attrs_l:
103 | # If name reference is null; default to camel-cased name.
104 | if attr._name is None:
105 | attr._name = pascalize(key)
106 |
107 | # Store attribute in dictionary.
108 | attrs['_items'][attr._name] = attr
109 |
110 | # Continue initialization.
111 | obj = super(Declarative, cls).__new__(cls, name, bases, attrs)
112 |
113 | # Add this element to the element registry.
114 | _element_registry[obj.name] = obj
115 |
116 | # Return the constructed element class.
117 | return obj
118 |
119 |
120 | class Component(object):
121 |
122 | # Tracks each time this field is created; used to keep fields
123 | # in order
124 | creation_counter = 0
125 |
126 | def __init__(self, type_, name=None, required=False, default=None):
127 | # Name of the attribute in its serialized form.
128 | self._name = name
129 |
130 | # Underlying type of the attribute.
131 | self.type = type_
132 |
133 | # Whether the attribute is required or not.
134 | self.required = required
135 |
136 | # The default value for this attribute.
137 | self.default = default
138 | if not callable(default):
139 | # Normalize self.default to always be a callable.
140 | self.default = lambda: default
141 |
142 | # Adjust the creation counter, and save our local copy.
143 | self.creation_counter = Component.creation_counter
144 | Component.creation_counter += 1
145 |
146 | def __delete__(self, instance):
147 | if instance is not None:
148 | # Being accessed as an instance; use the instance state.
149 | if self._name in instance._state:
150 | del instance._state[self._name]
151 | return
152 |
153 | # Prevent deletion.
154 | raise TypeError("attribute can't be deleted")
155 |
156 |
157 | class Element(Component):
158 |
159 | def __init__(self, type_, **kwargs):
160 | # If an element is a collection it is a list of elements.
161 | self.collection = kwargs.pop('collection', False)
162 |
163 | # Continue the initialization the base element.
164 | super(Element, self).__init__(type_, **kwargs)
165 |
166 | @property
167 | def name(self):
168 | # Return the namespaced name of the element.
169 | return '{%s}%s' % (self.type.meta.namespace[1], self._name)
170 |
171 | @property
172 | def namespace(self):
173 | # Return the namespace of the underlying type.
174 | return self.type.meta.namespace
175 |
176 | def prepare(self, instance):
177 | # Retrieve the value of this attribute from the instance.
178 | value = instance._state.get(self._name)
179 | if value is None and self.default:
180 | # No value; use the default callable.
181 | self.__set__(instance, self.default())
182 | value = instance._state.get(self._name)
183 |
184 | # Return the value.
185 | return value
186 |
187 | def deserialize(self, xml):
188 | return self.type.deserialize(xml)
189 |
190 | def __get__(self, instance, owner=None):
191 | if instance is not None:
192 | # Being accessed as an instance; use the instance state.
193 | value = instance._state.get(self._name)
194 | if value is None and self.collection:
195 | # No value and we need to be a collection of things.
196 | instance._state[self._name] = value = []
197 |
198 | if value is None:
199 | # Build a default one of ourself.
200 | instance._state[self._name] = value = self.type()
201 |
202 | # Return the value.
203 | return value
204 |
205 | # Return ourself.
206 | return self
207 |
208 | def __set__(self, instance, value):
209 | if instance is not None:
210 | if isinstance(value, str):
211 | # Value is just text; construct the type.
212 | value = self.type(value)
213 |
214 | # Being accessed as an instance; use the instance state.
215 | if self.collection:
216 | if self._name not in instance._state:
217 | instance._state[self._name] = []
218 |
219 | instance._state[self._name].append(value)
220 |
221 | else:
222 | instance._state[self._name] = value
223 |
224 | return
225 |
226 | # Prevent assignment.
227 | raise TypeError("attribute can't be assigned")
228 |
229 |
230 | class Attribute(Component):
231 |
232 | def __init__(self, type_, name=None, required=False, default=None):
233 | # Initialize the base element first.
234 | super(Attribute, self).__init__(
235 | type_, name=name, required=required, default=default)
236 |
237 | # Instantiate the type reference with no parameters.
238 | if isinstance(self.type, type):
239 | self.type = self.type()
240 |
241 | @property
242 | def name(self):
243 | return self._name
244 |
245 | def prepare(self, instance):
246 | # Retrieve the value of this attribute from the instance.
247 | value = instance._state.get(self.name)
248 | if value is None and self.default:
249 | # No value; use the default callable.
250 | value = self.default()
251 |
252 | # Run the value through the underyling type's preparation method.
253 | value = self.type.prepare(value)
254 |
255 | # Return the value.
256 | return value
257 |
258 | def clean(self, text):
259 | # Wipe off the passed text and squish it into a python object
260 | # if needed.
261 | return self.type.clean(text)
262 |
263 | def __get__(self, instance, owner=None):
264 | if instance is not None:
265 | # Being accessed as an instance; use the instance state.
266 | return instance._state.get(self._name)
267 |
268 | # Return ourself.
269 | return self
270 |
271 | def __set__(self, instance, value):
272 | if instance is not None:
273 | # Being accessed as an instance; use the instance state.
274 | instance._state[self._name] = value
275 | return
276 |
277 | # Prevent assignment.
278 | raise TypeError("attribute can't be assigned")
279 |
280 |
281 | class Base(six.with_metaclass(Declarative)):
282 |
283 | def __init__(self, text=None, **kwargs):
284 | # Instance state of the attribute.
285 | self._state = {}
286 |
287 | # Text of the element.
288 | self.text = text
289 |
290 | # Update the instance state with kwargs.
291 | self._state.update(kwargs)
292 |
293 | # The signature function tuple.
294 | self._sign_args = None
295 |
296 | @classproperty
297 | def name(cls):
298 | # Return the namespaced name of the element.
299 | return '{%s}%s' % (cls.meta.namespace[1], cls.meta.name)
300 |
301 | def prepare(self):
302 | """Prepare the date in the instance state for serialization.
303 | """
304 |
305 | # Create a collection for the attributes and elements of
306 | # this instance.
307 | attributes, elements = OrderedDict(), []
308 |
309 | # Initialize the namespace map.
310 | nsmap = dict([self.meta.namespace])
311 |
312 | # Iterate through all declared items.
313 | for name, item in self._items.items():
314 | if isinstance(item, Attribute):
315 | # Prepare the item as an attribute.
316 | attributes[name] = item.prepare(self)
317 |
318 | elif isinstance(item, Element):
319 | # Update the nsmap.
320 | nsmap.update([item.namespace])
321 |
322 | # Prepare the item as an element.
323 | elements.append(item)
324 |
325 | # Return the collected attributes and elements
326 | return attributes, elements, nsmap
327 |
328 | def _serialize_item(self, item):
329 | # Destructure the data.
330 | attributes, elements, nsmap = item.prepare()
331 |
332 | # Create the XML node.
333 | node = etree.Element(item.name, nsmap=nsmap)
334 |
335 | # Add the attributes.
336 | for name, value in attributes.items():
337 | if value is not None:
338 | node.attrib[name] = value
339 |
340 | # Set its text.
341 | node.text = item.text
342 |
343 | # Iterate and serialize all elements.
344 | for element in elements:
345 | self._serialize_element(element, node)
346 |
347 | # Return the node.
348 | return node
349 |
350 | def _serialize_element(self, element, parent=None):
351 | # Prepare the instance state for serialization.
352 | items = element.prepare(self)
353 | if not items:
354 | # No data to serialize; move along.
355 | return
356 |
357 | try:
358 | # Serialize the item(s).
359 | for item in items:
360 | parent.append(item.serialize())
361 |
362 | except TypeError:
363 | # Serialize the single item.
364 | parent.append(items.serialize())
365 |
366 | def serialize(self):
367 | """
368 | Serializes the data in the instance state as an
369 | XML representation.
370 | """
371 |
372 | # Serialize the root and return the serialized element.
373 | return self._serialize_item(self)
374 |
375 | def tostring(self):
376 | return etree.tostring(self.serialize())
377 |
378 | @classmethod
379 | def deserialize(cls, xml):
380 | # Instantiate an instance of ourself.
381 | instance = cls()
382 |
383 | # Set the text element if present.
384 | if xml.text:
385 | instance.text = xml.text
386 |
387 | # Iterate through the items and deserialize them on the instance.
388 | elements = iter(xml.getchildren())
389 | element = None
390 | index = 0
391 | items = list(cls._items.values())
392 | # print(items)
393 | while index < len(items):
394 | # Fetch the next item.
395 | item = items[index]
396 | index += 1
397 |
398 | if isinstance(item, Attribute):
399 | # Attempt to get the attribute from the
400 | # xml element.
401 | value = xml.attrib.get(item.name)
402 |
403 | # Clean the value using the item clean.
404 | value = item.clean(value)
405 |
406 | # Set it on the instance.
407 | item.__set__(instance, value)
408 |
409 | elif isinstance(item, Element):
410 | if element is None:
411 | try:
412 | # Get the next element in the chain.
413 | element = next(elements)
414 |
415 | except StopIteration:
416 | break
417 |
418 | # Resolve the element into a schema object.
419 | obj = _element_registry.get(element.tag)
420 | if obj is None:
421 | # Element is unknown; bail.
422 | element = None
423 | index -= 1
424 | continue
425 |
426 | # Is this element a subclass of the current item?
427 | if not issubclass(obj, item.type):
428 | # Nope; skip to the next item.
429 | continue
430 |
431 | # Deserialize the element.
432 | value = obj.deserialize(element)
433 |
434 | # Set it on the instance.
435 | item.__set__(instance, value)
436 |
437 | # Are we dealing with a "collection" ?
438 | if item.collection:
439 | index -= 1
440 |
441 | # Unset the current element reference.
442 | element = None
443 |
444 | # Return the deserialized instance.
445 | return instance
446 |
447 | @classmethod
448 | def fromstring(cls, text):
449 | return cls.deserialize(etree.XML(text))
450 |
--------------------------------------------------------------------------------
/saml/schema/saml.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from uuid import uuid4
3 | from datetime import datetime
4 | from . import types, base, VERSION
5 | Element = base.Element
6 |
7 |
8 | class Base(base.Base):
9 |
10 | class Meta:
11 | namespace = 'saml', 'urn:oasis:names:tc:SAML:2.0:assertion'
12 |
13 |
14 | class BaseID(Base):
15 | """Provides an extension point for identifiers.
16 | """
17 |
18 | # The security or administrative domain that qualifies the name.
19 | name_qualifier = base.Attribute(types.String)
20 |
21 | # Further qualifies a name with the name of a service provider.
22 | sp_name_qualifier = base.Attribute(types.String, name="SPNameQualifier")
23 |
24 |
25 | class NameID(BaseID):
26 | """
27 | Is the identifier used in various SAML assertion
28 | constructs [saml-core § 2.2.3].
29 | """
30 |
31 | class Format:
32 | # URI prefix for the values in this enumeration.
33 | _PREFIX = "urn:oasis:names:tc:SAML:"
34 |
35 | # The interpretation is left to individual implementations.
36 | UNSPECIFIED = '%s1.1:nameid-format:unspecified' % _PREFIX
37 |
38 | # Indicates a form of an email address.
39 | EMAIL = '%s1.1:nameid-format:emailAddress' % _PREFIX
40 |
41 | # Is in the form specified by the X.509 recommendation [XMLSig].
42 | X509 = '%s1.1:nameid-format:X509SubjectName' % _PREFIX
43 |
44 | # Is in the format of a Windows domain qualified name.
45 | WINDOWS = '%s1.1:nameid-format:WindowsDomainQualifiedName' % _PREFIX
46 |
47 | # Is in the form of a Kerberos principal name.
48 | KEREBOS = '%s2.0:nameid-format:kerberos' % _PREFIX
49 |
50 | # Is the identifier of an entity that provides SAML-based services.
51 | ENTITY = '%s2.0:nameid-format:entity' % _PREFIX
52 |
53 | # Is a persistent opaque identifier for a principal.
54 | PERSISTENT = '%s2.0:nameid-format:persistent' % _PREFIX
55 |
56 | # Is an identifier with transient semantics.
57 | TRANSIENT = '%s2.0:nameid-format:transient' % _PREFIX
58 |
59 | # A URI classification of string-based identifier information.
60 | format = base.Attribute(types.String)
61 |
62 | # A name identifier established by a service provider.
63 | sp_provided_id = base.Attribute(types.String, name="SPProvidedID")
64 |
65 |
66 | class Issuer(NameID):
67 | """
68 | Represents the issuer of a SAML assertion or protocol
69 | message [saml-core § 2.2.5].
70 | """
71 |
72 | # If no Format value is provided, then the value ENTITY is in effect.
73 | format = base.Attribute(types.String)
74 |
75 |
76 | class _Message(Base):
77 | """Contains common information found in most SAML/2.0 communications.
78 | """
79 |
80 | # The version of this message.
81 | version = base.Attribute(types.String, default=VERSION, required=True)
82 |
83 | # The identifier for this message.
84 | id = base.Attribute(types.String, name='ID', required=True,
85 | default=lambda: '_%s' % uuid4().hex)
86 |
87 | # The time instant of issue in UTC.
88 | issue_instant = base.Attribute(types.DateTime, required=True,
89 | default=datetime.utcnow)
90 |
91 | # The SAML authority that is making the claim(s) in the message.
92 | issuer = Element(Issuer, required=True)
93 |
94 |
95 | class Statement(Base):
96 | """An extension point for statements made about a subject.
97 | """
98 |
99 |
100 | class AuthenticationContextReference(Statement):
101 | """
102 | A URI reference identifying an authentication context class that
103 | describes the authentication context declaration that follows.
104 | """
105 |
106 | class Meta:
107 | name = 'AuthnContextClassRef'
108 |
109 | # URI prefix for the values in this enumeration.
110 | _PREFIX = "urn:oasis:names:tc:SAML:2.0:ac:classes:"
111 |
112 | # Authenticated through the use of a provided IP address.
113 | INTERNET_PROTOCOL = "{}InternetProtocol".format(_PREFIX)
114 |
115 | # Authenticated through a provided IP address and username/password.
116 | INTERNET_PROTOCOL_PASSWORD = "{}InternetProtocolPassword".format(
117 | _PREFIX)
118 |
119 | # Authenticated using a Kerberos ticket.
120 | KERBEROS = "{}Kerberos".format(_PREFIX)
121 |
122 | # Reflects no mobile customer registration procedures and an
123 | # authentication of the mobile device without requiring explicit
124 | # end-user interaction.
125 | MOBILE_ONE_FACTOR_UNREGISTERED = (
126 | "{}MobileOneFactorUnregistered".format(_PREFIX))
127 |
128 | # Reflects no mobile customer registration procedures and a
129 | # two-factor based authentication, such as secure device and user PIN.
130 | MOBILE_TWO_FACTOR_UNREGISTERED = (
131 | "{}MobileTwoFactorUnregistered".format(_PREFIX))
132 |
133 | # Reflects mobile contract customer registration procedures and a
134 | # single factor authentication.
135 | MOBILE_ONE_FACTOR_CONTRACT = "{}MobileOneFactorContract".format(
136 | _PREFIX)
137 |
138 | # Reflects mobile contract customer registration procedures and a
139 | # two-factor based authentication.
140 | MOBILE_TWO_FACTOR_CONTRACT = "{}MobileTwoFactorContract".format(
141 | _PREFIX)
142 |
143 | # Authenticates through the presentation of a password over an
144 | # unprotected HTTP session.
145 | PASSWORD = "{}Password".format(_PREFIX)
146 |
147 | # Authentication through the presentation of a password over a
148 | # protected session.
149 | PASSWORD_PROTECTED_TRANSPORT = "{}PasswordProtectedTransport".format(
150 | _PREFIX)
151 |
152 | # A principal had authenticated to an authentication authority at
153 | # some point in the past using any authentication context supported
154 | # by that authentication authority.
155 | PREVIOUS_SESSION = "{}PreviousSession".format(_PREFIX)
156 |
157 | # The principal authenticated by means of a digital signature where
158 | # the key was validated as part of an X.509 Public Key Infrastructure.
159 | X509 = "{}X509".format(_PREFIX)
160 |
161 | # The principal authenticated by means of a digital signature where
162 | # key was validated as part of a PGP Public Key Infrastructure.
163 | PGP = "{}PGP".format(_PREFIX)
164 |
165 | # The principal authenticated by means of a digital signature where
166 | # the key was validated via an SPKI Infrastructure.
167 | SPKI = "{}SPKI".format(_PREFIX)
168 |
169 | # This context class indicates that the principal authenticated by
170 | # means of a digital signature according to the processing rules
171 | # specified in the XML Digital Signature specification [XMLSig].
172 | XMLDSIG = "{}XMLDSig".format(_PREFIX)
173 |
174 | # A principal authenticates to an authentication authority using a
175 | # smartcard.
176 | SMARTCARD = "{}Smartcard".format(_PREFIX)
177 |
178 | # A principal authenticates to an authentication authority through
179 | # a two-factor authentication mechanism using a smartcard with
180 | # enclosed private key and a PIN.
181 | SMARTCARD_PKI = "{}SmartcardPKI".format(_PREFIX)
182 |
183 | # A principal uses an X.509 certificate stored in software to
184 | # authenticate to the authentication authority.
185 | SOFTWARE_PKI = "{}SoftwarePKI".format(_PREFIX)
186 |
187 | # The principal authenticated via the provision of a fixed-line
188 | # telephone number, transported via a telephony protocol such as ADSL.
189 | TELEPHONY = "{}Telephony".format(_PREFIX)
190 |
191 | # The principal is "roaming" (perhaps using a phone card) and
192 | # authenticates via the means of the line number, a user suffix,
193 | # and a password element.
194 | NOMAD_TELEPHONY = "{}NomadTelephony".format(_PREFIX)
195 |
196 | # The principal authenticated via the provision of a fixed-line
197 | # telephone number and a user suffix, transported via a telephony
198 | # protocol such as ADSL.
199 | PERSONAL_TELEPHONY = "{}PersonalTelephony".format(_PREFIX)
200 |
201 | # Authenticated via the means of the line number,
202 | # a user suffix, and a password element.
203 | AUTHENTICATED_TELEPHONY = "{}AuthenticatedTelephony".format(_PREFIX)
204 |
205 | # Authenticated by means of Secure Remote Password s[RFC 2945].
206 | SECURE_REMOTE_PASSWORD = "{}SecureRemotePassword".format(_PREFIX)
207 |
208 | # The principal authenticated by means of a client certificate,
209 | # secured with the SSL/TLS transport.
210 | TLS_CLIENT = "{}TLSClient".format(_PREFIX)
211 |
212 | # A principal authenticates through a time synchronization token.
213 | TIME_SYNC_TOKEN = "{}TimeSyncToken".format(_PREFIX)
214 |
215 | # Indicates that the authentication means are unspecified.
216 | UNSPECIFIED = "{}Unspecified".format(_PREFIX)
217 |
218 |
219 | class AuthenticationContext(Base):
220 | """Specifies the context of an authentication event.
221 | """
222 |
223 | class Meta:
224 | name = 'AuthnContext'
225 |
226 | # A URI reference identifying an authentication context class that
227 | # describes the authentication context declaration that follows.
228 | reference = Element(AuthenticationContextReference,
229 | default=AuthenticationContextReference.UNSPECIFIED)
230 |
231 | # TODO: or
232 | # TODO:
233 |
234 |
235 | class AttributeValue(Base):
236 | pass
237 |
238 |
239 | class Attribute(Base):
240 |
241 | name_ = base.Attribute(types.String, name="Name")
242 |
243 | name_format = base.Attribute(types.String)
244 |
245 | value = Element(AttributeValue)
246 |
247 |
248 | class AttributeStatement(Statement):
249 |
250 | attributes = Element(Attribute, collection=True)
251 |
252 |
253 | class SubjectLocality(Base):
254 | """
255 | Specifies the DNS domain name and IP address for the system from
256 | which the assertion subject was authenticated.
257 | """
258 |
259 |
260 | class AuthenticationStatement(Statement):
261 | """
262 | Describes a statement by the SAML authority asserting that the assertion
263 | subject was authenticated by a particular means at a particular time.
264 | """
265 |
266 | class Meta:
267 | name = 'AuthnStatement'
268 |
269 | # Specifies the time at which the authentication took place.
270 | authn_instant = base.Attribute(types.DateTime, required=True,
271 | default=datetime.utcnow)
272 |
273 | # Specifies the index of a particular session between the principal
274 | # identified by the subject and the authenticating authority.
275 | session_index = base.Attribute(types.String)
276 |
277 | # Specifies a time instant at which the session between the principal
278 | # identified by the subject and the SAML authority issuing this
279 | # statement MUST be considered ended.
280 | session_not_on_or_after = base.Attribute(types.DateTime)
281 |
282 | # Specifies the DNS domain name and IP address for the system from which
283 | # the assertion subject was apparently authenticated.
284 | subject_locality = Element(SubjectLocality)
285 |
286 | # The context used by the authenticating authority up to and including
287 | # the authentication event that yielded this statement.
288 | context = Element(AuthenticationContext)
289 |
290 |
291 | class SubjectConfirmationData(Base):
292 | """Specifies constraints on allowing a subject to be confirmed.
293 | """
294 |
295 | # TODO:
296 | # TODO: Arbitrary attributes
297 | # TODO: Arbitrary elements
298 |
299 | # A time instant before which the subject cannot be confirmed.
300 | not_before = base.Attribute(types.DateTime)
301 |
302 | # A time instant at which the subject can no longer be confirmed.
303 | not_on_or_after = base.Attribute(types.DateTime)
304 |
305 | # URI specifying to which an attesting entity can present the assertion.
306 | recipient = base.Attribute(types.String)
307 |
308 | # ID of a SAML message to the entity can present the assertion to.
309 | in_response_to = base.Attribute(types.String)
310 |
311 | # The network address to which the saml entity can present the assertion.
312 | address = base.Attribute(types.String)
313 |
314 |
315 | class SubjectConfirmation(Base):
316 | """
317 | Provides the means for a relying party to verify the
318 | correspondence of the subject of the assertion with the party with whom
319 | the relying party is communicating.
320 | """
321 |
322 | class Method:
323 |
324 | # URI namespace prefix of the values.
325 | _PREFIX = 'urn:oasis:names:tc:SAML:2.0:cm:'
326 |
327 | # The subject is confirmed by the indicated data.
328 | BEARER = '{}bearer'.format(_PREFIX)
329 |
330 | # The subject is confirmed by the holding of a key.
331 | HOLDER_OF_KEY = '{}holder-of-key'.format(_PREFIX)
332 |
333 | # URI reference that identifies a protocol to confirm the subject.
334 | method = base.Attribute(types.String, required=True, default=Method.BEARER)
335 |
336 | # Identifies the entity expected to satisfy the enclosed requirements.
337 | principal = Element(NameID)
338 |
339 | # Confirmation information and constraints.
340 | data = Element(SubjectConfirmationData)
341 |
342 |
343 | class Subject(Base):
344 | """The principal that is the subject of all statements in an assertion.
345 | """
346 |
347 | # Identifies the subject.
348 | principal = Element(NameID)
349 |
350 | # Information that allows the subject to be confirmed. If more than one
351 | # subject confirmation is provided, then satisfying any one of them is
352 | # sufficient to confirm the subject for the purpose of applying
353 | # the assertion.
354 | confirmation = Element(SubjectConfirmation, collection=True)
355 |
356 |
357 | class Condition(Base):
358 | """Serves as an extension point for new conditions.
359 | """
360 |
361 |
362 | class Conditions(Base):
363 | """
364 | Defines the SAML constructs that place constraints on the
365 | acceptable use of SAML assertions.
366 | """
367 |
368 | # A time instant before which the subject cannot be confirmed.
369 | not_before = base.Attribute(types.DateTime)
370 |
371 | # A time instant at which the subject can no longer be confirmed.
372 | not_on_or_after = base.Attribute(types.DateTime)
373 |
374 | # Specifies that the assertion is addressed to a particular audience.
375 | condition = Element(Condition, collection=True)
376 |
377 |
378 | class Audience(Base):
379 | """A URI reference that identifies an intended audience.
380 | """
381 |
382 |
383 | class AudienceRestriction(Condition):
384 | """
385 | Specifies that the assertion is addressed to one or more
386 | specific audiences.
387 | """
388 |
389 | audiences = Element(Audience, collection=True)
390 |
391 |
392 | class OneTimeUse(Condition):
393 | """
394 | Allows an authority to indicate that the information
395 | in the assertion is likely to change very soon and fresh information
396 | should be obtained for each use.
397 | """
398 |
399 |
400 | class Assertion(_Message):
401 | """
402 | This type specifies the basic information that is common to
403 | all assertions [saml-core § 2.3.3].
404 | """
405 |
406 | # The subject of the statement(s) in the assertion.
407 | subject = Element(Subject)
408 |
409 | # Conditions that MUST be evaluated when assessing the validity
410 | # of and/or when using the assertion.
411 | conditions = Element(Conditions)
412 |
413 | # Additional information related to the assertion that assists
414 | # processing in certain situations but which MAY be ignored by
415 | # applications that do not understand the advice or do not wish to
416 | # make use of it.
417 | # TODO:
418 |
419 | # Statements that are being asserted about the included subject.
420 | statements = Element(Statement, collection=True)
421 |
--------------------------------------------------------------------------------
/saml/schema/samlp.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from . import types, base, Element, saml
3 |
4 |
5 | class Base(base.Base):
6 |
7 | class Meta:
8 | namespace = 'samlp', 'urn:oasis:names:tc:SAML:2.0:protocol'
9 |
10 |
11 | class _Message(saml._Message, Base):
12 | """Contains common information found in most SAML/2.0 protocols.
13 | """
14 |
15 | # A URI reference indicating the address to which this request has
16 | # been sent.
17 | destination = base.Attribute(types.String)
18 |
19 | # Indicates whether or not (and under what conditions) consent has
20 | # been obtained from a principal in the sending of this request.
21 | consent = base.Attribute(types.String)
22 |
23 |
24 | class NameIDPolicy(Base):
25 | """
26 | Tailors the name identifier in the subjects of assertions
27 | resulting from an .
28 | """
29 |
30 | # Specifies the URI reference corresponding to a name identifier format
31 | # defined in this or another specification.
32 | format = base.Attribute(types.String)
33 |
34 | # Optionally specifies that the assertion subject's identifier be
35 | # returned (or created) in the namespace of a service provider
36 | # other than the requester, or in the namespace of an affiliation
37 | # group of service providers
38 | sp_name_qualifier = base.Attribute(types.String, name='SPNameQualifier')
39 |
40 | # A Boolean value used to indicate whether the identity provider is
41 | # allowed, in the course of fulfilling the request, to create a
42 | # new identifier to represent the principal.
43 | allow_create = base.Attribute(types.Boolean)
44 |
45 |
46 | class RequestedAuthenticationContext(Base):
47 |
48 | class Meta:
49 | name = 'RequestedAuthnContext'
50 |
51 | # Specifies the comparison method used to evaluate the requested
52 | # context classes or statements, one of "exact", "minimum",
53 | # "maximum", or "better". The default is "exact".
54 | comparison = base.Attribute(types.String)
55 |
56 | # Specifies one or more URI references identifying authentication
57 | # context classes or declarations.
58 | reference = Element(saml.AuthenticationContextReference, collection=True)
59 |
60 |
61 | class Protocol:
62 | """
63 | The available URIs that identifies a SAML protocol binding to be used when
64 | returning the message.
65 | """
66 |
67 | _PREFIX = 'urn:oasis:names:tc:SAML:2.0:bindings:'
68 |
69 | SOAP = '%sSOAP' % _PREFIX
70 |
71 | POAS = '%sPAOS' % _PREFIX
72 |
73 | REDIRECT = '%sHTTP-Redirect' % _PREFIX
74 |
75 | POST = '%sHTTP-POST' % _PREFIX
76 |
77 | ARTIFACT = '%sHTTP-Artifact' % _PREFIX
78 |
79 | URI = '%sURI' % _PREFIX
80 |
81 |
82 | class AuthenticationRequest(_Message):
83 | """
84 | Create a SAML AuthnRequest
85 | ::
86 | from saml import schema
87 | from datetime import datetime
88 |
89 | document = schema.AuthenticationRequest()
90 | document.id = '11111111-2222-3333-4444-555555555555'
91 | document.issue_instant = datetime(2000, 1, 1)
92 | document.assertion_consumer_service_index = 0
93 | document.attribute_consuming_service_index = 0
94 | document.issuer = 'https://sp.example.com/SAML2'
95 |
96 | policy = schema.NameIDPolicy()
97 | policy.allow_create = True
98 | policy.format = schema.NameID.Format.TRANSIENT
99 | document.policy = policy
100 |
101 | print document.tostring()
102 |
103 | Produces the following XML document:
104 |
105 | .. code-block:: xml
106 |
107 |
115 | https://sp.example.com/SAML2
116 |
119 |
120 | """
121 |
122 | class Meta:
123 | name = 'AuthnRequest'
124 |
125 | # A Boolean value. If "true", the identity provider MUST authenticate
126 | # the presenter directly rather than rely on a previous security context.
127 | force_authn = base.Attribute(types.Boolean)
128 |
129 | # A Boolean value. If "true", the identity provider and the user agent
130 | # itself MUST NOT visibly take control of the user interface from the
131 | # requester and interact with the presenter in a noticeable fashion.
132 | is_passive = base.Attribute(types.Boolean)
133 |
134 | # A URI reference that identifies a SAML protocol binding to be used when
135 | # returning the message.
136 | protocol = base.Attribute(types.String, name='ProtocolBinding')
137 |
138 | # Indirectly identifies the location to which the message
139 | # should be returned to the requester.
140 | assertion_consumer_service_index = base.Attribute(types.Integer)
141 |
142 | # Specifies by value the location to which the message MUST
143 | # be returned to the requester.
144 | assertion_consumer_service_url = base.Attribute(
145 | types.String, name='AssertionConsumerServiceURL')
146 |
147 | # Indirectly identifies information associated with the requester
148 | # describing the SAML attributes the requester desires or requires to be
149 | # supplied by the identity provider in the message.
150 | attribute_consuming_service_index = base.Attribute(types.Integer)
151 |
152 | # Specifies the human-readable name of the requester for use by the
153 | # presenter's user agent or the identity provider.
154 | provider_name = base.Attribute(types.String)
155 |
156 | # Specifies the requested subject of the resulting assertion(s).
157 | subject = Element(saml.Subject)
158 |
159 | # Specifies constraints on the name identifier to be used to represent
160 | # the requested subject.
161 | policy = Element(NameIDPolicy)
162 |
163 | # Specifies the requirements, if any, that the requester places on
164 | # the authentication context that applies to the responding provider's
165 | # authentication of the presenter.
166 | requested_context = Element(RequestedAuthenticationContext)
167 |
168 | # Specifies a set of identity providers trusted by the requester to
169 | # authenticate the presenter, as well as limitations and context
170 | # related to proxying of the message to subsequent identity
171 | # providers by the responder.
172 | # TODO: scoping = Element(Scoping)
173 |
174 |
175 | class StatusCode(Base):
176 |
177 | # URI prefix for the values in this enumeration.
178 | _PREFIX = "urn:oasis:names:tc:SAML:2.0:status:"
179 |
180 | # The request succeeded.
181 | SUCCESS = "{}Success".format(_PREFIX)
182 |
183 | # Failure due to an error on the part of the requester.
184 | REQUESTER = "{}Requester".format(_PREFIX)
185 |
186 | # The version of the request message was incorrect.
187 | VERSION_MISMATCH = "{}VersionMismatch".format(_PREFIX)
188 |
189 | # The provider wasn't able to successfully authenticate the principal.
190 | AUTHENTICATION_FAILED = "{}AuthnFailed".format(_PREFIX)
191 |
192 | # Unexpected or invalid content was encountered.
193 | INVALID_ATTRIBUTE_NAME_OR_VALUE = "{}InvalidAttrNameOrValue".format(
194 | _PREFIX)
195 |
196 | # The responding provider cannot support the requested name ID policy.
197 | INVALID_NAME_ID_POLICY = "{}InvalidNameIDPolicy".format(_PREFIX)
198 |
199 | # The authentication context requirements cannot be met.
200 | NO_AUTHENTICATION_CONTEXT = "{}NoAuthnContext".format(_PREFIX)
201 |
202 | # None of the supported identity providers are available.
203 | NO_AVAILABLE_IDP = "{}NoAvailableIDP".format(_PREFIX)
204 |
205 | # The responding provider cannot authenticate the principal passively.
206 | NO_PASSIVE = "{}NoPassive".format(_PREFIX)
207 |
208 | # None of the identity providers are supported by the intermediary.
209 | NO_SUPPORTED_IDP = "{}NoSupportedIDP".format(_PREFIX)
210 |
211 | # Not able to propagate logout to all other session participants.
212 | PARTIAL_LOGOUT = "{}PartialLogout".format(_PREFIX)
213 |
214 | # Cannot authenticate directly and not permitted to proxy the request.
215 | PROXY_COUNT_EXCEEDED = "{}ProxyCountExceeded".format(_PREFIX)
216 |
217 | # Is able to process the request but has chosen not to respond.
218 | REQUEST_DENIED = "{}RequestDenied".format(_PREFIX)
219 |
220 | # The SAML responder or SAML authority does not support the request.
221 | REQUEST_UNSUPPORTED = "{}RequestUnsupported".format(_PREFIX)
222 |
223 | # Deprecated protocol version specified in the request.
224 | REQUEST_VERSION_DEPRECATED = "{}RequestVersionDeprecated".format(
225 | _PREFIX)
226 |
227 | # Protocol version specified in the request message is too low.
228 | REQUEST_VERSION_TOO_LOW = "{}RequestVersionTooHigh".format(_PREFIX)
229 |
230 | # Protocol version specified in the request message is too high.
231 | REQUEST_VERSION_TOO_HIGH = "{}RequestVersionTooLow".format(_PREFIX)
232 |
233 | # Resource value provided in the request message is invalid.
234 | RESOURCE_NOT_RECOGNIZED = "{}ResourceNotRecognized".format(_PREFIX)
235 |
236 | # The response message would contain more elements than able.
237 | TOO_MANY_RESPONSES = "{}TooManyResponses".format(_PREFIX)
238 |
239 | # base.Attribute from an unknown attribute profile.
240 | UNKNOWN_ATTR_PROFILE = "{}UnknownAttrProfile".format(_PREFIX)
241 |
242 | # The responder does not recognize the principal.
243 | UNKNOWN_PRINCIPAL = "{}UnknownPrincipal".format(_PREFIX)
244 |
245 | # The SAML responder cannot properly fulfill the request.
246 | UNSUPPORTED_BINDING = "{}UnsupportedBinding".format(_PREFIX)
247 |
248 | # The status code value. This attribute contains a URI reference.
249 | value = base.Attribute(types.String)
250 |
251 | # A subordinate status code that provides more specific information
252 | # on an error condition.
253 | # TODO: code = Element('self')
254 |
255 |
256 | class Status(Base):
257 |
258 | # A code representing the status of the activity carried out in
259 | # response to the corresponding request.
260 | code = Element(StatusCode, required=True)
261 |
262 |
263 | class StatusResponse(_Message):
264 | """Extends the common message type with a status element.
265 | """
266 |
267 | # A reference to the identifier of the request to which the
268 | # response corresponds, if any.
269 | in_response_to = base.Attribute(types.String)
270 |
271 | # A code representing the status of the corresponding request.
272 | status = Element(Status)
273 |
274 |
275 | class Response(StatusResponse):
276 | """
277 | Create a SAML Response
278 | ::
279 | from saml import schema
280 | from datetime import datetime
281 |
282 | document = schema.Response()
283 | document.id = '11111111-1111-1111-1111-111111111111'
284 | document.in_response_to = '22222222-2222-2222-2222-222222222222'
285 | document.issue_instant = datetime(2000, 1, 1, 1)
286 | document.issuer = 'https://idp.example.org/SAML2'
287 | document.destination = 'https://sp.example.com/SAML2/SSO/POST'
288 | document.status.code.value = schema.StatusCode.SUCCESS
289 |
290 | # Create an assertion for the response.
291 | document.assertions = assertion = schema.Assertion()
292 | assertion.id = '33333333-3333-3333-3333-333333333333'
293 | assertion.issue_instant = datetime(2000, 1, 1, 2)
294 | assertion.issuer = 'https://idp.example.org/SAML2'
295 |
296 | # Create a subject.
297 | assertion.subject = schema.Subject()
298 | assertion.subject.principal = '44444444-4444-4444-4444-444444444444'
299 | assertion.subject.principal.format = schema.NameID.Format.TRANSIENT
300 | data = schema.SubjectConfirmationData()
301 | data.in_response_to = '22222222-2222-2222-2222-222222222222'
302 | data.not_on_or_after = datetime(2000, 1, 1, 1, 10)
303 | data.recipient = 'https://sp.example.com/SAML2/SSO/POST'
304 | confirmation = schema.SubjectConfirmation()
305 | confirmation.data = data
306 | assertion.subject.confirmation = confirmation
307 |
308 | # Create an authentication statement.
309 | statement = schema.AuthenticationStatement()
310 | assertion.statements.append(statement)
311 | statement.authn_instant = datetime(2000, 1, 1, 1, 3)
312 | statement.session_index = '33333333-3333-3333-3333-333333333333'
313 | reference = schema.AuthenticationContextReference
314 | statement.context.reference = reference.PASSWORD_PROTECTED_TRANSPORT
315 |
316 | # Create a authentication condition.
317 | assertion.conditions = conditions = schema.Conditions()
318 | conditions.not_before = datetime(2000, 1, 1, 1, 3)
319 | conditions.not_on_or_after = datetime(2000, 1, 1, 1, 9)
320 | condition = schema.AudienceRestriction()
321 | condition.audiences = 'https://sp.example.com/SAML2'
322 | conditions.condition = condition
323 |
324 | print document.tostring()
325 |
326 | Produces the following XML document:
327 |
328 | .. code-block:: xml
329 |
330 |
338 | https://idp.example.org/SAML2
339 |
340 |
342 |
343 |
347 | https://idp.example.org/SAML2
348 |
349 |
352 | 44444444-4444-4444-4444-444444444444
353 |
354 |
356 |
361 |
362 |
363 |
366 |
367 |
368 | https://sp.example.com/SAML2
369 |
370 |
371 |
372 |
375 |
376 |
377 | urn:oasis:names:tc:SAML:2.0:ac:classes:
378 | PasswordProtectedTransport
379 |
380 |
381 |
382 |
383 |
384 | """
385 |
386 | # Specifies an assertion by value, or optionally an encrypted assertion
387 | # by value.
388 | assertions = Element(saml.Assertion, collection=True)
389 |
390 |
391 | class Artifact(Base):
392 | """
393 | The artifact value that the requester received and now wishes to
394 | translate into the protocol message it represents.
395 | """
396 |
397 |
398 | class ArtifactResolve(_Message):
399 | """
400 | Used to request that a SAML protocol message be returned in an
401 | message by specifying an artifact that represents
402 | the SAML protocol message.
403 | """
404 |
405 | # The artifact value that the requester received and now wishes to
406 | # translate into the protocol message it represents.
407 | artifact = Element(Artifact, required=True)
408 |
409 |
410 | class ArtifactResponse(StatusResponse):
411 | """
412 | Responds to an request with an embedded message
413 | that was referenced by the given artifact.
414 | """
415 |
416 | # The embedded message.
417 | message = Element(_Message)
418 |
419 |
420 | class SessionIndex(Base):
421 | """The identifier that indexes a session at the message recipient.
422 | """
423 |
424 |
425 | class LogoutRequest(_Message):
426 | """
427 | Create a SAML LogoutRequest
428 | ::
429 | from saml import schema
430 | from datetime import datetime
431 |
432 | document = schema.LogoutRequest()
433 | document.id = '11111111-1111-1111-1111-111111111111'
434 | document.issue_instant = datetime(2000, 1, 1)
435 | document.issuer = 'https://idp.example.org/SAML2'
436 | document.destination = 'https://sp.example.org/SAML2/logout'
437 | document.principal = 'myemail@mydomain.com'
438 | document.principal.format = schema.NameID.Format.EMAIL
439 | document.principal.name_qualifier = 'https://idp.example.org/SAML2'
440 | document.session_index = 'SESSION-22222222-2222-2222-2222-222222222222'
441 |
442 | print document.tostring()
443 |
444 | Produces the following XML document:
445 |
446 | .. code-block:: xml
447 |
448 |
455 | https://idp.example.org/SAML2
456 |
459 | myemail@mydomain.com
460 |
461 |
462 | SESSION-22222222-2222-2222-2222-222222222222
463 |
464 |
465 | """
466 |
467 | # The time at which the request expires, after which the recipient
468 | # may discard the message.
469 | not_on_or_after = base.Attribute(types.DateTime)
470 |
471 | # An indication of the reason for the logout, in the
472 | # form of a URI reference.
473 | reason = base.Attribute(types.String)
474 |
475 | # The identifier and associated attributes
476 | # (in plaintext or encrypted form) that specify the principal as
477 | # currently recognized by the identity and service providers prior
478 | # to this request.
479 | principal = Element(saml.NameID, required=True)
480 |
481 | # The identifier that indexes this session at the message recipient.
482 | session_index = Element(SessionIndex)
483 |
484 |
485 | class LogoutResponse(StatusResponse):
486 | """
487 | Create a SAML LogoutResponse
488 | ::
489 | from saml import schema
490 | from datetime import datetime
491 |
492 | document = schema.LogoutResponse()
493 | document.id = '22222222-2222-2222-2222-222222222222'
494 | document.in_response_to = '11111111-1111-1111-1111-111111111111'
495 | document.issue_instant = datetime(2000, 1, 1)
496 | document.issuer = 'https://idp.example.org/SAML2'
497 | document.destination = 'https://sp.example.com/SAML2/SLO/POST'
498 | document.status.code.value = schema.StatusCode.SUCCESS
499 |
500 | print document.tostring()
501 |
502 | Produces the following XML document:
503 |
504 | .. code-block:: xml
505 |
506 |
514 | https://idp.example.org/SAML2
515 |
516 |
518 |
519 |
520 | """
521 |
--------------------------------------------------------------------------------