From 8c25af97e3f72b990a879e8002bc2c84194c4b81 Mon Sep 17 00:00:00 2001 From: Ken Cenerelli Date: Wed, 4 Mar 2026 11:30:08 -0800 Subject: [PATCH 1/4] docs: refactor getTokenFromWeb for OAuth2 callback handling Updated the getTokenFromWeb function to set the redirect URL and handle the OAuth2 callback with a local server. Fixes issue caused by the deprecation of Google's Out-Of-Band (OOB) OAuth flow. --- gmail/quickstart/quickstart.go | 43 ++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/gmail/quickstart/quickstart.go b/gmail/quickstart/quickstart.go index 295a9b1..5114cae 100644 --- a/gmail/quickstart/quickstart.go +++ b/gmail/quickstart/quickstart.go @@ -47,15 +47,48 @@ func getClient(config *oauth2.Config) *http.Client { // Request a token from the web, then returns the retrieved token. func getTokenFromWeb(config *oauth2.Config) *oauth2.Token { + // Set the redirect URL to match the local server we are about to start + config.RedirectURL = "http://localhost:8080" + authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) - fmt.Printf("Go to the following link in your browser then type the "+ - "authorization code: \n%v\n", authURL) + fmt.Printf("Go to the following link in your browser: \n%v\n", authURL) + + // Create a channel to wait for the authorization code + codeCh := make(chan string) + + // Create a local HTTP multiplexer and server to listen for the OAuth2 callback + m := http.NewServeMux() + server := &http.Server{Addr: ":8080", Handler: m} + + m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + code := r.URL.Query().Get("code") + if code != "" { + fmt.Fprintf(w, "Authentication successful! You may close this window.") + codeCh <- code + } else { + fmt.Fprintf(w, "Failed to get authorization code. You may close this window.") + codeCh <- "" + } + }) + + // Start the server in a goroutine + go func() { + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Unable to start local web server: %v", err) + } + }() + + // Wait for the code to be extracted from the callback + authCode := <-codeCh + + // Shutdown the server gracefully now that we have the code + server.Shutdown(context.Background()) - var authCode string - if _, err := fmt.Scan(&authCode); err != nil { - log.Fatalf("Unable to read authorization code: %v", err) + if authCode == "" { + log.Fatalf("Authorization failed or code not returned.") } + // Exchange the authorization code for an access token tok, err := config.Exchange(context.TODO(), authCode) if err != nil { log.Fatalf("Unable to retrieve token from web: %v", err) From 419a50c14fb500c71447701ac9f1574036a6a671 Mon Sep 17 00:00:00 2001 From: Ken Cenerelli Date: Wed, 4 Mar 2026 12:11:49 -0800 Subject: [PATCH 2/4] Enhance OAuth2 flow with dynamic port and state Refactor OAuth2 callback handling to use dynamic port and secure state parameter. --- gmail/quickstart/quickstart.go | 74 +++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 18 deletions(-) diff --git a/gmail/quickstart/quickstart.go b/gmail/quickstart/quickstart.go index 5114cae..71e8da4 100644 --- a/gmail/quickstart/quickstart.go +++ b/gmail/quickstart/quickstart.go @@ -22,8 +22,10 @@ import ( "encoding/json" "fmt" "log" + "net" "net/http" "os" + "time" "golang.org/x/oauth2" "golang.org/x/oauth2/google" @@ -45,51 +47,87 @@ func getClient(config *oauth2.Config) *http.Client { return config.Client(context.Background(), tok) } +// generateState creates a secure random string for the OAuth2 state parameter. +func generateState() string { + b := make([]byte, 16) + rand.Read(b) + return base64.URLEncoding.EncodeToString(b) +} + +// Request a token from the web, then returns the retrieved token. // Request a token from the web, then returns the retrieved token. func getTokenFromWeb(config *oauth2.Config) *oauth2.Token { - // Set the redirect URL to match the local server we are about to start - config.RedirectURL = "http://localhost:8080" + listener, err := net.Listen("tcp", "localhost:0") + if err != nil { + log.Fatalf("Unable to start local listener: %v", err) + } + + port := listener.Addr().(*net.TCPAddr).Port + config.RedirectURL = fmt.Sprintf("http://localhost:%d", port) + + // Generate a dynamic, cryptographically secure state parameter + state := generateState() - authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) + authURL := config.AuthCodeURL(state, oauth2.AccessTypeOffline) fmt.Printf("Go to the following link in your browser: \n%v\n", authURL) - // Create a channel to wait for the authorization code + // Channels to handle successful codes and errors codeCh := make(chan string) + errCh := make(chan error) - // Create a local HTTP multiplexer and server to listen for the OAuth2 callback m := http.NewServeMux() - server := &http.Server{Addr: ":8080", Handler: m} + server := &http.Server{Handler: m} m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if errStr := r.URL.Query().Get("error"); errStr != "" { + fmt.Fprintf(w, "Authentication error: %s. You may close this window.", errStr) + errCh <- errors.New(errStr) + return + } + + returnedState := r.URL.Query().Get("state") + if returnedState != state { + fmt.Fprintf(w, "Security error: invalid state parameter. You may close this window.") + errCh <- errors.New("invalid state parameter") + return + } + code := r.URL.Query().Get("code") if code != "" { fmt.Fprintf(w, "Authentication successful! You may close this window.") codeCh <- code } else { fmt.Fprintf(w, "Failed to get authorization code. You may close this window.") - codeCh <- "" + errCh <- errors.New("authorization code missing") } }) - // Start the server in a goroutine go func() { - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + if err := server.Serve(listener); err != nil && err != http.ErrServerClosed { log.Fatalf("Unable to start local web server: %v", err) } }() - // Wait for the code to be extracted from the callback - authCode := <-codeCh + var authCode string + + // Wait for a successful code, an error, or a timeout + select { + case authCode = <-codeCh: + // Success case, proceed to shutdown and exchange + case callbackErr := <-errCh: + server.Shutdown(context.Background()) + log.Fatalf("Authorization failed during callback: %v", callbackErr) + case <-time.After(3 * time.Minute): + // Timeout case: user took too long or closed the browser + server.Shutdown(context.Background()) + log.Fatalf("Authorization timed out after 3 minutes. Please try again.") + } - // Shutdown the server gracefully now that we have the code + // Shutdown the server gracefully upon success server.Shutdown(context.Background()) - if authCode == "" { - log.Fatalf("Authorization failed or code not returned.") - } - - // Exchange the authorization code for an access token - tok, err := config.Exchange(context.TODO(), authCode) + // Exchange the authorization code for an access token using context.Background() + tok, err := config.Exchange(context.Background(), authCode) if err != nil { log.Fatalf("Unable to retrieve token from web: %v", err) } From 7ce3053a522570787a6a75c3766e7b5120e6a5cd Mon Sep 17 00:00:00 2001 From: Ken Cenerelli Date: Wed, 4 Mar 2026 12:22:17 -0800 Subject: [PATCH 3/4] Add crypto/rand and encoding/base64 imports --- gmail/quickstart/quickstart.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gmail/quickstart/quickstart.go b/gmail/quickstart/quickstart.go index 71e8da4..dfdd597 100644 --- a/gmail/quickstart/quickstart.go +++ b/gmail/quickstart/quickstart.go @@ -19,6 +19,8 @@ package main import ( "context" + "crypto/rand" + "encoding/base64" "encoding/json" "fmt" "log" From dae1b7574b422fe291b331bc493d5afc8c6dfeee Mon Sep 17 00:00:00 2001 From: Ken Cenerelli Date: Wed, 4 Mar 2026 12:24:02 -0800 Subject: [PATCH 4/4] Add 'errors' package import to quickstart.go --- gmail/quickstart/quickstart.go | 1 + 1 file changed, 1 insertion(+) diff --git a/gmail/quickstart/quickstart.go b/gmail/quickstart/quickstart.go index dfdd597..74184aa 100644 --- a/gmail/quickstart/quickstart.go +++ b/gmail/quickstart/quickstart.go @@ -22,6 +22,7 @@ import ( "crypto/rand" "encoding/base64" "encoding/json" + "errors" "fmt" "log" "net"