diff --git a/docs/simplesamlphp-theming.md b/docs/simplesamlphp-theming.md index 7cc62a777b6aa7254fdd6a127a599a266dfd7162..e9b8c09eb2aca3f290fc045cf6121af3cb917592 100644 --- a/docs/simplesamlphp-theming.md +++ b/docs/simplesamlphp-theming.md @@ -22,19 +22,23 @@ If you want to customize the UI, the right way to do that is to create a new **t ### Configuring which theme to use -In `config.php` there is a configuration option that controls theming. Here is an example: +In `config.php` there is a configuration option that controls theming. You need to set that option and enable the module that contains the theme. Here is an example: - 'theme.use' => 'fancymodule:fancytheme', + 'module.enable' => [ + ... + 'fancymodule' => true, + ], -The `theme.use` parameter points to which theme that will be used. If some functionality in SimpleSAMLphp needs to present UI in example with the `logout.php` template, it will first look for `logout.php` in the `theme.use` theme, and if not found it will all fallback to look for the base templates. + 'theme.use' => 'fancymodule:fancytheme', -All required templates SHOULD be available as a base in the `templates` folder, and you SHOULD never change the base templates. To customize UI, add a new theme within a module that overrides the base templates, instead of modifying it. +The `theme.use` parameter points to which theme that will be used. If some functionality in SimpleSAMLphp needs to present UI in example with the `logout.twig` template, it will first look for `logout.twig` in the `theme.use` theme, and if not found it will all fallback to look for the base templates. -### Templates that include other files +All required templates SHOULD be available as a base in the `templates` folder, and you SHOULD never change the base templates. To customize UI, add a new theme within a module that overrides the base templates, instead of modifying it. -A template file may *include* other files. For example all the default templates will include a header and footer: the `login.php` template will first include `includes/header.php` then present the login page, and then include `includes/footer.php`. +### Override only specific templates -SimpleSAMLphp allows themes to override the included templates files only, if needed. That means you can create a new theme `fancytheme` that includes only a header and footer. The header file refers to the CSS files, which means that a simple way of making a new look on SimpleSAMLphp is to create a new theme, and copy the existing header, but point to your own CSS instead of the default CSS. +The SimpleSAMLphp templates are derived from a base template and include other templates as building blocks. You only need to override the templates or building blocks needed for your change. +SimpleSAMLphp allows themes to override the included templates files only, if needed. That means you can create a new theme `fancytheme` that includes only a header and footer template. These templates may refer to your own CSS files, which means that a simple way of making a new look on SimpleSAMLphp is to create a new theme, and copy the existing header, but point to your own CSS instead of the default CSS. Creating your first theme @@ -50,51 +54,22 @@ The first thing you need to do is having a SimpleSAMLphp module to place your th Then within this module, you can create a new theme named `fancytheme`. cd modules/mymodule - mkdir -p themes/fancytheme/default/includes + mkdir -p themes/fancytheme/default/ Now, configure SimpleSAMLphp to use your new theme in `config.php`: 'theme.use' => 'mymodule:fancytheme', -Next, we create `themes/fancytheme/default/includes`, and copy the header file from the base theme: - - cp templates/includes/header.php modules/mymodule/themes/fancytheme/default/includes/ - -In the `modules/mymodule/themes/fancytheme/default/includes/header.php` type in something and go to the SimpleSAMLphp front page to see that your new theme is in use. - -A good start is to modify the reference to the default CSS: - - <link rel="stylesheet" type="text/css" href="/<?php echo $this->data['baseurlpath']; ?>resources/default.css" /> - -to in example: - - <link rel="stylesheet" type="text/css" href="/<?php echo $this->data['baseurlpath']; ?>resources/fancytheme/default.css" /> - - -Examples ---------------------- - -To override the frontpage body, add the file: - - modules/mymodule/themes/fancytheme/default/frontpage.php - -In the path above `default` means that the frontpage template is not part of any modules. If you are replacing a template that is part of a module, then use the module name instead of `default`. - -For example, to override the `preprodwarning` template, (the file is located in `modules/preprodwarning/templates/warning.php`), you need to add a new file: - - modules/mymodule/themes/fancytheme/preprodwarning/warning.php - +Next, we copy the header file from the base theme: -Say in a module `foomodule`, some code requests to present the `bar.php` template, SimpleSAMLphp will: - - 1. first look in your theme for a replacement: `modules/mymodule/themes/fancytheme/foomodule/bar.php`. - 2. If not found, it will use the base template of that module: `modules/foomodule/templates/bar.php` + cp templates/_header.twig modules/mymodule/themes/fancytheme/default/ +In the `modules/mymodule/themes/fancytheme/default/includes/_header.twig` file type in something and go to the SimpleSAMLphp front page to see that your new theme is in use. Adding resource files --------------------- -You can put resource files within the www folder of your module, to make your module completely independent with included css, icons etc. +You can put resource files within the `www/assets` folder of your module, to make your module completely independent with included css, icons etc. ``` modules @@ -102,18 +77,17 @@ modules └───lib └───themes └───www - └───logo.png - └───style.css -``` - -Reference these resources in your custom PHP templates under `themes/fancytheme` by using a generator for the URL: -``` -<?php echo SimpleSAML\Module::getModuleURL('mymodule/logo.png'); ?> + └───assets + └───logo.png + └───style.css ``` +Reference these resources in your custom templates under `themes/fancytheme` by using a generator for the URL. Example for a custom CSS stylesheet file: ``` -<link rel="stylesheet" href="<?php echo SimpleSAML\Module::getModuleURL('mymodule/style.css'); ?>"> +{% block preload %} +<link rel="stylesheet" href="{{ asset('style.css', 'mymodule') }}"> +{% endblock %} ``` Migrating to Twig templates @@ -129,7 +103,7 @@ If you need to make more extensive customizations to the base template, you shou cp templates/base.twig modules/mymodule/themes/fancytheme/default/ -Any references to `$this->data['baseurlpath']` in old-style templates can be replaced with `{{baseurlpath}}` in Twig templates. Likewise, references to `\SimpleSAML\Module::getModuleURL()` can be replaced with `{{baseurlpath}}module.php/mymodule/...` +Any references to `$this->data['baseurlpath']` in old-style templates can be replaced with `{{baseurlpath}}` in Twig templates. Likewise, references to `\SimpleSAML\Module::getModuleURL()` can be replaced with `{{baseurlpath}}module.php/mymodule/...` or the `asset()` function like shown above. Within templates each module is defined as a separate namespace matching the module name. This allows one template to reference templates from other modules using Twig's `@namespace_name/template_path` notation. For instance, a template in `mymodule` can include the widget template from the `yourmodule` module using the notation `@yourmodule/widget.twig`. A special namespace, `__parent__`, exists to allow theme developers to more easily extend a module's stock template. diff --git a/lib/SimpleSAML/Locale/Localization.php b/lib/SimpleSAML/Locale/Localization.php index 21c606062789dee2388e4e08e4d86b1196d2eb53..ae621c2cfb64ac1d8afaa11068da96bba292d271 100644 --- a/lib/SimpleSAML/Locale/Localization.php +++ b/lib/SimpleSAML/Locale/Localization.php @@ -206,7 +206,7 @@ class Localization } // Locale for default language missing even, error out - $error = "Localization directory missing/broken for langcode '$langcode' and domain '$domain'"; + $error = "Localization directory '$langPath' missing/broken for langcode '$langcode' and domain '$domain'"; Logger::critical($_SERVER['PHP_SELF'] . ' - ' . $error); throw new \Exception($error); } diff --git a/lib/SimpleSAML/XHTML/Template.php b/lib/SimpleSAML/XHTML/Template.php index e75907e636441bab1a95e2a77ea1b2c682877350..f9df3c72cda1e154ec698ec6afb8b96507b8fbcf 100644 --- a/lib/SimpleSAML/XHTML/Template.php +++ b/lib/SimpleSAML/XHTML/Template.php @@ -351,11 +351,9 @@ class Template extends Response // setup directories & namespaces $themeDir = Module::getModuleDir($this->theme['module']) . '/themes/' . $this->theme['name']; - $subdirs = scandir($themeDir); + $subdirs = @scandir($themeDir); if (empty($subdirs)) { - // no subdirectories in the theme directory, nothing to do here - // this is probably wrong, log a message - Logger::warning('Empty theme directory for theme "' . $this->theme['name'] . '".'); + Logger::warning('Theme directory for theme "' . $this->theme['name'] . '" (' . $themeDir . ') is not readable or is empty.'); return []; } diff --git a/modules/saml/docs/sp.md b/modules/saml/docs/sp.md index f441b262bd4b7fe89e93e0e0535bc6714c40c484..499b46231535f171f5c823b40fe011fadc91377c 100644 --- a/modules/saml/docs/sp.md +++ b/modules/saml/docs/sp.md @@ -50,7 +50,11 @@ All these parameters override the equivalent option from the configuration. `saml:Extensions` -: The samlp:Extensions that will be sent in the login request. +: The samlp:Extensions (an XML chunk) that will be sent in the login request. + + +`saml:logout:Extensions` +: The samlp:Extensions (an XML chunk) that will be sent in the logout request. `saml:NameID` diff --git a/modules/saml/lib/Auth/Source/SP.php b/modules/saml/lib/Auth/Source/SP.php index 93e40e92298dac4b7cf61961321c58bcd75e803f..41412c4df5f1e8769e3cc7a0d4747bfbb6abba80 100644 --- a/modules/saml/lib/Auth/Source/SP.php +++ b/modules/saml/lib/Auth/Source/SP.php @@ -7,6 +7,7 @@ namespace SimpleSAML\Module\saml\Auth\Source; use SAML2\AuthnRequest; use SAML2\Binding; use SAML2\Constants; +use SAML2\LogoutRequest; use SAML2\XML\saml\NameID; use SimpleSAML\Assert\Assert; use SimpleSAML\Auth; @@ -616,7 +617,7 @@ class SP extends \SimpleSAML\Auth\Source $b = Binding::getBinding($dst['Binding']); - $this->sendSAML2AuthnRequest($state, $b, $ar); + $this->sendSAML2AuthnRequest($b, $ar); Assert::true(false); } @@ -627,17 +628,31 @@ class SP extends \SimpleSAML\Auth\Source * * This function does not return. * - * @param array &$state The state array. * @param \SAML2\Binding $binding The binding. * @param \SAML2\AuthnRequest $ar The authentication request. */ - public function sendSAML2AuthnRequest(array &$state, Binding $binding, AuthnRequest $ar): void + public function sendSAML2AuthnRequest(Binding $binding, AuthnRequest $ar): void { $binding->send($ar); Assert::true(false); } + /** + * Function to actually send the logout request. + * + * This function does not return. + * + * @param \SAML2\Binding $binding The binding. + * @param \SAML2\LogoutRequest $ar The logout request. + */ + public function sendSAML2LogoutRequest(Binding $binding, LogoutRequest $lr): void + { + $binding->send($lr); + Assert::true(false); + } + + /** * Send a SSO request to an IdP. * @@ -979,6 +994,12 @@ class SP extends \SimpleSAML\Auth\Source $lr->setRelayState($id); $lr->setDestination($endpoint['Location']); + if (isset($state['saml:logout:Extensions']) && count($state['saml:logout:Extensions']) > 0) { + $lr->setExtensions($state['saml:logout:Extensions']); + } elseif ($this->metadata->getArray('saml:logout:Extensions', null) !== null) { + $lr->setExtensions($this->metadata->getArray('saml:logout:Extensions')); + } + $encryptNameId = $idpMetadata->getBoolean('nameid.encryption', null); if ($encryptNameId === null) { $encryptNameId = $this->metadata->getBoolean('nameid.encryption', false); @@ -988,9 +1009,8 @@ class SP extends \SimpleSAML\Auth\Source } $b = Binding::getBinding($endpoint['Binding']); - $b->send($lr); - Assert::true(false); + $this->sendSAML2LogoutRequest($b, $lr); } diff --git a/tests/Utils/SpTester.php b/tests/Utils/SpTester.php index a83396bbca22b987380e19c35e5c435e69d2e01f..7f8d01884b53188f2b09149d3ca8963d07a21848 100644 --- a/tests/Utils/SpTester.php +++ b/tests/Utils/SpTester.php @@ -7,6 +7,7 @@ namespace SimpleSAML\Test\Utils; use ReflectionObject; use SAML2\AuthnRequest; use SAML2\Binding; +use SAML2\LogoutRequest; use SimpleSAML\Configuration; use SimpleSAML\Module\saml\Auth\Source\SP; @@ -41,15 +42,29 @@ class SpTester extends SP /** * override the method that sends the request to avoid sending anything */ - public function sendSAML2AuthnRequest(array &$state, Binding $binding, AuthnRequest $ar): void + public function sendSAML2AuthnRequest(Binding $binding, AuthnRequest $ar): void { // Exit test. Continuing would mean running into a assert(FALSE) throw new ExitTestException( [ - 'state' => $state, 'binding' => $binding, 'ar' => $ar, ] ); } + + + /** + * override the method that sends the request to avoid sending anything + */ + public function sendSAML2LogoutRequest(Binding $binding, LogoutRequest $lr): void + { + // Exit test. Continuing would mean running into a assert(FALSE) + throw new ExitTestException( + [ + 'binding' => $binding, + 'lr' => $lr, + ] + ); + } } diff --git a/tests/lib/SimpleSAML/Metadata/MetaDataStorageSourceTest.php b/tests/lib/SimpleSAML/Metadata/MetaDataStorageSourceTest.php index e7ab19347fcc0fac385cd7ce7b4e353d144e5216..7408ca592016553f80d604d9e9d55fc9f4682a79 100644 --- a/tests/lib/SimpleSAML/Metadata/MetaDataStorageSourceTest.php +++ b/tests/lib/SimpleSAML/Metadata/MetaDataStorageSourceTest.php @@ -117,6 +117,7 @@ xmlns:fed=\"http://docs.oasis-open.org/wsfed/federation/200706\"> </RoleDescriptor> <IDPSSODescriptor protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\"> <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"https://saml.idp/sso/\"/> +<SingleLogoutService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"https://saml.idp/logout/\"/> </IDPSSODescriptor> </EntityDescriptor> "; diff --git a/tests/modules/saml/lib/Auth/Source/SPTest.php b/tests/modules/saml/lib/Auth/Source/SPTest.php index e00a438ed2acb98ddc44ddda806cb228b6cc576f..312895a7f84e40b99ec8f40f3a6a3347597a10a3 100644 --- a/tests/modules/saml/lib/Auth/Source/SPTest.php +++ b/tests/modules/saml/lib/Auth/Source/SPTest.php @@ -8,7 +8,9 @@ use InvalidArgumentException; use PHPUnit\Framework\TestCase; use SAML2\AuthnRequest; use SAML2\Constants; +use SAML2\LogoutRequest; use SAML2\Utils; +use SAML2\XML\saml\NameID; use SimpleSAML\Configuration; use SimpleSAML\Error\Exception; use SimpleSAML\Module\saml\Error\NoAvailableIDP; @@ -136,6 +138,33 @@ class SPTest extends ClearStateTestCase } + /** + * Create a SAML LogoutRequest using \SimpleSAML\Module\saml\Auth\Source\SP + * + * @param array $state The state array to use in the test. This is an array of the parameters described in section + * 2 of https://simplesamlphp.org/docs/development/saml:sp + * + * @return \SAML2\LogoutRequest The LogoutRequest generated. + */ + private function createLogoutRequest(array $state = []): LogoutRequest + { + $info = ['AuthId' => 'default-sp']; + $config = ['entityID' => 'https://engine.surfconext.nl/authentication/idp/metadata']; + $as = new SpTester($info, $config); + + /** @var \SAML2\LogoutRequest $lr */ + $lr = null; + try { + $as->startSLO2($state); + $this->assertTrue(false, 'Expected ExitTestException'); + } catch (ExitTestException $e) { + $r = $e->getTestResult(); + $lr = $r['lr']; + } + return $lr; + } + + /** * Test generating an AuthnRequest * @test @@ -1248,4 +1277,51 @@ class SPTest extends ClearStateTestCase $this->assertEquals('urn:oasis:names:tc:SAML:2.0:protocol', $protocols[0]); } + /** + * Test sending a LogoutRequest + */ + public function testLogoutRequest(): void + { + $nameId = new NameID(); + $nameId->setValue('someone@example.com'); + + $dom = \SAML2\DOMDocumentFactory::create(); + $extension = $dom->createElementNS('urn:some:namespace', 'MyLogoutExtension'); + $extChunk = [new \SAML2\XML\Chunk($extension)]; + + $entityId = "https://engine.surfconext.nl/authentication/idp/metadata"; + $xml = MetaDataStorageSourceTest::generateIdpMetadataXml($entityId); + $c = [ + 'metadata.sources' => [ + ["type" => "xml", "xml" => $xml], + ], + ]; + Configuration::loadFromArray($c, '', 'simplesaml'); + + $state = [ + 'saml:logout:IdP' => $entityId, + 'saml:logout:NameID' => $nameId, + 'saml:logout:SessionIndex' => 'abc123', + 'saml:logout:Extensions' => $extChunk, + ]; + + $lr = $this->createLogoutRequest($state); + + /** @var \SAML2\XML\samlp\Extensions $extentions */ + $extensions = $lr->getExtensions(); + $this->assertcount(1, $state['saml:logout:Extensions']); + + $xml = $lr->toSignedXML(); + + /** @var \DOMNode[] $q */ + $q = Utils::xpQuery($xml, '/samlp:LogoutRequest/saml:NameID'); + $this->assertCount(1, $q); + $this->assertEquals('someone@example.com', $q[0]->nodeValue); + + $q = Utils::xpQuery($xml, '/samlp:LogoutRequest/samlp:Extensions'); + $this->assertCount(1, $q); + $this->assertCount(1, $q[0]->childNodes); + $this->assertEquals('MyLogoutExtension', $q[0]->firstChild->localName); + $this->assertEquals('urn:some:namespace', $q[0]->firstChild->namespaceURI); + } }