golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/iapclient/iapclient.go (about)

     1  // Copyright 2022 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package iapclient enables programmatic access to IAP-secured services. See
     6  // https://cloud.google.com/iap/docs/authentication-howto.
     7  //
     8  // Login will be done as necessary using offline browser-based authentication,
     9  // similarly to gcloud auth login. Credentials will be stored in the user's
    10  // config directory.
    11  package iapclient
    12  
    13  import (
    14  	"context"
    15  	"crypto/tls"
    16  	"encoding/json"
    17  	"fmt"
    18  	"io"
    19  	"net/http"
    20  	"net/url"
    21  	"os"
    22  	"path/filepath"
    23  	"strings"
    24  	"time"
    25  
    26  	"cloud.google.com/go/compute/metadata"
    27  	"golang.org/x/oauth2"
    28  	"golang.org/x/oauth2/google"
    29  	"google.golang.org/api/idtoken"
    30  	"google.golang.org/grpc"
    31  	"google.golang.org/grpc/credentials"
    32  	"google.golang.org/grpc/credentials/oauth"
    33  )
    34  
    35  var gomoteConfig = &oauth2.Config{
    36  	// Gomote client ID and secret.
    37  	ClientID:     "872405196845-odamr0j3kona7rp7fima6h4ummnd078t.apps.googleusercontent.com",
    38  	ClientSecret: "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",
    39  	Endpoint:     google.Endpoint,
    40  	Scopes:       []string{"email openid profile"},
    41  }
    42  
    43  func login(ctx context.Context) (*oauth2.Token, error) {
    44  	resp, err := http.PostForm("https://oauth2.googleapis.com/device/code", url.Values{
    45  		"client_id": []string{gomoteConfig.ClientID},
    46  		"scope":     gomoteConfig.Scopes,
    47  	})
    48  	if err != nil {
    49  		return nil, err
    50  	}
    51  	if resp.StatusCode != http.StatusOK {
    52  		return nil, fmt.Errorf("unexpected status on device code request %v", resp.Status)
    53  	}
    54  	codeResp := &codeResponse{}
    55  	if err := json.NewDecoder(resp.Body).Decode(&codeResp); err != nil {
    56  		return nil, err
    57  	}
    58  	fmt.Printf("Please visit %v in your browser and enter verification code:\n %v\n", codeResp.VerificationURL, codeResp.UserCode)
    59  
    60  	tick := time.NewTicker(time.Duration(codeResp.Interval) * time.Second)
    61  	defer tick.Stop()
    62  
    63  	refresh := &oauth2.Token{}
    64  outer:
    65  	for {
    66  		select {
    67  		case <-ctx.Done():
    68  			return nil, ctx.Err()
    69  		case <-tick.C:
    70  			resp, err := http.PostForm("https://oauth2.googleapis.com/token", url.Values{
    71  				"client_id":     []string{gomoteConfig.ClientID},
    72  				"client_secret": []string{gomoteConfig.ClientSecret},
    73  				"device_code":   []string{codeResp.DeviceCode},
    74  				"grant_type":    []string{"urn:ietf:params:oauth:grant-type:device_code"},
    75  			})
    76  			if err != nil {
    77  				return nil, err
    78  			}
    79  			if resp.StatusCode == http.StatusPreconditionRequired {
    80  				continue
    81  			}
    82  			if resp.StatusCode != http.StatusOK {
    83  				return nil, fmt.Errorf("unexpected status on token request %v", resp.Status)
    84  			}
    85  			if err := json.NewDecoder(resp.Body).Decode(refresh); err != nil {
    86  				return nil, err
    87  			}
    88  			break outer
    89  		}
    90  	}
    91  
    92  	if err := writeToken(refresh); err != nil {
    93  		fmt.Fprintf(os.Stderr, "warning: could not save token, you will be asked to log in again: %v\n", err)
    94  	}
    95  	return refresh, nil
    96  }
    97  
    98  // https://developers.google.com/identity/protocols/oauth2/limited-input-device#step-2:-handle-the-authorization-server-response
    99  type codeResponse struct {
   100  	DeviceCode      string `json:"device_code"`
   101  	Interval        int    `json:"interval"`
   102  	UserCode        string `json:"user_code"`
   103  	VerificationURL string `json:"verification_url"`
   104  }
   105  
   106  func writeToken(refresh *oauth2.Token) error {
   107  	configDir, err := os.UserConfigDir()
   108  	if err != nil {
   109  		return err
   110  	}
   111  	refreshBytes, err := json.Marshal(refresh)
   112  	if err != nil {
   113  		return err
   114  	}
   115  	err = os.MkdirAll(filepath.Join(configDir, "gomote"), 0755)
   116  	if err != nil {
   117  		return err
   118  	}
   119  	return os.WriteFile(filepath.Join(configDir, "gomote/iap-refresh-tv-token"), refreshBytes, 0600)
   120  }
   121  
   122  func cachedToken() (*oauth2.Token, error) {
   123  	configDir, err := os.UserConfigDir()
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  	refreshBytes, err := os.ReadFile(filepath.Join(configDir, "gomote/iap-refresh-tv-token"))
   128  	if err != nil {
   129  		if os.IsNotExist(err) {
   130  			return nil, nil
   131  		}
   132  		return nil, err
   133  	}
   134  	var refreshToken oauth2.Token
   135  	if err := json.Unmarshal(refreshBytes, &refreshToken); err != nil {
   136  		return nil, err
   137  	}
   138  	if !refreshToken.Valid() {
   139  		return nil, nil
   140  	}
   141  	return &refreshToken, nil
   142  }
   143  
   144  // TokenSource returns a TokenSource that can be used to access Go's
   145  // IAP-protected sites. It will prompt for login if necessary.
   146  func TokenSource(ctx context.Context) (oauth2.TokenSource, error) {
   147  	const audience = "872405196845-b6fu2qpi0fehdssmc8qo47h2u3cepi0e.apps.googleusercontent.com" // Go build IAP client ID.
   148  
   149  	if metadata.OnGCE() {
   150  		if project, err := metadata.ProjectID(); err == nil && (project == "symbolic-datum-552" || project == "go-security-trybots") {
   151  			return idtoken.NewTokenSource(ctx, audience)
   152  		}
   153  	}
   154  
   155  	refresh, err := cachedToken()
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  	if refresh == nil {
   160  		refresh, err = login(ctx)
   161  		if err != nil {
   162  			return nil, err
   163  		}
   164  	}
   165  	tokenSource := oauth2.ReuseTokenSource(nil, &jwtTokenSource{gomoteConfig, audience, refresh})
   166  	// Eagerly request a token to verify we're good. The source will cache it.
   167  	if _, err := tokenSource.Token(); err != nil {
   168  		return nil, err
   169  	}
   170  	return tokenSource, nil
   171  }
   172  
   173  // HTTPClient returns an http.Client that can be used to access Go's
   174  // IAP-protected sites. It will prompt for login if necessary.
   175  func HTTPClient(ctx context.Context) (*http.Client, error) {
   176  	ts, err := TokenSource(ctx)
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  	return oauth2.NewClient(ctx, ts), nil
   181  }
   182  
   183  // GRPCClient returns a *gprc.ClientConn that can access Go's IAP-protected
   184  // servers. It will prompt for login if necessary.
   185  func GRPCClient(ctx context.Context, addr string) (*grpc.ClientConn, error) {
   186  	ts, err := TokenSource(ctx)
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  	opts := []grpc.DialOption{
   191  		grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{InsecureSkipVerify: strings.HasPrefix(addr, "localhost:")})),
   192  		grpc.WithDefaultCallOptions(grpc.PerRPCCredentials(oauth.TokenSource{TokenSource: ts})),
   193  		grpc.WithBlock(),
   194  	}
   195  	return grpc.DialContext(ctx, addr, opts...)
   196  }
   197  
   198  type jwtTokenSource struct {
   199  	conf     *oauth2.Config
   200  	audience string
   201  	refresh  *oauth2.Token
   202  }
   203  
   204  // Token exchanges a refresh token for a JWT that works with IAP. As of writing, there
   205  // isn't anything to do this in the oauth2 library or google.golang.org/api/idtoken.
   206  func (s *jwtTokenSource) Token() (*oauth2.Token, error) {
   207  	resp, err := http.PostForm(s.conf.Endpoint.TokenURL, url.Values{
   208  		"client_id":     []string{s.conf.ClientID},
   209  		"client_secret": []string{s.conf.ClientSecret},
   210  		"refresh_token": []string{s.refresh.RefreshToken},
   211  		"grant_type":    []string{"refresh_token"},
   212  		"audience":      []string{s.audience},
   213  	})
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  	defer resp.Body.Close()
   218  	if resp.StatusCode != http.StatusOK {
   219  		body, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10))
   220  		return nil, fmt.Errorf("IAP token exchange failed: status %v, body %q", resp.Status, body)
   221  	}
   222  	body, err := io.ReadAll(resp.Body)
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  	var token jwtTokenJSON
   227  	if err := json.Unmarshal(body, &token); err != nil {
   228  		return nil, err
   229  	}
   230  	return &oauth2.Token{
   231  		TokenType:   "Bearer",
   232  		AccessToken: token.IDToken,
   233  	}, nil
   234  }
   235  
   236  type jwtTokenJSON struct {
   237  	IDToken string `json:"id_token"`
   238  }