Setting up Alfresco SAML authentication with LemonLDAP::NG

cancel
Showing results for 
Search instead for 
Did you mean: 

Setting up Alfresco SAML authentication with LemonLDAP::NG

alxgomz
Alfresco Employee
8 3 8,977

Introduction

Alfresco recently released a new module that allow modern SSO to be setup using the SAML protocol. SAML is a standard which  has a set of specifications defined by the OASIS consortium.

Like kerberos SAML is considered a secure approach to SSO, it involves signing messages and possibly encrypting them ; but unlike kerberos - which is more targeted to local networks or VPNs extended networks - SAML is really good fit for internet and SaaS services. Mainly SAML requires an Identity Provider (often referred to as IdP) and a Service Provider (SP) to communicate together. Many cloud services offer the SAML Service Provider features, and even sometimes the IdP feature (for example google: Set up your own custom SAML application - G Suite Administrator Help).

LemonLDAP::NG is an open-source software that is actually a handler for the httpd Apache webserver. LemonLDAP::NG supports a wide variety of authentication protocol (HTTP header based, CAS, OpenID Connect, OAuth, kerberos, ...) and backends (MySQL, LDAP, Flat files).

Pre-requisites & Context

LemonLDAP must be installed and configured with an LDAP backend.
Doing so is out of the scope of this document. Please refer to:

LemonLDAP::NG Download
LemonLDAP::NG DEB install page
LemonLDAP::NG LDAP backend configuration

If you just want to test SAML with LemonLDAP::NG and you don’t want the burden of setting up LDAP and configuring LemonLDAP::NG accordingly, you can use the default “demo” backend which is used be default “out of the box”.
In this case you can use the demo user “dwho” (password “dwho”).

At the moment the Alfresco SAML module doesn’t handle the user registry part of the repo. This means that users have to exist prior to login using SAML.
As a consequence, either Alfresco must be setup with ldap synchronisation enabled - synchronisation should be done against the same directory LemonLDAP::NG uses as an LDAP backend for authentication - or users must have been created by administrators (e.g. using the Share admin console, csv import, People API…)

Both the SAML Identity Provider and the Service Provider must be time synchronized using NTP.

In the document bellow we assert that the ACME company setup their SSO system using LemonLDAP::NG on the acme.com domain.

ComponentURL
authentication portal (where users are redirected in order to login)

https://auth.acme.com

manager (for administration purposes - used further in this document)

https://manager.acme.com


On the other end, their ECM system is hosted on a Debian-like system at alfresco.myecm.org (possibly a SaaS provider or their AWS instance of Alfresco). ACME wants to integrate the Share UI interface with their SSO system.

The Identity Provider

SAMLv2 required libraries

While Alfresco uses opensaml java bindings for its SAML module, LemonLDAP::NG uses the LASSO library perl bindings. Even though LemonLDAP::NG is installed and running, required library may not be installed as they are not direct dependencies.
LASSO is a pretty active project and bugs are fixed regularly. I would then advice to use the latest & greatest version available on their website instead of the one provided by your distribution.
For example if using a Debian based distribution:

$ cat <<EOT | sudo tee /etc/apt/source.list.d/lasso.list deb http://deb.entrouvert.org/ jessie main deb-src http://deb.entrouvert.org/ jessie main EOT
$ sudo wget -O - https://deb.entrouvert.org/entrouvert.gpg | sudo apt-key add -
$ sudo apt-get update
$ sudo apt-get install liblasso-perl

Make sure you are using the latest version of the LASSO library  and its perl bindings (2.5.1-2 fixes some important issues with SHA2)

LemonLDAP::NG SAMLv2 Identity Provider

As you may know SAML extensively uses XML Dsig. As a specification Dsig provides guidance on how to hash, sign and encrypt XML contents.
In SAML, signing and encrypting rely on asymmetric cryptographic keys.
We will then need to generate such keys (here RSA) to sign and possibly encrypt SAML messages. LemonLDAP offers the possibility to uses different keys for signing and encrypting SAML messages. If you plan to use both signing and encryption, please use the same key for both (follow the procedure bellow only once for signing, encryption will use the same key).

Login to the LemonLDAP::NG manager (usually manager.acme.com), in the menu “SAML 2 Service \ Security Parameters \ Signature” and click on “New keys”
Type in a password that will be used to encrypt the private key and remember it!

LemonLDAP::NG signing keys

you’ll need the password in order the generate certificates later on!

We now need to setup the SAML metadata that every Service Provider will use (among which Alfresco Share, and possibly AOS and Alfresco REST APIs).
In the LemonLDAP::NG manager, inside the menu “SAML 2 Service \ Organization”, fill the form with:

Display Name: the ACME company
Name: acme
URL: http://acme.com

Of course you will use values that match your environment

Next in the “General Parameters \ Issuer modules \ SAML” menu, make sure the SAML Issuer module is configured as follow:

Activation: On
Path: ^/saml/
Use rule: On

Note that it is possible to allow SAML connection only under certain condition, by using the “Special rule” option.
You then need to define a Perl expression that return either true or false (more information here).

And That’s it, LemonLDAP::NG is now a SAML Identity Provider!

In order to configure Alfresco Service providers we need to export the signing key as a certificate. To do so, copy the private key that was generated in the LemonLDAP::NG manager to a file (e.g. saml.key) and generate a self-signed cert using this private key.

$ openssl req -new -days 365 -nodes -x509 -key saml.key -out saml.crt

use something like CN=LemonLDAP, OU=Premier Services, O=Alfresco, L=Maidenhead, ST=Berkshire, C=UK as a subject

Keep the saml.crt file somewhere you can find it for later use.

SAMLv2 Service Provider

Install SAML Alfresco module package

The Alfresco SAML module can be downloaded from the Alfresco support portal. Only enterprise customers are entitled to this module.
So, we download alfresco-saml-1.0.x.zip and unzip it to some place. Then, after stopping Alfresco, we copy the amp files to the each amps directories within the alfresco install directory and deploy them.

$ cp alfresco-saml-repo-1.0.1.amp <ALFRESCO_HOME>/amps
$ cp alfresco-saml-share-1.0.1.amp <ALFRESCO_HOME>/amps_share
$ ./bin/apply_amp.sh

We now have to generate the certificate we will be using on the SP side:

$ keytool -genkeypair -alias my-saml-key -keypass change-me -storepass change-me -keystore my-saml.keystore -storetype JCEKS

You can use something like CN=Share, OU=Premier Services, O=Alfresco, L=Maidenhead, ST=Berkshire, C=UK as a subject

You can of course choose to use a different password and alias, just remember them for later use.

The keystore must be copied somewhere and Alfresco configured to retrieve it.

$ mv my-saml.keystore alf_data/keystore
$ cat <<EOT > alf_data/keystore/my-saml.keystore-metadata.properties
aliases=my-saml-key
keystore.password=change-me
my-saml-key.password=change-me
EOT
$ cat <<EOT >> tomcat/shared/classes/alfresco-global.properties

saml.keystore.location=\${dir.keystore}/my-saml.keystore
saml.keystore.keyMetaData.location=\${dir.keystore}/my-saml.keystore-metadata.properties
EOT

Make sure that:

  • the keystore file is readable to Alfresco (and only to alfresco).
  • the alias and passwords match the one you use when generating the keystore with the keytool command

Next step is to merge the whole <filter/> element provided in the saml distribution (in the share-config-custom.xml.sample file), to your own share-config-custom.xml (which should be located in your {extensionroot} directory).
Bellow is an example section of the CSRF policy:

...
    <config evaluator="string-compare" condition="CSRFPolicy" replace="true">

    <!--
        If using https make a CSRFPolicy with replace="true" and override the properties section.
        Note, localhost is there to allow local checks to succeed.

        I.e.
        <properties>
            <token>Alfresco-CSRFToken</token>
            <referer>https://your-domain.com/.*|http://localhost:8080/.*</referer>
            <origin>https://your-domain.com|http://localhost:8080</origin>
        </properties>
    -->

        <filter>

            <!-- SAML SPECIFIC CONFIG -  START -->

            <!--
             Since we have added the CSRF filter with filter-mapping of "/*" we will catch all public GET's to avoid them
             having to pass through the remaining rules.
             -->
            <rule>
                <request>
                    <method>GET</method>
                    <path>/res/.*</path>
                </request>
            </rule>

            <!-- Incoming posts from IDPs do not require a token -->
            <rule>
                <request>
                    <method>POST</method>
                    <path>/page/saml-authnresponse|/page/saml-logoutresponse|/page/saml-logoutrequest</path>
                </request>
            </rule>

            <!-- SAML SPECIFIC CONFIG -  STOP -->

            <!-- EVERYTHING BELOW FROM HERE IS COPIED FROM share-security-config.xml -->

            <!--
             Certain webscripts shall not be allowed to be accessed directly form the browser.
             Make sure to throw an error if they are used.
             -->
            <rule>
                <request>
                    <path>/proxy/alfresco/remoteadm/.*</path>
                </request>
                <action name="throwError">
                    <param name="message">It is not allowed to access this url from your browser</param>
                </action>
            </rule>

            <!--
             Certain Repo webscripts should be allowed to pass without a token since they have no Share knowledge.
             TODO: Refactor the publishing code so that form that is posted to this URL is a Share webscript with the right tokens.
             -->
            <rule>
                <request>
                    <method>POST</method>
                    <path>/proxy/alfresco/api/publishing/channels/.+</path>
                </request>
                <action name="assertReferer">
                    <param name="referer">{referer}</param>
                </action>
                <action name="assertOrigin">
                    <param name="origin">{origin}</param>
                </action>
            </rule>

            <!--
             Certain Surf POST requests from the WebScript console must be allowed to pass without a token since
             the Surf WebScript console code can't be dependent on a Share specific filter.
             -->
            <rule>
                <request>
                    <method>POST</method>
                    <path>/page/caches/dependency/clear|/page/index|/page/surfBugStatus|/page/modules/deploy|/page/modules/module|/page/api/javascript/debugger|/page/console</path>
                </request>
                <action name="assertReferer">
                    <param name="referer">{referer}</param>
                </action>
                <action name="assertOrigin">
                    <param name="origin">{origin}</param>
                </action>
            </rule>

            <!-- Certain Share POST requests does NOT require a token -->
            <rule>
                <request>
                    <method>POST</method>
                    <path>/page/dologin(\?.+)?|/page/site/[^/]+/start-workflow|/page/start-workflow|/page/context/[^/]+/start-workflow</path>
                </request>
                <action name="assertReferer">
                    <param name="referer">{referer}</param>
                </action>
                <action name="assertOrigin">
                    <param name="origin">{origin}</param>
                </action>
            </rule>

            <!-- Assert logout is done from a valid domain, if so clear the token when logging out -->
            <rule>
                <request>
                    <method>POST</method>
                    <path>/page/dologout(\?.+)?</path>
                </request>
                <action name="assertReferer">
                    <param name="referer">{referer}</param>
                </action>
                <action name="assertOrigin">
                    <param name="origin">{origin}</param>
                </action>
                <action name="clearToken">
                    <param name="session">{token}</param>
                    <param name="cookie">{token}</param>
                </action>
            </rule>

            <!-- Make sure the first token is generated -->
            <rule>
                <request>
                    <session>
                        <attribute name="_alf_USER_ID">.+</attribute>
                        <attribute name="{token}"/>
                        <!-- empty attribute element indicates null, meaning the token has not yet been set -->
                    </session>
                </request>
                <action name="generateToken">
                    <param name="session">{token}</param>
                    <param name="cookie">{token}</param>
                </action>
            </rule>

            <!-- Refresh token on new "page" visit when a user is logged in -->
            <rule>
                <request>
                    <method>GET</method>
                    <path>/page/.*</path>
                    <session>
                        <attribute name="_alf_USER_ID">.+</attribute>
                        <attribute name="{token}">.+</attribute>
                    </session>
                </request>
                <action name="generateToken">
                    <param name="session">{token}</param>
                    <param name="cookie">{token}</param>
                </action>
            </rule>

            <!--
             Verify multipart requests from logged in users contain the token as a parameter
             and also correct referer & origin header if available
             -->
            <rule>
                <request>
                    <method>POST</method>
                    <header name="Content-Type">multipart/.+</header>
                    <session>
                        <attribute name="_alf_USER_ID">.+</attribute>
                    </session>
                </request>
                <action name="assertToken">
                    <param name="session">{token}</param>
                    <param name="parameter">{token}</param>
                </action>
                <action name="assertReferer">
                    <param name="referer">{referer}</param>
                </action>
                <action name="assertOrigin">
                    <param name="origin">{origin}</param>
                </action>
            </rule>

            <!--
             Verify that all remaining state changing requests from logged in users' requests contains a token in the
             header and correct referer & origin headers if available. We "catch" all content types since just setting it to
             "application/json.*" since a webscript that doesn't require a json request body otherwise would be
             successfully executed using i.e."text/plain".
             -->
            <rule>
                <request>
                    <method>POST|PUT|DELETE</method>
                    <session>
                        <attribute name="_alf_USER_ID">.+</attribute>
                    </session>
                </request>
                <action name="assertToken">
                    <param name="session">{token}</param>
                    <param name="header">{token}</param>
                </action>
                <action name="assertReferer">
                    <param name="referer">{referer}</param>
                </action>
                <action name="assertOrigin">
                    <param name="origin">{origin}</param>
                </action>
            </rule>
        </filter>
    </config>
...

Configure SAML Alfresco module

We can now configure the SAML service providers we need. Alfresco offers 3 different service providers that can be configured/enabled separately:

  • Share (the Alfresco collaborative UI)
  • AOS (the new Sharepoint protocol interface)
  • REST api (the Alfresco RESTful api)

Configuration can be done in several ways.

Configuring SAML SP using subsystem files:

The alfresco SAML distribution comes with examples of the SAML configuration files. Reusing them is very convenient and allow quick setup.
We’ll copy the files for the required SP and configure each SP as needed.

$ cp -a ~/saml/alfresco/extension/subsystems tomcat/shared/classes/alfresco/extension

Then to configure Share SP, for example, make sure to rename sample files and make sure they contain the needed properties:

$ mv tomcat/shared/classes/alfresco/extension/subsystems/SAML/share/share/my-custom-share-sp.properties.sample tomcat/shared/classes/alfresco/extension/subsystems/SAML/share/share/my-custom-share-sp.properties

my-custom-share-sp.properties:

saml.sp.isEnabled=true
saml.sp.isEnforced=false
saml.sp.idp.spIssuer.namePrefix=
saml.sp.idp.description=LemonLDAP::NG
saml.sp.idp.sso.request.url=https://auth.acme.com/saml/singleSignOn
saml.sp.idp.slo.request.url=https://auth.acme.com/saml/singleLogout
saml.sp.idp.slo.response.url=https://auth.acme.com/saml/singleLogoutReturn
saml.sp.idp.spIssuer=http://alfresco.myecm.org:8080/share
saml.sp.user.mapping.id=Subject/NameID
saml.sp.idp.certificatePath=${dir.keystore}/saml.crt

Of course you should use URLs matching your domain name!

As the configuration points to the IdP certificate, we’ll also need to copy it to the Alfresco server as well (we generated this certificate earlier) in the alf_data/keystore folder (or any other folder you may have used as dir.keystore).

$ cp saml.crt alf_data/keystore

Configuring SAML SP using the Alfresco admin console:

Configure SAML service provider using the Alfresco admin console (/alfresco/s/enterprise/admin/admin-saml).
Set the following parameters:

Of course you should use URLs matching your domain name!

Bellow is a screenshot of what it would look like:

Force SAML connection unset lets the user login either as a SAML authenticated user, or as another user, using a different subsystem.

Download the metadata and certificates from the bottom of the page, and then import the certificate you generated earlier using openssl in the Alfresco admin console.
To finish with Alfresco configuration, tick the “Enable SAML authentication (SSO)” box.

Create the SAML Service provider on the Identity Provider

The identity provider must be aware of the SP and its configuration. Using the LemonLDAP manager go to the “SAML Service Provider” section and add a new service provider.
Give it a name like “Alfresco-Share”.

Upload the metadata file exported from Alfresco admin console.

Under the newly created SP, in the “Options \ Authentication response” menu set the following parameters:

Default NameID Format: Unspecified
Force NameID session key: uid

Note that you could use whatever session key that is available and fits your needs. Here, uid makes sense for use with Alfresco logins and works for the “Demo” authentication backend in LemonLDAP::NG. If a real LDAP backend is available and Alfresco is syncing users from that same LDAP directory, then the value for the session key used as NameID value should match the ldap.synchronization.userIdAttributeName defined in the Alfresco’s ldap authentication subsystem.

Optionnally, you can also send some more informations in the authentication response to the Share SP. To do so, under the newly created SP is a section called “Exported attributes”. Configure it as follow:

This requires that the appropriate keys are exported as variables whose names are used as "Key name".

So here, it we would have the following LemonLDAP::NG exported variables:

  • fisrtname
  • lastname
  • mail

Hacks ‘n tweaks

At this point, and given you met the pre-requisites, you should be able to login without any problems. However, there may still be some issues with SP-initiated logout (initiating logout from the IdP should work though), depending on the version of SP and IdP you use. Logouts rely on the SAML SLO profile and the way it's implemented in both Alfresco and LemonLDAP at the moment still have some interoperability issues.

On the Alfresco, SAML module version 1.0.1 is impacted by MNT-18064, which prevents SLO from working properly with LemonLDAP::NG. There is a small patch attached to the JIRA that can be used and adapted to match the NameID format used by your IdP (for the configuration described here, that would be "UNSPECIFIED"):

This JIRA is to be fixed in the next alfresco-saml module (probably 1.0.2). In the mean time you can use the patch in the JIRA 

The LemonLDAP::NG project crew kindly considered re-writing their sessionIndex generation algorythm in order to avoid interoperability problems and security issues. This is needed in order to work with Alfresco and should be added in 1.9.11. Thus, previous versions won’t work:

In the mean time you can use the patch attached to LEMONLDAP-1261

3 Comments