Skip to content
Snippets Groups Projects
Unverified Commit e16ea1ee authored by Jaime Pérez Crespo's avatar Jaime Pérez Crespo Committed by GitHub
Browse files

Merge pull request #1175 from simplesamlphp/feature/migrate-logout

Migrate logout to twig
parents b98a2b24 34600ac3
No related branches found
No related tags found
No related merge requests found
{% set pagetitle = '{logout:progress}'|trans %} {% set pagetitle = '{logout:progress}'|trans %}
{% extends "base.twig" %} {% extends "@core/logout-iframe.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 %}
{% 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 %}
...@@ -99,15 +99,21 @@ foreach ($state['core:Logout-IFrame:Associations'] as $association) { ...@@ -99,15 +99,21 @@ foreach ($state['core:Logout-IFrame:Associations'] as $association) {
$mdset = 'adfs-sp-remote'; $mdset = 'adfs-sp-remote';
} }
if ($association['core:Logout-IFrame:State'] === 'completed') {
continue;
}
$remaining[$key] = [ $remaining[$key] = [
'id' => $association['id'], 'id' => $association['id'],
'expires_on' => $association['Expires'], 'expires_on' => $association['Expires'],
'entityID' => $association['saml:entityID'], 'entityID' => $association['saml:entityID'],
'subject' => $association['saml:NameID'], 'subject' => $association['saml:NameID'],
'status' => $association['core:Logout-IFrame:State'], 'status' => $association['core:Logout-IFrame:State'],
'logoutURL' => $association['core:Logout-IFrame:URL'],
'metadata' => $mdh->getMetaDataConfig($association['saml:entityID'], $mdset)->toArray(), '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'])) { if (isset($association['core:Logout-IFrame:Timeout'])) {
$remaining[$key]['timeout'] = $association['core:Logout-IFrame:Timeout']; $remaining[$key]['timeout'] = $association['core:Logout-IFrame:Timeout'];
} }
......
/**
* 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
import SimpleSAMLLogout from './logout.js';
$(document).ready(function () {
new SimpleSAMLLogout($('body').attr('id'));
});
\ No newline at end of file
<!DOCTYPE html> {% set data = {}|merge({ "spid": spId }) %}
<html>
<head>
<title>Logout response from {{ assocId|escape('html') }}</title>
<script>
{% if errorMsg is defined %} {% if errorMsg is defined %}
window.parent.logoutFailed("{{ spId }}", "{{ errorMsg|escape }}"); {% set data = data|merge({ "error": errorMsg }) %}
{% else %}
window.parent.logoutCompleted("{{ spId }}");
{% endif %} {% endif %}
</script> {% extends "base.twig" %}
</head> {% block header %}{% endblock header %}
<body> {% block footer %}{% endblock footer %}
</body> {% block content %}
</html>
<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 %}
...@@ -18,6 +18,7 @@ module.exports = environment => { ...@@ -18,6 +18,7 @@ module.exports = environment => {
return { return {
entry: { entry: {
bundle: './src/js/bundle', bundle: './src/js/bundle',
logout: './src/js/logout/main',
stylesheet: './src/js/style' stylesheet: './src/js/style'
}, },
output: { output: {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment