From c6882388ea1699cf425e8ff92603423aebb8eb1a Mon Sep 17 00:00:00 2001 From: Gil Raphaelli Date: Wed, 3 Jun 2026 13:28:43 -0400 Subject: [PATCH] Add -google-adc flag for Application Default Credentials Support authenticating to Google with Application Default Credentials (GOOGLE_APPLICATION_CREDENTIALS, gcloud user creds, or the GCP metadata server) as an alternative to the interactive OAuth flow and google.config.json/google.creds.json files. - google: add NewClientFromADC and a tokenSource path through HasCreds, Service, and the OAuth web handlers (which become no-ops in ADC mode) - main: add -google-adc flag selecting the constructor - README: document the flag and credential discovery order Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 23 +++++++++++- cmd/google/findfolders/findfolders.go | 19 +++++++--- google/google.go | 50 +++++++++++++++++++++++++++ main.go | 19 +++++++--- 4 files changed, 100 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 2a30fac..152169f 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,27 @@ Once tokens have been obtained, `zat -no-server` will perform only archival duti ### Credentials * Obtain Google credentials + + There are two options: the interactive OAuth flow (default), or Application Default Credentials. + + **Option A: Application Default Credentials (ADC)** + + Run `zat -google-adc` to authenticate with [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) + instead of `google.config.json`/`google.creds.json`. There is no interactive login flow and no creds files to manage. + Credentials are discovered, in order, from: + 1. the `GOOGLE_APPLICATION_CREDENTIALS` environment variable (path to a service account key) + 2. the gcloud-managed user credentials file + 3. the GCP metadata server (when running on GCE, Cloud Run, etc.) + + For local user credentials, grant the Drive scope when logging in: + ``` + gcloud auth application-default login --scopes=https://www.googleapis.com/auth/drive,https://www.googleapis.com/auth/cloud-platform + ``` + + Note: a service account can only access Drive folders shared with it (or its own Drive) unless domain-wide delegation is configured. + + **Option B: interactive OAuth (default)** + * [Create an Oauth Client ID credential](https://console.cloud.google.com/apis/credentials) * You may need to create a project, or use a dev project you have access to. If the project doesn't have OAuth consent screen info, you'll need to add that as well. * Choose "internal" user type, give it a name similar to the project name, and add your contact email @@ -61,7 +82,7 @@ Once tokens have been obtained, `zat -no-server` will perform only archival duti } ``` -Once the credentials are in place, re-run `zat` and use the web server at http://localhost:8080/ to login to both Google and Zoom to create the `*.creds.json` files zat will use for the next run. +Once the credentials are in place, re-run `zat` and use the web server at http://localhost:8080/ to login to both Google and Zoom to create the `*.creds.json` files zat will use for the next run. (When using `-google-adc`, the Google web login is skipped — only Zoom needs the interactive flow.) ### Configuration diff --git a/cmd/google/findfolders/findfolders.go b/cmd/google/findfolders/findfolders.go index 1683438..2ed6b72 100644 --- a/cmd/google/findfolders/findfolders.go +++ b/cmd/google/findfolders/findfolders.go @@ -15,14 +15,23 @@ import ( func main() { andQuery := flag.String("query", "", "google drive query: https://developers.google.com/drive/api/v3/search-files") cfgDir := cmd.FlagConfigDir() + googleADC := flag.Bool("google-adc", false, + "authenticate to Google with Application Default Credentials instead of "+ + cmd.GoogleConfigPath+"/"+cmd.GoogleCredsPath) flag.Parse() logger := log.New(os.Stderr, "", cmd.LogFmt) - googleClient, err := google.NewClientFromFile( - logger, - path.Join(*cfgDir, cmd.GoogleConfigPath), - google.NewCredentialsManager(cmd.GoogleCredsPath).ClientOption, - ) + var googleClient *google.Client + var err error + if *googleADC { + googleClient, err = google.NewClientFromADC(context.TODO(), logger) + } else { + googleClient, err = google.NewClientFromFile( + logger, + path.Join(*cfgDir, cmd.GoogleConfigPath), + google.NewCredentialsManager(cmd.GoogleCredsPath).ClientOption, + ) + } if err != nil { logger.Fatal(err) } diff --git a/google/google.go b/google/google.go index affee7e..efadfad 100644 --- a/google/google.go +++ b/google/google.go @@ -21,9 +21,13 @@ type Client struct { logger *log.Logger httpClient *http.Client + // cloud creds config *oauth2.Config credentials *oauth2.Token cm *credentialsManager + + // ADC + tokenSource oauth2.TokenSource } type ClientOption func(*Client) @@ -67,6 +71,29 @@ func NewClientFromReader(logger *log.Logger, r io.Reader, options ...ClientOptio return NewClient(logger, config, options...) } +// NewClientFromADC builds a Client that authenticates with Application Default +// Credentials. Credentials are discovered from the GOOGLE_APPLICATION_CREDENTIALS +// environment variable, the gcloud-managed ADC file, or the GCP metadata server. +// No google.config.json / google.creds.json files are needed and the interactive +// OAuth flow is skipped. +func NewClientFromADC(ctx context.Context, logger *log.Logger, options ...ClientOption) (*Client, error) { + creds, err := google.FindDefaultCredentials(ctx, drive.DriveScope) + if err != nil { + return nil, err + } + c := &Client{ + logger: logger, + httpClient: http.DefaultClient, + tokenSource: creds.TokenSource, + } + + for _, o := range options { + o(c) + } + return c, nil +} + +// NewClient builds a Client that authenticates with Google Cloud Credentials func NewClient(logger *log.Logger, config *oauth2.Config, options ...ClientOption) (*Client, error) { c := &Client{ logger: logger, @@ -90,6 +117,16 @@ func (c *Client) updateCreds(token *oauth2.Token) { } func (c *Client) HasCreds() bool { + if c.tokenSource != nil { + // ADC: the token source handles fetching/refreshing. A successful + // Token() call confirms credentials are available and valid. + if _, err := c.tokenSource.Token(); err != nil { + c.logger.Printf("error getting ADC google token %s", err) + return false + } + return true + } + if c.credentials == nil { return false } @@ -114,6 +151,11 @@ func (c *Client) HasCreds() bool { } func (c *Client) OauthRedirect(w http.ResponseWriter, r *http.Request) { + if c.tokenSource != nil { + // ADC mode has no interactive OAuth flow. + http.Redirect(w, r, "/", http.StatusFound) + return + } c.logger.Println(c.config.RedirectURL) http.Redirect(w, r, c.config.RedirectURL, http.StatusFound) } @@ -121,6 +163,11 @@ func (c *Client) OauthRedirect(w http.ResponseWriter, r *http.Request) { // TODO: use / validate state token func (c *Client) OauthHandler() func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { + if c.tokenSource != nil { + // ADC mode has no interactive OAuth flow. + http.Redirect(w, r, "/", http.StatusFound) + return + } if r.FormValue("refresh") != "" || !c.credentials.Valid() { c.updateCreds(nil) } @@ -147,6 +194,9 @@ func (c *Client) OauthHandler() func(w http.ResponseWriter, r *http.Request) { } func (c *Client) Service(ctx context.Context) (*drive.Service, error) { + if c.tokenSource != nil { + return drive.NewService(ctx, option.WithTokenSource(c.tokenSource)) + } return drive.NewService(ctx, option.WithTokenSource(c.config.TokenSource(ctx, c.credentials))) } diff --git a/main.go b/main.go index 16d7606..cdfc20f 100644 --- a/main.go +++ b/main.go @@ -575,6 +575,9 @@ func main() { uploadFilter := flag.String("t", "", "comma separated list of file types to archive (mp4, m4a, timeline, transcript, chat, cc, csv), see: "+ "https://marketplace.zoom.us/docs/api-reference/zoom-api/cloud-recording/recordingget") + googleADC := flag.Bool("google-adc", false, + "authenticate to Google with Application Default Credentials instead of "+ + cmd.GoogleConfigPath+"/"+cmd.GoogleCredsPath) flag.Parse() logger := log.New(os.Stderr, "", cmd.LogFmt) @@ -583,11 +586,17 @@ func main() { http.DefaultClient = apmhttp.WrapClient(http.DefaultClient) http.DefaultTransport = apmhttp.WrapRoundTripper(http.DefaultTransport) - googleClient, err := google.NewClientFromFile( - logger, - path.Join(*cfgDir, cmd.GoogleConfigPath), - google.NewCredentialsManager(path.Join(*cfgDir, cmd.GoogleCredsPath)).ClientOption, - ) + var err error + var googleClient *google.Client + if *googleADC { + googleClient, err = google.NewClientFromADC(context.Background(), logger) + } else { + googleClient, err = google.NewClientFromFile( + logger, + path.Join(*cfgDir, cmd.GoogleConfigPath), + google.NewCredentialsManager(path.Join(*cfgDir, cmd.GoogleCredsPath)).ClientOption, + ) + } if err != nil { logger.Fatal(err) }