or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

auth-handlers.mdclient-credentials.mdcore-oauth2.mdendpoints.mdgoogle-auth.mdgoogle-downscope.mdgoogle-external-account.mdindex.mdjira-oauth.mdjwt-jws.md
tile.json

auth-handlers.mddocs/

Authorization Handlers

Custom authorization handlers for implementing 3-legged OAuth2 flows with flexible user consent mechanisms and PKCE support.

Package

import "golang.org/x/oauth2/authhandler"

Overview

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:

  • CLI applications that need to open browsers or display URLs
  • Desktop applications with custom UI for OAuth consent
  • Applications that need specialized authorization workflows
  • Environments where the standard redirect-based flow isn't suitable

Authorization Handler Function

type AuthorizationHandler func(authCodeURL string) (code string, state string, err error)

An AuthorizationHandler is a function that:

  1. Receives the authorization URL
  2. Prompts the user for consent (implementation-specific)
  3. Returns the authorization code and state after user approval

The handler implementation determines how to present the URL to the user and obtain the authorization code.

Creating Token Sources

Basic Token Source

func TokenSource(ctx context.Context, config *oauth2.Config, state string, authHandler AuthorizationHandler) oauth2.TokenSource

Returns 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 operation
  • config: 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 code

Token Source with PKCE

func TokenSourceWithPKCE(ctx context.Context, config *oauth2.Config, state string, authHandler AuthorizationHandler, pkce *PKCEParams) oauth2.TokenSource

Enhanced version with PKCE (Proof Key for Code Exchange) support for additional security against authorization code interception attacks.

Parameters:

  • Same as TokenSource, plus:
  • pkce: PKCE parameters (challenge, method, and verifier)

PKCE Parameters

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/

Example: CLI Application with Browser

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()
}

Example: With Local Callback Server

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)
}

Example: With PKCE

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.

Example: Desktop Application

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)
}

State Verification

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"

Security Considerations

  1. State Parameter: Always use a unique, random state parameter for each authorization flow
  2. PKCE: Use TokenSourceWithPKCE for enhanced security, especially in public clients
  3. HTTPS: Always use HTTPS redirect URIs in production
  4. Browser Security: Be cautious when opening browsers programmatically
  5. Timeout: Implement timeouts for authorization flows
  6. Error Handling: Handle authorization errors and rejections gracefully

Use Cases

Command-Line Tools

Perfect for CLI tools that need user authorization:

  • gcloud command-line tool
  • Developer tools and utilities
  • CI/CD integration tools

Desktop Applications

Native desktop apps with custom OAuth flows:

  • Electron applications
  • Native macOS/Windows/Linux apps
  • Cross-platform desktop tools

Embedded Devices

Devices without standard web browsers:

  • IoT devices with displays
  • Kiosks and terminals
  • Smart displays

Testing and Development

Useful for automated testing and development:

  • Integration tests requiring OAuth
  • Development environments
  • Debugging OAuth flows

Error Handling

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
}

Integration with Google OAuth

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)