Initial Commit

This commit is contained in:
netkas 2020-08-26 15:36:35 -04:00
commit 89ac56f31b
2668 changed files with 287757 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
build/

1
.ppm_package Normal file
View File

@ -0,0 +1 @@
libsrc

9
Makefile Normal file
View File

@ -0,0 +1,9 @@
clean:
rm -rf build
build:
mkdir build
ppm --no-intro --compile="libsrc" --directory="build"
install:
ppm --no-prompt --fix-conflict --install="build/net.intellivoid.accounts.saml.ppm"

4
README.md Normal file
View File

@ -0,0 +1,4 @@
# Intellivoid Accounts SAML
This library implements SAML support, a library ported from https://simplesamlphp.org
but rebuilt for PPM and Intellivoid's current infrastructure.

View File

@ -0,0 +1,39 @@
<?php
namespace SAML\Auth;
use SAML\Configuration;
use SAML\Session;
/**
* Factory class to get instances of \SimpleSAML\Auth\Simple for a given authentication source.
*/
class AuthenticationFactory
{
/** @var Configuration */
protected $config;
/** @var Session */
protected $session;
public function __construct(Configuration $config, Session $session)
{
$this->config = $config;
$this->session = $session;
}
/**
* Create a new instance of \SimpleSAML\Auth\Simple for the given authentication source.
*
* @param string $as The identifier of the authentication source, as indexed in the authsources.php configuration
* file.
*
* @return Simple
*/
public function create($as)
{
return new Simple($as, $this->config, $this->session);
}
}

View File

@ -0,0 +1,167 @@
<?php
namespace SAML\Auth;
use Exception;
use SAML\Module\saml\Auth\Source\SP;
use SAML\Session;
use SAML\Utils;
/**
* Implements the default behaviour for authentication.
*
* This class contains an implementation for default behaviour when authenticating. It will
* save the session information it got from the authentication client in the users session.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*
* @deprecated This class will be removed in SSP 2.0.
*/
class DefaultAuth
{
/**
* @deprecated This method will be removed in SSP 2.0. Use Source::initLogin() instead.
* @param string $authId
* @param string $return
* @param string|null $errorURL
* @param array $params
* @return void
*/
public static function initLogin(
$authId,
$return,
$errorURL = null,
array $params = []
) {
$as = self::getAuthSource($authId);
$as->initLogin($return, $errorURL, $params);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use
* State::getPersistentAuthData() instead.
* @param array &$state
* @return array
*/
public static function extractPersistentAuthState(array &$state)
{
return State::getPersistentAuthData($state);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use Source::loginCompleted() instead.
* @param array $state
* @return void
*/
public static function loginCompleted($state)
{
Source::loginCompleted($state);
}
/**
* @deprecated This method will be removed in SSP 2.0.
* @param string $returnURL
* @param string $authority
* @return void
*/
public static function initLogoutReturn($returnURL, $authority)
{
assert(is_string($returnURL));
assert(is_string($authority));
$session = Session::getSessionFromRequest();
$state = $session->getAuthData($authority, 'LogoutState');
$session->doLogout($authority);
$state['\SimpleSAML\Auth\DefaultAuth.ReturnURL'] = $returnURL;
$state['LogoutCompletedHandler'] = [get_class(), 'logoutCompleted'];
$as = Source::getById($authority);
if ($as === null) {
// The authority wasn't an authentication source...
self::logoutCompleted($state);
}
$as->logout($state);
}
/**
* @deprecated This method will be removed in SSP 2.0.
* @param string $returnURL
* @param string $authority
* @return void
*/
public static function initLogout($returnURL, $authority)
{
assert(is_string($returnURL));
assert(is_string($authority));
self::initLogoutReturn($returnURL, $authority);
Utils\HTTP::redirectTrustedURL($returnURL);
}
/**
* @deprecated This method will be removed in SSP 2.0.
* @param array $state
* @return void
*/
public static function logoutCompleted($state)
{
assert(is_array($state));
assert(array_key_exists('\SimpleSAML\Auth\DefaultAuth.ReturnURL', $state));
Utils\HTTP::redirectTrustedURL($state['\SimpleSAML\Auth\DefaultAuth.ReturnURL']);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use Source::logoutCallback() instead.
* @param array $state
* @return void
*/
public static function logoutCallback($state)
{
Source::logoutCallback($state);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use
* \SimpleSAML\Module\saml\Auth\Source\SP::handleUnsolicitedAuth() instead.
* @param string $authId
* @param array $state
* @param string $redirectTo
* @return void
*/
public static function handleUnsolicitedAuth($authId, array $state, $redirectTo)
{
SP::handleUnsolicitedAuth($authId, $state, $redirectTo);
}
/**
* Return an authentication source by ID.
*
* @param string $id The id of the authentication source.
* @return Source The authentication source.
* @throws Exception If the $id does not correspond with an authentication source.
*/
private static function getAuthSource($id)
{
$as = Source::getById($id);
if ($as === null) {
throw new Exception('Invalid authentication source: ' . $id);
}
return $as;
}
}

18
libsrc/SAML/Auth/LDAP.php Normal file
View File

@ -0,0 +1,18 @@
<?php
namespace SAML\Auth;
use Exception;
use SAML\Logger;
use SAML\Module\ldap\Auth\Ldap;
Logger::warning("The class \SimpleSAML\Auth\LDAP has been moved to the ldap module, please use \SimpleSAML\Module\saml\Auth\Ldap instead.");
/**
* @deprecated To be removed in 2.0
*/
if (class_exists('\SimpleSAML\Module\ldap\Auth\Ldap')) {
class_alias(Ldap::class, 'SimpleSAML\Auth\LDAP');
} else {
throw new Exception('The ldap module is either missing or disabled');
}

View File

@ -0,0 +1,391 @@
<?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;
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace SAML\Auth;
use Exception;
/**
* Base class for authentication processing filters.
*
* All authentication processing filters must support serialization.
*
* The current request is stored in an associative array. It has the following defined attributes:
* - 'Attributes' The attributes of the user.
* - 'Destination' Metadata of the destination (SP).
* - 'Source' Metadata of the source (IdP).
*
* It may also contain other attributes. If an authentication processing filter wishes to store other
* information in it, it should have a name on the form 'module:filter:attributename', to avoid name
* collisions.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
abstract class ProcessingFilter
{
/**
* Priority of this filter.
*
* Used when merging IdP and SP processing chains.
* The priority can be any integer. The default for most filters is 50. Filters may however
* specify their own default, if they typically should be amongst the first or the last filters.
*
* The prioroty can also be overridden by the user by specifying the '%priority' option.
*/
public $priority = 50;
/**
* Constructor for a processing filter.
*
* Any processing filter which implements its own constructor must call this
* constructor first.
*
* @param array &$config Configuration for this filter.
* @param mixed $reserved For future use.
*/
public function __construct(&$config, $reserved)
{
assert(is_array($config));
if (array_key_exists('%priority', $config)) {
$this->priority = $config['%priority'];
if (!is_int($this->priority)) {
throw new Exception('Invalid priority: ' . var_export($this->priority, true));
}
unset($config['%priority']);
}
}
/**
* Process a request.
*
* When a filter returns from this function, it is assumed to have completed its task.
*
* @param array &$request The request we are currently processing.
*/
abstract public function process(&$request);
}

399
libsrc/SAML/Auth/Simple.php Normal file
View File

@ -0,0 +1,399 @@
<?php
namespace SAML\Auth;
use SAML\Configuration;
use SAML\Error;
use SAML\Error\AuthSource;
use SAML\Module;
use SAML\Session;
use SAML\Utils;
/**
* Helper class for simple authentication applications.
*
* @package SimpleSAMLphp
*/
class Simple
{
/**
* The id of the authentication source we are accessing.
*
* @var string
*/
protected $authSource;
/** @var Configuration */
protected $app_config;
/** @var Session */
protected $session;
/**
* Create an instance with the specified authsource.
*
* @param string $authSource The id of the authentication source.
* @param Configuration|null $config Optional configuration to use.
* @param Session|null $session Optional session to use.
*/
public function __construct($authSource, Configuration $config = null, Session $session = null)
{
assert(is_string($authSource));
if ($config === null) {
$config = Configuration::getInstance();
}
$this->authSource = $authSource;
$this->app_config = $config->getConfigItem('application');
if ($session === null) {
$session = Session::getSessionFromRequest();
}
$this->session = $session;
}
/**
* Retrieve the implementing authentication source.
*
* @return Source The authentication source.
*
* @throws AuthSource If the requested auth source is unknown.
*/
public function getAuthSource()
{
$as = Source::getById($this->authSource);
if ($as === null) {
throw new AuthSource($this->authSource, 'Unknown authentication source.');
}
return $as;
}
/**
* Check if the user is authenticated.
*
* This function checks if the user is authenticated with the default authentication source selected by the
* 'default-authsource' option in 'config.php'.
*
* @return bool True if the user is authenticated, false if not.
*/
public function isAuthenticated()
{
return $this->session->isValid($this->authSource);
}
/**
* Require the user to be authenticated.
*
* If the user is authenticated, this function returns immediately.
*
* If the user isn't authenticated, this function will authenticate the user with the authentication source, and
* then return the user to the current page.
*
* This function accepts an array $params, which controls some parts of the authentication. See the login()
* method for a description.
*
* @param array $params Various options to the authentication request. See the documentation.
* @return void
*/
public function requireAuth(array $params = [])
{
if ($this->session->isValid($this->authSource)) {
// Already authenticated
return;
}
$this->login($params);
}
/**
* Start an authentication process.
*
* This function accepts an array $params, which controls some parts of the authentication. The accepted parameters
* depends on the authentication source being used. Some parameters are generic:
* - 'ErrorURL': A URL that should receive errors from the authentication.
* - 'KeepPost': If the current request is a POST request, keep the POST data until after the authentication.
* - 'ReturnTo': The URL the user should be returned to after authentication.
* - 'ReturnCallback': The function we should call after the user has finished authentication.
*
* Please note: this function never returns.
*
* @param array $params Various options to the authentication request.
* @return void
*/
public function login(array $params = [])
{
if (array_key_exists('KeepPost', $params)) {
$keepPost = (bool) $params['KeepPost'];
} else {
$keepPost = true;
}
if (array_key_exists('ReturnTo', $params)) {
$returnTo = (string) $params['ReturnTo'];
} else {
if (array_key_exists('ReturnCallback', $params)) {
$returnTo = (array) $params['ReturnCallback'];
} else {
$returnTo = Utils\HTTP::getSelfURL();
}
}
if (is_string($returnTo) && $keepPost && $_SERVER['REQUEST_METHOD'] === 'POST') {
$returnTo = Utils\HTTP::getPOSTRedirectURL($returnTo, $_POST);
}
if (array_key_exists('ErrorURL', $params)) {
$errorURL = (string) $params['ErrorURL'];
} else {
$errorURL = null;
}
if (!isset($params[State::RESTART]) && is_string($returnTo)) {
/*
* A URL to restart the authentication, in case the user bookmarks
* something, e.g. the discovery service page.
*/
$restartURL = $this->getLoginURL($returnTo);
$params[State::RESTART] = $restartURL;
}
$as = $this->getAuthSource();
$as->initLogin($returnTo, $errorURL, $params);
assert(false);
}
/**
* Log the user out.
*
* This function logs the user out. It will never return. By default, it will cause a redirect to the current page
* after logging the user out, but a different URL can be given with the $params parameter.
*
* Generic parameters are:
* - 'ReturnTo': The URL the user should be returned to after logout.
* - 'ReturnCallback': The function that should be called after logout.
* - 'ReturnStateParam': The parameter we should return the state in when redirecting.
* - 'ReturnStateStage': The stage the state array should be saved with.
*
* @param string|array|null $params Either the URL the user should be redirected to after logging out, or an array
* with parameters for the logout. If this parameter is null, we will return to the current page.
* @return void
*/
public function logout($params = null)
{
assert(is_array($params) || is_string($params) || $params === null);
if ($params === null) {
$params = Utils\HTTP::getSelfURL();
}
if (is_string($params)) {
$params = [
'ReturnTo' => $params,
];
}
assert(is_array($params));
assert(isset($params['ReturnTo']) || isset($params['ReturnCallback']));
if (isset($params['ReturnStateParam']) || isset($params['ReturnStateStage'])) {
assert(isset($params['ReturnStateParam'], $params['ReturnStateStage']));
}
if ($this->session->isValid($this->authSource)) {
$state = $this->session->getAuthData($this->authSource, 'LogoutState');
if ($state !== null) {
$params = array_merge($state, $params);
}
$this->session->doLogout($this->authSource);
$params['LogoutCompletedHandler'] = [get_class(), 'logoutCompleted'];
$as = Source::getById($this->authSource);
if ($as !== null) {
$as->logout($params);
}
}
self::logoutCompleted($params);
}
/**
* Called when logout operation completes.
*
* This function never returns.
*
* @param array $state The state after the logout.
* @return void
*/
public static function logoutCompleted($state)
{
assert(is_array($state));
assert(isset($state['ReturnTo']) || isset($state['ReturnCallback']));
if (isset($state['ReturnCallback'])) {
call_user_func($state['ReturnCallback'], $state);
assert(false);
} else {
$params = [];
if (isset($state['ReturnStateParam']) || isset($state['ReturnStateStage'])) {
assert(isset($state['ReturnStateParam'], $state['ReturnStateStage']));
$stateID = State::saveState($state, $state['ReturnStateStage']);
$params[$state['ReturnStateParam']] = $stateID;
}
Utils\HTTP::redirectTrustedURL($state['ReturnTo'], $params);
}
}
/**
* Retrieve attributes of the current user.
*
* This function will retrieve the attributes of the current user if the user is authenticated. If the user isn't
* authenticated, it will return an empty array.
*
* @return array The users attributes.
*/
public function getAttributes()
{
if (!$this->isAuthenticated()) {
// Not authenticated
return [];
}
// Authenticated
return $this->session->getAuthData($this->authSource, 'Attributes');
}
/**
* Retrieve authentication data.
*
* @param string $name The name of the parameter, e.g. 'Attributes', 'Expire' or 'saml:sp:IdP'.
*
* @return mixed|null The value of the parameter, or null if it isn't found or we are unauthenticated.
*/
public function getAuthData($name)
{
assert(is_string($name));
if (!$this->isAuthenticated()) {
return null;
}
return $this->session->getAuthData($this->authSource, $name);
}
/**
* Retrieve all authentication data.
*
* @return array|null All persistent authentication data, or null if we aren't authenticated.
*/
public function getAuthDataArray()
{
if (!$this->isAuthenticated()) {
return null;
}
return $this->session->getAuthState($this->authSource);
}
/**
* Retrieve a URL that can be used to log the user in.
*
* @param string|null $returnTo The page the user should be returned to afterwards. If this parameter is null, the
* user will be returned to the current page.
*
* @return string A URL which is suitable for use in link-elements.
*/
public function getLoginURL($returnTo = null)
{
assert($returnTo === null || is_string($returnTo));
if ($returnTo === null) {
$returnTo = Utils\HTTP::getSelfURL();
}
$login = Module::getModuleURL('core/as_login.php', [
'AuthId' => $this->authSource,
'ReturnTo' => $returnTo,
]);
return $login;
}
/**
* Retrieve a URL that can be used to log the user out.
*
* @param string|null $returnTo The page the user should be returned to afterwards. If this parameter is null, the
* user will be returned to the current page.
*
* @return string A URL which is suitable for use in link-elements.
*/
public function getLogoutURL($returnTo = null)
{
assert($returnTo === null || is_string($returnTo));
if ($returnTo === null) {
$returnTo = Utils\HTTP::getSelfURL();
}
$logout = Module::getModuleURL('core/as_logout.php', [
'AuthId' => $this->authSource,
'ReturnTo' => $returnTo,
]);
return $logout;
}
/**
* Process a URL and modify it according to the application/baseURL configuration option, if present.
*
* @param string|null $url The URL to process, or null if we want to use the current URL. Both partial and full
* URLs can be used as a parameter. The maximum precedence is given to the application/baseURL configuration option,
* then the URL specified (if it specifies scheme, host and port) and finally the environment observed in the
* server.
*
* @return string The URL modified according to the precedence rules.
*/
protected function getProcessedURL($url = null)
{
if ($url === null) {
$url = Utils\HTTP::getSelfURL();
}
$scheme = parse_url($url, PHP_URL_SCHEME);
$host = parse_url($url, PHP_URL_HOST) ? : Utils\HTTP::getSelfHost();
$port = parse_url($url, PHP_URL_PORT) ? : (
$scheme ? '' : ltrim(Utils\HTTP::getServerPort(), ':')
);
$scheme = $scheme ? : (Utils\HTTP::getServerHTTPS() ? 'https' : 'http');
$path = parse_url($url, PHP_URL_PATH) ? : '/';
$query = parse_url($url, PHP_URL_QUERY) ? : '';
$fragment = parse_url($url, PHP_URL_FRAGMENT) ? : '';
$port = !empty($port) ? ':' . $port : '';
if (($scheme === 'http' && $port === ':80') || ($scheme === 'https' && $port === ':443')) {
$port = '';
}
$base = trim($this->app_config->getString(
'baseURL',
$scheme . '://' . $host . $port
), '/');
return $base . $path . ($query ? '?' . $query : '') . ($fragment ? '#' . $fragment : '');
}
}

541
libsrc/SAML/Auth/Source.php Normal file
View File

@ -0,0 +1,541 @@
<?php
namespace SAML\Auth;
use Exception;
use SAML\Configuration;
use SAML\Error;
use SAML\Logger;
use SAML\Module;
use SAML\Session;
use SAML\Utils;
/**
* This class defines a base class for authentication source.
*
* An authentication source is any system which somehow authenticate the user.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
abstract class Source
{
/**
* The authentication source identifier. This identifier can be used to look up this object, for example when
* returning from a login form.
*
* @var string
*/
protected $authId;
/**
* Constructor for an authentication source.
*
* Any authentication source which implements its own constructor must call this
* constructor first.
*
* @param array $info Information about this authentication source.
* @param array &$config Configuration for this authentication source.
*/
public function __construct($info, &$config)
{
assert(is_array($info));
assert(is_array($config));
assert(array_key_exists('AuthId', $info));
$this->authId = $info['AuthId'];
}
/**
* Get sources of a specific type.
*
* @param string $type The type of the authentication source.
*
* @return Source[] Array of \SimpleSAML\Auth\Source objects of the specified type.
* @throws Exception If the authentication source is invalid.
*/
public static function getSourcesOfType($type)
{
assert(is_string($type));
$config = Configuration::getConfig('authsources.php');
$ret = [];
$sources = $config->getOptions();
foreach ($sources as $id) {
$source = $config->getArray($id);
self::validateSource($source, $id);
if ($source[0] !== $type) {
continue;
}
$ret[] = self::parseAuthSource($id, $source);
}
return $ret;
}
/**
* Retrieve the ID of this authentication source.
*
* @return string The ID of this authentication source.
*/
public function getAuthId()
{
return $this->authId;
}
/**
* Process a request.
*
* If an authentication source returns from this function, it is assumed to have
* authenticated the user, and should have set elements in $state with the attributes
* of the user.
*
* If the authentication process requires additional steps which make it impossible to
* complete before returning from this function, the authentication source should
* save the state, and at a later stage, load the state, update it with the authentication
* information about the user, and call completeAuth with the state array.
*
* @param array &$state Information about the current authentication.
* @return void
*/
abstract public function authenticate(&$state);
/**
* Reauthenticate an user.
*
* This function is called by the IdP to give the authentication source a chance to
* interact with the user even in the case when the user is already authenticated.
*
* @param array &$state Information about the current authentication.
* @return void
*/
public function reauthenticate(array &$state)
{
assert(isset($state['ReturnCallback']));
// the default implementation just copies over the previous authentication data
$session = Session::getSessionFromRequest();
$data = $session->getAuthState($this->authId);
if ($data === null) {
throw new Error\NoState();
}
foreach ($data as $k => $v) {
$state[$k] = $v;
}
}
/**
* Complete authentication.
*
* This function should be called if authentication has completed. It will never return,
* except in the case of exceptions. Exceptions thrown from this page should not be caught,
* but should instead be passed to the top-level exception handler.
*
* @param array &$state Information about the current authentication.
* @return void
*/
public static function completeAuth(&$state)
{
assert(is_array($state));
assert(array_key_exists('LoginCompletedHandler', $state));
State::deleteState($state);
$func = $state['LoginCompletedHandler'];
assert(is_callable($func));
call_user_func($func, $state);
assert(false);
}
/**
* Start authentication.
*
* This method never returns.
*
* @param string|array $return The URL or function we should direct the user to after authentication. If using a
* URL obtained from user input, please make sure to check it by calling \SimpleSAML\Utils\HTTP::checkURLAllowed().
* @param string|null $errorURL The URL we should direct the user to after failed authentication. Can be null, in
* which case a standard error page will be shown. If using a URL obtained from user input, please make sure to
* check it by calling \SimpleSAML\Utils\HTTP::checkURLAllowed().
* @param array $params Extra information about the login. Different authentication requestors may provide different
* information. Optional, will default to an empty array.
* @return void
*/
public function initLogin($return, $errorURL = null, array $params = [])
{
assert(is_string($return) || is_array($return));
assert(is_string($errorURL) || $errorURL === null);
$state = array_merge($params, [
'\SimpleSAML\Auth\DefaultAuth.id' => $this->authId, // TODO: remove in 2.0
'\SimpleSAML\Auth\Source.id' => $this->authId,
'\SimpleSAML\Auth\DefaultAuth.Return' => $return, // TODO: remove in 2.0
'\SimpleSAML\Auth\Source.Return' => $return,
'\SimpleSAML\Auth\DefaultAuth.ErrorURL' => $errorURL, // TODO: remove in 2.0
'\SimpleSAML\Auth\Source.ErrorURL' => $errorURL,
'LoginCompletedHandler' => [get_class(), 'loginCompleted'],
'LogoutCallback' => [get_class(), 'logoutCallback'],
'LogoutCallbackState' => [
'\SimpleSAML\Auth\DefaultAuth.logoutSource' => $this->authId, // TODO: remove in 2.0
'\SimpleSAML\Auth\Source.logoutSource' => $this->authId,
],
]);
if (is_string($return)) {
$state['\SimpleSAML\Auth\DefaultAuth.ReturnURL'] = $return; // TODO: remove in 2.0
$state['\SimpleSAML\Auth\Source.ReturnURL'] = $return;
}
if ($errorURL !== null) {
$state[State::EXCEPTION_HANDLER_URL] = $errorURL;
}
try {
$this->authenticate($state);
} catch (Error\Exception $e) {
State::throwException($state, $e);
} catch (Exception $e) {
$e = new Error\UnserializableException($e);
State::throwException($state, $e);
}
self::loginCompleted($state);
}
/**
* Called when a login operation has finished.
*
* This method never returns.
*
* @param array $state The state after the login has completed.
* @return void
*/
public static function loginCompleted($state)
{
assert(is_array($state));
assert(array_key_exists('\SimpleSAML\Auth\Source.Return', $state));
assert(array_key_exists('\SimpleSAML\Auth\Source.id', $state));
assert(array_key_exists('Attributes', $state));
assert(!array_key_exists('LogoutState', $state) || is_array($state['LogoutState']));
$return = $state['\SimpleSAML\Auth\Source.Return'];
// save session state
$session = Session::getSessionFromRequest();
$authId = $state['\SimpleSAML\Auth\Source.id'];
$session->doLogin($authId, State::getPersistentAuthData($state));
if (is_string($return)) {
// redirect...
Utils\HTTP::redirectTrustedURL($return);
} else {
call_user_func($return, $state);
}
assert(false);
}
/**
* Log out from this authentication source.
*
* This function should be overridden if the authentication source requires special
* steps to complete a logout operation.
*
* If the logout process requires a redirect, the state should be saved. Once the
* logout operation is completed, the state should be restored, and completeLogout
* should be called with the state. If this operation can be completed without
* showing the user a page, or redirecting, this function should return.
*
* @param array &$state Information about the current logout operation.
* @return void
*/
public function logout(&$state)
{
assert(is_array($state));
// default logout handler which doesn't do anything
}
/**
* Complete logout.
*
* This function should be called after logout has completed. It will never return,
* except in the case of exceptions. Exceptions thrown from this page should not be caught,
* but should instead be passed to the top-level exception handler.
*
* @param array &$state Information about the current authentication.
* @return void
*/
public static function completeLogout(&$state)
{
assert(is_array($state));
assert(array_key_exists('LogoutCompletedHandler', $state));
State::deleteState($state);
$func = $state['LogoutCompletedHandler'];
assert(is_callable($func));
call_user_func($func, $state);
assert(false);
}
/**
* Create authentication source object from configuration array.
*
* This function takes an array with the configuration for an authentication source object,
* and returns the object.
*
* @param string $authId The authentication source identifier.
* @param array $config The configuration.
*
* @return Source The parsed authentication source.
* @throws Exception If the authentication source is invalid.
*/
private static function parseAuthSource($authId, $config)
{
assert(is_string($authId));
assert(is_array($config));
self::validateSource($config, $authId);
$id = $config[0];
$info = ['AuthId' => $authId];
$authSource = null;
unset($config[0]);
try {
// Check whether or not there's a factory responsible for instantiating our Auth Source instance
$factoryClass = Module::resolveClass(
$id,
'Auth\Source\Factory',
'\SimpleSAML\Auth\SourceFactory'
);
/** @var SourceFactory $factory */
$factory = new $factoryClass();
$authSource = $factory->create($info, $config);
} catch (Exception $e) {
// If not, instantiate the Auth Source here
$className = Module::resolveClass($id, 'Auth\Source', '\SimpleSAML\Auth\Source');
$authSource = new $className($info, $config);
}
/** @var Source */
return $authSource;
}
/**
* Retrieve authentication source.
*
* This function takes an id of an authentication source, and returns the
* AuthSource object. If no authentication source with the given id can be found,
* NULL will be returned.
*
* If the $type parameter is specified, this function will return an
* authentication source of the given type. If no authentication source or if an
* authentication source of a different type is found, an exception will be thrown.
*
* @param string $authId The authentication source identifier.
* @param string|null $type The type of authentication source. If NULL, any type will be accepted.
*
* @return Source|null The AuthSource object, or NULL if no authentication
* source with the given identifier is found.
* @throws \SAML\Error\Exception If no such authentication source is found or it is invalid.
*/
public static function getById($authId, $type = null)
{
assert(is_string($authId));
assert($type === null || is_string($type));
// for now - load and parse config file
$config = Configuration::getConfig('authsources.php');
$authConfig = $config->getArray($authId, null);
if ($authConfig === null) {
if ($type !== null) {
throw new Error\Exception(
'No authentication source with id ' .
var_export($authId, true) . ' found.'
);
}
return null;
}
$ret = self::parseAuthSource($authId, $authConfig);
if ($type === null || $ret instanceof $type) {
return $ret;
}
// the authentication source doesn't have the correct type
throw new Error\Exception(
'Invalid type of authentication source ' .
var_export($authId, true) . '. Was ' . var_export(get_class($ret), true) .
', should be ' . var_export($type, true) . '.'
);
}
/**
* Called when the authentication source receives an external logout request.
*
* @param array $state State array for the logout operation.
* @return void
*/
public static function logoutCallback($state)
{
assert(is_array($state));
assert(array_key_exists('\SimpleSAML\Auth\Source.logoutSource', $state));
$source = $state['\SimpleSAML\Auth\Source.logoutSource'];
$session = Session::getSessionFromRequest();
if (!$session->isValid($source)) {
Logger::warning(
'Received logout from an invalid authentication source ' .
var_export($source, true)
);
return;
}
$session->doLogout($source);
}
/**
* Add a logout callback association.
*
* This function adds a logout callback association, which allows us to initiate
* a logout later based on the $assoc-value.
*
* Note that logout-associations exists per authentication source. A logout association
* from one authentication source cannot be called from a different authentication source.
*
* @param string $assoc The identifier for this logout association.
* @param array $state The state array passed to the authenticate-function.
* @return void
*/
protected function addLogoutCallback($assoc, $state)
{
assert(is_string($assoc));
assert(is_array($state));
if (!array_key_exists('LogoutCallback', $state)) {
// the authentication requester doesn't have a logout callback
return;
}
$callback = $state['LogoutCallback'];
if (array_key_exists('LogoutCallbackState', $state)) {
$callbackState = $state['LogoutCallbackState'];
} else {
$callbackState = [];
}
$id = strlen($this->authId) . ':' . $this->authId . $assoc;
$data = [
'callback' => $callback,
'state' => $callbackState,
];
$session = Session::getSessionFromRequest();
$session->setData(
'\SimpleSAML\Auth\Source.LogoutCallbacks',
$id,
$data,
Session::DATA_TIMEOUT_SESSION_END
);
}
/**
* Call a logout callback based on association.
*
* This function calls a logout callback based on an association saved with
* addLogoutCallback(...).
*
* This function always returns.
*
* @param string $assoc The logout association which should be called.
* @return void
*/
protected function callLogoutCallback($assoc)
{
assert(is_string($assoc));
$id = strlen($this->authId) . ':' . $this->authId . $assoc;
$session = Session::getSessionFromRequest();
$data = $session->getData('\SimpleSAML\Auth\Source.LogoutCallbacks', $id);
if ($data === null) {
// FIXME: fix for IdP-first flow (issue 397) -> reevaluate logout callback infrastructure
$session->doLogout($this->authId);
return;
}
assert(is_array($data));
assert(array_key_exists('callback', $data));
assert(array_key_exists('state', $data));
$callback = $data['callback'];
$callbackState = $data['state'];
$session->deleteData('\SimpleSAML\Auth\Source.LogoutCallbacks', $id);
call_user_func($callback, $callbackState);
}
/**
* Retrieve list of authentication sources.
*
* @return array The id of all authentication sources.
*/
public static function getSources()
{
$config = Configuration::getOptionalConfig('authsources.php');
return $config->getOptions();
}
/**
* Make sure that the first element of an auth source is its identifier.
*
* @param array $source An array with the auth source configuration.
* @param string $id The auth source identifier.
*
* @throws Exception If the first element of $source is not an identifier for the auth source.
* @return void
*/
protected static function validateSource($source, $id)
{
if (!array_key_exists(0, $source) || !is_string($source[0])) {
throw new Exception(
'Invalid authentication source \'' . $id .
'\': First element must be a string which identifies the authentication source.'
);
}
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace SAML\Auth;
interface SourceFactory
{
/**
* @param array $info
* @param array $config
* @return Source
*/
public function create(array $info, array $config);
}

435
libsrc/SAML/Auth/State.php Normal file
View File

@ -0,0 +1,435 @@
<?php
namespace SAML\Auth;
use Exception;
use SAML\Configuration;
use SAML\Error;
use SAML\Error\NoState;
use SAML\Logger;
use SAML\Session;
use SAML\Utils;
/**
* This is a helper class for saving and loading state information.
*
* The state must be an associative array. This class will add additional keys to this
* array. These keys will always start with '\SimpleSAML\Auth\State.'.
*
* It is also possible to add a restart URL to the state. If state information is lost, for
* example because it timed out, or the user loaded a bookmarked page, the loadState function
* will redirect to this URL. To use this, set $state[\SimpleSAML\Auth\State::RESTART] to this
* URL.
*
* Both the saveState and the loadState function takes in a $stage parameter. This parameter is
* a security feature, and is used to prevent the user from taking a state saved one place and
* using it as input a different place.
*
* The $stage parameter must be a unique string. To maintain uniqueness, it must be on the form
* "<classname>.<identifier>" or "<module>:<identifier>".
*
* There is also support for passing exceptions through the state.
* By defining an exception handler when creating the state array, users of the state
* array can call throwException with the state and the exception. This exception will
* be passed to the handler defined by the EXCEPTION_HANDLER_URL or EXCEPTION_HANDLER_FUNC
* elements of the state array. Note that internally this uses the request parameter name
* defined in EXCEPTION_PARAM, which, for technical reasons, cannot contain a ".".
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class State
{
/**
* The index in the state array which contains the identifier.
*/
const ID = '\SimpleSAML\Auth\State.id';
/**
* The index in the cloned state array which contains the identifier of the
* original state.
*/
const CLONE_ORIGINAL_ID = '\SimpleSAML\Auth\State.cloneOriginalId';
/**
* The index in the state array which contains the current stage.
*/
const STAGE = '\SimpleSAML\Auth\State.stage';
/**
* The index in the state array which contains the restart URL.
*/
const RESTART = '\SimpleSAML\Auth\State.restartURL';
/**
* The index in the state array which contains the exception handler URL.
*/
const EXCEPTION_HANDLER_URL = '\SimpleSAML\Auth\State.exceptionURL';
/**
* The index in the state array which contains the exception handler function.
*/
const EXCEPTION_HANDLER_FUNC = '\SimpleSAML\Auth\State.exceptionFunc';
/**
* The index in the state array which contains the exception data.
*/
const EXCEPTION_DATA = '\SimpleSAML\Auth\State.exceptionData';
/**
* The stage of a state with an exception.
*/
const EXCEPTION_STAGE = '\SimpleSAML\Auth\State.exceptionStage';
/**
* The URL parameter which contains the exception state id.
* Note that this does not contain a "." since it's used in the
* _REQUEST superglobal that does not allow dots.
*/
const EXCEPTION_PARAM = '\SimpleSAML\Auth\State_exceptionId';
/**
* State timeout.
*/
private static $stateTimeout = null;
/**
* Get the persistent authentication state from the state array.
*
* @param array $state The state array to analyze.
*
* @return array The persistent authentication state.
*/
public static function getPersistentAuthData(array $state)
{
// save persistent authentication data
$persistent = [];
if (array_key_exists('PersistentAuthData', $state)) {
foreach ($state['PersistentAuthData'] as $key) {
if (isset($state[$key])) {
$persistent[$key] = $state[$key];
}
}
}
// add those that should always be included
$mandatory = [
'Attributes',
'Expire',
'LogoutState',
'AuthInstant',
'RememberMe',
'saml:sp:NameID'
];
foreach ($mandatory as $key) {
if (isset($state[$key])) {
$persistent[$key] = $state[$key];
}
}
return $persistent;
}
/**
* Retrieve the ID of a state array.
*
* Note that this function will not save the state.
*
* @param array &$state The state array.
* @param bool $rawId Return a raw ID, without a restart URL. Defaults to FALSE.
*
* @return string Identifier which can be used to retrieve the state later.
*/
public static function getStateId(&$state, $rawId = false)
{
assert(is_array($state));
assert(is_bool($rawId));
if (!array_key_exists(self::ID, $state)) {
$state[self::ID] = Utils\Random::generateID();
}
$id = $state[self::ID];
if ($rawId || !array_key_exists(self::RESTART, $state)) {
// Either raw ID or no restart URL. In any case, return the raw ID.
return $id;
}
// We have a restart URL. Return the ID with that URL.
return $id . ':' . $state[self::RESTART];
}
/**
* Retrieve state timeout.
*
* @return integer State timeout.
*/
private static function getStateTimeout()
{
if (self::$stateTimeout === null) {
$globalConfig = Configuration::getInstance();
self::$stateTimeout = $globalConfig->getInteger('session.state.timeout', 60 * 60);
}
return self::$stateTimeout;
}
/**
* Save the state.
*
* This function saves the state, and returns an id which can be used to
* retrieve it later. It will also update the $state array with the identifier.
*
* @param array &$state The login request state.
* @param string $stage The current stage in the login process.
* @param bool $rawId Return a raw ID, without a restart URL.
*
* @return string Identifier which can be used to retrieve the state later.
*/
public static function saveState(&$state, $stage, $rawId = false)
{
assert(is_array($state));
assert(is_string($stage));
assert(is_bool($rawId));
$return = self::getStateId($state, $rawId);
$id = $state[self::ID];
// Save stage
$state[self::STAGE] = $stage;
// Save state
$serializedState = serialize($state);
$session = Session::getSessionFromRequest();
$session->setData('\SimpleSAML\Auth\State', $id, $serializedState, self::getStateTimeout());
Logger::debug('Saved state: ' . var_export($return, true));
return $return;
}
/**
* Clone the state.
*
* This function clones and returns the new cloned state.
*
* @param array $state The original request state.
*
* @return array Cloned state data.
*/
public static function cloneState(array $state)
{
$clonedState = $state;
if (array_key_exists(self::ID, $state)) {
$clonedState[self::CLONE_ORIGINAL_ID] = $state[self::ID];
unset($clonedState[self::ID]);
Logger::debug('Cloned state: ' . var_export($state[self::ID], true));
} else {
Logger::debug('Cloned state with undefined id.');
}
return $clonedState;
}
/**
* Retrieve saved state.
*
* This function retrieves saved state information. If the state information has been lost,
* it will attempt to restart the request by calling the restart URL which is embedded in the
* state information. If there is no restart information available, an exception will be thrown.
*
* @param string $id State identifier (with embedded restart information).
* @param string $stage The stage the state should have been saved in.
* @param bool $allowMissing Whether to allow the state to be missing.
*
* @return array|null State information, or NULL if the state is missing and $allowMissing is true.
*@throws Exception If the stage of the state is invalid and there's no URL defined to redirect to.
*
* @throws NoState If we couldn't find the state and there's no URL defined to redirect to.
*/
public static function loadState($id, $stage, $allowMissing = false)
{
assert(is_string($id));
assert(is_string($stage));
assert(is_bool($allowMissing));
Logger::debug('Loading state: ' . var_export($id, true));
$sid = self::parseStateID($id);
$session = Session::getSessionFromRequest();
$state = $session->getData('\SimpleSAML\Auth\State', $sid['id']);
if ($state === null) {
// Could not find saved data
if ($allowMissing) {
return null;
}
if ($sid['url'] === null) {
throw new NoState();
}
Utils\HTTP::redirectUntrustedURL($sid['url']);
}
$state = unserialize($state);
assert(is_array($state));
assert(array_key_exists(self::ID, $state));
assert(array_key_exists(self::STAGE, $state));
// Verify stage
if ($state[self::STAGE] !== $stage) {
/* This could be a user trying to bypass security, but most likely it is just
* someone using the back-button in the browser. We try to restart the
* request if that is possible. If not, show an error.
*/
$msg = 'Wrong stage in state. Was \'' . $state[self::STAGE] .
'\', should be \'' . $stage . '\'.';
Logger::warning($msg);
if ($sid['url'] === null) {
throw new Exception($msg);
}
Utils\HTTP::redirectUntrustedURL($sid['url']);
}
return $state;
}
/**
* Delete state.
*
* This function deletes the given state to prevent the user from reusing it later.
*
* @param array &$state The state which should be deleted.
* @return void
*/
public static function deleteState(&$state)
{
assert(is_array($state));
if (!array_key_exists(self::ID, $state)) {
// This state hasn't been saved
return;
}
Logger::debug('Deleting state: ' . var_export($state[self::ID], true));
$session = Session::getSessionFromRequest();
$session->deleteData('\SimpleSAML\Auth\State', $state[self::ID]);
}
/**
* Throw exception to the state exception handler.
*
* @param array $state The state array.
* @param \SAML\Error\Exception $exception The exception.
*
* @return void
*@throws \SAML\Error\Exception If there is no exception handler defined, it will just throw the $exception.
*/
public static function throwException($state, Error\Exception $exception)
{
assert(is_array($state));
if (array_key_exists(self::EXCEPTION_HANDLER_URL, $state)) {
// Save the exception
$state[self::EXCEPTION_DATA] = $exception;
$id = self::saveState($state, self::EXCEPTION_STAGE);
// Redirect to the exception handler
Utils\HTTP::redirectTrustedURL(
$state[self::EXCEPTION_HANDLER_URL],
[self::EXCEPTION_PARAM => $id]
);
} elseif (array_key_exists(self::EXCEPTION_HANDLER_FUNC, $state)) {
// Call the exception handler
$func = $state[self::EXCEPTION_HANDLER_FUNC];
assert(is_callable($func));
call_user_func($func, $exception, $state);
assert(false);
} else {
/*
* No exception handler is defined for the current state.
*/
throw $exception;
}
throw new Exception(); // This should never happen
}
/**
* Retrieve an exception state.
*
* @param string|null $id The exception id. Can be NULL, in which case it will be retrieved from the request.
*
* @return array|null The state array with the exception, or NULL if no exception was thrown.
*/
public static function loadExceptionState($id = null)
{
assert(is_string($id) || $id === null);
if ($id === null) {
if (!array_key_exists(self::EXCEPTION_PARAM, $_REQUEST)) {
// No exception
return null;
}
$id = $_REQUEST[self::EXCEPTION_PARAM];
}
/** @var array $state */
$state = self::loadState($id, self::EXCEPTION_STAGE);
assert(array_key_exists(self::EXCEPTION_DATA, $state));
return $state;
}
/**
* Get the ID and (optionally) a URL embedded in a StateID, in the form 'id:url'.
*
* @param string $stateId The state ID to use.
*
* @return array A hashed array with the ID and the URL (if any), in the 'id' and 'url' keys, respectively. If
* there's no URL in the input parameter, NULL will be returned as the value for the 'url' key.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function parseStateID($stateId)
{
$tmp = explode(':', $stateId, 2);
$id = $tmp[0];
$url = null;
if (count($tmp) === 2) {
$url = $tmp[1];
}
return ['id' => $id, 'url' => $url];
}
}

View File

@ -0,0 +1,154 @@
<?php
namespace SAML\Auth;
use InvalidArgumentException;
use SAML\Utils;
/**
* A class that generates and verifies time-limited tokens.
*
* @deprecated This class was deprecated in 1.18 and will be removed in a future release
*/
class TimeLimitedToken
{
/**
* @var string
*/
protected $secretSalt;
/**
* @var int
*/
protected $lifetime;
/**
* @var int
*/
protected $skew;
/**
* @var string
*/
protected $algo;
/**
* Create a new time-limited token.
*
* @param int $lifetime Token lifetime in seconds. Defaults to 900 (15 min).
* @param string $secretSalt A random and unique salt per installation. Defaults to the salt in the configuration.
* @param int $skew The allowed time skew (in seconds) to correct clock deviations. Defaults to 1 second.
* @param string $algo The hash algorithm to use to generate the tokens. Defaults to SHA-256.
*
* @throws InvalidArgumentException if the given parameters are invalid.
*/
public function __construct($lifetime = 900, $secretSalt = null, $skew = 1, $algo = 'sha256')
{
if ($secretSalt === null) {
$secretSalt = Utils\Config::getSecretSalt();
}
if (!in_array($algo, hash_algos(), true)) {
throw new InvalidArgumentException('Invalid hash algorithm "' . $algo . '"');
}
$this->secretSalt = $secretSalt;
$this->lifetime = $lifetime;
$this->skew = $skew;
$this->algo = $algo;
}
/**
* Add some given data to the current token. This data will be needed later too for token validation.
*
* This mechanism can be used to provide context for a token, such as a user identifier of the only subject
* authorised to use it. Note also that multiple data can be added to the token. This means that upon validation,
* not only the same data must be added, but also in the same order.
*
* @param string $data The data to incorporate into the current token.
* @return void
*/
public function addVerificationData($data)
{
$this->secretSalt .= '|' . $data;
}
/**
* Calculates a token value for a given offset.
*
* @param int $offset The offset to use.
* @param int|null $time The time stamp to which the offset is relative to. Defaults to the current time.
*
* @return string The token for the given time and offset.
*/
private function calculateTokenValue($offset, $time = null)
{
if ($time === null) {
$time = time();
}
// a secret salt that should be randomly generated for each installation
return hash(
$this->algo,
$offset . ':' . floor(($time - $offset) / ($this->lifetime + $this->skew)) . ':' . $this->secretSalt
);
}
/**
* Generates a token that contains an offset and a token value, using the current offset.
*
* @return string A time-limited token with the offset respect to the beginning of its time slot prepended.
*/
public function generate()
{
$time = time();
$current_offset = ($time - $this->skew) % ($this->lifetime + $this->skew);
return dechex($current_offset) . '-' . $this->calculateTokenValue($current_offset, $time);
}
/**
* @see generate
* @deprecated This method will be removed in SSP 2.0. Use generate() instead.
* @return string
*/
public function generate_token()
{
return $this->generate();
}
/**
* Validates a token by calculating the token value for the provided offset and comparing it.
*
* @param string $token The token to validate.
*
* @return bool True if the given token is currently valid, false otherwise.
*/
public function validate($token)
{
$splittoken = explode('-', $token);
if (count($splittoken) !== 2) {
return false;
}
$offset = intval(hexdec($splittoken[0]));
$value = $splittoken[1];
return ($this->calculateTokenValue($offset) === $value);
}
/**
* @see validate
* @deprecated This method will be removed in SSP 2.0. Use validate() instead.
* @param string $token
* @return bool
*/
public function validate_token($token)
{
return $this->validate($token);
}
}

View File

@ -0,0 +1,192 @@
<?php
/**
* Implementation of the Shibboleth 1.3 Artifact binding.
*
* @package SimpleSAMLphp
* @deprecated This class will be removed in a future release
*/
namespace SAML\Bindings\Shib13;
use Exception;
use SAML2\DOMDocumentFactory;
use SAML\Configuration;
use SAML\Error;
use SAML\Utils;
class Artifact
{
/**
* Parse the query string, and extract the SAMLart parameters.
*
* This function is required because each query contains multiple
* artifact with the same parameter name.
*
* @return array The artifacts.
*/
private static function getArtifacts()
{
assert(array_key_exists('QUERY_STRING', $_SERVER));
// We need to process the query string manually, to capture all SAMLart parameters
$artifacts = [];
$elements = explode('&', $_SERVER['QUERY_STRING']);
foreach ($elements as $element) {
list($name, $value) = explode('=', $element, 2);
$name = urldecode($name);
$value = urldecode($value);
if ($name === 'SAMLart') {
$artifacts[] = $value;
}
}
return $artifacts;
}
/**
* Build the request we will send to the IdP.
*
* @param array $artifacts The artifacts we will request.
* @return string The request, as an XML string.
*/
private static function buildRequest(array $artifacts)
{
$msg = '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">'.
'<SOAP-ENV:Body>'.
'<samlp:Request xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol"'.
' RequestID="'.Utils\Random::generateID().'"'.
' MajorVersion="1" MinorVersion="1"'.
' IssueInstant="'.Utils\Time::generateTimestamp().'"'.
'>';
foreach ($artifacts as $a) {
$msg .= '<samlp:AssertionArtifact>'.htmlspecialchars($a).'</samlp:AssertionArtifact>';
}
$msg .= '</samlp:Request>'.
'</SOAP-ENV:Body>'.
'</SOAP-ENV:Envelope>';
return $msg;
}
/**
* Extract the response element from the SOAP response.
*
* @param string $soapResponse The SOAP response.
* @return string The <saml1p:Response> element, as a string.
* @throws Error\Exception
*/
private static function extractResponse($soapResponse)
{
assert(is_string($soapResponse));
try {
$doc = DOMDocumentFactory::fromString($soapResponse);
} catch (Exception $e) {
throw new Error\Exception('Error parsing SAML 1 artifact response.');
}
$soapEnvelope = $doc->firstChild;
if (!Utils\XML::isDOMNodeOfType($soapEnvelope, 'Envelope', 'http://schemas.xmlsoap.org/soap/envelope/')) {
throw new Error\Exception('Expected artifact response to contain a <soap:Envelope> element.');
}
$soapBody = Utils\XML::getDOMChildren($soapEnvelope, 'Body', 'http://schemas.xmlsoap.org/soap/envelope/');
if (count($soapBody) === 0) {
throw new Error\Exception('Couldn\'t find <soap:Body> in <soap:Envelope>.');
}
$soapBody = $soapBody[0];
$responseElement = Utils\XML::getDOMChildren($soapBody, 'Response', 'urn:oasis:names:tc:SAML:1.0:protocol');
if (count($responseElement) === 0) {
throw new Error\Exception('Couldn\'t find <saml1p:Response> in <soap:Body>.');
}
$responseElement = $responseElement[0];
/*
* Save the <saml1p:Response> element. Note that we need to import it
* into a new document, in order to preserve namespace declarations.
*/
$newDoc = DOMDocumentFactory::create();
$newDoc->appendChild($newDoc->importNode($responseElement, true));
$responseXML = $newDoc->saveXML();
return $responseXML;
}
/**
* This function receives a SAML 1.1 artifact.
*
* @param Configuration $spMetadata The metadata of the SP.
* @param Configuration $idpMetadata The metadata of the IdP.
* @return string The <saml1p:Response> element, as an XML string.
* @throws Error\Exception
*/
public static function receive(Configuration $spMetadata, Configuration $idpMetadata)
{
$artifacts = self::getArtifacts();
$request = self::buildRequest($artifacts);
Utils\XML::debugSAMLMessage($request, 'out');
/** @var array $url */
$url = $idpMetadata->getDefaultEndpoint(
'ArtifactResolutionService',
['urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding']
);
$url = $url['Location'];
$peerPublicKeys = $idpMetadata->getPublicKeys('signing', true);
$certData = '';
foreach ($peerPublicKeys as $key) {
if ($key['type'] !== 'X509Certificate') {
continue;
}
$certData .= "-----BEGIN CERTIFICATE-----\n".
chunk_split($key['X509Certificate'], 64).
"-----END CERTIFICATE-----\n";
}
$file = Utils\System::getTempDir().DIRECTORY_SEPARATOR.sha1($certData).'.crt';
if (!file_exists($file)) {
Utils\System::writeFile($file, $certData);
}
$spKeyCertFile = Utils\Config::getCertPath($spMetadata->getString('privatekey'));
$opts = [
'ssl' => [
'verify_peer' => true,
'cafile' => $file,
'local_cert' => $spKeyCertFile,
'capture_peer_cert' => true,
'capture_peer_chain' => true,
],
'http' => [
'method' => 'POST',
'content' => $request,
'header' => 'SOAPAction: http://www.oasis-open.org/committees/security'."\r\n".
'Content-Type: text/xml',
],
];
// Fetch the artifact
/** @var string $response */
$response = Utils\HTTP::fetch($url, $opts);
Utils\XML::debugSAMLMessage($response, 'in');
// Find the response in the SOAP message
$response = self::extractResponse($response);
return $response;
}
}

View File

@ -0,0 +1,156 @@
<?php
/**
* Implementation of the Shibboleth 1.3 HTTP-POST binding.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
* @deprecated This class will be removed in a future release
*/
namespace SAML\Bindings\Shib13;
use Exception;
use SAML2\DOMDocumentFactory;
use SAML\Configuration;
use SAML\Metadata\MetaDataStorageHandler;
use SAML\Utils;
use SAML\XML\Shib13\AuthnResponse;
use SAML\XML\Signer;
class HTTPPost
{
/**
* @var Configuration
*/
private $configuration;
/**
* @var MetaDataStorageHandler
*/
private $metadata;
/**
* Constructor for the \SimpleSAML\Bindings\Shib13\HTTPPost class.
*
* @param Configuration $configuration The configuration to use.
* @param MetaDataStorageHandler $metadatastore A store where to find metadata.
*/
public function __construct(
Configuration $configuration,
MetaDataStorageHandler $metadatastore
) {
$this->configuration = $configuration;
$this->metadata = $metadatastore;
}
/**
* Send an authenticationResponse using HTTP-POST.
*
* @param string $response The response which should be sent.
* @param Configuration $idpmd The metadata of the IdP which is sending the response.
* @param Configuration $spmd The metadata of the SP which is receiving the response.
* @param string|null $relayState The relaystate for the SP.
* @param string $shire The shire which should receive the response.
* @return void
*/
public function sendResponse(
$response,
Configuration $idpmd,
Configuration $spmd,
$relayState,
$shire
) {
Utils\XML::checkSAMLMessage($response, 'saml11');
$privatekey = Utils\Crypto::loadPrivateKey($idpmd, true);
$publickey = Utils\Crypto::loadPublicKey($idpmd, true);
$responsedom = DOMDocumentFactory::fromString(str_replace("\r", "", $response));
$responseroot = $responsedom->getElementsByTagName('Response')->item(0);
$firstassertionroot = $responsedom->getElementsByTagName('Assertion')->item(0);
/* Determine what we should sign - either the Response element or the Assertion. The default is to sign the
* Assertion, but that can be overridden by the 'signresponse' option in the SP metadata or
* 'saml20.signresponse' in the global configuration.
*
* TODO: neither 'signresponse' nor 'shib13.signresponse' are valid options any longer. Remove!
*/
if ($spmd->hasValue('signresponse')) {
$signResponse = $spmd->getBoolean('signresponse');
} else {
$signResponse = $this->configuration->getBoolean('shib13.signresponse', true);
}
// check if we have an assertion to sign. Force to sign the response if not
if ($firstassertionroot === null) {
$signResponse = true;
}
$signer = new Signer([
'privatekey_array' => $privatekey,
'publickey_array' => $publickey,
'id' => ($signResponse ? 'ResponseID' : 'AssertionID'),
]);
if ($idpmd->hasValue('certificatechain')) {
$signer->addCertificate($idpmd->getString('certificatechain'));
}
if ($signResponse) {
// sign the response - this must be done after encrypting the assertion
// we insert the signature before the saml2p:Status element
$statusElements = Utils\XML::getDOMChildren($responseroot, 'Status', '@saml1p');
assert(count($statusElements) === 1);
$signer->sign($responseroot, $responseroot, $statusElements[0]);
} else {
// Sign the assertion
$signer->sign($firstassertionroot, $firstassertionroot);
}
$response = $responsedom->saveXML();
Utils\XML::debugSAMLMessage($response, 'out');
Utils\HTTP::submitPOSTData($shire, [
'TARGET' => $relayState,
'SAMLResponse' => base64_encode($response),
]);
}
/**
* Decode a received response.
*
* @param array $post POST data received.
* @return AuthnResponse The response decoded into an object.
* @throws Exception If there is no SAMLResponse parameter.
*/
public function decodeResponse($post)
{
assert(is_array($post));
if (!array_key_exists('SAMLResponse', $post)) {
throw new Exception('Missing required SAMLResponse parameter.');
}
$rawResponse = $post['SAMLResponse'];
$samlResponseXML = base64_decode($rawResponse);
Utils\XML::debugSAMLMessage($samlResponseXML, 'in');
Utils\XML::checkSAMLMessage($samlResponseXML, 'saml11');
$samlResponse = new AuthnResponse();
$samlResponse->setXML($samlResponseXML);
if (array_key_exists('TARGET', $post)) {
$samlResponse->setRelayState($post['TARGET']);
}
return $samlResponse;
}
}

File diff suppressed because it is too large Load Diff

298
libsrc/SAML/Database.php Normal file
View File

@ -0,0 +1,298 @@
<?php
namespace SAML;
use Exception;
use PDO;
use PDOException;
use PDOStatement;
/**
* This file implements functions to read and write to a group of database servers.
*
* This database class supports a single database, or a master/slave configuration with as many defined slaves as a
* user would like.
*
* The goal of this class is to provide a single mechanism to connect to a database that can be reused by any component
* within SimpleSAMLphp including modules. When using this class, the global configuration should be passed here, but in
* the case of a module that has a good reason to use a different database, such as sqlauth, an alternative config file
* can be provided.
*
* @author Tyler Antonio, University of Alberta. <tantonio@ualberta.ca>
* @package SimpleSAMLphp
*/
class Database
{
/**
* This variable holds the instance of the session - Singleton approach.
*/
private static $instance = [];
/**
* PDO Object for the Master database server
*/
private $dbMaster;
/**
* Array of PDO Objects for configured database slaves
*/
private $dbSlaves = [];
/**
* Prefix to apply to the tables
*/
private $tablePrefix;
/**
* Array with information on the last error occurred.
*/
private $lastError;
/**
* Retrieves the current database instance. Will create a new one if there isn't an existing connection.
*
* @param Configuration $altConfig Optional: Instance of a \SimpleSAML\Configuration class
*
* @return Database The shared database connection.
*/
public static function getInstance($altConfig = null)
{
$config = ($altConfig) ? $altConfig : Configuration::getInstance();
$instanceId = self::generateInstanceId($config);
// check if we already have initialized the session
if (isset(self::$instance[$instanceId])) {
return self::$instance[$instanceId];
}
// create a new session
self::$instance[$instanceId] = new Database($config);
return self::$instance[$instanceId];
}
/**
* Private constructor that restricts instantiation to getInstance().
*
* @param Configuration $config Instance of the \SimpleSAML\Configuration class
*/
private function __construct($config)
{
$driverOptions = $config->getArray('database.driver_options', []);
if ($config->getBoolean('database.persistent', true)) {
$driverOptions = [PDO::ATTR_PERSISTENT => true];
}
// connect to the master
$this->dbMaster = $this->connect(
$config->getString('database.dsn'),
$config->getString('database.username', null),
$config->getString('database.password', null),
$driverOptions
);
// connect to any configured slaves
$slaves = $config->getArray('database.slaves', []);
foreach ($slaves as $slave) {
array_push(
$this->dbSlaves,
$this->connect(
$slave['dsn'],
$slave['username'],
$slave['password'],
$driverOptions
)
);
}
$this->tablePrefix = $config->getString('database.prefix', '');
}
/**
* Generate an Instance ID based on the database configuration.
*
* @param Configuration $config Configuration class
*
* @return string $instanceId
*/
private static function generateInstanceId($config)
{
$assembledConfig = [
'master' => [
'database.dsn' => $config->getString('database.dsn'),
'database.username' => $config->getString('database.username', null),
'database.password' => $config->getString('database.password', null),
'database.prefix' => $config->getString('database.prefix', ''),
'database.persistent' => $config->getBoolean('database.persistent', false),
],
'slaves' => $config->getArray('database.slaves', []),
];
return sha1(serialize($assembledConfig));
}
/**
* This function connects to a database.
*
* @param string $dsn Database connection string
* @param string $username SQL user
* @param string $password SQL password
* @param array $options PDO options
*
* @return PDO object
*@throws Exception If an error happens while trying to connect to the database.
*/
private function connect($dsn, $username, $password, $options)
{
try {
$db = new PDO($dsn, $username, $password, $options);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
return $db;
} catch (PDOException $e) {
throw new Exception("Database error: " . $e->getMessage());
}
}
/**
* This function randomly selects a slave database server to query. In the event no slaves are configured, it will
* return the master.
*
* @return PDO object
*/
private function getSlave()
{
if (count($this->dbSlaves) > 0) {
$slaveId = rand(0, count($this->dbSlaves) - 1);
return $this->dbSlaves[$slaveId];
} else {
return $this->dbMaster;
}
}
/**
* This function simply applies the table prefix to a supplied table name.
*
* @param string $table Table to apply prefix to, if configured
*
* @return string Table with configured prefix
*/
public function applyPrefix($table)
{
return $this->tablePrefix . $table;
}
/**
* This function queries the database
*
* @param PDO $db PDO object to use
* @param string $stmt Prepared SQL statement
* @param array $params Parameters
*
* @return PDOStatement object
*@throws Exception If an error happens while trying to execute the query.
*/
private function query($db, $stmt, $params)
{
assert(is_object($db));
assert(is_string($stmt));
assert(is_array($params));
try {
$query = $db->prepare($stmt);
foreach ($params as $param => $value) {
if (is_array($value)) {
$query->bindValue(":$param", $value[0], ($value[1]) ? $value[1] : PDO::PARAM_STR);
} else {
$query->bindValue(":$param", $value, PDO::PARAM_STR);
}
}
$query->execute();
return $query;
} catch (PDOException $e) {
$this->lastError = $db->errorInfo();
throw new Exception("Database error: " . $e->getMessage());
}
}
/**
* This function queries the database without using a prepared statement.
*
* @param PDO $db PDO object to use
* @param string $stmt An SQL statement to execute, previously escaped.
*
* @return int The number of rows affected.
*@throws Exception If an error happens while trying to execute the query.
*/
private function exec($db, $stmt)
{
assert(is_object($db));
assert(is_string($stmt));
try {
return $db->exec($stmt);
} catch (PDOException $e) {
$this->lastError = $db->errorInfo();
throw new Exception("Database error: " . $e->getMessage());
}
}
/**
* This executes queries directly on the master.
*
* @param string $stmt Prepared SQL statement
* @param array $params Parameters
*
* @return int|false The number of rows affected by the query or false on error.
*/
public function write($stmt, $params = [])
{
$db = $this->dbMaster;
if (is_array($params)) {
$obj = $this->query($db, $stmt, $params);
return ($obj === false) ? $obj : $obj->rowCount();
} else {
return $this->exec($db, $stmt);
}
}
/**
* This executes queries on a database server that is determined by this::getSlave().
*
* @param string $stmt Prepared SQL statement
* @param array $params Parameters
*
* @return PDOStatement object
*/
public function read($stmt, $params = [])
{
$db = $this->getSlave();
return $this->query($db, $stmt, $params);
}
/**
* Return an array with information about the last operation performed in the database.
*
* @return array The array with error information.
*/
public function getLastError()
{
return $this->lastError;
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace SAML\Error;
/**
* Class for creating exceptions from assertion failures.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class Assertion extends Exception
{
/**
* The assertion which failed, or null if only an expression was passed to the
* assert-function.
*/
private $assertion;
/**
* Constructor for the assertion exception.
*
* Should only be called from the onAssertion handler.
*
* @param string|null $assertion The assertion which failed, or null if the assert-function was
* given an expression.
*/
public function __construct($assertion = null)
{
assert($assertion === null || is_string($assertion));
$msg = 'Assertion failed: ' . var_export($assertion, true);
parent::__construct($msg);
$this->assertion = $assertion;
}
/**
* Retrieve the assertion which failed.
*
* @return string|null The assertion which failed, or null if the assert-function was called with an expression.
*/
public function getAssertion()
{
return $this->assertion;
}
/**
* Install this assertion handler.
*
* This function will register this assertion handler. If will not enable assertions if they are
* disabled.
* @return void
*/
public static function installHandler()
{
assert_options(ASSERT_WARNING, 0);
assert_options(ASSERT_QUIET_EVAL, 0);
assert_options(ASSERT_CALLBACK, [Assertion::class, 'onAssertion']);
}
/**
* Handle assertion.
*
* This function handles an assertion.
*
* @param string $file The file assert was called from.
* @param int $line The line assert was called from.
* @param mixed $message The expression which was passed to the assert-function.
* @return void
*/
public static function onAssertion($file, $line, $message)
{
if (!empty($message)) {
$exception = new self($message);
} else {
$exception = new self();
}
$exception->logError();
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace SAML\Error;
/**
* Baseclass for auth source exceptions.
*
* @package SimpleSAMLphp_base
*
*/
class AuthSource extends Error
{
/**
* Authsource module name
* @var string
*/
private $authsource;
/**
* Reason why this request was invalid.
* @var string
*/
private $reason;
/**
* Create a new AuthSource error.
*
* @param string $authsource Authsource module name from where this error was thrown.
* @param string $reason Description of the error.
* @param \Exception|null $cause
*/
public function __construct($authsource, $reason, $cause = null)
{
assert(is_string($authsource));
assert(is_string($reason));
$this->authsource = $authsource;
$this->reason = $reason;
parent::__construct(
[
'AUTHSOURCEERROR',
'%AUTHSOURCE%' => htmlspecialchars(var_export($this->authsource, true)),
'%REASON%' => htmlspecialchars(var_export($this->reason, true))
],
$cause
);
$this->message = "Error with authentication source '$authsource': $reason";
}
/**
* Retrieve the authsource module name from where this error was thrown.
*
* @return string Authsource module name.
*/
public function getAuthSource()
{
return $this->authsource;
}
/**
* Retrieve the reason why the request was invalid.
*
* @return string The reason why the request was invalid.
*/
public function getReason()
{
return $this->reason;
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace SAML\Error;
/**
* Exception which will show a 400 Bad Request error page.
*
* This exception can be thrown from within an module page handler. The user will then be
* shown a 400 Bad Request error page.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class BadRequest extends Error
{
/**
* Reason why this request was invalid.
* @var string
*/
private $reason;
/**
* Create a new BadRequest error.
*
* @param string $reason Description of why the request was unacceptable.
*/
public function __construct($reason)
{
assert(is_string($reason));
$this->reason = $reason;
parent::__construct(['BADREQUEST', '%REASON%' => $this->reason]);
$this->httpCode = 400;
}
/**
* Retrieve the reason why the request was invalid.
*
* @return string The reason why the request was invalid.
*/
public function getReason()
{
return $this->reason;
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace SAML\Error;
/**
* Exception indicating illegal innput from user.
*
* @author Thomas Graff <thomas.graff@uninett.no>
* @package SimpleSAMLphp_base
*
*/
class BadUserInput extends User
{
}

View File

@ -0,0 +1,35 @@
<?php
namespace SAML\Error;
/**
* Exception to indicate that we cannot set a cookie.
*
* @author Jaime Pérez Crespo <jaime.perez@uninett.no>
* @package SimpleSAMLphp
*/
class CannotSetCookie extends Exception
{
/**
* The exception was thrown for unknown reasons.
*
* @var int
*/
const UNKNOWN = 0;
/**
* The exception was due to the HTTP headers being already sent, and therefore we cannot send additional headers to
* set the cookie.
*
* @var int
*/
const HEADERS_SENT = 1;
/**
* The exception was due to trying to set a secure cookie over an insecure channel.
*
* @var int
*/
const SECURE_COOKIE = 2;
}

View File

@ -0,0 +1,77 @@
<?php
namespace SAML\Error;
/**
* This exception represents a configuration error.
*
* @author Jaime Perez Crespo, UNINETT AS <jaime.perez@uninett.no>
* @package SimpleSAMLphp
*/
class ConfigurationError extends Error
{
/**
* The reason for this exception.
*
* @var null|string
*/
protected $reason;
/**
* The configuration file that caused this exception.
*
* @var null|string
*/
protected $config_file;
/**
* ConfigurationError constructor.
*
* @param string|null $reason The reason for this exception.
* @param string|null $file The configuration file that originated this error.
* @param array|null $config The configuration array that led to this problem.
*/
public function __construct($reason = null, $file = null, array $config = null)
{
$file_str = '';
$reason_str = '.';
$params = ['CONFIG'];
if ($file !== null) {
$params['%FILE%'] = $file;
$basepath = dirname(dirname(dirname(dirname(__FILE__)))) . '/';
$file_str = '(' . str_replace($basepath, '', $file) . ') ';
}
if ($reason !== null) {
$params['%REASON%'] = $reason;
$reason_str = ': ' . $reason;
}
$this->reason = $reason;
$this->config_file = $file;
parent::__construct($params);
$this->message = 'The configuration ' . $file_str . 'is invalid' . $reason_str;
}
/**
* Get the reason for this exception.
*
* @return null|string The reason for this exception.
*/
public function getReason()
{
return $this->reason;
}
/**
* Get the configuration file that caused this exception.
*
* @return null|string The configuration file that caused this exception.
*/
public function getConfFile()
{
return $this->config_file;
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace SAML\Error;
use SAML\Configuration;
use SAML\Logger;
use SAML\Utils;
/**
* This exception represents a configuration error that we cannot recover from.
*
* Throwing a critical configuration error indicates that the configuration available is not usable, and as such
* SimpleSAMLphp should not try to use it. However, in certain situations we might find a specific configuration
* error that makes part of the configuration unusable, while the rest we can still use. In those cases, we can
* just pass a configuration array to the constructor, making sure the offending configuration options are removed,
* reset to defaults or guessed to some usable value.
*
* If, for example, we have an error in the 'baseurlpath' configuration option, we can still load the configuration
* and substitute the value of that option with one guessed from the environment, using
* \SimpleSAML\Utils\HTTP::guessPath(). Doing so, the error is still critical, but at least we can recover up to a
* certain point and inform about the error in an ordered manner, without blank pages, logs out of place or even
* segfaults.
*
* @author Jaime Perez Crespo, UNINETT AS <jaime.perez@uninett.no>
* @package SimpleSAMLphp
*/
class CriticalConfigurationError extends ConfigurationError
{
/**
* This is the bare minimum configuration that we can use.
*
* @var array
*/
private static $minimum_config = [
'logging.handler' => 'errorlog',
'logging.level' => Logger::DEBUG,
'errorreporting' => false,
'debug' => true,
];
/**
* CriticalConfigurationError constructor.
*
* @param string|null $reason The reason for this critical error.
* @param string|null $file The configuration file that originated this error.
* @param array|null $config The configuration array that led to this problem.
*/
public function __construct($reason = null, $file = null, $config = null)
{
if ($config === null) {
$config = self::$minimum_config;
$config['baseurlpath'] = Utils\HTTP::guessBasePath();
}
Configuration::loadFromArray(
$config,
'',
'simplesaml'
);
parent::__construct($reason, $file);
}
/**
* @param \Exception $exception
*
* @return CriticalConfigurationError
*/
public static function fromException(\Exception $exception)
{
$reason = null;
$file = null;
if ($exception instanceof ConfigurationError) {
$reason = $exception->getReason();
$file = $exception->getConfFile();
} else {
$reason = $exception->getMessage();
}
return new CriticalConfigurationError($reason, $file);
}
}

278
libsrc/SAML/Error/Error.php Normal file
View File

@ -0,0 +1,278 @@
<?php
namespace SAML\Error;
use SAML\Configuration;
use SAML\Logger;
use SAML\Session;
use SAML\Utils;
use SAML\XHTML\Template;
/**
* Class that wraps SimpleSAMLphp errors in exceptions.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class Error extends Exception
{
/**
* The error code.
*
* @var string
*/
private $errorCode;
/**
* The http code.
*
* @var integer
*/
protected $httpCode = 500;
/**
* The error title tag in dictionary.
*
* @var string
*/
private $dictTitle;
/**
* The error description tag in dictionary.
*
* @var string
*/
private $dictDescr;
/**
* The name of module that threw the error.
*
* @var string|null
*/
private $module = null;
/**
* The parameters for the error.
*
* @var array
*/
private $parameters;
/**
* Name of custom include template for the error.
*
* @var string|null
*/
protected $includeTemplate = null;
/**
* Constructor for this error.
*
* The error can either be given as a string, or as an array. If it is an array, the first element in the array
* (with index 0), is the error code, while the other elements are replacements for the error text.
*
* @param mixed $errorCode One of the error codes defined in the errors dictionary.
* @param \Exception $cause The exception which caused this fatal error (if any). Optional.
* @param int|null $httpCode The HTTP response code to use. Optional.
*/
public function __construct($errorCode, \Exception $cause = null, $httpCode = null)
{
assert(is_string($errorCode) || is_array($errorCode));
if (is_array($errorCode)) {
$this->parameters = $errorCode;
unset($this->parameters[0]);
$this->errorCode = $errorCode[0];
} else {
$this->parameters = [];
$this->errorCode = $errorCode;
}
if (isset($httpCode)) {
$this->httpCode = $httpCode;
}
$this->dictTitle = ErrorCodes::getErrorCodeTitle($this->errorCode);
$this->dictDescr = ErrorCodes::getErrorCodeDescription($this->errorCode);
if (!empty($this->parameters)) {
$msg = $this->errorCode . '(';
foreach ($this->parameters as $k => $v) {
if ($k === 0) {
continue;
}
$msg .= var_export($k, true) . ' => ' . var_export($v, true) . ', ';
}
$msg = substr($msg, 0, -2) . ')';
} else {
$msg = $this->errorCode;
}
parent::__construct($msg, -1, $cause);
}
/**
* Retrieve the error code given when throwing this error.
*
* @return string The error code.
*/
public function getErrorCode()
{
return $this->errorCode;
}
/**
* Retrieve the error parameters given when throwing this error.
*
* @return array The parameters.
*/
public function getParameters()
{
return $this->parameters;
}
/**
* Retrieve the error title tag in dictionary.
*
* @return string The error title tag.
*/
public function getDictTitle()
{
return $this->dictTitle;
}
/**
* Retrieve the error description tag in dictionary.
*
* @return string The error description tag.
*/
public function getDictDescr()
{
return $this->dictDescr;
}
/**
* Set the HTTP return code for this error.
*
* This should be overridden by subclasses who want a different return code than 500 Internal Server Error.
* @return void
*/
protected function setHTTPCode()
{
http_response_code($this->httpCode);
}
/**
* Save an error report.
*
* @return array The array with the error report data.
*/
protected function saveError()
{
$data = $this->format(true);
$emsg = array_shift($data);
$etrace = implode("\n", $data);
$reportId = bin2hex(openssl_random_pseudo_bytes(4));
Logger::error('Error report with id ' . $reportId . ' generated.');
$config = Configuration::getInstance();
$session = Session::getSessionFromRequest();
if (isset($_SERVER['HTTP_REFERER'])) {
$referer = $_SERVER['HTTP_REFERER'];
// remove anything after the first '?' or ';', just in case it contains any sensitive data
$referer = explode('?', $referer, 2);
$referer = $referer[0];
$referer = explode(';', $referer, 2);
$referer = $referer[0];
} else {
$referer = 'unknown';
}
$errorData = [
'exceptionMsg' => $emsg,
'exceptionTrace' => $etrace,
'reportId' => $reportId,
'trackId' => $session->getTrackID(),
'url' => Utils\HTTP::getSelfURLNoQuery(),
'version' => $config->getVersion(),
'referer' => $referer,
];
$session->setData('core:errorreport', $reportId, $errorData);
return $errorData;
}
/**
* Display this error.
*
* This method displays a standard SimpleSAMLphp error page and exits.
* @return void
*/
public function show()
{
$this->setHTTPCode();
// log the error message
$this->logError();
$errorData = $this->saveError();
$config = Configuration::getInstance();
$data = [];
$data['showerrors'] = $config->getBoolean('showerrors', true);
$data['error'] = $errorData;
$data['errorCode'] = $this->errorCode;
$data['parameters'] = $this->parameters;
$data['module'] = $this->module;
$data['dictTitle'] = $this->dictTitle;
$data['dictDescr'] = $this->dictDescr;
$data['includeTemplate'] = $this->includeTemplate;
$data['clipboard.js'] = true;
// check if there is a valid technical contact email address
if (
$config->getBoolean('errorreporting', true)
&& $config->getString('technicalcontact_email', 'na@example.org') !== 'na@example.org'
) {
// enable error reporting
$baseurl = Utils\HTTP::getBaseURL();
$data['errorReportAddress'] = $baseurl . 'errorreport.php';
}
$data['email'] = '';
$session = Session::getSessionFromRequest();
$authorities = $session->getAuthorities();
foreach ($authorities as $authority) {
$attributes = $session->getAuthData($authority, 'Attributes');
if ($attributes !== null && array_key_exists('mail', $attributes) && count($attributes['mail']) > 0) {
$data['email'] = $attributes['mail'][0];
break; // enough, don't need to get all available mails, if more than one
}
}
$show_function = $config->getArray('errors.show_function', null);
if (isset($show_function)) {
assert(is_callable($show_function));
call_user_func($show_function, $config, $data);
assert(false);
} else {
$t = new Template($config, 'error.php', 'errors');
$translator = $t->getTranslator();
$t->data = array_merge($t->data, $data);
$t->data['dictTitleTranslated'] = $translator->t($t->data['dictTitle']);
$t->data['dictDescrTranslated'] = $translator->t($t->data['dictDescr'], $t->data['parameters']);
$t->show();
}
exit;
}
}

View File

@ -0,0 +1,191 @@
<?php
namespace SAML\Error;
use SAML\Locale\Translate;
/**
* Class that maps SimpleSAMLphp error codes to translateable strings.
*
* @author Hanne Moa, UNINETT AS. <hanne.moa@uninett.no>
* @package SimpleSAMLphp
*/
class ErrorCodes
{
/**
* Fetch all default translation strings for error code titles.
*
* @return array A map from error code to error code title
*/
final public static function defaultGetAllErrorCodeTitles()
{
return [
'ACSPARAMS' => Translate::noop('{errors:title_ACSPARAMS}'),
'ARSPARAMS' => Translate::noop('{errors:title_ARSPARAMS}'),
'AUTHSOURCEERROR' => Translate::noop('{errors:title_AUTHSOURCEERROR}'),
'BADREQUEST' => Translate::noop('{errors:title_BADREQUEST}'),
'CASERROR' => Translate::noop('{errors:title_CASERROR}'),
'CONFIG' => Translate::noop('{errors:title_CONFIG}'),
'CREATEREQUEST' => Translate::noop('{errors:title_CREATEREQUEST}'),
'DISCOPARAMS' => Translate::noop('{errors:title_DISCOPARAMS}'),
'GENERATEAUTHNRESPONSE' => Translate::noop('{errors:title_GENERATEAUTHNRESPONSE}'),
'INVALIDCERT' => Translate::noop('{errors:title_INVALIDCERT}'),
'LDAPERROR' => Translate::noop('{errors:title_LDAPERROR}'),
'LOGOUTINFOLOST' => Translate::noop('{errors:title_LOGOUTINFOLOST}'),
'LOGOUTREQUEST' => Translate::noop('{errors:title_LOGOUTREQUEST}'),
'MEMCACHEDOWN' => Translate::noop('{errors:title_MEMCACHEDOWN}'),
'METADATA' => Translate::noop('{errors:title_METADATA}'),
'METADATANOTFOUND' => Translate::noop('{errors:title_METADATANOTFOUND}'),
'NOACCESS' => Translate::noop('{errors:title_NOACCESS}'),
'NOCERT' => Translate::noop('{errors:title_NOCERT}'),
'NORELAYSTATE' => Translate::noop('{errors:title_NORELAYSTATE}'),
'NOSTATE' => Translate::noop('{errors:title_NOSTATE}'),
'NOTFOUND' => Translate::noop('{errors:title_NOTFOUND}'),
'NOTFOUNDREASON' => Translate::noop('{errors:title_NOTFOUNDREASON}'),
'NOTSET' => Translate::noop('{errors:title_NOTSET}'),
'NOTVALIDCERT' => Translate::noop('{errors:title_NOTVALIDCERT}'),
'PROCESSASSERTION' => Translate::noop('{errors:title_PROCESSASSERTION}'),
'PROCESSAUTHNREQUEST' => Translate::noop('{errors:title_PROCESSAUTHNREQUEST}'),
'RESPONSESTATUSNOSUCCESS' => Translate::noop('{errors:title_RESPONSESTATUSNOSUCCESS}'),
'SLOSERVICEPARAMS' => Translate::noop('{errors:title_SLOSERVICEPARAMS}'),
'SSOPARAMS' => Translate::noop('{errors:title_SSOPARAMS}'),
'UNHANDLEDEXCEPTION' => Translate::noop('{errors:title_UNHANDLEDEXCEPTION}'),
'UNKNOWNCERT' => Translate::noop('{errors:title_UNKNOWNCERT}'),
'USERABORTED' => Translate::noop('{errors:title_USERABORTED}'),
'WRONGUSERPASS' => Translate::noop('{errors:title_WRONGUSERPASS}'),
];
}
/**
* Fetch all translation strings for error code titles.
*
* Extend this to add error codes.
*
* @return array A map from error code to error code title
*/
public static function getAllErrorCodeTitles()
{
return self::defaultGetAllErrorCodeTitles();
}
/**
* Fetch all default translation strings for error code descriptions.
*
* @return array A map from error code to error code description
*/
final public static function defaultGetAllErrorCodeDescriptions()
{
return [
'ACSPARAMS' => Translate::noop('{errors:descr_ACSPARAMS}'),
'ARSPARAMS' => Translate::noop('{errors:descr_ARSPARAMS}'),
'AUTHSOURCEERROR' => Translate::noop('{errors:descr_AUTHSOURCEERROR}'),
'BADREQUEST' => Translate::noop('{errors:descr_BADREQUEST}'),
'CASERROR' => Translate::noop('{errors:descr_CASERROR}'),
'CONFIG' => Translate::noop('{errors:descr_CONFIG}'),
'CREATEREQUEST' => Translate::noop('{errors:descr_CREATEREQUEST}'),
'DISCOPARAMS' => Translate::noop('{errors:descr_DISCOPARAMS}'),
'GENERATEAUTHNRESPONSE' => Translate::noop('{errors:descr_GENERATEAUTHNRESPONSE}'),
'INVALIDCERT' => Translate::noop('{errors:descr_INVALIDCERT}'),
'LDAPERROR' => Translate::noop('{errors:descr_LDAPERROR}'),
'LOGOUTINFOLOST' => Translate::noop('{errors:descr_LOGOUTINFOLOST}'),
'LOGOUTREQUEST' => Translate::noop('{errors:descr_LOGOUTREQUEST}'),
'MEMCACHEDOWN' => Translate::noop('{errors:descr_MEMCACHEDOWN}'),
'METADATA' => Translate::noop('{errors:descr_METADATA}'),
'METADATANOTFOUND' => Translate::noop('{errors:descr_METADATANOTFOUND}'),
'NOACCESS' => Translate::noop('{errors:descr_NOACCESS}'),
'NOCERT' => Translate::noop('{errors:descr_NOCERT}'),
'NORELAYSTATE' => Translate::noop('{errors:descr_NORELAYSTATE}'),
'NOSTATE' => Translate::noop('{errors:descr_NOSTATE}'),
'NOTFOUND' => Translate::noop('{errors:descr_NOTFOUND}'),
'NOTFOUNDREASON' => Translate::noop('{errors:descr_NOTFOUNDREASON}'),
'NOTSET' => Translate::noop('{errors:descr_NOTSET}'),
'NOTVALIDCERT' => Translate::noop('{errors:descr_NOTVALIDCERT}'),
'PROCESSASSERTION' => Translate::noop('{errors:descr_PROCESSASSERTION}'),
'PROCESSAUTHNREQUEST' => Translate::noop('{errors:descr_PROCESSAUTHNREQUEST}'),
'RESPONSESTATUSNOSUCCESS' => Translate::noop('{errors:descr_RESPONSESTATUSNOSUCCESS}'),
'SLOSERVICEPARAMS' => Translate::noop('{errors:descr_SLOSERVICEPARAMS}'),
'SSOPARAMS' => Translate::noop('{errors:descr_SSOPARAMS}'),
'UNHANDLEDEXCEPTION' => Translate::noop('{errors:descr_UNHANDLEDEXCEPTION}'),
'UNKNOWNCERT' => Translate::noop('{errors:descr_UNKNOWNCERT}'),
'USERABORTED' => Translate::noop('{errors:descr_USERABORTED}'),
'WRONGUSERPASS' => Translate::noop('{errors:descr_WRONGUSERPASS}'),
];
}
/**
* Fetch all translation strings for error code descriptions.
*
* Extend this to add error codes.
*
* @return array A map from error code to error code description
*/
public static function getAllErrorCodeDescriptions()
{
return self::defaultGetAllErrorCodeDescriptions();
}
/**
* Get a map of both errorcode titles and descriptions
*
* Convenience-method for template-callers
*
* @return array An array containing both errorcode maps.
*/
public static function getAllErrorCodeMessages()
{
return [
'title' => self::getAllErrorCodeTitles(),
'descr' => self::getAllErrorCodeDescriptions(),
];
}
/**
* Fetch a translation string for a title for a given error code.
*
* @param string $errorCode The error code to look up
*
* @return string A string to translate
*/
public static function getErrorCodeTitle($errorCode)
{
$errorCodeTitles = self::getAllErrorCodeTitles();
return $errorCodeTitles[$errorCode];
}
/**
* Fetch a translation string for a description for a given error code.
*
* @param string $errorCode The error code to look up
*
* @return string A string to translate
*/
public static function getErrorCodeDescription($errorCode)
{
$errorCodeDescriptions = self::getAllErrorCodeDescriptions();
return $errorCodeDescriptions[$errorCode];
}
/**
* Get both title and description for a specific error code
*
* Convenience-method for template-callers
*
* @param string $errorCode The error code to look up
*
* @return array An array containing both errorcode strings.
*/
public static function getErrorCodeMessage($errorCode)
{
return [
'title' => self::getErrorCodeTitle($errorCode),
'descr' => self::getErrorCodeDescription($errorCode),
];
}
}

View File

@ -0,0 +1,329 @@
<?php
namespace SAML\Error;
use SAML\Configuration;
use SAML\Logger;
/**
* Base class for SimpleSAMLphp Exceptions
*
* This class tries to make sure that every exception is serializable.
*
* @author Thomas Graff <thomas.graff@uninett.no>
* @package SimpleSAMLphp
*/
class Exception extends \Exception
{
/**
* The backtrace for this exception.
*
* We need to save the backtrace, since we cannot rely on
* serializing the Exception::trace-variable.
*
* @var array
*/
private $backtrace = [];
/**
* The cause of this exception.
*
* @var Exception|null
*/
private $cause = null;
/**
* Constructor for this error.
*
* Note that the cause will be converted to a SimpleSAML\Error\UnserializableException unless it is a subclass of
* SimpleSAML\Error\Exception.
*
* @param string $message Exception message
* @param int $code Error code
* @param \Exception|null $cause The cause of this exception.
*/
public function __construct($message, $code = 0, \Exception $cause = null)
{
assert(is_string($message));
assert(is_int($code));
parent::__construct($message, $code);
$this->initBacktrace($this);
if ($cause !== null) {
$this->cause = Exception::fromException($cause);
}
}
/**
* Convert any exception into a \SimpleSAML\Error\Exception.
*
* @param \Exception $e The exception.
*
* @return Exception The new exception.
*/
public static function fromException(\Exception $e)
{
if ($e instanceof Exception) {
return $e;
}
return new UnserializableException($e);
}
/**
* Load the backtrace from the given exception.
*
* @param \Exception $exception The exception we should fetch the backtrace from.
* @return void
*/
protected function initBacktrace(\Exception $exception)
{
$this->backtrace = [];
// position in the top function on the stack
$pos = $exception->getFile() . ':' . $exception->getLine();
foreach ($exception->getTrace() as $t) {
$function = $t['function'];
if (array_key_exists('class', $t)) {
$function = $t['class'] . '::' . $function;
}
$this->backtrace[] = $pos . ' (' . $function . ')';
if (array_key_exists('file', $t)) {
$pos = $t['file'] . ':' . $t['line'];
} else {
$pos = '[builtin]';
}
}
$this->backtrace[] = $pos . ' (N/A)';
}
/**
* Retrieve the backtrace.
*
* @return array An array where each function call is a single item.
*/
public function getBacktrace()
{
return $this->backtrace;
}
/**
* Retrieve the cause of this exception.
*
* @return Exception|null The cause of this exception.
*/
public function getCause()
{
return $this->cause;
}
/**
* Retrieve the class of this exception.
*
* @return string The name of the class.
*/
public function getClass()
{
return get_class($this);
}
/**
* Format this exception for logging.
*
* Create an array of lines for logging.
*
* @param boolean $anonymize Whether the resulting messages should be anonymized or not.
*
* @return array Log lines that should be written out.
*/
public function format($anonymize = false)
{
$ret = [
$this->getClass() . ': ' . $this->getMessage(),
];
return array_merge($ret, $this->formatBacktrace($anonymize));
}
/**
* Format the backtrace for logging.
*
* Create an array of lines for logging from the backtrace.
*
* @param boolean $anonymize Whether the resulting messages should be anonymized or not.
*
* @return array All lines of the backtrace, properly formatted.
*/
public function formatBacktrace($anonymize = false)
{
$ret = [];
$basedir = Configuration::getInstance()->getBaseDir();
$e = $this;
do {
if ($e !== $this) {
$ret[] = 'Caused by: ' . $e->getClass() . ': ' . $e->getMessage();
}
$ret[] = 'Backtrace:';
$depth = count($e->backtrace);
foreach ($e->backtrace as $i => $trace) {
if ($anonymize) {
$trace = str_replace($basedir, '', $trace);
}
$ret[] = ($depth - $i - 1) . ' ' . $trace;
}
$e = $e->cause;
} while ($e !== null);
return $ret;
}
/**
* Print the backtrace to the log if the 'debug' option is enabled in the configuration.
* @param int $level
* @return void
*/
protected function logBacktrace($level = Logger::DEBUG)
{
// see if debugging is enabled for backtraces
$debug = Configuration::getInstance()->getArrayize('debug', ['backtraces' => false]);
if (
!(in_array('backtraces', $debug, true) // implicitly enabled
|| (array_key_exists('backtraces', $debug)
&& $debug['backtraces'] === true)
// explicitly set
// TODO: deprecate the old style and remove it in 2.0
|| (array_key_exists(0, $debug)
&& $debug[0] === true)) // old style 'debug' configuration option
) {
return;
}
$backtrace = $this->formatBacktrace();
$callback = [Logger::class];
$functions = [
Logger::ERR => 'error',
Logger::WARNING => 'warning',
Logger::INFO => 'info',
Logger::DEBUG => 'debug',
];
$callback[] = $functions[$level];
foreach ($backtrace as $line) {
call_user_func($callback, $line);
}
}
/**
* Print the exception to the log, by default with log level error.
*
* Override to allow errors extending this class to specify the log level themselves.
*
* @param int $default_level The log level to use if this method was not overridden.
* @return void
*/
public function log($default_level)
{
$fn = [
Logger::ERR => 'logError',
Logger::WARNING => 'logWarning',
Logger::INFO => 'logInfo',
Logger::DEBUG => 'logDebug',
];
call_user_func([$this, $fn[$default_level]], $default_level);
}
/**
* Print the exception to the log with log level error.
*
* This function will write this exception to the log, including a full backtrace.
* @return void
*/
public function logError()
{
Logger::error($this->getClass() . ': ' . $this->getMessage());
$this->logBacktrace(Logger::ERR);
}
/**
* Print the exception to the log with log level warning.
*
* This function will write this exception to the log, including a full backtrace.
* @return void
*/
public function logWarning()
{
Logger::warning($this->getClass() . ': ' . $this->getMessage());
$this->logBacktrace(Logger::WARNING);
}
/**
* Print the exception to the log with log level info.
*
* This function will write this exception to the log, including a full backtrace.
* @return void
*/
public function logInfo()
{
Logger::info($this->getClass() . ': ' . $this->getMessage());
$this->logBacktrace(Logger::INFO);
}
/**
* Print the exception to the log with log level debug.
*
* This function will write this exception to the log, including a full backtrace.
* @return void
*/
public function logDebug()
{
Logger::debug($this->getClass() . ': ' . $this->getMessage());
$this->logBacktrace(Logger::DEBUG);
}
/**
* Function for serialization.
*
* This function builds a list of all variables which should be serialized. It will serialize all variables except
* the Exception::trace variable.
*
* @return array Array with the variables that should be serialized.
*/
public function __sleep()
{
$ret = array_keys((array) $this);
foreach ($ret as $i => $e) {
if ($e === "\0Exception\0trace") {
unset($ret[$i]);
}
}
return $ret;
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace SAML\Error;
/**
* Exception indicating wrong password given by user.
*
* @author Thomas Graff <thomas.graff@uninett.no>
* @package SimpleSAMLphp_base
*
*/
class InvalidCredential extends User
{
}

View File

@ -0,0 +1,28 @@
<?php
namespace SAML\Error;
/**
* Error for missing metadata.
*
* @package SimpleSAMLphp
*/
class MetadataNotFound extends Error
{
/**
* Create the error
*
* @param string $entityId The entityID we were unable to locate.
*/
public function __construct($entityId)
{
assert(is_string($entityId));
$this->includeTemplate = 'core:no_metadata.tpl.php';
parent::__construct([
'METADATANOTFOUND',
'%ENTITYID%' => htmlspecialchars(var_export($entityId, true))
]);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace SAML\Error;
/**
* Class NoPassive
*
* @deprecated This class has been deprecated and will be removed in SimpleSAMLphp 2.0. Please use
* \SimpleSAML\Module\saml\Error\NoPassive instead.
*
* @see \SAML\Module\saml\Error\NoPassive
*/
class NoPassive extends Exception
{
}

View File

@ -0,0 +1,22 @@
<?php
namespace SAML\Error;
/**
* Exception which will show a page telling the user
* that we don't know what to do.
*
* @package SimpleSAMLphp
*/
class NoState extends Error
{
/**
* Create the error
*/
public function __construct()
{
$this->includeTemplate = 'core:no_state.tpl.php';
parent::__construct('NOSTATE');
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace SAML\Error;
use SAML\Utils;
/**
* Exception which will show a 404 Not Found error page.
*
* This exception can be thrown from within a module page handler. The user will then be shown a 404 Not Found error
* page.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class NotFound extends Error
{
/**
* Reason why the given page could not be found.
*/
private $reason;
/**
* Create a new NotFound error
*
* @param string $reason Optional description of why the given page could not be found.
*/
public function __construct($reason = null)
{
assert($reason === null || is_string($reason));
$url = Utils\HTTP::getSelfURL();
if ($reason === null) {
parent::__construct(['NOTFOUND', '%URL%' => $url]);
$this->message = "The requested page '$url' could not be found.";
} else {
parent::__construct(['NOTFOUNDREASON', '%URL%' => $url, '%REASON%' => $reason]);
$this->message = "The requested page '$url' could not be found. " . $reason;
}
$this->reason = $reason;
$this->httpCode = 404;
}
/**
* Retrieve the reason why the given page could not be found.
*
* @return string|null The reason why the page could not be found.
*/
public function getReason()
{
return $this->reason;
}
/**
* NotFound exceptions don't need to display a backtrace, as they are very simple and the trace is usually trivial,
* so just log the message without any backtrace at all.
*
* @param bool $anonymize Whether to anonymize the trace or not.
*
* @return array
*/
public function format($anonymize = false)
{
return [
$this->getClass() . ': ' . $this->getMessage(),
];
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace SAML\Error;
/**
* Class ProxyCountExceeded
*
* @deprecated This class has been deprecated and will be removed in SimpleSAMLphp 2.0. Please use
* \SimpleSAML\Module\saml\Error\ProxyCountExceeded instead.
*
* @see \SAML\Module\saml\Error\ProxyCountExceeded
*/
class ProxyCountExceeded extends Exception
{
}

View File

@ -0,0 +1,61 @@
<?php
namespace SAML\Error;
use PDOException;
/**
* Class for saving normal exceptions for serialization.
*
* This class is used by the \SimpleSAML\Auth\State class when it needs
* to serialize an exception which doesn't subclass the
* \SimpleSAML\Error\Exception class.
*
* It creates a new exception which contains the backtrace and message
* of the original exception.
*
* @package SimpleSAMLphp
*/
class UnserializableException extends Exception
{
/**
* The classname of the original exception.
*
* @var string
*/
private $class;
/**
* Create a serializable exception representing an unserializable exception.
*
* @param \Exception $original The original exception.
*/
public function __construct(\Exception $original)
{
$this->class = get_class($original);
$msg = $original->getMessage();
$code = $original->getCode();
if ($original instanceof PDOException) {
// PDOException uses a string as the code. Filter it out here.
$code = -1;
}
parent::__construct($msg, $code);
$this->initBacktrace($original);
}
/**
* Retrieve the class of this exception.
*
* @return string The classname.
*/
public function getClass()
{
return $this->class;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace SAML\Error;
/**
* Baseclass for user error exceptions
*
*
* @author Thomas Graff <thomas.graff@uninett.no>
* @package SimpleSAMLphp_base
*
*/
class User extends Exception
{
}

View File

@ -0,0 +1,22 @@
<?php
namespace SAML\Error;
/**
* Exception indicating user aborting the authentication process.
*
* @package SimpleSAMLphp
*/
class UserAborted extends Error
{
/**
* Create the error
*
* @param \Exception|null $cause The exception that caused this error.
*/
public function __construct(\Exception $cause = null)
{
parent::__construct('USERABORTED', $cause);
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace SAML\Error;
/**
* Exception indicating user not found by authsource.
*
* @author Thomas Graff <thomas.graff@uninett.no>
* @package SimpleSAMLphp_base
*
*/
class UserNotFound extends User
{
}

138
libsrc/SAML/HTTP/Router.php Normal file
View File

@ -0,0 +1,138 @@
<?php
namespace SAML\HTTP;
use Exception;
use SAML\Configuration;
use SAML\Module\ControllerResolver;
use SAML\Session;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\Routing\RequestContext;
/**
* Class that routes requests to responses.
*
* @package SimpleSAML
*/
class Router
{
/** @var ArgumentResolver */
protected $arguments;
/** @var Configuration|null */
protected $config = null;
/** @var RequestContext */
protected $context;
/** @var EventDispatcher */
protected $dispatcher;
/** @var Request|null */
protected $request = null;
/** @var ControllerResolver */
protected $resolver;
/** @var Session|null */
protected $session = null;
/** @var RequestStack|null */
protected $stack = null;
/**
* Router constructor.
*
* @param string $module
*/
public function __construct($module)
{
$this->arguments = new ArgumentResolver();
$this->context = new RequestContext();
$this->resolver = new ControllerResolver($module);
$this->dispatcher = new EventDispatcher();
}
/**
* Process a given request.
*
* If no specific arguments are given, the default instances will be used (configuration, session, etc).
*
* @param Request|null $request
* The request to process. Defaults to the current one.
*
* @return Response A response suitable for the given request.
*
* @throws Exception If an error occurs.
*/
public function process(Request $request = null)
{
if ($this->config === null) {
$this->setConfiguration(Configuration::getInstance());
}
if ($this->session === null) {
$this->setSession(Session::getSessionFromRequest());
}
if ($request === null) {
$this->request = Request::createFromGlobals();
} else {
$this->request = $request;
}
$stack = new RequestStack();
$stack->push($this->request);
$this->context->fromRequest($this->request);
$kernel = new HttpKernel($this->dispatcher, $this->resolver, $stack, $this->resolver);
return $kernel->handle($this->request);
}
/**
* Send a given response to the browser.
*
* @param Response $response The response to send.
* @return void
*/
public function send(Response $response)
{
if ($this->request === null) {
throw new Exception("No request found to respond to");
}
$response->prepare($this->request);
$response->send();
}
/**
* Set the configuration to use by the controller.
*
* @param Configuration $config
* @return void
*/
public function setConfiguration(Configuration $config)
{
$this->config = $config;
$this->resolver->setConfiguration($config);
}
/**
* Set the session to use by the controller.
*
* @param Session $session
* @return void
*/
public function setSession(Session $session)
{
$this->session = $session;
$this->resolver->setSession($session);
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace SAML\HTTP;
use Symfony\Component\HttpFoundation\Response;
/**
* Class modelling a response that consists on running some function.
*
* This is a helper class that allows us to have the new and the old architecture coexist. This way, classes and files
* that aren't PSR-7-aware can still be plugged into a PSR-7-compatible environment.
*
* @package SimpleSAML
*/
class RunnableResponse extends Response
{
/** @var array */
protected $arguments;
/** @var callable */
protected $callable;
/**
* RunnableResponse constructor.
*
* @param callable $callable A callable that we should run as part of this response.
* @param array $args An array of arguments to be passed to the callable. Note that each element of the array
*/
public function __construct(callable $callable, $args = [])
{
$this->arguments = $args;
$this->callable = $callable;
$this->charset = 'UTF-8';
parent::__construct();
}
/**
* Get the callable for this response.
*
* @return callable
*/
public function getCallable()
{
return $this->callable;
}
/**
* Get the arguments to the callable.
*
* @return array
*/
public function getArguments()
{
return $this->arguments;
}
/**
* "Send" this response by actually running the callable.
*
* @return self
*/
public function sendContent()
{
return call_user_func_array($this->callable, $this->arguments);
}
}

579
libsrc/SAML/IdP.php Normal file
View File

@ -0,0 +1,579 @@
<?php
namespace SAML;
use SAML\Error\Exception;
use SAML2\Constants as SAML2;
use SAML\Auth;
use SAML\Error;
use SAML\Metadata\MetaDataStorageHandler;
use SAML\Module\saml\Error\NoPassive;
use SAML\Utils;
/**
* IdP class.
*
* This class implements the various functions used by IdP.
*
* @package SimpleSAMLphp
*/
class IdP
{
/**
* A cache for resolving IdP id's.
*
* @var array
*/
private static $idpCache = [];
/**
* The identifier for this IdP.
*
* @var string
*/
private $id;
/**
* The "association group" for this IdP.
*
* We use this to support cross-protocol logout until
* we implement a cross-protocol IdP.
*
* @var string
*/
private $associationGroup;
/**
* The configuration for this IdP.
*
* @var Configuration
*/
private $config;
/**
* Our authsource.
*
* @var Auth\Simple
*/
private $authSource;
/**
* Initialize an IdP.
*
* @param string $id The identifier of this IdP.
*
* @throws Exception If the IdP is disabled or no such auth source was found.
*/
private function __construct($id)
{
assert(is_string($id));
$this->id = $id;
$this->associationGroup = $id;
$metadata = MetaDataStorageHandler::getMetadataHandler();
$globalConfig = Configuration::getInstance();
if (substr($id, 0, 6) === 'saml2:') {
if (!$globalConfig->getBoolean('enable.saml20-idp', false)) {
throw new Exception('enable.saml20-idp disabled in config.php.');
}
$this->config = $metadata->getMetaDataConfig(substr($id, 6), 'saml20-idp-hosted');
} elseif (substr($id, 0, 6) === 'saml1:') {
if (!$globalConfig->getBoolean('enable.shib13-idp', false)) {
throw new Exception('enable.shib13-idp disabled in config.php.');
}
$this->config = $metadata->getMetaDataConfig(substr($id, 6), 'shib13-idp-hosted');
} elseif (substr($id, 0, 5) === 'adfs:') {
if (!$globalConfig->getBoolean('enable.adfs-idp', false)) {
throw new Exception('enable.adfs-idp disabled in config.php.');
}
$this->config = $metadata->getMetaDataConfig(substr($id, 5), 'adfs-idp-hosted');
try {
// this makes the ADFS IdP use the same SP associations as the SAML 2.0 IdP
$saml2EntityId = $metadata->getMetaDataCurrentEntityID('saml20-idp-hosted');
$this->associationGroup = 'saml2:' . $saml2EntityId;
} catch (\Exception $e) {
// probably no SAML 2 IdP configured for this host. Ignore the error
}
} else {
throw new \Exception("Protocol not implemented.");
}
$auth = $this->config->getString('auth');
if (Auth\Source::getById($auth) !== null) {
$this->authSource = new Auth\Simple($auth);
} else {
throw new Exception('No such "' . $auth . '" auth source found.');
}
}
/**
* Retrieve the ID of this IdP.
*
* @return string The ID of this IdP.
*/
public function getId()
{
return $this->id;
}
/**
* Retrieve an IdP by ID.
*
* @param string $id The identifier of the IdP.
*
* @return IdP The IdP.
*/
public static function getById($id)
{
assert(is_string($id));
if (isset(self::$idpCache[$id])) {
return self::$idpCache[$id];
}
$idp = new self($id);
self::$idpCache[$id] = $idp;
return $idp;
}
/**
* Retrieve the IdP "owning" the state.
*
* @param array &$state The state array.
*
* @return IdP The IdP.
*/
public static function getByState(array &$state)
{
assert(isset($state['core:IdP']));
return self::getById($state['core:IdP']);
}
/**
* Retrieve the configuration for this IdP.
*
* @return Configuration The configuration object.
*/
public function getConfig()
{
return $this->config;
}
/**
* Get SP name.
*
* @param string $assocId The association identifier.
*
* @return array|null The name of the SP, as an associative array of language => text, or null if this isn't an SP.
*/
public function getSPName($assocId)
{
assert(is_string($assocId));
$prefix = substr($assocId, 0, 4);
$spEntityId = substr($assocId, strlen($prefix) + 1);
$metadata = MetaDataStorageHandler::getMetadataHandler();
if ($prefix === 'saml') {
try {
$spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote');
} catch (\Exception $e) {
try {
$spMetadata = $metadata->getMetaDataConfig($spEntityId, 'shib13-sp-remote');
} catch (\Exception $e) {
return null;
}
}
} else {
if ($prefix === 'adfs') {
$spMetadata = $metadata->getMetaDataConfig($spEntityId, 'adfs-sp-remote');
} else {
return null;
}
}
if ($spMetadata->hasValue('name')) {
return $spMetadata->getLocalizedString('name');
} elseif ($spMetadata->hasValue('OrganizationDisplayName')) {
return $spMetadata->getLocalizedString('OrganizationDisplayName');
} else {
return ['en' => $spEntityId];
}
}
/**
* Add an SP association.
*
* @param array $association The SP association.
* @return void
*/
public function addAssociation(array $association)
{
assert(isset($association['id']));
assert(isset($association['Handler']));
$association['core:IdP'] = $this->id;
$session = Session::getSessionFromRequest();
$session->addAssociation($this->associationGroup, $association);
}
/**
* Retrieve list of SP associations.
*
* @return array List of SP associations.
*/
public function getAssociations()
{
$session = Session::getSessionFromRequest();
return $session->getAssociations($this->associationGroup);
}
/**
* Remove an SP association.
*
* @param string $assocId The association id.
* @return void
*/
public function terminateAssociation($assocId)
{
assert(is_string($assocId));
$session = Session::getSessionFromRequest();
$session->terminateAssociation($this->associationGroup, $assocId);
}
/**
* Is the current user authenticated?
*
* @return boolean True if the user is authenticated, false otherwise.
*/
public function isAuthenticated()
{
return $this->authSource->isAuthenticated();
}
/**
* Called after authproc has run.
*
* @param array $state The authentication request state array.
* @return void
*/
public static function postAuthProc(array $state)
{
assert(is_callable($state['Responder']));
if (isset($state['core:SP'])) {
$session = Session::getSessionFromRequest();
$session->setData(
'core:idp-ssotime',
$state['core:IdP'] . ';' . $state['core:SP'],
time(),
Session::DATA_TIMEOUT_SESSION_END
);
}
call_user_func($state['Responder'], $state);
assert(false);
}
/**
* The user is authenticated.
*
* @param array $state The authentication request state array.
*
* @return void
*@throws Exception If we are not authenticated.
*/
public static function postAuth(array $state)
{
$idp = IdP::getByState($state);
if (!$idp->isAuthenticated()) {
throw new Exception('Not authenticated.');
}
$state['Attributes'] = $idp->authSource->getAttributes();
if (isset($state['SPMetadata'])) {
$spMetadata = $state['SPMetadata'];
} else {
$spMetadata = [];
}
if (isset($state['core:SP'])) {
$session = Session::getSessionFromRequest();
$previousSSOTime = $session->getData('core:idp-ssotime', $state['core:IdP'] . ';' . $state['core:SP']);
if ($previousSSOTime !== null) {
$state['PreviousSSOTimestamp'] = $previousSSOTime;
}
}
$idpMetadata = $idp->getConfig()->toArray();
$pc = new Auth\ProcessingChain($idpMetadata, $spMetadata, 'idp');
$state['ReturnCall'] = ['\SimpleSAML\IdP', 'postAuthProc'];
$state['Destination'] = $spMetadata;
$state['Source'] = $idpMetadata;
$pc->processState($state);
self::postAuthProc($state);
}
/**
* Authenticate the user.
*
* This function authenticates the user.
*
* @param array &$state The authentication request state.
*
* @return void
*@throws NoPassive If we were asked to do passive authentication.
*/
private function authenticate(array &$state)
{
if (isset($state['isPassive']) && (bool) $state['isPassive']) {
throw new NoPassive(SAML2::STATUS_RESPONDER, 'Passive authentication not supported.');
}
$this->authSource->login($state);
}
/**
* Re-authenticate the user.
*
* This function re-authenticates an user with an existing session. This gives the authentication source a chance
* to do additional work when re-authenticating for SSO.
*
* Note: This function is not used when ForceAuthn=true.
*
* @param array &$state The authentication request state.
*
* @throws \Exception If there is no auth source defined for this IdP.
* @return void
*/
private function reauthenticate(array &$state)
{
$sourceImpl = $this->authSource->getAuthSource();
$sourceImpl->reauthenticate($state);
}
/**
* Process authentication requests.
*
* @param array &$state The authentication request state.
* @return void
*/
public function handleAuthenticationRequest(array &$state)
{
assert(isset($state['Responder']));
$state['core:IdP'] = $this->id;
if (isset($state['SPMetadata']['entityid'])) {
$spEntityId = $state['SPMetadata']['entityid'];
} elseif (isset($state['SPMetadata']['entityID'])) {
$spEntityId = $state['SPMetadata']['entityID'];
} else {
$spEntityId = null;
}
$state['core:SP'] = $spEntityId;
// first, check whether we need to authenticate the user
if (isset($state['ForceAuthn']) && (bool) $state['ForceAuthn']) {
// force authentication is in effect
$needAuth = true;
} else {
$needAuth = !$this->isAuthenticated();
}
$state['IdPMetadata'] = $this->getConfig()->toArray();
$state['ReturnCallback'] = ['\SimpleSAML\IdP', 'postAuth'];
try {
if ($needAuth) {
$this->authenticate($state);
assert(false);
} else {
$this->reauthenticate($state);
}
$this->postAuth($state);
} catch (Exception $e) {
Auth\State::throwException($state, $e);
} catch (\Exception $e) {
$e = new Error\UnserializableException($e);
Auth\State::throwException($state, $e);
}
}
/**
* Find the logout handler of this IdP.
*
* @return IdP\LogoutHandlerInterface The logout handler class.
* @throws \Exception If we cannot find a logout handler.
*/
public function getLogoutHandler()
{
// find the logout handler
$logouttype = $this->getConfig()->getString('logouttype', 'traditional');
switch ($logouttype) {
case 'traditional':
$handler = '\SimpleSAML\IdP\TraditionalLogoutHandler';
break;
case 'iframe':
$handler = '\SimpleSAML\IdP\IFrameLogoutHandler';
break;
default:
throw new Exception('Unknown logout handler: ' . var_export($logouttype, true));
}
/** @var IdP\LogoutHandlerInterface */
return new $handler($this);
}
/**
* Finish the logout operation.
*
* This function will never return.
*
* @param array &$state The logout request state.
* @return void
*/
public function finishLogout(array &$state)
{
assert(isset($state['Responder']));
$idp = IdP::getByState($state);
call_user_func($state['Responder'], $idp, $state);
assert(false);
}
/**
* Process a logout request.
*
* This function will never return.
*
* @param array &$state The logout request state.
* @param string|null $assocId The association we received the logout request from, or null if there was no
* association.
* @return void
*/
public function handleLogoutRequest(array &$state, $assocId)
{
assert(isset($state['Responder']));
assert(is_string($assocId) || $assocId === null);
$state['core:IdP'] = $this->id;
$state['core:TerminatedAssocId'] = $assocId;
if ($assocId !== null) {
$this->terminateAssociation($assocId);
$session = Session::getSessionFromRequest();
$session->deleteData('core:idp-ssotime', $this->id . ';' . $state['saml:SPEntityId']);
}
// terminate the local session
$id = Auth\State::saveState($state, 'core:Logout:afterbridge');
$returnTo = Module::getModuleURL('core/idp/resumelogout.php', ['id' => $id]);
$this->authSource->logout($returnTo);
if ($assocId !== null) {
$handler = $this->getLogoutHandler();
$handler->startLogout($state, $assocId);
}
assert(false);
}
/**
* Process a logout response.
*
* This function will never return.
*
* @param string $assocId The association that is terminated.
* @param string|null $relayState The RelayState from the start of the logout.
* @param Exception|null $error The error that occurred during session termination (if any).
* @return void
*/
public function handleLogoutResponse($assocId, $relayState, Exception $error = null)
{
assert(is_string($assocId));
assert(is_string($relayState) || $relayState === null);
$index = strpos($assocId, ':');
assert(is_int($index));
$session = Session::getSessionFromRequest();
$session->deleteData('core:idp-ssotime', $this->id . ';' . substr($assocId, $index + 1));
$handler = $this->getLogoutHandler();
$handler->onResponse($assocId, $relayState, $error);
assert(false);
}
/**
* Log out, then redirect to a URL.
*
* This function never returns.
*
* @param string $url The URL the user should be returned to after logout.
* @return void
*/
public function doLogoutRedirect($url)
{
assert(is_string($url));
$state = [
'Responder' => ['\SimpleSAML\IdP', 'finishLogoutRedirect'],
'core:Logout:URL' => $url,
];
$this->handleLogoutRequest($state, null);
assert(false);
}
/**
* Redirect to a URL after logout.
*
* This function never returns.
*
* @param IdP $idp Deprecated. Will be removed.
* @param array &$state The logout state from doLogoutRedirect().
* @return void
*/
public static function finishLogoutRedirect(IdP $idp, array $state)
{
assert(isset($state['core:Logout:URL']));
Utils\HTTP::redirectTrustedURL($state['core:Logout:URL']);
assert(false);
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace SAML\IdP;
use SAML\Auth;
use SAML\Configuration;
use SAML\Error;
use SAML\Error\Exception;
use SAML\IdP;
use SAML\Module;
use SAML\Utils;
use SAML\XHTML\Template;
/**
* Class that handles iframe logout.
*
* @package SimpleSAMLphp
*/
class IFrameLogoutHandler implements LogoutHandlerInterface
{
/**
* The IdP we are logging out from.
*
* @var IdP
*/
private $idp;
/**
* LogoutIFrame constructor.
*
* @param IdP $idp The IdP to log out from.
*/
public function __construct(IdP $idp)
{
$this->idp = $idp;
}
/**
* Start the logout operation.
*
* @param array &$state The logout state.
* @param string|null $assocId The SP we are logging out from.
* @return void
*/
public function startLogout(array &$state, $assocId)
{
assert(is_string($assocId) || $assocId === null);
$associations = $this->idp->getAssociations();
if (count($associations) === 0) {
$this->idp->finishLogout($state);
}
foreach ($associations as $id => &$association) {
$idp = IdP::getByState($association);
$association['core:Logout-IFrame:Name'] = $idp->getSPName($id);
$association['core:Logout-IFrame:State'] = 'onhold';
}
$state['core:Logout-IFrame:Associations'] = $associations;
if (!is_null($assocId)) {
$spName = $this->idp->getSPName($assocId);
if ($spName === null) {
$spName = ['en' => $assocId];
}
$state['core:Logout-IFrame:From'] = $spName;
} else {
$state['core:Logout-IFrame:From'] = null;
}
$params = [
'id' => Auth\State::saveState($state, 'core:Logout-IFrame'),
];
if (isset($state['core:Logout-IFrame:InitType'])) {
$params['type'] = $state['core:Logout-IFrame:InitType'];
}
$url = Module::getModuleURL('core/idp/logout-iframe.php', $params);
Utils\HTTP::redirectTrustedURL($url);
}
/**
* Continue the logout operation.
*
* This function will never return.
*
* @param string $assocId The association that is terminated.
* @param string|null $relayState The RelayState from the start of the logout.
* @param Exception|null $error The error that occurred during session termination (if any).
* @return void
*/
public function onResponse($assocId, $relayState, Exception $error = null)
{
assert(is_string($assocId));
$this->idp->terminateAssociation($assocId);
$config = Configuration::getInstance();
$t = new Template($config, 'IFrameLogoutHandler.tpl.php');
$t->data['assocId'] = var_export($assocId, true);
$t->data['spId'] = sha1($assocId);
if (!is_null($error)) {
$t->data['errorMsg'] = $error->getMessage();
}
$t->show();
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace SAML\IdP;
use SAML\Error;
use SAML\Error\Exception;
use SAML\IdP;
/**
* Interface that all logout handlers must implement.
*
* @package SimpleSAMLphp
*/
interface LogoutHandlerInterface
{
/**
* Initialize this logout handler.
*
* @param IdP $idp The IdP we are logging out from.
*/
public function __construct(IdP $idp);
/**
* Start a logout operation.
*
* This function must never return.
*
* @param array &$state The logout state.
* @param string $assocId The association that started the logout.
* @return void
*/
public function startLogout(array &$state, $assocId);
/**
* Handles responses to our logout requests.
*
* This function will never return.
*
* @param string $assocId The association that is terminated.
* @param string|null $relayState The RelayState from the start of the logout.
* @param Exception|null $error The error that occurred during session termination (if any).
* @return void
*/
public function onResponse($assocId, $relayState, Exception $error = null);
}

View File

@ -0,0 +1,126 @@
<?php
namespace SAML\IdP;
use SAML\Auth;
use SAML\Error;
use SAML\Error\Exception;
use SAML\IdP;
use SAML\Logger;
use SAML\Utils;
/**
* Class that handles traditional logout.
*
* @package SimpleSAMLphp
*/
class TraditionalLogoutHandler implements LogoutHandlerInterface
{
/**
* The IdP we are logging out from.
*
* @var IdP
*/
private $idp;
/**
* TraditionalLogout constructor.
*
* @param IdP $idp The IdP to log out from.
*/
public function __construct(IdP $idp)
{
$this->idp = $idp;
}
/**
* Picks the next SP and issues a logout request.
*
* This function never returns.
*
* @param array &$state The logout state.
* @return void
*/
private function logoutNextSP(array &$state)
{
$association = array_pop($state['core:LogoutTraditional:Remaining']);
if ($association === null) {
$this->idp->finishLogout($state);
}
$relayState = Auth\State::saveState($state, 'core:LogoutTraditional', true);
$id = $association['id'];
Logger::info('Logging out of ' . var_export($id, true) . '.');
try {
$idp = IdP::getByState($association);
$url = call_user_func([$association['Handler'], 'getLogoutURL'], $idp, $association, $relayState);
Utils\HTTP::redirectTrustedURL($url);
} catch (\Exception $e) {
Logger::warning('Unable to initialize logout to ' . var_export($id, true) . '.');
$this->idp->terminateAssociation($id);
$state['core:Failed'] = true;
// Try the next SP
$this->logoutNextSP($state);
assert(false);
}
}
/**
* Start the logout operation.
*
* This function never returns.
*
* @param array &$state The logout state.
* @param string $assocId The association that started the logout.
* @return void
*/
public function startLogout(array &$state, $assocId)
{
$state['core:LogoutTraditional:Remaining'] = $this->idp->getAssociations();
$this->logoutNextSP($state);
}
/**
* Continue the logout operation.
*
* This function will never return.
*
* @param string $assocId The association that is terminated.
* @param string|null $relayState The RelayState from the start of the logout.
* @param Exception|null $error The error that occurred during session termination (if any).
* @return void
*
* @throws Exception If the RelayState was lost during logout.
*/
public function onResponse($assocId, $relayState, Exception $error = null)
{
assert(is_string($assocId));
assert(is_string($relayState) || $relayState === null);
if ($relayState === null) {
throw new Exception('RelayState lost during logout.');
}
$state = Auth\State::loadState($relayState, 'core:LogoutTraditional');
if ($error === null) {
Logger::info('Logged out of ' . var_export($assocId, true) . '.');
$this->idp->terminateAssociation($assocId);
} else {
Logger::warning('Error received from ' . var_export($assocId, true) . ' during logout:');
$error->logWarning();
$state['core:Failed'] = true;
}
$this->logoutNextSP($state);
}
}

View File

@ -0,0 +1,432 @@
<?php
/**
* Choosing the language to localize to for our minimalistic XHTML PHP based template system.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @author Hanne Moa, UNINETT AS. <hanne.moa@uninett.no>
* @package SimpleSAMLphp
*/
namespace SAML\Locale;
use SAML\Configuration;
use SAML\Logger;
use SAML\Utils;
class Language
{
/**
* This is the default language map. It is used to map languages codes from the user agent to other language codes.
*/
private static $defaultLanguageMap = ['nb' => 'no'];
/**
* The configuration to use.
*
* @var Configuration
*/
private $configuration;
/**
* An array holding a list of languages available.
*
* @var array
*/
private $availableLanguages;
/**
* The language currently in use.
*
* @var null|string
*/
private $language = null;
/**
* The language to use by default.
*
* @var string
*/
private $defaultLanguage;
/**
* An array holding a list of languages that are written from right to left.
*
* @var array
*/
private $rtlLanguages;
/**
* HTTP GET language parameter name.
*
* @var string
*/
private $languageParameterName;
/**
* A custom function to use in order to determine the language in use.
*
* @var callable|null
*/
private $customFunction;
/**
* A list of languages supported with their names localized.
* Indexed by something that mostly resembles ISO 639-1 code,
* with some charming SimpleSAML-specific variants...
* that must remain before 2.0 due to backwards compatibility
*
* @var array
*/
public static $language_names = [
'no' => 'Bokmål', // Norwegian Bokmål
'nn' => 'Nynorsk', // Norwegian Nynorsk
'se' => 'Sámegiella', // Northern Sami
'sma' => 'Åarjelh-saemien giele', // Southern Sami
'da' => 'Dansk', // Danish
'en' => 'English',
'de' => 'Deutsch', // German
'sv' => 'Svenska', // Swedish
'fi' => 'Suomeksi', // Finnish
'es' => 'Español', // Spanish
'ca' => 'Català', // Catalan
'fr' => 'Français', // French
'it' => 'Italiano', // Italian
'nl' => 'Nederlands', // Dutch
'lb' => 'Lëtzebuergesch', // Luxembourgish
'cs' => 'Čeština', // Czech
'sl' => 'Slovenščina', // Slovensk
'lt' => 'Lietuvių kalba', // Lithuanian
'hr' => 'Hrvatski', // Croatian
'hu' => 'Magyar', // Hungarian
'pl' => 'Język polski', // Polish
'pt' => 'Português', // Portuguese
'pt-br' => 'Português brasileiro', // Portuguese
'ru' => 'русский язык', // Russian
'et' => 'eesti keel', // Estonian
'tr' => 'Türkçe', // Turkish
'el' => 'ελληνικά', // Greek
'ja' => '日本語', // Japanese
'zh' => '简体中文', // Chinese (simplified)
'zh-tw' => '繁體中文', // Chinese (traditional)
'ar' => 'العربية', // Arabic
'fa' => 'پارسی', // Persian
'ur' => 'اردو', // Urdu
'he' => 'עִבְרִית', // Hebrew
'id' => 'Bahasa Indonesia', // Indonesian
'sr' => 'Srpski', // Serbian
'lv' => 'Latviešu', // Latvian
'ro' => 'Românește', // Romanian
'eu' => 'Euskara', // Basque
'af' => 'Afrikaans', // Afrikaans
'zu' => 'IsiZulu', // Zulu
'xh' => 'isiXhosa', // Xhosa
];
/**
* A mapping of SSP languages to locales
*
* @var array
*/
private $languagePosixMapping = [
'no' => 'nb_NO',
'nn' => 'nn_NO',
];
/**
* Constructor
*
* @param Configuration $configuration Configuration object
*/
public function __construct(Configuration $configuration)
{
$this->configuration = $configuration;
$this->availableLanguages = $this->getInstalledLanguages();
$this->defaultLanguage = $this->configuration->getString('language.default', 'en');
$this->languageParameterName = $this->configuration->getString('language.parameter.name', 'language');
$this->customFunction = $this->configuration->getArray('language.get_language_function', null);
$this->rtlLanguages = $this->configuration->getArray('language.rtl', []);
if (isset($_GET[$this->languageParameterName])) {
$this->setLanguage(
$_GET[$this->languageParameterName],
$this->configuration->getBoolean('language.parameter.setcookie', true)
);
}
}
/**
* Filter configured (available) languages against installed languages.
*
* @return array The set of languages both in 'language.available' and self::$language_names.
*/
private function getInstalledLanguages()
{
$configuredAvailableLanguages = $this->configuration->getArray('language.available', ['en']);
$availableLanguages = [];
foreach ($configuredAvailableLanguages as $code) {
if (array_key_exists($code, self::$language_names) && isset(self::$language_names[$code])) {
$availableLanguages[] = $code;
} else {
Logger::error("Language \"$code\" not installed. Check config.");
}
}
return $availableLanguages;
}
/**
* Rename to non-idiosyncratic language code.
*
* @param string $language Language code for the language to rename, if necessary.
*
* @return string The language code.
*/
public function getPosixLanguage($language)
{
if (isset($this->languagePosixMapping[$language])) {
return $this->languagePosixMapping[$language];
}
return $language;
}
/**
* This method will set a cookie for the user's browser to remember what language was selected.
*
* @param string $language Language code for the language to set.
* @param boolean $setLanguageCookie Whether to set the language cookie or not. Defaults to true.
* @return void
*/
public function setLanguage($language, $setLanguageCookie = true)
{
$language = strtolower($language);
if (in_array($language, $this->availableLanguages, true)) {
$this->language = $language;
if ($setLanguageCookie === true) {
self::setLanguageCookie($language);
}
}
}
/**
* This method will return the language selected by the user, or the default language. It looks first for a cached
* language code, then checks for a language cookie, then it tries to calculate the preferred language from HTTP
* headers.
*
* @return string The language selected by the user according to the processing rules specified, or the default
* language in any other case.
*/
public function getLanguage()
{
// language is set in object
if (isset($this->language)) {
return $this->language;
}
// run custom getLanguage function if defined
if (isset($this->customFunction) && is_callable($this->customFunction)) {
$customLanguage = call_user_func($this->customFunction, $this);
if ($customLanguage !== null && $customLanguage !== false) {
return $customLanguage;
}
}
// language is provided in a stored cookie
$languageCookie = self::getLanguageCookie();
if ($languageCookie !== null) {
$this->language = $languageCookie;
return $languageCookie;
}
// check if we can find a good language from the Accept-Language HTTP header
$httpLanguage = $this->getHTTPLanguage();
if ($httpLanguage !== null) {
return $httpLanguage;
}
// language is not set, and we get the default language from the configuration
return $this->getDefaultLanguage();
}
/**
* Get the localized name of a language, by ISO 639-2 code.
*
* @param string $code The ISO 639-2 code of the language.
*
* @return string|null The localized name of the language.
*/
public function getLanguageLocalizedName($code)
{
if (array_key_exists($code, self::$language_names) && isset(self::$language_names[$code])) {
return self::$language_names[$code];
}
Logger::error("Name for language \"$code\" not found. Check config.");
return null;
}
/**
* Get the language parameter name.
*
* @return string The language parameter name.
*/
public function getLanguageParameterName()
{
return $this->languageParameterName;
}
/**
* This method returns the preferred language for the user based on the Accept-Language HTTP header.
*
* @return string|null The preferred language based on the Accept-Language HTTP header,
* or null if none of the languages in the header is available.
*/
private function getHTTPLanguage()
{
$languageScore = Utils\HTTP::getAcceptLanguage();
// for now we only use the default language map. We may use a configurable language map in the future
$languageMap = self::$defaultLanguageMap;
// find the available language with the best score
$bestLanguage = null;
$bestScore = -1.0;
foreach ($languageScore as $language => $score) {
// apply the language map to the language code
if (array_key_exists($language, $languageMap)) {
$language = $languageMap[$language];
}
if (!in_array($language, $this->availableLanguages, true)) {
// skip this language - we don't have it
continue;
}
/* Some user agents use very limited precision of the quality value, but order the elements in descending
* order. Therefore we rely on the order of the output from getAcceptLanguage() matching the order of the
* languages in the header when two languages have the same quality.
*/
if ($score > $bestScore) {
$bestLanguage = $language;
$bestScore = $score;
}
}
return $bestLanguage;
}
/**
* Return the default language according to configuration.
*
* @return string The default language that has been configured. Defaults to english if not configured.
*/
public function getDefaultLanguage()
{
return $this->defaultLanguage;
}
/**
* Return an alias for a language code, if any.
*
* @param string $langcode
* @return string|null The alias, or null if the alias was not found.
*/
public function getLanguageCodeAlias($langcode)
{
if (isset(self::$defaultLanguageMap[$langcode])) {
return self::$defaultLanguageMap[$langcode];
}
// No alias found, which is fine
return null;
}
/**
* Return an indexed list of all languages available.
*
* @return array An array holding all the languages available as the keys of the array. The value for each key is
* true in case that the language specified by that key is currently active, or false otherwise.
*/
public function getLanguageList()
{
$current = $this->getLanguage();
$list = array_fill_keys($this->availableLanguages, false);
$list[$current] = true;
return $list;
}
/**
* Check whether a language is written from the right to the left or not.
*
* @return boolean True if the language is right-to-left, false otherwise.
*/
public function isLanguageRTL()
{
return in_array($this->getLanguage(), $this->rtlLanguages, true);
}
/**
* Retrieve the user-selected language from a cookie.
*
* @return string|null The selected language or null if unset.
*/
public static function getLanguageCookie()
{
$config = Configuration::getInstance();
$availableLanguages = $config->getArray('language.available', ['en']);
$name = $config->getString('language.cookie.name', 'language');
if (isset($_COOKIE[$name])) {
$language = strtolower((string) $_COOKIE[$name]);
if (in_array($language, $availableLanguages, true)) {
return $language;
}
}
return null;
}
/**
* This method will attempt to set the user-selected language in a cookie. It will do nothing if the language
* specified is not in the list of available languages, or the headers have already been sent to the browser.
*
* @param string $language The language set by the user.
* @return void
*/
public static function setLanguageCookie($language)
{
assert(is_string($language));
$language = strtolower($language);
$config = Configuration::getInstance();
$availableLanguages = $config->getArray('language.available', ['en']);
if (!in_array($language, $availableLanguages, true) || headers_sent()) {
return;
}
$name = $config->getString('language.cookie.name', 'language');
$params = [
'lifetime' => ($config->getInteger('language.cookie.lifetime', 60 * 60 * 24 * 900)),
'domain' => ($config->getString('language.cookie.domain', null)),
'path' => ($config->getString('language.cookie.path', '/')),
'secure' => ($config->getBoolean('language.cookie.secure', false)),
'httponly' => ($config->getBoolean('language.cookie.httponly', false)),
'samesite' => ($config->getString('language.cookie.samesite', null)),
];
Utils\HTTP::setCookie($name, $language, $params, false);
}
}

View File

@ -0,0 +1,309 @@
<?php
/**
* Glue to connect one or more translation/locale systems to the rest
*
* @author Hanne Moa, UNINETT AS. <hanne.moa@uninett.no>
* @package SimpleSAMLphp
*/
namespace SAML\Locale;
use Exception;
use Gettext\Translations;
use Gettext\Translator;
use SAML\Configuration;
use SAML\Logger;
class Localization
{
/**
* The configuration to use.
*
* @var Configuration
*/
private $configuration;
/**
* The default gettext domain.
*
* @var string
*/
const DEFAULT_DOMAIN = 'messages';
/**
* Old internationalization backend included in SimpleSAMLphp.
*
* @var string
*/
const SSP_I18N_BACKEND = 'SimpleSAMLphp';
/**
* An internationalization backend implemented purely in PHP.
*
* @var string
*/
const GETTEXT_I18N_BACKEND = 'gettext/gettext';
/**
* The default locale directory
*
* @var string
*/
private $localeDir;
/**
* Where specific domains are stored
*
* @var array
*/
private $localeDomainMap = [];
/**
* Pointer to currently active translator
*
* @var Translator
*/
private $translator;
/**
* Pointer to current Language
*
* @var Language
*/
private $language;
/**
* Language code representing the current Language
*
* @var string
*/
private $langcode;
/**
* The language backend to use
*
* @var string
*/
public $i18nBackend;
/**
* Constructor
*
* @param Configuration $configuration Configuration object
*/
public function __construct(Configuration $configuration)
{
$this->configuration = $configuration;
/** @var string $locales */
$locales = $this->configuration->resolvePath('locales');
$this->localeDir = $locales;
$this->language = new Language($configuration);
$this->langcode = $this->language->getPosixLanguage($this->language->getLanguage());
$this->i18nBackend = (
$this->configuration->getBoolean('usenewui', false)
? self::GETTEXT_I18N_BACKEND
: self::SSP_I18N_BACKEND
);
$this->setupL10N();
}
/**
* Dump the default locale directory
*
* @return string
*/
public function getLocaleDir()
{
return $this->localeDir;
}
/**
* Get the default locale dir for a specific module aka. domain
*
* @param string $domain Name of module/domain
*
* @return string
*/
public function getDomainLocaleDir($domain)
{
/** @var string $base */
$base = $this->configuration->resolvePath('modules');
$localeDir = $base . '/' . $domain . '/locales';
return $localeDir;
}
/**
* Add a new translation domain from a module
* (We're assuming that each domain only exists in one place)
*
* @param string $module Module name
* @param string $localeDir Absolute path if the module is housed elsewhere
* @return void
*/
public function addModuleDomain($module, $localeDir = null)
{
if (!$localeDir) {
$localeDir = $this->getDomainLocaleDir($module);
}
$this->addDomain($localeDir, $module);
}
/**
* Add a new translation domain
* (We're assuming that each domain only exists in one place)
*
* @param string $localeDir Location of translations
* @param string $domain Domain at location
* @return void
*/
public function addDomain($localeDir, $domain)
{
$this->localeDomainMap[$domain] = $localeDir;
Logger::debug("Localization: load domain '$domain' at '$localeDir'");
$this->loadGettextGettextFromPO($domain);
}
/**
* Get and check path of localization file
*
* @param string $domain Name of localization domain
* @throws Exception If the path does not exist even for the default, fallback language
*
* @return string
*/
public function getLangPath($domain = self::DEFAULT_DOMAIN)
{
$langcode = explode('_', $this->langcode);
$langcode = $langcode[0];
$localeDir = $this->localeDomainMap[$domain];
$langPath = $localeDir . '/' . $langcode . '/LC_MESSAGES/';
Logger::debug("Trying langpath for '$langcode' as '$langPath'");
if (is_dir($langPath) && is_readable($langPath)) {
return $langPath;
}
// Some langcodes have aliases..
$alias = $this->language->getLanguageCodeAlias($langcode);
if (isset($alias)) {
$langPath = $localeDir . '/' . $alias . '/LC_MESSAGES/';
Logger::debug("Trying langpath for alternative '$alias' as '$langPath'");
if (is_dir($langPath) && is_readable($langPath)) {
return $langPath;
}
}
// Language not found, fall back to default
$defLangcode = $this->language->getDefaultLanguage();
$langPath = $localeDir . '/' . $defLangcode . '/LC_MESSAGES/';
if (is_dir($langPath) && is_readable($langPath)) {
// Report that the localization for the preferred language is missing
$error = "Localization not found for langcode '$langcode' at '$langPath', falling back to langcode '" .
$defLangcode . "'";
Logger::error($_SERVER['PHP_SELF'] . ' - ' . $error);
return $langPath;
}
// Locale for default language missing even, error out
$error = "Localization directory missing/broken for langcode '$langcode' and domain '$domain'";
Logger::critical($_SERVER['PHP_SELF'] . ' - ' . $error);
throw new Exception($error);
}
/**
* Setup the translator
* @return void
*/
private function setupTranslator()
{
$this->translator = new Translator();
$this->translator->register();
}
/**
* Load translation domain from Gettext/Gettext using .po
*
* Note: Since Twig I18N does not support domains, all loaded files are
* merged. Use contexts if identical strings need to be disambiguated.
*
* @param string $domain Name of domain
* @param boolean $catchException Whether to catch an exception on error or return early
* @return void
*
* @throws Exception If something is wrong with the locale file for the domain and activated language
*/
private function loadGettextGettextFromPO($domain = self::DEFAULT_DOMAIN, $catchException = true)
{
try {
$langPath = $this->getLangPath($domain);
} catch (Exception $e) {
$error = "Something went wrong when trying to get path to language file, cannot load domain '$domain'.";
Logger::debug($_SERVER['PHP_SELF'] . ' - ' . $error);
if ($catchException) {
// bail out!
return;
} else {
throw $e;
}
}
$poFile = $domain . '.po';
$poPath = $langPath . $poFile;
if (file_exists($poPath) && is_readable($poPath)) {
$translations = Translations::fromPoFile($poPath);
$this->translator->loadTranslations($translations);
} else {
$error = "Localization file '$poFile' not found in '$langPath', falling back to default";
Logger::debug($_SERVER['PHP_SELF'] . ' - ' . $error);
}
}
/**
* Test to check if backend is set to default
*
* (if false: backend unset/there's an error)
*
* @return bool
*/
public function isI18NBackendDefault()
{
if ($this->i18nBackend === $this::SSP_I18N_BACKEND) {
return true;
}
return false;
}
/**
* Set up L18N if configured or fallback to old system
* @return void
*/
private function setupL10N()
{
if ($this->i18nBackend === self::SSP_I18N_BACKEND) {
Logger::debug("Localization: using old system");
return;
}
$this->setupTranslator();
// setup default domain
$this->addDomain($this->localeDir, self::DEFAULT_DOMAIN);
}
/**
* Show which domains are registered
*
* @return array
*/
public function getRegisteredDomains()
{
return $this->localeDomainMap;
}
}

View File

@ -0,0 +1,566 @@
<?php
/**
* The translation-relevant bits from our original minimalistic XHTML PHP based template system.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @author Hanne Moa, UNINETT AS. <hanne.moa@uninett.no>
* @package SimpleSAMLphp
*/
namespace SAML\Locale;
use Exception;
use Gettext\BaseTranslator;
use SAML\Configuration;
use SAML\Logger;
use SAML\Module;
class Translate
{
/**
* The configuration to be used for this translator.
*
* @var Configuration
*/
private $configuration;
/**
* Associative array of languages.
*
* @var array
*/
private $langtext = [];
/**
* Associative array of dictionaries.
*
* @var array
*/
private $dictionaries = [];
/**
* The default dictionary.
*
* @var string|null
*/
private $defaultDictionary = null;
/**
* The language object we'll use internally.
*
* @var Language
*/
private $language;
/**
* Constructor
*
* @param Configuration $configuration Configuration object
* @param string|null $defaultDictionary The default dictionary where tags will come from.
*/
public function __construct(Configuration $configuration, $defaultDictionary = null)
{
$this->configuration = $configuration;
$this->language = new Language($configuration);
if ($defaultDictionary !== null && substr($defaultDictionary, -4) === '.php') {
// TODO: drop this entire if clause for 2.0
// for backwards compatibility - print warning
$backtrace = debug_backtrace();
$where = $backtrace[0]['file'] . ':' . $backtrace[0]['line'];
Logger::warning(
'Deprecated use of new SimpleSAML\Locale\Translate(...) at ' . $where .
'. The last parameter is now a dictionary name, which should not end in ".php".'
);
$this->defaultDictionary = substr($defaultDictionary, 0, -4);
} else {
$this->defaultDictionary = $defaultDictionary;
}
}
/**
* Return the internal language object used by this translator.
*
* @return Language
*/
public function getLanguage()
{
return $this->language;
}
/**
* This method retrieves a dictionary with the name given.
*
* @param string $name The name of the dictionary, as the filename in the dictionary directory, without the
* '.php' ending.
*
* @return array An associative array with the dictionary.
*/
private function getDictionary($name)
{
assert(is_string($name));
if (!array_key_exists($name, $this->dictionaries)) {
$sepPos = strpos($name, ':');
if ($sepPos !== false) {
$module = substr($name, 0, $sepPos);
$fileName = substr($name, $sepPos + 1);
$dictDir = Module::getModuleDir($module) . '/dictionaries/';
} else {
$dictDir = $this->configuration->getPathValue('dictionarydir', 'dictionaries/') ?: 'dictionaries/';
$fileName = $name;
}
$this->dictionaries[$name] = $this->readDictionaryFile($dictDir . $fileName);
}
return $this->dictionaries[$name];
}
/**
* This method retrieves a tag as an array with language => string mappings.
*
* @param string $tag The tag name. The tag name can also be on the form '{<dictionary>:<tag>}', to retrieve a tag
* from the specific dictionary.
*
* @return array|null An associative array with language => string mappings, or null if the tag wasn't found.
*/
public function getTag($tag)
{
assert(is_string($tag));
// first check translations loaded by the includeInlineTranslation and includeLanguageFile methods
if (array_key_exists($tag, $this->langtext)) {
return $this->langtext[$tag];
}
// check whether we should use the default dictionary or a dictionary specified in the tag
if (substr($tag, 0, 1) === '{' && preg_match('/^{((?:\w+:)?\w+?):(.*)}$/D', $tag, $matches)) {
$dictionary = $matches[1];
$tag = $matches[2];
} else {
$dictionary = $this->defaultDictionary;
if ($dictionary === null) {
// we don't have any dictionary to load the tag from
return null;
}
}
$dictionary = $this->getDictionary($dictionary);
if (!array_key_exists($tag, $dictionary)) {
return null;
}
return $dictionary[$tag];
}
/**
* Retrieve the preferred translation of a given text.
*
* @param array $translations The translations, as an associative array with language => text mappings.
*
* @return string The preferred translation.
*
* @throws Exception If there's no suitable translation.
*/
public function getPreferredTranslation($translations)
{
assert(is_array($translations));
// look up translation of tag in the selected language
$selected_language = $this->language->getLanguage();
if (array_key_exists($selected_language, $translations)) {
return $translations[$selected_language];
}
// look up translation of tag in the default language
$default_language = $this->language->getDefaultLanguage();
if (array_key_exists($default_language, $translations)) {
return $translations[$default_language];
}
// check for english translation
if (array_key_exists('en', $translations)) {
return $translations['en'];
}
// pick the first translation available
if (count($translations) > 0) {
$languages = array_keys($translations);
return $translations[$languages[0]];
}
// we don't have anything to return
throw new Exception('Nothing to return from translation.');
}
/**
* Translate the name of an attribute.
*
* @param string $name The attribute name.
*
* @return string The translated attribute name, or the original attribute name if no translation was found.
*/
public function getAttributeTranslation($name)
{
// normalize attribute name
$normName = strtolower($name);
$normName = str_replace([":", "-"], "_", $normName);
// check for an extra dictionary
$extraDict = $this->configuration->getString('attributes.extradictionary', null);
if ($extraDict !== null) {
$dict = $this->getDictionary($extraDict);
if (array_key_exists($normName, $dict)) {
return $this->getPreferredTranslation($dict[$normName]);
}
}
// search the default attribute dictionary
$dict = $this->getDictionary('attributes');
if (array_key_exists('attribute_' . $normName, $dict)) {
return $this->getPreferredTranslation($dict['attribute_' . $normName]);
}
// no translations found
return $name;
}
/**
* Mark a string for translation without translating it.
*
* @param string $tag A tag name to mark for translation.
*
* @return string The tag, unchanged.
*/
public static function noop($tag)
{
return $tag;
}
/**
* Translate a tag into the current language, with a fallback to english.
*
* This function is used to look up a translation tag in dictionaries, and return the translation into the current
* language. If no translation into the current language can be found, english will be tried, and if that fails,
* placeholder text will be returned.
*
* An array can be passed as the tag. In that case, the array will be assumed to be on the form (language => text),
* and will be used as the source of translations.
*
* This function can also do replacements into the translated tag. It will search the translated tag for the keys
* provided in $replacements, and replace any found occurrences with the value of the key.
*
* @param string|array $tag A tag name for the translation which should be looked up, or an array with
* (language => text) mappings. The array version will go away in 2.0
* @param array $replacements An associative array of keys that should be replaced with values in the
* translated string.
* @param boolean $fallbackdefault Default translation to use as a fallback if no valid translation was found.
* @param array $oldreplacements
* @param bool $striptags
* @deprecated Not used in twig, gettext
*
* @return string|null The translated tag, or a placeholder value if the tag wasn't found.
*/
public function t(
$tag,
$replacements = [],
// TODO: remove this for 2.0. Assume true
$fallbackdefault = true,
// TODO: remove this for 2.0
$oldreplacements = [],
// TODO: remove this for 2.0
$striptags = false
) {
$backtrace = debug_backtrace();
$where = $backtrace[0]['file'] . ':' . $backtrace[0]['line'];
if (!$fallbackdefault) {
Logger::warning(
'Deprecated use of new SimpleSAML\Locale\Translate::t(...) at ' . $where .
'. This parameter will go away, the fallback will become' .
' identical to the $tag in 2.0.'
);
}
if (!is_array($replacements)) {
// TODO: remove this entire if for 2.0
// old style call to t(...). Print warning to log
Logger::warning(
'Deprecated use of SimpleSAML\Locale\Translate::t(...) at ' . $where .
'. Please update the code to use the new style of parameters.'
);
// for backwards compatibility
/** @psalm-suppress PossiblyInvalidArgument */
if (!$replacements && ($this->getTag($tag) === null)) {
Logger::warning(
'Code which uses $fallbackdefault === FALSE should be updated to use the getTag() method instead.'
);
return null;
}
$replacements = $oldreplacements;
}
if (is_array($tag)) {
$tagData = $tag;
Logger::warning(
'Deprecated use of new SimpleSAML\Locale\Translate::t(...) at ' . $where .
'. The $tag-parameter can only be a string in 2.0.'
);
} else {
$tagData = $this->getTag($tag);
if ($tagData === null) {
// tag not found
Logger::info('Translate: Looking up [' . $tag . ']: not translated at all.');
return $this->getStringNotTranslated($tag, $fallbackdefault);
}
}
$translated = $this->getPreferredTranslation($tagData);
foreach ($replacements as $k => $v) {
// try to translate if no replacement is given
if ($v == null) {
$v = $this->t($k);
}
$translated = str_replace($k, $v, $translated);
}
return $translated;
}
/**
* Return the string that should be used when no translation was found.
*
* @param string $tag A name tag of the string that should be returned.
* @param boolean $fallbacktag If set to true and string was not found in any languages, return the tag itself. If
* false return null.
*
* @return string The string that should be used, or the tag name if $fallbacktag is set to false.
*/
private function getStringNotTranslated($tag, $fallbacktag)
{
if ($fallbacktag) {
return 'not translated (' . $tag . ')';
} else {
return $tag;
}
}
/**
* Include a translation inline instead of putting translations in dictionaries. This function is recommended to be
* used ONLY for variable data, or when the translation is already provided by an external source, as a database
* or in metadata.
*
* @param string $tag The tag that has a translation
* @param array|string $translation The translation array
*
* @throws Exception If $translation is neither a string nor an array.
* @return void
*/
public function includeInlineTranslation($tag, $translation)
{
if (is_string($translation)) {
$translation = ['en' => $translation];
} elseif (!is_array($translation)) {
throw new Exception(
"Inline translation should be string or array. Is " . gettype($translation) . " now!"
);
}
Logger::debug('Translate: Adding inline language translation for tag [' . $tag . ']');
$this->langtext[$tag] = $translation;
}
/**
* Include a language file from the dictionaries directory.
*
* @param string $file File name of dictionary to include
* @param Configuration|null $otherConfig Optionally provide a different configuration object than the
* one provided in the constructor to be used to find the directory of the dictionary. This allows to combine
* dictionaries inside the SimpleSAMLphp main code distribution together with external dictionaries. Defaults to
* null.
* @return void
*/
public function includeLanguageFile($file, $otherConfig = null)
{
if (!empty($otherConfig)) {
$filebase = $otherConfig->getPathValue('dictionarydir', 'dictionaries/');
} else {
$filebase = $this->configuration->getPathValue('dictionarydir', 'dictionaries/');
}
$filebase = $filebase ?: 'dictionaries/';
$lang = $this->readDictionaryFile($filebase . $file);
Logger::debug('Translate: Merging language array. Loading [' . $file . ']');
$this->langtext = array_merge($this->langtext, $lang);
}
/**
* Read a dictionary file in JSON format.
*
* @param string $filename The absolute path to the dictionary file, minus the .definition.json ending.
*
* @return array An array holding all the translations in the file.
*/
private function readDictionaryJSON($filename)
{
$definitionFile = $filename . '.definition.json';
assert(file_exists($definitionFile));
$fileContent = file_get_contents($definitionFile);
$lang = json_decode($fileContent, true);
if (empty($lang)) {
Logger::error('Invalid dictionary definition file [' . $definitionFile . ']');
return [];
}
$translationFile = $filename . '.translation.json';
if (file_exists($translationFile)) {
$fileContent = file_get_contents($translationFile);
$moreTrans = json_decode($fileContent, true);
if (!empty($moreTrans)) {
$lang = array_merge_recursive($lang, $moreTrans);
}
}
return $lang;
}
/**
* Read a dictionary file in PHP format.
*
* @param string $filename The absolute path to the dictionary file.
*
* @return array An array holding all the translations in the file.
*/
private function readDictionaryPHP($filename)
{
$phpFile = $filename . '.php';
assert(file_exists($phpFile));
$lang = null;
include($phpFile);
if (isset($lang)) {
return $lang;
}
return [];
}
/**
* Read a dictionary file.
*
* @param string $filename The absolute path to the dictionary file.
*
* @return array An array holding all the translations in the file.
*/
private function readDictionaryFile($filename)
{
assert(is_string($filename));
Logger::debug('Translate: Reading dictionary [' . $filename . ']');
$jsonFile = $filename . '.definition.json';
if (file_exists($jsonFile)) {
return $this->readDictionaryJSON($filename);
}
$phpFile = $filename . '.php';
if (file_exists($phpFile)) {
return $this->readDictionaryPHP($filename);
}
Logger::error(
$_SERVER['PHP_SELF'] . ' - Translate: Could not find dictionary file at [' . $filename . ']'
);
return [];
}
/**
* Translate a singular text.
*
* @param string $original The string before translation.
*
* @return string The translated string.
*/
public static function translateSingularGettext($original)
{
$text = BaseTranslator::$current->gettext($original);
if (func_num_args() === 1) {
return $text;
}
$args = array_slice(func_get_args(), 1);
return strtr($text, is_array($args[0]) ? $args[0] : $args);
}
/**
* Translate a plural text.
*
* @param string $original The string before translation.
* @param string $plural
* @param string $value
*
* @return string The translated string.
*/
public static function translatePluralGettext($original, $plural, $value)
{
$text = BaseTranslator::$current->ngettext($original, $plural, $value);
if (func_num_args() === 3) {
return $text;
}
$args = array_slice(func_get_args(), 3);
return strtr($text, is_array($args[0]) ? $args[0] : $args);
}
/**
* Pick a translation from a given array of translations for the current language.
*
* @param array|null $context An array of options. The current language must be specified
* as an ISO 639 code accessible with the key "currentLanguage" in the array.
* @param array|null $translations An array of translations. Each translation has an
* ISO 639 code as its key, identifying the language it corresponds to.
*
* @return null|string The translation appropriate for the current language, or null if none found. If the
* $context or $translations arrays are null, or $context['currentLanguage'] is not defined, null is also returned.
*/
public static function translateFromArray($context, $translations)
{
if (!is_array($translations)) {
return null;
}
if (!is_array($context) || !isset($context['currentLanguage'])) {
return null;
}
if (isset($translations[$context['currentLanguage']])) {
return $translations[$context['currentLanguage']];
}
// we don't have a translation for the current language, load alternative priorities
$sspcfg = Configuration::getInstance();
$langcfg = $sspcfg->getConfigItem('language');
$priorities = $langcfg->getArray('priorities', []);
if (!empty($priorities[$context['currentLanguage']])) {
foreach ($priorities[$context['currentLanguage']] as $lang) {
if (isset($translations[$lang])) {
return $translations[$lang];
}
}
}
// nothing we can use, return null so that we can set a default
return null;
}
}

528
libsrc/SAML/Logger.php Normal file
View File

@ -0,0 +1,528 @@
<?php
namespace SAML;
use Exception;
use SAML\Logger\ErrorLogLoggingHandler;
use SAML\Logger\LoggingHandlerInterface;
use SAML\Logger\StandardErrorLoggingHandler;
/**
* The main logger class for SimpleSAMLphp.
*
* @author Lasse Birnbaum Jensen, SDU.
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @author Jaime Pérez Crespo, UNINETT AS <jaime.perez@uninett.no>
* @package SimpleSAMLphp
*/
class Logger
{
/**
* @var LoggingHandlerInterface
*/
private static $loggingHandler;
/**
* @var bool
*/
private static $initializing = false;
/**
* @var integer|null
*/
private static $logLevel = null;
/**
* @var boolean
*/
private static $captureLog = false;
/**
* @var array
*/
private static $capturedLog = [];
/**
* Array with messages logged before the logging handler was initialized.
*
* @var array
*/
private static $earlyLog = [];
/**
* List of log levels.
*
* This list is used to restore the log levels after some log levels have been disabled.
*
* @var array
*/
private static $logLevelStack = [];
/**
* The current mask of log levels disabled.
*
* Note: this mask is not directly related to the PHP error reporting level.
*
* @var int
*/
private static $logMask = 0;
/**
* This constant defines the string we set the track ID to while we are fetching the track ID from the session
* class. This is used to prevent infinite recursion.
*
* @var string
*/
const NO_TRACKID = '_NOTRACKIDYET_';
/**
* This variable holds the track ID we have retrieved from the session class. It can also be NULL, in which case
* we haven't fetched the track ID yet, or self::NO_TRACKID, which means that we are fetching the track ID now.
*
* @var string
*/
private static $trackid = self::NO_TRACKID;
/**
* This variable holds the format used to log any message. Its use varies depending on the log handler used (for
* instance, you cannot control here how dates are displayed when using syslog or errorlog handlers), but in
* general the options are:
*
* - %date{<format>}: the date and time, with its format specified inside the brackets. See the PHP documentation
* of the strftime() function for more information on the format. If the brackets are omitted, the standard
* format is applied. This can be useful if you just want to control the placement of the date, but don't care
* about the format.
*
* - %process: the name of the SimpleSAMLphp process. Remember you can configure this in the 'logging.processname'
* option. The SyslogLoggingHandler will just remove this.
*
* - %level: the log level (name or number depending on the handler used). Please note different logging handlers
* will print the log level differently.
*
* - %stat: if the log entry is intended for statistical purposes, it will print the string 'STAT ' (bear in mind
* the trailing space).
*
* - %trackid: the track ID, an identifier that allows you to track a single session.
*
* - %srcip: the IP address of the client. If you are behind a proxy, make sure to modify the
* $_SERVER['REMOTE_ADDR'] variable on your code accordingly to the X-Forwarded-For header.
*
* - %msg: the message to be logged.
*
* @var string The format of the log line.
*/
private static $format = '%date{%b %d %H:%M:%S} %process %level %stat[%trackid] %msg';
/**
* This variable tells if we have a shutdown function registered or not.
*
* @var bool
*/
private static $shutdownRegistered = false;
/**
* This variable tells if we are shutting down.
*
* @var bool
*/
private static $shuttingDown = false;
/** @var int */
const EMERG = 0;
/** @var int */
const ALERT = 1;
/** @var int */
const CRIT = 2;
/** @var int */
const ERR = 3;
/** @var int */
const WARNING = 4;
/** @var int */
const NOTICE = 5;
/** @var int */
const INFO = 6;
/** @var int */
const DEBUG = 7;
/**
* Log an emergency message.
*
* @param string $string The message to log.
* @return void
*/
public static function emergency($string)
{
self::log(self::EMERG, $string);
}
/**
* Log a critical message.
*
* @param string $string The message to log.
* @return void
*/
public static function critical($string)
{
self::log(self::CRIT, $string);
}
/**
* Log an alert.
*
* @param string $string The message to log.
* @return void
*/
public static function alert($string)
{
self::log(self::ALERT, $string);
}
/**
* Log an error.
*
* @param string $string The message to log.
* @return void
*/
public static function error($string)
{
self::log(self::ERR, $string);
}
/**
* Log a warning.
*
* @param string $string The message to log.
* @return void
*/
public static function warning($string)
{
self::log(self::WARNING, $string);
}
/**
* We reserve the notice level for statistics, so do not use this level for other kind of log messages.
*
* @param string $string The message to log.
* @return void
*/
public static function notice($string)
{
self::log(self::NOTICE, $string);
}
/**
* Info messages are a bit less verbose than debug messages. This is useful to trace a session.
*
* @param string $string The message to log.
* @return void
*/
public static function info($string)
{
self::log(self::INFO, $string);
}
/**
* Debug messages are very verbose, and will contain more information than what is necessary for a production
* system.
*
* @param string $string The message to log.
* @return void
*/
public static function debug($string)
{
self::log(self::DEBUG, $string);
}
/**
* Statistics.
*
* @param string $string The message to log.
* @return void
*/
public static function stats($string)
{
self::log(self::NOTICE, $string, true);
}
/**
* Set the logger to capture logs.
*
* @param boolean $val Whether to capture logs or not. Defaults to TRUE.
* @return void
*/
public static function setCaptureLog($val = true)
{
self::$captureLog = $val;
}
/**
* Get the captured log.
* @return array
*/
public static function getCapturedLog()
{
return self::$capturedLog;
}
/**
* Set the track identifier to use in all logs.
*
* @param string $trackId The track identifier to use during this session.
* @return void
*/
public static function setTrackId($trackId)
{
self::$trackid = $trackId;
self::flush();
}
/**
* Flush any pending log messages to the logging handler.
*
* @return void
*/
public static function flush()
{
foreach (self::$earlyLog as $msg) {
self::log($msg['level'], $msg['string'], $msg['statsLog']);
}
self::$earlyLog = [];
}
/**
* Flush any pending deferred logs during shutdown.
*
* This method is intended to be registered as a shutdown handler, so that any pending messages that weren't sent
* to the logging handler at that point, can still make it. It is therefore not intended to be called manually.
*
* @return void
*/
public static function shutdown()
{
if (self::$trackid === self::NO_TRACKID) {
try {
$s = Session::getSessionFromRequest();
} catch (Exception $e) {
// loading session failed. We don't care why, at this point we have a transient session, so we use that
$s = Session::getSessionFromRequest();
}
self::$trackid = $s->getTrackID();
}
self::$shuttingDown = true;
self::flush();
}
/**
* Evaluate whether errors of a certain error level are masked or not.
*
* @param int $errno The level of the error to check.
*
* @return bool True if the error is masked, false otherwise.
*/
public static function isErrorMasked($errno)
{
return ($errno & self::$logMask) || !($errno & error_reporting());
}
/**
* Disable error reporting for the given log levels.
*
* Every call to this function must be followed by a call to popErrorMask().
*
* @param int $mask The log levels that should be masked.
* @return void
*/
public static function maskErrors($mask)
{
assert(is_int($mask));
$currentEnabled = error_reporting();
self::$logLevelStack[] = [$currentEnabled, self::$logMask];
$currentEnabled &= ~$mask;
error_reporting($currentEnabled);
self::$logMask |= $mask;
}
/**
* Pop an error mask.
*
* This function restores the previous error mask.
*
* @return void
*/
public static function popErrorMask()
{
$lastMask = array_pop(self::$logLevelStack);
error_reporting($lastMask[0]);
self::$logMask = $lastMask[1];
}
/**
* Defer a message for later logging.
*
* @param int $level The log level corresponding to this message.
* @param string $message The message itself to log.
* @param boolean $stats Whether this is a stats message or a regular one.
* @return void
*/
private static function defer($level, $message, $stats)
{
// save the message for later
self::$earlyLog[] = ['level' => $level, 'string' => $message, 'statsLog' => $stats];
// register a shutdown handler if needed
if (!self::$shutdownRegistered) {
register_shutdown_function([self::class, 'shutdown']);
self::$shutdownRegistered = true;
}
}
/**
* @param string|null $handler
* @return void
* @throws Exception
*/
private static function createLoggingHandler($handler = null)
{
self::$initializing = true;
// a set of known logging handlers
$known_handlers = [
'syslog' => 'SimpleSAML\Logger\SyslogLoggingHandler',
'file' => 'SimpleSAML\Logger\FileLoggingHandler',
'errorlog' => 'SimpleSAML\Logger\ErrorLogLoggingHandler',
'stderr' => 'SimpleSAML\Logger\StandardErrorLoggingHandler',
];
// get the configuration
$config = Configuration::getInstance();
assert($config instanceof Configuration);
// setting minimum log_level
self::$logLevel = $config->getInteger('logging.level', self::INFO);
// get the metadata handler option from the configuration
if (is_null($handler)) {
$handler = $config->getString('logging.handler', 'syslog');
}
if (!array_key_exists($handler, $known_handlers) && class_exists($handler)) {
if (!in_array('SimpleSAML\Logger\LoggingHandlerInterface', class_implements($handler), true)) {
throw new Exception("The logging handler '$handler' is invalid.");
}
} else {
$handler = strtolower($handler);
if (!array_key_exists($handler, $known_handlers)) {
throw new Exception(
"Invalid value for the 'logging.handler' configuration option. Unknown handler '" . $handler . "'."
);
}
$handler = $known_handlers[$handler];
}
self::$format = $config->getString('logging.format', self::$format);
try {
/** @var LoggingHandlerInterface */
self::$loggingHandler = new $handler($config);
self::$loggingHandler->setLogFormat(self::$format);
self::$initializing = false;
} catch (Exception $e) {
self::$loggingHandler = new ErrorLogLoggingHandler($config);
self::$initializing = false;
self::log(self::CRIT, $e->getMessage(), false);
}
}
/**
* @param int $level
* @param string $string
* @param bool $statsLog
* @return void
*/
private static function log($level, $string, $statsLog = false)
{
if (self::$initializing) {
// some error occurred while initializing logging
self::defer($level, $string, $statsLog);
return;
} elseif (php_sapi_name() === 'cli' || defined('STDIN')) {
// we are being executed from the CLI, nowhere to log
if (!isset(self::$loggingHandler)) {
self::createLoggingHandler(StandardErrorLoggingHandler::class);
}
$_SERVER['REMOTE_ADDR'] = "CLI";
if (self::$trackid === self::NO_TRACKID) {
self::$trackid = 'CL' . bin2hex(openssl_random_pseudo_bytes(4));
}
} elseif (!isset(self::$loggingHandler)) {
// Initialize logging
self::createLoggingHandler();
}
if (self::$captureLog) {
$ts = microtime(true);
$msecs = (int) (($ts - (int) $ts) * 1000);
$ts = gmdate('H:i:s', $ts) . sprintf('.%03d', $msecs) . 'Z';
self::$capturedLog[] = $ts . ' ' . $string;
}
if (self::$logLevel >= $level || $statsLog) {
if (is_array($string)) {
$string = implode(",", $string);
}
$formats = ['%trackid', '%msg', '%srcip', '%stat'];
$replacements = [self::$trackid, $string, $_SERVER['REMOTE_ADDR']];
$stat = '';
if ($statsLog) {
$stat = 'STAT ';
}
array_push($replacements, $stat);
if (self::$trackid === self::NO_TRACKID && !self::$shuttingDown) {
// we have a log without track ID and we are not still shutting down, so defer logging
self::defer($level, $string, $statsLog);
return;
} elseif (self::$trackid === self::NO_TRACKID) {
// shutting down without a track ID, prettify it
array_shift($replacements);
array_unshift($replacements, 'N/A');
}
// we either have a track ID or we are shutting down, so just log the message
$string = str_replace($formats, $replacements, self::$format);
self::$loggingHandler->log($level, $string);
}
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace SAML\Logger;
use SAML\Configuration;
use SAML\Logger;
/**
* A class for logging to the default php error log.
*
* @author Lasse Birnbaum Jensen, SDU.
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class ErrorLogLoggingHandler implements LoggingHandlerInterface
{
/**
* This array contains the mappings from syslog log level to names.
*
* @var array
*/
private static $levelNames = [
Logger::EMERG => 'EMERG',
Logger::ALERT => 'ALERT',
Logger::CRIT => 'CRIT',
Logger::ERR => 'ERR',
Logger::WARNING => 'WARNING',
Logger::NOTICE => 'NOTICE',
Logger::INFO => 'INFO',
Logger::DEBUG => 'DEBUG',
];
/**
* The name of this process.
*
* @var string
*/
private $processname;
/**
* ErrorLogLoggingHandler constructor.
*
* @param Configuration $config The configuration object for this handler.
*/
public function __construct(Configuration $config)
{
$this->processname = $config->getString('logging.processname', 'SimpleSAMLphp');
}
/**
* Set the format desired for the logs.
*
* @param string $format The format used for logs.
* @return void
*/
public function setLogFormat($format)
{
// we don't need the format here
}
/**
* Log a message to syslog.
*
* @param int $level The log level.
* @param string $string The formatted message to log.
* @return void
*/
public function log($level, $string)
{
if (array_key_exists($level, self::$levelNames)) {
$levelName = self::$levelNames[$level];
} else {
$levelName = sprintf('UNKNOWN%d', $level);
}
$formats = ['%process', '%level'];
$replacements = [$this->processname, $levelName];
$string = str_replace($formats, $replacements, $string);
$string = preg_replace('/%\w+(\{[^\}]+\})?/', '', $string);
$string = trim($string);
error_log($string);
}
}

View File

@ -0,0 +1,125 @@
<?php
namespace SAML\Logger;
use Exception;
use SAML\Configuration;
use SAML\Logger;
use SAML\Utils;
use const PHP_EOL;
/**
* A logging handler that dumps logs to files.
*
* @author Lasse Birnbaum Jensen, SDU.
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
class FileLoggingHandler implements LoggingHandlerInterface
{
/**
* A string with the path to the file where we should log our messages.
*
* @var null|string
*/
protected $logFile = null;
/**
* This array contains the mappings from syslog log levels to names. Copied more or less directly from
* SimpleSAML\Logger\ErrorLogLoggingHandler.
*
* @var array
*/
private static $levelNames = [
Logger::EMERG => 'EMERGENCY',
Logger::ALERT => 'ALERT',
Logger::CRIT => 'CRITICAL',
Logger::ERR => 'ERROR',
Logger::WARNING => 'WARNING',
Logger::NOTICE => 'NOTICE',
Logger::INFO => 'INFO',
Logger::DEBUG => 'DEBUG',
];
/** @var string|null */
protected $processname = null;
/** @var string */
protected $format = "%b %d %H:%M:%S";
/**
* Build a new logging handler based on files.
* @param Configuration $config
*/
public function __construct(Configuration $config)
{
// get the metadata handler option from the configuration
$this->logFile = $config->getPathValue('loggingdir', 'log/') .
$config->getString('logging.logfile', 'simplesamlphp.log');
$this->processname = $config->getString('logging.processname', 'SimpleSAMLphp');
if (@file_exists($this->logFile)) {
if (!@is_writeable($this->logFile)) {
throw new Exception("Could not write to logfile: " . $this->logFile);
}
} else {
if (!@touch($this->logFile)) {
throw new Exception(
"Could not create logfile: " . $this->logFile .
" The logging directory is not writable for the web server user."
);
}
}
Utils\Time::initTimezone();
}
/**
* Set the format desired for the logs.
*
* @param string $format The format used for logs.
* @return void
*/
public function setLogFormat($format)
{
$this->format = $format;
}
/**
* Log a message to the log file.
*
* @param int $level The log level.
* @param string $string The formatted message to log.
* @return void
*/
public function log($level, $string)
{
if (!is_null($this->logFile)) {
// set human-readable log level. Copied from SimpleSAML\Logger\ErrorLogLoggingHandler.
$levelName = sprintf('UNKNOWN%d', $level);
if (array_key_exists($level, self::$levelNames)) {
$levelName = self::$levelNames[$level];
}
$formats = ['%process', '%level'];
$replacements = [$this->processname, $levelName];
$matches = [];
if (preg_match('/%date(?:\{([^\}]+)\})?/', $this->format, $matches)) {
$format = "%b %d %H:%M:%S";
if (isset($matches[1])) {
$format = $matches[1];
}
array_push($formats, $matches[0]);
array_push($replacements, strftime($format));
}
$string = str_replace($formats, $replacements, $string);
file_put_contents($this->logFile, $string . PHP_EOL, FILE_APPEND);
}
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace SAML\Logger;
use SAML\Configuration;
/**
* The interface that must be implemented by any log handler.
*
* @author Jaime Perez Crespo, UNINETT AS.
* @package SimpleSAMLphp
*/
interface LoggingHandlerInterface
{
/**
* Constructor for log handlers. It must accept receiving a \SimpleSAML\Configuration object.
*
* @param Configuration $config The configuration to use in this log handler.
*/
public function __construct(Configuration $config);
/**
* Log a message to its destination.
*
* @param int $level The log level.
* @param string $string The message to log.
* @return void
*/
public function log($level, $string);
/**
* Set the format desired for the logs.
*
* @param string $format The format used for logs.
* @return void
*/
public function setLogFormat($format);
}

View File

@ -0,0 +1,27 @@
<?php
namespace SAML\Logger;
use SAML\Configuration;
/**
* A logging handler that outputs all messages to standard error.
*
* @author Jaime Perez Crespo, UNINETT AS <jaime.perez@uninett.no>
* @package SimpleSAMLphp
*/
class StandardErrorLoggingHandler extends FileLoggingHandler
{
/**
* StandardError constructor.
*
* It runs the parent constructor and sets the log file to be the standard error descriptor.
*
* @param Configuration $config
*/
public function __construct(Configuration $config)
{
$this->processname = $config->getString('logging.processname', 'SimpleSAMLphp');
$this->logFile = 'php://stderr';
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace SAML\Logger;
use SAML\Configuration;
use SAML\Utils;
/**
* A logger that sends messages to syslog.
*
* @author Lasse Birnbaum Jensen, SDU.
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
class SyslogLoggingHandler implements LoggingHandlerInterface
{
/** @var bool */
private $isWindows = false;
/** @var string */
protected $format = "%b %d %H:%M:%S";
/**
* Build a new logging handler based on syslog.
* @param Configuration $config
*/
public function __construct(Configuration $config)
{
$facility = $config->getInteger('logging.facility', defined('LOG_LOCAL5') ? constant('LOG_LOCAL5') : LOG_USER);
$processname = $config->getString('logging.processname', 'SimpleSAMLphp');
// Setting facility to LOG_USER (only valid in Windows), enable log level rewrite on windows systems
if (Utils\System::getOS() === Utils\System::WINDOWS) {
$this->isWindows = true;
$facility = LOG_USER;
}
openlog($processname, LOG_PID, $facility);
}
/**
* Set the format desired for the logs.
*
* @param string $format The format used for logs.
* @return void
*/
public function setLogFormat($format)
{
$this->format = $format;
}
/**
* Log a message to syslog.
*
* @param int $level The log level.
* @param string $string The formatted message to log.
* @return void
*/
public function log($level, $string)
{
// changing log level to supported levels if OS is Windows
if ($this->isWindows) {
if ($level <= 4) {
$level = LOG_ERR;
} else {
$level = LOG_INFO;
}
}
$formats = ['%process', '%level'];
$replacements = ['', $level];
$string = str_replace($formats, $replacements, $string);
$string = preg_replace('/%\w+(\{[^\}]+\})?/', '', $string);
$string = trim($string);
syslog($level, $string);
}
}

514
libsrc/SAML/Memcache.php Normal file
View File

@ -0,0 +1,514 @@
<?php
namespace SAML;
use Exception;
use Memcached;
use SAML\Utils;
/**
* This file implements functions to read and write to a group of memcache
* servers.
*
* The goals of this storage class is to provide failover, redudancy and load
* balancing. This is accomplished by storing the data object to several
* groups of memcache servers. Each data object is replicated to every group
* of memcache servers, but it is only stored to one server in each group.
*
* For this code to work correctly, all web servers accessing the data must
* have the same clock (as measured by the time()-function). Different clock
* values will lead to incorrect behaviour.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class Memcache
{
/**
* Cache of the memcache servers we are using.
*
* @var \Memcache[]|Memcached[]|null
*/
private static $serverGroups = null;
/**
* The flavor of memcache PHP extension we are using.
*
* @var string
*/
private static $extension = '';
/**
* Find data stored with a given key.
*
* @param string $key The key of the data.
*
* @return mixed The data stored with the given key, or null if no data matching the key was found.
*/
public static function get($key)
{
Logger::debug("loading key $key from memcache");
$latestInfo = null;
$latestTime = 0.0;
$latestData = null;
$mustUpdate = false;
$allDown = true;
// search all the servers for the given id
foreach (self::getMemcacheServers() as $server) {
$serializedInfo = $server->get($key);
if ($serializedInfo === false) {
// either the server is down, or we don't have the value stored on that server
$mustUpdate = true;
$up = $server->getStats();
if ($up !== false) {
$allDown = false;
}
continue;
}
$allDown = false;
// unserialize the object
/** @var string $serializedInfo */
$info = unserialize($serializedInfo);
/*
* Make sure that this is an array with two keys:
* - 'timestamp': The time the data was saved.
* - 'data': The data.
*/
if (!is_array($info)) {
Logger::warning(
'Retrieved invalid data from a memcache server. Data was not an array.'
);
continue;
}
if (!array_key_exists('timestamp', $info)) {
Logger::warning(
'Retrieved invalid data from a memcache server. Missing timestamp.'
);
continue;
}
if (!array_key_exists('data', $info)) {
Logger::warning(
'Retrieved invalid data from a memcache server. Missing data.'
);
continue;
}
if ($latestInfo === null) {
// first info found
$latestInfo = $serializedInfo;
$latestTime = $info['timestamp'];
$latestData = $info['data'];
continue;
}
if ($info['timestamp'] === $latestTime && $serializedInfo === $latestInfo) {
// this data matches the data from the other server(s)
continue;
}
// different data from different servers. We need to update at least one of them to maintain sync
$mustUpdate = true;
// update if data in $info is newer than $latestData
if ($latestTime < $info['timestamp']) {
$latestInfo = $serializedInfo;
$latestTime = $info['timestamp'];
$latestData = $info['data'];
}
}
if ($latestData === null) {
if ($allDown) {
// all servers are down, panic!
$e = new Error\Error('MEMCACHEDOWN', null, 503);
throw new Error\Exception('All memcache servers are down', 503, $e);
}
// we didn't find any data matching the key
Logger::debug("key $key not found in memcache");
return null;
}
if ($mustUpdate) {
// we found data matching the key, but some of the servers need updating
Logger::debug("Memcache servers out of sync for $key, forcing sync");
self::set($key, $latestData);
}
return $latestData;
}
/**
* Save a key-value pair to the memcache servers.
*
* @param string $key The key of the data.
* @param mixed $value The value of the data.
* @param integer|null $expire The expiration timestamp of the data.
* @return void
*/
public static function set($key, $value, $expire = null)
{
Logger::debug("saving key $key to memcache");
$savedInfo = [
'timestamp' => microtime(true),
'data' => $value
];
if ($expire === null) {
$expire = self::getExpireTime();
}
$savedInfoSerialized = serialize($savedInfo);
// store this object to all groups of memcache servers
foreach (self::getMemcacheServers() as $server) {
if (self::$extension === Memcached::class) {
$server->set($key, $savedInfoSerialized, $expire);
} else {
$server->set($key, $savedInfoSerialized, 0, $expire);
}
}
}
/**
* Delete a key-value pair from the memcache servers.
*
* @param string $key The key we should delete.
* @return void
*/
public static function delete($key)
{
assert(is_string($key));
Logger::debug("deleting key $key from memcache");
// store this object to all groups of memcache servers
foreach (self::getMemcacheServers() as $server) {
$server->delete($key);
}
}
/**
* This function adds a server from the 'memcache_store.servers'
* configuration option to a Memcache object.
*
* The server parameter is an array with the following keys:
* - hostname
* Hostname or ip address to the memcache server.
* - port (optional)
* port number the memcache server is running on. This
* defaults to memcache.default_port if no value is given.
* The default value of memcache.default_port is 11211.
* - weight (optional)
* The weight of this server in the load balancing
* cluster.
* - timeout (optional)
* The timeout for contacting this server, in seconds.
* The default value is 3 seconds.
*
* @param \Memcache|Memcached $memcache The Memcache object we should add this server to.
* @param array $server An associative array with the configuration options for the server to add.
* @return void
*
* @throws Exception If any configuration option for the server is invalid.
*/
private static function addMemcacheServer($memcache, $server)
{
// the hostname option is required
if (!array_key_exists('hostname', $server)) {
throw new Exception(
"hostname setting missing from server in the 'memcache_store.servers' configuration option."
);
}
$hostname = $server['hostname'];
// the hostname must be a valid string
if (!is_string($hostname)) {
throw new Exception(
"Invalid hostname for server in the 'memcache_store.servers' configuration option. The hostname is" .
' supposed to be a string.'
);
}
// check if the user has specified a port number
if (strpos($hostname, 'unix:///') === 0) {
// force port to be 0 for sockets
$port = 0;
} elseif (array_key_exists('port', $server)) {
// get the port number from the array, and validate it
$port = (int) $server['port'];
if (($port <= 0) || ($port > 65535)) {
throw new Exception(
"Invalid port for server in the 'memcache_store.servers' configuration option. The port number" .
' is supposed to be an integer between 0 and 65535.'
);
}
} else {
// use the default port number from the ini-file
$port = (int) ini_get('memcache.default_port');
if ($port <= 0 || $port > 65535) {
// invalid port number from the ini-file. fall back to the default
$port = 11211;
}
}
// check if the user has specified a weight for this server
if (array_key_exists('weight', $server)) {
// get the weight and validate it
$weight = (int) $server['weight'];
if ($weight <= 0) {
throw new Exception(
"Invalid weight for server in the 'memcache_store.servers' configuration option. The weight is" .
' supposed to be a positive integer.'
);
}
} else {
// use a default weight of 1
$weight = 1;
}
// check if the user has specified a timeout for this server
if (array_key_exists('timeout', $server)) {
// get the timeout and validate it
$timeout = (int) $server['timeout'];
if ($timeout <= 0) {
throw new Exception(
"Invalid timeout for server in the 'memcache_store.servers' configuration option. The timeout is" .
' supposed to be a positive integer.'
);
}
} else {
// use a default timeout of 3 seconds
$timeout = 3;
}
// add this server to the Memcache object
if ($memcache instanceof Memcached) {
$memcache->addServer($hostname, $port);
} else {
$memcache->addServer($hostname, $port, true, $weight, $timeout, $timeout, true);
}
}
/**
* This function takes in a list of servers belonging to a group and
* creates a Memcache object from the servers in the group.
*
* @param array $group Array of servers which should be created as a group.
* @param string $index The index for this group. Specify if persistent connections are desired.
*
* @return \Memcache|Memcached A Memcache object of the servers in the group
*
* @throws Exception If the servers configuration is invalid.
*/
private static function loadMemcacheServerGroup(array $group, $index = null)
{
if (class_exists(Memcached::class)) {
if (is_string($index)) {
$memcache = new Memcached($index);
} else {
$memcache = new Memcached();
}
if (array_key_exists('options', $group)) {
$memcache->setOptions($group['options']);
unset($group['options']);
}
self::$extension = Memcached::class;
$servers = $memcache->getServerList();
if (count($servers) === count($group) && !$memcache->isPristine()) {
return $memcache;
}
$memcache->resetServerList();
} elseif (class_exists(\Memcache::class)) {
$memcache = new \Memcache();
self::$extension = \Memcache::class;
} else {
throw new Exception(
'Missing Memcached implementation. You must install either the Memcache or Memcached extension.'
);
}
if (self::$extension === \Memcache::class) {
Logger::warning(
"The use of PHP-extension memcache is deprecated. Please migrate to the memcached extension."
);
}
// iterate over all the servers in the group and add them to the Memcache object
foreach ($group as $index => $server) {
// make sure that we don't have an index. An index would be a sign of invalid configuration
if (!is_int($index)) {
throw new Exception(
"Invalid index on element in the 'memcache_store.servers' configuration option. Perhaps you" .
' have forgotten to add an array(...) around one of the server groups? The invalid index was: ' .
$index
);
}
// make sure that the server object is an array. Each server is an array with name-value pairs
if (!is_array($server)) {
throw new Exception(
'Invalid value for the server with index ' . $index .
'. Remeber that the \'memcache_store.servers\' configuration option' .
' contains an array of arrays of arrays.'
);
}
self::addMemcacheServer($memcache, $server);
}
/** @var \Memcache|Memcached */
return $memcache;
}
/**
* This function gets a list of all configured memcache servers. This list is initialized based
* on the content of 'memcache_store.servers' in the configuration.
*
* @return \Memcache[]|Memcached[] Array with Memcache objects.
*
* @throws Exception If the servers configuration is invalid.
*/
private static function getMemcacheServers()
{
// check if we have loaded the servers already
if (self::$serverGroups != null) {
return self::$serverGroups;
}
// initialize the servers-array
self::$serverGroups = [];
// load the configuration
$config = Configuration::getInstance();
$groups = $config->getArray('memcache_store.servers');
// iterate over all the groups in the 'memcache_store.servers' configuration option
foreach ($groups as $index => $group) {
/*
* Make sure that the group is an array. Each group is an array of servers. Each server is
* an array of name => value pairs for that server.
*/
if (!is_array($group)) {
throw new Exception(
"Invalid value for the server with index " . $index .
". Remeber that the 'memcache_store.servers' configuration option" .
' contains an array of arrays of arrays.'
);
}
// make sure that the group doesn't have an index. An index would be a sign of invalid configuration
if (is_int($index)) {
$index = null;
}
// parse and add this group to the server group list
self::$serverGroups[] = self::loadMemcacheServerGroup($group, $index);
}
return self::$serverGroups;
}
/**
* This is a helper-function which returns the expire value of data
* we should store to the memcache servers.
*
* The value is set depending on the configuration. If no value is
* set in the configuration, then we will use a default value of 0.
* 0 means that the item will never expire.
*
* @return integer The value which should be passed in the set(...) calls to the memcache objects.
*
* @throws Exception If the option 'memcache_store.expires' has a negative value.
*/
private static function getExpireTime()
{
// get the configuration instance
$config = Configuration::getInstance();
assert($config instanceof Configuration);
// get the expire-value from the configuration
$expire = $config->getInteger('memcache_store.expires', 0);
// it must be a positive integer
if ($expire < 0) {
throw new Exception(
"The value of 'memcache_store.expires' in the configuration can't be a negative integer."
);
}
/* If the configuration option is 0, then we should return 0. This allows the user to specify that the data
* shouldn't expire.
*/
if ($expire == 0) {
return 0;
}
/* The expire option is given as the number of seconds into the future an item should expire. We convert this
* to an actual timestamp.
*/
return (time() + $expire);
}
/**
* This function retrieves statistics about all memcache server groups.
*
* @return array Array with the names of each stat and an array with the value for each server group.
*
* @throws Exception If memcache server status couldn't be retrieved.
*/
public static function getStats()
{
$ret = [];
foreach (self::getMemcacheServers() as $sg) {
$stats = method_exists($sg, 'getExtendedStats') ? $sg->getExtendedStats() : $sg->getStats();
foreach ($stats as $server => $data) {
if ($data === false) {
throw new Exception('Failed to get memcache server status.');
}
}
$stats = Utils\Arrays::transpose($stats);
$ret = array_merge_recursive($ret, $stats);
}
return $ret;
}
/**
* Retrieve statistics directly in the form returned by getExtendedStats, for
* all server groups.
*
* @return array An array with the extended stats output for each server group.
*/
public static function getRawStats()
{
$ret = [];
foreach (self::getMemcacheServers() as $sg) {
$stats = method_exists($sg, 'getExtendedStats') ? $sg->getExtendedStats() : $sg->getStats();
$ret[] = $stats;
}
return $ret;
}
}

View File

@ -0,0 +1,416 @@
<?php
namespace SAML\Metadata;
use Exception;
use SAML\Utils\Time;
use SAML2\Constants;
use SAML2\XML\saml\Issuer;
use SAML\Configuration;
use SAML\Error;
use SAML\Logger;
use SAML\Utils;
use SAML\Error\MetadataNotFound;
use SAML\Utils\ClearableState;
/**
* This file defines a class for metadata handling.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
class MetaDataStorageHandler implements ClearableState
{
/**
* This static variable contains a reference to the current
* instance of the metadata handler. This variable will be null if
* we haven't instantiated a metadata handler yet.
*
* @var MetaDataStorageHandler|null
*/
private static $metadataHandler = null;
/**
* This is a list of all the metadata sources we have in our metadata
* chain. When we need metadata, we will look through this chain from start to end.
*
* @var MetaDataStorageSource[]
*/
private $sources;
/**
* This function retrieves the current instance of the metadata handler.
* The metadata handler will be instantiated if this is the first call
* to this function.
*
* @return MetaDataStorageHandler The current metadata handler instance.
*/
public static function getMetadataHandler()
{
if (self::$metadataHandler === null) {
self::$metadataHandler = new MetaDataStorageHandler();
}
return self::$metadataHandler;
}
/**
* This constructor initializes this metadata storage handler. It will load and
* parse the configuration, and initialize the metadata source list.
*/
protected function __construct()
{
$config = Configuration::getInstance();
$sourcesConfig = $config->getArray('metadata.sources', null);
// for backwards compatibility, and to provide a default configuration
if ($sourcesConfig === null) {
$type = $config->getString('metadata.handler', 'flatfile');
$sourcesConfig = [['type' => $type]];
}
try {
$this->sources = MetaDataStorageSource::parseSources($sourcesConfig);
} catch (Exception $e) {
throw new Exception(
"Invalid configuration of the 'metadata.sources' configuration option: " . $e->getMessage()
);
}
}
/**
* This function is used to generate some metadata elements automatically.
*
* @param string $property The metadata property which should be auto-generated.
* @param string $set The set we the property comes from.
*
* @return string The auto-generated metadata property.
* @throws Exception If the metadata cannot be generated automatically.
*/
public function getGenerated($property, $set)
{
// first we check if the user has overridden this property in the metadata
try {
$metadataSet = $this->getMetaDataCurrent($set);
if (array_key_exists($property, $metadataSet)) {
return $metadataSet[$property];
}
} catch (Exception $e) {
// probably metadata wasn't found. In any case we continue by generating the metadata
}
// get the configuration
$config = Configuration::getInstance();
assert($config instanceof Configuration);
$baseurl = Utils\HTTP::getSelfURLHost() . $config->getBasePath();
if ($set == 'saml20-sp-hosted') {
if ($property === 'SingleLogoutServiceBinding') {
return Constants::BINDING_HTTP_REDIRECT;
}
} elseif ($set == 'saml20-idp-hosted') {
switch ($property) {
case 'SingleSignOnService':
return $baseurl . 'saml2/idp/SSOService.php';
case 'SingleSignOnServiceBinding':
return Constants::BINDING_HTTP_REDIRECT;
case 'SingleLogoutService':
return $baseurl . 'saml2/idp/SingleLogoutService.php';
case 'SingleLogoutServiceBinding':
return Constants::BINDING_HTTP_REDIRECT;
}
} elseif ($set == 'shib13-idp-hosted') {
if ($property === 'SingleSignOnService') {
return $baseurl . 'shib13/idp/SSOService.php';
}
}
throw new Exception('Could not generate metadata property ' . $property . ' for set ' . $set . '.');
}
/**
* This function lists all known metadata in the given set. It is returned as an associative array
* where the key is the entity id.
*
* @param string $set The set we want to list metadata from.
* @param bool $showExpired A boolean specifying whether expired entities should be returned
*
* @return array An associative array with the metadata from from the given set.
*/
public function getList($set = 'saml20-idp-remote', $showExpired = false)
{
assert(is_string($set));
$result = [];
foreach ($this->sources as $source) {
$srcList = $source->getMetadataSet($set);
if ($showExpired === false) {
foreach ($srcList as $key => $le) {
if (array_key_exists('expire', $le) && ($le['expire'] < time())) {
unset($srcList[$key]);
Logger::warning(
"Dropping metadata entity " . var_export($key, true) . ", expired " .
Time::generateTimestamp($le['expire']) . "."
);
}
}
}
/* $result is the last argument to array_merge because we want the content already
* in $result to have precedence.
*/
$result = array_merge($srcList, $result);
}
return $result;
}
/**
* This function retrieves metadata for the current entity based on the hostname/path the request
* was directed to. It will throw an exception if it is unable to locate the metadata.
*
* @param string $set The set we want metadata from.
*
* @return array An associative array with the metadata.
*/
public function getMetaDataCurrent($set)
{
return $this->getMetaData(null, $set);
}
/**
* This function locates the current entity id based on the hostname/path combination the user accessed.
* It will throw an exception if it is unable to locate the entity id.
*
* @param string $set The set we look for the entity id in.
* @param string $type Do you want to return the metaindex or the entityID. [entityid|metaindex]
*
* @return string The entity id which is associated with the current hostname/path combination.
* @throws Exception If no default metadata can be found in the set for the current host.
*/
public function getMetaDataCurrentEntityID($set, $type = 'entityid')
{
assert(is_string($set));
// first we look for the hostname/path combination
$currenthostwithpath = Utils\HTTP::getSelfHostWithPath(); // sp.example.org/university
foreach ($this->sources as $source) {
$index = $source->getEntityIdFromHostPath($currenthostwithpath, $set, $type);
if ($index !== null) {
return $index;
}
}
// then we look for the hostname
$currenthost = Utils\HTTP::getSelfHost(); // sp.example.org
foreach ($this->sources as $source) {
$index = $source->getEntityIdFromHostPath($currenthost, $set, $type);
if ($index !== null) {
return $index;
}
}
// then we look for the DEFAULT entry
foreach ($this->sources as $source) {
$entityId = $source->getEntityIdFromHostPath('__DEFAULT__', $set, $type);
if ($entityId !== null) {
return $entityId;
}
}
// we were unable to find the hostname/path in any metadata source
throw new Exception(
'Could not find any default metadata entities in set [' . $set . '] for host [' . $currenthost . ' : ' .
$currenthostwithpath . ']'
);
}
/**
* This method will call getPreferredEntityIdFromCIDRhint() on all of the
* sources.
*
* @param string $set Which set of metadata we are looking it up in.
* @param string $ip IP address
*
* @return string|null The entity id of a entity which have a CIDR hint where the provided
* IP address match.
*/
public function getPreferredEntityIdFromCIDRhint($set, $ip)
{
foreach ($this->sources as $source) {
$entityId = $source->getPreferredEntityIdFromCIDRhint($set, $ip);
if ($entityId !== null) {
return $entityId;
}
}
return null;
}
/**
* This function loads the metadata for entity IDs in $entityIds. It is returned as an associative array
* where the key is the entity id. An empty array may be returned if no matching entities were found
* @param array $entityIds The entity ids to load
* @param string $set The set we want to get metadata from.
* @return array An associative array with the metadata for the requested entities, if found.
*/
public function getMetaDataForEntities(array $entityIds, $set)
{
$result = [];
foreach ($this->sources as $source) {
$srcList = $source->getMetaDataForEntities($entityIds, $set);
foreach ($srcList as $key => $le) {
if (array_key_exists('expire', $le)) {
if ($le['expire'] < time()) {
unset($srcList[$key]);
Logger::warning(
"Dropping metadata entity " . var_export($key, true) . ", expired " .
Time::generateTimestamp($le['expire']) . "."
);
continue;
}
}
// We found the entity id so remove it from the list that needs resolving
unset($entityIds[array_search($key, $entityIds)]);
}
$result = array_merge($srcList, $result);
}
return $result;
}
/**
* This function looks up the metadata for the given entity id in the given set. It will throw an
* exception if it is unable to locate the metadata.
*
* @param string|null $index The entity id we are looking up. This parameter may be NULL, in which case we look up
* the current entity id based on the current hostname/path.
* @param string $set The set of metadata we are looking up the entity id in.
*
* @return array The metadata array describing the specified entity.
* @throws Exception If metadata for the specified entity is expired.
* @throws MetadataNotFound If no metadata for the entity specified can be found.
*/
public function getMetaData($index, $set)
{
assert(is_string($set));
if ($index === null) {
$index = $this->getMetaDataCurrentEntityID($set, 'metaindex');
}
assert(is_string($index));
foreach ($this->sources as $source) {
$metadata = $source->getMetaData($index, $set);
if ($metadata !== null) {
if (array_key_exists('expire', $metadata)) {
if ($metadata['expire'] < time()) {
throw new Exception(
'Metadata for the entity [' . $index . '] expired ' .
(time() - $metadata['expire']) . ' seconds ago.'
);
}
}
$metadata['metadata-index'] = $index;
$metadata['metadata-set'] = $set;
assert(array_key_exists('entityid', $metadata));
return $metadata;
}
}
throw new MetadataNotFound($index);
}
/**
* Retrieve the metadata as a configuration object.
*
* This function will throw an exception if it is unable to locate the metadata.
*
* @param string $entityId The entity ID we are looking up.
* @param string $set The metadata set we are searching.
*
* @return Configuration The configuration object representing the metadata.
* @throws MetadataNotFound If no metadata for the entity specified can be found.
*/
public function getMetaDataConfig($entityId, $set)
{
assert(is_string($entityId));
assert(is_string($set));
$metadata = $this->getMetaData($entityId, $set);
return Configuration::loadFromArray($metadata, $set . '/' . var_export($entityId, true));
}
/**
* Search for an entity's metadata, given the SHA1 digest of its entity ID.
*
* @param string $sha1 The SHA1 digest of the entity ID.
* @param string $set The metadata set we are searching.
*
* @return null|Configuration The metadata corresponding to the entity, or null if the entity cannot be
* found.
*/
public function getMetaDataConfigForSha1($sha1, $set)
{
assert(is_string($sha1));
assert(is_string($set));
$result = [];
foreach ($this->sources as $source) {
$srcList = $source->getMetadataSet($set);
/* $result is the last argument to array_merge because we want the content already
* in $result to have precedence.
*/
$result = array_merge($srcList, $result);
}
foreach ($result as $remote_provider) {
if (sha1($remote_provider['entityid']) == $sha1) {
$remote_provider['metadata-set'] = $set;
return Configuration::loadFromArray(
$remote_provider,
$set . '/' . var_export($remote_provider['entityid'], true)
);
}
}
return null;
}
/**
* Clear any metadata cached.
* Allows for metadata configuration to be changed and reloaded during a given request. Most useful
* when running phpunit tests and needing to alter config.php and metadata sources between test cases
* @return void
*/
public static function clearInternalState()
{
self::$metadataHandler = null;
}
}

View File

@ -0,0 +1,128 @@
<?php
namespace SAML\Metadata;
use Exception;
use SAML\Configuration;
/**
* This file defines a flat file metadata source.
* Instantiation of session handler objects should be done through
* the class method getMetadataHandler().
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
class MetaDataStorageHandlerFlatFile extends MetaDataStorageSource
{
/**
* This is the directory we will load metadata files from. The path will always end
* with a '/'.
*
* @var string
*/
private $directory = '/';
/**
* This is an associative array which stores the different metadata sets we have loaded.
*
* @var array
*/
private $cachedMetadata = [];
/**
* This constructor initializes the flatfile metadata storage handler with the
* specified configuration. The configuration is an associative array with the following
* possible elements:
* - 'directory': The directory we should load metadata from. The default directory is
* set in the 'metadatadir' configuration option in 'config.php'.
*
* @param array $config An associative array with the configuration for this handler.
*/
protected function __construct($config)
{
assert(is_array($config));
// get the configuration
$globalConfig = Configuration::getInstance();
// find the path to the directory we should search for metadata in
if (array_key_exists('directory', $config)) {
$this->directory = $config['directory'] ?: 'metadata/';
} else {
$this->directory = $globalConfig->getString('metadatadir', 'metadata/');
}
/* Resolve this directory relative to the SimpleSAMLphp directory (unless it is
* an absolute path).
*/
/** @var string $base */
$base = $globalConfig->resolvePath($this->directory);
$this->directory = $base . '/';
}
/**
* This function loads the given set of metadata from a file our metadata directory.
* This function returns null if it is unable to locate the given set in the metadata directory.
*
* @param string $set The set of metadata we are loading.
*
* @return array|null An associative array with the metadata,
* or null if we are unable to load metadata from the given file.
* @throws Exception If the metadata set cannot be loaded.
*/
private function load($set)
{
$metadatasetfile = $this->directory . $set . '.php';
if (!file_exists($metadatasetfile)) {
return null;
}
$metadata = [];
include($metadatasetfile);
if (!is_array($metadata)) {
throw new Exception('Could not load metadata set [' . $set . '] from file: ' . $metadatasetfile);
}
return $metadata;
}
/**
* This function retrieves the given set of metadata. It will return an empty array if it is
* unable to locate it.
*
* @param string $set The set of metadata we are retrieving.
*
* @return array An associative array with the metadata. Each element in the array is an entity, and the
* key is the entity id.
*/
public function getMetadataSet($set)
{
if (array_key_exists($set, $this->cachedMetadata)) {
return $this->cachedMetadata[$set];
}
/** @var array|null $metadataSet */
$metadataSet = $this->load($set);
if ($metadataSet === null) {
$metadataSet = [];
}
// add the entity id of an entry to each entry in the metadata
foreach ($metadataSet as $entityId => &$entry) {
$entry = $this->updateEntityID($set, $entityId, $entry);
}
$this->cachedMetadata[$set] = $metadataSet;
return $metadataSet;
}
}

View File

@ -0,0 +1,313 @@
<?php
namespace SAML\Metadata;
use SAML\Database;
use SAML\Error;
use SAML\Error\Exception;
/**
* Class for handling metadata files stored in a database.
*
* This class has been based off a previous version written by
* mooknarf@gmail.com and patched to work with the latest version
* of SimpleSAMLphp
*
* @package SimpleSAMLphp
*/
class MetaDataStorageHandlerPdo extends MetaDataStorageSource
{
/**
* The PDO object
*/
private $db;
/**
* Prefix to apply to the metadata table
*/
private $tablePrefix;
/**
* This is an associative array which stores the different metadata sets we have loaded.
*/
private $cachedMetadata = [];
/**
* All the metadata sets supported by this MetaDataStorageHandler
*/
public $supportedSets = [
'adfs-idp-hosted',
'adfs-sp-remote',
'saml20-idp-hosted',
'saml20-idp-remote',
'saml20-sp-remote',
'shib13-idp-hosted',
'shib13-idp-remote',
'shib13-sp-hosted',
'shib13-sp-remote'
];
/**
* This constructor initializes the PDO metadata storage handler with the specified
* configuration. The configuration is an associative array with the following
* possible elements (set in config.php):
* - 'usePersistentConnection': TRUE/FALSE if database connection should be persistent.
* - 'dsn': The database connection string.
* - 'username': Database user name
* - 'password': Password for the database user.
*
* @param array $config An associative array with the configuration for this handler.
*/
public function __construct($config)
{
assert(is_array($config));
$this->db = Database::getInstance();
}
/**
* This function loads the given set of metadata from a file to a configured database.
* This function returns NULL if it is unable to locate the given set in the metadata directory.
*
* @param string $set The set of metadata we are loading.
*
* @return array|null $metadata Associative array with the metadata, or NULL if we are unable to load
* metadata from the given file.
*
* @throws \Exception If a database error occurs.
* @throws Exception If the metadata can be retrieved from the database, but cannot be decoded.
*/
private function load($set)
{
assert(is_string($set));
$tableName = $this->getTableName($set);
if (!in_array($set, $this->supportedSets, true)) {
return null;
}
$stmt = $this->db->read("SELECT entity_id, entity_data FROM $tableName");
if ($stmt->execute()) {
$metadata = [];
while ($d = $stmt->fetch()) {
$data = json_decode($d['entity_data'], true);
if ($data === null) {
throw new Exception("Cannot decode metadata for entity '${d['entity_id']}'");
}
if (!array_key_exists('entityid', $data)) {
$data['entityid'] = $d['entity_id'];
}
$metadata[$d['entity_id']] = $data;
}
return $metadata;
} else {
throw new \Exception(
'PDO metadata handler: Database error: ' . var_export($this->db->getLastError(), true)
);
}
}
/**
* Retrieve a list of all available metadata for a given set.
*
* @param string $set The set we are looking for metadata in.
*
* @return array $metadata An associative array with all the metadata for the given set.
*/
public function getMetadataSet($set)
{
assert(is_string($set));
if (array_key_exists($set, $this->cachedMetadata)) {
return $this->cachedMetadata[$set];
}
$metadataSet = $this->load($set);
if ($metadataSet === null) {
$metadataSet = [];
}
/** @var array $metadataSet */
foreach ($metadataSet as $entityId => &$entry) {
$entry = $this->updateEntityID($set, $entityId, $entry);
}
$this->cachedMetadata[$set] = $metadataSet;
return $metadataSet;
}
/**
* Retrieve a metadata entry.
*
* @param string $entityId The entityId we are looking up.
* @param string $set The set we are looking for metadata in.
*
* @return array|null An associative array with metadata for the given entity, or NULL if we are unable to
* locate the entity.
*/
public function getMetaData($entityId, $set)
{
assert(is_string($entityId));
assert(is_string($set));
// validate the metadata set is valid
if (!in_array($set, $this->supportedSets, true)) {
return null;
}
// support caching
if (isset($this->cachedMetadata[$entityId][$set])) {
return $this->cachedMetadata[$entityId][$set];
}
$tableName = $this->getTableName($set);
// according to the docs, it looks like *-idp-hosted metadata are the types
// that allow the __DYNAMIC:*__ entity id. with the current table design
// we need to lookup the specific metadata entry but also we need to lookup
// any dynamic entries to see if the dynamic hosted entity id matches
if (substr($set, -10) == 'idp-hosted') {
$stmt = $this->db->read(
"SELECT entity_id, entity_data FROM {$tableName} "
. "WHERE (entity_id LIKE :dynamicId OR entity_id = :entityId)",
['dynamicId' => '__DYNAMIC%', 'entityId' => $entityId]
);
} else {
// other metadata types should be able to match on entity id
$stmt = $this->db->read(
"SELECT entity_id, entity_data FROM {$tableName} WHERE entity_id = :entityId",
['entityId' => $entityId]
);
}
// throw pdo exception upon execution failure
if (!$stmt->execute()) {
throw new \Exception(
'PDO metadata handler: Database error: ' . var_export($this->db->getLastError(), true)
);
}
// load the metadata into an array
$metadataSet = [];
while ($d = $stmt->fetch()) {
$data = json_decode($d['entity_data'], true);
if (json_last_error() != JSON_ERROR_NONE) {
throw new Exception(
"Cannot decode metadata for entity '${d['entity_id']}'"
);
}
// update the entity id to either the key (if not dynamic or generate the dynamic hosted url)
$metadataSet[$d['entity_id']] = $this->updateEntityID($set, $entityId, $data);
}
$indexLookup = $this->lookupIndexFromEntityId($entityId, $metadataSet);
if (isset($indexLookup) && array_key_exists($indexLookup, $metadataSet)) {
$this->cachedMetadata[$indexLookup][$set] = $metadataSet[$indexLookup];
return $this->cachedMetadata[$indexLookup][$set];
}
return null;
}
/**
* Add metadata to the configured database
*
* @param string $index Entity ID
* @param string $set The set to add the metadata to
* @param array $entityData Metadata
*
* @return bool True/False if entry was successfully added
*/
public function addEntry($index, $set, $entityData)
{
assert(is_string($index));
assert(is_string($set));
assert(is_array($entityData));
if (!in_array($set, $this->supportedSets, true)) {
return false;
}
$tableName = $this->getTableName($set);
$metadata = $this->db->read(
"SELECT entity_id, entity_data FROM $tableName WHERE entity_id = :entity_id",
[
'entity_id' => $index,
]
);
$retrivedEntityIDs = $metadata->fetch();
$params = [
'entity_id' => $index,
'entity_data' => json_encode($entityData),
];
if ($retrivedEntityIDs !== false && count($retrivedEntityIDs) > 0) {
$rows = $this->db->write(
"UPDATE $tableName SET entity_data = :entity_data WHERE entity_id = :entity_id",
$params
);
} else {
$rows = $this->db->write(
"INSERT INTO $tableName (entity_id, entity_data) VALUES (:entity_id, :entity_data)",
$params
);
}
return $rows === 1;
}
/**
* Replace the -'s to an _ in table names for Metadata sets
* since SQL does not allow a - in a table name.
*
* @param string $table Table
*
* @return string Replaced table name
*/
private function getTableName($table)
{
assert(is_string($table));
return $this->db->applyPrefix(str_replace("-", "_", $this->tablePrefix . $table));
}
/**
* Initialize the configured database
*
* @return int|false The number of SQL statements successfully executed, false if some error occurred.
*/
public function initDatabase()
{
$stmt = 0;
$fine = true;
foreach ($this->supportedSets as $set) {
$tableName = $this->getTableName($set);
$rows = $this->db->write(
"CREATE TABLE IF NOT EXISTS $tableName (entity_id VARCHAR(255) PRIMARY KEY NOT NULL, entity_data " .
"TEXT NOT NULL)"
);
if ($rows === false) {
$fine = false;
} else {
$stmt += $rows;
}
}
if (!$fine) {
return false;
}
return $stmt;
}
}

View File

@ -0,0 +1,311 @@
<?php
namespace SAML\Metadata;
use SAML\Configuration;
use SAML\Logger;
use SAML\Utils;
/**
* Class for handling metadata files in serialized format.
*
* @package SimpleSAMLphp
*/
class MetaDataStorageHandlerSerialize extends MetaDataStorageSource
{
/**
* The file extension we use for our metadata files.
*
* @var string
*/
const EXTENSION = '.serialized';
/**
* The base directory where metadata is stored.
*
* @var string
*/
private $directory = '/';
/**
* Constructor for this metadata handler.
*
* Parses configuration.
*
* @param array $config The configuration for this metadata handler.
*/
public function __construct($config)
{
assert(is_array($config));
$globalConfig = Configuration::getInstance();
$cfgHelp = Configuration::loadFromArray($config, 'serialize metadata source');
$this->directory = $cfgHelp->getString('directory');
/* Resolve this directory relative to the SimpleSAMLphp directory (unless it is
* an absolute path).
*/
$this->directory = Utils\System::resolvePath($this->directory, $globalConfig->getBaseDir());
}
/**
* Helper function for retrieving the path of a metadata file.
*
* @param string $entityId The entity ID.
* @param string $set The metadata set.
*
* @return string The path to the metadata file.
*/
private function getMetadataPath($entityId, $set)
{
assert(is_string($entityId));
assert(is_string($set));
return $this->directory . '/' . rawurlencode($set) . '/' . rawurlencode($entityId) . self::EXTENSION;
}
/**
* Retrieve a list of all available metadata sets.
*
* @return array An array with the available sets.
*/
public function getMetadataSets()
{
$ret = [];
$dh = @opendir($this->directory);
if ($dh === false) {
Logger::warning(
'Serialize metadata handler: Unable to open directory: ' . var_export($this->directory, true)
);
return $ret;
}
while (($entry = readdir($dh)) !== false) {
if ($entry[0] === '.') {
// skip '..', '.' and hidden files
continue;
}
$path = $this->directory . '/' . $entry;
if (!is_dir($path)) {
Logger::warning(
'Serialize metadata handler: Metadata directory contained a file where only directories should ' .
'exist: ' . var_export($path, true)
);
continue;
}
$ret[] = rawurldecode($entry);
}
closedir($dh);
return $ret;
}
/**
* Retrieve a list of all available metadata for a given set.
*
* @param string $set The set we are looking for metadata in.
*
* @return array An associative array with all the metadata for the given set.
*/
public function getMetadataSet($set)
{
assert(is_string($set));
$ret = [];
$dir = $this->directory . '/' . rawurlencode($set);
if (!is_dir($dir)) {
// probably some code asked for a metadata set which wasn't available
return $ret;
}
$dh = @opendir($dir);
if ($dh === false) {
Logger::warning(
'Serialize metadata handler: Unable to open directory: ' . var_export($dir, true)
);
return $ret;
}
$extLen = strlen(self::EXTENSION);
while (($file = readdir($dh)) !== false) {
if (strlen($file) <= $extLen) {
continue;
}
if (substr($file, -$extLen) !== self::EXTENSION) {
continue;
}
$entityId = substr($file, 0, -$extLen);
$entityId = rawurldecode($entityId);
$md = $this->getMetaData($entityId, $set);
if ($md !== null) {
$ret[$entityId] = $md;
}
}
closedir($dh);
return $ret;
}
/**
* Retrieve a metadata entry.
*
* @param string $entityId The entityId we are looking up.
* @param string $set The set we are looking for metadata in.
*
* @return array|null An associative array with metadata for the given entity, or NULL if we are unable to
* locate the entity.
*/
public function getMetaData($entityId, $set)
{
assert(is_string($entityId));
assert(is_string($set));
$filePath = $this->getMetadataPath($entityId, $set);
if (!file_exists($filePath)) {
return null;
}
$data = @file_get_contents($filePath);
if ($data === false) {
/** @var array $error */
$error = error_get_last();
Logger::warning(
'Error reading file ' . $filePath . ': ' . $error['message']
);
return null;
}
$data = @unserialize($data);
if ($data === false) {
Logger::warning('Error unserializing file: ' . $filePath);
return null;
}
if (!array_key_exists('entityid', $data)) {
$data['entityid'] = $entityId;
}
return $data;
}
/**
* Save a metadata entry.
*
* @param string $entityId The entityId of the metadata entry.
* @param string $set The metadata set this metadata entry belongs to.
* @param array $metadata The metadata.
*
* @return bool True if successfully saved, false otherwise.
*/
public function saveMetadata($entityId, $set, $metadata)
{
assert(is_string($entityId));
assert(is_string($set));
assert(is_array($metadata));
$filePath = $this->getMetadataPath($entityId, $set);
$newPath = $filePath . '.new';
$dir = dirname($filePath);
if (!is_dir($dir)) {
Logger::info('Creating directory: ' . $dir);
$res = @mkdir($dir, 0777, true);
if ($res === false) {
/** @var array $error */
$error = error_get_last();
Logger::error('Failed to create directory ' . $dir . ': ' . $error['message']);
return false;
}
}
$data = serialize($metadata);
Logger::debug('Writing: ' . $newPath);
$res = file_put_contents($newPath, $data);
if ($res === false) {
/** @var array $error */
$error = error_get_last();
Logger::error('Error saving file ' . $newPath . ': ' . $error['message']);
return false;
}
$res = rename($newPath, $filePath);
if ($res === false) {
/** @var array $error */
$error = error_get_last();
Logger::error('Error renaming ' . $newPath . ' to ' . $filePath . ': ' . $error['message']);
return false;
}
return true;
}
/**
* Delete a metadata entry.
*
* @param string $entityId The entityId of the metadata entry.
* @param string $set The metadata set this metadata entry belongs to.
* @return void
*/
public function deleteMetadata($entityId, $set)
{
assert(is_string($entityId));
assert(is_string($set));
$filePath = $this->getMetadataPath($entityId, $set);
if (!file_exists($filePath)) {
Logger::warning(
'Attempted to erase nonexistent metadata entry ' .
var_export($entityId, true) . ' in set ' . var_export($set, true) . '.'
);
return;
}
$res = unlink($filePath);
if ($res === false) {
/** @var array $error */
$error = error_get_last();
Logger::error(
'Failed to delete file ' . $filePath .
': ' . $error['message']
);
}
}
/**
* This function loads the metadata for entity IDs in $entityIds. It is returned as an associative array
* where the key is the entity id. An empty array may be returned if no matching entities were found
* @param array $entityIds The entity ids to load
* @param string $set The set we want to get metadata from.
* @return array An associative array with the metadata for the requested entities, if found.
*/
public function getMetaDataForEntities(array $entityIds, $set)
{
return $this->getMetaDataForEntitiesIndividually($entityIds, $set);
}
}

View File

@ -0,0 +1,120 @@
<?php
namespace SAML\Metadata;
use Exception;
use SAML\Configuration;
/**
* This class implements a metadata source which loads metadata from XML files.
* The XML files should be in the SAML 2.0 metadata format.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class MetaDataStorageHandlerXML extends MetaDataStorageSource
{
/**
* This variable contains an associative array with the parsed metadata.
*
* @var array
*/
private $metadata;
/**
* This function initializes the XML metadata source. The configuration must contain one of
* the following options:
* - 'file': Path to a file with the metadata. This path is relative to the SimpleSAMLphp
* base directory.
* - 'url': URL we should download the metadata from. This is only meant for testing.
*
* @param array $config The configuration for this instance of the XML metadata source.
*
* @throws Exception If neither the 'file' or 'url' options are defined in the configuration.
*/
protected function __construct($config)
{
$src = $srcXml = null;
if (array_key_exists('file', $config)) {
// get the configuration
$globalConfig = Configuration::getInstance();
$src = $globalConfig->resolvePath($config['file']);
} elseif (array_key_exists('url', $config)) {
$src = $config['url'];
} elseif (array_key_exists('xml', $config)) {
$srcXml = $config['xml'];
} else {
throw new Exception("Missing one of 'file', 'url' and 'xml' in XML metadata source configuration.");
}
$SP1x = [];
$IdP1x = [];
$SP20 = [];
$IdP20 = [];
$AAD = [];
if (isset($src)) {
$entities = SAMLParser::parseDescriptorsFile($src);
} elseif (isset($srcXml)) {
$entities = SAMLParser::parseDescriptorsString($srcXml);
} else {
throw new Exception("Neither source file path/URI nor string data provided");
}
foreach ($entities as $entityId => $entity) {
$md = $entity->getMetadata1xSP();
if ($md !== null) {
$SP1x[$entityId] = $md;
}
$md = $entity->getMetadata1xIdP();
if ($md !== null) {
$IdP1x[$entityId] = $md;
}
$md = $entity->getMetadata20SP();
if ($md !== null) {
$SP20[$entityId] = $md;
}
$md = $entity->getMetadata20IdP();
if ($md !== null) {
$IdP20[$entityId] = $md;
}
$md = $entity->getAttributeAuthorities();
if (count($md) > 0) {
$AAD[$entityId] = $md[0];
}
}
$this->metadata = [
'shib13-sp-remote' => $SP1x,
'shib13-idp-remote' => $IdP1x,
'saml20-sp-remote' => $SP20,
'saml20-idp-remote' => $IdP20,
'attributeauthority-remote' => $AAD,
];
}
/**
* This function returns an associative array with metadata for all entities in the given set. The
* key of the array is the entity id.
*
* @param string $set The set we want to list metadata for.
*
* @return array An associative array with all entities in the given set.
*/
public function getMetadataSet($set)
{
if (array_key_exists($set, $this->metadata)) {
return $this->metadata[$set];
}
// we don't have this metadata set
return [];
}
}

View File

@ -0,0 +1,380 @@
<?php
namespace SAML\Metadata;
use Exception;
use SAML\Error;
use SAML\Module;
use SAML\Utils;
/**
* This abstract class defines an interface for metadata storage sources.
*
* It also contains the overview of the different metadata storage sources.
* A metadata storage source can be loaded by passing the configuration of it
* to the getSource static function.
*
* @author Olav Morken, UNINETT AS.
* @author Andreas Aakre Solberg, UNINETT AS.
* @package SimpleSAMLphp
*/
abstract class MetaDataStorageSource
{
/**
* Parse array with metadata sources.
*
* This function accepts an array with metadata sources, and returns an array with
* each metadata source as an object.
*
* @param array $sourcesConfig Array with metadata source configuration.
*
* @return array Parsed metadata configuration.
*
* @throws Exception If something is wrong in the configuration.
*/
public static function parseSources($sourcesConfig)
{
assert(is_array($sourcesConfig));
$sources = [];
foreach ($sourcesConfig as $sourceConfig) {
if (!is_array($sourceConfig)) {
throw new Exception("Found an element in metadata source configuration which wasn't an array.");
}
$sources[] = self::getSource($sourceConfig);
}
return $sources;
}
/**
* This function creates a metadata source based on the given configuration.
* The type of source is based on the 'type' parameter in the configuration.
* The default type is 'flatfile'.
*
* @param array $sourceConfig Associative array with the configuration for this metadata source.
*
* @return MetaDataStorageSource An instance of a metadata source with the given configuration.
*
* @throws Exception If the metadata source type is invalid.
*/
public static function getSource($sourceConfig)
{
assert(is_array($sourceConfig));
if (array_key_exists('type', $sourceConfig)) {
$type = $sourceConfig['type'];
} else {
$type = 'flatfile';
}
switch ($type) {
case 'flatfile':
return new MetaDataStorageHandlerFlatFile($sourceConfig);
case 'xml':
return new MetaDataStorageHandlerXML($sourceConfig);
case 'serialize':
return new MetaDataStorageHandlerSerialize($sourceConfig);
case 'mdx':
case 'mdq':
return new Sources\MDQ($sourceConfig);
case 'pdo':
return new MetaDataStorageHandlerPdo($sourceConfig);
default:
// metadata store from module
try {
$className = Module::resolveClass(
$type,
'MetadataStore',
'\SimpleSAML\Metadata\MetaDataStorageSource'
);
} catch (Exception $e) {
throw new Error\CriticalConfigurationError(
"Invalid 'type' for metadata source. Cannot find store '$type'.",
null
);
}
/** @var MetaDataStorageSource */
return new $className($sourceConfig);
}
}
/**
* This function attempts to generate an associative array with metadata for all entities in the
* given set. The key of the array is the entity id.
*
* A subclass should override this function if it is able to easily generate this list.
*
* @param string $set The set we want to list metadata for.
*
* @return array An associative array with all entities in the given set, or an empty array if we are
* unable to generate this list.
*/
public function getMetadataSet($set)
{
return [];
}
/**
* This function resolves an host/path combination to an entity id.
*
* This class implements this function using the getMetadataSet-function. A subclass should
* override this function if it doesn't implement the getMetadataSet function, or if the
* implementation of getMetadataSet is slow.
*
* @param string $hostPath The host/path combination we are looking up.
* @param string $set Which set of metadata we are looking it up in.
* @param string $type Do you want to return the metaindex or the entityID. [entityid|metaindex]
*
* @return string|null An entity id which matches the given host/path combination, or NULL if
* we are unable to locate one which matches.
*/
public function getEntityIdFromHostPath($hostPath, $set, $type = 'entityid')
{
$metadataSet = $this->getMetadataSet($set);
/** @psalm-suppress DocblockTypeContradiction */
if ($metadataSet === null) {
// this metadata source does not have this metadata set
return null;
}
foreach ($metadataSet as $index => $entry) {
if (!array_key_exists('host', $entry)) {
continue;
}
if ($hostPath === $entry['host']) {
if ($type === 'entityid') {
return $entry['entityid'];
} else {
return $index;
}
}
}
// no entries matched, we should return null
return null;
}
/**
* This function will go through all the metadata, and check the DiscoHints->IPHint
* parameter, which defines a network space (ip range) for each remote entry.
* This function returns the entityID for any of the entities that have an
* IP range which the IP falls within.
*
* @param string $set Which set of metadata we are looking it up in.
* @param string $ip IP address
* @param string $type Do you want to return the metaindex or the entityID. [entityid|metaindex]
*
* @return string|null The entity id of a entity which have a CIDR hint where the provided
* IP address match.
*/
public function getPreferredEntityIdFromCIDRhint($set, $ip, $type = 'entityid')
{
$metadataSet = $this->getMetadataSet($set);
foreach ($metadataSet as $index => $entry) {
$cidrHints = [];
// support hint.cidr for idp discovery
if (array_key_exists('hint.cidr', $entry) && is_array($entry['hint.cidr'])) {
$cidrHints = $entry['hint.cidr'];
}
// support discohints in idp metadata for idp discovery
if (
array_key_exists('DiscoHints', $entry)
&& array_key_exists('IPHint', $entry['DiscoHints'])
&& is_array($entry['DiscoHints']['IPHint'])
) {
// merge with hints derived from discohints, but prioritize hint.cidr in case it is used
$cidrHints = array_merge($entry['DiscoHints']['IPHint'], $cidrHints);
}
if (empty($cidrHints)) {
continue;
}
foreach ($cidrHints as $hint_entry) {
if (Utils\Net::ipCIDRcheck($hint_entry, $ip)) {
if ($type === 'entityid') {
return $entry['entityid'];
} else {
return $index;
}
}
}
}
// no entries matched, we should return null
return null;
}
/**
* This function retrieves metadata for the given entity id in the given set of metadata.
* It will return NULL if it is unable to locate the metadata.
*
* This class implements this function using the getMetadataSet-function. A subclass should
* override this function if it doesn't implement the getMetadataSet function, or if the
* implementation of getMetadataSet is slow.
*
* @param string $index The entityId or metaindex we are looking up.
* @param string $set The set we are looking for metadata in.
*
* @return array|null An associative array with metadata for the given entity, or NULL if we are unable to
* locate the entity.
*/
public function getMetaData($index, $set)
{
assert(is_string($index));
assert(isset($set));
$metadataSet = $this->getMetadataSet($set);
$indexLookup = $this->lookupIndexFromEntityId($index, $metadataSet);
if (isset($indexLookup) && array_key_exists($indexLookup, $metadataSet)) {
return $metadataSet[$indexLookup];
}
return null;
}
/**
* This function loads the metadata for entity IDs in $entityIds. It is returned as an associative array
* where the key is the entity id. An empty array may be returned if no matching entities were found.
* Subclasses should override if their getMetadataSet returns nothing or is slow. Subclasses may want to
* delegate to getMetaDataForEntitiesIndividually if loading entities one at a time is faster.
* @param array $entityIds The entity ids to load
* @param string $set The set we want to get metadata from.
* @return array An associative array with the metadata for the requested entities, if found.
*/
public function getMetaDataForEntities(array $entityIds, $set)
{
if (count($entityIds) === 1) {
return $this->getMetaDataForEntitiesIndividually($entityIds, $set);
}
$entities = $this->getMetadataSet($set);
return array_intersect_key($entities, array_flip($entityIds));
}
/**
* Loads metadata entities one at a time, rather than the default implementation of loading all entities
* and filtering.
* @see MetaDataStorageSource::getMetaDataForEntities()
* @param array $entityIds The entity ids to load
* @param string $set The set we want to get metadata from.
* @return array An associative array with the metadata for the requested entities, if found.
*/
protected function getMetaDataForEntitiesIndividually(array $entityIds, $set)
{
$entities = [];
foreach ($entityIds as $entityId) {
$metadata = $this->getMetaData($entityId, $set);
if ($metadata !== null) {
$entities[$entityId] = $metadata;
}
}
return $entities;
}
/**
* This method returns the full metadata set for a given entity id or null if the entity id cannot be found
* in the given metadata set.
*
* @param string $entityId
* @param array $metadataSet the already loaded metadata set
* @return mixed|null
*/
protected function lookupIndexFromEntityId($entityId, array $metadataSet)
{
assert(is_string($entityId));
assert(is_array($metadataSet));
// check for hostname
$currentHost = Utils\HTTP::getSelfHost(); // sp.example.org
foreach ($metadataSet as $index => $entry) {
// explicit index match
if ($index === $entityId) {
return $index;
}
if ($entry['entityid'] === $entityId) {
if ($entry['host'] === '__DEFAULT__' || $entry['host'] === $currentHost) {
return $index;
}
}
}
return null;
}
/**
* @param string $set
* @throws Exception
* @return string
*/
private function getDynamicHostedUrl($set)
{
assert(is_string($set));
// get the configuration
$baseUrl = Utils\HTTP::getBaseURL();
if ($set === 'saml20-idp-hosted') {
return $baseUrl . 'saml2/idp/metadata.php';
} elseif ($set === 'saml20-sp-hosted') {
return $baseUrl . 'saml2/sp/metadata.php';
} elseif ($set === 'shib13-idp-hosted') {
return $baseUrl . 'shib13/idp/metadata.php';
} elseif ($set === 'shib13-sp-hosted') {
return $baseUrl . 'shib13/sp/metadata.php';
} elseif ($set === 'adfs-idp-hosted') {
return 'urn:federation:' . Utils\HTTP::getSelfHost() . ':idp';
} else {
throw new Exception('Can not generate dynamic EntityID for metadata of this type: [' . $set . ']');
}
}
/**
* Updates the metadata entry's entity id and returns the modified array. If the entity id is __DYNAMIC:*__ a
* the current url is assigned. If it is explicit the entityid array key is updated to the entityId that was
* provided.
*
* @param string $metadataSet a metadata set (saml20-idp-hosted, saml20-sp-remote, etc)
* @param string $entityId the entity id we are modifying
* @param array $metadataEntry the fully populated metadata entry
* @return array modified metadata to include the valid entityid
*
* @throws Exception
*/
protected function updateEntityID($metadataSet, $entityId, array $metadataEntry)
{
assert(is_string($metadataSet));
assert(is_string($entityId));
assert(is_array($metadataEntry));
$modifiedMetadataEntry = $metadataEntry;
// generate a dynamic hosted url
if (preg_match('/__DYNAMIC(:[0-9]+)?__/', $entityId)) {
$modifiedMetadataEntry['entityid'] = $this->getDynamicHostedUrl($metadataSet);
} else {
// set the entityid metadata array key to the provided entity id
$modifiedMetadataEntry['entityid'] = $entityId;
}
return $modifiedMetadataEntry;
}
}

View File

@ -0,0 +1,835 @@
<?php
namespace SAML\Metadata;
use DOMElement;
use SAML2\Constants;
use SAML2\XML\md\AttributeAuthorityDescriptor;
use SAML2\XML\md\AttributeConsumingService;
use SAML2\XML\md\ContactPerson;
use SAML2\XML\md\EndpointType;
use SAML2\XML\md\EntityDescriptor;
use SAML2\XML\md\IDPSSODescriptor;
use SAML2\XML\md\IndexedEndpointType;
use SAML2\XML\md\Organization;
use SAML2\XML\md\RequestedAttribute;
use SAML2\XML\md\RoleDescriptor;
use SAML2\XML\md\SPSSODescriptor;
use SAML2\XML\mdattr\EntityAttributes;
use SAML2\XML\mdrpi\RegistrationInfo;
use SAML2\XML\mdui\DiscoHints;
use SAML2\XML\mdui\Keywords;
use SAML2\XML\mdui\Logo;
use SAML2\XML\mdui\UIInfo;
use SAML2\XML\saml\Attribute;
use SAML2\XML\saml\AttributeValue;
use SAML2\XML\shibmd\Scope;
use SAML\Configuration;
use SAML\Logger;
use SAML\Module\adfs\SAML2\XML\fed\SecurityTokenServiceType;
use SAML\Utils;
/**
* Class for generating SAML 2.0 metadata from SimpleSAMLphp metadata arrays.
*
* This class builds SAML 2.0 metadata for an entity by examining the metadata for the entity.
*
* @package SimpleSAMLphp
*/
class SAMLBuilder
{
/**
* The EntityDescriptor we are building.
*
* @var EntityDescriptor
*/
private $entityDescriptor;
/**
* The maximum time in seconds the metadata should be cached.
*
* @var int|null
*/
private $maxCache = null;
/**
* The maximum time in seconds since the current time that this metadata should be considered valid.
*
* @var int|null
*/
private $maxDuration = null;
/**
* Initialize the SAML builder.
*
* @param string $entityId The entity id of the entity.
* @param int|null $maxCache The maximum time in seconds the metadata should be cached. Defaults to null
* @param int|null $maxDuration The maximum time in seconds this metadata should be considered valid. Defaults
* to null.
* @return void
*/
public function __construct($entityId, $maxCache = null, $maxDuration = null)
{
assert(is_string($entityId));
$this->maxCache = $maxCache;
$this->maxDuration = $maxDuration;
$this->entityDescriptor = new EntityDescriptor();
$this->entityDescriptor->setEntityID($entityId);
}
/**
* @param array $metadata
* @return void
*/
private function setExpiration($metadata)
{
if (array_key_exists('expire', $metadata)) {
if ($metadata['expire'] - time() < $this->maxDuration) {
$this->maxDuration = $metadata['expire'] - time();
}
}
if ($this->maxCache !== null) {
$this->entityDescriptor->setCacheDuration('PT' . $this->maxCache . 'S');
}
if ($this->maxDuration !== null) {
$this->entityDescriptor->setValidUntil(time() + $this->maxDuration);
}
}
/**
* Retrieve the EntityDescriptor element which is generated for this entity.
*
* @return DOMElement The EntityDescriptor element of this entity.
*/
public function getEntityDescriptor()
{
$xml = $this->entityDescriptor->toXML();
$xml->ownerDocument->appendChild($xml);
return $xml;
}
/**
* Retrieve the EntityDescriptor as text.
*
* This function serializes this EntityDescriptor, and returns it as text.
*
* @param bool $formatted Whether the returned EntityDescriptor should be formatted first.
*
* @return string The serialized EntityDescriptor.
*/
public function getEntityDescriptorText($formatted = true)
{
assert(is_bool($formatted));
$xml = $this->getEntityDescriptor();
if ($formatted) {
Utils\XML::formatDOMElement($xml);
}
return $xml->ownerDocument->saveXML();
}
/**
* Add a SecurityTokenServiceType for ADFS metadata.
*
* @param array $metadata The metadata with the information about the SecurityTokenServiceType.
* @return void
*/
public function addSecurityTokenServiceType($metadata)
{
assert(is_array($metadata));
assert(isset($metadata['entityid']));
assert(isset($metadata['metadata-set']));
$metadata = Configuration::loadFromArray($metadata, $metadata['entityid']);
$defaultEndpoint = $metadata->getDefaultEndpoint('SingleSignOnService');
$e = new SecurityTokenServiceType();
$e->setLocation($defaultEndpoint['Location']);
$this->addCertificate($e, $metadata);
$this->entityDescriptor->addRoleDescriptor($e);
}
/**
* Add extensions to the metadata.
*
* @param Configuration $metadata The metadata to get extensions from.
* @param RoleDescriptor $e Reference to the element where the Extensions element should be included.
* @return void
*/
private function addExtensions(Configuration $metadata, RoleDescriptor $e)
{
if ($metadata->hasValue('tags')) {
$a = new Attribute();
$a->setName('tags');
foreach ($metadata->getArray('tags') as $tag) {
$a->addAttributeValue(new AttributeValue($tag));
}
$e->setExtensions(array_merge($e->getExtensions(), [$a]));
}
if ($metadata->hasValue('hint.cidr')) {
$a = new Attribute();
$a->setName('hint.cidr');
foreach ($metadata->getArray('hint.cidr') as $hint) {
$a->addAttributeValue(new AttributeValue($hint));
}
$e->setExtensions(array_merge($e->getExtensions(), [$a]));
}
if ($metadata->hasValue('scope')) {
foreach ($metadata->getArray('scope') as $scopetext) {
$s = new Scope();
$s->setScope($scopetext);
// Check whether $ ^ ( ) * | \ are in a scope -> assume regex.
if (1 === preg_match('/[\$\^\)\(\*\|\\\\]/', $scopetext)) {
$s->setIsRegexpScope(true);
} else {
$s->setIsRegexpScope(false);
}
$e->setExtensions(array_merge($e->getExtensions(), [$s]));
}
}
if ($metadata->hasValue('EntityAttributes')) {
$ea = new EntityAttributes();
foreach ($metadata->getArray('EntityAttributes') as $attributeName => $attributeValues) {
$a = new Attribute();
$a->setName($attributeName);
$a->setNameFormat('urn:oasis:names:tc:SAML:2.0:attrname-format:uri');
// Attribute names that is not URI is prefixed as this: '{nameformat}name'
if (preg_match('/^\{(.*?)\}(.*)$/', $attributeName, $matches)) {
$a->setName($matches[2]);
$nameFormat = $matches[1];
if ($nameFormat !== Constants::NAMEFORMAT_UNSPECIFIED) {
$a->setNameFormat($nameFormat);
}
}
foreach ($attributeValues as $attributeValue) {
$a->addAttributeValue(new AttributeValue($attributeValue));
}
$ea->addChildren($a);
}
$this->entityDescriptor->setExtensions(
array_merge($this->entityDescriptor->getExtensions(), [$ea])
);
}
if ($metadata->hasValue('RegistrationInfo')) {
$ri = new RegistrationInfo();
foreach ($metadata->getArray('RegistrationInfo') as $riName => $riValues) {
switch ($riName) {
case 'authority':
$ri->setRegistrationAuthority($riValues);
break;
case 'instant':
$ri->setRegistrationInstant(\SAML2\Utils::xsDateTimeToTimestamp($riValues));
break;
case 'policies':
$ri->setRegistrationPolicy($riValues);
break;
}
}
$this->entityDescriptor->setExtensions(
array_merge($this->entityDescriptor->getExtensions(), [$ri])
);
}
if ($metadata->hasValue('UIInfo')) {
$ui = new UIInfo();
foreach ($metadata->getArray('UIInfo') as $uiName => $uiValues) {
switch ($uiName) {
case 'DisplayName':
$ui->setDisplayName($uiValues);
break;
case 'Description':
$ui->setDescription($uiValues);
break;
case 'InformationURL':
$ui->setInformationURL($uiValues);
break;
case 'PrivacyStatementURL':
$ui->setPrivacyStatementURL($uiValues);
break;
case 'Keywords':
foreach ($uiValues as $lang => $keywords) {
$uiItem = new Keywords();
$uiItem->setLanguage($lang);
$uiItem->setKeywords($keywords);
$ui->addKeyword($uiItem);
}
break;
case 'Logo':
foreach ($uiValues as $logo) {
$uiItem = new Logo();
$uiItem->setUrl($logo['url']);
$uiItem->setWidth($logo['width']);
$uiItem->setHeight($logo['height']);
if (isset($logo['lang'])) {
$uiItem->setLanguage($logo['lang']);
}
$ui->addLogo($uiItem);
}
break;
}
}
$e->setExtensions(array_merge($e->getExtensions(), [$ui]));
}
if ($metadata->hasValue('DiscoHints')) {
$dh = new DiscoHints();
foreach ($metadata->getArray('DiscoHints') as $dhName => $dhValues) {
switch ($dhName) {
case 'IPHint':
$dh->setIPHint($dhValues);
break;
case 'DomainHint':
$dh->setDomainHint($dhValues);
break;
case 'GeolocationHint':
$dh->setGeolocationHint($dhValues);
break;
}
}
$e->setExtensions(array_merge($e->getExtensions(), [$dh]));
}
}
/**
* Add an Organization element based on data passed as parameters
*
* @param array $orgName An array with the localized OrganizationName.
* @param array $orgDisplayName An array with the localized OrganizationDisplayName.
* @param array $orgURL An array with the localized OrganizationURL.
* @return void
*/
public function addOrganization(array $orgName, array $orgDisplayName, array $orgURL)
{
$org = new Organization();
$org->setOrganizationName($orgName);
$org->setOrganizationDisplayName($orgDisplayName);
$org->setOrganizationURL($orgURL);
$this->entityDescriptor->setOrganization($org);
}
/**
* Add an Organization element based on metadata array.
*
* @param array $metadata The metadata we should extract the organization information from.
* @return void
*/
public function addOrganizationInfo(array $metadata)
{
if (
empty($metadata['OrganizationName']) ||
empty($metadata['OrganizationDisplayName']) ||
empty($metadata['OrganizationURL'])
) {
// empty or incomplete organization information
return;
}
$orgName = Utils\Arrays::arrayize($metadata['OrganizationName'], 'en');
$orgDisplayName = Utils\Arrays::arrayize($metadata['OrganizationDisplayName'], 'en');
$orgURL = Utils\Arrays::arrayize($metadata['OrganizationURL'], 'en');
$this->addOrganization($orgName, $orgDisplayName, $orgURL);
}
/**
* Add a list of endpoints to metadata.
*
* @param array $endpoints The endpoints.
* @param bool $indexed Whether the endpoints should be indexed.
*
* @return array An array of endpoint objects,
* either \SAML2\XML\md\EndpointType or \SAML2\XML\md\IndexedEndpointType.
*/
private static function createEndpoints(array $endpoints, $indexed)
{
assert(is_bool($indexed));
$ret = [];
foreach ($endpoints as &$ep) {
if ($indexed) {
$t = new IndexedEndpointType();
if (!isset($ep['index'])) {
// Find the maximum index
$maxIndex = -1;
foreach ($endpoints as $ep) {
if (!isset($ep['index'])) {
continue;
}
if ($ep['index'] > $maxIndex) {
$maxIndex = $ep['index'];
}
}
$ep['index'] = $maxIndex + 1;
}
$t->setIndex($ep['index']);
} else {
$t = new EndpointType();
}
$t->setBinding($ep['Binding']);
$t->setLocation($ep['Location']);
if (isset($ep['ResponseLocation'])) {
$t->setResponseLocation($ep['ResponseLocation']);
}
if (isset($ep['hoksso:ProtocolBinding'])) {
$t->setAttributeNS(
Constants::NS_HOK,
'hoksso:ProtocolBinding',
Constants::BINDING_HTTP_REDIRECT
);
}
$ret[] = $t;
}
return $ret;
}
/**
* Add an AttributeConsumingService element to the metadata.
*
* @param SPSSODescriptor $spDesc The SPSSODescriptor element.
* @param Configuration $metadata The metadata.
* @return void
*/
private function addAttributeConsumingService(
SPSSODescriptor $spDesc,
Configuration $metadata
) {
$attributes = $metadata->getArray('attributes', []);
$name = $metadata->getLocalizedString('name', null);
if ($name === null || count($attributes) == 0) {
// we cannot add an AttributeConsumingService without name and attributes
return;
}
$attributesrequired = $metadata->getArray('attributes.required', []);
/*
* Add an AttributeConsumingService element with information as name and description and list
* of requested attributes
*/
$attributeconsumer = new AttributeConsumingService();
$attributeconsumer->setIndex($metadata->getInteger('attributes.index', 0));
if ($metadata->hasValue('attributes.isDefault')) {
$attributeconsumer->setIsDefault($metadata->getBoolean('attributes.isDefault', false));
}
$attributeconsumer->setServiceName($name);
$attributeconsumer->setServiceDescription($metadata->getLocalizedString('description', []));
$nameFormat = $metadata->getString('attributes.NameFormat', Constants::NAMEFORMAT_UNSPECIFIED);
foreach ($attributes as $friendlyName => $attribute) {
$t = new RequestedAttribute();
$t->setName($attribute);
if (!is_int($friendlyName)) {
$t->setFriendlyName($friendlyName);
}
if ($nameFormat !== Constants::NAMEFORMAT_UNSPECIFIED) {
$t->setNameFormat($nameFormat);
}
if (in_array($attribute, $attributesrequired, true)) {
$t->setIsRequired(true);
}
$attributeconsumer->addRequestedAttribute($t);
}
$spDesc->addAttributeConsumingService($attributeconsumer);
}
/**
* Add a specific type of metadata to an entity.
*
* @param string $set The metadata set this metadata comes from.
* @param array $metadata The metadata.
* @return void
*/
public function addMetadata($set, $metadata)
{
assert(is_string($set));
assert(is_array($metadata));
$this->setExpiration($metadata);
switch ($set) {
case 'saml20-sp-remote':
$this->addMetadataSP20($metadata);
break;
case 'saml20-idp-remote':
$this->addMetadataIdP20($metadata);
break;
case 'shib13-sp-remote':
$this->addMetadataSP11($metadata);
break;
case 'shib13-idp-remote':
$this->addMetadataIdP11($metadata);
break;
case 'attributeauthority-remote':
$this->addAttributeAuthority($metadata);
break;
default:
Logger::warning('Unable to generate metadata for unknown type \'' . $set . '\'.');
}
}
/**
* Add SAML 2.0 SP metadata.
*
* @param array $metadata The metadata.
* @param array $protocols The protocols supported. Defaults to \SAML2\Constants::NS_SAMLP.
* @return void
*/
public function addMetadataSP20($metadata, $protocols = [Constants::NS_SAMLP])
{
assert(is_array($metadata));
assert(is_array($protocols));
assert(isset($metadata['entityid']));
assert(isset($metadata['metadata-set']));
$metadata = Configuration::loadFromArray($metadata, $metadata['entityid']);
$e = new SPSSODescriptor();
$e->setProtocolSupportEnumeration($protocols);
if ($metadata->hasValue('saml20.sign.assertion')) {
$e->setWantAssertionsSigned($metadata->getBoolean('saml20.sign.assertion'));
}
if ($metadata->hasValue('redirect.validate')) {
$e->setAuthnRequestsSigned($metadata->getBoolean('redirect.validate'));
} elseif ($metadata->hasValue('validate.authnrequest')) {
$e->setAuthnRequestsSigned($metadata->getBoolean('validate.authnrequest'));
}
$this->addExtensions($metadata, $e);
$this->addCertificate($e, $metadata);
$e->setSingleLogoutService(self::createEndpoints($metadata->getEndpoints('SingleLogoutService'), false));
$e->setNameIDFormat($metadata->getArrayizeString('NameIDFormat', []));
$endpoints = $metadata->getEndpoints('AssertionConsumerService');
foreach ($metadata->getArrayizeString('AssertionConsumerService.artifact', []) as $acs) {
$endpoints[] = [
'Binding' => Constants::BINDING_HTTP_ARTIFACT,
'Location' => $acs,
];
}
$e->setAssertionConsumerService(self::createEndpoints($endpoints, true));
$this->addAttributeConsumingService($e, $metadata);
$this->entityDescriptor->addRoleDescriptor($e);
foreach ($metadata->getArray('contacts', []) as $contact) {
if (array_key_exists('contactType', $contact) && array_key_exists('emailAddress', $contact)) {
$this->addContact($contact['contactType'], Utils\Config\Metadata::getContact($contact));
}
}
}
/**
* Add metadata of a SAML 2.0 identity provider.
*
* @param array $metadata The metadata.
* @return void
*/
public function addMetadataIdP20($metadata)
{
assert(is_array($metadata));
assert(isset($metadata['entityid']));
assert(isset($metadata['metadata-set']));
$metadata = Configuration::loadFromArray($metadata, $metadata['entityid']);
$e = new IDPSSODescriptor();
$e->setProtocolSupportEnumeration(array_merge($e->getProtocolSupportEnumeration(), [Constants::NS_SAMLP]));
if ($metadata->hasValue('sign.authnrequest')) {
$e->setWantAuthnRequestsSigned($metadata->getBoolean('sign.authnrequest'));
} elseif ($metadata->hasValue('redirect.sign')) {
$e->setWantAuthnRequestsSigned($metadata->getBoolean('redirect.sign'));
}
$this->addExtensions($metadata, $e);
$this->addCertificate($e, $metadata);
if ($metadata->hasValue('ArtifactResolutionService')) {
$e->setArtifactResolutionService(self::createEndpoints(
$metadata->getEndpoints('ArtifactResolutionService'),
true
));
}
$e->setSingleLogoutService(self::createEndpoints($metadata->getEndpoints('SingleLogoutService'), false));
$e->setNameIDFormat($metadata->getArrayizeString('NameIDFormat', []));
$e->setSingleSignOnService(self::createEndpoints($metadata->getEndpoints('SingleSignOnService'), false));
$this->entityDescriptor->addRoleDescriptor($e);
foreach ($metadata->getArray('contacts', []) as $contact) {
if (array_key_exists('contactType', $contact) && array_key_exists('emailAddress', $contact)) {
$this->addContact($contact['contactType'], Utils\Config\Metadata::getContact($contact));
}
}
}
/**
* Add metadata of a SAML 1.1 service provider.
*
* @param array $metadata The metadata.
* @return void
*/
public function addMetadataSP11($metadata)
{
assert(is_array($metadata));
assert(isset($metadata['entityid']));
assert(isset($metadata['metadata-set']));
$metadata = Configuration::loadFromArray($metadata, $metadata['entityid']);
$e = new SPSSODescriptor();
$e->setProtocolSupportEnumeration(
array_merge(
$e->getProtocolSupportEnumeration(),
['urn:oasis:names:tc:SAML:1.1:protocol']
)
);
$this->addCertificate($e, $metadata);
$e->setNameIDFormat($metadata->getArrayizeString('NameIDFormat', []));
$endpoints = $metadata->getEndpoints('AssertionConsumerService');
foreach ($metadata->getArrayizeString('AssertionConsumerService.artifact', []) as $acs) {
$endpoints[] = [
'Binding' => 'urn:oasis:names:tc:SAML:1.0:profiles:artifact-01',
'Location' => $acs,
];
}
$e->setAssertionConsumerService(self::createEndpoints($endpoints, true));
$this->addAttributeConsumingService($e, $metadata);
$this->entityDescriptor->addRoleDescriptor($e);
}
/**
* Add metadata of a SAML 1.1 identity provider.
*
* @param array $metadata The metadata.
* @return void
*/
public function addMetadataIdP11($metadata)
{
assert(is_array($metadata));
assert(isset($metadata['entityid']));
assert(isset($metadata['metadata-set']));
$metadata = Configuration::loadFromArray($metadata, $metadata['entityid']);
$e = new IDPSSODescriptor();
$e->setProtocolSupportEnumeration(
array_merge($e->getProtocolSupportEnumeration(), [
'urn:oasis:names:tc:SAML:1.1:protocol',
'urn:mace:shibboleth:1.0'
])
);
$this->addCertificate($e, $metadata);
$e->setNameIDFormat($metadata->getArrayizeString('NameIDFormat', []));
$e->setSingleSignOnService(self::createEndpoints($metadata->getEndpoints('SingleSignOnService'), false));
$this->entityDescriptor->addRoleDescriptor($e);
}
/**
* Add metadata of a SAML attribute authority.
*
* @param array $metadata The AttributeAuthorityDescriptor, in the format returned by
* \SimpleSAML\Metadata\SAMLParser.
* @return void
*/
public function addAttributeAuthority(array $metadata)
{
assert(is_array($metadata));
assert(isset($metadata['entityid']));
assert(isset($metadata['metadata-set']));
$metadata = Configuration::loadFromArray($metadata, $metadata['entityid']);
$e = new AttributeAuthorityDescriptor();
$e->setProtocolSupportEnumeration($metadata->getArray('protocols', [Constants::NS_SAMLP]));
$this->addExtensions($metadata, $e);
$this->addCertificate($e, $metadata);
$e->setAttributeService(self::createEndpoints($metadata->getEndpoints('AttributeService'), false));
$e->setAssertionIDRequestService(self::createEndpoints(
$metadata->getEndpoints('AssertionIDRequestService'),
false
));
$e->setNameIDFormat($metadata->getArrayizeString('NameIDFormat', []));
$this->entityDescriptor->addRoleDescriptor($e);
}
/**
* Add contact information.
*
* Accepts a contact type, and a contact array that must be previously sanitized.
*
* WARNING: This function will change its signature and no longer parse a 'name' element.
*
* @param string $type The type of contact. Deprecated.
* @param array $details The details about the contact.
*
* @return void
* @todo Change the signature to remove $type.
* @todo Remove the capability to pass a name and parse it inside the method.
*/
public function addContact($type, $details)
{
assert(is_string($type));
assert(is_array($details));
assert(in_array($type, ['technical', 'support', 'administrative', 'billing', 'other'], true));
// TODO: remove this check as soon as getContact() is called always before calling this function
$details = Utils\Config\Metadata::getContact($details);
$e = new ContactPerson();
$e->setContactType($type);
if (!empty($details['attributes'])) {
$e->setContactPersonAttributes($details['attributes']);
}
if (isset($details['company'])) {
$e->setCompany($details['company']);
}
if (isset($details['givenName'])) {
$e->setGivenName($details['givenName']);
}
if (isset($details['surName'])) {
$e->setSurName($details['surName']);
}
if (isset($details['emailAddress'])) {
$eas = $details['emailAddress'];
if (!is_array($eas)) {
$eas = [$eas];
}
foreach ($eas as $ea) {
$e->addEmailAddress($ea);
}
}
if (isset($details['telephoneNumber'])) {
$tlfNrs = $details['telephoneNumber'];
if (!is_array($tlfNrs)) {
$tlfNrs = [$tlfNrs];
}
foreach ($tlfNrs as $tlfNr) {
$e->addTelephoneNumber($tlfNr);
}
}
$this->entityDescriptor->addContactPerson($e);
}
/**
* Add a KeyDescriptor with an X509 certificate.
*
* @param RoleDescriptor $rd The RoleDescriptor the certificate should be added to.
* @param string $use The value of the 'use' attribute.
* @param string $x509data The certificate data.
* @return void
*/
private function addX509KeyDescriptor(RoleDescriptor $rd, $use, $x509data)
{
assert(in_array($use, ['encryption', 'signing'], true));
assert(is_string($x509data));
$keyDescriptor = \SAML2\Utils::createKeyDescriptor($x509data);
$keyDescriptor->setUse($use);
$rd->addKeyDescriptor($keyDescriptor);
}
/**
* Add a certificate.
*
* Helper function for adding a certificate to the metadata.
*
* @param RoleDescriptor $rd The RoleDescriptor the certificate should be added to.
* @param Configuration $metadata The metadata of the entity.
* @return void
*/
private function addCertificate(RoleDescriptor $rd, Configuration $metadata)
{
$keys = $metadata->getPublicKeys();
foreach ($keys as $key) {
if ($key['type'] !== 'X509Certificate') {
continue;
}
if (!isset($key['signing']) || $key['signing'] === true) {
$this->addX509KeyDescriptor($rd, 'signing', $key['X509Certificate']);
}
if (!isset($key['encryption']) || $key['encryption'] === true) {
$this->addX509KeyDescriptor($rd, 'encryption', $key['X509Certificate']);
}
}
if ($metadata->hasValue('https.certData')) {
$this->addX509KeyDescriptor($rd, 'signing', $metadata->getString('https.certData'));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,302 @@
<?php
namespace SAML\Metadata;
use DOMElement;
use Exception;
use RobRichards\XMLSecLibs\XMLSecurityKey;
use RobRichards\XMLSecLibs\XMLSecurityDSig;
use SAML\Error\CriticalConfigurationError;
use SAML2\DOMDocumentFactory;
use SAML\Configuration;
use SAML\Error;
use SAML\Utils;
/**
* This class implements a helper function for signing of metadata.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class Signer
{
/**
* This functions finds what key & certificate files should be used to sign the metadata
* for the given entity.
*
* @param Configuration $config Our \SimpleSAML\Configuration instance.
* @param array $entityMetadata The metadata of the entity.
* @param string $type A string which describes the type entity this is, e.g. 'SAML 2 IdP' or
* 'Shib 1.3 SP'.
*
* @return array An associative array with the keys 'privatekey', 'certificate', and optionally 'privatekey_pass'.
* @throws Exception If the key and certificate used to sign is unknown.
*/
private static function findKeyCert($config, $entityMetadata, $type)
{
// first we look for metadata.privatekey and metadata.certificate in the metadata
if (
array_key_exists('metadata.sign.privatekey', $entityMetadata)
|| array_key_exists('metadata.sign.certificate', $entityMetadata)
) {
if (
!array_key_exists('metadata.sign.privatekey', $entityMetadata)
|| !array_key_exists('metadata.sign.certificate', $entityMetadata)
) {
throw new Exception(
'Missing either the "metadata.sign.privatekey" or the' .
' "metadata.sign.certificate" configuration option in the metadata for' .
' the ' . $type . ' "' . $entityMetadata['entityid'] . '". If one of' .
' these options is specified, then the other must also be specified.'
);
}
$ret = [
'privatekey' => $entityMetadata['metadata.sign.privatekey'],
'certificate' => $entityMetadata['metadata.sign.certificate']
];
if (array_key_exists('metadata.sign.privatekey_pass', $entityMetadata)) {
$ret['privatekey_pass'] = $entityMetadata['metadata.sign.privatekey_pass'];
}
return $ret;
}
// then we look for default values in the global configuration
$privatekey = $config->getString('metadata.sign.privatekey', null);
$certificate = $config->getString('metadata.sign.certificate', null);
if ($privatekey !== null || $certificate !== null) {
if ($privatekey === null || $certificate === null) {
throw new Exception(
'Missing either the "metadata.sign.privatekey" or the' .
' "metadata.sign.certificate" configuration option in the global' .
' configuration. If one of these options is specified, then the other' .
' must also be specified.'
);
}
$ret = ['privatekey' => $privatekey, 'certificate' => $certificate];
$privatekey_pass = $config->getString('metadata.sign.privatekey_pass', null);
if ($privatekey_pass !== null) {
$ret['privatekey_pass'] = $privatekey_pass;
}
return $ret;
}
// as a last resort we attempt to use the privatekey and certificate option from the metadata
if (
array_key_exists('privatekey', $entityMetadata)
|| array_key_exists('certificate', $entityMetadata)
) {
if (
!array_key_exists('privatekey', $entityMetadata)
|| !array_key_exists('certificate', $entityMetadata)
) {
throw new Exception(
'Both the "privatekey" and the "certificate" option must' .
' be set in the metadata for the ' . $type . ' "' .
$entityMetadata['entityid'] . '" before it is possible to sign metadata' .
' from this entity.'
);
}
$ret = [
'privatekey' => $entityMetadata['privatekey'],
'certificate' => $entityMetadata['certificate']
];
if (array_key_exists('privatekey_pass', $entityMetadata)) {
$ret['privatekey_pass'] = $entityMetadata['privatekey_pass'];
}
return $ret;
}
throw new Exception(
'Could not find what key & certificate should be used to sign the metadata' .
' for the ' . $type . ' "' . $entityMetadata['entityid'] . '".'
);
}
/**
* Determine whether metadata signing is enabled for the given metadata.
*
* @param Configuration $config Our \SimpleSAML\Configuration instance.
* @param array $entityMetadata The metadata of the entity.
* @param string $type A string which describes the type entity this is, e.g. 'SAML 2 IdP' or
* 'Shib 1.3 SP'.
*
* @return boolean True if metadata signing is enabled, false otherwise.
* @throws Exception If the value of the 'metadata.sign.enable' option is not a boolean.
*/
private static function isMetadataSigningEnabled($config, $entityMetadata, $type)
{
// first check the metadata for the entity
if (array_key_exists('metadata.sign.enable', $entityMetadata)) {
if (!is_bool($entityMetadata['metadata.sign.enable'])) {
throw new Exception(
'Invalid value for the "metadata.sign.enable" configuration option for' .
' the ' . $type . ' "' . $entityMetadata['entityid'] . '". This option' .
' should be a boolean.'
);
}
return $entityMetadata['metadata.sign.enable'];
}
$enabled = $config->getBoolean('metadata.sign.enable', false);
return $enabled;
}
/**
* Determine the signature and digest algorithms to use when signing metadata.
*
* This method will look for the 'metadata.sign.algorithm' key in the $entityMetadata array, or look for such
* a configuration option in the $config object.
*
* @param Configuration $config The global configuration.
* @param array $entityMetadata An array containing the metadata related to this entity.
* @param string $type A string describing the type of entity. E.g. 'SAML 2 IdP' or 'Shib 1.3 SP'.
*
* @return array An array with two keys, 'algorithm' and 'digest', corresponding to the signature and digest
* algorithms to use, respectively.
*
* @throws CriticalConfigurationError
*/
private static function getMetadataSigningAlgorithm($config, $entityMetadata, $type)
{
// configure the algorithm to use
if (array_key_exists('metadata.sign.algorithm', $entityMetadata)) {
if (!is_string($entityMetadata['metadata.sign.algorithm'])) {
throw new CriticalConfigurationError(
"Invalid value for the 'metadata.sign.algorithm' configuration option for the " . $type .
"'" . $entityMetadata['entityid'] . "'. This option has restricted values"
);
}
$alg = $entityMetadata['metadata.sign.algorithm'];
} else {
$alg = $config->getString('metadata.sign.algorithm', XMLSecurityKey::RSA_SHA256);
}
$supported_algs = [
XMLSecurityKey::RSA_SHA1,
XMLSecurityKey::RSA_SHA256,
XMLSecurityKey::RSA_SHA384,
XMLSecurityKey::RSA_SHA512,
];
if (!in_array($alg, $supported_algs, true)) {
throw new CriticalConfigurationError("Unknown signature algorithm '$alg'");
}
switch ($alg) {
case XMLSecurityKey::RSA_SHA256:
$digest = XMLSecurityDSig::SHA256;
break;
case XMLSecurityKey::RSA_SHA384:
$digest = XMLSecurityDSig::SHA384;
break;
case XMLSecurityKey::RSA_SHA512:
$digest = XMLSecurityDSig::SHA512;
break;
default:
$digest = XMLSecurityDSig::SHA1;
}
return [
'algorithm' => $alg,
'digest' => $digest,
];
}
/**
* Signs the given metadata if metadata signing is enabled.
*
* @param string $metadataString A string with the metadata.
* @param array $entityMetadata The metadata of the entity.
* @param string $type A string which describes the type entity this is, e.g. 'SAML 2 IdP' or 'Shib 1.3 SP'.
*
* @return string The $metadataString with the signature embedded.
* @throws Exception If the certificate or private key cannot be loaded, or the metadata doesn't parse properly.
*/
public static function sign($metadataString, $entityMetadata, $type)
{
$config = Configuration::getInstance();
// check if metadata signing is enabled
if (!self::isMetadataSigningEnabled($config, $entityMetadata, $type)) {
return $metadataString;
}
// find the key & certificate which should be used to sign the metadata
$keyCertFiles = self::findKeyCert($config, $entityMetadata, $type);
$keyFile = Utils\Config::getCertPath($keyCertFiles['privatekey']);
if (!file_exists($keyFile)) {
throw new Exception(
'Could not find private key file [' . $keyFile . '], which is needed to sign the metadata'
);
}
$keyData = file_get_contents($keyFile);
$certFile = Utils\Config::getCertPath($keyCertFiles['certificate']);
if (!file_exists($certFile)) {
throw new Exception(
'Could not find certificate file [' . $certFile . '], which is needed to sign the metadata'
);
}
$certData = file_get_contents($certFile);
// convert the metadata to a DOM tree
try {
$xml = DOMDocumentFactory::fromString($metadataString);
} catch (Exception $e) {
throw new Exception('Error parsing self-generated metadata.');
}
$signature_cf = self::getMetadataSigningAlgorithm($config, $entityMetadata, $type);
// load the private key
$objKey = new XMLSecurityKey($signature_cf['algorithm'], ['type' => 'private']);
if (array_key_exists('privatekey_pass', $keyCertFiles)) {
$objKey->passphrase = $keyCertFiles['privatekey_pass'];
}
$objKey->loadKey($keyData, false);
// get the EntityDescriptor node we should sign
/** @var DOMElement $rootNode */
$rootNode = $xml->firstChild;
$rootNode->setAttribute('ID', '_' . hash('sha256', $metadataString));
// sign the metadata with our private key
$objXMLSecDSig = new XMLSecurityDSig();
$objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
$objXMLSecDSig->addReferenceList(
[$rootNode],
$signature_cf['digest'],
['http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N],
['id_name' => 'ID', 'overwrite' => false]
);
$objXMLSecDSig->sign($objKey);
// add the certificate to the signature
$objXMLSecDSig->add509Cert($certData, true);
// add the signature to the metadata
$objXMLSecDSig->insertSignature($rootNode, $rootNode->firstChild);
// return the DOM tree as a string
return $xml->saveXML();
}
}

View File

@ -0,0 +1,376 @@
<?php
namespace SAML\Metadata\Sources;
use Exception;
use RobRichards\XMLSecLibs\XMLSecurityDSig;
use SAML\Configuration;
use SAML\Error;
use SAML\Logger;
use SAML\Metadata\MetaDataStorageSource;
use SAML\Metadata\SAMLParser;
use SAML\Utils;
/**
* This class implements SAML Metadata Query Protocol
*
* @author Andreas Åkre Solberg, UNINETT AS.
* @author Olav Morken, UNINETT AS.
* @author Tamas Frank, NIIFI
* @package SimpleSAMLphp
*/
class MDQ extends MetaDataStorageSource
{
/**
* The URL of MDQ server (url:port)
*
* @var string
*/
private $server;
/**
* The fingerprint of the certificate used to sign the metadata. You don't need this option if you don't want to
* validate the signature on the metadata.
*
* @var string|null
*/
private $validateFingerprint;
/**
* @var string
*/
private $validateFingerprintAlgorithm;
/**
* The cache directory, or null if no cache directory is configured.
*
* @var string|null
*/
private $cacheDir;
/**
* The maximum cache length, in seconds.
*
* @var integer
*/
private $cacheLength;
/**
* This function initializes the dynamic XML metadata source.
*
* Options:
* - 'server': URL of the MDQ server (url:port). Mandatory.
* - 'validateFingerprint': The fingerprint of the certificate used to sign the metadata.
* You don't need this option if you don't want to validate the signature on the metadata.
* Optional.
* - 'cachedir': Directory where metadata can be cached. Optional.
* - 'cachelength': Maximum time metadata cah be cached, in seconds. Default to 24
* hours (86400 seconds).
*
* @param array $config The configuration for this instance of the XML metadata source.
*
* @throws Exception If no server option can be found in the configuration.
*/
protected function __construct($config)
{
assert(is_array($config));
if (!array_key_exists('server', $config)) {
throw new Exception(__CLASS__ . ": the 'server' configuration option is not set.");
} else {
$this->server = $config['server'];
}
if (array_key_exists('validateFingerprint', $config)) {
$this->validateFingerprint = $config['validateFingerprint'];
} else {
$this->validateFingerprint = null;
}
if (isset($config['validateFingerprintAlgorithm'])) {
$this->validateFingerprintAlgorithm = $config['validateFingerprintAlgorithm'];
} else {
$this->validateFingerprintAlgorithm = XMLSecurityDSig::SHA1;
}
if (array_key_exists('cachedir', $config)) {
$globalConfig = Configuration::getInstance();
$this->cacheDir = $globalConfig->resolvePath($config['cachedir']);
} else {
$this->cacheDir = null;
}
if (array_key_exists('cachelength', $config)) {
$this->cacheLength = $config['cachelength'];
} else {
$this->cacheLength = 86400;
}
}
/**
* This function is not implemented.
*
* @param string $set The set we want to list metadata for.
*
* @return array An empty array.
*/
public function getMetadataSet($set)
{
// we don't have this metadata set
return [];
}
/**
* Find the cache file name for an entity,
*
* @param string $set The metadata set this entity belongs to.
* @param string $entityId The entity id of this entity.
*
* @return string The full path to the cache file.
*/
private function getCacheFilename($set, $entityId)
{
assert(is_string($set));
assert(is_string($entityId));
if ($this->cacheDir === null) {
throw new Error\ConfigurationError("Missing cache directory configuration.");
}
$cachekey = sha1($entityId);
return $this->cacheDir . '/' . $set . '-' . $cachekey . '.cached.xml';
}
/**
* Load a entity from the cache.
*
* @param string $set The metadata set this entity belongs to.
* @param string $entityId The entity id of this entity.
*
* @return array|NULL The associative array with the metadata for this entity, or NULL
* if the entity could not be found.
* @throws Exception If an error occurs while loading metadata from cache.
*/
private function getFromCache($set, $entityId)
{
assert(is_string($set));
assert(is_string($entityId));
if (empty($this->cacheDir)) {
return null;
}
$cachefilename = $this->getCacheFilename($set, $entityId);
if (!file_exists($cachefilename)) {
return null;
}
if (!is_readable($cachefilename)) {
throw new Exception(__CLASS__ . ': could not read cache file for entity [' . $cachefilename . ']');
}
Logger::debug(__CLASS__ . ': reading cache [' . $entityId . '] => [' . $cachefilename . ']');
/* Ensure that this metadata isn't older that the cachelength option allows. This
* must be verified based on the file, since this option may be changed after the
* file is written.
*/
$stat = stat($cachefilename);
if ($stat['mtime'] + $this->cacheLength <= time()) {
Logger::debug(__CLASS__ . ': cache file older that the cachelength option allows.');
return null;
}
$rawData = file_get_contents($cachefilename);
if (empty($rawData)) {
/** @var array $error */
$error = error_get_last();
throw new Exception(
__CLASS__ . ': error reading metadata from cache file "' . $cachefilename . '": ' . $error['message']
);
}
$data = unserialize($rawData);
if ($data === false) {
throw new Exception(__CLASS__ . ': error unserializing cached data from file "' . $cachefilename . '".');
}
if (!is_array($data)) {
throw new Exception(__CLASS__ . ': Cached metadata from "' . $cachefilename . '" wasn\'t an array.');
}
return $data;
}
/**
* Save a entity to the cache.
*
* @param string $set The metadata set this entity belongs to.
* @param string $entityId The entity id of this entity.
* @param array $data The associative array with the metadata for this entity.
*
* @throws Exception If metadata cannot be written to cache.
* @return void
*/
private function writeToCache($set, $entityId, $data)
{
assert(is_string($set));
assert(is_string($entityId));
assert(is_array($data));
if (empty($this->cacheDir)) {
return;
}
$cachefilename = $this->getCacheFilename($set, $entityId);
if (!is_writable(dirname($cachefilename))) {
throw new Exception(__CLASS__ . ': could not write cache file for entity [' . $cachefilename . ']');
}
Logger::debug(__CLASS__ . ': Writing cache [' . $entityId . '] => [' . $cachefilename . ']');
file_put_contents($cachefilename, serialize($data));
}
/**
* Retrieve metadata for the correct set from a SAML2Parser.
*
* @param SAMLParser $entity A SAML2Parser representing an entity.
* @param string $set The metadata set we are looking for.
*
* @return array|NULL The associative array with the metadata, or NULL if no metadata for
* the given set was found.
*/
private static function getParsedSet(SAMLParser $entity, $set)
{
assert(is_string($set));
switch ($set) {
case 'saml20-idp-remote':
return $entity->getMetadata20IdP();
case 'saml20-sp-remote':
return $entity->getMetadata20SP();
case 'shib13-idp-remote':
return $entity->getMetadata1xIdP();
case 'shib13-sp-remote':
return $entity->getMetadata1xSP();
case 'attributeauthority-remote':
$ret = $entity->getAttributeAuthorities();
return $ret[0];
default:
Logger::warning(__CLASS__ . ': unknown metadata set: \'' . $set . '\'.');
}
return null;
}
/**
* Overriding this function from the superclass \SimpleSAML\Metadata\MetaDataStorageSource.
*
* This function retrieves metadata for the given entity id in the given set of metadata.
* It will return NULL if it is unable to locate the metadata.
*
* This class implements this function using the getMetadataSet-function. A subclass should
* override this function if it doesn't implement the getMetadataSet function, or if the
* implementation of getMetadataSet is slow.
*
* @param string $index The entityId or metaindex we are looking up.
* @param string $set The set we are looking for metadata in.
*
* @return array|null An associative array with metadata for the given entity, or NULL if we are unable to
* locate the entity.
* @throws Exception If an error occurs while validating the signature or the metadata is in an
* incorrect set.
*/
public function getMetaData($index, $set)
{
assert(is_string($index));
assert(is_string($set));
Logger::info(__CLASS__ . ': loading metadata entity [' . $index . '] from [' . $set . ']');
// read from cache if possible
try {
$data = $this->getFromCache($set, $index);
} catch (Exception $e) {
Logger::error($e->getMessage());
// proceed with fetching metadata even if the cache is broken
$data = null;
}
if ($data !== null && array_key_exists('expires', $data) && $data['expires'] < time()) {
// metadata has expired
$data = null;
}
if (isset($data)) {
// metadata found in cache and not expired
Logger::debug(__CLASS__ . ': using cached metadata for: ' . $index . '.');
return $data;
}
// look at Metadata Query Protocol: https://github.com/iay/md-query/blob/master/draft-young-md-query.txt
$mdq_url = $this->server . '/entities/' . urlencode($index);
Logger::debug(__CLASS__ . ': downloading metadata for "' . $index . '" from [' . $mdq_url . ']');
try {
$xmldata = Utils\HTTP::fetch($mdq_url);
} catch (Exception $e) {
// Avoid propagating the exception, make sure we can handle the error later
$xmldata = false;
}
if (empty($xmldata)) {
$error = error_get_last();
Logger::info('Unable to fetch metadata for "' . $index . '" from ' . $mdq_url . ': ' .
(is_array($error) ? $error['message'] : 'no error available'));
return null;
}
/** @var string $xmldata */
$entity = SAMLParser::parseString($xmldata);
Logger::debug(__CLASS__ . ': completed parsing of [' . $mdq_url . ']');
if ($this->validateFingerprint !== null) {
if (
!$entity->validateFingerprint(
$this->validateFingerprint,
$this->validateFingerprintAlgorithm
)
) {
throw new Exception(__CLASS__ . ': error, could not verify signature for entity: ' . $index . '".');
}
}
$data = self::getParsedSet($entity, $set);
if ($data === null) {
throw new Exception(__CLASS__ . ': no metadata for set "' . $set . '" available from "' . $index . '".');
}
try {
$this->writeToCache($set, $index, $data);
} catch (Exception $e) {
// Proceed without writing to cache
Logger::error('Error writing MDQ result to cache: ' . $e->getMessage());
}
return $data;
}
/**
* This function loads the metadata for entity IDs in $entityIds. It is returned as an associative array
* where the key is the entity id. An empty array may be returned if no matching entities were found
* @param array $entityIds The entity ids to load
* @param string $set The set we want to get metadata from.
* @return array An associative array with the metadata for the requested entities, if found.
*/
public function getMetaDataForEntities(array $entityIds, $set)
{
return $this->getMetaDataForEntitiesIndividually($entityIds, $set);
}
}

580
libsrc/SAML/Module.php Normal file
View File

@ -0,0 +1,580 @@
<?php
namespace SAML;
use SAML\Error\Exception;
use SAML\HTTP\Router;
use SAML\Utils;
use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Helper class for accessing information about modules.
*
* @author Olav Morken <olav.morken@uninett.no>, UNINETT AS.
* @author Boy Baukema, SURFnet.
* @author Jaime Perez <jaime.perez@uninett.no>, UNINETT AS.
* @package SimpleSAMLphp
*/
class Module
{
/**
* Index pages: file names to attempt when accessing directories.
*
* @var array
*/
public static $indexFiles = ['index.php', 'index.html', 'index.htm', 'index.txt'];
/**
* MIME Types
*
* The key is the file extension and the value the corresponding MIME type.
*
* @var array
*/
public static $mimeTypes = [
'bmp' => 'image/x-ms-bmp',
'css' => 'text/css',
'gif' => 'image/gif',
'htm' => 'text/html',
'html' => 'text/html',
'shtml' => 'text/html',
'ico' => 'image/vnd.microsoft.icon',
'jpe' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'jpg' => 'image/jpeg',
'js' => 'text/javascript',
'pdf' => 'application/pdf',
'png' => 'image/png',
'svg' => 'image/svg+xml',
'svgz' => 'image/svg+xml',
'swf' => 'application/x-shockwave-flash',
'swfl' => 'application/x-shockwave-flash',
'txt' => 'text/plain',
'xht' => 'application/xhtml+xml',
'xhtml' => 'application/xhtml+xml',
];
/**
* A list containing the modules currently installed.
*
* @var array
*/
public static $modules = [];
/**
* A cache containing specific information for modules, like whether they are enabled or not, or their hooks.
*
* @var array
*/
public static $module_info = [];
/**
* Retrieve the base directory for a module.
*
* The returned path name will be an absolute path.
*
* @param string $module Name of the module
*
* @return string The base directory of a module.
*/
public static function getModuleDir($module)
{
$baseDir = dirname(dirname(dirname(__FILE__))) . '/modules';
$moduleDir = $baseDir . '/' . $module;
return $moduleDir;
}
/**
* Determine whether a module is enabled.
*
* Will return false if the given module doesn't exist.
*
* @param string $module Name of the module
*
* @return bool True if the given module is enabled, false otherwise.
*
* @throws \Exception If module.enable is set and is not boolean.
*/
public static function isModuleEnabled($module)
{
$config = Configuration::getOptionalConfig();
return self::isModuleEnabledWithConf($module, $config->getArray('module.enable', []));
}
/**
* Handler for module requests.
*
* This controller receives requests for pages hosted by modules, and processes accordingly. Depending on the
* configuration and the actual request, it will run a PHP script and exit, or return a Response produced either
* by another controller or by a static file.
*
* @param Request|null $request The request to process. Defaults to the current one.
*
* @return Response|BinaryFileResponse Returns a Response object that can be sent to the browser.
* @throws Error\BadRequest In case the request URI is malformed.
* @throws Error\NotFound In case the request URI is invalid or the resource it points to cannot be found.
*/
public static function process(Request $request = null)
{
if ($request === null) {
$request = Request::createFromGlobals();
}
if ($request->server->get('PATH_INFO') === '/') {
throw new Error\NotFound('No PATH_INFO to module.php');
}
$url = $request->server->get('PATH_INFO');
assert(substr($url, 0, 1) === '/');
/* clear the PATH_INFO option, so that a script can detect whether it is called with anything following the
*'.php'-ending.
*/
unset($_SERVER['PATH_INFO']);
$modEnd = strpos($url, '/', 1);
if ($modEnd === false) {
// the path must always be on the form /module/
throw new Error\NotFound('The URL must at least contain a module name followed by a slash.');
}
$module = substr($url, 1, $modEnd - 1);
$url = substr($url, $modEnd + 1);
if ($url === false) {
$url = '';
}
if (!self::isModuleEnabled($module)) {
throw new Error\NotFound('The module \'' . $module . '\' was either not found, or wasn\'t enabled.');
}
/* Make sure that the request isn't suspicious (contains references to current directory or parent directory or
* anything like that. Searching for './' in the URL will detect both '../' and './'. Searching for '\' will
* detect attempts to use Windows-style paths.
*/
if (strpos($url, '\\') !== false) {
throw new Error\BadRequest('Requested URL contained a backslash.');
} elseif (strpos($url, './') !== false) {
throw new Error\BadRequest('Requested URL contained \'./\'.');
}
$config = Configuration::getInstance();
// rebuild REQUEST_URI and SCRIPT_NAME just in case we need to. This is needed for server aliases and rewrites
$translated_uri = $config->getBasePath() . 'module.php/' . $module . '/' . $url;
$request->server->set('REQUEST_URI', $translated_uri);
$request->server->set('SCRIPT_NAME', $config->getBasePath() . 'module.php');
$request_files = array_filter(
$request->files->all(),
function ($val) {
return !is_null($val);
}
);
$request->initialize(
$request->query->all(),
$request->request->all(),
$request->attributes->all(),
$request->cookies->all(),
$request_files,
$request->server->all(),
$request->getContent()
);
if ($config->getBoolean('usenewui', false) === true) {
$router = new Router($module);
try {
return $router->process($request);
} catch (FileLocatorFileNotFoundException $e) {
// no routes configured for this module, fall back to the old system
} catch (NotFoundHttpException $e) {
// this module has been migrated, but the route wasn't found
}
}
$moduleDir = self::getModuleDir($module) . '/www/';
// check for '.php/' in the path, the presence of which indicates that another php-script should handle the
// request
for ($phpPos = strpos($url, '.php/'); $phpPos !== false; $phpPos = strpos($url, '.php/', $phpPos + 1)) {
$newURL = substr($url, 0, $phpPos + 4);
$param = substr($url, $phpPos + 4);
if (is_file($moduleDir . $newURL)) {
/* $newPath points to a normal file. Point execution to that file, and save the remainder of the path
* in PATH_INFO.
*/
$url = $newURL;
$request->server->set('PATH_INFO', $param);
$_SERVER['PATH_INFO'] = $param;
break;
}
}
$path = $moduleDir . $url;
if ($path[strlen($path) - 1] === '/') {
// path ends with a slash - directory reference. Attempt to find index file in directory
foreach (self::$indexFiles as $if) {
if (file_exists($path . $if)) {
$path .= $if;
break;
}
}
}
if (is_dir($path)) {
/* Path is a directory - maybe no index file was found in the previous step, or maybe the path didn't end
* with a slash. Either way, we don't do directory listings.
*/
throw new Error\NotFound('Directory listing not available.');
}
if (!file_exists($path)) {
// file not found
Logger::info('Could not find file \'' . $path . '\'.');
throw new Error\NotFound('The URL wasn\'t found in the module.');
}
if (mb_strtolower(substr($path, -4), 'UTF-8') === '.php') {
// PHP file - attempt to run it
/* In some environments, $_SERVER['SCRIPT_NAME'] is already set with $_SERVER['PATH_INFO']. Check for that
* case, and append script name only if necessary.
*
* Contributed by Travis Hegner.
*/
$script = "/$module/$url";
if (strpos($request->getScriptName(), $script) === false) {
$request->server->set('SCRIPT_NAME', $request->getScriptName() . '/' . $module . '/' . $url);
}
require($path);
exit();
}
// some other file type - attempt to serve it
// find MIME type for file, based on extension
$contentType = null;
if (preg_match('#\.([^/\.]+)$#D', $path, $type)) {
$type = strtolower($type[1]);
if (array_key_exists($type, self::$mimeTypes)) {
$contentType = self::$mimeTypes[$type];
}
}
if ($contentType === null) {
/* We were unable to determine the MIME type from the file extension. Fall back to mime_content_type (if it
* exists).
*/
if (function_exists('mime_content_type')) {
$contentType = mime_content_type($path);
} else {
// mime_content_type doesn't exist. Return a default MIME type
Logger::warning('Unable to determine mime content type of file: ' . $path);
$contentType = 'application/octet-stream';
}
}
$assetConfig = $config->getConfigItem('assets');
$cacheConfig = $assetConfig->getConfigItem('caching');
$response = new BinaryFileResponse($path);
$response->setCache([
// "public" allows response caching even if the request was authenticated,
// which is exactly what we want for static resources
'public' => true,
'max_age' => (string)$cacheConfig->getInteger('max_age', 86400)
]);
$response->setAutoLastModified();
if ($cacheConfig->getBoolean('etag', false)) {
$response->setAutoEtag();
}
$response->isNotModified($request);
$response->headers->set('Content-Type', $contentType);
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE);
$response->prepare($request);
return $response;
}
/**
* @param string $module
* @param array $mod_config
* @return bool
*/
private static function isModuleEnabledWithConf($module, $mod_config)
{
if (isset(self::$module_info[$module]['enabled'])) {
return self::$module_info[$module]['enabled'];
}
if (!empty(self::$modules) && !in_array($module, self::$modules, true)) {
return false;
}
$moduleDir = self::getModuleDir($module);
if (!is_dir($moduleDir)) {
self::$module_info[$module]['enabled'] = false;
return false;
}
if (isset($mod_config[$module])) {
if (is_bool($mod_config[$module])) {
self::$module_info[$module]['enabled'] = $mod_config[$module];
return $mod_config[$module];
}
throw new \Exception("Invalid module.enable value for the '$module' module.");
}
if (
assert_options(ASSERT_ACTIVE)
&& !file_exists($moduleDir . '/default-enable')
&& !file_exists($moduleDir . '/default-disable')
) {
Logger::error("Missing default-enable or default-disable file for the module $module");
}
if (file_exists($moduleDir . '/enable')) {
self::$module_info[$module]['enabled'] = true;
return true;
}
if (!file_exists($moduleDir . '/disable') && file_exists($moduleDir . '/default-enable')) {
self::$module_info[$module]['enabled'] = true;
return true;
}
self::$module_info[$module]['enabled'] = false;
return false;
}
/**
* Get available modules.
*
* @return array One string for each module.
*
* @throws \Exception If we cannot open the module's directory.
*/
public static function getModules()
{
if (!empty(self::$modules)) {
return self::$modules;
}
$path = self::getModuleDir('.');
$dh = scandir($path);
if ($dh === false) {
throw new \Exception('Unable to open module directory "' . $path . '".');
}
foreach ($dh as $f) {
if ($f[0] === '.') {
continue;
}
if (!is_dir($path . '/' . $f)) {
continue;
}
self::$modules[] = $f;
}
return self::$modules;
}
/**
* Resolve module class.
*
* This function takes a string on the form "<module>:<class>" and converts it to a class
* name. It can also check that the given class is a subclass of a specific class. The
* resolved classname will be "\SimleSAML\Module\<module>\<$type>\<class>.
*
* It is also possible to specify a full classname instead of <module>:<class>.
*
* An exception will be thrown if the class can't be resolved.
*
* @param string $id The string we should resolve.
* @param string $type The type of the class.
* @param string|null $subclass The class should be a subclass of this class. Optional.
*
* @return string The classname.
*
* @throws \Exception If the class cannot be resolved.
*/
public static function resolveClass($id, $type, $subclass = null)
{
assert(is_string($id));
assert(is_string($type));
assert(is_string($subclass) || $subclass === null);
$tmp = explode(':', $id, 2);
if (count($tmp) === 1) {
// no module involved
$className = $tmp[0];
if (!class_exists($className)) {
throw new \Exception("Could not resolve '$id': no class named '$className'.");
}
} else {
// should be a module
// make sure empty types are handled correctly
$type = (empty($type)) ? '\\' : '\\' . $type . '\\';
$className = 'SimpleSAML\\Module\\' . $tmp[0] . $type . $tmp[1];
if (!class_exists($className)) {
// check for the old-style class names
$type = str_replace('\\', '_', $type);
$oldClassName = 'sspmod_' . $tmp[0] . $type . $tmp[1];
if (!class_exists($oldClassName)) {
throw new \Exception("Could not resolve '$id': no class named '$className' or '$oldClassName'.");
}
$className = $oldClassName;
}
}
if ($subclass !== null && !is_subclass_of($className, $subclass)) {
throw new \Exception(
'Could not resolve \'' . $id . '\': The class \'' . $className
. '\' isn\'t a subclass of \'' . $subclass . '\'.'
);
}
return $className;
}
/**
* Get absolute URL to a specified module resource.
*
* This function creates an absolute URL to a resource stored under ".../modules/<module>/www/".
*
* @param string $resource Resource path, on the form "<module name>/<resource>"
* @param array $parameters Extra parameters which should be added to the URL. Optional.
*
* @return string The absolute URL to the given resource.
*/
public static function getModuleURL($resource, array $parameters = [])
{
assert(is_string($resource));
assert($resource[0] !== '/');
$url = Utils\HTTP::getBaseURL() . 'module.php/' . $resource;
if (!empty($parameters)) {
$url = Utils\HTTP::addURLParameters($url, $parameters);
}
return $url;
}
/**
* Get the available hooks for a given module.
*
* @param string $module The module where we should look for hooks.
*
* @return array An array with the hooks available for this module. Each element is an array with two keys: 'file'
* points to the file that contains the hook, and 'func' contains the name of the function implementing that hook.
* When there are no hooks defined, an empty array is returned.
*/
public static function getModuleHooks($module)
{
if (isset(self::$modules[$module]['hooks'])) {
return self::$modules[$module]['hooks'];
}
$hook_dir = self::getModuleDir($module) . '/hooks';
if (!is_dir($hook_dir)) {
return [];
}
$hooks = [];
$files = scandir($hook_dir);
foreach ($files as $file) {
if ($file[0] === '.') {
continue;
}
if (!preg_match('/hook_(\w+)\.php/', $file, $matches)) {
continue;
}
$hook_name = $matches[1];
$hook_func = $module . '_hook_' . $hook_name;
$hooks[$hook_name] = ['file' => $hook_dir . '/' . $file, 'func' => $hook_func];
}
return $hooks;
}
/**
* Call a hook in all enabled modules.
*
* This function iterates over all enabled modules and calls a hook in each module.
*
* @param string $hook The name of the hook.
* @param mixed &$data The data which should be passed to each hook. Will be passed as a reference.
* @return void
*
* @throws Exception If an invalid hook is found in a module.
*/
public static function callHooks($hook, &$data = null)
{
assert(is_string($hook));
$modules = self::getModules();
$config = Configuration::getOptionalConfig()->getArray('module.enable', []);
sort($modules);
foreach ($modules as $module) {
if (!self::isModuleEnabledWithConf($module, $config)) {
continue;
}
if (!isset(self::$module_info[$module]['hooks'])) {
self::$module_info[$module]['hooks'] = self::getModuleHooks($module);
}
if (!isset(self::$module_info[$module]['hooks'][$hook])) {
continue;
}
require_once(self::$module_info[$module]['hooks'][$hook]['file']);
if (!is_callable(self::$module_info[$module]['hooks'][$hook]['func'])) {
throw new Error\Exception('Invalid hook \'' . $hook . '\' for module \'' . $module . '\'.');
}
$fn = self::$module_info[$module]['hooks'][$hook]['func'];
$fn($data);
}
}
/**
* Handle a valid request that ends with a trailing slash.
*
* This method removes the trailing slash and redirects to the resulting URL.
*
* @param Request $request The request to process by this controller method.
*
* @return RedirectResponse A redirection to the URI specified in the request, but without the trailing slash.
*/
public static function removeTrailingSlash(Request $request)
{
$pathInfo = $request->server->get('PATH_INFO');
$url = str_replace($pathInfo, rtrim($pathInfo, ' /'), $request->getRequestUri());
return new RedirectResponse($url, 308);
}
}

View File

@ -0,0 +1,196 @@
<?php
namespace SAML\Module;
use SAML\Auth\AuthenticationFactory;
use SAML\Configuration;
use SAML\Error\Exception;
use SAML\Module;
use SAML\Session;
use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
use Symfony\Component\HttpKernel\Controller\ControllerResolver as SymfonyControllerResolver;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Loader\YamlFileLoader;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* A class to resolve module controllers based on a given request.
*
* This class allows us to find a controller (a callable) that's configured for a given URL.
*
* @package SimpleSAML
*/
class ControllerResolver extends SymfonyControllerResolver implements ArgumentResolverInterface
{
/** @var ArgumentMetadataFactory */
protected $argFactory;
/** @var ContainerBuilder */
protected $container;
/** @var string */
protected $module;
/** @var array */
protected $params = [];
/** @var RouteCollection|null */
protected $routes;
/**
* Build a module controller resolver.
*
* @param string $module The name of the module.
*/
public function __construct($module)
{
parent::__construct();
$this->module = $module;
$loader = new YamlFileLoader(
new FileLocator(Module::getModuleDir($this->module))
);
$this->argFactory = new ArgumentMetadataFactory();
$this->container = new ContainerBuilder();
$this->container->autowire(AuthenticationFactory::class, AuthenticationFactory::class);
try {
$this->routes = $loader->load('routes.yaml');
$redirect = new Route(
'/{url}',
['_controller' => '\SimpleSAML\Module::removeTrailingSlash'],
['url' => '.*/$']
);
$this->routes->add('trailing-slash', $redirect);
$this->routes->addPrefix('/' . $this->module);
} catch (FileLocatorFileNotFoundException $e) {
}
}
/**
* Get the controller associated with a given URL, based on a request.
*
* This method searches for a 'routes.yaml' file in the root of the module, defining valid routes for the module
* and mapping them given controllers. It's input is a Request object with the request that we want to serve.
*
* @param Request $request The request we need to find a controller for.
*
* @return callable|false A controller (as a callable) that can handle the request, or false if we cannot find
* one suitable for the given request.
*/
public function getController(Request $request)
{
if ($this->routes === null) {
return false;
}
$ctxt = new RequestContext();
$ctxt->fromRequest($request);
try {
$matcher = new UrlMatcher($this->routes, $ctxt);
$this->params = $matcher->match($ctxt->getPathInfo());
list($class, $method) = explode('::', $this->params['_controller']);
$this->container->register($class, $class)->setAutowired(true)->setPublic(true);
$this->container->compile();
return [$this->container->get($class), $method];
} catch (ResourceNotFoundException $e) {
// no route defined matching this request
}
return false;
}
/**
* Get the arguments that should be passed to a controller from a given request.
*
* When the signature of the controller includes arguments with type Request, the given request will be passed to
* those. Otherwise, they'll be matched by name. If no value is available for a given argument, the method will
* try to set a default value or null, if possible.
*
* @param Request $request The request that holds all the information needed by the controller.
* @param callable $controller A controller for the given request.
*
* @return array An array of arguments that should be passed to the controller, in order.
*
* @throws Exception If we don't find anything suitable for an argument in the controller's
* signature.
*/
public function getArguments(Request $request, $controller)
{
$args = [];
$metadata = $this->argFactory->createArgumentMetadata($controller);
/** @var ArgumentMetadata $argMeta */
foreach ($metadata as $argMeta) {
if ($argMeta->getType() === Request::class) {
// add request argument
$args[] = $request;
continue;
}
$argName = $argMeta->getName();
if (array_key_exists($argName, $this->params)) {
// add argument by name
$args[] = $this->params[$argName];
continue;
}
// URL does not contain value for this argument
if ($argMeta->hasDefaultValue()) {
// it has a default value
$args[] = $argMeta->getDefaultValue();
}
// no default value
if ($argMeta->isNullable()) {
$args[] = null;
}
throw new Exception('Missing value for argument ' . $argName . '. This is probably a bug.');
}
return $args;
}
/**
* Set the configuration to use by the controllers.
*
* @param Configuration $config
* @return void
*/
public function setConfiguration(Configuration $config)
{
$this->container->set(Configuration::class, $config);
$this->container->register(Configuration::class)->setSynthetic(true)->setAutowired(true);
}
/**
* Set the session to use by the controllers.
*
* @param Session $session
* @return void
*/
public function setSession(Session $session)
{
$this->container->set(Session::class, $session);
$this->container->register(Session::class)
->setSynthetic(true)
->setAutowired(true)
->addMethodCall('setConfiguration', [new Reference(Configuration::class)]);
}
}

1181
libsrc/SAML/Session.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,168 @@
<?php
/**
* This file is part of SimpleSAMLphp. See the file COPYING in the
* root of the distribution for licence information.
*
* This file defines a base class for session handling.
* Instantiation of session handler objects should be done through
* the class method getSessionHandler().
*
* @author Olav Morken, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
namespace SAML;
use Exception;
use SAML\Error\CannotSetCookie;
abstract class SessionHandler
{
/**
* This static variable contains a reference to the current
* instance of the session handler. This variable will be NULL if
* we haven't instantiated a session handler yet.
*
* @var SessionHandler
*/
protected static $sessionHandler;
/**
* This function retrieves the current instance of the session handler.
* The session handler will be instantiated if this is the first call
* to this function.
*
* @return SessionHandler The current session handler.
*
* @throws Exception If we cannot instantiate the session handler.
*/
public static function getSessionHandler()
{
if (self::$sessionHandler === null) {
self::createSessionHandler();
}
return self::$sessionHandler;
}
/**
* This constructor is included in case it is needed in the
* future. Including it now allows us to write parent::__construct() in
* the subclasses of this class.
*/
protected function __construct()
{
}
/**
* Create a new session id.
*
* @return string The new session id.
*/
abstract public function newSessionId();
/**
* Retrieve the session ID saved in the session cookie, if there's one.
*
* @return string|null The session id saved in the cookie or null if no session cookie was set.
*/
abstract public function getCookieSessionId();
/**
* Retrieve the session cookie name.
*
* @return string The session cookie name.
*/
abstract public function getSessionCookieName();
/**
* Save the session.
*
* @param Session $session The session object we should save.
*/
abstract public function saveSession(Session $session);
/**
* Load the session.
*
* @param string|null $sessionId The ID of the session we should load, or null to use the default.
*
* @return Session|null The session object, or null if it doesn't exist.
*/
abstract public function loadSession($sessionId = null);
/**
* Check whether the session cookie is set.
*
* This function will only return false if is is certain that the cookie isn't set.
*
* @return bool True if it was set, false if not.
*/
abstract public function hasSessionCookie();
/**
* Set a session cookie.
*
* @param string $sessionName The name of the session.
* @param string|null $sessionID The session ID to use. Set to null to delete the cookie.
* @param array|null $cookieParams Additional parameters to use for the session cookie.
*
* @throws CannotSetCookie If we can't set the cookie.
*/
abstract public function setCookie($sessionName, $sessionID, array $cookieParams = null);
/**
* Initialize the session handler.
*
* This function creates an instance of the session handler which is
* selected in the 'store.type' configuration directive. If no
* session handler is selected, then we will fall back to the default
* PHP session handler.
*
* @return void
*
* @throws Exception If we cannot instantiate the session handler.
*/
private static function createSessionHandler()
{
$store = Store::getInstance();
if ($store === false) {
self::$sessionHandler = new SessionHandlerPHP();
} else {
/** @var Store $store At this point, $store can only be an object */
self::$sessionHandler = new SessionHandlerStore($store);
}
}
/**
* Get the cookie parameters that should be used for session cookies.
*
* @return array An array with the cookie parameters.
* @link http://www.php.net/manual/en/function.session-get-cookie-params.php
*/
public function getCookieParams()
{
$config = Configuration::getInstance();
return [
'lifetime' => $config->getInteger('session.cookie.lifetime', 0),
'path' => $config->getString('session.cookie.path', '/'),
'domain' => $config->getString('session.cookie.domain', null),
'secure' => $config->getBoolean('session.cookie.secure', false),
'samesite' => $config->getString('session.cookie.samesite', null),
'httponly' => true,
];
}
}

View File

@ -0,0 +1,173 @@
<?php
/**
* This file is part of SimpleSAMLphp. See the file COPYING in the root of the distribution for licence information.
*
* This file defines a base class for session handlers that need to store the session id in a cookie. It takes care of
* storing and retrieving the session id.
*
* @author Olav Morken, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
* @abstract
*/
namespace SAML;
use SAML\Error\CannotSetCookie;
use SAML\Utils;
abstract class SessionHandlerCookie extends SessionHandler
{
/**
* This variable contains the current session id.
*
* @var string|null
*/
private $session_id = null;
/**
* This variable contains the session cookie name.
*
* @var string
*/
protected $cookie_name;
/**
* This constructor initializes the session id based on what we receive in a cookie. We create a new session id and
* set a cookie with this id if we don't have a session id.
*/
protected function __construct()
{
// call the constructor in the base class in case it should become necessary in the future
parent::__construct();
$config = Configuration::getInstance();
$this->cookie_name = $config->getString('session.cookie.name', 'SimpleSAMLSessionID');
}
/**
* Create a new session id.
*
* @return string The new session id.
*/
public function newSessionId()
{
$this->session_id = self::createSessionID();
Session::createSession($this->session_id);
return $this->session_id;
}
/**
* Retrieve the session ID saved in the session cookie, if there's one.
*
* @return string|null The session id saved in the cookie or null if no session cookie was set.
*/
public function getCookieSessionId()
{
if ($this->session_id === null) {
if ($this->hasSessionCookie()) {
// attempt to retrieve the session id from the cookie
$this->session_id = $_COOKIE[$this->cookie_name];
}
// check if we have a valid session id
if (!self::isValidSessionID($this->session_id)) {
// invalid, disregard this session
return null;
}
}
return $this->session_id;
}
/**
* Retrieve the session cookie name.
*
* @return string The session cookie name.
*/
public function getSessionCookieName()
{
return $this->cookie_name;
}
/**
* This static function creates a session id. A session id consists of 32 random hexadecimal characters.
*
* @return string A random session id.
*/
private static function createSessionID()
{
return bin2hex(openssl_random_pseudo_bytes(16));
}
/**
* This static function validates a session id. A session id is valid if it only consists of characters which are
* allowed in a session id and it is the correct length.
*
* @param string $session_id The session ID we should validate.
*
* @return boolean True if this session ID is valid, false otherwise.
*/
private static function isValidSessionID($session_id)
{
if (!is_string($session_id)) {
return false;
}
if (strlen($session_id) != 32) {
return false;
}
if (preg_match('/[^0-9a-f]/', $session_id)) {
return false;
}
return true;
}
/**
* Check whether the session cookie is set.
*
* This function will only return false if is is certain that the cookie isn't set.
*
* @return boolean True if it was set, false otherwise.
*/
public function hasSessionCookie()
{
return array_key_exists($this->cookie_name, $_COOKIE);
}
/**
* Set a session cookie.
*
* @param string $sessionName The name of the session.
* @param string|null $sessionID The session ID to use. Set to null to delete the cookie.
* @param array|null $cookieParams Additional parameters to use for the session cookie.
* @return void
*
* @throws CannotSetCookie If we can't set the cookie.
*/
public function setCookie($sessionName, $sessionID, array $cookieParams = null)
{
assert(is_string($sessionName));
assert(is_string($sessionID) || $sessionID === null);
if ($cookieParams !== null) {
$params = array_merge($this->getCookieParams(), $cookieParams);
} else {
$params = $this->getCookieParams();
}
Utils\HTTP::setCookie($sessionName, $sessionID, $params, true);
}
}

View File

@ -0,0 +1,393 @@
<?php
/**
* This file is part of SimpleSAMLphp. See the file COPYING in the root of the distribution for licence information.
*
* This file defines a session handler which uses the default php session handler for storage.
*
* @author Olav Morken, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
namespace SAML;
use SAML\Error;
use SAML\Error\CannotSetCookie;
use SAML\Error\Exception;
use SAML\Utils;
class SessionHandlerPHP extends SessionHandler
{
/**
* This variable contains the session cookie name.
*
* @var string
*/
protected $cookie_name;
/**
* An associative array containing the details of a session existing previously to creating or loading one with this
* session handler. The keys of the array will be:
*
* - id: the ID of the session, as returned by session_id().
* - name: the name of the session, as returned by session_name().
* - cookie_params: the parameters of the session cookie, as returned by session_get_cookie_params().
*
* @var array
*/
private $previous_session = [];
/**
* Initialize the PHP session handling. This constructor is protected because it should only be called from
* \SimpleSAML\SessionHandler::createSessionHandler(...).
*/
protected function __construct()
{
// call the parent constructor in case it should become necessary in the future
parent::__construct();
$config = Configuration::getInstance();
$this->cookie_name = $config->getString('session.phpsession.cookiename', null);
if (session_status() === PHP_SESSION_ACTIVE) {
if (session_name() === $this->cookie_name || $this->cookie_name === null) {
Logger::warning(
'There is already a PHP session with the same name as SimpleSAMLphp\'s session, or the ' .
"'session.phpsession.cookiename' configuration option is not set. Make sure to set " .
"SimpleSAMLphp's cookie name with a value not used by any other applications."
);
}
/*
* We shouldn't have a session at this point, so it might be an application session. Save the details to
* retrieve it later and commit.
*/
$this->previous_session['cookie_params'] = session_get_cookie_params();
$this->previous_session['id'] = session_id();
$this->previous_session['name'] = session_name();
session_write_close();
}
if (empty($this->cookie_name)) {
$this->cookie_name = session_name();
} elseif (!headers_sent() || version_compare(PHP_VERSION, '7.2', '<')) {
session_name($this->cookie_name);
}
$params = $this->getCookieParams();
if (!headers_sent()) {
if (version_compare(PHP_VERSION, '7.3.0', '>=')) {
/** @psalm-suppress InvalidArgument This annotation may be removed in Psalm >=3.0.15 */
session_set_cookie_params([
'lifetime' => $params['lifetime'],
'path' => $params['path'],
'domain' => $params['domain'],
'secure' => $params['secure'],
'httponly' => $params['httponly'],
'samesite' => $params['samesite'],
]);
} else {
session_set_cookie_params(
$params['lifetime'],
$params['path'],
is_null($params['domain']) ? '' : $params['domain'],
$params['secure'],
$params['httponly']
);
}
}
$savepath = $config->getString('session.phpsession.savepath', null);
if (!empty($savepath)) {
session_save_path($savepath);
}
}
/**
* Restore a previously-existing session.
*
* Use this method to restore a previous PHP session existing before SimpleSAMLphp initialized its own session.
*
* WARNING: do not use this method directly, unless you know what you are doing. Calling this method directly,
* outside of \SimpleSAML\Session, could cause SimpleSAMLphp's session to be lost or mess the application's one. The
* session must always be saved properly before calling this method. If you don't understand what this is about,
* don't use this method.
*
* @return void
*/
public function restorePrevious()
{
if (empty($this->previous_session)) {
return; // nothing to do here
}
// close our own session
session_write_close();
session_name($this->previous_session['name']);
if (version_compare(PHP_VERSION, '7.3.0', '>=')) {
session_set_cookie_params($this->previous_session['cookie_params']);
} else {
session_set_cookie_params(
$this->previous_session['cookie_params']['lifetime'],
$this->previous_session['cookie_params']['path'],
$this->previous_session['cookie_params']['domain'],
$this->previous_session['cookie_params']['secure'],
$this->previous_session['cookie_params']['httponly']
);
}
session_id($this->previous_session['id']);
$this->previous_session = [];
@session_start();
/*
* At this point, we have restored a previously-existing session, so we can't continue to use our session here.
* Therefore, we need to load our session again in case we need it. We remove this handler from the parent
* class so that the handler is initialized again if we ever need to do something with the session.
*/
parent::$sessionHandler = null;
}
/**
* Create a new session id.
*
* @return string The new session id.
*/
public function newSessionId()
{
$sessionId = false;
if (function_exists('session_create_id') && version_compare(PHP_VERSION, '7.2', '<')) {
// generate new (secure) session id
$sid_length = (int) ini_get('session.sid_length');
$sid_bits_per_char = (int) ini_get('session.sid_bits_per_character');
if (($sid_length * $sid_bits_per_char) < 128) {
Logger::warning("Unsafe defaults used for sessionId generation!");
}
/**
* This annotation may be removed as soon as we start using vimeo/psalm 3.x
* @psalm-suppress TooFewArguments
*/
$sessionId = session_create_id();
}
if (!$sessionId) {
Logger::warning("Secure session ID generation failed, falling back to custom ID generation.");
$sessionId = bin2hex(openssl_random_pseudo_bytes(16));
}
Session::createSession($sessionId);
return $sessionId;
}
/**
* Retrieve the session ID saved in the session cookie, if there's one.
*
* @return string|null The session id saved in the cookie or null if no session cookie was set.
*
* @throws Exception If the cookie is marked as secure but we are not using HTTPS.
*/
public function getCookieSessionId()
{
if (!$this->hasSessionCookie()) {
return null; // there's no session cookie, can't return ID
}
if (version_compare(PHP_VERSION, '7.2', 'ge') && headers_sent()) {
// latest versions of PHP don't allow loading a session when output sent, get the ID from the cookie
return $_COOKIE[$this->cookie_name];
}
// do not rely on session_id() as it can return the ID of a previous session. Get it from the cookie instead.
session_id($_COOKIE[$this->cookie_name]);
$session_cookie_params = session_get_cookie_params();
if ($session_cookie_params['secure'] && !Utils\HTTP::isHTTPS()) {
throw new Exception('Session start with secure cookie not allowed on http.');
}
@session_start();
return session_id();
}
/**
* Retrieve the session cookie name.
*
* @return string The session cookie name.
*/
public function getSessionCookieName()
{
return $this->cookie_name;
}
/**
* Save the current session to the PHP session array.
*
* @param Session $session The session object we should save.
* @return void
*/
public function saveSession(Session $session)
{
$_SESSION['SimpleSAMLphp_SESSION'] = serialize($session);
}
/**
* Load the session from the PHP session array.
*
* @param string|null $sessionId The ID of the session we should load, or null to use the default.
*
* @return Session|null The session object, or null if it doesn't exist.
*
* @throws Exception If it wasn't possible to disable session cookies or we are trying to load a
* PHP session with a specific identifier and it doesn't match with the current session identifier.
*/
public function loadSession($sessionId = null)
{
assert(is_string($sessionId) || $sessionId === null);
if ($sessionId !== null) {
if (session_id() === '' && !(version_compare(PHP_VERSION, '7.2', 'ge') && headers_sent())) {
// session not initiated with getCookieSessionId(), start session without setting cookie
$ret = ini_set('session.use_cookies', '0');
if ($ret === false) {
throw new Exception('Disabling PHP option session.use_cookies failed.');
}
session_id($sessionId);
@session_start();
} elseif ($sessionId !== session_id()) {
throw new Exception('Cannot load PHP session with a specific ID.');
}
} elseif (session_id() === '') {
$this->getCookieSessionId();
}
if (!isset($_SESSION['SimpleSAMLphp_SESSION'])) {
return null;
}
$session = $_SESSION['SimpleSAMLphp_SESSION'];
assert(is_string($session));
$session = unserialize($session);
return ($session !== false) ? $session : null;
}
/**
* Check whether the session cookie is set.
*
* This function will only return false if is is certain that the cookie isn't set.
*
* @return boolean True if it was set, false otherwise.
*/
public function hasSessionCookie()
{
return array_key_exists($this->cookie_name, $_COOKIE);
}
/**
* Get the cookie parameters that should be used for session cookies.
*
* This function contains some adjustments from the default to provide backwards-compatibility.
*
* @return array The cookie parameters for our sessions.
* @throws Exception If both 'session.phpsession.limitedpath' and 'session.cookie.path' options
* are set at the same time in the configuration.
*@link http://www.php.net/manual/en/function.session-get-cookie-params.php
*
*/
public function getCookieParams()
{
$config = Configuration::getInstance();
$ret = parent::getCookieParams();
if ($config->hasValue('session.phpsession.limitedpath') && $config->hasValue('session.cookie.path')) {
throw new Exception(
'You cannot set both the session.phpsession.limitedpath and session.cookie.path options.'
);
} elseif ($config->hasValue('session.phpsession.limitedpath')) {
$ret['path'] = $config->getBoolean(
'session.phpsession.limitedpath',
false
) ? $config->getBasePath() : '/';
}
$ret['httponly'] = $config->getBoolean('session.phpsession.httponly', true);
if (version_compare(PHP_VERSION, '7.3.0', '<')) {
// in older versions of PHP we need a nasty hack to set RFC6265bis SameSite attribute
if ($ret['samesite'] !== null and !preg_match('/;\s+samesite/i', $ret['path'])) {
$ret['path'] .= '; SameSite=' . $ret['samesite'];
}
}
return $ret;
}
/**
* Set a session cookie.
*
* @param string $sessionName The name of the session.
* @param string|null $sessionID The session ID to use. Set to null to delete the cookie.
* @param array|null $cookieParams Additional parameters to use for the session cookie.
* @return void
*
* @throws CannotSetCookie If we can't set the cookie.
*/
public function setCookie($sessionName, $sessionID, array $cookieParams = null)
{
if ($cookieParams === null) {
$cookieParams = session_get_cookie_params();
}
if ($cookieParams['secure'] && !Utils\HTTP::isHTTPS()) {
throw new CannotSetCookie(
'Setting secure cookie on plain HTTP is not allowed.',
CannotSetCookie::SECURE_COOKIE
);
}
if (headers_sent()) {
throw new CannotSetCookie(
'Headers already sent.',
CannotSetCookie::HEADERS_SENT
);
}
if (session_id() !== '') {
// session already started, close it
session_write_close();
}
if (version_compare(PHP_VERSION, '7.3.0', '>=')) {
/** @psalm-suppress InvalidArgument This annotation may be removed in Psalm >=3.0.15 */
session_set_cookie_params($cookieParams);
} else {
session_set_cookie_params(
$cookieParams['lifetime'],
$cookieParams['path'],
is_null($cookieParams['domain']) ? '' : $cookieParams['domain'],
$cookieParams['secure'],
$cookieParams['httponly']
);
}
session_id(strval($sessionID));
@session_start();
}
}

View File

@ -0,0 +1,85 @@
<?php
/**
* Session storage in the data store.
*
* @package SimpleSAMLphp
*/
namespace SAML;
class SessionHandlerStore extends SessionHandlerCookie
{
/**
* The data store we save the session to.
*
* @var Store
*/
private $store;
/**
* Initialize the session.
*
* @param Store $store The store to use.
*/
protected function __construct(Store $store)
{
parent::__construct();
$this->store = $store;
}
/**
* Load a session from the data store.
*
* @param string|null $sessionId The ID of the session we should load, or null to use the default.
*
* @return Session|null The session object, or null if it doesn't exist.
*/
public function loadSession($sessionId = null)
{
assert(is_string($sessionId) || $sessionId === null);
if ($sessionId === null) {
$sessionId = $this->getCookieSessionId();
if ($sessionId === null) {
// no session cookie, nothing to load
return null;
}
}
$session = $this->store->get('session', $sessionId);
if ($session !== null) {
assert($session instanceof Session);
return $session;
}
return null;
}
/**
* Save a session to the data store.
*
* @param Session $session The session object we should save.
* @return void
*/
public function saveSession(Session $session)
{
if ($session->isTransient()) {
// transient session, nothing to save
return;
}
/** @var string $sessionId */
$sessionId = $session->getSessionId();
$config = Configuration::getInstance();
$sessionDuration = $config->getInteger('session.duration', 8 * 60 * 60);
$expire = time() + $sessionDuration;
$this->store->set('session', $sessionId, $session, $expire);
}
}

103
libsrc/SAML/Stats.php Normal file
View File

@ -0,0 +1,103 @@
<?php
namespace SAML;
/**
* Statistics handler class.
*
* This class is responsible for taking a statistics event and logging it.
*
* @package SimpleSAMLphp
*/
class Stats
{
/**
* Whether this class is initialized.
*
* @var boolean
*/
private static $initialized = false;
/**
* The statistics output callbacks.
*
* @var array
*/
private static $outputs = null;
/**
* Create an output from a configuration object.
*
* @param Configuration $config The configuration object.
*
* @return mixed A new instance of the configured class.
*/
private static function createOutput(Configuration $config)
{
$cls = $config->getString('class');
$cls = Module::resolveClass($cls, 'Stats\Output', '\SimpleSAML\Stats\Output');
$output = new $cls($config);
return $output;
}
/**
* Initialize the outputs.
*
* @return void
*/
private static function initOutputs()
{
$config = Configuration::getInstance();
$outputCfgs = $config->getConfigList('statistics.out');
self::$outputs = [];
foreach ($outputCfgs as $cfg) {
self::$outputs[] = self::createOutput($cfg);
}
}
/**
* Notify about an event.
*
* @param string $event The event.
* @param array $data Event data. Optional.
*
* @return void|boolean False if output is not enabled, void otherwise.
*/
public static function log($event, array $data = [])
{
assert(is_string($event));
assert(!isset($data['op']));
assert(!isset($data['time']));
assert(!isset($data['_id']));
if (!self::$initialized) {
self::initOutputs();
self::$initialized = true;
}
if (empty(self::$outputs)) {
// not enabled
return false;
}
$data['op'] = $event;
$data['time'] = microtime(true);
// the ID generation is designed to cluster IDs related in time close together
$int_t = (int) $data['time'];
$hd = openssl_random_pseudo_bytes(16);
$data['_id'] = sprintf('%016x%s', $int_t, bin2hex($hd));
foreach (self::$outputs as $out) {
$out->emit($data);
}
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace SAML\Stats;
use SAML\Configuration;
/**
* Interface for statistics outputs.
*
* @package SimpleSAMLphp
*/
abstract class Output
{
/**
* Initialize the output.
*
* @param Configuration $config The configuration for this output.
*/
public function __construct(Configuration $config)
{
// do nothing by default
}
/**
* Write a stats event.
*
* @param array $data The event.
*/
abstract public function emit(array $data);
}

116
libsrc/SAML/Store.php Normal file
View File

@ -0,0 +1,116 @@
<?php
namespace SAML;
use Exception;
use SAML\Error;
use SAML\Error\CriticalConfigurationError;
/**
* Base class for data stores.
*
* @package SimpleSAMLphp
*/
abstract class Store implements Utils\ClearableState
{
/**
* Our singleton instance.
*
* This is false if the data store isn't enabled, and null if we haven't attempted to initialize it.
*
* @var Store|false|null
*/
private static $instance;
/**
* Retrieve our singleton instance.
*
* @return Store|false The data store, or false if it isn't enabled.
*
* @throws CriticalConfigurationError
*/
public static function getInstance()
{
if (self::$instance !== null) {
return self::$instance;
}
$config = Configuration::getInstance();
$storeType = $config->getString('store.type', 'phpsession');
switch ($storeType) {
case 'phpsession':
// we cannot support advanced features with the PHP session store
self::$instance = false;
break;
case 'memcache':
self::$instance = new Store\Memcache();
break;
case 'sql':
self::$instance = new Store\SQL();
break;
case 'redis':
self::$instance = new Store\Redis();
break;
default:
// datastore from module
try {
$className = Module::resolveClass($storeType, 'Store', '\SimpleSAML\Store');
} catch (Exception $e) {
$c = $config->toArray();
$c['store.type'] = 'phpsession';
throw new CriticalConfigurationError(
"Invalid 'store.type' configuration option. Cannot find store '$storeType'.",
null,
$c
);
}
/** @var Store|false */
self::$instance = new $className();
}
return self::$instance;
}
/**
* Retrieve a value from the data store.
*
* @param string $type The data type.
* @param string $key The key.
*
* @return mixed|null The value.
*/
abstract public function get($type, $key);
/**
* Save a value to the data store.
*
* @param string $type The data type.
* @param string $key The key.
* @param mixed $value The value.
* @param int|null $expire The expiration time (unix timestamp), or null if it never expires.
*/
abstract public function set($type, $key, $value, $expire = null);
/**
* Delete a value from the data store.
*
* @param string $type The data type.
* @param string $key The key.
*/
abstract public function delete($type, $key);
/**
* Clear any SSP specific state, such as SSP environmental variables or cached internals.
* @return void
*/
public static function clearInternalState()
{
self::$instance = null;
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace SAML\Store;
use SAML\Configuration;
use SAML\Store;
/**
* A memcache based data store.
*
* @package SimpleSAMLphp
*/
class Memcache extends Store
{
/**
* This variable contains the session name prefix.
*
* @var string
*/
private $prefix;
/**
* This function implements the constructor for this class. It loads the Memcache configuration.
*/
protected function __construct()
{
$config = Configuration::getInstance();
$this->prefix = $config->getString('memcache_store.prefix', 'simpleSAMLphp');
}
/**
* Retrieve a value from the data store.
*
* @param string $type The data type.
* @param string $key The key.
* @return mixed|null The value.
*/
public function get($type, $key)
{
assert(is_string($type));
assert(is_string($key));
return \SAML\Memcache::get($this->prefix . '.' . $type . '.' . $key);
}
/**
* Save a value to the data store.
*
* @param string $type The data type.
* @param string $key The key.
* @param mixed $value The value.
* @param int|null $expire The expiration time (unix timestamp), or NULL if it never expires.
* @return void
*/
public function set($type, $key, $value, $expire = null)
{
assert(is_string($type));
assert(is_string($key));
assert($expire === null || (is_int($expire) && $expire > 2592000));
if ($expire === null) {
$expire = 0;
}
\SAML\Memcache::set($this->prefix . '.' . $type . '.' . $key, $value, $expire);
}
/**
* Delete a value from the data store.
*
* @param string $type The data type.
* @param string $key The key.
* @return void
*/
public function delete($type, $key)
{
assert(is_string($type));
assert(is_string($key));
\SAML\Memcache::delete($this->prefix . '.' . $type . '.' . $key);
}
}

132
libsrc/SAML/Store/Redis.php Normal file
View File

@ -0,0 +1,132 @@
<?php
namespace SAML\Store;
use Predis\Client;
use SAML\Configuration;
use SAML\Error;
use SAML\Store;
/**
* A data store using Redis to keep the data.
*
* @package SimpleSAMLphp
*/
class Redis extends Store
{
/** @var Client */
public $redis;
/**
* Initialize the Redis data store.
* @param Client|null $redis
*/
public function __construct($redis = null)
{
assert($redis === null || is_subclass_of($redis, Client::class));
if (!class_exists(Client::class)) {
throw new Error\CriticalConfigurationError('predis/predis is not available.');
}
if ($redis === null) {
$config = Configuration::getInstance();
$host = $config->getString('store.redis.host', 'localhost');
$port = $config->getInteger('store.redis.port', 6379);
$prefix = $config->getString('store.redis.prefix', 'SimpleSAMLphp');
$password = $config->getString('store.redis.password', '');
$database = $config->getInteger('store.redis.database', 0);
$redis = new Client(
[
'scheme' => 'tcp',
'host' => $host,
'port' => $port,
'database' => $database,
] + (!empty($password) ? ['password' => $password] : []),
[
'prefix' => $prefix,
]
);
}
$this->redis = $redis;
}
/**
* Deconstruct the Redis data store.
*/
public function __destruct()
{
if (method_exists($this->redis, 'disconnect')) {
$this->redis->disconnect();
}
}
/**
* Retrieve a value from the data store.
*
* @param string $type The type of the data.
* @param string $key The key to retrieve.
*
* @return mixed|null The value associated with that key, or null if there's no such key.
*/
public function get($type, $key)
{
assert(is_string($type));
assert(is_string($key));
$result = $this->redis->get("{$type}.{$key}");
if ($result === false || $result === null) {
return null;
}
return unserialize($result);
}
/**
* Save a value in the data store.
*
* @param string $type The type of the data.
* @param string $key The key to insert.
* @param mixed $value The value itself.
* @param int|null $expire The expiration time (unix timestamp), or null if it never expires.
* @return void
*/
public function set($type, $key, $value, $expire = null)
{
assert(is_string($type));
assert(is_string($key));
assert($expire === null || (is_int($expire) && $expire > 2592000));
$serialized = serialize($value);
if ($expire === null) {
$this->redis->set("{$type}.{$key}", $serialized);
} else {
// setex expire time is in seconds (not unix timestamp)
$this->redis->setex("{$type}.{$key}", $expire - time(), $serialized);
}
}
/**
* Delete an entry from the data store.
*
* @param string $type The type of the data
* @param string $key The key to delete.
* @return void
*/
public function delete($type, $key)
{
assert(is_string($type));
assert(is_string($key));
$this->redis->del("{$type}.{$key}");
}
}

411
libsrc/SAML/Store/SQL.php Normal file
View File

@ -0,0 +1,411 @@
<?php
namespace SAML\Store;
use Exception;
use PDO;
use PDOException;
use SAML\Configuration;
use SAML\Logger;
use SAML\Store;
/**
* A data store using a RDBMS to keep the data.
*
* @package SimpleSAMLphp
*/
class SQL extends Store
{
/**
* The PDO object for our database.
*
* @var PDO
*/
public $pdo;
/**
* Our database driver.
*
* @var string
*/
public $driver;
/**
* The prefix we should use for our tables.
*
* @var string
*/
public $prefix;
/**
* Associative array of table versions.
*
* @var array
*/
private $tableVersions;
/**
* Initialize the SQL data store.
*/
public function __construct()
{
$config = Configuration::getInstance();
$dsn = $config->getString('store.sql.dsn');
$username = $config->getString('store.sql.username', null);
$password = $config->getString('store.sql.password', null);
$options = $config->getArray('store.sql.options', null);
$this->prefix = $config->getString('store.sql.prefix', 'simpleSAMLphp');
try {
$this->pdo = new PDO($dsn, $username, $password, $options);
} catch (PDOException $e) {
throw new Exception("Database error: " . $e->getMessage());
}
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->driver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
if ($this->driver === 'mysql') {
$this->pdo->exec('SET time_zone = "+00:00"');
}
$this->initTableVersionTable();
$this->initKVTable();
}
/**
* Initialize the table-version table.
* @return void
*/
private function initTableVersionTable()
{
$this->tableVersions = [];
try {
$fetchTableVersion = $this->pdo->query('SELECT _name, _version FROM ' . $this->prefix . '_tableVersion');
} catch (PDOException $e) {
$this->pdo->exec(
'CREATE TABLE ' . $this->prefix .
'_tableVersion (_name VARCHAR(30) NOT NULL UNIQUE, _version INTEGER NOT NULL)'
);
return;
}
while (($row = $fetchTableVersion->fetch(PDO::FETCH_ASSOC)) !== false) {
$this->tableVersions[$row['_name']] = (int) $row['_version'];
}
}
/**
* Initialize key-value table.
* @return void
*/
private function initKVTable()
{
$current_version = $this->getTableVersion('kvstore');
$text_t = 'TEXT';
$time_field = 'TIMESTAMP';
if ($this->driver === 'mysql') {
// TEXT data type has size constraints that can be hit at some point, so we use LONGTEXT instead
$text_t = 'LONGTEXT';
}
if ($this->driver === 'sqlsrv') {
// TIMESTAMP will not work for MSSQL. TIMESTAMP is automatically generated and cannot be inserted
// so we use DATETIME instead
$time_field = 'DATETIME';
}
/**
* Queries for updates, grouped by version.
* New updates can be added as a new array in this array
*/
$table_updates = [
[
'CREATE TABLE ' . $this->prefix .
'_kvstore (_type VARCHAR(30) NOT NULL, _key VARCHAR(50) NOT NULL, _value ' . $text_t .
' NOT NULL, _expire ' . $time_field . ', PRIMARY KEY (_key, _type))',
$this->driver === 'sqlite' || $this->driver === 'sqlsrv' || $this->driver === 'pgsql' ?
'CREATE INDEX ' . $this->prefix . '_kvstore_expire ON ' . $this->prefix . '_kvstore (_expire)' :
'ALTER TABLE ' . $this->prefix . '_kvstore ADD INDEX ' . $this->prefix . '_kvstore_expire (_expire)'
],
/**
* This upgrade removes the default NOT NULL constraint on the _expire field in MySQL.
* Because SQLite does not support field alterations, the approach is to:
* Create a new table without the NOT NULL constraint
* Copy the current data to the new table
* Drop the old table
* Rename the new table correctly
* Read the index
*/
[
'CREATE TABLE ' . $this->prefix .
'_kvstore_new (_type VARCHAR(30) NOT NULL, _key VARCHAR(50) NOT NULL, _value ' . $text_t .
' NOT NULL, _expire ' . $time_field . ' NULL, PRIMARY KEY (_key, _type))',
'INSERT INTO ' . $this->prefix . '_kvstore_new SELECT * FROM ' . $this->prefix . '_kvstore',
'DROP TABLE ' . $this->prefix . '_kvstore',
// FOR MSSQL use EXEC sp_rename to rename a table (RENAME won't work)
$this->driver === 'sqlsrv' ?
'EXEC sp_rename ' . $this->prefix . '_kvstore_new, ' . $this->prefix . '_kvstore' :
'ALTER TABLE ' . $this->prefix . '_kvstore_new RENAME TO ' . $this->prefix . '_kvstore',
$this->driver === 'sqlite' || $this->driver === 'sqlsrv' || $this->driver === 'pgsql' ?
'CREATE INDEX ' . $this->prefix . '_kvstore_expire ON ' . $this->prefix . '_kvstore (_expire)' :
'ALTER TABLE ' . $this->prefix . '_kvstore ADD INDEX ' . $this->prefix . '_kvstore_expire (_expire)'
]
];
$latest_version = count($table_updates);
if ($current_version == $latest_version) {
return;
}
// Only run queries for after the current version
$updates_to_run = array_slice($table_updates, $current_version);
foreach ($updates_to_run as $version_updates) {
foreach ($version_updates as $query) {
$this->pdo->exec($query);
}
}
$this->setTableVersion('kvstore', $latest_version);
}
/**
* Get table version.
*
* @param string $name Table name.
*
* @return int The table version, or 0 if the table doesn't exist.
*/
public function getTableVersion($name)
{
assert(is_string($name));
if (!isset($this->tableVersions[$name])) {
return 0;
}
return $this->tableVersions[$name];
}
/**
* Set table version.
*
* @param string $name Table name.
* @param int $version Table version.
* @return void
*/
public function setTableVersion($name, $version)
{
assert(is_string($name));
assert(is_int($version));
$this->insertOrUpdate(
$this->prefix . '_tableVersion',
['_name'],
['_name' => $name, '_version' => $version]
);
$this->tableVersions[$name] = $version;
}
/**
* Insert or update a key-value in the store.
*
* Since various databases implement different methods for doing this, we abstract it away here.
*
* @param string $table The table we should update.
* @param array $keys The key columns.
* @param array $data Associative array with columns.
* @return void
*/
public function insertOrUpdate($table, array $keys, array $data)
{
assert(is_string($table));
$colNames = '(' . implode(', ', array_keys($data)) . ')';
$values = 'VALUES(:' . implode(', :', array_keys($data)) . ')';
switch ($this->driver) {
case 'mysql':
$query = 'REPLACE INTO ' . $table . ' ' . $colNames . ' ' . $values;
$query = $this->pdo->prepare($query);
$query->execute($data);
break;
case 'sqlite':
$query = 'INSERT OR REPLACE INTO ' . $table . ' ' . $colNames . ' ' . $values;
$query = $this->pdo->prepare($query);
$query->execute($data);
break;
default:
$updateCols = [];
$condCols = [];
$condData = [];
foreach ($data as $col => $value) {
$tmp = $col . ' = :' . $col;
if (in_array($col, $keys, true)) {
$condCols[] = $tmp;
$condData[$col] = $value;
} else {
$updateCols[] = $tmp;
}
}
$selectQuery = 'SELECT * FROM ' . $table . ' WHERE ' . implode(' AND ', $condCols);
$selectQuery = $this->pdo->prepare($selectQuery);
$selectQuery->execute($condData);
if (count($selectQuery->fetchAll()) > 0) {
// Update
$insertOrUpdateQuery = 'UPDATE ' . $table . ' SET ' . implode(',', $updateCols);
$insertOrUpdateQuery .= ' WHERE ' . implode(' AND ', $condCols);
$insertOrUpdateQuery = $this->pdo->prepare($insertOrUpdateQuery);
} else {
// Insert
$insertOrUpdateQuery = 'INSERT INTO ' . $table . ' ' . $colNames . ' ' . $values;
$insertOrUpdateQuery = $this->pdo->prepare($insertOrUpdateQuery);
}
$insertOrUpdateQuery->execute($data);
break;
}
}
/**
* Clean the key-value table of expired entries.
* @return void
*/
private function cleanKVStore()
{
Logger::debug('store.sql: Cleaning key-value store.');
$query = 'DELETE FROM ' . $this->prefix . '_kvstore WHERE _expire < :now';
$params = ['now' => gmdate('Y-m-d H:i:s')];
$query = $this->pdo->prepare($query);
$query->execute($params);
}
/**
* Retrieve a value from the data store.
*
* @param string $type The type of the data.
* @param string $key The key to retrieve.
*
* @return mixed|null The value associated with that key, or null if there's no such key.
*/
public function get($type, $key)
{
assert(is_string($type));
assert(is_string($key));
if (strlen($key) > 50) {
$key = sha1($key);
}
$query = 'SELECT _value FROM ' . $this->prefix .
'_kvstore WHERE _type = :type AND _key = :key AND (_expire IS NULL OR _expire > :now)';
$params = ['type' => $type, 'key' => $key, 'now' => gmdate('Y-m-d H:i:s')];
$query = $this->pdo->prepare($query);
$query->execute($params);
$row = $query->fetch(PDO::FETCH_ASSOC);
if ($row === false) {
return null;
}
$value = $row['_value'];
if (is_resource($value)) {
$value = stream_get_contents($value);
}
$value = urldecode($value);
$value = unserialize($value);
if ($value === false) {
return null;
}
return $value;
}
/**
* Save a value in the data store.
*
* @param string $type The type of the data.
* @param string $key The key to insert.
* @param mixed $value The value itself.
* @param int|null $expire The expiration time (unix timestamp), or null if it never expires.
* @return void
*/
public function set($type, $key, $value, $expire = null)
{
assert(is_string($type));
assert(is_string($key));
assert($expire === null || (is_int($expire) && $expire > 2592000));
if (rand(0, 1000) < 10) {
$this->cleanKVStore();
}
if (strlen($key) > 50) {
$key = sha1($key);
}
if ($expire !== null) {
$expire = gmdate('Y-m-d H:i:s', $expire);
}
$value = serialize($value);
$value = rawurlencode($value);
$data = [
'_type' => $type,
'_key' => $key,
'_value' => $value,
'_expire' => $expire,
];
$this->insertOrUpdate($this->prefix . '_kvstore', ['_type', '_key'], $data);
}
/**
* Delete an entry from the data store.
*
* @param string $type The type of the data
* @param string $key The key to delete.
* @return void
*/
public function delete($type, $key)
{
assert(is_string($type));
assert(is_string($key));
if (strlen($key) > 50) {
$key = sha1($key);
}
$data = [
'_type' => $type,
'_key' => $key,
];
$query = 'DELETE FROM ' . $this->prefix . '_kvstore WHERE _type=:_type AND _key=:_key';
$query = $this->pdo->prepare($query);
$query->execute($data);
}
}

886
libsrc/SAML/Utilities.php Normal file
View File

@ -0,0 +1,886 @@
<?php
namespace SAML;
use DOMElement;
use DOMNode;
use Exception;
use SAML\Auth\State;
use SAML\Error\Error;
use SAML\Utils\Arrays;
use SAML\Utils\Attributes;
use SAML\Utils\Auth;
use SAML\Utils\Config;
use SAML\Utils\Config\Metadata;
use SAML\Utils\Crypto;
use SAML\Utils\HTTP;
use SAML\Utils\Net;
use SAML\Utils\Random;
use SAML\Utils\System;
use SAML\Utils\Time;
use SAML\Utils\XML;
use SAML\XML\Validator;
use SAML2\Utils;
/**
* Misc static functions that is used several places.in example parsing and id generation.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*
* @deprecated This entire class will be removed in SimpleSAMLphp 2.0.
*/
class Utilities
{
/**
* @deprecated This property will be removed in SSP 2.0. Please use SimpleSAML\Logger::isErrorMasked() instead.
* @var int
*/
public static $logMask = 0;
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getSelfHost() instead.
* @return string
*/
public static function getSelfHost()
{
return HTTP::getSelfHost();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getSelfURLHost() instead.
* @return string
*/
public static function selfURLhost()
{
return HTTP::getSelfURLHost();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::isHTTPS() instead.
* @return bool
*/
public static function isHTTPS()
{
return HTTP::isHTTPS();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getSelfURLNoQuery()
* instead.
* @return string
*/
public static function selfURLNoQuery()
{
return HTTP::getSelfURLNoQuery();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getSelfHostWithPath()
* instead.
* @return string
*/
public static function getSelfHostWithPath()
{
return HTTP::getSelfHostWithPath();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getFirstPathElement()
* instead.
* @param bool $trailingslash
* @return string
*/
public static function getFirstPathElement($trailingslash = true)
{
return HTTP::getFirstPathElement($trailingslash);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getSelfURL() instead.
* @return string
*/
public static function selfURL()
{
return HTTP::getSelfURL();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getBaseURL() instead.
* @return string
*/
public static function getBaseURL()
{
return HTTP::getBaseURL();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::addURLParameters() instead.
* @param string $url
* @param array $parameters
* @return string
*/
public static function addURLparameter($url, $parameters)
{
return HTTP::addURLParameters($url, $parameters);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Utils\HTTP::checkURLAllowed() instead.
* @param string $url
* @param array|null $trustedSites
* @return string
*/
public static function checkURLAllowed($url, array $trustedSites = null)
{
return HTTP::checkURLAllowed($url, $trustedSites);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Auth\State::parseStateID() instead.
* @param string $stateId
* @return array
*/
public static function parseStateID($stateId)
{
return State::parseStateID($stateId);
}
/**
* @deprecated This method will be removed in SSP 2.0.
* @param string|null $start
* @param string|null $end
* @return bool
*/
public static function checkDateConditions($start = null, $end = null)
{
$currentTime = time();
if (!empty($start)) {
$startTime = Utils::xsDateTimeToTimestamp($start);
// Allow for a 10 minute difference in Time
if (($startTime < 0) || (($startTime - 600) > $currentTime)) {
return false;
}
}
if (!empty($end)) {
$endTime = Utils::xsDateTimeToTimestamp($end);
if (($endTime < 0) || ($endTime <= $currentTime)) {
return false;
}
}
return true;
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Random::generateID() instead.
* @return string
*/
public static function generateID()
{
return Random::generateID();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Utils\Time::generateTimestamp()
* instead.
* @param int|null $instant
* @return string
*/
public static function generateTimestamp($instant = null)
{
return Time::generateTimestamp($instant);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Utils\Time::parseDuration() instead.
* @param string $duration
* @param int|null $timestamp
* @return int
*/
public static function parseDuration($duration, $timestamp = null)
{
return Time::parseDuration($duration, $timestamp);
}
/**
* @param string $trackId
* @param int|null $errorCode
* @param Exception|null $e
* @return void
*@throws Error
* @deprecated This method will be removed in SSP 2.0. Please raise a SimpleSAML\Error\Error exception instead.
*/
public static function fatalError($trackId = 'na', $errorCode = null, Exception $e = null)
{
throw new Error($errorCode, $e);
}
/**
* @deprecated This method will be removed in version 2.0. Use SimpleSAML\Utils\Net::ipCIDRcheck() instead.
* @param string $cidr
* @param string|null $ip
* @return bool
*/
public static function ipCIDRcheck($cidr, $ip = null)
{
return Net::ipCIDRcheck($cidr, $ip);
}
/**
* @param string $url
* @param array $parameters
* @return void
*/
private static function doRedirect($url, $parameters = [])
{
assert(is_string($url));
assert(!empty($url));
assert(is_array($parameters));
if (!empty($parameters)) {
$url = self::addURLparameter($url, $parameters);
}
/* Set the HTTP result code. This is either 303 See Other or
* 302 Found. HTTP 303 See Other is sent if the HTTP version
* is HTTP/1.1 and the request type was a POST request.
*/
if ($_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.1' &&
$_SERVER['REQUEST_METHOD'] === 'POST'
) {
$code = 303;
} else {
$code = 302;
}
if (strlen($url) > 2048) {
Logger::warning('Redirecting to a URL longer than 2048 bytes.');
}
// Set the location header
header('Location: '.$url, true, $code);
// Disable caching of this response
header('Pragma: no-cache');
header('Cache-Control: no-cache, must-revalidate');
// Show a minimal web page with a clickable link to the URL
echo '<?xml version="1.0" encoding="UTF-8"?>'."\n";
echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"'.
' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'."\n";
echo '<html xmlns="http://www.w3.org/1999/xhtml">';
echo '<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Redirect</title>
</head>';
echo '<body>';
echo '<h1>Redirect</h1>';
echo '<p>';
echo 'You were redirected to: ';
echo '<a id="redirlink" href="'.
htmlspecialchars($url).'">'.htmlspecialchars($url).'</a>';
echo '<script type="text/javascript">document.getElementById("redirlink").focus();</script>';
echo '</p>';
echo '</body>';
echo '</html>';
// End script execution
exit;
}
/**
* @deprecated 1.12.0 This method will be removed from the API. Instead, use the redirectTrustedURL() or
* redirectUntrustedURL() functions accordingly.
* @param string $url
* @param array $parameters
* @param array|null $allowed_redirect_hosts
* @return void
*/
public static function redirect($url, $parameters = [], $allowed_redirect_hosts = null)
{
assert(is_string($url));
assert(strlen($url) > 0);
assert(is_array($parameters));
if ($allowed_redirect_hosts !== null) {
$url = self::checkURLAllowed($url, $allowed_redirect_hosts);
} else {
$url = self::normalizeURL($url);
}
self::doRedirect($url, $parameters);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::redirectTrustedURL()
* instead.
* @param string $url
* @param array $parameters
* @return void
*/
public static function redirectTrustedURL($url, $parameters = [])
{
HTTP::redirectTrustedURL($url, $parameters);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::redirectUntrustedURL()
* instead.
* @param string $url
* @param array $parameters
* @return void
*/
public static function redirectUntrustedURL($url, $parameters = [])
{
HTTP::redirectUntrustedURL($url, $parameters);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Arrays::transpose() instead.
* @param array $in
* @return mixed
*/
public static function transposeArray($in)
{
return Arrays::transpose($in);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::isDOMNodeOfType()
* instead.
* @param DOMNode $element
* @param string $name
* @param string $nsURI
* @return bool
*/
public static function isDOMElementOfType(DOMNode $element, $name, $nsURI)
{
return XML::isDOMNodeOfType($element, $name, $nsURI);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::getDOMChildren() instead.
* @param DOMElement $element
* @param string $localName
* @param string $namespaceURI
* @return array
*/
public static function getDOMChildren(DOMElement $element, $localName, $namespaceURI)
{
return XML::getDOMChildren($element, $localName, $namespaceURI);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::getDOMText() instead.
* @param DOMNode $element
* @return string
*/
public static function getDOMText($element)
{
return XML::getDOMText($element);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getAcceptLanguage()
* instead.
* @return array
*/
public static function getAcceptLanguage()
{
return HTTP::getAcceptLanguage();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::isValid() instead.
* @param string $xml
* @param string $schema
* @return string|false
*/
public static function validateXML($xml, $schema)
{
$result = XML::isValid($xml, $schema);
return ($result === true) ? '' : $result;
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::checkSAMLMessage() instead.
* @param string $message
* @param string $type
* @return void
*/
public static function validateXMLDocument($message, $type)
{
XML::checkSAMLMessage($message, $type);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use openssl_random_pseudo_bytes() instead.
* @param int $length
* @return string
*/
public static function generateRandomBytes($length)
{
assert(is_int($length));
return openssl_random_pseudo_bytes($length);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use bin2hex() instead.
* @param string $bytes
* @return string
*/
public static function stringToHex($bytes)
{
$ret = '';
for ($i = 0; $i < strlen($bytes); $i++) {
$ret .= sprintf('%02x', ord($bytes[$i]));
}
return $ret;
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\System::resolvePath() instead.
* @param string $path
* @param string|null $base
* @return string
*/
public static function resolvePath($path, $base = null)
{
return System::resolvePath($path, $base);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::resolveURL() instead.
* @param string $url
* @param string|null $base
* @return string
*/
public static function resolveURL($url, $base = null)
{
return HTTP::resolveURL($url, $base);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::normalizeURL() instead.
* @param string $url
* @return string
*/
public static function normalizeURL($url)
{
return HTTP::normalizeURL($url);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::parseQueryString() instead.
* @param string $query_string
* @return array
*/
public static function parseQueryString($query_string)
{
return HTTP::parseQueryString($query_string);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use
* SimpleSAML\Utils\Attributes::normalizeAttributesArray() instead.
* @param array $attributes
* @return array
*/
public static function parseAttributes($attributes)
{
return Attributes::normalizeAttributesArray($attributes);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Config::getSecretSalt() instead.
* @return string
*/
public static function getSecretSalt()
{
return Config::getSecretSalt();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please call error_get_last() directly.
* @return string
*/
public static function getLastError()
{
if (!function_exists('error_get_last')) {
return '[Cannot get error message]';
}
$error = error_get_last();
if ($error === null) {
return '[No error message found]';
}
return $error['message'];
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Config::getCertPath() instead.
* @param string $path
* @return string
*/
public static function resolveCert($path)
{
return Config::getCertPath($path);
}
/**
* @param Configuration $metadata
* @param bool $required
* @param string $prefix
* @return array|null
*@deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Crypto::loadPublicKey() instead.
*/
public static function loadPublicKey(Configuration $metadata, $required = false, $prefix = '')
{
return Crypto::loadPublicKey($metadata, $required, $prefix);
}
/**
* @param Configuration $metadata
* @param bool $required
* @param string $prefix
* @return array|null
*@deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Crypto::loadPrivateKey() instead.
*/
public static function loadPrivateKey(Configuration $metadata, $required = false, $prefix = '')
{
return Crypto::loadPrivateKey($metadata, $required, $prefix);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::formatDOMElement() instead.
* @param DOMElement $root
* @param string $indentBase
* @return void
*/
public static function formatDOMElement(DOMElement $root, $indentBase = '')
{
XML::formatDOMElement($root, $indentBase);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::formatXMLString() instead.
* @param string $xml
* @param string $indentBase
* @return string
*/
public static function formatXMLString($xml, $indentBase = '')
{
return XML::formatXMLString($xml, $indentBase);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Arrays::arrayize() instead.
* @param mixed $data
* @param int $index
* @return array
*/
public static function arrayize($data, $index = 0)
{
return Arrays::arrayize($data, $index);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Auth::isAdmin() instead.
* @return bool
*/
public static function isAdmin()
{
return Auth::isAdmin();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Auth::getAdminLoginURL instead();
* @param string|null $returnTo
* @return string
*/
public static function getAdminLoginURL($returnTo = null)
{
return Auth::getAdminLoginURL($returnTo);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Auth::requireAdmin() instead.
* @return void
*/
public static function requireAdmin()
{
Auth::requireAdmin();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::submitPOSTData() instead.
* @param string $destination
* @param array $post
* @return void
*/
public static function postRedirect($destination, $post)
{
HTTP::submitPOSTData($destination, $post);
}
/**
* @deprecated This method will be removed in SSP 2.0. PLease use SimpleSAML\Utils\HTTP::getPOSTRedirectURL()
* instead.
* @param string $destination
* @param array $post
* @return string
*/
public static function createPostRedirectLink($destination, $post)
{
return HTTP::getPOSTRedirectURL($destination, $post);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getPOSTRedirectURL()
* instead.
* @param string $destination
* @param array $post
* @return string
* @throws Error If the current session is a transient session.
*/
public static function createHttpPostRedirectLink($destination, $post)
{
assert(is_string($destination));
assert(is_array($post));
$postId = Random::generateID();
$postData = [
'post' => $post,
'url' => $destination,
];
$session = Session::getSessionFromRequest();
if ($session->isTransient()) {
throw new Error('Cannot save data to a transient session');
}
$session->setData('core_postdatalink', $postId, $postData);
$redirInfo = base64_encode(Crypto::aesEncrypt($session->getSessionId().':'.$postId));
$url = Module::getModuleURL('core/postredirect.php', ['RedirInfo' => $redirInfo]);
$url = preg_replace("#^https:#", "http:", $url);
return $url;
}
/**
* @deprecated This method will be removed in SSP 2.0.
* @param string $certificate
* @param string $caFile
* @return void
*/
public static function validateCA($certificate, $caFile)
{
Validator::validateCertificate($certificate, $caFile);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Time::initTimezone() instead.
* @return void
*/
public static function initTimezone()
{
Time::initTimezone();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\System::writeFile() instead.
* @param string $filename
* @param string $data
* @param int $mode
* @return void
*/
public static function writeFile($filename, $data, $mode = 0600)
{
System::writeFile($filename, $data, $mode);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\System::getTempDir instead.
* @return string
*/
public static function getTempDir()
{
return System::getTempDir();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Logger::maskErrors() instead.
* @param int $mask
* @return void
*/
public static function maskErrors($mask)
{
Logger::maskErrors($mask);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Logger::popErrorMask() instead.
* @return void
*/
public static function popErrorMask()
{
Logger::popErrorMask();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use
* SimpleSAML\Utils\Config\Metadata::getDefaultEndpoint() instead.
* @param array $endpoints
* @param array|null $bindings
* @return array|null
*/
public static function getDefaultEndpoint(array $endpoints, array $bindings = null)
{
return Metadata::getDefaultEndpoint($endpoints, $bindings);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::checkSessionCookie()
* instead.
* @param string|null $retryURL
* @return void
*/
public static function checkCookie($retryURL = null)
{
HTTP::checkSessionCookie($retryURL);
}
/**
* @param string|DOMElement $message
* @param string $type
* @return void
*@deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::debugSAMLMessage() instead.
*/
public static function debugMessage($message, $type)
{
XML::debugSAMLMessage($message, $type);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::fetch() instead.
* @param string $path
* @param array $context
* @param bool $getHeaders
* @return string|array
*/
public static function fetch($path, $context = [], $getHeaders = false)
{
return HTTP::fetch($path, $context, $getHeaders);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Crypto::aesEncrypt() instead.
* @param string $clear
* @return string
*/
public static function aesEncrypt($clear)
{
return Crypto::aesEncrypt($clear);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Crypto::aesDecrypt() instead.
* @param string $encData
* @return string
*/
public static function aesDecrypt($encData)
{
return Crypto::aesDecrypt($encData);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\System::getOS() instead.
* @return bool
*/
public static function isWindowsOS()
{
return System::getOS() === System::WINDOWS;
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::setCookie() instead.
* @param string $name
* @param string|null $value
* @param array|null $params
* @param bool $throw
* @return void
*/
public static function setCookie($name, $value, array $params = null, $throw = true)
{
HTTP::setCookie($name, $value, $params, $throw);
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace SAML\Utils;
/**
* Array-related utility methods.
*
* @package SimpleSAMLphp
*/
class Arrays
{
/**
* Put a non-array variable into an array.
*
* @param mixed $data The data to place into an array.
* @param mixed $index The index or key of the array where to place the data. Defaults to 0.
*
* @return array An array with one element containing $data, with key $index, or $data itself if it's already an
* array.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function arrayize($data, $index = 0)
{
return (is_array($data)) ? $data : [$index => $data];
}
/**
* This function transposes a two-dimensional array, so that $a['k1']['k2'] becomes $a['k2']['k1'].
*
* @param array $array The two-dimensional array to transpose.
*
* @return mixed The transposed array, or false if $array is not a valid two-dimensional array.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
*/
public static function transpose($array)
{
if (!is_array($array)) {
return false;
}
$ret = [];
foreach ($array as $k1 => $a2) {
if (!is_array($a2)) {
return false;
}
foreach ($a2 as $k2 => $v) {
if (!array_key_exists($k2, $ret)) {
$ret[$k2] = [];
}
$ret[$k2][$k1] = $v;
}
}
return $ret;
}
}

View File

@ -0,0 +1,136 @@
<?php
namespace SAML\Utils;
use InvalidArgumentException;
use SAML\Error;
use SAML\Error\Exception;
/**
* Attribute-related utility methods.
*
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
* @package SimpleSAML
*/
class Attributes
{
/**
* Look for an attribute in a normalized attributes array, failing if it's not there.
*
* @param array $attributes The normalized array containing attributes.
* @param string $expected The name of the attribute we are looking for.
* @param bool $allow_multiple Whether to allow multiple values in the attribute or not.
*
* @return mixed The value of the attribute we are expecting. If the attribute has multiple values and
* $allow_multiple is set to true, the first value will be returned.
*
* @throws InvalidArgumentException If $attributes is not an array or $expected is not a string.
* @throws Exception If the expected attribute was not found in the attributes array.
*/
public static function getExpectedAttribute($attributes, $expected, $allow_multiple = false)
{
if (!is_array($attributes)) {
throw new InvalidArgumentException(
'The attributes array is not an array, it is: ' . print_r($attributes, true) . '.'
);
}
if (!is_string($expected)) {
throw new InvalidArgumentException(
'The expected attribute is not a string, it is: ' . print_r($expected, true) . '.'
);
}
if (!array_key_exists($expected, $attributes)) {
throw new Exception("No such attribute '" . $expected . "' found.");
}
$attribute = $attributes[$expected];
if (!is_array($attribute)) {
throw new InvalidArgumentException('The attributes array is not normalized, values should be arrays.');
}
if (count($attribute) === 0) {
throw new Exception("Empty attribute '" . $expected . "'.'");
} elseif (count($attribute) > 1) {
if ($allow_multiple === false) {
throw new Exception(
'More than one value found for the attribute, multiple values not allowed.'
);
}
}
return reset($attribute);
}
/**
* Validate and normalize an array with attributes.
*
* This function takes in an associative array with attributes, and parses and validates
* this array. On success, it will return a normalized array, where each attribute name
* is an index to an array of one or more strings. On failure an exception will be thrown.
* This exception will contain an message describing what is wrong.
*
* @param array $attributes The array containing attributes that we should validate and normalize.
*
* @return array The normalized attributes array.
* @throws InvalidArgumentException If input is not an array, array keys are not strings or attribute values are
* not strings.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function normalizeAttributesArray($attributes)
{
if (!is_array($attributes)) {
throw new InvalidArgumentException(
'The attributes array is not an array, it is: ' . print_r($attributes, true) . '".'
);
}
$newAttrs = [];
foreach ($attributes as $name => $values) {
if (!is_string($name)) {
throw new InvalidArgumentException('Invalid attribute name: "' . print_r($name, true) . '".');
}
$values = Arrays::arrayize($values);
foreach ($values as $value) {
if (!is_string($value)) {
throw new InvalidArgumentException(
'Invalid attribute value for attribute ' . $name . ': "' . print_r($value, true) . '".'
);
}
}
$newAttrs[$name] = $values;
}
return $newAttrs;
}
/**
* Extract an attribute's namespace, or revert to default.
*
* This function takes in a namespaced attribute name and splits it in a namespace/attribute name tuple.
* When no namespace is found in the attribute name, it will be namespaced with the default namespace.
* This default namespace can be overriden by supplying a second parameter to this function.
*
* @param string $name The namespaced attribute name.
* @param string $defaultns The default namespace that should be used when no namespace is found.
*
* @return array The attribute name, split to the namespace and the actual attribute name.
*/
public static function getAttributeNamespace($name, $defaultns)
{
$slash = strrpos($name, '/');
if ($slash !== false) {
$defaultns = substr($name, 0, $slash);
$name = substr($name, $slash + 1);
}
return [htmlspecialchars($defaultns), htmlspecialchars($name)];
}
}

101
libsrc/SAML/Utils/Auth.php Normal file
View File

@ -0,0 +1,101 @@
<?php
namespace SAML\Utils;
use InvalidArgumentException;
use SAML\Auth as Authentication;
use SAML\Error;
use SAML\Error\Exception;
use SAML\Module;
use SAML\Session;
/**
* Auth-related utility methods.
*
* @package SimpleSAMLphp
*/
class Auth
{
/**
* Retrieve a admin login URL.
*
* @param string|NULL $returnTo The URL the user should arrive on after admin authentication. Defaults to null.
*
* @return string A URL which can be used for admin authentication.
* @throws InvalidArgumentException If $returnTo is neither a string nor null.
*/
public static function getAdminLoginURL($returnTo = null)
{
if (!(is_string($returnTo) || is_null($returnTo))) {
throw new InvalidArgumentException('Invalid input parameters.');
}
if ($returnTo === null) {
$returnTo = HTTP::getSelfURL();
}
return Module::getModuleURL('core/login-admin.php', ['ReturnTo' => $returnTo]);
}
/**
* Retrieve a admin logout URL.
*
* @param string|NULL $returnTo The URL the user should arrive on after admin authentication. Defaults to null.
*
* @return string A URL which can be used for logging out.
* @throws InvalidArgumentException If $returnTo is neither a string nor null.
*/
public static function getAdminLogoutURL($returnTo = null)
{
if (!(is_string($returnTo) || is_null($returnTo))) {
throw new InvalidArgumentException('Invalid input parameters.');
}
$as = new Authentication\Simple('admin');
return $as->getLogoutURL($returnTo = null);
}
/**
* Check whether the current user is admin.
*
* @return boolean True if the current user is an admin user, false otherwise.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function isAdmin()
{
$session = Session::getSessionFromRequest();
return $session->isValid('admin') || $session->isValid('login-admin');
}
/**
* Require admin access to the current page.
*
* This is a helper function for limiting a page to those with administrative access. It will redirect the user to
* a login page if the current user doesn't have admin access.
*
* @return void This function will only return if the user is admin.
* @throws Exception If no "admin" authentication source was configured.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function requireAdmin()
{
if (self::isAdmin()) {
return;
}
// not authenticated as admin user, start authentication
if (Authentication\Source::getById('admin') !== null) {
$as = new Authentication\Simple('admin');
$as->login();
} else {
throw new Exception(
'Cannot find "admin" auth source, and admin privileges are required.'
);
}
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace SAML\Utils;
/**
* Indicates an implementation caches state internally and may be cleared.
*
* Primarily designed to allow SSP state to be cleared between unit tests.
* @package SimpleSAML\Utils
*/
interface ClearableState
{
/**
* Clear any cached internal state.
*/
public static function clearInternalState();
}

View File

@ -0,0 +1,95 @@
<?php
namespace SAML\Utils;
use InvalidArgumentException;
use SAML\Configuration;
/**
* Utility class for SimpleSAMLphp configuration management and manipulation.
*
* @package SimpleSAMLphp
*/
class Config
{
/**
* Resolves a path that may be relative to the cert-directory.
*
* @param string $path The (possibly relative) path to the file.
*
* @return string The file path.
* @throws InvalidArgumentException If $path is not a string.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function getCertPath($path)
{
if (!is_string($path)) {
throw new InvalidArgumentException('Invalid input parameters.');
}
$globalConfig = Configuration::getInstance();
$base = $globalConfig->getPathValue('certdir', 'cert/');
return System::resolvePath($path, $base);
}
/**
* Retrieve the secret salt.
*
* This function retrieves the value which is configured as the secret salt. It will check that the value exists
* and is set to a non-default value. If it isn't, an exception will be thrown.
*
* The secret salt can be used as a component in hash functions, to make it difficult to test all possible values
* in order to retrieve the original value. It can also be used as a simple method for signing data, by hashing the
* data together with the salt.
*
* @return string The secret salt.
* @throws InvalidArgumentException If the secret salt hasn't been configured.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function getSecretSalt()
{
$secretSalt = Configuration::getInstance()->getString('secretsalt');
if ($secretSalt === 'defaultsecretsalt') {
throw new InvalidArgumentException('The "secretsalt" configuration option must be set to a secret value.');
}
return $secretSalt;
}
/**
* Returns the path to the config dir
*
* If the SIMPLESAMLPHP_CONFIG_DIR environment variable has been set, it takes precedence over the default
* $simplesamldir/config directory.
*
* @return string The path to the configuration directory.
*/
public static function getConfigDir()
{
$configDir = dirname(dirname(dirname(__DIR__))) . '/config';
/** @var string|false $configDirEnv */
$configDirEnv = getenv('SIMPLESAMLPHP_CONFIG_DIR');
if ($configDirEnv === false) {
$configDirEnv = getenv('REDIRECT_SIMPLESAMLPHP_CONFIG_DIR');
}
if ($configDirEnv !== false) {
if (!is_dir($configDirEnv)) {
throw new InvalidArgumentException(
sprintf(
'Config directory specified by environment variable SIMPLESAMLPHP_CONFIG_DIR is not a ' .
'directory. Given: "%s"',
$configDirEnv
)
);
}
$configDir = $configDirEnv;
}
return $configDir;
}
}

View File

@ -0,0 +1,343 @@
<?php
namespace SAML\Utils\Config;
use InvalidArgumentException;
use SAML2\Constants;
use SAML\Configuration;
use SAML\Logger;
/**
* Class with utilities to fetch different configuration objects from metadata configuration arrays.
*
* @package SimpleSAMLphp
* @author Jaime Pérez Crespo, UNINETT AS <jaime.perez@uninett.no>
*/
class Metadata
{
/**
* The string that identities Entity Categories.
*
* @var string
*/
public static $ENTITY_CATEGORY = 'http://macedir.org/entity-category';
/**
* The string the identifies the REFEDS "Hide From Discovery" Entity Category.
*
* @var string
*/
public static $HIDE_FROM_DISCOVERY = 'http://refeds.org/category/hide-from-discovery';
/**
* Valid options for the ContactPerson element
*
* The 'attributes' option isn't defined in section 2.3.2.2 of the OASIS document, but
* it is required to allow additons to the main contact person element for trust
* frameworks.
*
* @var array The valid configuration options for a contact configuration array.
* @see "Metadata for the OASIS Security Assertion Markup Language (SAML) V2.0", section 2.3.2.2.
*/
public static $VALID_CONTACT_OPTIONS = [
'contactType',
'emailAddress',
'givenName',
'surName',
'telephoneNumber',
'company',
'attributes',
];
/**
* @var array The valid types of contact for a contact configuration array.
* @see "Metadata for the OASIS Security Assertion Markup Language (SAML) V2.0", section 2.3.2.2.
*/
public static $VALID_CONTACT_TYPES = [
'technical',
'support',
'administrative',
'billing',
'other',
];
/**
* Parse and sanitize a contact from an array.
*
* Accepts an array with the following elements:
* - contactType The type of the contact (as string). Mandatory.
* - emailAddress Email address (as string), or array of email addresses. Optional.
* - telephoneNumber Telephone number of contact (as string), or array of telephone numbers. Optional.
* - name Full name of contact, either as <GivenName> <SurName>, or as <SurName>, <GivenName>. Optional.
* - surName Surname of contact (as string). Optional.
* - givenName Given name of contact (as string). Optional.
* - company Company name of contact (as string). Optional.
*
* The following values are allowed as "contactType":
* - technical
* - support
* - administrative
* - billing
* - other
*
* If given a "name" it will try to decompose it into its given name and surname, only if neither givenName nor
* surName are present. It works as follows:
* - "surname1 surname2, given_name1 given_name2"
* givenName: "given_name1 given_name2"
* surname: "surname1 surname2"
* - "given_name surname"
* givenName: "given_name"
* surname: "surname"
*
* otherwise it will just return the name as "givenName" in the resulting array.
*
* @param array $contact The contact to parse and sanitize.
*
* @return array An array holding valid contact configuration options. If a key 'name' was part of the input array,
* it will try to decompose the name into its parts, and place the parts into givenName and surName, if those are
* missing.
* @throws InvalidArgumentException If $contact is neither an array nor null, or the contact does not conform to
* valid configuration rules for contacts.
*/
public static function getContact($contact)
{
if (!(is_array($contact) || is_null($contact))) {
throw new InvalidArgumentException('Invalid input parameters');
}
// check the type
if (!isset($contact['contactType']) || !in_array($contact['contactType'], self::$VALID_CONTACT_TYPES, true)) {
$types = join(', ', array_map(
/**
* @param string $t
* @return string
*/
function ($t) {
return '"' . $t . '"';
},
self::$VALID_CONTACT_TYPES
));
throw new InvalidArgumentException('"contactType" is mandatory and must be one of ' . $types . ".");
}
// check attributes is an associative array
if (isset($contact['attributes'])) {
if (
empty($contact['attributes'])
|| !is_array($contact['attributes'])
|| count(array_filter(array_keys($contact['attributes']), 'is_string')) === 0
) {
throw new InvalidArgumentException('"attributes" must be an array and cannot be empty.');
}
}
// try to fill in givenName and surName from name
if (isset($contact['name']) && !isset($contact['givenName']) && !isset($contact['surName'])) {
// first check if it's comma separated
$names = explode(',', $contact['name'], 2);
if (count($names) === 2) {
$contact['surName'] = preg_replace('/\s+/', ' ', trim($names[0]));
$contact['givenName'] = preg_replace('/\s+/', ' ', trim($names[1]));
} else {
// check if it's in "given name surname" format
$names = explode(' ', preg_replace('/\s+/', ' ', trim($contact['name'])));
if (count($names) === 2) {
$contact['givenName'] = preg_replace('/\s+/', ' ', trim($names[0]));
$contact['surName'] = preg_replace('/\s+/', ' ', trim($names[1]));
} else {
// nothing works, return it as given name
$contact['givenName'] = preg_replace('/\s+/', ' ', trim($contact['name']));
}
}
}
// check givenName
if (
isset($contact['givenName'])
&& (
empty($contact['givenName'])
|| !is_string($contact['givenName'])
)
) {
throw new InvalidArgumentException('"givenName" must be a string and cannot be empty.');
}
// check surName
if (
isset($contact['surName'])
&& (
empty($contact['surName'])
|| !is_string($contact['surName'])
)
) {
throw new InvalidArgumentException('"surName" must be a string and cannot be empty.');
}
// check company
if (
isset($contact['company'])
&& (
empty($contact['company'])
|| !is_string($contact['company'])
)
) {
throw new InvalidArgumentException('"company" must be a string and cannot be empty.');
}
// check emailAddress
if (isset($contact['emailAddress'])) {
if (
empty($contact['emailAddress'])
|| !(
is_string($contact['emailAddress'])
|| is_array($contact['emailAddress'])
)
) {
throw new InvalidArgumentException('"emailAddress" must be a string or an array and cannot be empty.');
}
if (is_array($contact['emailAddress'])) {
foreach ($contact['emailAddress'] as $address) {
if (!is_string($address) || empty($address)) {
throw new InvalidArgumentException('Email addresses must be a string and cannot be empty.');
}
}
}
}
// check telephoneNumber
if (isset($contact['telephoneNumber'])) {
if (
empty($contact['telephoneNumber'])
|| !(
is_string($contact['telephoneNumber'])
|| is_array($contact['telephoneNumber'])
)
) {
throw new InvalidArgumentException(
'"telephoneNumber" must be a string or an array and cannot be empty.'
);
}
if (is_array($contact['telephoneNumber'])) {
foreach ($contact['telephoneNumber'] as $address) {
if (!is_string($address) || empty($address)) {
throw new InvalidArgumentException('Telephone numbers must be a string and cannot be empty.');
}
}
}
}
// make sure only valid options are outputted
return array_intersect_key($contact, array_flip(self::$VALID_CONTACT_OPTIONS));
}
/**
* Find the default endpoint in an endpoint array.
*
* @param array $endpoints An array with endpoints.
* @param array $bindings An array with acceptable bindings. Can be null if any binding is allowed.
*
* @return array|NULL The default endpoint, or null if no acceptable endpoints are used.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function getDefaultEndpoint(array $endpoints, array $bindings = null)
{
$firstNotFalse = null;
$firstAllowed = null;
// look through the endpoint list for acceptable endpoints
foreach ($endpoints as $ep) {
if ($bindings !== null && !in_array($ep['Binding'], $bindings, true)) {
// unsupported binding, skip it
continue;
}
if (isset($ep['isDefault'])) {
if ($ep['isDefault'] === true) {
// this is the first endpoint with isDefault set to true
return $ep;
}
// isDefault is set to false, but the endpoint is still usable as a last resort
if ($firstAllowed === null) {
// this is the first endpoint that we can use
$firstAllowed = $ep;
}
} else {
if ($firstNotFalse === null) {
// this is the first endpoint without isDefault set
$firstNotFalse = $ep;
}
}
}
if ($firstNotFalse !== null) {
// we have an endpoint without isDefault set to false
return $firstNotFalse;
}
/* $firstAllowed either contains the first endpoint we can use, or it contains null if we cannot use any of the
* endpoints. Either way we return its value.
*/
return $firstAllowed;
}
/**
* Determine if an entity should be hidden in the discovery service.
*
* This method searches for the "Hide From Discovery" REFEDS Entity Category, and tells if the entity should be
* hidden or not depending on it.
*
* @see https://refeds.org/category/hide-from-discovery
*
* @param array $metadata An associative array with the metadata representing an entity.
*
* @return boolean True if the entity should be hidden, false otherwise.
*/
public static function isHiddenFromDiscovery(array $metadata)
{
Logger::maskErrors(E_ALL);
$hidden = in_array(self::$HIDE_FROM_DISCOVERY, $metadata['EntityAttributes'][self::$ENTITY_CATEGORY], true);
Logger::popErrorMask();
return $hidden === true;
}
/**
* This method parses the different possible values of the NameIDPolicy metadata configuration.
*
* @param mixed $nameIdPolicy
*
* @return null|array
*/
public static function parseNameIdPolicy($nameIdPolicy)
{
$policy = null;
if (is_string($nameIdPolicy)) {
// handle old configurations where 'NameIDPolicy' was used to specify just the format
$policy = ['Format' => $nameIdPolicy, 'AllowCreate' => true];
} elseif (is_array($nameIdPolicy)) {
// handle current configurations specifying an array in the NameIDPolicy config option
$nameIdPolicy_cf = Configuration::loadFromArray($nameIdPolicy);
$policy = [
'Format' => $nameIdPolicy_cf->getString('Format', Constants::NAMEID_TRANSIENT),
'AllowCreate' => $nameIdPolicy_cf->getBoolean('AllowCreate', true),
];
$spNameQualifier = $nameIdPolicy_cf->getString('SPNameQualifier', false);
if ($spNameQualifier !== false) {
$policy['SPNameQualifier'] = $spNameQualifier;
}
} elseif ($nameIdPolicy === null) {
// when NameIDPolicy is unset or set to null, default to transient as before
$policy = ['Format' => Constants::NAMEID_TRANSIENT, 'AllowCreate' => true];
}
return $policy;
}
}

View File

@ -0,0 +1,470 @@
<?php
namespace SAML\Utils;
use InvalidArgumentException;
use SAML\Configuration;
use SAML\Error;
/**
* A class for cryptography-related functions.
*
* @package SimpleSAMLphp
*/
class Crypto
{
/**
* Decrypt data using AES-256-CBC and the key provided as a parameter.
*
* @param string $ciphertext The HMAC of the encrypted data, the IV used and the encrypted data, concatenated.
* @param string $secret The secret to use to decrypt the data.
*
* @return string The decrypted data.
* @throws InvalidArgumentException If $ciphertext is not a string.
* @throws Error\Exception If the openssl module is not loaded.
*
* @see \SAML\Utils\Crypto::aesDecrypt()
*/
private static function aesDecryptInternal($ciphertext, $secret)
{
if (!is_string($ciphertext)) {
throw new InvalidArgumentException(
'Input parameter "$ciphertext" must be a string with more than 48 characters.'
);
}
/** @var int $len */
$len = mb_strlen($ciphertext, '8bit');
if ($len < 48) {
throw new InvalidArgumentException(
'Input parameter "$ciphertext" must be a string with more than 48 characters.'
);
}
if (!function_exists("openssl_decrypt")) {
throw new Error\Exception("The openssl PHP module is not loaded.");
}
// derive encryption and authentication keys from the secret
$key = openssl_digest($secret, 'sha512');
$hmac = mb_substr($ciphertext, 0, 32, '8bit');
$iv = mb_substr($ciphertext, 32, 16, '8bit');
$msg = mb_substr($ciphertext, 48, $len - 48, '8bit');
// authenticate the ciphertext
if (self::secureCompare(hash_hmac('sha256', $iv . $msg, substr($key, 64, 64), true), $hmac)) {
$plaintext = openssl_decrypt(
$msg,
'AES-256-CBC',
substr($key, 0, 64),
defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : 1,
$iv
);
if ($plaintext !== false) {
return $plaintext;
}
}
throw new Error\Exception("Failed to decrypt ciphertext.");
}
/**
* Decrypt data using AES-256-CBC and the system-wide secret salt as key.
*
* @param string $ciphertext The HMAC of the encrypted data, the IV used and the encrypted data, concatenated.
*
* @return string The decrypted data.
* @throws InvalidArgumentException If $ciphertext is not a string.
* @throws Error\Exception If the openssl module is not loaded.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function aesDecrypt($ciphertext)
{
return self::aesDecryptInternal($ciphertext, Config::getSecretSalt());
}
/**
* Encrypt data using AES-256-CBC and the key provided as a parameter.
*
* @param string $data The data to encrypt.
* @param string $secret The secret to use to encrypt the data.
*
* @return string An HMAC of the encrypted data, the IV and the encrypted data, concatenated.
* @throws InvalidArgumentException If $data is not a string.
* @throws Error\Exception If the openssl module is not loaded.
*
* @see \SAML\Utils\Crypto::aesEncrypt()
*/
private static function aesEncryptInternal($data, $secret)
{
if (!is_string($data)) {
throw new InvalidArgumentException('Input parameter "$data" must be a string.');
}
if (!function_exists("openssl_encrypt")) {
throw new Error\Exception('The openssl PHP module is not loaded.');
}
// derive encryption and authentication keys from the secret
$key = openssl_digest($secret, 'sha512');
// generate a random IV
$iv = openssl_random_pseudo_bytes(16);
// encrypt the message
/** @var string|false $ciphertext */
$ciphertext = openssl_encrypt(
$data,
'AES-256-CBC',
substr($key, 0, 64),
defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : 1,
$iv
);
if ($ciphertext === false) {
throw new Error\Exception("Failed to encrypt plaintext.");
}
// return the ciphertext with proper authentication
return hash_hmac('sha256', $iv . $ciphertext, substr($key, 64, 64), true) . $iv . $ciphertext;
}
/**
* Encrypt data using AES-256-CBC and the system-wide secret salt as key.
*
* @param string $data The data to encrypt.
*
* @return string An HMAC of the encrypted data, the IV and the encrypted data, concatenated.
* @throws InvalidArgumentException If $data is not a string.
* @throws Error\Exception If the openssl module is not loaded.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function aesEncrypt($data)
{
return self::aesEncryptInternal($data, Config::getSecretSalt());
}
/**
* Convert data from DER to PEM encoding.
*
* @param string $der Data encoded in DER format.
* @param string $type The type of data we are encoding, as expressed by the PEM header. Defaults to "CERTIFICATE".
* @return string The same data encoded in PEM format.
* @see RFC7648 for known types and PEM format specifics.
*/
public static function der2pem($der, $type = 'CERTIFICATE')
{
return "-----BEGIN " . $type . "-----\n" .
chunk_split(base64_encode($der), 64, "\n") .
"-----END " . $type . "-----\n";
}
/**
* Load a private key from metadata.
*
* This function loads a private key from a metadata array. It looks for the following elements:
* - 'privatekey': Name of a private key file in the cert-directory.
* - 'privatekey_pass': Password for the private key.
*
* It returns and array with the following elements:
* - 'PEM': Data for the private key, in PEM-format.
* - 'password': Password for the private key.
*
* @param Configuration $metadata The metadata array the private key should be loaded from.
* @param bool $required Whether the private key is required. If this is true, a
* missing key will cause an exception. Defaults to false.
* @param string $prefix The prefix which should be used when reading from the metadata
* array. Defaults to ''.
* @param bool $full_path Whether the filename found in the configuration contains the
* full path to the private key or not. Default to false.
*
* @return array|NULL Extracted private key, or NULL if no private key is present.
* @throws InvalidArgumentException If $required is not boolean or $prefix is not a string.
* @throws Error\Exception If no private key is found in the metadata, or it was not possible to load
* it.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function loadPrivateKey(Configuration $metadata, $required = false, $prefix = '', $full_path = false)
{
if (!is_bool($required) || !is_string($prefix) || !is_bool($full_path)) {
throw new InvalidArgumentException('Invalid input parameters.');
}
$file = $metadata->getString($prefix . 'privatekey', null);
if ($file === null) {
// no private key found
if ($required) {
throw new Error\Exception('No private key found in metadata.');
} else {
return null;
}
}
if (!$full_path) {
$file = Config::getCertPath($file);
}
$data = @file_get_contents($file);
if ($data === false) {
throw new Error\Exception('Unable to load private key from file "' . $file . '"');
}
$ret = [
'PEM' => $data,
'password' => $metadata->getString($prefix . 'privatekey_pass', null),
];
return $ret;
}
/**
* Get public key or certificate from metadata.
*
* This function implements a function to retrieve the public key or certificate from a metadata array.
*
* It will search for the following elements in the metadata:
* - 'certData': The certificate as a base64-encoded string.
* - 'certificate': A file with a certificate or public key in PEM-format.
* - 'certFingerprint': The fingerprint of the certificate. Can be a single fingerprint, or an array of multiple
* valid fingerprints. (deprecated)
*
* This function will return an array with these elements:
* - 'PEM': The public key/certificate in PEM-encoding.
* - 'certData': The certificate data, base64 encoded, on a single line. (Only present if this is a certificate.)
* - 'certFingerprint': Array of valid certificate fingerprints. (Deprecated. Only present if this is a
* certificate.)
*
* @param Configuration $metadata The metadata.
* @param bool $required Whether the public key is required. If this is TRUE, a missing key
* will cause an exception. Default is FALSE.
* @param string $prefix The prefix which should be used when reading from the metadata array.
* Defaults to ''.
*
* @return array|NULL Public key or certificate data, or NULL if no public key or certificate was found.
* @throws InvalidArgumentException If $metadata is not an instance of \SimpleSAML\Configuration, $required is not
* boolean or $prefix is not a string.
* @throws Error\Exception If no public key is found in the metadata, or it was not possible to load
* it.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Lasse Birnbaum Jensen
*/
public static function loadPublicKey(Configuration $metadata, $required = false, $prefix = '')
{
if (!is_bool($required) || !is_string($prefix)) {
throw new InvalidArgumentException('Invalid input parameters.');
}
$keys = $metadata->getPublicKeys(null, false, $prefix);
if (!empty($keys)) {
foreach ($keys as $key) {
if ($key['type'] !== 'X509Certificate') {
continue;
}
if ($key['signing'] !== true) {
continue;
}
$certData = $key['X509Certificate'];
$pem = "-----BEGIN CERTIFICATE-----\n" .
chunk_split($certData, 64) .
"-----END CERTIFICATE-----\n";
$certFingerprint = strtolower(sha1(base64_decode($certData)));
return [
'certData' => $certData,
'PEM' => $pem,
'certFingerprint' => [$certFingerprint],
];
}
// no valid key found
} elseif ($metadata->hasValue($prefix . 'certFingerprint')) {
// we only have a fingerprint available
$fps = $metadata->getArrayizeString($prefix . 'certFingerprint');
// normalize fingerprint(s) - lowercase and no colons
foreach ($fps as &$fp) {
assert(is_string($fp));
$fp = strtolower(str_replace(':', '', $fp));
}
/*
* We can't build a full certificate from a fingerprint, and may as well return an array with only the
* fingerprint(s) immediately.
*/
return ['certFingerprint' => $fps];
}
// no public key/certificate available
if ($required) {
throw new Error\Exception('No public key / certificate found in metadata.');
} else {
return null;
}
}
/**
* Convert from PEM to DER encoding.
*
* @param string $pem Data encoded in PEM format.
* @return string The same data encoded in DER format.
* @throws InvalidArgumentException If $pem is not encoded in PEM format.
* @see RFC7648 for PEM format specifics.
*/
public static function pem2der($pem)
{
$pem = trim($pem);
$begin = "-----BEGIN ";
$end = "-----END ";
$lines = explode("\n", $pem);
$last = count($lines) - 1;
if (strpos($lines[0], $begin) !== 0) {
throw new InvalidArgumentException("pem2der: input is not encoded in PEM format.");
}
unset($lines[0]);
if (strpos($lines[$last], $end) !== 0) {
throw new InvalidArgumentException("pem2der: input is not encoded in PEM format.");
}
unset($lines[$last]);
return base64_decode(implode($lines));
}
/**
* This function hashes a password with a given algorithm.
*
* @param string $password The password to hash.
* @param string|null $algorithm @deprecated The hashing algorithm, uppercase, optionally
* prepended with 'S' (salted). See hash_algos() for a complete list of hashing algorithms.
* @param string|null $salt @deprecated An optional salt to use.
*
* @return string The hashed password.
* @throws InvalidArgumentException If the input parameter is not a string.
* @throws Error\Exception If the algorithm specified is not supported.
*
* @see hash_algos()
*
* @author Dyonisius Visser, TERENA <visser@terena.org>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function pwHash($password, $algorithm = null, $salt = null)
{
if (!is_null($algorithm)) {
// @deprecated Old-style
if (!is_string($algorithm) || !is_string($password)) {
throw new InvalidArgumentException('Invalid input parameters.');
}
// hash w/o salt
if (in_array(strtolower($algorithm), hash_algos(), true)) {
$alg_str = '{' . str_replace('SHA1', 'SHA', $algorithm) . '}'; // LDAP compatibility
$hash = hash(strtolower($algorithm), $password, true);
return $alg_str . base64_encode($hash);
}
// hash w/ salt
if ($salt === null) {
// no salt provided, generate one
// default 8 byte salt, but 4 byte for LDAP SHA1 hashes
$bytes = ($algorithm == 'SSHA1') ? 4 : 8;
$salt = openssl_random_pseudo_bytes($bytes);
}
if ($algorithm[0] == 'S' && in_array(substr(strtolower($algorithm), 1), hash_algos(), true)) {
$alg = substr(strtolower($algorithm), 1); // 'sha256' etc
$alg_str = '{' . str_replace('SSHA1', 'SSHA', $algorithm) . '}'; // LDAP compatibility
$hash = hash($alg, $password . $salt, true);
return $alg_str . base64_encode($hash . $salt);
}
throw new Error\Exception('Hashing algorithm \'' . strtolower($algorithm) . '\' is not supported');
} else {
if (!is_string($password)) {
throw new InvalidArgumentException('Invalid input parameter.');
} elseif (!is_string($hash = password_hash($password, PASSWORD_DEFAULT))) {
throw new InvalidArgumentException('Error while hashing password.');
}
return $hash;
}
}
/**
* Compare two strings securely.
*
* This method checks if two strings are equal in constant time, avoiding timing attacks. Use it every time we need
* to compare a string with a secret that shouldn't be leaked, i.e. when verifying passwords, one-time codes, etc.
*
* @param string $known A known string.
* @param string $user A user-provided string to compare with the known string.
*
* @return bool True if both strings are equal, false otherwise.
*/
public static function secureCompare($known, $user)
{
return hash_equals($known, $user);
}
/**
* This function checks if a password is valid
*
* @param string $hash The password as it appears in password file, optionally prepended with algorithm.
* @param string $password The password to check in clear.
*
* @return boolean True if the hash corresponds with the given password, false otherwise.
* @throws InvalidArgumentException If the input parameters are not strings.
* @throws Error\Exception If the algorithm specified is not supported.
*
* @author Dyonisius Visser, TERENA <visser@terena.org>
*/
public static function pwValid($hash, $password)
{
if (!is_string($hash) || !is_string($password)) {
throw new InvalidArgumentException('Invalid input parameters.');
}
if (password_verify($password, $hash)) {
return true;
}
// return $hash === $password
// @deprecated remove everything below this line for 2.0
// match algorithm string (e.g. '{SSHA256}', '{MD5}')
if (preg_match('/^{(.*?)}(.*)$/', $hash, $matches)) {
// LDAP compatibility
$alg = preg_replace('/^(S?SHA)$/', '${1}1', $matches[1]);
// hash w/o salt
if (in_array(strtolower($alg), hash_algos(), true)) {
return self::secureCompare($hash, self::pwHash($password, $alg));
}
// hash w/ salt
if ($alg[0] === 'S' && in_array(substr(strtolower($alg), 1), hash_algos(), true)) {
$php_alg = substr(strtolower($alg), 1);
// get hash length of this algorithm to learn how long the salt is
$hash_length = strlen(hash($php_alg, '', true));
$salt = substr(base64_decode($matches[2]), $hash_length);
return self::secureCompare($hash, self::pwHash($password, $alg, $salt));
}
throw new Error\Exception('Hashing algorithm \'' . strtolower($alg) . '\' is not supported');
} else {
return $hash === $password;
}
}
}

307
libsrc/SAML/Utils/EMail.php Normal file
View File

@ -0,0 +1,307 @@
<?php
namespace SAML\Utils;
use Exception;
use InvalidArgumentException;
use PHPMailer\PHPMailer\PHPMailer;
use SAML\Configuration;
use SAML\Logger;
use SAML\XHTML\Template;
/**
* E-mailer class that can generate a formatted e-mail from array
* input data.
*
* @author Jørn Åne de Jong, Uninett AS <jorn.dejong@uninett.no>
* @package SimpleSAMLphp
*/
class EMail
{
/** @var array Dictionary with multivalues */
private $data = [];
/** @var string Introduction text */
private $text = '';
/** @var PHPMailer The mailer instance */
private $mail;
/**
* Constructor
*
* If $from or $to is not provided, the <code>technicalcontact_email</code>
* from the configuration is used.
*
* @param string $subject The subject of the e-mail
* @param string $from The from-address (both envelope and header)
* @param string $to The recipient
*
* @throws \PHPMailer\PHPMailer\Exception
*/
public function __construct($subject, $from = null, $to = null)
{
$this->mail = new PHPMailer(true);
$this->mail->Subject = $subject;
$this->mail->setFrom($from ?: static::getDefaultMailAddress());
$this->mail->addAddress($to ?: static::getDefaultMailAddress());
static::initFromConfig($this);
}
/**
* Get the default e-mail address from the configuration
* This is used both as source and destination address
* unless something else is provided at the constructor.
*
* It will refuse to return the SimpleSAMLphp default address,
* which is na@example.org.
*
* @return string Default mail address
*/
public static function getDefaultMailAddress()
{
$config = Configuration::getInstance();
$address = $config->getString('technicalcontact_email', 'na@example.org');
$address = preg_replace('/^mailto:/i', '', $address);
if ('na@example.org' === $address) {
throw new Exception('technicalcontact_email must be changed from the default value');
}
return $address;
}
/**
* Set the data that should be embedded in the e-mail body
*
* @param array $data The data that should be embedded in the e-mail body
* @return void
*/
public function setData(array $data)
{
/*
* Convert every non-array value to an array with the original
* as its only element. This guarantees that every value of $data
* can be iterated over.
*/
$this->data = array_map(
/**
* @param mixed $v
* @return array
*/
function ($v) {
return is_array($v) ? $v : [$v];
},
$data
);
}
/**
* Set an introduction text for the e-mail
*
* @param string $text Introduction text
* @return void
*/
public function setText($text)
{
$this->text = $text;
}
/**
* Add a Reply-To address to the mail
*
* @param string $address Reply-To e-mail address
* @return void
*/
public function addReplyTo($address)
{
$this->mail->addReplyTo($address);
}
/**
* Send the mail
*
* @param bool $plainTextOnly Do not send HTML payload
* @return void
*
* @throws \PHPMailer\PHPMailer\Exception
*/
public function send($plainTextOnly = false)
{
if ($plainTextOnly) {
$this->mail->isHTML(false);
$this->mail->Body = $this->generateBody('mailtxt.twig');
} else {
$this->mail->isHTML(true);
$this->mail->Body = $this->generateBody('mailhtml.twig');
$this->mail->AltBody = $this->generateBody('mailtxt.twig');
}
$this->mail->send();
}
/**
* Sets the method by which the email will be sent. Currently supports what
* PHPMailer supports: sendmail, mail and smtp.
*
* @param string $transportMethod the transport method
* @param array $transportOptions options for the transport method
*
* @return void
*
* @throws InvalidArgumentException
*/
public function setTransportMethod($transportMethod, array $transportOptions = [])
{
assert(is_string($transportMethod));
assert(is_array($transportOptions));
switch (strtolower($transportMethod)) {
// smtp transport method
case 'smtp':
$this->mail->isSMTP();
// set the host (required)
if (isset($transportOptions['host'])) {
$this->mail->Host = $transportOptions['host'];
} else {
// throw an exception otherwise
throw new InvalidArgumentException("Missing Required Email Transport Parameter 'host'");
}
// set the port (optional, assume standard SMTP port 25 if not provided)
$this->mail->Port = (isset($transportOptions['port'])) ? (int)$transportOptions['port'] : 25;
// smtp auth: enabled if username or password is set
if (isset($transportOptions['username']) || isset($transportOptions['password'])) {
$this->mail->SMTPAuth = true;
}
// smtp auth: username
if (isset($transportOptions['username'])) {
$this->mail->Username = $transportOptions['username'];
}
// smtp auth: password
if (isset($transportOptions['password'])) {
$this->mail->Password = $transportOptions['password'];
}
// smtp security: encryption type
if (isset($transportOptions['secure'])) {
$this->mail->SMTPSecure = $transportOptions['secure'];
}
// smtp security: enable or disable smtp auto tls
if (isset($transportOptions['autotls'])) {
$this->mail->SMTPAutoTLS = (bool)$transportOptions['autotls'];
}
break;
//mail transport method
case 'mail':
$this->mail->isMail();
break;
// sendmail transport method
case 'sendmail':
$this->mail->isSendmail();
// override the default path of the sendmail executable
if (isset($transportOptions['path'])) {
$this->mail->Sendmail = $transportOptions['path'];
}
break;
default:
throw new InvalidArgumentException(
"Invalid Mail Transport Method - Check 'mail.transport.method' Configuration Option"
);
}
}
/**
* Initializes the provided EMail object with the configuration provided from the SimpleSAMLphp configuration.
*
* @param EMail $EMail
* @return EMail
* @throws Exception
*/
public static function initFromConfig(EMail $EMail)
{
assert($EMail instanceof EMail);
$config = Configuration::getInstance();
$EMail->setTransportMethod(
$config->getString('mail.transport.method', 'mail'),
$config->getArrayize('mail.transport.options', [])
);
return $EMail;
}
/**
* Generate the body of the e-mail
*
* @param string $template The name of the template to use
*
* @return string The body of the e-mail
*/
public function generateBody($template)
{
$config = Configuration::getInstance();
$newui = $config->getBoolean('usenewui', false);
if ($newui === false) {
$result = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>SimpleSAMLphp Email report</title>
<style type="text/css">
pre, div.box {
margin: .4em 2em .4em 1em;
padding: 4px;
}
pre {
background: #eee;
border: 1px solid #aaa;
}
</style>
</head>
<body>
<h1>' . htmlspecialchars($this->mail->Subject) . '</h1>
<div class="container" style="background: #fafafa; border: 1px solid #eee; margin: 2em; padding: .6em;">
<blockquote>"' . htmlspecialchars($this->text) . '"</blockquote>
</div>';
foreach ($this->data as $name => $values) {
$result .= '<h2>' . htmlspecialchars($name) . '</h2><ul>';
foreach ($values as $value) {
$result .= '<li><pre>' . htmlspecialchars($value) . '</pre></li>';
}
$result .= '</ul>';
}
} else {
$t = new Template($config, $template);
$twig = $t->getTwig();
if (!isset($twig)) {
throw new Exception(
'Even though we explicitly configure that we want Twig,'
. ' the Template class does not give us Twig. This is a bug.'
);
}
$result = $twig->render($template, [
'subject' => $this->mail->Subject,
'text' => $this->text,
'data' => $this->data
]);
}
return $result;
}
}

1319
libsrc/SAML/Utils/HTTP.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,273 @@
<?php
namespace SAML\Utils;
/**
* Provides a non-static wrapper for the HTTP utility class.
*
* @package SimpleSAML\Utils
*/
class HttpAdapter
{
/**
* @see HTTP::getServerHTTPS()
* @return bool
*/
public function getServerHTTPS()
{
return HTTP::getServerHTTPS();
}
/**
* @see HTTP::getServerPort()
* @return string
*/
public function getServerPort()
{
return HTTP::getServerPort();
}
/**
* @see HTTP::addURLParameters()
*
* @param string $url
* @param array $parameters
* @return string
*/
public function addURLParameters($url, $parameters)
{
return HTTP::addURLParameters($url, $parameters);
}
/**
* @see HTTP::checkSessionCookie()
*
* @param string|null $retryURL
* @return void
*/
public function checkSessionCookie($retryURL = null)
{
HTTP::checkSessionCookie($retryURL);
}
/**
* @see HTTP::checkURLAllowed()
*
* @param string $url
* @param array|null $trustedSites
* @return string
*/
public function checkURLAllowed($url, array $trustedSites = null)
{
return HTTP::checkURLAllowed($url, $trustedSites);
}
/**
* @see HTTP::fetch()
*
* @param string $url
* @param array $context
* @param bool $getHeaders
* @return array|string
*/
public function fetch($url, $context = [], $getHeaders = false)
{
return HTTP::fetch($url, $context, $getHeaders);
}
/**
* @see HTTP::getAcceptLanguage()
* @return array
*/
public function getAcceptLanguage()
{
return HTTP::getAcceptLanguage();
}
/**
* @see HTTP::guessBasePath()
* @return string
*/
public function guessBasePath()
{
return HTTP::guessBasePath();
}
/**
* @see HTTP::getBaseURL()
* @return string
*/
public function getBaseURL()
{
return HTTP::getBaseURL();
}
/**
* @see HTTP::getFirstPathElement()
*
* @param bool $trailingslash
* @return string
*/
public function getFirstPathElement($trailingslash = true)
{
return HTTP::getFirstPathElement($trailingslash);
}
/**
* @see HTTP::getPOSTRedirectURL()
*
* @param string $destination
* @param array $data
* @return string
*/
public function getPOSTRedirectURL($destination, $data)
{
return HTTP::getPOSTRedirectURL($destination, $data);
}
/**
* @see HTTP::getSelfHost()
* @return string
*/
public function getSelfHost()
{
return HTTP::getSelfHost();
}
/**
* @see HTTP::getSelfHostWithNonStandardPort()
* @return string
*/
public function getSelfHostWithNonStandardPort()
{
return HTTP::getSelfHostWithNonStandardPort();
}
/**
* @see HTTP::getSelfHostWithPath()
* @return string
*/
public function getSelfHostWithPath()
{
return HTTP::getSelfHostWithPath();
}
/**
* @see HTTP::getSelfURL()
* @return string
*/
public function getSelfURL()
{
return HTTP::getSelfURL();
}
/**
* @see HTTP::getSelfURLHost()
* @return string
*/
public function getSelfURLHost()
{
return HTTP::getSelfURLHost();
}
/**
* @see HTTP::getSelfURLNoQuery()
* @return string
*/
public function getSelfURLNoQuery()
{
return HTTP::getSelfURLNoQuery();
}
/**
* @see HTTP::isHTTPS()
* @return bool
*/
public function isHTTPS()
{
return HTTP::isHTTPS();
}
/**
* @see HTTP::normalizeURL()
* @param string $url
* @return string
*/
public function normalizeURL($url)
{
return HTTP::normalizeURL($url);
}
/**
* @see HTTP::parseQueryString()
*
* @param string $query_string
* @return array
*/
public function parseQueryString($query_string)
{
return HTTP::parseQueryString($query_string);
}
/**
* @see HTTP::redirectTrustedURL()
*
* @param string $url
* @param array $parameters
* @return void
*/
public function redirectTrustedURL($url, $parameters = [])
{
HTTP::redirectTrustedURL($url, $parameters);
}
/**
* @see HTTP::redirectUntrustedURL()
*
* @param string $url
* @param array $parameters
* @return void
*/
public function redirectUntrustedURL($url, $parameters = [])
{
HTTP::redirectUntrustedURL($url, $parameters);
}
/**
* @see HTTP::resolveURL()
*
* @param string $url
* @param string|null $base
* @return string
*/
public function resolveURL($url, $base = null)
{
return HTTP::resolveURL($url, $base);
}
/**
* @see HTTP::setCookie()
*
* @param string $name
* @param string $value
* @param array|null $params
* @param bool $throw
* @return void
*/
public function setCookie($name, $value, $params = null, $throw = true)
{
HTTP::setCookie($name, $value, $params, $throw);
}
/**
* @see HTTP::submitPOSTData()
*
* @param string $destination
* @param array $data
* @return void
*/
public function submitPOSTData($destination, $data)
{
HTTP::submitPOSTData($destination, $data);
}
}

85
libsrc/SAML/Utils/Net.php Normal file
View File

@ -0,0 +1,85 @@
<?php
namespace SAML\Utils;
/**
* Net-related utility methods.
*
* @package SimpleSAMLphp
*/
class Net
{
/**
* Check whether an IP address is part of a CIDR.
*
* @param string $cidr The network CIDR address.
* @param string $ip The IP address to check. Optional. Current remote address will be used if none specified. Do
* not rely on default parameter if running behind load balancers.
*
* @return boolean True if the IP address belongs to the specified CIDR, false otherwise.
*
* @author Andreas Åkre Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Brook Schofield, GÉANT
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function ipCIDRcheck($cidr, $ip = null)
{
if ($ip === null) {
$ip = $_SERVER['REMOTE_ADDR'];
}
if (strpos($cidr, '/') === false) {
return false;
}
list ($net, $mask) = explode('/', $cidr);
$mask = intval($mask);
$ip_ip = [];
$ip_net = [];
if (strstr($ip, ':') || strstr($net, ':')) {
// Validate IPv6 with inet_pton, convert to hex with bin2hex
// then store as a long with hexdec
$ip_pack = @inet_pton($ip);
$net_pack = @inet_pton($net);
if ($ip_pack === false || $net_pack === false) {
// not valid IPv6 address (warning silenced)
return false;
}
$ip_ip = str_split(bin2hex($ip_pack), 8);
foreach ($ip_ip as &$value) {
$value = hexdec($value);
}
$ip_net = str_split(bin2hex($net_pack), 8);
foreach ($ip_net as &$value) {
$value = hexdec($value);
}
} else {
$ip_ip[0] = ip2long($ip);
$ip_net[0] = ip2long($net);
}
for ($i = 0; $mask > 0 && $i < sizeof($ip_ip); $i++) {
if ($mask > 32) {
$iteration_mask = 32;
} else {
$iteration_mask = $mask;
}
$mask -= 32;
$ip_mask = ~((1 << (32 - $iteration_mask)) - 1);
$ip_net_mask = $ip_net[$i] & $ip_mask;
$ip_ip_mask = $ip_ip[$i] & $ip_mask;
if ($ip_ip_mask != $ip_net_mask) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace SAML\Utils;
/**
* Utility class for random data generation and manipulation.
*
* @package SimpleSAMLphp
*/
class Random
{
/**
* The fixed length of random identifiers.
*/
const ID_LENGTH = 43;
/**
* Generate a random identifier, ID_LENGTH bytes long.
*
* @return string A ID_LENGTH-bytes long string with a random, hex-encoded string.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function generateID()
{
return '_' . bin2hex(openssl_random_pseudo_bytes((int) ((self::ID_LENGTH - 1) / 2)));
}
}

View File

@ -0,0 +1,260 @@
<?php
namespace SAML\Utils;
use InvalidArgumentException;
use SAML\Configuration;
use SAML\Error;
/**
* System-related utility methods.
*
* @package SimpleSAMLphp
*/
class System
{
const WINDOWS = 1;
const LINUX = 2;
const OSX = 3;
const HPUX = 4;
const UNIX = 5;
const BSD = 6;
const IRIX = 7;
const SUNOS = 8;
/**
* This function returns the Operating System we are running on.
*
* @return mixed A predefined constant identifying the OS we are running on. False if we are unable to determine it.
*
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function getOS()
{
if (stristr(PHP_OS, 'LINUX')) {
return self::LINUX;
}
if (stristr(PHP_OS, 'DARWIN')) {
return self::OSX;
}
if (stristr(PHP_OS, 'WIN')) {
return self::WINDOWS;
}
if (stristr(PHP_OS, 'BSD')) {
return self::BSD;
}
if (stristr(PHP_OS, 'UNIX')) {
return self::UNIX;
}
if (stristr(PHP_OS, 'HP-UX')) {
return self::HPUX;
}
if (stristr(PHP_OS, 'IRIX')) {
return self::IRIX;
}
if (stristr(PHP_OS, 'SUNOS')) {
return self::SUNOS;
}
return false;
}
/**
* This function retrieves the path to a directory where temporary files can be saved.
*
* @return string Path to a temporary directory, without a trailing directory separator.
* @throws Error\Exception If the temporary directory cannot be created or it exists and does not belong
* to the current user.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function getTempDir()
{
$globalConfig = Configuration::getInstance();
$tempDir = rtrim(
$globalConfig->getString(
'tempdir',
sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'simplesaml'
),
DIRECTORY_SEPARATOR
);
if (!is_dir($tempDir)) {
if (!mkdir($tempDir, 0700, true)) {
$error = error_get_last();
throw new Error\Exception(
'Error creating temporary directory "' . $tempDir . '": ' .
(is_array($error) ? $error['message'] : 'no error available')
);
}
} elseif (function_exists('posix_getuid')) {
// check that the owner of the temp directory is the current user
$stat = lstat($tempDir);
if ($stat['uid'] !== posix_getuid()) {
throw new Error\Exception(
'Temporary directory "' . $tempDir . '" does not belong to the current user.'
);
}
}
return $tempDir;
}
/**
* Resolve a (possibly) relative path from the given base path.
*
* A path which starts with a stream wrapper pattern (e.g. s3://) will not be touched
* and returned as is - regardles of the value given as base path.
* If it starts with a '/' it is assumed to be absolute, all others are assumed to be
* relative. The default base path is the root of the SimpleSAMLphp installation.
*
* @param string $path The path we should resolve.
* @param string|null $base The base path, where we should search for $path from. Default value is the root of the
* SimpleSAMLphp installation.
*
* @return string An absolute path referring to $path.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function resolvePath($path, $base = null)
{
if ($base === null) {
$config = Configuration::getInstance();
$base = $config->getBaseDir();
}
// normalise directory separator
$base = str_replace('\\', '/', $base);
$path = str_replace('\\', '/', $path);
// remove trailing slashes
$base = rtrim($base, '/');
$path = rtrim($path, '/');
// check for absolute path
if (substr($path, 0, 1) === '/') {
// absolute path. */
$ret = '/';
} elseif (static::pathContainsDriveLetter($path)) {
$ret = '';
} else {
// path relative to base
$ret = $base;
}
if (static::pathContainsStreamWrapper($path)) {
$ret = $path;
} else {
$path = explode('/', $path);
foreach ($path as $d) {
if ($d === '.') {
continue;
} elseif ($d === '..') {
$ret = dirname($ret);
} else {
if ($ret && substr($ret, -1) !== '/') {
$ret .= '/';
}
$ret .= $d;
}
}
}
return $ret;
}
/**
* Atomically write a file.
*
* This is a helper function for writing data atomically to a file. It does this by writing the file data to a
* temporary file, then renaming it to the required file name.
*
* @param string $filename The path to the file we want to write to.
* @param string $data The data we should write to the file.
* @param int $mode The permissions to apply to the file. Defaults to 0600.
*
* @throws InvalidArgumentException If any of the input parameters doesn't have the proper types.
* @throws Error\Exception If the file cannot be saved, permissions cannot be changed or it is not
* possible to write to the target file.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Andjelko Horvat
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*
* @return void
*/
public static function writeFile($filename, $data, $mode = 0600)
{
if (!is_string($filename) || !is_string($data) || !is_numeric($mode)) {
throw new InvalidArgumentException('Invalid input parameters');
}
$tmpFile = self::getTempDir() . DIRECTORY_SEPARATOR . rand();
$res = @file_put_contents($tmpFile, $data);
if ($res === false) {
$error = error_get_last();
throw new Error\Exception(
'Error saving file "' . $tmpFile . '": ' .
(is_array($error) ? $error['message'] : 'no error available')
);
}
if (self::getOS() !== self::WINDOWS) {
if (!chmod($tmpFile, $mode)) {
unlink($tmpFile);
$error = error_get_last();
//$error = (is_array($error) ? $error['message'] : 'no error available');
throw new Error\Exception(
'Error changing file mode of "' . $tmpFile . '": ' .
(is_array($error) ? $error['message'] : 'no error available')
);
}
}
if (!rename($tmpFile, $filename)) {
unlink($tmpFile);
$error = error_get_last();
throw new Error\Exception(
'Error moving "' . $tmpFile . '" to "' . $filename . '": ' .
(is_array($error) ? $error['message'] : 'no error available')
);
}
if (function_exists('opcache_invalidate')) {
opcache_invalidate($filename);
}
}
/**
* Check if the supplied path contains a Windows-style drive letter.
*
* @param string $path
*
* @return bool
*/
private static function pathContainsDriveLetter($path)
{
$letterAsciiValue = ord(strtoupper(substr($path, 0, 1)));
return substr($path, 1, 1) === ':'
&& $letterAsciiValue >= 65 && $letterAsciiValue <= 90;
}
/**
* Check if the supplied path contains a stream wrapper
* @param string $path
* @return bool
*/
private static function pathContainsStreamWrapper($path)
{
return preg_match('/^[\w\d]*:\/{2}/', $path) === 1;
}
}

171
libsrc/SAML/Utils/Time.php Normal file
View File

@ -0,0 +1,171 @@
<?php
/**
* Time-related utility methods.
*
* @package SimpleSAMLphp
*/
namespace SAML\Utils;
use InvalidArgumentException;
use SAML\Configuration;
use SAML\Error;
use SAML\Error\Exception;
use SAML\Logger;
class Time
{
/**
* Whether the timezone has been initialized or not.
*
* @var bool
*/
private static $tz_initialized = false;
/**
* This function generates a timestamp on the form used by the SAML protocols.
*
* @param int $instant The time the timestamp should represent. Defaults to current time.
*
* @return string The timestamp.
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function generateTimestamp($instant = null)
{
if ($instant === null) {
$instant = time();
}
return gmdate('Y-m-d\TH:i:s\Z', $instant);
}
/**
* Initialize the timezone.
*
* This function should be called before any calls to date().
*
* @return void
*@throws Exception If the timezone set in the configuration is invalid.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*
*/
public static function initTimezone()
{
if (self::$tz_initialized) {
return;
}
$globalConfig = Configuration::getInstance();
$timezone = $globalConfig->getString('timezone', null);
if ($timezone !== null) {
if (!date_default_timezone_set($timezone)) {
throw new Exception('Invalid timezone set in the "timezone" option in config.php.');
}
self::$tz_initialized = true;
return;
}
// we don't have a timezone configured
Logger::maskErrors(E_ALL);
$serverTimezone = date_default_timezone_get();
Logger::popErrorMask();
// set the timezone to the default
date_default_timezone_set($serverTimezone);
self::$tz_initialized = true;
}
/**
* Interpret a ISO8601 duration value relative to a given timestamp. Please note no fractions are allowed, neither
* durations specified in the formats PYYYYMMDDThhmmss nor P[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss].
*
* @param string $duration The duration, as a string.
* @param int $timestamp The unix timestamp we should apply the duration to. Optional, default to the current
* time.
*
* @return int The new timestamp, after the duration is applied.
* @throws InvalidArgumentException If $duration is not a valid ISO 8601 duration or if the input parameters do
* not have the right data types.
*/
public static function parseDuration($duration, $timestamp = null)
{
if (!(is_string($duration) && (is_int($timestamp) || is_null($timestamp)))) {
throw new InvalidArgumentException('Invalid input parameters');
}
// parse the duration. We use a very strict pattern
$durationRegEx = '#^(-?)P(?:(?:(?:(\\d+)Y)?(?:(\\d+)M)?(?:(\\d+)D)?(?:T(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)' .
'(?:[.,]\d+)?S)?)?)|(?:(\\d+)W))$#D';
if (!preg_match($durationRegEx, $duration, $matches)) {
throw new InvalidArgumentException('Invalid ISO 8601 duration: ' . $duration);
}
$durYears = (empty($matches[2]) ? 0 : (int) $matches[2]);
$durMonths = (empty($matches[3]) ? 0 : (int) $matches[3]);
$durDays = (empty($matches[4]) ? 0 : (int) $matches[4]);
$durHours = (empty($matches[5]) ? 0 : (int) $matches[5]);
$durMinutes = (empty($matches[6]) ? 0 : (int) $matches[6]);
$durSeconds = (empty($matches[7]) ? 0 : (int) $matches[7]);
$durWeeks = (empty($matches[8]) ? 0 : (int) $matches[8]);
if (!empty($matches[1])) {
// negative
$durYears = -$durYears;
$durMonths = -$durMonths;
$durDays = -$durDays;
$durHours = -$durHours;
$durMinutes = -$durMinutes;
$durSeconds = -$durSeconds;
$durWeeks = -$durWeeks;
}
if ($timestamp === null) {
$timestamp = time();
}
if ($durYears !== 0 || $durMonths !== 0) {
/* Special handling of months and years, since they aren't a specific interval, but
* instead depend on the current time.
*/
/* We need the year and month from the timestamp. Unfortunately, PHP doesn't have the
* gmtime function. Instead we use the gmdate function, and split the result.
*/
$yearmonth = explode(':', gmdate('Y:n', $timestamp));
$year = (int) ($yearmonth[0]);
$month = (int) ($yearmonth[1]);
// remove the year and month from the timestamp
$timestamp -= gmmktime(0, 0, 0, $month, 1, $year);
// add years and months, and normalize the numbers afterwards
$year += $durYears;
$month += $durMonths;
while ($month > 12) {
$year += 1;
$month -= 12;
}
while ($month < 1) {
$year -= 1;
$month += 12;
}
// add year and month back into timestamp
$timestamp += gmmktime(0, 0, 0, $month, 1, $year);
}
// add the other elements
$timestamp += $durWeeks * 7 * 24 * 60 * 60;
$timestamp += $durDays * 24 * 60 * 60;
$timestamp += $durHours * 60 * 60;
$timestamp += $durMinutes * 60;
$timestamp += $durSeconds;
return $timestamp;
}
}

480
libsrc/SAML/Utils/XML.php Normal file
View File

@ -0,0 +1,480 @@
<?php
/**
* Utility class for XML and DOM manipulation.
*
* @package SimpleSAMLphp
*/
namespace SAML\Utils;
use DOMComment;
use DOMDocument;
use DOMElement;
use DOMException;
use DOMNode;
use DOMText;
use Exception;
use InvalidArgumentException;
use SAML2\DOMDocumentFactory;
use SAML\Configuration;
use SAML\Error;
use SAML\Logger;
use SAML\XML\Errors;
class XML
{
/**
* This function performs some sanity checks on XML documents, and optionally validates them against their schema
* if the 'validatexml' debugging option is enabled. A warning will be printed to the log if validation fails.
*
* @param string $message The SAML document we want to check.
* @param string $type The type of document. Can be one of:
* - 'saml20'
* - 'saml11'
* - 'saml-meta'
*
* @return void
*
* @throws \SAML\Error\Exception If $message contains a doctype declaration.
*
* @throws InvalidArgumentException If $message is not a string or $type is not a string containing one of the
* values allowed.
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function checkSAMLMessage($message, $type)
{
$allowed_types = ['saml20', 'saml11', 'saml-meta'];
if (!(is_string($message) && in_array($type, $allowed_types, true))) {
throw new InvalidArgumentException('Invalid input parameters.');
}
// a SAML message should not contain a doctype-declaration
if (strpos($message, '<!DOCTYPE') !== false) {
throw new Error\Exception('XML contained a doctype declaration.');
}
// see if debugging is enabled for XML validation
$debug = Configuration::getInstance()->getArrayize('debug', ['validatexml' => false]);
$enabled = Configuration::getInstance()->getBoolean('debug.validatexml', false);
if (
!(in_array('validatexml', $debug, true) // implicitly enabled
|| (array_key_exists('validatexml', $debug)
&& $debug['validatexml'] === true)
// explicitly enabled
// TODO: deprecate this option and remove it in 2.0
|| $enabled) // old 'debug.validatexml' configuration option
) {
// XML validation is disabled
return;
}
$result = true;
switch ($type) {
case 'saml11':
$result = self::isValid($message, 'oasis-sstc-saml-schema-protocol-1.1.xsd');
break;
case 'saml20':
$result = self::isValid($message, 'saml-schema-protocol-2.0.xsd');
break;
case 'saml-meta':
$result = self::isValid($message, 'saml-schema-metadata-2.0.xsd');
}
if (is_string($result)) {
Logger::warning($result);
}
}
/**
* Helper function to log SAML messages that we send or receive.
*
* @param string|DOMElement $message The message, as an string containing the XML or an XML element.
* @param string $type Whether this message is sent or received, encrypted or decrypted. The following
* values are supported:
* - 'in': for messages received.
* - 'out': for outgoing messages.
* - 'decrypt': for decrypted messages.
* - 'encrypt': for encrypted messages.
*
* @return void
*
* @throws InvalidArgumentException If $type is not a string or $message is neither a string nor a \DOMElement.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function debugSAMLMessage($message, $type)
{
if (!(is_string($type) && (is_string($message) || $message instanceof DOMElement))) {
throw new InvalidArgumentException('Invalid input parameters.');
}
// see if debugging is enabled for SAML messages
$debug = Configuration::getInstance()->getArrayize('debug', ['saml' => false]);
if (
!(in_array('saml', $debug, true) // implicitly enabled
|| (array_key_exists('saml', $debug)
&& $debug['saml'] === true)
// explicitly enabled
// TODO: deprecate the old style and remove it in 2.0
|| (array_key_exists(0, $debug)
&& $debug[0] === true)) // old style 'debug'
) {
// debugging messages is disabled
return;
}
if ($message instanceof DOMElement) {
$message = $message->ownerDocument->saveXML($message);
}
switch ($type) {
case 'in':
Logger::debug('Received message:');
break;
case 'out':
Logger::debug('Sending message:');
break;
case 'decrypt':
Logger::debug('Decrypted message:');
break;
case 'encrypt':
Logger::debug('Encrypted message:');
break;
default:
assert(false);
}
$str = self::formatXMLString($message);
foreach (explode("\n", $str) as $line) {
Logger::debug($line);
}
}
/**
* Format a DOM element.
*
* This function takes in a DOM element, and inserts whitespace to make it more readable. Note that whitespace
* added previously will be removed.
*
* @param DOMNode $root The root element which should be formatted.
* @param string $indentBase The indentation this element should be assumed to have. Defaults to an empty
* string.
*
* @return void
*
* @throws InvalidArgumentException If $root is not a DOMElement or $indentBase is not a string.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function formatDOMElement(DOMNode $root, $indentBase = '')
{
if (!is_string($indentBase)) {
throw new InvalidArgumentException('Invalid input parameters');
}
// check what this element contains
$fullText = ''; // all text in this element
$textNodes = []; // text nodes which should be deleted
$childNodes = []; // other child nodes
for ($i = 0; $i < $root->childNodes->length; $i++) {
/** @var DOMNode $child */
$child = $root->childNodes->item($i);
if ($child instanceof DOMText) {
$textNodes[] = $child;
$fullText .= $child->wholeText;
} elseif ($child instanceof DOMComment || $child instanceof DOMElement) {
$childNodes[] = $child;
} else {
// unknown node type. We don't know how to format this
return;
}
}
$fullText = trim($fullText);
if (strlen($fullText) > 0) {
// we contain textelf
$hasText = true;
} else {
$hasText = false;
}
$hasChildNode = (count($childNodes) > 0);
if ($hasText && $hasChildNode) {
// element contains both text and child nodes - we don't know how to format this one
return;
}
// remove text nodes
foreach ($textNodes as $node) {
$root->removeChild($node);
}
if ($hasText) {
// only text - add a single text node to the element with the full text
$root->appendChild(new DOMText($fullText));
return;
}
if (!$hasChildNode) {
// empty node. Nothing to do
return;
}
/* Element contains only child nodes - add indentation before each one, and
* format child elements.
*/
$childIndentation = $indentBase . ' ';
foreach ($childNodes as $node) {
// add indentation before node
$root->insertBefore(new DOMText("\n" . $childIndentation), $node);
// format child elements
if ($node instanceof DOMElement) {
self::formatDOMElement($node, $childIndentation);
}
}
// add indentation before closing tag
$root->appendChild(new DOMText("\n" . $indentBase));
}
/**
* Format an XML string.
*
* This function formats an XML string using the formatDOMElement() function.
*
* @param string $xml An XML string which should be formatted.
* @param string $indentBase Optional indentation which should be applied to all the output. Optional, defaults
* to ''.
*
* @return string The formatted string.
* @throws InvalidArgumentException If the parameters are not strings.
* @throws DOMException If the input does not parse correctly as an XML string.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function formatXMLString($xml, $indentBase = '')
{
if (!is_string($xml) || !is_string($indentBase)) {
throw new InvalidArgumentException('Invalid input parameters');
}
try {
$doc = DOMDocumentFactory::fromString($xml);
} catch (Exception $e) {
throw new DOMException('Error parsing XML string.');
}
$root = $doc->firstChild;
self::formatDOMElement($root, $indentBase);
return $doc->saveXML($root);
}
/**
* This function finds direct descendants of a DOM element with the specified
* localName and namespace. They are returned in an array.
*
* This function accepts the same shortcuts for namespaces as the isDOMNodeOfType function.
*
* @param DOMNode $element The element we should look in.
* @param string $localName The name the element should have.
* @param string $namespaceURI The namespace the element should have.
*
* @return array Array with the matching elements in the order they are found. An empty array is
* returned if no elements match.
* @throws InvalidArgumentException If $element is not an instance of DOMElement, $localName is not a string or
* $namespaceURI is not a string.
*/
public static function getDOMChildren(DOMNode $element, $localName, $namespaceURI)
{
if (!is_string($localName) || !is_string($namespaceURI)) {
throw new InvalidArgumentException('Invalid input parameters.');
}
$ret = [];
for ($i = 0; $i < $element->childNodes->length; $i++) {
/** @var DOMNode $child */
$child = $element->childNodes->item($i);
// skip text nodes and comment elements
if ($child instanceof DOMText || $child instanceof DOMComment) {
continue;
}
if (self::isDOMNodeOfType($child, $localName, $namespaceURI) === true) {
$ret[] = $child;
}
}
return $ret;
}
/**
* This function extracts the text from DOMElements which should contain only text content.
*
* @param DOMElement $element The element we should extract text from.
*
* @return string The text content of the element.
* @throws \SAML\Error\Exception If the element contains a non-text child node.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function getDOMText(DOMElement $element)
{
$txt = '';
for ($i = 0; $i < $element->childNodes->length; $i++) {
/** @var DOMElement $child */
$child = $element->childNodes->item($i);
if (!($child instanceof DOMText)) {
throw new Error\Exception($element->localName . ' contained a non-text child node.');
}
$txt .= $child->wholeText;
}
$txt = trim($txt);
return $txt;
}
/**
* This function checks if the DOMElement has the correct localName and namespaceURI.
*
* We also define the following shortcuts for namespaces:
* - '@ds': 'http://www.w3.org/2000/09/xmldsig#'
* - '@md': 'urn:oasis:names:tc:SAML:2.0:metadata'
* - '@saml1': 'urn:oasis:names:tc:SAML:1.0:assertion'
* - '@saml1md': 'urn:oasis:names:tc:SAML:profiles:v1metadata'
* - '@saml1p': 'urn:oasis:names:tc:SAML:1.0:protocol'
* - '@saml2': 'urn:oasis:names:tc:SAML:2.0:assertion'
* - '@saml2p': 'urn:oasis:names:tc:SAML:2.0:protocol'
*
* @param DOMNode $element The element we should check.
* @param string $name The local name the element should have.
* @param string $nsURI The namespaceURI the element should have.
*
* @return boolean True if both namespace and local name matches, false otherwise.
* @throws InvalidArgumentException If the namespace shortcut is unknown.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function isDOMNodeOfType(DOMNode $element, $name, $nsURI)
{
if (!is_string($name) || !is_string($nsURI) || strlen($nsURI) === 0) {
// most likely a comment-node
return false;
}
// check if the namespace is a shortcut, and expand it if it is
if ($nsURI[0] === '@') {
// the defined shortcuts
$shortcuts = [
'@ds' => 'http://www.w3.org/2000/09/xmldsig#',
'@md' => 'urn:oasis:names:tc:SAML:2.0:metadata',
'@saml1' => 'urn:oasis:names:tc:SAML:1.0:assertion',
'@saml1md' => 'urn:oasis:names:tc:SAML:profiles:v1metadata',
'@saml1p' => 'urn:oasis:names:tc:SAML:1.0:protocol',
'@saml2' => 'urn:oasis:names:tc:SAML:2.0:assertion',
'@saml2p' => 'urn:oasis:names:tc:SAML:2.0:protocol',
'@shibmd' => 'urn:mace:shibboleth:metadata:1.0',
];
// check if it is a valid shortcut
if (!array_key_exists($nsURI, $shortcuts)) {
throw new InvalidArgumentException('Unknown namespace shortcut: ' . $nsURI);
}
// expand the shortcut
$nsURI = $shortcuts[$nsURI];
}
if ($element->localName !== $name) {
return false;
}
if ($element->namespaceURI !== $nsURI) {
return false;
}
return true;
}
/**
* This function attempts to validate an XML string against the specified schema. It will parse the string into a
* DOM document and validate this document against the schema.
*
* Note that this function returns values that are evaluated as a logical true, both when validation works and when
* it doesn't. Please use strict comparisons to check the values returned.
*
* @param string|DOMDocument $xml The XML string or document which should be validated.
* @param string $schema The filename of the schema that should be used to validate the document.
*
* @return bool|string Returns a string with errors found if validation fails. True if validation passes ok.
* @throws InvalidArgumentException If $schema is not a string, or $xml is neither a string nor a \DOMDocument.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function isValid($xml, $schema)
{
if (!(is_string($schema) && (is_string($xml) || $xml instanceof DOMDocument))) {
throw new InvalidArgumentException('Invalid input parameters.');
}
Errors::begin();
if ($xml instanceof DOMDocument) {
$dom = $xml;
$res = true;
} else {
try {
$dom = DOMDocumentFactory::fromString($xml);
$res = true;
} catch (Exception $e) {
$res = false;
}
}
if ($res) {
$config = Configuration::getInstance();
/** @var string $schemaPath */
$schemaPath = $config->resolvePath('schemas');
$schemaFile = $schemaPath . '/' . $schema;
libxml_set_external_entity_loader(
function ($public, $system, $context) {
if (filter_var($system, FILTER_VALIDATE_URL) === $system) {
return null;
}
return $system;
}
);
$res = $dom->schemaValidate($schemaFile);
if ($res) {
Errors::end();
return true;
}
$errorText = "Schema validation failed on XML string:\n";
} else {
$errorText = "Failed to parse XML string for schema validation:\n";
}
$errors = Errors::end();
$errorText .= Errors::formatErrors($errors);
return $errorText;
}
}

View File

@ -0,0 +1,666 @@
<?php
namespace SAML\XHTML;
use Exception;
use SAML\Configuration;
use SAML\Logger;
use SAML\Metadata\MetaDataStorageHandler;
use SAML\Session;
use SAML\Utils;
/**
* This class implements a generic IdP discovery service, for use in various IdP
* discovery service pages. This should reduce code duplication.
*
* Experimental support added for Extended IdP Metadata Discovery Protocol by Andreas 2008-08-28
* More information: https://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-idp-discovery.pdf
*
* @author Jaime Pérez <jaime.perez@uninett.no>, UNINETT AS.
* @author Olav Morken, UNINETT AS.
* @author Andreas Åkre Solberg <andreas@uninett.no>, UNINETT AS.
* @package SimpleSAMLphp
*/
class IdPDisco
{
/**
* An instance of the configuration class.
*
* @var Configuration
*/
protected $config;
/**
* The identifier of this discovery service.
*
* @var string
*/
protected $instance;
/**
* An instance of the metadata handler, which will allow us to fetch metadata about IdPs.
*
* @var MetaDataStorageHandler
*/
protected $metadata;
/**
* The users session.
*
* @var Session
*/
protected $session;
/**
* The metadata sets we find allowed entities in, in prioritized order.
*
* @var array
*/
protected $metadataSets;
/**
* The entity id of the SP which accesses this IdP discovery service.
*
* @var string
*/
protected $spEntityId;
/**
* HTTP parameter from the request, indicating whether the discovery service
* can interact with the user or not.
*
* @var boolean
*/
protected $isPassive;
/**
* The SP request to set the IdPentityID...
*
* @var string|null
*/
protected $setIdPentityID = null;
/**
* The name of the query parameter which should contain the users choice of IdP.
* This option default to 'entityID' for Shibboleth compatibility.
*
* @var string
*/
protected $returnIdParam;
/**
* The list of scoped idp's. The intersection between the metadata idpList
* and scopedIDPList (given as a $_GET IDPList[] parameter) is presented to
* the user. If the intersection is empty the metadata idpList is used.
*
* @var array
*/
protected $scopedIDPList = [];
/**
* The URL the user should be redirected to after choosing an IdP.
*
* @var string
*/
protected $returnURL;
/**
* Initializes this discovery service.
*
* The constructor does the parsing of the request. If this is an invalid request, it will throw an exception.
*
* @param array $metadataSets Array with metadata sets we find remote entities in.
* @param string $instance The name of this instance of the discovery service.
*
* @throws Exception If the request is invalid.
*/
public function __construct(array $metadataSets, $instance)
{
assert(is_string($instance));
// initialize standard classes
$this->config = Configuration::getInstance();
$this->metadata = MetaDataStorageHandler::getMetadataHandler();
$this->session = Session::getSessionFromRequest();
$this->instance = $instance;
$this->metadataSets = $metadataSets;
$this->log('Accessing discovery service.');
// standard discovery service parameters
if (!array_key_exists('entityID', $_GET)) {
throw new Exception('Missing parameter: entityID');
} else {
$this->spEntityId = $_GET['entityID'];
}
if (!array_key_exists('returnIDParam', $_GET)) {
$this->returnIdParam = 'entityID';
} else {
$this->returnIdParam = $_GET['returnIDParam'];
}
$this->log('returnIdParam initially set to [' . $this->returnIdParam . ']');
if (!array_key_exists('return', $_GET)) {
throw new Exception('Missing parameter: return');
} else {
$this->returnURL = Utils\HTTP::checkURLAllowed($_GET['return']);
}
$this->isPassive = false;
if (array_key_exists('isPassive', $_GET)) {
if ($_GET['isPassive'] === 'true') {
$this->isPassive = true;
}
}
$this->log('isPassive initially set to [' . ($this->isPassive ? 'TRUE' : 'FALSE') . ']');
if (array_key_exists('IdPentityID', $_GET)) {
$this->setIdPentityID = $_GET['IdPentityID'];
}
if (array_key_exists('IDPList', $_REQUEST)) {
$this->scopedIDPList = $_REQUEST['IDPList'];
}
}
/**
* Log a message.
*
* This is an helper function for logging messages. It will prefix the messages with our
* discovery service type.
*
* @param string $message The message which should be logged.
* @return void
*/
protected function log($message)
{
Logger::info('idpDisco.' . $this->instance . ': ' . $message);
}
/**
* Retrieve cookie with the given name.
*
* This function will retrieve a cookie with the given name for the current discovery
* service type.
*
* @param string $name The name of the cookie.
*
* @return string|null The value of the cookie with the given name, or null if no cookie with that name exists.
*/
protected function getCookie($name)
{
$prefixedName = 'idpdisco_' . $this->instance . '_' . $name;
if (array_key_exists($prefixedName, $_COOKIE)) {
return $_COOKIE[$prefixedName];
} else {
return null;
}
}
/**
* Save cookie with the given name and value.
*
* This function will save a cookie with the given name and value for the current discovery
* service type.
*
* @param string $name The name of the cookie.
* @param string $value The value of the cookie.
* @return void
*/
protected function setCookie($name, $value)
{
$prefixedName = 'idpdisco_' . $this->instance . '_' . $name;
$params = [
// we save the cookies for 90 days
'lifetime' => (60 * 60 * 24 * 90),
// the base path for cookies. This should be the installation directory for SimpleSAMLphp
'path' => $this->config->getBasePath(),
'httponly' => false,
];
Utils\HTTP::setCookie($prefixedName, $value, $params, false);
}
/**
* Validates the given IdP entity id.
*
* Takes a string with the IdP entity id, and returns the entity id if it is valid, or
* null if not.
*
* @param string|null $idp The entity id we want to validate. This can be null, in which case we will return null.
*
* @return string|null The entity id if it is valid, null if not.
*/
protected function validateIdP($idp)
{
if ($idp === null) {
return null;
}
if (!$this->config->getBoolean('idpdisco.validate', true)) {
return $idp;
}
foreach ($this->metadataSets as $metadataSet) {
try {
$this->metadata->getMetaData($idp, $metadataSet);
return $idp;
} catch (Exception $e) {
// continue
}
}
$this->log('Unable to validate IdP entity id [' . $idp . '].');
// the entity id wasn't valid
return null;
}
/**
* Retrieve the users choice of IdP.
*
* This function finds out which IdP the user has manually chosen, if any.
*
* @return string|null The entity id of the IdP the user has chosen, or null if the user has made no choice.
*/
protected function getSelectedIdP()
{
/* Parameter set from the Extended IdP Metadata Discovery Service Protocol, indicating that the user prefers
* this IdP.
*/
if (!empty($this->setIdPentityID)) {
return $this->validateIdP($this->setIdPentityID);
}
// user has clicked on a link, or selected the IdP from a drop-down list
if (array_key_exists('idpentityid', $_GET)) {
return $this->validateIdP($_GET['idpentityid']);
}
/* Search for the IdP selection from the form used by the links view. This form uses a name which equals
* idp_<entityid>, so we search for that.
*
* Unfortunately, php replaces periods in the name with underscores, and there is no reliable way to get them
* back. Therefore we do some quick and dirty parsing of the query string.
*/
$qstr = $_SERVER['QUERY_STRING'];
$matches = [];
if (preg_match('/(?:^|&)idp_([^=]+)=/', $qstr, $matches)) {
return $this->validateIdP(urldecode($matches[1]));
}
// no IdP chosen
return null;
}
/**
* Retrieve the users saved choice of IdP.
*
* @return string|null The entity id of the IdP the user has saved, or null if the user hasn't saved any choice.
*/
protected function getSavedIdP()
{
if (!$this->config->getBoolean('idpdisco.enableremember', false)) {
// saving of IdP choices is disabled
return null;
}
if ($this->getCookie('remember') === '1') {
$this->log('Return previously saved IdP because of remember cookie set to 1');
return $this->getPreviousIdP();
}
if ($this->isPassive) {
$this->log('Return previously saved IdP because of isPassive');
return $this->getPreviousIdP();
}
return null;
}
/**
* Retrieve the previous IdP the user used.
*
* @return string|null The entity id of the previous IdP the user used, or null if this is the first time.
*/
protected function getPreviousIdP()
{
return $this->validateIdP($this->getCookie('lastidp'));
}
/**
* Retrieve a recommended IdP based on the IP address of the client.
*
* @return string|null The entity ID of the IdP if one is found, or null if not.
*/
protected function getFromCIDRhint()
{
foreach ($this->metadataSets as $metadataSet) {
$idp = $this->metadata->getPreferredEntityIdFromCIDRhint($metadataSet, $_SERVER['REMOTE_ADDR']);
if (!empty($idp)) {
return $idp;
}
}
return null;
}
/**
* Try to determine which IdP the user should most likely use.
*
* This function will first look at the previous IdP the user has chosen. If the user
* hasn't chosen an IdP before, it will look at the IP address.
*
* @return string|null The entity id of the IdP the user should most likely use.
*/
protected function getRecommendedIdP()
{
$idp = $this->getPreviousIdP();
if ($idp !== null) {
$this->log('Preferred IdP from previous use [' . $idp . '].');
return $idp;
}
$idp = $this->getFromCIDRhint();
if (!empty($idp)) {
$this->log('Preferred IdP from CIDR hint [' . $idp . '].');
return $idp;
}
return null;
}
/**
* Save the current IdP choice to a cookie.
*
* @param string $idp The entityID of the IdP.
* @return void
*/
protected function setPreviousIdP($idp)
{
assert(is_string($idp));
$this->log('Choice made [' . $idp . '] Setting cookie.');
$this->setCookie('lastidp', $idp);
}
/**
* Determine whether the choice of IdP should be saved.
*
* @return boolean True if the choice should be saved, false otherwise.
*/
protected function saveIdP()
{
if (!$this->config->getBoolean('idpdisco.enableremember', false)) {
// saving of IdP choices is disabled
return false;
}
if (array_key_exists('remember', $_GET)) {
return true;
}
return false;
}
/**
* Determine which IdP the user should go to, if any.
*
* @return string|null The entity id of the IdP the user should be sent to, or null if the user should choose.
*/
protected function getTargetIdP()
{
// first, check if the user has chosen an IdP
$idp = $this->getSelectedIdP();
if ($idp !== null) {
// the user selected this IdP. Save the choice in a cookie
$this->setPreviousIdP($idp);
if ($this->saveIdP()) {
$this->setCookie('remember', '1');
} else {
$this->setCookie('remember', '0');
}
return $idp;
}
$this->log('getSelectedIdP() returned null');
// check if the user has saved an choice earlier
$idp = $this->getSavedIdP();
if ($idp !== null) {
$this->log('Using saved choice [' . $idp . '].');
return $idp;
}
// the user has made no choice
return null;
}
/**
* Retrieve the list of IdPs which are stored in the metadata.
*
* @return array An array with entityid => metadata mappings.
*/
protected function getIdPList()
{
$idpList = [];
foreach ($this->metadataSets as $metadataSet) {
$newList = $this->metadata->getList($metadataSet);
/*
* Note that we merge the entities in reverse order. This ensures that it is the entity in the first
* metadata set that "wins" if two metadata sets have the same entity.
*/
$idpList = array_merge($newList, $idpList);
}
return $idpList;
}
/**
* Return the list of scoped idp
*
* @return array An array of IdP entities
*/
protected function getScopedIDPList()
{
return $this->scopedIDPList;
}
/**
* Filter the list of IdPs.
*
* This method returns the IdPs that comply with the following conditions:
* - The IdP does not have the 'hide.from.discovery' configuration option.
*
* @param array $list An associative array containing metadata for the IdPs to apply the filtering to.
*
* @return array An associative array containing metadata for the IdPs that were not filtered out.
*/
protected function filterList($list)
{
foreach ($list as $entity => $metadata) {
if (array_key_exists('hide.from.discovery', $metadata) && $metadata['hide.from.discovery'] === true) {
unset($list[$entity]);
}
}
return $list;
}
/**
* Check if an IdP is set or if the request is passive, and redirect accordingly.
*
* @return void If there is no IdP targeted and this is not a passive request.
*/
protected function start()
{
$idp = $this->getTargetIdP();
if ($idp !== null) {
$extDiscoveryStorage = $this->config->getString('idpdisco.extDiscoveryStorage', null);
if ($extDiscoveryStorage !== null) {
$this->log('Choice made [' . $idp . '] (Forwarding to external discovery storage)');
Utils\HTTP::redirectTrustedURL($extDiscoveryStorage, [
'entityID' => $this->spEntityId,
'IdPentityID' => $idp,
'returnIDParam' => $this->returnIdParam,
'isPassive' => 'true',
'return' => $this->returnURL
]);
} else {
$this->log(
'Choice made [' . $idp . '] (Redirecting the user back. returnIDParam='
. $this->returnIdParam . ')'
);
Utils\HTTP::redirectTrustedURL($this->returnURL, [$this->returnIdParam => $idp]);
}
}
if ($this->isPassive) {
$this->log('Choice not made. (Redirecting the user back without answer)');
Utils\HTTP::redirectTrustedURL($this->returnURL);
}
}
/**
* Handles a request to this discovery service.
*
* The IdP disco parameters should be set before calling this function.
* @return void
*/
public function handleRequest()
{
$this->start();
// no choice made. Show discovery service page
$idpList = $this->getIdPList();
$idpList = $this->filterList($idpList);
$preferredIdP = $this->getRecommendedIdP();
$idpintersection = array_intersect(array_keys($idpList), $this->getScopedIDPList());
if (sizeof($idpintersection) > 0) {
$idpList = array_intersect_key($idpList, array_fill_keys($idpintersection, null));
}
$idpintersection = array_values($idpintersection);
if (sizeof($idpintersection) == 1) {
$this->log(
'Choice made [' . $idpintersection[0] . '] (Redirecting the user back. returnIDParam=' .
$this->returnIdParam . ')'
);
Utils\HTTP::redirectTrustedURL(
$this->returnURL,
[$this->returnIdParam => $idpintersection[0]]
);
}
/*
* Make use of an XHTML template to present the select IdP choice to the user. Currently the supported options
* is either a drop down menu or a list view.
*/
switch ($this->config->getString('idpdisco.layout', 'links')) {
case 'dropdown':
$templateFile = 'selectidp-dropdown.php';
break;
case 'links':
$templateFile = 'selectidp-links.php';
break;
default:
throw new Exception('Invalid value for the \'idpdisco.layout\' option.');
}
$t = new Template($this->config, $templateFile, 'disco');
$fallbackLanguage = 'en';
$defaultLanguage = $this->config->getString('language.default', $fallbackLanguage);
$translator = $t->getTranslator();
$language = $translator->getLanguage()->getLanguage();
$tryLanguages = [0 => $language, 1 => $defaultLanguage, 2 => $fallbackLanguage];
$newlist = [];
foreach ($idpList as $entityid => $data) {
$newlist[$entityid]['entityid'] = $entityid;
foreach ($tryLanguages as $lang) {
if ($name = $this->getEntityDisplayName($data, $lang)) {
$newlist[$entityid]['name'] = $name;
break 1;
}
}
if (empty($newlist[$entityid]['name'])) {
$newlist[$entityid]['name'] = $entityid;
}
foreach ($tryLanguages as $lang) {
if (!empty($data['description'][$lang])) {
$newlist[$entityid]['description'] = $data['description'][$lang];
break 1;
}
}
if (!empty($data['icon'])) {
$newlist[$entityid]['icon'] = $data['icon'];
$newlist[$entityid]['iconurl'] = Utils\HTTP::resolveURL($data['icon']);
}
}
usort(
$newlist,
/**
* @param array $idpentry1
* @param array $idpentry2
* @return int
*/
function (array $idpentry1, array $idpentry2) {
return strcasecmp($idpentry1['name'], $idpentry2['name']);
}
);
$t->data['idplist'] = $newlist;
$t->data['preferredidp'] = $preferredIdP;
$t->data['return'] = $this->returnURL;
$t->data['returnIDParam'] = $this->returnIdParam;
$t->data['entityID'] = $this->spEntityId;
$t->data['urlpattern'] = htmlspecialchars(Utils\HTTP::getSelfURLNoQuery());
$t->data['rememberenabled'] = $this->config->getBoolean('idpdisco.enableremember', false);
$t->show();
}
/**
* @param array $idpData
* @param string $language
* @return string|null
*/
private function getEntityDisplayName(array $idpData, $language)
{
if (isset($idpData['UIInfo']['DisplayName'][$language])) {
return $idpData['UIInfo']['DisplayName'][$language];
} elseif (isset($idpData['name'][$language])) {
return $idpData['name'][$language];
} elseif (isset($idpData['OrganizationDisplayName'][$language])) {
return $idpData['OrganizationDisplayName'][$language];
}
return null;
}
}

View File

@ -0,0 +1,907 @@
<?php
/**
* A minimalistic XHTML PHP based template system implemented for SimpleSAMLphp.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
namespace SAML\XHTML;
use Exception;
use InvalidArgumentException;
use SAML\Configuration;
use SAML\Locale\Language;
use SAML\Locale\Localization;
use SAML\Locale\Translate;
use SAML\Logger;
use SAML\Module;
use SAML\TwigConfigurableI18n\Twig\Environment as Twig_Environment;
use SAML\TwigConfigurableI18n\Twig\Extensions\Extension\I18n as Twig_Extensions_Extension_I18n;
use SAML\Utils;
use Symfony\Component\HttpFoundation\Response;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Loader\FilesystemLoader;
use Twig\TwigFilter;
use Twig\TwigFunction;
class Template extends Response
{
/**
* The data associated with this template, accessible within the template itself.
*
* @var array
*/
public $data = [];
/**
* A translator instance configured to work with this template.
*
* @var Translate
*/
private $translator;
/**
* The localization backend
*
* @var Localization
*/
private $localization;
/**
* The configuration to use in this template.
*
* @var Configuration
*/
private $configuration;
/**
* The file to load in this template.
*
* @var string
*/
private $template = 'default.php';
/**
* The twig environment.
*
* @var Environment
* @psalm-suppress PropertyNotSetInConstructor Remove this annotation in 2.0
*/
private $twig;
/**
* The template name.
*
* @var string
*/
private $twig_template;
/**
* Current module, if any.
*
* @var string
*/
private $module;
/**
* Whether to use the new user interface or not.
*
* @var bool
*/
private $useNewUI = false;
/**
* A template controller, if any.
*
* Used to intercept certain parts of the template handling, while keeping away unwanted/unexpected hooks. Set
* the 'theme.controller' configuration option to a class that implements the
* \SimpleSAML\XHTML\TemplateControllerInterface interface to use it.
*
* @var TemplateControllerInterface|null
*/
private $controller = null;
/**
* Whether we are using a non-default theme or not.
*
* If we are using a theme, this variable holds an array with two keys: "module" and "name", those being the name
* of the module and the name of the theme, respectively. If we are using the default theme, the variable has
* the 'default' string in the "name" key, and 'null' in the "module" key.
*
* @var array
*/
private $theme = ['module' => null, 'name' => 'default'];
/**
* Constructor
*
* @param Configuration $configuration Configuration object
* @param string $template Which template file to load
* @param string|null $defaultDictionary The default dictionary where tags will come from.
*/
public function __construct(Configuration $configuration, $template, $defaultDictionary = null)
{
$this->configuration = $configuration;
$this->template = $template;
// TODO: do not remove the slash from the beginning, change the templates instead!
$this->data['baseurlpath'] = ltrim($this->configuration->getBasePath(), '/');
// parse module and template name
list($this->module) = $this->findModuleAndTemplateName($template);
// parse config to find theme and module theme is in, if any
list($this->theme['module'], $this->theme['name']) = $this->findModuleAndTemplateName(
$this->configuration->getString('theme.use', 'default')
);
// initialize internationalization system
$this->translator = new Translate($configuration, $defaultDictionary);
$this->localization = new Localization($configuration);
// check if we are supposed to use the new UI
$this->useNewUI = $this->configuration->getBoolean('usenewui', false);
if ($this->useNewUI) {
// check if we need to attach a theme controller
$controller = $this->configuration->getString('theme.controller', false);
if (
$controller
&& class_exists($controller)
&& in_array(TemplateControllerInterface::class, class_implements($controller))
) {
/** @var TemplateControllerInterface $this->controller */
$this->controller = new $controller();
}
$this->twig = $this->setupTwig();
}
$this->charset = 'UTF-8';
parent::__construct();
}
/**
* Return the URL of an asset, including a cache-buster parameter that depends on the last modification time of
* the original file.
* @param string $asset
* @param string|null $module
* @return string
*/
public function asset($asset, $module = null)
{
$baseDir = $this->configuration->getBaseDir();
if (is_null($module)) {
$file = $baseDir . 'www/assets/' . $asset;
$basePath = $this->configuration->getBasePath();
$path = $basePath . 'assets/' . $asset;
} else {
$file = $baseDir . 'modules/' . $module . '/www/assets/' . $asset;
$path = Module::getModuleUrl($module . '/assets/' . $asset);
}
if (!file_exists($file)) {
// don't be too harsh if an asset is missing, just pretend it's there...
return $path;
}
$tag = $this->configuration->getVersion();
if ($tag === 'master') {
$tag = strval(filemtime($file));
}
$tag = substr(hash('md5', $tag), 0, 5);
return $path . '?tag=' . $tag;
}
/**
* Get the normalized template name.
*
* @return string The name of the template to use.
*/
public function getTemplateName()
{
return $this->normalizeTemplateName($this->template);
}
/**
* Normalize the name of the template to one of the possible alternatives.
*
* @param string $templateName The template name to normalize.
* @return string The filename we need to look for.
*/
private function normalizeTemplateName($templateName)
{
if (strripos($templateName, '.twig')) {
return $templateName;
}
$phppos = strripos($templateName, '.php');
if ($phppos) {
$templateName = substr($templateName, 0, $phppos);
}
$tplpos = strripos($templateName, '.tpl');
if ($tplpos) {
$templateName = substr($templateName, 0, $tplpos);
}
if ($this->useNewUI) {
return $templateName . '.twig';
}
return $templateName;
}
/**
* Set up the places where twig can look for templates.
*
* @return TemplateLoader The twig template loader or false if the template does not exist.
* @throws LoaderError In case a failure occurs.
*/
private function setupTwigTemplatepaths()
{
$filename = $this->normalizeTemplateName($this->template);
// get namespace if any
list($namespace, $filename) = $this->findModuleAndTemplateName($filename);
$this->twig_template = ($namespace !== null) ? '@' . $namespace . '/' . $filename : $filename;
$loader = new TemplateLoader();
$templateDirs = $this->findThemeTemplateDirs();
if ($this->module && $this->module != 'core') {
$modDir = TemplateLoader::getModuleTemplateDir($this->module);
$templateDirs[] = [$this->module => $modDir];
$templateDirs[] = ['__parent__' => $modDir];
}
if ($this->theme['module']) {
try {
$templateDirs[] = [
$this->theme['module'] => TemplateLoader::getModuleTemplateDir($this->theme['module'])
];
} catch (InvalidArgumentException $e) {
// either the module is not enabled or it has no "templates" directory, ignore
}
}
$templateDirs[] = ['core' => TemplateLoader::getModuleTemplateDir('core')];
// default, themeless templates are checked last
$templateDirs[] = [
FilesystemLoader::MAIN_NAMESPACE => $this->configuration->resolvePath('templates')
];
foreach ($templateDirs as $entry) {
$loader->addPath($entry[key($entry)], key($entry));
}
return $loader;
}
/**
* Setup twig.
* @return Environment
* @throws Exception if the template does not exist
*/
private function setupTwig()
{
$auto_reload = $this->configuration->getBoolean('template.auto_reload', true);
$cache = $this->configuration->getString('template.cache', false);
// set up template paths
$loader = $this->setupTwigTemplatepaths();
// abort if twig template does not exist
if (!$loader->exists($this->twig_template)) {
throw new Exception('Template-file \"' . $this->getTemplateName() . '\" does not exist.');
}
// load extra i18n domains
if ($this->module) {
$this->localization->addModuleDomain($this->module);
}
if ($this->theme['module'] !== null && $this->theme['module'] !== $this->module) {
$this->localization->addModuleDomain($this->theme['module']);
}
// set up translation
$options = [
'cache' => $cache,
'auto_reload' => $auto_reload,
'translation_function' => [Translate::class, 'translateSingularGettext'],
'translation_function_plural' => [Translate::class, 'translatePluralGettext'],
];
$twig = new Twig_Environment($loader, $options);
$twig->addExtension(new Twig_Extensions_Extension_I18n());
$twig->addFunction(new TwigFunction('moduleURL', [Module::class, 'getModuleURL']));
// initialize some basic context
$langParam = $this->configuration->getString('language.parameter.name', 'language');
$twig->addGlobal('languageParameterName', $langParam);
$twig->addGlobal('localeBackend', Localization::GETTEXT_I18N_BACKEND);
$twig->addGlobal('currentLanguage', $this->translator->getLanguage()->getLanguage());
$twig->addGlobal('isRTL', false); // language RTL configuration
if ($this->translator->getLanguage()->isLanguageRTL()) {
$twig->addGlobal('isRTL', true);
}
$queryParams = $_GET; // add query parameters, in case we need them in the template
if (isset($queryParams[$langParam])) {
unset($queryParams[$langParam]);
}
$twig->addGlobal('queryParams', $queryParams);
$twig->addGlobal('templateId', str_replace('.twig', '', $this->normalizeTemplateName($this->template)));
$twig->addGlobal('isProduction', $this->configuration->getBoolean('production', true));
$twig->addGlobal('baseurlpath', ltrim($this->configuration->getBasePath(), '/'));
// add a filter for translations out of arrays
$twig->addFilter(
new TwigFilter(
'translateFromArray',
[Translate::class, 'translateFromArray'],
['needs_context' => true]
)
);
// add an asset() function
$twig->addFunction(new TwigFunction('asset', [$this, 'asset']));
if ($this->controller !== null) {
$this->controller->setUpTwig($twig);
}
return $twig;
}
/**
* Add overriding templates from the configured theme.
*
* @return array An array of module => templatedir lookups.
*/
private function findThemeTemplateDirs()
{
if (!isset($this->theme['module'])) {
// no module involved
return [];
}
// setup directories & namespaces
$themeDir = Module::getModuleDir($this->theme['module']) . '/themes/' . $this->theme['name'];
$subdirs = scandir($themeDir);
if (empty($subdirs)) {
// no subdirectories in the theme directory, nothing to do here
// this is probably wrong, log a message
Logger::warning('Empty theme directory for theme "' . $this->theme['name'] . '".');
return [];
}
$themeTemplateDirs = [];
foreach ($subdirs as $entry) {
// discard anything that's not a directory. Expression is negated to profit from lazy evaluation
if (!($entry !== '.' && $entry !== '..' && is_dir($themeDir . '/' . $entry))) {
continue;
}
// set correct name for the default namespace
$ns = ($entry === 'default') ? FilesystemLoader::MAIN_NAMESPACE : $entry;
$themeTemplateDirs[] = [$ns => $themeDir . '/' . $entry];
}
return $themeTemplateDirs;
}
/**
* Get the template directory of a module, if it exists.
*
* @param string $module
* @return string The templates directory of a module
*
* @throws InvalidArgumentException If the module is not enabled or it has no templates directory.
*/
private function getModuleTemplateDir($module)
{
if (!Module::isModuleEnabled($module)) {
throw new InvalidArgumentException('The module \'' . $module . '\' is not enabled.');
}
$moduledir = Module::getModuleDir($module);
// check if module has a /templates dir, if so, append
$templatedir = $moduledir . '/templates';
if (!is_dir($templatedir)) {
throw new InvalidArgumentException('The module \'' . $module . '\' has no templates directory.');
}
return $templatedir;
}
/**
* Add the templates from a given module.
*
* Note that the module must be installed, enabled, and contain a "templates" directory.
*
* @param string $module The module where we need to search for templates.
* @throws InvalidArgumentException If the module is not enabled or it has no templates directory.
* @return void
*/
public function addTemplatesFromModule($module)
{
$dir = TemplateLoader::getModuleTemplateDir($module);
/** @var FilesystemLoader $loader */
$loader = $this->twig->getLoader();
$loader->addPath($dir, $module);
}
/**
* Generate an array for its use in the language bar, indexed by the ISO 639-2 codes of the languages available,
* containing their localized names and the URL that should be used in order to change to that language.
*
* @return array|null The array containing information of all available languages.
*/
private function generateLanguageBar()
{
$languages = $this->translator->getLanguage()->getLanguageList();
ksort($languages);
$langmap = null;
if (count($languages) > 1) {
$parameterName = $this->getTranslator()->getLanguage()->getLanguageParameterName();
$langmap = [];
foreach ($languages as $lang => $current) {
$lang = strtolower($lang);
$langname = $this->translator->getLanguage()->getLanguageLocalizedName($lang);
$url = false;
if (!$current) {
$url = htmlspecialchars(Utils\HTTP::addURLParameters(
'',
[$parameterName => $lang]
));
}
$langmap[$lang] = [
'name' => $langname,
'url' => $url,
];
}
}
return $langmap;
}
/**
* Set some default context
* @return void
*/
private function twigDefaultContext()
{
// show language bar by default
if (!isset($this->data['hideLanguageBar'])) {
$this->data['hideLanguageBar'] = false;
}
// get languagebar
$this->data['languageBar'] = null;
if ($this->data['hideLanguageBar'] === false) {
$languageBar = $this->generateLanguageBar();
if (is_null($languageBar)) {
$this->data['hideLanguageBar'] = true;
} else {
$this->data['languageBar'] = $languageBar;
}
}
// assure that there is a <title> and <h1>
if (isset($this->data['header']) && !isset($this->data['pagetitle'])) {
$this->data['pagetitle'] = $this->data['header'];
}
if (!isset($this->data['pagetitle'])) {
$this->data['pagetitle'] = 'SimpleSAMLphp';
}
$this->data['year'] = date('Y');
$this->data['header'] = $this->configuration->getValue('theme.header', 'SimpleSAMLphp');
}
/**
* Get the contents produced by this template.
*
* @return string The HTML rendered by this template, as a string.
* @throws Exception if the template cannot be found.
*/
protected function getContents()
{
$this->twigDefaultContext();
if ($this->controller) {
$this->controller->display($this->data);
}
return $this->twig->render($this->twig_template, $this->data);
}
/**
* Send this template as a response.
*
* @return Response This response.
* @throws Exception if the template cannot be found.
*/
public function send()
{
$this->content = $this->getContents();
return parent::send();
}
/**
* Show the template to the user.
*
* This method is a remnant of the old templating system, where templates where shown manually instead of
* returning a response.
*
* @return void
* @deprecated Do not use this method, use Twig + send() instead. Will be removed in 2.0
*/
public function show()
{
if ($this->useNewUI) {
echo $this->getContents();
} else {
require($this->findTemplatePath($this->template));
}
}
/**
* Find module the template is in, if any
*
* @param string $template The relative path from the theme directory to the template file.
*
* @return array An array with the name of the module and template
*/
private function findModuleAndTemplateName($template)
{
$tmp = explode(':', $template, 2);
return (count($tmp) === 2) ? [$tmp[0], $tmp[1]] : [null, $tmp[0]];
}
/**
* Find template path.
*
* This function locates the given template based on the template name. It will first search for the template in
* the current theme directory, and then the default theme.
*
* The template name may be on the form <module name>:<template path>, in which case it will search for the
* template file in the given module.
*
* @param string $template The relative path from the theme directory to the template file.
* @param bool $throw_exception
*
* @return string|null The absolute path to the template file.
*
* @throws Exception If the template file couldn't be found.
*/
private function findTemplatePath($template, $throw_exception = true)
{
assert(is_string($template));
$extensions = ['.tpl.php', '.php'];
list($templateModule, $templateName) = $this->findModuleAndTemplateName($template);
$templateModule = ($templateModule !== null) ? $templateModule : 'default';
// first check the current theme
if ($this->theme['module'] !== null) {
// .../module/<themeModule>/themes/<themeName>/<templateModule>/<templateName>
$filename = Module::getModuleDir($this->theme['module']) .
'/themes/' . $this->theme['name'] . '/' . $templateModule . '/' . $templateName;
} elseif ($templateModule !== 'default') {
// .../module/<templateModule>/templates/<templateName>
$filename = Module::getModuleDir($templateModule) . '/templates/' . $templateName;
} else {
// .../templates/<theme>/<templateName>
$base = $this->configuration->getPathValue('templatedir', 'templates/') ?: 'templates/';
$filename = $base . $templateName;
}
$filename = $this->normalizeTemplateName($filename);
foreach ($extensions as $extension) {
if (file_exists($filename . $extension)) {
return $filename . $extension;
}
}
// not found in current theme
Logger::debug(
$_SERVER['PHP_SELF'] . ' - Template: Could not find template file [' . $template . '] at [' .
$filename . '] - now trying the base template'
);
// try default theme
if ($templateModule !== 'default') {
// .../module/<templateModule>/templates/<templateName>
$filename = Module::getModuleDir($templateModule) . '/templates/' . $templateName;
} else {
// .../templates/<templateName>
$base = $this->configuration->getPathValue('templatedir', 'templates/') ?: 'templates/';
$filename = $base . '/' . $templateName;
}
$filename = $this->normalizeTemplateName($filename);
foreach ($extensions as $extension) {
if (file_exists($filename . $extension)) {
return $filename . $extension;
}
}
// not found in default template
if ($throw_exception) {
// log error and throw exception
$error = 'Template: Could not find template file [' . $template . '] at [' . $filename . ']';
Logger::critical($_SERVER['PHP_SELF'] . ' - ' . $error);
throw new Exception($error);
} else {
// missing template expected, return NULL
return null;
}
}
/**
* Return the internal translator object used by this template.
*
* @return Translate The translator that will be used with this template.
*/
public function getTranslator()
{
return $this->translator;
}
/**
* Return the internal localization object used by this template.
*
* @return Localization The localization object that will be used with this template.
*/
public function getLocalization()
{
return $this->localization;
}
/**
* Get the current instance of Twig in use.
*
* @return Environment The Twig instance in use, or null if Twig is not used.
*/
public function getTwig()
{
return $this->twig;
}
/*
* Deprecated methods of this interface, all of them should go away.
*/
/**
* @param string $name
*
* @return string
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::getLanguage()
* instead.
*/
public function getAttributeTranslation($name)
{
return $this->translator->getAttributeTranslation($name);
}
/**
* @return string
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::getLanguage()
* instead.
*/
public function getLanguage()
{
return $this->translator->getLanguage()->getLanguage();
}
/**
* @param string $language
* @param bool $setLanguageCookie
* @return void
*
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::setLanguage()
* instead.
*/
public function setLanguage($language, $setLanguageCookie = true)
{
$this->translator->getLanguage()->setLanguage($language, $setLanguageCookie);
}
/**
* @return null|string
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::getLanguageCookie()
* instead.
*/
public static function getLanguageCookie()
{
return Language::getLanguageCookie();
}
/**
* @param string $language
* @return void
*
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::setLanguageCookie()
* instead.
*/
public static function setLanguageCookie($language)
{
Language::setLanguageCookie($language);
}
/**
* Wraps Language->getLanguageList
*
* @return array
*/
private function getLanguageList()
{
return $this->translator->getLanguage()->getLanguageList();
}
/**
* @param string $tag
*
* @return array|null
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Translate::getTag() instead.
*/
public function getTag($tag)
{
return $this->translator->getTag($tag);
}
/**
* Temporary wrapper for \SimpleSAML\Locale\Translate::getPreferredTranslation().
*
* @deprecated This method will be removed in SSP 2.0. Please use
* \SimpleSAML\Locale\Translate::getPreferredTranslation() instead.
*
* @param array $translations
* @return string
*/
public function getTranslation($translations)
{
return $this->translator->getPreferredTranslation($translations);
}
/**
* Includes a file relative to the template base directory.
* This function can be used to include headers and footers etc.
*
* @deprecated This function will be removed in SSP 2.0. Use Twig-templates instead
* @param string $file
* @return void
*/
private function includeAtTemplateBase($file)
{
$data = $this->data;
$filename = $this->findTemplatePath($file);
include($filename);
}
/**
* Wraps Translate->includeInlineTranslation()
*
* @param string $tag
* @param string $translation
* @return void
*@deprecated This method will be removed in SSP 2.0. Please use
* \SimpleSAML\Locale\Translate::includeInlineTranslation() instead.
*
* @see \SAML\Locale\Translate::includeInlineTranslation()
*/
public function includeInlineTranslation($tag, $translation)
{
$this->translator->includeInlineTranslation($tag, $translation);
}
/**
* @param string $file
* @param Configuration|null $otherConfig
* @return void
*
* @deprecated This method will be removed in SSP 2.0. Please use
* \SimpleSAML\Locale\Translate::includeLanguageFile() instead.
*/
public function includeLanguageFile($file, $otherConfig = null)
{
$this->translator->includeLanguageFile($file, $otherConfig);
}
/**
* Wrap Language->isLanguageRTL
*
* @return bool
*/
private function isLanguageRTL()
{
return $this->translator->getLanguage()->isLanguageRTL();
}
/**
* Merge two translation arrays.
*
* @param array $def The array holding string definitions.
* @param array $lang The array holding translations for every string.
*
* @return array The recursive merge of both arrays.
* @deprecated This method will be removed in SimpleSAMLphp 2.0. Please use array_merge_recursive() instead.
*/
public static function lang_merge($def, $lang)
{
foreach ($def as $key => $value) {
if (array_key_exists($key, $lang)) {
$def[$key] = array_merge($value, $lang[$key]);
}
}
return $def;
}
/**
* Behave like Language->noop to mark a tag for translation but actually do it later.
*
* @param string $tag
* @return string
*@see \SAML\Locale\Translate::noop()
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Translate::noop() instead.
*
*/
public static function noop($tag)
{
return $tag;
}
/**
* Wrap Language->t to translate tag into the current language, with a fallback to english.
*
* @param string $tag
* @param array $replacements
* @param bool $fallbackdefault
* @param array $oldreplacements
* @param bool $striptags
* @return string|null
*@see \SAML\Locale\Translate::t()
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Translate::t() instead.
*
*/
public function t(
$tag,
$replacements = [],
$fallbackdefault = true,
$oldreplacements = [],
$striptags = false
) {
return $this->translator->t($tag, $replacements, $fallbackdefault, $oldreplacements, $striptags);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace SAML\XHTML;
use Twig\Environment;
/**
* Interface that allows modules to run several hooks for templates.
*
* @package SimpleSAMLphp
*/
interface TemplateControllerInterface
{
/**
* Implement to modify the twig environment after its initialization (e.g. add filters or extensions).
*
* @param Environment $twig The current twig environment.
*
* @return void
*/
public function setUpTwig(Environment &$twig);
/**
* Implement to add, delete or modify the data passed to the template.
*
* This method will be called right before displaying the template.
*
* @param array $data The current data used by the template.
*
* @return void
*/
public function display(&$data);
}

View File

@ -0,0 +1,83 @@
<?php
namespace SAML\XHTML;
use InvalidArgumentException;
use SAML\Module;
use Twig\Loader\FilesystemLoader;
/**
* This class extends the Twig\Loader\FilesystemLoader so that we can load templates from modules in twig, even
* when the main template is not part of a module (or the same one).
*
* @package simplesamlphp/simplesamlphp
*
* @psalm-suppress DeprecatedInterface This suppress may be removed when Twig 3.0 becomes the default
*/
class TemplateLoader extends FilesystemLoader
{
/**
* This method adds a namespace dynamically so that we can load templates from modules whenever we want.
*
* {@inheritdoc}
*
* @param string $name
* @param bool $throw
* @return string|false|null
*/
protected function findTemplate($name, $throw = true)
{
list($namespace, $shortname) = $this->parseModuleName($name);
if (!in_array($namespace, $this->paths, true) && $namespace !== self::MAIN_NAMESPACE) {
$this->addPath(self::getModuleTemplateDir($namespace), $namespace);
}
return parent::findTemplate($name, $throw);
}
/**
* Parse the name of a template in a module.
*
* @param string $name The full name of the template, including namespace and template name / path.
* @param string $default
*
* @return array An array with the corresponding namespace and name of the template. The namespace defaults to
* \Twig\Loader\FilesystemLoader::MAIN_NAMESPACE, if none was specified in $name.
*/
protected function parseModuleName($name, $default = self::MAIN_NAMESPACE)
{
if (strpos($name, ':')) {
// we have our old SSP format
list($namespace, $shortname) = explode(':', $name, 2);
$shortname = strtr($shortname, [
'.tpl.php' => '.twig',
'.php' => '.twig',
]);
return [$namespace, $shortname];
}
return [$default, $name];
}
/**
* Get the template directory of a module, if it exists.
*
* @param string $module
* @return string The templates directory of a module.
*
* @throws InvalidArgumentException If the module is not enabled or it has no templates directory.
*/
public static function getModuleTemplateDir($module)
{
if (!Module::isModuleEnabled($module)) {
throw new InvalidArgumentException('The module \'' . $module . '\' is not enabled.');
}
$moduledir = Module::getModuleDir($module);
// check if module has a /templates dir, if so, append
$templatedir = $moduledir . '/templates';
if (!is_dir($templatedir)) {
throw new InvalidArgumentException('The module \'' . $module . '\' has no templates directory.');
}
return $templatedir;
}
}

145
libsrc/SAML/XML/Errors.php Normal file
View File

@ -0,0 +1,145 @@
<?php
/**
* This class defines an interface for accessing errors from the XML library.
*
* In PHP versions which doesn't support accessing error information, this class
* will hide that, and pretend that no errors were logged.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
namespace SAML\XML;
use LibXMLError;
class Errors
{
/**
* @var array This is an stack of error logs. The topmost element is the one we are currently working on.
*/
private static $errorStack = [];
/**
* @var bool This is the xml error state we had before we began logging.
*/
private static $xmlErrorState;
/**
* Append current XML errors to the current stack level.
*
* @return void
*/
private static function addErrors()
{
$currentErrors = libxml_get_errors();
libxml_clear_errors();
$level = count(self::$errorStack) - 1;
self::$errorStack[$level] = array_merge(self::$errorStack[$level], $currentErrors);
}
/**
* Start error logging.
*
* A call to this function will begin a new error logging context. Every call must have
* a corresponding call to end().
*
* @return void
*/
public static function begin()
{
// Check whether the error access functions are present
if (!function_exists('libxml_use_internal_errors')) {
return;
}
if (count(self::$errorStack) === 0) {
// No error logging is currently in progress. Initialize it.
self::$xmlErrorState = libxml_use_internal_errors(true);
libxml_clear_errors();
} else {
/* We have already started error logging. Append the current errors to the
* list of errors in this level.
*/
self::addErrors();
}
// Add a new level to the error stack
self::$errorStack[] = [];
}
/**
* End error logging.
*
* @return array An array with the LibXMLErrors which has occurred since begin() was called.
*/
public static function end()
{
// Check whether the error access functions are present
if (!function_exists('libxml_use_internal_errors')) {
// Pretend that no errors occurred
return [];
}
// Add any errors which may have occurred
self::addErrors();
$ret = array_pop(self::$errorStack);
if (count(self::$errorStack) === 0) {
// Disable our error logging and restore the previous state
libxml_use_internal_errors(self::$xmlErrorState);
}
return $ret;
}
/**
* Format an error as a string.
*
* This function formats the given LibXMLError object as a string.
*
* @param LibXMLError $error The LibXMLError which should be formatted.
* @return string A string representing the given LibXMLError.
*/
public static function formatError($error)
{
assert($error instanceof LibXMLError);
return 'level=' . $error->level
. ',code=' . $error->code
. ',line=' . $error->line
. ',col=' . $error->column
. ',msg=' . trim($error->message);
}
/**
* Format a list of errors as a string.
*
* This fucntion takes an array of LibXMLError objects and creates a string with all the errors.
* Each error will be separated by a newline, and the string will end with a newline-character.
*
* @param array $errors An array of errors.
* @return string A string representing the errors. An empty string will be returned if there were no
* errors in the array.
*/
public static function formatErrors($errors)
{
assert(is_array($errors));
$ret = '';
foreach ($errors as $error) {
$ret .= self::formatError($error) . "\n";
}
return $ret;
}
}

117
libsrc/SAML/XML/Parser.php Normal file
View File

@ -0,0 +1,117 @@
<?php
/**
* This file will help doing XPath queries in SAML 2 XML documents.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
namespace SAML\XML;
use Exception;
use SimpleXMLElement;
class Parser
{
/** @var SimpleXMLElement */
public $simplexml;
/**
* @param string $xml
*/
public function __construct($xml)
{
$this->simplexml = new SimpleXMLElement($xml);
$this->simplexml->registerXPathNamespace('saml2', 'urn:oasis:names:tc:SAML:2.0:assertion');
$this->simplexml->registerXPathNamespace('saml2meta', 'urn:oasis:names:tc:SAML:2.0:metadata');
$this->simplexml->registerXPathNamespace('ds', 'http://www.w3.org/2000/09/xmldsig#');
}
/**
* @param SimpleXMLElement $element
* @return Parser
* @psalm-return \SimpleSAML\XML\Parser
*/
public static function fromSimpleXMLElement(SimpleXMLElement $element)
{
// Traverse all existing namespaces in element
$namespaces = $element->getNamespaces();
foreach ($namespaces as $prefix => $ns) {
$element[(($prefix === '') ? 'xmlns' : 'xmlns:' . $prefix)] = $ns;
}
/* Create a new parser with the xml document where the namespace definitions
* are added.
*/
$xml = $element->asXML();
if ($xml === false) {
throw new Exception('Error converting SimpleXMLElement to well-formed XML string.');
}
return new Parser($xml);
}
/**
* @param string $xpath
* @param string $defvalue
* @throws Exception
* @return string
*/
public function getValueDefault($xpath, $defvalue)
{
try {
/** @var string */
return $this->getValue($xpath, true);
} catch (Exception $e) {
return $defvalue;
}
}
/**
* @param string $xpath
* @param bool $required
* @throws Exception
* @return string|null
*/
public function getValue($xpath, $required = false)
{
$result = $this->simplexml->xpath($xpath);
if (!is_array($result) || empty($result)) {
if ($required) {
throw new Exception(
'Could not get value from XML document using the following XPath expression: ' . $xpath
);
} else {
return null;
}
}
return (string) $result[0];
}
/**
* @param array $xpath
* @param bool $required
* @throws Exception
* @return string|null
*/
public function getValueAlternatives(array $xpath, $required = false)
{
foreach ($xpath as $x) {
$seek = $this->getValue($x);
if ($seek) {
return $seek;
}
}
if ($required) {
throw new Exception(
'Could not get value from XML document using multiple alternative XPath expressions.'
);
} else {
return null;
}
}
}

View File

@ -0,0 +1,89 @@
<?php
/**
* The Shibboleth 1.3 Authentication Request. Not part of SAML 1.1,
* but an extension using query paramters no XML.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
* @deprecated This class will be removed in a future release
*/
namespace SAML\XML\Shib13;
use SAML\Metadata\MetaDataStorageHandler;
class AuthnRequest
{
/** @var string|null */
private $issuer = null;
/** @var string|null */
private $relayState = null;
/**
* @param string|null $relayState
* @return void
*/
public function setRelayState($relayState)
{
$this->relayState = $relayState;
}
/**
* @return string|null
*/
public function getRelayState()
{
return $this->relayState;
}
/**
* @param string|null $issuer
* @return void
*/
public function setIssuer($issuer)
{
$this->issuer = $issuer;
}
/**
* @return string|null
*/
public function getIssuer()
{
return $this->issuer;
}
/**
* @param string $destination
* @param string $shire
* @return string
*/
public function createRedirect($destination, $shire)
{
$metadata = MetaDataStorageHandler::getMetadataHandler();
$idpmetadata = $metadata->getMetaDataConfig($destination, 'shib13-idp-remote');
$desturl = $idpmetadata->getDefaultEndpoint(
'SingleSignOnService',
['urn:mace:shibboleth:1.0:profiles:AuthnRequest']
);
$desturl = $desturl['Location'];
$target = $this->getRelayState();
$issuer = $this->getIssuer();
assert($issuer !== null);
$url = $desturl . '?' .
'providerId=' . urlencode($issuer) .
'&shire=' . urlencode($shire) .
(isset($target) ? '&target=' . urlencode($target) : '');
return $url;
}
}

Some files were not shown because too many files have changed in this diff Show More