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 ¶
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
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 ¶
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:
- ctx: Context for token requests (used as fallback for backward compatibility)
- tokenURL: OAuth2 token endpoint (e.g., "https://auth.example.com/oauth/v2/token")
- clientID: OAuth2 client identifier
- clientSecret: OAuth2 client secret
- scopes: Space-separated list of OAuth2 scopes (e.g., "openid profile email")
- opts: Optional configuration options (WithLogger, WithLoggingEnabled)
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