blob: ca6c46c332833bd1cc737b603450d3c8ebca4b0a [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/options"
"v.io/v23/security"
"v.io/x/lib/vlog"
"v.io/x/ref/services/identity"
)
func exchangeMacaroonForBlessing(ctx *context.T, macaroonChan <-chan string) (security.Blessings, error) {
service, macaroon, serviceKey, err := prepareBlessArgs(ctx, macaroonChan)
if err != nil {
return security.Blessings{}, err
}
ctx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel()
// Authorize the server by its public key (obtained from macaroonChan).
// Must skip authorization during name resolution because the identity
// service is not a trusted root yet.
blessings, err := identity.MacaroonBlesserClient(service).Bless(ctx, macaroon, options.SkipServerEndpointAuthorization{}, options.ServerPublicKey{serviceKey})
if err != nil {
return security.Blessings{}, fmt.Errorf("failed to get blessing from %q: %v", service, err)
}
return blessings, nil
}
func prepareBlessArgs(ctx *context.T, macaroonChan <-chan string) (service, macaroon string, root security.PublicKey, err error) {
macaroon = <-macaroonChan
service = <-macaroonChan
marshalKey, err := base64.URLEncoding.DecodeString(<-macaroonChan)
if err != nil {
return "", "", nil, fmt.Errorf("failed to decode root key: %v", err)
}
root, err = security.UnmarshalPublicKey(marshalKey)
if err != nil {
return "", "", nil, fmt.Errorf("failed to unmarshal root key: %v", err)
}
return service, macaroon, root, nil
}
func getMacaroonForBlessRPC(key security.PublicKey, blessServerURL string, blessedChan <-chan string, browser bool) (<-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 {
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 <- r.FormValue("macaroon")
result <- r.FormValue("object_name")
result <- 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>`))