blob: 955f90d8b788ee91b74ac454ab685a1a3eb32959 [file] [log] [blame]
// Copyright 2022 Go 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 iapclient enables programmatic access to IAP-secured services. See
// https://cloud.google.com/iap/docs/authentication-howto.
//
// Login will be done as necessary using offline browser-based authentication,
// similarly to gcloud auth login. Credentials will be stored in the user's
// config directory.
package iapclient
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
var gomoteConfig = &oauth2.Config{
// Gomote client ID and secret.
ClientID: "872405196845-cc4c60gbf7mrmutpocsgl1asjb65du73.apps.googleusercontent.com",
ClientSecret: "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",
Endpoint: google.Endpoint,
RedirectURL: "urn:ietf:wg:oauth:2.0:oob",
Scopes: []string{"openid email"},
}
func login(ctx context.Context) (*oauth2.Token, error) {
const xsrfToken = "unused" // We don't actually get redirects, so we have no chance to check this.
codeURL := gomoteConfig.AuthCodeURL(xsrfToken, oauth2.AccessTypeOffline)
fmt.Printf("Go to the following link in your browser:\n\n\t%v\n\nEnter verification code: ", codeURL)
var code string
fmt.Scanln(&code)
refresh, err := gomoteConfig.Exchange(ctx, code, oauth2.AccessTypeOffline)
if err != nil {
return nil, err
}
if err := writeToken(refresh); err != nil {
fmt.Fprintf(os.Stderr, "warning: could not save token, you will be asked to log in again: %v\n", err)
}
return refresh, nil
}
func writeToken(refresh *oauth2.Token) error {
configDir, err := os.UserConfigDir()
if err != nil {
return err
}
refreshBytes, err := json.Marshal(refresh)
if err != nil {
return err
}
err = os.MkdirAll(filepath.Join(configDir, "gomote"), 0755)
if err != nil {
return err
}
return os.WriteFile(filepath.Join(configDir, "gomote/iap-refresh-token"), refreshBytes, 0600)
}
func cachedToken() (*oauth2.Token, error) {
configDir, err := os.UserConfigDir()
if err != nil {
return nil, err
}
refreshBytes, err := os.ReadFile(filepath.Join(configDir, "gomote/iap-refresh-token"))
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
var refreshToken oauth2.Token
if err := json.Unmarshal(refreshBytes, &refreshToken); err != nil {
return nil, err
}
return &refreshToken, nil
}
// TokenSource returns a TokenSource that can be used to access Go's
// IAP-protected sites. It will prompt for login if necessary.
func TokenSource(ctx context.Context) (oauth2.TokenSource, error) {
refresh, err := cachedToken()
if err != nil {
return nil, err
}
if refresh == nil {
refresh, err = login(ctx)
if err != nil {
return nil, err
}
}
const audience = "872405196845-b6fu2qpi0fehdssmc8qo47h2u3cepi0e.apps.googleusercontent.com" // Go build IAP client ID.
tokenSource := oauth2.ReuseTokenSource(nil, &jwtTokenSource{gomoteConfig, audience, refresh})
// Eagerly request a token to verify we're good. The source will cache it.
if _, err := tokenSource.Token(); err != nil {
return nil, err
}
return tokenSource, nil
}
// HTTPClient returns an http.Client that can be used to access Go's
// IAP-protected sites. It will prompt for login if necessary.
func HTTPClient(ctx context.Context) (*http.Client, error) {
ts, err := TokenSource(ctx)
if err != nil {
return nil, err
}
return oauth2.NewClient(ctx, ts), nil
}
type jwtTokenSource struct {
conf *oauth2.Config
audience string
refresh *oauth2.Token
}
// Token exchanges a refresh token for a JWT that works with IAP. As of writing, there
// isn't anything to do this in the oauth2 library or google.golang.org/api/idtoken.
func (s *jwtTokenSource) Token() (*oauth2.Token, error) {
resp, err := http.PostForm(s.conf.Endpoint.TokenURL, url.Values{
"client_id": []string{s.conf.ClientID},
"client_secret": []string{s.conf.ClientSecret},
"refresh_token": []string{s.refresh.RefreshToken},
"grant_type": []string{"refresh_token"},
"audience": []string{s.audience},
})
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10))
return nil, fmt.Errorf("IAP token exchange failed: status %v, body %q", resp.Status, body)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var token jwtTokenJSON
if err := json.Unmarshal(body, &token); err != nil {
return nil, err
}
return &oauth2.Token{
TokenType: "Bearer",
AccessToken: token.IDToken,
}, nil
}
type jwtTokenJSON struct {
IDToken string `json:"id_token"`
}