Custom authorization handlers for implementing 3-legged OAuth2 flows with flexible user consent mechanisms and PKCE support.
import "golang.org/x/oauth2/authhandler"The authhandler package provides a TokenSource implementation that uses custom authorization handlers to obtain user consent in 3-legged OAuth2 flows. This is useful for:
type AuthorizationHandler func(authCodeURL string) (code string, state string, err error)An AuthorizationHandler is a function that:
The handler implementation determines how to present the URL to the user and obtain the authorization code.
func TokenSource(ctx context.Context, config *oauth2.Config, state string, authHandler AuthorizationHandler) oauth2.TokenSourceReturns an oauth2.TokenSource that fetches access tokens using a 3-legged OAuth flow with the provided authorization handler.
Parameters:
ctx: Context used for OAuth2 Exchange operationconfig: Full OAuth2 configuration (ClientID, ClientSecret, Endpoint, Scopes)state: Unique state string for CSRF protection (verified before token exchange)authHandler: Function to obtain user consent and authorization codefunc TokenSourceWithPKCE(ctx context.Context, config *oauth2.Config, state string, authHandler AuthorizationHandler, pkce *PKCEParams) oauth2.TokenSourceEnhanced version with PKCE (Proof Key for Code Exchange) support for additional security against authorization code interception attacks.
Parameters:
TokenSource, plus:pkce: PKCE parameters (challenge, method, and verifier)type PKCEParams struct {
// Challenge is the unpadded, base64-url-encoded encrypted code verifier
Challenge string
// ChallengeMethod is the encryption method (e.g., "S256")
ChallengeMethod string
// Verifier is the original, non-encrypted secret
Verifier string
}See: https://www.oauth.com/oauth2-servers/pkce/
package main
import (
"context"
"fmt"
"log"
"os/exec"
"runtime"
"golang.org/x/oauth2"
"golang.org/x/oauth2/authhandler"
"golang.org/x/oauth2/google"
)
func main() {
ctx := context.Background()
// Configure OAuth2
config := &oauth2.Config{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
Scopes: []string{"email", "profile"},
Endpoint: google.Endpoint,
RedirectURL: "http://localhost:8080/callback",
}
// Create authorization handler that opens browser
authHandler := func(authURL string) (string, string, error) {
// Open browser
fmt.Printf("Opening browser for authorization...\n")
openBrowser(authURL)
// In a real app, start a local server to receive callback
// Here we'll just prompt for manual entry
fmt.Printf("Visit: %s\n", authURL)
fmt.Printf("After authorizing, paste the code: ")
var code string
fmt.Scanln(&code)
// Return code and state
return code, "random-state", nil
}
// Create token source
ts := authhandler.TokenSource(ctx, config, "random-state", authHandler)
// Get token (triggers auth flow)
token, err := ts.Token()
if err != nil {
log.Fatal(err)
}
// Create authenticated client
client := oauth2.NewClient(ctx, ts)
// Use client...
fmt.Printf("Access Token: %s\n", token.AccessToken)
}
func openBrowser(url string) error {
var cmd string
var args []string
switch runtime.GOOS {
case "windows":
cmd = "cmd"
args = []string{"/c", "start"}
case "darwin":
cmd = "open"
default: // "linux", "freebsd", "openbsd", "netbsd"
cmd = "xdg-open"
}
args = append(args, url)
return exec.Command(cmd, args...).Start()
}package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/authhandler"
"golang.org/x/oauth2/google"
)
func main() {
ctx := context.Background()
config := &oauth2.Config{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
Scopes: []string{"email", "profile"},
Endpoint: google.Endpoint,
RedirectURL: "http://localhost:8080/callback",
}
// Create authorization handler with callback server
authHandler := func(authURL string) (string, string, error) {
codeChan := make(chan string)
stateChan := make(chan string)
errChan := make(chan error)
// Start local server for callback
server := &http.Server{Addr: ":8080"}
http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
if code == "" {
errChan <- fmt.Errorf("no code in callback")
return
}
codeChan <- code
stateChan <- state
fmt.Fprintf(w, "Authorization successful! You can close this window.")
// Shutdown server
go func() {
time.Sleep(100 * time.Millisecond)
server.Shutdown(context.Background())
}()
})
// Start server in background
go func() {
if err := server.ListenAndServe(); err != http.ErrServerClosed {
errChan <- err
}
}()
// Open browser
fmt.Printf("Visit: %s\n", authURL)
// Wait for callback or error
select {
case code := <-codeChan:
state := <-stateChan
return code, state, nil
case err := <-errChan:
return "", "", err
case <-time.After(5 * time.Minute):
return "", "", fmt.Errorf("authorization timeout")
}
}
// Create token source
ts := authhandler.TokenSource(ctx, config, "random-state", authHandler)
// Get token
token, err := ts.Token()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Got token: %s\n", token.AccessToken)
}package main
import (
"context"
"crypto/sha256"
"encoding/base64"
"fmt"
"log"
"math/rand"
"golang.org/x/oauth2"
"golang.org/x/oauth2/authhandler"
"golang.org/x/oauth2/google"
)
func main() {
ctx := context.Background()
config := &oauth2.Config{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
Scopes: []string{"email", "profile"},
Endpoint: google.Endpoint,
RedirectURL: "http://localhost:8080/callback",
}
// Generate PKCE parameters
pkce := generatePKCE()
// Authorization handler
authHandler := func(authURL string) (string, string, error) {
fmt.Printf("Visit: %s\n", authURL)
var code string
fmt.Printf("Enter authorization code: ")
fmt.Scanln(&code)
return code, "random-state", nil
}
// Create token source with PKCE
ts := authhandler.TokenSourceWithPKCE(ctx, config, "random-state", authHandler, pkce)
// Get token
token, err := ts.Token()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Access Token: %s\n", token.AccessToken)
}
func generatePKCE() *authhandler.PKCEParams {
// Generate random verifier
verifier := make([]byte, 32)
rand.Read(verifier)
verifierStr := base64.RawURLEncoding.EncodeToString(verifier)
// Generate S256 challenge
hash := sha256.Sum256([]byte(verifierStr))
challenge := base64.RawURLEncoding.EncodeToString(hash[:])
return &authhandler.PKCEParams{
Challenge: challenge,
ChallengeMethod: "S256",
Verifier: verifierStr,
}
}Note: For production PKCE usage, prefer using oauth2.GenerateVerifier() and oauth2.S256ChallengeFromVerifier() from the main oauth2 package.
package main
import (
"context"
"fmt"
"golang.org/x/oauth2"
"golang.org/x/oauth2/authhandler"
"golang.org/x/oauth2/google"
)
// Simulated UI dialog
func showAuthDialog(url string) (string, error) {
// In a real desktop app, this would:
// 1. Show a dialog with the URL or embedded browser
// 2. Wait for user to complete authorization
// 3. Extract and return the code
fmt.Printf("Please authorize at: %s\n", url)
var code string
fmt.Printf("Enter code: ")
fmt.Scanln(&code)
return code, nil
}
func main() {
ctx := context.Background()
config := &oauth2.Config{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
Scopes: []string{"email"},
Endpoint: google.Endpoint,
RedirectURL: "urn:ietf:wg:oauth:2.0:oob", // Out-of-band
}
// Desktop-specific auth handler
authHandler := func(authURL string) (string, string, error) {
code, err := showAuthDialog(authURL)
if err != nil {
return "", "", err
}
return code, "desktop-state", nil
}
ts := authhandler.TokenSource(ctx, config, "desktop-state", authHandler)
token, err := ts.Token()
if err != nil {
panic(err)
}
client := oauth2.NewClient(ctx, ts)
// Use client...
fmt.Printf("Authenticated! Token: %s\n", token.AccessToken)
}The authhandler package automatically verifies that the state parameter matches between the authorization request and response. This prevents CSRF attacks.
// State mismatch returns an error
authHandler := func(authURL string) (string, string, error) {
// ... obtain code ...
return code, "wrong-state", nil // Will cause error if doesn't match
}
ts := authhandler.TokenSource(ctx, config, "expected-state", authHandler)
_, err := ts.Token() // Error: "state mismatch in 3-legged-OAuth flow"TokenSourceWithPKCE for enhanced security, especially in public clientsPerfect for CLI tools that need user authorization:
gcloud command-line toolNative desktop apps with custom OAuth flows:
Devices without standard web browsers:
Useful for automated testing and development:
ts := authhandler.TokenSource(ctx, config, state, authHandler)
token, err := ts.Token()
if err != nil {
// Check for specific errors
switch {
case err.Error() == "state mismatch in 3-legged-OAuth flow":
log.Println("CSRF attack detected or state mismatch")
default:
if rerr, ok := err.(*oauth2.RetrieveError); ok {
log.Printf("OAuth error: %s - %s",
rerr.ErrorCode, rerr.ErrorDescription)
}
log.Printf("Authorization failed: %v", err)
}
return
}import (
"golang.org/x/oauth2/authhandler"
"golang.org/x/oauth2/google"
)
// Google-specific configuration
config := &oauth2.Config{
ClientID: "xxx.apps.googleusercontent.com",
ClientSecret: "secret",
Scopes: []string{
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
},
Endpoint: google.Endpoint,
RedirectURL: "http://localhost:8080/callback",
}
ts := authhandler.TokenSource(ctx, config, state, handler)