SAML ADFS authentication in Drupal

Parent Feed: 

Drupal as consumer/SP, ADFS as IdP

Photo of Pascal Morin
Tue, 2017-01-10 13:24By pascal

We'll try to cover the needed step to authenticate (and create if need) Drupal 7 users against an external Active Directory Federation server, using SimpleSAMLphp and the simplesamlphp_auth module.

Examples are given for a Debian server, using Nginx and php-fpm, but most of the configuration would be similar for other setups, and except for the application integration part, the SimpleSAML setup part should apply to any php integration.

Assumptions

We'll assume that:

Dependencies

We'll use memcache to store user sessions (although you can use a MySQL-like database instead), which is done through the php-memcache extension (NOT php-memcached):

sudo apt-get install memcached

sudo apt-get install php5-memcache

Once again, in case you read too fast, php-memcache, NOT php-memcached !

1. Nginx configuration: accessing SimpleSAML library through the web

Given that SAML is based on redirects, the first thing you'd need to do is being able to access the library over HTTPS.

1.1. Install the library

Download the library from https://simplesamlphp.org/download and extract it in Drupal's library folder, so it ends up under sites/all/libraries/simplesamlphp-1.14.8 in the repo (that will match /var/www/drupal-sp.master/www/sites/all/libraries/simplesamlphp-1.14.8 once deployed on the server).

WARNING: This location is an example: it means the private key(s) and other config files are by default accessible worldwide. In a production environment, make sure sites/all/libraries/simplesamlphp-1.14.8 is not reachable directly, or better yet: move it outside the webroot and amend the webserver configuration accordingly.

1.2. Amend Nginx' fastcgi configuration

We need to add a line to the /etc/nginx/fastcgi_params file so it supports path_info (in short, paths in the form of file.php/parameter1/parameter2). Instead of amending the default one, we took the view of duplicating it and amending the copy instead:

sudo cp /etc/nginx/fastcgi_params /etc/nginx/fastcgi_params_path_info

and add the following at the end: "fastcgi_param PATH_INFO $fastcgi_path_info;"

The resulting file should now look like:

fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
fastcgi_param  QUERY_STRING       $query_string;
fastcgi_param  REQUEST_METHOD     $request_method;
fastcgi_param  CONTENT_TYPE       $content_type;
fastcgi_param  CONTENT_LENGTH     $content_length;
 
fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  DOCUMENT_ROOT      $document_root;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;
 
fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;
 
fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME        $server_name;
 
fastcgi_param  HTTPS              $https if_not_empty;
 
fastcgi_param  HTTP_PROXY         "";
 
fastcgi_param PATH_INFO $fastcgi_path_info;
 
 
# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param  REDIRECT_STATUS    200;

1.3 Setup the vhost

While it is possible to use a different subdomain for your application and the SimpleSAML library (look at 'session.cookie.domain' on SimpleSAML side and '$cookie_domain' on Drupal side), the simplest and most common approach is to have both under the same domain. Typically, our Drupal installation being at https://drupal-sp.example.com/, we would make the library accessible at https://drupal-sp.example.com/simplesaml/. Note the 'simplesaml' location has been chosen for simplicity of the example, but it could be set to anything, https://drupal-sp.example.com/auth/, https://drupal-sp.example.com/whatever/, ...

What we need to do is setting up an alias for our /simplesaml location to the library webroot on the filesystem at /var/www/drupal-sp.master/www/sites/all/libraries/simplesamlphp-1.14.8/www. The resulting vhost file (trimmed non-relevant parts) would be similar to:

server {
        server_name drupal-sp.example.com/;
        listen 443 ssl;
        # DRUPAL_ROOT
        root /var/www/drupal-sp.master/www;
        fastcgi_param HTTPS on;
        #### BEGIN SAML specific config ####
        # We could use any alias, as long it matches the config.php of SimpleSAML.
        location /simplesaml {
          # Point to our library folder webroot.
          alias /var/www/drupal-sp.master/www/sites/all/libraries/simplesamlphp-1.14.8/www;
          location ~ ^(?<prefix>/simplesaml)(?<phpfile>.+?\.php)(?<pathinfo>/.*)?$ {
            fastcgi_split_path_info ^(.+?\.php)(/.+)$;
            fastcgi_pass             127.0.0.1:9000;
            fastcgi_index index.php;
            # Note we include the fastcgi config copied and modified above.
            include fastcgi_params_path_info;
            fastcgi_param SCRIPT_FILENAME $document_root$phpfile;
            fastcgi_param PATH_INFO $pathinfo if_not_empty;
         }
         # This looks extremly weird, and it is. Workaround a NGINX bug. @see https://trac.nginx.org/nginx/ticket/97
         # Without this the assets (js/css/img) won't be served.
         location ~ ^(?<prefix>/simplesaml/resources/)(?<assetfile>.+?\.(js|css|png|jpg|jpeg|gif|ico))$ {
           return 301 $scheme://$server_name/sites/all/libraries/simplesamlphp-1.14.8/www/resources/$assetfile;
         }
        }
        #### END SAML specific config ####
        ## Rest of your app specific directives.
}

WARNING: I repeat. This location is an example: it means the private key(s) and other config files are by default accessible worldwide. In a production environment, make sure sites/all/libraries/simplesamlphp-1.14.8 is not reachable directly, or better yet: move it outside the webroot and amend the webserver configuration accordingly.

2. SimpleSAML configuration

2.1 config.php

This is where the main "global" configuration directives reside, and it is located under the "config" folder of the library. Most of the default are fine as is, we are mainly going to set:

  • the main path
  • the memcache settings
  • the debug mode

The resulting file would end up with the following options:

<?php
 
/*
 * The configuration of SimpleSAMLphp
 * 
 */
 
$config = array(
  /**
   * Full url to the library. Trailing slash is needed.
   */
  'baseurlpath' => 'https://drupal-sp.example.com/simplesaml/',
  /**
   * Debug data. Turn all that to FALSE on prod.
   */
  'debug' => TRUE,
  'showerrors' => TRUE,
  'errorreporting' => TRUE,
  /**
   * Admin access, do not enable on prod.
   */
  'auth.adminpassword' => 'MYPASSWORD',
  'admin.protectindexpage' => FALSE,
  'admin.protectmetadata' => FALSE,
  /**
   * This is a secret salt used by SimpleSAMLphp when it needs to generate a secure hash
   * of a value. It must be changed from its default value to a secret value. The value of
   * 'secretsalt' can be any valid string of any length.
   *
   * A possible way to generate a random salt is by running the following command from a unix shell:
   * tr -c -d '0123456789abcdefghijklmnopqrstuvwxyz' </dev/urandom | dd bs=32 count=1 2>/dev/null;echo
   */
  'secretsalt' => 'omqczwt6klfdn244787098jqlfjdsql',
  /*
   * Some information about the technical persons running this installation.
   * The email address will be used as the recipient address for error reports, and
   * also as the technical contact in generated metadata.
   */
  'technicalcontact_name' => 'Administrator',
  'technicalcontact_email' => [email protected]',
  /**
   * We want debugging on while setting our install up.
   */
  'logging.level' => SimpleSAML_Logger::DEBUG,
  'logging.handler' => 'syslog',
  /**
   * Sessions will be held in memcache.
   */
  'store.type' => 'memcache',
  'memcache_store.servers' => array(
    array(
      array('hostname' => 'localhost'),
    ),
  ),
);

At this point, you should be able to reach https://drupal-sp.example.com/simplesaml/ and login as 'Admin' using the local SimpleSAML authentication mechanism. Check the "configuration" tab to access basic sanity checks on your config.

2.3 authsources.php

We're now ready to define our new authentication source in the config/authsources.php file, that holds an array of all available authentication sources. You'll notice there's already an "admin" one, which is the default local one defined by the SimpleSAML php library.

We assume that nothing has yet been created on the ADFS side (we'll do that at a later step), if that's not your case, you should match the entityID with the one provided to you.

<?php
 
$config = array(
  'admin' => array(
    // Default local auth source.
    'core:AdminPassword',
  ),
  'example-drupal-sp' => array(
    // Type of auth source.
    'saml:SP',
    // Unique identifier of your SP.
    // This can be set to anything, as long as no other SP use it on the IDP already.
    'entityID' => 'urn:drupal:example',
    // IDP url identifier. A mean of getting the precise url is given later (@todo link)
    'idp' => 'http://adfs-idp.example.com/adfs/services/trust',
    // This must be explicitely set to NULL, we rely on metadata.
    'NameIDPolicy' => NULL,
    // These are the SP-side certificate, that we will need to generate.
    // The location is relative to the 'cert' directive specified in the
    // config.php file.
    'privatekey' => 'saml.pem',
    'certificate' => 'saml.crt',
    // Enforce signing of redirections, using our certificate.
    'sign.logout' => TRUE,
    'redirect.sign' => TRUE,
    'assertion.encryption' => TRUE,
    // Enforce use of sha256.
    'signature.algorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
  ),
);

If you navigate to the "Authentication" tab > "Test configured authentication sources" within the /simplesaml admin interface, you should now see your new source appear as "example-drupal-sp". Don't try yet to use it, it will just throw exceptions for now !

Certificate generation

We now need to generate the certificates we did specify in the auth source, they will be used to sign our requests. We'll stick with the default location and create them under the "cert" directory of the library.

cd sites/all/libraries/simplesamlphp-1.14.8/cert

openssl req -x509 -sha256 -nodes -days 3652 -newkey rsa:2048 -keyout saml.pem -out saml.crt

2.4 saml20-idp-remote.php

The last piece of information that is needed for the two systems to be able to communicate is the metadata specification, which defines the various endpoints and protocols to use. The ADFS server conveniently exposes those, so we just need to get hold of them, and convert it to php. The federation XML file is usually located under /FederationMetadata/2007-06/FederationMetadata.xml but you can find it out from the ADFS management tool, by expanding "Services" > "Endpoints" and looking at the Metadata section.

In our case we would download "https://adfs-idp.example.com/FederationMetadata/2007-06/FederationMetada...". The next step is to convert it to a php array, with an utility provided by the SimpleSAML library, located at /simplesaml/admin/metadata-converter.php. This will generate 2 php arrays ready to copy paste:

  • saml20-sp-remote: we can ignore this one in our setup, as we are only acting as a Service Provider
  • saml20-idp-remote: this is the data we will need, and we are going to copy/paste it into metadata/saml20-idp-remote.php.

The array key should match the value of what we had specified in our authsources:

authsources.php:

'idp' => 'http://adfs-idp.example.com/adfs/services/trust',

saml20-idp-remote.php

$metadata['http://adfs-idp.example.com/adfs/services/trust'] = array(

If that's not the case, amend your authsource. Pay especially attention to the fact that the idp may identify itself as http://, not https://, even though all requests go through https endpoints.

3. ADFS setup

Now the SP side component is ready, we need to configure the IDP side.

3.1 SP metadata

In the same way our SP needs metadata to know how to talk to the IDP, the ADFS server needs to how to communicate with the SimpleSAML library, and must be provided some metadata. Here again, the SimpleSAML php library helps us a lot by exposing its metadata, ready for the server to consume.

You can obtain this url within the /simplesaml admin section, under the "Federation" tab, by clicking on the "View metadata" link underneath your "example-drupal-sp" source. This will display the actual metadata, along with the dedicated url to obtain it, https://drupal-sp.example.com/simplesaml/module.php/saml/sp/metadata.php... in our case.

3.2 Trust configuration

In the ADFS admin tool on the Windows server, create a new "Relying Party Trust". This should launch a Wizard similar to:

Enter the SP metadata URL (https://drupal-sp.example.com/simplesaml/module.php/saml/sp/metadata.php...) into the "Federation metadata address" field and press next. This should be enough to setup all the needed configuration, even though you will get a notice about some metadata fields being ignored, which you can ignore.

3.3 Claims

These claims serve as mappings between ADFS and SAML "fields", and are accessible trough the "Edit Claim Rules..." entry.

3.3.1 LDAP attributes

Create an "Issuance Transform Rule", of type "Send LDAP Attributes as Claims" with the fields you need to pass back to the SP. Some are preset and defined in the metadata, but you can add custom ones: In this example UPN and Email are using the set schema, while DisplayName is custom: