Code Monkey home page Code Monkey logo

python3-saml's Introduction

SAML Python Toolkit (compatible with Python3)

Python package PyPI Downloads Coverage Status PyPi Version Python versions

Add SAML support to your Python software using this library. Forget those complicated libraries and use the open source library provided by the SAML tool community.

This version supports Python3. Python 2 support was deprecated on Jan 1st, 2020: python-saml

Warning

Version 1.16.X is the latest version supporting Python2, consider its use deprecated. 1.17 won't be Python2 and old Python3 compatible.

Version 1.13.0 sets sha256 and rsa-sha256 as default algorithms

Version 1.8.0 sets strict mode active by default

Update python3-saml to 1.5.0, this version includes security improvements for preventing XEE and Xpath Injections.

Update python3-saml to 1.4.0, this version includes a fix for the CVE-2017-11427 vulnerability.

This version also changes how the calculate fingerprint method works, and will expect as input a formatted X.509 certificate.

Update python3-saml to 1.2.6 that adds the use defusedxml that will prevent XEE and other attacks based on the abuse of XML. (CVE-2017-9672)

Update python3-saml to >= 1.2.1, 1.2.0 had a bug on signature validation process (when using wantAssertionsSigned and wantMessagesSigned). CVE-2016-1000251

1.2.0 version includes a security patch that contains extra validations that will prevent signature wrapping attacks.

python3-saml < v1.2.0 is vulnerable and allows signature wrapping!

Security Guidelines

If you believe you have discovered a security vulnerability in this toolkit, please report it by mail to the maintainer: [email protected]

Why add SAML support to my software?

SAML is an XML-based standard for web browser single sign-on and is defined by the OASIS Security Services Technical Committee. The standard has been around since 2002, but lately it is becoming popular due its advantages:

  • Usability - One-click access from portals or intranets, deep linking, password elimination and automatically renewing sessions make life easier for the user.
  • Security - Based on strong digital signatures for authentication and integrity, SAML is a secure single sign-on protocol that the largest and most security conscious enterprises in the world rely on.
  • Speed - SAML is fast. One browser redirect is all it takes to securely sign a user into an application.
  • Phishing Prevention - If you don’t have a password for an app, you can’t be tricked into entering it on a fake login page.
  • IT Friendly - SAML simplifies life for IT because it centralizes authentication, provides greater visibility and makes directory integration easier.
  • Opportunity - B2B cloud vendor should support SAML to facilitate the integration of their product.

General Description

SAML Python toolkit lets you turn your Python application into a SP (Service Provider) that can be connected to an IdP (Identity Provider).

Supports:

  • SSO and SLO (SP-Initiated and IdP-Initiated).
  • Assertion and nameId encryption.
  • Assertion signatures.
  • Message signatures: AuthNRequest, LogoutRequest, LogoutResponses.
  • Enable an Assertion Consumer Service endpoint.
  • Enable a Single Logout Service endpoint.
  • Publish the SP metadata (which can be signed).

Key Features:

  • saml2int - Implements the SAML 2.0 Web Browser SSO Profile.
  • Session-less - Forget those common conflicts between the SP and the final app, the toolkit delegate session in the final app.
  • Easy to use - Programmer will be allowed to code high-level and low-level programming, 2 easy to use APIs are available.
  • Tested - Thoroughly tested.

Installation

Dependencies

  • python => 3.7
  • xmlsec Python bindings for the XML Security Library.
  • lxml Python bindings for the libxml2 and libxslt libraries.
  • isodate An ISO 8601 date/time/ duration parser and formatter

Review the pyproject.toml file to know the version of the library that python3-saml is using

Code

Option 1. Download from GitHub

The toolkit is hosted on GitHub. You can download it from:

Find the core of the library at src/onelogin/saml2 folder.

Option 2. Download from pypi

The toolkit is hosted in pypi, you can find the python3-saml package at https://pypi.python.org/pypi/python3-saml

You can install it executing:

$ pip install python3-saml

If you want to know how a project can handle python packages review this guide and review this sampleproject

NOTE

To avoid libxml2 library version incompatibilities between xmlsec and lxml it is recommended that lxml is not installed from binary.

This can be ensured by executing:

$ pip install --force-reinstall --no-binary lxml lxml

Security Warning

In production, the strict parameter MUST be set as "true". Otherwise your environment is not secure and will be exposed to attacks.

In production also we highly recommend to register on the settings the IdP certificate instead of using the fingerprint method. The fingerprint, is a hash, so at the end is open to a collision attack that can end on a signature validation bypass. Other SAML toolkits deprecated that mechanism, we maintain it for compatibility and also to be used on test environment.

Avoiding Open Redirect attacks

Some implementations uses the RelayState parameter as a way to control the flow when SSO and SLO succeeded. So basically the user is redirected to the value of the RelayState.

If you are using Signature Validation on the HTTP-Redirect binding, you will have the RelayState value integrity covered, otherwise, and on HTTP-POST binding, you can't trust the RelayState so before executing the validation, you need to verify that its value belong a trusted and expected URL.

Read more about Open Redirect CWE-601.

Avoiding Replay attacks

A replay attack is basically try to reuse an intercepted valid SAML Message in order to impersonate a SAML action (SSO or SLO).

SAML Messages have a limited timelife (NotBefore, NotOnOrAfter) that make harder this kind of attacks, but they are still possible.

In order to avoid them, the SP can keep a list of SAML Messages or Assertion IDs already validated and processed. Those values only need to be stored the amount of time of the SAML Message life time, so we don't need to store all processed message/assertion Ids, but the most recent ones.

The OneLogin_Saml2_Auth class contains the get_last_request_id, get_last_message_id and get_last_assertion_id methods to retrieve the IDs

Checking that the ID of the current Message/Assertion does not exists in the list of the ones already processed will prevent replay attacks.

Getting Started

Knowing the toolkit

The new SAML Toolkit contains different folders (certs, lib, demo-django, demo-flask and tests) and some files.

Let's start describing them:

src

This folder contains the heart of the toolkit, onelogin/saml2 folder contains the new version of the classes and methods that are described in a later section.

demo-django

This folder contains a Django project that will be used as demo to show how to add SAML support to the Django Framework. demo is the main folder of the Django project (with its settings.py, views.py, urls.py), templates is the Django templates of the project and saml is a folder that contains the certs folder that could be used to store the X.509 public and private key, and the SAML toolkit settings (settings.json and advanced_settings.json).

Notice about certs

SAML requires a X.509 cert to sign and encrypt elements like NameID, Message, Assertion, Metadata.

If our environment requires sign or encrypt support, the certs folder may contain the X.509 cert and the private key that the SP will use:

  • sp.crt The public cert of the SP
  • sp.key The private key of the SP

Or also we can provide those data in the setting file at the x509cert and the privateKey JSON parameters of the sp element.

Sometimes we could need a signature on the metadata published by the SP, in this case we could use the X.509 cert previously mentioned or use a new X.509 cert: metadata.crt and metadata.key.

Use sp_new.crt if you are in a key rollover process and you want to publish that X.509 certificate on Service Provider metadata.

If you want to create self-signed certs, you can do it at the https://www.samltool.com/self_signed_certs.php service, or using the command:

openssl req -new -x509 -days 3652 -nodes -out sp.crt -keyout sp.key

demo-flask

This folder contains a Flask project that will be used as demo to show how to add SAML support to the Flask Framework. index.py is the main Flask file that has all the code, this file uses the templates stored at the templates folder. In the saml folder we found the certs folder to store the X.509 public and private key, and the SAML toolkit settings (settings.json and advanced_settings.json).

demo_pyramid

This folder contains a Pyramid project that will be used as demo to show how to add SAML support to the Pyramid Web Framework. \_\_init__.py is the main file that configures the app and its routes, views.py is where all the logic and SAML handling takes place, and the templates are stored in the templates folder. The saml folder is the same as in the other two demos.

demo-tornado

This folder contains a Tornado project that will be used as demo to show how to add SAML support to the Tornado Framework. views.py (with its settings.py) is the main Flask file that has all the code, this file uses the templates stored at the templates folder. In the saml folder we found the certs folder to store the X.509 public and private key, and the SAML toolkit settings (settings.json and advanced_settings.json).

It requires python3.8 (it's using tornado 6.4.1)

tests

Contains the unit test of the toolkit.

In order to execute the test you only need to load the virtualenv with the toolkit installed on it properly:

make install-test

and execute:

make pytest

The previous line will run the tests for the whole toolkit. You can also run the tests for a specific module. To do so for the auth module you would have to execute this:

pytest tests/src/OneLogin/saml2_tests/auth_test.py::OneLogin_Saml2_Auth_Test

Or for an specific method:

pytest tests/src/OneLogin/saml2_tests/auth_test.py::OneLogin_Saml2_Auth_Test::testBuildRequestSignature

How It Works

Settings

First of all we need to configure the toolkit. The SP's info, the IdP's info, and in some cases, configure advanced security issues like signatures and encryption.

There are two ways to provide the settings information:

  • Use a settings.json file that we should locate in any folder, but indicates its path with the custom_base_path parameter.

  • Use a JSON object with the setting data and provide it directly to the constructor of the class (if your toolkit integation requires certs, remember to provide the custom_base_path as part of the settings or as a parameter in the constructor).

In the demo-django and in the demo-flask folders you will find a saml folder, inside there is a certs folder and a settings.json and advanced_settings.json file. Those files contain the settings for the SAML toolkit. Copy them in your project and set the correct values.

This is the settings.json file:

{
    // If strict is True, then the Python Toolkit will reject unsigned
    // or unencrypted messages if it expects them to be signed or encrypted.
    // Also it will reject the messages if the SAML standard is not strictly
    // followed. Destination, NameId, Conditions ... are validated too.
    "strict": true,

    // Enable debug mode (outputs errors).
    "debug": true,

    // Service Provider Data that we are deploying.
    "sp": {
        // Identifier of the SP entity  (must be a URI)
        "entityId": "https://<sp_domain>/metadata/",
        // Specifies info about where and how the <AuthnResponse> message MUST be
        // returned to the requester, in this case our SP.
        "assertionConsumerService": {
            // URL Location where the <Response> from the IdP will be returned
            "url": "https://<sp_domain>/?acs",
            // SAML protocol binding to be used when returning the <Response>
            // message. SAML Toolkit supports this endpoint for the
            // HTTP-POST binding only.
            "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
        },
        // Specifies info about where and how the <Logout Request/Response> message MUST be sent.
        "singleLogoutService": {
            // URL Location where the <LogoutRequest> from the IdP will be sent (IdP-initiated logout)
            "url": "https://<sp_domain>/?sls",
            // URL Location where the <LogoutResponse> from the IdP will sent (SP-initiated logout, reply)
            // OPTIONAL: only specify if different from url parameter
            //"responseUrl": "https://<sp_domain>/?sls",
            // SAML protocol binding to be used when returning the <Response>
            // message. SAML Toolkit supports the HTTP-Redirect binding
            // only for this endpoint.
            "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
        },
        // If you need to specify requested attributes, set a
        // attributeConsumingService. nameFormat, attributeValue and
        // friendlyName can be omitted
        "attributeConsumingService": {
                // OPTIONAL: only specify if SP requires this.
                // index is an integer which identifies the attributeConsumingService used
                // to the SP. SAML toolkit supports configuring only one attributeConsumingService
                // but in certain cases the SP requires a different value.  Defaults to '1'.
                // "index": '1',
                "serviceName": "SP test",
                "serviceDescription": "Test Service",
                "requestedAttributes": [
                    {
                        "name": "",
                        "isRequired": false,
                        "nameFormat": "",
                        "friendlyName": "",
                        "attributeValue": []
                    }
                ]
        },
        // Specifies the constraints on the name identifier to be used to
        // represent the requested subject.
        // Take a look on src/onelogin/saml2/constants.py to see the NameIdFormat that are supported.
        "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
        // Usually X.509 cert and privateKey of the SP are provided by files placed at
        // the certs folder. But we can also provide them with the following parameters
        "x509cert": "",
        "privateKey": ""

        /*
         * Key rollover
         * If you plan to update the SP X.509cert and privateKey
         * you can define here the new X.509cert and it will be
         * published on the SP metadata so Identity Providers can
         * read them and get ready for rollover.
         */
        // 'x509certNew': '',
    },

    // Identity Provider Data that we want connected with our SP.
    "idp": {
        // Identifier of the IdP entity  (must be a URI)
        "entityId": "https://app.onelogin.com/saml/metadata/<onelogin_connector_id>",
        // SSO endpoint info of the IdP. (Authentication Request protocol)
        "singleSignOnService": {
            // URL Target of the IdP where the Authentication Request Message
            // will be sent.
            "url": "https://app.onelogin.com/trust/saml2/http-post/sso/<onelogin_connector_id>",
            // SAML protocol binding to be used when returning the <Response>
            // message. SAML Toolkit supports the HTTP-Redirect binding
            // only for this endpoint.
            "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
        },
        // SLO endpoint info of the IdP.
        "singleLogoutService": {
            // URL Location where the <LogoutRequest> from the IdP will be sent (IdP-initiated logout)
            "url": "https://app.onelogin.com/trust/saml2/http-redirect/slo/<onelogin_connector_id>",
            // URL Location where the <LogoutResponse> from the IdP will sent (SP-initiated logout, reply)
            // OPTIONAL: only specify if different from url parameter
            "responseUrl": "https://app.onelogin.com/trust/saml2/http-redirect/slo_return/<onelogin_connector_id>",
            // SAML protocol binding to be used when returning the <Response>
            // message. SAML Toolkit supports the HTTP-Redirect binding
            // only for this endpoint.
            "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
        },
        // Public X.509 certificate of the IdP
        "x509cert": "<onelogin_connector_cert>"
        /*
         *  Instead of using the whole X.509cert you can use a fingerprint in order to
         *  validate a SAMLResponse (but you still need the X.509cert to validate LogoutRequest and LogoutResponse using the HTTP-Redirect binding).
         *  But take in mind that the algorithm for the fingerprint should be as strong as the algorithm in a normal certificate signature
	 *  (e.g. SHA256 or strong)
         *
         *  (openssl x509 -noout -fingerprint -in "idp.crt" to generate it,
         *  or add for example the -sha256 , -sha384 or -sha512 parameter)
         *
         *  If a fingerprint is provided, then the certFingerprintAlgorithm is required in order to
         *  let the toolkit know which algorithm was used.
         Possible values: sha1, sha256, sha384 or sha512
         *  'sha1' is the default value.
         *
         *  Notice that if you want to validate any SAML Message sent by the HTTP-Redirect binding, you
         *  will need to provide the whole X.509cert.
         */
        // "certFingerprint": "",
        // "certFingerprintAlgorithm": "sha1",

        /* In some scenarios the IdP uses different certificates for
         * signing/encryption, or is under key rollover phase and
         * more than one certificate is published on IdP metadata.
         * In order to handle that the toolkit offers that parameter.
         * (when used, 'X.509cert' and 'certFingerprint' values are
         * ignored).
         */
        // 'x509certMulti': {
        //      'signing': [
        //          '<cert1-string>'
        //      ],
        //      'encryption': [
        //          '<cert2-string>'
        //      ]
        // }
    }
}

In addition to the required settings data (idp, sp), extra settings can be defined in advanced_settings.json:

{
    // Security settings
    "security": {

        /** signatures and encryptions offered **/

        // Indicates that the nameID of the <samlp:logoutRequest> sent by this SP
        // will be encrypted.
        "nameIdEncrypted": false,

        // Indicates whether the <samlp:AuthnRequest> messages sent by this SP
        // will be signed.  [Metadata of the SP will offer this info]
        "authnRequestsSigned": false,

        // Indicates whether the <samlp:logoutRequest> messages sent by this SP
        // will be signed.
        "logoutRequestSigned": false,

        // Indicates whether the <samlp:logoutResponse> messages sent by this SP
        // will be signed.
        "logoutResponseSigned": false,

        /* Sign the Metadata
         false || true (use sp certs) || {
                                            "keyFileName": "metadata.key",
                                            "certFileName": "metadata.crt"
                                         }
        */
        "signMetadata": false,

        /** signatures and encryptions required **/

        // Indicates a requirement for the <samlp:Response>, <samlp:LogoutRequest>
        // and <samlp:LogoutResponse> elements received by this SP to be signed.
        "wantMessagesSigned": false,

        // Indicates a requirement for the <saml:Assertion> elements received by
        // this SP to be signed. [Metadata of the SP will offer this info]
        "wantAssertionsSigned": false,

        // Indicates a requirement for the <saml:Assertion>
        // elements received by this SP to be encrypted.
        "wantAssertionsEncrypted": false,

        // Indicates a requirement for the NameID element on the SAMLResponse
        // received by this SP to be present.
        "wantNameId": true,

        // Indicates a requirement for the NameID received by
        // this SP to be encrypted.
        "wantNameIdEncrypted": false,

        // Indicates a requirement for the AttributeStatement element
        "wantAttributeStatement": true,

        // Authentication context.
        // Set to false and no AuthContext will be sent in the AuthNRequest,
        // Set true or don't present this parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'
        // Set an array with the possible auth context values: array ('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509'),
        "requestedAuthnContext": true,
	// Allows the authn comparison parameter to be set, defaults to 'exact' if the setting is not present.
        "requestedAuthnContextComparison": "exact",
        // Set to true to check that the AuthnContext(s) received match(es) the requested.
        "failOnAuthnContextMismatch": false,

        // In some environment you will need to set how long the published metadata of the Service Provider gonna be valid.
        // is possible to not set the 2 following parameters (or set to null) and default values will be set (2 days, 1 week)
        // Provide the desire TimeStamp, for example 2015-06-26T20:00:00Z
        "metadataValidUntil": null,
        // Provide the desire Duration, for example PT518400S (6 days)
        "metadataCacheDuration": null,

        // If enabled, URLs with single-label-domains will
        // be allowed and not rejected by the settings validator (Enable it under Docker/Kubernetes/testing env, not recommended on production)
        "allowSingleLabelDomains": false,

        // Algorithm that the toolkit will use on signing process. Options:
        //    'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
        //    'http://www.w3.org/2000/09/xmldsig#dsa-sha1'
        //    'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
        //    'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384'
        //    'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512'
        "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",

        // Algorithm that the toolkit will use on digest process. Options:
        //    'http://www.w3.org/2000/09/xmldsig#sha1'
        //    'http://www.w3.org/2001/04/xmlenc#sha256'
        //    'http://www.w3.org/2001/04/xmldsig-more#sha384'
        //    'http://www.w3.org/2001/04/xmlenc#sha512'
        'digestAlgorithm': "http://www.w3.org/2001/04/xmlenc#sha256",

        // Specify if you want the SP to view assertions with duplicated Name or FriendlyName attributes to be valid
        // Defaults to false if not specified
        'allowRepeatAttributeName': false,

        // If the toolkit receive a message signed with a
        // deprecated algorithm (defined at the constant class)
        // will raise an error and reject the message
        "rejectDeprecatedAlgorithm": true
    },

    // Contact information template, it is recommended to suply a
    // technical and support contacts.
    "contactPerson": {
        "technical": {
            "givenName": "technical_name",
            "emailAddress": "[email protected]"
        },
        "support": {
            "givenName": "support_name",
            "emailAddress": "[email protected]"
        }
    },

    // Organization information template, the info in en_US lang is
    // recommended, add more if required.
    "organization": {
        "en-US": {
            "name": "sp_test",
            "displayname": "SP test",
            "url": "http://sp.example.com"
        }
    }
}

In the security section, you can set the way that the SP will handle the messages and assertions. Contact the admin of the IdP and ask them what the IdP expects, and decide what validations will handle the SP and what requirements the SP will have and communicate them to the IdP's admin too.

Once we know what kind of data could be configured, let's talk about the way settings are handled within the toolkit.

The settings files described (settings.json and advanced_settings.json) are loaded by the toolkit if not other dict with settings info is provided in the constructors of the toolkit. Let's see some examples.

# Initializes toolkit with settings.json & advanced_settings.json files.
auth = OneLogin_Saml2_Auth(req)
# or
settings = OneLogin_Saml2_Settings()

# Initializes toolkit with settings.json & advanced_settings.json files from a custom base path.
custom_folder = '/var/www/django-project'
auth = OneLogin_Saml2_Auth(req, custom_base_path=custom_folder)
# or
settings = OneLogin_Saml2_Settings(custom_base_path=custom_folder)

# Initializes toolkit with the dict provided.
auth = OneLogin_Saml2_Auth(req, settings_data)
# or
settings = OneLogin_Saml2_Settings(settings_data)

You can declare the settings_data in the file that contains the constructor execution or locate them in any file and load the file in order to get the dict available as we see in the following example:

filename = "/var/www/django-project/custom_settings.json" # The custom_settings.json contains a
json_data_file = open(filename, 'r')                      # settings_data dict.
settings_data = json.load(json_data_file)
json_data_file.close()

auth = OneLogin_Saml2_Auth(req, settings_data)

Metadata Based Configuration

The method above requires a little extra work to manually specify attributes about the IdP. (And your SP application)

There's an easier method -- use a metadata exchange. Metadata is just an XML file that defines the capabilities of both the IdP and the SP application. It also contains the X.509 public key certificates which add to the trusted relationship. The IdP administrator can also configure custom settings for an SP based on the metadata.

Using parse_remote IdP metadata can be obtained and added to the settings without further ado.

Take in mind that the OneLogin_Saml2_IdPMetadataParser class does not validate in any way the URL that is introduced in order to be parsed.

Usually the same administrator that handles the Service Provider also sets the URL to the IdP, which should be a trusted resource.

But there are other scenarios, like a SAAS app where the administrator of the app delegates this functionality to other users. In this case, extra precaution should be taken in order to validate such URL inputs and avoid attacks like SSRF.

idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://example.com/auth/saml2/idp/metadata')

You can specify a timeout in seconds for metadata retrieval, without it is not guaranteed that the request will complete

idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://example.com/auth/saml2/idp/metadata', timeout=5)

If the Metadata contains several entities, the relevant EntityDescriptor can be specified when retrieving the settings from the IdpMetadataParser by its entityId value:

idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(https://example.com/metadatas, entity_id='idp_entity_id')

How load the library

In order to use the toolkit library you need to import the file that contains the class that you will need on the top of your python file.

from onelogin.saml2.auth import OneLogin_Saml2_Auth
from onelogin.saml2.settings import OneLogin_Saml2_Settings
from onelogin.saml2.utils import OneLogin_Saml2_Utils

The Request

Building a OneLogin\_Saml2\_Auth object requires a request parameter:

auth = OneLogin_Saml2_Auth(req)

This parameter has the following scheme:

req = {
    "http_host": "",
    "script_name": "",
    "get_data": "",
    "post_data": "",

    # Advanced request options
    "https": "",
    "request_uri": "",
    "query_string": "",
    "validate_signature_from_qs": False,
    "lowercase_urlencoding": False
}

Each Python framework builds its own request object, you may map its data to match what the SAML toolkit expects. Let`s see some examples:

def prepare_from_django_request(request):
    return {
        'http_host': request.META['HTTP_HOST'],
        'script_name': request.META['PATH_INFO'],
        'get_data': request.GET.copy(),
        'post_data': request.POST.copy()
    }

def prepare_from_flask_request(request):
    url_data = urlparse(request.url)
    return {
        'http_host': request.netloc,
        'script_name': request.path,
        'get_data': request.args.copy(),
        'post_data': request.form.copy()
    }

An explanation of some advanced request parameters:

  • https - Defaults to off. Set this to on if you receive responses over HTTPS.

  • request_uri - The path where your SAML server receives requests. Set this if requests are not received at the server's root.

  • query_string - Set this with additional query parameters that should be passed to the request endpoint.

  • validate_signature_from_qs - If True, use query_string to validate request and response signatures. Otherwise, use get_data. Defaults to False. Note that when using get_data, query parameters need to be url-encoded for validation. By default we use upper-case url-encoding. Some IdPs, notably Microsoft AD, use lower-case url-encoding, which makes signature validation to fail. To fix this issue, either pass query_string and set validate_signature_from_qs to True, which works for all IdPs, or set lowercase_urlencoding to True, which only works for AD.

Initiate SSO

In order to send an AuthNRequest to the IdP:

from onelogin.saml2.auth import OneLogin_Saml2_Auth

req = prepare_request_for_toolkit(request)
auth = OneLogin_Saml2_Auth(req)   # Constructor of the SP, loads settings.json
                                  # and advanced_settings.json

auth.login()      # This method will build and return a AuthNRequest URL that can be
                  # either redirected to, or printed out onto the screen as a hyperlink

The AuthNRequest will be sent signed or unsigned based on the security info of the advanced_settings.json file (i.e. authnRequestsSigned).

The IdP will then return the SAML Response to the user's client. The client is then forwarded to the Assertion Consumer Service (ACS) of the SP with this information.

We can set a return_to url parameter to the login function and that will be converted as a RelayState parameter:

target_url = 'https://example.com'
auth.login(return_to=target_url)

The login method can receive 3 more optional parameters:

  • force_authn When true, the AuthNReuqest will set the ForceAuthn='true'
  • is_passive When true, the AuthNReuqest will set the Ispassive='true'
  • set_nameid_policy When true, the AuthNReuqest will set a nameIdPolicy element.

If a match on the future SAMLResponse ID and the AuthNRequest ID to be sent is required, that AuthNRequest ID must to be extracted and stored for future validation, we can get that ID by

auth.get_last_request_id()

The SP Endpoints

Related to the SP there are 3 important endpoints: The metadata view, the ACS view and the SLS view. The toolkit provides examples of those views in the demos, but let's see an example.

SP Metadata

This code will provide the XML metadata file of our SP, based on the info that we provided in the settings files.

req = prepare_request_for_toolkit(request)
auth = OneLogin_Saml2_Auth(req)
saml_settings = auth.get_settings()
metadata = saml_settings.get_sp_metadata()
errors = saml_settings.validate_metadata(metadata)
if len(errors) == 0:
    print(metadata)
else:
    print("Error found on Metadata: %s" % (', '.join(errors)))

The get_sp_metadata will return the metadata signed or not based on the security info of the advanced_settings.json (signMetadata).

Before the XML metadata is exposed, a check takes place to ensure that the info to be provided is valid.

Instead of using the Auth object, you can directly use

saml_settings = OneLogin_Saml2_Settings(settings=None, custom_base_path=None, sp_validation_only=True)

to get the settings object and with the sp_validation_only=True parameter we will avoid the IdP settings validation.

Assertion Consumer Service (ACS)

This code handles the SAML response that the IdP forwards to the SP through the user's client.

req = prepare_request_for_toolkit(request)
auth = OneLogin_Saml2_Auth(req)
auth.process_response()
errors = auth.get_errors()
if not errors:
    if auth.is_authenticated():
        request.session['samlUserdata'] = auth.get_attributes()
        if 'RelayState' in req['post_data'] and
          OneLogin_Saml2_Utils.get_self_url(req) != req['post_data']['RelayState']:
            # To avoid 'Open Redirect' attacks, before execute the redirection confirm
                # the value of the req['post_data']['RelayState'] is a trusted URL.
            auth.redirect_to(req['post_data']['RelayState'])
        else:
            for attr_name in request.session['samlUserdata'].keys():
                print('%s ==> %s' % (attr_name, '|| '.join(request.session['samlUserdata'][attr_name])))
    else:
      print('Not authenticated')
else:
    print("Error when processing SAML Response: %s %s" % (', '.join(errors), auth.get_last_error_reason()))

The SAML response is processed and then checked that there are no errors. It also verifies that the user is authenticated and stored the userdata in session.

At that point there are 2 possible alternatives:

  • If no RelayState is provided, we could show the user data in this view or however we wanted.
  • If RelayState is provided, a redirection takes place.

Notice that we saved the user data in the session before the redirection to have the user data available at the RelayState view.

In order to retrieve attributes we use:

attributes = auth.get_attributes()

With this method we get a dict with all the user data provided by the IdP in the assertion of the SAML response.

If we execute print attributes we could get:

{
    "cn": ["Jhon"],
    "sn": ["Doe"],
    "mail": ["Doe"],
    "groups": ["users", "members"]
}

Each attribute name can be used as a key to obtain the value. Every attribute is a list of values. A single-valued attribute is a list of a single element.

The following code is equivalent:

attributes = auth.get_attributes()
print(attributes['cn'])

print(auth.get_attribute('cn'))

Before trying to get an attribute, check that the user is authenticated. If the user isn't authenticated, an empty dict will be returned. For example, if we call to get_attributes before a auth.process_response, the get_attributes() will return an empty dict.

Single Logout Service (SLS)

This code handles the Logout Request and the Logout Responses.

delete_session_callback = lambda: request.session.flush()
url = auth.process_slo(delete_session_cb=delete_session_callback)
errors = auth.get_errors()
if len(errors) == 0:
    if url is not None:
        # To avoid 'Open Redirect' attacks, before execute the redirection confirm
        # the value of the url is a trusted URL.
        return redirect(url)
    else:
        print("Successfully Logged out")
else:
    print("Error when processing SLO: %s %s" % (', '.join(errors), auth.get_last_error_reason()))

If the SLS endpoints receives a Logout Response, the response is validated and the session could be closed, using the callback.

# Part of the process_slo method
logout_response = OneLogin_Saml2_Logout_Response(self.__settings, self.__request_data['get_data']['SAMLResponse'])
if not logout_response.is_valid(self.__request_data, request_id):
    self.__errors.append('invalid_logout_response')
elif logout_response.get_status() != OneLogin_Saml2_Constants.STATUS_SUCCESS:
    self.__errors.append('logout_not_success')
elif not keep_local_session:
    OneLogin_Saml2_Utils.delete_local_session(delete_session_cb)

If the SLS endpoints receives an Logout Request, the request is validated, the session is closed and a Logout Response is sent to the SLS endpoint of the IdP.

# Part of the process_slo method
request = OneLogin_Saml2_Utils.decode_base64_and_inflate(self.__request_data['get_data']['SAMLRequest'])
if not OneLogin_Saml2_Logout_Request.is_valid(self.__settings, request, self.__request_data):
    self.__errors.append('invalid_logout_request')
else:
    if not keep_local_session:
        OneLogin_Saml2_Utils.delete_local_session(delete_session_cb)

    in_response_to = request.id
    response_builder = OneLogin_Saml2_Logout_Response(self.__settings)
    response_builder.build(in_response_to)
    logout_response = response_builder.get_response()

    parameters = {'SAMLResponse': logout_response}
    if 'RelayState' in self.__request_data['get_data']:
        parameters['RelayState'] = self.__request_data['get_data']['RelayState']

    security = self.__settings.get_security_data()
    if 'logoutResponseSigned' in security and security['logoutResponseSigned']:
        parameters['SigAlg'] = OneLogin_Saml2_Constants.RSA_SHA1
        parameters['Signature'] = self.build_response_signature(logout_response, parameters.get('RelayState', None))

    return self.redirect_to(self.get_slo_url(), parameters)

If we don't want that process_slo to destroy the session, pass a true parameter to the process_slo method:

keepLocalSession = true
auth.process_slo(keep_local_session=keepLocalSession);

Initiate SLO

In order to send a Logout Request to the IdP:

The Logout Request will be sent signed or unsigned based on the security info of the advanced_settings.json (logoutRequestSigned).

The IdP will return the Logout Response through the user's client to the Single Logout Service (SLS) of the SP.

We can set a return_to url parameter to the logout function and that will be converted as a RelayState parameter:

target_url = 'https://example.com'
auth.logout(return_to=target_url)

Also there are another 5 optional parameters that can be set:

  • name_id: That will be used to build the LogoutRequest. If no name_id parameter is set and the auth object processed a SAML Response with a NameId, then this NameId will be used.
  • session_index: SessionIndex that identifies the session of the user.
  • nq: IDP Name Qualifier.
  • name_id_format: The NameID Format that will be set in the LogoutRequest.
  • spnq: The NameID SP NameQualifier will be set in the LogoutRequest.

If no name_id is provided, the LogoutRequest will contain a NameID with the entity Format. If name_id is provided and no name_id_format is provided, the NameIDFormat of the settings will be used.

If a match on the LogoutResponse ID and the LogoutRequest ID to be sent is required, that LogoutRequest ID must to be extracted and stored for future validation, we can get that ID by:

auth.get_last_request_id()

Example of a view that initiates the SSO request and handles the response (is the acs target)

We can code a unique file that initiates the SSO process, handle the response, get the attributes, initiate the SLO and processes the logout response.

Note: Review the demos, in a later section we explain the demo use case further in detail.

req = prepare_request_for_toolkit(request)  # Process the request and build the request dict that
                                            # the toolkit expects

auth = OneLogin_Saml2_Auth(req)             # Initialize the SP SAML instance

if 'sso' in request.args:                   # SSO action (SP-SSO initited).  Will send an AuthNRequest to the IdP
    return redirect(auth.login())
elif 'sso2' in request.args:                       # Another SSO init action
    return_to = '%sattrs/' % request.host_url      # but set a custom RelayState URL
    return redirect(auth.login(return_to))
elif 'slo' in request.args:                     # SLO action. Will sent a Logout Request to IdP
    nameid = request.session['samlNameId']
    nameid_format = request.session['samlNameIdFormat']
    nameid_nq = request.session['samlNameIdNameQualifier']
    nameid_spnq = request.session['samlNameIdSPNameQualifier']
    session_index = request.session['samlSessionIndex']
    return redirect(auth.logout(None, nameid, session_index, nameid_nq, nameid_format, nameid_spnq))
elif 'acs' in request.args:                 # Assertion Consumer Service
    auth.process_response()                     # Process the Response of the IdP
    errors = auth.get_errors()              # This method receives an array with the errors
    if len(errors) == 0:                    # that could took place during the process
        if not auth.is_authenticated():         # This check if the response was ok and the user
            msg = "Not authenticated"           # data retrieved or not (user authenticated)
        else:
            request.session['samlUserdata'] = auth.get_attributes()     # Retrieves user data
            request.session['samlNameId'] = auth.get_nameid()
            request.session['samlNameIdFormat'] = auth.get_nameid_format()
            request.session['samlNameIdNameQualifier'] = auth.get_nameid_nq()
            request.session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq()
            request.session['samlSessionIndex'] = auth.get_session_index()
            self_url = OneLogin_Saml2_Utils.get_self_url(req)
            if 'RelayState' in request.form and self_url != request.form['RelayState']:
                # To avoid 'Open Redirect' attacks, before execute the redirection confirm
                # the value of the request.form['RelayState'] is a trusted URL.
                return redirect(auth.redirect_to(request.form['RelayState']))   # Redirect if there is a relayState
            else:                           # If there is user data we save that to print it later.
                msg = ''
                for attr_name in request.session['samlUserdata'].keys():
                    msg += '%s ==> %s' % (attr_name, '|| '.join(request.session['samlUserdata'][attr_name]))
elif 'sls' in request.args:                                             # Single Logout Service
    delete_session_callback = lambda: session.clear()           # Obtain session clear callback
    url = auth.process_slo(delete_session_cb=delete_session_callback)   # Process the Logout Request & Logout Response
    errors = auth.get_errors()              #  Retrieves possible validation errors
    if len(errors) == 0:
        if url is not None:
            # To avoid 'Open Redirect' attacks, before execute the redirection confirm
            # the value of the url is a trusted URL.
            return redirect(url)
        else:
            msg = "Successfully logged out"

if len(errors) == 0:
  print(msg)
else:
  print(', '.join(errors))

SP Key rollover

If you plan to update the SP x509cert and privateKey you can define the new x509cert as settings['sp']['x509certNew'] and it will be published on the SP metadata so Identity Providers can read them and get ready for rollover.

IdP with multiple certificates

In some scenarios the IdP uses different certificates for signing/encryption, or is under key rollover phase and more than one certificate is published on IdP metadata.

In order to handle that the toolkit offers the settings['idp']['x509certMulti'] parameter.

When that parameter is used, x509cert and certFingerprint values will be ignored by the toolkit.

The x509certMulti is an array with 2 keys:

  • signing: An array of certs that will be used to validate IdP signature
  • encryption: An array with one unique cert that will be used to encrypt data to be sent to the IdP.

Replay attacks

In order to avoid replay attacks, you can store the ID of the SAML messages already processed, to avoid processing them twice. Since the Messages expires and will be invalidated due that fact, you don't need to store those IDs longer than the time frame that you currently accepting.

Get the ID of the last processed message/assertion with the get_last_message_id/get_last_assertion_id method of the Auth object.

Main classes and methods

Described below are the main classes and methods that can be invoked from the SAML2 library.

OneLogin_Saml2_Auth - auth.py

Main class of SAML Python Toolkit

  • __init__ Initializes the SP SAML instance.
  • login Initiates the SSO process.
  • logout Initiates the SLO process.
  • process_response Process the SAML Response sent by the IdP.
  • process_slo Process the SAML Logout Response / Logout Request sent by the IdP.
  • redirect_to Redirects the user to the url past by parameter or to the url that we defined in our SSO Request.
  • is_authenticated Checks if the user is authenticated or not.
  • get_attributes Returns the set of SAML attributes.
  • get_attribute Returns the requested SAML attribute.
  • get_nameid Returns the nameID.
  • get_session_index Gets the SessionIndex from the AuthnStatement.
  • get_session_expiration Gets the SessionNotOnOrAfter from the AuthnStatement.
  • get_errors Returns a list with code errors if something went wrong.
  • get_last_error_reason Returns the reason of the last error
  • get_sso_url Gets the SSO url.
  • get_slo_url Gets the SLO url.
  • get_last_request_id The ID of the last Request SAML message generated (AuthNRequest, LogoutRequest).
  • get_last_authn_contexts Returns the list of authentication contexts sent in the last SAML Response.
  • build_request_signature Builds the Signature of the SAML Request.
  • build_response_signature Builds the Signature of the SAML Response.
  • get_settings Returns the settings info.
  • set_strict Set the strict mode active/disable.
  • get_last_request_xml Returns the most recently-constructed/processed XML SAML request (AuthNRequest, LogoutRequest)
  • get_last_response_xml Returns the most recently-constructed/processed XML SAML response (SAMLResponse, LogoutResponse). If the SAMLResponse had an encrypted assertion, decrypts it.
  • get_last_response_in_response_to The InResponseTo ID of the most recently processed SAML Response.
  • get_last_message_id The ID of the last Response SAML message processed.
  • get_last_assertion_id The ID of the last assertion processed.
  • get_last_assertion_not_on_or_after The NotOnOrAfter value of the valid SubjectConfirmationData node (if any) of the last assertion processed (is only calculated with strict = true)
  • get_last_assertion_issue_instant The IssueInstant value of the last assertion processed.

OneLogin_Saml2_Auth - authn_request.py

SAML 2 Authentication Request class

  • __init__ This class handles an AuthNRequest. It builds an AuthNRequest object.
  • get_request Returns unsigned AuthnRequest.
  • get_id Returns the AuthNRequest ID.
  • get_xml Returns the XML that will be sent as part of the request.

OneLogin_Saml2_Response - response.py

SAML 2 Authentication Response class

  • __init__ Constructs the SAML Response object.
  • is_valid Determines if the SAML Response is valid. Includes checking of the signature by a certificate.
  • check_status Check if the status of the response is success or not
  • get_audiences Gets the audiences
  • get_issuers Gets the issuers (from message and from assertion)
  • get_nameid_data Gets the NameID Data provided by the SAML Response from the IdP (returns a dict)
  • get_nameid Gets the NameID provided by the SAML Response from the IdP (returns a string)
  • get_session_not_on_or_after Gets the SessionNotOnOrAfter from the AuthnStatement
  • get_session_index Gets the SessionIndex from the AuthnStatement
  • get_attributes Gets the Attributes from the AttributeStatement element.
  • validate_num_assertions Verifies that the document only contains a single Assertion (encrypted or not)
  • validate_timestamps Verifies that the document is valid according to Conditions Element
  • get_error After execute a validation process, if fails this method returns the cause
  • get_xml_document Returns the SAML Response document (If contains an encrypted assertion, decrypts it).
  • get_id the ID of the response
  • get_assertion_id the ID of the assertion in the response
  • get_assertion_not_on_or_after the NotOnOrAfter value of the valid SubjectConfirmationData if any

OneLogin_Saml2_LogoutRequest - logout_request.py

SAML 2 Logout Request class

  • __init__ Constructs the Logout Request object.
  • get_request Returns the Logout Request deflated, base64-encoded.
  • get_id Returns the ID of the Logout Request. (If you have the object you can access to the id attribute)
  • get_nameid_data Gets the NameID Data of the the Logout Request (returns a dict).
  • get_nameid Gets the NameID of the Logout Request Message (returns a string).
  • get_issuer Gets the Issuer of the Logout Request Message.
  • get_session_indexes Gets the SessionIndexes from the Logout Request.
  • is_valid Checks if the Logout Request received is valid.
  • get_error After execute a validation process, if fails this method returns the cause.
  • get_xml Returns the XML that will be sent as part of the request or that was received at the SP

OneLogin_Saml2_LogoutResponse - logout_response.py

SAML 2 Logout Response class

  • __init__ Constructs a Logout Response object.
  • get_issuer Gets the Issuer of the Logout Response Message
  • get_status Gets the Status of the Logout Response.
  • is_valid Determines if the SAML LogoutResponse is valid
  • build Creates a Logout Response object.
  • get_response Returns a Logout Response object.
  • get_error After execute a validation process, if fails this method returns the cause.
  • get_xml Returns the XML that will be sent as part of the response or that was received at the SP

OneLogin_Saml2_Settings - settings.py

Configuration of the SAML Python Toolkit

  • __init__ Initializes the settings: Sets the paths of the different folders and Loads settings info from settings file or array/object provided.
  • check_settings Checks the settings info.
  • check_idp_settings Checks the IdP settings info.
  • check_sp_settings Checks the SP settings info.
  • get_errors Returns an array with the errors, the array is empty when the settings is ok.
  • get_sp_metadata Gets the SP metadata. The XML representation.
  • validate_metadata Validates an XML SP Metadata.
  • get_base_path Returns base path.
  • get_cert_path Returns cert path.
  • get_lib_path Returns lib path.
  • get_ext_lib_path Returns external lib path.
  • get_schemas_path Returns schema path.
  • check_sp_certs Checks if the X.509 certs of the SP exists and are valid.
  • get_sp_key Returns the X.509 private key of the SP.
  • get_sp_cert Returns the X.509 public cert of the SP.
  • get_sp_cert_new Returns the future X.509 public cert of the SP.
  • get_idp_cert Returns the X.509 public cert of the IdP.
  • get_sp_data Gets the SP data.
  • get_idp_data Gets the IdP data.
  • get_security_data Gets security data.
  • get_contacts Gets contacts data.
  • get_organization Gets organization data.
  • format_idp_cert Formats the IdP cert.
  • format_idp_cert_multi Formats all registered IdP certs.
  • format_sp_cert Formats the SP cert.
  • format_sp_cert_new Formats the SP cert new.
  • format_sp_key Formats the private key.
  • set_strict Activates or deactivates the strict mode.
  • is_strict Returns if the strict mode is active.
  • is_debug_active Returns if the debug is active.

OneLogin_Saml2_Metadata - metadata.py

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

  • builder Generates the metadata of the SP based on the settings.
  • sign_metadata Signs the metadata with the key/cert provided.
  • add_x509_key_descriptors Adds the X.509 descriptors (sign/encryption) to the metadata

OneLogin_Saml2_Utils - utils.py

Auxiliary class that contains several methods

  • decode_base64_and_inflate Base64 decodes and then inflates according to RFC1951.
  • deflate_and_base64_encode Deflates and the base64 encodes a string.
  • format_cert Returns a X.509 cert (adding header & footer if required).
  • format_private_key Returns a private key (adding header & footer if required).
  • redirect Executes a redirection to the provided url (or return the target url).
  • get_self_url_host Returns the protocol + the current host + the port (if different than common ports).
  • get_self_host Returns the current host.
  • is_https Checks if https or http.
  • get_self_url_no_query Returns the URL of the current host + current view.
  • get_self_routed_url_no_query Returns the routed URL of the current host + current view.
  • get_self_url Returns the URL of the current host + current view + query.
  • generate_unique_id Generates an unique string (used for example as ID for assertions).
  • parse_time_to_SAML Converts a UNIX timestamp to SAML2 timestamp on the form yyyy-mm-ddThh:mm:ss(.s+)?Z.
  • parse_SAML_to_time Converts a SAML2 timestamp on the form yyyy-mm-ddThh:mm:ss(.s+)?Z to a UNIX timestamp.
  • now Returns unix timestamp of actual time.
  • parse_duration Interprets a ISO8601 duration value relative to a given timestamp.
  • get_expire_time Compares 2 dates and returns the earliest.
  • delete_local_session Deletes the local session.
  • calculate_X.509_fingerprint Calculates the fingerprint of a X.509 cert.
  • format_finger_print Formats a fingerprint.
  • generate_name_id Generates a nameID.
  • get_status Gets Status from a Response.
  • decrypt_element Decrypts an encrypted element.
  • write_temp_file Writes some content into a temporary file and returns it.
  • add_sign Adds signature key and senders certificate to an element (Message or Assertion).
  • validate_sign Validates a signature (Message or Assertion).
  • validate_binary_sign Validates signed bynary data (Used to validate GET Signature).

OneLogin_Saml2_XML- xml_utils.py

A class that contains methods to handle XMLs

  • to_string Serialize an element to an encoded string representation of its XML tree.
  • to_etree Parses an XML document or fragment from a string.
  • validate_xml Validates a xml against a schema
  • query Extracts nodes that match the query from the Element
  • extract_tag_text

OneLogin_Saml2_IdPMetadataParser - idp_metadata_parser.py

A class that contains methods to obtain and parse metadata from IdP

  • get_metadata Get the metadata XML from the provided URL
  • parse_remote Get the metadata XML from the provided URL and parse it, returning a dict with extracted data
  • parse Parse the Identity Provider metadata and returns a dict with extracted data
  • merge_settings Will update the settings with the provided new settings data extracted from the IdP metadata

For more info, look at the source code. Each method is documented and details about what does and how to use it are provided. Make sure to also check the doc folder where HTML documentation about the classes and methods is provided.

Demos included in the toolkit

The toolkit includes 3 demos to teach how use the toolkit (A Django, Flask and a Tornado project), take a look on it. Demos require that SP and IdP are well configured before test it, so edit the settings files.

Notice that each python framework has it own way to handle routes/urls and process request, so focus on how it deployed. New demos using other python frameworks are welcome as a contribution.

Getting Started

We said that this toolkit includes a Django application demo and a Flask application demo, let's see how fast is it to deploy them.

Virtualenv

The use of a virtualenv is highly recommended.

Virtualenv helps isolating the python environment used to run the toolkit. You can find more details and an installation guide in the official documentation.

Once you have your virtualenv ready and loaded, then you can install the toolkit executing this:

 make install-req

Demo Flask

You'll need a virtualenv with the toolkit installed on it.

To run the demo you need to install the requirements first. Load your virtualenv and execute:

 pip install -r demo-flask/requirements.txt

This will install flask and its dependencies. Once it has finished, you have to complete the configuration of the toolkit. You'll find it at demo-flask/settings.json

Now, with the virtualenv loaded, you can run the demo like this:

 cd demo-flask
 python index.py

You'll have the demo running at http://localhost:8000

Content

The flask project contains:

  • index.py Is the main flask file, where or the SAML handle take place.

  • templates. Is the folder where flask stores the templates of the project. It was implemented a base.html template that is extended by index.html and attrs.html, the templates of our simple demo that shows messages, user attributes when available and login and logout links.

  • saml Is a folder that contains the 'certs' folder that could be used to store the X.509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json).

SP setup

The SAML Python Toolkit allows you to provide the settings info in 2 ways: Settings files or define a setting dict. In the demo-flask, it uses the first method.

In the index.py file we define the app.config['SAML_PATH'], that will target to the saml folder. We require it in order to load the settings files.

First we need to edit the saml/settings.json file, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt.

IdP setup

Once the SP is configured, the metadata of the SP is published at the /metadata url. Based on that info, configure the IdP.

How it works

  1. First time you access to the main view (http://localhost:8000), you can select to login and return to the same view or login and be redirected to /?attrs (attrs view).

  2. When you click:

    2.1 in the first link, we access to /?sso (index view). An AuthNRequest is sent to the IdP, we authenticate at the IdP and then a Response is sent through the user's client to the SP, specifically the Assertion Consumer Service view: /?acs. Notice that a RelayState parameter is set to the url that initiated the process, the index view.

    2.2 in the second link we access to /?attrs (attrs view), we will expetience have the same process described at 2.1 with the diference that as RelayState is set the attrs url.

  3. The SAML Response is processed in the ACS /?acs, if the Response is not valid, the process stops here and a message is shown. Otherwise we are redirected to the RelayState view. a) / or b) /?attrs

  4. We are logged in the app and the user attributes are showed. At this point, we can test the single log out functionality.

The single log out functionality could be tested by 2 ways.

5.1 SLO Initiated by SP. Click on the ``logout`` link at the SP, after that a Logout Request is sent to the IdP, the session at the IdP is closed and replies through the client to the SP with a Logout Response (sent to the Single Logout Service endpoint). The SLS endpoint ``/?sls`` of the SP process the Logout Response and if is valid, close the user session of the local app. Notice that the SLO Workflow starts and ends at the SP.

5.2 SLO Initiated by IdP. In this case, the action takes place on the IdP side, the logout process is initiated at the IdP, sends a Logout Request to the SP (SLS endpoint, ``/?sls``). The SLS endpoint of the SP process the Logout Request and if is valid, close the session of the user at the local app and send a Logout Response to the IdP (to the SLS endpoint of the IdP). The IdP receives the Logout Response, process it and close the session at of the IdP. Notice that the SLO Workflow starts and ends at the IdP.

Notice that all the SAML Requests and Responses are handled at a unique view (index) and how GET parameters are used to know the action that must be done.

Demo Tornado

You'll need a virtualenv with the toolkit installed on it.

First of all you need some packages, execute:

apt-get install libxml2-dev libxmlsec1-dev libxmlsec1-openssl

To run the demo you need to install the requirements first. Load your virtualenv and execute:

 pip install -r demo-tornado/requirements.txt

This will install tornado and its dependencies. Once it has finished, you have to complete the configuration of the toolkit. You'll find it at demo-tornado/saml/settings.json

Now, with the virtualenv loaded, you can run the demo like this:

 cd demo-tornado
 python views.py

You'll have the demo running at http://localhost:8000

Content

The tornado project contains:

  • views.py Is the main flask file, where or the SAML handle take place.

  • settings.py Contains the base path and the path where is located the saml folder and the template folder

  • templates. Is the folder where tornado stores the templates of the project. It was implemented a base.html template that is extended by index.html and attrs.html, the templates of our simple demo that shows messages, user attributes when available and login and logout links.

  • saml Is a folder that contains the 'certs' folder that could be used to store the X.509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json).

SP setup

The SAML Python Toolkit allows you to provide the settings info in 2 ways: Settings files or define a setting dict. In the demo-tornado, it uses the first method.

In the settings.py file we define the SAML_PATH, that will target to the saml folder. We require it in order to load the settings files.

First we need to edit the saml/settings.json file, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt.

IdP setup

Once the SP is configured, the metadata of the SP is published at the /metadata url. Based on that info, configure the IdP.

How it works

  1. First time you access to the main view (http://localhost:8000), you can select to login and return to the same view or login and be redirected to /?attrs (attrs view).

  2. When you click:

    2.1 in the first link, we access to /?sso (index view). An AuthNRequest is sent to the IdP, we authenticate at the IdP and then a Response is sent through the user's client to the SP, specifically the Assertion Consumer Service view: /?acs. Notice that a RelayState parameter is set to the url that initiated the process, the index view.

    2.2 in the second link we access to /?attrs (attrs view), we will expetience have the same process described at 2.1 with the diference that as RelayState is set the attrs url.

  3. The SAML Response is processed in the ACS /?acs, if the Response is not valid, the process stops here and a message is shown. Otherwise we are redirected to the RelayState view. a) / or b) /?attrs

  4. We are logged in the app and the user attributes are showed. At this point, we can test the single log out functionality.

The single log out functionality could be tested by 2 ways.

5.1 SLO Initiated by SP. Click on the ``logout`` link at the SP, after that a Logout Request is sent to the IdP, the session at the IdP is closed and replies through the client to the SP with a Logout Response (sent to the Single Logout Service endpoint). The SLS endpoint ``/?sls`` of the SP process the Logout Response and if is valid, close the user session of the local app. Notice that the SLO Workflow starts and ends at the SP.

5.2 SLO Initiated by IdP. In this case, the action takes place on the IdP side, the logout process is initiated at the IdP, sends a Logout Request to the SP (SLS endpoint, ``/?sls``). The SLS endpoint of the SP process the Logout Request and if is valid, close the session of the user at the local app and send a Logout Response to the IdP (to the SLS endpoint of the IdP). The IdP receives the Logout Response, process it and close the session at of the IdP. Notice that the SLO Workflow starts and ends at the IdP.

Notice that all the SAML Requests and Responses are handled at a unique view (index) and how GET parameters are used to know the action that must be done.

Demo Django

You'll need a virtualenv with the toolkit installed on it.

To run the demo you need to install the requirements first. Load your virtualenv and execute:

 pip install -r demo-django/requirements.txt

This will install django and its dependencies. Once it has finished, you have to complete the configuration of the toolkit.

Later, with the virtualenv loaded, you can run the demo like this:

 cd demo-django
 python manage.py runserver 0.0.0.0:8000

You'll have the demo running at http://localhost:8000.

Note that many of the configuration files expect HTTPS. This is not required by the demo, as replacing these SP URLs with HTTP will work just fine. HTTPS is however highly encouraged, and left as an exercise for the reader for their specific needs.

If you want to integrate a production django application, take a look on this SAMLServiceProviderBackend that uses our toolkit to add SAML support: https://github.com/KristianOellegaard/django-saml-service-provider

Content

The django project contains:

  • manage.py. A file that is automatically created in each Django project. Is a thin wrapper around django-admin.py that takes care of putting the project’s package on sys.path and sets the DJANGO_SETTINGS_MODULE environment variable.

  • saml Is a folder that contains the 'certs' folder that could be used to store the X.509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json).

  • demo Is the main folder of the django project, that contains the typical files:

    • settings.py Contains the default parameters of a django project except the SAML_FOLDER parameter, that may contain the path where is located the saml folder.
    • urls.py A file that define url routes. In the demo we defined '/' that is related to the index view, '/attrs' that is related with the attrs view and '/metadata', related to the metadata view.
    • views.py This file contains the views of the django project and some aux methods.
    • wsgi.py A file that let as deploy django using WSGI, the Python standard for web servers and applications.
  • templates. Is the folder where django stores the templates of the project. It was implemented a base.html template that is extended by index.html and attrs.html, the templates of our simple demo that shows messages, user attributes when available and login and logout links.

SP setup

The SAML Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In the demo-django it used the first method.

After set the SAML_FOLDER in the demo/settings.py, the settings of the Python toolkit will be loaded on the Django web.

First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt.

IdP setup

Once the SP is configured, the metadata of the SP is published at the /metadata url. Based on that info, configure the IdP.

How it works

This demo works very similar to the flask-demo (We did it intentionally).

Getting up and running on Heroku

Getting python3-saml up and running on Heroku will require some extra legwork: python3-saml depends on python-xmlsec which depends on headers from the xmlsec1-dev Linux package to install correctly.

First you will need to add the apt buildpack to your build server:

heroku buildpacks:add --index=1 -a your-app heroku-community/apt
heroku buildpacks:add --index=2 -a your-app heroku/python

You can confirm the buildpacks have been added in the correct order with heroku buildpacks -a your-app, you should see the apt buildpack first followed by the Python buildpack.

Then add an Aptfile into the root of your repository containing the libxmlsec1-dev package, the file should look like:

libxmlsec1-dev

Finally, add python3-saml to your requirements.txt and git push to trigger a build.

Demo Pyramid

Unlike the other two projects, you don't need a pre-existing virtualenv to get up and running here, since Pyramid comes from the buildout school of thought.

To run the demo you need to install Pyramid, the requirements, etc.:

 cd demo_pyramid
 python3 -m venv env
 env/bin/pip install --upgrade pip setuptools
 env/bin/pip install -e ".[testing]"

If you want to make sure the tests pass, run:

 env/bin/pytest

Next, edit the settings in demo_pyramid/saml/settings.json. (Pyramid runs on port 6543 by default.)

Now you can run the demo like this:

 env/bin/pserve development.ini

If that worked, the demo is now running at http://localhost:6543.

Content

The Pyramid project contains:

  • __init__.py is the main Pyramid file that configures the app and its routes.

  • views.py is where all the SAML handling takes place.

  • templates is the folder where Pyramid stores the templates of the project. It was implemented a layout.jinja2 template that is extended by index.jinja2 and attrs.jinja2, the templates of our simple demo that shows messages, user attributes when available and login and logout links.

  • saml is a folder that contains the 'certs' folder that could be used to store the X.509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json).

SP setup

The SAML Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In demo_pyramid the first method is used.

In the views.py file we define the SAML_PATH, which will target the saml folder. We require it in order to load the settings files.

First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt.

IdP setup

Once the SP is configured, the metadata of the SP is published at the /metadata url. Based on that info, configure the IdP.

How it works

  1. First time you access to the main view (http://localhost:6543), you can select to login and return to the same view or login and be redirected to /?attrs (attrs view).

  2. When you click:

    2.1 in the first link, we access to /?sso (index view). An AuthNRequest is sent to the IdP, we authenticate at the IdP and then a Response is sent through the user's client to the SP, specifically the Assertion Consumer Service view: /?acs. Notice that a RelayState parameter is set to the url that initiated the process, the index view.

    2.2 in the second link we access to /?attrs (attrs view), we will experience the same process described at 2.1 with the diference that as RelayState is set the attrs url.

  3. The SAML Response is processed in the ACS /?acs, if the Response is not valid, the process stops here and a message is shown. Otherwise we are redirected to the RelayState view. a) / or b) /?attrs

  4. We are logged in the app and the user attributes are showed. At this point, we can test the single log out functionality.

The single log out functionality could be tested by 2 ways.

5.1 SLO Initiated by SP. Click on the "logout" link at the SP, after that a Logout Request is sent to the IdP, the session at the IdP is closed and replies through the client to the SP with a Logout Response (sent to the Single Logout Service endpoint). The SLS endpoint /?sls of the SP process the Logout Response and if is valid, close the user session of the local app. Notice that the SLO Workflow starts and ends at the SP.

5.2 SLO Initiated by IdP. In this case, the action takes place on the IdP side, the logout process is initiated at the IdP, sends a Logout Request to the SP (SLS endpoint, /?sls). The SLS endpoint of the SP process the Logout Request and if is valid, close the session of the user at the local app and send a Logout Response to the IdP (to the SLS endpoint of the IdP). The IdP receives the Logout Response, process it and close the session at of the IdP. Notice that the SLO Workflow starts and ends at the IdP.

Notice that all the SAML Requests and Responses are handled at a unique view (index) and how GET parameters are used to know the action that must be done.

python3-saml's People

Contributors

aidanlister avatar akx avatar beritjanssen avatar bgaifullin avatar bmorgan21 avatar bmwiedemann avatar bzvestey avatar cclauss avatar charlescbeebe avatar daxxog avatar dependabot[bot] avatar dzanin avatar fuhrysteve avatar gkhaburzaniya-onelogin avatar guneskaan avatar jgehrcke avatar jmahoney-eab avatar kipparker avatar mtyaka avatar nosnilmot avatar not-ol-github avatar op-codento avatar palbee avatar pitbulk avatar quantus avatar samm0ss avatar thelinuxkid avatar toopy avatar up_the_irons avatar y-trobinso avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

python3-saml's Issues

Server port is not always reliable under django and nginx

Hi everyone. I've been having an issue with the redirect url, my setup is more or less this one (I'm using docker, but this is not related to it):

  1. docker container with the backend running under uwsgi
  2. docker container with nginx acting as a server proxy to the first container

The nginx container uses the default port 80 meanwhile the uwsgi is using the port 8000.

Here is the function that creates the redirect url: https://github.com/onelogin/python3-saml/blob/master/src/onelogin/saml2/utils.py#L226

I can see that it is using the server_port value, which can be wrong sometimes, like in this case.

Django 1.9 has a get_port function which takes care of the specific settings https://github.com/django/django/blob/6f1318734f0f3b6e62b782b0251a4e676e542e0b/django/http/request.py#L112

I'm not sure if this can be fixed via a nginx configuration, I've tried a couple of times with no luck.

I'm happy to submit a PR but I need some guidance on the project since I'm fairly new to it :)

IdP-initiated SLO does not work with Python3

SLO does not function properly. Probably only affects Python3.

Symptoms:

calling OneLogin_Saml2_Auth.process_slo() leads to:

  • in the log: invalid_logout_request
  • defusedxml writes into stdout: Unicode strings with encoding declaration are not supported. Please use bytes input or XML fragments without declaration.

Cause

logout_request.py:115:

self.__logout_request = compat.to_string(logout_request)

Afterwards python3-saml in OneLogin_Saml2_XML.to_etree() calls defusedxml.lxml.fromstring with text argument containing a str instance (unicode in Python3), which causes the error.

Notes:

defusedxml.lxml.fromstring is actually called twice during OneLogin_Saml2_Auth.process_slo().

  • First time while extracting ID of the request as far as I can remember. It passes correct arguments to defusedxml.lxml.fromstring.
  • Second time while validating the request. This time with invalid arguments which causes defusedxml to fail.

Possible solution:

Replacing in logout_request.py:115

self.__logout_request = compat.to_string(logout_request)

with

self.__logout_request = logout_request

solves the problem and makes SLO to function.

I am however not sure why compat.to_string() was used and which side effects removing it might cause.

Versions

python3-saml==1.3.0
defusedxml==0.5.0
lxml==4.1.1
xmlsec==1.3.3

ImportError: lxml.etree does not export expected C function adoptExternalDocument

Encountered this issue when running this library within a Linux environment/container (python:3.6-slim):

app_1  | + python3 manage.py migrate
app_1  | Traceback (most recent call last):
app_1  |   File "manage.py", line 22, in <module>
app_1  |     execute_from_command_line(sys.argv)
app_1  |   File "/usr/local/lib/python3.6/site-packages/django/core/management/__init__.py", line 363, in execute_from_command_line
app_1  |     utility.execute()
app_1  |   File "/usr/local/lib/python3.6/site-packages/django/core/management/__init__.py", line 355, in execute
app_1  |     self.fetch_command(subcommand).run_from_argv(self.argv)
app_1  |   File "/usr/local/lib/python3.6/site-packages/django/core/management/base.py", line 283, in run_from_argv
app_1  |     self.execute(*args, **cmd_options)
app_1  |   File "/usr/local/lib/python3.6/site-packages/django/core/management/base.py", line 327, in execute
app_1  |     self.check()
app_1  |   File "/usr/local/lib/python3.6/site-packages/django/core/management/base.py", line 359, in check
app_1  |     include_deployment_checks=include_deployment_checks,
app_1  |   File "/usr/local/lib/python3.6/site-packages/django/core/management/commands/migrate.py", line 62, in _run_checks
app_1  |     issues.extend(super(Command, self)._run_checks(**kwargs))
app_1  |   File "/usr/local/lib/python3.6/site-packages/django/core/management/base.py", line 346, in _run_checks
app_1  |     return checks.run_checks(**kwargs)
app_1  |   File "/usr/local/lib/python3.6/site-packages/django/core/checks/registry.py", line 81, in run_checks
app_1  |     new_errors = check(app_configs=app_configs)
app_1  |   File "/usr/local/lib/python3.6/site-packages/django/core/checks/urls.py", line 16, in check_url_config
app_1  |     return check_resolver(resolver)
app_1  |   File "/usr/local/lib/python3.6/site-packages/django/core/checks/urls.py", line 26, in check_resolver
app_1  |     return check_method()
app_1  |   File "/usr/local/lib/python3.6/site-packages/django/urls/resolvers.py", line 254, in check
app_1  |     for pattern in self.url_patterns:
app_1  |   File "/usr/local/lib/python3.6/site-packages/django/utils/functional.py", line 35, in __get__
app_1  |     res = instance.__dict__[self.name] = self.func(instance)
app_1  |   File "/usr/local/lib/python3.6/site-packages/django/urls/resolvers.py", line 405, in url_patterns
app_1  |     patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
app_1  |   File "/usr/local/lib/python3.6/site-packages/django/utils/functional.py", line 35, in __get__
app_1  |     res = instance.__dict__[self.name] = self.func(instance)
app_1  |   File "/usr/local/lib/python3.6/site-packages/django/urls/resolvers.py", line 398, in urlconf_module
app_1  |     return import_module(self.urlconf_name)
app_1  |   File "/usr/local/lib/python3.6/importlib/__init__.py", line 126, in import_module
app_1  |     return _bootstrap._gcd_import(name[level:], package, level)
app_1  |   File "<frozen importlib._bootstrap>", line 994, in _gcd_import
app_1  |   File "<frozen importlib._bootstrap>", line 971, in _find_and_load
app_1  |   File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
app_1  |   File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
app_1  |   File "<frozen importlib._bootstrap_external>", line 678, in exec_module
app_1  |   File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
app_1  |   File "/app/customer_success_site/urls.py", line 23, in <module>
app_1  |     url(r'', include('tools.urls')),
app_1  |   File "/usr/local/lib/python3.6/site-packages/django/conf/urls/__init__.py", line 50, in include
app_1  |     urlconf_module = import_module(urlconf_module)
app_1  |   File "/usr/local/lib/python3.6/importlib/__init__.py", line 126, in import_module
app_1  |     return _bootstrap._gcd_import(name[level:], package, level)
app_1  |   File "<frozen importlib._bootstrap>", line 994, in _gcd_import
app_1  |   File "<frozen importlib._bootstrap>", line 971, in _find_and_load
app_1  |   File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
app_1  |   File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
app_1  |   File "<frozen importlib._bootstrap_external>", line 678, in exec_module
app_1  |   File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
app_1  |   File "/app/tools/urls.py", line 2, in <module>
app_1  |     from . import views
app_1  |   File "/app/tools/views.py", line 19, in <module>
app_1  |     from .saml import init_saml_auth, prepare_django_request
app_1  |   File "/app/tools/saml.py", line 4, in <module>
app_1  |     from onelogin.saml2.auth import OneLogin_Saml2_Auth
app_1  |   File "/usr/local/lib/python3.6/site-packages/onelogin/saml2/auth.py", line 14, in <module>
app_1  |     import xmlsec
app_1  | ImportError: lxml.etree does not export expected C function adoptExternalDocument

Not sure what's the issue here, but noticed that it uninstalled lxml 4.1.1 when I tried pip install:

Installing collected packages: beautifulsoup4, pytz, Django, gunicorn, htmlmin, lxml, mysqlclient, numpy, six, python-dateutil, pandas, psycopg2, rcssmin, requests, rjsmin, SQLAlchemy, xlrd, XlsxWriter, ujson, MarkupSafe, Jinja2, isodate, defusedxml, python3-saml
  Found existing installation: lxml 4.1.1
    Uninstalling lxml-4.1.1:
      Successfully uninstalled lxml-4.1.1
Successfully installed Django-1.11 Jinja2-2.9.6 MarkupSafe-1.0 SQLAlchemy-1.1.9 XlsxWriter-0.9.8 beautifulsoup4-4.6.0 defusedxml-0.5.0 gunicorn-19.7.1 htmlmin-0.1.10 isodate-0.6.0 lxml-3.7.3 mysqlclient-1.3.10 numpy-1.12.1 pandas-0.21.0 psycopg2-2.7.1 python-dateutil-2.6.0 python3-saml-1.3.0 pytz-2017.2 rcssmin-1.0.6 requests-2.14.2 rjsmin-1.0.12 six-1.10.0 ujson-1.35 xlrd-1.0.0

Feature support: SLO ResponseLocation and NameID NameQualifier

Hi,

I need 2 features that I don't see implemented, unless I didn't find them.

a) Our IdP uses ResponseLocation in addition to Location for its SingleLogoutService.
I.E. its metadata contains:

<ns0:SingleLogoutService
  Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
  Location="https://<idp url>/idp/saml2/slo"
  ResponseLocation="https://<idp url>/idp/saml2/slo_return"  # this extra URL
/>

But it seems that using this second URL (optional in the SAML spec) isn't supported in python3-saml.

To implement it, it seems that we would need to:

  • Support the response location URL in OneLogin_Saml2_Settings.
  • Update logic in OneLogin_Saml2_Auth.process_slo in the case of SAMLResponse to use that URL when available or fall back on the current one.

b) Our IdP seems to require NameQualifier to be passed as an attribute of NameID in the LogoutRequest resulting from a logout initiated on the SP side.

<saml:NameID
    Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
    # NameQualifier missing here
>transient id
</saml:NameID>

I see that the NameQualifier is included in the login assertion, and I think we can access it and keep track of it in our session. For the rest of the implementation we would have to:

  • Modify OneLogin_Saml2_Auth.logout to accept name_qualifier and pass it to OneLogin_Saml2_Logout_Request.
  • Modify OneLogin_Saml2_Logout_Request to accept name_qualifier and pass it to OneLogin_Saml2_Utils.generate_name_id.
  • Modify OneLogin_Saml2_Utils.generate_name_id to accept name_qualifier and use it when building the XML element.

Would supporting these features fit with the project vision?
Would you implement them, or accept pull requests matching the high level implementations described above?

Thank you.
Pierre

RelayState Should Not Be Required in Request

According to the SAML Profile for Single Sign-On AuthnRequests, the Service Provider MAY send a RelayState along with the request, but that it's not required. See section 4.1.3.1 of this document:
https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf

However, in OneLogin_Saml2_Auth.login() (in auth.py) this parameter is always set, even if return_to is a blank string. For example, a redirect_url returned by login() could look like:

http://acme.onelogin.com/saml/login?SAMLRequest=<b64_encoded_req>&RelayState=

This could potentially be misleading for Identity Providers that detect the RelayState parameter in the request only to discover that it's blank.

I propose that the RelayState parameter should not be set at all if return_to is None, or an empty string (''). If you think that's a reasonable fix, I'll create a PR for you. 🙂

Undocumented "req" Parameters

There are 3 undocumented parameters that could potentially be passed as part the req dict used to initialize the OneLogin_Saml2_Auth client. These are:

  1. https - Unsuspecting users could potentially see validation of responses fail if they're received over HTTPS and the user didn't explicitly configure that (since the default is http)

  2. lowercase_urlencoding - There's a short comment in utils.py describing why this is an option, but users may not be aware of this option if they're using ADFS

  3. request_uri - Used in utils.py#L339-L380 to build self URLs

In the documentation users should at least be aware of these options, what they're for, and when they should be used. Ideally no one should have to read your source code to use your client. 🙂

Include SAML extensions in AuthnRequest

I've asked this in SO but got no response, and from looking at the code I could not find the correct API.

I need to include an Extensions block in AuthnRequests, for example:

<Extensions>
  <stork:QualityAuthenticationAssuranceLevel xmlns:stork="urn:oasis:names:tc:SAML:2.0:metadata">3</stork:QualityAuthenticationAssuranceLevel>
  <fa:RequestedAttributes xmlns:fa="http://autenticacao.cartaodecidadao.pt/atributos">
    <fa:RequestedAttribute Name="http://interop.gov.pt/MDC/Cidadao/NIC" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="True" />
    <fa:RequestedAttribute Name="http://interop.gov.pt/MDC/Cidadao/NIF" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="False" />
    <fa:RequestedAttribute Name="http://interop.gov.pt/MDC/Cidadao/Foto" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="False" />
    <fa:RequestedAttribute Name="http://interop.gov.pt/MDC/Cidadao/Morada" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="False" />
  </fa:RequestedAttributes>
  <fa:FAAALevel xmlns:fa="http://autenticacao.cartaodecidadao.pt/atributos">3</fa:FAAALevel>
</Extensions>

I have the schema of these elements, for example:

 <xs:schema targetNamespace="http://autenticacao.cartaodecidadao.pt/atributos" xmlns="http://autenticacao.cartaodecidadao.pt/atributos" xmlns:xs="http://www.w3.org/2001/XMLSchema" >
  <xs:element name="FAAALevel">
     <xs:simpleType>
       <xs:restriction base="xs:integer">
          <xs:minInclusive value="1"/>
          <xs:maxInclusive value="4"/>
       </xs:restriction>
     </xs:simpleType>
  </xs:element>
</xs:schema>

Does python3-saml allow me to include these blocks in AuthnRequests?

XML metadata parser does not expose binding type

The metadata parser does not set the binding key in the singleSignOnService dictionary, it only stores the url key. A settings consumer cannot guess which binding is to be used, so we need to add the binding.

Authentication failed: SAML login failed: ['invalid_response']

The response URL is adding a port number to the URL.

(The response was received at https://www.site.com:80/complete/saml/ instead of https://www.site.com/complete/saml/)

I am connecting to a ADFS Server. Is this a SP issue? or a IDP setting issue?

There is no AttributeStatement on the Response

Is it necessary to require an Attribute Statement in the SAML Response?

When setting up a SAML app in Okta (the Identity Provider in this case), the form says that configuring Attribute Statements is optional. That leaves us open to the above error though. I've also run into this issue when using Centrify as an IdP.

So would it be possible to remove this check and what would be the implications of that? Thanks for the help!

Reference: Check out the Attribute Statements section in step 7
http://developer.okta.com/docs/guides/setting_up_a_saml_application_in_okta.html

Better instructions for Heroku

It's a serious pain getting this package up and running on Heroku!

This seems like it should work, but I get fatal error: xmlsec/xmltree.h: No such file or directory

As a fabfile.py for reproducability:

@task
def configure_as_build_server(app):
    local('heroku buildpacks:set --index=1 -a {app} https://github.com/ddollar/heroku-buildpack-apt'.format(app=app))
    local('heroku buildpacks:set --index=2 -a {app} https://github.com/heroku/heroku-buildpack-python'.format(app=app))

With the following Aptfile:

libxml2-dev
libxmlsec1-dev

Could there be some documentation added?

csrf django issue

We have csrf middelware enabeled.
It appears it's not happy with the saml request.

Error:
CSRF token missing or incorrect is the error in django

How can this be fixed?

serviceName and serviceDescription are not xml-escaped

When building metadata XML from config dict, the keys serviceName and serviceDescription are included using %s in XML string concatenation.

If they contain for example & they will generate a malformed XML document and libxml2 will error out.

They should be xml encoded before being included in the resulting xml document.

This might affect also other values ?

Note: This can introduce a breaking change if people noticed that, and provide an xml-encoded value in serviceName and serviceDescription, resulting in a double encoding.

One response test doesn't behave in a reasonable way

While writing PR #37 I noticed that the test testOnlyRetrieveAssertionWithIDThatMatchesSignatureReference behaves in a really odd way. The self.assertTrue(response.is_valid(self.get_request_data())) line raises an AssertionException that moves the execution to the line self.assertEqual(.... That leaves these lines that never get executed. The test function comment says that the function should tests get_nameid function, but it doesn't even get called during that test.

I would except that each test have just one execution path that can lead to test passing, but that is not the case with this tests. I don't feel like I understand this test enough to offer a fix for this test case.

SAML Response with a duplicate attribute name

Hello,

I've been looking into why an error is thrown when an attribute element has duplicated name when in the XML spec this approach is used to for an attribute to have multiple values. I've used your online tool for validating a SAML response [0] that has an attribute element with a duplicate name and this comes back as a valid response.

After further debugging I found that the error is thrown in src/response.py in the get_attributes method. I also looked at the commit where this change took place aeb25b. The commit message indates security improvements.

I'm looking for some clarity on why this change was necessary. I understand there are things I am probably not accounting for but my main goal is a discussion on the change so I can take this back to my Identity Provider or if needed implement a fix.

Tested SAML response

       <saml:AttributeStatement>
            <saml:Attribute Name="Role">
                <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
                                     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                                     xsi:type="xs:string"
                                     >Employee</saml:AttributeValue>
            </saml:Attribute>
            <saml:Attribute Name="Role">
                <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
                                     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                                     xsi:type="xs:string"
                                     >Matthew Owens</saml:AttributeValue>
            </saml:Attribute>
            <saml:Attribute Name="Role">
                <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
                                     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                                     xsi:type="xs:string"
                                     >Users</saml:AttributeValue>
            </saml:Attribute>
            <saml:Attribute Name="Role">
                <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
                                     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                                     xsi:type="xs:string"
                                     >authenticated</saml:AttributeValue>
            </saml:Attribute>
        </saml:AttributeStatement>

[0] - https://www.samltool.com/validate_xml.php

Questions about python-saml and python3-saml

Hi,

I am adding SAML SSO to a project and am evaluating python-saml and python3-saml.

So far my project is Python 2/3 compatible and I would like to keep it that way, unless I have a
strong reason to change it.

According to their respective setup.py files, python-saml is Python 2 only while python3-saml is
Python 2 and 3, so using python3-saml seems like a good solution for me.

What is the relationship between the 2 projects? It seems that python-saml is more active and more
used than python3-saml. Are features and fixes applied to the first one before getting applied to
the second one later?

For production code, can we rely on python3-saml or should we use python-saml?

I have been using the Pypi version of python3-saml, which is version 1.0.0.
That version has a bug which I am running into which is fixed in the following commit:

commit c0a2081aa167aa3b5fb99e8de9f3c4636ca88ed4
Author: Patrick Arminio <[email protected]>
Date:   Fri Jan 8 11:32:19 2016 +0100

    Made make_root, make_child and cleanup_namespaces static

diff --git a/src/onelogin/saml2/xml_utils.py b/src/onelogin/saml2/xml_utils.py
index bc7500d..f7f590b 100644
--- a/src/onelogin/saml2/xml_utils.py
+++ b/src/onelogin/saml2/xml_utils.py
@@ -27,9 +27,9 @@ class OneLogin_Saml2_XML(object):
     _unparse_etree = staticmethod(etree.tostring)

     dump = staticmethod(etree.dump)
-    make_root = etree.Element
-    make_child = etree.SubElement
-    cleanup_namespaces = etree.cleanup_namespaces
+    make_root = staticmethod(etree.Element)
+    make_child = staticmethod(etree.SubElement)
+    cleanup_namespaces = staticmethod(etree.cleanup_namespaces)

     @staticmethod
     def to_string(xml, **kwargs):

Python-saml doesn't seem to have that issue.

Is a new version of python3-saml planned to include this fix?

Thank you.

Extracting session index from logout request

When handling a logout request, there doesn't seem to be a simple way to extract/obtain the session index from a logout request.

Is there such an API available to obtain this information?

Doing auth.get_session_index() returns None.

Getting error <audience_url> is not a valid audience for this Response

I have found working with this package and onelogin a bit odd at time. Day before yesterday I setup every thing properly, got everything running as well. I saved the configuration from Configuration tab in web portal. Now, when I am trying to login I am getting error saying "XYZ is not a valid audience for this Response". I mean how does it work one time and refuses to work next time with same configuration is a bit odd for me. After tweaking with settings this is how I got it to work first time. But now same set of configuration is not working anymore.

SAML Consumer URL
http://mydomain/assertion-consumer-service/

SAML Single Logout URL
http://mydomain/logout/

SAML Audience
http://mydomain/

SAML Recipient
http://mydomain/assertion-consumer-service/

XML metadata parser does not allow for SSO POST binding extraction

Right now, the parser only extracts one SSO URL (fine), and only for the redirect binding (that's a severe limitation). We should expose a parameter to instruct the parser to either extract the first

  • SSO URL with redirect binding or the first
  • SSO URL with POST binding

Impossible to catch exceptions that aren't OneLogin_Saml2_Error

I'm having an issue where the library is throwing an Exception that is of the base Exception type. I want to handle errors gracefully that occur within this library, but don't really want to catch every single exception that occurs.

Is there a particular reason this library uses OneLogin_Saml2_Error in some places, but others, like in https://github.com/onelogin/python3-saml/blob/master/src/onelogin/saml2/response.py#L301, just use Exception? Would it be possible to introduce a new class of Exception which is used in place of these so that they can be caught more easily without catching all Exception types.

Cheers

Api to extract response ids?

What is the recommended method to detect and stop replay attacts in the IdP initiated scenario?

My initial idea was to persist all previously seen Response and Assertion ids ignore responses that contain preciously seen Response or Assertion ids. Unfortunately I've not been able to find any good way to extract these ids from the response. They seem to be added to a local variable called "verified_ids" but there is no way to access it.

Single Logout Service never called

Hi,

When I log out of my Django application that is a service provider, I am correctly redirected to the logout page of the IDP that I am using (Salesforce in this case). However, when I then navigate back to my site (i.e. the service provider), I am not presented with the login page, but instead it seems like I'm already logged in. I then figured out that my singleLogoutService url is never being hit, and so auth.process_slo() is never being executed (so my session is not being deleted). Do you know what could be causing this?

Thanks.

How to fix destination validation?

Hello!

I am using your python3-saml module and I have some misunderstanding with ports, when implementing SAML2 SSO for our app. The validation of the destination fails because there is a validation of the current_url here and it constructs that url in utils. But for some reason the port in the current_url is not default, but 8080.

I understand, that this port is extracted from our side, but this does not have much sense, since the request itself is made on the default url https://example.com/sso/saml2/?acs, but our IDP must have ACS url with port 8080 in his SAMLResponse https://example.com:8080/sso/saml2/?acs to be able to pass this validation.

So, I have a misunderstanding here. What exactly this validation checks? Is it possible to disable it and do I increase the security risk in this case?

Thank you.

Running cert validation tests segfaults

All of the relevant information should be there:

vagrant@dev:/vagrant/python3-saml$ sudo apt-get update
vagrant@dev:/vagrant/python3-saml$ sudo apt-get upgrade
vagrant@dev:/vagrant/python3-saml$ sudo apt-get install libxml2-dev libxslt1-dev pkg-config libxmlsec1-dev gdb git
vagrant@dev:/vagrant/python3-saml$ sudo pip install xmlsec
vagrant@dev:/vagrant/python3-saml$ sudo pip install isodate
vagrant@dev:/vagrant/python3-saml$ sudo pip install -e '.[test]'
vagrant@dev:/vagrant/python3-saml$ python setup.py test
...
testValidateSign (tests.src.OneLogin.saml2_tests.utils_test.OneLogin_Saml2_Utils_Test) ... Segmentation fault (core dumped)
vagrant@dev:/vagrant/python3-saml$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION="Ubuntu 14.04.4 LTS"
vagrant@dev:/vagrant/python3-saml$ pip -V
pip 7.0.2 from /usr/local/lib/python2.7/dist-packages/pip-7.0.2-py2.7.egg (python 2.7)
vagrant@dev:/vagrant/python3-saml$ python -V
Python 2.7.6
vagrant@dev:/vagrant/python3-saml$ gdb python
...
(gdb) run setup.py test
...
testValidateSign (tests.src.OneLogin.saml2_tests.utils_test.OneLogin_Saml2_Utils_Test) ... 
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff4dd21da in xmlSecKeyDataIdListFindByNode () from /usr/lib/libxmlsec1.so.1
(gdb) quit
A debugging session is active.

    Inferior 1 [process 18736] will be killed.

Quit anyway? (y or n) y
vagrant@dev:/vagrant/python3-saml$ pip freeze
...
isodate==0.5.4
xmlsec==0.5.0
...

Local ADFS Server Settings

Does any one have any info as to the setting i need to put in the settings file to point Python SAML at a local adfs server?

OneLogin_Saml2_Utils.add_sign Removes the xmlns:xs Namespace, breaking XML schema validation

Problem

When using OneLogin_Saml2_Utils.add_sign the xmlns:xs namespace gets removed from the <Assertion/> element. I suspect this happens here: https://github.com/onelogin/python3-saml/blob/master/src/onelogin/saml2/xml_utils.py#L49

As a result, this causes a response signed by this utility to fail validation when passed to OneLogin_Saml2_Auth.process_response. Specifically, this is because the xsi:type="xs:string" is not recognized when validating the response against the AuthnResponse schema.

Solution

Don't remove the xmlns:xs namespace during the call to OneLogin_Saml2_XML.to_string method used by add_sign.

Reply address is empty, Azure AD SAML application

Hi I'm playing with flask demo and I still receive this error.

AADSTS50011: Reply address '' specified by the request is not a valid URL. Allowed schemes: 'http,https,ms-app'

Could you please tell me where to start debugging?
I got successful redirect to signin page but then the error.

my config in AAD:

"replyUrls": [
"https://localhost:9876/callback"
],

Let me know if you need more.

Malformed XML results in a lxml.etree.XMLSyntaxError exception

After creating a OneLogin_Saml2_Auth object with a request that contains malformed XML and calling process_response(), the call will raise a lxml.etree.XMLSyntaxError exception.

This is reasonable behavior, but I think it could be improved. I think that in general, libraries should try to avoid raising exceptions that their dependencies raise. There is already a OneLogin_Saml2_Error exception that is raised in various error cases. What would you think about catching lxml.etree.Error exceptions in OneLogin_Saml2_XML and raising a OneLogin_Saml2_Error exception with a new error code for XML parse failures?

The status code of the Response was not Success, was Responder

Instead of a "Success" status after login with ADFS, we're getting the following error: "The status code of the Response was not Success, was Responder". In the python-saml project issues, someone had this same issue and it was caused by requestedAuthnContext being set to true in advanced_settings. I have set it to false, yet I still get this error. Are there any more settings I should check in my app before asking the IDP ADFS person to check their logs?

Thanks.

lxml ValueError exception in process_slo()

SP is using python3-saml version 1.2.6
IdP is Shibboleth Identity Provider version 3.3.0

When calling process_slo() the following exception is thrown:

    logout_response = OneLogin_Saml2_Logout_Response(self.__settings, get_data['SAMLResponse'])
  File "/opt/env/lib/python3.6/site-packages/onelogin/saml2/logout_response.py", line 41, in __init__
    self.document = OneLogin_Saml2_XML.to_etree(self.__logout_response)
  File "/opt/env/lib/python3.6/site-packages/onelogin/saml2/xml_utils.py", line 66, in to_etree
    return OneLogin_Saml2_XML._parse_etree(xml)
  File "/opt/env/lib/python3.6/site-packages/defusedxml/lxml.py", line 143, in fromstring
    rootelement = _etree.fromstring(text, parser, base_url=base_url)
  File "src/lxml/lxml.etree.pyx", line 3228, in lxml.etree.fromstring (src/lxml/lxml.etree.c:79609)
  File "src/lxml/parser.pxi", line 1843, in lxml.etree._parseMemoryDocument (src/lxml/lxml.etree.c:119069)
ValueError: Unicode strings with encoding declaration are not supported. Please use bytes input or XML fragments without declaration.

As the exception says the SLO SAMLResponse contain an encoding tag, i.e. <?xml version="1.0" encoding="UTF-8"?> which isn't allowed when feeding etree.fromstring with a unicode string.

Not sure what the best solution would be in this case, but changing compat.to_string()
to compat.to_bytes() seem to work:
https://github.com/onelogin/python3-saml/blob/16cd67c0efa329ac016abdb8471cb0d654a68825/src/onelogin/saml2/logout_response.py#L40

auth.get_session_expiration()

I'm trying to retrieve the NotOnOrAfter value from the response using the get_session_expiration() inside of auth.py. The only thing done here was I've added this request to the Flask index.py right after receiving the attributes, name_id and session_index to demonstrate this problem.

    elif 'acs' in request.args:
        auth.process_response()
        errors = auth.get_errors()
        not_auth_warn = not auth.is_authenticated()
        if len(errors) == 0:
            session['samlUserdata'] = auth.get_attributes()
            session['samlNameId'] = auth.get_nameid()
            session['samlSessionIndex'] = auth.get_session_index()
            session['samlSessionExpiration'] = auth.get_session_expiration()

The variable session['samlSessionExpiration'] returns None instead of the date/time. I know this works within the response.py as it is used to authenticate the IdP response. Why would this return "None" instead of the date in the auth.py?

IdPMetadataParser.merge_settings() does a shallow merge

The current merger uses Python's dict update() method which operates only on the first hierarchy level, it does not do a deep merge of nested dictionaries. This mode of operation is insufficient for the settings merger (i.e. it does not fulfill what's currently documented: "Will update the settings with the provided new settings data extracted from the IdP metadata").

"All Rights Reserved"

The LICENSE file claims MIT, however all the source files include the following:

Copyright (c) 2014, OneLogin, Inc.
All rights reserved.

Please update the source file headers to include the license text, and not say "All rights reserved" to clarify. The year should likely be updated as well to include all the years in which code was committed (and thus published) under this copyright.

This same issue also affects python-saml tree.

Validation errors not shown

Hello!

For some reason the validation errors of the response here, are not shown. I only get a list with a single "invalid response" message.

I am trying to guess, that it is due to the fact, that during the validation all the error messages are assigned to the response object, (i.e. response.__errors) but the self.__errors does not get the actual validation errors from the response object.

It would be nice to fix it, since this kind of debugging takes lots of time. I need to use pdb to find out the exact validation error.

Thank you.

Redirection URL is not always returned by `auth.process_slo`

In auth.py, the docs for the function process_slo state the following:
:returns: Redirection url

This is not the case in the processing of the SAMLResponse (lhttps://github.com/onelogin/python3-saml/blame/master/src/onelogin/saml2/auth.py#L143) part of the function (although it is in the SAMLRequest part, https://github.com/onelogin/python3-saml/blame/master/src/onelogin/saml2/auth.py#L159). This means that a RelayState can only be provided in a IdP-initiated logout.

In the readme (under Initiate SLO) the following example code is provided:

target_url = 'https://example.com'
auth.logout(return_to=target_url)

This suggests that the RelayState is actually an important part of the Service Provider initiated SLO.

An easy solution would be to add the following code at line 158:

if 'RelayState' in self.__request_data['get_data']:
    return self.redirect_to()

I would create a pull request with these changes myself, but I am not entirely sure if this is intended behaviour or if this change should indeed be made.

HTTP_X_FORWARDED

Hi Onelogin team,

I'm attempting to run the demo-flask code however my application is configured behind nginx to allow for encryption over https. In the source code there is a note on HTTP_X_FORWARDED fields, and I don't believe I have these configured. I've tried searching to see if there was additional information but found nothing on github. Could you provide more information on this note? How would I change the configuration of the app and/or nginx to accomplish this?

I've created print() calls to help with troubleshooting. It looks like the application believes the protocol is http on port 443 instead of using https.

def prepare_flask_request(request):
    # If server is behind proxys or balancers use the HTTP_X_FORWARDED fields
    url_data = urlparse(request.url)
    print (request.url)
    print (url_data)

OUTPUT FROM LOG FILES: 
http://example.com:443/?acs
ParseResult(scheme='http', netloc='example.com:443', path='/', params='', query='acs', fragment='')

IDP in python3-saml

Hi, Can I create IDP by using python3-saml. I read the documentation and could not found any related page.

win7 python3-saml install error

How to solve this problem for Very anxious。thank you
LINK : fatal error LNK1159: no output file specified
error: command 'C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\VC\Tools\MSVC\14.13.26128\bin\HostX86\x64\link.exe' failed with exit status 1159

Missing code snippet in response.py resulting in signature verification failed

There seems to be a difference between python-saml and python3-saml in response.py:

See these two commits:
SAML-Toolkits/python-saml@3eb11bc
and
a509c88

in python-saml, there is this snippet of code, which is missing in python3-saml:

multicerts = None
if 'x509certMulti' in idp_data and 'signing' in idp_data['x509certMulti'] and idp_data['x509certMulti']['signing']:
    multicerts = idp_data['x509certMulti']['signing']

This results in a signature validation failed error because python3-saml incorrectly tries to read x509cert which is empty when there are multiple certificates.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.