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 ;
0 commit comments