github.com/grailbio/base@v0.0.11/cmd/grail-access/google.go (about)

     1  // Copyright 2018 GRAIL, Inc. All rights reserved.
     2  // Use of this source code is governed by the Apache-2.0
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"context"
     9  	"crypto/rand"
    10  	"encoding/hex"
    11  	"fmt"
    12  	"net"
    13  	"net/http"
    14  	"strings"
    15  	"sync"
    16  
    17  	"github.com/grailbio/base/log"
    18  	"github.com/grailbio/base/security/identity"
    19  	"github.com/grailbio/base/web/webutil"
    20  	"golang.org/x/oauth2"
    21  	goauth2 "google.golang.org/api/oauth2/v1"
    22  	vcontext "v.io/v23/context"
    23  	"v.io/v23/security"
    24  	"v.io/x/lib/vlog"
    25  )
    26  
    27  const defaultGoogleBlesserFlag = "/ticket-server.eng.grail.com:8102/blesser/google"
    28  
    29  func fetchGoogleBlessings(ctx *vcontext.T) (security.Blessings, error) {
    30  	if blesserFlag == "" {
    31  		blesserFlag = defaultGoogleBlesserFlag
    32  	}
    33  	idToken, err := fetchIDToken(ctx)
    34  	if err != nil {
    35  		return security.Blessings{}, err
    36  	}
    37  	stub := identity.GoogleBlesserClient(blesserFlag)
    38  	return stub.BlessGoogle(ctx, idToken)
    39  }
    40  
    41  // fetchIDToken obtains a Google ID Token using an OAuth2 flow with Google. The
    42  // user will be instructed to use and URL or a browser will automatically open.
    43  func fetchIDToken(ctx context.Context) (string, error) {
    44  	stateBytes := make([]byte, 16)
    45  	if _, err := rand.Read(stateBytes); err != nil {
    46  		return "", err
    47  	}
    48  	state := hex.EncodeToString(stateBytes)
    49  
    50  	code := ""
    51  	wg := sync.WaitGroup{}
    52  	wg.Add(1)
    53  	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    54  		if r.URL.Path != "/" {
    55  			return
    56  		}
    57  		if got, want := r.FormValue("state"), state; got != want {
    58  			log.Fatalf("Bad state: got %q, want %q", got, want)
    59  		}
    60  		code = r.FormValue("code")
    61  		w.Header().Set("Content-Type", "text/html")
    62  		// JavaScript only allows closing windows/tab that were open via
    63  		// JavaScript.
    64  		_, _ = fmt.Fprintf(w, `<html><body>Code received. Please close this tab/window.</body></html>`)
    65  		wg.Done()
    66  	})
    67  
    68  	ln, err := net.Listen("tcp", "localhost:")
    69  	if err != nil {
    70  		return "", err
    71  	}
    72  	vlog.Infof("listening: %v\n", ln.Addr().String())
    73  	port := strings.Split(ln.Addr().String(), ":")[1]
    74  	server := http.Server{Addr: "localhost:"}
    75  	go server.Serve(ln.(*net.TCPListener)) // nolint: errcheck
    76  
    77  	config := &oauth2.Config{
    78  		ClientID:     clientID,
    79  		ClientSecret: clientSecret,
    80  		Scopes:       []string{goauth2.UserinfoEmailScope},
    81  		RedirectURL:  fmt.Sprintf("http://localhost:%s", port),
    82  		Endpoint: oauth2.Endpoint{
    83  			AuthURL:  googleOauth2Flag + "/v2/auth",
    84  			TokenURL: googleOauth2Flag + "/token",
    85  		},
    86  	}
    87  
    88  	url := config.AuthCodeURL(state, oauth2.AccessTypeOnline)
    89  
    90  	if browserFlag {
    91  		fmt.Printf("Opening %q...\n", url)
    92  		if webutil.StartBrowser(url) {
    93  			wg.Wait()
    94  			if err = server.Shutdown(ctx); err != nil {
    95  				vlog.Errorf("shutting down: %v", err)
    96  			}
    97  		} else {
    98  			browserFlag = false
    99  		}
   100  	}
   101  
   102  	if !browserFlag {
   103  		config.RedirectURL = "urn:ietf:wg:oauth:2.0:oob"
   104  		url := config.AuthCodeURL(state, oauth2.AccessTypeOnline)
   105  		fmt.Printf("The attempt to automatically open a browser failed. Please open the following link:\n\n\t%s\n\n", url)
   106  		fmt.Printf("Paste the received code and then press enter: ")
   107  		if _, err := fmt.Scanf("%s", &code); err != nil {
   108  			return "", err
   109  		}
   110  		fmt.Println("")
   111  	}
   112  
   113  	vlog.VI(1).Infof("code: %+v", code)
   114  	token, err := config.Exchange(ctx, code)
   115  	if err != nil {
   116  		return "", err
   117  	}
   118  	vlog.VI(1).Infof("ID token: +%v", token.Extra("id_token").(string))
   119  	return token.Extra("id_token").(string), nil
   120  }