diff --git a/modules/core/templates/logout-iframe-wrapper.twig b/modules/core/templates/logout-iframe-wrapper.twig
index 7c9e415d072d9d25553bdb7e8eee4b05c76a4810..a1baa200c0560f829872d64d6492d0cd951f701b 100644
--- a/modules/core/templates/logout-iframe-wrapper.twig
+++ b/modules/core/templates/logout-iframe-wrapper.twig
@@ -1,16 +1,2 @@
-
 {% set pagetitle = '{logout:progress}'|trans %}
-{% extends "base.twig" %}
-
-{% block content %}
-    {# pretty arbitrary height, but should have enough safety margins for most cases #}
-    {% set iframeHeight = (25 + (SPs|length * 4)) %}
-
-    <iframe style="width:100%; height: {{ iframeHeight }}em; border:0;" src="logout-iframe.php?type=embed&id={{ auth_state|escape('url') }}"></iframe>
-
-    {% for assocId, sp in SPs %}
-    {% if attribute(sp, 'core:Logout-IFrame:State') == 'inprogress' %}
-    <iframe style="width:0; height:0; border:0;" src="{{ attribute(sp, 'core:Logout-IFrame:URL')|escape('html') }}</iframe>
-    {% endif %}
-    {% endfor %}
-{% endblock %}
+{% extends "@core/logout-iframe.twig" %}
diff --git a/modules/core/templates/logout-iframe.twig b/modules/core/templates/logout-iframe.twig
new file mode 100644
index 0000000000000000000000000000000000000000..e3959ba6282ceb0891b5dea7bb20067921ad25bb
--- /dev/null
+++ b/modules/core/templates/logout-iframe.twig
@@ -0,0 +1,123 @@
+{% set pagetitle = 'Logging out...'|trans %}
+{% extends "base.twig" %}
+
+{% block preload %}
+
+    <link rel="preload" href="{{ asset('js/logout.js') }}" as="script">
+    {%- if type != "init" %}
+      {%- set content = '2' %}
+      {%- if remaining_services|length == 0 %}
+        {%- set content = '0; url=logout-iframe-done.php?id=' ~ auth_state %}
+      {%- endif %}
+
+    <meta http-equiv="refresh" content="{{ content }}">
+    {% endif %}
+{% endblock preload %}
+
+{% block content %}
+
+    <h1>{{ pagetitle }}</h1>
+  {%- if terminated_service %}
+    {%- set SP = terminated_service['name']|translateFromArray|default('the service'|trans)|e %}
+
+    <p>{% trans %}You are now successfully logged out from {{ SP }}.{% endtrans %}</p>
+  {%- endif %}
+  {%- if remaining_services %}
+    {%- set failed = false  %}
+    {%- set remaining = 0 %}
+    {%- if remaining_services|length > 0 %}
+
+    <p>{% trans %}You are also logged in on these services:{% endtrans %}</p>
+    {%- endif %}
+
+    <div class="custom-restricted-width">
+      <ul class="fa-ul">
+    {%- for key, sp in remaining_services %}
+      {%- set timeout = 5 %}
+      {%- set name = sp['metadata']['name']|translateFromArray|default(sp['entityID']) %}
+      {%- set icon = 'circle-o-notch'  %}
+      {%- if sp['status'] == 'completed' %}
+        {%- set icon = 'check-circle' %}
+      {%- elseif sp['status'] == 'failed' %}
+        {%- set icon = 'exclamation-circle' %}
+        {%- set failed = true %}
+      {%- elseif (sp['status'] == 'onhold' or sp['status'] == 'inprogress') %}
+        {%- set remaining = remaining + 1 %}
+      {%- endif %}
+      {%- if type == 'nojs' and sp['status'] == 'inprogress' %}
+        {%- set icon = icon ~ ' fa-spin' %}
+      {%- endif %}
+
+        <li id="sp-{{ key }}" data-id="{{ key }}" data-status="{{ sp['status'] }}"
+            {#- #} data-timeout="{{ timeout }}">
+          <span class="fa-li"><i id="icon-{{ key }}" class="fa fa-{{ icon }}"></i></span>
+          {{ name }}
+      {%- if sp['status'] != 'completed' and sp['status'] != 'failed' %}
+        {%- if type == 'nojs' %}
+
+          <iframe id="iframe-{{ key }}" class="hidden" src="{{ sp['logoutURL'] }}"></iframe>
+        {%- else %}
+
+          <iframe id="iframe-{{ key }}" class="hidden" data-url="{{ sp['logoutURL'] }}"></iframe>
+        {%- endif %}
+      {%- else %}
+        {%- if sp['status'] == 'failed' %}
+          ({% trans %}logout is not supported{% endtrans %})
+        {%- endif %}
+      {%- endif %}
+
+        </li>
+    {%- endfor %}
+
+      </ul>
+    </div>
+    <br>
+    <div id="error-message"{% if not failed or type == 'init' %} class="hidden"{% endif %}>
+      <div class="message-box error">
+        {% trans %}Unable to log out of one or more services. To ensure that all your
+        {#- #} sessions are closed, you are encouraged to <i>close your webbrowser</i>.{% endtrans %}
+      </div>
+    </div>
+    <form id="error-form" action="logout-iframe-done.php"
+          {%- if (not failed or type == 'init') and remaining %} class="hidden"{% endif %}>
+      <input type="hidden" name="id" value="{{ auth_state }}">
+      <button type="submit" id="btn-continue" name="ok" class="pure-button pure-button-red">
+        {%- trans %}Continue{% endtrans -%}
+      </button>
+    </form>
+    <div id="original-actions"{% if type != 'init' %} class="hidden"{% endif %}>
+      <p>{% trans %}Do you want to logout from all the services above?{% endtrans %}</p>
+      <div class="pure-button-group two-elements">
+        <form id="startform" action="logout-iframe.php">
+          <input type="hidden" name="id" value="{{ auth_state }}">
+          <noscript>
+            <input type="hidden" name="type" value="nojs" id="logout-type-selector">
+          </noscript>
+          <button type="submit" id="btn-all" name="ok" class="pure-button pure-button-red">
+            {%- trans %}Yes, all services{% endtrans -%}
+          </button>
+        </form>
+        <form action="logout-iframe-done.php">
+          <input type="hidden" name="id" value="{{ auth_state }}">
+          <input type="hidden" name="cancel" value="">
+          <button id="btn-cancel" class="pure-button" type="submit">
+            {%- if terminated_service %}{% trans %}No, only {{ SP }}{% endtrans %}
+            {%- else %}{% trans %}No{% endtrans %}{% endif -%}
+          </button>
+        </form>
+      </div>
+    </div>
+  {%- else %}
+    <form id="error-form" action="logout-iframe-done.php">
+      <input type="hidden" name="id" value="{{ auth_state }}">
+      <button type="submit" id="btn-continue" name="ok" class="pure-button pure-button-red">
+        {%- trans %}Continue{% endtrans -%}
+      </button>
+    </form>
+  {% endif %}
+{% endblock %}
+
+{% block postload %}
+
+    <script src="{{ asset('js/logout.js') }}"></script>
+{% endblock postload %}
diff --git a/modules/core/www/idp/logout-iframe.php b/modules/core/www/idp/logout-iframe.php
index 5e58666aedfdd2c12a3b5bc1011c89fe7656c582..ea0024942a2158205a7c12d60d46d25c7a5e813f 100644
--- a/modules/core/www/idp/logout-iframe.php
+++ b/modules/core/www/idp/logout-iframe.php
@@ -99,15 +99,21 @@ foreach ($state['core:Logout-IFrame:Associations'] as $association) {
         $mdset = 'adfs-sp-remote';
     }
 
+    if ($association['core:Logout-IFrame:State'] === 'completed') {
+        continue;
+    }
+
     $remaining[$key] = [
         'id' => $association['id'],
         'expires_on' => $association['Expires'],
         'entityID' => $association['saml:entityID'],
         'subject' => $association['saml:NameID'],
         'status' => $association['core:Logout-IFrame:State'],
-        'logoutURL' => $association['core:Logout-IFrame:URL'],
         'metadata' => $mdh->getMetaDataConfig($association['saml:entityID'], $mdset)->toArray(),
     ];
+    if (isset($association['core:Logout-IFrame:URL'])) {
+        $remaining[$key]['logoutURL'] = $association['core:Logout-IFrame:URL'];
+    }
     if (isset($association['core:Logout-IFrame:Timeout'])) {
         $remaining[$key]['timeout'] = $association['core:Logout-IFrame:Timeout'];
     }
diff --git a/src/js/logout/logout.js b/src/js/logout/logout.js
new file mode 100644
index 0000000000000000000000000000000000000000..aec766b8ffd7a8f07f91f4e3b311938d76ba9f7c
--- /dev/null
+++ b/src/js/logout/logout.js
@@ -0,0 +1,255 @@
+/**
+ * This class is used for the logout page.
+ *
+ * It allows the user to start logout from all the services where a session exists (if any). Logout will be
+ * triggered by loading an iframe where we send a SAML logout request to the SingleLogoutService endpoint of the
+ * given SP. After successful response back from the SP, we will load a small template in the iframe that loads
+ * this class again (IFrameLogoutHandler branch of the constructor), and sends a message to the main page
+ * (core:logout-iframe branch).
+ *
+ * The iframes communicate the logout status for their corresponding association via an event message, for which the
+ * main page is listening (the clearAssociation() method). Upon reception of a message, we'll check if there was an
+ * error or not, and call the appropriate method (either completed() or failed()).
+ */
+class SimpleSAMLLogout {
+    constructor(page) {
+        if (page === 'core:logout-iframe') { // main page
+            this.populateData();
+            if (Object.keys(this.sps).length === 0) {
+                // all SPs completed logout, this was a reload
+                this.btncontinue.click();
+            }
+            this.btnall.on('click', this.initLogout.bind(this));
+            window.addEventListener('message', this.clearAssociation.bind(this), false);
+
+        } else if (page === 'IFrameLogoutHandler') { // iframe
+            let data = $('i[id="data"]');
+            let message = {
+                spId: $(data).data('spid')
+            };
+            if ($(data).data('error')) {
+                message.error = $(data).data('error');
+            }
+
+            window.parent.postMessage(JSON.stringify(message), SimpleSAMLLogout.getOrigin());
+        }
+    }
+
+
+    /**
+     * Clear an association when it is signaled from an iframe (either failed or completed).
+     *
+     * @param event The event containing the message from the iframe.
+     */
+    clearAssociation(event) {
+        if (event.origin !== SimpleSAMLLogout.getOrigin()) {
+            // we don't accept events from other origins
+            return;
+        }
+        let data = JSON.parse(event.data);
+        if (typeof data.error === 'undefined') {
+            this.completed(data.spId);
+        } else {
+            this.failed(data.spId, data.error);
+        }
+
+        if (Object.keys(this.sps).length === 0) {
+            if (this.nfailed === 0) {
+                // all SPs successfully logged out, continue w/o user interaction
+                this.btncontinue.click();
+            }
+        }
+    }
+
+
+    /**
+     * Mark logout as completed for a given SP.
+     *
+     * This method will be called by the SimpleSAML\IdP\IFrameLogoutHandler class upon successful logout from the SP.
+     *
+     * @param id The ID of the SP that completed logout successfully.
+     */
+    completed(id) {
+        if (typeof this.sps[id] === 'undefined') {
+            return;
+        }
+
+        this.sps[id].icon.removeClass('fa-spin');
+        this.sps[id].icon.removeClass('fa-circle-o-notch');
+        this.sps[id].icon.addClass('fa-check-circle');
+        this.sps[id].element.toggle();
+        delete this.sps[id];
+        this.finish();
+    }
+
+
+    /**
+     * Mark logout as failed for a given SP.
+     *
+     * This method will be called by the SimpleSAML\IdP\IFrameLogoutHandler class upon logout failure from the SP.
+     *
+     * @param id The ID of the SP that failed to complete logout.
+     * @param reason The reason why logout failed.
+     */
+    failed(id, reason) {
+        if (typeof this.sps[id] === 'undefined') {
+            return;
+        }
+
+        this.sps[id].element.addClass('error');
+        $(this.sps[id].icon).removeClass('fa-spin fa-circle-o-notch');
+        $(this.sps[id].icon).addClass('fa-exclamation-circle');
+
+        if (this.errmsg.hasClass('hidden')) {
+            this.errmsg.removeClass('hidden');
+        }
+        if (this.errfrm.hasClass('hidden')) {
+            this.errfrm.removeClass('hidden');
+        }
+
+        delete this.sps[id];
+        this.nfailed++;
+        this.finish();
+    }
+
+
+    /**
+     * Finish the logout process, acting according to the current situation:
+     *
+     * - If there were failures, an error message is shown telling the user to close the browser.
+     * - If everything went ok, then we just continue back to the service that started logout.
+     *
+     * Note: this method won't do anything if there are SPs pending logout (e.g. waiting for the timeout).
+     */
+    finish() {
+        if (Object.keys(this.sps).length > 0) { // pending services
+            return;
+        }
+
+        if (typeof this.timeout !== 'undefined') {
+            clearTimeout(this.timeout);
+        }
+
+        if (this.nfailed > 0) { // some services failed to log out
+            this.errmsg.removeClass('hidden');
+            this.errfrm.removeClass('hidden');
+            this.actions.addClass('hidden');
+
+        } else { // all services done
+            this.btncontinue.click();
+        }
+    }
+
+
+    /**
+     * Get the origin of the current page.
+     */
+    static getOrigin() {
+        let origin = window.location.origin;
+        if (!origin) {
+            // IE < 11 does not support window.location.origin
+            origin = window.location.protocol + "//" + window.location.hostname +
+                (window.location.port ? ':' + window.location.port: '');
+        }
+        return origin;
+    }
+
+
+    /**
+     * This method starts logout on all SPs where we are currently logged in.
+     *
+     * @param event The click event on the "Yes, all services" button.
+     */
+    initLogout(event) {
+        event.preventDefault();
+
+        this.btnall.prop('disabled', true);
+        this.btncancel.prop('disabled', true);
+        Object.keys(this.sps).forEach((function (id) {
+            this.sps[id].status = 'inprogress';
+            this.sps[id].startTime = (new Date()).getTime();
+            this.sps[id].iframe.attr('src', this.sps[id].iframe.data('url'));
+            this.sps[id].icon.addClass('fa-spin');
+        }).bind(this));
+        this.initTimeout();
+    }
+
+
+    /**
+     * Set timeouts for all logout operations.
+     *
+     * If an SP didn't reply by the timeout, we'll mark it as failed.
+     */
+    initTimeout() {
+        let timeout = 10;
+
+        for (const id in this.sps) {
+            if (typeof id === 'undefined') {
+                continue;
+            }
+            if (!this.sps.hasOwnProperty(id)) {
+                continue;
+            }
+            if (this.sps[id].status !== 'inprogress') {
+                continue;
+            }
+            let now = ((new Date()).getTime() - this.sps[id].startTime) / 1000;
+
+            if (this.sps[id].timeout <= now) {
+                this.failed(id, 'Timed out', window.document);
+            } else {
+                // get the lowest timeout we have
+                if ((this.sps[id].timeout - now) < timeout) {
+                    timeout = this.sps[id].timeout - now;
+                }
+            }
+        }
+
+        if (Object.keys(this.sps).length > 0) {
+            // we have associations left, check them again as soon as one expires
+            this.timeout = setTimeout(this.initTimeout.bind(this), timeout * 1000);
+        } else {
+            this.finish();
+        }
+    }
+
+
+    /**
+     * This method populates the data we need from data-* properties in the page.
+     */
+    populateData() {
+        this.sps = {};
+        this.btnall = $('button[id="btn-all"]');
+        this.btncancel = $('button[id="btn-cancel"]');
+        this.btncontinue = $('button[id="btn-continue"]');
+        this.actions = $('div[id="original-actions"]');
+        this.errmsg = $('div[id="error-message"]');
+        this.errfrm = $('form[id="error-form"]');
+        this.nfailed = 0;
+        let that = this;
+
+        // initialise SP status and timeout arrays
+        $('li[id^="sp-"]').each(function () {
+            let id = $(this).data('id');
+            let iframe = $('iframe[id="iframe-'+id+'"]');
+            let status = $(this).data('status');
+
+            switch (status) {
+                case 'failed':
+                    that.nfailed++;
+                case 'completed':
+                    return;
+            }
+
+            that.sps[id] = {
+                status: status,
+                timeout: $(this).data('timeout'),
+                element: $(this),
+                iframe: iframe,
+                icon: $('i[id="icon-'+id+'"]'),
+            };
+        });
+    }
+}
+
+export default SimpleSAMLLogout;
\ No newline at end of file
diff --git a/src/js/logout/main.js b/src/js/logout/main.js
new file mode 100644
index 0000000000000000000000000000000000000000..16c508f9ce3506785070cfada90d71c12f9a7f73
--- /dev/null
+++ b/src/js/logout/main.js
@@ -0,0 +1,5 @@
+import SimpleSAMLLogout from './logout.js';
+
+$(document).ready(function () {
+    new SimpleSAMLLogout($('body').attr('id'));
+});
\ No newline at end of file
diff --git a/templates/IFrameLogoutHandler.twig b/templates/IFrameLogoutHandler.twig
index ad4d4a69856df57610541a23844c1d6f8046f667..946dd71a2c934cd2be838e810dc30977877841c1 100644
--- a/templates/IFrameLogoutHandler.twig
+++ b/templates/IFrameLogoutHandler.twig
@@ -1,16 +1,16 @@
-<!DOCTYPE html>
-<html>
-    <head>
-        <title>Logout response from {{ assocId|escape('html') }}</title>
-        <script>
+{% set data = {}|merge({ "spid": spId }) %}
 {% if errorMsg is defined %}
-            window.parent.logoutFailed("{{ spId }}", "{{ errorMsg|escape }}");
-{% else %}
-            window.parent.logoutCompleted("{{ spId }}");
+    {% set data = data|merge({ "error": errorMsg }) %}
 {% endif %}
-        </script>
-    </head>
-    <body>
-    </body>
-</html>
+{% extends "base.twig" %}
+{% block header %}{% endblock header %}
+{% block footer %}{% endblock footer %}
+{% block content %}
+
+          <i id="data"{% for k,v in data %} data-{{ k }}="{{ v }}"{% endfor %}></i>
+{% endblock content %}
+{% block postload %}
+
+    <script src="{{ asset('js/logout.js') }}"></script>
+{% endblock postload %}
 
diff --git a/webpack.config.js b/webpack.config.js
index 2dff590d58a232a22edb9060b4ff94c9d3758f57..1237e29c1b8d9a13d360fe594e2c3c934856f57c 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -18,6 +18,7 @@ module.exports = environment => {
     return {
         entry: {
             bundle: './src/js/bundle',
+            logout: './src/js/logout/main',
             stylesheet: './src/js/style'
         },
         output: {