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
- func AppendAudienceScopes(scopes string, audiences []string) (string, error)
- func FormatAudienceScope(audience string) (string, error)
- func FormatAudienceScopes(audiences []string) ([]string, error)
- func ValidateAudience(audience string) error
- func ValidateAudiences(audiences []string) error
- type Config
- type Provider
- func (p *Provider) AuthorizationURL(state string, codeChallenge string, codeChallengeMethod string, ...) string
- func (p *Provider) DefaultScopes() []string
- func (p *Provider) ExchangeCode(ctx context.Context, code string, verifier string) (*oauth2.Token, error)
- func (p *Provider) HealthCheck(ctx context.Context) error
- func (p *Provider) IssuerURL() string
- func (p *Provider) JWKSURI(ctx context.Context) (string, error)
- func (p *Provider) Name() string
- func (p *Provider) RefreshToken(ctx context.Context, refreshToken string) (*oauth2.Token, error)
- func (p *Provider) RevokeToken(ctx context.Context, token string) error
- func (p *Provider) ValidateToken(ctx context.Context, accessToken string) (*providers.UserInfo, error)
Constants ¶
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
}
const MaxAudienceCount = 50
MaxAudienceCount is the maximum number of audiences allowed in a single call. This prevents DoS attacks via excessive processing.
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
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
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
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
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
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 ¶
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 ¶
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 ¶
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 ¶
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
IssuerURL returns the Dex issuer URL for JWT validation.
func (*Provider) JWKSURI ¶ added in v0.2.39
JWKSURI returns the JWKS URI for Dex from the discovery document. This implements the JWKSProvider interface for SSO token forwarding.
func (*Provider) RefreshToken ¶
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 ¶
RevokeToken revokes a token at Dex's revocation endpoint if available. Gracefully degrades if revocation endpoint is not supported.