| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "crypto/rand" |
| "encoding/base64" |
| "fmt" |
| "html/template" |
| "net" |
| "net/http" |
| "net/url" |
| "os" |
| "os/exec" |
| "strings" |
| "time" |
| |
| "v.io/v23/context" |
| "v.io/v23/naming" |
| "v.io/v23/options" |
| "v.io/v23/security" |
| "v.io/x/lib/vlog" |
| "v.io/x/ref/services/identity" |
| ) |
| |
| type formResult struct { |
| macaroon string |
| objectNames []string |
| rootKey string |
| } |
| |
| func exchangeMacaroonForBlessing(ctx *context.T, macaroonChan <-chan formResult) (security.Blessings, error) { |
| objectNames, macaroon, serviceKey, err := prepareBlessArgs(ctx, macaroonChan) |
| if err != nil { |
| return security.Blessings{}, err |
| } |
| |
| ctx, cancel := context.WithTimeout(ctx, time.Minute) |
| defer cancel() |
| |
| me := &naming.MountEntry{} |
| for _, n := range objectNames { |
| var server string |
| // me.Name is the suffix part of the address and should be the |
| // same for all object names. |
| server, me.Name = naming.SplitAddressName(n) |
| me.Servers = append(me.Servers, naming.MountedServer{Server: server}) |
| } |
| |
| // Authorize the server by its public key (obtained from macaroonChan). |
| // |
| // objectNames is either: |
| // - a list of pre-resolved names, or |
| // - a single unresolved object name. (DEPRECATED) |
| // |
| // First we try the pre-resolved names. If the call fails, we try the |
| // unresolved name, in which case, we must skip authorization during |
| // name resolution because the blessings of the nameservers are not |
| // rooted at recognized keys yet. |
| blessings, err := identity.MacaroonBlesserClient("").Bless( |
| ctx, |
| macaroon, |
| options.ServerAuthorizer{security.PublicKeyAuthorizer(serviceKey)}, |
| options.Preresolved{me}) |
| if err != nil && len(objectNames) == 1 { |
| // For backward compatibility, try to resolve the service name. |
| blessings, err = identity.MacaroonBlesserClient(objectNames[0]).Bless( |
| ctx, |
| macaroon, |
| options.ServerAuthorizer{security.PublicKeyAuthorizer(serviceKey)}, |
| options.NameResolutionAuthorizer{security.AllowEveryone()}) |
| } |
| if err != nil { |
| return blessings, fmt.Errorf("failed to get blessing from %q: %v", objectNames, err) |
| } |
| return blessings, nil |
| } |
| |
| func prepareBlessArgs(ctx *context.T, macaroonChan <-chan formResult) (service []string, macaroon string, root security.PublicKey, err error) { |
| result := <-macaroonChan |
| |
| marshalKey, err := base64.URLEncoding.DecodeString(result.rootKey) |
| if err != nil { |
| return nil, "", nil, fmt.Errorf("failed to decode root key: %v", err) |
| } |
| root, err = security.UnmarshalPublicKey(marshalKey) |
| if err != nil { |
| return nil, "", nil, fmt.Errorf("failed to unmarshal root key: %v", err) |
| } |
| |
| return result.objectNames, result.macaroon, root, nil |
| } |
| |
| func getMacaroonForBlessRPC(key security.PublicKey, blessServerURL string, blessedChan <-chan string, browser bool) (<-chan formResult, 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 formResult) |
| |
| 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 { |
| Blessings string |
| ErrShort string |
| ErrLong string |
| Browser bool |
| }{ |
| Browser: browser, |
| } |
| 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) |
| } |
| }() |
| |
| defer close(result) |
| if r.FormValue("state") != state { |
| tmplArgs.ErrShort = "Unexpected request" |
| tmplArgs.ErrLong = "Mismatched state parameter. Possible cross-site-request-forgery?" |
| return |
| } |
| if errEnc := r.FormValue("error"); errEnc != "" { |
| tmplArgs.ErrShort = "Failed to get blessings" |
| errBytes, err := base64.URLEncoding.DecodeString(errEnc) |
| if err != nil { |
| tmplArgs.ErrLong = err.Error() |
| } else { |
| tmplArgs.ErrLong = string(errBytes) |
| } |
| return |
| } |
| result <- formResult{ |
| macaroon: r.FormValue("macaroon"), |
| objectNames: r.Form["object_name"], |
| rootKey: r.FormValue("root_key"), |
| } |
| |
| blessed, ok := <-blessedChan |
| if !ok { |
| tmplArgs.ErrShort = "No blessings received" |
| tmplArgs.ErrLong = "Unable to obtain blessings from the Vanadium service" |
| return |
| } |
| tmplArgs.Blessings = blessed |
| ln.Close() |
| }) |
| go http.Serve(ln, nil) |
| |
| // Print the link to start the flow. |
| url, err := seekBlessingsURL(key, blessServerURL, redirectURL, state) |
| if err != nil { |
| return nil, fmt.Errorf("failed to create seekBlessingsURL: %s", err) |
| } |
| fmt.Fprintln(os.Stdout, "Please visit the following URL to seek blessings:") |
| fmt.Fprintln(os.Stdout, 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). |
| if len(openCommand) != 0 && browser { |
| exec.Command(openCommand, url).Start() |
| } |
| return result, nil |
| } |
| |
| func seekBlessingsURL(key security.PublicKey, blessServerURL, redirectURL, state string) (string, error) { |
| baseURL, err := url.Parse(joinURL(blessServerURL, identity.SeekBlessingsRoute)) |
| if err != nil { |
| return "", fmt.Errorf("failed to parse url: %v", err) |
| } |
| keyBytes, err := key.MarshalBinary() |
| if err != nil { |
| return "", fmt.Errorf("failed to marshal public key: %v", err) |
| } |
| params := url.Values{} |
| params.Add("redirect_url", redirectURL) |
| params.Add("state", state) |
| params.Add("public_key", base64.URLEncoding.EncodeToString(keyBytes)) |
| 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"> |
| <!--Excluding any third-party hosted resources like scripts and stylesheets because otherwise we run the risk of leaking the macaroon out of this page (e.g., via the referrer header) --> |
| <title>Vanadium Identity: Google</title> |
| {{if and .Browser .Blessings}} |
| <!--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> |
| {{if .ErrShort}} |
| <center><h1><span style="color:#FF6E40;">Error: </span>{{.ErrShort}}</h1></center> |
| <center><h2>{{.ErrLong}}</h2></center> |
| {{else}} |
| <center><h1>Received blessings: <tt>{{.Blessings}}</tt></h1></center> |
| <center><h2>You may close this tab now.</h2></center> |
| {{end}} |
| </div> |
| </body> |
| </html>`)) |