Merge "services/device/internal/impl: shard device unit tests (2)"
diff --git a/examples/hello/hello_test.go b/examples/hello/hello_v23_test.go
similarity index 100%
rename from examples/hello/hello_test.go
rename to examples/hello/hello_v23_test.go
diff --git a/profiles/internal/rpc/client.go b/profiles/internal/rpc/client.go
index 2efc1b3..f00c4ba 100644
--- a/profiles/internal/rpc/client.go
+++ b/profiles/internal/rpc/client.go
@@ -82,33 +82,19 @@
 	// TODO(jhahn): Add monitoring the network interface changes.
 	ipNets []*net.IPNet
 
-	// We support concurrent calls to StartCall and Close, so we must protect the
-	// vcMap.  Everything else is initialized upon client construction, and safe
-	// to use concurrently.
-	vcMapMu sync.Mutex
-	vcMap   map[vcMapKey]*vcInfo
+	vcCache *vc.VCCache
 
 	dc vc.DischargeClient
 }
 
 var _ rpc.Client = (*client)(nil)
 
-type vcInfo struct {
-	vc       stream.VC
-	remoteEP naming.Endpoint
-}
-
-type vcMapKey struct {
-	endpoint        string
-	clientPublicKey string // clientPublicKey = "" means we are running unencrypted (i.e. SecurityNone)
-}
-
 func InternalNewClient(streamMgr stream.Manager, ns namespace.T, opts ...rpc.ClientOpt) (rpc.Client, error) {
 	c := &client{
 		streamMgr: streamMgr,
 		ns:        ns,
 		ipNets:    ipNetworks(),
-		vcMap:     make(map[vcMapKey]*vcInfo),
+		vcCache:   vc.NewVCCache(),
 	}
 	c.dc = InternalNewDischargeClient(nil, c, 0)
 	for _, opt := range opts {
@@ -125,54 +111,54 @@
 }
 
 func (c *client) createFlow(ctx *context.T, principal security.Principal, ep naming.Endpoint, vcOpts []stream.VCOpt) (stream.Flow, *verror.SubErr) {
-	c.vcMapMu.Lock()
-	defer c.vcMapMu.Unlock()
-
 	suberr := func(err error) *verror.SubErr {
 		return &verror.SubErr{Err: err, Options: verror.Print}
 	}
 
-	if c.vcMap == nil {
+	found, err := c.vcCache.ReservedFind(ep, principal)
+	if err != nil {
 		return nil, suberr(verror.New(errClientCloseAlreadyCalled, ctx))
 	}
-
-	vcKey := vcMapKey{endpoint: ep.String()}
-	if principal != nil {
-		vcKey.clientPublicKey = principal.PublicKey().String()
-	}
-	if vcinfo := c.vcMap[vcKey]; vcinfo != nil {
-		if flow, err := vcinfo.vc.Connect(); err == nil {
+	defer c.vcCache.Unreserve(ep, principal)
+	if found != nil {
+		// We are serializing the creation of all flows per VC. This is okay
+		// because if one flow creation is to block, it is likely that all others
+		// for that VC would block as well.
+		if flow, err := found.Connect(); err == nil {
 			return flow, nil
 		}
 		// If the vc fails to establish a new flow, we assume it's
-		// broken, remove it from the map, and proceed to establishing
+		// broken, remove it from the cache, and proceed to establishing
 		// a new vc.
+		//
+		// TODO(suharshs): The decision to redial 1 time when the dialing the vc
+		// in the cache fails is a bit inconsistent with the behavior when a newly
+		// dialed vc.Connect fails. We should revisit this.
+		//
 		// TODO(caprita): Should we distinguish errors due to vc being
 		// closed from other errors?  If not, should we call vc.Close()
-		// before removing the vc from the map?
-		delete(c.vcMap, vcKey)
+		// before removing the vc from the cache?
+		if err := c.vcCache.Delete(found); err != nil {
+			return nil, suberr(verror.New(errClientCloseAlreadyCalled, ctx))
+		}
 	}
+
 	sm := c.streamMgr
-	c.vcMapMu.Unlock()
-	vc, err := sm.Dial(ep, principal, vcOpts...)
-	c.vcMapMu.Lock()
+	v, err := sm.Dial(ep, principal, vcOpts...)
 	if err != nil {
 		return nil, suberr(err)
 	}
-	if c.vcMap == nil {
+
+	flow, err := v.Connect()
+	if err != nil {
+		return nil, suberr(err)
+	}
+
+	if err := c.vcCache.Insert(v.(*vc.VC)); err != nil {
 		sm.ShutdownEndpoint(ep)
 		return nil, suberr(verror.New(errClientCloseAlreadyCalled, ctx))
 	}
-	if othervc, exists := c.vcMap[vcKey]; exists {
-		go vc.Close(nil)
-		vc = othervc.vc
-	} else {
-		c.vcMap[vcKey] = &vcInfo{vc: vc, remoteEP: ep}
-	}
-	flow, err := vc.Connect()
-	if err != nil {
-		return nil, suberr(err)
-	}
+
 	return flow, nil
 }
 
@@ -744,12 +730,9 @@
 
 func (c *client) Close() {
 	defer vlog.LogCall()()
-	c.vcMapMu.Lock()
-	for _, v := range c.vcMap {
-		c.streamMgr.ShutdownEndpoint(v.remoteEP)
+	for _, v := range c.vcCache.Close() {
+		c.streamMgr.ShutdownEndpoint(v.RemoteEndpoint())
 	}
-	c.vcMap = nil
-	c.vcMapMu.Unlock()
 }
 
 // flowClient implements the RPC client-side protocol for a single RPC, over a
diff --git a/profiles/internal/rpc/stream/vc/vc_cache.go b/profiles/internal/rpc/stream/vc/vc_cache.go
new file mode 100644
index 0000000..d962cfa
--- /dev/null
+++ b/profiles/internal/rpc/stream/vc/vc_cache.go
@@ -0,0 +1,114 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vc
+
+import (
+	"sync"
+
+	"v.io/v23/naming"
+	"v.io/v23/security"
+	"v.io/v23/verror"
+)
+
+var errVCCacheClosed = reg(".errVCCacheClosed", "vc cache has been closed")
+
+// VCCache implements a set of VIFs keyed by the endpoint of the remote end and the
+// local principal. Multiple goroutines can invoke methods on the VCCache simultaneously.
+type VCCache struct {
+	mu      sync.Mutex
+	cache   map[vcKey]*VC  // GUARDED_BY(mu)
+	started map[vcKey]bool // GUARDED_BY(mu)
+	cond    *sync.Cond
+}
+
+// NewVCCache returns a new cache for VCs.
+func NewVCCache() *VCCache {
+	c := &VCCache{
+		cache:   make(map[vcKey]*VC),
+		started: make(map[vcKey]bool),
+	}
+	c.cond = sync.NewCond(&c.mu)
+	return c
+}
+
+// ReservedFind returns a VC where the remote end of the underlying connection
+// is identified by the provided (ep, p.PublicKey). Returns nil if there is no
+// such VC.
+//
+// Iff the cache is closed, ReservedFind will return an error.
+// If ReservedFind has no error, the caller is required to call Unreserve, to avoid deadlock.
+// The ep, and p.PublicKey in Unreserve must be the same as used in the ReservedFind call.
+// During this time, all new ReservedFind calls for this ep and p will Block until
+// the corresponding Unreserve call is made.
+func (c *VCCache) ReservedFind(ep naming.Endpoint, p security.Principal) (*VC, error) {
+	k := c.key(ep, p)
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	for c.started[k] {
+		c.cond.Wait()
+	}
+	if c.cache == nil {
+		return nil, verror.New(errVCCacheClosed, nil)
+	}
+	c.started[k] = true
+	return c.cache[k], nil
+}
+
+// Unreserve marks the status of the ep, p as no longer started, and
+// broadcasts waiting threads.
+func (c *VCCache) Unreserve(ep naming.Endpoint, p security.Principal) {
+	c.mu.Lock()
+	delete(c.started, c.key(ep, p))
+	c.cond.Broadcast()
+	c.mu.Unlock()
+}
+
+// Insert adds vc to the cache and returns an error iff the cache has been closed.
+func (c *VCCache) Insert(vc *VC) error {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	if c.cache == nil {
+		return verror.New(errVCCacheClosed, nil)
+	}
+	c.cache[c.key(vc.RemoteEndpoint(), vc.LocalPrincipal())] = vc
+	return nil
+}
+
+// Close marks the VCCache as closed and returns the VCs remaining in the cache.
+func (c *VCCache) Close() []*VC {
+	c.mu.Lock()
+	vcs := make([]*VC, 0, len(c.cache))
+	for _, vc := range c.cache {
+		vcs = append(vcs, vc)
+	}
+	c.cache = nil
+	c.started = nil
+	c.mu.Unlock()
+	return vcs
+}
+
+// Delete removes vc from the cache, returning an error iff the cache has been closed.
+func (c *VCCache) Delete(vc *VC) error {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	if c.cache == nil {
+		return verror.New(errVCCacheClosed, nil)
+	}
+	delete(c.cache, c.key(vc.RemoteEndpoint(), vc.LocalPrincipal()))
+	return nil
+}
+
+type vcKey struct {
+	remoteEP       string
+	localPublicKey string // localPublicKey = "" means we are running unencrypted (i.e. SecurityNone)
+}
+
+func (c *VCCache) key(ep naming.Endpoint, p security.Principal) vcKey {
+	k := vcKey{remoteEP: ep.String()}
+	if p != nil {
+		k.localPublicKey = p.PublicKey().String()
+	}
+	return k
+}
diff --git a/profiles/internal/rpc/stream/vc/vc_cache_test.go b/profiles/internal/rpc/stream/vc/vc_cache_test.go
new file mode 100644
index 0000000..8bfa144
--- /dev/null
+++ b/profiles/internal/rpc/stream/vc/vc_cache_test.go
@@ -0,0 +1,123 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vc
+
+import (
+	"testing"
+
+	inaming "v.io/x/ref/profiles/internal/naming"
+	"v.io/x/ref/test/testutil"
+)
+
+func TestInsertDelete(t *testing.T) {
+	cache := NewVCCache()
+	ep, err := inaming.NewEndpoint("foo:8888")
+	if err != nil {
+		t.Fatal(err)
+	}
+	p := testutil.NewPrincipal("test")
+	vc := &VC{remoteEP: ep, localPrincipal: p}
+	otherEP, err := inaming.NewEndpoint("foo:8888")
+	if err != nil {
+		t.Fatal(err)
+	}
+	otherP := testutil.NewPrincipal("test")
+	otherVC := &VC{remoteEP: otherEP, localPrincipal: otherP}
+
+	cache.Insert(vc)
+	cache.Insert(otherVC)
+	cache.Delete(vc)
+	if got, want := cache.Close(), []*VC{otherVC}; !vcsEqual(got, want) {
+		t.Errorf("got %v, want %v", got, want)
+	}
+}
+
+func TestInsertClose(t *testing.T) {
+	cache := NewVCCache()
+	ep, err := inaming.NewEndpoint("foo:8888")
+	if err != nil {
+		t.Fatal(err)
+	}
+	p := testutil.NewPrincipal("test")
+	vc := &VC{remoteEP: ep, localPrincipal: p}
+
+	if err := cache.Insert(vc); err != nil {
+		t.Errorf("the cache is not closed yet")
+	}
+	if got, want := cache.Close(), []*VC{vc}; !vcsEqual(got, want) {
+		t.Errorf("got %v, want %v", got, want)
+	}
+	if err := cache.Insert(vc); err == nil {
+		t.Errorf("the cache has been closed")
+	}
+}
+
+func TestReservedFind(t *testing.T) {
+	cache := NewVCCache()
+	ep, err := inaming.NewEndpoint("foo:8888")
+	if err != nil {
+		t.Fatal(err)
+	}
+	p := testutil.NewPrincipal("test")
+	vc := &VC{remoteEP: ep, localPrincipal: p}
+	cache.Insert(vc)
+
+	// We should be able to find the vc in the cache.
+	if got, err := cache.ReservedFind(ep, p); err != nil || got != vc {
+		t.Errorf("got %v, want %v, err: %v", got, vc, err)
+	}
+
+	// If we change the endpoint or the principal, we should get nothing.
+	otherEP, err := inaming.NewEndpoint("bar: 7777")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if got, err := cache.ReservedFind(otherEP, p); err != nil || got != nil {
+		t.Errorf("got %v, want <nil>, err: %v", got, err)
+	}
+	if got, err := cache.ReservedFind(ep, testutil.NewPrincipal("wrong")); err != nil || got != nil {
+		t.Errorf("got %v, want <nil>, err: %v", got, err)
+	}
+
+	// A subsequent ReservedFind call that matches a previous failed ReservedFind
+	// should block until a matching Unreserve call is made.
+	ch := make(chan *VC, 1)
+	go func(ch chan *VC) {
+		vc, err := cache.ReservedFind(otherEP, p)
+		if err != nil {
+			t.Fatal(err)
+		}
+		ch <- vc
+	}(ch)
+
+	// We insert the otherEP into the cache.
+	otherVC := &VC{remoteEP: otherEP, localPrincipal: p}
+	cache.Insert(otherVC)
+	cache.Unreserve(otherEP, p)
+
+	// Now the cache.BlcokingFind should have returned the correct otherVC.
+	if cachedVC := <-ch; cachedVC != otherVC {
+		t.Errorf("got %v, want %v", cachedVC, otherVC)
+	}
+}
+
+func vcsEqual(a, b []*VC) bool {
+	if len(a) != len(b) {
+		return false
+	}
+	m := make(map[*VC]int)
+	for _, v := range a {
+		m[v]++
+	}
+	for _, v := range b {
+		m[v]--
+	}
+	for _, i := range m {
+		if i != 0 {
+			return false
+		}
+	}
+	return true
+}
diff --git a/profiles/internal/rpc/stream/vif/set.go b/profiles/internal/rpc/stream/vif/set.go
index b966735..497e43b 100644
--- a/profiles/internal/rpc/stream/vif/set.go
+++ b/profiles/internal/rpc/stream/vif/set.go
@@ -45,7 +45,8 @@
 	return s.find(network, address, true)
 }
 
-// Unblock broadcasts all threads
+// Unblock marks the status of the network, address as no longer started, and
+// broadcasts waiting threads.
 func (s *Set) Unblock(network, address string) {
 	s.mu.Lock()
 	delete(s.started, key(network, address))
@@ -60,7 +61,7 @@
 	return s.find(network, address, false)
 }
 
-// Insert adds a VIF to the set
+// Insert adds a VIF to the set.
 func (s *Set) Insert(vif *VIF) {
 	addr := vif.conn.RemoteAddr()
 	k := key(addr.Network(), addr.String())
@@ -76,7 +77,7 @@
 	vif.addSet(s)
 }
 
-// Delete removes a VIF from the set
+// Delete removes a VIF from the set.
 func (s *Set) Delete(vif *VIF) {
 	vif.removeSet(s)
 	addr := vif.conn.RemoteAddr()
diff --git a/services/identity/internal/caveats/browser_caveat_selector.go b/services/identity/internal/caveats/browser_caveat_selector.go
index 792f4b2..37e1079 100644
--- a/services/identity/internal/caveats/browser_caveat_selector.go
+++ b/services/identity/internal/caveats/browser_caveat_selector.go
@@ -28,7 +28,7 @@
 
 func (s *browserCaveatSelector) Render(blessingExtension, state, redirectURL string, w http.ResponseWriter, r *http.Request) error {
 	tmplargs := struct {
-		Extension, Macaroon, MacaroonURL, AssetsPrefix, BlessingName string
+		Email, Macaroon, MacaroonURL, AssetsPrefix, BlessingName string
 	}{blessingExtension, state, redirectURL, s.assetsPrefix, s.blessingName}
 	w.Header().Set("Context-Type", "text/html")
 	if err := templates.SelectCaveats.Execute(w, tmplargs); err != nil {
diff --git a/services/identity/internal/server/identityd.go b/services/identity/internal/server/identityd.go
index 80c8f8c..9a34e15 100644
--- a/services/identity/internal/server/identityd.go
+++ b/services/identity/internal/server/identityd.go
@@ -188,6 +188,7 @@
 			GoogleServers, DischargeServers []string
 			ListBlessingsRoute              string
 			AssetsPrefix                    string
+			Email                           string
 		}{
 			Self:               principal.BlessingStore().Default(),
 			GoogleServers:      args.GoogleServers,
diff --git a/services/identity/internal/templates/caveats.go b/services/identity/internal/templates/caveats.go
index 047de74..72caced 100644
--- a/services/identity/internal/templates/caveats.go
+++ b/services/identity/internal/templates/caveats.go
@@ -11,11 +11,119 @@
 var selectCaveats = template.Must(template.New("bless").Parse(`<!doctype html>
 <html>
 <head>
-  {{template "head" .}}
-  <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>
-
   <title>Add Blessing - Vanadium Identity Provider</title>
+
+  {{template "head" .}}
+
+</head>
+
+<body class="identityprovider-layout">
+
+  <header>
+    <nav class="left">
+      <a href="#" class="logo">Vanadium</a>
+      <span class="service-name">Identity Provider</span>
+    </nav>
+    <nav class="right">
+      <a href="#">{{.Email}}</a>
+    </nav>
+  </header>
+
+  <main class="add-blessing">
+
+    <form method="POST" id="caveats-form" name="input"
+    action="{{.MacaroonURL}}" role="form">
+      <input type="text" class="hidden" name="macaroon" value="{{.Macaroon}}">
+
+      <h1 class="page-head">Add blessing</h1>
+      <p>
+        This blessing allows the Vanadium Identity Provider to authorize your
+        application's credentials and provides your application access to the
+        data associated with your Google Account. Blessing names contain the
+        email address associated with your Google Account, and will be visible
+        to peers you connect to.
+      </p>
+
+      <div class="note">
+        <p>
+          <strong>
+            Using Vanadium in production applications is discouraged at this
+          time.</strong><br>
+          During this preview, the
+          <a href="https://v.io/glossary.html#blessing-root" target="_">
+            blessing root
+          </a>
+          may change without notice.
+        </p>
+      </div>
+
+      <label for="blessingExtension">Blessing name</label>
+      <div class="value">
+        {{.BlessingName}}/{{.Email}}/
+        <input name="blessingExtension" type="text" placeholder="extension">
+        <input type="hidden" id="timezoneOffset" name="timezoneOffset">
+      </div>
+
+      <label>Caveats</label>
+      <div class="caveatRow">
+        <div class="define-caveat">
+          <span class="selected value RevocationCaveatSelected">
+            Active until revoked
+          </span>
+          <span class="selected value ExpiryCaveatSelected hidden">
+            Expires on
+          </span>
+          <span class="selected value MethodCaveatSelected hidden">
+            Allowed methods are
+          </span>
+          <span class="selected value PeerBlessingsCaveatSelected hidden">
+            Allowed peers are
+          </span>
+
+          <select name="caveat" class="caveats hidden">
+            <option name="RevocationCaveat" value="RevocationCaveat"
+            class="cavOption">Active until revoked</option>
+            <option name="ExpiryCaveat" value="ExpiryCaveat"
+            class="cavOption">Expires on</option>
+            <option name="MethodCaveat" value="MethodCaveat"
+            class="cavOption">Allowed methods are</option>
+            <option name="PeerBlessingsCaveat" value="PeerBlessingsCaveat"
+            class="cavOption">Allowed peers are</option>
+          </select>
+
+          <input type="text" class="caveatInput hidden"
+            id="RevocationCaveat" name="RevocationCaveat">
+          <input type="datetime-local" class="caveatInput expiry hidden"
+            id="ExpiryCaveat" name="ExpiryCaveat">
+          <input type="text" class="caveatInput hidden"
+           id="MethodCaveat" name="MethodCaveat"
+           placeholder="comma-separated method list">
+          <input type="text" class="caveatInput hidden"
+            id="PeerBlessingsCaveat" name="PeerBlessingsCaveat"
+            placeholder="comma-separated blessing list">
+        </div>
+        <div class="add-caveat">
+          <a href="#" class="addMore">Add more caveats</a>
+        </div>
+      </div>
+
+      <div class="action-buttons">
+        <button class="button-tertiary" id="cancel" type="button">Cancel</button>
+        <button class="button-primary" type="submit">Bless</button>
+      </div>
+
+      <p class="disclaimer-text">
+        By clicking "Bless", you agree to the Google
+        <a href="https://www.google.com/intl/en/policies/terms/">General Terms of Service</a>,
+        <a href="https://developers.google.com/terms/">APIs Terms of Service</a>,
+        and <a href="https://www.google.com/intl/en/policies/privacy/">Privacy Policy</a>
+      </p>
+    </form>
+  </main>
+
+  <script src="{{.AssetsPrefix}}/identity/moment.js"></script>
+  <script src="{{.AssetsPrefix}}/identity/jquery.js"></script>
+
   <script>
   $(document).ready(function() {
     var numCaveats = 1;
@@ -125,110 +233,5 @@
     });
   });
   </script>
-</head>
-
-<body class="identityprovider-layout">
-
-  <header>
-    <nav class="left">
-      <a href="#" class="logo">Vanadium</a>
-      <span class="service-name">Identity Provider</span>
-    </nav>
-    <nav class="right">
-      <a href="#">{{.Extension}}</a>
-    </nav>
-  </header>
-
-  <main class="add-blessing">
-    <form method="POST" id="caveats-form" name="input"
-    action="{{.MacaroonURL}}" role="form">
-      <input type="text" class="hidden" name="macaroon" value="{{.Macaroon}}">
-
-      <h1 class="page-head">Add blessing</h1>
-      <p>
-        This blessing allows the Vanadium Identity Provider to authorize your
-        application's credentials and provides your application access to the
-        data associated with your Google Account. Blessing names contain the
-        email address associated with your Google Account, and will be visible
-        to peers you connect to.
-      </p>
-
-      <div class="note">
-        <p>
-          <strong>
-            Using Vanadium in production applications is discouraged at this
-          time.</strong><br>
-          During this preview, the
-          <a href="https://v.io/glossary.html#blessing-root" target="_blank">
-            blessing root
-          </a>
-          may change without notice.
-        </p>
-      </div>
-
-      <label for="blessingExtension">Blessing name</label>
-      <div class="value">
-        {{.BlessingName}}/{{.Extension}}/
-        <input name="blessingExtension" type="text" placeholder="extension">
-        <input type="hidden" id="timezoneOffset" name="timezoneOffset">
-      </div>
-
-      <label>Caveats</label>
-      <div class="caveatRow">
-        <div class="define-caveat">
-          <span class="selected value RevocationCaveatSelected">
-            Active until revoked
-          </span>
-          <span class="selected value ExpiryCaveatSelected hidden">
-            Expires on
-          </span>
-          <span class="selected value MethodCaveatSelected hidden">
-            Allowed methods are
-          </span>
-          <span class="selected value PeerBlessingsCaveatSelected hidden">
-            Allowed peers are
-          </span>
-
-          <select name="caveat" class="caveats hidden">
-            <option name="RevocationCaveat" value="RevocationCaveat"
-            class="cavOption">Active until revoked</option>
-            <option name="ExpiryCaveat" value="ExpiryCaveat"
-            class="cavOption">Expires on</option>
-            <option name="MethodCaveat" value="MethodCaveat"
-            class="cavOption">Allowed methods are</option>
-            <option name="PeerBlessingsCaveat" value="PeerBlessingsCaveat"
-            class="cavOption">Allowed peers are</option>
-          </select>
-
-          <input type="text" class="caveatInput hidden"
-            id="RevocationCaveat" name="RevocationCaveat">
-          <input type="datetime-local" class="caveatInput expiry hidden"
-            id="ExpiryCaveat" name="ExpiryCaveat">
-          <input type="text" class="caveatInput hidden"
-           id="MethodCaveat" name="MethodCaveat"
-           placeholder="comma-separated method list">
-          <input type="text" class="caveatInput hidden"
-            id="PeerBlessingsCaveat" name="PeerBlessingsCaveat"
-            placeholder="comma-separated blessing list">
-        </div>
-        <div class="add-caveat">
-          <a href="#" class="addMore">Add more caveats</a>
-        </div>
-      </div>
-
-      <div class="action-buttons">
-        <button class="button-tertiary" id="cancel" type="button">Cancel</button>
-        <button class="button-primary" type="submit">Bless</button>
-      </div>
-
-      <p class="disclaimer-text">
-        By clicking "Bless", you agree to the Google
-        <a href="https://www.google.com/intl/en/policies/terms/">General Terms of Service</a>,
-        <a href="https://developers.google.com/terms/">APIs Terms of Service</a>,
-        and <a href="https://www.google.com/intl/en/policies/privacy/">Privacy Policy</a>
-      </p>
-    </form>
-  </main>
-
 </body>
 </html>`))
diff --git a/services/identity/internal/templates/head.go b/services/identity/internal/templates/head.go
deleted file mode 100644
index 609deba..0000000
--- a/services/identity/internal/templates/head.go
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package templates
-
-var headPartial = `{{define "head"}}
-  <meta
-     name="viewport"
-     content="width=device-width,
-              initial-scale=1,
-              maximum-scale=1,
-              user-scalable=no,
-              minimal-ui">
-  <meta
-     name="apple-mobile-web-app-capable"
-     content="yes">
-
-  <meta
-     name="apple-mobile-web-app-status-bar-style"
-     content="black">
-
-
-  <link href='//fonts.googleapis.com/css?family=Source+Code+Pro:400,500|Roboto:500,400italic,300,500italic,300italic,400'
-    rel='stylesheet'
-    type='text/css'>
-
-  <link rel="stylesheet" href="{{.AssetsPrefix}}/identity.css">
-
-  <link rel="apple-touch-icon" sizes="57x57" href="{{.AssetsPrefix}}/favicons/apple-touch-icon-57x57.png">
-  <link rel="apple-touch-icon" sizes="114x114" href="{{.AssetsPrefix}}/favicons/apple-touch-icon-114x114.png">
-  <link rel="apple-touch-icon" sizes="72x72" href="{{.AssetsPrefix}}/favicons/apple-touch-icon-72x72.png">
-  <link rel="apple-touch-icon" sizes="144x144" href="{{.AssetsPrefix}}/favicons/apple-touch-icon-144x144.png">
-  <link rel="apple-touch-icon" sizes="60x60" href="{{.AssetsPrefix}}/favicons/apple-touch-icon-60x60.png">
-  <link rel="apple-touch-icon" sizes="120x120" href="{{.AssetsPrefix}}/favicons/apple-touch-icon-120x120.png">
-  <link rel="apple-touch-icon" sizes="76x76" href="{{.AssetsPrefix}}/favicons/apple-touch-icon-76x76.png">
-  <link rel="apple-touch-icon" sizes="152x152" href="{{.AssetsPrefix}}/favicons/apple-touch-icon-152x152.png">
-  <link rel="apple-touch-icon" sizes="180x180" href="{{.AssetsPrefix}}/favicons/apple-touch-icon-180x180.png">
-  <link rel="icon" type="image/png" href="{{.AssetsPrefix}}/favicons/favicon-192x192.png" sizes="192x192">
-  <link rel="icon" type="image/png" href="{{.AssetsPrefix}}/favicons/favicon-160x160.png" sizes="160x160">
-  <link rel="icon" type="image/png" href="{{.AssetsPrefix}}/favicons/favicon-96x96.png" sizes="96x96">
-  <link rel="icon" type="image/png" href="{{.AssetsPrefix}}/favicons/favicon-16x16.png" sizes="16x16">
-  <link rel="icon" type="image/png" href="{{.AssetsPrefix}}/favicons/favicon-32x32.png" sizes="32x32">
-  <meta name="msapplication-TileColor" content="#da532c">
-  <meta name="msapplication-TileImage" content="{{.AssetsPrefix}}/favicons/mstile-144x144.png">
-{{end}}`
diff --git a/services/identity/internal/templates/home.go b/services/identity/internal/templates/home.go
index 5ab6136..03d8d67 100644
--- a/services/identity/internal/templates/home.go
+++ b/services/identity/internal/templates/home.go
@@ -6,91 +6,31 @@
 
 import "html/template"
 
-var Home = template.Must(home.Parse(headPartial))
+var Home = template.Must(homeWithHeader.Parse(sidebarPartial))
+var homeWithHead = template.Must(home.Parse(headPartial))
+var homeWithHeader = template.Must(homeWithHead.Parse(headerPartial))
 
 var home = template.Must(template.New("main").Parse(`<!doctype html>
 <html>
 <head>
-  {{template "head" .}}
   <title>Vanadium Identity Provider</title>
+  {{template "head" .}}
 </head>
 
-<body class="home-layout">
-<main>
-<section class="intro">
-  <div class="intro-container">
-    <h1 class="head">
-      Vanadium Identity Provider
-    </h1>
-
-    <h3>
-      This is a Vanadium Identity Provider that provides blessings with the
-      name prefix {{.Self}}.
-    </h3>
-
-    <div class="buttons grid">
-      <a href="/auth/google/{{.ListBlessingsRoute}}" class="button-passive cell">
-        Your Blessings
+<body class="identityprovider-layout">
+  {{template "header" .}}
+  <main>
+    <h1 class="page-head">Authorize Vanadium apps with Google</h1>
+    <p>
+      The Vanadium Identity Provider authorizes Vanadium blessings based on your Google Account.<br>
+      <a href="http://v.io/glossary.html#identity-provider">Learn more</a>
+    </p>
+    <p>
+      <a href="/auth/google/{{.ListBlessingsRoute}}" class="button-passive">
+        Show blessings
       </a>
-    </div>
-  </div>
-</section>
-
-<section class="mission">
-  <div class="grid">
-    <div class="cell">
-      <h2>Public Key</h2>
-      <p>
-        The public key of this provider is <code>{{.Self.PublicKey}}</code>.</br>
-        The root names and public key (in DER encoded <a href="http://en.wikipedia.org/wiki/X.690#DER_encoding">format</a>)
-        are available in a <a class="btn btn-xs btn-primary" href="/auth/blessing-root">JSON</a> object.
-      </p>
-    </div>
-    {{if .GoogleServers}}
-    <div class="cell">
-      <h2>Blessings</h2>
-      <p>
-        Blessings (using Google OAuth to fetch an email address) are provided via
-        Vanadium RPCs to: <code>{{range .GoogleServers}}{{.}}{{end}}</code>
-      </p>
-    </div>
-    {{end}}
-  </div>
-</section>
-
-<section class="mission">
-  <div class="grid">
-    {{if .ListBlessingsRoute}}
-    <div class="cell">
-      <h2>Blessings Log</h2>
-      <p>
-        You can <a class="btn btn-xs btn-primary" href="/auth/google/{{.ListBlessingsRoute}}">enumerate</a>
-        blessings provided with your email address.
-      </p>
-    </div>
-    {{end}}
-    {{if .DischargeServers}}
-    <div class="cell">
-      <h2>Discharges</h2>
-      <p>
-        RevocationCaveat Discharges are provided via Vanadium RPCs to:
-        <code>{{range .DischargeServers}}{{.}}{{end}}</code>
-      </p>
-    </div>
-    {{end}}
-  </div>
-</section>
-
-<footer>
-  <nav class="main">
-    <a href="https://github.com/vanadium/issues/issues/new?labels=Area:%20Website">Site Feedback</a>
-  </nav>
-
-  <nav class="social">
-    <a href="https://github.com/vanadium" class="icon-github"></a>
-    <a href="https://twitter.com/vdotio" class="icon-twitter"></a>
-  </nav>
-</footer>
-</main>
+    </p>
+    {{template "sidebar" .}}
+  </main>
 </body>
 </html>`))
diff --git a/services/identity/internal/templates/list_blessings.go b/services/identity/internal/templates/list_blessings.go
index 8b26163..944ba97 100644
--- a/services/identity/internal/templates/list_blessings.go
+++ b/services/identity/internal/templates/list_blessings.go
@@ -6,25 +6,89 @@
 
 import "html/template"
 
-var ListBlessings = template.Must(listBlessings.Parse(headPartial))
+var ListBlessings = template.Must(listWithHeader.Parse(sidebarPartial))
+var listWithHead = template.Must(listBlessings.Parse(headPartial))
+var listWithHeader = template.Must(listWithHead.Parse(headerPartial))
 
 var listBlessings = template.Must(template.New("auditor").Parse(`<!doctype html>
 <html>
 <head>
+  <title>Blessings for {{.Email}} - Vanadium Identity Provider</title>
   {{template "head" .}}
-  <title>Blessings for {{.Email}}</title>
-  <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>
+  <link rel="stylesheet" href="{{.AssetsPrefix}}/identity/toastr.css">
+</head>
 
+<body class="identityprovider-layout">
+  {{template "header" .}}
+  <main>
+    <h1 class="page-head">Authorize Vanadium apps with Google</h1>
+    <p>
+      The Vanadium Identity Provider authorizes Vanadium blessings based on your Google Account.<br>
+      <a href="http://v.io/glossary.html#identity-provider">Learn more</a>
+    </p>
+
+    {{range .Log}}
+      {{if .Error}}
+        <h1>Error</h1>
+        <p>
+          Failed to read audit log.<br>
+          {{.Error}}
+        </p>
+      {{else}}
+        <div class="blessings-list">
+          <div class="blessings-header">
+            <h1>Your blessings</h1>
+            <h5>Issued</h5>
+            <h5>Revoked</h5>
+          </div>
+
+          <div class="blessings-item">
+            <div class="blessing-details">
+              <h3>{{.Blessed}}</h3>
+              <p>
+                <b>Public Key</b><br>
+                {{.Blessed.PublicKey}}
+              </p>
+              <p class="blessing-caveats">
+                <b>Caveats</b><br>
+                {{range $index, $cav := .Caveats}}
+                  {{if ne $index 0}}
+                  {{end}}
+                  {{$cav}}<br>
+                {{end}}
+              </p>
+            </div>
+
+            <div class="blessing-issued unixtime" data-unixtime={{.Timestamp.Unix}}>{{.Timestamp.String}}</div>
+
+            <div class="blessing-revoked">
+              {{ if .Token }}
+              <button class="revoke button-passive" value="{{.Token}}">Revoke</button>
+              {{ else if not .RevocationTime.IsZero }}
+                <p class="unixtime" data-unixtime={{.RevocationTime.Unix}}>{{.RevocationTime.String}}</p>
+              {{ end }}
+            </div>
+        </div>
+      {{end}}
+    {{else}}
+      <h1>Your blessings</h1>
+      <p>
+        <a href="http://v.io/installation">Install Vanadium</a> to set up your first blessing.
+      </p>
+    {{end}}
+  {{template "sidebar" .}}
+  </main>
+
+  <script src="{{.AssetsPrefix}}/identity/toastr.js"></script>
+  <script src="{{.AssetsPrefix}}/identity/moment.js"></script>
+  <script src="{{.AssetsPrefix}}/identity/jquery.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.html("<a href='#'>" + m.format("MMM DD, YYYY h:mm:ss a") + "</a>");
       elem.data("style", "fromNow");
     } else {
       elem.html("<a href='#'>" + m.fromNow() + "</a>");
@@ -53,7 +117,7 @@
           failMessage(revokeButton);
           return;
         }
-        revokeButton.replaceWith("<div>Just Revoked!</div>");
+        revokeButton.replaceWith("<div>Revoked just now</div>");
       }).fail(function(xhr, textStatus){
         failMessage(revokeButton);
         console.error('Bad request: %s', status, xhr)
@@ -66,100 +130,9 @@
       $(this).addClass("bg-danger");
     });
     toastr.options.closeButton = true;
-    toastr.error('Unable to revoke identity!', 'Error!')
+    toastr.error('Unable to revoke identity', 'Error')
   }
   </script>
-</head>
 
-<body class="default-layout">
-  <header>
-    <nav class="left">
-      <a href="#" class="logo">Vanadium</a>
-    </nav>
-
-    <nav class="main">
-      <a href="#">Blessing Log</a>
-    </nav>
-
-    <nav class="right">
-      <a href="#">{{.Email}}</a>
-    </nav>
-  </header>
-
-  <main style="margin-left: 0px; max-width: 100%;">
-
-    <!-- Begin ID Server information -->
-    <div class="grid">
-      <div class="cell">
-        <h2>Public Key</h2>
-        <p>
-          The public key of this provider is <code>{{.Self.PublicKey}}</code>.</br>
-          The root names and public key (in DER encoded <a href="http://en.wikipedia.org/wiki/X.690#DER_encoding">format</a>)
-          are available in a <a class="btn btn-xs btn-primary" href="/auth/blessing-root">JSON</a> object.
-        </p>
-      </div>
-      {{if .GoogleServers}}
-      <div class="cell">
-        <h2>Blessings</h2>
-        <p>
-          Blessings (using Google OAuth to fetch an email address) are provided via
-          Vanadium RPCs to: <code>{{range .GoogleServers}}{{.}}{{end}}</code>
-        </p>
-      </div>
-      {{end}}
-      {{if .DischargeServers}}
-      <div class="cell">
-        <h2>Discharges</h2>
-        <p>
-          RevocationCaveat Discharges are provided via Vanadium RPCs to:
-          <code>{{range .DischargeServers}}{{.}}{{end}}</code>
-        </p>
-      </div>
-      {{end}}
-    </div>
-    <!-- End ID Server information -->
-
-    <table class="blessing-table">
-        <tr>
-        <td>Blessed as</td>
-        <td>Public Key</td>
-        <td>Issued</td>
-        <td class="td-wide">Caveats</td>
-        <td>Revoked</td>
-        </tr>
-        {{range .Log}}
-          {{if .Error}}
-            <tr class="">
-              <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 class="td-wide">
-            {{range $index, $cav := .Caveats}}
-              {{if ne $index 0}}
-                <hr>
-              {{end}}
-              {{$cav}}</br>
-            {{end}}
-            </td>
-            <td>
-              {{ if .Token }}
-              <button class="revoke button-passive" 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}}
-    </table>
-  </main>
 </body>
 </html>`))
diff --git a/services/identity/internal/templates/partials.go b/services/identity/internal/templates/partials.go
new file mode 100644
index 0000000..3594acf
--- /dev/null
+++ b/services/identity/internal/templates/partials.go
@@ -0,0 +1,116 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package templates
+
+var headPartial = `{{define "head"}}
+  <meta
+     name="viewport"
+     content="width=device-width,
+              initial-scale=1,
+              maximum-scale=1,
+              user-scalable=no,
+              minimal-ui">
+  <meta
+     name="apple-mobile-web-app-capable"
+     content="yes">
+
+  <meta
+     name="apple-mobile-web-app-status-bar-style"
+     content="black">
+
+
+  <link href='//fonts.googleapis.com/css?family=Source+Code+Pro:400,500|Roboto:500,400italic,300,500italic,300italic,400'
+    rel='stylesheet'
+    type='text/css'>
+
+  <link rel="stylesheet" href="{{.AssetsPrefix}}/identity.css">
+
+  <link rel="apple-touch-icon" sizes="57x57" href="{{.AssetsPrefix}}/favicons/apple-touch-icon-57x57.png">
+  <link rel="apple-touch-icon" sizes="114x114" href="{{.AssetsPrefix}}/favicons/apple-touch-icon-114x114.png">
+  <link rel="apple-touch-icon" sizes="72x72" href="{{.AssetsPrefix}}/favicons/apple-touch-icon-72x72.png">
+  <link rel="apple-touch-icon" sizes="144x144" href="{{.AssetsPrefix}}/favicons/apple-touch-icon-144x144.png">
+  <link rel="apple-touch-icon" sizes="60x60" href="{{.AssetsPrefix}}/favicons/apple-touch-icon-60x60.png">
+  <link rel="apple-touch-icon" sizes="120x120" href="{{.AssetsPrefix}}/favicons/apple-touch-icon-120x120.png">
+  <link rel="apple-touch-icon" sizes="76x76" href="{{.AssetsPrefix}}/favicons/apple-touch-icon-76x76.png">
+  <link rel="apple-touch-icon" sizes="152x152" href="{{.AssetsPrefix}}/favicons/apple-touch-icon-152x152.png">
+  <link rel="apple-touch-icon" sizes="180x180" href="{{.AssetsPrefix}}/favicons/apple-touch-icon-180x180.png">
+  <link rel="icon" type="image/png" href="{{.AssetsPrefix}}/favicons/favicon-192x192.png" sizes="192x192">
+  <link rel="icon" type="image/png" href="{{.AssetsPrefix}}/favicons/favicon-160x160.png" sizes="160x160">
+  <link rel="icon" type="image/png" href="{{.AssetsPrefix}}/favicons/favicon-96x96.png" sizes="96x96">
+  <link rel="icon" type="image/png" href="{{.AssetsPrefix}}/favicons/favicon-16x16.png" sizes="16x16">
+  <link rel="icon" type="image/png" href="{{.AssetsPrefix}}/favicons/favicon-32x32.png" sizes="32x32">
+  <meta name="msapplication-TileColor" content="#da532c">
+  <meta name="msapplication-TileImage" content="{{.AssetsPrefix}}/favicons/mstile-144x144.png">
+{{end}}`
+
+var headerPartial = `{{define "header"}}
+  <header>
+    <nav class="left">
+      <a href="#" class="logo">Vanadium</a>
+      <span class="service-name">Identity Provider</span>
+    </nav>
+    <nav class="right">
+      {{if .Email}}
+        <a href="#">{{.Email}}</a>
+      {{end}}
+    </nav>
+  </header>
+{{end}}`
+
+var sidebarPartial = `{{define "sidebar"}}
+<section class="provider-info">
+  <div class="provider-info-section">
+    <h5>Root name</h5>
+    <span class="provider-address">
+      dev.v.io/root
+    </span>
+
+    <h5>Public key</h5>
+    <span class="provider-address">
+      {{.Self.PublicKey}}
+    </span>
+
+    <p>
+      Get this provider’s root name and public key as a <a
+      href="http://en.wikipedia.org/wiki/X.690#DER_encoding" target="_blank">
+      DER</a>-encoded <a href="/auth/blessing-root" target="_blank">
+      JSON object</a>.
+    </p>
+  </div>
+
+  {{if .GoogleServers}}
+    <div class="provider-info-section">
+      <h5>Blessings</h5>
+      <p>
+        Provided via Vanadium RPC to:
+        <span class="provider-address">
+          {{range .GoogleServers}}{{.}}{{end}}
+        </span>
+      </p>
+    </div>
+  {{end}}
+
+  {{if .DischargeServers}}
+    <div class="provider-info-section">
+      <h5>Discharges</h5>
+      <p>
+        Provided via Vanadium RPC to:
+        <span class="provider-address">
+          {{range .DischargeServers}}{{.}}{{end}}
+        </span>
+      </p>
+    </div>
+  {{end}}
+
+  <div class="provider-info-section">
+    <h5>Learn more</h5>
+    <p>
+      Vanadium Concepts: <a href="">Security</a><br>
+      Tutorial: <a href="">Principals and Blessings</a><br>
+      Tutorial: <a href="">Third Party Caveats</a><br>
+    </p>
+  </div>
+</section>
+{{end}}`