oidc

package
v0.2.113 Latest Latest
Warning

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

Go to latest
Published: Apr 28, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package oidc provides shared OpenID Connect client utilities for OAuth providers.

This package implements secure OIDC discovery, input validation, and common utilities used by OIDC-based providers (Dex, Keycloak, Azure AD, etc.).

Security Features

  • SSRF protection for issuer URLs (blocks private IPs, localhost, link-local)
  • HTTPS enforcement for all endpoints
  • Input validation for parameters and claims
  • Discovery document caching with TTL
  • Thread-safe operations

Example Usage

// Create discovery client
client := oidc.NewDiscoveryClient(nil, 1*time.Hour, logger)

// Discover OIDC endpoints
doc, err := client.Discover(ctx, "https://dex.example.com")
if err != nil {
    return err
}

// Use discovered endpoints
config := &oauth2.Config{
    Endpoint: oauth2.Endpoint{
        AuthURL:  doc.AuthorizationEndpoint,
        TokenURL: doc.TokenEndpoint,
    },
}

Package oidc provides OIDC (OpenID Connect) utilities for OAuth providers.

Index

Constants

View Source
const (
	// GrantTypeTokenExchange is the RFC 8693 grant type for token exchange.
	// #nosec G101 -- This is an RFC-defined URN identifier, not a credential.
	GrantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange"

	// TokenTypeIDToken is the token type URN for ID tokens.
	// #nosec G101 -- This is an RFC-defined URN identifier, not a credential.
	TokenTypeIDToken = "urn:ietf:params:oauth:token-type:id_token"

	// TokenTypeAccessToken is the token type URN for access tokens.
	// #nosec G101 -- This is an RFC-defined URN identifier, not a credential.
	TokenTypeAccessToken = "urn:ietf:params:oauth:token-type:access_token"

	// TokenTypeRefreshToken is the token type URN for refresh tokens.
	// #nosec G101 -- This is an RFC-defined URN identifier, not a credential.
	TokenTypeRefreshToken = "urn:ietf:params:oauth:token-type:refresh_token"

	// TokenTypeJWT is the token type URN for JWT tokens.
	// #nosec G101 -- This is an RFC-defined URN identifier, not a credential.
	TokenTypeJWT = "urn:ietf:params:oauth:token-type:jwt"

	// DefaultCacheMaxEntries is the default maximum number of entries in the token exchange cache.
	// This prevents unbounded memory growth in long-running services.
	DefaultCacheMaxEntries = 10000
)

RFC 8693 OAuth 2.0 Token Exchange constants. These are standard URN identifiers defined in RFC 8693, not credentials.

View Source
const (

	// KeyTypeRSA is the JWK key type for RSA keys.
	KeyTypeRSA = "RSA"
	// KeyTypeEC is the JWK key type for Elliptic Curve keys.
	KeyTypeEC = "EC"
)
View Source
const (
	// DefaultCacheTTL is the default time-to-live for cached JWKS and discovery documents.
	DefaultCacheTTL = 1 * time.Hour

	// DefaultHTTPTimeout is the default timeout for HTTP requests to OIDC endpoints.
	DefaultHTTPTimeout = 10 * time.Second

	// DefaultClockSkewLeeway is the default leeway for JWT time validation (exp, nbf, iat).
	// This accounts for clock drift between the token issuer and this server.
	// 30 seconds is a reasonable default that handles minor clock skew without
	// creating a significant security window.
	//
	// Security consideration: A larger leeway increases the window during which
	// an expired token might be accepted, but too small a leeway may cause
	// legitimate tokens to be rejected due to clock drift.
	DefaultClockSkewLeeway = 30 * time.Second
)

Default configuration constants for OIDC clients.

View Source
const (
	// DefaultTLSHandshakeTimeout is the timeout for TLS handshake operations.
	DefaultTLSHandshakeTimeout = 10 * time.Second

	// DefaultMaxIdleConns is the maximum number of idle connections to keep.
	DefaultMaxIdleConns = 10

	// DefaultIdleConnTimeout is how long idle connections are kept before closing.
	DefaultIdleConnTimeout = 90 * time.Second

	// DefaultDialerKeepAlive is the keep-alive period for TCP connections.
	DefaultDialerKeepAlive = 30 * time.Second
)

HTTP transport configuration constants shared across HTTP clients. These ensure consistent behavior for all OIDC-related HTTP operations.

View Source
const (
	// DefaultMaxGroups is the default maximum number of groups accepted in an OIDC groups claim.
	// Set to 600 to accommodate enterprise environments (Active Directory, Azure AD, LDAP,
	// large GitHub organizations) where users commonly have 500+ group memberships.
	DefaultMaxGroups = 600

	// DefaultMaxGroupNameLength is the default maximum length of a single group name.
	DefaultMaxGroupNameLength = 256
)

Default limits for groups validation.

Variables

View Source
var ErrIssuerMismatch = errors.New("issuer mismatch")

ErrIssuerMismatch is returned (wrapped) when a JWT's iss claim does not match the expected issuer. Callers that need to distinguish issuer mismatch from other validation failures (for audit classification, metric labeling, etc.) should use errors.Is against this sentinel rather than string-matching the message.

Functions

func GenerateCacheKey added in v0.2.45

func GenerateCacheKey(tokenEndpoint, connectorID, userID string) string

GenerateCacheKey creates a cache key from the token endpoint, connector ID, and user ID. This creates a key that uniquely identifies the exchange target without including the subject token itself (for security).

The key is a SHA-256 hash of the input parameters, which:

  • Prevents collision attacks (e.g., if parameters contain delimiter characters)
  • Creates fixed-length keys for consistent memory usage
  • Does not expose sensitive information if the cache is inspected

Security Requirements

IMPORTANT: The userID parameter MUST be extracted from trusted, validated claims in the subject token (e.g., the "sub" claim after JWT signature verification), NOT from user input or unvalidated sources. Using untrusted userID values could lead to cache poisoning attacks where an attacker retrieves tokens cached for other users.

Example:

// Extract userID from validated JWT claims (after signature verification)
userID := validatedClaims.Subject
key := GenerateCacheKey("https://dex.cluster-b.example.com/token", "cluster-a", userID)
cachedToken := cache.Get(key)

func GetAudienceFromClaims added in v0.2.39

func GetAudienceFromClaims(claims jwt.MapClaims) []string

GetAudienceFromClaims extracts the audience claim from JWT claims. The audience can be a single string or an array of strings. Returns nil if no audience is present.

func IsJWT added in v0.2.39

func IsJWT(token string) bool

IsJWT checks if a token string looks like a JWT (has 3 parts separated by dots). This is a quick syntactic check, not cryptographic validation.

func NewPrivateIPAllowedHTTPClient added in v0.2.40

func NewPrivateIPAllowedHTTPClient(timeout time.Duration) *http.Client

NewPrivateIPAllowedHTTPClient creates an HTTP client without SSRF protection. This client allows connections to private IP addresses, which is necessary for private IdP deployments (e.g., internal Dex).

WARNING: This reduces SSRF protection. Only use when connecting to trusted internal services where the IdP legitimately runs on private networks.

Parameters:

  • timeout: HTTP client timeout (0 uses default 10 seconds)

Security Features:

  • TLS Verification: Uses default TLS settings (no InsecureSkipVerify)
  • No SSRF Protection: Private, loopback, and link-local addresses are ALLOWED

Use Cases:

  • Home lab deployments with internal Dex
  • Air-gapped environments
  • Enterprise deployments with private IdPs

Example:

client := NewPrivateIPAllowedHTTPClient(30 * time.Second)
resp, err := client.Get("https://dex.internal/keys")

func NewSSRFSafeHTTPClient added in v0.2.39

func NewSSRFSafeHTTPClient(timeout time.Duration) *http.Client

NewSSRFSafeHTTPClient creates an HTTP client with DNS rebinding protection. This client validates that resolved IP addresses are not private/restricted at connection time, preventing DNS rebinding attacks.

Parameters:

  • timeout: HTTP client timeout (0 uses default 10 seconds)

Security Features:

  • DNS Rebinding Protection: Validates resolved IPs at connection time
  • SSRF Protection: Blocks private, loopback, and link-local addresses
  • TLS Verification: Uses default TLS settings (no InsecureSkipVerify)

Example:

client := NewSSRFSafeHTTPClient(30 * time.Second)
resp, err := client.Get("https://example.com/jwks")

func ParseUnverifiedClaims added in v0.2.39

func ParseUnverifiedClaims(tokenString string) (jwt.MapClaims, error)

ParseUnverifiedClaims extracts claims from a JWT without verifying the signature. This is useful for routing decisions (e.g., checking audience) before full validation. SECURITY: Never trust the claims returned from this function for authorization decisions.

func SSRFSafeDialContext added in v0.2.39

func SSRFSafeDialContext(dialer *net.Dialer) func(ctx context.Context, network, addr string) (net.Conn, error)

SSRFSafeDialContext creates a DialContext function that validates resolved IPs to prevent DNS rebinding attacks. DNS rebinding occurs when an attacker controls a DNS server that initially returns a public IP (passing URL validation) but later returns a private IP (when the actual connection is made).

Security Features:

  • Validates each resolved IP before allowing connection
  • Blocks loopback, private, and link-local addresses
  • Prevents access to cloud metadata services (169.254.169.254)

Example:

transport := &http.Transport{
    DialContext: SSRFSafeDialContext(&net.Dialer{Timeout: 10 * time.Second}),
}
client := &http.Client{Transport: transport}

func ValidateConnectorID

func ValidateConnectorID(connectorID string) error

ValidateConnectorID validates a Dex connector_id parameter. Connector IDs should be alphanumeric with hyphens/underscores only.

Security Considerations:

  • Character Whitelist: Prevents injection attacks
  • Length Limit: Prevents DoS via extremely long values

Example:

if err := ValidateConnectorID("github"); err != nil {
    return fmt.Errorf("invalid connector: %w", err)
}

func ValidateExternalURL added in v0.2.39

func ValidateExternalURL(rawURL, context string) error

ValidateExternalURL validates an external URL with SSRF protection. It enforces HTTPS and blocks private IP ranges to prevent Server-Side Request Forgery attacks. This is a generic version of ValidateIssuerURL that accepts a context parameter for error messages.

Security Considerations:

  • HTTPS Enforcement: Prevents credential interception
  • Private IP Blocking: Prevents SSRF against internal services (Kubernetes API, metadata services, etc.)
  • Loopback Blocking: Prevents attacks against localhost services
  • Link-local Blocking: Prevents metadata service attacks (169.254.169.254)

Example:

if err := ValidateExternalURL("https://provider.example.com/.well-known/jwks", "JWKS URI"); err != nil {
    return fmt.Errorf("invalid JWKS URI: %w", err)
}

func ValidateGroups

func ValidateGroups(groups []string, maxGroups int) ([]string, bool, error)

ValidateGroups validates and truncates an OIDC groups claim. If maxGroups is <= 0, DefaultMaxGroups (600) is used.

Groups exceeding maxGroups are truncated (not rejected) so authentication can proceed. The second return value indicates whether truncation occurred. Individual group names exceeding DefaultMaxGroupNameLength are rejected with an error, as oversized names may indicate an injection attempt.

Returns a defensive copy of the groups slice.

Security Considerations:

  • Array Size Limit: Prevents memory exhaustion from excessive groups
  • String Length Limit: Prevents memory exhaustion from long group names

Example:

validated, truncated, err := ValidateGroups(groups, 0)
if err != nil {
    return fmt.Errorf("invalid groups: %w", err)
}
if truncated {
    slog.Warn("groups truncated", "original", len(groups))
}

func ValidateHTTPSURL

func ValidateHTTPSURL(rawURL, context string) error

ValidateHTTPSURL validates that a URL uses HTTPS scheme. This is a reusable helper to enforce HTTPS across all endpoints.

Example:

if err := ValidateHTTPSURL("https://example.com", "issuer"); err != nil {
    return err
}

func ValidateIssuerURL

func ValidateIssuerURL(issuerURL string) error

ValidateIssuerURL validates an OIDC issuer URL with SSRF protection. It enforces HTTPS and blocks private IP ranges to prevent Server-Side Request Forgery attacks.

This is a convenience wrapper around ValidateExternalURL with "issuer URL" as the context.

Security Considerations:

  • HTTPS Enforcement: Prevents credential interception
  • Private IP Blocking: Prevents SSRF against internal services (Kubernetes API, metadata services, etc.)
  • Loopback Blocking: Prevents attacks against localhost services
  • Link-local Blocking: Prevents metadata service attacks (169.254.169.254)

Example:

if err := ValidateIssuerURL("https://dex.example.com"); err != nil {
    return fmt.Errorf("invalid issuer: %w", err)
}

func ValidateScopes

func ValidateScopes(scopes []string) error

ValidateScopes validates OAuth scopes.

Security Considerations:

  • Array Size Limit: Prevents DoS from excessive scopes
  • String Length Limit: Prevents memory exhaustion
  • Empty Scope Detection: Prevents malformed requests

Example:

scopes := []string{"openid", "profile", "email"}
if err := ValidateScopes(scopes); err != nil {
    return fmt.Errorf("invalid scopes: %w", err)
}

Types

type CachedExchangeToken added in v0.2.45

type CachedExchangeToken struct {
	// AccessToken is the cached access token.
	AccessToken string

	// ExpiresAt is when the token expires.
	ExpiresAt time.Time

	// IssuedTokenType is the type of the cached token.
	IssuedTokenType string
}

CachedExchangeToken holds a cached token with its expiration time.

type DiscoveryClient

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

DiscoveryClient fetches and caches OIDC discovery documents. It provides SSRF protection and HTTPS enforcement for all discovered endpoints.

The client is thread-safe and can be used concurrently from multiple goroutines.

func NewDiscoveryClient

func NewDiscoveryClient(httpClient *http.Client, cacheTTL time.Duration, logger *slog.Logger) *DiscoveryClient

NewDiscoveryClient creates a new OIDC discovery client with default configuration.

Parameters:

  • httpClient: HTTP client to use for requests (nil uses default with 10s timeout)
  • cacheTTL: Time-to-live for cached discovery documents (0 uses default 1 hour)
  • logger: Logger for debug/info messages (nil uses default logger)

Example:

client := oidc.NewDiscoveryClient(nil, 1*time.Hour, slog.Default())
doc, err := client.Discover(ctx, "https://dex.example.com")

func NewTestDiscoveryClient added in v0.2.7

func NewTestDiscoveryClient(httpClient *http.Client, cacheTTL time.Duration, logger *slog.Logger) *DiscoveryClient

NewTestDiscoveryClient creates a discovery client that skips SSRF validation.

⚠️ CRITICAL SECURITY WARNING ⚠️

This function bypasses ALL security protections including:

  • Private IP blocking (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
  • Loopback blocking (127.0.0.1, ::1)
  • Link-local blocking (169.254.169.254 - AWS metadata service)
  • HTTPS enforcement

NEVER USE THIS IN PRODUCTION CODE!

This function exists ONLY for unit tests with httptest.Server on localhost. The forbidigo linter is configured to detect and prevent misuse.

Intended Use (test files only):

func TestOIDCProvider(t *testing.T) {
    testServer := httptest.NewTLSServer(mockOIDCHandler)
    defer testServer.Close()
    client := oidc.NewTestDiscoveryClient(testServer.Client(), 1*time.Hour, nil)
    // ... test code ...
}

Security Enforcement:

  • Linter rules prevent usage outside *_test.go files
  • Code review must verify all usages are in test code
  • CI/CD should fail if this appears in production code paths

func (*DiscoveryClient) ClearCache

func (c *DiscoveryClient) ClearCache()

ClearCache clears the discovery document cache. This is useful for forcing a refresh of all cached documents.

Example:

client.ClearCache() // Force refresh on next Discover() call

func (*DiscoveryClient) Discover

func (c *DiscoveryClient) Discover(ctx context.Context, issuerURL string) (*DiscoveryDocument, error)

Discover fetches the OIDC discovery document for an issuer. It validates the issuer URL for security (SSRF protection) and caches results.

Security Features:

  • SSRF protection via ValidateIssuerURL
  • HTTPS enforcement for issuer and all discovered endpoints
  • Document caching with TTL to reduce attack surface

Example:

doc, err := client.Discover(ctx, "https://dex.example.com")
if err != nil {
    return fmt.Errorf("discovery failed: %w", err)
}
// Use doc.AuthorizationEndpoint, doc.TokenEndpoint, etc.

type DiscoveryDocument

type DiscoveryDocument struct {
	Issuer                            string   `json:"issuer"`
	AuthorizationEndpoint             string   `json:"authorization_endpoint"`
	TokenEndpoint                     string   `json:"token_endpoint"`
	UserInfoEndpoint                  string   `json:"userinfo_endpoint"`
	RevocationEndpoint                string   `json:"revocation_endpoint,omitempty"`
	JWKSUri                           string   `json:"jwks_uri"`
	ScopesSupported                   []string `json:"scopes_supported,omitempty"`
	ResponseTypesSupported            []string `json:"response_types_supported"`
	GrantTypesSupported               []string `json:"grant_types_supported,omitempty"`
	CodeChallengeMethodsSupported     []string `json:"code_challenge_methods_supported,omitempty"`
	TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"`
}

DiscoveryDocument represents an OIDC discovery document. It contains the OpenID Connect provider metadata as defined in RFC 8414.

type IDTokenClaims added in v0.2.39

type IDTokenClaims struct {
	jwt.RegisteredClaims

	// Standard OIDC claims
	Email         string   `json:"email,omitempty"`
	EmailVerified bool     `json:"email_verified,omitempty"`
	Name          string   `json:"name,omitempty"`
	GivenName     string   `json:"given_name,omitempty"`
	FamilyName    string   `json:"family_name,omitempty"`
	Picture       string   `json:"picture,omitempty"`
	Locale        string   `json:"locale,omitempty"`
	Groups        []string `json:"groups,omitempty"`
}

IDTokenClaims represents the standard claims in an OIDC ID token.

func ValidateIDToken added in v0.2.39

func ValidateIDToken(ctx context.Context, tokenString string, jwksClient *JWKSClient, jwksURI, expectedIssuer string, trustedAudiences []string) (*IDTokenClaims, error)

ValidateIDToken validates an ID token (JWT) using the provider's JWKS. This is used for SSO token forwarding where ID tokens are passed as Bearer tokens.

Validation includes:

  • Signature verification using JWKS
  • Expiration check (exp claim, required)
  • Not-before check (nbf claim, validated if present)
  • Issued-at validation (iat claim, validated if present)
  • Clock skew tolerance (DefaultClockSkewLeeway = 30 seconds)
  • Issuer validation (if expectedIssuer is non-empty)
  • Audience validation (checks if any audience matches trustedAudiences)

Returns the parsed claims if validation succeeds.

type JWK added in v0.2.39

type JWK struct {
	Kty string `json:"kty"` // Key Type: "RSA" or "EC"
	Use string `json:"use"` // Public Key Use (e.g., "sig")
	Kid string `json:"kid"` // Key ID
	Alg string `json:"alg"` // Algorithm (e.g., "RS256", "ES256")

	// RSA key parameters
	N string `json:"n,omitempty"` // RSA modulus (base64url)
	E string `json:"e,omitempty"` // RSA exponent (base64url)

	// EC key parameters
	Crv string `json:"crv,omitempty"` // EC curve name (e.g., "P-256", "P-384", "P-521")
	X   string `json:"x,omitempty"`   // EC x coordinate (base64url)
	Y   string `json:"y,omitempty"`   // EC y coordinate (base64url)
}

JWK represents a JSON Web Key per RFC 7517. Supports both RSA and EC (Elliptic Curve) key types.

func (*JWK) ECDSAPublicKey added in v0.2.39

func (j *JWK) ECDSAPublicKey() (*ecdsa.PublicKey, error)

ECDSAPublicKey converts a JWK to an ECDSA public key for signature verification. Supports P-256 (ES256), P-384 (ES384), and P-521 (ES512) curves. Returns an error if the key type is not EC or the curve is unsupported.

func (*JWK) PublicKey added in v0.2.39

func (j *JWK) PublicKey() (any, error)

PublicKey returns the appropriate public key based on the key type (RSA or EC). This is the preferred method for obtaining a key for signature verification.

func (*JWK) RSAPublicKey added in v0.2.39

func (j *JWK) RSAPublicKey() (*rsa.PublicKey, error)

RSAPublicKey converts a JWK to an RSA public key for signature verification. Returns an error if the key type is not RSA.

type JWKS added in v0.2.39

type JWKS struct {
	Keys []JWK `json:"keys"`
}

JWKS represents a JSON Web Key Set per RFC 7517.

func (*JWKS) GetKey added in v0.2.39

func (j *JWKS) GetKey(kid string) *JWK

GetKey retrieves a key from the JWKS by key ID. Returns nil if the key is not found.

type JWKSClient added in v0.2.39

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

JWKSClient fetches and caches JWKS (JSON Web Key Sets) for JWT validation. It provides SSRF protection and caches keys with configurable TTL.

The client is thread-safe and can be used concurrently from multiple goroutines.

func NewJWKSClient added in v0.2.39

func NewJWKSClient(httpClient *http.Client, cacheTTL time.Duration, logger *slog.Logger) *JWKSClient

NewJWKSClient creates a new JWKS client with default configuration.

Parameters:

  • httpClient: HTTP client to use for requests (nil uses SSRF-safe client with DNS rebinding protection)
  • cacheTTL: Time-to-live for cached JWKS (0 uses default 1 hour)
  • logger: Logger for debug/info messages (nil uses default logger)

Security Features (when httpClient is nil):

  • DNS Rebinding Protection: Validates resolved IPs at connection time
  • SSRF Protection: Blocks private, loopback, and link-local addresses
  • TLS Verification: Uses default TLS settings

func NewJWKSClientWithOptions added in v0.2.40

func NewJWKSClientWithOptions(opts JWKSClientOptions) *JWKSClient

NewJWKSClientWithOptions creates a new JWKS client with configurable options. This allows fine-grained control over SSRF protection for private IdP deployments.

When AllowPrivateIP is true:

  • SSRF protection is disabled (private, loopback, and link-local IPs are allowed)
  • HTTPS is still enforced
  • A warning is logged about reduced security

Security Features:

  • TLS Verification: Uses default TLS settings (no InsecureSkipVerify)
  • Response Size Limit: Limits response body to 1MB
  • Key Count Limit: Limits JWKS to 100 keys

Example:

client := NewJWKSClientWithOptions(JWKSClientOptions{
    AllowPrivateIP: true,  // For internal Dex
    Logger:         logger,
})

func (*JWKSClient) ClearCache added in v0.2.39

func (c *JWKSClient) ClearCache()

ClearCache clears the JWKS cache.

func (*JWKSClient) FetchJWKS added in v0.2.39

func (c *JWKSClient) FetchJWKS(ctx context.Context, jwksURI string) (*JWKS, error)

FetchJWKS fetches the JWKS from a given URI with caching. Uses HTTPS validation and SSRF protection for security.

Security Features:

  • SSRF Protection: Validates URI to block private IPs, loopback, and link-local addresses (unless AllowPrivateIP is enabled for private IdP deployments)
  • HTTPS Enforcement: Always requires HTTPS (even with AllowPrivateIP)
  • Response Size Limit: Limits response body to 1MB to prevent memory exhaustion
  • Key Count Limit: Limits JWKS to 100 keys to prevent memory exhaustion
  • Caching: Reduces attack surface by caching valid responses

type JWKSClientOptions added in v0.2.40

type JWKSClientOptions struct {
	// HTTPClient is the HTTP client to use for requests.
	// If nil, an appropriate client is created based on AllowPrivateIP setting.
	HTTPClient *http.Client

	// CacheTTL is the time-to-live for cached JWKS (0 uses default 1 hour).
	CacheTTL time.Duration

	// Logger is the logger for debug/info messages (nil uses default logger).
	Logger *slog.Logger

	// AllowPrivateIP allows JWKS endpoints to resolve to private IP addresses.
	// WARNING: Reduces SSRF protection. Only enable for internal/VPN deployments
	// where the IdP legitimately runs on private networks.
	AllowPrivateIP bool
}

JWKSClientOptions contains optional configuration for JWKSClient.

type TokenExchangeCache added in v0.2.45

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

TokenExchangeCache caches exchanged tokens to reduce exchange requests. This is useful for avoiding repeated token exchanges for the same subject token when making multiple requests to the same remote cluster.

The cache is thread-safe and can be used concurrently from multiple goroutines.

Cache Key Format

The cache uses a SHA-256 hash of the token endpoint, connector ID, and user ID to create collision-resistant cache keys.

Memory Management

The cache uses LRU (Least Recently Used) eviction to prevent unbounded memory growth. When the cache reaches its maximum size (default: 10,000 entries), the least recently accessed entries are evicted to make room for new ones.

Security Considerations

  • Token Caching: Reduces the number of token exchange calls, limiting token exposure
  • Expiry Buffer: Tokens are considered expired 30 seconds before their actual expiry to account for clock skew and network latency
  • Memory Limits: LRU eviction prevents memory exhaustion attacks
  • Hash-based Keys: Cache keys use SHA-256 to prevent collision attacks

func NewTokenExchangeCache added in v0.2.45

func NewTokenExchangeCache() *TokenExchangeCache

NewTokenExchangeCache creates a new token exchange cache with default max entries.

func NewTokenExchangeCacheWithMaxEntries added in v0.2.45

func NewTokenExchangeCacheWithMaxEntries(maxEntries int) *TokenExchangeCache

NewTokenExchangeCacheWithMaxEntries creates a new token exchange cache with custom max entries. Set maxEntries to 0 for unlimited (not recommended for production).

func (*TokenExchangeCache) Cleanup added in v0.2.45

func (c *TokenExchangeCache) Cleanup() int

Cleanup removes expired tokens from the cache. This should be called periodically for long-running services to prevent memory growth from accumulated expired tokens.

func (*TokenExchangeCache) Clear added in v0.2.45

func (c *TokenExchangeCache) Clear()

Clear removes all tokens from the cache.

func (*TokenExchangeCache) Delete added in v0.2.45

func (c *TokenExchangeCache) Delete(key string)

Delete removes a token from the cache.

func (*TokenExchangeCache) Get added in v0.2.45

Get retrieves a cached token by key. Returns nil if the token is not found or has expired. Accessing a token moves it to the front of the LRU list.

The key should be generated using GenerateCacheKey.

func (*TokenExchangeCache) GetStats added in v0.2.45

GetStats returns statistics about the cache for monitoring.

func (*TokenExchangeCache) Set added in v0.2.45

func (c *TokenExchangeCache) Set(key, token, issuedTokenType string, expiresIn int)

Set stores a token in the cache with the given expiration. If the cache is at capacity, the least recently used entry is evicted.

Parameters:

  • key: Cache key (use GenerateCacheKey to create)
  • token: The access token to cache
  • issuedTokenType: The type of the issued token
  • expiresIn: Token lifetime in seconds (30-second buffer is applied)

func (*TokenExchangeCache) Size added in v0.2.45

func (c *TokenExchangeCache) Size() int

Size returns the number of tokens in the cache (including expired ones).

type TokenExchangeCacheStats added in v0.2.45

type TokenExchangeCacheStats struct {
	// CurrentEntries is the number of entries currently in the cache.
	CurrentEntries int
	// MaxEntries is the maximum allowed entries (0 = unlimited).
	MaxEntries int
	// TotalEvictions is the number of LRU evictions performed.
	TotalEvictions int64
	// MemoryPressure is the percentage of max capacity used (0-100).
	MemoryPressure float64
}

TokenExchangeCacheStats provides statistics about the cache for monitoring.

type TokenExchangeClient added in v0.2.45

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

TokenExchangeClient performs RFC 8693 OAuth 2.0 Token Exchange operations. It enables cross-cluster SSO scenarios where each cluster has its own Identity Provider (e.g., separate Dex instances).

The client is thread-safe and can be used concurrently from multiple goroutines.

Security Features

  • SSRF protection via URL validation (unless AllowPrivateIP is enabled)
  • HTTPS enforcement for token endpoints
  • Response size limiting to prevent memory exhaustion
  • Subject token size limiting (64KB max)
  • Structured logging for security monitoring

Rate Limiting

This client does not implement rate limiting internally. For production deployments, callers SHOULD implement rate limiting to prevent abuse, especially when the token exchange endpoint is exposed to user-controlled input. Consider using:

  • Per-user rate limits to prevent individual users from overwhelming the IdP
  • Global rate limits to protect against coordinated attacks
  • The TokenExchangeCache to reduce redundant exchange requests

Example Usage

client := oidc.NewTokenExchangeClient(nil)
resp, err := client.Exchange(ctx, oidc.TokenExchangeRequest{
    TokenEndpoint:    "https://dex.cluster-b.example.com/token",
    SubjectToken:     userIDToken,
    SubjectTokenType: oidc.TokenTypeIDToken,
    ConnectorID:      "cluster-a-dex",
    Scope:            "openid profile email groups",
})
if err != nil {
    return err
}
// Use resp.AccessToken for requests to Cluster B

func NewTokenExchangeClient added in v0.2.45

func NewTokenExchangeClient(logger *slog.Logger) *TokenExchangeClient

NewTokenExchangeClient creates a new token exchange client with default configuration.

Parameters:

  • logger: Logger for debug/info messages (nil uses default logger)

Security Features:

  • DNS Rebinding Protection: Validates resolved IPs at connection time
  • SSRF Protection: Blocks private, loopback, and link-local addresses
  • TLS Verification: Uses default TLS settings

func NewTokenExchangeClientWithOptions added in v0.2.45

func NewTokenExchangeClientWithOptions(opts TokenExchangeClientOptions) *TokenExchangeClient

NewTokenExchangeClientWithOptions creates a new token exchange client with configurable options. This allows fine-grained control over SSRF protection for private IdP deployments.

When AllowPrivateIP is true:

  • SSRF protection is disabled (private, loopback, and link-local IPs are allowed)
  • HTTPS is still enforced
  • A warning is logged about reduced security

Example:

client := NewTokenExchangeClientWithOptions(TokenExchangeClientOptions{
    AllowPrivateIP: true,  // For internal Dex
    Logger:         logger,
})

func (*TokenExchangeClient) Exchange added in v0.2.45

Exchange performs the RFC 8693 token exchange operation.

This method exchanges a subject token from one Identity Provider for a token from another Identity Provider. This is useful for cross-cluster SSO scenarios where each cluster has its own Dex instance.

Security Features:

  • SSRF Protection: Validates token endpoint URL (unless AllowPrivateIP is enabled)
  • HTTPS Enforcement: Always requires HTTPS
  • Response Size Limit: Limits response body to 1MB
  • Audit Logging: Logs exchange operations for security monitoring

Parameters:

  • ctx: Context for cancellation and timeouts
  • req: Token exchange request parameters

Returns:

  • TokenExchangeResponse with the new token if successful
  • Error if validation fails, network error occurs, or server returns an error

type TokenExchangeClientOptions added in v0.2.45

type TokenExchangeClientOptions struct {
	// HTTPClient is the HTTP client to use for requests.
	// If nil, an appropriate client is created based on AllowPrivateIP setting.
	HTTPClient *http.Client

	// Logger is the logger for debug/info messages (nil uses default logger).
	Logger *slog.Logger

	// AllowPrivateIP allows token endpoints to resolve to private IP addresses.
	// WARNING: Reduces SSRF protection. Only enable for internal/VPN deployments
	// where the IdP legitimately runs on private networks.
	AllowPrivateIP bool
}

TokenExchangeClientOptions contains optional configuration for TokenExchangeClient.

type TokenExchangeErrorResponse added in v0.2.45

type TokenExchangeErrorResponse struct {
	Error            string `json:"error"`
	ErrorDescription string `json:"error_description,omitempty"`
	ErrorURI         string `json:"error_uri,omitempty"`
}

TokenExchangeErrorResponse represents an OAuth 2.0 error response.

type TokenExchangeRequest added in v0.2.45

type TokenExchangeRequest struct {
	// TokenEndpoint is the remote IdP's token endpoint URL.
	// Required. Must use HTTPS.
	TokenEndpoint string

	// SubjectToken is the original token to exchange.
	// Required.
	SubjectToken string

	// SubjectTokenType is the type of the subject token.
	// Use TokenTypeIDToken or TokenTypeAccessToken.
	// Defaults to TokenTypeIDToken if not specified.
	SubjectTokenType string

	// ConnectorID is the connector ID on the remote IdP that validates this token.
	// For Dex, this is passed via the "connector_id" parameter.
	// Required.
	ConnectorID string

	// Scope is the requested scope for the new token (optional).
	Scope string

	// RequestedTokenType is the type of token to receive (optional).
	// Defaults to access_token if not specified by the server.
	RequestedTokenType string

	// ClientID for client authentication (optional, for confidential clients).
	ClientID string

	// ClientSecret for client authentication (optional, for confidential clients).
	ClientSecret string

	// Audience is the intended audience of the new token (optional).
	// RFC 8693 Section 2.1: This is the logical name of the target service.
	Audience string

	// Resource is the absolute URI of the target resource (optional).
	// RFC 8693 Section 2.1: Indicates the location of the target service.
	Resource string
}

TokenExchangeRequest represents the token exchange request parameters.

type TokenExchangeResponse added in v0.2.45

type TokenExchangeResponse struct {
	// AccessToken is the security token issued by the authorization server.
	AccessToken string `json:"access_token"`

	// IssuedTokenType is the type of token issued (e.g., access_token, id_token).
	IssuedTokenType string `json:"issued_token_type"`

	// TokenType is the token type (typically "Bearer").
	TokenType string `json:"token_type"`

	// ExpiresIn is the lifetime in seconds of the access token.
	ExpiresIn int `json:"expires_in,omitempty"`

	// Scope is the scope of the access token.
	Scope string `json:"scope,omitempty"`

	// RefreshToken is a refresh token (optional).
	RefreshToken string `json:"refresh_token,omitempty"`
}

TokenExchangeResponse represents the token exchange response.

Jump to

Keyboard shortcuts

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