oidc

package
v1.0.13 Latest Latest
Warning

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

Go to latest
Published: Jul 18, 2026 License: Apache-2.0 Imports: 21 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"
)

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

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

	// DefaultJWKSRefetchBackoff bounds how often a JWKS refetch can be triggered
	// by a token whose kid is absent from the cached key set. A legitimate
	// issuer key rotation self-heals on the first such token (the refetch
	// repopulates the cache); the backoff exists only to stop a flood of tokens
	// carrying bogus kids from hammering the issuer's JWKS endpoint — at most
	// one refetch per backoff window per URI (negative caching).
	DefaultJWKSRefetchBackoff = 1 * time.Minute
)

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.

View Source
var ErrKeyNotFound = errors.New("key not found in JWKS")

ErrKeyNotFound is returned (wrapped) when a JWT's kid header does not match any key in the JWKS. ValidateIDToken uses errors.Is against this to decide whether to force a bounded JWKS refetch (the issuer may have rotated its signing key after the JWKS was cached).

View Source
var ErrNonceMismatch = errors.New("nonce mismatch")

ErrNonceMismatch is returned by ValidateNonceClaim when the expected nonce is non-empty and either does not equal the claim or the claim is absent.

View Source
var ErrTokenExpired = errors.New("token expired")

ErrTokenExpired is returned (wrapped) when a JWT's exp claim is in the past beyond the configured leeway. Wraps the underlying go-jose sentinel so errors.Is against either this or josejwt.ErrExpired works.

View Source
var ErrTokenNotValidYet = errors.New("token not valid yet")

ErrTokenNotValidYet is returned (wrapped) when a JWT's nbf claim is in the future beyond the configured leeway.

Functions

func GetAudienceFromClaims added in v0.2.39

func GetAudienceFromClaims(claims map[string]any) []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 HostScopedPrivateIPDialContext added in v0.15.0

func HostScopedPrivateIPDialContext(dialer *net.Dialer, allowedHosts []string) func(ctx context.Context, network, addr string) (net.Conn, error)

HostScopedPrivateIPDialContext creates a DialContext that allows private IP resolution only for the explicitly listed hostnames; all other hosts are subject to the normal SSRF/DNS-rebinding guard.

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 NewHostScopedPrivateIPHTTPClient added in v0.15.0

func NewHostScopedPrivateIPHTTPClient(allowedHosts []string, timeout time.Duration, rootCAs *x509.CertPool) *http.Client

NewHostScopedPrivateIPHTTPClient creates an HTTP client that allows private IP resolution only for the explicitly listed hostnames. All other hosts go through the normal SSRF/DNS-rebinding guard. Use this instead of NewPrivateIPAllowedHTTPClient when the private endpoint is a known in-cluster service — it narrows the SSRF escape hatch to only the expected host.

rootCAs, when non-nil, is the CA pool the client verifies the IdP's certificate against; nil uses the system pool.

func NewPrivateIPAllowedHTTPClient added in v0.2.40

func NewPrivateIPAllowedHTTPClient(timeout time.Duration, rootCAs *x509.CertPool) *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: enforced (never InsecureSkipVerify). Verifies against the provided rootCAs pool (for an internal-CA IdP), or the system pool when rootCAs is nil.
  • No SSRF Protection: Private, loopback, and link-local addresses are ALLOWED
  • Host-pinned redirects: a redirect to a different host is refused. Because this client cannot filter private IPs, it pins requests to the host that was originally dialed so a discovery/JWKS endpoint cannot 302 the fetch to an arbitrary internal target.

Use Cases:

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

Parameters:

  • timeout: HTTP client timeout (0 uses default 10 seconds)
  • rootCAs: CA pool to verify the IdP's certificate against; nil uses the system pool. Pass the internal CA when the private IdP presents a certificate from a CA not in the system pool.

Example:

client := NewPrivateIPAllowedHTTPClient(30 * time.Second, dexCAPool)
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) (map[string]any, 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 RevokeAtEndpoint added in v0.2.136

func RevokeAtEndpoint(ctx context.Context, httpClient *http.Client, endpoint, token, clientID, clientSecret string) error

RevokeAtEndpoint posts an RFC 7009 token revocation to endpoint. When clientID is non-empty the request authenticates via HTTP Basic (client_secret_basic); endpoints that do not require client authentication (e.g. Google's public revocation endpoint) leave clientID/clientSecret empty and skip the header.

Per RFC 7009 §2.2 the server SHOULD respond 200 even for an unknown token, so anything other than 200 surfaces as an error to the caller.

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 ValidateNonceClaim added in v0.2.129

func ValidateNonceClaim(claimNonce, expectedNonce string) error

ValidateNonceClaim returns nil when expectedNonce is empty; otherwise it requires claimNonce to equal expectedNonce under a constant-time compare.

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 ActorClaim added in v0.7.0

type ActorClaim struct {
	Issuer  string      `json:"iss,omitempty"`
	Subject string      `json:"sub,omitempty"`
	Act     *ActorClaim `json:"act,omitempty"`
}

ActorClaim is the RFC 8693 §4.4 act claim decoded from an OIDC token or mcp-oauth-minted access token. It identifies the acting party in a delegation chain: Issuer and Subject correspond to the agent service account that obtained the token on behalf of the human subject.

Act carries the next hop further from the subject (RFC 8693 §4.4 nested act): in a multi-hop A2A chain human → agentA → agentB, a token minted for the second hop has Act = agentB and Act.Act = agentA. Nil at the end of the chain.

func (*ActorClaim) Chain added in v0.10.0

func (c *ActorClaim) Chain() []ActorClaim

Chain flattens a nested act claim into a slice ordered from the outermost (most recent) actor to the innermost, each element stripped of its own nested Act. Returns nil for a nil receiver. Resource servers authorizing a multi-hop delegation match any element rather than only the leaf.

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. Cold-cache fetches for the same issuer URL are coalesced via singleflight so a fleet bounce cannot stampede the IdP.

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 NewDiscoveryClientWithOptions added in v0.2.191

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

NewDiscoveryClientWithOptions creates a discovery client applying the given options after default initialization. Use WithSkipValidation only in tests.

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 DiscoveryOption added in v0.2.191

type DiscoveryOption func(*DiscoveryClient)

DiscoveryOption is a functional option for NewDiscoveryClientWithOptions.

func WithSkipValidation added in v0.2.191

func WithSkipValidation() DiscoveryOption

WithSkipValidation returns a DiscoveryOption that disables SSRF and HTTPS validation on the discovery client. Intended exclusively for unit tests that spin up httptest servers on loopback addresses. Production callers must not use this option — it bypasses all URL security checks.

type IDTokenClaims added in v0.2.39

type IDTokenClaims struct {
	josejwt.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"`

	// Nonce is the OIDC `nonce` claim. Callers compare it to the expected value
	// via ValidateNonceClaim; this struct does not enforce equality.
	Nonce string `json:"nonce,omitempty"`

	// Act is the RFC 8693 §4.4 delegation claim. Non-nil only in tokens that
	// were minted via a token-exchange with a validated actor_token.
	Act *ActorClaim `json:"act,omitempty"`
}

IDTokenClaims represents the standard claims in an OIDC ID token. The embedded josejwt.Claims carries the registered claims (iss, sub, aud, exp, nbf, iat, jti); the additional fields are OIDC-specific extensions defined by OpenID Connect Core 1.0 §5.1.

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 (alg pinned to the supported asymmetric set)
  • 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 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: enforced (never InsecureSkipVerify); permissive and host-scoped clients verify against opts.RootCAs when provided, otherwise the system pool
  • 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
    RootCAs:        internalCAPool,
    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) (*jose.JSONWebKeySet, 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

	// AllowPrivateIPHosts lists hostnames whose JWKS URLs are permitted to
	// resolve to private IP addresses. All other hosts remain subject to the
	// normal SSRF/DNS-rebinding guard. Prefer this over AllowPrivateIP when
	// the private endpoint is a known in-cluster service (e.g.
	// muster.agentic-platform.svc.cluster.local). Ignored when AllowPrivateIP
	// is true.
	AllowPrivateIPHosts []string

	// RootCAs is the CA pool used to verify the JWKS endpoint's TLS certificate
	// when AllowPrivateIP or AllowPrivateIPHosts is set (e.g. an internal-CA
	// Dex). nil uses the system pool. Ignored when HTTPClient is provided.
	RootCAs *x509.CertPool
}

JWKSClientOptions contains optional configuration for JWKSClient.

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:

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

	// RootCAs is the CA pool used to verify the token endpoint's TLS certificate
	// when AllowPrivateIP is set (e.g. an internal-CA Dex). nil uses the system
	// pool. Ignored when HTTPClient is provided.
	RootCAs *x509.CertPool
}

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