From 42a7cde1ac9e7c8a04f4f21877bb2c89a5428d77 Mon Sep 17 00:00:00 2001
From: ale <ale@incal.net>
Date: Sun, 18 Feb 2018 13:13:57 +0000
Subject: [PATCH] Test Javascript-based logout

Using CORS-enabled requests in the background.
---
 httpsso/handler.go           |   2 +
 server/bindata.go            | 123 ++++++++++++++++++++++++++++++-----
 server/http.go               |   1 +
 server/static/css/signin.css |  13 ++++
 server/static/js/logout.js   |  37 +++++++++++
 server/templates/logout.html |  24 ++++++-
 server/templates/page.html   |   5 +-
 7 files changed, 185 insertions(+), 20 deletions(-)
 create mode 100644 server/static/js/logout.js

diff --git a/httpsso/handler.go b/httpsso/handler.go
index 7d42db8..26bb143 100644
--- a/httpsso/handler.go
+++ b/httpsso/handler.go
@@ -130,6 +130,8 @@ func (s *SSOWrapper) handleLogout(w http.ResponseWriter, req *http.Request, sess
 	}
 
 	w.Header().Set("Content-Type", "text/plain")
+	w.Header().Set("Access-Control-Allow-Origin", strings.TrimRight(s.serverURL, "/"))
+	w.Header().Set("Access-Control-Allow-Credentials", "true")
 	io.WriteString(w, "OK")
 }
 
diff --git a/server/bindata.go b/server/bindata.go
index 0dbb789..4c4d717 100644
--- a/server/bindata.go
+++ b/server/bindata.go
@@ -4,6 +4,7 @@
 // static/css/signin.css
 // static/js/bootstrap-4.0.0-beta.min.js
 // static/js/jquery-3.2.1.min.js
+// static/js/logout.js
 // static/js/popper-1.11.0.min.js
 // static/js/u2f-api.js
 // static/js/u2f.js
@@ -73,7 +74,7 @@ func staticCssBootstrapMinCss() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "static/css/bootstrap.min.css", size: 124962, mode: os.FileMode(420), modTime: time.Unix(1509120975, 0)}
+	info := bindataFileInfo{name: "static/css/bootstrap.min.css", size: 124962, mode: os.FileMode(436), modTime: time.Unix(1510996183, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -120,6 +121,19 @@ var _staticCssSigninCss = []byte(`body {
   font-weight: bold;
   color: red;
 }
+
+/* logout page */
+.logout-status {
+  font-weight: bold;
+}
+.logout-status-ok {
+  background-color: green;
+  color: white;
+}
+.logout-status-error {
+  background-color: red;
+  color: white;
+}
 `)
 
 func staticCssSigninCssBytes() ([]byte, error) {
@@ -132,7 +146,7 @@ func staticCssSigninCss() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "static/css/signin.css", size: 802, mode: os.FileMode(420), modTime: time.Unix(1511166680, 0)}
+	info := bindataFileInfo{name: "static/css/signin.css", size: 992, mode: os.FileMode(436), modTime: time.Unix(1518958548, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -154,7 +168,7 @@ func staticJsBootstrap400BetaMinJs() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "static/js/bootstrap-4.0.0-beta.min.js", size: 51143, mode: os.FileMode(420), modTime: time.Unix(1509120962, 0)}
+	info := bindataFileInfo{name: "static/js/bootstrap-4.0.0-beta.min.js", size: 51143, mode: os.FileMode(436), modTime: time.Unix(1510996183, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -175,7 +189,61 @@ func staticJsJquery321MinJs() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "static/js/jquery-3.2.1.min.js", size: 86659, mode: os.FileMode(420), modTime: time.Unix(1509120962, 0)}
+	info := bindataFileInfo{name: "static/js/jquery-3.2.1.min.js", size: 86659, mode: os.FileMode(436), modTime: time.Unix(1510996183, 0)}
+	a := &asset{bytes: bytes, info: info}
+	return a, nil
+}
+
+var _staticJsLogoutJs = []byte(`var idlogout = {};
+
+idlogout.get_services = function() {
+    return JSON.parse($('#services').attr('data_values'));
+};
+
+idlogout.logout_service = function(service) {
+    var logout_url = service.url + 'sso_logout';
+    console.log('logging out of ' + service.name);
+    $.ajax({
+        type: 'GET',
+        url: logout_url,
+        contentType: 'text/plain',
+        xhrFields: {
+            withCredentials: true
+        },
+        success: function() {
+            $('#status_'+service.idx).class('logout-status-ok').text('OK');
+            console.log('successful logout for ' + service.name);
+        },
+        error: function() {
+            $('#status_'+service.idx).class('logout-status-error').text('ERROR');
+            console.log('error logging out of ' + service.name);
+        }
+    });
+};
+
+idlogout.logout = function() {
+    var services = idlogout.get_services();
+    $.each(services, func(index, arg) {
+        idlogout.logout_service(arg);
+    });
+};
+
+$(function() {
+    idlogout.logout();
+});
+`)
+
+func staticJsLogoutJsBytes() ([]byte, error) {
+	return _staticJsLogoutJs, nil
+}
+
+func staticJsLogoutJs() (*asset, error) {
+	bytes, err := staticJsLogoutJsBytes()
+	if err != nil {
+		return nil, err
+	}
+
+	info := bindataFileInfo{name: "static/js/logout.js", size: 1013, mode: os.FileMode(436), modTime: time.Unix(1518958930, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -197,7 +265,7 @@ func staticJsPopper1110MinJs() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "static/js/popper-1.11.0.min.js", size: 19033, mode: os.FileMode(420), modTime: time.Unix(1509120962, 0)}
+	info := bindataFileInfo{name: "static/js/popper-1.11.0.min.js", size: 19033, mode: os.FileMode(436), modTime: time.Unix(1510996183, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -962,7 +1030,7 @@ func staticJsU2fApiJs() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "static/js/u2f-api.js", size: 20880, mode: os.FileMode(420), modTime: time.Unix(1509120962, 0)}
+	info := bindataFileInfo{name: "static/js/u2f-api.js", size: 20880, mode: os.FileMode(436), modTime: time.Unix(1510996183, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -1031,7 +1099,7 @@ func staticJsU2fJs() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "static/js/u2f.js", size: 1281, mode: os.FileMode(420), modTime: time.Unix(1509260310, 0)}
+	info := bindataFileInfo{name: "static/js/u2f.js", size: 1281, mode: os.FileMode(436), modTime: time.Unix(1510996183, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -1069,7 +1137,7 @@ func templatesLogin_otpHtml() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "templates/login_otp.html", size: 529, mode: os.FileMode(420), modTime: time.Unix(1509218738, 0)}
+	info := bindataFileInfo{name: "templates/login_otp.html", size: 529, mode: os.FileMode(436), modTime: time.Unix(1510996183, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -1129,7 +1197,7 @@ func templatesLogin_passwordHtml() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "templates/login_password.html", size: 1074, mode: os.FileMode(420), modTime: time.Unix(1509218731, 0)}
+	info := bindataFileInfo{name: "templates/login_password.html", size: 1074, mode: os.FileMode(436), modTime: time.Unix(1510996183, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -1169,7 +1237,7 @@ func templatesLogin_u2fHtml() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "templates/login_u2f.html", size: 498, mode: os.FileMode(420), modTime: time.Unix(1509260387, 0)}
+	info := bindataFileInfo{name: "templates/login_u2f.html", size: 498, mode: os.FileMode(436), modTime: time.Unix(1510996183, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -1180,18 +1248,36 @@ var _templatesLogoutHtml = []byte(`{{template "header" .}}
     <div class="form-signin">
       <h1 class="form-signin-heading>">Sign Out</h1>
 
+      <noscript>
+        <p>
+          It seems that Javascript is disabled.
+        </p>
+
+        <p>
+          We can try to log you out using third-party cookies, but if
+          you have a privacy extension that disables that, you will
+          need to <b>QUIT YOUR BROWSER COMPLETELY</b> to sign yourself
+          out of the current session!
+        </p>
+      </noscript>
+      
       <p>
         Signing you out from all services...
       </p>
 
       <ul>
-        {{range .Services}}
+        {{range $i, $svc := .Services}}
         <li>
-          <img src="{{.URL}}" class="logout-img"> {{.Name}}
+          <noscript>
+            <img src="{{$svc.URL}}">
+          </noscript>
+          <div class="logout-status" id="status_{{$i}}">...</div> {{$svc.Name}}
         </li>
         {{end}}
       </ul>
-      
+
+      <div id="#services" data_values="[{{range $i, $svc := .Services}}{{if gt $i 0}},{{end}}{%22idx%22:{{$i}},%22name%22:%22{{$svc.Name}}%22,%22url%22:%22{{$svc.URL}}%22}{{end}}]"></div>
+
     </div>
 {{else}}
     <form class="form-signin" action="/logout" method="post">
@@ -1227,7 +1313,7 @@ func templatesLogoutHtml() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "templates/logout.html", size: 820, mode: os.FileMode(420), modTime: time.Unix(1511166680, 0)}
+	info := bindataFileInfo{name: "templates/logout.html", size: 1503, mode: os.FileMode(436), modTime: time.Unix(1518959579, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -1239,7 +1325,7 @@ var _templatesPageHtml = []byte(`{{define "header"}}<!DOCTYPE html>
     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
     {{if .U2FSignRequest}}<meta name="u2f_request" value="{{json .U2FSignRequest}}">{{end}}
     <link rel="stylesheet" href="/static/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M">
-    <link rel="stylesheet" href="/static/css/signin.css" integrity="sha384-cd2kbbMX+cYhUlp/Xc7Mu9yBphBGNEvZpeIltWsgUMlkt1kNO3hytQQeTglDcMF/">
+    <link rel="stylesheet" href="/static/css/signin.css" integrity="sha384-Qj/laxKROb+o3N4XlayJF2LOuybTRxjeOP+DEeYYEwQUiVtNjaMdgnPbN5ffI/Ub">
     <title>Sign In</title>
   </head>
 
@@ -1256,6 +1342,9 @@ var _templatesPageHtml = []byte(`{{define "header"}}<!DOCTYPE html>
 {{if .U2FSignRequest}}
     <script type="text/javascript" src="/static/js/u2f-api.js" integrity="sha384-9ChevE6pp8ArGK03HgolnFjZbF3webZQtYkwcabzbcI28Lx1/2x2j2fbaAWD4cgR"></script>
     <script type="text/javascript" src="/static/js/u2f.js" integrity="sha384-vd6lytRvVm189G5gr34wlOvN672vVBceTZqV+lTSeec0DBLc0GlWLyKDHc6mrIZS"></script>
+{{end}}
+{{if .IncludeLogoutScripts}}
+    <script type="text/javascript" src="/static/js/logout.js" integrity="sha384-kRP1MtnGgO2BwMmtNODaqnREJyxzsGanV92uEKCf54ilYWxtHSNNYuw3YiCQ+ElP"></script>
 {{end}}
   </body>
 </html>
@@ -1272,7 +1361,7 @@ func templatesPageHtml() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "templates/page.html", size: 1493, mode: os.FileMode(420), modTime: time.Unix(1511337830, 0)}
+	info := bindataFileInfo{name: "templates/page.html", size: 1686, mode: os.FileMode(436), modTime: time.Unix(1518959588, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -1333,6 +1422,7 @@ var _bindata = map[string]func() (*asset, error){
 	"static/css/signin.css": staticCssSigninCss,
 	"static/js/bootstrap-4.0.0-beta.min.js": staticJsBootstrap400BetaMinJs,
 	"static/js/jquery-3.2.1.min.js": staticJsJquery321MinJs,
+	"static/js/logout.js": staticJsLogoutJs,
 	"static/js/popper-1.11.0.min.js": staticJsPopper1110MinJs,
 	"static/js/u2f-api.js": staticJsU2fApiJs,
 	"static/js/u2f.js": staticJsU2fJs,
@@ -1391,6 +1481,7 @@ var _bintree = &bintree{nil, map[string]*bintree{
 		"js": &bintree{nil, map[string]*bintree{
 			"bootstrap-4.0.0-beta.min.js": &bintree{staticJsBootstrap400BetaMinJs, map[string]*bintree{}},
 			"jquery-3.2.1.min.js": &bintree{staticJsJquery321MinJs, map[string]*bintree{}},
+			"logout.js": &bintree{staticJsLogoutJs, map[string]*bintree{}},
 			"popper-1.11.0.min.js": &bintree{staticJsPopper1110MinJs, map[string]*bintree{}},
 			"u2f-api.js": &bintree{staticJsU2fApiJs, map[string]*bintree{}},
 			"u2f.js": &bintree{staticJsU2fJs, map[string]*bintree{}},
diff --git a/server/http.go b/server/http.go
index cd4936c..efd91d6 100644
--- a/server/http.go
+++ b/server/http.go
@@ -254,6 +254,7 @@ func (h *Server) handleLogout(w http.ResponseWriter, req *http.Request, session
 	}
 	if req.Method == "POST" {
 		data["IsPOST"] = true
+		data["IncludeLogoutScripts"] = true
 
 		// Clear the local session.
 		httpSession, _ := h.authSessionStore.Get(req, authSessionKey)
diff --git a/server/static/css/signin.css b/server/static/css/signin.css
index 8fc5332..f949ff7 100644
--- a/server/static/css/signin.css
+++ b/server/static/css/signin.css
@@ -40,3 +40,16 @@ body {
   font-weight: bold;
   color: red;
 }
+
+/* logout page */
+.logout-status {
+  font-weight: bold;
+}
+.logout-status-ok {
+  background-color: green;
+  color: white;
+}
+.logout-status-error {
+  background-color: red;
+  color: white;
+}
diff --git a/server/static/js/logout.js b/server/static/js/logout.js
new file mode 100644
index 0000000..ca1bbef
--- /dev/null
+++ b/server/static/js/logout.js
@@ -0,0 +1,37 @@
+var idlogout = {};
+
+idlogout.get_services = function() {
+    return JSON.parse($('#services').attr('data_values'));
+};
+
+idlogout.logout_service = function(service) {
+    var logout_url = service.url + 'sso_logout';
+    console.log('logging out of ' + service.name);
+    $.ajax({
+        type: 'GET',
+        url: logout_url,
+        contentType: 'text/plain',
+        xhrFields: {
+            withCredentials: true
+        },
+        success: function() {
+            $('#status_'+service.idx).class('logout-status-ok').text('OK');
+            console.log('successful logout for ' + service.name);
+        },
+        error: function() {
+            $('#status_'+service.idx).class('logout-status-error').text('ERROR');
+            console.log('error logging out of ' + service.name);
+        }
+    });
+};
+
+idlogout.logout = function() {
+    var services = idlogout.get_services();
+    $.each(services, func(index, arg) {
+        idlogout.logout_service(arg);
+    });
+};
+
+$(function() {
+    idlogout.logout();
+});
diff --git a/server/templates/logout.html b/server/templates/logout.html
index 7a08280..a36c602 100644
--- a/server/templates/logout.html
+++ b/server/templates/logout.html
@@ -4,18 +4,36 @@
     <div class="form-signin">
       <h1 class="form-signin-heading>">Sign Out</h1>
 
+      <noscript>
+        <p>
+          It seems that Javascript is disabled.
+        </p>
+
+        <p>
+          We can try to log you out using third-party cookies, but if
+          you have a privacy extension that disables that, you will
+          need to <b>QUIT YOUR BROWSER COMPLETELY</b> to sign yourself
+          out of the current session!
+        </p>
+      </noscript>
+      
       <p>
         Signing you out from all services...
       </p>
 
       <ul>
-        {{range .Services}}
+        {{range $i, $svc := .Services}}
         <li>
-          <img src="{{.URL}}" class="logout-img"> {{.Name}}
+          <noscript>
+            <img src="{{$svc.URL}}">
+          </noscript>
+          <div class="logout-status" id="status_{{$i}}">...</div> {{$svc.Name}}
         </li>
         {{end}}
       </ul>
-      
+
+      <div id="#services" data_values="[{{range $i, $svc := .Services}}{{if gt $i 0}},{{end}}{%22idx%22:{{$i}},%22name%22:%22{{$svc.Name}}%22,%22url%22:%22{{$svc.URL}}%22}{{end}}]"></div>
+
     </div>
 {{else}}
     <form class="form-signin" action="/logout" method="post">
diff --git a/server/templates/page.html b/server/templates/page.html
index e16b0f6..336b45d 100644
--- a/server/templates/page.html
+++ b/server/templates/page.html
@@ -5,7 +5,7 @@
     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
     {{if .U2FSignRequest}}<meta name="u2f_request" value="{{json .U2FSignRequest}}">{{end}}
     <link rel="stylesheet" href="/static/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M">
-    <link rel="stylesheet" href="/static/css/signin.css" integrity="sha384-cd2kbbMX+cYhUlp/Xc7Mu9yBphBGNEvZpeIltWsgUMlkt1kNO3hytQQeTglDcMF/">
+    <link rel="stylesheet" href="/static/css/signin.css" integrity="sha384-Qj/laxKROb+o3N4XlayJF2LOuybTRxjeOP+DEeYYEwQUiVtNjaMdgnPbN5ffI/Ub">
     <title>Sign In</title>
   </head>
 
@@ -22,6 +22,9 @@
 {{if .U2FSignRequest}}
     <script type="text/javascript" src="/static/js/u2f-api.js" integrity="sha384-9ChevE6pp8ArGK03HgolnFjZbF3webZQtYkwcabzbcI28Lx1/2x2j2fbaAWD4cgR"></script>
     <script type="text/javascript" src="/static/js/u2f.js" integrity="sha384-vd6lytRvVm189G5gr34wlOvN672vVBceTZqV+lTSeec0DBLc0GlWLyKDHc6mrIZS"></script>
+{{end}}
+{{if .IncludeLogoutScripts}}
+    <script type="text/javascript" src="/static/js/logout.js" integrity="sha384-kRP1MtnGgO2BwMmtNODaqnREJyxzsGanV92uEKCf54ilYWxtHSNNYuw3YiCQ+ElP"></script>
 {{end}}
   </body>
 </html>
-- 
GitLab