392 lines
13 KiB
PHP
392 lines
13 KiB
PHP
<?php
|
|
|
|
namespace SAML\Auth;
|
|
|
|
use Exception;
|
|
use SAML\Configuration;
|
|
use SAML\Error;
|
|
use SAML\Error\UnserializableException;
|
|
use SAML\Logger;
|
|
use SAML\Module;
|
|
use SAML\Utils;
|
|
|
|
/**
|
|
* Class for implementing authentication processing chains for IdPs.
|
|
*
|
|
* This class implements a system for additional steps which should be taken by an IdP before
|
|
* submitting a response to a SP. Examples of additional steps can be additional authentication
|
|
* checks, or attribute consent requirements.
|
|
*
|
|
* @author Olav Morken, UNINETT AS.
|
|
* @package SimpleSAMLphp
|
|
*/
|
|
|
|
class ProcessingChain
|
|
{
|
|
/**
|
|
* The list of remaining filters which should be applied to the state.
|
|
*/
|
|
const FILTERS_INDEX = '\SimpleSAML\Auth\ProcessingChain.filters';
|
|
|
|
|
|
/**
|
|
* The stage we use for completed requests.
|
|
*/
|
|
const COMPLETED_STAGE = '\SimpleSAML\Auth\ProcessingChain.completed';
|
|
|
|
|
|
/**
|
|
* The request parameter we will use to pass the state identifier when we redirect after
|
|
* having completed processing of the state.
|
|
*/
|
|
const AUTHPARAM = 'AuthProcId';
|
|
|
|
|
|
/**
|
|
* All authentication processing filters, in the order they should be applied.
|
|
*/
|
|
private $filters;
|
|
|
|
|
|
/**
|
|
* Initialize an authentication processing chain for the given service provider
|
|
* and identity provider.
|
|
*
|
|
* @param array $idpMetadata The metadata for the IdP.
|
|
* @param array $spMetadata The metadata for the SP.
|
|
* @param string $mode
|
|
*/
|
|
public function __construct($idpMetadata, $spMetadata, $mode = 'idp')
|
|
{
|
|
assert(is_array($idpMetadata));
|
|
assert(is_array($spMetadata));
|
|
|
|
$this->filters = [];
|
|
|
|
$config = Configuration::getInstance();
|
|
$configauthproc = $config->getArray('authproc.' . $mode, null);
|
|
|
|
if (!empty($configauthproc)) {
|
|
$configfilters = self::parseFilterList($configauthproc);
|
|
self::addFilters($this->filters, $configfilters);
|
|
}
|
|
|
|
if (array_key_exists('authproc', $idpMetadata)) {
|
|
$idpFilters = self::parseFilterList($idpMetadata['authproc']);
|
|
self::addFilters($this->filters, $idpFilters);
|
|
}
|
|
|
|
if (array_key_exists('authproc', $spMetadata)) {
|
|
$spFilters = self::parseFilterList($spMetadata['authproc']);
|
|
self::addFilters($this->filters, $spFilters);
|
|
}
|
|
|
|
Logger::debug('Filter config for ' . $idpMetadata['entityid'] . '->' .
|
|
$spMetadata['entityid'] . ': ' . str_replace("\n", '', var_export($this->filters, true)));
|
|
}
|
|
|
|
|
|
/**
|
|
* Sort & merge filter configuration
|
|
*
|
|
* Inserts unsorted filters into sorted filter list. This sort operation is stable.
|
|
*
|
|
* @param array &$target Target filter list. This list must be sorted.
|
|
* @param array $src Source filters. May be unsorted.
|
|
* @return void
|
|
*/
|
|
private static function addFilters(&$target, $src)
|
|
{
|
|
assert(is_array($target));
|
|
assert(is_array($src));
|
|
|
|
foreach ($src as $filter) {
|
|
$fp = $filter->priority;
|
|
|
|
// Find insertion position for filter
|
|
for ($i = count($target) - 1; $i >= 0; $i--) {
|
|
if ($target[$i]->priority <= $fp) {
|
|
// The new filter should be inserted after this one
|
|
break;
|
|
}
|
|
}
|
|
/* $i now points to the filter which should preceede the current filter. */
|
|
array_splice($target, $i + 1, 0, [$filter]);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Parse an array of authentication processing filters.
|
|
*
|
|
* @param array $filterSrc Array with filter configuration.
|
|
* @return array Array of ProcessingFilter objects.
|
|
*/
|
|
private static function parseFilterList($filterSrc)
|
|
{
|
|
assert(is_array($filterSrc));
|
|
|
|
$parsedFilters = [];
|
|
|
|
foreach ($filterSrc as $priority => $filter) {
|
|
if (is_string($filter)) {
|
|
$filter = ['class' => $filter];
|
|
}
|
|
|
|
if (!is_array($filter)) {
|
|
throw new Exception('Invalid authentication processing filter configuration: ' .
|
|
'One of the filters wasn\'t a string or an array.');
|
|
}
|
|
|
|
$parsedFilters[] = self::parseFilter($filter, $priority);
|
|
}
|
|
|
|
return $parsedFilters;
|
|
}
|
|
|
|
|
|
/**
|
|
* Parse an authentication processing filter.
|
|
*
|
|
* @param array $config Array with the authentication processing filter configuration.
|
|
* @param int $priority The priority of the current filter, (not included in the filter
|
|
* definition.)
|
|
* @return ProcessingFilter The parsed filter.
|
|
*/
|
|
private static function parseFilter($config, $priority)
|
|
{
|
|
assert(is_array($config));
|
|
|
|
if (!array_key_exists('class', $config)) {
|
|
throw new Exception('Authentication processing filter without name given.');
|
|
}
|
|
|
|
$className = Module::resolveClass(
|
|
$config['class'],
|
|
'Auth\Process',
|
|
'\SimpleSAML\Auth\ProcessingFilter'
|
|
);
|
|
$config['%priority'] = $priority;
|
|
unset($config['class']);
|
|
|
|
/** @var ProcessingFilter */
|
|
return new $className($config, null);
|
|
}
|
|
|
|
|
|
/**
|
|
* Process the given state.
|
|
*
|
|
* This function will only return if processing completes. If processing requires showing
|
|
* a page to the user, we will not be able to return from this function. There are two ways
|
|
* this can be handled:
|
|
* - Redirect to a URL: We will redirect to the URL set in $state['ReturnURL'].
|
|
* - Call a function: We will call the function set in $state['ReturnCall'].
|
|
*
|
|
* If an exception is thrown during processing, it should be handled by the caller of
|
|
* this function. If the user has redirected to a different page, the exception will be
|
|
* returned through the exception handler defined on the state array. See
|
|
* State for more information.
|
|
*
|
|
* @param array &$state The state we are processing.
|
|
* @return void
|
|
*@throws UnserializableException
|
|
* @throws \SAML\Error\Exception
|
|
* @see State::EXCEPTION_HANDLER_URL
|
|
* @see State::EXCEPTION_HANDLER_FUNC
|
|
*
|
|
* @see State
|
|
*/
|
|
public function processState(&$state)
|
|
{
|
|
assert(is_array($state));
|
|
assert(array_key_exists('ReturnURL', $state) || array_key_exists('ReturnCall', $state));
|
|
assert(!array_key_exists('ReturnURL', $state) || !array_key_exists('ReturnCall', $state));
|
|
|
|
$state[self::FILTERS_INDEX] = $this->filters;
|
|
|
|
try {
|
|
// TODO: remove this in SSP 2.0
|
|
if (!array_key_exists('UserID', $state)) {
|
|
// No unique user ID present. Attempt to add one.
|
|
self::addUserID($state);
|
|
}
|
|
|
|
while (count($state[self::FILTERS_INDEX]) > 0) {
|
|
$filter = array_shift($state[self::FILTERS_INDEX]);
|
|
$filter->process($state);
|
|
}
|
|
} catch (Error\Exception $e) {
|
|
// No need to convert the exception
|
|
throw $e;
|
|
} catch (Exception $e) {
|
|
/*
|
|
* To be consistent with the exception we return after an redirect,
|
|
* we convert this exception before returning it.
|
|
*/
|
|
throw new UnserializableException($e);
|
|
}
|
|
|
|
// Completed
|
|
}
|
|
|
|
|
|
/**
|
|
* Continues processing of the state.
|
|
*
|
|
* This function is used to resume processing by filters which for example needed to show
|
|
* a page to the user.
|
|
*
|
|
* This function will never return. Exceptions thrown during processing will be passed
|
|
* to whatever exception handler is defined in the state array.
|
|
*
|
|
* @param array $state The state we are processing.
|
|
* @return void
|
|
*/
|
|
public static function resumeProcessing($state)
|
|
{
|
|
assert(is_array($state));
|
|
|
|
while (count($state[self::FILTERS_INDEX]) > 0) {
|
|
$filter = array_shift($state[self::FILTERS_INDEX]);
|
|
try {
|
|
$filter->process($state);
|
|
} catch (Error\Exception $e) {
|
|
State::throwException($state, $e);
|
|
} catch (Exception $e) {
|
|
$e = new UnserializableException($e);
|
|
State::throwException($state, $e);
|
|
}
|
|
}
|
|
|
|
// Completed
|
|
|
|
assert(array_key_exists('ReturnURL', $state) || array_key_exists('ReturnCall', $state));
|
|
assert(!array_key_exists('ReturnURL', $state) || !array_key_exists('ReturnCall', $state));
|
|
|
|
|
|
if (array_key_exists('ReturnURL', $state)) {
|
|
/*
|
|
* Save state information, and redirect to the URL specified
|
|
* in $state['ReturnURL'].
|
|
*/
|
|
$id = State::saveState($state, self::COMPLETED_STAGE);
|
|
Utils\HTTP::redirectTrustedURL($state['ReturnURL'], [self::AUTHPARAM => $id]);
|
|
} else {
|
|
/* Pass the state to the function defined in $state['ReturnCall']. */
|
|
|
|
// We are done with the state array in the session. Delete it.
|
|
State::deleteState($state);
|
|
|
|
$func = $state['ReturnCall'];
|
|
assert(is_callable($func));
|
|
|
|
call_user_func($func, $state);
|
|
assert(false);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Process the given state passivly.
|
|
*
|
|
* Modules with user interaction are expected to throw an \SimpleSAML\Module\saml\Error\NoPassive exception
|
|
* which are silently ignored. Exceptions of other types are passed further up the call stack.
|
|
*
|
|
* This function will only return if processing completes.
|
|
*
|
|
* @param array &$state The state we are processing.
|
|
* @return void
|
|
*/
|
|
public function processStatePassive(&$state)
|
|
{
|
|
assert(is_array($state));
|
|
// Should not be set when calling this method
|
|
assert(!array_key_exists('ReturnURL', $state));
|
|
|
|
// Notify filters about passive request
|
|
$state['isPassive'] = true;
|
|
|
|
$state[self::FILTERS_INDEX] = $this->filters;
|
|
|
|
// TODO: remove this in SSP 2.0
|
|
if (!array_key_exists('UserID', $state)) {
|
|
// No unique user ID present. Attempt to add one.
|
|
self::addUserID($state);
|
|
}
|
|
|
|
while (count($state[self::FILTERS_INDEX]) > 0) {
|
|
$filter = array_shift($state[self::FILTERS_INDEX]);
|
|
try {
|
|
$filter->process($state);
|
|
} catch (Error\NoPassive $e) {
|
|
// @deprecated will be removed in 2.0
|
|
// Ignore \SimpleSAML\Error\NoPassive exceptions
|
|
} catch (Module\saml\Error\NoPassive $e) {
|
|
// Ignore \SimpleSAML\Module\saml\Error\NoPassive exceptions
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve a state which has finished processing.
|
|
*
|
|
* @param string $id The state identifier.
|
|
* @see State::parseStateID()
|
|
* @return array|null The state referenced by the $id parameter.
|
|
*/
|
|
public static function fetchProcessedState($id)
|
|
{
|
|
assert(is_string($id));
|
|
|
|
return State::loadState($id, self::COMPLETED_STAGE);
|
|
}
|
|
|
|
|
|
/**
|
|
* @deprecated This method will be removed in SSP 2.0.
|
|
* @param array &$state
|
|
* @return void
|
|
*/
|
|
private static function addUserID(&$state)
|
|
{
|
|
assert(is_array($state));
|
|
assert(array_key_exists('Attributes', $state));
|
|
|
|
if (isset($state['Destination']['userid.attribute'])) {
|
|
$attributeName = $state['Destination']['userid.attribute'];
|
|
Logger::debug("The 'userid.attribute' option has been deprecated.");
|
|
} elseif (isset($state['Source']['userid.attribute'])) {
|
|
$attributeName = $state['Source']['userid.attribute'];
|
|
Logger::debug("The 'userid.attribute' option has been deprecated.");
|
|
} else {
|
|
// Default attribute
|
|
$attributeName = 'eduPersonPrincipalName';
|
|
}
|
|
|
|
if (!array_key_exists($attributeName, $state['Attributes'])) {
|
|
return;
|
|
}
|
|
|
|
$uid = $state['Attributes'][$attributeName];
|
|
if (count($uid) === 0) {
|
|
Logger::warning('Empty user id attribute [' . $attributeName . '].');
|
|
return;
|
|
}
|
|
|
|
if (count($uid) > 1) {
|
|
Logger::warning('Multiple attribute values for user id attribute [' . $attributeName . '].');
|
|
return;
|
|
}
|
|
|
|
// TODO: the attribute value should be trimmed
|
|
$uid = $uid[0];
|
|
|
|
if (empty($uid)) {
|
|
Logger::warning('Empty value in attribute ' . $attributeName . ". on user. Cannot set UserID.");
|
|
return;
|
|
}
|
|
$state['UserID'] = $uid;
|
|
}
|
|
}
|