services/identity: Give Identity Service a makeover.

The identity server now both uses v.io's CSS and has some of its own.

The identity service should be started with --assetsprefix flag that has the
host that serves images, css, etc that it needs to look pretty.
The assets in pracitve will be hosted from v.io.

This change also changes relative links starting with "/" to "/auth" to
allow the links to work through a proxy. This means the identity server
serves all its stuff at host:port/auth/...

Here are some screenshots to make it easier to review :)
Home Page: https://screenshot.googleplex.com/rEDCeBLB5j.png
Add Caveats Page: https://screenshot.googleplex.com/AVMDyGzLNJ
View Blessings Page: https://screenshot.googleplex.com/DNtUFNOOLT

MultiPart: 1/3
Change-Id: I93029095b84f28d5fad1f80b6b92ea796daf106c
diff --git a/cmd/principal/doc.go b/cmd/principal/doc.go
index e3b6eb5..4dcd0ef 100644
--- a/cmd/principal/doc.go
+++ b/cmd/principal/doc.go
@@ -155,7 +155,7 @@
  -for_peer=...
    If non-empty, the blessings obtained will be marked for peers matching this
    pattern in the store
- -from=https://auth.dev.v.io:8125/google
+ -from=https://dev.v.io/auth/google
    URL to use to begin the seek blessings process
  -set_default=true
    If true, the blessings obtained will be set as the default blessing in the
diff --git a/cmd/principal/main.go b/cmd/principal/main.go
index bc6d9d7..5eda485 100644
--- a/cmd/principal/main.go
+++ b/cmd/principal/main.go
@@ -711,7 +711,7 @@
 	cmdBless.Flags.StringVar(&flagBlessRemoteKey, "remote_key", "", "Public key of the remote principal to bless (obtained from the 'recvblessings' command run by the remote principal")
 	cmdBless.Flags.StringVar(&flagBlessRemoteToken, "remote_token", "", "Token provided by principal running the 'recvblessings' command")
 
-	cmdSeekBlessings.Flags.StringVar(&flagSeekBlessingsFrom, "from", "https://auth.dev.v.io:8125/google", "URL to use to begin the seek blessings process")
+	cmdSeekBlessings.Flags.StringVar(&flagSeekBlessingsFrom, "from", "https://dev.v.io/auth/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.")
diff --git a/services/identity/identityd/identityd_v23_test.go b/services/identity/identityd/identityd_v23_test.go
index d9cd6de..e2960c1 100644
--- a/services/identity/identityd/identityd_v23_test.go
+++ b/services/identity/identityd/identityd_v23_test.go
@@ -24,7 +24,7 @@
 	args := []string{
 		"seekblessings",
 		"--browser=false",
-		fmt.Sprintf("--from=%s/google", httpaddr),
+		fmt.Sprintf("--from=%s/auth/google", httpaddr),
 		"-v=3",
 	}
 	inv := principal.Start(args...)
diff --git a/services/identity/identityd/main.go b/services/identity/identityd/main.go
index af72b27..c6157d9 100644
--- a/services/identity/identityd/main.go
+++ b/services/identity/identityd/main.go
@@ -35,9 +35,10 @@
 	emailClassifier     util.EmailClassifier
 
 	// Flags controlling the HTTP server
-	host      = flag.String("host", defaultHost(), "Hostname the HTTP server listens on. This can be the name of the host running the webserver, but if running behind a NAT or load balancer, this should be the host name that clients will connect to. For example, if set to 'x.com', Vanadium identities will have the IssuerName set to 'x.com' and clients can expect to find the root name and public key of the signer at 'x.com/blessing-root'.")
-	httpaddr  = flag.String("httpaddr", "localhost:8125", "Address on which the HTTP server listens on.")
-	tlsconfig = flag.String("tlsconfig", "", "Comma-separated list of TLS certificate and private key files, in that order. This must be provided.")
+	host         = flag.String("host", defaultHost(), "Hostname the HTTP server listens on. This can be the name of the host running the webserver, but if running behind a NAT or load balancer, this should be the host name that clients will connect to. For example, if set to 'x.com', Vanadium identities will have the IssuerName set to 'x.com' and clients can expect to find the root name and public key of the signer at 'x.com/blessing-root'.")
+	httpaddr     = flag.String("httpaddr", "localhost:8125", "Address on which the HTTP server listens on.")
+	tlsconfig    = flag.String("tlsconfig", "", "Comma-separated list of TLS certificate and private key files, in that order. This must be provided.")
+	assetsprefix = flag.String("assetsprefix", "", "host serving the web assets for the identity server")
 )
 
 func main() {
@@ -76,8 +77,9 @@
 		reader,
 		revocationManager,
 		googleOAuthBlesserParams(googleoauth, revocationManager),
-		caveats.NewBrowserCaveatSelector(),
-		&emailClassifier)
+		caveats.NewBrowserCaveatSelector(*assetsprefix),
+		&emailClassifier,
+		*assetsprefix)
 	s.Serve(ctx, &listenSpec, *host, *httpaddr, *tlsconfig)
 }
 
diff --git a/services/identity/identityd_test/main.go b/services/identity/identityd_test/main.go
index 179b3aa..ec30535 100644
--- a/services/identity/identityd_test/main.go
+++ b/services/identity/identityd_test/main.go
@@ -26,9 +26,10 @@
 
 var (
 	// Flags controlling the HTTP server
-	host      = flag.String("host", "localhost", "Hostname the HTTP server listens on. This can be the name of the host running the webserver, but if running behind a NAT or load balancer, this should be the host name that clients will connect to. For example, if set to 'x.com', Vanadium identities will have the IssuerName set to 'x.com' and clients can expect to find the root name and public key of the signer at 'x.com/blessing-root'.")
-	httpaddr  = flag.String("httpaddr", "localhost:0", "Address on which the HTTP server listens on.")
-	tlsconfig = flag.String("tlsconfig", "", "Comma-separated list of TLS certificate and private key files, in that order. This must be provided.")
+	host         = flag.String("host", "localhost", "Hostname the HTTP server listens on. This can be the name of the host running the webserver, but if running behind a NAT or load balancer, this should be the host name that clients will connect to. For example, if set to 'x.com', Vanadium identities will have the IssuerName set to 'x.com' and clients can expect to find the root name and public key of the signer at 'x.com/blessing-root'.")
+	httpaddr     = flag.String("httpaddr", "localhost:0", "Address on which the HTTP server listens on.")
+	tlsconfig    = flag.String("tlsconfig", "", "Comma-separated list of TLS certificate and private key files, in that order. This must be provided.")
+	assetsprefix = flag.String("assetsprefix", "", "host serving the web assets for the identity server")
 )
 
 func main() {
@@ -70,7 +71,8 @@
 		revocationManager,
 		params,
 		caveats.NewMockCaveatSelector(),
-		nil)
+		nil,
+		*assetsprefix)
 	s.Serve(ctx, &listenSpec, *host, *httpaddr, *tlsconfig)
 }
 
diff --git a/services/identity/internal/caveats/browser_caveat_selector.go b/services/identity/internal/caveats/browser_caveat_selector.go
index 354e0d7..88c667c 100644
--- a/services/identity/internal/caveats/browser_caveat_selector.go
+++ b/services/identity/internal/caveats/browser_caveat_selector.go
@@ -7,30 +7,33 @@
 import (
 	"errors"
 	"fmt"
-	"html/template"
 	"net/http"
 	"strings"
 	"time"
 
+	"v.io/x/ref/services/identity/internal/templates"
+
 	"v.io/v23/security"
 )
 
-type browserCaveatSelector struct{}
+type browserCaveatSelector struct {
+	assetsPrefix string
+}
 
 // NewBrowserCaveatSelector returns a caveat selector that renders a form in the
 // to accept user caveat selections.
-func NewBrowserCaveatSelector() CaveatSelector {
-	return &browserCaveatSelector{}
+func NewBrowserCaveatSelector(assetsPrefix string) CaveatSelector {
+	return &browserCaveatSelector{assetsPrefix}
 }
 
 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", "PeerBlessingsCaveat"}, state, redirectURL}
+		Extension                           string
+		CaveatList                          []string
+		Macaroon, MacaroonURL, AssetsPrefix string
+	}{blessingExtension, []string{"ExpiryCaveat", "MethodCaveat", "PeerBlessingsCaveat"}, state, redirectURL, s.assetsPrefix}
 	w.Header().Set("Context-Type", "text/html")
-	if err := tmplSelectCaveats.Execute(w, tmplargs); err != nil {
+	if err := templates.SelectCaveats.Execute(w, tmplargs); err != nil {
 		return err
 	}
 	return nil
@@ -133,117 +136,3 @@
 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">
-      {{else if eq $name "PeerBlessingsCaveat"}}
-      <input type="text" id="{{$name}}" class="form-control caveatInput" name="{{$name}}" placeholder="comma-separated blessing-pattern 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/internal/oauth/handler.go b/services/identity/internal/oauth/handler.go
index 6380843..95d8f38 100644
--- a/services/identity/internal/oauth/handler.go
+++ b/services/identity/internal/oauth/handler.go
@@ -40,6 +40,7 @@
 	"v.io/x/ref/services/identity/internal/auditor"
 	"v.io/x/ref/services/identity/internal/caveats"
 	"v.io/x/ref/services/identity/internal/revocation"
+	"v.io/x/ref/services/identity/internal/templates"
 	"v.io/x/ref/services/identity/internal/util"
 )
 
@@ -82,6 +83,8 @@
 	OAuthProvider OAuthProvider
 	// CaveatSelector is used to obtain caveats from the user when seeking a blessing.
 	CaveatSelector caveats.CaveatSelector
+	// AssetsPrefix is the host where web assets for rendering the list blessings template are stored.
+	AssetsPrefix string
 }
 
 // BlessingMacaroon contains the data that is encoded into the macaroon for creating blessings.
@@ -167,12 +170,13 @@
 		Error          error
 	}
 	tmplargs := struct {
-		Log                chan tmplentry
-		Email, RevokeRoute string
+		Log                              chan tmplentry
+		Email, RevokeRoute, AssetsPrefix string
 	}{
-		Log:         make(chan tmplentry),
-		Email:       email,
-		RevokeRoute: revokeRoute,
+		Log:          make(chan tmplentry),
+		Email:        email,
+		RevokeRoute:  revokeRoute,
+		AssetsPrefix: h.args.AssetsPrefix,
 	}
 	entrych := h.args.BlessingLogReader.Read(email)
 
@@ -207,7 +211,7 @@
 			ch <- tmplEntry
 		}
 	}(tmplargs.Log)
-	if err := tmplViewBlessings.Execute(w, tmplargs); err != nil {
+	if err := templates.ListBlessings.Execute(w, tmplargs); err != nil {
 		vlog.Errorf("Unable to execute audit page template: %v", err)
 		util.HTTPServerError(w, err)
 	}
diff --git a/services/identity/internal/oauth/view_blessings_template.go b/services/identity/internal/oauth/view_blessings_template.go
deleted file mode 100644
index 8e34250..0000000
--- a/services/identity/internal/oauth/view_blessings_template.go
+++ /dev/null
@@ -1,122 +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 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/internal/server/identityd.go b/services/identity/internal/server/identityd.go
index 477fefc..08784bd 100644
--- a/services/identity/internal/server/identityd.go
+++ b/services/identity/internal/server/identityd.go
@@ -8,7 +8,6 @@
 import (
 	"crypto/rand"
 	"fmt"
-	"html/template"
 	mrand "math/rand"
 	"net"
 	"net/http"
@@ -34,6 +33,7 @@
 	"v.io/x/ref/services/identity/internal/handlers"
 	"v.io/x/ref/services/identity/internal/oauth"
 	"v.io/x/ref/services/identity/internal/revocation"
+	"v.io/x/ref/services/identity/internal/templates"
 	"v.io/x/ref/services/identity/internal/util"
 	services "v.io/x/ref/services/security"
 	"v.io/x/ref/services/security/discharger"
@@ -60,6 +60,7 @@
 	caveatSelector     caveats.CaveatSelector
 	emailClassifier    *util.EmailClassifier
 	rootedObjectAddrs  []naming.Endpoint
+	assetsPrefix       string
 }
 
 // NewIdentityServer returns a IdentityServer that:
@@ -67,16 +68,16 @@
 // - 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.OAuthBlesserParams, caveatSelector caveats.CaveatSelector, emailClassifier *util.EmailClassifier) *IdentityServer {
+func NewIdentityServer(oauthProvider oauth.OAuthProvider, auditor audit.Auditor, blessingLogReader auditor.BlessingLogReader, revocationManager revocation.RevocationManager, oauthBlesserParams blesser.OAuthBlesserParams, caveatSelector caveats.CaveatSelector, emailClassifier *util.EmailClassifier, assetsPrefix string) *IdentityServer {
 	return &IdentityServer{
-		oauthProvider,
-		auditor,
-		blessingLogReader,
-		revocationManager,
-		oauthBlesserParams,
-		caveatSelector,
-		emailClassifier,
-		nil,
+		oauthProvider:      oauthProvider,
+		auditor:            auditor,
+		blessingLogReader:  blessingLogReader,
+		revocationManager:  revocationManager,
+		oauthBlesserParams: oauthBlesserParams,
+		caveatSelector:     caveatSelector,
+		emailClassifier:    emailClassifier,
+		assetsPrefix:       assetsPrefix,
 	}
 }
 
@@ -135,7 +136,7 @@
 
 	// json-encoded public key and blessing names of this server
 	principal := v23.GetPrincipal(ctx)
-	http.Handle("/blessing-root", handlers.BlessingRoot{principal})
+	http.Handle("/auth/blessing-root", handlers.BlessingRoot{principal})
 
 	macaroonKey := make([]byte, 32)
 	if _, err := rand.Read(macaroonKey); err != nil {
@@ -149,7 +150,7 @@
 
 	externalHttpaddr := httpaddress(host, httpaddr)
 
-	n := "/google/"
+	n := "/auth/google/"
 	h, err := oauth.NewHandler(oauth.HandlerArgs{
 		Principal:               principal,
 		MacaroonKey:             macaroonKey,
@@ -161,6 +162,7 @@
 		OAuthProvider:           s.oauthProvider,
 		CaveatSelector:          s.caveatSelector,
 		EmailClassifier:         s.emailClassifier,
+		AssetsPrefix:            s.assetsPrefix,
 	})
 	if err != nil {
 		vlog.Fatalf("Failed to create HTTP handler for oauth authentication: %v", err)
@@ -172,8 +174,10 @@
 			Self                            security.Blessings
 			GoogleServers, DischargeServers []string
 			ListBlessingsRoute              string
+			AssetsPrefix                    string
 		}{
-			Self: principal.BlessingStore().Default(),
+			Self:         principal.BlessingStore().Default(),
+			AssetsPrefix: s.assetsPrefix,
 		}
 		if s.revocationManager != nil {
 			args.DischargeServers = appendSuffixTo(published, dischargerService)
@@ -185,7 +189,7 @@
 		if s.blessingLogReader != nil {
 			args.ListBlessingsRoute = oauth.ListBlessingsRoute
 		}
-		if err := tmpl.Execute(w, args); err != nil {
+		if err := templates.Home.Execute(w, args); err != nil {
 			vlog.Info("Failed to render template:", err)
 		}
 	})
@@ -286,42 +290,3 @@
 	}
 	return fmt.Sprintf("https://%s:%v", host, port)
 }
-
-var tmpl = template.Must(template.New("main").Parse(`<!doctype html>
-<html>
-<head>
-<meta charset="UTF-8">
-<title>Vanadium Identity Server</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">
-</head>
-<body>
-<div class="container">
-<div class="page-header"><h2>{{.Self}}</h2><h4>A Vanadium Blessing Provider</h4></div>
-<div class="well">
-This is a Vanadium identity provider that provides blessings with the name prefix <mark>{{.Self}}</mark>.
-<br/>
-The public key of this provider is {{.Self.PublicKey}}.
-<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="/blessing-root">JSON</a> object.
-</div>
-
-<div class="well">
-<ul>
-{{if .GoogleServers}}
-<li>Blessings (using Google OAuth to fetch an email address) are provided via Vanadium RPCs to: <tt>{{range .GoogleServers}}{{.}}{{end}}</tt></li>
-{{end}}
-{{if .DischargeServers}}
-<li>RevocationCaveat Discharges are provided via Vanadium RPCs to: <tt>{{range .DischargeServers}}{{.}}{{end}}</tt></li>
-{{end}}
-{{if .ListBlessingsRoute}}
-<li>You can <a class="btn btn-xs btn-primary" href="/google/{{.ListBlessingsRoute}}">enumerate</a> blessings provided with your
-email address.</li>
-{{end}}
-</ul>
-</div>
-
-</div>
-</body>
-</html>`))
diff --git a/services/identity/internal/templates/caveats.go b/services/identity/internal/templates/caveats.go
new file mode 100644
index 0000000..c4d7bf3
--- /dev/null
+++ b/services/identity/internal/templates/caveats.go
@@ -0,0 +1,137 @@
+// 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
+
+import "html/template"
+
+var SelectCaveats = template.Must(selectCaveats.Parse(headPartial))
+
+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>Blessings: Select Caveats</title>
+  <script>
+  $(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 'Add Caveat' 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 'Remove Caveat' button.
+      $(this).replaceWith('<button type="button" class="button-passive right removeCaveat">Remove Caveat</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="default-layout">
+
+<header>
+  <nav class="left">
+    <a href="#" class="logo">Vanadium</a>
+  </nav>
+
+  <nav class="main">
+    <a href="#">Select Caveats</a>
+  </nav>
+
+  <nav class="right">
+    <a href="#">{{.Extension}}</a>
+  </nav>
+</header>
+
+<main style="max-width: 80%; margin-left: 10px;">
+  <form method="POST" id="caveats-form" name="input" action="{{.MacaroonURL}}" role="form">
+  <h3>{{.Extension}}</h3>
+  <input type="text" class="hidden" name="macaroon" value="{{.Macaroon}}">
+  <div class="grid">
+    <div class="cell">
+      <label for="blessing-extension">Extension</label>
+      <input name="blessingExtension" type="text" 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>
+    <label for="required-caveat">Expiration</label>
+    <div name="required-caveat">
+      <div>
+        <label>
+        <input type="radio" name="requiredCaveat" id="requiredCaveat" value="Revocation" checked>
+        When explicitly revoked
+        </label>
+      </div>
+      <div>
+        <div>
+          <input type="radio" name="requiredCaveat" id="requiredCaveat" value="Expiry">
+          <input type="datetime-local" id="expiry" name="expiry">
+        </div>
+      </div>
+    </div>
+  </div>
+  <h4>Additional caveats</h4>
+  <span>Optional additional restrictions on the use of the blessing</span>
+  <div class="grid caveatRow">
+    <div class="cell">
+      <select name="caveat" class="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>
+
+      {{range $index, $name := $caveatList}}
+        {{if eq $name "ExpiryCaveat"}}
+        <input type="datetime-local" class="caveatInput" id="{{$name}}" name="{{$name}}">
+        {{else if eq $name "MethodCaveat"}}
+        <input type="text" id="{{$name}}" class="caveatInput" name="{{$name}}" placeholder="comma-separated method list">
+        {{else if eq $name "PeerBlessingsCaveat"}}
+        <input type="text" id="{{$name}}" class="form-control caveatInput" name="{{$name}}" placeholder="comma-separated blessing-pattern list">
+        {{end}}
+      {{end}}
+      <button type="button" class="button-passive right addCaveat">Add Caveat</button>
+    </div>
+  </div>
+  <br/>
+  <div class="grid">
+    <button class="cell button-passive" type="submit">Bless</button>
+  </div>
+  </form>
+</main>
+
+</body>
+</html>`))
diff --git a/services/identity/internal/templates/head.go b/services/identity/internal/templates/head.go
new file mode 100644
index 0000000..35426f5
--- /dev/null
+++ b/services/identity/internal/templates/head.go
@@ -0,0 +1,45 @@
+// 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
new file mode 100644
index 0000000..430675e
--- /dev/null
+++ b/services/identity/internal/templates/home.go
@@ -0,0 +1,96 @@
+// 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
+
+import "html/template"
+
+var Home = template.Must(home.Parse(headPartial))
+
+var home = template.Must(template.New("main").Parse(`<!doctype html>
+<html>
+<head>
+  {{template "head" .}}
+  <title>Vanadium Identity Provider</title>
+</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
+      </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/veyron/release-issues/issues/new?labels=www">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>
+</body>
+</html>`))
diff --git a/services/identity/internal/templates/list_blessings.go b/services/identity/internal/templates/list_blessings.go
new file mode 100644
index 0000000..4703b04
--- /dev/null
+++ b/services/identity/internal/templates/list_blessings.go
@@ -0,0 +1,130 @@
+// 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
+
+import "html/template"
+
+var ListBlessings = template.Must(listBlessings.Parse(headPartial))
+
+var listBlessings = template.Must(template.New("auditor").Parse(`<!doctype html>
+<html>
+<head>
+  {{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>
+
+  <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: "/auth/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 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%;">
+    <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 .Caveats}}
+              {{.}}</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/modules/test_identityd.go b/services/identity/modules/test_identityd.go
index bbbd3c1..db4c2a7 100644
--- a/services/identity/modules/test_identityd.go
+++ b/services/identity/modules/test_identityd.go
@@ -84,7 +84,8 @@
 		revocationManager,
 		params,
 		caveats.NewMockCaveatSelector(),
-		nil)
+		nil,
+		"")
 
 	l := v23.GetListenSpec(ctx)