Newer
Older
use Exception;
use SimpleSAML\Auth;
use SimpleSAML\Configuration;
use SimpleSAML\HTTP\RunnableResponse;
use SimpleSAML\Locale\Translate;
use SimpleSAML\Logger;
use SimpleSAML\Metadata\MetaDataStorageHandler;
use SimpleSAML\Metadata\SAMLBuilder;
use SimpleSAML\Metadata\SAMLParser;
use SimpleSAML\Metadata\Signer;
use SimpleSAML\Module;
use SimpleSAML\Module\adfs\IdP\ADFS as ADFS_IdP;
use SimpleSAML\Module\saml\IdP\SAML1 as SAML1_IdP;
use SimpleSAML\Module\saml\IdP\SAML2 as SAML2_IdP;
use SimpleSAML\Utils;
use SimpleSAML\XHTML\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\VarExporter\VarExporter;
/**
* Controller class for the admin module.
*
* This class serves the federation views available in the module.
*
* @package SimpleSAML\Module\admin
*/
{
/** @var \SimpleSAML\Configuration */
protected $config;
/**
* @var \SimpleSAML\Auth\Source|string
* @psalm-var \SimpleSAML\Auth\Source|class-string
*/
protected $authSource = Auth\Source::class;
/**
* @var \SimpleSAML\Utils\Auth|string
* @psalm-var \SimpleSAML\Utils\Auth|class-string
*/
protected $authUtils = Utils\Auth::class;
/** @var \SimpleSAML\Metadata\MetaDataStorageHandler */
protected $mdHandler;
/** @var Menu */
protected $menu;
/**
* FederationController constructor.
*
* @param \SimpleSAML\Configuration $config The configuration to use.
*/
public function __construct(Configuration $config)
{
$this->config = $config;
$this->menu = new Menu();
$this->mdHandler = MetaDataStorageHandler::getMetadataHandler();
}
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
/**
* Inject the \SimpleSAML\Auth\Source dependency.
*
* @param \SimpleSAML\Auth\Source $authSource
*/
public function setAuthSource(Auth\Source $authSource): void
{
$this->authSource = $authSource;
}
/**
* Inject the \SimpleSAML\Utils\Auth dependency.
*
* @param \SimpleSAML\Utils\Auth $authUtils
*/
public function setAuthUtils(Utils\Auth $authUtils): void
{
$this->authUtils = $authUtils;
}
/**
* Inject the \SimpleSAML\Metadata\MetadataStorageHandler dependency.
*
* @param \SimpleSAML\Metadata\MetaDataStorageHandler $mdHandler
*/
public function setMetadataStorageHandler(MetadataStorageHandler $mdHandler): void
{
$this->mdHandler = $mdHandler;
}
/**
* Display the federation page.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @return \SimpleSAML\XHTML\Template
* @throws \SimpleSAML\Error\Exception
public function main(/** @scrutinizer ignore-unused */ Request $request): Template
// initialize basic metadata array
$hostedSPs = $this->getHostedSP();
$hostedIdPs = $this->getHostedIdP();
$entries = [
'hosted' => array_merge($hostedSPs, $hostedIdPs),
'remote' => [
'saml20-idp-remote' => !empty($hostedSPs) ? $this->mdHandler->getList('saml20-idp-remote', true) : [],
'saml20-sp-remote' => $this->config->getBoolean('enable.saml20-idp', false) === true
? $this->mdHandler->getList('saml20-sp-remote', true) : [],
'adfs-sp-remote' => ($this->config->getBoolean('enable.adfs-idp', false) === true) &&
Module::isModuleEnabled('adfs') ? $this->mdHandler->getList('adfs-sp-remote', true) : [],
],
];
// initialize template and language
$t = new Template($this->config, 'admin:federation.twig');
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
$language = $t->getTranslator()->getLanguage()->getLanguage();
$defaultLang = $this->config->getString('language.default', 'en');
// process hosted entities
foreach ($entries['hosted'] as $index => $entity) {
if (isset($entity['name']) && is_string($entity['name'])) {
// if the entity has no internationalized name, fake it
$entries['hosted'][$index]['name'] = [$language => $entity['name']];
}
}
// clean up empty remote entries
foreach ($entries['remote'] as $key => $value) {
if (empty($value)) {
unset($entries['remote'][$key]);
}
}
$translators = [
'name' => 'name_translated',
'descr' => 'descr_translated',
'OrganizationDisplayName' => 'organizationdisplayname_translated',
];
foreach ($entries['remote'] as $key => $set) {
foreach ($set as $entityid => $entity) {
foreach ($translators as $old => $new) {
if (isset($entity[$old][$language])) {
$entries['remote'][$key][$entityid][$new] = $entity[$old][$language];
} elseif (isset($entity[$old][$defaultLang])) {
$entries['remote'][$key][$entityid][$new] = $entity[$old][$defaultLang];
} elseif (isset($entity[$old]['en'])) {
$entries['remote'][$key][$entityid][$new] = $entity[$old]['en'];
} elseif (isset($entries['remote'][$key][$entityid][$old])) {
$old_entry = $entries['remote'][$key][$entityid][$old];
$entries['remote'][$key][$entityid][$new] = is_array($old_entry) ? $entityid : $old_entry;
}
}
}
}
$t->data = [
'links' => [
[
'href' => Module::getModuleURL('admin/federation/metadata-converter'),
'text' => Translate::noop('XML to SimpleSAMLphp metadata converter'),
]
],
'entries' => $entries,
'mdtype' => [
'saml20-sp-remote' => Translate::noop('SAML 2.0 SP metadata'),
'saml20-sp-hosted' => Translate::noop('SAML 2.0 SP metadata'),
'saml20-idp-remote' => Translate::noop('SAML 2.0 IdP metadata'),
'saml20-idp-hosted' => Translate::noop('SAML 2.0 IdP metadata'),
'adfs-sp-remote' => Translate::noop('ADFS SP metadata'),
'adfs-sp-hosted' => Translate::noop('ADFS SP metadata'),
'adfs-idp-remote' => Translate::noop('ADFS IdP metadata'),
'adfs-idp-hosted' => Translate::noop('ADFS IdP metadata'),
],
'logouturl' => Utils\Auth::getAdminLogoutURL(),
];
Module::callHooks('federationpage', $t);
Assert::isInstanceOf($t, Template::class);
$this->menu->addOption('logout', $t->data['logouturl'], Translate::noop('Log out'));
return $this->menu->insert($t);
}
/**
* Get a list of the hosted IdP entities, including SAML 2, SAML 1.1 and ADFS.
*
* @return array
* @throws \Exception
*/
private function getHostedIdP(): array
{
$entities = [];
// SAML 2
if ($this->config->getBoolean('enable.saml20-idp', false)) {
try {
$idps = $this->mdHandler->getList('saml20-idp-hosted');
$saml2entities = [];
if (count($idps) > 1) {
foreach ($idps as $index => $idp) {
$idp['url'] = Module::getModuleURL('saml2/idp/metadata/' . $idp['auth']);
$idp['metadata-set'] = 'saml20-idp-hosted';
$idp['metadata-index'] = $index;
$idp['metadata_array'] = SAML2_IdP::getHostedMetadata($idp['entityid']);
$saml2entities[] = $idp;
}
} else {
$saml2entities['saml20-idp'] = $this->mdHandler->getMetaDataCurrent('saml20-idp-hosted');
$saml2entities['saml20-idp']['url'] = Utils\HTTP::getBaseURL() . 'saml2/idp/metadata.php';
$saml2entities['saml20-idp']['metadata_array'] = SAML2_IdP::getHostedMetadata(
$this->mdHandler->getMetaDataCurrentEntityID('saml20-idp-hosted')
);
}
foreach ($saml2entities as $index => $entity) {
$builder = new SAMLBuilder($entity['entityid']);
$builder->addMetadataIdP20($entity['metadata_array']);
$builder->addOrganizationInfo($entity['metadata_array']);
foreach ($entity['metadata_array']['contacts'] as $contact) {
$builder->addContact($contact['contactType'], $contact);
}
$entity['metadata'] = Signer::sign(
$builder->getEntityDescriptorText(),
$entity['metadata_array'],
'SAML 2 IdP'
);
$entities[$index] = $entity;
}
} catch (\Exception $e) {
Logger::error('Federation: Error loading saml20-idp: ' . $e->getMessage());
}
}
// ADFS
if ($this->config->getBoolean('enable.adfs-idp', false) && Module::isModuleEnabled('adfs')) {
try {
$idps = $this->mdHandler->getList('adfs-idp-hosted');
$adfsentities = [];
if (count($idps) > 1) {
foreach ($idps as $index => $idp) {
$idp['url'] = Module::getModuleURL('adfs/idp/metadata/' . $idp['auth']);
$idp['metadata-set'] = 'adfs-idp-hosted';
$idp['metadata-index'] = $index;
$idp['metadata_array'] = ADFS_IdP::getHostedMetadata($idp['entityid']);
$adfsentities[] = $idp;
}
} else {
$adfsentities['adfs-idp'] = $this->mdHandler->getMetaDataCurrent('adfs-idp-hosted');
$adfsentities['adfs-idp']['url'] = Module::getModuleURL('adfs/idp/metadata.php');
$adfsentities['adfs-idp']['metadata_array'] = ADFS_IdP::getHostedMetadata(
$this->mdHandler->getMetaDataCurrentEntityID('adfs-idp-hosted')
);
}
foreach ($adfsentities as $index => $entity) {
$builder = new SAMLBuilder($entity['entityid']);
$builder->addSecurityTokenServiceType($entity['metadata_array']);
$builder->addOrganizationInfo($entity['metadata_array']);
foreach ($entity['metadata_array']['contacts'] as $contact) {
$builder->addContact($contact['contactType'], $contact);
}
$entity['metadata'] = Signer::sign(
$builder->getEntityDescriptorText(),
$entity['metadata_array'],
'ADFS IdP'
);
$entities[$index] = $entity;
}
} catch (\Exception $e) {
Logger::error('Federation: Error loading adfs-idp: ' . $e->getMessage());
}
}
// process certificate information and dump the metadata array
foreach ($entities as $index => $entity) {
$entities[$index]['type'] = $entity['metadata-set'];
foreach ($entity['metadata_array']['keys'] as $kidx => $key) {
$key['url'] = Module::getModuleURL(
'admin/federation/cert',
[
'set' => $entity['metadata-set'],
'idp' => $entity['metadata-index'],
'prefix' => $key['prefix'],
]
);
$key['name'] = 'idp';
unset($entity['metadata_array']['keys'][$kidx]['prefix']);
$entities[$index]['certificates'][] = $key;
}
// only one key, reduce
if (count($entity['metadata_array']['keys']) === 1) {
$cert = array_pop($entity['metadata_array']['keys']);
$entity['metadata_array']['certData'] = $cert['X509Certificate'];
unset($entity['metadata_array']['keys']);
}
$entities[$index]['metadata_array'] = VarExporter::export($entity['metadata_array']);
}
return $entities;
}
/**
* Get an array of entities describing the local SP instances.
*
* @return array
* @throws \SimpleSAML\Error\Exception If OrganizationName is set for an SP instance but OrganizationURL is not.
*/
private function getHostedSP(): array
{
$entities = [];
/** @var \SimpleSAML\Module\saml\Auth\Source\SP $source */
foreach ($this->authSource::getSourcesOfType('saml:SP') as $source) {
$metadata = $source->getHostedMetadata();
$certificates = $metadata['keys'];
if (count($metadata['keys']) === 1) {
$cert = array_pop($metadata['keys']);
$metadata['certData'] = $cert['X509Certificate'];
unset($metadata['keys']);
}
} else {
$certificates = [];
}
// get the name
$name = $source->getMetadata()->getLocalizedString(
'name',
$source->getMetadata()->getLocalizedString('OrganizationDisplayName', $source->getAuthId())
);
$builder = new SAMLBuilder($source->getEntityId());
$builder->addMetadataSP20($metadata, $source->getSupportedProtocols());
$builder->addOrganizationInfo($metadata);
$xml = $builder->getEntityDescriptorText(true);
// sanitize the resulting array
unset($metadata['UIInfo']);
unset($metadata['metadata-set']);
unset($metadata['entityid']);
// sanitize the attributes array to remove friendly names
if (isset($metadata['attributes']) && is_array($metadata['attributes'])) {
$metadata['attributes'] = array_values($metadata['attributes']);
}
// sign the metadata if enabled
$xml = Signer::sign($xml, $source->getMetadata()->toArray(), 'SAML 2 SP');
$entities[] = [
'authid' => $source->getAuthId(),
'entityid' => $source->getEntityId(),
'type' => 'saml20-sp-hosted',
'url' => $source->getMetadataURL(),
'name' => $name,
'metadata' => $xml,
'metadata_array' => VarExporter::export($metadata),
'certificates' => $certificates,
];
}
return $entities;
}
/**
* Metadata converter
*
* @param \Symfony\Component\HttpFoundation\Request $request The current request.
*
* @return \SimpleSAML\XHTML\Template
*/
public function metadataConverter(Request $request): Template
if ($xmlfile = $request->files->get('xmlfile')) {
$xmldata = trim(file_get_contents($xmlfile->getPathname()));
} elseif ($xmldata = $request->request->get('xmldata')) {
$xmldata = trim($xmldata);
}
$error = null;
Utils\XML::checkSAMLMessage($xmldata, 'saml-meta');
$entities = null;
try {
$entities = SAMLParser::parseDescriptorsString($xmldata);
} catch (Exception $e) {
$error = $e->getMessage();
if ($entities !== null) {
// get all metadata for the entities
foreach ($entities as &$entity) {
$entity = [
'saml20-sp-remote' => $entity->getMetadata20SP(),
'saml20-idp-remote' => $entity->getMetadata20IdP(),
];
}
// transpose from $entities[entityid][type] to $output[type][entityid]
$output = Utils\Arrays::transpose($entities);
// merge all metadata of each type to a single string which should be added to the corresponding file
foreach ($output as $type => &$entities) {
$text = '';
foreach ($entities as $entityId => $entityMetadata) {
if ($entityMetadata === null) {
continue;
}
/**
* remove the entityDescriptor element because it is unused,
* and only makes the output harder to read
*/
unset($entityMetadata['entityDescriptor']);
/**
* Remove any expire from the metadata. This is not so useful
* for manually converted metadata and frequently gives rise
* to unexpected results when copy-pased statically.
*/
unset($entityMetadata['expire']);
$text .= '$metadata[' . var_export($entityId, true) . '] = '
. VarExporter::export($entityMetadata) . ";\n";
$entities = $text;
}
}
} else {
$xmldata = '';
$output = [];
}
$t = new Template($this->config, 'admin:metadata_converter.twig');
'logouturl' => Utils\Auth::getAdminLogoutURL(),
'xmldata' => $xmldata,
'output' => $output,
'error' => $error,
$this->menu->addOption('logout', $t->data['logouturl'], Translate::noop('Log out'));
return $this->menu->insert($t);
}
/**
* Download a certificate for a given entity.
*
* @param \Symfony\Component\HttpFoundation\Request $request The current request.
* @return \Symfony\Component\HttpFoundation\Response PEM-encoded certificate.
public function downloadCert(Request $request): Response
$set = $request->get('set');
if ($set === 'saml20-sp-hosted') {
$sourceID = $request->get('source');
/**
* The second argument ensures non-nullable return-value
* @var \SimpleSAML\Module\saml\Auth\Source\SP $source
*/
$source = $this->authSource::getById($sourceID, Module\saml\Auth\Source\SP::class);
$entityID = $request->get('entity');
$mdconfig = $this->mdHandler->getMetaDataConfig($entityID, $set);
/** @var array $certInfo Second param ensures non-nullable return-value */
$certInfo = Utils\Crypto::loadPublicKey($mdconfig, true, $prefix);
$response = new Response($certInfo['PEM']);
$disposition = $response->headers->makeDisposition(
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
'cert.pem'
);
$response->headers->set('Content-Disposition', $disposition);
$response->headers->set('Content-Type', 'application/x-pem-file');
return $response;
}
/**
* Show remote entity metadata
*
* @param \Symfony\Component\HttpFoundation\Request $request The current request.
*/
public function showRemoteEntity(Request $request): Template
$entityId = $request->get('entityid');
$set = $request->get('set');
$metadata = $this->mdHandler->getMetaData($entityId, $set);
$t = new Template($this->config, 'admin:show_metadata.twig');
Jaime Pérez Crespo
committed
$t->data['entityid'] = $entityId;
$t->data['metadata'] = VarExporter::export($metadata);
return $t;
}