Skip to content

Commit eee050b

Browse files
committed
Migrate the iframe logout templates.
Non-JS version still needs work. We need to review also non-iframe logout.
1 parent 832a972 commit eee050b

6 files changed

Lines changed: 376 additions & 14 deletions

File tree

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
{% set pagetitle = 'Logging out...'|trans %}
2+
{% extends "base.twig" %}
3+
4+
{% block preload %}
5+
6+
<link rel="preload" href="{{ asset('js/logout.js') }}" as="script">
7+
{% endblock preload %}
8+
9+
{% block content %}
10+
11+
<h1>{{ pagetitle }}</h1>
12+
{%- if terminated_service %}
13+
{%- set SP = terminated_service['name']|translateFromArray|default('the service'|trans)|e %}
14+
15+
<p>{% trans %}You are now successfully logged out from {{ SP }}.{% endtrans %}</p>
16+
{%- endif %}
17+
{%- if remaining_services %}
18+
{%- set failed = false %}
19+
{%- set remaining = 0 %}
20+
21+
<p>{% trans %}You are also logged in on these services:{% endtrans %}</p>
22+
<div class="custom-restricted-width">
23+
<ul class="fa-ul">
24+
{%- for key, sp in remaining_services %}
25+
{%- set timeout = 5 %}
26+
{%- set name = sp['metadata']['name']|translateFromArray|default(sp['entityID']) %}
27+
{%- set icon = 'circle-o-notch' %}
28+
{%- if sp['status'] == 'completed' %}
29+
{%- set icon = 'check-circle' %}
30+
{%- elseif sp['status'] == 'failed' %}
31+
{%- set icon = 'exclamation-circle' %}
32+
{%- elseif sp['status'] == 'onhold' or sp['status'] == 'inprogress '%}
33+
{%- set remaining = remaining + 1 %}
34+
{%- endif %}
35+
36+
<li id="sp-{{ key }}" data-id="{{ key }}" data-status="{{ sp['status'] }}"
37+
{#- #} data-timeout="{{ timeout }}">
38+
<span class="fa-li"><i id="icon-{{ key }}" class="fa fa-{{ icon }}"></i></span>
39+
{{ name }} ({{ sp['status'] }})
40+
{%- if sp['status'] != 'completed' and sp['status'] != 'failed' %}
41+
{%- if type == 'nojs' %}
42+
43+
<iframe id="iframe-{{ key }}" class="hidden" src="{{ sp['logoutURL'] }}"></iframe>
44+
{%- else %}
45+
46+
<iframe id="iframe-{{ key }}" class="hidden" data-url="{{ sp['logoutURL'] }}"></iframe>
47+
{%- endif %}
48+
{%- else %}
49+
{%- if sp['status'] == 'failed' %}
50+
51+
({% trans %}logout is not supported{% endtrans %})
52+
{%- endif %}
53+
{%- endif %}
54+
55+
</li>
56+
{%- endfor %}
57+
58+
</ul>
59+
</div>
60+
<br/>
61+
<div id="error-message"{% if not failed %} class="hidden"{% endif %}>
62+
<div class="message-box error">
63+
{% trans %}Unable to log out of one or more services. To ensure that all your
64+
{#- #} sessions are closed, you are encouraged to <i>close your webbrowser</i>.{% endtrans %}
65+
</div>
66+
</div>
67+
<form id="error-form" action="logout-iframe-done.php"{% if remaining %} class="hidden"{% endif %}>
68+
<input type="hidden" name="id" value="{{ auth_state }}">
69+
<button type="submit" id="btn-continue" name="ok" class="pure-button pure-button-red">
70+
{%- trans %}Continue{% endtrans -%}
71+
</button>
72+
</form>
73+
<div id="original-actions"{% if remaining == 0 %} class="hidden"{% endif %}>
74+
<p>{% trans %}Do you want to logout from all the services above?{% endtrans %}</p>
75+
<div class="pure-button-group two-elements">
76+
<form id="startform" action="logout-iframe.php">
77+
<input type="hidden" name="id" value="{{ auth_state }}">
78+
<input type="hidden" name="type" value="nojs" id="logout-type-selector">{# ?? #}
79+
<button type="submit" id="btn-all" name="ok" class="pure-button pure-button-red">
80+
{%- trans %}Yes, all services{% endtrans -%}
81+
</button>
82+
</form>
83+
<form action="logout-iframe-done.php">
84+
<input type="hidden" name="id" value="{{ auth_state }}">
85+
<input type="hidden" name="cancel" value="">
86+
<button id="btn-cancel" class="pure-button" type="submit">
87+
{%- if terminated_service %}{% trans %}No, only {{ SP }}{% endtrans %}
88+
{%- else %}{% trans %}No{% endtrans %}{% endif -%}
89+
</button>
90+
</form>
91+
</div>
92+
</div>
93+
{% endif %}
94+
{% endblock %}
95+
96+
{% block postload %}
97+
98+
<script type="application/javascript" src="{{ asset('js/logout.js') }}"></script>
99+
{% endblock postload %}

modules/core/www/idp/logout-iframe.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,11 @@
105105
'entityID' => $association['saml:entityID'],
106106
'subject' => $association['saml:NameID'],
107107
'status' => $association['core:Logout-IFrame:State'],
108-
'logoutURL' => $association['core:Logout-IFrame:URL'],
109108
'metadata' => $mdh->getMetaDataConfig($association['saml:entityID'], $mdset)->toArray(),
110109
];
110+
if (isset($association['core:Logout-IFrame:URL'])) {
111+
$remaining[$key]['logoutURL'] = $association['core:Logout-IFrame:URL'];
112+
}
111113
if (isset($association['core:Logout-IFrame:Timeout'])) {
112114
$remaining[$key]['timeout'] = $association['core:Logout-IFrame:Timeout'];
113115
}

src/js/logout/logout.js

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
/**
2+
* This class is used for the logout page.
3+
*
4+
* It allows the user to start logout from all the services where a session exists (if any). Logout will be
5+
* triggered by loading an iframe where we send a SAML logout request to the SingleLogoutService endpoint of the
6+
* given SP. After successful response back from the SP, we will load a small template in the iframe that loads
7+
* this class again (IFrameLogoutHandler branch of the constructor), and sends a message to the main page
8+
* (core:logout-iframe branch).
9+
*
10+
* The iframes communicate the logout status for their corresponding association via an event message, for which the
11+
* main page is listening (the clearAssociation() method). Upon reception of a message, we'll check if there was an
12+
* error or not, and call the appropriate method (either completed() or failed()).
13+
*/
14+
class SimpleSAMLLogout {
15+
constructor(page) {
16+
if (page === 'core:logout-iframe') { // main page
17+
this.populateData();
18+
if (Object.keys(this.sps).length === 0) {
19+
// all SPs completed logout, this was a reload
20+
this.btncontinue.click();
21+
}
22+
this.btnall.on('click', this.initLogout.bind(this));
23+
window.addEventListener('message', this.clearAssociation.bind(this), false);
24+
25+
} else if (page === 'IFrameLogoutHandler') { // iframe
26+
let data = $('i[id="data"]');
27+
let message = {
28+
spId: $(data).data('spid')
29+
};
30+
if ($(data).data('error')) {
31+
message.error = $(data).data('error');
32+
}
33+
34+
window.parent.postMessage(JSON.stringify(message), SimpleSAMLLogout.getOrigin());
35+
}
36+
}
37+
38+
39+
/**
40+
* Clear an association when it is signaled from an iframe (either failed or completed).
41+
*
42+
* @param event The event containing the message from the iframe.
43+
*/
44+
clearAssociation(event) {
45+
if (event.origin !== SimpleSAMLLogout.getOrigin()) {
46+
// we don't accept events from other origins
47+
return;
48+
}
49+
let data = JSON.parse(event.data);
50+
if (typeof data.error === 'undefined') {
51+
this.completed(data.spId);
52+
} else {
53+
this.failed(data.spId, data.error);
54+
}
55+
56+
if (Object.keys(this.sps).length === 0) {
57+
if (this.nfailed === 0) {
58+
// all SPs successfully logged out, continue w/o user interaction
59+
this.btncontinue.click();
60+
}
61+
}
62+
}
63+
64+
65+
/**
66+
* Mark logout as completed for a given SP.
67+
*
68+
* This method will be called by the SimpleSAML\IdP\IFrameLogoutHandler class upon successful logout from the SP.
69+
*
70+
* @param id The ID of the SP that completed logout successfully.
71+
*/
72+
completed(id) {
73+
if (typeof this.sps[id] === 'undefined') {
74+
return;
75+
}
76+
77+
this.sps[id].icon.removeClass('fa-spin');
78+
this.sps[id].icon.removeClass('fa-circle-o-notch');
79+
this.sps[id].icon.addClass('fa-check-circle');
80+
this.sps[id].element.toggle();
81+
delete this.sps[id];
82+
this.finish();
83+
}
84+
85+
86+
/**
87+
* Mark logout as failed for a given SP.
88+
*
89+
* This method will be called by the SimpleSAML\IdP\IFrameLogoutHandler class upon logout failure from the SP.
90+
*
91+
* @param id The ID of the SP that failed to complete logout.
92+
* @param reason The reason why logout failed.
93+
*/
94+
failed(id, reason) {
95+
if (typeof this.sps[id] === 'undefined') {
96+
return;
97+
}
98+
99+
this.sps[id].element.addClass('error');
100+
$(this.sps[id].icon).removeClass('fa-spin fa-circle-o-notch');
101+
$(this.sps[id].icon).addClass('fa-exclamation-circle');
102+
103+
if (this.errmsg.hasClass('hidden')) {
104+
this.errmsg.removeClass('hidden');
105+
}
106+
if (this.errfrm.hasClass('hidden')) {
107+
this.errfrm.removeClass('hidden');
108+
}
109+
110+
delete this.sps[id];
111+
this.nfailed++;
112+
this.finish();
113+
}
114+
115+
116+
/**
117+
* Finish the logout process, acting according to the current situation:
118+
*
119+
* - If there were failures, an error message is shown telling the user to close the browser.
120+
* - If everything went ok, then we just continue back to the service that started logout.
121+
*
122+
* Note: this method won't do anything if there are SPs pending logout (e.g. waiting for the timeout).
123+
*/
124+
finish() {
125+
if (Object.keys(this.sps).length > 0) { // pending services
126+
return;
127+
}
128+
129+
if (typeof this.timeout !== 'undefined') {
130+
clearTimeout(this.timeout);
131+
}
132+
133+
if (this.nfailed > 0) { // some services failed to log out
134+
this.errmsg.removeClass('hidden');
135+
this.errfrm.removeClass('hidden');
136+
this.actions.addClass('hidden');
137+
138+
} else { // all services done
139+
this.btncontinue.click();
140+
}
141+
}
142+
143+
144+
/**
145+
* Get the origin of the current page.
146+
*/
147+
static getOrigin() {
148+
let origin = window.location.origin;
149+
if (!origin) {
150+
// IE < 11 does not support window.location.origin
151+
origin = window.location.protocol + "//" + window.location.hostname +
152+
(window.location.port ? ':' + window.location.port: '');
153+
}
154+
return origin;
155+
}
156+
157+
158+
/**
159+
* This method starts logout on all SPs where we are currently logged in.
160+
*
161+
* @param event The click event on the "Yes, all services" button.
162+
*/
163+
initLogout(event) {
164+
event.preventDefault();
165+
166+
this.btnall.prop('disabled', true);
167+
this.btncancel.prop('disabled', true);
168+
Object.keys(this.sps).forEach((function (id) {
169+
this.sps[id].status = 'inprogress';
170+
this.sps[id].startTime = (new Date()).getTime();
171+
this.sps[id].iframe.attr('src', this.sps[id].iframe.data('url'));
172+
this.sps[id].icon.addClass('fa-spin');
173+
}).bind(this));
174+
this.initTimeout();
175+
}
176+
177+
178+
/**
179+
* Set timeouts for all logout operations.
180+
*
181+
* If an SP didn't reply by the timeout, we'll mark it as failed.
182+
*/
183+
initTimeout() {
184+
let timeout = 10;
185+
186+
for (const id in this.sps) {
187+
if (typeof id === 'undefined') {
188+
continue;
189+
}
190+
if (!this.sps.hasOwnProperty(id)) {
191+
continue;
192+
}
193+
if (this.sps[id].status !== 'inprogress') {
194+
continue;
195+
}
196+
let now = ((new Date()).getTime() - this.sps[id].startTime) / 1000;
197+
198+
if (this.sps[id].timeout <= now) {
199+
this.failed(id, 'Timed out', window.document);
200+
} else {
201+
// get the lowest timeout we have
202+
if ((this.sps[id].timeout - now) < timeout) {
203+
timeout = this.sps[id].timeout - now;
204+
}
205+
}
206+
}
207+
208+
if (Object.keys(this.sps).length > 0) {
209+
// we have associations left, check them again as soon as one expires
210+
this.timeout = setTimeout(this.initTimeout.bind(this), timeout * 1000);
211+
} else {
212+
this.finish();
213+
}
214+
}
215+
216+
217+
/**
218+
* This method populates the data we need from data-* properties in the page.
219+
*/
220+
populateData() {
221+
this.sps = {};
222+
this.btnall = $('button[id="btn-all"]');
223+
this.btncancel = $('button[id="btn-cancel"]');
224+
this.btncontinue = $('button[id="btn-continue"]');
225+
this.actions = $('div[id="original-actions"]');
226+
this.errmsg = $('div[id="error-message"]');
227+
this.errfrm = $('form[id="error-form"]');
228+
this.nfailed = 0;
229+
let that = this;
230+
231+
// initialise SP status and timeout arrays
232+
$('li[id^="sp-"]').each(function () {
233+
let id = $(this).data('id');
234+
let iframe = $('iframe[id="iframe-'+id+'"]');
235+
let status = $(this).data('status');
236+
237+
switch (status) {
238+
case 'failed':
239+
that.nfailed++;
240+
case 'completed':
241+
return;
242+
}
243+
244+
that.sps[id] = {
245+
status: status,
246+
timeout: $(this).data('timeout'),
247+
element: $(this),
248+
iframe: iframe,
249+
icon: $('i[id="icon-'+id+'"]'),
250+
};
251+
});
252+
}
253+
}
254+
255+
export default SimpleSAMLLogout;

src/js/logout/main.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import SimpleSAMLLogout from './logout.js';
2+
3+
$(document).ready(function () {
4+
new SimpleSAMLLogout($('body').attr('id'));
5+
});

0 commit comments

Comments
 (0)