blob: c3694117a15fd8611eeab327d4853f0adea18420 [file] [log] [blame]
// 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>`))