blob: 05d55466d240b8461b23352ec089c8317180eece [file] [log] [blame]
Suharsh Sivakumard308c7e2014-10-03 12:46:50 -07001package main
2
3import (
4 "crypto/rand"
5 "encoding/base64"
6 "fmt"
7 "html/template"
8 "net"
9 "net/http"
10 "net/url"
11 "os"
12 "os/exec"
13 "strings"
14
15 "veyron.io/veyron/veyron/services/identity/googleoauth"
16 "veyron.io/veyron/veyron2/vlog"
17)
18
19func getMacaroonForBlessRPC(blessServerURL string, blessedChan <-chan string) (<-chan string, error) {
20 // Setup a HTTP server to recieve a blessing macaroon from the identity server.
21 // Steps:
22 // 1. Generate a state token to be included in the HTTP request
23 // (though, arguably, the random port assigment for the HTTP server is enough
24 // for XSRF protection)
25 // 2. Setup a HTTP server which will receive the final blessing macaroon from the id server.
26 // 3. Print out the link (to start the auth flow) for the user to click.
27 // 4. Return the macaroon and the rpc object name(where to make the MacaroonBlesser.Bless RPC call)
28 // in the "result" channel.
29 var stateBuf [32]byte
30 if _, err := rand.Read(stateBuf[:]); err != nil {
31 return nil, fmt.Errorf("failed to generate state token for OAuth: %v", err)
32 }
33 state := base64.URLEncoding.EncodeToString(stateBuf[:])
34
35 ln, err := net.Listen("tcp", "127.0.0.1:0")
36 if err != nil {
37 return nil, fmt.Errorf("failed to setup authorization code interception server: %v", err)
38 }
39 result := make(chan string)
40
41 redirectURL := fmt.Sprintf("http://%s/macaroon", ln.Addr())
42 http.HandleFunc("/macaroon", func(w http.ResponseWriter, r *http.Request) {
43 w.Header().Set("Content-Type", "text/html")
44 tmplArgs := struct {
45 Blessing, ErrShort, ErrLong string
46 }{}
47 defer func() {
48 if len(tmplArgs.ErrShort) > 0 {
49 w.WriteHeader(http.StatusBadRequest)
50 }
51 if err := tmpl.Execute(w, tmplArgs); err != nil {
52 vlog.Info("Failed to render template:", err)
53 }
54 }()
55
56 toolState := r.FormValue("state")
57 if toolState != state {
58 tmplArgs.ErrShort = "Unexpected request"
59 tmplArgs.ErrLong = "Mismatched state parameter. Possible cross-site-request-forging?"
60 return
61 }
62 result <- r.FormValue("macaroon")
63 result <- r.FormValue("object_name")
64 defer close(result)
65 blessed, ok := <-blessedChan
66 if !ok {
67 tmplArgs.ErrShort = "No blessing received"
68 tmplArgs.ErrLong = "Unable to obtain blessing from the Veyron service"
69 return
70 }
71 tmplArgs.Blessing = blessed
72 ln.Close()
73 })
74 go http.Serve(ln, nil)
75
76 // Print the link to start the flow.
77 url, err := seekBlessingURL(blessServerURL, redirectURL, state)
78 if err != nil {
79 return nil, fmt.Errorf("failed to create seekBlessingURL: %s", err)
80 }
81 fmt.Fprintln(os.Stderr, "Please visit the following URL to complete the blessing creation:")
82 fmt.Fprintln(os.Stderr, url)
83 // Make an attempt to start the browser as a convenience.
84 // If it fails, doesn't matter - the client can see the URL printed above.
85 // Use exec.Command().Start instead of exec.Command().Run since there is no
86 // need to wait for the command to return (and indeed on some window managers,
87 // the command will not exit until the browser is closed).
88 exec.Command(openCommand, url).Start()
89 return result, nil
90}
91
92func seekBlessingURL(blessServerURL, redirectURL, state string) (string, error) {
93 baseURL, err := url.Parse(joinURL(blessServerURL, googleoauth.SeekBlessingsRoute))
94 if err != nil {
95 return "", fmt.Errorf("failed to parse url: %v", err)
96 }
97 params := url.Values{}
98 params.Add("redirect_url", redirectURL)
99 params.Add("state", state)
100 baseURL.RawQuery = params.Encode()
101 return baseURL.String(), nil
102}
103
104func joinURL(baseURL, suffix string) string {
105 if !strings.HasSuffix(baseURL, "/") {
106 baseURL += "/"
107 }
108 return baseURL + suffix
109}
110
111var tmpl = template.Must(template.New("name").Parse(`<!doctype html>
112<html>
113<head>
114<meta charset="UTF-8">
115<title>Veyron Identity: Google</title>
116<meta name="viewport" content="width=device-width, initial-scale=1.0">
117<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
118{{if .Blessing}}
119<!--Attempt to close the window. Though this script does not work on many browser configurations-->
120<script type="text/javascript">window.close();</script>
121{{end}}
122</head>
123<body>
124<div class="container">
125{{if .ErrShort}}
126<h1><span class="label label-danger">error</span>{{.ErrShort}}</h1>
127<div class="well">{{.ErrLong}}</div>
128{{else}}
129<h3>Received blessing: <tt>{{.Blessing}}</tt></h3>
130<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>
131{{end}}
132</div>
133</body>
134</html>`))