diff --git a/modules/cron/lib/Controller/Cron.php b/modules/cron/lib/Controller/Cron.php
new file mode 100644
index 0000000000000000000000000000000000000000..10eb967f9a2a8e10ee793adbd8f9dfcb40c6a3ae
--- /dev/null
+++ b/modules/cron/lib/Controller/Cron.php
@@ -0,0 +1,150 @@
+<?php
+
+namespace SimpleSAML\Module\cron\Controller;
+
+use SimpleSAML\Auth;
+use SimpleSAML\Auth\AuthenticationFactory;
+use SimpleSAML\Configuration;
+use SimpleSAML\Error;
+use SimpleSAML\HTTP\RunnableResponse;
+use SimpleSAML\Logger;
+use SimpleSAML\Module;
+use SimpleSAML\Session;
+use SimpleSAML\Utils;
+use SimpleSAML\XHTML\Template;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Controller class for the cron module.
+ *
+ * This class serves the different views available in the module.
+ *
+ * @package SimpleSAML\Module\cron
+ */
+class Cron
+{
+    /** @var \SimpleSAML\Configuration */
+    protected $config;
+
+    /** @var \SimpleSAML\Configuration */
+    protected $cronconfig;
+
+    /** @var \SimpleSAML\Session */
+    protected $session;
+
+
+    /**
+     * Controller constructor.
+     *
+     * It initializes the global configuration and auth source configuration for the controllers implemented here.
+     *
+     * @param \SimpleSAML\Configuration              $config The configuration to use by the controllers.
+     * @param \SimpleSAML\Configuration              $moduleConfig The module-configuration to use by the controllers.
+     * @param \SimpleSAML\Session                    $session The session to use by the controllers.
+     *
+     * @throws \Exception
+     */
+    public function __construct(
+        Configuration $config,
+        Session $session
+    ) {
+        $this->config = $config;
+        $this->cronconfig = Configuration::getConfig('module_cron.php');
+        $this->session = $session;
+    }
+
+
+    /**
+     * Show cron info.
+     *
+     * @return \SimpleSAML\XHTML\Template
+     *   An HTML template or a redirection if we are not authenticated.
+     */
+    public function info()
+    {
+        Utils\Auth::requireAdmin();
+
+        $key = $this->cronconfig->getValue('key', 'secret');
+        $tags = $this->cronconfig->getValue('allowed_tags');
+
+        $def = [
+            'weekly' => "22 0 * * 0",
+            'daily' => "02 0 * * *",
+            'hourly' => "01 * * * *",
+            'default' => "XXXXXXXXXX",
+        ];
+
+        $urls = [];
+        foreach ($tags as $tag) {
+            $urls[] = [
+                'exec_href' => Module::getModuleURL('cron') . '/run/' . $tag . '/' . $key,
+                'href' => Module::getModuleURL('cron') . '/run/' . $tag . '/' . $key . '/xhtml',
+                'tag' => $tag,
+                'int' => (array_key_exists($tag, $def) ? $def[$tag] : $def['default']),
+            ];
+        }
+
+        $t = new Template($this->config, 'cron:croninfo.tpl.php', 'cron:cron');
+        $t->data['urls'] = $urls;
+        return $t;
+    }
+
+
+    /**
+     * Execute a cronjob.
+     *
+     * This controller will start a cron operation
+     *
+     * @param string $tag The tag
+     * @param string $key The secret key
+     * @param string|null $output The output format, defaulting to xhtml
+     *
+     * @return \SimpleSAML\XHTML\Template|\Symfony\Component\HttpFoundation\Response
+     *   An HTML template, a redirect or a "runnable" response.
+     *
+     * @throws \SimpleSAML\Error\Exception
+     */
+    public function run($tag, $key, $output)
+    {
+        $configKey = $this->cronconfig->getValue('key', 'secret');
+        if ($key !== $configKey) {
+            Logger::error('Cron - Wrong key provided. Cron will not run.');
+            exit;
+        }
+
+        $cron = new \SimpleSAML\Module\cron\Cron();
+        if ($tag === null || !$cron->isValidTag($tag)) {
+            Logger::error('Cron - Illegal tag [' . $tag . '].');
+            exit;
+        }
+
+        $url = Utils\HTTP::getSelfURL();
+        $time = date(DATE_RFC822);
+
+        $croninfo = $cron->runTag($tag);
+        $summary = $croninfo['summary'];
+
+        if ($this->cronconfig->getValue('sendemail', true) && count($summary) > 0) {
+            $mail = new Utils\EMail('SimpleSAMLphp cron report');
+            $mail->setData(['url' => $url, 'tag' => $croninfo['tag'], 'summary' => $croninfo['summary']]);
+            try {
+                $mail->send();
+            } catch (\PHPMailer\PHPMailer\Exception $e) {
+                Logger::warning("Unable to send cron report");
+            }
+        }
+
+        if ($output === 'xhtml') {
+            $t = new Template($this->config, 'cron:croninfo-result.tpl.php', 'cron:cron');
+            $t->data['tag'] = $croninfo['tag'];
+            $t->data['time'] = $time;
+            $t->data['url'] = $url;
+            $t->data['mail_required'] = isset($mail);
+            $t->data['mail_sent'] = !isset($e);
+            $t->data['summary'] = $summary;
+            return $t;
+        }
+        return new Response();
+    }
+}
diff --git a/modules/cron/routing/routes/routes.yaml b/modules/cron/routing/routes/routes.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d25718ad1571cdfd3a44821af605c898fd79af8a
--- /dev/null
+++ b/modules/cron/routing/routes/routes.yaml
@@ -0,0 +1,6 @@
+cron-croninfo:
+    path:       /info
+    defaults:   { _controller: 'SimpleSAML\Module\cron\Controller\Cron::info' }
+cron-run:
+    path:       /run/{tag}/{key}/{output}
+    defaults:   { _controller: 'SimpleSAML\Module\cron\Controller\Cron::run', output: null }
diff --git a/modules/cron/templates/croninfo-result.twig b/modules/cron/templates/croninfo-result.twig
index f226ffe07e9c24c14680f98223b10bb0b5ce7ef2..eb755dd2e464b5cee924a501821643b4960e2255 100644
--- a/modules/cron/templates/croninfo-result.twig
+++ b/modules/cron/templates/croninfo-result.twig
@@ -22,9 +22,14 @@ Tag: {{ tag }}
 {% for sum in summary %}
         <li> {{ sum }}</li>
 {% endfor %}
-</ul><br />
-</p>
+</ul>
+</p><br />
 </code>
+</div>
 
+{% if mail_required == true and mail_sent == false %}
+<div class="message-box error">
+Cron-report was not emailed due to an error.
 </div>
+{% endif %}
 {% endblock %}
diff --git a/modules/cron/templates/croninfo.tpl.php b/modules/cron/templates/croninfo.tpl.php
index 50efe15a1ec53e6ef527448cc4b3e72cb5aa13b8..dc1bf0583098993a14127e1214e9385f8e05afdc 100644
--- a/modules/cron/templates/croninfo.tpl.php
+++ b/modules/cron/templates/croninfo.tpl.php
@@ -13,7 +13,7 @@ $run_text = $this->t('run_text');
 <?php
 foreach ($this->data['urls'] as $url) {
     echo "# ".$run_text. ' ['.$url['tag'].']'."\n";
-    echo $url['int']." curl --silent \"".$url['href']."\" > /dev/null 2>&1\n";
+    echo $url['int']." curl --silent \"".$url['exec_href']."\" > /dev/null 2>&1\n";
 }
 ?>
         </code></pre>
@@ -23,7 +23,7 @@ foreach ($this->data['urls'] as $url) {
         <ul>
 <?php
 foreach ($this->data['urls'] as $url) {
-    echo '        <li><a href="'.$url['href'].'&amp;output=xhtml">'.$run_text.' ['.$url['tag'].']'.'</a></li>';
+    echo '        <li><a href="'.$url['href'].'">'.$run_text.' ['.$url['tag'].']'.'</a></li>';
 }
 ?>
         </ul>
diff --git a/modules/cron/templates/croninfo.twig b/modules/cron/templates/croninfo.twig
index d2ea32988d69d46edbd58b76d1e48de895df60f2..12d9813ffcf9e2137f66ca3412739f067ea0f8ae 100644
--- a/modules/cron/templates/croninfo.twig
+++ b/modules/cron/templates/croninfo.twig
@@ -14,16 +14,17 @@
         <code id="cronlist">
         {% for url in urls %}
             # {{ 'Run cron:'|trans }} [{{ url.tag }}]<br />
-            {{ url.int }} curl --silent "{{ url.href }}" > /dev/null 2>&amp;1<br />
+            {{ url.int }} curl --silent "{{ url.exec_href }}" > /dev/null 2>&amp;1<br />
         {% endfor %}
-    </code></div><br />
+        <br />
+        </code>
+    </div>
 
-    <p>{{ 'Click here to run the cron jobs:'|trans }}</p>
-      <ul>
+    <p>{{ 'Click here to run the cron jobs:'|trans }}
+    <ul>
         {% for url in urls %}
-        <li><a href="{{ url.href }}&amp;output=xhtml">{{ 'Run cron:'|trans }} {{ url.tag }}</a></li>
+        <li><a href="{{ url.href }}">{{ 'Run cron:'|trans }} {{ url.tag }}</a></li>
         {% endfor %}
-      </ul>
-    </p>
+    </ul>
 
 {% endblock %}    
diff --git a/modules/cron/www/assets/css/cron.css b/modules/cron/www/assets/css/cron.css
index 73cc3f9a4ba1091d36e7cc40bdc06a853c63bfbc..1f26a78d05023baa187fc0368d1f375de7de2807 100644
--- a/modules/cron/www/assets/css/cron.css
+++ b/modules/cron/www/assets/css/cron.css
@@ -1,4 +1,4 @@
 code#cronlist {
-    font-size: 0.8vw;
+    font-size: 0.7vw;
     white-space: nowrap;
 }
diff --git a/modules/cron/www/cron.php b/modules/cron/www/cron.php
index 2afe851fbc9230f1fff10eaabf122f5dbaadca19..d77b307bed35618aa4cf9f365befa619ac9c0ad3 100644
--- a/modules/cron/www/cron.php
+++ b/modules/cron/www/cron.php
@@ -1,38 +1,19 @@
 <?php
 
-$config = \SimpleSAML\Configuration::getInstance();
-$cronconfig = \SimpleSAML\Configuration::getConfig('module_cron.php');
+namespace SimpleSAML\Module\cron;
 
-if (!is_null($cronconfig->getValue('key'))) {
-    if ($_REQUEST['key'] !== $cronconfig->getValue('key')) {
-        \SimpleSAML\Logger::error('Cron - Wrong key provided. Cron will not run.');
-        exit;
-    }
-}
+use SimpleSAML\Configuration;
+use SimpleSAML\Session;
+use Symfony\Component\HttpFoundation\Request;
 
-$cron = new \SimpleSAML\Module\cron\Cron();
-if (!$cron->isValidTag($_REQUEST['tag'])) {
-    SimpleSAML\Logger::error('Cron - Illegal tag [' . $_REQUEST['tag'] . '].');
-    exit;
-}
+$config = Configuration::getInstance();
+$session = Session::getSessionFromRequest();
+$request = Request::createFromGlobals();
 
-$url = \SimpleSAML\Utils\HTTP::getSelfURL();
-$time = date(DATE_RFC822);
+$tag = $request->get('tag');
+$key = $request->get('key');
+$output = $request->get('output');
 
-$croninfo = $cron->runTag($_REQUEST['tag']);
-$summary = $croninfo['summary'];
-
-if ($cronconfig->getValue('sendemail', true) && count($summary) > 0) {
-    $mail = new \SimpleSAML\Utils\EMail('SimpleSAMLphp cron report');
-    $mail->setData(['url' => $url, 'tag' => $croninfo['tag'], 'summary' => $croninfo['summary']]);
-    $mail->send();
-}
-
-if (isset($_REQUEST['output']) && $_REQUEST['output'] == "xhtml") {
-    $t = new \SimpleSAML\XHTML\Template($config, 'cron:croninfo-result.tpl.php', 'cron:cron');
-    $t->data['tag'] = $croninfo['tag'];
-    $t->data['time'] = $time;
-    $t->data['url'] = $url;
-    $t->data['summary'] = $summary;
-    $t->show();
-}
+$controller = new Controller\Cron($config, $session);
+$response = $controller->run($tag, $key, $output);
+$response->send();
diff --git a/modules/cron/www/croninfo.php b/modules/cron/www/croninfo.php
index 8a0aac4d52a90de827a0344822163813ce15cfc4..ffabdb0fb5c0b2d06e3ff68f923e744c2a4c992d 100644
--- a/modules/cron/www/croninfo.php
+++ b/modules/cron/www/croninfo.php
@@ -5,35 +5,16 @@
  * initializes the SimpleSAMLphp config class with the correct path.
  */
 
-require_once('_include.php');
 
-// Load SimpleSAMLphp configuration and metadata
-$config = \SimpleSAML\Configuration::getInstance();
-$session = \SimpleSAML\Session::getSessionFromRequest();
+namespace SimpleSAML\Module\cron;
 
-\SimpleSAML\Utils\Auth::requireAdmin();
+use SimpleSAML\Configuration;
+use SimpleSAML\Session;
+use Symfony\Component\HttpFoundation\Request;
 
-$cronconfig = \SimpleSAML\Configuration::getConfig('module_cron.php');
+$config = Configuration::getInstance();
+$session = Session::getSessionFromRequest();
 
-$key = $cronconfig->getValue('key', '');
-$tags = $cronconfig->getValue('allowed_tags');
-
-$def = [
-    'weekly' => "22 0 * * 0",
-    'daily' => "02 0 * * *",
-    'hourly' => "01 * * * *",
-    'default' => "XXXXXXXXXX",
-];
-
-$urls = [];
-foreach ($tags as $tag) {
-    $urls[] = [
-        'href' => \SimpleSAML\Module::getModuleURL('cron/cron.php', ['key' => $key, 'tag' => $tag]),
-        'tag' => $tag,
-        'int' => (array_key_exists($tag, $def) ? $def[$tag] : $def['default']),
-    ];
-}
-
-$t = new \SimpleSAML\XHTML\Template($config, 'cron:croninfo.tpl.php', 'cron:cron');
-$t->data['urls'] = $urls;
-$t->show();
+$controller = new Controller\Cron($config, $session);
+$response = $controller->info();
+$response->send();