"core": Support BlessingUsingAccessTOken in identityd_test
This CL refactors the identity server implementation in order
to support 'BlessUsingAccessToken' in test mode. In test mode
this method simply returns a blessing under a dummy email address.
Apart from this the CL also introduces tests for OAuthBlesser service
and also does some minor cleanups.
Change-Id: Ieaf6764cd77d419081324c29e05a024002d210c1
diff --git a/lib/modules/core/test_identityd.go b/lib/modules/core/test_identityd.go
index 9330214..b49859d 100644
--- a/lib/modules/core/test_identityd.go
+++ b/lib/modules/core/test_identityd.go
@@ -69,19 +69,21 @@
auditor, reader := auditor.NewMockBlessingAuditor()
revocationManager := revocation.NewMockRevocationManager()
+ oauthProvider := oauth.NewMockOAuth()
- googleParams := blesser.GoogleParams{
+ params := blesser.OAuthBlesserParams{
+ OAuthProvider: oauthProvider,
BlessingDuration: duration,
DomainRestriction: *googleDomain,
RevocationManager: revocationManager,
}
s := server.NewIdentityServer(
- oauth.NewMockOAuth(),
+ oauthProvider,
auditor,
reader,
revocationManager,
- googleParams,
+ params,
caveats.NewMockCaveatSelector())
l := initListenSpec(ifl)
diff --git a/services/identity/blesser/macaroon.go b/services/identity/blesser/macaroon.go
index 4e58635..259aafe 100644
--- a/services/identity/blesser/macaroon.go
+++ b/services/identity/blesser/macaroon.go
@@ -5,6 +5,7 @@
"time"
"v.io/core/veyron/services/identity"
+ "v.io/core/veyron/services/identity/oauth"
"v.io/core/veyron/services/identity/util"
"v.io/core/veyron2/ipc"
@@ -16,13 +17,6 @@
key []byte
}
-// BlessingMacaroon contains the data that is encoded into the macaroon for creating blessings.
-type BlessingMacaroon struct {
- Creation time.Time
- Caveats []security.Caveat
- Name string
-}
-
// NewMacaroonBlesserServer provides an identity.MacaroonBlesser Service that generates blessings
// after unpacking a BlessingMacaroon.
func NewMacaroonBlesserServer(key []byte) identity.MacaroonBlesserServerStub {
@@ -35,7 +29,7 @@
if err != nil {
return empty, err
}
- var m BlessingMacaroon
+ var m oauth.BlessingMacaroon
if err := vom2.Decode(inputs, &m); err != nil {
return empty, err
}
diff --git a/services/identity/blesser/macaroon_test.go b/services/identity/blesser/macaroon_test.go
index 3973489..9fa1923 100644
--- a/services/identity/blesser/macaroon_test.go
+++ b/services/identity/blesser/macaroon_test.go
@@ -6,10 +6,9 @@
"testing"
"time"
- vsecurity "v.io/core/veyron/security"
+ "v.io/core/veyron/services/identity/oauth"
"v.io/core/veyron/services/identity/util"
- "v.io/core/veyron2/ipc"
"v.io/core/veyron2/security"
"v.io/core/veyron2/vom2"
)
@@ -17,12 +16,12 @@
func TestMacaroonBlesser(t *testing.T) {
var (
key = make([]byte, 16)
- provider, user = newPrincipal(t), newPrincipal(t)
+ provider, user = newPrincipal(), newPrincipal()
cOnlyMethodFoo = newCaveat(security.MethodCaveat("Foo"))
context = &serverCall{
p: provider,
- local: blessSelf(t, provider, "provider"),
- remote: blessSelf(t, user, "self-signed-user"),
+ local: blessSelf(provider, "provider"),
+ remote: blessSelf(user, "self-signed-user"),
}
)
if _, err := rand.Read(key); err != nil {
@@ -30,86 +29,44 @@
}
blesser := NewMacaroonBlesserServer(key)
- m := BlessingMacaroon{Creation: time.Now().Add(-1 * time.Hour), Name: "foo"}
- if got, err := blesser.Bless(context, newMacaroon(t, key, m)); err == nil || err.Error() != "macaroon has expired" {
- t.Errorf("Got (%v, %v)", got, err)
+ m := oauth.BlessingMacaroon{Creation: time.Now().Add(-1 * time.Hour), Name: "foo"}
+ wantErr := "macaroon has expired"
+ if _, err := blesser.Bless(context, newMacaroon(t, key, m)); err == nil || err.Error() != wantErr {
+ t.Errorf("Bless(...) failed with error: %v, want: %v", err, wantErr)
}
- m = BlessingMacaroon{Creation: time.Now(), Name: "user", Caveats: []security.Caveat{cOnlyMethodFoo}}
- if result, err := blesser.Bless(context, newMacaroon(t, key, m)); err != nil {
- t.Errorf("Got (%v, %v)", result, err)
- } else {
- b, err := security.NewBlessings(result)
- if err != nil {
- t.Fatalf("Unable to decode response into a security.Blessings object: %v", err)
- }
- if !reflect.DeepEqual(b.PublicKey(), user.PublicKey()) {
- t.Errorf("Received blessing for public key %v. Client:%v, Blesser:%v", b.PublicKey(), user.PublicKey(), provider.PublicKey())
- }
- // Context at a server to which the user will present her blessings.
- server := newPrincipal(t)
- serverCtxNoMethod := &serverCall{
- p: server,
- remote: b,
- }
- serverCtxFoo := &serverCall{
- p: server,
- remote: b,
- method: "Foo",
- }
- // When the server does not recognize the provider, it should not see any strings for the client's blessings.
- if got := b.ForContext(serverCtxNoMethod); len(got) > 0 {
- t.Errorf("Got blessing that returned %v for an empty security.Context (%v)", got, b)
- }
- if got := b.ForContext(serverCtxFoo); len(got) > 0 {
- t.Errorf("Got blessing that returned %v for an empty security.Context (%v)", got, b)
- }
- // But once it recognizes the provider, serverCtxFoo should see the "provider/user" name.
- server.AddToRoots(b)
- if got := b.ForContext(serverCtxNoMethod); len(got) > 0 {
- t.Errorf("Got blessing that returned %v for an empty security.Context (%v)", got, b)
- }
- if got, want := b.ForContext(serverCtxFoo), []string{"provider/user"}; !reflect.DeepEqual(got, want) {
- t.Errorf("Got %v, want %v", got, want)
- }
- }
-}
-
-type serverCall struct {
- ipc.ServerCall
- method string
- p security.Principal
- local, remote security.Blessings
-}
-
-func (c *serverCall) Method() string { return c.method }
-func (c *serverCall) LocalPrincipal() security.Principal { return c.p }
-func (c *serverCall) LocalBlessings() security.Blessings { return c.local }
-func (c *serverCall) RemoteBlessings() security.Blessings { return c.remote }
-
-func newPrincipal(t *testing.T) security.Principal {
- p, err := vsecurity.NewPrincipal()
+ m = oauth.BlessingMacaroon{Creation: time.Now(), Name: "user", Caveats: []security.Caveat{cOnlyMethodFoo}}
+ result, err := blesser.Bless(context, newMacaroon(t, key, m))
if err != nil {
- panic(err)
+ t.Errorf("Bless failed: %v", err)
}
- return p
-}
-func blessSelf(t *testing.T, p security.Principal, name string) security.Blessings {
- b, err := p.BlessSelf(name)
+ b, err := security.NewBlessings(result)
if err != nil {
- t.Fatal(err)
+ t.Fatalf("Unable to decode response into a security.Blessings object: %v", err)
}
- return b
+ if !reflect.DeepEqual(b.PublicKey(), user.PublicKey()) {
+ t.Errorf("Received blessing for public key %v. Client:%v, Blesser:%v", b.PublicKey(), user.PublicKey(), provider.PublicKey())
+ }
+
+ // When the user does not recognize the provider, it should not see any strings for
+ // the client's blessings.
+ if got := user.BlessingsInfo(b); got != nil {
+ t.Errorf("Got blessing with info %v, want nil", got)
+ }
+ // But once it recognizes the provider, it should see exactly the name
+ // "provider/user" for the caveat cOnlyMethodFoo.
+ user.AddToRoots(b)
+ binfo := user.BlessingsInfo(b)
+ if num := len(binfo); num != 1 {
+ t.Errorf("Got blessings with %d names, want exactly one name", num)
+ }
+ wantName := "provider/user"
+ if cavs := binfo[wantName]; !reflect.DeepEqual(cavs, []security.Caveat{cOnlyMethodFoo}) {
+ t.Errorf("BlessingsInfo %v does not have name %s for the caveat %v", binfo, wantName)
+ }
}
-func newCaveat(c security.Caveat, err error) security.Caveat {
- if err != nil {
- panic(err)
- }
- return c
-}
-
-func newMacaroon(t *testing.T, key []byte, m BlessingMacaroon) string {
+func newMacaroon(t *testing.T, key []byte, m oauth.BlessingMacaroon) string {
encMac, err := vom2.Encode(m)
if err != nil {
t.Fatal(err)
diff --git a/services/identity/blesser/oauth.go b/services/identity/blesser/oauth.go
index 9fba5e0..bf4e6d9 100644
--- a/services/identity/blesser/oauth.go
+++ b/services/identity/blesser/oauth.go
@@ -1,41 +1,34 @@
package blesser
import (
- "encoding/json"
"fmt"
- "net/http"
"strings"
"time"
"v.io/core/veyron/services/identity"
+ "v.io/core/veyron/services/identity/oauth"
"v.io/core/veyron/services/identity/revocation"
"v.io/core/veyron2/ipc"
"v.io/core/veyron2/security"
- "v.io/core/veyron2/vlog"
)
-type googleOAuth struct {
+type oauthBlesser struct {
+ oauthProvider oauth.OAuthProvider
authcodeClient struct{ ID, Secret string }
- accessTokenClients []AccessTokenClient
+ accessTokenClients []oauth.AccessTokenClient
duration time.Duration
domain string
dischargerLocation string
revocationManager revocation.RevocationManager
}
-// AccessTokenClient represents a client of the BlessUsingAccessToken RPCs.
-type AccessTokenClient struct {
- // Descriptive name of the client.
- Name string
- // OAuth Client ID.
- ClientID string
-}
-
-// GoogleParams represents all the parameters provided to NewGoogleOAuthBlesserServer
-type GoogleParams struct {
+// OAuthBlesserParams represents all the parameters provided to NewOAuthBlesserServer
+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.
- AccessTokenClients []AccessTokenClient
+ AccessTokenClients []oauth.AccessTokenClient
// If non-empty, only email addresses from this domain will be blessed.
DomainRestriction string
// The object name of the discharger service. If this is empty then revocation caveats will not be granted.
@@ -46,16 +39,17 @@
BlessingDuration time.Duration
}
-// NewGoogleOAuthBlesserServer provides an identity.OAuthBlesserService that uses authorization
-// codes to obtain the username of a client and provide blessings with that name.
+// NewOAuthBlesserServer provides an identity.OAuthBlesserService that uses OAuth2
+// access tokens to obtain the username of a client and provide blessings with that
+// name.
//
-// For more details, see documentation on Google OAuth 2.0 flows:
-// https://developers.google.com/accounts/docs/OAuth2
-//
-// Blessings generated by this server expire after duration. If domain is non-empty, then blessings
-// are generated only for email addresses from that domain.
-func NewGoogleOAuthBlesserServer(p GoogleParams) interface{} {
- return identity.OAuthBlesserServer(&googleOAuth{
+// 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. If domain is non-empty, then
+// blessings are generated only for email addresses from that domain.
+func NewOAuthBlesserServer(p OAuthBlesserParams) identity.OAuthBlesserServerStub {
+ return identity.OAuthBlesserServer(&oauthBlesser{
+ oauthProvider: p.OAuthProvider,
duration: p.BlessingDuration,
domain: p.DomainRestriction,
dischargerLocation: p.DischargerLocation,
@@ -64,62 +58,26 @@
})
}
-func (b *googleOAuth) BlessUsingAccessToken(ctx ipc.ServerContext, accesstoken string) (security.WireBlessings, string, error) {
+func (b *oauthBlesser) BlessUsingAccessToken(ctx ipc.ServerContext, accessToken string) (security.WireBlessings, string, error) {
var noblessings security.WireBlessings
- if len(b.accessTokenClients) == 0 {
- return noblessings, "", fmt.Errorf("server not configured for blessing based on access tokens")
- }
- // URL from: https://developers.google.com/accounts/docs/OAuth2UserAgent#validatetoken
- tokeninfo, err := http.Get("https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=" + accesstoken)
+ email, clientName, err := b.oauthProvider.GetEmailAndClientName(accessToken, b.accessTokenClients)
if err != nil {
- return noblessings, "", fmt.Errorf("unable to use token: %v", err)
+ return noblessings, "", err
}
- if tokeninfo.StatusCode != http.StatusOK {
- return noblessings, "", fmt.Errorf("unable to verify access token: %v", tokeninfo.StatusCode)
- }
- // tokeninfo contains a JSON-encoded struct
- var token struct {
- IssuedTo string `json:"issued_to"`
- Audience string `json:"audience"`
- UserID string `json:"user_id"`
- Scope string `json:"scope"`
- ExpiresIn int64 `json:"expires_in"`
- Email string `json:"email"`
- VerifiedEmail bool `json:"verified_email"`
- AccessType string `json:"access_type"`
- }
- if err := json.NewDecoder(tokeninfo.Body).Decode(&token); err != nil {
- return noblessings, "", fmt.Errorf("invalid JSON response from Google's tokeninfo API: %v", err)
- }
- var client AccessTokenClient
- audienceMatch := false
- for _, c := range b.accessTokenClients {
- if token.Audience == c.ClientID {
- client = c
- audienceMatch = true
- break
- }
- }
- if !audienceMatch {
- vlog.Infof("Got access token [%+v], wanted one of client ids %v", token, b.accessTokenClients)
- return noblessings, "", fmt.Errorf("token not meant for this purpose, confused deputy? https://developers.google.com/accounts/docs/OAuth2UserAgent#validatetoken")
- }
- if !token.VerifiedEmail {
- return noblessings, "", fmt.Errorf("email not verified")
- }
- // Append client.Name to the blessing (e.g., "android", "chrome"). Since blessings issued by
- // this process do not have many caveats on them and typically have a large expiry duration,
- // we append this suffix so that servers can explicitly distinguish these clients while
- // specifying authorization policies (say, via ACLs).
- return b.bless(ctx, token.Email, client.Name)
+ return b.bless(ctx, email, clientName)
}
-func (b *googleOAuth) bless(ctx ipc.ServerContext, email, extension string) (security.WireBlessings, string, error) {
+func (b *oauthBlesser) bless(ctx ipc.ServerContext, email, clientName string) (security.WireBlessings, string, error) {
var noblessings security.WireBlessings
if len(b.domain) > 0 && strings.HasSuffix(email, "@"+b.domain) {
return noblessings, "", fmt.Errorf("domain restrictions preclude blessings for %q", email)
}
- extension = email + security.ChainSeparator + extension
+ // Append clientName (e.g., "android", "chrome") to the email and then bless under that.
+ // Since blessings issued by this process do not have many caveats on them and typically
+ // have a large expiry duration, we include the clientName in the extension so that
+ // servers can explicitly distinguish these clients while specifying authorization policies
+ // (say, via ACLs).
+ extension := email + security.ChainSeparator + clientName
self := ctx.LocalPrincipal()
if self == nil {
return noblessings, "", fmt.Errorf("server error: no authentication happened")
diff --git a/services/identity/blesser/oauth_test.go b/services/identity/blesser/oauth_test.go
new file mode 100644
index 0000000..4b20fd8
--- /dev/null
+++ b/services/identity/blesser/oauth_test.go
@@ -0,0 +1,60 @@
+package blesser
+
+import (
+ "reflect"
+ "testing"
+ "time"
+
+ "v.io/core/veyron/services/identity/oauth"
+
+ "v.io/core/veyron2/security"
+)
+
+func TestOAuthBlesser(t *testing.T) {
+ var (
+ provider, user = newPrincipal(), newPrincipal()
+ context = &serverCall{
+ p: provider,
+ local: blessSelf(provider, "provider"),
+ remote: blessSelf(user, "self-signed-user"),
+ }
+ )
+ blesser := NewOAuthBlesserServer(OAuthBlesserParams{
+ OAuthProvider: oauth.NewMockOAuth(),
+ BlessingDuration: time.Hour,
+ })
+
+ result, extension, err := blesser.BlessUsingAccessToken(context, "test-access-token")
+ if err != nil {
+ t.Errorf("BlessUsingAccessToken failed: %v", err)
+ }
+
+ wantExtension := oauth.MockEmail + security.ChainSeparator + oauth.MockClient
+ if extension != wantExtension {
+ t.Errorf("got extension: %s, want: %s", extension, wantExtension)
+ }
+
+ b, err := security.NewBlessings(result)
+ if err != nil {
+ t.Fatalf("Unable to decode response into a security.Blessings object: %v", err)
+ }
+ if !reflect.DeepEqual(b.PublicKey(), user.PublicKey()) {
+ t.Errorf("Received blessing for public key %v. Client:%v, Blesser:%v", b.PublicKey(), user.PublicKey(), provider.PublicKey())
+ }
+
+ // When the user does not recognize the provider, it should not see any strings for
+ // the client's blessings.
+ if got := user.BlessingsInfo(b); got != nil {
+ t.Errorf("Got blessing with info %v, want nil", got)
+ }
+ // But once it recognizes the provider, it should see exactly the name
+ // "provider/testemail@google.com/test-client".
+ user.AddToRoots(b)
+ binfo := user.BlessingsInfo(b)
+ if num := len(binfo); num != 1 {
+ t.Errorf("Got blessings with %d names, want exactly one name", num)
+ }
+ if _, ok := binfo["provider"+security.ChainSeparator+wantExtension]; !ok {
+ t.Errorf("BlessingsInfo %v does not have name %s", binfo, wantExtension)
+ }
+}
diff --git a/services/identity/blesser/util_test.go b/services/identity/blesser/util_test.go
new file mode 100644
index 0000000..b20ef76
--- /dev/null
+++ b/services/identity/blesser/util_test.go
@@ -0,0 +1,43 @@
+package blesser
+
+import (
+ vsecurity "v.io/core/veyron/security"
+
+ "v.io/core/veyron2/ipc"
+ "v.io/core/veyron2/security"
+)
+
+type serverCall struct {
+ ipc.ServerCall
+ method string
+ p security.Principal
+ local, remote security.Blessings
+}
+
+func (c *serverCall) Method() string { return c.method }
+func (c *serverCall) LocalPrincipal() security.Principal { return c.p }
+func (c *serverCall) LocalBlessings() security.Blessings { return c.local }
+func (c *serverCall) RemoteBlessings() security.Blessings { return c.remote }
+
+func newPrincipal() security.Principal {
+ p, err := vsecurity.NewPrincipal()
+ if err != nil {
+ panic(err)
+ }
+ return p
+}
+
+func blessSelf(p security.Principal, name string) security.Blessings {
+ b, err := p.BlessSelf(name)
+ if err != nil {
+ panic(err)
+ }
+ return b
+}
+
+func newCaveat(c security.Caveat, err error) security.Caveat {
+ if err != nil {
+ panic(err)
+ }
+ return c
+}
diff --git a/services/identity/identityd/main.go b/services/identity/identityd/main.go
index fddca33..8c173ed 100644
--- a/services/identity/identityd/main.go
+++ b/services/identity/identityd/main.go
@@ -76,7 +76,7 @@
auditor,
reader,
revocationManager,
- oauthBlesserGoogleParams(revocationManager),
+ googleOAuthBlesserParams(googleoauth, revocationManager),
caveats.NewBrowserCaveatSelector())
s.Serve(&static.ListenSpec, *host, *httpaddr, *tlsconfig)
}
@@ -99,8 +99,9 @@
flag.PrintDefaults()
}
-func oauthBlesserGoogleParams(revocationManager revocation.RevocationManager) blesser.GoogleParams {
- googleParams := blesser.GoogleParams{
+func googleOAuthBlesserParams(oauthProvider oauth.OAuthProvider, revocationManager revocation.RevocationManager) blesser.OAuthBlesserParams {
+ params := blesser.OAuthBlesserParams{
+ OAuthProvider: oauthProvider,
BlessingDuration: 365 * 24 * time.Hour,
DomainRestriction: *googleDomain,
RevocationManager: revocationManager,
@@ -108,14 +109,14 @@
if clientID, err := getOAuthClientID(*googleConfigChrome); err != nil {
vlog.Info(err)
} else {
- googleParams.AccessTokenClients = append(googleParams.AccessTokenClients, blesser.AccessTokenClient{Name: "chrome", ClientID: clientID})
+ params.AccessTokenClients = append(params.AccessTokenClients, oauth.AccessTokenClient{Name: "chrome", ClientID: clientID})
}
if clientID, err := getOAuthClientID(*googleConfigAndroid); err != nil {
vlog.Info(err)
} else {
- googleParams.AccessTokenClients = append(googleParams.AccessTokenClients, blesser.AccessTokenClient{Name: "android", ClientID: clientID})
+ params.AccessTokenClients = append(params.AccessTokenClients, oauth.AccessTokenClient{Name: "android", ClientID: clientID})
}
- return googleParams
+ return params
}
func dbFromConfigDatabase(database string) (*sql.DB, error) {
diff --git a/services/identity/identityd_test/main.go b/services/identity/identityd_test/main.go
index 2223224..f1d2e07 100644
--- a/services/identity/identityd_test/main.go
+++ b/services/identity/identityd_test/main.go
@@ -48,19 +48,21 @@
auditor, reader := auditor.NewMockBlessingAuditor()
revocationManager := revocation.NewMockRevocationManager()
+ oauthProvider := oauth.NewMockOAuth()
- googleParams := blesser.GoogleParams{
+ params := blesser.OAuthBlesserParams{
+ OAuthProvider: oauthProvider,
BlessingDuration: duration,
DomainRestriction: *googleDomain,
RevocationManager: revocationManager,
}
s := server.NewIdentityServer(
- oauth.NewMockOAuth(),
+ oauthProvider,
auditor,
reader,
revocationManager,
- googleParams,
+ params,
caveats.NewMockCaveatSelector())
s.Serve(&static.ListenSpec, *host, *httpaddr, *tlsconfig)
}
diff --git a/services/identity/oauth/googleoauth.go b/services/identity/oauth/googleoauth.go
index 17d6b72..541120e 100644
--- a/services/identity/oauth/googleoauth.go
+++ b/services/identity/oauth/googleoauth.go
@@ -6,6 +6,8 @@
"fmt"
"net/http"
"os"
+
+ "v.io/core/veyron2/vlog"
)
// googleOAuth implements the OAuthProvider interface with google oauth 2.0.
@@ -86,6 +88,55 @@
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")
+ }
+ // 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)
+ if err != nil {
+ return "", "", fmt.Errorf("unable to use token: %v", err)
+ }
+ if tokeninfo.StatusCode != http.StatusOK {
+ return "", "", fmt.Errorf("unable to verify access token, OAuth2 TokenInfo endpoint responded with StatusCode: %v", tokeninfo.StatusCode)
+ }
+ // tokeninfo contains a JSON-encoded struct
+ var token struct {
+ IssuedTo string `json:"issued_to"`
+ Audience string `json:"audience"`
+ UserID string `json:"user_id"`
+ Scope string `json:"scope"`
+ ExpiresIn int64 `json:"expires_in"`
+ Email string `json:"email"`
+ VerifiedEmail bool `json:"verified_email"`
+ AccessType string `json:"access_type"`
+ }
+ 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 {
+ vlog.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")
+ }
+ if !token.VerifiedEmail {
+ return "", "", fmt.Errorf("email not verified")
+ }
+ return token.Email, client.Name, nil
+}
+
func (g *googleOAuth) oauthConfig(redirectUrl string) *oauth.Config {
return &oauth.Config{
ClientId: g.clientID,
diff --git a/services/identity/oauth/handler.go b/services/identity/oauth/handler.go
index 2718404..0cec987 100644
--- a/services/identity/oauth/handler.go
+++ b/services/identity/oauth/handler.go
@@ -31,7 +31,6 @@
"time"
"v.io/core/veyron/services/identity/auditor"
- "v.io/core/veyron/services/identity/blesser"
"v.io/core/veyron/services/identity/caveats"
"v.io/core/veyron/services/identity/revocation"
"v.io/core/veyron/services/identity/util"
@@ -80,6 +79,13 @@
CaveatSelector caveats.CaveatSelector
}
+// BlessingMacaroon contains the data that is encoded into the macaroon for creating blessings.
+type BlessingMacaroon struct {
+ Creation time.Time
+ Caveats []security.Caveat
+ Name string
+}
+
func redirectURL(baseURL, suffix string) string {
if !strings.HasSuffix(baseURL, "/") {
baseURL += "/"
@@ -358,7 +364,7 @@
util.HTTPBadRequest(w, r, fmt.Errorf("server disallows attempts to bless with no caveats"))
return
}
- m := blesser.BlessingMacaroon{
+ m := BlessingMacaroon{
Creation: time.Now(),
Caveats: caveats,
Name: name,
diff --git a/services/identity/oauth/mockoauth.go b/services/identity/oauth/mockoauth.go
index 8fa1806..8c87df4 100644
--- a/services/identity/oauth/mockoauth.go
+++ b/services/identity/oauth/mockoauth.go
@@ -1,5 +1,10 @@
package oauth
+const (
+ MockEmail = "testemail@google.com"
+ MockClient = "test-client"
+)
+
// mockOAuth is a mock OAuthProvider for use in tests.
type mockOAuth struct{}
@@ -11,6 +16,10 @@
return redirectUrl + "?state=" + state
}
-func (m *mockOAuth) ExchangeAuthCodeForEmail(authCode string, url string) (email string, err error) {
- return "testemail@google.com", nil
+func (m *mockOAuth) ExchangeAuthCodeForEmail(string, string) (string, error) {
+ return MockEmail, nil
+}
+
+func (m *mockOAuth) GetEmailAndClientName(string, []AccessTokenClient) (string, string, error) {
+ return MockEmail, MockClient, nil
}
diff --git a/services/identity/oauth/oauth_provider.go b/services/identity/oauth/oauth_provider.go
index be0e36f..ac11802 100644
--- a/services/identity/oauth/oauth_provider.go
+++ b/services/identity/oauth/oauth_provider.go
@@ -1,11 +1,24 @@
package oauth
+// AccessTokenClient represents a client of an OAuthProvider.
+type AccessTokenClient struct {
+ // Descriptive name of the client.
+ Name string
+ // OAuth Client ID.
+ ClientID string
+}
+
// OAuthProvider authenticates users to the identity server via the OAuth2 Web Server flow.
type OAuthProvider interface {
// AuthURL is the URL the user must visit in order to authenticate with the OAuthProvider.
// After authentication, the user will be re-directed to redirectURL with the provided state.
AuthURL(redirectUrl string, state string) (url string)
- // ExchangeAuthCodeForEmail exchanges the provided authCode for the email of an
- // authenticated user.
+ // 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)
}
diff --git a/services/identity/server/identityd.go b/services/identity/server/identityd.go
index 18f0e3d..49c087b 100644
--- a/services/identity/server/identityd.go
+++ b/services/identity/server/identityd.go
@@ -32,6 +32,12 @@
)
const (
+ // TODO(ataly, ashankar, suharshs): The name "google" for the oauthBlesserService does
+ // not seem appropriate given our modular construction of the identity server. The
+ // oauthBlesserService can use any oauthProvider of its choosing, i.e., it does not
+ // always have to be "google". One option would be change the value to "oauth". This
+ // would also make the name analogous to that of macaroonService. Note that this option
+ // also requires changing the extension.
oauthBlesserService = "google"
macaroonService = "macaroon"
dischargerService = "discharger"
@@ -42,7 +48,7 @@
auditor audit.Auditor
blessingLogReader auditor.BlessingLogReader
revocationManager revocation.RevocationManager
- oauthBlesserParams blesser.GoogleParams
+ oauthBlesserParams blesser.OAuthBlesserParams
caveatSelector caveats.CaveatSelector
}
@@ -51,7 +57,7 @@
// - auditor and blessingLogReader to audit the root principal and read audit logs
// - revocationManager to store revocation data and grant discharges
// - oauthBlesserParams to configure the identity.OAuthBlesser service
-func NewIdentityServer(oauthProvider oauth.OAuthProvider, auditor audit.Auditor, blessingLogReader auditor.BlessingLogReader, revocationManager revocation.RevocationManager, oauthBlesserParams blesser.GoogleParams, caveatSelector caveats.CaveatSelector) *identityd {
+func NewIdentityServer(oauthProvider oauth.OAuthProvider, auditor audit.Auditor, blessingLogReader auditor.BlessingLogReader, revocationManager revocation.RevocationManager, oauthBlesserParams blesser.OAuthBlesserParams, caveatSelector caveats.CaveatSelector) *identityd {
return &identityd{
oauthProvider,
auditor,
@@ -124,7 +130,7 @@
if s.revocationManager != nil {
args.DischargeServers = appendSuffixTo(published, dischargerService)
}
- var emptyParams blesser.GoogleParams
+ var emptyParams blesser.OAuthBlesserParams
if !reflect.DeepEqual(s.oauthBlesserParams, emptyParams) {
args.GoogleServers = appendSuffixTo(published, oauthBlesserService)
}
@@ -176,11 +182,11 @@
// newDispatcher returns a dispatcher for both the blessing and the
// discharging service.
-func newDispatcher(macaroonKey []byte, blesserParams blesser.GoogleParams) ipc.Dispatcher {
+func newDispatcher(macaroonKey []byte, blesserParams blesser.OAuthBlesserParams) ipc.Dispatcher {
d := dispatcher(map[string]interface{}{
macaroonService: blesser.NewMacaroonBlesserServer(macaroonKey),
dischargerService: services.DischargerServer(discharger.NewDischarger()),
- oauthBlesserService: blesser.NewGoogleOAuthBlesserServer(blesserParams),
+ oauthBlesserService: blesser.NewOAuthBlesserServer(blesserParams),
})
return d
}
@@ -198,7 +204,7 @@
return nil, nil, verror.Make(verror.NoExist, nil, suffix)
}
-func oauthBlesserParams(inputParams blesser.GoogleParams, revocationManager revocation.RevocationManager, ep naming.Endpoint) blesser.GoogleParams {
+func oauthBlesserParams(inputParams blesser.OAuthBlesserParams, revocationManager revocation.RevocationManager, ep naming.Endpoint) blesser.OAuthBlesserParams {
inputParams.DischargerLocation = naming.JoinAddressName(ep.String(), dischargerService)
return inputParams
}