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);
+    }
 }