dex

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: 12 Imported by: 1

Documentation

Overview

Package dex implements the OAuth provider interface for Dex (https://dexidp.io/). It supports OIDC authentication with Dex-specific optimizations including connector_id support for bypassing the connector selection UI and proper handling of refresh token rotation.

Package dex provides a Dex OAuth provider implementation with OIDC support.

Dex (https://dexidp.io/) is an identity service that uses OpenID Connect to drive authentication for other apps. It acts as a portal to other identity providers through "connectors" like LDAP, SAML, GitHub, GitLab, Google, etc.

Features

This package implements Dex-specific optimizations beyond generic OIDC:

  • connector_id Support: Bypass Dex's connector selection UI by specifying a connector
  • Groups Claim: Automatically includes the 'groups' scope to retrieve user group memberships
  • Refresh Token Rotation: Properly handles Dex's strict refresh token rotation policy
  • OIDC Discovery: Dynamically fetches endpoints via OIDC discovery with SSRF protection
  • Cross-Client Audience Scopes: Helper functions for SSO token forwarding to multiple clients

Security Features

  • SSRF Protection: Validates issuer URLs to block private IPs and localhost
  • HTTPS Enforcement: All endpoints must use HTTPS
  • Input Validation: Validates connector_id and groups claim for security
  • Discovery Caching: Caches OIDC discovery documents with TTL

Example Usage

// Create Dex provider with connector_id to skip selection UI
dexProvider, err := dex.NewProvider(&dex.Config{
    IssuerURL:    "https://dex.example.com",
    ClientID:     "my-client-id",
    ClientSecret: "my-client-secret",
    RedirectURL:  "http://localhost:8080/oauth/callback",
    ConnectorID:  "github", // Optional: skip connector selection
})
if err != nil {
    log.Fatal(err)
}

// Use with mcp-oauth server
server, err := mcpoauth.NewServer(&mcpoauth.ServerConfig{
    Provider: dexProvider,
    // ... other config ...
})

Dex-Specific Configuration

Connector ID:

The connector_id parameter allows bypassing Dex's connector selection screen.
This is useful when you have a single connector or want to direct users to
a specific authentication method.

Reference: https://dexidp.io/docs/configuration/custom-scopes-claims-clients/#authentication-through-connector_id

Groups Claim:

Dex requires the 'groups' scope to return group memberships in the userinfo response.
This provider includes 'groups' in the default scopes automatically.

Groups are validated for security (max 100 groups, max 256 chars per group name).

Refresh Token Rotation:

Dex implements strict refresh token rotation - each token refresh returns a NEW
refresh token and invalidates the old one. This provider handles rotation correctly
by returning the complete oauth2.Token with the new refresh token.

Reference: https://dexidp.io/docs/configuration/custom-scopes-claims-clients/#refresh-token-rotation

Default Scopes

The provider uses the following default scopes if none are specified:

  • openid: Required for OIDC authentication
  • profile: User profile information (name, picture, etc.)
  • email: User email address
  • groups: User group memberships (Dex-specific)
  • offline_access: Refresh token support

You can override these by providing custom Scopes in the Config.

Cross-Client Audience Scopes

Dex supports a cross-client token issuance pattern where a token can be made valid for multiple clients by requesting special audience scopes. This is useful for SSO scenarios where a user authenticates once but needs access to multiple downstream services (e.g., Kubernetes OIDC via dex-k8s-authenticator).

IMPORTANT: Mandatory Scope Merging

When audience scopes (prefixed with "audience:server:client_id:") are configured in the provider's default scopes, they are AUTOMATICALLY MERGED into ALL authorization requests. This behavior is intentional to ensure SSO token forwarding works correctly.

Administrator implications:

  • Tokens will ALWAYS include the configured audience claims
  • Clients CANNOT opt out of these audiences by requesting custom scopes
  • This affects token size and validation on downstream services
  • The server logs configured audience scopes at startup for visibility

Example configuration with mandatory audience:

provider, err := dex.NewProvider(&dex.Config{
    IssuerURL:    "https://dex.example.com",
    ClientID:     "mcp-oauth",
    ClientSecret: "secret",
    RedirectURL:  "http://localhost:8080/oauth/callback",
    Scopes: []string{
        "openid", "profile", "email", "groups", "offline_access",
        "audience:server:client_id:dex-k8s-authenticator", // Always merged
    },
})

With this configuration, even if a client requests only ["openid", "email"], the resulting authorization request will include "audience:server:client_id:dex-k8s-authenticator".

Use the audience helper functions to format these scopes:

// Format a single audience scope (returns error for invalid input)
scope, err := dex.FormatAudienceScope("dex-k8s-authenticator")
if err != nil {
    return fmt.Errorf("invalid audience: %w", err)
}
// scope = "audience:server:client_id:dex-k8s-authenticator"

// Format multiple audience scopes
scopes, err := dex.FormatAudienceScopes([]string{"k8s-auth", "api-gateway"})
if err != nil {
    return fmt.Errorf("invalid audiences: %w", err)
}
// scopes = ["audience:server:client_id:k8s-auth", "audience:server:client_id:api-gateway"]

// Append audience scopes to existing OAuth scopes
allScopes, err := dex.AppendAudienceScopes("openid profile email", []string{"k8s-auth"})
if err != nil {
    return fmt.Errorf("invalid audiences: %w", err)
}
// allScopes = "openid profile email audience:server:client_id:k8s-auth"

// Check if a scope is an audience scope using the exported constant
if strings.HasPrefix(scope, dex.AudienceScopePrefix) {
    // handle audience scope
}

// Validate an audience string before use (optional, functions validate internally)
if err := dex.ValidateAudience("k8s-auth"); err != nil {
    return fmt.Errorf("invalid audience: %w", err)
}

Audience Scope Security

All audience helper functions validate input to prevent scope injection attacks. OAuth scopes are space-delimited (RFC 6749 Section 3.3), so an unvalidated audience string containing spaces could inject additional scopes into the OAuth request.

Security measures:

  • Character whitelist: Only alphanumeric, hyphens, and underscores allowed
  • Length limit: Maximum 256 characters per audience
  • Count limit: Maximum 50 audiences per call
  • Empty string handling: Filtered out (not treated as error)

Reference: https://dexidp.io/docs/custom-scopes-claims-clients/#cross-client-trust-and-authorized-party

Index

Constants

View Source
const AudienceScopePrefix = "audience:server:client_id:"

AudienceScopePrefix is the Dex-specific prefix for cross-client audience scopes. This can be used to check if a scope is an audience scope:

if strings.HasPrefix(scope, dex.AudienceScopePrefix) {
    // handle audience scope
}
View Source
const MaxAudienceCount = 50

MaxAudienceCount is the maximum number of audiences allowed in a single call. This prevents DoS attacks via excessive processing.

View Source
const MaxAudienceLength = 256

MaxAudienceLength is the maximum allowed length for an audience string. This prevents DoS attacks via memory exhaustion from extremely long values.

Variables

This section is empty.

Functions

func AppendAudienceScopes added in v0.2.53

func AppendAudienceScopes(scopes string, audiences []string) (string, error)

AppendAudienceScopes appends cross-client audience scopes to an existing scope string. The existing scopes and audience scopes are joined with spaces, following the OAuth 2.0 scope format (RFC 6749 Section 3.3).

Returns an error if any audience contains invalid characters. Empty audience strings are filtered out (not treated as errors).

If audiences is empty or contains only empty strings, the original scopes string is returned unchanged. If scopes is empty, only the audience scopes are returned.

Example:

result, err := dex.AppendAudienceScopes("openid profile email", []string{"k8s-auth"})
// result = "openid profile email audience:server:client_id:k8s-auth"

Use case: Adding audience scopes to token exchange or OAuth authorization requests to request tokens valid for additional downstream clients.

func FormatAudienceScope added in v0.2.53

func FormatAudienceScope(audience string) (string, error)

FormatAudienceScope formats a client ID as a Dex cross-client audience scope. This enables a token to be valid for the specified client in addition to the requesting client.

Returns an error if the audience contains invalid characters or is empty. Only alphanumeric characters, hyphens, and underscores are allowed.

Example:

scope, err := dex.FormatAudienceScope("dex-k8s-authenticator")
// scope = "audience:server:client_id:dex-k8s-authenticator"

Use case: When a user authenticates through one client but needs access to resources protected by another client (e.g., Kubernetes OIDC via dex-k8s-authenticator).

func FormatAudienceScopes added in v0.2.53

func FormatAudienceScopes(audiences []string) ([]string, error)

FormatAudienceScopes formats multiple client IDs as Dex cross-client audience scopes. Empty strings in the input are filtered out (not treated as errors).

Returns an error if any non-empty audience contains invalid characters. Only alphanumeric characters, hyphens, and underscores are allowed.

Example:

scopes, err := dex.FormatAudienceScopes([]string{"k8s-auth", "api-gateway", ""})
// scopes = []string{
//     "audience:server:client_id:k8s-auth",
//     "audience:server:client_id:api-gateway",
// }

Use case: When a token needs to be valid for multiple downstream services simultaneously for cross-cluster or multi-service SSO.

func ValidateAudience added in v0.2.53

func ValidateAudience(audience string) error

ValidateAudience validates a client ID for use in cross-client audience scopes.

Security Considerations:

  • Character Whitelist: Only alphanumeric, hyphens, and underscores are allowed. This prevents scope injection attacks where spaces could inject additional scopes.
  • Length Limit: Prevents DoS via memory exhaustion from extremely long values.
  • Empty Check: Empty audiences are rejected as they would create invalid scopes.

Example:

if err := dex.ValidateAudience("k8s-auth"); err != nil {
    return fmt.Errorf("invalid audience: %w", err)
}

func ValidateAudiences added in v0.2.53

func ValidateAudiences(audiences []string) error

ValidateAudiences validates multiple client IDs for use in cross-client audience scopes.

Security Considerations:

  • Count Limit: Maximum of 50 audiences to prevent DoS.
  • Per-audience validation: Each audience is validated individually.

Example:

if err := dex.ValidateAudiences([]string{"k8s-auth", "api-gateway"}); err != nil {
    return fmt.Errorf("invalid audiences: %w", err)
}

Types

type Config

type Config struct {
	// IssuerURL is the Dex issuer URL (e.g., https://dex.example.com)
	IssuerURL string

	// ClientID is the OAuth client ID
	ClientID string

	// ClientSecret is the OAuth client secret
	ClientSecret string

	// RedirectURL is the OAuth redirect URL
	RedirectURL string

	// ConnectorID is the optional Dex connector to use (e.g., "github", "ldap")
	// When set, bypasses the Dex connector selection UI
	ConnectorID string

	// Scopes are optional custom scopes (defaults to Dex-optimized scopes if empty)
	// Default: ["openid", "profile", "email", "groups", "offline_access"]
	Scopes []string

	// HTTPClient is an optional custom HTTP client
	HTTPClient *http.Client

	// RequestTimeout is the timeout for provider API calls (default: 30s)
	RequestTimeout time.Duration

	// MaxGroups is the maximum number of groups to accept from the OIDC groups claim.
	// Groups beyond this limit are truncated (not rejected) and a warning is logged.
	// Default: oidc.DefaultMaxGroups (600). Set higher for enterprise environments
	// with very large group counts (e.g., Active Directory).
	MaxGroups int

	// Logger is an optional structured logger for operational warnings (e.g., group truncation).
	// Default: slog.Default()
	Logger *slog.Logger
	// contains filtered or unexported fields
}

Config holds Dex OAuth configuration

type Provider

type Provider struct {
	*oauth2.Config
	// contains filtered or unexported fields
}

Provider implements the providers.Provider interface for Dex OAuth. It uses OIDC discovery to fetch endpoints dynamically and supports Dex-specific features like connector_id parameter and groups claim.

func NewProvider

func NewProvider(cfg *Config) (*Provider, error)

NewProvider creates a new Dex OAuth provider. It performs OIDC discovery to fetch authorization and token endpoints.

func (*Provider) AuthorizationURL

func (p *Provider) AuthorizationURL(state string, codeChallenge string, codeChallengeMethod string, scopes []string, authOpts *providers.AuthorizationURLOptions) string

AuthorizationURL generates the Dex OAuth authorization URL with PKCE support. If connector_id is configured, it appends the parameter to bypass Dex's connector selection UI. If scopes is empty, the provider's default configured scopes are used. opts contains optional OIDC parameters like prompt, login_hint, max_age (nil for defaults).

func (*Provider) DefaultScopes

func (p *Provider) DefaultScopes() []string

DefaultScopes returns the provider's configured default scopes. Returns a deep copy to prevent external modification.

func (*Provider) ExchangeCode

func (p *Provider) ExchangeCode(ctx context.Context, code string, verifier string) (*oauth2.Token, error)

ExchangeCode exchanges an authorization code for tokens with PKCE verification. Returns standard oauth2.Token.

func (*Provider) HealthCheck

func (p *Provider) HealthCheck(ctx context.Context) error

HealthCheck verifies that the Dex OIDC discovery endpoint is reachable. It performs a lightweight check by fetching the OpenID Connect discovery document.

Security Considerations:

  • This method is designed for server-side health monitoring (k8s probes, monitoring systems)
  • DO NOT expose the returned error messages directly to untrusted clients
  • Error messages may contain HTTP status codes that could leak provider state information
  • For public health endpoints, return generic "healthy/unhealthy" status only

func (*Provider) IssuerURL added in v0.2.39

func (p *Provider) IssuerURL() string

IssuerURL returns the Dex issuer URL for JWT validation.

func (*Provider) JWKSURI added in v0.2.39

func (p *Provider) JWKSURI(ctx context.Context) (string, error)

JWKSURI returns the JWKS URI for Dex from the discovery document. This implements the JWKSProvider interface for SSO token forwarding.

func (*Provider) Name

func (p *Provider) Name() string

Name returns the provider name

func (*Provider) RefreshToken

func (p *Provider) RefreshToken(ctx context.Context, refreshToken string) (*oauth2.Token, error)

RefreshToken refreshes an expired token using a refresh token. CRITICAL: Dex implements refresh token rotation - it returns a NEW refresh token on every refresh operation. The oauth2 library automatically captures this new token. Returns standard oauth2.Token with the new refresh token.

func (*Provider) RevokeToken

func (p *Provider) RevokeToken(ctx context.Context, token string) error

RevokeToken revokes a token at Dex's revocation endpoint if available. Gracefully degrades if revocation endpoint is not supported.

func (*Provider) ValidateToken

func (p *Provider) ValidateToken(ctx context.Context, accessToken string) (*providers.UserInfo, error)

ValidateToken validates an access token by calling Dex's userinfo endpoint. It parses user information including groups claim if available.

Jump to

Keyboard shortcuts

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