veyron/{services,tools}/identity: Make the identity tool much more usable and secure.
Identity providers in veyron essentially verify that a client has a valid
claim to a particular name and then issue a blessing with that name, rooted
at the identity provider's self-signed certificate.
Prior to this commit:
- There was an HTTP server that would use OAuth to validate that a client
has a valid claim to a particular name (managed by Google)
- And then ship the PrivateID (including a newly minted private key) over the
wire to the caller.
- The command-line tool could not interact with this flow and thus one
had to visit the HTTP page and "copy-paste" the response into a file.
This scheme had various issues:
- The private key was known to the HTTP server and was shipped across
the wire. In veyron we intend to *NEVER* ship private keys outside
the host machine.
- The command-line tool wasn't usable for the most useful identities
(i.e., those with a blessing from the HTTP server).
This commit changes the flow:
- A Veyron service is provided that uses an OAuth authentication code
to obtain the username of a client and then provides a blessing
via a veyron RPC. Thus, no private keys are shipped on the wire
and ownership of the private key by the cient is ensured via the
Veyron authentication protocol
- The command-line tool initiates the OAuth flow, obtains the authentication
code and the uses that to invoke the Veyron service to get a blessing.
This hopefully makes the tool significantly more usable and avoids the
need for any copy-pastes or any private keys being shipped around.
For now, the original HTTP server code is kept, but once WSPR is
also migrated to this scheme, we hope to remove the code that
transmits a private key over the wire.
Change-Id: I720b59bf23747b69c60681cb564279efc9d0fccf
diff --git a/runtimes/google/rt/security.go b/runtimes/google/rt/security.go
index b14babf..466cb8a 100644
--- a/runtimes/google/rt/security.go
+++ b/runtimes/google/rt/security.go
@@ -49,7 +49,7 @@
var err error
if file := os.Getenv("VEYRON_IDENTITY"); len(file) > 0 {
if rt.id, err = loadIdentityFromFile(file); err != nil || rt.id == nil {
- return fmt.Errorf("Could not load identity from %q: %v", file, err)
+ return fmt.Errorf("Could not load identity from the VEYRON_IDENTITY environment variable (%q): %v", file, err)
}
} else {
name := defaultIdentityName()
diff --git a/services/identity/blesser/oauth.go b/services/identity/blesser/oauth.go
new file mode 100644
index 0000000..eda2081
--- /dev/null
+++ b/services/identity/blesser/oauth.go
@@ -0,0 +1,47 @@
+package blesser
+
+import (
+ "time"
+
+ "veyron/services/identity"
+ "veyron/services/identity/googleoauth"
+ "veyron2"
+ "veyron2/ipc"
+ "veyron2/vdl/vdlutil"
+)
+
+// Expiry time of blessings issued by the Google OAuth Blesser Server.
+// TODO(ashankar): This is ridiculously large! Add third-party revocation
+// caveat instead?
+const BlessingExpiry = 365 * 24 * time.Hour
+
+type googleOAuth struct {
+ rt veyron2.Runtime
+ clientID, clientSecret string
+}
+
+// NewGoogleOAuthBlesserServer provides an identity.OAuthBlesserService that uses authorization
+// codes 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
+func NewGoogleOAuthBlesserServer(rt veyron2.Runtime, clientID, clientSecret string) interface{} {
+ return identity.NewServerOAuthBlesser(&googleOAuth{rt, clientID, clientSecret})
+}
+
+func (b *googleOAuth) Bless(ctx ipc.ServerContext, authcode, redirectURL string) (vdlutil.Any, error) {
+ config := googleoauth.NewOAuthConfig(b.clientID, b.clientSecret, redirectURL)
+ name, err := googleoauth.ExchangeAuthCodeForEmail(config, authcode)
+ if err != nil {
+ return nil, err
+ }
+ self := b.rt.Identity()
+ // Use the blessing that was used to authenticate with the client to bless it.
+ if self, err = self.Derive(ctx.LocalID()); err != nil {
+ return nil, err
+ }
+ // TODO(ashankar,ataly): Use the same set of caveats as is used by the HTTP handler.
+ // For example, a third-party revocation caveat?
+ // TODO(ashankar,rthellend): Also want the domain restriction here?
+ return self.Bless(ctx.RemoteID(), name, BlessingExpiry, nil)
+}
diff --git a/services/identity/googleoauth/handler.go b/services/identity/googleoauth/handler.go
index 6807f4a..2ce6fee 100644
--- a/services/identity/googleoauth/handler.go
+++ b/services/identity/googleoauth/handler.go
@@ -55,20 +55,28 @@
return fmt.Sprintf("%s://%s%soauth2callback", scheme, a.Addr, a.Prefix)
}
+// URL used to verify GoogleIDTokens.
+// (From https://developers.google.com/accounts/docs/OAuth2Login#validatinganidtoken)
+const VerifyURL = "https://www.googleapis.com/oauth2/v1/tokeninfo?id_token="
+
// NewHandler returns an http.Handler that expects to be rooted at args.Prefix
// and can be used to use OAuth 2.0 to authenticate with Google, mint a new
// identity and bless it with the Google email address.
func NewHandler(args HandlerArgs) http.Handler {
- config := &oauth.Config{
- ClientId: args.ClientID,
- ClientSecret: args.ClientSecret,
- RedirectURL: args.redirectURL(),
+ config := NewOAuthConfig(args.ClientID, args.ClientSecret, args.redirectURL())
+ return &handler{config, args.Addr, args.MinExpiryDays, util.NewCSRFCop(), args.Runtime, args.RestrictEmailDomain}
+}
+
+// NewOAuthConfig returns the oauth.Config required for obtaining just the email address from Google using OAuth 2.0.
+func NewOAuthConfig(clientID, clientSecret, redirectURL string) *oauth.Config {
+ return &oauth.Config{
+ ClientId: clientID,
+ ClientSecret: clientSecret,
+ RedirectURL: redirectURL,
Scope: "email",
AuthURL: "https://accounts.google.com/o/oauth2/auth",
TokenURL: "https://accounts.google.com/o/oauth2/token",
}
- verifyURL := "https://www.googleapis.com/oauth2/v1/tokeninfo?id_token="
- return &handler{config, args.Addr, args.MinExpiryDays, util.NewCSRFCop(), args.Runtime, verifyURL, args.RestrictEmailDomain}
}
type handler struct {
@@ -77,7 +85,6 @@
minExpiryDays int
csrfCop *util.CSRFCop
rt veyron2.Runtime
- verifyURL string
domain string
}
@@ -108,44 +115,13 @@
util.HTTPBadRequest(w, r, fmt.Errorf("Suspected request forgery: %v", err))
return
}
- transport := &oauth.Transport{Config: h.config}
- t, err := transport.Exchange(r.FormValue("code"))
+ email, err := ExchangeAuthCodeForEmail(h.config, r.FormValue("code"))
if err != nil {
- util.HTTPBadRequest(w, r, fmt.Errorf("Failed to fetch GoogleIDToken: %v", err))
+ util.HTTPBadRequest(w, r, err)
return
}
- // Ideally, would validate the token ourselves without an HTTP roundtrip.
- // However, for now, as per:
- // https://developers.google.com/accounts/docs/OAuth2Login#validatinganidtoken
- // pay an HTTP round-trip to have Google do this.
- if t.Extra == nil {
- util.HTTPServerError(w, fmt.Errorf("No GoogleIDToken found in exchange"))
- return
- }
- tinfo, err := http.Get(h.verifyURL + t.Extra["id_token"])
- if err != nil {
- util.HTTPServerError(w, err)
- return
- }
- if tinfo.StatusCode != http.StatusOK {
- util.HTTPBadRequest(w, r, fmt.Errorf("failed to decode GoogleIDToken: %q", tinfo.Status))
- return
- }
- var gtoken token
- if err = json.NewDecoder(tinfo.Body).Decode(>oken); err != nil {
- util.HTTPBadRequest(w, r, fmt.Errorf("invalid response from Google's tokeninfo API: %v", err))
- return
- }
- if !gtoken.VerifiedEmail {
- util.HTTPBadRequest(w, r, fmt.Errorf("email not verified: %#v", gtoken))
- return
- }
- if gtoken.Issuer != "accounts.google.com" {
- util.HTTPBadRequest(w, r, fmt.Errorf("invalid issuer: %v", gtoken.Issuer))
- return
- }
- if gtoken.Audience != h.config.ClientId {
- util.HTTPBadRequest(w, r, fmt.Errorf("unexpected audience(%v) in GoogleIDToken", gtoken.Audience))
+ if len(h.domain) > 0 && !strings.HasSuffix(email, "@"+h.domain) {
+ util.HTTPServerError(w, fmt.Errorf("email domain in %s is not allowed", email))
return
}
minted, err := h.rt.NewIdentity("unblessed")
@@ -153,11 +129,7 @@
util.HTTPServerError(w, fmt.Errorf("Failed to mint new identity: %v", err))
return
}
- if len(h.domain) > 0 && !strings.HasSuffix(gtoken.Email, "@"+h.domain) {
- util.HTTPServerError(w, fmt.Errorf("Email domain in %s is not allowed", gtoken.Email))
- return
- }
- blessing, err := h.rt.Identity().Bless(minted.PublicID(), gtoken.Email, 24*time.Hour*time.Duration(h.minExpiryDays), nil)
+ blessing, err := h.rt.Identity().Bless(minted.PublicID(), email, 24*time.Hour*time.Duration(h.minExpiryDays), nil)
if err != nil {
util.HTTPServerError(w, fmt.Errorf("%v.Bless(%q, %q, %d days, nil) failed: %v", h.rt.Identity(), minted, h.minExpiryDays, err))
return
@@ -171,6 +143,44 @@
util.HTTPSend(w, blessed)
}
+// ExchangeAuthCodeForEmail exchanges the authorization code (which must
+// have been obtained with scope=email) for an OAuth token and then uses Google's
+// tokeninfo API to extract the email address from that token.
+func ExchangeAuthCodeForEmail(config *oauth.Config, authcode string) (string, error) {
+ t, err := (&oauth.Transport{Config: config}).Exchange(authcode)
+ if err != nil {
+ return "", fmt.Errorf("failed to exchange authorization code for token: %v", err)
+ }
+ // Ideally, would validate the token ourselves without an HTTP roundtrip.
+ // However, for now, as per:
+ // https://developers.google.com/accounts/docs/OAuth2Login#validatinganidtoken
+ // pay an HTTP round-trip to have Google do this.
+ if t.Extra == nil || len(t.Extra["id_token"]) == 0 {
+ return "", fmt.Errorf("no GoogleIDToken found in OAuth token")
+ }
+ tinfo, err := http.Get(VerifyURL + t.Extra["id_token"])
+ if err != nil {
+ return "", fmt.Errorf("failed to talk to GoogleIDToken verifier (%q): %v", VerifyURL, err)
+ }
+ if tinfo.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("failed to verify GoogleIDToken: %s", tinfo.Status)
+ }
+ var gtoken token
+ if err := json.NewDecoder(tinfo.Body).Decode(>oken); err != nil {
+ return "", fmt.Errorf("invalid JSON response from Google's tokeninfo API: %v", err)
+ }
+ if !gtoken.VerifiedEmail {
+ return "", fmt.Errorf("email not verified: %#v", gtoken)
+ }
+ if gtoken.Issuer != "accounts.google.com" {
+ return "", fmt.Errorf("invalid issuer: %v", gtoken.Issuer)
+ }
+ if gtoken.Audience != config.ClientId {
+ return "", fmt.Errorf("unexpected audience(%v) in GoogleIDToken", gtoken.Audience)
+ }
+ return gtoken.Email, nil
+}
+
// IDToken JSON message returned by Google's verification endpoint.
//
// This differs from the description in:
diff --git a/services/identity/googleoauth/utils.go b/services/identity/googleoauth/utils.go
index 19d455e..dbd5026 100644
--- a/services/identity/googleoauth/utils.go
+++ b/services/identity/googleoauth/utils.go
@@ -12,21 +12,25 @@
// API Access console for your application
// (https://code.google.com/apis/console).
func ClientIDAndSecretFromJSON(r io.Reader) (id, secret string, err error) {
- var x map[string]interface{}
- if err = json.NewDecoder(r).Decode(&x); err != nil {
+ var full, x map[string]interface{}
+ if err = json.NewDecoder(r).Decode(&full); err != nil {
return
}
var ok bool
- if x, ok = x["web"].(map[string]interface{}); !ok {
- err = fmt.Errorf("web not found")
- return
+ typ := "web"
+ if x, ok = full[typ].(map[string]interface{}); !ok {
+ typ = "installed"
+ if x, ok = full[typ].(map[string]interface{}); !ok {
+ err = fmt.Errorf("web or installed configuration not found")
+ return
+ }
}
if id, ok = x["client_id"].(string); !ok {
- err = fmt.Errorf("web.client_id not found")
+ err = fmt.Errorf("%s.client_id not found", typ)
return
}
if secret, ok = x["client_secret"].(string); !ok {
- err = fmt.Errorf("web.client_secret not found")
+ err = fmt.Errorf("%s.client_secret not found", typ)
return
}
return
diff --git a/services/identity/identity.vdl b/services/identity/identity.vdl
new file mode 100644
index 0000000..c803076
--- /dev/null
+++ b/services/identity/identity.vdl
@@ -0,0 +1,18 @@
+// Package identity defines services for identity providers in the veyron ecosystem.
+package identity
+
+
+// OAuthBlesser exchanges the provided authorization code for an email addres
+// from an OAuth-based identity provider and uses the email address as the
+// name to bless the client with.
+//
+// The redirect URL used to obtain the authorization code must also be
+// provided in order to ensure a successful exchange.
+type OAuthBlesser interface {
+ // TODO(ashankar,toddw): Once the "OneOf" type becomes available in VDL,
+ // then the "any" should be replaced by:
+ // OneOf<wire.ChainPublicID, []wire.ChainPublicID>
+ // where wire is from:
+ // import "veyron2/security/wire"
+ Bless(authcode, redirecturl string) (blessing any, err error)
+}
\ No newline at end of file
diff --git a/services/identity/identity.vdl.go b/services/identity/identity.vdl.go
new file mode 100644
index 0000000..5f3c637
--- /dev/null
+++ b/services/identity/identity.vdl.go
@@ -0,0 +1,197 @@
+// This file was auto-generated by the veyron vdl tool.
+// Source: identity.vdl
+
+// Package identity defines services for identity providers in the veyron ecosystem.
+package identity
+
+import (
+ // The non-user imports are prefixed with "_gen_" to prevent collisions.
+ _gen_veyron2 "veyron2"
+ _gen_context "veyron2/context"
+ _gen_ipc "veyron2/ipc"
+ _gen_naming "veyron2/naming"
+ _gen_rt "veyron2/rt"
+ _gen_vdlutil "veyron2/vdl/vdlutil"
+ _gen_wiretype "veyron2/wiretype"
+)
+
+// OAuthBlesser exchanges the provided authorization code for an email addres
+// from an OAuth-based identity provider and uses the email address as the
+// name to bless the client with.
+//
+// The redirect URL used to obtain the authorization code must also be
+// provided in order to ensure a successful exchange.
+// OAuthBlesser is the interface the client binds and uses.
+// OAuthBlesser_ExcludingUniversal is the interface without internal framework-added methods
+// to enable embedding without method collisions. Not to be used directly by clients.
+type OAuthBlesser_ExcludingUniversal interface {
+ // TODO(ashankar,toddw): Once the "OneOf" type becomes available in VDL,
+ // then the "any" should be replaced by:
+ // OneOf<wire.ChainPublicID, []wire.ChainPublicID>
+ // where wire is from:
+ // import "veyron2/security/wire"
+ Bless(ctx _gen_context.T, authcode string, redirecturl string, opts ..._gen_ipc.CallOpt) (reply _gen_vdlutil.Any, err error)
+}
+type OAuthBlesser interface {
+ _gen_ipc.UniversalServiceMethods
+ OAuthBlesser_ExcludingUniversal
+}
+
+// OAuthBlesserService is the interface the server implements.
+type OAuthBlesserService interface {
+
+ // TODO(ashankar,toddw): Once the "OneOf" type becomes available in VDL,
+ // then the "any" should be replaced by:
+ // OneOf<wire.ChainPublicID, []wire.ChainPublicID>
+ // where wire is from:
+ // import "veyron2/security/wire"
+ Bless(context _gen_ipc.ServerContext, authcode string, redirecturl string) (reply _gen_vdlutil.Any, err error)
+}
+
+// BindOAuthBlesser returns the client stub implementing the OAuthBlesser
+// interface.
+//
+// If no _gen_ipc.Client is specified, the default _gen_ipc.Client in the
+// global Runtime is used.
+func BindOAuthBlesser(name string, opts ..._gen_ipc.BindOpt) (OAuthBlesser, error) {
+ var client _gen_ipc.Client
+ switch len(opts) {
+ case 0:
+ client = _gen_rt.R().Client()
+ case 1:
+ switch o := opts[0].(type) {
+ case _gen_veyron2.Runtime:
+ client = o.Client()
+ case _gen_ipc.Client:
+ client = o
+ default:
+ return nil, _gen_vdlutil.ErrUnrecognizedOption
+ }
+ default:
+ return nil, _gen_vdlutil.ErrTooManyOptionsToBind
+ }
+ stub := &clientStubOAuthBlesser{client: client, name: name}
+
+ return stub, nil
+}
+
+// NewServerOAuthBlesser creates a new server stub.
+//
+// It takes a regular server implementing the OAuthBlesserService
+// interface, and returns a new server stub.
+func NewServerOAuthBlesser(server OAuthBlesserService) interface{} {
+ return &ServerStubOAuthBlesser{
+ service: server,
+ }
+}
+
+// clientStubOAuthBlesser implements OAuthBlesser.
+type clientStubOAuthBlesser struct {
+ client _gen_ipc.Client
+ name string
+}
+
+func (__gen_c *clientStubOAuthBlesser) Bless(ctx _gen_context.T, authcode string, redirecturl string, opts ..._gen_ipc.CallOpt) (reply _gen_vdlutil.Any, err error) {
+ var call _gen_ipc.Call
+ if call, err = __gen_c.client.StartCall(ctx, __gen_c.name, "Bless", []interface{}{authcode, redirecturl}, opts...); err != nil {
+ return
+ }
+ if ierr := call.Finish(&reply, &err); ierr != nil {
+ err = ierr
+ }
+ return
+}
+
+func (__gen_c *clientStubOAuthBlesser) UnresolveStep(ctx _gen_context.T, opts ..._gen_ipc.CallOpt) (reply []string, err error) {
+ var call _gen_ipc.Call
+ if call, err = __gen_c.client.StartCall(ctx, __gen_c.name, "UnresolveStep", nil, opts...); err != nil {
+ return
+ }
+ if ierr := call.Finish(&reply, &err); ierr != nil {
+ err = ierr
+ }
+ return
+}
+
+func (__gen_c *clientStubOAuthBlesser) Signature(ctx _gen_context.T, opts ..._gen_ipc.CallOpt) (reply _gen_ipc.ServiceSignature, err error) {
+ var call _gen_ipc.Call
+ if call, err = __gen_c.client.StartCall(ctx, __gen_c.name, "Signature", nil, opts...); err != nil {
+ return
+ }
+ if ierr := call.Finish(&reply, &err); ierr != nil {
+ err = ierr
+ }
+ return
+}
+
+func (__gen_c *clientStubOAuthBlesser) GetMethodTags(ctx _gen_context.T, method string, opts ..._gen_ipc.CallOpt) (reply []interface{}, err error) {
+ var call _gen_ipc.Call
+ if call, err = __gen_c.client.StartCall(ctx, __gen_c.name, "GetMethodTags", []interface{}{method}, opts...); err != nil {
+ return
+ }
+ if ierr := call.Finish(&reply, &err); ierr != nil {
+ err = ierr
+ }
+ return
+}
+
+// ServerStubOAuthBlesser wraps a server that implements
+// OAuthBlesserService and provides an object that satisfies
+// the requirements of veyron2/ipc.ReflectInvoker.
+type ServerStubOAuthBlesser struct {
+ service OAuthBlesserService
+}
+
+func (__gen_s *ServerStubOAuthBlesser) GetMethodTags(call _gen_ipc.ServerCall, method string) ([]interface{}, error) {
+ // TODO(bprosnitz) GetMethodTags() will be replaces with Signature().
+ // Note: This exhibits some weird behavior like returning a nil error if the method isn't found.
+ // This will change when it is replaced with Signature().
+ switch method {
+ case "Bless":
+ return []interface{}{}, nil
+ default:
+ return nil, nil
+ }
+}
+
+func (__gen_s *ServerStubOAuthBlesser) Signature(call _gen_ipc.ServerCall) (_gen_ipc.ServiceSignature, error) {
+ result := _gen_ipc.ServiceSignature{Methods: make(map[string]_gen_ipc.MethodSignature)}
+ result.Methods["Bless"] = _gen_ipc.MethodSignature{
+ InArgs: []_gen_ipc.MethodArgument{
+ {Name: "authcode", Type: 3},
+ {Name: "redirecturl", Type: 3},
+ },
+ OutArgs: []_gen_ipc.MethodArgument{
+ {Name: "blessing", Type: 65},
+ {Name: "err", Type: 66},
+ },
+ }
+
+ result.TypeDefs = []_gen_vdlutil.Any{
+ _gen_wiretype.NamedPrimitiveType{Type: 0x1, Name: "anydata", Tags: []string(nil)}, _gen_wiretype.NamedPrimitiveType{Type: 0x1, Name: "error", Tags: []string(nil)}}
+
+ return result, nil
+}
+
+func (__gen_s *ServerStubOAuthBlesser) UnresolveStep(call _gen_ipc.ServerCall) (reply []string, err error) {
+ if unresolver, ok := __gen_s.service.(_gen_ipc.Unresolver); ok {
+ return unresolver.UnresolveStep(call)
+ }
+ if call.Server() == nil {
+ return
+ }
+ var published []string
+ if published, err = call.Server().Published(); err != nil || published == nil {
+ return
+ }
+ reply = make([]string, len(published))
+ for i, p := range published {
+ reply[i] = _gen_naming.Join(p, call.Name())
+ }
+ return
+}
+
+func (__gen_s *ServerStubOAuthBlesser) Bless(call _gen_ipc.ServerCall, authcode string, redirecturl string) (reply _gen_vdlutil.Any, err error) {
+ reply, err = __gen_s.service.Bless(call, authcode, redirecturl)
+ return
+}
diff --git a/services/identity/identityd/main.go b/services/identity/identityd/main.go
index b1832e8..f4f5105 100644
--- a/services/identity/identityd/main.go
+++ b/services/identity/identityd/main.go
@@ -9,24 +9,30 @@
"os"
"strings"
+ "veyron/lib/signals"
+ "veyron/services/identity/blesser"
"veyron/services/identity/googleoauth"
"veyron/services/identity/handlers"
"veyron/services/identity/util"
+ "veyron2"
+ "veyron2/ipc"
"veyron2/rt"
"veyron2/security"
"veyron2/vlog"
)
var (
- address = flag.String("address", "localhost:8125", "Address on which the HTTP server listens on.")
+ 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. If empty, will not use HTTPS.")
+ // TODO(ashankar): Revisit the choices for -vaddr and -vprotocol once the proxy design in relation to mounttables has been finalized.
+ address = flag.String("vaddr", "proxy.envyor.com:8100", "Address on which the Veyron blessing server listens on. Enabled iff --google_config is set")
+ protocol = flag.String("vprotocol", "veyron", "Protocol used to interpret --vaddr")
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', Veyron identities will have the IssuerName set to 'x.com' and clients can expect to find the public key of the signer at 'x.com/pubkey/'.")
- tlsconfig = flag.String("tlsconfig", "", "Comma-separated list of TLS certificate and private key files. If empty, will not use HTTPS.")
minExpiryDays = flag.Int("min_expiry_days", 365, "Minimum expiry time (in days) of identities issued by this server")
googleConfig = flag.String("google_config", "", "Path to the JSON-encoded file containing the ClientID for web applications registered with the Google Developer Console. (Use the 'Download JSON' link on the Google APIs console).")
googleDomain = flag.String("google_domain", "", "An optional domain name. When set, only email addresses from this domain are allowed to authenticate via Google OAuth")
-
- generate = flag.String("generate", "", "If non-empty, instead of running an HTTP server, a new identity will be created with the provided name and saved to --identity (if specified) and dumped to STDOUT in base64-encoded-vom")
- identity = flag.String("identity", "", "Path to the file where the VOM-encoded security.PrivateID created with --generate will be written.")
+ generate = flag.String("generate", "", "If non-empty, instead of running an HTTP server, a new identity will be created with the provided name and saved to --identity (if specified) and dumped to STDOUT in base64-encoded-vom")
+ identity = flag.String("identity", "", "Path to the file where the VOM-encoded security.PrivateID created with --generate will be written.")
)
func main() {
@@ -47,41 +53,78 @@
http.Handle("/random/", handlers.Random{r}) // mint identities with a random name
}
http.HandleFunc("/bless/", handlers.Bless) // use a provided PrivateID to bless a provided PublicID
+
// Google OAuth
- if enableGoogleOAuth() {
- _, port, err := net.SplitHostPort(*address)
+ if enabled, clientID, clientSecret := enableGoogleOAuth(); enabled {
+ // Setup veyron service
+ server, err := setupGoogleBlessingServer(r, clientID, clientSecret)
if err != nil {
- vlog.Fatalf("Failed to parse %q: %v", *address, err)
+ vlog.Fatalf("failed to setup veyron services for blessing: %v", err)
}
- f, err := os.Open(*googleConfig)
+ defer server.Stop()
+
+ // Setup HTTP handler
+ _, port, err := net.SplitHostPort(*httpaddr)
if err != nil {
- vlog.Fatalf("Failed to open %q: %v", *googleConfig, err)
+ vlog.Fatalf("Failed to parse %q: %v", *httpaddr, err)
}
- clientid, secret, err := googleoauth.ClientIDAndSecretFromJSON(f)
- if err != nil {
- vlog.Fatalf("Failed to decode %q: %v", *googleConfig, err)
- }
- f.Close()
n := "/google/"
http.Handle(n, googleoauth.NewHandler(googleoauth.HandlerArgs{
UseTLS: enableTLS(),
Addr: fmt.Sprintf("%s:%s", *host, port),
Prefix: n,
- ClientID: clientid,
- ClientSecret: secret,
+ ClientID: clientID,
+ ClientSecret: clientSecret,
MinExpiryDays: *minExpiryDays,
Runtime: r,
RestrictEmailDomain: *googleDomain,
}))
}
- startHTTPServer(*address)
+ go runHTTPServer(*httpaddr)
+ <-signals.ShutdownOnSignals()
}
-func enableTLS() bool { return len(*tlsconfig) > 0 }
-func enableGoogleOAuth() bool { return len(*googleConfig) > 0 }
-func enableRandomHandler() bool { return !enableGoogleOAuth() }
+func setupGoogleBlessingServer(r veyron2.Runtime, clientID, clientSecret string) (ipc.Server, error) {
+ server, err := r.NewServer()
+ if err != nil {
+ return nil, fmt.Errorf("failed to create new ipc.Server: %v", err)
+ }
+ ep, err := server.Listen(*protocol, *address)
+ if err != nil {
+ return nil, fmt.Errorf("server.Listen(%q, %q) failed: %v", "tcp", *address, err)
+ }
+ allowEveryoneACL := security.ACL{security.AllPrincipals: security.AllLabels}
+ objectname := fmt.Sprintf("identity/%s/google", r.Identity().PublicID().Names()[0])
+ if err := server.Serve(objectname, ipc.SoloDispatcher(blesser.NewGoogleOAuthBlesserServer(r, clientID, clientSecret), security.NewACLAuthorizer(allowEveryoneACL))); err != nil {
+ return nil, fmt.Errorf("failed to start Veyron service: %v", err)
+ }
+ vlog.Infof("Google blessing service enabled at endpoint %v and name %q", ep, objectname)
+ return server, nil
+}
-func startHTTPServer(addr string) {
+func enableTLS() bool { return len(*tlsconfig) > 0 }
+func enableRandomHandler() bool {
+ disable, _, _ := enableGoogleOAuth()
+ return !disable
+}
+func enableGoogleOAuth() (enabled bool, clientID, clientSecret string) {
+ fname := *googleConfig
+ if len(fname) == 0 {
+ return false, "", ""
+ }
+ f, err := os.Open(fname)
+ if err != nil {
+ vlog.Fatalf("Failed to open %q: %v", fname, err)
+ }
+ defer f.Close()
+ clientID, clientSecret, err = googleoauth.ClientIDAndSecretFromJSON(f)
+ if err != nil {
+ vlog.Fatalf("Failed to decode JSON in %q: %v", fname, err)
+ }
+ return true, clientID, clientSecret
+}
+
+func runHTTPServer(addr string) {
if !enableTLS() {
vlog.Infof("Starting HTTP server (without TLS) at http://%v", addr)
if err := http.ListenAndServe(addr, nil); err != nil {
@@ -142,7 +185,7 @@
This HTTP server mints veyron identities. The public key of the identity of this server is available in
<a class="btn btn-xs btn-info" href="/pubkey/base64vom">base64-encoded-vom-encoded</a> format.
</div>`))
- if enableGoogleOAuth() {
+ if ok, _, _ := enableGoogleOAuth(); ok {
w.Write([]byte(`<a class="btn btn-lg btn-primary" href="/google/auth">Google</a> `))
}
if enableRandomHandler() {
diff --git a/tools/identity/googleoauth.go b/tools/identity/googleoauth.go
new file mode 100644
index 0000000..1aedd1f
--- /dev/null
+++ b/tools/identity/googleoauth.go
@@ -0,0 +1,112 @@
+package main
+
+import (
+ "crypto/rand"
+ "encoding/base64"
+ "fmt"
+ "html/template"
+ "net"
+ "net/http"
+ "os"
+ "os/exec"
+
+ "veyron/services/identity/googleoauth"
+ "veyron2/vlog"
+)
+
+func getOAuthAuthorizationCodeFromGoogle(clientID string, blessing <-chan string) (<-chan string, error) {
+ // Setup an HTTP server so that OAuth authorization codes can be intercepted.
+ // Steps:
+ // 1. Generate a state token to be included in the HTTP request
+ // (though, arguably, the random port assignment for the HTTP server is
+ // enough for XSRF protecetion)
+ // 2. Setup an HTTP server which will intercept redirect links from the OAuth flow.
+ // 3. Print out the link for the user to click
+ // 4. Return the authorization code obtained from the redirect to the "result" channel.
+ var stateBuf [32]byte
+ if _, err := rand.Read(stateBuf[:]); err != nil {
+ return nil, fmt.Errorf("failed to generate state token for OAuth: %v", err)
+ }
+ state := base64.URLEncoding.EncodeToString(stateBuf[:])
+
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ return nil, fmt.Errorf("failed to setup OAuth authorization code interception: %v", err)
+ }
+ redirectURL := fmt.Sprintf("http://%s", ln.Addr())
+ result := make(chan string, 1)
+ result <- redirectURL
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ tmplArgs := struct {
+ Blessing, ErrShort, ErrLong string
+ }{}
+ defer func() {
+ if len(tmplArgs.ErrShort) > 0 {
+ w.WriteHeader(http.StatusBadRequest)
+ }
+ if err := tmpl.Execute(w, tmplArgs); err != nil {
+ vlog.Info("Failed to render template:", err)
+ }
+ }()
+ if urlstate := r.FormValue("state"); urlstate != state {
+ tmplArgs.ErrShort = "Unexpected request"
+ tmplArgs.ErrLong = "Mismatched state parameter. Possible cross-site-request-forging?"
+ return
+ }
+ code := r.FormValue("code")
+ if len(code) == 0 {
+ tmplArgs.ErrShort = "No authorization code received"
+ tmplArgs.ErrLong = "Expected Google to issue an authorization code using 'code' as a URL parameter in the redirect"
+ return
+ }
+ ln.Close() // No need for the HTTP server anymore
+ result <- code
+ blessed, ok := <-blessing
+ defer close(result)
+ if !ok {
+ tmplArgs.ErrShort = "No blessing received"
+ tmplArgs.ErrLong = "Unable to obtain blessing from the Veyron service"
+ return
+ }
+ tmplArgs.Blessing = blessed
+ return
+ })
+ go http.Serve(ln, nil)
+
+ // Print out the link to start the OAuth flow (to STDERR so that STDOUT output contains
+ // only the final blessing) and try to open it in the browser as well.
+ //
+ // TODO(ashankar): Detect devices with limited I/O and then decide to use the device flow
+ // instead? See: https://developers.google.com/accounts/docs/OAuth2#device
+ url := googleoauth.NewOAuthConfig(clientID, "", redirectURL).AuthCodeURL(state)
+ fmt.Fprintln(os.Stderr, "Please visit the following URL to authenticate with Google:")
+ fmt.Fprintln(os.Stderr, url)
+ exec.Command(openCommand, url).Run()
+ return result, nil
+}
+
+var tmpl = template.Must(template.New("name").Parse(`<!doctype html>
+<html>
+<head>
+<meta charset="UTF-8">
+<title>Veyron Identity: Google</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">
+{{if .Blessing}}
+<!--Attempt to close the window. Though this script does not work on many browser configurations-->
+<script type="text/javascript">window.close();</script>
+{{end}}
+</head>
+<body>
+<div class="container">
+{{if .ErrShort}}
+<h1><span class="label label-danger">error</span>{{.ErrShort}}</h1>
+<div class="well">{{.ErrLong}}</div>
+{{else}}
+<h3>Received blessing: <tt>{{.Blessing}}</tt></h3>
+<div class="well">If the name is prefixed with "unknown/", ignore that. You can close this window, the command line tool has retrieved the blessing</div>
+{{end}}
+</div>
+</body>
+</html>`))
diff --git a/tools/identity/main.go b/tools/identity/main.go
index d440ffd..bb314c1 100644
--- a/tools/identity/main.go
+++ b/tools/identity/main.go
@@ -2,94 +2,265 @@
import (
"bytes"
- "flag"
"fmt"
"io"
"os"
"os/user"
- "path"
"time"
+ "veyron/lib/cmdline"
+ "veyron/services/identity"
"veyron/services/identity/util"
+ "veyron2"
"veyron2/rt"
"veyron2/security"
+ "veyron2/vdl/vdlutil"
"veyron2/vlog"
)
var (
- name = flag.String("name", "", "Name for the generated identity. By default a name derived from the username of the currently logged-in user and hostname is used")
- blesser = flag.String("blesser", "", "Path to a file containing the blessor (or - for STDIN)")
- duration = flag.Duration("duration", 24*time.Hour, "Duration for which a blessing will be valid. Ignored if --blesser is empty")
- interpret = flag.String("interpret", "", "Path to a file containing an identity to interpret (or - for STDIN)")
+ // Flags for the "bless" command
+ flagBlessWith string
+ flagBlessFor time.Duration
+
+ // Flags for the "seekblessing" command
+ flagSeekBlessingFor string
+ flagSeekBlessingOAuthClientID string
+ flagSeekBlessingFrom string
+
+ cmdPrint = &cmdline.Command{
+ Name: "print",
+ Short: "Print out information about the provided identity",
+ Long: `
+Print dumps out information about the identity encoded in the provided file,
+or if no filename is provided, then the identity that would be used by binaries
+started in the same environment.
+`,
+ ArgsName: "[<file>]",
+ ArgsLong: `
+<file> is the path to a file containing a base64-encoded, VOM encoded identity,
+typically obtained from this tool. - is used for STDIN and an empty string
+implies the identity encoded in the environment.
+`,
+ Run: func(cmd *cmdline.Command, args []string) error {
+ if len(args) > 1 {
+ return fmt.Errorf("require at most one argument, <file>, provided %d", len(args))
+ }
+ id := rt.R().Identity()
+ if len(args) == 1 {
+ if err := decode(args[0], &id); err != nil {
+ return err
+ }
+ }
+ fmt.Println("Name : ", id.PublicID())
+ fmt.Printf("Go Type : %T\n", id)
+ fmt.Println("PublicKey: <Cannot print the elliptic curve>")
+ fmt.Println(" X: ", id.PublicID().PublicKey().X)
+ fmt.Println(" Y: ", id.PublicID().PublicKey().Y)
+ fmt.Println("Any caveats in the identity are not printed")
+ return nil
+ },
+ }
+
+ cmdGenerate = &cmdline.Command{
+ Name: "generate",
+ Short: "Generate an identity with a newly minted private key",
+ Long: `
+Generate a new private key and create an identity that binds <name> to
+this key.
+
+Since the generated identity has a newly minted key, it will be typically
+unusable at other veyron services as those services have placed no trust
+in this key. In such cases, you likely want to seek a blessing for this
+generated identity using the 'bless' command.
+`,
+ ArgsName: "[<name>]",
+ ArgsLong: `
+<name> is the name to bind the newly minted private key to. If not specified,
+a name will be generated based on the hostname of the machine and the name of
+the user running this command.
+`,
+ Run: func(cmd *cmdline.Command, args []string) error {
+ r := rt.R()
+ var name string
+ switch len(args) {
+ case 0:
+ name = defaultIdentityName()
+ case 1:
+ name = args[0]
+ default:
+ return fmt.Errorf("require at most one argument, provided %d", len(args))
+ }
+ id, err := r.NewIdentity(name)
+ if err != nil {
+ return fmt.Errorf("veyron2.Runtime.NewIdentity(%q) failed: %v", name, err)
+ }
+ output, err := util.Base64VomEncode(id)
+ if err != nil {
+ return fmt.Errorf("failed to encode identity: %v", err)
+ }
+ fmt.Println(output)
+ return nil
+ },
+ }
+
+ cmdBless = &cmdline.Command{
+ Name: "bless",
+ Short: "Bless another identity with your own",
+ Long: `
+Bless uses the identity of the tool (either from an environment variable or
+explicitly specified using --with) to bless another identity encoded in a
+file (or STDIN). No caveats are applied to this blessing other than expiration,
+which is specified with --for.
+
+The output consists of a base64-vom encoded security.PrivateID or security.PublicID,
+depending on what was provided as input.
+
+For example, if the tool has an identity veyron/user/device, then
+bless /tmp/blessee batman
+will generate a blessing with the name veyron/user/device/batman
+
+The identity of the tool can be specified with the --with flag:
+bless --with /tmp/id /tmp/blessee batman
+`,
+ ArgsName: "<file> <name> [--with=<file>]",
+ ArgsLong: `
+<file> is the name of the file containing a base64-vom encoded security.PublicID
+or security.PrivateID
+
+<name> is the name to use for the blessing.
+`,
+ Run: func(cmd *cmdline.Command, args []string) error {
+ if len(args) != 2 {
+ return fmt.Errorf("expected exactly two arguments (<file> and <name>), got %d", len(args))
+ }
+ blesser := rt.R().Identity()
+ if len(flagBlessWith) > 0 {
+ if err := decode(flagBlessWith, &blesser); err != nil {
+ return err
+ }
+ }
+ name := args[1]
+ var blessee security.PublicID
+ var private security.PrivateID
+ if err := decode(args[0], &blessee); err != nil {
+ if err := decode(args[0], &private); err != nil {
+ return fmt.Errorf("failed to extract security.PublicID or security.PrivateID: %v", err)
+ }
+ blessee = private.PublicID()
+ }
+ blessed, err := blesser.Bless(blessee, name, flagBlessFor, nil)
+ if err != nil {
+ return err
+ }
+ var object interface{} = blessed
+ if private != nil {
+ object, err = private.Derive(blessed)
+ if err != nil {
+ return err
+ }
+ }
+ output, err := util.Base64VomEncode(object)
+ if err != nil {
+ return err
+ }
+ fmt.Println(output)
+ return nil
+ },
+ }
+
+ cmdSeekBlessing = &cmdline.Command{
+ Name: "seekblessing",
+ Short: "Seek a blessing from the default veyron identity provider",
+ Long: `
+Seeks a blessing from a default, hardcoded Veyron identity provider which
+requires the caller to first authenticate with Google using OAuth. Simply
+run the command to see what happens.
+
+The blessing is sought for the identity that this tool is using. An alternative
+can be provided with the --for flag.
+`,
+ Run: func(cmd *cmdline.Command, args []string) error {
+ r := rt.R()
+ id := r.Identity()
+
+ if len(flagSeekBlessingFor) > 0 {
+ if err := decode(flagSeekBlessingFor, &id); err != nil {
+ return err
+ }
+ var err error
+ if r, err = rt.New(veyron2.RuntimeID(id)); err != nil {
+ return err
+ }
+ }
+
+ blessedChan := make(chan string)
+ defer close(blessedChan)
+ authcodeChan, err := getOAuthAuthorizationCodeFromGoogle(flagSeekBlessingOAuthClientID, blessedChan)
+ if err != nil {
+ return fmt.Errorf("failed to get authorization code from Google: %v", err)
+ }
+ redirectURL := <-authcodeChan
+ authcode := <-authcodeChan
+
+ wait := time.Second
+ const maxWait = 20 * time.Second
+ var reply vdlutil.Any
+ for {
+ blesser, err := identity.BindOAuthBlesser(flagSeekBlessingFrom, r.Client())
+ if err == nil {
+ reply, err = blesser.Bless(r.NewContext(), authcode, redirectURL)
+ }
+ if err != nil {
+ vlog.Infof("Failed to get blessing from %q: %v, will try again in %v", flagSeekBlessingFrom, err, wait)
+ time.Sleep(wait)
+ if wait = wait + 2; wait > maxWait {
+ wait = maxWait
+ }
+ continue
+ }
+ blessed, ok := reply.(security.PublicID)
+ if !ok {
+ return fmt.Errorf("received %T, want security.PublicID", reply)
+ }
+ if id, err = id.Derive(blessed); err != nil {
+ return fmt.Errorf("received incompatible blessing from %q: %v", flagSeekBlessingFrom, err)
+ }
+ output, err := util.Base64VomEncode(id)
+ if err != nil {
+ return fmt.Errorf("failed to encode blessing: %v", err)
+ }
+ fmt.Println(output)
+ blessedChan <- fmt.Sprint(blessed)
+ // Wait for getOAuthAuthenticationCodeFromGoogle to clean up:
+ <-authcodeChan
+ return nil
+ }
+ },
+ }
)
-func init() {
- flag.Usage = func() {
- bname := path.Base(os.Args[0])
- fmt.Fprintf(os.Stderr, `%s: Tool to generate Veyron identities
-
-This tool generates veyron private identities (security.PrivateID) and dumps
-the generated identity to STDOUT in base64-VOM-encoded format.
-
-Typical usage:
-* no flags
- A self-signed identity with a default name of <user>@<hostname> is generated,
- where <user> is the username of the currently logged-in user and <hostname> is
- the host name as reported by the kernel.
-
-* --name NAME
- A self-signed identity for NAME will be generated and dumped to STDOUT
-
-* --name NAME --blesser BLESSER
- BLESSER must be the path to a readable file (or - for STDIN) containing a
- base64-VOM-encoded security.PrivateID that will be used to generate an
- identity with NAME as the blessing name.
-
-* --interpret INTERPRET
- INTERPRET must be the path to a readable file (or - for STDIN) containing a
- base64-VOM-encoded security.PrivateID. This identity will decoded and
- some information will be printed to STDOUT.
-
-For example:
-%s --name "foo" | %s --name "bar" --blesser - | %s --interpret -
-
-Full flags:
-`, os.Args[0], bname, bname, bname)
- flag.PrintDefaults()
- }
-}
-
func main() {
- // It is currently not possible to regenerate the currently used identity
- // if it becomes corrupt because this tool needs VEYRON_IDENTITY to be "",
- // or point to a valid identity for runtime initialization. Temporarily
- // set the VEYRON_IDENTITY to "" as a workaround.
- // TODO(bprosnitz) Fix this.
- os.Setenv("VEYRON_IDENTITY", "")
-
rt.Init()
+ cmdBless.Flags.StringVar(&flagBlessWith, "with", "", "Path to file containing identity to bless with (or - for STDIN)")
+ cmdBless.Flags.DurationVar(&flagBlessFor, "for", 365*24*time.Hour, "Expiry time of blessing (defaults to 1 year)")
+ cmdSeekBlessing.Flags.StringVar(&flagSeekBlessingFor, "for", "", "Path to file containing identity to bless (or - for STDIN)")
+ cmdSeekBlessing.Flags.StringVar(&flagSeekBlessingOAuthClientID, "clientid", "761523829214-4ms7bae18ef47j6590u9ncs19ffuo7b3.apps.googleusercontent.com", "OAuth client ID used to make OAuth request for an authorization code")
+ cmdSeekBlessing.Flags.StringVar(&flagSeekBlessingFrom, "from", "/proxy.envyor.com:8101/identity/veyron-test/google", "Object name of Veyron service running the identity.OAuthBlesser service to seek blessings from")
- if len(*name) == 0 && len(*interpret) == 0 {
- generate(defaultIdentityName())
- }
-
- if len(*name) > 0 {
- generate(*name)
- }
-
- if len(*interpret) > 0 {
- id := load(*interpret)
- fmt.Println("Name : ", id.PublicID())
- fmt.Printf("Go Type : %T\n", id)
- fmt.Println("PublicKey: <Cannot print the elliptic curve>")
- fmt.Println(" X: ", id.PublicID().PublicKey().X)
- fmt.Println(" Y: ", id.PublicID().PublicKey().Y)
- fmt.Println("Any caveats in the identity are not printed")
- }
+ (&cmdline.Command{
+ Name: "identity",
+ Short: "Create an manage veyron identities",
+ Long: `
+The identity tool helps create and manage keys and blessings that are used for
+identification in veyron.
+`,
+ Children: []*cmdline.Command{cmdPrint, cmdGenerate, cmdBless, cmdSeekBlessing},
+ }).Main()
}
-func load(fname string) security.PrivateID {
+func decode(fname string, val interface{}) error {
if len(fname) == 0 {
return nil
}
@@ -98,43 +269,17 @@
if fname == "-" {
f = os.Stdin
} else if f, err = os.Open(fname); err != nil {
- vlog.Fatalf("Failed to open %q: %v", fname, err)
+ return fmt.Errorf("failed to open %q: %v", fname, err)
}
defer f.Close()
var buf bytes.Buffer
if _, err := io.Copy(&buf, f); err != nil {
- vlog.Fatalf("Failed to read %q: %v", fname, err)
+ return fmt.Errorf("failed to read %q: %v", fname, err)
}
- var ret security.PrivateID
- if err := util.Base64VomDecode(buf.String(), &ret); err != nil || ret == nil {
- vlog.Fatalf("Failed to decode %q: %v", fname, err)
+ if err := util.Base64VomDecode(buf.String(), val); err != nil || val == nil {
+ return fmt.Errorf("failed to decode %q: %v", fname, err)
}
- return ret
-}
-
-func generate(name string) {
- r := rt.R()
- output, err := r.NewIdentity(name)
- if err != nil {
- vlog.Fatalf("Runtime.NewIdentity(%q): %v", name, err)
- }
- if len(*blesser) > 0 {
- blesser := load(*blesser)
- blessed, err := blesser.Bless(output.PublicID(), name, *duration, nil)
- if err != nil {
- vlog.Fatalf("%q.Bless failed: %v", blesser, err)
- }
- derived, err := output.Derive(blessed)
- if err != nil {
- vlog.Fatalf("%q.Derive(%q) failed: %v", output, blessed, err)
- }
- output = derived
- }
- str, err := util.Base64VomEncode(output)
- if err != nil {
- vlog.Fatalf("Base64VomEncode(%q) failed: %v", output, err)
- }
- fmt.Println(str)
+ return nil
}
func defaultIdentityName() string {
diff --git a/tools/identity/main_darwin.go b/tools/identity/main_darwin.go
new file mode 100644
index 0000000..bceafd2
--- /dev/null
+++ b/tools/identity/main_darwin.go
@@ -0,0 +1,5 @@
+// +build darwin
+
+package main
+
+const openCommand = "open"
diff --git a/tools/identity/main_linux.go b/tools/identity/main_linux.go
new file mode 100644
index 0000000..cb73c65
--- /dev/null
+++ b/tools/identity/main_linux.go
@@ -0,0 +1,5 @@
+// +build linux
+
+package main
+
+const openCommand = "xdg-open"