blob: 578cc03019d3db768cbf4788d70f43f3a96f7c87 [file] [log] [blame]
Ankur1d46f552014-10-09 12:13:31 -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 Blessings, 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-forgery?"
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 blessings received"
68 tmplArgs.ErrLong = "Unable to obtain blessings from the Veyron service"
69 return
70 }
71 tmplArgs.Blessings = blessed
72 ln.Close()
73 })
74 go http.Serve(ln, nil)
75
76 // Print the link to start the flow.
77 url, err := seekBlessingsURL(blessServerURL, redirectURL, state)
78 if err != nil {
79 return nil, fmt.Errorf("failed to create seekBlessingsURL: %s", err)
80 }
81 fmt.Fprintln(os.Stderr, "Please visit the following URL to seek blessings:")
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 if len(openCommand) != 0 {
89 exec.Command(openCommand, url).Start()
90 }
91 return result, nil
92}
93
94func seekBlessingsURL(blessServerURL, redirectURL, state string) (string, error) {
95 baseURL, err := url.Parse(joinURL(blessServerURL, googleoauth.SeekBlessingsRoute))
96 if err != nil {
97 return "", fmt.Errorf("failed to parse url: %v", err)
98 }
99 params := url.Values{}
100 params.Add("redirect_url", redirectURL)
101 params.Add("state", state)
102 baseURL.RawQuery = params.Encode()
103 return baseURL.String(), nil
104}
105
106func joinURL(baseURL, suffix string) string {
107 if !strings.HasSuffix(baseURL, "/") {
108 baseURL += "/"
109 }
110 return baseURL + suffix
111}
112
113var tmpl = template.Must(template.New("name").Parse(`<!doctype html>
114<html>
115<head>
116<meta charset="UTF-8">
117<title>Veyron Identity: Google</title>
118<meta name="viewport" content="width=device-width, initial-scale=1.0">
119<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
120{{if .Blessings}}
121<!--Attempt to close the window. Though this script does not work on many browser configurations-->
122<script type="text/javascript">window.close();</script>
123{{end}}
124</head>
125<body>
126<div class="container">
127{{if .ErrShort}}
128<h1><span class="label label-danger">error</span>{{.ErrShort}}</h1>
129<div class="well">{{.ErrLong}}</div>
130{{else}}
131<h3>Received blessings: <tt>{{.Blessings}}</tt></h3>
132{{end}}
133</div>
134</body>
135</html>`))