saml_sp.module in SAML Service Provider 7
Same filename and directory in other branches
SAML Service Provider
Allow users to log in to Drupal via a third-party SAML Identity Provider. Users authenticate to the third-party SAML IDP (e.g. http://idp.example.com) and a series of redirects allows that authentication to be recognised in Drupal.
Uses the OneLogin PHP-SAML toolkit: https://github.com/onelogin/php-saml
File
saml_sp.moduleView source
<?php
/**
* @file
* SAML Service Provider
*
* Allow users to log in to Drupal via a third-party SAML Identity Provider.
* Users authenticate to the third-party SAML IDP (e.g. http://idp.example.com)
* and a series of redirects allows that authentication to be recognised in
* Drupal.
*
* Uses the OneLogin PHP-SAML toolkit: https://github.com/onelogin/php-saml
*/
// Path to this directory. Requires PHP 5.3 or greater.
define('DRUPAL_SAML_SP__HOME', __DIR__);
// Default name to identify this application to IDPs.
define('DRUPAL_SAML_SP__APP_NAME_DEFAULT', 'drupal-saml-sp');
// Expect a response from the IDP within 2 minutes.
define('SAML_SP_REQUEST_CACHE_TIMEOUT', 120);
/**
* Implements hook_theme().
*/
function saml_sp_theme() {
return array(
'saml_sp__idp_list' => array(
'render element' => 'idps',
'file' => 'saml_sp.theme.inc',
),
);
}
/**
* Implements hook_permission().
*/
function saml_sp_permission() {
return array(
'configure saml sp' => array(
'title' => t('Configure SAML SP'),
'description' => t('Configure the SAML Service Provider integration.'),
'restrict access' => TRUE,
),
);
}
/**
* Implements hook_menu().
*/
function saml_sp_menu() {
$items = array();
$items['admin/config/people/saml_sp'] = array(
'title' => 'SAML Service Providers',
'page callback' => 'saml_sp__admin_overview',
'access arguments' => array(
'configure saml sp',
),
'file' => 'saml_sp.admin.inc',
);
$items['admin/config/people/saml_sp/IDP'] = array(
'title' => 'Identiy Providers',
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -10,
);
// Add a new IDP.
$items['admin/config/people/saml_sp/IDP/add'] = array(
'title' => 'Add SAML IDP',
'page callback' => 'drupal_get_form',
'page arguments' => array(
'saml_sp__configure_idp_form',
),
'access arguments' => array(
'configure saml sp',
),
'file' => 'saml_sp.admin.inc',
'type' => MENU_LOCAL_ACTION,
);
// Configure an existing IDP.
$items['admin/config/people/saml_sp/IDP/%saml_sp_idp'] = array(
'title' => 'SAML IDP: @idp_name',
'title callback' => 'saml_sp__menu_title',
'title arguments' => array(
'SAML IDP: @idp_name',
5,
),
'page callback' => 'drupal_get_form',
'page arguments' => array(
'saml_sp__configure_idp_form',
5,
),
'access arguments' => array(
'configure saml sp',
),
'file' => 'saml_sp.admin.inc',
);
// Confirmation form to delete an IDP.
$items['admin/config/people/saml_sp/IDP/%saml_sp_idp/delete'] = array(
'title' => 'Delete SAML IDP: @idp_name',
'title callback' => 'saml_sp__menu_title',
'title arguments' => array(
'Delete SAML IDP: @idp_name',
5,
),
'page callback' => 'drupal_get_form',
'page arguments' => array(
'saml_sp__delete_idp_form',
5,
),
'access arguments' => array(
'configure saml sp',
),
'file' => 'saml_sp.admin.inc',
);
// Export the IDP configuration to code.
$items['admin/config/people/saml_sp/IDP/%saml_sp_idp/export'] = array(
'title' => 'Export SAML IDP: @idp_name',
'title callback' => 'saml_sp__menu_title',
'title arguments' => array(
'Export SAML IDP: @idp_name',
5,
),
'page callback' => 'saml_sp__export_idp',
'page arguments' => array(
5,
),
'access arguments' => array(
'configure saml sp',
),
'file' => 'saml_sp.admin.inc',
);
// SAML endpoint for all requests.
// Some IDPs ignore the URL provided in the authentication request
// (the AssertionConsumerServiceURL attribute) and hard-code a return URL in
// their configuration, therefore all modules using SAML SP will have the
// same consumer endpoint: /saml/consume.
// A unique ID is generated for each outbound request, and responses are
// expected to reference this ID in the `inresponseto` attribute of the
// `<samlp:response` XML node.
$items['saml/consume'] = array(
'page callback' => 'saml_sp__endpoint',
// This endpoint should not be under access control.
'access callback' => TRUE,
'file' => 'saml_sp.pages.inc',
'type' => MENU_CALLBACK,
);
return $items;
}
/**
* Title callback for SAML SP menu items.
*/
function saml_sp__menu_title($title, $idp) {
return t($title, array(
'@idp_name' => $idp->name,
));
}
/******************************************************************************
* CRUD handlers.
*****************************************************************************/
/**
* Load a single IDP.
* Also a menu argument loader.
*
* @param String $idp_machine_name
*
* @return Object
*/
function saml_sp_idp_load($idp_machine_name) {
$all_idps = saml_sp__load_all_idps();
return isset($all_idps[$idp_machine_name]) ? $all_idps[$idp_machine_name] : FALSE;
}
/**
* Save an IDP configuration.
*
* @param Object $idp
* A populated IDP object, with the keys:
* - name
* - machine_name
* - app_name
* - login_url
* - x509_cert
*
* @return Int
* One of:
* - SAVED_NEW
* - SAVED_UPDATED
*/
function saml_sp_idp_save($idp) {
// Prevent PHP notices by ensure 'export_type' is populated.
if (empty($idp->export_type)) {
$idp->export_type = NULL;
}
// Handle changes of machine name (if which case $idp->orig_machine_name
// should be populated).
if (!empty($idp->orig_machine_name)) {
saml_sp_idp_delete($idp->orig_machine_name);
$idp->export_type = NULL;
}
// Delegate to the CTools CRUD handler.
$result = ctools_export_crud_save('saml_sp_idps', $idp);
return isset($idp->orig_machine_name) && $result == SAVED_NEW ? SAVED_UPDATED : $result;
}
/**
* Delete an IDP.
*
* @param String $idp_machine_name
*/
function saml_sp_idp_delete($idp_machine_name) {
// No success feedback is provided.
ctools_export_crud_delete('saml_sp_idps', $idp_machine_name);
}
/**
* Load all the registered IDPs.
*
* @return Array
* An array of IDP objects, keyed by the machine name.
*/
function saml_sp__load_all_idps() {
// Use CTools export API to fetch all presets.
ctools_include('export');
$result = ctools_export_crud_load_all('saml_sp_idps');
return $result;
}
/******************************************************************************
* API library integration.
*****************************************************************************/
/**
* Get the SAML settings for an IDP.
*
* @param Object $idp
* An IDP object, such as that provided by saml_sp_idp_load($machine_name).
*
* @return OneLogin_Saml_Settings
* IDP Settings data.
*/
function saml_sp__get_settings($idp) {
// Require all the relevant libraries.
_saml_sp__prepare();
// The consumer endpoint will always be /saml/consume.
$endpoint_url = url("saml/consume", array(
'absolute' => TRUE,
));
$settings = new OneLogin_Saml_Settings();
// URL of the IDP server.
$settings->idpSingleSignOnUrl = $idp->login_url;
// The IDP's public x.509 certificate.
$settings->idpPublicCertificate = $idp->x509_cert;
// Name to identify this application
$settings->spIssuer = $idp->app_name;
// Drupal URL to consume the response from the IDP.
$settings->spReturnUrl = $endpoint_url;
// Tells the IdP to return the email address of the current user
$settings->requestedNameIdFormat = OneLogin_Saml_Settings::NAMEID_EMAIL_ADDRESS;
// Invoke hook_saml_sp_settings_alter().
drupal_alter('saml_sp_settings', $settings);
return $settings;
}
/******************************************************************************
* Start and finish SAML authentication process.
*****************************************************************************/
/**
* Start a SAML authentication request.
*
* @param Object $idp
* @param String $callback
* A function to call with the results of the SAML authentication process.
*/
function saml_sp_start($idp, $callback) {
// Settings is a OneLogin_Saml_Settings object.
$settings = saml_sp__get_settings($idp);
$authRequest = new OneLogin_Saml_AuthRequest($settings);
$url = $authRequest
->getRedirectUrl();
// Track the ID of the outbound request.
$prefix_length = strlen($settings->idpSingleSignOnUrl . "?SAMLRequest=");
$id = _saml_sp__extract_outbound_id(substr($url, $prefix_length));
saml_sp__track_request($id, $idp, $callback);
// Redirect the user to the IDP.
header("Location: {$url}");
}
/**
* Track an outbound request.
*
* @param String $id
* The unique ID of an outbound request.
* $param Object $idp
* IDP data.
* @param String $callback
* The function to invoke on completion of a SAML authentication request.
*/
function saml_sp__track_request($id, $idp, $callback) {
$data = array(
'id' => $id,
'idp' => $idp->machine_name,
'callback' => $callback,
);
$expire = REQUEST_TIME + SAML_SP_REQUEST_CACHE_TIMEOUT;
cache_set($id, $data, 'saml_sp_request_tracking_cache', $expire);
}
/**
* Get the IDP and callback from a tracked request.
*
*
* @param String $id
* The unique ID of an outbound request.
*
* @return Array|FALSE
* An array of tracked data, giving the keys:
* - id The original outbound ID.
* - idp The machine name of the IDP.
* - callback The function to invoke on authentication.
*/
function saml_sp__get_tracked_request($id) {
if ($cache = cache_get($id, 'saml_sp_request_tracking_cache')) {
return $cache->data;
}
return FALSE;
}
/******************************************************************************
* Internal helper functions.
*****************************************************************************/
/**
* Get a default IDP object.
*/
function _saml_sp__default_idp() {
return (object) array(
'name' => '',
'machine_name' => '',
// If the app-name is NULL, the global app-name will be used instead.
'app_name' => NULL,
'login_url' => '',
'x509_cert' => '',
);
}
/**
* Load the required OneLogin SAML-PHP toolkit files.
*/
function _saml_sp__prepare() {
static $has_run = FALSE;
if (!$has_run) {
require_once DRUPAL_SAML_SP__HOME . '/lib/ext/xmlseclibs/xmlseclibs.php';
require_once DRUPAL_SAML_SP__HOME . '/lib/src/OneLogin/Saml/AuthRequest.php';
require_once DRUPAL_SAML_SP__HOME . '/lib/src/OneLogin/Saml/Response.php';
require_once DRUPAL_SAML_SP__HOME . '/lib/src/OneLogin/Saml/Settings.php';
require_once DRUPAL_SAML_SP__HOME . '/lib/src/OneLogin/Saml/XmlSec.php';
$has_run = TRUE;
}
}
/**
* Extract the unique ID of an outbound request.
*
* @param String $encoded_url
* The response of OneLogin_Saml_AuthRequest::getRedirectUrl(), which is
* multiple-encoded.
*
* @return String|FALSE
* The unique ID of the outbound request, if it can be decoded.
* This will be OneLogin_Saml_AuthRequest::ID_PREFIX, followed by a sha1 hash.
*/
function _saml_sp__extract_outbound_id($encoded_url) {
$string = $encoded_url;
$string = @urldecode($string);
$string = @base64_decode($string);
$string = @gzinflate($string);
// This regex is based on the prelude provided in
// OneLogin_Saml_AuthRequest::getRedirectUrl().
$regex = '/^<samlp:AuthnRequest
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="(' . OneLogin_Saml_AuthRequest::ID_PREFIX . '[0-9a-f]{40})"/m';
$result = FALSE;
if (preg_match($regex, $string, $matches)) {
$result = $matches[1];
}
return $result;
}
/**
* Extract the unique ID in an inbound request.
*
* @param String $assertion
* UUEncoded SAML assertion from the IdP (i.e. the POST request).
*
* @return String|FALSE
* The unique ID of the inbound request, if it can be decoded.
* This will be OneLogin_Saml_AuthRequest::ID_PREFIX, followed by a sha1 hash.
*/
function _saml_sp__extract_inbound_id($assertion) {
// Decode the request.
$xml = base64_decode($assertion);
// Load the XML.
$document = new DOMDocument();
if ($document
->loadXML($xml)) {
try {
$id = @$document->firstChild->attributes
->getNamedItem('InResponseTo')->value;
return $id;
} catch (Exception $e) {
return FALSE;
}
}
return FALSE;
}
Functions
Name | Description |
---|---|
saml_sp_idp_delete | Delete an IDP. |
saml_sp_idp_load | Load a single IDP. Also a menu argument loader. |
saml_sp_idp_save | Save an IDP configuration. |
saml_sp_menu | Implements hook_menu(). |
saml_sp_permission | Implements hook_permission(). |
saml_sp_start | Start a SAML authentication request. |
saml_sp_theme | Implements hook_theme(). |
saml_sp__get_settings | Get the SAML settings for an IDP. |
saml_sp__get_tracked_request | Get the IDP and callback from a tracked request. |
saml_sp__load_all_idps | Load all the registered IDPs. |
saml_sp__menu_title | Title callback for SAML SP menu items. |
saml_sp__track_request | Track an outbound request. |
_saml_sp__default_idp | Get a default IDP object. |
_saml_sp__extract_inbound_id | Extract the unique ID in an inbound request. |
_saml_sp__extract_outbound_id | Extract the unique ID of an outbound request. |
_saml_sp__prepare | Load the required OneLogin SAML-PHP toolkit files. |