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 }