diff --git a/modules/.gitignore b/modules/.gitignore
index 063665917650c84fdff7c342704432b959a01263..b9fe2ce7b77a10cd00d3ce1e615d82048c14b597 100644
--- a/modules/.gitignore
+++ b/modules/.gitignore
@@ -4,6 +4,7 @@
 
 # Explicitly include modules that ship with simplesamlphp
 !/adfs/
+!/admin/
 !/authcrypt/
 !/authfacebook/
 !/authlinkedin/
diff --git a/modules/admin/lib/ConfigController.php b/modules/admin/lib/ConfigController.php
index 127ecfb7d3f869b5f2c131512577694b006691dc..5c196b83d61932dd1e989986df0a84604007689c 100644
--- a/modules/admin/lib/ConfigController.php
+++ b/modules/admin/lib/ConfigController.php
@@ -17,7 +17,6 @@ use Symfony\Component\HttpFoundation\Response;
  */
 class ConfigController
 {
-
     const LATEST_VERSION_STATE_KEY = 'core:latest_simplesamlphp_version';
     const RELEASES_API = 'https://api.github.com/repos/simplesamlphp/simplesamlphp/releases/latest';
 
diff --git a/modules/admin/lib/TestController.php b/modules/admin/lib/TestController.php
new file mode 100644
index 0000000000000000000000000000000000000000..d97faf59db06746b52294c6abf13b303d5e8e4c4
--- /dev/null
+++ b/modules/admin/lib/TestController.php
@@ -0,0 +1,247 @@
+<?php
+
+namespace SimpleSAML\Module\admin;
+
+use SimpleSAML\HTTP\RunnableResponse;
+use SimpleSAML\Locale\Translate;
+use SimpleSAML\Utils\HTTP;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Controller class for the admin module.
+ *
+ * This class serves the 'Test authentication sources' views available in the module.
+ *
+ * @package SimpleSAML\Module\admin
+ */
+class TestController
+{
+
+    /** @var \SimpleSAML\Configuration */
+    protected $config;
+
+    /** @var Menu */
+    protected $menu;
+
+    /** @var \SimpleSAML\Session */
+    protected $session;
+
+
+    /**
+     * ConfigController constructor.
+     *
+     * @param \SimpleSAML\Configuration $config The configuration to use.
+     * @param \SimpleSAML\Session $session The current user session.
+     */
+    public function __construct(\SimpleSAML\Configuration $config, \SimpleSAML\Session $session)
+    {
+        $this->config = $config;
+        $this->session = $session;
+        $this->menu = new Menu();
+    }
+
+
+    /**
+     * Display the list of available authsources.
+     *
+     * @return \SimpleSAML\XHTML\Template
+     */
+    public function main(Request $request, $as)
+    {
+        \SimpleSAML\Utils\Auth::requireAdmin();
+        if (is_null($as)) {
+            $t = new \SimpleSAML\XHTML\Template($this->config, 'admin:authsource_list.twig');
+            $t->data = [
+                'sources' => \SimpleSAML\Auth\Source::getSources(),
+            ];
+        } else {
+            $authsource = new \SimpleSAML\Auth\Simple($as);
+            if (!is_null($request->query->get('logout'))) {
+                $authsource->logout($this->config->getBasePath().'logout.php');
+            } else if (!is_null($request->query->get(\SimpleSAML\Auth\State::EXCEPTION_PARAM))) {
+                // This is just a simple example of an error
+                $state = \SimpleSAML\Auth\State::loadExceptionState();
+                assert(array_key_exists(\SimpleSAML\Auth\State::EXCEPTION_DATA, $state));
+                throw $state[\SimpleSAML\Auth\State::EXCEPTION_DATA];
+            }
+
+            if (!$authsource->isAuthenticated()) {
+                $url = \SimpleSAML\Module::getModuleURL('admin/test/' .$as, []);
+                $params = [
+                    'ErrorURL' => $url,
+                    'ReturnTo' => $url,
+                ];
+                $authsource->login($params);
+            }
+
+            $attributes = $authsource->getAttributes();
+            $authData = $authsource->getAuthDataArray();
+            $nameId = !is_null($authsource->getAuthData('saml:sp:NameID')) ? $authsource->getAuthData('saml:sp:NameID') : false;
+
+            $t = new \SimpleSAML\XHTML\Template($this->config, 'admin:status.twig', 'attributes');
+            $t->data = [
+                'attributes' => $attributes,
+                'attributesHtml' => $this->present_attributes($t, $attributes, ''),
+                'authData' => $authData,
+                'nameid' => $nameId,
+                'logouturl' => \SimpleSAML\Utils\HTTP::getSelfURLNoQuery().'?as='.urlencode($as).'&logout',
+            ];
+
+            if ($nameId !== false) {
+                $this->data['nameidHtml'] = present_nameid($t, $nameId);
+            }
+        }
+
+        \SimpleSAML\Module::callHooks('configpage', $t);
+        $this->menu->addOption('logout', \SimpleSAML\Utils\Auth::getAdminLogoutURL(), Translate::noop('Log out'));
+        return $this->menu->insert($t);
+    }
+
+
+    private function present_nameid(\SimpleSAML\XHTML\Template $t, \SAML2\XML\saml\NameID $nameId)
+    {
+        $result = '';
+        if ($nameId->getValue() === null) {
+            $list = ["NameID" => [$t->t('{status:subject_notset}')]];
+            $result .= "<p>NameID: <span class=\"notset\">".$t->t('{status:subject_notset}')."</span></p>";
+        } else {
+            $list = [
+                "NameId" => [$nameId->getValue()],
+            ];
+            if ($nameId->getFormat() !== null) {
+                $list[$t->t('{status:subject_format}')] = [$nameId->getFormat()];
+            }
+            if ($nameId->getNameQualifier() !== null) {
+                $list['NameQualifier'] = [$nameId->getNameQualifier()];
+            }
+            if ($nameId->getSPNameQualifier() !== null) {
+                $list['SPNameQualifier'] = [$nameId->getSPNameQualifier()];
+            }
+            if ($nameId->getSPProvidedID() !== null) {
+                $list['SPProvidedID'] = [$nameId->getSPProvidedID()];
+            }
+        }
+        return $result.present_attributes($t, $list, '');
+    }
+
+
+    private function present_attributes(\SimpleSAML\XHTML\Template $t, $attributes, $nameParent)
+    {
+        $alternate = ['pure-table-odd', 'pure-table-even'];
+        $i = 0;
+        $parentStr = (strlen($nameParent) > 0) ? strtolower($nameParent).'_' : '';
+        $str = (strlen($nameParent) > 0) ? '<table class="pure-table pure-table-attributes" summary="attribute overview">' :
+            '<table id="table_with_attributes" class="pure-table pure-table-attributes" summary="attribute overview">';
+        foreach ($attributes as $name => $value) {
+            $nameraw = $name;
+            $trans = $t->getTranslator();
+            $name = $trans->getAttributeTranslation($parentStr.$nameraw);
+            if (preg_match('/^child_/', $nameraw)) {
+                $parentName = preg_replace('/^child_/', '', $nameraw);
+                foreach ($value as $child) {
+                    $str .= '<tr class="odd"><td colspan="2" style="padding: 2em">'.
+                        $this->present_attributes($t, $child, $parentName).'</td></tr>';
+                }
+            } else {
+                if (sizeof($value) > 1) {
+                    $str .= '<tr class="'.$alternate[($i++ % 2)].'"><td class="attrname">';
+                    if ($nameraw !== $name) {
+                        $str .= htmlspecialchars($name).'<br/>';
+                    }
+                    $str .= '<code>'.htmlspecialchars($nameraw).'</code>';
+                    $str .= '</td><td class="attrvalue"><ul>';
+                    foreach ($value as $listitem) {
+                        if ($nameraw === 'jpegPhoto') {
+                            $str .= '<li><img src="data:image/jpeg;base64,'.htmlspecialchars($listitem).'" /></li>';
+                        } else {
+                            $str .= '<li>'.$this->present_assoc($listitem).'</li>';
+                        }
+                    }
+                    $str .= '</ul></td></tr>';
+                } elseif (isset($value[0])) {
+                    $str .= '<tr class="'.$alternate[($i++ % 2)].'"><td class="attrname">';
+                    if ($nameraw !== $name) {
+                        $str .= htmlspecialchars($name).'<br/>';
+                    }
+                    $str .= '<code>'.htmlspecialchars($nameraw).'</code>';
+                    $str .= '</td>';
+                    if ($nameraw === 'jpegPhoto') {
+                        $str .= '<td class="attrvalue"><img src="data:image/jpeg;base64,'.htmlspecialchars($value[0]).
+                            '" /></td></tr>';
+                    } elseif (is_a($value[0], 'DOMNodeList')) {
+                        // try to see if we have a NameID here
+                        /** @var \DOMNodeList $value [0] */
+                        $n = $value[0]->length;
+                        for ($idx = 0; $idx < $n; $idx++) {
+                            $elem = $value[0]->item($idx);
+                            /* @var \DOMElement $elem */
+                            if (!($elem->localName === 'NameID' && $elem->namespaceURI === \SAML2\Constants::NS_SAML)) {
+                                continue;
+                            }
+                            $str .= $this->present_eptid($trans, new \SAML2\XML\saml\NameID($elem));
+                            break; // we only support one NameID here
+                        }
+                        $str .= '</td></tr>';
+                    } elseif (is_a($value[0], '\SAML2\XML\saml\NameID')) {
+                        $str .= $this->present_eptid($trans, $value[0]);
+                        $str .= '</td></tr>';
+                    } else {
+                        $str .= '<td class="attrvalue">'.htmlspecialchars($value[0]).'</td></tr>';
+                    }
+                }
+            }
+            $str .= "\n";
+        }
+        $str .= '</table>';
+        return $str;
+    }
+
+    private function present_list($attr)
+    {
+        if (is_array($attr) && count($attr) > 1) {
+            $str = '<ul>';
+            foreach ($attr as $value) {
+                $str .= '<li>'.htmlspecialchars($attr).'</li>';
+            }
+            $str .= '</ul>';
+            return $str;
+        } else {
+            return htmlspecialchars($attr[0]);
+        }
+    }
+
+    private function present_assoc($attr)
+    {
+        if (is_array($attr)) {
+            $str = '<dl>';
+            foreach ($attr as $key => $value) {
+                $str .= "\n".'<dt>'.htmlspecialchars($key).'</dt><dd>'.$this->present_list($value).'</dd>';
+            }
+            $str .= '</dl>';
+            return $str;
+        } else {
+            return htmlspecialchars($attr);
+        }
+    }
+
+    private function present_eptid(\SimpleSAML\Locale\Translate $t, \SAML2\XML\saml\NameID $nameID)
+    {
+        $eptid = [
+            'NameID' => [$nameID->getValue()],
+        ];
+        if ($nameID->getFormat() !== null) {
+            $eptid[$t->t('{status:subject_format}')] = [$nameID->getFormat()];
+        }
+        if ($nameID->getNameQualifier() !== null) {
+            $eptid['NameQualifier'] = [$nameID->getNameQualifier()];
+        }
+        if ($nameID->getSPNameQualifier() !== null) {
+            $eptid['SPNameQualifier'] = [$nameID->getSPNameQualifier()];
+        }
+        if ($nameID->getSPProvidedID() !== null) {
+            $eptid['SPProvidedID'] = [$nameID->getSPProvidedID()];
+        }
+        return '<td class="attrvalue">'.$this->present_assoc($eptid);
+    }
+}
diff --git a/modules/core/templates/authsource_list.twig b/modules/admin/templates/authsource_list.twig
similarity index 52%
rename from modules/core/templates/authsource_list.twig
rename to modules/admin/templates/authsource_list.twig
index c966180b7fb54b8ef8b304f65034ba1f7dfd1457..6262d5be34f0248b18dd313b73ef82df4b06ccf7 100644
--- a/modules/core/templates/authsource_list.twig
+++ b/modules/admin/templates/authsource_list.twig
@@ -1,11 +1,12 @@
 {% set pagetitle = 'Test Authentication Sources'|trans %}
+{% set frontpage_section = 'test' %}
 {% extends "base.twig" %}
 
 {% block content %}
-    <h1>{{ header }}</h1>
+    {%- include "@admin/includes/menu.twig" %}
     <ul>
     {% for key, name in sources %}
-        <li><a href="?as={{ name|escape('url') }}">{{ name|escape('html') }}</a></li>
+        <li><a href="test/{{ name|escape('url') }}">{{ name|escape('html') }}</a></li>
     {% endfor %}
     </ul>
 {% endblock %}
diff --git a/modules/admin/templates/status.twig b/modules/admin/templates/status.twig
new file mode 100644
index 0000000000000000000000000000000000000000..6d75486d4b0d2bc7f1d4b011d88e8182321abd53
--- /dev/null
+++ b/modules/admin/templates/status.twig
@@ -0,0 +1,35 @@
+{% set pagetitle = 'SimpleSAMLphp installation page'|trans %}
+{% set frontpage_section = 'test' %}
+{% extends "base.twig" %}
+
+{% block content %}
+    {%- include "@admin/includes/menu.twig" %}
+    <h2>{{ '{status:header_saml20_sp}'|trans }}</h2>
+
+    <p>{{ '{status:intro}'|trans }}</p>
+
+    <h2>{{ '{status:attributes_header}'|trans }}</h2>
+
+    {{ attributesHtml|raw }}
+
+    {% if nameidHtml -%}
+    <h2>{{ '{status:subject_header}'|trans }}</h2>
+    {{  nameidHtml|raw }}
+    {%- endif %}
+
+    {% if authData -%}
+    <h2>{{ '{status:authData_header}'|trans }}</h2>
+    <details><summary>{{ '{status:authData_summary}'|trans }}</summary> 
+      <pre>{{ authData|json_encode|raw }}</pre>
+    </details>
+    {%- endif %}    
+
+    {% if logout -%}
+    <h2>{{ '{status:logout}'|trans }}</h2>
+    <p>{{ logout }}</p> 
+    {%- endif %}
+
+    {% if logouturl -%}
+    <a href="{{ logouturl }}">{{ '{status:logout}'|trans }}</a>
+    {%- endif %}
+{% endblock %}