veyron/services/identity: Adding CSRF protection to revocation interface in
identity server.

* Tokens are embedded into JS on page load.
* Server validates that token against the request's cookie.
* Server also checks that token matches one of the caveatIDs that the user can
  revoke based on a map that is stored in memory.

Future:
* Consider a timeout on entries in the DirectoryStore.
* Ensure that tokens are refreshed frequently enough to ensure that this works.

Change-Id: If7ca60ed9cd2695592cf575657fe406dc29bbf7c
diff --git a/services/identity/googleoauth/handler.go b/services/identity/googleoauth/handler.go
index 96b0709..2ac5641 100644
--- a/services/identity/googleoauth/handler.go
+++ b/services/identity/googleoauth/handler.go
@@ -14,6 +14,7 @@
 	"encoding/base64"
 	"encoding/json"
 	"fmt"
+	"io/ioutil"
 	"net/http"
 	"path"
 	"strings"
@@ -40,6 +41,8 @@
 	// Prefix for the audit log from which data will be sourced.
 	// (auditor.ReadAuditLog).
 	Auditor string
+	// The RevocationManager is used to revoke blessings granted with a revocation caveat.
+	RevocationManager *revocation.RevocationManager
 }
 
 func (a *HandlerArgs) redirectURL() string {
@@ -61,7 +64,8 @@
 // identity and bless it with the Google email address.
 func NewHandler(args HandlerArgs) http.Handler {
 	config := NewOAuthConfig(args.ClientID, args.ClientSecret, args.redirectURL())
-	return &handler{config, util.NewCSRFCop(), args.Auditor}
+	tokenMap := make(map[tokenCaveatIDKey]bool)
+	return &handler{config, util.NewCSRFCop(), args.Auditor, args.RevocationManager, tokenMap}
 }
 
 // NewOAuthConfig returns the oauth.Config required for obtaining just the email address from Google using OAuth 2.0.
@@ -76,10 +80,16 @@
 	}
 }
 
+type tokenCaveatIDKey struct {
+	token, caveatID string
+}
+
 type handler struct {
-	config  *oauth.Config
-	csrfCop *util.CSRFCop
-	auditor string
+	config                   *oauth.Config
+	csrfCop                  *util.CSRFCop
+	auditor                  string
+	revocationManager        *revocation.RevocationManager
+	tokenRevocationCaveatMap map[tokenCaveatIDKey]bool
 }
 
 func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -88,13 +98,15 @@
 		h.auth(w, r)
 	case "oauth2callback":
 		h.callback(w, r)
+	case "revoke":
+		h.revoke(w, r)
 	default:
 		util.HTTPBadRequest(w, r, nil)
 	}
 }
 
 func (h *handler) auth(w http.ResponseWriter, r *http.Request) {
-	csrf, err := h.csrfCop.NewToken(w, r)
+	csrf, err := h.csrfCop.NewToken(w, r, "VeyronHTTPIdentityClientID")
 	if err != nil {
 		vlog.Infof("Failed to create CSRF token[%v] for request %#v", err, r)
 		util.HTTPBadRequest(w, r, fmt.Errorf("Suspected automated request: %v", err))
@@ -104,7 +116,7 @@
 }
 
 func (h *handler) callback(w http.ResponseWriter, r *http.Request) {
-	if err := h.csrfCop.ValidateToken(r.FormValue("state"), r); err != nil {
+	if err := h.csrfCop.ValidateToken(r.FormValue("state"), r, "VeyronHTTPIdentityClientID"); err != nil {
 		vlog.Infof("Invalid CSRF token: %v in request: %#v", err, r)
 		util.HTTPBadRequest(w, r, fmt.Errorf("Suspected request forgery: %v", err))
 		return
@@ -114,6 +126,13 @@
 		util.HTTPBadRequest(w, r, err)
 		return
 	}
+	// Create a new token to protect ensure that the revocation calls are protected.
+	csrf, err := h.csrfCop.NewToken(w, r, "VeyronHTTPIdentityRevocationID")
+	if err != nil {
+		vlog.Infof("Failed to create CSRF token[%v] for request %#v", err, r)
+		util.HTTPBadRequest(w, r, fmt.Errorf("Suspected automated request: %v", err))
+		return
+	}
 	type tmplentry struct {
 		Blessee            security.PublicID
 		Start, End         time.Time
@@ -121,11 +140,12 @@
 		RevocationCaveatID string
 	}
 	tmplargs := struct {
-		Log   chan tmplentry
-		Email string
+		Log              chan tmplentry
+		Email, CSRFToken string
 	}{
-		Log:   make(chan tmplentry),
-		Email: email,
+		Log:       make(chan tmplentry),
+		Email:     email,
+		CSRFToken: csrf,
 	}
 	if entrych, err := auditor.ReadAuditLog(h.auditor, email); err != nil {
 		vlog.Errorf("Unable to read audit log: %v", err)
@@ -151,6 +171,10 @@
 				}
 				if blessEntry.RevocationCaveat != nil {
 					tmplentry.RevocationCaveatID = base64.URLEncoding.EncodeToString([]byte(blessEntry.RevocationCaveat.ID()))
+					// TODO(suharshs): Add a timeout that removes old entries to reduce storage space.
+					// TODO(suharshs): Make this map from CSRFToken to Email address, have
+					// revocation manager have map from caveatID to Email address in DirectoryStore.
+					h.tokenRevocationCaveatMap[tokenCaveatIDKey{csrf, tmplentry.RevocationCaveatID}] = true
 				}
 				// TODO(suharshs): Make the UI depend on where the caveatID exists and if it hasn't been revoked.
 				// Use the revocation manager IsRevoked function.
@@ -165,6 +189,68 @@
 	}
 }
 
+func (h *handler) revoke(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+	const (
+		success = `{"success": "true"}`
+		failure = `{"success": "false"}`
+	)
+	if h.revocationManager == nil {
+		vlog.Infof("no provided revocation manager")
+		w.Write([]byte(failure))
+		return
+	}
+
+	content, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		vlog.Infof("Failed to parse request: %s", err)
+		w.Write([]byte(failure))
+		return
+	}
+	var requestParams struct {
+		CaveatID, CSRFToken string
+	}
+	if err := json.Unmarshal(content, &requestParams); err != nil {
+		vlog.Infof("json.Unmarshal failed : %s", err)
+		w.Write([]byte(failure))
+		return
+	}
+
+	if err := h.validateTokenCaveatID(requestParams.CSRFToken, requestParams.CaveatID, r); err != nil {
+		vlog.Infof("failed to validate token for caveat: %s", err)
+		w.Write([]byte(failure))
+		return
+	}
+
+	decodedCaveatID, err := base64.URLEncoding.DecodeString(requestParams.CaveatID)
+	if err != nil {
+		vlog.Infof("base64 decoding failed: %s", err)
+		w.Write([]byte(failure))
+		return
+	}
+
+	caveatID := security.ThirdPartyCaveatID(string(decodedCaveatID))
+
+	if err := h.revocationManager.Revoke(caveatID); err != nil {
+		vlog.Infof("Revocation failed: %s", err)
+		w.Write([]byte(failure))
+		return
+	}
+
+	w.Write([]byte(success))
+	return
+}
+
+func (h *handler) validateTokenCaveatID(CSRFToken, revocationCaveatID string, r *http.Request) error {
+	if err := h.csrfCop.ValidateToken(CSRFToken, r, "VeyronHTTPIdentityRevocationID"); err != nil {
+		return fmt.Errorf("invalid CSRF token: %v in request: %#v", err, r)
+	}
+	if _, exists := h.tokenRevocationCaveatMap[tokenCaveatIDKey{CSRFToken, revocationCaveatID}]; exists {
+		return nil
+	}
+	return fmt.Errorf("this token has no matching entry for the provided caveat ID")
+}
+
 // ExchangeAuthCodeForEmail exchanges the authorization code (which must
 // have been obtained with scope=email) for an OAuth token and then uses Google's
 // tokeninfo API to extract the email address from that token.
diff --git a/services/identity/googleoauth/template.go b/services/identity/googleoauth/template.go
index 4c03534..be8b525 100644
--- a/services/identity/googleoauth/template.go
+++ b/services/identity/googleoauth/template.go
@@ -41,16 +41,21 @@
 
   // Setup the revoke buttons click events.
   $(".revoke").click(function() {
-    // TODO(suharshs): Implement "authorization" so that just making this post request with the URL doesn't work.
     var revokeButton = $(this);
     $.ajax({
-      url: "/revoke/",
+      url: "/google/revoke",
       type: "POST",
-      data: $(this).val()
+      data: JSON.stringify({
+        "CaveatID": revokeButton.val(),
+        "CSRFToken": "{{.CSRFToken}}"
+      })
     }).done(function(data) {
       // TODO(suharshs): Have a fail message, add a strikethrough on the revoked caveats.
       console.log(data)
       revokeButton.remove()
+    }).fail(function(jqXHR, textStatus){
+      console.log(jqXHR)
+      console.log("The request failed :( :", textStatus)
     });
   });
 });
diff --git a/services/identity/handlers/revoke.go b/services/identity/handlers/revoke.go
deleted file mode 100644
index ff3d5a4..0000000
--- a/services/identity/handlers/revoke.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package handlers
-
-import (
-	"encoding/base64"
-	"fmt"
-	"io/ioutil"
-	"net/http"
-	"veyron/services/identity/revocation"
-
-	"veyron2/security"
-)
-
-// Revoke is an http.Handler implementation that revokes a Veyron PrivateID.
-type Revoke struct {
-	RevocationManager *revocation.RevocationManager
-}
-
-// TODO(suharshs): Move this to the googleoauth handler to enable authorization on this.
-func (h Revoke) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	w.Header().Set("Content-Type", "application/json")
-	const (
-		success = `{"success": "true"}`
-		failure = `{"success": "false"}`
-	)
-	// Get the caveat string from the request.
-	// TODO(suharshs): Send a multi part form value from the client to server and parse it here.
-	content, err := ioutil.ReadAll(r.Body)
-	if err != nil {
-		fmt.Printf("Failed to parse request: %s", err)
-		w.Write([]byte(failure))
-		return
-	}
-	decoded_caveatID, err := base64.URLEncoding.DecodeString(string(content))
-	if err != nil {
-		fmt.Printf("base64 decoding failed: %s", err)
-		w.Write([]byte(failure))
-		return
-	}
-
-	caveatID := security.ThirdPartyCaveatID(string(decoded_caveatID))
-
-	if err := h.RevocationManager.Revoke(caveatID); err != nil {
-		fmt.Printf("Revocation failed: %s", err)
-		w.Write([]byte(failure))
-		return
-	}
-
-	w.Write([]byte(success))
-	return
-}
diff --git a/services/identity/identityd/main.go b/services/identity/identityd/main.go
index 68cebb2..20c40af 100644
--- a/services/identity/identityd/main.go
+++ b/services/identity/identityd/main.go
@@ -75,9 +75,6 @@
 		http.Handle("/random/", handlers.Random{r}) // mint identities with a random name
 	}
 	http.HandleFunc("/bless/", handlers.Bless) // use a provided PrivateID to bless a provided PublicID
-	if revocationManager != nil {
-		http.Handle("/revoke/", handlers.Revoke{revocationManager}) // revoke an identity that was provided by the server.
-	}
 
 	// Google OAuth
 	ipcServer, ipcServerEP, err := setupGoogleBlessingDischargingServer(r, revocationManager)
@@ -90,10 +87,11 @@
 	if enabled, clientID, clientSecret := enableGoogleOAuth(*googleConfigWeb); enabled && len(*auditprefix) > 0 {
 		n := "/google/"
 		http.Handle(n, googleoauth.NewHandler(googleoauth.HandlerArgs{
-			Addr:         fmt.Sprintf("%s%s", httpaddress(), n),
-			ClientID:     clientID,
-			ClientSecret: clientSecret,
-			Auditor:      *auditprefix,
+			Addr:              fmt.Sprintf("%s%s", httpaddress(), n),
+			ClientID:          clientID,
+			ClientSecret:      clientSecret,
+			Auditor:           *auditprefix,
+			RevocationManager: revocationManager,
 		}))
 	}
 	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
diff --git a/services/identity/revocation/revocation_manager.go b/services/identity/revocation/revocation_manager.go
index e37dc90..b988696 100644
--- a/services/identity/revocation/revocation_manager.go
+++ b/services/identity/revocation/revocation_manager.go
@@ -54,8 +54,7 @@
 func (r *RevocationManager) IsRevoked(caveatID security.ThirdPartyCaveatID) bool {
 	token, err := r.caveatMap.Get(hex.EncodeToString([]byte(caveatID)))
 	if err == nil {
-		exists, _ := revocationMap.Exists(token)
-		return exists
+		return revocationMap.Exists(token)
 	}
 	return false
 }
@@ -69,11 +68,7 @@
 		return fmt.Errorf("missing call to NewRevocationManager")
 	}
 	revocationLock.RUnlock()
-	exists, err := revocationMap.Exists(hex.EncodeToString(cav[:]))
-	if err != nil {
-		return err
-	}
-	if exists {
+	if revocationMap.Exists(hex.EncodeToString(cav[:])) {
 		return fmt.Errorf("revoked")
 	}
 	return nil
diff --git a/services/identity/util/csrf.go b/services/identity/util/csrf.go
index ee7666f..516cf84 100644
--- a/services/identity/util/csrf.go
+++ b/services/identity/util/csrf.go
@@ -13,11 +13,13 @@
 	"veyron2/vlog"
 )
 
+const cookieLen = 16
+
 type CSRFCop struct{}
 
 func NewCSRFCop() *CSRFCop { return new(CSRFCop) }
 
-func (*CSRFCop) maybeSetCookie(w http.ResponseWriter, req *http.Request) ([]byte, error) {
+func (*CSRFCop) maybeSetCookie(w http.ResponseWriter, req *http.Request, cookieName string) ([]byte, error) {
 	cookie, err := req.Cookie(cookieName)
 	switch err {
 	case nil:
@@ -30,7 +32,7 @@
 	default:
 		vlog.Infof("Error decoding cookie %q in request: %v. Regenerating one.", cookieName, err)
 	}
-	cookie, v := newCookie()
+	cookie, v := newCookie(cookieName)
 	if cookie == nil || v == nil {
 		return nil, fmt.Errorf("failed to create cookie")
 	}
@@ -40,8 +42,8 @@
 
 // NewToken creates an anti-cross-site-request-forgery, aka CSRF aka XSRF token.
 // It returns an error if the token could not be created.
-func (c *CSRFCop) NewToken(w http.ResponseWriter, r *http.Request) (string, error) {
-	cookie, err := c.maybeSetCookie(w, r)
+func (c *CSRFCop) NewToken(w http.ResponseWriter, r *http.Request, cookieName string) (string, error) {
+	cookie, err := c.maybeSetCookie(w, r, cookieName)
 	if err != nil {
 		return "", fmt.Errorf("bad cookie: %v", err)
 	}
@@ -57,7 +59,7 @@
 // otherwise.
 // The returned error should not be shown to end-users, it is meant for
 // consumption of the server process only.
-func (c *CSRFCop) ValidateToken(token string, req *http.Request) error {
+func (c *CSRFCop) ValidateToken(token string, req *http.Request, cookieName string) error {
 	cookie, err := req.Cookie(cookieName)
 	if err != nil {
 		return err
@@ -83,12 +85,7 @@
 	Error   error
 }
 
-const (
-	cookieName = "VeyronHTTPIdentityClientID"
-	cookieLen  = 16
-)
-
-func newCookie() (*http.Cookie, []byte) {
+func newCookie(cookieName string) (*http.Cookie, []byte) {
 	b := make([]byte, cookieLen)
 	if n, err := rand.Read(b); n != cookieLen || err != nil {
 		vlog.Errorf("newCookie failed: Read %d random bytes, wanted %d. err: %v", n, cookieLen, err)
diff --git a/services/identity/util/directory_store.go b/services/identity/util/directory_store.go
index ef722d5..7c93638 100644
--- a/services/identity/util/directory_store.go
+++ b/services/identity/util/directory_store.go
@@ -13,9 +13,9 @@
 	dir string
 }
 
-func (s DirectoryStore) Exists(key string) (bool, error) {
+func (s DirectoryStore) Exists(key string) bool {
 	_, err := os.Stat(s.pathName(key))
-	return !os.IsNotExist(err), nil
+	return !os.IsNotExist(err)
 }
 
 func (s DirectoryStore) Put(key, value string) error {