dex

package module
v0.0.0-...-002f203 Latest Latest
Warning

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

Go to latest
Published: Jun 8, 2026 License: MIT Imports: 15 Imported by: 0

README

Dex

Since :material-tag: main

Introduction

The Testcontainers module for Dex, a CNCF OIDC provider.

Adding this module to your project dependencies

go get github.com/testcontainers/testcontainers-go/modules/dex

Usage example

ctx := context.Background()

app, err := dex.NewClient("my-app",
    dex.WithClientSecret("secret"),
    dex.WithClientRedirectURIs("http://localhost:8080/callback"),
    dex.WithClientGrantTypes("authorization_code", "refresh_token"),
    dex.WithClientName("My App"),
)
if err != nil {
    log.Fatalf("new client: %v", err)
}

user, err := dex.NewUser("user@example.com", "user", "password")
if err != nil {
    log.Fatalf("new user: %v", err)
}

c, err := dex.Run(ctx, "dexidp/dex:v2.45.1",
    dex.WithClient(app),
    dex.WithUser(user),
)
if err != nil {
    log.Fatalf("run dex: %v", err)
}
defer testcontainers.TerminateContainer(c)

fmt.Println("issuer:", c.IssuerURL())

Supported grants

authorization_code, refresh_token, password. Declare per-client via WithClientGrantTypes(...).

client_credentials requires Dex ≥ v2.46.0 (or dexidp/dex:master until that release ships) with the feature flag enabled. Dex gates this grant behind the env var DEX_CLIENT_CREDENTIAL_GRANT_ENABLED_BY_DEFAULT=true. Use WithEnableClientCredentials() to set it automatically. The module does not validate the image tag — the caller must pin a compatible image.

svc, err := dex.NewClient("svc",
    dex.WithClientSecret("s"),
    dex.WithClientName("Service"),
    dex.WithClientGrantTypes("client_credentials"),
)
// ...
c, err := dex.Run(ctx, "dexidp/dex:master",
    dex.WithEnableClientCredentials(),
    dex.WithClient(svc),
)

Clients added at runtime via AddClient inherit Dex's defaults (authorization_code + refresh_token) because Dex's gRPC api.Client proto has no grant_types field. Clients needing other grants must be declared pre-start via WithClient.

Connectors

  • ConnectorPassword — Dex's built-in static password DB (default; enabled automatically when a user is seeded via WithUser). Disable via WithDisablePasswordDB() when running connector-only flows.
  • ConnectorMock — Dex's mockCallback test connector (returns a fixed user, kilgore@kilgore.trout).

Issuer URL

By default, the issuer is derived from the host and mapped HTTP port: http://<host>:<mappedPort>/dex. Pass WithIssuer(...) when the issuer must be reachable from other containers (for example via a shared Docker network and a network alias). The caller owns reachability when overriding.

ID token claims

Dex's password connector emits these standard claims in ID tokens:

  • sub — stable user ID (auto-generated UUID when constructed via NewUser without WithUserID).
  • email — user's email address.
  • email_verified — always true for static password entries.
  • name — the value of User's username.
  • iss — the issuer URL.
  • aud — the client ID.

Dex does NOT emit the preferred_username claim; use the name claim instead when a human-readable identifier is needed.

Module reference

Types
  • Client — opaque OAuth2 client value. Construct with NewClient(id, opts...).
  • User — opaque password entry. Construct with NewUser(email, username, password, opts...).
  • ConnectorTypeConnectorPassword, ConnectorMock
  • StorageStorageSQLite (default), StorageMemory
Client options (ClientOption)
  • WithClientSecret(string)
  • WithClientName(string)
  • WithClientRedirectURIs(...string)
  • WithClientGrantTypes(...string)
  • WithClientPublic()
User options (UserOption)
  • WithUserID(string) — pin a stable subject claim. Omit to auto-generate UUIDv4.
Module options
  • WithClient(Client)
  • WithUser(User)
  • WithConnector(type, id, name)
  • WithIssuer(url)
  • WithSkipApprovalScreen(bool)
  • WithStorage(Storage)StorageSQLite (default) or StorageMemory
  • WithDisablePasswordDB() — opt out of the built-in password DB
  • WithLogger(*slog.Logger) — captures Dex logs
  • WithLogLevel(slog.Level) — sets Dex's logger.level YAML key
  • WithEnableClientCredentials() — enables the OAuth2 client_credentials grant via feature flag (requires Dex ≥ v2.46.0 or :master)
Endpoint getters
  • IssuerURL() string
  • ConfigEndpoint() string
  • JWKSEndpoint() string
  • TokenEndpoint() string
  • AuthEndpoint() string
  • GRPCEndpoint(ctx context.Context) (string, error) — only getter that may return an error (Docker host/port lookup).
Runtime mutation (gRPC)

AddClient, RemoveClient, AddUser, RemoveUser. Not safe for concurrent use.

  • AddClient returns ErrClientExists when the ID is already registered.
  • AddUser returns ErrUserExists when the email is already registered.
  • RemoveClient wraps ErrClientNotFound when the ID is absent.
  • RemoveUser wraps ErrUserNotFound when the email is absent.

Known limitations

  • Runtime-added clients inherit Dex's default grants; non-default grants must be declared pre-start via WithClient.
  • client_credentials requires WithEnableClientCredentials() and Dex ≥ v2.46.0 or the :master image tag. On v2.45.x and earlier the token endpoint returns unsupported_grant_type regardless of client config.
  • gRPC admin API is plaintext. TLS not supported day 1.
  • mockCallback emits a fixed user; parameterized flows need the password connector with a pre-seeded user.
  • SQLite storage is container-local and ephemeral.
  • Bcrypt cost ≥ 10 is enforced by Dex for both YAML and gRPC password paths.

Documentation

Overview

Package dex provides a testcontainers module for the Dex OIDC provider.

Supported grants: authorization_code, refresh_token, password. The client_credentials grant requires Dex ≥ v2.46.0 (or dexidp/dex:master) together with WithEnableClientCredentials() — this sets the DEX_CLIENT_CREDENTIAL_GRANT_ENABLED_BY_DEFAULT=true env var that gates the feature. Earlier releases return unsupported_grant_type.

Example:

ctx := context.Background()
app, err := dex.NewClient("my-app",
    dex.WithClientSecret("s3cr3t"),
    dex.WithClientRedirectURIs("http://localhost/callback"),
)
if err != nil { log.Fatal(err) }
user, err := dex.NewUser("u@example.com", "u", "p")
if err != nil { log.Fatal(err) }

c, err := dex.Run(ctx, "dexidp/dex:v2.45.1",
    dex.WithClient(app),
    dex.WithUser(user),
)
defer testcontainers.TerminateContainer(c)

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrClientExists is returned by AddClient when a client with the given
	// ID is already registered.
	ErrClientExists = errors.New("dex: client already exists")
	// ErrClientNotFound is returned by RemoveClient when no client matches
	// the given ID.
	ErrClientNotFound = errors.New("dex: client not found")
	// ErrUserExists is returned by AddUser when a user with the given email
	// is already registered.
	ErrUserExists = errors.New("dex: user already exists")
	// ErrUserNotFound is returned by RemoveUser when no user matches the
	// given email.
	ErrUserNotFound = errors.New("dex: user not found")
	// ErrNoAuthSource is returned when the rendered Dex config would boot
	// with no working authentication source — neither the password DB nor
	// any connector. The password DB is enabled by default; callers must
	// explicitly disable it via WithDisablePasswordDB to trigger this error.
	ErrNoAuthSource = errors.New("dex: no auth source configured")
)

Functions

This section is empty.

Types

type Client

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

Client is a static OAuth2 client registered with Dex at boot time via WithClient. Construct with NewClient so invalid configuration surfaces at call-site rather than at Run.

func NewClient

func NewClient(id string, opts ...ClientOption) (Client, error)

NewClient creates a Client registered statically at boot. ID is required; every other field is optional and may be set through ClientOption values.

Returns an error when the ID is blank or any option rejects its input.

func (Client) ID

func (c Client) ID() string

ID returns the client_id.

type ClientOption

type ClientOption func(*Client) error

ClientOption configures a Client during NewClient.

func WithClientGrantTypes

func WithClientGrantTypes(grants ...string) ClientOption

WithClientGrantTypes appends to the allowed OAuth2 grants. Defaults to ["authorization_code", "refresh_token"] when unset. Accepted values: authorization_code, refresh_token, client_credentials, password.

Only takes effect for clients registered via WithClient (YAML). Clients added at runtime via AddClient inherit Dex's defaults because the gRPC api.Client proto has no grant_types field.

func WithClientName

func WithClientName(n string) ClientOption

WithClientName sets the human-readable display name shown on Dex's consent screen.

func WithClientPublic

func WithClientPublic() ClientOption

WithClientPublic marks the client as public — no secret, intended for PKCE flows from untrusted clients (mobile, SPA).

func WithClientRedirectURIs

func WithClientRedirectURIs(uris ...string) ClientOption

WithClientRedirectURIs appends to the list of allowed redirect URIs. At least one is required for authorization_code clients. Values are appended across calls; blank entries are rejected.

func WithClientSecret

func WithClientSecret(s string) ClientOption

WithClientSecret sets the client secret. Required for confidential clients; omit for public (PKCE) clients via WithClientPublic.

type ConnectorType

type ConnectorType string

ConnectorType selects a Dex connector kind.

const (
	// ConnectorPassword enables Dex's built-in static password connector.
	// Users must be registered via WithUser or AddUser.
	ConnectorPassword ConnectorType = "password"
	// ConnectorMock enables Dex's mockCallback connector — a test-only
	// connector that bypasses the login form and returns a fixed user.
	ConnectorMock ConnectorType = "mockCallback"
)

type Container

type Container struct {
	testcontainers.Container
	// contains filtered or unexported fields
}

Container is a running Dex OIDC provider.

func Run

Run starts Dex. The image is required (tc-go convention). Module options (WithClient, WithUser, WithIssuer, ...) and generic tc-go customizers may be mixed in the opts slice.

Example (AuthorizationCode)
// runContainer {
ctx := context.Background()

app, err := dex.NewClient("my-app",
	dex.WithClientSecret("secret"),
	dex.WithClientRedirectURIs("http://localhost:8080/callback"),
	dex.WithClientGrantTypes("authorization_code", "refresh_token"),
	dex.WithClientName("My App"),
)
if err != nil {
	log.Fatalf("new client: %v", err)
}
user, err := dex.NewUser("u@example.com", "u", "p")
if err != nil {
	log.Fatalf("new user: %v", err)
}

c, err := dex.Run(ctx, "dexidp/dex:v2.45.1",
	dex.WithClient(app),
	dex.WithUser(user),
)
if err != nil {
	log.Fatalf("run: %v", err)
}
defer func() { _ = testcontainers.TerminateContainer(c) }()
// }

_ = oauth2.Config{
	ClientID:     "my-app",
	ClientSecret: "secret",
	RedirectURL:  "http://localhost:8080/callback",
	Endpoint:     oauth2.Endpoint{AuthURL: c.AuthEndpoint(), TokenURL: c.TokenEndpoint()},
	Scopes:       []string{"openid", "email"},
}
fmt.Println("has issuer:", c.IssuerURL() != "")
Output:
has issuer: true
Example (PasswordGrant)
// Dex's recommended machine-to-machine pattern: ROPC with a dedicated
// service-account user. (client_credentials requires an upstream
// connector — see module README.)
ctx := context.Background()

svc, err := dex.NewClient("svc",
	dex.WithClientSecret("s"),
	dex.WithClientName("Service"),
	dex.WithClientGrantTypes("password"),
)
if err != nil {
	log.Fatalf("new client: %v", err)
}
user, err := dex.NewUser("svc@svc.local", "svc", "svc-secret")
if err != nil {
	log.Fatalf("new user: %v", err)
}

c, err := dex.Run(ctx, "dexidp/dex:v2.45.1",
	dex.WithClient(svc),
	dex.WithUser(user),
)
if err != nil {
	log.Fatalf("run: %v", err)
}
defer func() { _ = testcontainers.TerminateContainer(c) }()

cfg := oauth2.Config{
	ClientID: "svc", ClientSecret: "s",
	Endpoint: oauth2.Endpoint{TokenURL: c.TokenEndpoint()},
	Scopes:   []string{"openid"},
}
tok, err := cfg.PasswordCredentialsToken(ctx, "svc@svc.local", "svc-secret")
if err != nil {
	panic(fmt.Errorf("token: %w", err))
}
fmt.Println("has access token:", tok.AccessToken != "")
Output:
has access token: true

func (*Container) AddClient

func (c *Container) AddClient(ctx context.Context, cl Client) error

AddClient creates a client via Dex's gRPC admin API.

Note: Dex's api.Client proto has no grant_types field — clients added this way inherit Dex's defaults (authorization_code + refresh_token). For custom grants, register the client pre-start via WithClient.

Not safe for concurrent use.

func (*Container) AddUser

func (c *Container) AddUser(ctx context.Context, u User) error

AddUser registers a user in Dex's password DB via gRPC.

Not safe for concurrent use.

func (*Container) AuthEndpoint

func (c *Container) AuthEndpoint() string

AuthEndpoint returns the OAuth2 authorization URL.

func (*Container) ConfigEndpoint

func (c *Container) ConfigEndpoint() string

ConfigEndpoint returns the OIDC discovery document URL.

func (*Container) GRPCEndpoint

func (c *Container) GRPCEndpoint(ctx context.Context) (string, error)

GRPCEndpoint returns host:mappedPort for Dex's gRPC admin API. Errors propagate from the Docker API, or report that the container has not been started.

func (*Container) IssuerURL

func (c *Container) IssuerURL() string

IssuerURL returns Dex's issuer URL. Empty if Run has not started.

func (*Container) JWKSEndpoint

func (c *Container) JWKSEndpoint() string

JWKSEndpoint returns the JSON Web Key Set URL.

func (*Container) RemoveClient

func (c *Container) RemoveClient(ctx context.Context, id string) error

RemoveClient deletes a client by ID.

Not safe for concurrent use.

func (*Container) RemoveUser

func (c *Container) RemoveUser(ctx context.Context, email string) error

RemoveUser deletes a user by email.

Not safe for concurrent use.

func (*Container) TokenEndpoint

func (c *Container) TokenEndpoint() string

TokenEndpoint returns the OAuth2 token URL.

type Option

type Option func(*options) error

Option is a functional option for the Dex module. Options return an error so user-supplied values can be validated at Run time rather than failing silently in the rendered YAML.

NOTE: Options must be passed directly to dex.Run. They satisfy the testcontainers.ContainerCustomizer interface only so the Run signature can accept them alongside generic tc-go customizers (e.g. network.With*) — Option.Customize is a no-op, so an Option forwarded through any wrapper that dispatches via Customize (instead of type-asserting to Option) is silently dropped.

func WithClient

func WithClient(c Client) Option

WithClient registers a static client in Dex's YAML config. Unlike gRPC-added clients, these may declare custom grant types.

func WithConnector

func WithConnector(t ConnectorType, id, name string) Option

WithConnector enables a Dex connector by type. For ConnectorPassword this is a no-op — the password DB is enabled by default and the template handles it separately; id and name are ignored and blank-field validation is skipped in that case. For other connectors (e.g. ConnectorMock) the entry is added to the rendered YAML, and blank id or name returns an error.

func WithDisablePasswordDB

func WithDisablePasswordDB() Option

WithDisablePasswordDB disables Dex's built-in password connector. The caller must then configure at least one other connector via WithConnector, otherwise Run returns ErrNoAuthSource.

func WithEnableClientCredentials

func WithEnableClientCredentials() Option

WithEnableClientCredentials enables Dex's OAuth2 client_credentials grant via the DEX_CLIENT_CREDENTIAL_GRANT_ENABLED_BY_DEFAULT=true environment variable.

Requires Dex ≥ v2.46.0 or the dexidp/dex:master image tag. Earlier releases silently ignore the flag and token exchanges fail with unsupported_grant_type. This module does not validate the image tag — the caller must pin a compatible image.

func WithIssuer

func WithIssuer(url string) Option

WithIssuer overrides the default host:mappedPort-derived issuer. When set, Run uses the fast-path (direct YAML bind-mount). Callers are responsible for ensuring the URL is reachable from every client (tests and sibling containers).

func WithLogLevel

func WithLogLevel(level slog.Level) Option

WithLogLevel sets Dex's own --log-level flag. Accepts a standard library slog.Level; values are mapped to Dex's level vocabulary (debug, info, warn, error). Default: slog.LevelInfo.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger routes Dex container logs through the supplied slog.Logger. When unset, Dex container logs are discarded. Calling WithLogger(nil) is a no-op; to discard logs again after setting a logger, drop the option rather than passing nil.

func WithSkipApprovalScreen

func WithSkipApprovalScreen(skip bool) Option

WithSkipApprovalScreen toggles Dex's oauth2.skipApprovalScreen. Default: true.

func WithStorage

func WithStorage(s Storage) Option

WithStorage sets Dex's storage backend. Default: StorageSQLite.

func WithUser

func WithUser(u User) Option

WithUser registers a static password entry. The password DB connector is enabled by default, so no extra option is needed to consume the entry.

func (Option) Customize

Customize is a no-op; real state mutation happens inside Run. See the Option type-level doc for why this is a no-op.

type Storage

type Storage string

Storage selects Dex's storage backend. Defaults to StorageSQLite.

const (
	// StorageSQLite uses an on-disk SQLite database inside the container.
	// Ephemeral — destroyed when the container is removed.
	StorageSQLite Storage = "sqlite3"
	// StorageMemory keeps all state in process memory. Fastest; unsuitable
	// when multiple Dex replicas need to share state.
	StorageMemory Storage = "memory"
)

type User

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

User is a static password entry in Dex's password connector. Construct with NewUser.

func NewUser

func NewUser(email, username, password string, opts ...UserOption) (User, error)

NewUser creates a static password entry. Email, username and password are required; a user ID may be pinned via WithUserID (else a UUIDv4 is generated at YAML render time).

func (User) Email

func (u User) Email() string

Email returns the email address.

type UserOption

type UserOption func(*User) error

UserOption configures a User during NewUser.

func WithUserID

func WithUserID(id string) UserOption

WithUserID pins the stable subject claim. When unset, NewUser leaves userID blank and a UUIDv4 is generated at YAML render time.

Jump to

Keyboard shortcuts

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