cmd/principal,services/identity: Propagate cancellation and errors
from identity server to principal tool.

This change does the following:
(1) Clicking the cancel button at the addcaveats page sends a cancel
    error to the identity server.
(2) All errors encountered after a toolRedirectURL is decoded are
    propagated to that url.
(3) If the principal tool recieves a identity server error, it will
    not attempt to seek a blessings.

This ensure that cancelling a auth flow doesn't leave the principal
tool hanging, and clearly tells the user that they were successful
in their cancellation.

address vanadium/issues#492

Change-Id: Ia142161bcd85bf5d84971e64d7d645ff98d49bfb
diff --git a/cmd/principal/bless.go b/cmd/principal/bless.go
index 80db361..ee0ef1a 100644
--- a/cmd/principal/bless.go
+++ b/cmd/principal/bless.go
@@ -96,16 +96,25 @@
 			}
 		}()
 
-		toolState := r.FormValue("state")
-		if toolState != state {
+		defer close(result)
+		if r.FormValue("state") != state {
 			tmplArgs.ErrShort = "Unexpected request"
 			tmplArgs.ErrLong = "Mismatched state parameter. Possible cross-site-request-forgery?"
 			return
 		}
+		if errEnc := r.FormValue("error"); errEnc != "" {
+			tmplArgs.ErrShort = "Failed to get blessings"
+			errBytes, err := base64.URLEncoding.DecodeString(errEnc)
+			if err != nil {
+				tmplArgs.ErrLong = err.Error()
+			} else {
+				tmplArgs.ErrLong = string(errBytes)
+			}
+			return
+		}
 		result <- r.FormValue("macaroon")
 		result <- r.FormValue("object_name")
 		result <- r.FormValue("root_key")
-		defer close(result)
 		blessed, ok := <-blessedChan
 		if !ok {
 			tmplArgs.ErrShort = "No blessings received"
diff --git a/services/identity/internal/caveats/browser_caveat_selector.go b/services/identity/internal/caveats/browser_caveat_selector.go
index cbb0400..09746a1 100644
--- a/services/identity/internal/caveats/browser_caveat_selector.go
+++ b/services/identity/internal/caveats/browser_caveat_selector.go
@@ -15,6 +15,8 @@
 	"v.io/v23/security"
 )
 
+var ErrSeekblessingsCancelled = fmt.Errorf("seekblessings has been cancelled")
+
 type browserCaveatSelector struct {
 	assetsPrefix string
 }
@@ -37,11 +39,15 @@
 }
 
 func (s *browserCaveatSelector) ParseSelections(r *http.Request) (caveats []CaveatInfo, state string, additionalExtension string, err error) {
+	state = r.FormValue("macaroon")
+	if r.FormValue("cancelled") == "true" {
+		err = ErrSeekblessingsCancelled
+		return
+	}
+	additionalExtension = r.FormValue("blessingExtension")
 	if caveats, err = s.caveats(r); err != nil {
 		return
 	}
-	state = r.FormValue("macaroon")
-	additionalExtension = r.FormValue("blessingExtension")
 	return
 }
 
diff --git a/services/identity/internal/oauth/handler.go b/services/identity/internal/oauth/handler.go
index b9d1892..97e962c 100644
--- a/services/identity/internal/oauth/handler.go
+++ b/services/identity/internal/oauth/handler.go
@@ -393,7 +393,8 @@
 func (h *handler) sendMacaroon(w http.ResponseWriter, r *http.Request) {
 	var inputMacaroon addCaveatsMacaroon
 	caveatInfos, macaroonString, blessingExtension, err := h.args.CaveatSelector.ParseSelections(r)
-	if err != nil {
+	cancelled := err == caveats.ErrSeekblessingsCancelled
+	if !cancelled && err != nil {
 		util.HTTPBadRequest(w, r, fmt.Errorf("failed to parse blessing information: %v", err))
 		return
 	}
@@ -401,10 +402,19 @@
 		util.HTTPBadRequest(w, r, fmt.Errorf("suspected request forgery: %v", err))
 		return
 	}
-
+	// Construct the url to send back to the tool.
+	baseURL, err := validLoopbackURL(inputMacaroon.ToolRedirectURL)
+	if err != nil {
+		util.HTTPBadRequest(w, r, fmt.Errorf("invalid ToolRedirectURL: %v", err))
+		return
+	}
+	// Now that we have a valid tool redirect url, we can send the errors to the tool.
+	if cancelled {
+		h.sendErrorToTool(w, r, inputMacaroon.ToolState, baseURL, caveats.ErrSeekblessingsCancelled)
+	}
 	caveats, err := h.caveats(caveatInfos)
 	if err != nil {
-		util.HTTPBadRequest(w, r, fmt.Errorf("failed to create caveats: %v", err))
+		h.sendErrorToTool(w, r, inputMacaroon.ToolState, baseURL, fmt.Errorf("failed to create caveats: %v", err))
 		return
 	}
 	parts := []string{inputMacaroon.Email}
@@ -412,7 +422,7 @@
 		parts = append(parts, blessingExtension)
 	}
 	if len(caveats) == 0 {
-		util.HTTPBadRequest(w, r, fmt.Errorf("server disallows attempts to bless with no caveats"))
+		h.sendErrorToTool(w, r, inputMacaroon.ToolState, baseURL, fmt.Errorf("server disallows attempts to bless with no caveats"))
 		return
 	}
 	m := BlessingMacaroon{
@@ -422,18 +432,12 @@
 	}
 	macBytes, err := vom.Encode(m)
 	if err != nil {
-		util.HTTPServerError(w, fmt.Errorf("failed to encode BlessingsMacaroon: %v", err))
-		return
-	}
-	// Construct the url to send back to the tool.
-	baseURL, err := validLoopbackURL(inputMacaroon.ToolRedirectURL)
-	if err != nil {
-		util.HTTPBadRequest(w, r, fmt.Errorf("invalid ToolRedirectURL: %v", err))
+		h.sendErrorToTool(w, r, inputMacaroon.ToolState, baseURL, fmt.Errorf("failed to encode BlessingsMacaroon: %v", err))
 		return
 	}
 	marshalKey, err := h.args.Principal.PublicKey().MarshalBinary()
 	if err != nil {
-		util.HTTPServerError(w, fmt.Errorf("failed to marshal public key: %v", err))
+		h.sendErrorToTool(w, r, inputMacaroon.ToolState, baseURL, fmt.Errorf("failed to marshal public key: %v", err))
 		return
 	}
 	encKey := base64.URLEncoding.EncodeToString(marshalKey)
@@ -446,6 +450,15 @@
 	http.Redirect(w, r, baseURL.String(), http.StatusFound)
 }
 
+func (h *handler) sendErrorToTool(w http.ResponseWriter, r *http.Request, toolState string, baseURL *url.URL, err error) {
+	errEnc := base64.URLEncoding.EncodeToString([]byte(err.Error()))
+	params := url.Values{}
+	params.Add("error", errEnc)
+	params.Add("state", toolState)
+	baseURL.RawQuery = params.Encode()
+	http.Redirect(w, r, baseURL.String(), http.StatusFound)
+}
+
 func (h *handler) caveats(caveatInfos []caveats.CaveatInfo) (cavs []security.Caveat, err error) {
 	caveatFactories := caveats.NewCaveatFactory()
 	for _, caveatInfo := range caveatInfos {
diff --git a/services/identity/internal/templates/caveats.go b/services/identity/internal/templates/caveats.go
index eef93ad..79baa0c 100644
--- a/services/identity/internal/templates/caveats.go
+++ b/services/identity/internal/templates/caveats.go
@@ -108,8 +108,9 @@
       </div>
 
       <div class="action-buttons">
+        <input type="text" class="hidden" name="cancelled" id="cancelled" value="false">
         <button class="button-tertiary" id="cancel" type="button">Cancel</button>
-        <button class="button-primary" type="submit">Bless</button>
+        <button class="button-primary" type="submit" id="bless">Bless</button>
       </div>
 
       <p class="disclaimer-text">
@@ -194,11 +195,11 @@
       $(this).replaceWith('<button type="button" class="button-passive right removeCaveat hidden">Remove</button>');
     });
 
-    // Upon clicking submit, caveats that have not been added yet should be removed,
+    // Upon clicking bless, caveats that have not been added yet should be removed,
     // before they are sent to the server.
-    $('#caveats-form').submit(function(){
+    $('#bless').click(function(){
       $('.addCaveat').parents('.caveatRow').remove();
-      return true;
+      $("#caveats-form").submit();
     });
 
     // Upon clicking the 'Remove Caveat' button, the caveat row should be removed.
@@ -231,7 +232,8 @@
 
     // Activate the cancel button.
     $('#cancel').click(function(){
-      window.close();
+      $("#cancelled").val("true");
+      $("#caveats-form").submit();
     });
   });
   </script>