From 89814397af9cdc3a358fbd878f3a8edd30285a30 Mon Sep 17 00:00:00 2001
From: ale <ale@incal.net>
Date: Sat, 8 Feb 2020 11:34:19 +0000
Subject: [PATCH] Use the json function to encode logout service metadata

Add tests for correct encoding, and improve the logout Javascript code
with a final status message warning the user if there were any errors.
---
 server/bindata.go            | 47 ++++++++++++++++++++++++++++++------
 server/http.go               |  6 +----
 server/http_test.go          | 26 +++++++++++++++++++-
 server/sri_map.go            |  2 +-
 server/static/js/logout.js   | 31 +++++++++++++++++++++---
 server/templates/logout.html | 12 ++++++++-
 6 files changed, 105 insertions(+), 19 deletions(-)

diff --git a/server/bindata.go b/server/bindata.go
index d7a1cd7..2ca4977 100644
--- a/server/bindata.go
+++ b/server/bindata.go
@@ -221,7 +221,7 @@ idlogout.get_services = function() {
     return JSON.parse($('#services').attr('data-services'));
 };
 
-idlogout.logout_service = function(idx, service) {
+idlogout.logout_service = function(idx, service, successCallback, errorCallback) {
     console.log('logging out of ' + service.name);
     $.ajax({
         type: 'GET',
@@ -233,24 +233,47 @@ idlogout.logout_service = function(idx, service) {
         success: function() {
             $('#status_'+idx).addClass('logout-status-ok').text('OK');
             console.log('successful logout for ' + service.name);
+            successCallback(service);
         },
         error: function() {
             $('#status_'+idx).addClass('logout-status-error').text('ERROR');
             console.log('error logging out of ' + service.name);
+            errorCallback(service);
         }
     });
 };
 
-idlogout.logout = function() {
+idlogout.logout = function(doneCallback) {
     var services = idlogout.get_services();
+    var remaining = services.length;
+    var ok = 0, errors = 0;
+    var maybeDone = function() {
+        remaining--;
+        if (remaining <= 0) {
+            doneCallback(ok, errors);
+        }
+    };
     $.each(services, function(index, arg) {
-        idlogout.logout_service(index, arg);
+        idlogout.logout_service(index, arg, function(svc) {
+            ok++;
+            maybeDone();
+        }, function(svc) {
+            errors++;
+            maybeDone();
+        });
     });
 };
 
 $(function() {
     $('.logout-status').show();
-    idlogout.logout();
+    idlogout.logout(function(ok, errors) {
+        if (errors > 0) {
+            console.log('there were errors in the logout process, we have reached an unsafe state');
+            $('#logout_err').show();
+        } else {
+            $('#logout_ok').show();
+        }
+    });
 });
 `)
 
@@ -264,7 +287,7 @@ func staticJsLogoutJs() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "static/js/logout.js", size: 1005, mode: os.FileMode(420), modTime: time.Unix(1535013418, 0)}
+	info := bindataFileInfo{name: "static/js/logout.js", size: 1726, mode: os.FileMode(420), modTime: time.Unix(1581161609, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -1380,7 +1403,17 @@ var _templatesLogoutHtml = []byte(`{{define "title"}}Sign Out{{end}}
           {{end}}
       </tbody></table>
 
-      <div id="services" data-services="{{.ServicesJSON}}"></div>
+      <div id="services" data-services="{{json .Services}}"></div>
+
+      <p id="logout_ok" class="hidden">
+        You have been successfully logged out from all services.
+      </p>
+
+      <p id="logout_err" class="hidden">
+        There were some errors in the logout process.
+        <b>You must quit the browser</b> to avoid leaving open
+        sessions around!
+      </p>
 
     </div>
 
@@ -1403,7 +1436,7 @@ func templatesLogoutHtml() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "templates/logout.html", size: 1192, mode: os.FileMode(420), modTime: time.Unix(1581160289, 0)}
+	info := bindataFileInfo{name: "templates/logout.html", size: 1505, mode: os.FileMode(420), modTime: time.Unix(1581161411, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
diff --git a/server/http.go b/server/http.go
index 53334e7..1e156c2 100644
--- a/server/http.go
+++ b/server/http.go
@@ -357,12 +357,8 @@ func (h *Server) handleLogout(w http.ResponseWriter, req *http.Request) {
 			URL:  serviceLogoutCallback(svc),
 		})
 	}
-
-	svcJSON, _ := json.Marshal(svcs) // nolint
 	data := map[string]interface{}{
-		"Services":             svcs,
-		"ServicesJSON":         string(svcJSON),
-		"IncludeLogoutScripts": true,
+		"Services": svcs,
 	}
 
 	// Close the keystore.
diff --git a/server/http_test.go b/server/http_test.go
index 64e266e..3c932a5 100644
--- a/server/http_test.go
+++ b/server/http_test.go
@@ -7,6 +7,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"html"
 	"io"
 	"io/ioutil"
 	"net"
@@ -271,6 +272,8 @@ func checkAuthFailure(t testing.TB, resp *http.Response) {
 	}
 }
 
+var logoutServicesRx = regexp.MustCompile(`<div id="services" data-services="([^"]*)">`)
+
 func checkLogoutPage(t testing.TB, resp *http.Response) {
 	if resp.Request.URL.Path != "/logout" {
 		t.Errorf("request path is not /logout (%s)", resp.Request.URL.String())
@@ -281,9 +284,30 @@ func checkLogoutPage(t testing.TB, resp *http.Response) {
 		t.Fatalf("reading body: %v", err)
 	}
 
-	if s := string(data); !strings.Contains(s, "Signing you out from all services") {
+	s := string(data)
+	// Check signature string of logout page.
+	if !strings.Contains(s, "Signing you out from all services") {
 		t.Fatalf("not the logout page:\n%s", s)
 	}
+	// Check presence of fallback service logout URL.
+	if !strings.Contains(s, "<img src=\"https://service.example.com/sso_logout\"") {
+		t.Fatalf("logout page does not contain fallback service logout URL:\n%s", s)
+	}
+	// Parse the JSON in the services div.
+	m := logoutServicesRx.FindStringSubmatch(s)
+	if len(m) == 0 {
+		t.Fatalf("logout page does not contain JSON-encoded services:\n%s", s)
+	}
+	var svcs []logoutServiceInfo
+	if err := json.Unmarshal([]byte(html.UnescapeString(m[1])), &svcs); err != nil {
+		t.Fatalf("error decoding JSON services: %v", err)
+	}
+	if len(svcs) != 1 {
+		t.Fatalf("expected 1 service, got %d: %v", len(svcs), svcs)
+	}
+	if svcs[0].URL != "https://service.example.com/sso_logout" {
+		t.Fatalf("bad service logout URL: %s", svcs[0].URL)
+	}
 }
 
 func extractSSOTicket(dest *string) func(testing.TB, *http.Response) {
diff --git a/server/sri_map.go b/server/sri_map.go
index 5e43bc5..4149e69 100644
--- a/server/sri_map.go
+++ b/server/sri_map.go
@@ -5,7 +5,7 @@ var sriMap = map[string]string{
 	"/static/css/signin.css": "sha384-Eg6R7EvngjwfifE75qnyhwjgbAuSpVLDEWoYuNWpF2Yn7QsqKCvxpFsR5YCrzV5d",
 	"/static/js/bootstrap-4.1.3.min.js": "sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy",
 	"/static/js/jquery-3.3.1.min.js": "sha384-tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT",
-	"/static/js/logout.js": "sha384-lChVngGLNFXetIJTSxc+scDpi1vsBL+7Xa4r2uZpQFP/6Y2z9eCDXe/Y4IUdklRD",
+	"/static/js/logout.js": "sha384-XfIFkQSluN5TZzrogxlZGauRX7e9Do42s6Y/Cx6RX9qcKtgw0FGSbEpPbCqQbFnb",
 	"/static/js/popper-1.14.3.min.js": "sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49",
 	"/static/js/u2f-api.js": "sha384-9ChevE6pp8ArGK03HgolnFjZbF3webZQtYkwcabzbcI28Lx1/2x2j2fbaAWD4cgR",
 	"/static/js/u2f.js": "sha384-7zZy25ajTABErGlCQgcyRDpQDS9QVZv9o+95IfvCjWftQe20f411F1a39Ge5xmCe",
diff --git a/server/static/js/logout.js b/server/static/js/logout.js
index b3b8a4a..e465487 100644
--- a/server/static/js/logout.js
+++ b/server/static/js/logout.js
@@ -4,7 +4,7 @@ idlogout.get_services = function() {
     return JSON.parse($('#services').attr('data-services'));
 };
 
-idlogout.logout_service = function(idx, service) {
+idlogout.logout_service = function(idx, service, successCallback, errorCallback) {
     console.log('logging out of ' + service.name);
     $.ajax({
         type: 'GET',
@@ -16,22 +16,45 @@ idlogout.logout_service = function(idx, service) {
         success: function() {
             $('#status_'+idx).addClass('logout-status-ok').text('OK');
             console.log('successful logout for ' + service.name);
+            successCallback(service);
         },
         error: function() {
             $('#status_'+idx).addClass('logout-status-error').text('ERROR');
             console.log('error logging out of ' + service.name);
+            errorCallback(service);
         }
     });
 };
 
-idlogout.logout = function() {
+idlogout.logout = function(doneCallback) {
     var services = idlogout.get_services();
+    var remaining = services.length;
+    var ok = 0, errors = 0;
+    var maybeDone = function() {
+        remaining--;
+        if (remaining <= 0) {
+            doneCallback(ok, errors);
+        }
+    };
     $.each(services, function(index, arg) {
-        idlogout.logout_service(index, arg);
+        idlogout.logout_service(index, arg, function(svc) {
+            ok++;
+            maybeDone();
+        }, function(svc) {
+            errors++;
+            maybeDone();
+        });
     });
 };
 
 $(function() {
     $('.logout-status').show();
-    idlogout.logout();
+    idlogout.logout(function(ok, errors) {
+        if (errors > 0) {
+            console.log('there were errors in the logout process, we have reached an unsafe state');
+            $('#logout_err').show();
+        } else {
+            $('#logout_ok').show();
+        }
+    });
 });
diff --git a/server/templates/logout.html b/server/templates/logout.html
index 066e40e..3af52f2 100644
--- a/server/templates/logout.html
+++ b/server/templates/logout.html
@@ -38,7 +38,17 @@
           {{end}}
       </tbody></table>
 
-      <div id="services" data-services="{{.ServicesJSON}}"></div>
+      <div id="services" data-services="{{json .Services}}"></div>
+
+      <p id="logout_ok" class="hidden">
+        You have been successfully logged out from all services.
+      </p>
+
+      <p id="logout_err" class="hidden">
+        There were some errors in the logout process.
+        <b>You must quit the browser</b> to avoid leaving open
+        sessions around!
+      </p>
 
     </div>
 
-- 
GitLab