Skip to content
Snippets Groups Projects
logout.js 8.78 KiB
Newer Older
/**
 * 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 {
Tim van Dijen's avatar
Tim van Dijen committed
    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.onclick(function () {
                this.initLogout.bind(this);
                return true;
            });
            window.addEventListener('message', this.clearAssociation.bind(this), false);
        } else if (page === 'IFrameLogoutHandler') { // iframe
            var data = document.querySelector('i[id="data"]');
            var message = {
                spId: data.getAttribute('data-spid')
            if (data.hasAttribute('data-error')) {
                message.error = data.getAttribute('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.
     */
Tim van Dijen's avatar
Tim van Dijen committed
    clearAssociation(event)
    {
        if (event.origin !== SimpleSAMLLogout.getOrigin()) {
            // we don't accept events from other origins
            return;
        }
        var 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.
     */
Tim van Dijen's avatar
Tim van Dijen committed
    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.
     */
Tim van Dijen's avatar
Tim van Dijen committed
    failed(id, reason)
    {
        if (typeof this.sps[id] === 'undefined') {
            return;
        }

        this.sps[id].element.addClass('error');
        //var icon = document.getElementById(this.sps[id].icon);
        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).
     */
Tim van Dijen's avatar
Tim van Dijen committed
    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.
     */
Tim van Dijen's avatar
Tim van Dijen committed
    static getOrigin()
    {
        var origin = window.location.origin;
        if (!origin) {
            // IE < 11 does not support window.location.origin
            origin = window.location.protocol + "//" + window.location.hostname +
Tim van Dijen's avatar
Tim van Dijen committed
                (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.
     */
Tim van Dijen's avatar
Tim van Dijen committed
    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.
     */
Tim van Dijen's avatar
Tim van Dijen committed
    initTimeout()
    {
        var 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;
            }
            var 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.
     */
Tim van Dijen's avatar
Tim van Dijen committed
    populateData()
    {
        this.btnall = document.querySelector('button[id="btn-all"]');
        this.btncancel = document.querySelector('button[id="btn-cancel"]');
        this.btncontinue = document.querySelector('button[id="btn-continue"]');
        this.actions = document.querySelector('div[id="original-actions"]');
        this.errmsg = document.querySelector('div[id="error-message"]');
        this.errfrm = document.querySelector('form[id="error-form"]');
        this.nfailed = 0;
        var that = this;

        // initialise SP status and timeout arrays
        document.querySelectorAll('li[id^="sp-"]').forEach(function (currentValue, index, arr) {
            var id = currentValue.getAttribute('data-id');
            var iframe = document.querySelector('iframe[id="iframe-' + id + '"]');
            var status = currentValue.getAttribute('data-status');

            switch (status) {
                case 'failed':
                    that.nfailed++;
                case 'completed':
                    return;
            }

            that.sps[id] = {
                status: status,
                timeout: currentValue.getAttribute('data-timeout'),
                element: currentValue,
                icon: document.querySelector('i[id="icon-' + id + '"]'),
Tim van Dijen's avatar
Tim van Dijen committed
export default SimpleSAMLLogout;