"x/ref/services/identity": HTTP service for BlessUsingAccessToken

Currently the only mechanism to exchange an OAuth2 access token
for a blessing is a Vanadium RPC service. Unfortunately this makes
implementing our security model on other platforms (e.g., Mojo)
depend on the RPC system.

This CL adds a REST API to exchange an OAuth2 token for a blessing.
The API takes an OAuth2 token, the client's public key and caveats,
and returns a blessing bound to the provided public key for the
identity represented by the OAuth2 token.

The name of the resulting blessing is of the form
<idp>/<clientID>/<email> where <clientID> and <email> is the client ID
and email associated with the token respectively.

MultiPart: 1/2
Change-Id: I463fd24c2f77f03d4d1bb970336b025886368a08
diff --git a/services/identity/identity.vdl b/services/identity/identity.vdl
index 9ecba7b..978fbac 100644
--- a/services/identity/identity.vdl
+++ b/services/identity/identity.vdl
@@ -15,11 +15,14 @@
 // though the Google implementation also has informative documentation at
 // https://developers.google.com/accounts/docs/OAuth2
 //
-// WARNING: There is no binding between the channel over which the access token
-// was obtained (typically https) and the channel used to make the RPC (a
-// vanadium virtual circuit).
-// Thus, if Mallory possesses the access token associated with Alice's account,
-// she may be able to obtain a blessing with Alice's name on it.
+// WARNING: There is no binding between the channel over which the access
+// token was obtained (typically https) and the channel used to make the RPC
+// (a vanadium virtual circuit). Thus, if Mallory possesses the access token
+// associated with Alice's account she may be able to obtain a blessing with
+// Alice's name on it.
+//
+// TODO(ataly): Get rid of this service once all clients have been
+// switched to use the HTTP OAuthBlessingHandler service.
 type OAuthBlesser interface {
   // BlessUsingAccessToken uses the provided access token to obtain the email
   // address and returns a blessing along with the email address.
diff --git a/services/identity/identity.vdl.go b/services/identity/identity.vdl.go
index dd261eb..8aa9da2 100644
--- a/services/identity/identity.vdl.go
+++ b/services/identity/identity.vdl.go
@@ -48,11 +48,14 @@
 // though the Google implementation also has informative documentation at
 // https://developers.google.com/accounts/docs/OAuth2
 //
-// WARNING: There is no binding between the channel over which the access token
-// was obtained (typically https) and the channel used to make the RPC (a
-// vanadium virtual circuit).
-// Thus, if Mallory possesses the access token associated with Alice's account,
-// she may be able to obtain a blessing with Alice's name on it.
+// WARNING: There is no binding between the channel over which the access
+// token was obtained (typically https) and the channel used to make the RPC
+// (a vanadium virtual circuit). Thus, if Mallory possesses the access token
+// associated with Alice's account she may be able to obtain a blessing with
+// Alice's name on it.
+//
+// TODO(ataly): Get rid of this service once all clients have been
+// switched to use the HTTP OAuthBlessingHandler service.
 type OAuthBlesserClientMethods interface {
 	// BlessUsingAccessToken uses the provided access token to obtain the email
 	// address and returns a blessing along with the email address.
@@ -96,11 +99,14 @@
 // though the Google implementation also has informative documentation at
 // https://developers.google.com/accounts/docs/OAuth2
 //
-// WARNING: There is no binding between the channel over which the access token
-// was obtained (typically https) and the channel used to make the RPC (a
-// vanadium virtual circuit).
-// Thus, if Mallory possesses the access token associated with Alice's account,
-// she may be able to obtain a blessing with Alice's name on it.
+// WARNING: There is no binding between the channel over which the access
+// token was obtained (typically https) and the channel used to make the RPC
+// (a vanadium virtual circuit). Thus, if Mallory possesses the access token
+// associated with Alice's account she may be able to obtain a blessing with
+// Alice's name on it.
+//
+// TODO(ataly): Get rid of this service once all clients have been
+// switched to use the HTTP OAuthBlessingHandler service.
 type OAuthBlesserServerMethods interface {
 	// BlessUsingAccessToken uses the provided access token to obtain the email
 	// address and returns a blessing along with the email address.
@@ -166,7 +172,7 @@
 var descOAuthBlesser = rpc.InterfaceDesc{
 	Name:    "OAuthBlesser",
 	PkgPath: "v.io/x/ref/services/identity",
-	Doc:     "// OAuthBlesser exchanges OAuth access tokens for\n// an email address from an OAuth-based identity provider and uses the email\n// address obtained to bless the client.\n//\n// OAuth is described in RFC 6749 (http://tools.ietf.org/html/rfc6749),\n// though the Google implementation also has informative documentation at\n// https://developers.google.com/accounts/docs/OAuth2\n//\n// WARNING: There is no binding between the channel over which the access token\n// was obtained (typically https) and the channel used to make the RPC (a\n// vanadium virtual circuit).\n// Thus, if Mallory possesses the access token associated with Alice's account,\n// she may be able to obtain a blessing with Alice's name on it.",
+	Doc:     "// OAuthBlesser exchanges OAuth access tokens for\n// an email address from an OAuth-based identity provider and uses the email\n// address obtained to bless the client.\n//\n// OAuth is described in RFC 6749 (http://tools.ietf.org/html/rfc6749),\n// though the Google implementation also has informative documentation at\n// https://developers.google.com/accounts/docs/OAuth2\n//\n// WARNING: There is no binding between the channel over which the access\n// token was obtained (typically https) and the channel used to make the RPC\n// (a vanadium virtual circuit). Thus, if Mallory possesses the access token\n// associated with Alice's account she may be able to obtain a blessing with\n// Alice's name on it.\n//\n// TODO(ataly): Get rid of this service once all clients have been\n// switched to use the HTTP OAuthBlessingHandler service.",
 	Methods: []rpc.MethodDesc{
 		{
 			Name: "BlessUsingAccessToken",
diff --git a/services/identity/identitylib/test_identityd.go b/services/identity/identitylib/test_identityd.go
index 054d5c5..1038239 100644
--- a/services/identity/identitylib/test_identityd.go
+++ b/services/identity/identitylib/test_identityd.go
@@ -68,14 +68,23 @@
 		}
 	}
 
+	mockClientID := "test-client-id"
+	mockClientName := "test-client"
+
 	auditor, reader := auditor.NewMockBlessingAuditor()
 	revocationManager := revocation.NewMockRevocationManager(ctx)
-	oauthProvider := oauth.NewMockOAuth("testemail@example.com")
+	oauthProvider := oauth.NewMockOAuth("testemail@example.com", mockClientID)
 
 	params := blesser.OAuthBlesserParams{
 		OAuthProvider:     oauthProvider,
 		BlessingDuration:  duration,
 		RevocationManager: revocationManager,
+		AccessTokenClients: []oauth.AccessTokenClient{
+			oauth.AccessTokenClient{
+				Name:     mockClientName,
+				ClientID: mockClientID,
+			},
+		},
 	}
 
 	s := server.NewIdentityServer(
diff --git a/services/identity/internal/blesser/oauth.go b/services/identity/internal/blesser/oauth.go
index 40138e6..980829e 100644
--- a/services/identity/internal/blesser/oauth.go
+++ b/services/identity/internal/blesser/oauth.go
@@ -18,20 +18,11 @@
 	"v.io/v23/security"
 )
 
-type oauthBlesser struct {
-	oauthProvider      oauth.OAuthProvider
-	authcodeClient     struct{ ID, Secret string }
-	accessTokenClients []oauth.AccessTokenClient
-	duration           time.Duration
-	dischargerLocation string
-	revocationManager  revocation.RevocationManager
-}
-
-// OAuthBlesserParams represents all the parameters provided to NewOAuthBlesserServer
+// OAuthBlesserParams represents all the parameters provided to NewOAuthBlessingServer.
 type OAuthBlesserParams struct {
 	// The OAuth provider that must have issued the access tokens accepted by ths service.
 	OAuthProvider oauth.OAuthProvider
-	// The OAuth client IDs and names for the clients of the BlessUsingAccessToken RPCs.
+	// The OAuth client IDs and names for the clients of this service.
 	AccessTokenClients []oauth.AccessTokenClient
 	// The object name of the discharger service. If this is empty then revocation caveats will not be granted.
 	DischargerLocation string
@@ -41,6 +32,15 @@
 	BlessingDuration time.Duration
 }
 
+type oauthBlesser struct {
+	oauthProvider      oauth.OAuthProvider
+	authcodeClient     struct{ ID, Secret string }
+	accessTokenClients []oauth.AccessTokenClient
+	duration           time.Duration
+	dischargerLocation string
+	revocationManager  revocation.RevocationManager
+}
+
 // NewOAuthBlesserServer provides an identity.OAuthBlesserService that uses OAuth2
 // access tokens to obtain the username of a client and provide blessings with that
 // name.
@@ -48,6 +48,8 @@
 // Blessings generated by this service carry a third-party revocation caveat if a
 // RevocationManager is specified by the params or they carry an ExpiryCaveat that
 // expires after the duration specified by the params.
+// TODO(ataly): Get rid of this service once all clients have been
+// switched to use the HTTP OAuthBlessingHandler service.
 func NewOAuthBlesserServer(p OAuthBlesserParams) identity.OAuthBlesserServerStub {
 	return identity.OAuthBlesserServer(&oauthBlesser{
 		oauthProvider:      p.OAuthProvider,
@@ -60,7 +62,14 @@
 
 func (b *oauthBlesser) BlessUsingAccessToken(ctx *context.T, call rpc.ServerCall, accessToken string) (security.Blessings, string, error) {
 	var noblessings security.Blessings
-	email, clientName, err := b.oauthProvider.GetEmailAndClientName(accessToken, b.accessTokenClients)
+	if len(b.accessTokenClients) == 0 {
+		return noblessings, "", fmt.Errorf("no expected AccessTokenClients specified")
+	}
+	email, clientID, err := b.oauthProvider.GetEmailAndClientID(accessToken)
+	if err != nil {
+		return noblessings, "", err
+	}
+	clientName, err := oauth.ClientName(clientID, b.accessTokenClients)
 	if err != nil {
 		return noblessings, "", err
 	}
@@ -69,7 +78,14 @@
 
 func (b *oauthBlesser) BlessUsingAccessTokenWithCaveats(ctx *context.T, call rpc.ServerCall, accessToken string, caveats []security.Caveat) (security.Blessings, string, error) {
 	var noblessings security.Blessings
-	email, clientName, err := b.oauthProvider.GetEmailAndClientName(accessToken, b.accessTokenClients)
+	if len(b.accessTokenClients) == 0 {
+		return noblessings, "", fmt.Errorf("no expected AccessTokenClients specified")
+	}
+	email, clientID, err := b.oauthProvider.GetEmailAndClientID(accessToken)
+	if err != nil {
+		return noblessings, "", err
+	}
+	clientName, err := oauth.ClientName(clientID, b.accessTokenClients)
 	if err != nil {
 		return noblessings, "", err
 	}
diff --git a/services/identity/internal/blesser/oauth_test.go b/services/identity/internal/blesser/oauth_test.go
index bb8941f..6b56a72 100644
--- a/services/identity/internal/blesser/oauth_test.go
+++ b/services/identity/internal/blesser/oauth_test.go
@@ -27,9 +27,17 @@
 		ctx, call      = fakeContextAndCall(provider, user)
 	)
 	mockEmail := "testemail@example.com"
+	mockClientID := "test-client-id"
+	mockClientName := "test-client"
 	blesser := NewOAuthBlesserServer(OAuthBlesserParams{
-		OAuthProvider:    oauth.NewMockOAuth(mockEmail),
+		OAuthProvider:    oauth.NewMockOAuth(mockEmail, mockClientID),
 		BlessingDuration: time.Hour,
+		AccessTokenClients: []oauth.AccessTokenClient{
+			oauth.AccessTokenClient{
+				Name:     mockClientName,
+				ClientID: mockClientID,
+			},
+		},
 	})
 
 	b, extension, err := blesser.BlessUsingAccessToken(ctx, call, "test-access-token")
@@ -37,7 +45,7 @@
 		t.Errorf("BlessUsingAccessToken failed: %v", err)
 	}
 
-	wantExtension := join(mockEmail, oauth.MockClient)
+	wantExtension := join(mockEmail, mockClientName)
 	if extension != wantExtension {
 		t.Errorf("got extension: %s, want: %s", extension, wantExtension)
 	}
@@ -69,9 +77,17 @@
 		ctx, call      = fakeContextAndCall(provider, user)
 	)
 	mockEmail := "testemail@example.com"
+	mockClientID := "test-client-id"
+	mockClientName := "test-client"
 	blesser := NewOAuthBlesserServer(OAuthBlesserParams{
-		OAuthProvider:    oauth.NewMockOAuth(mockEmail),
+		OAuthProvider:    oauth.NewMockOAuth(mockEmail, mockClientID),
 		BlessingDuration: time.Hour,
+		AccessTokenClients: []oauth.AccessTokenClient{
+			oauth.AccessTokenClient{
+				Name:     mockClientName,
+				ClientID: mockClientID,
+			},
+		},
 	})
 
 	expiryCav, err := security.NewExpiryCaveat(time.Now().Add(time.Minute))
@@ -89,7 +105,7 @@
 		t.Errorf("BlessUsingAccessToken failed: %v", err)
 	}
 
-	wantExtension := join(mockEmail, oauth.MockClient)
+	wantExtension := join(mockEmail, mockClientName)
 	if extension != wantExtension {
 		t.Errorf("got extension: %s, want: %s", extension, wantExtension)
 	}
diff --git a/services/identity/internal/handlers/bless.go b/services/identity/internal/handlers/bless.go
new file mode 100644
index 0000000..e7607c5
--- /dev/null
+++ b/services/identity/internal/handlers/bless.go
@@ -0,0 +1,160 @@
+// 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 handlers
+
+import (
+	"encoding/base64"
+	"fmt"
+	"net/http"
+	"strings"
+	"time"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/security"
+	"v.io/v23/vom"
+	"v.io/x/ref/services/identity/internal/blesser"
+	"v.io/x/ref/services/identity/internal/util"
+)
+
+const (
+	PublicKeyFormKey   = "public_key"
+	AccessTokenFormKey = "token"
+	CaveatsFormKey     = "caveats"
+)
+
+type accessTokenBlesser struct {
+	ctx    *context.T
+	params blesser.OAuthBlesserParams
+}
+
+// NewOAuthBlessingHandler returns an http.Handler that uses OAuth2 access tokens
+// to obtain the username of the requestor and reponds with blessings for that username.
+//
+// The blessings are namespaced under the ClientID for the access token. In particular,
+// the name of the granted blessing is of the form <idp>/<clientID>/<email> where <idp>
+// is the name of the default blessings used by the identity provider.
+//
+// Blessings generated by this service carry a third-party revocation caveat if a
+// RevocationManager is specified by the params or they carry an ExpiryCaveat that
+// expires after the duration specified by the params.
+//
+// The handler expects the following parameters in the form data sent during a
+// request
+// - "public_key": Base64 DER encoded PKIX representation of the client's public key
+// - "caveats": Base64 VOM encoded list of caveats
+// - "token": OAuth2 access token
+//
+// WARNINGS:
+//   - There is no binding between the channel over which the access token
+//     was obtained and the channel used to make this request.
+//   - There is no "proof of possession of private key" required by the server.
+// Thus, if Mallory (attacker) possesses the access token associated with Alice's
+// account (victim), she may be able to obtain a blessing with Alice's name on it
+// for any public key of her choice.
+func NewOAuthBlessingHandler(ctx *context.T, params blesser.OAuthBlesserParams) http.Handler {
+	return &accessTokenBlesser{ctx, params}
+}
+
+func (a *accessTokenBlesser) blessingCaveats(r *http.Request, p security.Principal) ([]security.Caveat, error) {
+	caveatsVom, err := base64.URLEncoding.DecodeString(r.FormValue(CaveatsFormKey))
+	if err != nil {
+		return nil, fmt.Errorf("failed to base64 decode caveats: %v", err)
+	}
+
+	var caveats []security.Caveat
+	if err := vom.Decode(caveatsVom, &caveats); err != nil {
+		return nil, fmt.Errorf("failed to VOM decode caveats: %v", err)
+	}
+
+	// TODO(suharshs, ataly): Should we ensure that we have at least a
+	// revocation or expiry caveat?
+	if len(caveats) == 0 {
+		var (
+			cav security.Caveat
+			err error
+		)
+		if a.params.RevocationManager != nil {
+			cav, err = a.params.RevocationManager.NewCaveat(p.PublicKey(), a.params.DischargerLocation)
+		} else {
+			cav, err = security.NewExpiryCaveat(time.Now().Add(a.params.BlessingDuration))
+		}
+		if err != nil {
+			return nil, fmt.Errorf("failed to construct caveats: %v", err)
+		}
+		caveats = append(caveats, cav)
+	}
+	return caveats, nil
+
+}
+
+func (a *accessTokenBlesser) remotePublicKey(r *http.Request) (security.PublicKey, error) {
+	publicKeyVom, err := base64.URLEncoding.DecodeString(r.FormValue(PublicKeyFormKey))
+	if err != nil {
+		return nil, fmt.Errorf("failed to base64 decode public key: %v", err)
+	}
+	return security.UnmarshalPublicKey(publicKeyVom)
+}
+
+func (a *accessTokenBlesser) blessingExtension(r *http.Request) (string, error) {
+	email, clientID, err := a.params.OAuthProvider.GetEmailAndClientID(r.FormValue(AccessTokenFormKey))
+	if err != nil {
+		return "", err
+	}
+	// We use <clientID>/<email> as the extension in order to namespace the blessing under
+	// the <clientID>. This has the downside that the blessing cannot be used to act on
+	// behalf of the user, i.e., services access controlled to blessings matching"<idp>/<email>"
+	// would not authorize this blessing.
+	//
+	// The alternative is to use the extension <email>/<clientID> however this is risky as it
+	// may provide too much authority to the app, especially since we don't have a set of default
+	// caveats to apply to the blessing.
+	//
+	// TODO(ataly, ashankar): Think about changing to the extension <email>/<clientID>.
+	return strings.Join([]string{clientID, email}, security.ChainSeparator), nil
+}
+
+func (a *accessTokenBlesser) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	remoteKey, err := a.remotePublicKey(r)
+	if err != nil {
+		a.ctx.Info("Failed to decode public key [%v] for request %#v", err, r)
+		util.HTTPServerError(w, err)
+		return
+	}
+
+	p := v23.GetPrincipal(a.ctx)
+	with := p.BlessingStore().Default()
+
+	caveats, err := a.blessingCaveats(r, p)
+	if err != nil {
+		a.ctx.Info("Failed to constuct caveats for blessing [%v] for request %#v", err, r)
+		util.HTTPServerError(w, err)
+		return
+	}
+
+	extension, err := a.blessingExtension(r)
+	if err != nil {
+		a.ctx.Info("Failed to process access token [%v] for request %#v", err, r)
+		util.HTTPServerError(w, err)
+		return
+	}
+
+	blessings, err := p.Bless(remoteKey, with, extension, caveats[0], caveats[1:]...)
+	if err != nil {
+		a.ctx.Info("Failed to Bless [%v] for request %#v", err, r)
+		util.HTTPServerError(w, fmt.Errorf("Bless failed: %v", err))
+		return
+	}
+
+	blessingsVom, err := vom.Encode(blessings)
+	if err != nil {
+		a.ctx.Info("Failed to VOM encode blessings [%v] for request %#v", err, r)
+		util.HTTPServerError(w, fmt.Errorf("failed to VOM encode blessings: %v", err))
+		return
+	}
+	blessingsVomB64 := base64.URLEncoding.EncodeToString(blessingsVom)
+	w.Header().Set("Content-Type", "application/text")
+	w.Write([]byte(blessingsVomB64))
+}
diff --git a/services/identity/internal/handlers/handlers_test.go b/services/identity/internal/handlers/handlers_test.go
index 353c579..3740451 100644
--- a/services/identity/internal/handlers/handlers_test.go
+++ b/services/identity/internal/handlers/handlers_test.go
@@ -2,20 +2,32 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package handlers
+package handlers_test
 
 import (
+	"bytes"
 	"encoding/base64"
 	"encoding/json"
+	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
+	"net/url"
 	"reflect"
 	"sort"
 	"testing"
+	"time"
 
+	"v.io/v23"
 	"v.io/v23/security"
+	"v.io/v23/vom"
 
+	_ "v.io/x/ref/runtime/factories/generic"
 	"v.io/x/ref/services/identity"
+	"v.io/x/ref/services/identity/internal/blesser"
+	"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/test"
 	"v.io/x/ref/test/testutil"
 )
 
@@ -24,7 +36,7 @@
 	blessingNames := []string{"test-root"}
 	p := testutil.NewPrincipal(blessingNames...)
 
-	ts := httptest.NewServer(BlessingRoot{p})
+	ts := httptest.NewServer(handlers.BlessingRoot{p})
 	defer ts.Close()
 	response, err := http.Get(ts.URL)
 	if err != nil {
@@ -56,3 +68,165 @@
 		t.Errorf("Response has incorrect public key.  Got %v, want %v", got, want)
 	}
 }
+
+func TestBlessUsingAccessToken(t *testing.T) {
+	var (
+		blesserPrin = testutil.NewPrincipal("blesser")
+		blesseePrin = testutil.NewPrincipal("blessee")
+
+		methodCav, _ = security.NewMethodCaveat("foo")
+		expiryCav, _ = security.NewExpiryCaveat(time.Now().Add(time.Hour))
+
+		mkReqURL = func(baseURLStr string, caveats []security.Caveat) string {
+			baseURL, err := url.Parse(baseURLStr)
+			if err != nil {
+				t.Fatal(err)
+			}
+			caveatsVom, err := vom.Encode(caveats)
+			if err != nil {
+				t.Fatal(err)
+			}
+			keyBytes, err := blesseePrin.PublicKey().MarshalBinary()
+			if err != nil {
+				t.Fatal(err)
+			}
+			params := url.Values{}
+			params.Add(handlers.CaveatsFormKey, base64.URLEncoding.EncodeToString(caveatsVom))
+			params.Add(handlers.PublicKeyFormKey, base64.URLEncoding.EncodeToString(keyBytes))
+			params.Add(handlers.AccessTokenFormKey, "mocktoken")
+			baseURL.RawQuery = params.Encode()
+			return baseURL.String()
+		}
+
+		decodeBlessings = func(blessingsVomB64 string) security.Blessings {
+			blessingsVom, err := base64.URLEncoding.DecodeString(blessingsVomB64)
+			if err != nil {
+				t.Fatal(err)
+			}
+			var res security.Blessings
+			if err := vom.Decode(blessingsVom, &res); err != nil {
+				t.Fatal(err)
+			}
+			return res
+		}
+	)
+
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+	var err error
+	if ctx, err = v23.WithPrincipal(ctx, blesserPrin); err != nil {
+		t.Fatal(err)
+	}
+
+	// Make the blessee trust the blesser's roots
+	if err := blesseePrin.AddToRoots(blesserPrin.BlessingStore().Default()); err != nil {
+		t.Fatal(err)
+	}
+
+	testEmail := "foo@bar.com"
+	testClientID := "test-client-id"
+	revocationManager := revocation.NewMockRevocationManager(ctx)
+	oauthProvider := oauth.NewMockOAuth(testEmail, testClientID)
+
+	testcases := []struct {
+		params  blesser.OAuthBlesserParams
+		caveats []security.Caveat
+	}{
+		{
+			blesser.OAuthBlesserParams{
+				OAuthProvider:    oauthProvider,
+				BlessingDuration: 24 * time.Hour,
+			},
+			nil,
+		},
+		{
+			blesser.OAuthBlesserParams{
+				OAuthProvider:     oauthProvider,
+				RevocationManager: revocationManager,
+			},
+			nil,
+		},
+		{
+			blesser.OAuthBlesserParams{
+				OAuthProvider:     oauthProvider,
+				RevocationManager: revocationManager,
+			},
+			[]security.Caveat{expiryCav, methodCav},
+		},
+	}
+	for _, testcase := range testcases {
+		ts := httptest.NewServer(handlers.NewOAuthBlessingHandler(ctx, testcase.params))
+		defer ts.Close()
+
+		response, err := http.Get(mkReqURL(ts.URL, testcase.caveats))
+		if err != nil {
+			t.Fatal(err)
+		}
+		blessingsVomB64, err := ioutil.ReadAll(response.Body)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		blessings := decodeBlessings(string(blessingsVomB64))
+
+		// Blessing should be bound to the blessee.
+		if got, want := blessings.PublicKey(), blesseePrin.PublicKey(); !reflect.DeepEqual(got, want) {
+			t.Errorf("got blessings for public key %v, want blessings for public key %v", got, want)
+		}
+
+		// Verify the name and caveats on the blessings.
+		binfo := blesseePrin.BlessingsInfo(blessings)
+		if len(binfo) != 1 {
+			t.Errorf("got blessings with %d names, want blessings with 1 name", len(binfo))
+		}
+		wantName := "blesser" + security.ChainSeparator + testClientID + security.ChainSeparator + testEmail
+		caveats, ok := binfo[wantName]
+		if !ok {
+			t.Errorf("expected blessing with name %v, got none", wantName)
+		}
+
+		if len(testcase.caveats) > 0 {
+			// The blessing must have exactly those caveats that were provided in the request.
+			if !caveatsMatch(t, caveats, testcase.caveats) {
+				t.Errorf("got blessings with caveats %v, want blessings with caveats %v", caveats, testcase.caveats)
+			}
+		} else if len(caveats) != 1 {
+			t.Errorf("got blessings with %d caveats, want blessings with 1 caveats", len(caveats))
+		} else if testcase.params.RevocationManager != nil && caveats[0].Id != security.PublicKeyThirdPartyCaveat.Id {
+			// The blessing must have a third-party revocation caveat.
+			t.Errorf("got blessings with caveat (%v), want blessings with a PublicKeyThirdPartyCaveat", caveats[0].Id)
+		} else if testcase.params.RevocationManager == nil && caveats[0].Id != security.ExpiryCaveat.Id {
+			// The blessing must have an expiry caveat.
+			t.Errorf("got blessings with caveat (%v), want blessings with an ExpiryCaveat", caveats[0].Id)
+		}
+	}
+}
+
+type caveatsSorter struct {
+	caveats []security.Caveat
+	t       *testing.T
+}
+
+func (c caveatsSorter) Len() int      { return len(c.caveats) }
+func (c caveatsSorter) Swap(i, j int) { c.caveats[i], c.caveats[j] = c.caveats[j], c.caveats[i] }
+func (c caveatsSorter) Less(i, j int) bool {
+	b_i, err := vom.Encode(c.caveats[i])
+	if err != nil {
+		c.t.Fatal(err)
+	}
+	b_j, err := vom.Encode(c.caveats[j])
+	if err != nil {
+		c.t.Fatal(err)
+	}
+	return bytes.Compare(b_i, b_j) == -1
+}
+
+func caveatsMatch(t *testing.T, got, want []security.Caveat) bool {
+	if len(got) != len(want) {
+		return false
+	}
+	g, w := caveatsSorter{got, t}, caveatsSorter{want, t}
+	sort.Sort(g)
+	sort.Sort(w)
+	return reflect.DeepEqual(g, w)
+}
diff --git a/services/identity/internal/identityd_test/main.go b/services/identity/internal/identityd_test/main.go
index a34f35f..3b3b152 100644
--- a/services/identity/internal/identityd_test/main.go
+++ b/services/identity/internal/identityd_test/main.go
@@ -90,14 +90,23 @@
 		}
 	}
 
+	mockClientID := "test-client-id"
+	mockClientName := "test-client"
+
 	auditor, reader := auditor.NewMockBlessingAuditor()
 	revocationManager := revocation.NewMockRevocationManager(ctx)
-	oauthProvider := oauth.NewMockOAuth(oauthEmail)
+	oauthProvider := oauth.NewMockOAuth(oauthEmail, mockClientID)
 
 	params := blesser.OAuthBlesserParams{
 		OAuthProvider:     oauthProvider,
 		BlessingDuration:  duration,
 		RevocationManager: revocationManager,
+		AccessTokenClients: []oauth.AccessTokenClient{
+			oauth.AccessTokenClient{
+				Name:     mockClientName,
+				ClientID: mockClientID,
+			},
+		},
 	}
 
 	caveatSelector := caveats.NewMockCaveatSelector()
diff --git a/services/identity/internal/oauth/googleoauth.go b/services/identity/internal/oauth/googleoauth.go
index a2f20de..4f09b09 100644
--- a/services/identity/internal/oauth/googleoauth.go
+++ b/services/identity/internal/oauth/googleoauth.go
@@ -105,13 +105,9 @@
 	return gtoken.Email, nil
 }
 
-// GetEmailAndClientName uses Google's tokeninfo API to verify that the token has been issued
-// for one of the provided 'accessTokenClients' and if so returns the email and client name
-// from the tokeninfo obtained.
-func (g *googleOAuth) GetEmailAndClientName(accessToken string, accessTokenClients []AccessTokenClient) (string, string, error) {
-	if len(accessTokenClients) == 0 {
-		return "", "", fmt.Errorf("no expected AccessTokenClients specified")
-	}
+// GetEmailAndClientID uses Google's tokeninfo API to determine the email and clientID
+// associated with the token.
+func (g *googleOAuth) GetEmailAndClientID(accessToken string) (string, string, error) {
 	// As per https://developers.google.com/accounts/docs/OAuth2UserAgent#validatetoken
 	// we obtain the 'info' for the token via an HTTP roundtrip to Google.
 	tokeninfo, err := http.Get(g.verifyURL + "access_token=" + accessToken)
@@ -136,25 +132,12 @@
 	if err := json.NewDecoder(tokeninfo.Body).Decode(&token); err != nil {
 		return "", "", fmt.Errorf("invalid JSON response from Google's tokeninfo API: %v", err)
 	}
-	var client AccessTokenClient
-	audienceMatch := false
-	for _, c := range accessTokenClients {
-		if token.Audience == c.ClientID {
-			client = c
-			audienceMatch = true
-			break
-		}
-	}
-	if !audienceMatch {
-		g.ctx.Infof("Got access token [%+v], wanted one of client ids %v", token, accessTokenClients)
-		return "", "", fmt.Errorf("token not meant for this purpose, confused deputy? https://developers.google.com/accounts/docs/OAuth2UserAgent#validatetoken")
-	}
 	// We check both "verified_email" and "email_verified" here because the token response sometimes
 	// contains one and sometimes contains the other.
 	if !token.VerifiedEmail && !token.EmailVerified {
 		return "", "", fmt.Errorf("email not verified")
 	}
-	return token.Email, client.Name, nil
+	return token.Email, token.Audience, nil
 }
 
 func (g *googleOAuth) oauthConfig(redirectUrl string) *oauth2.Config {
diff --git a/services/identity/internal/oauth/mockoauth.go b/services/identity/internal/oauth/mockoauth.go
index 3a0e828..897fc69 100644
--- a/services/identity/internal/oauth/mockoauth.go
+++ b/services/identity/internal/oauth/mockoauth.go
@@ -4,15 +4,14 @@
 
 package oauth
 
-const MockClient = "test-client"
-
 // mockOAuth is a mock OAuthProvider for use in tests.
 type mockOAuth struct {
-	email string
+	email    string
+	clientID string
 }
 
-func NewMockOAuth(mockEmail string) OAuthProvider {
-	return &mockOAuth{email: mockEmail}
+func NewMockOAuth(mockEmail, mockClientID string) OAuthProvider {
+	return &mockOAuth{email: mockEmail, clientID: mockClientID}
 }
 
 func (m *mockOAuth) AuthURL(redirectUrl string, state string, _ AuthURLApproval) string {
@@ -23,6 +22,6 @@
 	return m.email, nil
 }
 
-func (m *mockOAuth) GetEmailAndClientName(string, []AccessTokenClient) (string, string, error) {
-	return m.email, MockClient, nil
+func (m *mockOAuth) GetEmailAndClientID(string) (string, string, error) {
+	return m.email, m.clientID, nil
 }
diff --git a/services/identity/internal/oauth/oauth_provider.go b/services/identity/internal/oauth/oauth_provider.go
index 8ba6239..1928a54 100644
--- a/services/identity/internal/oauth/oauth_provider.go
+++ b/services/identity/internal/oauth/oauth_provider.go
@@ -4,14 +4,6 @@
 
 package oauth
 
-// AccessTokenClient represents a client of an OAuthProvider.
-type AccessTokenClient struct {
-	// Descriptive name of the client.
-	Name string
-	// OAuth Client ID.
-	ClientID string
-}
-
 // Option to OAuthProvider.AuthURL controlling whether previously provided user consent can be re-used.
 type AuthURLApproval bool
 
@@ -28,9 +20,6 @@
 	// ExchangeAuthCodeForEmail exchanges the provided authCode for the email of the
 	// authenticated user on behalf of the token has been issued.
 	ExchangeAuthCodeForEmail(authCode string, url string) (email string, err error)
-	// GetEmailAndClientName verifies that the provided 'accessToken' is issued to one
-	// of the provided accessTokenClients, and if so returns the email of the
-	// authenticated user on behalf of whom the token has been issued, and also the
-	// client name associated with the token.
-	GetEmailAndClientName(accessToken string, accessTokenClients []AccessTokenClient) (email string, clientName string, err error)
+	// GetEmailAndClientID returns the email and clientID associated with the token.
+	GetEmailAndClientID(accessToken string) (email string, clientID string, err error)
 }
diff --git a/services/identity/internal/oauth/utils.go b/services/identity/internal/oauth/utils.go
index 23b9fb5..cf4e341 100644
--- a/services/identity/internal/oauth/utils.go
+++ b/services/identity/internal/oauth/utils.go
@@ -10,6 +10,14 @@
 	"io"
 )
 
+// AccessTokenClient represents a client of an OAuthProvider.
+type AccessTokenClient struct {
+	// Descriptive name of the client.
+	Name string
+	// OAuth Client ID.
+	ClientID string
+}
+
 // ClientIDFromJSON parses JSON-encoded API access information in 'r' and returns
 // the extracted ClientID.
 // This JSON-encoded data is typically available as a download from the Google
@@ -52,6 +60,18 @@
 	return
 }
 
+// ClientName checks if the provided clientID is present in one of the provided
+// 'clients' and if so returns the corresponding client name. It returns an error
+// otherwise.
+func ClientName(clientID string, clients []AccessTokenClient) (string, error) {
+	for _, c := range clients {
+		if clientID == c.ClientID {
+			return c.Name, nil
+		}
+	}
+	return "", fmt.Errorf("unrecognized client ID, confused deputy? https://developers.google.com/accounts/docs/OAuth2UserAgent#validatetoken")
+}
+
 func decodeAccessMapFromJSON(r io.Reader) (data map[string]interface{}, typ string, err error) {
 	var full map[string]interface{}
 	if err = json.NewDecoder(r).Decode(&full); err != nil {
diff --git a/services/identity/internal/server/identityd.go b/services/identity/internal/server/identityd.go
index e85a8a4..a14a6ba 100644
--- a/services/identity/internal/server/identityd.go
+++ b/services/identity/internal/server/identityd.go
@@ -139,7 +139,7 @@
 		ctx.Fatalf("macaroonKey generation failed: %v", err)
 	}
 
-	rpcServer, published, err := s.setupServices(ctx, listenSpec, macaroonKey)
+	rpcServer, published, err := s.setupBlessingServices(ctx, listenSpec, macaroonKey)
 	if err != nil {
 		ctx.Fatalf("Failed to setup vanadium services for blessing: %v", err)
 	}
@@ -207,8 +207,9 @@
 	return names
 }
 
-// Starts the blessing services and the discharging service on the same port.
-func (s *IdentityServer) setupServices(ctx *context.T, listenSpec *rpc.ListenSpec, macaroonKey []byte) (rpc.Server, []string, error) {
+// Starts the Vanadium and HTTP services for blessing, and the Vanadium service for discharging.
+// All Vanadium services are started on the same port.
+func (s *IdentityServer) setupBlessingServices(ctx *context.T, listenSpec *rpc.ListenSpec, macaroonKey []byte) (rpc.Server, []string, error) {
 	server, err := v23.NewServer(ctx)
 	if err != nil {
 		return nil, nil, fmt.Errorf("failed to create new rpc.Server: %v", err)
@@ -228,11 +229,17 @@
 	} else {
 		rootedObjectAddr = s.rootedObjectAddrs[0].Name()
 	}
-	dispatcher := newDispatcher(macaroonKey, oauthBlesserParams(s.oauthBlesserParams, rootedObjectAddr))
+
+	params := oauthBlesserParams(s.oauthBlesserParams, rootedObjectAddr)
+	dispatcher := newDispatcher(macaroonKey, params)
 	if err := server.ServeDispatcher(objectAddr, dispatcher); err != nil {
 		return nil, nil, fmt.Errorf("failed to start Vanadium services: %v", err)
 	}
-	ctx.Infof("Blessing and discharger services will be published at %v", rootedObjectAddr)
+	ctx.Infof("Vanadium Blessing and discharger services will be published at %v", rootedObjectAddr)
+
+	// Start the HTTP Handler for the OAuth2 access token based blesser.
+	http.Handle("/auth/google/bless", handlers.NewOAuthBlessingHandler(ctx, params))
+
 	return server, []string{rootedObjectAddr}, nil
 }