# SAML Triggers The [SAML](https://wiki.oasis-open.org/security/FrontPage) support in Helix requires two parts: 1) the desktop agent that handles the SAML login request with the identity provider (IdP), and 2) the triggers that facilitate initiating the request and processing the response. This document is concerned with the installation and configuration of those triggers. ## Requirements * Python 2.7 or Python 3.6+ * `libxml2` and `xmlsec` libraries * Compiler tools (e.g. `gcc`) * libtool library (`ltdl`) ## Installation The triggers are written in [Python](https://www.python.org) and rely on the [onelogin/python3-saml](https://github.com/onelogin/python3-saml) Python module, which in turn requires several other modules, including native libraries. The Python modules can all be installed using the [pip](https://pypi.org/project/pip/) utility, which can be installed like so: ```shell $ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py $ python get-pip.py ``` Instructions for installation *without* using pip are available below. The required native libraries are [libxml2](http://xmlsoft.org) and [xmlsec](https://www.aleksey.com/xmlsec/). These are usually available as packages on the popular Linux distributions. Examples are given below for [Ubuntu](https://www.ubuntu.com) Linux. ### Python 2.7 Depending on your operating system, there is distribution-specific preliminary steps, followed by the platform-independent Python module installation. #### CentOS 6 Installing Python 2.7 on CentOS 6 involves using the Software Collections, and the related `scl` tool. Some of the Python modules require the compiler tools, and so we install the `development tools` group. The `ltdl` library is used for building the modules. ```shell $ sudo yum groupinstall -y "development tools" $ sudo yum install libtool-ltdl-devel $ sudo yum install centos-release-scl $ sudo yum install python27 python27-python-devel python27-python-virtualenv $ sudo yum install libxml2-devel xmlsec1-devel xmlsec1-openssl $ scl enable python27 bash ``` #### Ubuntu 16 ```shell $ sudo apt-get install python2.7 python2.7-dev python-virtualenv $ sudo apt-get install libxml2-dev libxmlsec1-dev libxmlsec1-openssl ``` #### Common ```shell $ python -m virtualenv --python=python2.7 pysaml $ source pysaml/bin/activate $ pip install --upgrade pip $ pip install -r requirements.txt ``` This example creates a Python virtual environment, which is not required, but helps to keep the dependencies isolated from the rest of the system. ### Python 3.x #### CentOS 6 Installing Python 3.6 on CentOS 6 involves using the Software Collections, and the related `scl` tool. Some of the Python modules require the compiler tools, and so we install the `development tools` group. The `ltdl` library is used for building the modules. ```shell $ sudo yum groupinstall -y "development tools" $ sudo yum install libtool-ltdl-devel $ sudo yum install centos-release-scl $ sudo yum install rh-python36 rh-python36-python-devel rh-python36-python-virtualenv $ sudo yum install libxml2-devel xmlsec1-devel xmlsec1-openssl $ scl enable rh-python36 bash ``` #### Ubuntu 16 ```shell $ sudo apt-get install python3 python3-dev python3-virtualenv $ sudo apt-get install libxml2-dev libxmlsec1-dev libxmlsec1-openssl ``` #### Common ```shell $ python3 -m virtualenv --python=python3 pysaml $ source pysaml/bin/activate $ pip install --upgrade pip $ pip install -r requirements.txt ``` This example creates a Python virtual environment, which is not required, but helps to keep the dependencies isolated from the rest of the system. ### Installing without using pip The Python dependencies can be installed without the use of pip, if, for instance, your security policies require reviewing the software prior to installation. #### Prerequisites To install the Python modules from source, you will need to have the [setuptools](https://pypi.org/project/setuptools/) module installed. If you create a Python virtual environment, setuptools will be provided automatically. #### Installation Procedure To start, download the compressed source for each of the required modules: * https://pypi.org/project/requests/ * https://pypi.org/project/six/ * https://pypi.org/project/lxml/ * https://pypi.org/project/xmlsec/ * https://pypi.org/project/isodate/ * https://pypi.org/project/defusedxml/ * https://pypi.org/project/python3-saml/ Next, extract each of those in turn, running the following sequence of commands, replacing `module` with the appropriate module name and version: ``` $ tar zxf module.tar.gz $ cd module $ python setup.py build $ python setup.py install ``` ## Configuration The SAML configuration consists of two parts, that of the identity provider (IdP), and that of the service provider (SP). The service provider in this scenario is these trigger scripts; they act as the service provider, creating the login request URL and validating the SAML response. The configuration for the service provider is defined in `settings.json` and `advanced_settings.json`, which are described in more detail below. As for the IdP configuration, there are two choices: the easiest is to use the IdP metadata URL, from which the triggers will request the IdP configuration. The second option is to configure the IdP via the `settings.json` file. The advantage of using the URL is that the IdP may change its certificate from time to time, and that is automatically conveyed via the metadata. The advantage of using the settings file is that the trigger does not need to fetch a resource from the IdP every time a user logs in. ### Using the metadata URL Usually the IdP web site will provide a URL for its configuration, referred to as "metadata". Find that URL and use it to set the `auth.sso.args` Perforce configuration setting, then add `%ssoArgs%` to the trigger entry in Perforce. For example: ```shell $ p4 configure set auth.sso.args=--idpUrl=http://192.168.24.3:7000/metadata $ p4 triggers -o Triggers: saml-pre auth-pre-sso auth "/ps/pysaml/bin/python /ps/saml_pre_sso.py %ssoArgs% %email%" saml-sso auth-check-sso auth "/ps/pysaml/bin/python /ps/saml_validate.py %ssoArgs% %email%" saml-slo auth-invalidate auth "/ps/pysaml/bin/python /ps/saml_logout.py %ssoArgs% %email%" ``` ### Using the settings file Add something like the following to the `settings.json` file in order to define the IdP configuration. Note that this file is in [JSON](https://www.json.org) format, so be sure to use double-quotes for both names and values, and add commas between name/value pairs. The `"idp"` definition is at the same level as the `"sp"` definition that already exists in the settings file. ```json { "sp": { ... }, "idp": { "entityId": "urn:example:idp", "singleSignOnService": { "url": "http://192.168.24.3:7000/saml/sso", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, "singleLogoutService": { "url": "http://192.168.24.3:7000/saml/slo", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, "certFingerprint": "", "certFingerprintAlgorithm": "sha1", "x509cert": "-----BEGIN CERTIFICATE-----\n!!FILL IN THIS BLANK!!\n-----END CERTIFICATE-----\n" } } ``` Additional information regarding the settings file is available on the [python3-saml](https://github.com/onelogin/python3-saml) project page. ### SAML configuration All of the various SAML details are configured in the `settings.json` and `advanced_settings.json` files. These two files should be in the same directory as the Python triggers. Most of the settings are self-explanatory, with additional information available on the [python3-saml](https://github.com/onelogin/python3-saml) project page. The most important settings are those related to the definition of the "service provider", which must match that found in the application configuration within the identity provider. That is, both the trigger configuration and the IdP must agree on the URLs, signing keys, issuer name, and so on. For instance, the **Recipient** will be the `url` value under `assertionConsumerService` in the `settings.json` file, and **Audience** will be the `entityId` value from that same file. The **Relay State** field, if any, can be left blank. ### Certificates If the identity provider expects login and/or logout requests to be signed by the service provider, then the X.509 certificates should be installed in a directory named `certs`, in the same directory as the Python triggers. The files are named `sp.crt` and `sp.key` (public certificate and private key, respectively). To create self-signed certificates valid for one year, use the `openssl` command, like so: ```shell $ openssl req -new -x509 -days 365 -nodes -out sp.crt -keyout sp.key ``` ### Configure non-SSO access Before installing the SSO triggers, make sure that you have administrative access that does not require using SSO. Once the SSO triggers are installed, all authentication will require using SSO, even for administrators. One approach would be to create a Perforce group whose `Timeout` is `unlimited`, then add the administrator to that group, and then login as the administrator to acquire the new long-lived ticket. Another approach would be to enable non-SSO logins, by setting the `auth.sso.allow.passwd` configurable to `1`; note, however, that once this is enabled, every Perforce user must have a password defined in the server. This is normally the case when the `security` level is set to 3 or higher. After making changes to the security settings, you will need to restart the server (e.g. `p4dctl stop despot` and `p4dctl start despot`). ### Defining the Triggers Once the trigger scripts are in place, and the configuration files have been updated, you can define the trigger entries in Helix Server. They should look similar to the following, replacing the paths to `python` and the triggers with whatever is appropriate for your system: ``` saml-pre auth-pre-sso auth "/path/pysaml/bin/python /path/saml_pre_sso.py %ssoArgs% %email%" saml-sso auth-check-sso auth "/path/pysaml/bin/python /path/saml_validate.py %ssoArgs% %email%" saml-slo auth-invalidate auth "/path/pysaml/bin/python /path/saml_logout.py %ssoArgs% %email%" ``` This example assumes that the SAML name identifier is the user's email address, hence the `%email%`. If instead the IdP is using a username, and the Perforce user names match what is configured in the IdP, then change the `%email%` to `%user%` in the example above. After adding the triggers, you will need to restart the server (e.g. `p4dctl stop despot` and `p4dctl start despot`). #### Logout with Okta If your IdP services are provided by [Okta](https://www.okta.com) then you will want to adjust the arguments to the `saml_logout.py` script slightly, by adding `--okta` between `saml_logout.py` and `%ssoArgs%` in the trigger table entry. This informs the trigger to use the Okta API to perform the logout, as that works better for our scenario which is operating outside of the web browser. #### CentOS If using CentOS and the Software Collections, the trigger definitions above would be changed to look something like this: ``` saml-pre auth-pre-sso auth "scl enable python27 -- /path/pysaml/bin/python /path/saml_pre_sso.py %ssoArgs% %email%" saml-sso auth-check-sso auth "scl enable python27 -- /path/pysaml/bin/python /path/saml_validate.py %ssoArgs% %email%" saml-slo auth-invalidate auth "scl enable python27 -- /path/pysaml/bin/python /path/saml_logout.py %ssoArgs% %email%" ``` Use `python27` for the 2.7 version, or `rh-python36` for the 3.6 version. ## Swarm Integration When using Swarm with SAML integration enabled, the trigger invocation will need to be adjusted slightly. In the trigger table entry for `auth-check-sso`, add the option `--no-id` after `saml_validate.py`. This will disable the request identifier verification in the trigger, since the initial login request is generated in Swarm. ## Details The `auth-pre-sso` trigger `saml_pre_sso.py` is used to generate the SAML login URL. This is printed to standard output, and delivered to the desktop agent specified in the `P4LOGINSSO` environment setting on the client. The trigger can sign the request using private keys, if so configured. The `auth-check-sso` trigger `saml_validate.py` receives the SAML response from the desktop agent and validates that it matches expectations. This includes verifying that the response is associated with the request initiated by `saml_pre_sso.py`, that the `NameID` matches the user value (either `%email%` or `%user%`, whichever was provided to the trigger), and that the response is otherwise a valid SAML response (e.g. time constraints, signature). The `auth-invalidate` trigger `saml_logout.py` is invoked by `p4d` when the user invokes `p4 logout`, and is used to send a SAML logout request to the IdP. This happens "behind the scenes", and does not involve the desktop agent. The trigger generates the logout request using the `NameID`, session index, and browser cookies provided by the agent during the login process. The logout request can be signed, if the trigger is so configured. ## Troubleshooting The `saml_logout.py` trigger provides some basic logging, which is enabled with the `--debug` command line option. The trigger will write to `~/saml/logs` directory by default. Each time the trigger runs it overwrites the previous log file, so this is really only useful for debugging.