veyron/services/identity: Create Test Identity Server and
integration test for identity server.

-Factored out CaveatSelector from identityServer, now almost
 all components of the identity server are modular and uncoupled.

Change-Id: I61059fe9704d70c42b63242c81900c82135f3941
diff --git a/services/identity/auditor/blessing_auditor_test.go b/services/identity/auditor/blessing_auditor_test.go
index 7970ae3..9e53889 100644
--- a/services/identity/auditor/blessing_auditor_test.go
+++ b/services/identity/auditor/blessing_auditor_test.go
@@ -11,8 +11,7 @@
 )
 
 func TestBlessingAuditor(t *testing.T) {
-	db := &mockDatabase{}
-	auditor, reader := &blessingAuditor{db}, &blessingLogReader{db}
+	auditor, reader := NewMockBlessingAuditor()
 
 	p, err := vsecurity.NewPrincipal()
 	if err != nil {
@@ -87,23 +86,6 @@
 	}
 }
 
-type mockDatabase struct {
-	NextEntry databaseEntry
-}
-
-func (db *mockDatabase) Insert(entry databaseEntry) error {
-	db.NextEntry = entry
-	return nil
-}
-func (db *mockDatabase) Query(email string) <-chan databaseEntry {
-	c := make(chan databaseEntry)
-	go func() {
-		c <- db.NextEntry
-		close(c)
-	}()
-	return c
-}
-
 func newThirdPartyCaveat(t *testing.T, p security.Principal) security.ThirdPartyCaveat {
 	tp, err := security.NewPublicKeyCaveat(p.PublicKey(), "location", security.ThirdPartyRequirements{}, newCaveat(security.MethodCaveat("method")))
 	if err != nil {
diff --git a/services/identity/auditor/mock_auditor.go b/services/identity/auditor/mock_auditor.go
new file mode 100644
index 0000000..6fe6c72
--- /dev/null
+++ b/services/identity/auditor/mock_auditor.go
@@ -0,0 +1,32 @@
+package auditor
+
+import (
+	"reflect"
+	"veyron.io/veyron/veyron/security/audit"
+)
+
+func NewMockBlessingAuditor() (audit.Auditor, BlessingLogReader) {
+	db := &mockDatabase{}
+	return &blessingAuditor{db}, &blessingLogReader{db}
+}
+
+type mockDatabase struct {
+	NextEntry databaseEntry
+}
+
+func (db *mockDatabase) Insert(entry databaseEntry) error {
+	db.NextEntry = entry
+	return nil
+}
+
+func (db *mockDatabase) Query(email string) <-chan databaseEntry {
+	c := make(chan databaseEntry)
+	go func() {
+		var empty databaseEntry
+		if !reflect.DeepEqual(db.NextEntry, empty) {
+			c <- db.NextEntry
+		}
+		close(c)
+	}()
+	return c
+}
diff --git a/services/identity/caveats/browser_caveat_selector.go b/services/identity/caveats/browser_caveat_selector.go
new file mode 100644
index 0000000..cb2ab4f
--- /dev/null
+++ b/services/identity/caveats/browser_caveat_selector.go
@@ -0,0 +1,226 @@
+package caveats
+
+import (
+	"fmt"
+	"html/template"
+	"net/http"
+	"strings"
+	"time"
+)
+
+type browserCaveatSelector struct{}
+
+// NewBrowserCaveatSelector returns a caveat selector that renders a form in the
+// to accept user caveat selections.
+func NewBrowserCaveatSelector() CaveatSelector {
+	return &browserCaveatSelector{}
+}
+
+func (s *browserCaveatSelector) Render(blessingExtension, state, redirectURL string, w http.ResponseWriter, r *http.Request) error {
+	tmplargs := struct {
+		Extension             string
+		CaveatList            []string
+		Macaroon, MacaroonURL string
+	}{blessingExtension, []string{"ExpiryCaveat", "MethodCaveat"}, state, redirectURL}
+	w.Header().Set("Context-Type", "text/html")
+	if err := tmplSelectCaveats.Execute(w, tmplargs); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (s *browserCaveatSelector) ParseSelections(r *http.Request) (caveats []CaveatInfo, state string, additionalExtension string, err error) {
+	if caveats, err = s.caveats(r); err != nil {
+		return
+	}
+	state = r.FormValue("macaroon")
+	additionalExtension = r.FormValue("blessingExtension")
+	return
+}
+
+func (s *browserCaveatSelector) caveats(r *http.Request) ([]CaveatInfo, error) {
+	if err := r.ParseForm(); err != nil {
+		return nil, err
+	}
+	var caveats []CaveatInfo
+	// Fill in the required caveat.
+	switch required := r.FormValue("requiredCaveat"); required {
+	case "Expiry":
+		expiry, err := newExpiryCaveatInfo(r.FormValue("expiry"), r.FormValue("timezoneOffset"))
+		if err != nil {
+			return nil, fmt.Errorf("failed to create ExpiryCaveat: %v", err)
+		}
+		caveats = append(caveats, expiry)
+	case "Revocation":
+		revocation := newRevocationCaveatInfo()
+		caveats = append(caveats, revocation)
+	default:
+		return nil, fmt.Errorf("%q is not a valid required caveat", required)
+	}
+	if len(caveats) != 1 {
+		return nil, fmt.Errorf("server does not allow for un-restricted blessings")
+	}
+
+	// And find any additional ones
+	for i, cavName := range r.Form["caveat"] {
+		var err error
+		var caveat CaveatInfo
+		switch cavName {
+		case "ExpiryCaveat":
+			if caveat, err = newExpiryCaveatInfo(r.Form[cavName][i], r.FormValue("timezoneOffset")); err != nil {
+				return nil, fmt.Errorf("unable to create caveat %s: %v", cavName, err)
+			}
+		case "MethodCaveat":
+			if caveat, err = newMethodCaveatInfo(strings.Split(r.Form[cavName][i], ",")); err != nil {
+				return nil, fmt.Errorf("unable to create caveat %s: %v", cavName, err)
+			}
+		case "none":
+			continue
+		default:
+			return nil, fmt.Errorf("unable to create caveat %s: caveat does not exist", cavName)
+		}
+		caveats = append(caveats, caveat)
+	}
+	return caveats, nil
+}
+
+func newExpiryCaveatInfo(timestamp, utcOffset string) (CaveatInfo, error) {
+	var empty CaveatInfo
+	t, err := time.Parse("2006-01-02T15:04", timestamp)
+	if err != nil {
+		return empty, fmt.Errorf("parseTime failed: %v", err)
+	}
+	// utcOffset is returned as minutes from JS, so we need to parse it to a duration.
+	offset, err := time.ParseDuration(utcOffset + "m")
+	if err != nil {
+		return empty, fmt.Errorf("failed to parse duration: %v", err)
+	}
+	return CaveatInfo{"Expiry", []interface{}{t.Add(offset)}}, nil
+}
+
+func newMethodCaveatInfo(methods []string) (CaveatInfo, error) {
+	if len(methods) < 1 {
+		return CaveatInfo{}, fmt.Errorf("must pass at least one method")
+	}
+	var ifaces []interface{}
+	for _, m := range methods {
+		ifaces = append(ifaces, m)
+	}
+	return CaveatInfo{"Method", ifaces}, nil
+}
+
+func newRevocationCaveatInfo() CaveatInfo {
+	return CaveatInfo{Type: "Revocation"}
+}
+
+var tmplSelectCaveats = template.Must(template.New("bless").Parse(`<!doctype html>
+<html>
+<head>
+<meta charset="UTF-8">
+<title>Blessings: Select caveats</title>
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
+<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/css/toastr.min.css">
+<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.7.0/moment.min.js"></script>
+<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
+<script src="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js"></script>
+<script>
+  // TODO(suharshs): Move this and other JS/CSS to an assets directory in identity server.
+  $(document).ready(function() {
+    $('.caveatInput').hide(); // Hide all the inputs at start.
+
+    // When a caveat selector changes show the corresponding input box.
+    $('body').on('change', '.caveats', function (){
+      // Grab the div encapsulating the select and the corresponding inputs.
+      var caveatSelector = $(this).parents(".caveatRow");
+      // Hide the visible inputs and show the selected one.
+      caveatSelector.find('.caveatInput').hide();
+      caveatSelector.find('#'+$(this).val()).show();
+    });
+
+    // Upon clicking the '+' button a new caveat selector should appear.
+    $('body').on('click', '.addCaveat', function() {
+      var selector = $(this).parents(".caveatRow");
+      var newSelector = selector.clone();
+      // Hide all inputs since nothing is selected in this clone.
+      newSelector.find('.caveatInput').hide();
+      selector.after(newSelector);
+      // Change the '+' button to a '-' button.
+      $(this).replaceWith('<button type="button" class="btn btn-danger btn-sm removeCaveat">-</button>')
+    });
+
+    // Upon clicking the '-' button caveats should be removed.
+    $('body').on('click', '.removeCaveat', function() {
+      $(this).parents('.caveatRow').remove();
+    });
+
+    // Get the timezoneOffset for the server to create a correct expiry caveat.
+    // The offset is the minutes between UTC and local time.
+    var d = new Date();
+    $('#timezoneOffset').val(d.getTimezoneOffset());
+
+    // Set the datetime picker to have a default value of one day from now.
+    var m = moment().add(1, 'd').format("YYYY-MM-DDTHH:MM")
+    $('#expiry').val(m);
+    $('#ExpiryCaveat').val(m);
+  });
+</script>
+</head>
+<body class="container">
+<form class="form-horizontal" method="POST" id="caveats-form" name="input" action="{{.MacaroonURL}}" role="form">
+<h2 class="form-signin-heading">{{.Extension}}</h2>
+<input type="text" class="hidden" name="macaroon" value="{{.Macaroon}}">
+<div class="form-group form-group-lg">
+  <label class="col-sm-2" for="blessing-extension">Extension</label>
+  <div class="col-sm-10">
+  <input name="blessingExtension" type="text" class="form-control" id="blessing-extension" placeholder="(optional) name of the device/application for which the blessing is being sought, e.g. homelaptop">
+  <input type="text" class="hidden" id="timezoneOffset" name="timezoneOffset">
+  </div>
+</div>
+<div class="form-group form-group-lg">
+  <label class="col-sm-2" for="required-caveat">Expiration</label>
+  <div class="col-sm-10" class="input-group" name="required-caveat">
+    <div class="radio">
+      <label>
+      <input type="radio" name="requiredCaveat" id="requiredCaveat" value="Revocation" checked>
+      When explicitly revoked
+      </label>
+    </div>
+    <div class="radio">
+      <div class="input-group">
+        <input type="radio" name="requiredCaveat" id="requiredCaveat" value="Expiry">
+        <input type="datetime-local" id="expiry" name="expiry">
+      </div>
+    </div>
+  </div>
+</div>
+<h4 class="form-signin-heading">Additional caveats</h4>
+<span class="help-text">Optional additional restrictions on the use of the blessing</span>
+<div class="caveatRow row">
+  <div class="col-md-4">
+    <select name="caveat" class="form-control caveats">
+      <option value="none" selected="selected">Select a caveat.</option>
+      {{ $caveatList := .CaveatList }}
+      {{range $index, $name := $caveatList}}
+      <option name="{{$name}}" value="{{$name}}">{{$name}}</option>
+      {{end}}
+    </select>
+  </div>
+  <div class="col-md-7">
+    {{range $index, $name := $caveatList}}
+      {{if eq $name "ExpiryCaveat"}}
+      <input type="datetime-local" class="form-control caveatInput" id="{{$name}}" name="{{$name}}">
+      {{else if eq $name "MethodCaveat"}}
+      <input type="text" id="{{$name}}" class="form-control caveatInput" name="{{$name}}" placeholder="comma-separated method list">
+      {{end}}
+    {{end}}
+  </div>
+  <div class="col-md-1">
+    <button type="button" class="btn btn-info btn-sm addCaveat">+</button>
+  </div>
+</div>
+<br/>
+<button class="btn btn-lg btn-primary btn-block" type="submit">Bless</button>
+</form>
+</body>
+</html>`))
diff --git a/services/identity/caveats/caveat_factory.go b/services/identity/caveats/caveat_factory.go
new file mode 100644
index 0000000..3797ca7
--- /dev/null
+++ b/services/identity/caveats/caveat_factory.go
@@ -0,0 +1,92 @@
+package caveats
+
+import (
+	"fmt"
+	"time"
+
+	"veyron.io/veyron/veyron/services/identity/revocation"
+
+	"veyron.io/veyron/veyron2/security"
+)
+
+type CaveatFactory interface {
+	New(caveatInfo CaveatInfo) (security.Caveat, error)
+}
+
+type CaveatInfo struct {
+	Type string
+	Args []interface{}
+}
+
+type caveatFactory map[string]func(args ...interface{}) (security.Caveat, error)
+
+func NewCaveatFactory() CaveatFactory {
+	return caveatFactory{
+		"Expiry":     expiryCaveat,
+		"Method":     methodCaveat,
+		"Revocation": revocationCaveat,
+	}
+}
+
+func (c caveatFactory) New(caveatInfo CaveatInfo) (security.Caveat, error) {
+	fact, exists := c[caveatInfo.Type]
+	if !exists {
+		return security.Caveat{}, fmt.Errorf("caveat %s does not exist in CaveatFactory", caveatInfo.Type)
+	}
+	return fact(caveatInfo.Args...)
+}
+
+func expiryCaveat(args ...interface{}) (security.Caveat, error) {
+	var empty security.Caveat
+	if len(args) != 1 {
+		return empty, fmt.Errorf("expiry caveat: must input exactly one time argument")
+	}
+	t, ok := args[0].(time.Time)
+	if !ok {
+		return empty, fmt.Errorf("expiry caveat: received arg of type %T, expected time.Time", args[0])
+	}
+	return security.ExpiryCaveat(t)
+}
+
+func methodCaveat(args ...interface{}) (security.Caveat, error) {
+	var empty security.Caveat
+	if len(args) < 1 {
+		return empty, fmt.Errorf("method caveat requires at least one argument")
+	}
+	methods, err := interfacesToStrings(args)
+	if err != nil {
+		return empty, fmt.Errorf("method caveat: %v", err)
+	}
+	return security.MethodCaveat(methods[0], methods[1:]...)
+}
+
+func interfacesToStrings(args []interface{}) (s []string, err error) {
+	for _, arg := range args {
+		a, ok := arg.(string)
+		if !ok {
+			return nil, fmt.Errorf("received arg of type %T, expected string", arg)
+		}
+		s = append(s, a)
+	}
+	return s, nil
+}
+
+func revocationCaveat(args ...interface{}) (security.Caveat, error) {
+	var empty security.Caveat
+	if len(args) != 3 {
+		return empty, fmt.Errorf("revocation caveat: must input a revocation manager, publickey, and discharge location")
+	}
+	revocationManager, ok := args[0].(revocation.RevocationManager)
+	if !ok {
+		return empty, fmt.Errorf("revocation caveat: received args of type %T, expected revocation.RevocationManager", args[0])
+	}
+	publicKey, ok := args[1].(security.PublicKey)
+	if !ok {
+		return empty, fmt.Errorf("revocation caveat: received args of type %T, expected security.PublicKey", args[1])
+	}
+	dischargerLocation, ok := args[2].(string)
+	if !ok {
+		return empty, fmt.Errorf("revocation caveat: received args of type %T, expected string", args[2])
+	}
+	return revocationManager.NewCaveat(publicKey, dischargerLocation)
+}
diff --git a/services/identity/caveats/caveat_selector.go b/services/identity/caveats/caveat_selector.go
new file mode 100644
index 0000000..8876a55
--- /dev/null
+++ b/services/identity/caveats/caveat_selector.go
@@ -0,0 +1,19 @@
+package caveats
+
+import (
+	"net/http"
+)
+
+// CaveatSelector is used to render a web page where the user can select caveats
+// to be added to a blessing being granted
+type CaveatSelector interface {
+	// Render renders the caveat input form. When the user has completed inputing caveats,
+	// Render should redirect to the specified redirect route.
+	// blessingExtension is the extension used for the blessings that is being caveated.
+	// state is any state passed by the caller (e.g., for CSRF mitigation) and is returned by ParseSelections.
+	// redirectRoute is the route to be returned to.
+	Render(blessingExtension, state, redirectURL string, w http.ResponseWriter, r *http.Request) error
+	// ParseSelections parse the users choices of Caveats, and returns the information needed to create them,
+	// the state passed to Render, and any additionalExtension selected by the user to further extend the blessing.
+	ParseSelections(r *http.Request) (caveats []CaveatInfo, state string, additionalExtension string, err error)
+}
diff --git a/services/identity/caveats/mock_caveat_selector.go b/services/identity/caveats/mock_caveat_selector.go
new file mode 100644
index 0000000..6c20637
--- /dev/null
+++ b/services/identity/caveats/mock_caveat_selector.go
@@ -0,0 +1,35 @@
+package caveats
+
+import (
+	"net/http"
+	"time"
+)
+
+type mockCaveatSelector struct {
+	state string
+}
+
+// NewMockCaveatSelector returns a CaveatSelector that always returns a default set
+// of caveats: [exprity caveat with a 1h expiry, revocation caveat, and a method caveat
+// for methods "methodA" and "methodB"] and the additional extension: "test-extension"
+// This selector is only meant to be used during testing.
+func NewMockCaveatSelector() CaveatSelector {
+	return &mockCaveatSelector{}
+}
+
+func (s *mockCaveatSelector) Render(_, state, redirectURL string, w http.ResponseWriter, r *http.Request) error {
+	s.state = state
+	http.Redirect(w, r, redirectURL, http.StatusFound)
+	return nil
+}
+
+func (s *mockCaveatSelector) ParseSelections(r *http.Request) (caveats []CaveatInfo, state string, additionalExtension string, err error) {
+	caveats = []CaveatInfo{
+		CaveatInfo{"Revocation", []interface{}{}},
+		CaveatInfo{"Expiry", []interface{}{time.Now().Add(time.Hour)}},
+		CaveatInfo{"Method", []interface{}{"methodA", "methodB"}},
+	}
+	state = s.state
+	additionalExtension = "test-extension"
+	return
+}
diff --git a/services/identity/identityd/main.go b/services/identity/identityd/main.go
index c1c9655..c18e7ab 100644
--- a/services/identity/identityd/main.go
+++ b/services/identity/identityd/main.go
@@ -16,6 +16,7 @@
 
 	"veyron.io/veyron/veyron/services/identity/auditor"
 	"veyron.io/veyron/veyron/services/identity/blesser"
+	"veyron.io/veyron/veyron/services/identity/caveats"
 	"veyron.io/veyron/veyron/services/identity/oauth"
 	"veyron.io/veyron/veyron/services/identity/revocation"
 	"veyron.io/veyron/veyron/services/identity/server"
@@ -64,7 +65,13 @@
 		vlog.Fatalf("Failed to start RevocationManager: %v", err)
 	}
 
-	server.NewIdentityServer(googleoauth, auditor, reader, revocationManager, oauthBlesserGoogleParams(revocationManager)).Serve()
+	server.NewIdentityServer(
+		googleoauth,
+		auditor,
+		reader,
+		revocationManager,
+		oauthBlesserGoogleParams(revocationManager),
+		caveats.NewBrowserCaveatSelector()).Serve()
 }
 
 func usage() {
diff --git a/services/identity/identityd_test/main.go b/services/identity/identityd_test/main.go
new file mode 100644
index 0000000..371ff64
--- /dev/null
+++ b/services/identity/identityd_test/main.go
@@ -0,0 +1,58 @@
+// HTTP server that uses OAuth to create security.Blessings objects.
+package main
+
+import (
+	"flag"
+	"fmt"
+	"os"
+	"time"
+
+	"veyron.io/veyron/veyron/services/identity/auditor"
+	"veyron.io/veyron/veyron/services/identity/blesser"
+	"veyron.io/veyron/veyron/services/identity/caveats"
+	"veyron.io/veyron/veyron/services/identity/oauth"
+	"veyron.io/veyron/veyron/services/identity/revocation"
+	"veyron.io/veyron/veyron/services/identity/server"
+)
+
+var (
+	googleDomain = flag.String("google_domain", "", "An optional domain name. When set, only email addresses from this domain are allowed to authenticate via Google OAuth")
+)
+
+func main() {
+	flag.Usage = usage
+	flag.Parse()
+
+	auditor, reader := auditor.NewMockBlessingAuditor()
+	revocationManager := revocation.NewMockRevocationManager()
+
+	server.NewIdentityServer(
+		oauth.NewMockOAuth(),
+		auditor,
+		reader,
+		revocationManager,
+		oauthBlesserGoogleParams(revocationManager),
+		caveats.NewMockCaveatSelector()).Serve()
+}
+
+func usage() {
+	fmt.Fprintf(os.Stderr, `%s starts an test version of the identityd server that
+mocks out oauth, auditing, and revocation.
+
+To generate TLS certificates so the HTTP server can use SSL:
+go run $GOROOT/src/pkg/crypto/tls/generate_cert.go --host <IP address>
+
+Flags:
+`, os.Args[0])
+	flag.PrintDefaults()
+}
+
+func oauthBlesserGoogleParams(revocationManager revocation.RevocationManager) blesser.GoogleParams {
+	googleParams := blesser.GoogleParams{
+		BlessingDuration:  365 * 24 * time.Hour,
+		DomainRestriction: *googleDomain,
+		RevocationManager: revocationManager,
+	}
+	// TODO(suharshs): Figure out the test for this.
+	return googleParams
+}
diff --git a/services/identity/oauth/handler.go b/services/identity/oauth/handler.go
index b85e16d..5ad758a 100644
--- a/services/identity/oauth/handler.go
+++ b/services/identity/oauth/handler.go
@@ -33,6 +33,7 @@
 
 	"veyron.io/veyron/veyron/services/identity/auditor"
 	"veyron.io/veyron/veyron/services/identity/blesser"
+	"veyron.io/veyron/veyron/services/identity/caveats"
 	"veyron.io/veyron/veyron/services/identity/revocation"
 	"veyron.io/veyron/veyron/services/identity/util"
 	"veyron.io/veyron/veyron2"
@@ -76,6 +77,8 @@
 	DomainRestriction string
 	// OAuthProvider is used to authenticate and get a blessee email.
 	OAuthProvider OAuthProvider
+	// CaveatSelector is used to obtain caveats from the user when seeking a blessing.
+	CaveatSelector caveats.CaveatSelector
 }
 
 func redirectURL(baseURL, suffix string) string {
@@ -300,8 +303,6 @@
 	ToolRedirectURL, ToolState, Email string
 }
 
-var caveatList = []string{"ExpiryCaveat", "MethodCaveat"}
-
 func (h *handler) addCaveats(w http.ResponseWriter, r *http.Request) {
 	var inputMacaroon seekBlessingsMacaroon
 	if err := h.csrfCop.ValidateToken(r.FormValue("state"), r, clientIDCookie, &inputMacaroon); err != nil {
@@ -327,34 +328,33 @@
 		util.HTTPServerError(w, fmt.Errorf("failed to create new token: %v", err))
 		return
 	}
-	tmplargs := struct {
-		Extension               string
-		CaveatList              []string
-		Macaroon, MacaroonRoute string
-	}{email, caveatList, outputMacaroon, sendMacaroonRoute}
-	w.Header().Set("Context-Type", "text/html")
-	if err := tmplSelectCaveats.Execute(w, tmplargs); err != nil {
-		vlog.Errorf("Unable to execute bless page template: %v", err)
+	if err := h.args.CaveatSelector.Render(email, outputMacaroon, redirectURL(h.args.Addr, sendMacaroonRoute), w, r); err != nil {
+		vlog.Errorf("Unable to invoke render caveat selector: %v", err)
 		util.HTTPServerError(w, err)
 	}
 }
 
 func (h *handler) sendMacaroon(w http.ResponseWriter, r *http.Request) {
 	var inputMacaroon addCaveatsMacaroon
-	if err := h.csrfCop.ValidateToken(r.FormValue("macaroon"), r, clientIDCookie, &inputMacaroon); err != nil {
-		util.HTTPBadRequest(w, r, fmt.Errorf("Suspected request forgery: %v", err))
+	caveatInfos, macaroonString, blessingExtension, err := h.args.CaveatSelector.ParseSelections(r)
+	if err != nil {
+		util.HTTPBadRequest(w, r, fmt.Errorf("failed to parse blessing information: %v", err))
 		return
 	}
-	blessingExtension := r.FormValue("blessingExtension")
+	if err := h.csrfCop.ValidateToken(macaroonString, r, clientIDCookie, &inputMacaroon); err != nil {
+		util.HTTPBadRequest(w, r, fmt.Errorf("suspected request forgery: %v", err))
+		return
+	}
+
+	caveats, err := h.caveats(caveatInfos)
+	if err != nil {
+		util.HTTPBadRequest(w, r, fmt.Errorf("failed to create caveats: %v", err))
+		return
+	}
 	name := inputMacaroon.Email
 	if len(blessingExtension) > 0 {
 		name = name + security.ChainSeparator + blessingExtension
 	}
-	caveats, err := h.caveats(r)
-	if err != nil {
-		util.HTTPBadRequest(w, r, fmt.Errorf("failed to extract caveats: ", err))
-		return
-	}
 	if len(caveats) == 0 {
 		util.HTTPBadRequest(w, r, fmt.Errorf("server disallows attempts to bless with no caveats"))
 		return
@@ -366,13 +366,13 @@
 		Name:     name,
 	}
 	if err := vom.NewEncoder(buf).Encode(m); err != nil {
-		util.HTTPServerError(w, fmt.Errorf("failed to encode BlessingsMacaroon: ", err))
+		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: ", err))
+		util.HTTPBadRequest(w, r, fmt.Errorf("invalid ToolRedirectURL: %v", err))
 		return
 	}
 	params := url.Values{}
@@ -383,74 +383,17 @@
 	http.Redirect(w, r, baseURL.String(), http.StatusFound)
 }
 
-func (h *handler) caveats(r *http.Request) ([]security.Caveat, error) {
-	if err := r.ParseForm(); err != nil {
-		return nil, err
-	}
-	var caveats []security.Caveat
-	// Fill in the required caveat.
-	switch required := r.FormValue("requiredCaveat"); required {
-	case "Expiry":
-		expiry, err := newExpiryCaveat(r.FormValue("expiry"), r.FormValue("timezoneOffset"))
+func (h *handler) caveats(caveatInfos []caveats.CaveatInfo) (cavs []security.Caveat, err error) {
+	caveatFactories := caveats.NewCaveatFactory()
+	for _, caveatInfo := range caveatInfos {
+		if caveatInfo.Type == "Revocation" {
+			caveatInfo.Args = []interface{}{h.args.RevocationManager, h.args.R.Principal().PublicKey(), h.args.DischargerLocation}
+		}
+		cav, err := caveatFactories.New(caveatInfo)
 		if err != nil {
-			return nil, fmt.Errorf("failed to create ExpiryCaveat: %v", err)
+			return nil, err
 		}
-		caveats = append(caveats, expiry)
-	case "Revocation":
-		if h.args.RevocationManager == nil {
-			return nil, fmt.Errorf("server not configured to support revocation")
-		}
-		revocation, err := h.args.RevocationManager.NewCaveat(h.args.R.Principal().PublicKey(), h.args.DischargerLocation)
-		if err != nil {
-			return nil, fmt.Errorf("failed to create revocation caveat: %v", err)
-		}
-		caveats = append(caveats, revocation)
-	default:
-		return nil, fmt.Errorf("%q is not a valid required caveat", required)
+		cavs = append(cavs, cav)
 	}
-	if len(caveats) != 1 {
-		return nil, fmt.Errorf("server does not allow for un-restricted blessings")
-	}
-
-	// And find any additional ones
-	for i, cavName := range r.Form["caveat"] {
-		var err error
-		var caveat security.Caveat
-		switch cavName {
-		case "ExpiryCaveat":
-			caveat, err = newExpiryCaveat(r.Form[cavName][i], r.FormValue("timezoneOffset"))
-		case "MethodCaveat":
-			caveat, err = newMethodCaveat(strings.Split(r.Form[cavName][i], ","))
-		case "none":
-			continue
-		default:
-			return nil, fmt.Errorf("unable to create caveat %s: caveat does not exist", cavName)
-		}
-		if err != nil {
-			return nil, fmt.Errorf("unable to create caveat %s: %v", cavName, err)
-		}
-		caveats = append(caveats, caveat)
-	}
-	return caveats, nil
-}
-
-func newExpiryCaveat(timestamp, utcOffset string) (security.Caveat, error) {
-	var empty security.Caveat
-	t, err := time.Parse("2006-01-02T15:04", timestamp)
-	if err != nil {
-		return empty, fmt.Errorf("parseTime failed: %v", err)
-	}
-	// utcOffset is returned as minutes from JS, so we need to parse it to a duration.
-	offset, err := time.ParseDuration(utcOffset + "m")
-	if err != nil {
-		return empty, fmt.Errorf("failed to parse duration: %v", err)
-	}
-	return security.ExpiryCaveat(t.Add(offset))
-}
-
-func newMethodCaveat(methods []string) (security.Caveat, error) {
-	if len(methods) < 1 {
-		return security.Caveat{}, fmt.Errorf("must pass at least one method")
-	}
-	return security.MethodCaveat(methods[0], methods[1:]...)
+	return
 }
diff --git a/services/identity/oauth/mockoauth.go b/services/identity/oauth/mockoauth.go
new file mode 100644
index 0000000..8fa1806
--- /dev/null
+++ b/services/identity/oauth/mockoauth.go
@@ -0,0 +1,16 @@
+package oauth
+
+// mockOAuth is a mock OAuthProvider for use in tests.
+type mockOAuth struct{}
+
+func NewMockOAuth() OAuthProvider {
+	return &mockOAuth{}
+}
+
+func (m *mockOAuth) AuthURL(redirectUrl string, state string) string {
+	return redirectUrl + "?state=" + state
+}
+
+func (m *mockOAuth) ExchangeAuthCodeForEmail(authCode string, url string) (email string, err error) {
+	return "testemail@google.com", nil
+}
diff --git a/services/identity/oauth/template.go b/services/identity/oauth/template.go
deleted file mode 100644
index 698635a..0000000
--- a/services/identity/oauth/template.go
+++ /dev/null
@@ -1,230 +0,0 @@
-package oauth
-
-import "html/template"
-
-var tmplViewBlessings = template.Must(template.New("auditor").Parse(`<!doctype html>
-<html>
-<head>
-<meta charset="UTF-8">
-<title>Blessings for {{.Email}}</title>
-<meta name="viewport" content="width=device-width, initial-scale=1.0">
-<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
-<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/css/toastr.min.css">
-<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.7.0/moment.min.js"></script>
-<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
-<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.0/jquery-ui.min.js"></script>
-<script src="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js"></script>
-<script>
-function setTimeText(elem) {
-  var timestamp = elem.data("unixtime");
-  var m = moment(timestamp*1000.0);
-  var style = elem.data("style");
-  if (style === "absolute") {
-    elem.html("<a href='#'>" + m.format("dd, MMM Do YYYY, h:mm:ss a") + "</a>");
-    elem.data("style", "fromNow");
-  } else {
-    elem.html("<a href='#'>" + m.fromNow() + "</a>");
-    elem.data("style", "absolute");
-  }
-}
-
-$(document).ready(function() {
-  $(".unixtime").each(function() {
-    // clicking the timestamp should toggle the display format.
-    $(this).click(function() { setTimeText($(this)); });
-    setTimeText($(this));
-  });
-
-  // Setup the revoke buttons click events.
-  $(".revoke").click(function() {
-    var revokeButton = $(this);
-    $.ajax({
-      url: "/google/{{.RevokeRoute}}",
-      type: "POST",
-      data: JSON.stringify({
-        "Token": revokeButton.val()
-      })
-    }).done(function(data) {
-      if (data.success == "false") {
-        failMessage(revokeButton);
-        return;
-      }
-      revokeButton.replaceWith("<div>Just Revoked!</div>");
-    }).fail(function(xhr, textStatus){
-      failMessage(revokeButton);
-      console.error('Bad request: %s', status, xhr)
-    });
-  });
-});
-
-function failMessage(revokeButton) {
-  revokeButton.parent().parent().fadeIn(function(){
-    $(this).addClass("bg-danger");
-  });
-  toastr.options.closeButton = true;
-  toastr.error('Unable to revoke identity!', 'Error!')
-}
-
-</script>
-</head>
-<body>
-<div class="container">
-<h3>Blessing log for {{.Email}}</h3>
-<table class="table table-bordered table-hover table-responsive">
-<thead>
-  <tr>
-  <th>Blessed as</th>
-  <th>Public Key</th>
-  <th>Issued</th>
-  <th>Caveats</th>
-  <th>Revoked</th>
-  </tr>
-</thead>
-<tbody>
-{{range .Log}}
-  {{if .Error}}
-    <tr class="bg-danger">
-      <td colspan="5">Failed to read audit log: Error: {{.Error}}</td>
-    </tr>
-  {{else}}
-    <tr>
-    <td>{{.Blessed}}</td>
-    <td>{{.Blessed.PublicKey}}</td>
-    <td><div class="unixtime" data-unixtime={{.Timestamp.Unix}}>{{.Timestamp.String}}</div></td>
-    <td>
-    {{range .Caveats}}
-      {{.}}</br>
-    {{end}}
-    </td>
-    <td>
-      {{ if .Token }}
-      <button class="revoke" value="{{.Token}}">Revoke</button>
-      {{ else if not .RevocationTime.IsZero }}
-        <div class="unixtime" data-unixtime={{.RevocationTime.Unix}}>{{.RevocationTime.String}}</div>
-      {{ end }}
-    </td>
-    </tr>
-  {{end}}
-{{else}}
-  <tr>
-  <td colspan=5>No blessings issued</td>
-  </tr>
-{{end}}
-</tbody>
-</table>
-<hr/>
-</div>
-</body>
-</html>`))
-
-var tmplSelectCaveats = template.Must(template.New("bless").Parse(`<!doctype html>
-<html>
-<head>
-<meta charset="UTF-8">
-<title>Blessings: Select caveats</title>
-<meta name="viewport" content="width=device-width, initial-scale=1.0">
-<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
-<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/css/toastr.min.css">
-<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.7.0/moment.min.js"></script>
-<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
-<script src="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js"></script>
-<script>
-  // TODO(suharshs): Move this and other JS/CSS to an assets directory in identity server.
-  $(document).ready(function() {
-    $('.caveatInput').hide(); // Hide all the inputs at start.
-
-    // When a caveat selector changes show the corresponding input box.
-    $('body').on('change', '.caveats', function (){
-      // Grab the div encapsulating the select and the corresponding inputs.
-      var caveatSelector = $(this).parents(".caveatRow");
-      // Hide the visible inputs and show the selected one.
-      caveatSelector.find('.caveatInput').hide();
-      caveatSelector.find('#'+$(this).val()).show();
-    });
-
-    // Upon clicking the '+' button a new caveat selector should appear.
-    $('body').on('click', '.addCaveat', function() {
-      var selector = $(this).parents(".caveatRow");
-      var newSelector = selector.clone();
-      // Hide all inputs since nothing is selected in this clone.
-      newSelector.find('.caveatInput').hide();
-      selector.after(newSelector);
-      // Change the '+' button to a '-' button.
-      $(this).replaceWith('<button type="button" class="btn btn-danger btn-sm removeCaveat">-</button>')
-    });
-
-    // Upon clicking the '-' button caveats should be removed.
-    $('body').on('click', '.removeCaveat', function() {
-      $(this).parents('.caveatRow').remove();
-    });
-
-    // Get the timezoneOffset for the server to create a correct expiry caveat.
-    // The offset is the minutes between UTC and local time.
-    var d = new Date();
-    $('#timezoneOffset').val(d.getTimezoneOffset());
-
-    // Set the datetime picker to have a default value of one day from now.
-    var m = moment().add(1, 'd').format("YYYY-MM-DDTHH:MM")
-    $('#expiry').val(m);
-    $('#ExpiryCaveat').val(m);
-  });
-</script>
-</head>
-<body class="container">
-<form class="form-horizontal" method="POST" id="caveats-form" name="input" action="/google/{{.MacaroonRoute}}" role="form">
-<h2 class="form-signin-heading">{{.Extension}}</h2>
-<input type="text" class="hidden" name="macaroon" value="{{.Macaroon}}">
-<div class="form-group form-group-lg">
-  <label class="col-sm-2" for="blessing-extension">Extension</label>
-  <div class="col-sm-10">
-  <input name="blessingExtension" type="text" class="form-control" id="blessing-extension" placeholder="(optional) name of the device/application for which the blessing is being sought, e.g. homelaptop">
-  <input type="text" class="hidden" id="timezoneOffset" name="timezoneOffset">
-  </div>
-</div>
-<div class="form-group form-group-lg">
-  <label class="col-sm-2" for="required-caveat">Expiration</label>
-  <div class="col-sm-10" class="input-group" name="required-caveat">
-    <div class="radio">
-      <label>
-      <input type="radio" name="requiredCaveat" id="requiredCaveat" value="Revocation" checked>
-      When explicitly revoked
-      </label>
-    </div>
-    <div class="radio">
-      <div class="input-group">
-        <input type="radio" name="requiredCaveat" id="requiredCaveat" value="Expiry">
-        <input type="datetime-local" id="expiry" name="expiry">
-      </div>
-    </div>
-  </div>
-</div>
-<h4 class="form-signin-heading">Additional caveats</h4>
-<span class="help-text">Optional additional restrictions on the use of the blessing</span>
-<div class="caveatRow row">
-  <div class="col-md-4">
-    <select name="caveat" class="form-control caveats">
-      <option value="none" selected="selected">Select a caveat.</option>
-      {{ $caveatList := .CaveatList }}
-      {{range $index, $name := $caveatList}}
-      <option name="{{$name}}" value="{{$name}}">{{$name}}</option>
-      {{end}}
-    </select>
-  </div>
-  <div class="col-md-7">
-    {{range $index, $name := $caveatList}}
-      {{if eq $name "ExpiryCaveat"}}
-      <input type="datetime-local" class="form-control caveatInput" id="{{$name}}" name="{{$name}}">
-      {{else if eq $name "MethodCaveat"}}
-      <input type="text" id="{{$name}}" class="form-control caveatInput" name="{{$name}}" placeholder="comma-separated method list">
-      {{end}}
-    {{end}}
-  </div>
-  <div class="col-md-1">
-    <button type="button" class="btn btn-info btn-sm addCaveat">+</button>
-  </div>
-</div>
-<br/>
-<button class="btn btn-lg btn-primary btn-block" type="submit">Bless</button>
-</form>
-</body>
-</html>`))
diff --git a/services/identity/oauth/view_blessings_template.go b/services/identity/oauth/view_blessings_template.go
new file mode 100644
index 0000000..66334a7
--- /dev/null
+++ b/services/identity/oauth/view_blessings_template.go
@@ -0,0 +1,118 @@
+package oauth
+
+import "html/template"
+
+var tmplViewBlessings = template.Must(template.New("auditor").Parse(`<!doctype html>
+<html>
+<head>
+<meta charset="UTF-8">
+<title>Blessings for {{.Email}}</title>
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
+<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/css/toastr.min.css">
+<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.7.0/moment.min.js"></script>
+<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
+<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.0/jquery-ui.min.js"></script>
+<script src="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js"></script>
+<script>
+function setTimeText(elem) {
+  var timestamp = elem.data("unixtime");
+  var m = moment(timestamp*1000.0);
+  var style = elem.data("style");
+  if (style === "absolute") {
+    elem.html("<a href='#'>" + m.format("dd, MMM Do YYYY, h:mm:ss a") + "</a>");
+    elem.data("style", "fromNow");
+  } else {
+    elem.html("<a href='#'>" + m.fromNow() + "</a>");
+    elem.data("style", "absolute");
+  }
+}
+
+$(document).ready(function() {
+  $(".unixtime").each(function() {
+    // clicking the timestamp should toggle the display format.
+    $(this).click(function() { setTimeText($(this)); });
+    setTimeText($(this));
+  });
+
+  // Setup the revoke buttons click events.
+  $(".revoke").click(function() {
+    var revokeButton = $(this);
+    $.ajax({
+      url: "/google/{{.RevokeRoute}}",
+      type: "POST",
+      data: JSON.stringify({
+        "Token": revokeButton.val()
+      })
+    }).done(function(data) {
+      if (data.success == "false") {
+        failMessage(revokeButton);
+        return;
+      }
+      revokeButton.replaceWith("<div>Just Revoked!</div>");
+    }).fail(function(xhr, textStatus){
+      failMessage(revokeButton);
+      console.error('Bad request: %s', status, xhr)
+    });
+  });
+});
+
+function failMessage(revokeButton) {
+  revokeButton.parent().parent().fadeIn(function(){
+    $(this).addClass("bg-danger");
+  });
+  toastr.options.closeButton = true;
+  toastr.error('Unable to revoke identity!', 'Error!')
+}
+
+</script>
+</head>
+<body>
+<div class="container">
+<h3>Blessing log for {{.Email}}</h3>
+<table class="table table-bordered table-hover table-responsive">
+<thead>
+  <tr>
+  <th>Blessed as</th>
+  <th>Public Key</th>
+  <th>Issued</th>
+  <th>Caveats</th>
+  <th>Revoked</th>
+  </tr>
+</thead>
+<tbody>
+{{range .Log}}
+  {{if .Error}}
+    <tr class="bg-danger">
+      <td colspan="5">Failed to read audit log: Error: {{.Error}}</td>
+    </tr>
+  {{else}}
+    <tr>
+    <td>{{.Blessed}}</td>
+    <td>{{.Blessed.PublicKey}}</td>
+    <td><div class="unixtime" data-unixtime={{.Timestamp.Unix}}>{{.Timestamp.String}}</div></td>
+    <td>
+    {{range .Caveats}}
+      {{.}}</br>
+    {{end}}
+    </td>
+    <td>
+      {{ if .Token }}
+      <button class="revoke" value="{{.Token}}">Revoke</button>
+      {{ else if not .RevocationTime.IsZero }}
+        <div class="unixtime" data-unixtime={{.RevocationTime.Unix}}>{{.RevocationTime.String}}</div>
+      {{ end }}
+    </td>
+    </tr>
+  {{end}}
+{{else}}
+  <tr>
+  <td colspan=5>No blessings issued</td>
+  </tr>
+{{end}}
+</tbody>
+</table>
+<hr/>
+</div>
+</body>
+</html>`))
diff --git a/services/identity/revocation/mock_revocation_manager.go b/services/identity/revocation/mock_revocation_manager.go
new file mode 100644
index 0000000..560a553
--- /dev/null
+++ b/services/identity/revocation/mock_revocation_manager.go
@@ -0,0 +1,35 @@
+package revocation
+
+import (
+	"time"
+)
+
+func NewMockRevocationManager() RevocationManager {
+	revocationDB = &mockDatabase{make(map[string][]byte), make(map[string]*time.Time)}
+	return &revocationManager{}
+}
+
+type mockDatabase struct {
+	tpCavIDToRevCavID   map[string][]byte
+	revCavIDToTimestamp map[string]*time.Time
+}
+
+func (m *mockDatabase) InsertCaveat(thirdPartyCaveatID string, revocationCaveatID []byte) error {
+	m.tpCavIDToRevCavID[thirdPartyCaveatID] = revocationCaveatID
+	return nil
+}
+
+func (m *mockDatabase) Revoke(thirdPartyCaveatID string) error {
+	timestamp := time.Now()
+	m.revCavIDToTimestamp[string(m.tpCavIDToRevCavID[thirdPartyCaveatID])] = &timestamp
+	return nil
+}
+
+func (m *mockDatabase) IsRevoked(revocationCaveatID []byte) (bool, error) {
+	_, exists := m.revCavIDToTimestamp[string(revocationCaveatID)]
+	return exists, nil
+}
+
+func (m *mockDatabase) RevocationTime(thirdPartyCaveatID string) (*time.Time, error) {
+	return m.revCavIDToTimestamp[string(m.tpCavIDToRevCavID[thirdPartyCaveatID])], nil
+}
diff --git a/services/identity/revocation/revocation_test.go b/services/identity/revocation/revocation_test.go
index 09034c9..147882c 100644
--- a/services/identity/revocation/revocation_test.go
+++ b/services/identity/revocation/revocation_test.go
@@ -3,7 +3,6 @@
 import (
 	"bytes"
 	"testing"
-	"time"
 
 	"veyron.io/veyron/veyron2"
 	"veyron.io/veyron/veyron2/naming"
@@ -16,38 +15,8 @@
 	"veyron.io/veyron/veyron/services/security/discharger"
 )
 
-type mockDatabase struct {
-	tpCavIDToRevCavID   map[string][]byte
-	revCavIDToTimestamp map[string]*time.Time
-}
-
-func (m *mockDatabase) InsertCaveat(thirdPartyCaveatID string, revocationCaveatID []byte) error {
-	m.tpCavIDToRevCavID[thirdPartyCaveatID] = revocationCaveatID
-	return nil
-}
-
-func (m *mockDatabase) Revoke(thirdPartyCaveatID string) error {
-	timestamp := time.Now()
-	m.revCavIDToTimestamp[string(m.tpCavIDToRevCavID[thirdPartyCaveatID])] = &timestamp
-	return nil
-}
-
-func (m *mockDatabase) IsRevoked(revocationCaveatID []byte) (bool, error) {
-	_, exists := m.revCavIDToTimestamp[string(revocationCaveatID)]
-	return exists, nil
-}
-
-func (m *mockDatabase) RevocationTime(thirdPartyCaveatID string) (*time.Time, error) {
-	return m.revCavIDToTimestamp[string(m.tpCavIDToRevCavID[thirdPartyCaveatID])], nil
-}
-
-func newRevocationManager(t *testing.T) RevocationManager {
-	revocationDB = &mockDatabase{make(map[string][]byte), make(map[string]*time.Time)}
-	return &revocationManager{}
-}
-
 func revokerSetup(t *testing.T, r veyron2.Runtime) (dischargerKey security.PublicKey, dischargerEndpoint string, revoker RevocationManager, closeFunc func(), runtime veyron2.Runtime) {
-	revokerService := newRevocationManager(t)
+	revokerService := NewMockRevocationManager()
 	dischargerServer, err := r.NewServer()
 	if err != nil {
 		t.Fatalf("r.NewServer: %s", err)
diff --git a/services/identity/server/identityd.go b/services/identity/server/identityd.go
index 6e767f4..399364e 100644
--- a/services/identity/server/identityd.go
+++ b/services/identity/server/identityd.go
@@ -25,6 +25,7 @@
 	"veyron.io/veyron/veyron/security/audit"
 	"veyron.io/veyron/veyron/services/identity/auditor"
 	"veyron.io/veyron/veyron/services/identity/blesser"
+	"veyron.io/veyron/veyron/services/identity/caveats"
 	"veyron.io/veyron/veyron/services/identity/handlers"
 	"veyron.io/veyron/veyron/services/identity/oauth"
 	"veyron.io/veyron/veyron/services/identity/revocation"
@@ -53,6 +54,7 @@
 	blessingLogReader  auditor.BlessingLogReader
 	revocationManager  revocation.RevocationManager
 	oauthBlesserParams blesser.GoogleParams
+	caveatSelector     caveats.CaveatSelector
 }
 
 // NewIdentityServer returns a IdentityServer that:
@@ -60,13 +62,14 @@
 // - auditor and blessingLogReader to audit the root principal and read audit logs
 // - revocationManager to store revocation data and grant discharges
 // - oauthBlesserParams to configure the identity.OAuthBlesser service
-func NewIdentityServer(oauthProvider oauth.OAuthProvider, auditor audit.Auditor, blessingLogReader auditor.BlessingLogReader, revocationManager revocation.RevocationManager, oauthBlesserParams blesser.GoogleParams) *identityd {
+func NewIdentityServer(oauthProvider oauth.OAuthProvider, auditor audit.Auditor, blessingLogReader auditor.BlessingLogReader, revocationManager revocation.RevocationManager, oauthBlesserParams blesser.GoogleParams, caveatSelector caveats.CaveatSelector) *identityd {
 	return &identityd{
 		oauthProvider,
 		auditor,
 		blessingLogReader,
 		revocationManager,
 		oauthBlesserParams,
+		caveatSelector,
 	}
 }
 
@@ -103,6 +106,7 @@
 		DischargerLocation:      naming.JoinAddressName(published[0], dischargerService),
 		MacaroonBlessingService: naming.JoinAddressName(published[0], macaroonService),
 		OAuthProvider:           s.oauthProvider,
+		CaveatSelector:          s.caveatSelector,
 	})
 	if err != nil {
 		vlog.Fatalf("Failed to create HTTP handler for oauth authentication: %v", err)
diff --git a/services/identity/test.sh b/services/identity/test.sh
new file mode 100755
index 0000000..6a20639
--- /dev/null
+++ b/services/identity/test.sh
@@ -0,0 +1,81 @@
+#!/bin/bash
+
+# Test that tests the routes of the identityd server.
+
+source "$(go list -f {{.Dir}} veyron.io/veyron/shell/lib)/shell_test.sh"
+
+readonly WORKDIR="${shell_test_WORK_DIR}"
+
+build() {
+  IDENTITYD_BIN="$(shell_test::build_go_binary 'veyron.io/veyron/veyron/services/identity/identityd_test')"
+  PRINCIPAL_BIN="$(shell_test::build_go_binary 'veyron.io/veyron/veyron/tools/principal')"
+}
+
+# These certificatese were created with "generate_cert.go  --host=localhost --duration=87600h --ecdsa-curve=P256"
+CERT="-----BEGIN CERTIFICATE-----
+MIIBbTCCARSgAwIBAgIRANKYmC0v3pK+VohyJOdD1hgwCgYIKoZIzj0EAwIwEjEQ
+MA4GA1UEChMHQWNtZSBDbzAeFw0xNDExMjEyMjEwNTJaFw0yNDExMTgyMjEwNTJa
+MBIxEDAOBgNVBAoTB0FjbWUgQ28wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASv
+heWcWcZT7d5Sm/uoWhBUJJPBSREN4qGzBV7yFYUFvHJ9mNaEcopo/6BopJRbvUmj
+CQMVDZVMm5Er/f8HgCngo0swSTAOBgNVHQ8BAf8EBAMCAKAwEwYDVR0lBAwwCgYI
+KwYBBQUHAwEwDAYDVR0TAQH/BAIwADAUBgNVHREEDTALgglsb2NhbGhvc3QwCgYI
+KoZIzj0EAwIDRwAwRAIgAkwh+mi5YlIxYzxzT7bQj/ZYU5pufxHt+F+a75gbm7AC
+IAI9+axCPawySY+UYvjO14hklsyy3LnSf1mNHyeGydMM
+-----END CERTIFICATE-----"
+
+KEY="-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIHxiR6vjOn1jF1KS0V//pXrulxss9PwUgV/7/QVeV2zCoAoGCCqGSM49
+AwEHoUQDQgAEr4XlnFnGU+3eUpv7qFoQVCSTwUkRDeKhswVe8hWFBbxyfZjWhHKK
+aP+gaKSUW71JowkDFQ2VTJuRK/3/B4Ap4A==
+-----END EC PRIVATE KEY-----"
+
+# runprincipal starts the principal tool, extracts the url and curls it, to avoid the
+# dependence the principal tool has on a browser.
+runprincipal() {
+  local PFILE="${WORKDIR}/principalfile"
+  # Start the tool in the background.
+  "${PRINCIPAL_BIN}"  seekblessings --browser=false --from=https://localhost:8125/google -v=3 2> "${PFILE}" &
+  sleep 2
+  # Search for the url and run it.
+  cat "${PFILE}" | grep https |
+  while read url; do
+    RESULT=$(curl -L --insecure -c ${WORKDIR}/cookiejar $url);
+    # Clear out the file
+    echo $RESULT;
+    break;
+  done;
+  rm "${PFILE}";
+}
+
+main() {
+  cd "${WORKDIR}"
+  build
+
+  # Setup the certificate files.
+  echo "${CERT}" > "${WORKDIR}/cert.pem"
+  echo "${KEY}" > "${WORKDIR}/key.pem"
+
+  shell_test::setup_server_test || shell_test::fail "line ${LINENO} failed to setup server test"
+  unset VEYRON_CREDENTIALS
+
+  # Start the identityd server in test identity server.
+  shell_test::start_server "${IDENTITYD_BIN}" --host=localhost --tlsconfig="${WORKDIR}/cert.pem,${WORKDIR}/key.pem" -veyron.tcp.address=127.0.0.1:0
+  echo Identityd Log File: $START_SERVER_LOG_FILE
+  export VEYRON_CREDENTIALS="$(shell::tmp_dir)"
+
+  # Test an initial seekblessings call, with a specified VEYRON_CREDENTIALS.
+  WANT="Received blessings"
+  GOT=$(runprincipal)
+  if [[ ! "${GOT}" =~ "${WANT}" ]]; then
+    shell_test::fail "line ${LINENO} failed first seekblessings call"
+  fi
+  # Test that a subsequent call succeed with the same credentials. This means that the blessings and principal from the first call works correctly.
+  GOT=$(runprincipal)
+  if [[ ! "${GOT}" =~ "${WANT}" ]]; then
+    shell_test::fail "line ${LINENO} failed second seekblessings call"
+  fi
+
+  shell_test::pass
+}
+
+main "$@"
\ No newline at end of file
diff --git a/tools/principal/bless.go b/tools/principal/bless.go
index d009b69..be171b2 100644
--- a/tools/principal/bless.go
+++ b/tools/principal/bless.go
@@ -16,7 +16,7 @@
 	"veyron.io/veyron/veyron2/vlog"
 )
 
-func getMacaroonForBlessRPC(blessServerURL string, blessedChan <-chan string) (<-chan string, error) {
+func getMacaroonForBlessRPC(blessServerURL string, blessedChan <-chan string, browser bool) (<-chan string, error) {
 	// Setup a HTTP server to recieve a blessing macaroon from the identity server.
 	// Steps:
 	// 1. Generate a state token to be included in the HTTP request
@@ -85,7 +85,7 @@
 	// Use exec.Command().Start instead of exec.Command().Run since there is no
 	// need to wait for the command to return (and indeed on some window managers,
 	// the command will not exit until the browser is closed).
-	if len(openCommand) != 0 {
+	if len(openCommand) != 0 && browser {
 		exec.Command(openCommand, url).Start()
 	}
 	return result, nil
diff --git a/tools/principal/main.go b/tools/principal/main.go
index 432ef60..3f9ed67 100644
--- a/tools/principal/main.go
+++ b/tools/principal/main.go
@@ -42,6 +42,7 @@
 	flagSeekBlessingsFrom       string
 	flagSeekBlessingsSetDefault bool
 	flagSeekBlessingsForPeer    string
+	flagSeekBlessingsBrowser    bool
 
 	// Flags common to many commands
 	flagAddToRoots      bool
@@ -593,7 +594,7 @@
 
 			blessedChan := make(chan string)
 			defer close(blessedChan)
-			macaroonChan, err := getMacaroonForBlessRPC(flagSeekBlessingsFrom, blessedChan)
+			macaroonChan, err := getMacaroonForBlessRPC(flagSeekBlessingsFrom, blessedChan, flagSeekBlessingsBrowser)
 			if err != nil {
 				return fmt.Errorf("failed to get macaroon from Veyron blesser: %v", err)
 			}
@@ -729,6 +730,7 @@
 	cmdSeekBlessings.Flags.StringVar(&flagSeekBlessingsFrom, "from", "https://auth.dev.v.io:8125/google", "URL to use to begin the seek blessings process")
 	cmdSeekBlessings.Flags.BoolVar(&flagSeekBlessingsSetDefault, "set_default", true, "If true, the blessings obtained will be set as the default blessing in the store")
 	cmdSeekBlessings.Flags.StringVar(&flagSeekBlessingsForPeer, "for_peer", string(security.AllPrincipals), "If non-empty, the blessings obtained will be marked for peers matching this pattern in the store")
+	cmdSeekBlessings.Flags.BoolVar(&flagSeekBlessingsBrowser, "browser", true, "If false, the seekblessings command will not open the browser and only print the url to visit.")
 	cmdSeekBlessings.Flags.BoolVar(&flagAddToRoots, "add_to_roots", true, "If true, the root certificate of the blessing will be added to the principal's set of recognized root certificates")
 
 	cmdStoreSet.Flags.BoolVar(&flagAddToRoots, "add_to_roots", true, "If true, the root certificate of the blessing will be added to the principal's set of recognized root certificates")