Skip to content
Snippets Groups Projects
Unverified Commit 731aae6f authored by Tim van Dijen's avatar Tim van Dijen Committed by GitHub
Browse files

Merge pull request #1506 from simplesamlphp/logout_ext

Feature: Add logout Extensions
parents 38141030 78df9ce4
No related branches found
No related tags found
No related merge requests found
...@@ -22,19 +22,23 @@ If you want to customize the UI, the right way to do that is to create a new **t ...@@ -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 ### 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 Creating your first theme
...@@ -50,51 +54,22 @@ The first thing you need to do is having a SimpleSAMLphp module to place your th ...@@ -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`. Then within this module, you can create a new theme named `fancytheme`.
cd modules/mymodule 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`: Now, configure SimpleSAMLphp to use your new theme in `config.php`:
'theme.use' => 'mymodule:fancytheme', 'theme.use' => 'mymodule:fancytheme',
Next, we create `themes/fancytheme/default/includes`, and copy the header file from the base theme: Next, we 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
Say in a module `foomodule`, some code requests to present the `bar.php` template, SimpleSAMLphp will: cp templates/_header.twig modules/mymodule/themes/fancytheme/default/
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`
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 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 modules
...@@ -102,18 +77,17 @@ modules ...@@ -102,18 +77,17 @@ modules
└───lib └───lib
└───themes └───themes
└───www └───www
└───logo.png └───assets
└───style.css └───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'); ?>
``` ```
Reference these resources in your custom templates under `themes/fancytheme` by using a generator for the URL.
Example for a custom CSS stylesheet file: 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 Migrating to Twig templates
...@@ -129,7 +103,7 @@ If you need to make more extensive customizations to the base template, you shou ...@@ -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/ 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. 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.
......
...@@ -206,7 +206,7 @@ class Localization ...@@ -206,7 +206,7 @@ class Localization
} }
// Locale for default language missing even, error out // 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); Logger::critical($_SERVER['PHP_SELF'] . ' - ' . $error);
throw new \Exception($error); throw new \Exception($error);
} }
......
...@@ -351,11 +351,9 @@ class Template extends Response ...@@ -351,11 +351,9 @@ class Template extends Response
// setup directories & namespaces // setup directories & namespaces
$themeDir = Module::getModuleDir($this->theme['module']) . '/themes/' . $this->theme['name']; $themeDir = Module::getModuleDir($this->theme['module']) . '/themes/' . $this->theme['name'];
$subdirs = scandir($themeDir); $subdirs = @scandir($themeDir);
if (empty($subdirs)) { if (empty($subdirs)) {
// no subdirectories in the theme directory, nothing to do here Logger::warning('Theme directory for theme "' . $this->theme['name'] . '" (' . $themeDir . ') is not readable or is empty.');
// this is probably wrong, log a message
Logger::warning('Empty theme directory for theme "' . $this->theme['name'] . '".');
return []; return [];
} }
......
...@@ -50,7 +50,11 @@ All these parameters override the equivalent option from the configuration. ...@@ -50,7 +50,11 @@ All these parameters override the equivalent option from the configuration.
`saml:Extensions` `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` `saml:NameID`
......
...@@ -7,6 +7,7 @@ namespace SimpleSAML\Module\saml\Auth\Source; ...@@ -7,6 +7,7 @@ namespace SimpleSAML\Module\saml\Auth\Source;
use SAML2\AuthnRequest; use SAML2\AuthnRequest;
use SAML2\Binding; use SAML2\Binding;
use SAML2\Constants; use SAML2\Constants;
use SAML2\LogoutRequest;
use SAML2\XML\saml\NameID; use SAML2\XML\saml\NameID;
use SimpleSAML\Assert\Assert; use SimpleSAML\Assert\Assert;
use SimpleSAML\Auth; use SimpleSAML\Auth;
...@@ -616,7 +617,7 @@ class SP extends \SimpleSAML\Auth\Source ...@@ -616,7 +617,7 @@ class SP extends \SimpleSAML\Auth\Source
$b = Binding::getBinding($dst['Binding']); $b = Binding::getBinding($dst['Binding']);
$this->sendSAML2AuthnRequest($state, $b, $ar); $this->sendSAML2AuthnRequest($b, $ar);
Assert::true(false); Assert::true(false);
} }
...@@ -627,17 +628,31 @@ class SP extends \SimpleSAML\Auth\Source ...@@ -627,17 +628,31 @@ class SP extends \SimpleSAML\Auth\Source
* *
* This function does not return. * This function does not return.
* *
* @param array &$state The state array.
* @param \SAML2\Binding $binding The binding. * @param \SAML2\Binding $binding The binding.
* @param \SAML2\AuthnRequest $ar The authentication request. * @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); $binding->send($ar);
Assert::true(false); 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. * Send a SSO request to an IdP.
* *
...@@ -979,6 +994,12 @@ class SP extends \SimpleSAML\Auth\Source ...@@ -979,6 +994,12 @@ class SP extends \SimpleSAML\Auth\Source
$lr->setRelayState($id); $lr->setRelayState($id);
$lr->setDestination($endpoint['Location']); $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); $encryptNameId = $idpMetadata->getBoolean('nameid.encryption', null);
if ($encryptNameId === null) { if ($encryptNameId === null) {
$encryptNameId = $this->metadata->getBoolean('nameid.encryption', false); $encryptNameId = $this->metadata->getBoolean('nameid.encryption', false);
...@@ -988,9 +1009,8 @@ class SP extends \SimpleSAML\Auth\Source ...@@ -988,9 +1009,8 @@ class SP extends \SimpleSAML\Auth\Source
} }
$b = Binding::getBinding($endpoint['Binding']); $b = Binding::getBinding($endpoint['Binding']);
$b->send($lr);
Assert::true(false); $this->sendSAML2LogoutRequest($b, $lr);
} }
......
...@@ -7,6 +7,7 @@ namespace SimpleSAML\Test\Utils; ...@@ -7,6 +7,7 @@ namespace SimpleSAML\Test\Utils;
use ReflectionObject; use ReflectionObject;
use SAML2\AuthnRequest; use SAML2\AuthnRequest;
use SAML2\Binding; use SAML2\Binding;
use SAML2\LogoutRequest;
use SimpleSAML\Configuration; use SimpleSAML\Configuration;
use SimpleSAML\Module\saml\Auth\Source\SP; use SimpleSAML\Module\saml\Auth\Source\SP;
...@@ -41,15 +42,29 @@ class SpTester extends SP ...@@ -41,15 +42,29 @@ class SpTester extends SP
/** /**
* override the method that sends the request to avoid sending anything * 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) // Exit test. Continuing would mean running into a assert(FALSE)
throw new ExitTestException( throw new ExitTestException(
[ [
'state' => $state,
'binding' => $binding, 'binding' => $binding,
'ar' => $ar, '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,
]
);
}
} }
...@@ -117,6 +117,7 @@ xmlns:fed=\"http://docs.oasis-open.org/wsfed/federation/200706\"> ...@@ -117,6 +117,7 @@ xmlns:fed=\"http://docs.oasis-open.org/wsfed/federation/200706\">
</RoleDescriptor> </RoleDescriptor>
<IDPSSODescriptor protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\"> <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/\"/> <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> </IDPSSODescriptor>
</EntityDescriptor> </EntityDescriptor>
"; ";
......
...@@ -8,7 +8,9 @@ use InvalidArgumentException; ...@@ -8,7 +8,9 @@ use InvalidArgumentException;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use SAML2\AuthnRequest; use SAML2\AuthnRequest;
use SAML2\Constants; use SAML2\Constants;
use SAML2\LogoutRequest;
use SAML2\Utils; use SAML2\Utils;
use SAML2\XML\saml\NameID;
use SimpleSAML\Configuration; use SimpleSAML\Configuration;
use SimpleSAML\Error\Exception; use SimpleSAML\Error\Exception;
use SimpleSAML\Module\saml\Error\NoAvailableIDP; use SimpleSAML\Module\saml\Error\NoAvailableIDP;
...@@ -136,6 +138,33 @@ class SPTest extends ClearStateTestCase ...@@ -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 generating an AuthnRequest
* @test * @test
...@@ -1248,4 +1277,51 @@ class SPTest extends ClearStateTestCase ...@@ -1248,4 +1277,51 @@ class SPTest extends ClearStateTestCase
$this->assertEquals('urn:oasis:names:tc:SAML:2.0:protocol', $protocols[0]); $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);
}
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment