oauth2client

package
v1.2.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 25, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package oauth2client provides an OAuth2 client-credentials token manager for gRPC and HTTP clients.

It caches bearer tokens, refreshes them before expiry, and offers gRPC client interceptors to inject Authorization headers automatically. Token fetches honor contexts for cancellation, are thread-safe, and can log refresh events via optional Logger interfaces.

Features

  • Client-credentials flow with automatic caching and early refresh
  • Context-aware token fetching with cancellation and deadline support
  • gRPC unary and stream client interceptors that inject Bearer tokens
  • Optional logging (WithLogger, WithLoggingEnabled)
  • Shareable token manager across multiple gRPC and HTTP clients

Quick Start

tm := oauth2client.NewTokenManager(
    ctx,
    "https://auth.example.com/oauth/v2/token",
    "client-id",
    "client-secret",
    "openid profile email",
    oauth2client.WithLoggingEnabled(),
)

conn, err := grpc.NewClient(
    "server:9090",
    grpc.WithUnaryInterceptor(tm.UnaryClientInterceptor()),
    grpc.WithStreamInterceptor(tm.StreamClientInterceptor()),
)
if err != nil {
    log.Fatal(err)
}

client := http.Client{Transport: httpclient.NewOAuth2Transport(tm, nil)}

Notes

  • GetTokenWithContext is preferred; GetToken is kept for backward compatibility.
  • TokenManager is safe for concurrent use and uses double-checked locking.
Example

Example demonstrates basic usage of TokenManager with gRPC interceptors.

package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"sync"

	"github.com/AmmannChristian/go-authx/oauth2client"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/test/bufconn"
)

const bufSize = 1024

var (
	bufListener = bufconn.Listen(bufSize)
	bufServer   = grpc.NewServer()
	bufOnce     sync.Once
)

func startBufServer() {
	bufOnce.Do(func() {
		go func() {
			_ = bufServer.Serve(bufListener)
		}()
	})
}

func dialBufConn(opts ...grpc.DialOption) (*grpc.ClientConn, error) {
	startBufServer()

	dialOpts := []grpc.DialOption{
		grpc.WithContextDialer(func(c context.Context, _ string) (net.Conn, error) {
			select {
			case <-c.Done():
				return nil, c.Err()
			default:
			}
			return bufListener.Dial()
		}),
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	}

	dialOpts = append(dialOpts, opts...)
	return grpc.NewClient("bufnet", dialOpts...)
}

func main() {
	ctx := context.Background()

	// Create token manager
	tm := oauth2client.NewTokenManager(
		ctx,
		"https://auth.example.com/oauth/v2/token",
		"client-id",
		"client-secret",
		"openid profile email",
	)

	// Use with gRPC client
	conn, err := dialBufConn(
		grpc.WithUnaryInterceptor(tm.UnaryClientInterceptor()),
		grpc.WithStreamInterceptor(tm.StreamClientInterceptor()),
	)
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	fmt.Println("gRPC client configured with OAuth2 authentication")
}
Output:
gRPC client configured with OAuth2 authentication

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Logger

type Logger interface {
	Printf(format string, args ...any)
}

Logger is an interface for optional logging in TokenManager. Implementations can log token refresh events if desired.

type Option

type Option func(*TokenManager)

Option is a functional option for configuring TokenManager.

func WithHTTPClient added in v1.2.3

func WithHTTPClient(c *http.Client) Option

WithHTTPClient overrides the HTTP client used for token requests. Only applicable to TokenManagers created with NewPrivateKeyJWTTokenManager. When not set, a default client with a 5-second timeout is used.

func WithLogger

func WithLogger(logger Logger) Option

WithLogger sets a custom logger for token refresh events. If not set, no logging will occur.

func WithLoggingEnabled

func WithLoggingEnabled() Option

WithLoggingEnabled enables logging using the default Go log package. This is a convenience option that sets the logger to log.Default().

type TokenManager

type TokenManager struct {
	// contains filtered or unexported fields
}

TokenManager manages OAuth2 tokens with automatic refresh. It is safe for concurrent access.

func NewPrivateKeyJWTTokenManager added in v1.2.3

func NewPrivateKeyJWTTokenManager(
	ctx context.Context,
	issuerURI string,
	keyFileJSON string,
	scopes string,
	opts ...Option,
) (*TokenManager, error)

NewPrivateKeyJWTTokenManager creates a TokenManager that obtains tokens via the urn:ietf:params:oauth:grant-type:jwt-bearer grant using a ZITADEL service-account key file.

keyFileJSON must be a ZITADEL service-account JSON:

{"type":"application","keyId":"...","key":"-----BEGIN ...","clientId":"...","appId":"..."}

issuerURI is the ZITADEL issuer, e.g. "https://my-org.zitadel.cloud". scopes is space-separated, e.g. "openid". opts may include WithLogger, WithLoggingEnabled, WithHTTPClient.

func NewTokenManager

func NewTokenManager(ctx context.Context, tokenURL, clientID, clientSecret, scopes string, opts ...Option) *TokenManager

NewTokenManager creates a new OAuth2 token manager using client credentials flow.

Parameters:

Example

ExampleNewTokenManager demonstrates creating a new token manager.

package main

import (
	"context"
	"fmt"

	"github.com/AmmannChristian/go-authx/oauth2client"
)

func main() {
	ctx := context.Background()

	tm := oauth2client.NewTokenManager(
		ctx,
		"https://auth.example.com/oauth/v2/token",
		"my-client-id",
		"my-client-secret",
		"openid profile",
	)

	fmt.Printf("TokenManager created for client: %s\n", "my-client-id")
	_ = tm // Use the token manager

}
Output:
TokenManager created for client: my-client-id

func (*TokenManager) GetToken deprecated

func (tm *TokenManager) GetToken() (string, error)

GetToken returns a valid access token, fetching or refreshing if necessary. This method is thread-safe and uses double-checked locking to minimize lock contention.

Deprecated: Use GetTokenWithContext instead to properly handle context cancellation and deadlines. This method uses a background context and cannot be cancelled by the caller.

Returns:

  • string: Valid access token
  • error: Error if token fetch/refresh fails
Example

ExampleTokenManager_GetToken demonstrates manual token retrieval.

package main

import (
	"context"
	"fmt"

	"github.com/AmmannChristian/go-authx/oauth2client"
)

func main() {
	ctx := context.Background()

	tm := oauth2client.NewTokenManager(
		ctx,
		"https://auth.example.com/oauth/v2/token",
		"client-id",
		"client-secret",
		"openid",
	)

	// This would normally fetch a real token
	// For demonstration purposes, we just show the pattern
	_, err := tm.GetToken()
	if err != nil {
		// Handle error (in production this would connect to real OAuth2 server)
		fmt.Println("Token fetch attempted")
	}

}
Output:
Token fetch attempted

func (*TokenManager) GetTokenWithContext

func (tm *TokenManager) GetTokenWithContext(ctx context.Context) (string, error)

GetTokenWithContext returns a valid access token, fetching or refreshing if necessary. This method respects the provided context's cancellation and deadline. This method is thread-safe and uses double-checked locking to minimize lock contention.

Parameters:

  • ctx: Context for the token request (used for cancellation and deadlines)

Returns:

  • string: Valid access token
  • error: Error if token fetch/refresh fails or context is cancelled

func (*TokenManager) StreamClientInterceptor

func (tm *TokenManager) StreamClientInterceptor() grpc.StreamClientInterceptor

StreamClientInterceptor returns a gRPC stream client interceptor that automatically adds OAuth2 Bearer tokens to request metadata.

The interceptor adds the token as "authorization: Bearer <token>" to the outgoing request context metadata. If token fetch fails, stream creation is aborted with an error. The interceptor respects the RPC context's cancellation and deadline.

Usage:

conn, err := grpc.NewClient(
    "server:9090",
    grpc.WithStreamInterceptor(tokenManager.StreamClientInterceptor()),
)
Example

ExampleTokenManager_StreamClientInterceptor demonstrates using the stream interceptor.

package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"sync"

	"github.com/AmmannChristian/go-authx/oauth2client"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/test/bufconn"
)

const bufSize = 1024

var (
	bufListener = bufconn.Listen(bufSize)
	bufServer   = grpc.NewServer()
	bufOnce     sync.Once
)

func startBufServer() {
	bufOnce.Do(func() {
		go func() {
			_ = bufServer.Serve(bufListener)
		}()
	})
}

func dialBufConn(opts ...grpc.DialOption) (*grpc.ClientConn, error) {
	startBufServer()

	dialOpts := []grpc.DialOption{
		grpc.WithContextDialer(func(c context.Context, _ string) (net.Conn, error) {
			select {
			case <-c.Done():
				return nil, c.Err()
			default:
			}
			return bufListener.Dial()
		}),
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	}

	dialOpts = append(dialOpts, opts...)
	return grpc.NewClient("bufnet", dialOpts...)
}

func main() {
	ctx := context.Background()

	tm := oauth2client.NewTokenManager(
		ctx,
		"https://auth.example.com/oauth/v2/token",
		"client-id",
		"client-secret",
		"openid",
	)

	// Create gRPC connection with stream interceptor
	conn, err := dialBufConn(
		grpc.WithStreamInterceptor(tm.StreamClientInterceptor()),
	)
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	fmt.Println("Stream interceptor configured")
}
Output:
Stream interceptor configured

func (*TokenManager) UnaryClientInterceptor

func (tm *TokenManager) UnaryClientInterceptor() grpc.UnaryClientInterceptor

UnaryClientInterceptor returns a gRPC unary client interceptor that automatically adds OAuth2 Bearer tokens to request metadata.

The interceptor adds the token as "authorization: Bearer <token>" to the outgoing request context metadata. If token fetch fails, the RPC call is aborted with an error. The interceptor respects the RPC context's cancellation and deadline.

Usage:

conn, err := grpc.NewClient(
    "server:9090",
    grpc.WithUnaryInterceptor(tokenManager.UnaryClientInterceptor()),
)
Example

ExampleTokenManager_UnaryClientInterceptor demonstrates using the unary interceptor.

package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"sync"

	"github.com/AmmannChristian/go-authx/oauth2client"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/test/bufconn"
)

const bufSize = 1024

var (
	bufListener = bufconn.Listen(bufSize)
	bufServer   = grpc.NewServer()
	bufOnce     sync.Once
)

func startBufServer() {
	bufOnce.Do(func() {
		go func() {
			_ = bufServer.Serve(bufListener)
		}()
	})
}

func dialBufConn(opts ...grpc.DialOption) (*grpc.ClientConn, error) {
	startBufServer()

	dialOpts := []grpc.DialOption{
		grpc.WithContextDialer(func(c context.Context, _ string) (net.Conn, error) {
			select {
			case <-c.Done():
				return nil, c.Err()
			default:
			}
			return bufListener.Dial()
		}),
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	}

	dialOpts = append(dialOpts, opts...)
	return grpc.NewClient("bufnet", dialOpts...)
}

func main() {
	ctx := context.Background()

	tm := oauth2client.NewTokenManager(
		ctx,
		"https://auth.example.com/oauth/v2/token",
		"client-id",
		"client-secret",
		"openid",
	)

	// Create gRPC connection with unary interceptor
	conn, err := dialBufConn(
		grpc.WithUnaryInterceptor(tm.UnaryClientInterceptor()),
	)
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	fmt.Println("Unary interceptor configured")
}
Output:
Unary interceptor configured

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL