runff 1.0 commit
This commit is contained in:
361
lib/SimpleSAML/Metadata/MetaDataStorageHandler.php
Executable file
361
lib/SimpleSAML/Metadata/MetaDataStorageHandler.php
Executable file
@@ -0,0 +1,361 @@
|
||||
<?php
|
||||
|
||||
|
||||
/**
|
||||
* This file defines a class for metadata handling.
|
||||
*
|
||||
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
class SimpleSAML_Metadata_MetaDataStorageHandler
|
||||
{
|
||||
|
||||
|
||||
/**
|
||||
* 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 SimpleSAML_Metadata_MetaDataStorageHandler
|
||||
*/
|
||||
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 SimpleSAML_Metadata_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 SimpleSAML_Metadata_MetaDataStorageHandler The current metadata handler instance.
|
||||
*/
|
||||
public static function getMetadataHandler()
|
||||
{
|
||||
if (self::$metadataHandler === null) {
|
||||
self::$metadataHandler = new SimpleSAML_Metadata_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 = SimpleSAML_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 = array(array('type' => $type));
|
||||
}
|
||||
|
||||
try {
|
||||
$this->sources = SimpleSAML_Metadata_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 = SimpleSAML_Configuration::getInstance();
|
||||
assert($config instanceof SimpleSAML_Configuration);
|
||||
|
||||
$baseurl = \SimpleSAML\Utils\HTTP::getSelfURLHost().$config->getBasePath();
|
||||
|
||||
if ($set == 'saml20-sp-hosted') {
|
||||
if ($property === 'SingleLogoutServiceBinding') {
|
||||
return \SAML2\Constants::BINDING_HTTP_REDIRECT;
|
||||
}
|
||||
} elseif ($set == 'saml20-idp-hosted') {
|
||||
switch ($property) {
|
||||
case 'SingleSignOnService':
|
||||
return $baseurl.'saml2/idp/SSOService.php';
|
||||
|
||||
case 'SingleSignOnServiceBinding':
|
||||
return \SAML2\Constants::BINDING_HTTP_REDIRECT;
|
||||
|
||||
case 'SingleLogoutService':
|
||||
return $baseurl.'saml2/idp/SingleLogoutService.php';
|
||||
|
||||
case 'SingleLogoutServiceBinding':
|
||||
return \SAML2\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.
|
||||
*
|
||||
* @return array An associative array with the metadata from from the given set.
|
||||
*/
|
||||
public function getList($set = 'saml20-idp-remote')
|
||||
{
|
||||
assert(is_string($set));
|
||||
|
||||
$result = array();
|
||||
|
||||
foreach ($this->sources as $source) {
|
||||
$srcList = $source->getMetadataSet($set);
|
||||
|
||||
foreach ($srcList as $key => $le) {
|
||||
if (array_key_exists('expire', $le)) {
|
||||
if ($le['expire'] < time()) {
|
||||
unset($srcList[$key]);
|
||||
SimpleSAML\Logger::warning(
|
||||
"Dropping metadata entity ".var_export($key, true).", expired ".
|
||||
SimpleSAML\Utils\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 = \SimpleSAML\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 = \SimpleSAML\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 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 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 $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 SimpleSAML_Error_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 SimpleSAML_Error_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 SimpleSAML_Configuration The configuration object representing the metadata.
|
||||
* @throws SimpleSAML_Error_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 SimpleSAML_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|SimpleSAML_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 = array();
|
||||
|
||||
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 SimpleSAML_Configuration::loadFromArray(
|
||||
$remote_provider,
|
||||
$set.'/'.var_export($remote_provider['entityid'], true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
144
lib/SimpleSAML/Metadata/MetaDataStorageHandlerFlatFile.php
Executable file
144
lib/SimpleSAML/Metadata/MetaDataStorageHandlerFlatFile.php
Executable file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
|
||||
/**
|
||||
* 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 SimpleSAML_Metadata_MetaDataStorageHandlerFlatFile extends SimpleSAML_Metadata_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 = array();
|
||||
|
||||
|
||||
/**
|
||||
* 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 = SimpleSAML_Configuration::getInstance();
|
||||
|
||||
// find the path to the directory we should search for metadata in
|
||||
if (array_key_exists('directory', $config)) {
|
||||
$this->directory = $config['directory'];
|
||||
} else {
|
||||
$this->directory = $globalConfig->getString('metadatadir', 'metadata/');
|
||||
}
|
||||
|
||||
/* Resolve this directory relative to the SimpleSAMLphp directory (unless it is
|
||||
* an absolute path).
|
||||
*/
|
||||
$this->directory = $globalConfig->resolvePath($this->directory).'/';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 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 = array();
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
$metadataSet = $this->load($set);
|
||||
if ($metadataSet === null) {
|
||||
$metadataSet = array();
|
||||
}
|
||||
|
||||
// add the entity id of an entry to each entry in the metadata
|
||||
foreach ($metadataSet as $entityId => &$entry) {
|
||||
if (preg_match('/__DYNAMIC(:[0-9]+)?__/', $entityId)) {
|
||||
$entry['entityid'] = $this->generateDynamicHostedEntityID($set);
|
||||
} else {
|
||||
$entry['entityid'] = $entityId;
|
||||
}
|
||||
}
|
||||
|
||||
$this->cachedMetadata[$set] = $metadataSet;
|
||||
|
||||
return $metadataSet;
|
||||
}
|
||||
|
||||
|
||||
private function generateDynamicHostedEntityID($set)
|
||||
{
|
||||
// get the configuration
|
||||
$baseurl = \SimpleSAML\Utils\HTTP::getBaseURL();
|
||||
|
||||
if ($set === 'saml20-idp-hosted') {
|
||||
return $baseurl.'saml2/idp/metadata.php';
|
||||
} elseif ($set === 'shib13-idp-hosted') {
|
||||
return $baseurl.'shib13/idp/metadata.php';
|
||||
} elseif ($set === 'wsfed-sp-hosted') {
|
||||
return 'urn:federation:'.\SimpleSAML\Utils\HTTP::getSelfHost();
|
||||
} elseif ($set === 'adfs-idp-hosted') {
|
||||
return 'urn:federation:'.\SimpleSAML\Utils\HTTP::getSelfHost().':idp';
|
||||
} else {
|
||||
throw new Exception('Can not generate dynamic EntityID for metadata of this type: ['.$set.']');
|
||||
}
|
||||
}
|
||||
}
|
||||
306
lib/SimpleSAML/Metadata/MetaDataStorageHandlerPdo.php
Executable file
306
lib/SimpleSAML/Metadata/MetaDataStorageHandlerPdo.php
Executable file
@@ -0,0 +1,306 @@
|
||||
<?php
|
||||
|
||||
|
||||
/**
|
||||
* 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 SimpleSAML_Metadata_MetaDataStorageHandlerPdo extends SimpleSAML_Metadata_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 = array();
|
||||
|
||||
/**
|
||||
* All the metadata sets supported by this MetaDataStorageHandler
|
||||
*/
|
||||
public $supportedSets = array(
|
||||
'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',
|
||||
'wsfed-idp-remote',
|
||||
'wsfed-sp-hosted'
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* 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 = SimpleSAML\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 $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 SimpleSAML_Error_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 = array();
|
||||
|
||||
while ($d = $stmt->fetch()) {
|
||||
$data = json_decode($d['entity_data'], true);
|
||||
if ($data === null) {
|
||||
throw new SimpleSAML_Error_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 = array();
|
||||
}
|
||||
|
||||
foreach ($metadataSet as $entityId => &$entry) {
|
||||
if (preg_match('/__DYNAMIC(:[0-9]+)?__/', $entityId)) {
|
||||
$entry['entityid'] = $this->generateDynamicHostedEntityID($set);
|
||||
} else {
|
||||
$entry['entityid'] = $entityId;
|
||||
}
|
||||
}
|
||||
|
||||
$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 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));
|
||||
|
||||
$tableName = $this->getTableName($set);
|
||||
|
||||
if (!in_array($set, $this->supportedSets, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt = $this->db->read("SELECT entity_id, entity_data FROM $tableName WHERE entity_id=:entityId", array('entityId' => $entityId));
|
||||
if ($stmt->execute()) {
|
||||
$rowCount = 0;
|
||||
$data = null;
|
||||
|
||||
while ($d = $stmt->fetch()) {
|
||||
if (++$rowCount > 1) {
|
||||
SimpleSAML\Logger::warning("Duplicate match for $entityId in set $set");
|
||||
break;
|
||||
}
|
||||
$data = json_decode($d['entity_data'], true);
|
||||
if ($data === null) {
|
||||
throw new SimpleSAML_Error_Exception("Cannot decode metadata for entity '${d['entity_id']}'");
|
||||
}
|
||||
if (!array_key_exists('entityid', $data)) {
|
||||
$data['entityid'] = $d['entity_id'];
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
} else {
|
||||
throw new Exception('PDO metadata handler: Database error: '.var_export($this->db->getLastError(), true));
|
||||
}
|
||||
}
|
||||
|
||||
private function generateDynamicHostedEntityID($set)
|
||||
{
|
||||
assert(is_string($set));
|
||||
|
||||
// get the configuration
|
||||
$baseurl = \SimpleSAML\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 === 'wsfed-sp-hosted') {
|
||||
return 'urn:federation:'.\SimpleSAML\Utils\HTTP::getSelfHost();
|
||||
} elseif ($set === 'adfs-idp-hosted') {
|
||||
return 'urn:federation:'.\SimpleSAML\Utils\HTTP::getSelfHost().':idp';
|
||||
} else {
|
||||
throw new Exception('Can not generate dynamic EntityID for metadata of this type: ['.$set.']');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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",
|
||||
array(
|
||||
'entity_id' => $index,
|
||||
)
|
||||
);
|
||||
|
||||
$retrivedEntityIDs = $metadata->fetch();
|
||||
|
||||
$params = array(
|
||||
'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 === 0) {
|
||||
$fine = false;
|
||||
} else {
|
||||
$stmt += $rows;
|
||||
}
|
||||
}
|
||||
if (!$fine) {
|
||||
return false;
|
||||
}
|
||||
return $stmt;
|
||||
}
|
||||
}
|
||||
286
lib/SimpleSAML/Metadata/MetaDataStorageHandlerSerialize.php
Executable file
286
lib/SimpleSAML/Metadata/MetaDataStorageHandlerSerialize.php
Executable file
@@ -0,0 +1,286 @@
|
||||
<?php
|
||||
|
||||
|
||||
/**
|
||||
* Class for handling metadata files in serialized format.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
class SimpleSAML_Metadata_MetaDataStorageHandlerSerialize extends SimpleSAML_Metadata_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 = SimpleSAML_Configuration::getInstance();
|
||||
|
||||
$cfgHelp = SimpleSAML_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 = $globalConfig->resolvePath($this->directory);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 = array();
|
||||
|
||||
$dh = @opendir($this->directory);
|
||||
if ($dh === false) {
|
||||
SimpleSAML\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)) {
|
||||
SimpleSAML\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 = array();
|
||||
|
||||
$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) {
|
||||
SimpleSAML\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 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) {
|
||||
$error = error_get_last();
|
||||
SimpleSAML\Logger::warning(
|
||||
'Error reading file '.$filePath.': '.$error['message']
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = @unserialize($data);
|
||||
if ($data === false) {
|
||||
SimpleSAML\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 boolean 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)) {
|
||||
SimpleSAML\Logger::info('Creating directory: '.$dir);
|
||||
$res = @mkdir($dir, 0777, true);
|
||||
if ($res === false) {
|
||||
$error = error_get_last();
|
||||
SimpleSAML\Logger::error('Failed to create directory '.$dir.': '.$error['message']);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$data = serialize($metadata);
|
||||
|
||||
SimpleSAML\Logger::debug('Writing: '.$newPath);
|
||||
|
||||
$res = file_put_contents($newPath, $data);
|
||||
if ($res === false) {
|
||||
$error = error_get_last();
|
||||
SimpleSAML\Logger::error('Error saving file '.$newPath.': '.$error['message']);
|
||||
return false;
|
||||
}
|
||||
|
||||
$res = rename($newPath, $filePath);
|
||||
if ($res === false) {
|
||||
$error = error_get_last();
|
||||
SimpleSAML\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.
|
||||
*/
|
||||
public function deleteMetadata($entityId, $set)
|
||||
{
|
||||
assert(is_string($entityId));
|
||||
assert(is_string($set));
|
||||
|
||||
$filePath = $this->getMetadataPath($entityId, $set);
|
||||
|
||||
if (!file_exists($filePath)) {
|
||||
SimpleSAML\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) {
|
||||
$error = error_get_last();
|
||||
SimpleSAML\Logger::error(
|
||||
'Failed to delete file '.$filePath.
|
||||
': '.$error['message']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
116
lib/SimpleSAML/Metadata/MetaDataStorageHandlerXML.php
Executable file
116
lib/SimpleSAML/Metadata/MetaDataStorageHandlerXML.php
Executable file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
|
||||
/**
|
||||
* 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 SimpleSAML_Metadata_MetaDataStorageHandlerXML extends SimpleSAML_Metadata_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 = SimpleSAML_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 = array();
|
||||
$IdP1x = array();
|
||||
$SP20 = array();
|
||||
$IdP20 = array();
|
||||
$AAD = array();
|
||||
|
||||
if(isset($src)) {
|
||||
$entities = SimpleSAML_Metadata_SAMLParser::parseDescriptorsFile($src);
|
||||
} elseif(isset($srcXml)) {
|
||||
$entities = SimpleSAML_Metadata_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 = array(
|
||||
'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 array();
|
||||
}
|
||||
}
|
||||
275
lib/SimpleSAML/Metadata/MetaDataStorageSource.php
Executable file
275
lib/SimpleSAML/Metadata/MetaDataStorageSource.php
Executable file
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
|
||||
/**
|
||||
* 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 SimpleSAML_Metadata_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 = array();
|
||||
|
||||
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 mixed 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 SimpleSAML_Metadata_MetaDataStorageHandlerFlatFile($sourceConfig);
|
||||
case 'xml':
|
||||
return new SimpleSAML_Metadata_MetaDataStorageHandlerXML($sourceConfig);
|
||||
case 'serialize':
|
||||
return new SimpleSAML_Metadata_MetaDataStorageHandlerSerialize($sourceConfig);
|
||||
case 'mdx':
|
||||
case 'mdq':
|
||||
return new \SimpleSAML\Metadata\Sources\MDQ($sourceConfig);
|
||||
case 'pdo':
|
||||
return new SimpleSAML_Metadata_MetaDataStorageHandlerPdo($sourceConfig);
|
||||
default:
|
||||
// metadata store from module
|
||||
try {
|
||||
$className = SimpleSAML\Module::resolveClass(
|
||||
$type,
|
||||
'MetadataStore',
|
||||
'SimpleSAML_Metadata_MetaDataStorageSource'
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
throw new SimpleSAML\Error\CriticalConfigurationError(
|
||||
"Invalid 'type' for metadata source. Cannot find store '$type'.",
|
||||
null
|
||||
);
|
||||
}
|
||||
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 array();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
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 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 = array();
|
||||
|
||||
// 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 (SimpleSAML\Utils\Net::ipCIDRcheck($hint_entry, $ip)) {
|
||||
if ($type === 'entityid') {
|
||||
return $entry['entityid'];
|
||||
} else {
|
||||
return $index;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no entries matched, we should return null
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
*
|
||||
*/
|
||||
private function lookupIndexFromEntityId($entityId, $set)
|
||||
{
|
||||
assert(is_string($entityId));
|
||||
assert(isset($set));
|
||||
|
||||
$metadataSet = $this->getMetadataSet($set);
|
||||
|
||||
// check for hostname
|
||||
$currenthost = \SimpleSAML\Utils\HTTP::getSelfHost(); // sp.example.org
|
||||
|
||||
foreach ($metadataSet as $index => $entry) {
|
||||
if ($index === $entityId) {
|
||||
return $index;
|
||||
}
|
||||
if ($entry['entityid'] === $entityId) {
|
||||
if ($entry['host'] === '__DEFAULT__' || $entry['host'] === $currenthost) {
|
||||
return $index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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);
|
||||
|
||||
if (array_key_exists($index, $metadataSet)) {
|
||||
return $metadataSet[$index];
|
||||
}
|
||||
|
||||
$indexlookup = $this->lookupIndexFromEntityId($index, $set);
|
||||
if (isset($indexlookup) && array_key_exists($indexlookup, $metadataSet)) {
|
||||
return $metadataSet[$indexlookup];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
777
lib/SimpleSAML/Metadata/SAMLBuilder.php
Executable file
777
lib/SimpleSAML/Metadata/SAMLBuilder.php
Executable file
@@ -0,0 +1,777 @@
|
||||
<?php
|
||||
|
||||
|
||||
/**
|
||||
* 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 SimpleSAML_Metadata_SAMLBuilder
|
||||
{
|
||||
|
||||
|
||||
/**
|
||||
* The EntityDescriptor we are building.
|
||||
*
|
||||
* @var \SAML2\XML\md\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 double|null $maxCache The maximum time in seconds the metadata should be cached. Defaults to null
|
||||
* @param double|null $maxDuration The maximum time in seconds this metadata should be considered valid. Defaults
|
||||
* to null.
|
||||
*/
|
||||
public function __construct($entityId, $maxCache = null, $maxDuration = null)
|
||||
{
|
||||
assert(is_string($entityId));
|
||||
|
||||
$this->maxCache = $maxCache;
|
||||
$this->maxDuration = $maxDuration;
|
||||
|
||||
$this->entityDescriptor = new \SAML2\XML\md\EntityDescriptor();
|
||||
$this->entityDescriptor->entityID = $entityId;
|
||||
}
|
||||
|
||||
|
||||
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->cacheDuration = 'PT'.$this->maxCache.'S';
|
||||
}
|
||||
if ($this->maxDuration !== null) {
|
||||
$this->entityDescriptor->validUntil = 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) {
|
||||
SimpleSAML\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.
|
||||
*/
|
||||
public function addSecurityTokenServiceType($metadata)
|
||||
{
|
||||
assert(is_array($metadata));
|
||||
assert(isset($metadata['entityid']));
|
||||
assert(isset($metadata['metadata-set']));
|
||||
|
||||
$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);
|
||||
$defaultEndpoint = $metadata->getDefaultEndpoint('SingleSignOnService');
|
||||
$e = new sspmod_adfs_SAML2_XML_fed_SecurityTokenServiceType();
|
||||
$e->Location = $defaultEndpoint['Location'];
|
||||
|
||||
$this->addCertificate($e, $metadata);
|
||||
|
||||
$this->entityDescriptor->RoleDescriptor[] = $e;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add extensions to the metadata.
|
||||
*
|
||||
* @param SimpleSAML_Configuration $metadata The metadata to get extensions from.
|
||||
* @param \SAML2\XML\md\RoleDescriptor $e Reference to the element where the Extensions element should be included.
|
||||
*/
|
||||
private function addExtensions(SimpleSAML_Configuration $metadata, \SAML2\XML\md\RoleDescriptor $e)
|
||||
{
|
||||
if ($metadata->hasValue('tags')) {
|
||||
$a = new \SAML2\XML\saml\Attribute();
|
||||
$a->Name = 'tags';
|
||||
foreach ($metadata->getArray('tags') as $tag) {
|
||||
$a->AttributeValue[] = new \SAML2\XML\saml\AttributeValue($tag);
|
||||
}
|
||||
$e->Extensions[] = $a;
|
||||
}
|
||||
|
||||
if ($metadata->hasValue('hint.cidr')) {
|
||||
$a = new \SAML2\XML\saml\Attribute();
|
||||
$a->Name = 'hint.cidr';
|
||||
foreach ($metadata->getArray('hint.cidr') as $hint) {
|
||||
$a->AttributeValue[] = new \SAML2\XML\saml\AttributeValue($hint);
|
||||
}
|
||||
$e->Extensions[] = $a;
|
||||
}
|
||||
|
||||
if ($metadata->hasValue('scope')) {
|
||||
foreach ($metadata->getArray('scope') as $scopetext) {
|
||||
$s = new \SAML2\XML\shibmd\Scope();
|
||||
$s->scope = $scopetext;
|
||||
// Check whether $ ^ ( ) * | \ are in a scope -> assume regex.
|
||||
if (1 === preg_match('/[\$\^\)\(\*\|\\\\]/', $scopetext)) {
|
||||
$s->regexp = true;
|
||||
} else {
|
||||
$s->regexp = false;
|
||||
}
|
||||
$e->Extensions[] = $s;
|
||||
}
|
||||
}
|
||||
|
||||
if ($metadata->hasValue('EntityAttributes')) {
|
||||
$ea = new \SAML2\XML\mdattr\EntityAttributes();
|
||||
foreach ($metadata->getArray('EntityAttributes') as $attributeName => $attributeValues) {
|
||||
$a = new \SAML2\XML\saml\Attribute();
|
||||
$a->Name = $attributeName;
|
||||
$a->NameFormat = '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->Name = $matches[2];
|
||||
$nameFormat = $matches[1];
|
||||
if ($nameFormat !== \SAML2\Constants::NAMEFORMAT_UNSPECIFIED) {
|
||||
$a->NameFormat = $nameFormat;
|
||||
}
|
||||
}
|
||||
foreach ($attributeValues as $attributeValue) {
|
||||
$a->AttributeValue[] = new \SAML2\XML\saml\AttributeValue($attributeValue);
|
||||
}
|
||||
$ea->children[] = $a;
|
||||
}
|
||||
$this->entityDescriptor->Extensions[] = $ea;
|
||||
}
|
||||
|
||||
if ($metadata->hasValue('RegistrationInfo')) {
|
||||
$ri = new \SAML2\XML\mdrpi\RegistrationInfo();
|
||||
foreach ($metadata->getArray('RegistrationInfo') as $riName => $riValues) {
|
||||
switch ($riName) {
|
||||
case 'authority':
|
||||
$ri->registrationAuthority = $riValues;
|
||||
break;
|
||||
case 'instant':
|
||||
$ri->registrationInstant = \SAML2\Utils::xsDateTimeToTimestamp($riValues);
|
||||
break;
|
||||
case 'policies':
|
||||
$ri->RegistrationPolicy = $riValues;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->entityDescriptor->Extensions[] = $ri;
|
||||
}
|
||||
|
||||
if ($metadata->hasValue('UIInfo')) {
|
||||
$ui = new \SAML2\XML\mdui\UIInfo();
|
||||
foreach ($metadata->getArray('UIInfo') as $uiName => $uiValues) {
|
||||
switch ($uiName) {
|
||||
case 'DisplayName':
|
||||
$ui->DisplayName = $uiValues;
|
||||
break;
|
||||
case 'Description':
|
||||
$ui->Description = $uiValues;
|
||||
break;
|
||||
case 'InformationURL':
|
||||
$ui->InformationURL = $uiValues;
|
||||
break;
|
||||
case 'PrivacyStatementURL':
|
||||
$ui->PrivacyStatementURL = $uiValues;
|
||||
break;
|
||||
case 'Keywords':
|
||||
foreach ($uiValues as $lang => $keywords) {
|
||||
$uiItem = new \SAML2\XML\mdui\Keywords();
|
||||
$uiItem->lang = $lang;
|
||||
$uiItem->Keywords = $keywords;
|
||||
$ui->Keywords[] = $uiItem;
|
||||
}
|
||||
break;
|
||||
case 'Logo':
|
||||
foreach ($uiValues as $logo) {
|
||||
$uiItem = new \SAML2\XML\mdui\Logo();
|
||||
$uiItem->url = $logo['url'];
|
||||
$uiItem->width = $logo['width'];
|
||||
$uiItem->height = $logo['height'];
|
||||
if (isset($logo['lang'])) {
|
||||
$uiItem->lang = $logo['lang'];
|
||||
}
|
||||
$ui->Logo[] = $uiItem;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
$e->Extensions[] = $ui;
|
||||
}
|
||||
|
||||
if ($metadata->hasValue('DiscoHints')) {
|
||||
$dh = new \SAML2\XML\mdui\DiscoHints();
|
||||
foreach ($metadata->getArray('DiscoHints') as $dhName => $dhValues) {
|
||||
switch ($dhName) {
|
||||
case 'IPHint':
|
||||
$dh->IPHint = $dhValues;
|
||||
break;
|
||||
case 'DomainHint':
|
||||
$dh->DomainHint = $dhValues;
|
||||
break;
|
||||
case 'GeolocationHint':
|
||||
$dh->GeolocationHint = $dhValues;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$e->Extensions[] = $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.
|
||||
*/
|
||||
public function addOrganization(array $orgName, array $orgDisplayName, array $orgURL)
|
||||
{
|
||||
$org = new \SAML2\XML\md\Organization();
|
||||
|
||||
$org->OrganizationName = $orgName;
|
||||
$org->OrganizationDisplayName = $orgDisplayName;
|
||||
$org->OrganizationURL = $orgURL;
|
||||
|
||||
$this->entityDescriptor->Organization = $org;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add an Organization element based on metadata array.
|
||||
*
|
||||
* @param array $metadata The metadata we should extract the organization information from.
|
||||
*/
|
||||
public function addOrganizationInfo(array $metadata)
|
||||
{
|
||||
if (empty($metadata['OrganizationName']) ||
|
||||
empty($metadata['OrganizationDisplayName']) ||
|
||||
empty($metadata['OrganizationURL'])
|
||||
) {
|
||||
// empty or incomplete organization information
|
||||
return;
|
||||
}
|
||||
|
||||
$orgName = SimpleSAML\Utils\Arrays::arrayize($metadata['OrganizationName'], 'en');
|
||||
$orgDisplayName = SimpleSAML\Utils\Arrays::arrayize($metadata['OrganizationDisplayName'], 'en');
|
||||
$orgURL = SimpleSAML\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 = array();
|
||||
|
||||
foreach ($endpoints as &$ep) {
|
||||
if ($indexed) {
|
||||
$t = new \SAML2\XML\md\IndexedEndpointType();
|
||||
} else {
|
||||
$t = new \SAML2\XML\md\EndpointType();
|
||||
}
|
||||
|
||||
$t->Binding = $ep['Binding'];
|
||||
$t->Location = $ep['Location'];
|
||||
if (isset($ep['ResponseLocation'])) {
|
||||
$t->ResponseLocation = $ep['ResponseLocation'];
|
||||
}
|
||||
if (isset($ep['hoksso:ProtocolBinding'])) {
|
||||
$t->setAttributeNS(
|
||||
\SAML2\Constants::NS_HOK,
|
||||
'hoksso:ProtocolBinding',
|
||||
\SAML2\Constants::BINDING_HTTP_REDIRECT
|
||||
);
|
||||
}
|
||||
|
||||
if ($indexed) {
|
||||
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->index = $ep['index'];
|
||||
}
|
||||
|
||||
$ret[] = $t;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add an AttributeConsumingService element to the metadata.
|
||||
*
|
||||
* @param \SAML2\XML\md\SPSSODescriptor $spDesc The SPSSODescriptor element.
|
||||
* @param SimpleSAML_Configuration $metadata The metadata.
|
||||
*/
|
||||
private function addAttributeConsumingService(
|
||||
\SAML2\XML\md\SPSSODescriptor $spDesc,
|
||||
SimpleSAML_Configuration $metadata
|
||||
) {
|
||||
$attributes = $metadata->getArray('attributes', array());
|
||||
$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', array());
|
||||
|
||||
/*
|
||||
* Add an AttributeConsumingService element with information as name and description and list
|
||||
* of requested attributes
|
||||
*/
|
||||
$attributeconsumer = new \SAML2\XML\md\AttributeConsumingService();
|
||||
|
||||
$attributeconsumer->index = $metadata->getInteger('attributes.index', 0);
|
||||
|
||||
if ($metadata->hasValue('attributes.isDefault')) {
|
||||
$attributeconsumer->isDefault = $metadata->getBoolean('attributes.isDefault', false);
|
||||
}
|
||||
|
||||
$attributeconsumer->ServiceName = $name;
|
||||
$attributeconsumer->ServiceDescription = $metadata->getLocalizedString('description', array());
|
||||
|
||||
$nameFormat = $metadata->getString('attributes.NameFormat', \SAML2\Constants::NAMEFORMAT_UNSPECIFIED);
|
||||
foreach ($attributes as $friendlyName => $attribute) {
|
||||
$t = new \SAML2\XML\md\RequestedAttribute();
|
||||
$t->Name = $attribute;
|
||||
if (!is_int($friendlyName)) {
|
||||
$t->FriendlyName = $friendlyName;
|
||||
}
|
||||
if ($nameFormat !== \SAML2\Constants::NAMEFORMAT_UNSPECIFIED) {
|
||||
$t->NameFormat = $nameFormat;
|
||||
}
|
||||
if (in_array($attribute, $attributesrequired, true)) {
|
||||
$t->isRequired = true;
|
||||
}
|
||||
$attributeconsumer->RequestedAttribute[] = $t;
|
||||
}
|
||||
|
||||
$spDesc->AttributeConsumingService[] = $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.
|
||||
*/
|
||||
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:
|
||||
SimpleSAML\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.
|
||||
*/
|
||||
public function addMetadataSP20($metadata, $protocols = array(\SAML2\Constants::NS_SAMLP))
|
||||
{
|
||||
assert(is_array($metadata));
|
||||
assert(is_array($protocols));
|
||||
assert(isset($metadata['entityid']));
|
||||
assert(isset($metadata['metadata-set']));
|
||||
|
||||
$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);
|
||||
|
||||
$e = new \SAML2\XML\md\SPSSODescriptor();
|
||||
$e->protocolSupportEnumeration = $protocols;
|
||||
|
||||
if ($metadata->hasValue('saml20.sign.assertion')) {
|
||||
$e->WantAssertionsSigned = $metadata->getBoolean('saml20.sign.assertion');
|
||||
}
|
||||
|
||||
if ($metadata->hasValue('redirect.validate')) {
|
||||
$e->AuthnRequestsSigned = $metadata->getBoolean('redirect.validate');
|
||||
} elseif ($metadata->hasValue('validate.authnrequest')) {
|
||||
$e->AuthnRequestsSigned = $metadata->getBoolean('validate.authnrequest');
|
||||
}
|
||||
|
||||
$this->addExtensions($metadata, $e);
|
||||
|
||||
$this->addCertificate($e, $metadata);
|
||||
|
||||
$e->SingleLogoutService = self::createEndpoints($metadata->getEndpoints('SingleLogoutService'), false);
|
||||
|
||||
$e->NameIDFormat = $metadata->getArrayizeString('NameIDFormat', array());
|
||||
|
||||
$endpoints = $metadata->getEndpoints('AssertionConsumerService');
|
||||
foreach ($metadata->getArrayizeString('AssertionConsumerService.artifact', array()) as $acs) {
|
||||
$endpoints[] = array(
|
||||
'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact',
|
||||
'Location' => $acs,
|
||||
);
|
||||
}
|
||||
$e->AssertionConsumerService = self::createEndpoints($endpoints, true);
|
||||
|
||||
$this->addAttributeConsumingService($e, $metadata);
|
||||
|
||||
$this->entityDescriptor->RoleDescriptor[] = $e;
|
||||
|
||||
foreach ($metadata->getArray('contacts', array()) as $contact) {
|
||||
if (array_key_exists('contactType', $contact) && array_key_exists('emailAddress', $contact)) {
|
||||
$this->addContact($contact['contactType'], \SimpleSAML\Utils\Config\Metadata::getContact($contact));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add metadata of a SAML 2.0 identity provider.
|
||||
*
|
||||
* @param array $metadata The metadata.
|
||||
*/
|
||||
public function addMetadataIdP20($metadata)
|
||||
{
|
||||
assert(is_array($metadata));
|
||||
assert(isset($metadata['entityid']));
|
||||
assert(isset($metadata['metadata-set']));
|
||||
|
||||
$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);
|
||||
|
||||
$e = new \SAML2\XML\md\IDPSSODescriptor();
|
||||
$e->protocolSupportEnumeration[] = 'urn:oasis:names:tc:SAML:2.0:protocol';
|
||||
|
||||
if ($metadata->hasValue('sign.authnrequest')) {
|
||||
$e->WantAuthnRequestsSigned = $metadata->getBoolean('sign.authnrequest');
|
||||
} elseif ($metadata->hasValue('redirect.sign')) {
|
||||
$e->WantAuthnRequestsSigned = $metadata->getBoolean('redirect.sign');
|
||||
}
|
||||
|
||||
$this->addExtensions($metadata, $e);
|
||||
|
||||
$this->addCertificate($e, $metadata);
|
||||
|
||||
if ($metadata->hasValue('ArtifactResolutionService')) {
|
||||
$e->ArtifactResolutionService = self::createEndpoints(
|
||||
$metadata->getEndpoints('ArtifactResolutionService'),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
$e->SingleLogoutService = self::createEndpoints($metadata->getEndpoints('SingleLogoutService'), false);
|
||||
|
||||
$e->NameIDFormat = $metadata->getArrayizeString('NameIDFormat', array());
|
||||
|
||||
$e->SingleSignOnService = self::createEndpoints($metadata->getEndpoints('SingleSignOnService'), false);
|
||||
|
||||
$this->entityDescriptor->RoleDescriptor[] = $e;
|
||||
|
||||
foreach ($metadata->getArray('contacts', array()) as $contact) {
|
||||
if (array_key_exists('contactType', $contact) && array_key_exists('emailAddress', $contact)) {
|
||||
$this->addContact($contact['contactType'], \SimpleSAML\Utils\Config\Metadata::getContact($contact));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add metadata of a SAML 1.1 service provider.
|
||||
*
|
||||
* @param array $metadata The metadata.
|
||||
*/
|
||||
public function addMetadataSP11($metadata)
|
||||
{
|
||||
assert(is_array($metadata));
|
||||
assert(isset($metadata['entityid']));
|
||||
assert(isset($metadata['metadata-set']));
|
||||
|
||||
$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);
|
||||
|
||||
$e = new \SAML2\XML\md\SPSSODescriptor();
|
||||
$e->protocolSupportEnumeration[] = 'urn:oasis:names:tc:SAML:1.1:protocol';
|
||||
|
||||
$this->addCertificate($e, $metadata);
|
||||
|
||||
$e->NameIDFormat = $metadata->getArrayizeString('NameIDFormat', array());
|
||||
|
||||
$endpoints = $metadata->getEndpoints('AssertionConsumerService');
|
||||
foreach ($metadata->getArrayizeString('AssertionConsumerService.artifact', array()) as $acs) {
|
||||
$endpoints[] = array(
|
||||
'Binding' => 'urn:oasis:names:tc:SAML:1.0:profiles:artifact-01',
|
||||
'Location' => $acs,
|
||||
);
|
||||
}
|
||||
$e->AssertionConsumerService = self::createEndpoints($endpoints, true);
|
||||
|
||||
$this->addAttributeConsumingService($e, $metadata);
|
||||
|
||||
$this->entityDescriptor->RoleDescriptor[] = $e;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add metadata of a SAML 1.1 identity provider.
|
||||
*
|
||||
* @param array $metadata The metadata.
|
||||
*/
|
||||
public function addMetadataIdP11($metadata)
|
||||
{
|
||||
assert(is_array($metadata));
|
||||
assert(isset($metadata['entityid']));
|
||||
assert(isset($metadata['metadata-set']));
|
||||
|
||||
$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);
|
||||
|
||||
$e = new \SAML2\XML\md\IDPSSODescriptor();
|
||||
$e->protocolSupportEnumeration[] = 'urn:oasis:names:tc:SAML:1.1:protocol';
|
||||
$e->protocolSupportEnumeration[] = 'urn:mace:shibboleth:1.0';
|
||||
|
||||
$this->addCertificate($e, $metadata);
|
||||
|
||||
$e->NameIDFormat = $metadata->getArrayizeString('NameIDFormat', array());
|
||||
|
||||
$e->SingleSignOnService = self::createEndpoints($metadata->getEndpoints('SingleSignOnService'), false);
|
||||
|
||||
$this->entityDescriptor->RoleDescriptor[] = $e;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add metadata of a SAML attribute authority.
|
||||
*
|
||||
* @param array $metadata The AttributeAuthorityDescriptor, in the format returned by
|
||||
* SimpleSAML_Metadata_SAMLParser.
|
||||
*/
|
||||
public function addAttributeAuthority(array $metadata)
|
||||
{
|
||||
assert(is_array($metadata));
|
||||
assert(isset($metadata['entityid']));
|
||||
assert(isset($metadata['metadata-set']));
|
||||
|
||||
$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);
|
||||
|
||||
$e = new \SAML2\XML\md\AttributeAuthorityDescriptor();
|
||||
$e->protocolSupportEnumeration = $metadata->getArray('protocols', array(\SAML2\Constants::NS_SAMLP));
|
||||
|
||||
$this->addExtensions($metadata, $e);
|
||||
$this->addCertificate($e, $metadata);
|
||||
|
||||
$e->AttributeService = self::createEndpoints($metadata->getEndpoints('AttributeService'), false);
|
||||
$e->AssertionIDRequestService = self::createEndpoints(
|
||||
$metadata->getEndpoints('AssertionIDRequestService'),
|
||||
false
|
||||
);
|
||||
|
||||
$e->NameIDFormat = $metadata->getArrayizeString('NameIDFormat', array());
|
||||
|
||||
$this->entityDescriptor->RoleDescriptor[] = $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.
|
||||
*
|
||||
* @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, array('technical', 'support', 'administrative', 'billing', 'other'), true));
|
||||
|
||||
// TODO: remove this check as soon as getContact() is called always before calling this function
|
||||
$details = \SimpleSAML\Utils\Config\Metadata::getContact($details);
|
||||
|
||||
$e = new \SAML2\XML\md\ContactPerson();
|
||||
$e->contactType = $type;
|
||||
|
||||
if (!empty($details['attributes'])) {
|
||||
$e->ContactPersonAttributes = $details['attributes'];
|
||||
}
|
||||
|
||||
if (isset($details['company'])) {
|
||||
$e->Company = $details['company'];
|
||||
}
|
||||
if (isset($details['givenName'])) {
|
||||
$e->GivenName = $details['givenName'];
|
||||
}
|
||||
if (isset($details['surName'])) {
|
||||
$e->SurName = $details['surName'];
|
||||
}
|
||||
|
||||
if (isset($details['emailAddress'])) {
|
||||
$eas = $details['emailAddress'];
|
||||
if (!is_array($eas)) {
|
||||
$eas = array($eas);
|
||||
}
|
||||
foreach ($eas as $ea) {
|
||||
$e->EmailAddress[] = $ea;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($details['telephoneNumber'])) {
|
||||
$tlfNrs = $details['telephoneNumber'];
|
||||
if (!is_array($tlfNrs)) {
|
||||
$tlfNrs = array($tlfNrs);
|
||||
}
|
||||
foreach ($tlfNrs as $tlfNr) {
|
||||
$e->TelephoneNumber[] = $tlfNr;
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityDescriptor->ContactPerson[] = $e;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a KeyDescriptor with an X509 certificate.
|
||||
*
|
||||
* @param \SAML2\XML\md\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.
|
||||
*/
|
||||
private function addX509KeyDescriptor(\SAML2\XML\md\RoleDescriptor $rd, $use, $x509data)
|
||||
{
|
||||
assert(in_array($use, array('encryption', 'signing'), true));
|
||||
assert(is_string($x509data));
|
||||
|
||||
$keyDescriptor = \SAML2\Utils::createKeyDescriptor($x509data);
|
||||
$keyDescriptor->use = $use;
|
||||
$rd->KeyDescriptor[] = $keyDescriptor;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a certificate.
|
||||
*
|
||||
* Helper function for adding a certificate to the metadata.
|
||||
*
|
||||
* @param \SAML2\XML\md\RoleDescriptor $rd The RoleDescriptor the certificate should be added to.
|
||||
* @param SimpleSAML_Configuration $metadata The metadata of the entity.
|
||||
*/
|
||||
private function addCertificate(\SAML2\XML\md\RoleDescriptor $rd, SimpleSAML_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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
1478
lib/SimpleSAML/Metadata/SAMLParser.php
Executable file
1478
lib/SimpleSAML/Metadata/SAMLParser.php
Executable file
File diff suppressed because it is too large
Load Diff
283
lib/SimpleSAML/Metadata/Signer.php
Executable file
283
lib/SimpleSAML/Metadata/Signer.php
Executable file
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
|
||||
|
||||
/**
|
||||
* This class implements a helper function for signing of metadata.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS.
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
class SimpleSAML_Metadata_Signer
|
||||
{
|
||||
|
||||
/**
|
||||
* This functions finds what key & certificate files should be used to sign the metadata
|
||||
* for the given entity.
|
||||
*
|
||||
* @param SimpleSAML_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 = array(
|
||||
'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 = array('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 = array(
|
||||
'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 SimpleSAML_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 SimpleSAML_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 \SimpleSAML\Error\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 \SimpleSAML\Error\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 = array(
|
||||
XMLSecurityKey::RSA_SHA1,
|
||||
XMLSecurityKey::RSA_SHA256,
|
||||
XMLSecurityKey::RSA_SHA384,
|
||||
XMLSecurityKey::RSA_SHA512,
|
||||
);
|
||||
|
||||
if (!in_array($alg, $supported_algs, true)) {
|
||||
throw new \SimpleSAML\Error\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 array(
|
||||
'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 = SimpleSAML_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 = \SimpleSAML\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 = \SimpleSAML\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 = \SAML2\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'], array('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
|
||||
$rootNode = $xml->firstChild;
|
||||
|
||||
// sign the metadata with our private key
|
||||
$objXMLSecDSig = new XMLSecurityDSig();
|
||||
|
||||
$objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
|
||||
|
||||
$objXMLSecDSig->addReferenceList(
|
||||
array($rootNode),
|
||||
$signature_cf['digest'],
|
||||
array('http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N),
|
||||
array('id_name' => 'ID')
|
||||
);
|
||||
|
||||
$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();
|
||||
}
|
||||
}
|
||||
337
lib/SimpleSAML/Metadata/Sources/MDQ.php
Executable file
337
lib/SimpleSAML/Metadata/Sources/MDQ.php
Executable file
@@ -0,0 +1,337 @@
|
||||
<?php
|
||||
|
||||
namespace SimpleSAML\Metadata\Sources;
|
||||
|
||||
use SimpleSAML\Logger;
|
||||
use SimpleSAML\Utils\HTTP;
|
||||
|
||||
/**
|
||||
* 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 \SimpleSAML_Metadata_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;
|
||||
|
||||
/**
|
||||
* 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 (array_key_exists('cachedir', $config)) {
|
||||
$globalConfig = \SimpleSAML_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 array();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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));
|
||||
|
||||
$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)) {
|
||||
$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.
|
||||
*/
|
||||
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 \SimpleSAML_Metadata_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(\SimpleSAML_Metadata_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 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 = 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 = \SimpleSAML_Metadata_SAMLParser::parseString($xmldata);
|
||||
Logger::debug(__CLASS__.': completed parsing of ['.$mdq_url.']');
|
||||
|
||||
if ($this->validateFingerprint !== null) {
|
||||
if (!$entity->validateFingerprint($this->validateFingerprint)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user