veyron/services/identity, veyron/tools/identity: Identity tool can request blessing with
caveats.
* New oauth flow to keep the blessing process secure from malicious identity tools.
* The oauth flow can be seen https://docs.google.com/a/google.com/document/d/1SRoc2cKE9iE1fWR7aSmMoccZoi4ZE8BQL7sr1LDNVkk/edit?usp=sharing.
Change-Id: I534f216953a1825cce899ffbfd82768db49b4108
diff --git a/tools/identity/bless.go b/tools/identity/bless.go
new file mode 100644
index 0000000..05d5546
--- /dev/null
+++ b/tools/identity/bless.go
@@ -0,0 +1,134 @@
+package main
+
+import (
+ "crypto/rand"
+ "encoding/base64"
+ "fmt"
+ "html/template"
+ "net"
+ "net/http"
+ "net/url"
+ "os"
+ "os/exec"
+ "strings"
+
+ "veyron.io/veyron/veyron/services/identity/googleoauth"
+ "veyron.io/veyron/veyron2/vlog"
+)
+
+func getMacaroonForBlessRPC(blessServerURL string, blessedChan <-chan string) (<-chan string, error) {
+ // Setup a HTTP server to recieve a blessing macaroon from the identity server.
+ // Steps:
+ // 1. Generate a state token to be included in the HTTP request
+ // (though, arguably, the random port assigment for the HTTP server is enough
+ // for XSRF protection)
+ // 2. Setup a HTTP server which will receive the final blessing macaroon from the id server.
+ // 3. Print out the link (to start the auth flow) for the user to click.
+ // 4. Return the macaroon and the rpc object name(where to make the MacaroonBlesser.Bless RPC call)
+ // in 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 authorization code interception server: %v", err)
+ }
+ result := make(chan string)
+
+ redirectURL := fmt.Sprintf("http://%s/macaroon", ln.Addr())
+ http.HandleFunc("/macaroon", 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)
+ }
+ }()
+
+ toolState := r.FormValue("state")
+ if toolState != state {
+ tmplArgs.ErrShort = "Unexpected request"
+ tmplArgs.ErrLong = "Mismatched state parameter. Possible cross-site-request-forging?"
+ return
+ }
+ result <- r.FormValue("macaroon")
+ result <- r.FormValue("object_name")
+ defer close(result)
+ blessed, ok := <-blessedChan
+ if !ok {
+ tmplArgs.ErrShort = "No blessing received"
+ tmplArgs.ErrLong = "Unable to obtain blessing from the Veyron service"
+ return
+ }
+ tmplArgs.Blessing = blessed
+ ln.Close()
+ })
+ go http.Serve(ln, nil)
+
+ // Print the link to start the flow.
+ url, err := seekBlessingURL(blessServerURL, redirectURL, state)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create seekBlessingURL: %s", err)
+ }
+ fmt.Fprintln(os.Stderr, "Please visit the following URL to complete the blessing creation:")
+ fmt.Fprintln(os.Stderr, url)
+ // Make an attempt to start the browser as a convenience.
+ // If it fails, doesn't matter - the client can see the URL printed above.
+ // Use exec.Command().Start instead of exec.Command().Run since there is no
+ // need to wait for the command to return (and indeed on some window managers,
+ // the command will not exit until the browser is closed).
+ exec.Command(openCommand, url).Start()
+ return result, nil
+}
+
+func seekBlessingURL(blessServerURL, redirectURL, state string) (string, error) {
+ baseURL, err := url.Parse(joinURL(blessServerURL, googleoauth.SeekBlessingsRoute))
+ if err != nil {
+ return "", fmt.Errorf("failed to parse url: %v", err)
+ }
+ params := url.Values{}
+ params.Add("redirect_url", redirectURL)
+ params.Add("state", state)
+ baseURL.RawQuery = params.Encode()
+ return baseURL.String(), nil
+}
+
+func joinURL(baseURL, suffix string) string {
+ if !strings.HasSuffix(baseURL, "/") {
+ baseURL += "/"
+ }
+ return baseURL + suffix
+}
+
+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>`))