Repository: dexidp/dex
Pull request #3558 (2024-05-31 14:44:43)

feat: new Prometheus metrics build_info
<!--
Thank you for sending a pull request! Here are some tips for contributors:

1. Fill the description template below.
2. Sign a DCO (if you haven't already signed it).
3. Include appropriate tests (if necessary). Make sure that all CI checks passed.
4. If the Pull Request is a work in progress, make use of GitHub's "Draft PR" feature and mark it as such.
-->

#### Overview

<!-- Describe your changes briefly here. -->

Add a new metric `dex_build_info`, with `version` which will be set to the Git tag version.

```
$ curl -s http://127.0.0.1:5558/metrics | grep build
# HELP dex_build_info A metric with a constant '1' value labeled by version from which Dex was built.
# TYPE dex_build_info gauge
dex_build_info{goarch="arm64",goversion="go1.22.2",version="43a9a144a710803b6a3036368cec3bec9f6f02de-dirty"} 1 
```

#### What this PR does / why we need it

<!--
- Please state in detail why we need this PR and what it solves.
- If your PR closes some of the existing issues, please add links to them here.
  Mentioned issues will be automatically closed.
  Usage: "Closes #<issue number>", or "Closes (paste link of issue)"
-->

#### Special notes for your reviewer



Patch:
diff --git a/.github/workflows/analysis-scorecard.yaml b/.github/workflows/analysis-scorecard.yaml
index e752fd78..05347950 100644
--- a/.github/workflows/analysis-scorecard.yaml
+++ b/.github/workflows/analysis-scorecard.yaml
@@ -42,6 +42,6 @@ jobs:
           retention-days: 5
 
       - name: Upload results to GitHub Security tab
-        uses: github/codeql-action/upload-sarif@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
+        uses: github/codeql-action/upload-sarif@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12
         with:
           sarif_file: results.sarif
diff --git a/.github/workflows/artifacts.yaml b/.github/workflows/artifacts.yaml
index 1c047ea4..e2d45849 100644
--- a/.github/workflows/artifacts.yaml
+++ b/.github/workflows/artifacts.yaml
@@ -16,22 +16,21 @@ on:
     outputs:
       container-image-name:
         description: Container image name
-        value: ${{ jobs.container-image.outputs.name }}
+        value: ${{ jobs.container-images.outputs.name }}
       container-image-digest:
         description: Container image digest
-        value: ${{ jobs.container-image.outputs.digest }}
+        value: ${{ jobs.container-images.outputs.digest }}
       container-image-ref:
         description: Container image ref
-        value: ${{ jobs.container-image.outputs.ref }}
+        value: ${{ jobs.container-images.outputs.ref }}
 
 permissions:
   contents: read
 
 jobs:
-  container-image:
-    name: Container image
+  container-images:
+    name: Container images
     runs-on: ubuntu-latest
-
     strategy:
       matrix:
         variant:
@@ -119,7 +118,7 @@ jobs:
 
       - name: Build and push image
         id: build
-        uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0
+        uses: docker/build-push-action@1a162644f9a7e87d8f4b053101d1d9a712edc18c # v6.3.0
         with:
           context: .
           platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
@@ -179,14 +178,14 @@ jobs:
 
       # TODO: uncomment when the action is working for non ghcr.io pushes. GH Issue: https://github.com/actions/attest-build-provenance/issues/80
       # - name: Generate build provenance attestation
-      #   uses: actions/attest-build-provenance@173725a1209d09b31f9d30a3890cf2757ebbff0d # v1.1.2
+      #   uses: actions/attest-build-provenance@534b352d658f90498fd148d231fdbf88f3886a3a # v1.3.1
       #   with:
       #     subject-name: dexidp/dex
       #     subject-digest: ${{ steps.build.outputs.digest }}
       #     push-to-registry: true
 
       - name: Generate build provenance attestation
-        uses: actions/attest-build-provenance@173725a1209d09b31f9d30a3890cf2757ebbff0d # v1.1.2
+        uses: actions/attest-build-provenance@534b352d658f90498fd148d231fdbf88f3886a3a # v1.3.1
         with:
           subject-name: ghcr.io/dexidp/dex
           subject-digest: ${{ steps.build.outputs.digest }}
@@ -194,7 +193,7 @@ jobs:
         if: inputs.publish
 
       - name: Run Trivy vulnerability scanner
-        uses: aquasecurity/trivy-action@fd25fed6972e341ff0007ddb61f77e88103953c2 # 0.21.0
+        uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # 0.24.0
         with:
           input: image
           format: sarif
@@ -208,6 +207,6 @@ jobs:
           retention-days: 5
 
       - name: Upload Trivy scan results to GitHub Security tab
-        uses: github/codeql-action/upload-sarif@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
+        uses: github/codeql-action/upload-sarif@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12
         with:
           sarif_file: trivy-results.sarif
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 9bf709c9..f1e3180c 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -175,4 +175,4 @@ jobs:
         uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
 
       - name: Dependency Review
-        uses: actions/dependency-review-action@0c155c5e8556a497adf53f2c18edabf945ed8e70 # v4.3.2
+        uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4
diff --git a/Dockerfile b/Dockerfile
index e5907ff5..e27a8844 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -35,27 +35,27 @@ RUN make release-binary
 
 RUN xx-verify /go/bin/dex && xx-verify /go/bin/docker-entrypoint
 
-FROM alpine:3.20.0@sha256:77726ef6b57ddf65bb551896826ec38bc3e53f75cdde31354fbffb4f25238ebd AS stager
+FROM alpine:3.20.1@sha256:b89d9c93e9ed3597455c90a0b88a8bbb5cb7188438f70953fede212a0c4394e0 AS stager
 
 RUN mkdir -p /var/dex
 RUN mkdir -p /etc/dex
 COPY config.docker.yaml /etc/dex/
 
-FROM alpine:3.20.0@sha256:77726ef6b57ddf65bb551896826ec38bc3e53f75cdde31354fbffb4f25238ebd AS gomplate
+FROM alpine:3.20.1@sha256:b89d9c93e9ed3597455c90a0b88a8bbb5cb7188438f70953fede212a0c4394e0 AS gomplate
 
 ARG TARGETOS
 ARG TARGETARCH
 ARG TARGETVARIANT
 
-ENV GOMPLATE_VERSION=v3.11.7
+ENV GOMPLATE_VERSION=v3.11.8
 
 RUN wget -O /usr/local/bin/gomplate \
   "https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${TARGETOS:-linux}-${TARGETARCH:-amd64}${TARGETVARIANT}" \
   && chmod +x /usr/local/bin/gomplate
 
 # For Dependabot to detect base image versions
-FROM alpine:3.20.0@sha256:77726ef6b57ddf65bb551896826ec38bc3e53f75cdde31354fbffb4f25238ebd AS alpine
-FROM gcr.io/distroless/static-debian12:nonroot@sha256:e9ac71e2b8e279a8372741b7a0293afda17650d926900233ec3a7b2b7c22a246 AS distroless
+FROM alpine:3.20.1@sha256:b89d9c93e9ed3597455c90a0b88a8bbb5cb7188438f70953fede212a0c4394e0 AS alpine
+FROM gcr.io/distroless/static-debian12:nonroot@sha256:8dd8d3ca2cf283383304fd45a5c9c74d5f2cd9da8d3b077d720e264880077c65 AS distroless
 
 FROM $BASE_IMAGE
 
diff --git a/cmd/dex/config.go b/cmd/dex/config.go
index 33c0db47..b6d34f37 100644
--- a/cmd/dex/config.go
+++ b/cmd/dex/config.go
@@ -4,6 +4,7 @@ import (
 	"encoding/base64"
 	"encoding/json"
 	"fmt"
+	"log/slog"
 	"net/http"
 	"os"
 	"strings"
@@ -11,7 +12,6 @@ import (
 	"golang.org/x/crypto/bcrypt"
 
 	"github.com/dexidp/dex/pkg/featureflags"
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/server"
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/ent"
@@ -236,7 +236,7 @@ type Storage struct {
 
 // StorageConfig is a configuration that can create a storage.
 type StorageConfig interface {
-	Open(logger log.Logger) (storage.Storage, error)
+	Open(logger *slog.Logger) (storage.Storage, error)
 }
 
 var (
@@ -386,7 +386,7 @@ type Expiry struct {
 // Logger holds configuration required to customize logging for dex.
 type Logger struct {
 	// Level sets logging level severity.
-	Level string `json:"level"`
+	Level slog.Level `json:"level"`
 
 	// Format specifies the format to be used for logging.
 	Format string `json:"format"`
diff --git a/cmd/dex/serve.go b/cmd/dex/serve.go
index 9461a622..19bf2637 100644
--- a/cmd/dex/serve.go
+++ b/cmd/dex/serve.go
@@ -6,6 +6,7 @@ import (
 	"crypto/x509"
 	"errors"
 	"fmt"
+	"log/slog"
 	"net"
 	"net/http"
 	"net/http/pprof"
@@ -28,14 +29,12 @@ import (
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/collectors"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
-	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials"
 	"google.golang.org/grpc/reflection"
 
 	"github.com/dexidp/dex/api/v2"
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/server"
 	"github.com/dexidp/dex/storage"
 )
@@ -51,6 +50,15 @@ type serveOptions struct {
 	grpcAddr      string
 }
 
+var buildInfo = prometheus.NewGaugeVec(
+	prometheus.GaugeOpts{
+		Name:      "build_info",
+		Namespace: "dex",
+		Help:      "A metric with a constant '1' value labeled by version from which Dex was built.",
+	},
+	[]string{"version", "go_version", "platform"},
+)
+
 func commandServe() *cobra.Command {
 	options := serveOptions{}
 
@@ -98,24 +106,30 @@ func runServe(options serveOptions) error {
 		return fmt.Errorf("invalid config: %v", err)
 	}
 
-	logger.Infof(
-		"Dex Version: %s, Go Version: %s, Go OS/ARCH: %s %s",
-		version,
-		runtime.Version(),
-		runtime.GOOS,
-		runtime.GOARCH,
+	logger.Info(
+		"Version info",
+		"dex_version", version,
+		slog.Group("go",
+			"version", runtime.Version(),
+			"os", runtime.GOOS,
+			"arch", runtime.GOARCH,
+		),
 	)
 
-	if c.Logger.Level != "" {
-		logger.Infof("config using log level: %s", c.Logger.Level)
+	if c.Logger.Level != slog.LevelInfo {
+		logger.Info("config using log level", "level", c.Logger.Level)
 	}
 	if err := c.Validate(); err != nil {
 		return err
 	}
 
-	logger.Infof("config issuer: %s", c.Issuer)
+	logger.Info("config issuer", "issuer", c.Issuer)
 
 	prometheusRegistry := prometheus.NewRegistry()
+
+	prometheusRegistry.MustRegister(buildInfo)
+	recordBuildInfo()
+
 	err = prometheusRegistry.Register(collectors.NewGoCollector())
 	if err != nil {
 		return fmt.Errorf("failed to register Go runtime metrics: %v", err)
@@ -188,7 +202,7 @@ func runServe(options serveOptions) error {
 	}
 	defer s.Close()
 
-	logger.Infof("config storage: %s", c.Storage.Type)
+	logger.Info("config storage", "storage_type", c.Storage.Type)
 
 	if len(c.StaticClients) > 0 {
 		for i, client := range c.StaticClients {
@@ -213,7 +227,7 @@ func runServe(options serveOptions) error {
 				}
 				c.StaticClients[i].Secret = os.Getenv(client.SecretEnv)
 			}
-			logger.Infof("config static client: %s", client.Name)
+			logger.Info("config static client", "client_name", client.Name)
 		}
 		s = storage.WithStaticClients(s, c.StaticClients)
 	}
@@ -233,7 +247,7 @@ func runServe(options serveOptions) error {
 		if c.Config == nil {
 			return fmt.Errorf("invalid config: no config field for connector %q", c.ID)
 		}
-		logger.Infof("config connector: %s", c.ID)
+		logger.Info("config connector", "connector_id", c.ID)
 
 		// convert to a storage connector object
 		conn, err := ToStorageConnector(c)
@@ -249,22 +263,22 @@ func runServe(options serveOptions) error {
 			Name: "Email",
 			Type: server.LocalConnector,
 		})
-		logger.Infof("config connector: local passwords enabled")
+		logger.Info("config connector: local passwords enabled")
 	}
 
 	s = storage.WithStaticConnectors(s, storageConnectors)
 
 	if len(c.OAuth2.ResponseTypes) > 0 {
-		logger.Infof("config response types accepted: %s", c.OAuth2.ResponseTypes)
+		logger.Info("config response types accepted", "response_types", c.OAuth2.ResponseTypes)
 	}
 	if c.OAuth2.SkipApprovalScreen {
-		logger.Infof("config skipping approval screen")
+		logger.Info("config skipping approval screen")
 	}
 	if c.OAuth2.PasswordConnector != "" {
-		logger.Infof("config using password grant connector: %s", c.OAuth2.PasswordConnector)
+		logger.Info("config using password grant connector", "password_connector", c.OAuth2.PasswordConnector)
 	}
 	if len(c.Web.AllowedOrigins) > 0 {
-		logger.Infof("config allowed origins: %s", c.Web.AllowedOrigins)
+		logger.Info("config allowed origins", "origins", c.Web.AllowedOrigins)
 	}
 
 	// explicitly convert to UTC.
@@ -294,7 +308,7 @@ func runServe(options serveOptions) error {
 		if err != nil {
 			return fmt.Errorf("invalid config value %q for signing keys expiry: %v", c.Expiry.SigningKeys, err)
 		}
-		logger.Infof("config signing keys expire after: %v", signingKeys)
+		logger.Info("config signing keys", "expire_after", signingKeys)
 		serverConfig.RotateKeysAfter = signingKeys
 	}
 	if c.Expiry.IDTokens != "" {
@@ -302,7 +316,7 @@ func runServe(options serveOptions) error {
 		if err != nil {
 			return fmt.Errorf("invalid config value %q for id token expiry: %v", c.Expiry.IDTokens, err)
 		}
-		logger.Infof("config id tokens valid for: %v", idTokens)
+		logger.Info("config id tokens", "valid_for", idTokens)
 		serverConfig.IDTokensValidFor = idTokens
 	}
 	if c.Expiry.AuthRequests != "" {
@@ -310,7 +324,7 @@ func runServe(options serveOptions) error {
 		if err != nil {
 			return fmt.Errorf("invalid config value %q for auth request expiry: %v", c.Expiry.AuthRequests, err)
 		}
-		logger.Infof("config auth requests valid for: %v", authRequests)
+		logger.Info("config auth requests", "valid_for", authRequests)
 		serverConfig.AuthRequestsValidFor = authRequests
 	}
 	if c.Expiry.DeviceRequests != "" {
@@ -318,7 +332,7 @@ func runServe(options serveOptions) error {
 		if err != nil {
 			return fmt.Errorf("invalid config value %q for device request expiry: %v", c.Expiry.AuthRequests, err)
 		}
-		logger.Infof("config device requests valid for: %v", deviceRequests)
+		logger.Info("config device requests", "valid_for", deviceRequests)
 		serverConfig.DeviceRequestsValidFor = deviceRequests
 	}
 	refreshTokenPolicy, err := server.NewRefreshTokenPolicy(
@@ -368,7 +382,7 @@ func runServe(options serveOptions) error {
 	if c.Telemetry.HTTP != "" {
 		const name = "telemetry"
 
-		logger.Infof("listening (%s) on %s", name, c.Telemetry.HTTP)
+		logger.Info("listening on", "server", name, "address", c.Telemetry.HTTP)
 
 		l, err := net.Listen("tcp", c.Telemetry.HTTP)
 		if err != nil {
@@ -390,9 +404,9 @@ func runServe(options serveOptions) error {
 			ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
 			defer cancel()
 
-			logger.Debugf("starting graceful shutdown (%s)", name)
+			logger.Debug("starting graceful shutdown", "server", name)
 			if err := server.Shutdown(ctx); err != nil {
-				logger.Errorf("graceful shutdown (%s): %v", name, err)
+				logger.Error("graceful shutdown", "server", name, "err", err)
 			}
 		})
 	}
@@ -401,7 +415,7 @@ func runServe(options serveOptions) error {
 	if c.Web.HTTP != "" {
 		const name = "http"
 
-		logger.Infof("listening (%s) on %s", name, c.Web.HTTP)
+		logger.Info("listening on", "server", name, "address", c.Web.HTTP)
 
 		l, err := net.Listen("tcp", c.Web.HTTP)
 		if err != nil {
@@ -419,9 +433,9 @@ func runServe(options serveOptions) error {
 			ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
 			defer cancel()
 
-			logger.Debugf("starting graceful shutdown (%s)", name)
+			logger.Debug("starting graceful shutdown", "server", name)
 			if err := server.Shutdown(ctx); err != nil {
-				logger.Errorf("graceful shutdown (%s): %v", name, err)
+				logger.Error("graceful shutdown", "server", name, "err", err)
 			}
 		})
 	}
@@ -430,7 +444,7 @@ func runServe(options serveOptions) error {
 	if c.Web.HTTPS != "" {
 		const name = "https"
 
-		logger.Infof("listening (%s) on %s", name, c.Web.HTTPS)
+		logger.Info("listening on", "server", name, "address", c.Web.HTTPS)
 
 		l, err := net.Listen("tcp", c.Web.HTTPS)
 		if err != nil {
@@ -470,16 +484,16 @@ func runServe(options serveOptions) error {
 			ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
 			defer cancel()
 
-			logger.Debugf("starting graceful shutdown (%s)", name)
+			logger.Debug("starting graceful shutdown", "server", name)
 			if err := server.Shutdown(ctx); err != nil {
-				logger.Errorf("graceful shutdown (%s): %v", name, err)
+				logger.Error("graceful shutdown", "server", name, "err", err)
 			}
 		})
 	}
 
 	// Set up grpc server
 	if c.GRPC.Addr != "" {
-		logger.Infof("listening (grpc) on %s", c.GRPC.Addr)
+		logger.Info("listening on", "server", "grpc", "address", c.GRPC.Addr)
 
 		grpcListener, err := net.Listen("tcp", c.GRPC.Addr)
 		if err != nil {
@@ -498,7 +512,7 @@ func runServe(options serveOptions) error {
 		group.Add(func() error {
 			return grpcSrv.Serve(grpcListener)
 		}, func(err error) {
-			logger.Debugf("starting graceful shutdown (grpc)")
+			logger.Debug("starting graceful shutdown", "server", "grpc")
 			grpcSrv.GracefulStop()
 		})
 	}
@@ -508,53 +522,29 @@ func runServe(options serveOptions) error {
 		if _, ok := err.(run.SignalError); !ok {
 			return fmt.Errorf("run groups: %w", err)
 		}
-		logger.Infof("%v, shutdown now", err)
+		logger.Info("shutdown now", "err", err)
 	}
 	return nil
 }
 
-var (
-	logLevels  = []string{"debug", "info", "error"}
-	logFormats = []string{"json", "text"}
-)
-
-type utcFormatter struct {
-	f logrus.Formatter
-}
-
-func (f *utcFormatter) Format(e *logrus.Entry) ([]byte, error) {
-	e.Time = e.Time.UTC()
-	return f.f.Format(e)
-}
-
-func newLogger(level string, format string) (log.Logger, error) {
-	var logLevel logrus.Level
-	switch strings.ToLower(level) {
-	case "debug":
-		logLevel = logrus.DebugLevel
-	case "", "info":
-		logLevel = logrus.InfoLevel
-	case "error":
-		logLevel = logrus.ErrorLevel
-	default:
-		return nil, fmt.Errorf("log level is not one of the supported values (%s): %s", strings.Join(logLevels, ", "), level)
-	}
+var logFormats = []string{"json", "text"}
 
-	var formatter utcFormatter
+func newLogger(level slog.Level, format string) (*slog.Logger, error) {
+	var handler slog.Handler
 	switch strings.ToLower(format) {
 	case "", "text":
-		formatter.f = &logrus.TextFormatter{DisableColors: true}
+		handler = slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
+			Level: level,
+		})
 	case "json":
-		formatter.f = &logrus.JSONFormatter{}
+		handler = slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
+			Level: level,
+		})
 	default:
 		return nil, fmt.Errorf("log format is not one of the supported values (%s): %s", strings.Join(logFormats, ", "), format)
 	}
 
-	return &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &formatter,
-		Level:     logLevel,
-	}, nil
+	return slog.New(handler), nil
 }
 
 func applyConfigOverrides(options serveOptions, config *Config) {
@@ -600,7 +590,7 @@ func pprofHandler(router *http.ServeMux) {
 
 // newTLSReloader returns a [tls.Config] with GetCertificate or GetConfigForClient set
 // to reload certificates from the given paths on SIGHUP or on file creates (atomic update via rename).
-func newTLSReloader(logger log.Logger, certFile, keyFile, caFile string, baseConfig *tls.Config) (*tls.Config, error) {
+func newTLSReloader(logger *slog.Logger, certFile, keyFile, caFile string, baseConfig *tls.Config) (*tls.Config, error) {
 	// trigger reload on channel
 	sigc := make(chan os.Signal, 1)
 	signal.Notify(sigc, syscall.SIGHUP)
@@ -631,7 +621,7 @@ func newTLSReloader(logger log.Logger, certFile, keyFile, caFile string, baseCon
 	// recommended by fsnotify: watch the dir to handle renames
 	// https://pkg.go.dev/github.com/fsnotify/fsnotify#hdr-Watching_files
 	for dir := range watchDirs {
-		logger.Debugf("watching dir: %v", dir)
+		logger.Debug("watching dir", "dir", dir)
 		err := watcher.Add(dir)
 		if err != nil {
 			return nil, fmt.Errorf("watch dir for TLS reloader: %v", err)
@@ -654,19 +644,19 @@ func newTLSReloader(logger log.Logger, certFile, keyFile, caFile string, baseCon
 		for {
 			select {
 			case sig := <-sigc:
-				logger.Debug("reloading cert from signal: %v", sig)
+				logger.Debug("reloading cert from signal", "signal", sig)
 			case evt := <-watcher.Events:
 				if _, ok := watchFiles[evt.Name]; !ok || !evt.Has(fsnotify.Create) {
 					continue loop
 				}
-				logger.Debug("reloading cert from fsnotify: %v %v", evt.Name, evt.Op.String())
+				logger.Debug("reloading cert from fsnotify", "event", evt.Name, "operation", evt.Op.String())
 			case err := <-watcher.Errors:
-				logger.Errorf("TLS reloader watch: %v", err)
+				logger.Error("TLS reloader watch", "err", err)
 			}
 
 			loaded, err := loadTLSConfig(certFile, keyFile, caFile, baseConfig)
 			if err != nil {
-				logger.Errorf("reload TLS config: %v", err)
+				logger.Error("reload TLS config", "err", err)
 			}
 			ptr.Store(loaded)
 		}
@@ -707,3 +697,8 @@ func loadTLSConfig(certFile, keyFile, caFile string, baseConfig *tls.Config) (*t
 	}
 	return loadedConfig, nil
 }
+
+// recordBuildInfo publishes information about Dex version and runtime info through an info metric (gauge).
+func recordBuildInfo() {
+	buildInfo.WithLabelValues(version, runtime.Version(), fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)).Set(1)
+}
diff --git a/cmd/docker-entrypoint/main.go b/cmd/docker-entrypoint/main.go
index 0c507d17..b0f8e277 100644
--- a/cmd/docker-entrypoint/main.go
+++ b/cmd/docker-entrypoint/main.go
@@ -17,6 +17,10 @@ func main() {
 	// Note that this docker-entrypoint program is args[0], and it is provided with the true process
 	// args.
 	args := os.Args[1:]
+	if len(args) == 0 {
+		fmt.Println("error: no args passed to entrypoint")
+		os.Exit(1)
+	}
 
 	if err := run(args, realExec, realWhich); err != nil {
 		fmt.Println("error:", err.Error())
diff --git a/connector/atlassiancrowd/atlassiancrowd.go b/connector/atlassiancrowd/atlassiancrowd.go
index aa142203..d3683284 100644
--- a/connector/atlassiancrowd/atlassiancrowd.go
+++ b/connector/atlassiancrowd/atlassiancrowd.go
@@ -7,6 +7,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"log/slog"
 	"net"
 	"net/http"
 	"strings"
@@ -14,7 +15,6 @@ import (
 
 	"github.com/dexidp/dex/connector"
 	"github.com/dexidp/dex/pkg/groups"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 // Config holds configuration options for Atlassian Crowd connector.
@@ -80,16 +80,16 @@ type crowdAuthenticationError struct {
 }
 
 // Open returns a strategy for logging in through Atlassian Crowd
-func (c *Config) Open(_ string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	if c.BaseURL == "" {
 		return nil, fmt.Errorf("crowd: no baseURL provided for crowd connector")
 	}
-	return &crowdConnector{Config: *c, logger: logger}, nil
+	return &crowdConnector{Config: *c, logger: logger.With(slog.Group("connector", "type", "atlassiancrowd", "id", id))}, nil
 }
 
 type crowdConnector struct {
 	Config
-	logger log.Logger
+	logger *slog.Logger
 }
 
 var (
@@ -375,7 +375,7 @@ func (c *crowdConnector) identityFromCrowdUser(user crowdUser) connector.Identit
 		identity.PreferredUsername = user.Email
 	default:
 		if c.PreferredUsernameField != "" {
-			c.logger.Warnf("preferred_username left empty. Invalid crowd field mapped to preferred_username: %s", c.PreferredUsernameField)
+			c.logger.Warn("preferred_username left empty. Invalid crowd field mapped to preferred_username", "field", c.PreferredUsernameField)
 		}
 	}
 
@@ -436,12 +436,12 @@ func (c *crowdConnector) validateCrowdResponse(resp *http.Response) ([]byte, err
 	}
 
 	if resp.StatusCode == http.StatusForbidden && strings.Contains(string(body), "The server understood the request but refuses to authorize it.") {
-		c.logger.Debugf("crowd response validation failed: %s", string(body))
+		c.logger.Debug("crowd response validation failed", "response", string(body))
 		return nil, fmt.Errorf("dex is forbidden from making requests to the Atlassian Crowd application by URL %q", c.BaseURL)
 	}
 
 	if resp.StatusCode == http.StatusUnauthorized && string(body) == "Application failed to authenticate" {
-		c.logger.Debugf("crowd response validation failed: %s", string(body))
+		c.logger.Debug("crowd response validation failed", "response", string(body))
 		return nil, fmt.Errorf("dex failed to authenticate Crowd Application with ID %q", c.ClientID)
 	}
 	return body, nil
diff --git a/connector/authproxy/authproxy.go b/connector/authproxy/authproxy.go
index 465c3e3d..61353382 100644
--- a/connector/authproxy/authproxy.go
+++ b/connector/authproxy/authproxy.go
@@ -5,12 +5,12 @@ package authproxy
 
 import (
 	"fmt"
+	"log/slog"
 	"net/http"
 	"net/url"
 	"strings"
 
 	"github.com/dexidp/dex/connector"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 // Config holds the configuration parameters for a connector which returns an
@@ -27,7 +27,7 @@ type Config struct {
 }
 
 // Open returns an authentication strategy which requires no user interaction.
-func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	userIDHeader := c.UserIDHeader
 	if userIDHeader == "" {
 		userIDHeader = "X-Remote-User-Id"
@@ -51,7 +51,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
 		emailHeader:  emailHeader,
 		groupHeader:  groupHeader,
 		groups:       c.Groups,
-		logger:       logger,
+		logger:       logger.With(slog.Group("connector", "type", "authproxy", "id", id)),
 		pathSuffix:   "/" + id,
 	}, nil
 }
@@ -64,7 +64,7 @@ type callback struct {
 	emailHeader  string
 	groupHeader  string
 	groups       []string
-	logger       log.Logger
+	logger       *slog.Logger
 	pathSuffix   string
 }
 
diff --git a/connector/bitbucketcloud/bitbucketcloud.go b/connector/bitbucketcloud/bitbucketcloud.go
index 27eafb52..5f802e34 100644
--- a/connector/bitbucketcloud/bitbucketcloud.go
+++ b/connector/bitbucketcloud/bitbucketcloud.go
@@ -7,6 +7,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 	"sync"
 	"time"
@@ -16,7 +17,6 @@ import (
 
 	"github.com/dexidp/dex/connector"
 	"github.com/dexidp/dex/pkg/groups"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 const (
@@ -42,7 +42,7 @@ type Config struct {
 }
 
 // Open returns a strategy for logging in through Bitbucket.
-func (c *Config) Open(_ string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	b := bitbucketConnector{
 		redirectURI:       c.RedirectURI,
 		teams:             c.Teams,
@@ -51,7 +51,7 @@ func (c *Config) Open(_ string, logger log.Logger) (connector.Connector, error)
 		includeTeamGroups: c.IncludeTeamGroups,
 		apiURL:            apiURL,
 		legacyAPIURL:      legacyAPIURL,
-		logger:            logger,
+		logger:            logger.With(slog.Group("connector", "type", "bitbucketcloud", "id", id)),
 	}
 
 	return &b, nil
@@ -73,7 +73,7 @@ type bitbucketConnector struct {
 	teams        []string
 	clientID     string
 	clientSecret string
-	logger       log.Logger
+	logger       *slog.Logger
 	apiURL       string
 	legacyAPIURL string
 
diff --git a/connector/gitea/gitea.go b/connector/gitea/gitea.go
index 6b020994..62523185 100644
--- a/connector/gitea/gitea.go
+++ b/connector/gitea/gitea.go
@@ -7,6 +7,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 	"strconv"
 	"sync"
@@ -15,7 +16,6 @@ import (
 	"golang.org/x/oauth2"
 
 	"github.com/dexidp/dex/connector"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 // Config holds configuration options for gitea logins.
@@ -51,7 +51,7 @@ type giteaUser struct {
 }
 
 // Open returns a strategy for logging in through Gitea
-func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	if c.BaseURL == "" {
 		c.BaseURL = "https://gitea.com"
 	}
@@ -61,7 +61,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
 		orgs:          c.Orgs,
 		clientID:      c.ClientID,
 		clientSecret:  c.ClientSecret,
-		logger:        logger,
+		logger:        logger.With(slog.Group("connector", "type", "gitea", "id", id)),
 		loadAllGroups: c.LoadAllGroups,
 		useLoginAsID:  c.UseLoginAsID,
 	}, nil
@@ -84,7 +84,7 @@ type giteaConnector struct {
 	orgs         []Org
 	clientID     string
 	clientSecret string
-	logger       log.Logger
+	logger       *slog.Logger
 	httpClient   *http.Client
 	// if set to true and no orgs are configured then connector loads all user claims (all orgs and team)
 	loadAllGroups bool
diff --git a/connector/github/github.go b/connector/github/github.go
index 6cb0db09..a7818579 100644
--- a/connector/github/github.go
+++ b/connector/github/github.go
@@ -7,6 +7,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 	"regexp"
 	"strconv"
@@ -18,7 +19,6 @@ import (
 	"github.com/dexidp/dex/connector"
 	groups_pkg "github.com/dexidp/dex/pkg/groups"
 	"github.com/dexidp/dex/pkg/httpclient"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 const (
@@ -66,7 +66,7 @@ type Org struct {
 }
 
 // Open returns a strategy for logging in through GitHub.
-func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	if c.Org != "" {
 		// Return error if both 'org' and 'orgs' fields are used.
 		if len(c.Orgs) > 0 {
@@ -82,7 +82,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
 		clientID:             c.ClientID,
 		clientSecret:         c.ClientSecret,
 		apiURL:               apiURL,
-		logger:               logger,
+		logger:               logger.With(slog.Group("connector", "type", "github", "id", id)),
 		useLoginAsID:         c.UseLoginAsID,
 		preferredEmailDomain: c.PreferredEmailDomain,
 	}
@@ -142,7 +142,7 @@ type githubConnector struct {
 	orgs         []Org
 	clientID     string
 	clientSecret string
-	logger       log.Logger
+	logger       *slog.Logger
 	// apiURL defaults to "https://api.github.com"
 	apiURL string
 	// hostName of the GitHub enterprise account.
@@ -362,7 +362,7 @@ func (c *githubConnector) groupsForOrgs(ctx context.Context, client *http.Client
 		if len(org.Teams) == 0 {
 			inOrgNoTeams = true
 		} else if teams = groups_pkg.Filter(teams, org.Teams); len(teams) == 0 {
-			c.logger.Infof("github: user %q in org %q but no teams", userName, org.Name)
+			c.logger.Info("user in org but no teams", "user", userName, "org", org.Name)
 		}
 
 		for _, teamName := range teams {
@@ -667,7 +667,7 @@ func (c *githubConnector) userInOrg(ctx context.Context, client *http.Client, us
 	switch resp.StatusCode {
 	case http.StatusNoContent:
 	case http.StatusFound, http.StatusNotFound:
-		c.logger.Infof("github: user %q not in org %q or application not authorized to read org data", userName, orgName)
+		c.logger.Info("user not in org or application not authorized to read org data", "user", userName, "org", orgName)
 	default:
 		err = fmt.Errorf("github: unexpected return status: %q", resp.Status)
 	}
diff --git a/connector/gitlab/gitlab.go b/connector/gitlab/gitlab.go
index 099cd2ef..fdb2c482 100644
--- a/connector/gitlab/gitlab.go
+++ b/connector/gitlab/gitlab.go
@@ -7,6 +7,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 	"strconv"
 	"time"
@@ -15,7 +16,6 @@ import (
 
 	"github.com/dexidp/dex/connector"
 	"github.com/dexidp/dex/pkg/groups"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 const (
@@ -46,7 +46,7 @@ type gitlabUser struct {
 }
 
 // Open returns a strategy for logging in through GitLab.
-func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	if c.BaseURL == "" {
 		c.BaseURL = "https://gitlab.com"
 	}
@@ -55,7 +55,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
 		redirectURI:  c.RedirectURI,
 		clientID:     c.ClientID,
 		clientSecret: c.ClientSecret,
-		logger:       logger,
+		logger:       logger.With(slog.Group("connector", "type", "gitlab", "id", id)),
 		groups:       c.Groups,
 		useLoginAsID: c.UseLoginAsID,
 	}, nil
@@ -78,7 +78,7 @@ type gitlabConnector struct {
 	groups       []string
 	clientID     string
 	clientSecret string
-	logger       log.Logger
+	logger       *slog.Logger
 	httpClient   *http.Client
 	// if set to true will use the user's handle rather than their numeric id as the ID
 	useLoginAsID bool
diff --git a/connector/google/google.go b/connector/google/google.go
index c3042970..a370b93b 100644
--- a/connector/google/google.go
+++ b/connector/google/google.go
@@ -5,6 +5,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"log/slog"
 	"net/http"
 	"os"
 	"strings"
@@ -21,7 +22,6 @@ import (
 
 	"github.com/dexidp/dex/connector"
 	pkg_groups "github.com/dexidp/dex/pkg/groups"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 const (
@@ -67,9 +67,10 @@ type Config struct {
 }
 
 // Open returns a connector which can be used to login users through Google.
-func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, err error) {
+func (c *Config) Open(id string, logger *slog.Logger) (conn connector.Connector, err error) {
+	logger = logger.With(slog.Group("connector", "type", "google", "id", id))
 	if c.AdminEmail != "" {
-		log.Deprecated(logger, `google: use "domainToAdminEmail.*: %s" option instead of "adminEmail: %s".`, c.AdminEmail, c.AdminEmail)
+		logger.Warn(`use "domainToAdminEmail.*" option instead of "adminEmail"`, "deprecated", true)
 		if c.DomainToAdminEmail == nil {
 			c.DomainToAdminEmail = make(map[string]string)
 		}
@@ -152,7 +153,7 @@ type googleConnector struct {
 	oauth2Config                   *oauth2.Config
 	verifier                       *oidc.IDTokenVerifier
 	cancel                         context.CancelFunc
-	logger                         log.Logger
+	logger                         *slog.Logger
 	hostedDomains                  []string
 	groups                         []string
 	serviceAccountFilePath         string
@@ -340,7 +341,7 @@ func (c *googleConnector) findAdminService(domain string) (*admin.Service, error
 	adminSrv, ok := c.adminSrv[domain]
 	if !ok {
 		adminSrv, ok = c.adminSrv[wildcardDomainToAdminEmail]
-		c.logger.Debugf("using wildcard (%s) admin email to fetch groups", c.domainToAdminEmail[wildcardDomainToAdminEmail])
+		c.logger.Debug("using wildcard admin email to fetch groups", "admin_email", c.domainToAdminEmail[wildcardDomainToAdminEmail])
 	}
 
 	if !ok {
@@ -377,7 +378,7 @@ func getCredentialsFromFilePath(serviceAccountFilePath string) ([]byte, error) {
 // If the default credential is empty, it attempts to create a new service with metadata credentials.
 // If successful, it returns the service and nil error.
 // If unsuccessful, it returns the error and a nil service.
-func getCredentialsFromDefault(ctx context.Context, email string, logger log.Logger) ([]byte, *admin.Service, error) {
+func getCredentialsFromDefault(ctx context.Context, email string, logger *slog.Logger) ([]byte, *admin.Service, error) {
 	credential, err := google.FindDefaultCredentials(ctx)
 	if err != nil {
 		return nil, nil, fmt.Errorf("failed to fetch application default credentials: %w", err)
@@ -397,9 +398,9 @@ func getCredentialsFromDefault(ctx context.Context, email string, logger log.Log
 
 // createServiceWithMetadataServer creates a new service using metadata server.
 // If an error occurs during the process, it is returned along with a nil service.
-func createServiceWithMetadataServer(ctx context.Context, adminEmail string, logger log.Logger) (*admin.Service, error) {
+func createServiceWithMetadataServer(ctx context.Context, adminEmail string, logger *slog.Logger) (*admin.Service, error) {
 	serviceAccountEmail, err := metadata.Email("default")
-	logger.Infof("discovered serviceAccountEmail: %s", serviceAccountEmail)
+	logger.Info("discovered serviceAccountEmail", "email", serviceAccountEmail)
 
 	if err != nil {
 		return nil, fmt.Errorf("unable to get service account email from metadata server: %v", err)
@@ -423,7 +424,7 @@ func createServiceWithMetadataServer(ctx context.Context, adminEmail string, log
 // createDirectoryService sets up super user impersonation and creates an admin client for calling
 // the google admin api. If no serviceAccountFilePath is defined, the application default credential
 // is used.
-func createDirectoryService(serviceAccountFilePath, email string, logger log.Logger) (service *admin.Service, err error) {
+func createDirectoryService(serviceAccountFilePath, email string, logger *slog.Logger) (service *admin.Service, err error) {
 	var jsonCredentials []byte
 
 	ctx := context.Background()
diff --git a/connector/keystone/keystone.go b/connector/keystone/keystone.go
index 03f47331..cdfdb558 100644
--- a/connector/keystone/keystone.go
+++ b/connector/keystone/keystone.go
@@ -7,19 +7,21 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 
+	"github.com/google/uuid"
+
 	"github.com/dexidp/dex/connector"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 type conn struct {
-	Domain        string
+	Domain        domainKeystone
 	Host          string
 	AdminUsername string
 	AdminPassword string
 	client        *http.Client
-	Logger        log.Logger
+	Logger        *slog.Logger
 }
 
 type userKeystone struct {
@@ -29,8 +31,8 @@ type userKeystone struct {
 }
 
 type domainKeystone struct {
-	ID   string `json:"id"`
-	Name string `json:"name"`
+	ID   string `json:"id,omitempty"`
+	Name string `json:"name,omitempty"`
 }
 
 // Config holds the configuration parameters for Keystone connector.
@@ -71,13 +73,9 @@ type password struct {
 }
 
 type user struct {
-	Name     string `json:"name"`
-	Domain   domain `json:"domain"`
-	Password string `json:"password"`
-}
-
-type domain struct {
-	ID string `json:"id"`
+	Name     string         `json:"name"`
+	Domain   domainKeystone `json:"domain"`
+	Password string         `json:"password"`
 }
 
 type token struct {
@@ -111,13 +109,27 @@ var (
 )
 
 // Open returns an authentication strategy using Keystone.
-func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
+	_, err := uuid.Parse(c.Domain)
+	var domain domainKeystone
+	// check if the supplied domain is a UUID or the special "default" value
+	// which is treated as an ID and not a name
+	if err == nil || c.Domain == "default" {
+		domain = domainKeystone{
+			ID: c.Domain,
+		}
+	} else {
+		domain = domainKeystone{
+			Name: c.Domain,
+		}
+	}
+
 	return &conn{
-		Domain:        c.Domain,
+		Domain:        domain,
 		Host:          c.Host,
 		AdminUsername: c.AdminUsername,
 		AdminPassword: c.AdminPassword,
-		Logger:        logger,
+		Logger:        logger.With(slog.Group("connector", "type", "keystone", "id", id)),
 		client:        http.DefaultClient,
 	}, nil
 }
@@ -202,7 +214,7 @@ func (p *conn) getTokenResponse(ctx context.Context, username, pass string) (res
 				Password: password{
 					User: user{
 						Name:     username,
-						Domain:   domain{ID: p.Domain},
+						Domain:   p.Domain,
 						Password: pass,
 					},
 				},
@@ -287,7 +299,7 @@ func (p *conn) getUserGroups(ctx context.Context, userID string, token string) (
 	req = req.WithContext(ctx)
 	resp, err := p.client.Do(req)
 	if err != nil {
-		p.Logger.Errorf("keystone: error while fetching user %q groups\n", userID)
+		p.Logger.Error("error while fetching user groups", "user_id", userID, "err", err)
 		return nil, err
 	}
 
diff --git a/connector/ldap/ldap.go b/connector/ldap/ldap.go
index bb434a6c..b5729fe8 100644
--- a/connector/ldap/ldap.go
+++ b/connector/ldap/ldap.go
@@ -7,6 +7,7 @@ import (
 	"crypto/x509"
 	"encoding/json"
 	"fmt"
+	"log/slog"
 	"net"
 	"os"
 	"strings"
@@ -14,7 +15,6 @@ import (
 	"github.com/go-ldap/ldap/v3"
 
 	"github.com/dexidp/dex/connector"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 // Config holds the configuration parameters for the LDAP connector. The LDAP
@@ -188,12 +188,12 @@ func parseScope(s string) (int, bool) {
 // Function exists here to allow backward compatibility between old and new
 // group to user matching implementations.
 // See "Config.GroupSearch.UserMatchers" comments for the details
-func userMatchers(c *Config, logger log.Logger) []UserMatcher {
+func userMatchers(c *Config, logger *slog.Logger) []UserMatcher {
 	if len(c.GroupSearch.UserMatchers) > 0 && c.GroupSearch.UserMatchers[0].UserAttr != "" {
 		return c.GroupSearch.UserMatchers
 	}
 
-	log.Deprecated(logger, `LDAP: use groupSearch.userMatchers option instead of "userAttr/groupAttr" fields.`)
+	logger.Warn(`use "groupSearch.userMatchers" option instead of "userAttr/groupAttr" fields`, "deprecated", true)
 	return []UserMatcher{
 		{
 			UserAttr:  c.GroupSearch.UserAttr,
@@ -203,7 +203,8 @@ func userMatchers(c *Config, logger log.Logger) []UserMatcher {
 }
 
 // Open returns an authentication strategy using LDAP.
-func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
+	logger = logger.With(slog.Group("connector", "type", "ldap", "id", id))
 	conn, err := c.OpenConnector(logger)
 	if err != nil {
 		return nil, err
@@ -217,7 +218,7 @@ type refreshData struct {
 }
 
 // OpenConnector is the same as Open but returns a type with all implemented connector interfaces.
-func (c *Config) OpenConnector(logger log.Logger) (interface {
+func (c *Config) OpenConnector(logger *slog.Logger) (interface {
 	connector.Connector
 	connector.PasswordConnector
 	connector.RefreshConnector
@@ -226,7 +227,7 @@ func (c *Config) OpenConnector(logger log.Logger) (interface {
 	return c.openConnector(logger)
 }
 
-func (c *Config) openConnector(logger log.Logger) (*ldapConnector, error) {
+func (c *Config) openConnector(logger *slog.Logger) (*ldapConnector, error) {
 	requiredFields := []struct {
 		name string
 		val  string
@@ -300,7 +301,7 @@ type ldapConnector struct {
 
 	tlsConfig *tls.Config
 
-	logger log.Logger
+	logger *slog.Logger
 }
 
 var (
@@ -359,7 +360,7 @@ func (c *ldapConnector) getAttrs(e ldap.Entry, name string) []string {
 		return []string{e.DN}
 	}
 
-	c.logger.Debugf("%q attribute is not fround in entry", name)
+	c.logger.Debug("attribute is not fround in entry", "attribute", name)
 	return nil
 }
 
@@ -438,8 +439,8 @@ func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.E
 		req.Attributes = append(req.Attributes, c.UserSearch.PreferredUsernameAttrAttr)
 	}
 
-	c.logger.Infof("performing ldap search %s %s %s",
-		req.BaseDN, scopeString(req.Scope), req.Filter)
+	c.logger.Info("performing ldap search",
+		"base_dn", req.BaseDN, "scope", scopeString(req.Scope), "filter", req.Filter)
 	resp, err := conn.Search(req)
 	if err != nil {
 		return ldap.Entry{}, false, fmt.Errorf("ldap: search with filter %q failed: %v", req.Filter, err)
@@ -447,11 +448,11 @@ func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.E
 
 	switch n := len(resp.Entries); n {
 	case 0:
-		c.logger.Errorf("ldap: no results returned for filter: %q", filter)
+		c.logger.Error("no results returned for filter", "filter", filter)
 		return ldap.Entry{}, false, nil
 	case 1:
 		user = *resp.Entries[0]
-		c.logger.Infof("username %q mapped to entry %s", username, user.DN)
+		c.logger.Info("username mapped to entry", "username", username, "user_dn", user.DN)
 		return user, true, nil
 	default:
 		return ldap.Entry{}, false, fmt.Errorf("ldap: filter returned multiple (%d) results: %q", n, filter)
@@ -491,11 +492,11 @@ func (c *ldapConnector) Login(ctx context.Context, s connector.Scopes, username,
 			if ldapErr, ok := err.(*ldap.Error); ok {
 				switch ldapErr.ResultCode {
 				case ldap.LDAPResultInvalidCredentials:
-					c.logger.Errorf("ldap: invalid password for user %q", user.DN)
+					c.logger.Error("invalid password for user", "user_dn", user.DN)
 					incorrectPass = true
 					return nil
 				case ldap.LDAPResultConstraintViolation:
-					c.logger.Errorf("ldap: constraint violation for user %q: %s", user.DN, ldapErr.Error())
+					c.logger.Error("constraint violation for user", "user_dn", user.DN, "err", ldapErr.Error())
 					incorrectPass = true
 					return nil
 				}
@@ -581,7 +582,7 @@ func (c *ldapConnector) Refresh(ctx context.Context, s connector.Scopes, ident c
 
 func (c *ldapConnector) groups(ctx context.Context, user ldap.Entry) ([]string, error) {
 	if c.GroupSearch.BaseDN == "" {
-		c.logger.Debugf("No groups returned for %q because no groups baseDN has been configured.", c.getAttr(user, c.UserSearch.NameAttr))
+		c.logger.Debug("No groups returned because no groups baseDN has been configured.", "base_dn", c.getAttr(user, c.UserSearch.NameAttr))
 		return nil, nil
 	}
 
@@ -602,8 +603,8 @@ func (c *ldapConnector) groups(ctx context.Context, user ldap.Entry) ([]string,
 
 			gotGroups := false
 			if err := c.do(ctx, func(conn *ldap.Conn) error {
-				c.logger.Infof("performing ldap search %s %s %s",
-					req.BaseDN, scopeString(req.Scope), req.Filter)
+				c.logger.Info("performing ldap search",
+					"base_dn", req.BaseDN, "scope", scopeString(req.Scope), "filter", req.Filter)
 				resp, err := conn.Search(req)
 				if err != nil {
 					return fmt.Errorf("ldap: search failed: %v", err)
@@ -616,7 +617,7 @@ func (c *ldapConnector) groups(ctx context.Context, user ldap.Entry) ([]string,
 			}
 			if !gotGroups {
 				// TODO(ericchiang): Is this going to spam the logs?
-				c.logger.Errorf("ldap: groups search with filter %q returned no groups", filter)
+				c.logger.Error("groups search returned no groups", "filter", filter)
 			}
 		}
 	}
diff --git a/connector/linkedin/linkedin.go b/connector/linkedin/linkedin.go
index f79f1c49..f17d17cc 100644
--- a/connector/linkedin/linkedin.go
+++ b/connector/linkedin/linkedin.go
@@ -6,13 +6,13 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 	"strings"
 
 	"golang.org/x/oauth2"
 
 	"github.com/dexidp/dex/connector"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 const (
@@ -29,7 +29,7 @@ type Config struct {
 }
 
 // Open returns a strategy for logging in through LinkedIn
-func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	return &linkedInConnector{
 		oauth2Config: &oauth2.Config{
 			ClientID:     c.ClientID,
@@ -41,7 +41,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
 			Scopes:      []string{"r_liteprofile", "r_emailaddress"},
 			RedirectURL: c.RedirectURI,
 		},
-		logger: logger,
+		logger: logger.With(slog.Group("connector", "type", "linkedin", "id", id)),
 	}, nil
 }
 
@@ -51,7 +51,7 @@ type connectorData struct {
 
 type linkedInConnector struct {
 	oauth2Config *oauth2.Config
-	logger       log.Logger
+	logger       *slog.Logger
 }
 
 // LinkedIn doesn't provide refresh tokens, so refresh tokens issued by Dex
diff --git a/connector/microsoft/microsoft.go b/connector/microsoft/microsoft.go
index bbc3d6c6..2fcf6a75 100644
--- a/connector/microsoft/microsoft.go
+++ b/connector/microsoft/microsoft.go
@@ -8,6 +8,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 	"strings"
 	"sync"
@@ -17,7 +18,6 @@ import (
 
 	"github.com/dexidp/dex/connector"
 	groups_pkg "github.com/dexidp/dex/pkg/groups"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 // GroupNameFormat represents the format of the group identifier
@@ -66,7 +66,7 @@ type Config struct {
 }
 
 // Open returns a strategy for logging in through Microsoft.
-func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	m := microsoftConnector{
 		apiURL:               strings.TrimSuffix(c.APIURL, "/"),
 		graphURL:             strings.TrimSuffix(c.GraphURL, "/"),
@@ -78,7 +78,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
 		groups:               c.Groups,
 		groupNameFormat:      c.GroupNameFormat,
 		useGroupsAsWhitelist: c.UseGroupsAsWhitelist,
-		logger:               logger,
+		logger:               logger.With(slog.Group("connector", "type", "microsoft", "id", id)),
 		emailToLowercase:     c.EmailToLowercase,
 		promptType:           c.PromptType,
 		domainHint:           c.DomainHint,
@@ -133,7 +133,7 @@ type microsoftConnector struct {
 	groupNameFormat      GroupNameFormat
 	groups               []string
 	useGroupsAsWhitelist bool
-	logger               log.Logger
+	logger               *slog.Logger
 	emailToLowercase     bool
 	promptType           string
 	domainHint           string
diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go
index 2fe39fd4..413a813a 100644
--- a/connector/oauth/oauth.go
+++ b/connector/oauth/oauth.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"log/slog"
 	"net/http"
 	"strings"
 
@@ -13,7 +14,6 @@ import (
 
 	"github.com/dexidp/dex/connector"
 	"github.com/dexidp/dex/pkg/httpclient"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 type oauthConnector struct {
@@ -31,7 +31,7 @@ type oauthConnector struct {
 	emailVerifiedKey     string
 	groupsKey            string
 	httpClient           *http.Client
-	logger               log.Logger
+	logger               *slog.Logger
 }
 
 type connectorData struct {
@@ -58,7 +58,7 @@ type Config struct {
 	} `json:"claimMapping"`
 }
 
-func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	var err error
 
 	userIDKey := c.UserIDKey
@@ -99,7 +99,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
 		userInfoURL:          c.UserInfoURL,
 		scopes:               c.Scopes,
 		redirectURI:          c.RedirectURI,
-		logger:               logger,
+		logger:               logger.With(slog.Group("connector", "type", "oauth", "id", id)),
 		userIDKey:            userIDKey,
 		userNameKey:          userNameKey,
 		preferredUsernameKey: preferredUsernameKey,
diff --git a/connector/oidc/oidc.go b/connector/oidc/oidc.go
index fd715970..6213e068 100644
--- a/connector/oidc/oidc.go
+++ b/connector/oidc/oidc.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"log/slog"
 	"net/http"
 	"net/url"
 	"strings"
@@ -17,7 +18,6 @@ import (
 	"github.com/dexidp/dex/connector"
 	groups_pkg "github.com/dexidp/dex/pkg/groups"
 	"github.com/dexidp/dex/pkg/httpclient"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 // Config holds configuration options for OpenID Connect logins.
@@ -107,10 +107,13 @@ type ProviderDiscoveryOverrides struct {
 	// AuthURL provides a way to user overwrite the Auth URL
 	// from the .well-known/openid-configuration authorization_endpoint
 	AuthURL string `json:"authURL"`
+	// JWKSURL provides a way to user overwrite the JWKS URL
+	// from the .well-known/openid-configuration jwks_uri
+	JWKSURL string `json:"jwksURL"`
 }
 
 func (o *ProviderDiscoveryOverrides) Empty() bool {
-	return o.TokenURL == "" && o.AuthURL == ""
+	return o.TokenURL == "" && o.AuthURL == "" && o.JWKSURL == ""
 }
 
 func getProvider(ctx context.Context, issuer string, overrides ProviderDiscoveryOverrides) (*oidc.Provider, error) {
@@ -151,7 +154,9 @@ func getProvider(ctx context.Context, issuer string, overrides ProviderDiscovery
 	if overrides.AuthURL != "" {
 		config.AuthURL = overrides.AuthURL
 	}
-
+	if overrides.JWKSURL != "" {
+		config.JWKSURL = overrides.JWKSURL
+	}
 	return config.NewProvider(context.Background()), nil
 }
 
@@ -201,7 +206,7 @@ func knownBrokenAuthHeaderProvider(issuerURL string) bool {
 
 // Open returns a connector which can be used to login users through an upstream
 // OpenID Connect provider.
-func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, err error) {
+func (c *Config) Open(id string, logger *slog.Logger) (conn connector.Connector, err error) {
 	if len(c.HostedDomains) > 0 {
 		return nil, fmt.Errorf("support for the Hosted domains option had been deprecated and removed, consider switching to the Google connector")
 	}
@@ -220,7 +225,7 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e
 		return nil, err
 	}
 	if !c.ProviderDiscoveryOverrides.Empty() {
-		logger.Warnf("overrides for connector %q are set, this can be a vulnerability when not properly configured", id)
+		logger.Warn("overrides for connector are set, this can be a vulnerability when not properly configured", "connector_id", id)
 	}
 
 	endpoint := provider.Endpoint()
@@ -261,7 +266,7 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e
 		verifier: provider.Verifier(
 			&oidc.Config{ClientID: clientID},
 		),
-		logger:                    logger,
+		logger:                    logger.With(slog.Group("connector", "type", "oidc", "id", id)),
 		cancel:                    cancel,
 		httpClient:                httpClient,
 		insecureSkipEmailVerified: c.InsecureSkipEmailVerified,
@@ -291,7 +296,7 @@ type oidcConnector struct {
 	oauth2Config              *oauth2.Config
 	verifier                  *oidc.IDTokenVerifier
 	cancel                    context.CancelFunc
-	logger                    log.Logger
+	logger                    *slog.Logger
 	httpClient                *http.Client
 	insecureSkipEmailVerified bool
 	insecureEnableGroups      bool
diff --git a/connector/openshift/openshift.go b/connector/openshift/openshift.go
index 99d1b5b2..4519a85b 100644
--- a/connector/openshift/openshift.go
+++ b/connector/openshift/openshift.go
@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 	"strings"
 
@@ -13,7 +14,6 @@ import (
 	"github.com/dexidp/dex/connector"
 	"github.com/dexidp/dex/pkg/groups"
 	"github.com/dexidp/dex/pkg/httpclient"
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage/kubernetes/k8sapi"
 )
 
@@ -44,7 +44,7 @@ type openshiftConnector struct {
 	clientID     string
 	clientSecret string
 	cancel       context.CancelFunc
-	logger       log.Logger
+	logger       *slog.Logger
 	httpClient   *http.Client
 	oauth2Config *oauth2.Config
 	insecureCA   bool
@@ -62,7 +62,7 @@ type user struct {
 
 // Open returns a connector which can be used to login users through an upstream
 // OpenShift OAuth2 provider.
-func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, err error) {
+func (c *Config) Open(id string, logger *slog.Logger) (conn connector.Connector, err error) {
 	var rootCAs []string
 	if c.RootCA != "" {
 		rootCAs = append(rootCAs, c.RootCA)
@@ -78,7 +78,7 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e
 
 // OpenWithHTTPClient returns a connector which can be used to login users through an upstream
 // OpenShift OAuth2 provider. It provides the ability to inject a http.Client.
-func (c *Config) OpenWithHTTPClient(id string, logger log.Logger,
+func (c *Config) OpenWithHTTPClient(id string, logger *slog.Logger,
 	httpClient *http.Client,
 ) (conn connector.Connector, err error) {
 	ctx, cancel := context.WithCancel(context.Background())
@@ -96,7 +96,7 @@ func (c *Config) OpenWithHTTPClient(id string, logger log.Logger,
 		clientID:     c.ClientID,
 		clientSecret: c.ClientSecret,
 		insecureCA:   c.InsecureCA,
-		logger:       logger,
+		logger:       logger.With(slog.Group("connector", "type", "openshift", "id", id)),
 		redirectURI:  c.RedirectURI,
 		rootCA:       c.RootCA,
 		groups:       c.Groups,
diff --git a/connector/saml/saml.go b/connector/saml/saml.go
index fbfb1986..1ab8e544 100644
--- a/connector/saml/saml.go
+++ b/connector/saml/saml.go
@@ -8,6 +8,7 @@ import (
 	"encoding/pem"
 	"encoding/xml"
 	"fmt"
+	"log/slog"
 	"os"
 	"strings"
 	"sync"
@@ -21,10 +22,8 @@ import (
 
 	"github.com/dexidp/dex/connector"
 	"github.com/dexidp/dex/pkg/groups"
-	"github.com/dexidp/dex/pkg/log"
 )
 
-//nolint
 const (
 	bindingRedirect = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
 	bindingPOST     = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
@@ -120,11 +119,12 @@ func (c certStore) Certificates() (roots []*x509.Certificate, err error) {
 
 // Open validates the config and returns a connector. It does not actually
 // validate connectivity with the provider.
-func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
+	logger = logger.With(slog.Group("connector", "type", "saml", "id", id))
 	return c.openConnector(logger)
 }
 
-func (c *Config) openConnector(logger log.Logger) (*provider, error) {
+func (c *Config) openConnector(logger *slog.Logger) (*provider, error) {
 	requiredFields := []struct {
 		name, val string
 	}{
@@ -252,7 +252,7 @@ type provider struct {
 
 	nameIDPolicyFormat string
 
-	logger log.Logger
+	logger *slog.Logger
 }
 
 func (p *provider) POSTData(s connector.Scopes, id string) (action, value string, err error) {
@@ -389,7 +389,7 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str
 	// Log the actual attributes we got back from the server. This helps debug
 	// configuration errors on the server side, where the SAML server doesn't
 	// send us the correct attributes.
-	p.logger.Infof("parsed and verified saml response attributes %s", attributes)
+	p.logger.Info("parsed and verified saml response attributes", "attributes", attributes)
 
 	// Grab the email.
 	if ident.Email, _ = attributes.get(p.emailAttr); ident.Email == "" {
diff --git a/examples/go.mod b/examples/go.mod
index d3a47917..704d4e88 100644
--- a/examples/go.mod
+++ b/examples/go.mod
@@ -3,21 +3,21 @@ module github.com/dexidp/dex/examples
 go 1.21
 
 require (
-	github.com/coreos/go-oidc/v3 v3.10.0
+	github.com/coreos/go-oidc/v3 v3.11.0
 	github.com/dexidp/dex/api/v2 v2.1.0
-	github.com/spf13/cobra v1.8.0
-	golang.org/x/oauth2 v0.20.0
-	google.golang.org/grpc v1.64.0
+	github.com/spf13/cobra v1.8.1
+	golang.org/x/oauth2 v0.21.0
+	google.golang.org/grpc v1.65.0
 )
 
 require (
-	github.com/go-jose/go-jose/v4 v4.0.1 // indirect
+	github.com/go-jose/go-jose/v4 v4.0.2 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
-	golang.org/x/crypto v0.21.0 // indirect
-	golang.org/x/net v0.23.0 // indirect
-	golang.org/x/sys v0.18.0 // indirect
-	golang.org/x/text v0.14.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
-	google.golang.org/protobuf v1.33.0 // indirect
+	golang.org/x/crypto v0.25.0 // indirect
+	golang.org/x/net v0.27.0 // indirect
+	golang.org/x/sys v0.22.0 // indirect
+	golang.org/x/text v0.16.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
+	google.golang.org/protobuf v1.34.1 // indirect
 )
diff --git a/examples/go.sum b/examples/go.sum
index a6869c59..bd9db8e4 100644
--- a/examples/go.sum
+++ b/examples/go.sum
@@ -1,12 +1,12 @@
-github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
-github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
-github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
+github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
+github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dexidp/dex/api/v2 v2.1.0 h1:V7XTnG2HM2bqWZMABDQpf4EA6F+0jWPsv9pGaUIDo+k=
 github.com/dexidp/dex/api/v2 v2.1.0/go.mod h1:s91/6CI290JhYN1F8aiRifLF71qRGLVZvzq68uC6Ln4=
-github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
-github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
+github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
+github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -14,28 +14,28 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
-github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
+github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
+github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
 github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
-golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
-golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
-golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
-golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
-golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
-golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
-golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
-google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
-google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
-google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
+golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
+golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
+golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
+golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
+golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
+google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
+google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/go.mod b/go.mod
index 82a57891..bfb3954f 100644
--- a/go.mod
+++ b/go.mod
@@ -17,6 +17,7 @@ require (
 	github.com/go-jose/go-jose/v4 v4.0.2
 	github.com/go-ldap/ldap/v3 v3.4.6
 	github.com/go-sql-driver/mysql v1.8.1
+	github.com/google/uuid v1.6.0
 	github.com/gorilla/handlers v1.5.2
 	github.com/gorilla/mux v1.8.1
 	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
@@ -28,17 +29,16 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/prometheus/client_golang v1.19.1
 	github.com/russellhaering/goxmldsig v1.4.0
-	github.com/sirupsen/logrus v1.9.3
 	github.com/spf13/cobra v1.8.0
 	github.com/stretchr/testify v1.9.0
-	go.etcd.io/etcd/client/pkg/v3 v3.5.13
-	go.etcd.io/etcd/client/v3 v3.5.13
-	golang.org/x/crypto v0.23.0
+	go.etcd.io/etcd/client/pkg/v3 v3.5.14
+	go.etcd.io/etcd/client/v3 v3.5.14
+	golang.org/x/crypto v0.25.0
 	golang.org/x/exp v0.0.0-20221004215720-b9f4876ce741
-	golang.org/x/net v0.25.0
-	golang.org/x/oauth2 v0.20.0
+	golang.org/x/net v0.27.0
+	golang.org/x/oauth2 v0.21.0
 	google.golang.org/api v0.182.0
-	google.golang.org/grpc v1.64.0
+	google.golang.org/grpc v1.65.0
 	google.golang.org/protobuf v1.34.1
 )
 
@@ -53,7 +53,7 @@ require (
 	github.com/agext/levenshtein v1.2.1 // indirect
 	github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
-	github.com/cespare/xxhash/v2 v2.2.0 // indirect
+	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/coreos/go-semver v0.3.0 // indirect
 	github.com/coreos/go-systemd/v22 v22.3.2 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
@@ -66,7 +66,6 @@ require (
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/google/go-cmp v0.6.0 // indirect
 	github.com/google/s2a-go v0.1.7 // indirect
-	github.com/google/uuid v1.6.0 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
 	github.com/googleapis/gax-go/v2 v2.12.4 // indirect
 	github.com/hashicorp/hcl/v2 v2.13.0 // indirect
@@ -85,7 +84,7 @@ require (
 	github.com/spf13/cast v1.4.1 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/zclconf/go-cty v1.8.0 // indirect
-	go.etcd.io/etcd/api/v3 v3.5.13 // indirect
+	go.etcd.io/etcd/api/v3 v3.5.14 // indirect
 	go.opencensus.io v0.24.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
 	go.opentelemetry.io/otel v1.24.0 // indirect
@@ -94,11 +93,11 @@ require (
 	go.uber.org/atomic v1.7.0 // indirect
 	go.uber.org/multierr v1.6.0 // indirect
 	go.uber.org/zap v1.17.0 // indirect
-	golang.org/x/mod v0.15.0 // indirect
-	golang.org/x/sys v0.20.0 // indirect
-	golang.org/x/text v0.15.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e // indirect
+	golang.org/x/mod v0.17.0 // indirect
+	golang.org/x/sys v0.22.0 // indirect
+	golang.org/x/text v0.16.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index c305e95f..58db5310 100644
--- a/go.sum
+++ b/go.sum
@@ -38,8 +38,8 @@ github.com/beevik/etree v1.4.0/go.mod h1:cyWiXwGoasx60gHvtnEh5x8+uIjUVnjWqBvEnhn
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
-github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
@@ -191,8 +191,6 @@ github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
 github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
-github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
-github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
 github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
@@ -223,12 +221,12 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA=
 github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
-go.etcd.io/etcd/api/v3 v3.5.13 h1:8WXU2/NBge6AUF1K1gOexB6e07NgsN1hXK0rSTtgSp4=
-go.etcd.io/etcd/api/v3 v3.5.13/go.mod h1:gBqlqkcMMZMVTMm4NDZloEVJzxQOQIls8splbqBDa0c=
-go.etcd.io/etcd/client/pkg/v3 v3.5.13 h1:RVZSAnWWWiI5IrYAXjQorajncORbS0zI48LQlE2kQWg=
-go.etcd.io/etcd/client/pkg/v3 v3.5.13/go.mod h1:XxHT4u1qU12E2+po+UVPrEeL94Um6zL58ppuJWXSAB8=
-go.etcd.io/etcd/client/v3 v3.5.13 h1:o0fHTNJLeO0MyVbc7I3fsCf6nrOqn5d+diSarKnB2js=
-go.etcd.io/etcd/client/v3 v3.5.13/go.mod h1:cqiAeY8b5DEEcpxvgWKsbLIWNM/8Wy2xJSDMtioMcoI=
+go.etcd.io/etcd/api/v3 v3.5.14 h1:vHObSCxyB9zlF60w7qzAdTcGaglbJOpSj1Xj9+WGxq0=
+go.etcd.io/etcd/api/v3 v3.5.14/go.mod h1:BmtWcRlQvwa1h3G2jvKYwIQy4PkHlDej5t7uLMUdJUU=
+go.etcd.io/etcd/client/pkg/v3 v3.5.14 h1:SaNH6Y+rVEdxfpA2Jr5wkEvN6Zykme5+YnbCkxvuWxQ=
+go.etcd.io/etcd/client/pkg/v3 v3.5.14/go.mod h1:8uMgAokyG1czCtIdsq+AGyYQMvpIKnSvPjFMunkgeZI=
+go.etcd.io/etcd/client/v3 v3.5.14 h1:CWfRs4FDaDoSz81giL7zPpZH2Z35tbOrAJkkjMqOupg=
+go.etcd.io/etcd/client/v3 v3.5.14/go.mod h1:k3XfdV/VIHy/97rqWjoUzrj9tk7GgJGH9J8L4dNXmAk=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
@@ -253,8 +251,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
 golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
-golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
-golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
+golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20221004215720-b9f4876ce741 h1:fGZugkZk2UgYBxtpKmvub51Yno1LJDeEsRp2xGD+0gY=
 golang.org/x/exp v0.0.0-20221004215720-b9f4876ce741/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
@@ -265,8 +263,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
-golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -283,11 +281,11 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
 golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
-golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
+golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
-golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
+golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -304,14 +302,13 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
-golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@@ -327,8 +324,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
-golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
 golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
 golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -353,17 +350,17 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae h1:AH34z6WAGVNkllnKs5raNq3yRq93VnjBG6rpfub/jYk=
-google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae/go.mod h1:FfiGhwUm6CJviekPrc0oJ+7h29e+DmWU6UtjX0ZvI7Y=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e h1:Elxv5MwEkCI9f5SkoL6afed6NTdxaGoAo39eANBwHL8=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
+google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
+google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
-google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
+google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
+google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
diff --git a/pkg/log/deprecated.go b/pkg/log/deprecated.go
deleted file mode 100644
index f20e8b4c..00000000
--- a/pkg/log/deprecated.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package log
-
-func Deprecated(logger Logger, f string, args ...interface{}) {
-	logger.Warnf("Deprecated: "+f, args...)
-}
diff --git a/pkg/log/logger.go b/pkg/log/logger.go
deleted file mode 100644
index 4f3cdd38..00000000
--- a/pkg/log/logger.go
+++ /dev/null
@@ -1,18 +0,0 @@
-// Package log provides a logger interface for logger libraries
-// so that dex does not depend on any of them directly.
-// It also includes a default implementation using Logrus (used by dex previously).
-package log
-
-// Logger serves as an adapter interface for logger libraries
-// so that dex does not depend on any of them directly.
-type Logger interface {
-	Debug(args ...interface{})
-	Info(args ...interface{})
-	Warn(args ...interface{})
-	Error(args ...interface{})
-
-	Debugf(format string, args ...interface{})
-	Infof(format string, args ...interface{})
-	Warnf(format string, args ...interface{})
-	Errorf(format string, args ...interface{})
-}
diff --git a/server/api.go b/server/api.go
index c0eacb8f..6a0071ef 100644
--- a/server/api.go
+++ b/server/api.go
@@ -4,11 +4,11 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"log/slog"
 
 	"golang.org/x/crypto/bcrypt"
 
 	"github.com/dexidp/dex/api/v2"
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/server/internal"
 	"github.com/dexidp/dex/storage"
 )
@@ -29,10 +29,10 @@ const (
 )
 
 // NewAPI returns a server which implements the gRPC API interface.
-func NewAPI(s storage.Storage, logger log.Logger, version string) api.DexServer {
+func NewAPI(s storage.Storage, logger *slog.Logger, version string) api.DexServer {
 	return dexAPI{
 		s:       s,
-		logger:  logger,
+		logger:  logger.With("component", "api"),
 		version: version,
 	}
 }
@@ -41,7 +41,7 @@ type dexAPI struct {
 	api.UnimplementedDexServer
 
 	s       storage.Storage
-	logger  log.Logger
+	logger  *slog.Logger
 	version string
 }
 
@@ -89,7 +89,7 @@ func (d dexAPI) CreateClient(ctx context.Context, req *api.CreateClientReq) (*ap
 		if err == storage.ErrAlreadyExists {
 			return &api.CreateClientResp{AlreadyExists: true}, nil
 		}
-		d.logger.Errorf("api: failed to create client: %v", err)
+		d.logger.Error("failed to create client", "err", err)
 		return nil, fmt.Errorf("create client: %v", err)
 	}
 
@@ -122,7 +122,7 @@ func (d dexAPI) UpdateClient(ctx context.Context, req *api.UpdateClientReq) (*ap
 		if err == storage.ErrNotFound {
 			return &api.UpdateClientResp{NotFound: true}, nil
 		}
-		d.logger.Errorf("api: failed to update the client: %v", err)
+		d.logger.Error("failed to update the client", "err", err)
 		return nil, fmt.Errorf("update client: %v", err)
 	}
 	return &api.UpdateClientResp{}, nil
@@ -134,7 +134,7 @@ func (d dexAPI) DeleteClient(ctx context.Context, req *api.DeleteClientReq) (*ap
 		if err == storage.ErrNotFound {
 			return &api.DeleteClientResp{NotFound: true}, nil
 		}
-		d.logger.Errorf("api: failed to delete client: %v", err)
+		d.logger.Error("failed to delete client", "err", err)
 		return nil, fmt.Errorf("delete client: %v", err)
 	}
 	return &api.DeleteClientResp{}, nil
@@ -181,7 +181,7 @@ func (d dexAPI) CreatePassword(ctx context.Context, req *api.CreatePasswordReq)
 		if err == storage.ErrAlreadyExists {
 			return &api.CreatePasswordResp{AlreadyExists: true}, nil
 		}
-		d.logger.Errorf("api: failed to create password: %v", err)
+		d.logger.Error("failed to create password", "err", err)
 		return nil, fmt.Errorf("create password: %v", err)
 	}
 
@@ -218,7 +218,7 @@ func (d dexAPI) UpdatePassword(ctx context.Context, req *api.UpdatePasswordReq)
 		if err == storage.ErrNotFound {
 			return &api.UpdatePasswordResp{NotFound: true}, nil
 		}
-		d.logger.Errorf("api: failed to update password: %v", err)
+		d.logger.Error("failed to update password", "err", err)
 		return nil, fmt.Errorf("update password: %v", err)
 	}
 
@@ -235,7 +235,7 @@ func (d dexAPI) DeletePassword(ctx context.Context, req *api.DeletePasswordReq)
 		if err == storage.ErrNotFound {
 			return &api.DeletePasswordResp{NotFound: true}, nil
 		}
-		d.logger.Errorf("api: failed to delete password: %v", err)
+		d.logger.Error("failed to delete password", "err", err)
 		return nil, fmt.Errorf("delete password: %v", err)
 	}
 	return &api.DeletePasswordResp{}, nil
@@ -251,7 +251,7 @@ func (d dexAPI) GetVersion(ctx context.Context, req *api.VersionReq) (*api.Versi
 func (d dexAPI) ListPasswords(ctx context.Context, req *api.ListPasswordReq) (*api.ListPasswordResp, error) {
 	passwordList, err := d.s.ListPasswords()
 	if err != nil {
-		d.logger.Errorf("api: failed to list passwords: %v", err)
+		d.logger.Error("failed to list passwords", "err", err)
 		return nil, fmt.Errorf("list passwords: %v", err)
 	}
 
@@ -286,12 +286,12 @@ func (d dexAPI) VerifyPassword(ctx context.Context, req *api.VerifyPasswordReq)
 				NotFound: true,
 			}, nil
 		}
-		d.logger.Errorf("api: there was an error retrieving the password: %v", err)
+		d.logger.Error("there was an error retrieving the password", "err", err)
 		return nil, fmt.Errorf("verify password: %v", err)
 	}
 
 	if err := bcrypt.CompareHashAndPassword(password.Hash, []byte(req.Password)); err != nil {
-		d.logger.Infof("api: password check failed: %v", err)
+		d.logger.Info("password check failed", "err", err)
 		return &api.VerifyPasswordResp{
 			Verified: false,
 		}, nil
@@ -304,7 +304,7 @@ func (d dexAPI) VerifyPassword(ctx context.Context, req *api.VerifyPasswordReq)
 func (d dexAPI) ListRefresh(ctx context.Context, req *api.ListRefreshReq) (*api.ListRefreshResp, error) {
 	id := new(internal.IDTokenSubject)
 	if err := internal.Unmarshal(req.UserId, id); err != nil {
-		d.logger.Errorf("api: failed to unmarshal ID Token subject: %v", err)
+		d.logger.Error("failed to unmarshal ID Token subject", "err", err)
 		return nil, err
 	}
 
@@ -315,7 +315,7 @@ func (d dexAPI) ListRefresh(ctx context.Context, req *api.ListRefreshReq) (*api.
 			// An empty list should be returned instead of an error.
 			return &api.ListRefreshResp{}, nil
 		}
-		d.logger.Errorf("api: failed to list refresh tokens %t here : %v", err == storage.ErrNotFound, err)
+		d.logger.Error("failed to list refresh tokens here", "err", err)
 		return nil, err
 	}
 
@@ -338,7 +338,7 @@ func (d dexAPI) ListRefresh(ctx context.Context, req *api.ListRefreshReq) (*api.
 func (d dexAPI) RevokeRefresh(ctx context.Context, req *api.RevokeRefreshReq) (*api.RevokeRefreshResp, error) {
 	id := new(internal.IDTokenSubject)
 	if err := internal.Unmarshal(req.UserId, id); err != nil {
-		d.logger.Errorf("api: failed to unmarshal ID Token subject: %v", err)
+		d.logger.Error("failed to unmarshal ID Token subject", "err", err)
 		return nil, err
 	}
 
@@ -349,7 +349,7 @@ func (d dexAPI) RevokeRefresh(ctx context.Context, req *api.RevokeRefreshReq) (*
 	updater := func(old storage.OfflineSessions) (storage.OfflineSessions, error) {
 		refreshRef := old.Refresh[req.ClientId]
 		if refreshRef == nil || refreshRef.ID == "" {
-			d.logger.Errorf("api: refresh token issued to client %q for user %q not found for deletion", req.ClientId, id.UserId)
+			d.logger.Error("refresh token issued to client not found for deletion", "client_id", req.ClientId, "user_id", id.UserId)
 			notFound = true
 			return old, storage.ErrNotFound
 		}
@@ -366,7 +366,7 @@ func (d dexAPI) RevokeRefresh(ctx context.Context, req *api.RevokeRefreshReq) (*
 		if err == storage.ErrNotFound {
 			return &api.RevokeRefreshResp{NotFound: true}, nil
 		}
-		d.logger.Errorf("api: failed to update offline session object: %v", err)
+		d.logger.Error("failed to update offline session object", "err", err)
 		return nil, err
 	}
 
@@ -379,7 +379,7 @@ func (d dexAPI) RevokeRefresh(ctx context.Context, req *api.RevokeRefreshReq) (*
 	// TODO(ericchiang): we don't have any good recourse if this call fails.
 	// Consider garbage collection of refresh tokens with no associated ref.
 	if err := d.s.DeleteRefresh(refreshID); err != nil {
-		d.logger.Errorf("failed to delete refresh token: %v", err)
+		d.logger.Error("failed to delete refresh token", "err", err)
 		return nil, err
 	}
 
diff --git a/server/deviceflowhandlers.go b/server/deviceflowhandlers.go
index 5683e944..6f8aae03 100644
--- a/server/deviceflowhandlers.go
+++ b/server/deviceflowhandlers.go
@@ -13,7 +13,6 @@ import (
 
 	"golang.org/x/net/html"
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 )
 
@@ -49,7 +48,7 @@ func (s *Server) handleDeviceExchange(w http.ResponseWriter, r *http.Request) {
 			invalidAttempt = false
 		}
 		if err := s.templates.device(r, w, s.getDeviceVerificationURI(), userCode, invalidAttempt); err != nil {
-			s.logger.Errorf("Server template error: %v", err)
+			s.logger.Error("server template error", "err", err)
 			s.renderError(r, w, http.StatusNotFound, "Page not found")
 		}
 	default:
@@ -65,7 +64,7 @@ func (s *Server) handleDeviceCode(w http.ResponseWriter, r *http.Request) {
 	case http.MethodPost:
 		err := r.ParseForm()
 		if err != nil {
-			s.logger.Errorf("Could not parse Device Request body: %v", err)
+			s.logger.Error("could not parse Device Request body", "err", err)
 			s.tokenErrHelper(w, errInvalidRequest, "", http.StatusNotFound)
 			return
 		}
@@ -86,7 +85,7 @@ func (s *Server) handleDeviceCode(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 
-		s.logger.Infof("Received device request for client %v with scopes %v", clientID, scopes)
+		s.logger.Info("received device request", "client_id", clientID, "scoped", scopes)
 
 		// Make device code
 		deviceCode := storage.NewDeviceCode()
@@ -108,7 +107,7 @@ func (s *Server) handleDeviceCode(w http.ResponseWriter, r *http.Request) {
 		}
 
 		if err := s.storage.CreateDeviceRequest(ctx, deviceReq); err != nil {
-			s.logger.Errorf("Failed to store device request; %v", err)
+			s.logger.Error("failed to store device request", "err", err)
 			s.tokenErrHelper(w, errInvalidRequest, "", http.StatusInternalServerError)
 			return
 		}
@@ -127,14 +126,14 @@ func (s *Server) handleDeviceCode(w http.ResponseWriter, r *http.Request) {
 		}
 
 		if err := s.storage.CreateDeviceToken(ctx, deviceToken); err != nil {
-			s.logger.Errorf("Failed to store device token %v", err)
+			s.logger.Error("failed to store device token", "err", err)
 			s.tokenErrHelper(w, errInvalidRequest, "", http.StatusInternalServerError)
 			return
 		}
 
 		u, err := url.Parse(s.issuerURL.String())
 		if err != nil {
-			s.logger.Errorf("Could not parse issuer URL %v", err)
+			s.logger.Error("could not parse issuer URL", "err", err)
 			s.tokenErrHelper(w, errInvalidRequest, "", http.StatusInternalServerError)
 			return
 		}
@@ -175,14 +174,14 @@ func (s *Server) handleDeviceCode(w http.ResponseWriter, r *http.Request) {
 }
 
 func (s *Server) handleDeviceTokenDeprecated(w http.ResponseWriter, r *http.Request) {
-	log.Deprecated(s.logger, `The /device/token endpoint was called. It will be removed, use /token instead.`)
+	s.logger.Warn(`the /device/token endpoint was called. It will be removed, use /token instead.`, "deprecated", true)
 
 	w.Header().Set("Content-Type", "application/json")
 	switch r.Method {
 	case http.MethodPost:
 		err := r.ParseForm()
 		if err != nil {
-			s.logger.Warnf("Could not parse Device Token Request body: %v", err)
+			s.logger.Warn("could not parse Device Token Request body", "err", err)
 			s.tokenErrHelper(w, errInvalidRequest, "", http.StatusBadRequest)
 			return
 		}
@@ -212,7 +211,7 @@ func (s *Server) handleDeviceToken(w http.ResponseWriter, r *http.Request) {
 	deviceToken, err := s.storage.GetDeviceToken(deviceCode)
 	if err != nil {
 		if err != storage.ErrNotFound {
-			s.logger.Errorf("failed to get device code: %v", err)
+			s.logger.Error("failed to get device code", "err", err)
 		}
 		s.tokenErrHelper(w, errInvalidRequest, "Invalid Device code.", http.StatusBadRequest)
 		return
@@ -242,7 +241,7 @@ func (s *Server) handleDeviceToken(w http.ResponseWriter, r *http.Request) {
 		}
 		// Update device token last request time in storage
 		if err := s.storage.UpdateDeviceToken(deviceCode, updater); err != nil {
-			s.logger.Errorf("failed to update device token: %v", err)
+			s.logger.Error("failed to update device token", "err", err)
 			s.renderError(r, w, http.StatusInternalServerError, "")
 			return
 		}
@@ -259,7 +258,7 @@ func (s *Server) handleDeviceToken(w http.ResponseWriter, r *http.Request) {
 		case providedCodeVerifier != "" && codeChallengeFromStorage != "":
 			calculatedCodeChallenge, err := s.calculateCodeChallenge(providedCodeVerifier, deviceToken.PKCE.CodeChallengeMethod)
 			if err != nil {
-				s.logger.Error(err)
+				s.logger.Error("failed to calculate code challenge", "err", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 				return
 			}
@@ -304,7 +303,7 @@ func (s *Server) handleDeviceCallback(w http.ResponseWriter, r *http.Request) {
 		if err != nil || s.now().After(authCode.Expiry) {
 			errCode := http.StatusBadRequest
 			if err != nil && err != storage.ErrNotFound {
-				s.logger.Errorf("failed to get auth code: %v", err)
+				s.logger.Error("failed to get auth code", "err", err)
 				errCode = http.StatusInternalServerError
 			}
 			s.renderError(r, w, errCode, "Invalid or expired auth code.")
@@ -316,7 +315,7 @@ func (s *Server) handleDeviceCallback(w http.ResponseWriter, r *http.Request) {
 		if err != nil || s.now().After(deviceReq.Expiry) {
 			errCode := http.StatusBadRequest
 			if err != nil && err != storage.ErrNotFound {
-				s.logger.Errorf("failed to get device code: %v", err)
+				s.logger.Error("failed to get device code", "err", err)
 				errCode = http.StatusInternalServerError
 			}
 			s.renderError(r, w, errCode, "Invalid or expired user code.")
@@ -326,7 +325,7 @@ func (s *Server) handleDeviceCallback(w http.ResponseWriter, r *http.Request) {
 		client, err := s.storage.GetClient(deviceReq.ClientID)
 		if err != nil {
 			if err != storage.ErrNotFound {
-				s.logger.Errorf("failed to get client: %v", err)
+				s.logger.Error("failed to get client", "err", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 			} else {
 				s.tokenErrHelper(w, errInvalidClient, "Invalid client credentials.", http.StatusUnauthorized)
@@ -340,7 +339,7 @@ func (s *Server) handleDeviceCallback(w http.ResponseWriter, r *http.Request) {
 
 		resp, err := s.exchangeAuthCode(ctx, w, authCode, client)
 		if err != nil {
-			s.logger.Errorf("Could not exchange auth code for client %q: %v", deviceReq.ClientID, err)
+			s.logger.Error("could not exchange auth code for clien", "client_id", deviceReq.ClientID, "err", err)
 			s.renderError(r, w, http.StatusInternalServerError, "Failed to exchange auth code.")
 			return
 		}
@@ -350,7 +349,7 @@ func (s *Server) handleDeviceCallback(w http.ResponseWriter, r *http.Request) {
 		if err != nil || s.now().After(old.Expiry) {
 			errCode := http.StatusBadRequest
 			if err != nil && err != storage.ErrNotFound {
-				s.logger.Errorf("failed to get device token: %v", err)
+				s.logger.Error("failed to get device token", "err", err)
 				errCode = http.StatusInternalServerError
 			}
 			s.renderError(r, w, errCode, "Invalid or expired device code.")
@@ -363,7 +362,7 @@ func (s *Server) handleDeviceCallback(w http.ResponseWriter, r *http.Request) {
 			}
 			respStr, err := json.MarshalIndent(resp, "", "  ")
 			if err != nil {
-				s.logger.Errorf("failed to marshal device token response: %v", err)
+				s.logger.Error("failed to marshal device token response", "err", err)
 				s.renderError(r, w, http.StatusInternalServerError, "")
 				return old, err
 			}
@@ -375,13 +374,13 @@ func (s *Server) handleDeviceCallback(w http.ResponseWriter, r *http.Request) {
 
 		// Update refresh token in the storage, store the token and mark as complete
 		if err := s.storage.UpdateDeviceToken(deviceReq.DeviceCode, updater); err != nil {
-			s.logger.Errorf("failed to update device token: %v", err)
+			s.logger.Error("failed to update device token", "err", err)
 			s.renderError(r, w, http.StatusBadRequest, "")
 			return
 		}
 
 		if err := s.templates.deviceSuccess(r, w, client.Name); err != nil {
-			s.logger.Errorf("Server template error: %v", err)
+			s.logger.Error("Server template error", "err", err)
 			s.renderError(r, w, http.StatusNotFound, "Page not found")
 		}
 
@@ -396,7 +395,7 @@ func (s *Server) verifyUserCode(w http.ResponseWriter, r *http.Request) {
 	case http.MethodPost:
 		err := r.ParseForm()
 		if err != nil {
-			s.logger.Warnf("Could not parse user code verification request body : %v", err)
+			s.logger.Warn("could not parse user code verification request body", "err", err)
 			s.renderError(r, w, http.StatusBadRequest, "")
 			return
 		}
@@ -413,10 +412,10 @@ func (s *Server) verifyUserCode(w http.ResponseWriter, r *http.Request) {
 		deviceRequest, err := s.storage.GetDeviceRequest(userCode)
 		if err != nil || s.now().After(deviceRequest.Expiry) {
 			if err != nil && err != storage.ErrNotFound {
-				s.logger.Errorf("failed to get device request: %v", err)
+				s.logger.Error("failed to get device request", "err", err)
 			}
 			if err := s.templates.device(r, w, s.getDeviceVerificationURI(), userCode, true); err != nil {
-				s.logger.Errorf("Server template error: %v", err)
+				s.logger.Error("Server template error", "err", err)
 				s.renderError(r, w, http.StatusNotFound, "Page not found")
 			}
 			return
diff --git a/server/handlers.go b/server/handlers.go
index ccd534d9..42f3ebe5 100644
--- a/server/handlers.go
+++ b/server/handlers.go
@@ -35,13 +35,13 @@ func (s *Server) handlePublicKeys(w http.ResponseWriter, r *http.Request) {
 	// TODO(ericchiang): Cache this.
 	keys, err := s.storage.GetKeys()
 	if err != nil {
-		s.logger.Errorf("failed to get keys: %v", err)
+		s.logger.Error("failed to get keys", "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
 		return
 	}
 
 	if keys.SigningKeyPub == nil {
-		s.logger.Errorf("No public keys found.")
+		s.logger.Error("no public keys found.")
 		s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
 		return
 	}
@@ -56,7 +56,7 @@ func (s *Server) handlePublicKeys(w http.ResponseWriter, r *http.Request) {
 
 	data, err := json.MarshalIndent(jwks, "", "  ")
 	if err != nil {
-		s.logger.Errorf("failed to marshal discovery data: %v", err)
+		s.logger.Error("failed to marshal discovery data", "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
 		return
 	}
@@ -132,7 +132,7 @@ func (s *Server) discoveryHandler() (http.HandlerFunc, error) {
 func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
 	// Extract the arguments
 	if err := r.ParseForm(); err != nil {
-		s.logger.Errorf("Failed to parse arguments: %v", err)
+		s.logger.Error("failed to parse arguments", "err", err)
 
 		s.renderError(r, w, http.StatusBadRequest, err.Error())
 		return
@@ -142,7 +142,7 @@ func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
 
 	connectors, err := s.storage.ListConnectors()
 	if err != nil {
-		s.logger.Errorf("Failed to get list of connectors: %v", err)
+		s.logger.Error("failed to get list of connectors", "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Failed to retrieve connector list.")
 		return
 	}
@@ -185,7 +185,7 @@ func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if err := s.templates.login(r, w, connectorInfos); err != nil {
-		s.logger.Errorf("Server template error: %v", err)
+		s.logger.Error("server template error", "err", err)
 	}
 }
 
@@ -193,7 +193,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
 	ctx := r.Context()
 	authReq, err := s.parseAuthorizationRequest(r)
 	if err != nil {
-		s.logger.Errorf("Failed to parse authorization request: %v", err)
+		s.logger.Error("failed to parse authorization request", "err", err)
 
 		switch authErr := err.(type) {
 		case *redirectedAuthErr:
@@ -209,22 +209,22 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
 
 	connID, err := url.PathUnescape(mux.Vars(r)["connector"])
 	if err != nil {
-		s.logger.Errorf("Failed to parse connector: %v", err)
+		s.logger.Error("failed to parse connector", "err", err)
 		s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist")
 		return
 	}
 
 	conn, err := s.getConnector(connID)
 	if err != nil {
-		s.logger.Errorf("Failed to get connector: %v", err)
+		s.logger.Error("Failed to get connector", "err", err)
 		s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist")
 		return
 	}
 
 	// Set the connector being used for the login.
 	if authReq.ConnectorID != "" && authReq.ConnectorID != connID {
-		s.logger.Errorf("Mismatched connector ID in auth request: %s vs %s",
-			authReq.ConnectorID, connID)
+		s.logger.Error("mismatched connector ID in auth request",
+			"auth_request_connector_id", authReq.ConnectorID, "connector_id", connID)
 		s.renderError(r, w, http.StatusBadRequest, "Bad connector ID")
 		return
 	}
@@ -234,7 +234,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
 	// Actually create the auth request
 	authReq.Expiry = s.now().Add(s.authRequestsValidFor)
 	if err := s.storage.CreateAuthRequest(ctx, *authReq); err != nil {
-		s.logger.Errorf("Failed to create authorization request: %v", err)
+		s.logger.Error("failed to create authorization request", "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Failed to connect to the database.")
 		return
 	}
@@ -260,7 +260,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
 			// TODO(ericchiang): Is this appropriate or should we also be using a nonce?
 			callbackURL, err := conn.LoginURL(scopes, s.absURL("/callback"), authReq.ID)
 			if err != nil {
-				s.logger.Errorf("Connector %q returned error when creating callback: %v", connID, err)
+				s.logger.Error("connector returned error when creating callback", "connector_id", connID, "err", err)
 				s.renderError(r, w, http.StatusInternalServerError, "Login error.")
 				return
 			}
@@ -278,7 +278,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
 		case connector.SAMLConnector:
 			action, value, err := conn.POSTData(scopes, authReq.ID)
 			if err != nil {
-				s.logger.Errorf("Creating SAML data: %v", err)
+				s.logger.Error("creating SAML data", "err", err)
 				s.renderError(r, w, http.StatusInternalServerError, "Connector Login Error")
 				return
 			}
@@ -321,36 +321,36 @@ func (s *Server) handlePasswordLogin(w http.ResponseWriter, r *http.Request) {
 	authReq, err := s.storage.GetAuthRequest(authID)
 	if err != nil {
 		if err == storage.ErrNotFound {
-			s.logger.Errorf("Invalid 'state' parameter provided: %v", err)
+			s.logger.Error("invalid 'state' parameter provided", "err", err)
 			s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist.")
 			return
 		}
-		s.logger.Errorf("Failed to get auth request: %v", err)
+		s.logger.Error("failed to get auth request", "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Database error.")
 		return
 	}
 
 	connID, err := url.PathUnescape(mux.Vars(r)["connector"])
 	if err != nil {
-		s.logger.Errorf("Failed to parse connector: %v", err)
+		s.logger.Error("failed to parse connector", "err", err)
 		s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist")
 		return
 	} else if connID != "" && connID != authReq.ConnectorID {
-		s.logger.Errorf("Connector mismatch: authentication started with id %q, but password login for id %q was triggered", authReq.ConnectorID, connID)
+		s.logger.Error("connector mismatch: password login triggered for different connector from authentication start", "start_connector_id", authReq.ConnectorID, "password_connector_id", connID)
 		s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
 		return
 	}
 
 	conn, err := s.getConnector(authReq.ConnectorID)
 	if err != nil {
-		s.logger.Errorf("Failed to get connector with id %q : %v", authReq.ConnectorID, err)
+		s.logger.Error("failed to get connector", "connector_id", authReq.ConnectorID, "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
 		return
 	}
 
 	pwConn, ok := conn.Connector.(connector.PasswordConnector)
 	if !ok {
-		s.logger.Errorf("Expected password connector in handlePasswordLogin(), but got %v", pwConn)
+		s.logger.Error("expected password connector in handlePasswordLogin()", "password_connector", pwConn)
 		s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
 		return
 	}
@@ -358,7 +358,7 @@ func (s *Server) handlePasswordLogin(w http.ResponseWriter, r *http.Request) {
 	switch r.Method {
 	case http.MethodGet:
 		if err := s.templates.password(r, w, r.URL.String(), "", usernamePrompt(pwConn), false, backLink); err != nil {
-			s.logger.Errorf("Server template error: %v", err)
+			s.logger.Error("server template error", "err", err)
 		}
 	case http.MethodPost:
 		username := r.FormValue("login")
@@ -367,20 +367,20 @@ func (s *Server) handlePasswordLogin(w http.ResponseWriter, r *http.Request) {
 
 		identity, ok, err := pwConn.Login(ctx, scopes, username, password)
 		if err != nil {
-			s.logger.Errorf("Failed to login user: %v", err)
+			s.logger.Error("failed to login user", "err", err)
 			s.renderError(r, w, http.StatusInternalServerError, fmt.Sprintf("Login error: %v", err))
 			return
 		}
 		if !ok {
 			if err := s.templates.password(r, w, r.URL.String(), username, usernamePrompt(pwConn), true, backLink); err != nil {
-				s.logger.Errorf("Server template error: %v", err)
+				s.logger.Error("server template error", "err", err)
 			}
-			s.logger.Errorf("Failed login attempt for user: %q. Invalid credentials.", username)
+			s.logger.Error("failed login attempt: Invalid credentials.", "user", username)
 			return
 		}
 		redirectURL, canSkipApproval, err := s.finalizeLogin(ctx, identity, authReq, conn.Connector)
 		if err != nil {
-			s.logger.Errorf("Failed to finalize login: %v", err)
+			s.logger.Error("failed to finalize login", "err", err)
 			s.renderError(r, w, http.StatusInternalServerError, "Login error.")
 			return
 		}
@@ -388,7 +388,7 @@ func (s *Server) handlePasswordLogin(w http.ResponseWriter, r *http.Request) {
 		if canSkipApproval {
 			authReq, err = s.storage.GetAuthRequest(authReq.ID)
 			if err != nil {
-				s.logger.Errorf("Failed to get finalized auth request: %v", err)
+				s.logger.Error("failed to get finalized auth request", "err", err)
 				s.renderError(r, w, http.StatusInternalServerError, "Login error.")
 				return
 			}
@@ -424,29 +424,29 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
 	authReq, err := s.storage.GetAuthRequest(authID)
 	if err != nil {
 		if err == storage.ErrNotFound {
-			s.logger.Errorf("Invalid 'state' parameter provided: %v", err)
+			s.logger.Error("invalid 'state' parameter provided", "err", err)
 			s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist.")
 			return
 		}
-		s.logger.Errorf("Failed to get auth request: %v", err)
+		s.logger.Error("failed to get auth request", "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Database error.")
 		return
 	}
 
 	connID, err := url.PathUnescape(mux.Vars(r)["connector"])
 	if err != nil {
-		s.logger.Errorf("Failed to get connector with id %q : %v", authReq.ConnectorID, err)
+		s.logger.Error("failed to get connector", "connector_id", authReq.ConnectorID, "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
 		return
 	} else if connID != "" && connID != authReq.ConnectorID {
-		s.logger.Errorf("Connector mismatch: authentication started with id %q, but callback for id %q was triggered", authReq.ConnectorID, connID)
+		s.logger.Error("connector mismatch: callback triggered for different connector than authentication start", "authentication_start_connector_id", authReq.ConnectorID, "connector_id", connID)
 		s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
 		return
 	}
 
 	conn, err := s.getConnector(authReq.ConnectorID)
 	if err != nil {
-		s.logger.Errorf("Failed to get connector with id %q : %v", authReq.ConnectorID, err)
+		s.logger.Error("failed to get connector", "connector_id", authReq.ConnectorID, "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
 		return
 	}
@@ -455,14 +455,14 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
 	switch conn := conn.Connector.(type) {
 	case connector.CallbackConnector:
 		if r.Method != http.MethodGet {
-			s.logger.Errorf("SAML request mapped to OAuth2 connector")
+			s.logger.Error("SAML request mapped to OAuth2 connector")
 			s.renderError(r, w, http.StatusBadRequest, "Invalid request")
 			return
 		}
 		identity, err = conn.HandleCallback(parseScopes(authReq.Scopes), r)
 	case connector.SAMLConnector:
 		if r.Method != http.MethodPost {
-			s.logger.Errorf("OAuth2 request mapped to SAML connector")
+			s.logger.Error("OAuth2 request mapped to SAML connector")
 			s.renderError(r, w, http.StatusBadRequest, "Invalid request")
 			return
 		}
@@ -473,14 +473,14 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
 	}
 
 	if err != nil {
-		s.logger.Errorf("Failed to authenticate: %v", err)
+		s.logger.Error("failed to authenticate", "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, fmt.Sprintf("Failed to authenticate: %v", err))
 		return
 	}
 
 	redirectURL, canSkipApproval, err := s.finalizeLogin(ctx, identity, authReq, conn.Connector)
 	if err != nil {
-		s.logger.Errorf("Failed to finalize login: %v", err)
+		s.logger.Error("failed to finalize login", "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Login error.")
 		return
 	}
@@ -488,7 +488,7 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
 	if canSkipApproval {
 		authReq, err = s.storage.GetAuthRequest(authReq.ID)
 		if err != nil {
-			s.logger.Errorf("Failed to get finalized auth request: %v", err)
+			s.logger.Error("failed to get finalized auth request", "err", err)
 			s.renderError(r, w, http.StatusInternalServerError, "Login error.")
 			return
 		}
@@ -526,8 +526,9 @@ func (s *Server) finalizeLogin(ctx context.Context, identity connector.Identity,
 		email += " (unverified)"
 	}
 
-	s.logger.Infof("login successful: connector %q, username=%q, preferred_username=%q, email=%q, groups=%q",
-		authReq.ConnectorID, claims.Username, claims.PreferredUsername, email, claims.Groups)
+	s.logger.Info("login successful",
+		"connector_id", authReq.ConnectorID, "username", claims.Username,
+		"preferred_username", claims.PreferredUsername, "email", email, "groups", claims.Groups)
 
 	// we can skip the redirect to /approval and go ahead and send code if it's not required
 	if s.skipApproval && !authReq.ForceApprovalPrompt {
@@ -561,7 +562,7 @@ func (s *Server) finalizeLogin(ctx context.Context, identity connector.Identity,
 	session, err := s.storage.GetOfflineSessions(identity.UserID, authReq.ConnectorID)
 	if err != nil {
 		if err != storage.ErrNotFound {
-			s.logger.Errorf("failed to get offline session: %v", err)
+			s.logger.Error("failed to get offline session", "err", err)
 			return "", false, err
 		}
 		offlineSessions := storage.OfflineSessions{
@@ -574,7 +575,7 @@ func (s *Server) finalizeLogin(ctx context.Context, identity connector.Identity,
 		// Create a new OfflineSession object for the user and add a reference object for
 		// the newly received refreshtoken.
 		if err := s.storage.CreateOfflineSessions(ctx, offlineSessions); err != nil {
-			s.logger.Errorf("failed to create offline session: %v", err)
+			s.logger.Error("failed to create offline session", "err", err)
 			return "", false, err
 		}
 
@@ -588,7 +589,7 @@ func (s *Server) finalizeLogin(ctx context.Context, identity connector.Identity,
 		}
 		return old, nil
 	}); err != nil {
-		s.logger.Errorf("failed to update offline session: %v", err)
+		s.logger.Error("failed to update offline session", "err", err)
 		return "", false, err
 	}
 
@@ -609,12 +610,12 @@ func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
 
 	authReq, err := s.storage.GetAuthRequest(r.FormValue("req"))
 	if err != nil {
-		s.logger.Errorf("Failed to get auth request: %v", err)
+		s.logger.Error("failed to get auth request", "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Database error.")
 		return
 	}
 	if !authReq.LoggedIn {
-		s.logger.Errorf("Auth request does not have an identity for approval")
+		s.logger.Error("auth request does not have an identity for approval")
 		s.renderError(r, w, http.StatusInternalServerError, "Login process not yet finalized.")
 		return
 	}
@@ -633,12 +634,12 @@ func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
 	case http.MethodGet:
 		client, err := s.storage.GetClient(authReq.ClientID)
 		if err != nil {
-			s.logger.Errorf("Failed to get client %q: %v", authReq.ClientID, err)
+			s.logger.Error("Failed to get client", "client_id", authReq.ClientID, "err", err)
 			s.renderError(r, w, http.StatusInternalServerError, "Failed to retrieve client.")
 			return
 		}
 		if err := s.templates.approval(r, w, authReq.ID, authReq.Claims.Username, client.Name, authReq.Scopes); err != nil {
-			s.logger.Errorf("Server template error: %v", err)
+			s.logger.Error("server template error", "err", err)
 		}
 	case http.MethodPost:
 		if r.FormValue("approval") != "approve" {
@@ -658,7 +659,7 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
 
 	if err := s.storage.DeleteAuthRequest(authReq.ID); err != nil {
 		if err != storage.ErrNotFound {
-			s.logger.Errorf("Failed to delete authorization request: %v", err)
+			s.logger.Error("Failed to delete authorization request", "err", err)
 			s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
 		} else {
 			s.renderError(r, w, http.StatusBadRequest, "User session error.")
@@ -704,7 +705,7 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
 				PKCE:          authReq.PKCE,
 			}
 			if err := s.storage.CreateAuthCode(ctx, code); err != nil {
-				s.logger.Errorf("Failed to create auth code: %v", err)
+				s.logger.Error("Failed to create auth code", "err", err)
 				s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
 				return
 			}
@@ -713,7 +714,7 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
 			// rejected earlier. If we got here we're using the code flow.
 			if authReq.RedirectURI == redirectURIOOB {
 				if err := s.templates.oob(r, w, code.ID); err != nil {
-					s.logger.Errorf("Server template error: %v", err)
+					s.logger.Error("server template error", "err", err)
 				}
 				return
 			}
@@ -725,14 +726,14 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
 
 			accessToken, _, err = s.newAccessToken(authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce, authReq.ConnectorID)
 			if err != nil {
-				s.logger.Errorf("failed to create new access token: %v", err)
+				s.logger.Error("failed to create new access token", "err", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 				return
 			}
 
 			idToken, idTokenExpiry, err = s.newIDToken(authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce, accessToken, code.ID, authReq.ConnectorID)
 			if err != nil {
-				s.logger.Errorf("failed to create ID token: %v", err)
+				s.logger.Error("failed to create ID token", "err", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 				return
 			}
@@ -807,7 +808,7 @@ func (s *Server) withClientFromStorage(w http.ResponseWriter, r *http.Request, h
 	client, err := s.storage.GetClient(clientID)
 	if err != nil {
 		if err != storage.ErrNotFound {
-			s.logger.Errorf("failed to get client: %v", err)
+			s.logger.Error("failed to get client", "err", err)
 			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 		} else {
 			s.tokenErrHelper(w, errInvalidClient, "Invalid client credentials.", http.StatusUnauthorized)
@@ -817,9 +818,9 @@ func (s *Server) withClientFromStorage(w http.ResponseWriter, r *http.Request, h
 
 	if subtle.ConstantTimeCompare([]byte(client.Secret), []byte(clientSecret)) != 1 {
 		if clientSecret == "" {
-			s.logger.Infof("missing client_secret on token request for client: %s", client.ID)
+			s.logger.Info("missing client_secret on token request", "client_id", client.ID)
 		} else {
-			s.logger.Infof("invalid client_secret on token request for client: %s", client.ID)
+			s.logger.Info("invalid client_secret on token request", "client_id", client.ID)
 		}
 		s.tokenErrHelper(w, errInvalidClient, "Invalid client credentials.", http.StatusUnauthorized)
 		return
@@ -837,14 +838,14 @@ func (s *Server) handleToken(w http.ResponseWriter, r *http.Request) {
 
 	err := r.ParseForm()
 	if err != nil {
-		s.logger.Errorf("Could not parse request body: %v", err)
+		s.logger.Error("could not parse request body", "err", err)
 		s.tokenErrHelper(w, errInvalidRequest, "", http.StatusBadRequest)
 		return
 	}
 
 	grantType := r.PostFormValue("grant_type")
 	if !contains(s.supportedGrantTypes, grantType) {
-		s.logger.Errorf("unsupported grant type: %v", grantType)
+		s.logger.Error("unsupported grant type", "grant_type", grantType)
 		s.tokenErrHelper(w, errUnsupportedGrantType, "", http.StatusBadRequest)
 		return
 	}
@@ -890,7 +891,7 @@ func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client s
 	authCode, err := s.storage.GetAuthCode(code)
 	if err != nil || s.now().After(authCode.Expiry) || authCode.ClientID != client.ID {
 		if err != storage.ErrNotFound {
-			s.logger.Errorf("failed to get auth code: %v", err)
+			s.logger.Error("failed to get auth code", "err", err)
 			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 		} else {
 			s.tokenErrHelper(w, errInvalidGrant, "Invalid or expired code parameter.", http.StatusBadRequest)
@@ -906,7 +907,7 @@ func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client s
 	case providedCodeVerifier != "" && codeChallengeFromStorage != "":
 		calculatedCodeChallenge, err := s.calculateCodeChallenge(providedCodeVerifier, authCode.PKCE.CodeChallengeMethod)
 		if err != nil {
-			s.logger.Error(err)
+			s.logger.Error("failed to calculate code challenge", "err", err)
 			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 			return
 		}
@@ -940,20 +941,20 @@ func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client s
 func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, authCode storage.AuthCode, client storage.Client) (*accessTokenResponse, error) {
 	accessToken, _, err := s.newAccessToken(client.ID, authCode.Claims, authCode.Scopes, authCode.Nonce, authCode.ConnectorID)
 	if err != nil {
-		s.logger.Errorf("failed to create new access token: %v", err)
+		s.logger.Error("failed to create new access token", "err", err)
 		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 		return nil, err
 	}
 
 	idToken, expiry, err := s.newIDToken(client.ID, authCode.Claims, authCode.Scopes, authCode.Nonce, accessToken, authCode.ID, authCode.ConnectorID)
 	if err != nil {
-		s.logger.Errorf("failed to create ID token: %v", err)
+		s.logger.Error("failed to create ID token", "err", err)
 		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 		return nil, err
 	}
 
 	if err := s.storage.DeleteAuthCode(authCode.ID); err != nil {
-		s.logger.Errorf("failed to delete auth code: %v", err)
+		s.logger.Error("failed to delete auth code", "err", err)
 		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 		return nil, err
 	}
@@ -964,7 +965,7 @@ func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, au
 		// Connectors like `saml` do not implement RefreshConnector.
 		conn, err := s.getConnector(authCode.ConnectorID)
 		if err != nil {
-			s.logger.Errorf("connector with ID %q not found: %v", authCode.ConnectorID, err)
+			s.logger.Error("connector not found", "connector_id", authCode.ConnectorID, "err", err)
 			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 			return false
 		}
@@ -1000,13 +1001,13 @@ func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, au
 			Token:     refresh.Token,
 		}
 		if refreshToken, err = internal.Marshal(token); err != nil {
-			s.logger.Errorf("failed to marshal refresh token: %v", err)
+			s.logger.Error("failed to marshal refresh token", "err", err)
 			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 			return nil, err
 		}
 
 		if err := s.storage.CreateRefresh(ctx, refresh); err != nil {
-			s.logger.Errorf("failed to create refresh token: %v", err)
+			s.logger.Error("failed to create refresh token", "err", err)
 			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 			return nil, err
 		}
@@ -1019,7 +1020,7 @@ func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, au
 			if deleteToken {
 				// Delete newly created refresh token from storage.
 				if err := s.storage.DeleteRefresh(refresh.ID); err != nil {
-					s.logger.Errorf("failed to delete refresh token: %v", err)
+					s.logger.Error("failed to delete refresh token", "err", err)
 					s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 					return
 				}
@@ -1036,7 +1037,7 @@ func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, au
 		// Try to retrieve an existing OfflineSession object for the corresponding user.
 		if session, err := s.storage.GetOfflineSessions(refresh.Claims.UserID, refresh.ConnectorID); err != nil {
 			if err != storage.ErrNotFound {
-				s.logger.Errorf("failed to get offline session: %v", err)
+				s.logger.Error("failed to get offline session", "err", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 				deleteToken = true
 				return nil, err
@@ -1051,7 +1052,7 @@ func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, au
 			// Create a new OfflineSession object for the user and add a reference object for
 			// the newly received refreshtoken.
 			if err := s.storage.CreateOfflineSessions(ctx, offlineSessions); err != nil {
-				s.logger.Errorf("failed to create offline session: %v", err)
+				s.logger.Error("failed to create offline session", "err", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 				deleteToken = true
 				return nil, err
@@ -1060,7 +1061,7 @@ func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, au
 			if oldTokenRef, ok := session.Refresh[tokenRef.ClientID]; ok {
 				// Delete old refresh token from storage.
 				if err := s.storage.DeleteRefresh(oldTokenRef.ID); err != nil && err != storage.ErrNotFound {
-					s.logger.Errorf("failed to delete refresh token: %v", err)
+					s.logger.Error("failed to delete refresh token", "err", err)
 					s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 					deleteToken = true
 					return nil, err
@@ -1072,7 +1073,7 @@ func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, au
 				old.Refresh[tokenRef.ClientID] = &tokenRef
 				return old, nil
 			}); err != nil {
-				s.logger.Errorf("failed to update offline session: %v", err)
+				s.logger.Error("failed to update offline session", "err", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 				deleteToken = true
 				return nil, err
@@ -1184,7 +1185,7 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli
 	password := q.Get("password")
 	identity, ok, err := passwordConnector.Login(ctx, parseScopes(scopes), username, password)
 	if err != nil {
-		s.logger.Errorf("Failed to login user: %v", err)
+		s.logger.Error("failed to login user", "err", err)
 		s.tokenErrHelper(w, errInvalidRequest, "Could not login user", http.StatusBadRequest)
 		return
 	}
@@ -1205,14 +1206,14 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli
 
 	accessToken, _, err := s.newAccessToken(client.ID, claims, scopes, nonce, connID)
 	if err != nil {
-		s.logger.Errorf("password grant failed to create new access token: %v", err)
+		s.logger.Error("password grant failed to create new access token", "err", err)
 		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 		return
 	}
 
 	idToken, expiry, err := s.newIDToken(client.ID, claims, scopes, nonce, accessToken, "", connID)
 	if err != nil {
-		s.logger.Errorf("password grant failed to create new ID token: %v", err)
+		s.logger.Error("password grant failed to create new ID token", "err", err)
 		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 		return
 	}
@@ -1252,13 +1253,13 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli
 			Token:     refresh.Token,
 		}
 		if refreshToken, err = internal.Marshal(token); err != nil {
-			s.logger.Errorf("failed to marshal refresh token: %v", err)
+			s.logger.Error("failed to marshal refresh token", "err", err)
 			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 			return
 		}
 
 		if err := s.storage.CreateRefresh(ctx, refresh); err != nil {
-			s.logger.Errorf("failed to create refresh token: %v", err)
+			s.logger.Error("failed to create refresh token", "err", err)
 			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 			return
 		}
@@ -1271,7 +1272,7 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli
 			if deleteToken {
 				// Delete newly created refresh token from storage.
 				if err := s.storage.DeleteRefresh(refresh.ID); err != nil {
-					s.logger.Errorf("failed to delete refresh token: %v", err)
+					s.logger.Error("failed to delete refresh token", "err", err)
 					s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 					return
 				}
@@ -1288,7 +1289,7 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli
 		// Try to retrieve an existing OfflineSession object for the corresponding user.
 		if session, err := s.storage.GetOfflineSessions(refresh.Claims.UserID, refresh.ConnectorID); err != nil {
 			if err != storage.ErrNotFound {
-				s.logger.Errorf("failed to get offline session: %v", err)
+				s.logger.Error("failed to get offline session", "err", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 				deleteToken = true
 				return
@@ -1304,7 +1305,7 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli
 			// Create a new OfflineSession object for the user and add a reference object for
 			// the newly received refreshtoken.
 			if err := s.storage.CreateOfflineSessions(ctx, offlineSessions); err != nil {
-				s.logger.Errorf("failed to create offline session: %v", err)
+				s.logger.Error("failed to create offline session", "err", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 				deleteToken = true
 				return
@@ -1314,9 +1315,9 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli
 				// Delete old refresh token from storage.
 				if err := s.storage.DeleteRefresh(oldTokenRef.ID); err != nil {
 					if err == storage.ErrNotFound {
-						s.logger.Warnf("database inconsistent, refresh token missing: %v", oldTokenRef.ID)
+						s.logger.Warn("database inconsistent, refresh token missing", "token_id", oldTokenRef.ID)
 					} else {
-						s.logger.Errorf("failed to delete refresh token: %v", err)
+						s.logger.Error("failed to delete refresh token", "err", err)
 						s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 						deleteToken = true
 						return
@@ -1330,7 +1331,7 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli
 				old.ConnectorData = identity.ConnectorData
 				return old, nil
 			}); err != nil {
-				s.logger.Errorf("failed to update offline session: %v", err)
+				s.logger.Error("failed to update offline session", "err", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 				deleteToken = true
 				return
@@ -1346,7 +1347,7 @@ func (s *Server) handleTokenExchange(w http.ResponseWriter, r *http.Request, cli
 	ctx := r.Context()
 
 	if err := r.ParseForm(); err != nil {
-		s.logger.Errorf("could not parse request body: %v", err)
+		s.logger.Error("could not parse request body", "err", err)
 		s.tokenErrHelper(w, errInvalidRequest, "", http.StatusBadRequest)
 		return
 	}
@@ -1375,19 +1376,19 @@ func (s *Server) handleTokenExchange(w http.ResponseWriter, r *http.Request, cli
 
 	conn, err := s.getConnector(connID)
 	if err != nil {
-		s.logger.Errorf("failed to get connector: %v", err)
+		s.logger.Error("failed to get connector", "err", err)
 		s.tokenErrHelper(w, errInvalidRequest, "Requested connector does not exist.", http.StatusBadRequest)
 		return
 	}
 	teConn, ok := conn.Connector.(connector.TokenIdentityConnector)
 	if !ok {
-		s.logger.Errorf("connector doesn't implement token exchange: %v", connID)
+		s.logger.Error("connector doesn't implement token exchange", "connector_id", connID)
 		s.tokenErrHelper(w, errInvalidRequest, "Requested connector does not exist.", http.StatusBadRequest)
 		return
 	}
 	identity, err := teConn.TokenIdentity(ctx, subjectTokenType, subjectToken)
 	if err != nil {
-		s.logger.Errorf("failed to verify subject token: %v", err)
+		s.logger.Error("failed to verify subject token", "err", err)
 		s.tokenErrHelper(w, errAccessDenied, "", http.StatusUnauthorized)
 		return
 	}
@@ -1415,7 +1416,7 @@ func (s *Server) handleTokenExchange(w http.ResponseWriter, r *http.Request, cli
 		return
 	}
 	if err != nil {
-		s.logger.Errorf("token exchange failed to create new %v token: %v", requestedTokenType, err)
+		s.logger.Error("token exchange failed to create new token", "requested_token_type", requestedTokenType, "err", err)
 		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 		return
 	}
@@ -1451,7 +1452,7 @@ func (s *Server) toAccessTokenResponse(idToken, accessToken, refreshToken string
 func (s *Server) writeAccessToken(w http.ResponseWriter, resp *accessTokenResponse) {
 	data, err := json.Marshal(resp)
 	if err != nil {
-		s.logger.Errorf("failed to marshal access token response: %v", err)
+		s.logger.Error("failed to marshal access token response", "err", err)
 		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 		return
 	}
@@ -1466,13 +1467,13 @@ func (s *Server) writeAccessToken(w http.ResponseWriter, resp *accessTokenRespon
 
 func (s *Server) renderError(r *http.Request, w http.ResponseWriter, status int, description string) {
 	if err := s.templates.err(r, w, status, description); err != nil {
-		s.logger.Errorf("server template error: %v", err)
+		s.logger.Error("server template error", "err", err)
 	}
 }
 
 func (s *Server) tokenErrHelper(w http.ResponseWriter, typ string, description string, statusCode int) {
 	if err := tokenErr(w, typ, description, statusCode); err != nil {
-		s.logger.Errorf("token error response: %v", err)
+		s.logger.Error("token error response", "err", err)
 	}
 }
 
diff --git a/server/introspectionhandler.go b/server/introspectionhandler.go
index f0d1f807..8c6e4419 100644
--- a/server/introspectionhandler.go
+++ b/server/introspectionhandler.go
@@ -179,14 +179,14 @@ func (s *Server) getTokenFromRequest(r *http.Request) (string, TokenTypeEnum, er
 	token := r.PostForm.Get("token")
 	tokenType, err := s.guessTokenType(r.Context(), token)
 	if err != nil {
-		s.logger.Error(err)
+		s.logger.Error("failed to guess token type", "err", err)
 		return "", 0, newIntrospectInternalServerError()
 	}
 
 	requestTokenType := r.PostForm.Get("token_type_hint")
 	if requestTokenType != "" {
 		if tokenType.String() != requestTokenType {
-			s.logger.Warnf("Token type hint doesn't match token type: %s != %s", requestTokenType, tokenType)
+			s.logger.Warn("token type hint doesn't match token type", "request_token_type", requestTokenType, "token_type", tokenType)
 		}
 	}
 
@@ -211,13 +211,13 @@ func (s *Server) introspectRefreshToken(_ context.Context, token string) (*Intro
 			return nil, newIntrospectInactiveTokenError()
 		}
 
-		s.logger.Errorf("failed to get refresh token: %v", err)
+		s.logger.Error("failed to get refresh token", "err", err)
 		return nil, newIntrospectInternalServerError()
 	}
 
 	subjectString, sErr := genSubject(rCtx.storageToken.Claims.UserID, rCtx.storageToken.ConnectorID)
 	if sErr != nil {
-		s.logger.Errorf("failed to marshal offline session ID: %v", err)
+		s.logger.Error("failed to marshal offline session ID", "err", err)
 		return nil, newIntrospectInternalServerError()
 	}
 
@@ -253,19 +253,19 @@ func (s *Server) introspectAccessToken(ctx context.Context, token string) (*Intr
 
 	var claims IntrospectionExtra
 	if err := idToken.Claims(&claims); err != nil {
-		s.logger.Errorf("Error while fetching token claims: %s", err.Error())
+		s.logger.Error("error while fetching token claims", "err", err.Error())
 		return nil, newIntrospectInternalServerError()
 	}
 
 	clientID, err := getClientID(idToken.Audience, claims.AuthorizingParty)
 	if err != nil {
-		s.logger.Error("Error while fetching client_id from token: %s", err.Error())
+		s.logger.Error("error while fetching client_id from token:", "err", err.Error())
 		return nil, newIntrospectInternalServerError()
 	}
 
 	client, err := s.storage.GetClient(clientID)
 	if err != nil {
-		s.logger.Error("Error while fetching client from storage: %s", err.Error())
+		s.logger.Error("error while fetching client from storage", "err", err.Error())
 		return nil, newIntrospectInternalServerError()
 	}
 
@@ -299,7 +299,7 @@ func (s *Server) handleIntrospect(w http.ResponseWriter, r *http.Request) {
 			introspect, err = s.introspectRefreshToken(ctx, token)
 		default:
 			// Token type is neither handled token types.
-			s.logger.Errorf("Unknown token type: %s", tokenType)
+			s.logger.Error("unknown token type", "token_type", tokenType)
 			introspectInactiveErr(w)
 			return
 		}
@@ -309,7 +309,7 @@ func (s *Server) handleIntrospect(w http.ResponseWriter, r *http.Request) {
 		if intErr, ok := err.(*introspectionError); ok {
 			s.introspectErrHelper(w, intErr.typ, intErr.desc, intErr.code)
 		} else {
-			s.logger.Errorf("An unknown error occurred: %s", err.Error())
+			s.logger.Error("an unknown error occurred", "err", err.Error())
 			s.introspectErrHelper(w, errServerError, "An unknown error occurred", http.StatusInternalServerError)
 		}
 
@@ -332,7 +332,7 @@ func (s *Server) introspectErrHelper(w http.ResponseWriter, typ string, descript
 	}
 
 	if err := tokenErr(w, typ, description, statusCode); err != nil {
-		s.logger.Errorf("introspect error response: %v", err)
+		s.logger.Error("introspect error response", "err", err)
 	}
 }
 
diff --git a/server/oauth2.go b/server/oauth2.go
index 3589e493..3d9cfc8f 100644
--- a/server/oauth2.go
+++ b/server/oauth2.go
@@ -353,7 +353,7 @@ func genSubject(userID string, connID string) (string, error) {
 func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []string, nonce, accessToken, code, connID string) (idToken string, expiry time.Time, err error) {
 	keys, err := s.storage.GetKeys()
 	if err != nil {
-		s.logger.Errorf("Failed to get keys: %v", err)
+		s.logger.Error("failed to get keys", "err", err)
 		return "", expiry, err
 	}
 
@@ -371,7 +371,7 @@ func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []str
 
 	subjectString, err := genSubject(claims.UserID, connID)
 	if err != nil {
-		s.logger.Errorf("failed to marshal offline session ID: %v", err)
+		s.logger.Error("failed to marshal offline session ID", "err", err)
 		return "", expiry, fmt.Errorf("failed to marshal offline session ID: %v", err)
 	}
 
@@ -386,7 +386,7 @@ func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []str
 	if accessToken != "" {
 		atHash, err := accessTokenHash(signingAlg, accessToken)
 		if err != nil {
-			s.logger.Errorf("error computing at_hash: %v", err)
+			s.logger.Error("error computing at_hash", "err", err)
 			return "", expiry, fmt.Errorf("error computing at_hash: %v", err)
 		}
 		tok.AccessTokenHash = atHash
@@ -395,7 +395,7 @@ func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []str
 	if code != "" {
 		cHash, err := accessTokenHash(signingAlg, code)
 		if err != nil {
-			s.logger.Errorf("error computing c_hash: %v", err)
+			s.logger.Error("error computing c_hash", "err", err)
 			return "", expiry, fmt.Errorf("error computing c_hash: #{err}")
 		}
 		tok.CodeHash = cHash
@@ -482,7 +482,7 @@ func (s *Server) parseAuthorizationRequest(r *http.Request) (*storage.AuthReques
 		if err == storage.ErrNotFound {
 			return nil, newDisplayedErr(http.StatusNotFound, "Invalid client_id (%q).", clientID)
 		}
-		s.logger.Errorf("Failed to get client: %v", err)
+		s.logger.Error("failed to get client", "err", err)
 		return nil, newDisplayedErr(http.StatusInternalServerError, "Database error.")
 	}
 
@@ -501,7 +501,7 @@ func (s *Server) parseAuthorizationRequest(r *http.Request) (*storage.AuthReques
 	if connectorID != "" {
 		connectors, err := s.storage.ListConnectors()
 		if err != nil {
-			s.logger.Errorf("Failed to list connectors: %v", err)
+			s.logger.Error("failed to list connectors", "err", err)
 			return nil, newRedirectedErr(errServerError, "Unable to retrieve connectors")
 		}
 		if !validateConnectorID(connectors, connectorID) {
@@ -637,7 +637,7 @@ func (s *Server) validateCrossClientTrust(clientID, peerID string) (trusted bool
 	peer, err := s.storage.GetClient(peerID)
 	if err != nil {
 		if err != storage.ErrNotFound {
-			s.logger.Errorf("Failed to get client: %v", err)
+			s.logger.Error("failed to get client", "err", err)
 			return false, err
 		}
 		return false, nil
diff --git a/server/refreshhandlers.go b/server/refreshhandlers.go
index cb53802b..01a0f435 100644
--- a/server/refreshhandlers.go
+++ b/server/refreshhandlers.go
@@ -87,7 +87,7 @@ func (s *Server) getRefreshTokenFromStorage(clientID *string, token *internal.Re
 	refresh, err := s.storage.GetRefresh(token.RefreshId)
 	if err != nil {
 		if err != storage.ErrNotFound {
-			s.logger.Errorf("failed to get refresh token: %v", err)
+			s.logger.Error("failed to get refresh token", "err", err)
 			return nil, newInternalServerError()
 		}
 		return nil, invalidErr
@@ -95,7 +95,7 @@ func (s *Server) getRefreshTokenFromStorage(clientID *string, token *internal.Re
 
 	// Only check ClientID if it was provided;
 	if clientID != nil && (refresh.ClientID != *clientID) {
-		s.logger.Errorf("client %s trying to claim token for client %s", clientID, refresh.ClientID)
+		s.logger.Error("trying to claim token for different client", "client_id", clientID, "refresh_client_id", refresh.ClientID)
 		// According to https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 Dex should respond with an
 		//  invalid grant error if token has already been claimed by another client.
 		return nil, &refreshError{msg: errInvalidGrant, desc: invalidErr.desc, code: http.StatusBadRequest}
@@ -108,18 +108,18 @@ func (s *Server) getRefreshTokenFromStorage(clientID *string, token *internal.Re
 		case refresh.ObsoleteToken != token.Token:
 			fallthrough
 		case refresh.ObsoleteToken == "":
-			s.logger.Errorf("refresh token with id %s claimed twice", refresh.ID)
+			s.logger.Error("refresh token claimed twice", "token_id", refresh.ID)
 			return nil, invalidErr
 		}
 	}
 
 	if s.refreshTokenPolicy.CompletelyExpired(refresh.CreatedAt) {
-		s.logger.Errorf("refresh token with id %s expired", refresh.ID)
+		s.logger.Error("refresh token expired", "token_id", refresh.ID)
 		return nil, expiredErr
 	}
 
 	if s.refreshTokenPolicy.ExpiredBecauseUnused(refresh.LastUsed) {
-		s.logger.Errorf("refresh token with id %s expired due to inactivity", refresh.ID)
+		s.logger.Error("refresh token expired due to inactivity", "token_id", refresh.ID)
 		return nil, expiredErr
 	}
 
@@ -128,7 +128,7 @@ func (s *Server) getRefreshTokenFromStorage(clientID *string, token *internal.Re
 	// Get Connector
 	refreshCtx.connector, err = s.getConnector(refresh.ConnectorID)
 	if err != nil {
-		s.logger.Errorf("connector with ID %q not found: %v", refresh.ConnectorID, err)
+		s.logger.Error("connector not found", "connector_id", refresh.ConnectorID, "err", err)
 		return nil, newInternalServerError()
 	}
 
@@ -137,7 +137,7 @@ func (s *Server) getRefreshTokenFromStorage(clientID *string, token *internal.Re
 	switch {
 	case err != nil:
 		if err != storage.ErrNotFound {
-			s.logger.Errorf("failed to get offline session: %v", err)
+			s.logger.Error("failed to get offline session", "err", err)
 			return nil, newInternalServerError()
 		}
 	case len(refresh.ConnectorData) > 0:
@@ -191,11 +191,11 @@ func (s *Server) refreshWithConnector(ctx context.Context, rCtx *refreshContext,
 	if refreshConn, ok := rCtx.connector.Connector.(connector.RefreshConnector); ok {
 		// Set connector data to the one received from an offline session
 		ident.ConnectorData = rCtx.connectorData
-		s.logger.Debugf("connector data before refresh: %s", ident.ConnectorData)
+		s.logger.Debug("connector data before refresh", "connector_data", ident.ConnectorData)
 
 		newIdent, err := refreshConn.Refresh(ctx, parseScopes(rCtx.scopes), ident)
 		if err != nil {
-			s.logger.Errorf("failed to refresh identity: %v", err)
+			s.logger.Error("failed to refresh identity", "err", err)
 			return ident, newInternalServerError()
 		}
 
@@ -216,7 +216,7 @@ func (s *Server) updateOfflineSession(refresh *storage.RefreshToken, ident conne
 			old.ConnectorData = ident.ConnectorData
 		}
 
-		s.logger.Debugf("saved connector data: %s %s", ident.UserID, ident.ConnectorData)
+		s.logger.Debug("saved connector data", "user_id", ident.UserID, "connector_data", ident.ConnectorData)
 
 		return old, nil
 	}
@@ -225,7 +225,7 @@ func (s *Server) updateOfflineSession(refresh *storage.RefreshToken, ident conne
 	// in offline session for the user.
 	err := s.storage.UpdateOfflineSessions(refresh.Claims.UserID, refresh.ConnectorID, offlineSessionUpdater)
 	if err != nil {
-		s.logger.Errorf("failed to update offline session: %v", err)
+		s.logger.Error("failed to update offline session", "err", err)
 		return newInternalServerError()
 	}
 
@@ -316,7 +316,7 @@ func (s *Server) updateRefreshToken(ctx context.Context, rCtx *refreshContext) (
 	// Update refresh token in the storage.
 	err := s.storage.UpdateRefreshToken(rCtx.storageToken.ID, refreshTokenUpdater)
 	if err != nil {
-		s.logger.Errorf("failed to update refresh token: %v", err)
+		s.logger.Error("failed to update refresh token", "err", err)
 		return nil, ident, newInternalServerError()
 	}
 
@@ -366,21 +366,21 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie
 
 	accessToken, _, err := s.newAccessToken(client.ID, claims, rCtx.scopes, rCtx.storageToken.Nonce, rCtx.storageToken.ConnectorID)
 	if err != nil {
-		s.logger.Errorf("failed to create new access token: %v", err)
+		s.logger.Error("failed to create new access token", "err", err)
 		s.refreshTokenErrHelper(w, newInternalServerError())
 		return
 	}
 
 	idToken, expiry, err := s.newIDToken(client.ID, claims, rCtx.scopes, rCtx.storageToken.Nonce, accessToken, "", rCtx.storageToken.ConnectorID)
 	if err != nil {
-		s.logger.Errorf("failed to create ID token: %v", err)
+		s.logger.Error("failed to create ID token", "err", err)
 		s.refreshTokenErrHelper(w, newInternalServerError())
 		return
 	}
 
 	rawNewToken, err := internal.Marshal(newToken)
 	if err != nil {
-		s.logger.Errorf("failed to marshal refresh token: %v", err)
+		s.logger.Error("failed to marshal refresh token", "err", err)
 		s.refreshTokenErrHelper(w, newInternalServerError())
 		return
 	}
diff --git a/server/rotation.go b/server/rotation.go
index 77a9926e..dfd776d6 100644
--- a/server/rotation.go
+++ b/server/rotation.go
@@ -8,11 +8,11 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"log/slog"
 	"time"
 
 	"github.com/go-jose/go-jose/v4"
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 )
 
@@ -61,7 +61,7 @@ type keyRotator struct {
 	strategy rotationStrategy
 	now      func() time.Time
 
-	logger log.Logger
+	logger *slog.Logger
 }
 
 // startKeyRotation begins key rotation in a new goroutine, closing once the context is canceled.
@@ -74,9 +74,9 @@ func (s *Server) startKeyRotation(ctx context.Context, strategy rotationStrategy
 	// Try to rotate immediately so properly configured storages will have keys.
 	if err := rotator.rotate(); err != nil {
 		if err == errAlreadyRotated {
-			s.logger.Infof("Key rotation not needed: %v", err)
+			s.logger.Info("key rotation not needed", "err", err)
 		} else {
-			s.logger.Errorf("failed to rotate keys: %v", err)
+			s.logger.Error("failed to rotate keys", "err", err)
 		}
 	}
 
@@ -87,7 +87,7 @@ func (s *Server) startKeyRotation(ctx context.Context, strategy rotationStrategy
 				return
 			case <-time.After(time.Second * 30):
 				if err := rotator.rotate(); err != nil {
-					s.logger.Errorf("failed to rotate keys: %v", err)
+					s.logger.Error("failed to rotate keys", "err", err)
 				}
 			}
 		}
@@ -102,7 +102,7 @@ func (k keyRotator) rotate() error {
 	if k.now().Before(keys.NextRotation) {
 		return nil
 	}
-	k.logger.Infof("keys expired, rotating")
+	k.logger.Info("keys expired, rotating")
 
 	// Generate the key outside of a storage transaction.
 	key, err := k.strategy.key()
@@ -174,7 +174,7 @@ func (k keyRotator) rotate() error {
 	if err != nil {
 		return err
 	}
-	k.logger.Infof("keys rotated, next rotation: %s", nextRotation)
+	k.logger.Info("keys rotated", "next_rotation", nextRotation)
 	return nil
 }
 
@@ -187,10 +187,10 @@ type RefreshTokenPolicy struct {
 
 	now func() time.Time
 
-	logger log.Logger
+	logger *slog.Logger
 }
 
-func NewRefreshTokenPolicy(logger log.Logger, rotation bool, validIfNotUsedFor, absoluteLifetime, reuseInterval string) (*RefreshTokenPolicy, error) {
+func NewRefreshTokenPolicy(logger *slog.Logger, rotation bool, validIfNotUsedFor, absoluteLifetime, reuseInterval string) (*RefreshTokenPolicy, error) {
 	r := RefreshTokenPolicy{now: time.Now, logger: logger}
 	var err error
 
@@ -199,7 +199,7 @@ func NewRefreshTokenPolicy(logger log.Logger, rotation bool, validIfNotUsedFor,
 		if err != nil {
 			return nil, fmt.Errorf("invalid config value %q for refresh token valid if not used for: %v", validIfNotUsedFor, err)
 		}
-		logger.Infof("config refresh tokens valid if not used for: %v", validIfNotUsedFor)
+		logger.Info("config refresh tokens", "valid_if_not_used_for", validIfNotUsedFor)
 	}
 
 	if absoluteLifetime != "" {
@@ -207,7 +207,7 @@ func NewRefreshTokenPolicy(logger log.Logger, rotation bool, validIfNotUsedFor,
 		if err != nil {
 			return nil, fmt.Errorf("invalid config value %q for refresh tokens absolute lifetime: %v", absoluteLifetime, err)
 		}
-		logger.Infof("config refresh tokens absolute lifetime: %v", absoluteLifetime)
+		logger.Info("config refresh tokens", "absolute_lifetime", absoluteLifetime)
 	}
 
 	if reuseInterval != "" {
@@ -215,11 +215,11 @@ func NewRefreshTokenPolicy(logger log.Logger, rotation bool, validIfNotUsedFor,
 		if err != nil {
 			return nil, fmt.Errorf("invalid config value %q for refresh tokens reuse interval: %v", reuseInterval, err)
 		}
-		logger.Infof("config refresh tokens reuse interval: %v", reuseInterval)
+		logger.Info("config refresh tokens", "reuse_interval", reuseInterval)
 	}
 
 	r.rotateRefreshTokens = !rotation
-	logger.Infof("config refresh tokens rotation enabled: %v", r.rotateRefreshTokens)
+	logger.Info("config refresh tokens rotation", "enabled", r.rotateRefreshTokens)
 	return &r, nil
 }
 
diff --git a/server/server.go b/server/server.go
index dddbb137..5c1a97b8 100644
--- a/server/server.go
+++ b/server/server.go
@@ -7,6 +7,7 @@ import (
 	"errors"
 	"fmt"
 	"io/fs"
+	"log/slog"
 	"net/http"
 	"net/url"
 	"os"
@@ -42,7 +43,6 @@ import (
 	"github.com/dexidp/dex/connector/oidc"
 	"github.com/dexidp/dex/connector/openshift"
 	"github.com/dexidp/dex/connector/saml"
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/web"
 )
@@ -108,7 +108,7 @@ type Config struct {
 
 	Web WebConfig
 
-	Logger log.Logger
+	Logger *slog.Logger
 
 	PrometheusRegistry *prometheus.Registry
 
@@ -189,7 +189,7 @@ type Server struct {
 
 	refreshTokenPolicy *RefreshTokenPolicy
 
-	logger log.Logger
+	logger *slog.Logger
 }
 
 // NewServer constructs a server from the provided config.
@@ -556,10 +556,11 @@ func (s *Server) startGarbageCollection(ctx context.Context, frequency time.Dura
 				return
 			case <-time.After(frequency):
 				if r, err := s.storage.GarbageCollect(now()); err != nil {
-					s.logger.Errorf("garbage collection failed: %v", err)
+					s.logger.ErrorContext(ctx, "garbage collection failed", "err", err)
 				} else if !r.IsEmpty() {
-					s.logger.Infof("garbage collection run, delete auth requests=%d, auth codes=%d, device requests=%d, device tokens=%d",
-						r.AuthRequests, r.AuthCodes, r.DeviceRequests, r.DeviceTokens)
+					s.logger.InfoContext(ctx, "garbage collection run, delete auth",
+						"requests", r.AuthRequests, "auth_codes", r.AuthCodes,
+						"device_requests", r.DeviceRequests, "device_tokens", r.DeviceTokens)
 				}
 			}
 		}
@@ -568,7 +569,7 @@ func (s *Server) startGarbageCollection(ctx context.Context, frequency time.Dura
 
 // ConnectorConfig is a configuration that can open a connector.
 type ConnectorConfig interface {
-	Open(id string, logger log.Logger) (connector.Connector, error)
+	Open(id string, logger *slog.Logger) (connector.Connector, error)
 }
 
 // ConnectorsConfig variable provides an easy way to return a config struct
@@ -596,7 +597,7 @@ var ConnectorsConfig = map[string]func() ConnectorConfig{
 }
 
 // openConnector will parse the connector config and open the connector.
-func openConnector(logger log.Logger, conn storage.Connector) (connector.Connector, error) {
+func openConnector(logger *slog.Logger, conn storage.Connector) (connector.Connector, error) {
 	var c connector.Connector
 
 	f, ok := ConnectorsConfig[conn.Type]
diff --git a/storage/ent/mysql.go b/storage/ent/mysql.go
index 4a9407f9..008f7bad 100644
--- a/storage/ent/mysql.go
+++ b/storage/ent/mysql.go
@@ -7,6 +7,7 @@ import (
 	"crypto/x509"
 	"database/sql"
 	"fmt"
+	"log/slog"
 	"net"
 	"os"
 	"strconv"
@@ -15,7 +16,6 @@ import (
 	entSQL "entgo.io/ent/dialect/sql"
 	"github.com/go-sql-driver/mysql" // Register mysql driver.
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/ent/client"
 	"github.com/dexidp/dex/storage/ent/db"
@@ -39,7 +39,7 @@ type MySQL struct {
 }
 
 // Open always returns a new in sqlite3 storage.
-func (m *MySQL) Open(logger log.Logger) (storage.Storage, error) {
+func (m *MySQL) Open(logger *slog.Logger) (storage.Storage, error) {
 	logger.Debug("experimental ent-based storage driver is enabled")
 	drv, err := m.driver()
 	if err != nil {
diff --git a/storage/ent/postgres.go b/storage/ent/postgres.go
index ac091e70..dad81df4 100644
--- a/storage/ent/postgres.go
+++ b/storage/ent/postgres.go
@@ -5,6 +5,7 @@ import (
 	"crypto/sha256"
 	"database/sql"
 	"fmt"
+	"log/slog"
 	"net"
 	"regexp"
 	"strconv"
@@ -14,13 +15,11 @@ import (
 	entSQL "entgo.io/ent/dialect/sql"
 	_ "github.com/lib/pq" // Register postgres driver.
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/ent/client"
 	"github.com/dexidp/dex/storage/ent/db"
 )
 
-//nolint
 const (
 	// postgres SSL modes
 	pgSSLDisable    = "disable"
@@ -37,7 +36,7 @@ type Postgres struct {
 }
 
 // Open always returns a new in sqlite3 storage.
-func (p *Postgres) Open(logger log.Logger) (storage.Storage, error) {
+func (p *Postgres) Open(logger *slog.Logger) (storage.Storage, error) {
 	logger.Debug("experimental ent-based storage driver is enabled")
 	drv, err := p.driver()
 	if err != nil {
diff --git a/storage/ent/sqlite.go b/storage/ent/sqlite.go
index c0b442f4..8c5287ef 100644
--- a/storage/ent/sqlite.go
+++ b/storage/ent/sqlite.go
@@ -3,12 +3,12 @@ package ent
 import (
 	"context"
 	"crypto/sha256"
+	"log/slog"
 	"strings"
 
 	"entgo.io/ent/dialect/sql"
 	_ "github.com/mattn/go-sqlite3" // Register sqlite driver.
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/ent/client"
 	"github.com/dexidp/dex/storage/ent/db"
@@ -20,7 +20,7 @@ type SQLite3 struct {
 }
 
 // Open always returns a new in sqlite3 storage.
-func (s *SQLite3) Open(logger log.Logger) (storage.Storage, error) {
+func (s *SQLite3) Open(logger *slog.Logger) (storage.Storage, error) {
 	logger.Debug("experimental ent-based storage driver is enabled")
 
 	// Implicitly set foreign_keys pragma to "on" because it is required by ent
diff --git a/storage/etcd/config.go b/storage/etcd/config.go
index 7f1a7b4f..a8aee39a 100644
--- a/storage/etcd/config.go
+++ b/storage/etcd/config.go
@@ -1,13 +1,13 @@
 package etcd
 
 import (
+	"log/slog"
 	"time"
 
 	"go.etcd.io/etcd/client/pkg/v3/transport"
 	clientv3 "go.etcd.io/etcd/client/v3"
 	"go.etcd.io/etcd/client/v3/namespace"
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 )
 
@@ -34,11 +34,11 @@ type Etcd struct {
 }
 
 // Open creates a new storage implementation backed by Etcd
-func (p *Etcd) Open(logger log.Logger) (storage.Storage, error) {
+func (p *Etcd) Open(logger *slog.Logger) (storage.Storage, error) {
 	return p.open(logger)
 }
 
-func (p *Etcd) open(logger log.Logger) (*conn, error) {
+func (p *Etcd) open(logger *slog.Logger) (*conn, error) {
 	cfg := clientv3.Config{
 		Endpoints:   p.Endpoints,
 		DialTimeout: defaultDialTimeout,
diff --git a/storage/etcd/etcd.go b/storage/etcd/etcd.go
index e4b24b4a..f65701ff 100644
--- a/storage/etcd/etcd.go
+++ b/storage/etcd/etcd.go
@@ -4,12 +4,12 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"log/slog"
 	"strings"
 	"time"
 
 	clientv3 "go.etcd.io/etcd/client/v3"
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 )
 
@@ -33,7 +33,7 @@ var _ storage.Storage = (*conn)(nil)
 
 type conn struct {
 	db     *clientv3.Client
-	logger log.Logger
+	logger *slog.Logger
 }
 
 func (c *conn) Close() error {
@@ -52,7 +52,7 @@ func (c *conn) GarbageCollect(now time.Time) (result storage.GCResult, err error
 	for _, authRequest := range authRequests {
 		if now.After(authRequest.Expiry) {
 			if err := c.deleteKey(ctx, keyID(authRequestPrefix, authRequest.ID)); err != nil {
-				c.logger.Errorf("failed to delete auth request: %v", err)
+				c.logger.Error("failed to delete auth request", "err", err)
 				delErr = fmt.Errorf("failed to delete auth request: %v", err)
 			}
 			result.AuthRequests++
@@ -70,7 +70,7 @@ func (c *conn) GarbageCollect(now time.Time) (result storage.GCResult, err error
 	for _, authCode := range authCodes {
 		if now.After(authCode.Expiry) {
 			if err := c.deleteKey(ctx, keyID(authCodePrefix, authCode.ID)); err != nil {
-				c.logger.Errorf("failed to delete auth code %v", err)
+				c.logger.Error("failed to delete auth code", "err", err)
 				delErr = fmt.Errorf("failed to delete auth code: %v", err)
 			}
 			result.AuthCodes++
@@ -85,7 +85,7 @@ func (c *conn) GarbageCollect(now time.Time) (result storage.GCResult, err error
 	for _, deviceRequest := range deviceRequests {
 		if now.After(deviceRequest.Expiry) {
 			if err := c.deleteKey(ctx, keyID(deviceRequestPrefix, deviceRequest.UserCode)); err != nil {
-				c.logger.Errorf("failed to delete device request %v", err)
+				c.logger.Error("failed to delete device request", "err", err)
 				delErr = fmt.Errorf("failed to delete device request: %v", err)
 			}
 			result.DeviceRequests++
@@ -100,7 +100,7 @@ func (c *conn) GarbageCollect(now time.Time) (result storage.GCResult, err error
 	for _, deviceToken := range deviceTokens {
 		if now.After(deviceToken.Expiry) {
 			if err := c.deleteKey(ctx, keyID(deviceTokenPrefix, deviceToken.DeviceCode)); err != nil {
-				c.logger.Errorf("failed to delete device token %v", err)
+				c.logger.Error("failed to delete device token", "err", err)
 				delErr = fmt.Errorf("failed to delete device token: %v", err)
 			}
 			result.DeviceTokens++
diff --git a/storage/kubernetes/client.go b/storage/kubernetes/client.go
index af0a2338..1a1653b3 100644
--- a/storage/kubernetes/client.go
+++ b/storage/kubernetes/client.go
@@ -13,6 +13,7 @@ import (
 	"hash"
 	"hash/fnv"
 	"io"
+	"log/slog"
 	"net"
 	"net/http"
 	"net/url"
@@ -27,7 +28,6 @@ import (
 	"github.com/ghodss/yaml"
 	"golang.org/x/net/http2"
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/kubernetes/k8sapi"
 )
@@ -36,7 +36,7 @@ type client struct {
 	client    *http.Client
 	baseURL   string
 	namespace string
-	logger    log.Logger
+	logger    *slog.Logger
 
 	// Hash function to map IDs (which could span a large range) to Kubernetes names.
 	// While this is not currently upgradable, it could be in the future.
@@ -268,7 +268,7 @@ func (cli *client) detectKubernetesVersion() error {
 
 	clusterVersion, err := semver.NewVersion(version.GitVersion)
 	if err != nil {
-		cli.logger.Warnf("cannot detect Kubernetes version (%s): %v", clusterVersion, err)
+		cli.logger.Warn("cannot detect Kubernetes version", "version", clusterVersion, "err", err)
 		return nil
 	}
 
@@ -358,7 +358,7 @@ func defaultTLSConfig() *tls.Config {
 	}
 }
 
-func newClient(cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace string, logger log.Logger, inCluster bool) (*client, error) {
+func newClient(cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace string, logger *slog.Logger, inCluster bool) (*client, error) {
 	tlsConfig := defaultTLSConfig()
 	data := func(b string, file string) ([]byte, error) {
 		if b != "" {
@@ -418,7 +418,7 @@ func newClient(cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace string, l
 
 	apiVersion := "dex.coreos.com/v1"
 
-	logger.Infof("kubernetes client apiVersion = %s", apiVersion)
+	logger.Info("kubernetes client", "api_version", apiVersion)
 	return &client{
 		client: &http.Client{
 			Transport: t,
diff --git a/storage/kubernetes/lock.go b/storage/kubernetes/lock.go
index 12075e81..c67380dc 100644
--- a/storage/kubernetes/lock.go
+++ b/storage/kubernetes/lock.go
@@ -53,14 +53,14 @@ func (l *refreshTokenLock) Unlock(id string) {
 
 	r, err := l.cli.getRefreshToken(id)
 	if err != nil {
-		l.cli.logger.Debugf("failed to get resource to release lock for refresh token %s: %v", id, err)
+		l.cli.logger.Debug("failed to get resource to release lock for refresh token", "token_id", id, "err", err)
 		return
 	}
 
 	r.Annotations = nil
 	err = l.cli.put(resourceRefreshToken, r.ObjectMeta.Name, r)
 	if err != nil {
-		l.cli.logger.Debugf("failed to release lock for refresh token %s: %v", id, err)
+		l.cli.logger.Debug("failed to release lock for refresh token", "token_id", id, "err", err)
 	}
 }
 
@@ -114,7 +114,7 @@ func (l *refreshTokenLock) setLockAnnotation(id string) (bool, error) {
 		return false, nil
 	}
 
-	l.cli.logger.Debugf("break lock annotation error: %v", err)
+	l.cli.logger.Debug("break lock annotation", "error", err)
 	if isKubernetesAPIConflictError(err) {
 		l.waitingState = true
 		// after breaking error waiting for the lock to be released
diff --git a/storage/kubernetes/storage.go b/storage/kubernetes/storage.go
index c08362b8..8b6d5c9c 100644
--- a/storage/kubernetes/storage.go
+++ b/storage/kubernetes/storage.go
@@ -4,12 +4,12 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"log/slog"
 	"math/rand"
 	"net/http"
 	"strings"
 	"time"
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/kubernetes/k8sapi"
 )
@@ -53,7 +53,7 @@ type Config struct {
 }
 
 // Open returns a storage using Kubernetes third party resource.
-func (c *Config) Open(logger log.Logger) (storage.Storage, error) {
+func (c *Config) Open(logger *slog.Logger) (storage.Storage, error) {
 	cli, err := c.open(logger, false)
 	if err != nil {
 		return nil, err
@@ -66,7 +66,7 @@ func (c *Config) Open(logger log.Logger) (storage.Storage, error) {
 //
 // waitForResources controls if errors creating the resources cause this method to return
 // immediately (used during testing), or if the client will asynchronously retry.
-func (c *Config) open(logger log.Logger, waitForResources bool) (*client, error) {
+func (c *Config) open(logger *slog.Logger, waitForResources bool) (*client, error) {
 	if c.InCluster && (c.KubeConfigFile != "") {
 		return nil, errors.New("cannot specify both 'inCluster' and 'kubeConfigFile'")
 	}
@@ -155,12 +155,12 @@ func (cli *client) registerCustomResources() (ok bool) {
 
 		r := definitions[i]
 		var i interface{}
-		cli.logger.Infof("checking if custom resource %s has already been created...", r.ObjectMeta.Name)
+		cli.logger.Info("checking if custom resource has already been created...", "object", r.ObjectMeta.Name)
 		if err := cli.list(r.Spec.Names.Plural, &i); err == nil {
-			cli.logger.Infof("The custom resource %s already available, skipping create", r.ObjectMeta.Name)
+			cli.logger.Info("the custom resource already available, skipping create", "object", r.ObjectMeta.Name)
 			continue
 		} else {
-			cli.logger.Infof("failed to list custom resource %s, attempting to create: %v", r.ObjectMeta.Name, err)
+			cli.logger.Info("failed to list custom resource, attempting to create", "object", r.ObjectMeta.Name, "err", err)
 		}
 
 		err = cli.postResource(cli.crdAPIVersion, "", "customresourcedefinitions", r)
@@ -169,17 +169,17 @@ func (cli *client) registerCustomResources() (ok bool) {
 		if err != nil {
 			switch err {
 			case storage.ErrAlreadyExists:
-				cli.logger.Infof("custom resource already created %s", resourceName)
+				cli.logger.Info("custom resource already created", "object", resourceName)
 			case storage.ErrNotFound:
-				cli.logger.Errorf("custom resources not found, please enable the respective API group")
+				cli.logger.Error("custom resources not found, please enable the respective API group")
 				ok = false
 			default:
-				cli.logger.Errorf("creating custom resource %s: %v", resourceName, err)
+				cli.logger.Error("creating custom resource", "object", resourceName, "err", err)
 				ok = false
 			}
 			continue
 		}
-		cli.logger.Errorf("create custom resource %s", resourceName)
+		cli.logger.Error("create custom resource", "object", resourceName)
 	}
 	return ok
 }
@@ -197,7 +197,7 @@ func (cli *client) waitForCRDs(ctx context.Context) error {
 				break
 			}
 
-			cli.logger.Errorf("checking CRD: %v", err)
+			cli.logger.ErrorContext(ctx, "checking CRD", "err", err)
 
 			select {
 			case <-ctx.Done():
@@ -556,7 +556,7 @@ func (cli *client) UpdateKeys(updater func(old storage.Keys) (storage.Keys, erro
 		err = cli.post(resourceKeys, newKeys)
 		if err != nil && errors.Is(err, storage.ErrAlreadyExists) {
 			// We need to tolerate conflicts here in case of HA mode.
-			cli.logger.Debugf("Keys creation failed: %v. It is possible that keys have already been created by another dex instance.", err)
+			cli.logger.Debug("Keys creation failed. It is possible that keys have already been created by another dex instance.", "err", err)
 			return errors.New("keys already created by another server instance")
 		}
 
@@ -569,7 +569,7 @@ func (cli *client) UpdateKeys(updater func(old storage.Keys) (storage.Keys, erro
 	if isKubernetesAPIConflictError(err) {
 		// We need to tolerate conflicts here in case of HA mode.
 		// Dex instances run keys rotation at the same time because they use SigningKey.nextRotation CR field as a trigger.
-		cli.logger.Debugf("Keys rotation failed: %v. It is possible that keys have already been rotated by another dex instance.", err)
+		cli.logger.Debug("Keys rotation failed. It is possible that keys have already been rotated by another dex instance.", "err", err)
 		return errors.New("keys already rotated by another server instance")
 	}
 
@@ -622,7 +622,7 @@ func (cli *client) GarbageCollect(now time.Time) (result storage.GCResult, err e
 	for _, authRequest := range authRequests.AuthRequests {
 		if now.After(authRequest.Expiry) {
 			if err := cli.delete(resourceAuthRequest, authRequest.ObjectMeta.Name); err != nil {
-				cli.logger.Errorf("failed to delete auth request: %v", err)
+				cli.logger.Error("failed to delete auth request", "err", err)
 				delErr = fmt.Errorf("failed to delete auth request: %v", err)
 			}
 			result.AuthRequests++
@@ -640,7 +640,7 @@ func (cli *client) GarbageCollect(now time.Time) (result storage.GCResult, err e
 	for _, authCode := range authCodes.AuthCodes {
 		if now.After(authCode.Expiry) {
 			if err := cli.delete(resourceAuthCode, authCode.ObjectMeta.Name); err != nil {
-				cli.logger.Errorf("failed to delete auth code %v", err)
+				cli.logger.Error("failed to delete auth code", "err", err)
 				delErr = fmt.Errorf("failed to delete auth code: %v", err)
 			}
 			result.AuthCodes++
@@ -655,7 +655,7 @@ func (cli *client) GarbageCollect(now time.Time) (result storage.GCResult, err e
 	for _, deviceRequest := range deviceRequests.DeviceRequests {
 		if now.After(deviceRequest.Expiry) {
 			if err := cli.delete(resourceDeviceRequest, deviceRequest.ObjectMeta.Name); err != nil {
-				cli.logger.Errorf("failed to delete device request: %v", err)
+				cli.logger.Error("failed to delete device request", "err", err)
 				delErr = fmt.Errorf("failed to delete device request: %v", err)
 			}
 			result.DeviceRequests++
@@ -670,7 +670,7 @@ func (cli *client) GarbageCollect(now time.Time) (result storage.GCResult, err e
 	for _, deviceToken := range deviceTokens.DeviceTokens {
 		if now.After(deviceToken.Expiry) {
 			if err := cli.delete(resourceDeviceToken, deviceToken.ObjectMeta.Name); err != nil {
-				cli.logger.Errorf("failed to delete device token: %v", err)
+				cli.logger.Error("failed to delete device token", "err", err)
 				delErr = fmt.Errorf("failed to delete device token: %v", err)
 			}
 			result.DeviceTokens++
diff --git a/storage/memory/memory.go b/storage/memory/memory.go
index 8e080c9f..4399c61d 100644
--- a/storage/memory/memory.go
+++ b/storage/memory/memory.go
@@ -3,18 +3,18 @@ package memory
 
 import (
 	"context"
+	"log/slog"
 	"strings"
 	"sync"
 	"time"
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 )
 
 var _ storage.Storage = (*memStorage)(nil)
 
 // New returns an in memory storage.
-func New(logger log.Logger) storage.Storage {
+func New(logger *slog.Logger) storage.Storage {
 	return &memStorage{
 		clients:         make(map[string]storage.Client),
 		authCodes:       make(map[string]storage.AuthCode),
@@ -36,7 +36,7 @@ type Config struct { // The in memory implementation has no config.
 }
 
 // Open always returns a new in memory storage.
-func (c *Config) Open(logger log.Logger) (storage.Storage, error) {
+func (c *Config) Open(logger *slog.Logger) (storage.Storage, error) {
 	return New(logger), nil
 }
 
@@ -55,7 +55,7 @@ type memStorage struct {
 
 	keys storage.Keys
 
-	logger log.Logger
+	logger *slog.Logger
 }
 
 type offlineSessionID struct {
diff --git a/storage/sql/config.go b/storage/sql/config.go
index 8b782425..5379aeb6 100644
--- a/storage/sql/config.go
+++ b/storage/sql/config.go
@@ -5,6 +5,7 @@ import (
 	"crypto/x509"
 	"database/sql"
 	"fmt"
+	"log/slog"
 	"net"
 	"os"
 	"regexp"
@@ -15,7 +16,6 @@ import (
 	"github.com/go-sql-driver/mysql"
 	"github.com/lib/pq"
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 )
 
@@ -31,7 +31,6 @@ const (
 	mysqlErrUnknownSysVar       = 1193
 )
 
-//nolint
 const (
 	// postgres SSL modes
 	pgSSLDisable    = "disable"
@@ -40,7 +39,6 @@ const (
 	pgSSLVerifyFull = "verify-full"
 )
 
-//nolint
 const (
 	// MySQL SSL modes
 	mysqlSSLTrue       = "true"
@@ -84,7 +82,7 @@ type Postgres struct {
 }
 
 // Open creates a new storage implementation backed by Postgres.
-func (p *Postgres) Open(logger log.Logger) (storage.Storage, error) {
+func (p *Postgres) Open(logger *slog.Logger) (storage.Storage, error) {
 	conn, err := p.open(logger)
 	if err != nil {
 		return nil, err
@@ -164,7 +162,7 @@ func (p *Postgres) createDataSourceName() string {
 	return strings.Join(parameters, " ")
 }
 
-func (p *Postgres) open(logger log.Logger) (*conn, error) {
+func (p *Postgres) open(logger *slog.Logger) (*conn, error) {
 	dataSourceName := p.createDataSourceName()
 
 	db, err := sql.Open("postgres", dataSourceName)
@@ -216,7 +214,7 @@ type MySQL struct {
 }
 
 // Open creates a new storage implementation backed by MySQL.
-func (s *MySQL) Open(logger log.Logger) (storage.Storage, error) {
+func (s *MySQL) Open(logger *slog.Logger) (storage.Storage, error) {
 	conn, err := s.open(logger)
 	if err != nil {
 		return nil, err
@@ -224,7 +222,7 @@ func (s *MySQL) Open(logger log.Logger) (storage.Storage, error) {
 	return conn, nil
 }
 
-func (s *MySQL) open(logger log.Logger) (*conn, error) {
+func (s *MySQL) open(logger *slog.Logger) (*conn, error) {
 	cfg := mysql.Config{
 		User:                 s.User,
 		Passwd:               s.Password,
diff --git a/storage/sql/sql.go b/storage/sql/sql.go
index 0a292169..d671021f 100644
--- a/storage/sql/sql.go
+++ b/storage/sql/sql.go
@@ -3,14 +3,13 @@ package sql
 
 import (
 	"database/sql"
+	"log/slog"
 	"regexp"
 	"time"
 
 	// import third party drivers
 	_ "github.com/lib/pq"
 	_ "github.com/mattn/go-sqlite3"
-
-	"github.com/dexidp/dex/pkg/log"
 )
 
 // flavor represents a specific SQL implementation, and is used to translate query strings
@@ -131,7 +130,7 @@ func (c *conn) translateArgs(args []interface{}) []interface{} {
 type conn struct {
 	db                 *sql.DB
 	flavor             *flavor
-	logger             log.Logger
+	logger             *slog.Logger
 	alreadyExistsCheck func(err error) bool
 }
 
diff --git a/storage/sql/sqlite.go b/storage/sql/sqlite.go
index 43df671a..2d29e607 100644
--- a/storage/sql/sqlite.go
+++ b/storage/sql/sqlite.go
@@ -6,10 +6,10 @@ package sql
 import (
 	"database/sql"
 	"fmt"
+	"log/slog"
 
 	sqlite3 "github.com/mattn/go-sqlite3"
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 )
 
@@ -20,7 +20,7 @@ type SQLite3 struct {
 }
 
 // Open creates a new storage implementation backed by SQLite3
-func (s *SQLite3) Open(logger log.Logger) (storage.Storage, error) {
+func (s *SQLite3) Open(logger *slog.Logger) (storage.Storage, error) {
 	conn, err := s.open(logger)
 	if err != nil {
 		return nil, err
@@ -28,7 +28,7 @@ func (s *SQLite3) Open(logger log.Logger) (storage.Storage, error) {
 	return conn, nil
 }
 
-func (s *SQLite3) open(logger log.Logger) (*conn, error) {
+func (s *SQLite3) open(logger *slog.Logger) (*conn, error) {
 	db, err := sql.Open("sqlite3", s.File)
 	if err != nil {
 		return nil, err
diff --git a/storage/static.go b/storage/static.go
index e8902b9b..ca04937a 100644
--- a/storage/static.go
+++ b/storage/static.go
@@ -3,9 +3,8 @@ package storage
 import (
 	"context"
 	"errors"
+	"log/slog"
 	"strings"
-
-	"github.com/dexidp/dex/pkg/log"
 )
 
 // Tests for this code are in the "memory" package, since this package doesn't
@@ -90,17 +89,17 @@ type staticPasswordsStorage struct {
 	// A map of passwords that is indexed by lower-case email ids
 	passwordsByEmail map[string]Password
 
-	logger log.Logger
+	logger *slog.Logger
 }
 
 // WithStaticPasswords returns a storage with a read-only set of passwords.
-func WithStaticPasswords(s Storage, staticPasswords []Password, logger log.Logger) Storage {
+func WithStaticPasswords(s Storage, staticPasswords []Password, logger *slog.Logger) Storage {
 	passwordsByEmail := make(map[string]Password, len(staticPasswords))
 	for _, p := range staticPasswords {
 		// Enable case insensitive email comparison.
 		lowerEmail := strings.ToLower(p.Email)
 		if _, ok := passwordsByEmail[lowerEmail]; ok {
-			logger.Errorf("Attempting to create StaticPasswords with the same email id: %s", p.Email)
+			logger.Error("attempting to create StaticPasswords with the same email id", "email", p.Email)
 		}
 		passwordsByEmail[lowerEmail] = p
 	}
diff --git a/web/static/main.css b/web/static/main.css
index 7ab0316f..f5c61d7f 100644
--- a/web/static/main.css
+++ b/web/static/main.css
@@ -32,6 +32,11 @@ body {
   outline: none;
 }
 
+.dex-btn:disabled {
+  cursor: not-allowed;
+  opacity: 0.5;
+}
+
 .dex-btn-icon {
   background-position: center;
   background-repeat: no-repeat;
diff --git a/web/templates/password.html b/web/templates/password.html
index 8c77b26e..3e1c2ed2 100644
--- a/web/templates/password.html
+++ b/web/templates/password.html
@@ -32,4 +32,12 @@
   {{ end }}
 </div>
 
+
+<script type="text/javascript">
+  document.querySelector('form').onsubmit = function(e) {
+    var el = document.querySelector('#submit-login');
+    el.setAttribute('disabled', 'disabled');
+  };
+</script>
+
 {{ template "footer.html" . }}


Test patch:
diff --git a/cmd/dex/config_test.go b/cmd/dex/config_test.go
index e3169650..95a79eb2 100644
--- a/cmd/dex/config_test.go
+++ b/cmd/dex/config_test.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"log/slog"
 	"os"
 	"testing"
 
@@ -219,7 +220,7 @@ logger:
 			DeviceRequests: "10m",
 		},
 		Logger: Logger{
-			Level:  "debug",
+			Level:  slog.LevelDebug,
 			Format: "json",
 		},
 	}
@@ -426,7 +427,7 @@ logger:
 			AuthRequests: "25h",
 		},
 		Logger: Logger{
-			Level:  "debug",
+			Level:  slog.LevelDebug,
 			Format: "json",
 		},
 	}
diff --git a/cmd/dex/serve_test.go b/cmd/dex/serve_test.go
new file mode 100644
index 00000000..9e214480
--- /dev/null
+++ b/cmd/dex/serve_test.go
@@ -0,0 +1,29 @@
+package main
+
+import (
+	"log/slog"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestNewLogger(t *testing.T) {
+	t.Run("JSON", func(t *testing.T) {
+		logger, err := newLogger(slog.LevelInfo, "json")
+		require.NoError(t, err)
+		require.NotEqual(t, (*slog.Logger)(nil), logger)
+	})
+
+	t.Run("Text", func(t *testing.T) {
+		logger, err := newLogger(slog.LevelError, "text")
+		require.NoError(t, err)
+		require.NotEqual(t, (*slog.Logger)(nil), logger)
+	})
+
+	t.Run("Unknown", func(t *testing.T) {
+		logger, err := newLogger(slog.LevelError, "gofmt")
+		require.Error(t, err)
+		require.Equal(t, "log format is not one of the supported values (json, text): gofmt", err.Error())
+		require.Equal(t, (*slog.Logger)(nil), logger)
+	})
+}
diff --git a/connector/atlassiancrowd/atlassiancrowd_test.go b/connector/atlassiancrowd/atlassiancrowd_test.go
index 36789a39..b4c35a13 100644
--- a/connector/atlassiancrowd/atlassiancrowd_test.go
+++ b/connector/atlassiancrowd/atlassiancrowd_test.go
@@ -7,12 +7,11 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 	"net/http/httptest"
 	"reflect"
 	"testing"
-
-	"github.com/sirupsen/logrus"
 )
 
 func TestUserGroups(t *testing.T) {
@@ -151,11 +150,7 @@ type TestServerResponse struct {
 func newTestCrowdConnector(baseURL string) crowdConnector {
 	connector := crowdConnector{}
 	connector.BaseURL = baseURL
-	connector.logger = &logrus.Logger{
-		Out:       io.Discard,
-		Level:     logrus.DebugLevel,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-	}
+	connector.logger = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 	return connector
 }
 
diff --git a/connector/authproxy/authproxy_test.go b/connector/authproxy/authproxy_test.go
index 5e098722..fdcf4038 100644
--- a/connector/authproxy/authproxy_test.go
+++ b/connector/authproxy/authproxy_test.go
@@ -2,12 +2,11 @@ package authproxy
 
 import (
 	"io"
+	"log/slog"
 	"net/http"
 	"reflect"
 	"testing"
 
-	"github.com/sirupsen/logrus"
-
 	"github.com/dexidp/dex/connector"
 )
 
@@ -23,7 +22,7 @@ const (
 	testUserID       = "1234567890"
 )
 
-var logger = &logrus.Logger{Out: io.Discard, Formatter: &logrus.TextFormatter{}}
+var logger = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 func TestUser(t *testing.T) {
 	config := Config{
diff --git a/connector/github/github_test.go b/connector/github/github_test.go
index af8099e9..088cbb23 100644
--- a/connector/github/github_test.go
+++ b/connector/github/github_test.go
@@ -6,6 +6,8 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"io"
+	"log/slog"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -449,6 +451,7 @@ func Test_isPreferredEmailDomain(t *testing.T) {
 }
 
 func Test_Open_PreferredDomainConfig(t *testing.T) {
+	log := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 	tests := []struct {
 		preferredEmailDomain string
 		email                string
@@ -476,7 +479,7 @@ func Test_Open_PreferredDomainConfig(t *testing.T) {
 			c := Config{
 				PreferredEmailDomain: test.preferredEmailDomain,
 			}
-			_, err := c.Open("id", nil)
+			_, err := c.Open("id", log)
 
 			expectEquals(t, err, test.expected)
 		})
diff --git a/connector/google/google_test.go b/connector/google/google_test.go
index 2fa2b783..bafcadc8 100644
--- a/connector/google/google_test.go
+++ b/connector/google/google_test.go
@@ -4,6 +4,8 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"io"
+	"log/slog"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -11,7 +13,6 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/assert"
 	admin "google.golang.org/api/admin/directory/v1"
 	"google.golang.org/api/option"
@@ -51,7 +52,7 @@ func testSetup() *httptest.Server {
 }
 
 func newConnector(config *Config) (*googleConnector, error) {
-	log := logrus.New()
+	log := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 	conn, err := config.Open("id", log)
 	if err != nil {
 		return nil, err
diff --git a/connector/keystone/keystone_test.go b/connector/keystone/keystone_test.go
index 8f1ea1bb..9b0590df 100644
--- a/connector/keystone/keystone_test.go
+++ b/connector/keystone/keystone_test.go
@@ -17,11 +17,13 @@ import (
 const (
 	invalidPass = "WRONG_PASS"
 
-	testUser   = "test_user"
-	testPass   = "test_pass"
-	testEmail  = "test@example.com"
-	testGroup  = "test_group"
-	testDomain = "default"
+	testUser          = "test_user"
+	testPass          = "test_pass"
+	testEmail         = "test@example.com"
+	testGroup         = "test_group"
+	testDomainAltName = "altdomain"
+	testDomainID      = "default"
+	testDomainName    = "Default"
 )
 
 var (
@@ -32,8 +34,26 @@ var (
 	authTokenURL     = ""
 	usersURL         = ""
 	groupsURL        = ""
+	domainsURL       = ""
 )
 
+type userReq struct {
+	Name     string   `json:"name"`
+	Email    string   `json:"email"`
+	Enabled  bool     `json:"enabled"`
+	Password string   `json:"password"`
+	Roles    []string `json:"roles"`
+	DomainID string   `json:"domain_id,omitempty"`
+}
+
+type domainResponse struct {
+	Domain domainKeystone `json:"domain"`
+}
+
+type domainsResponse struct {
+	Domains []domainKeystone `json:"domains"`
+}
+
 type groupResponse struct {
 	Group struct {
 		ID string `json:"id"`
@@ -49,7 +69,7 @@ func getAdminToken(t *testing.T, adminName, adminPass string) (token, id string)
 				Password: password{
 					User: user{
 						Name:     adminName,
-						Domain:   domain{ID: testDomain},
+						Domain:   domainKeystone{ID: testDomainID},
 						Password: adminPass,
 					},
 				},
@@ -89,16 +109,91 @@ func getAdminToken(t *testing.T, adminName, adminPass string) (token, id string)
 	return token, tokenResp.Token.User.ID
 }
 
-func createUser(t *testing.T, token, userName, userEmail, userPass string) string {
+func getOrCreateDomain(t *testing.T, token, domainName string) string {
+	t.Helper()
+
+	domainSearchURL := domainsURL + "?name=" + domainName
+	reqGet, err := http.NewRequest("GET", domainSearchURL, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	reqGet.Header.Set("X-Auth-Token", token)
+	reqGet.Header.Add("Content-Type", "application/json")
+	respGet, err := http.DefaultClient.Do(reqGet)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	dataGet, err := io.ReadAll(respGet.Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer respGet.Body.Close()
+
+	domainsResp := new(domainsResponse)
+	err = json.Unmarshal(dataGet, &domainsResp)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if len(domainsResp.Domains) >= 1 {
+		return domainsResp.Domains[0].ID
+	}
+
+	createDomainData := map[string]interface{}{
+		"domain": map[string]interface{}{
+			"name":    domainName,
+			"enabled": true,
+		},
+	}
+
+	body, err := json.Marshal(createDomainData)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	req, err := http.NewRequest("POST", domainsURL, bytes.NewBuffer(body))
+	if err != nil {
+		t.Fatal(err)
+	}
+	req.Header.Set("X-Auth-Token", token)
+	req.Header.Add("Content-Type", "application/json")
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if resp.StatusCode != 201 {
+		t.Fatalf("failed to create domain %s", domainName)
+	}
+
+	data, err := io.ReadAll(resp.Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer resp.Body.Close()
+
+	domainResp := new(domainResponse)
+	err = json.Unmarshal(data, &domainResp)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	return domainResp.Domain.ID
+}
+
+func createUser(t *testing.T, token, domainID, userName, userEmail, userPass string) string {
 	t.Helper()
 
 	createUserData := map[string]interface{}{
-		"user": map[string]interface{}{
-			"name":     userName,
-			"email":    userEmail,
-			"enabled":  true,
-			"password": userPass,
-			"roles":    []string{"admin"},
+		"user": userReq{
+			DomainID: domainID,
+			Name:     userName,
+			Email:    userEmail,
+			Enabled:  true,
+			Password: userPass,
+			Roles:    []string{"admin"},
 		},
 	}
 
@@ -214,7 +309,7 @@ func TestIncorrectCredentialsLogin(t *testing.T) {
 	setupVariables(t)
 	c := conn{
 		client: http.DefaultClient,
-		Host:   keystoneURL, Domain: testDomain,
+		Host:   keystoneURL, Domain: domainKeystone{ID: testDomainID},
 		AdminUsername: adminUser, AdminPassword: adminPass,
 	}
 	s := connector.Scopes{OfflineAccess: true, Groups: true}
@@ -238,10 +333,11 @@ func TestValidUserLogin(t *testing.T) {
 	token, _ := getAdminToken(t, adminUser, adminPass)
 
 	type tUser struct {
-		username string
-		domain   string
-		email    string
-		password string
+		createDomain bool
+		domain       domainKeystone
+		username     string
+		email        string
+		password     string
 	}
 
 	type expect struct {
@@ -258,10 +354,11 @@ func TestValidUserLogin(t *testing.T) {
 		{
 			name: "test with email address",
 			input: tUser{
-				username: testUser,
-				domain:   testDomain,
-				email:    testEmail,
-				password: testPass,
+				createDomain: false,
+				domain:       domainKeystone{ID: testDomainID},
+				username:     testUser,
+				email:        testEmail,
+				password:     testPass,
 			},
 			expected: expect{
 				username:      testUser,
@@ -272,10 +369,11 @@ func TestValidUserLogin(t *testing.T) {
 		{
 			name: "test without email address",
 			input: tUser{
-				username: testUser,
-				domain:   testDomain,
-				email:    "",
-				password: testPass,
+				createDomain: false,
+				domain:       domainKeystone{ID: testDomainID},
+				username:     testUser,
+				email:        "",
+				password:     testPass,
 			},
 			expected: expect{
 				username:      testUser,
@@ -283,11 +381,66 @@ func TestValidUserLogin(t *testing.T) {
 				verifiedEmail: false,
 			},
 		},
+		{
+			name: "test with default domain Name",
+			input: tUser{
+				createDomain: false,
+				domain:       domainKeystone{Name: testDomainName},
+				username:     testUser,
+				email:        testEmail,
+				password:     testPass,
+			},
+			expected: expect{
+				username:      testUser,
+				email:         testEmail,
+				verifiedEmail: true,
+			},
+		},
+		{
+			name: "test with custom domain Name",
+			input: tUser{
+				createDomain: true,
+				domain:       domainKeystone{Name: testDomainAltName},
+				username:     testUser,
+				email:        testEmail,
+				password:     testPass,
+			},
+			expected: expect{
+				username:      testUser,
+				email:         testEmail,
+				verifiedEmail: true,
+			},
+		},
+		{
+			name: "test with custom domain ID",
+			input: tUser{
+				createDomain: true,
+				domain:       domainKeystone{},
+				username:     testUser,
+				email:        testEmail,
+				password:     testPass,
+			},
+			expected: expect{
+				username:      testUser,
+				email:         testEmail,
+				verifiedEmail: true,
+			},
+		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			userID := createUser(t, token, tt.input.username, tt.input.email, tt.input.password)
+			domainID := ""
+			if tt.input.createDomain == true {
+				domainID = getOrCreateDomain(t, token, testDomainAltName)
+				t.Logf("getOrCreateDomain ID: %s\n", domainID)
+
+				// if there was nothing set then use the dynamically generated domain ID
+				if tt.input.domain.ID == "" && tt.input.domain.Name == "" {
+					tt.input.domain.ID = domainID
+				}
+			}
+			userID := createUser(t, token, domainID, tt.input.username, tt.input.email, tt.input.password)
 			defer deleteResource(t, token, userID, usersURL)
 
 			c := conn{
@@ -298,7 +451,7 @@ func TestValidUserLogin(t *testing.T) {
 			s := connector.Scopes{OfflineAccess: true, Groups: true}
 			identity, validPW, err := c.Login(context.Background(), s, tt.input.username, tt.input.password)
 			if err != nil {
-				t.Fatal(err.Error())
+				t.Fatalf("Login failed for user %s: %v", tt.input.username, err.Error())
 			}
 			t.Log(identity)
 			if identity.Username != tt.expected.username {
@@ -330,7 +483,7 @@ func TestUseRefreshToken(t *testing.T) {
 
 	c := conn{
 		client: http.DefaultClient,
-		Host:   keystoneURL, Domain: testDomain,
+		Host:   keystoneURL, Domain: domainKeystone{ID: testDomainID},
 		AdminUsername: adminUser, AdminPassword: adminPass,
 	}
 	s := connector.Scopes{OfflineAccess: true, Groups: true}
@@ -352,11 +505,11 @@ func TestUseRefreshToken(t *testing.T) {
 func TestUseRefreshTokenUserDeleted(t *testing.T) {
 	setupVariables(t)
 	token, _ := getAdminToken(t, adminUser, adminPass)
-	userID := createUser(t, token, testUser, testEmail, testPass)
+	userID := createUser(t, token, "", testUser, testEmail, testPass)
 
 	c := conn{
 		client: http.DefaultClient,
-		Host:   keystoneURL, Domain: testDomain,
+		Host:   keystoneURL, Domain: domainKeystone{ID: testDomainID},
 		AdminUsername: adminUser, AdminPassword: adminPass,
 	}
 	s := connector.Scopes{OfflineAccess: true, Groups: true}
@@ -382,12 +535,12 @@ func TestUseRefreshTokenUserDeleted(t *testing.T) {
 func TestUseRefreshTokenGroupsChanged(t *testing.T) {
 	setupVariables(t)
 	token, _ := getAdminToken(t, adminUser, adminPass)
-	userID := createUser(t, token, testUser, testEmail, testPass)
+	userID := createUser(t, token, "", testUser, testEmail, testPass)
 	defer deleteResource(t, token, userID, usersURL)
 
 	c := conn{
 		client: http.DefaultClient,
-		Host:   keystoneURL, Domain: testDomain,
+		Host:   keystoneURL, Domain: domainKeystone{ID: testDomainID},
 		AdminUsername: adminUser, AdminPassword: adminPass,
 	}
 	s := connector.Scopes{OfflineAccess: true, Groups: true}
@@ -419,12 +572,12 @@ func TestUseRefreshTokenGroupsChanged(t *testing.T) {
 func TestNoGroupsInScope(t *testing.T) {
 	setupVariables(t)
 	token, _ := getAdminToken(t, adminUser, adminPass)
-	userID := createUser(t, token, testUser, testEmail, testPass)
+	userID := createUser(t, token, "", testUser, testEmail, testPass)
 	defer deleteResource(t, token, userID, usersURL)
 
 	c := conn{
 		client: http.DefaultClient,
-		Host:   keystoneURL, Domain: testDomain,
+		Host:   keystoneURL, Domain: domainKeystone{ID: testDomainID},
 		AdminUsername: adminUser, AdminPassword: adminPass,
 	}
 	s := connector.Scopes{OfflineAccess: true, Groups: false}
@@ -474,6 +627,7 @@ func setupVariables(t *testing.T) {
 	authTokenURL = keystoneURL + "/v3/auth/tokens/"
 	usersURL = keystoneAdminURL + "/v3/users/"
 	groupsURL = keystoneAdminURL + "/v3/groups/"
+	domainsURL = keystoneAdminURL + "/v3/domains/"
 }
 
 func expectEquals(t *testing.T, a interface{}, b interface{}) {
diff --git a/connector/ldap/ldap_test.go b/connector/ldap/ldap_test.go
index f00f1ead..de85b6a2 100644
--- a/connector/ldap/ldap_test.go
+++ b/connector/ldap/ldap_test.go
@@ -4,11 +4,11 @@ import (
 	"context"
 	"fmt"
 	"io"
+	"log/slog"
 	"os"
 	"testing"
 
 	"github.com/kylelemons/godebug/pretty"
-	"github.com/sirupsen/logrus"
 
 	"github.com/dexidp/dex/connector"
 )
@@ -567,7 +567,7 @@ func runTests(t *testing.T, connMethod connectionMethod, config *Config, tests [
 	c.BindDN = "cn=admin,dc=example,dc=org"
 	c.BindPW = "admin"
 
-	l := &logrus.Logger{Out: io.Discard, Formatter: &logrus.TextFormatter{}}
+	l := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	conn, err := c.openConnector(l)
 	if err != nil {
diff --git a/connector/mock/connectortest.go b/connector/mock/connectortest.go
index e97f9865..7e5979a9 100644
--- a/connector/mock/connectortest.go
+++ b/connector/mock/connectortest.go
@@ -5,16 +5,16 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"log/slog"
 	"net/http"
 	"net/url"
 
 	"github.com/dexidp/dex/connector"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 // NewCallbackConnector returns a mock connector which requires no user interaction. It always returns
 // the same (fake) identity.
-func NewCallbackConnector(logger log.Logger) connector.Connector {
+func NewCallbackConnector(logger *slog.Logger) connector.Connector {
 	return &Callback{
 		Identity: connector.Identity{
 			UserID:        "0-385-28089-0",
@@ -39,7 +39,7 @@ var (
 type Callback struct {
 	// The returned identity.
 	Identity connector.Identity
-	Logger   log.Logger
+	Logger   *slog.Logger
 }
 
 // LoginURL returns the URL to redirect the user to login with.
@@ -74,7 +74,8 @@ func (m *Callback) TokenIdentity(ctx context.Context, subjectTokenType, subjectT
 type CallbackConfig struct{}
 
 // Open returns an authentication strategy which requires no user interaction.
-func (c *CallbackConfig) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *CallbackConfig) Open(id string, logger *slog.Logger) (connector.Connector, error) {
+	logger = logger.With(slog.Group("connector", "type", "callback", "id", id))
 	return NewCallbackConnector(logger), nil
 }
 
@@ -86,7 +87,7 @@ type PasswordConfig struct {
 }
 
 // Open returns an authentication strategy which prompts for a predefined username and password.
-func (c *PasswordConfig) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *PasswordConfig) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	if c.Username == "" {
 		return nil, errors.New("no username supplied")
 	}
@@ -99,7 +100,7 @@ func (c *PasswordConfig) Open(id string, logger log.Logger) (connector.Connector
 type passwordConnector struct {
 	username string
 	password string
-	logger   log.Logger
+	logger   *slog.Logger
 }
 
 func (p passwordConnector) Close() error { return nil }
diff --git a/connector/oauth/oauth_test.go b/connector/oauth/oauth_test.go
index 62cbd8d5..d06c0c08 100644
--- a/connector/oauth/oauth_test.go
+++ b/connector/oauth/oauth_test.go
@@ -6,6 +6,8 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"io"
+	"log/slog"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -13,7 +15,6 @@ import (
 	"testing"
 
 	"github.com/go-jose/go-jose/v4"
-	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/assert"
 
 	"github.com/dexidp/dex/connector"
@@ -270,7 +271,7 @@ func newConnector(t *testing.T, serverURL string) *oauthConnector {
 	testConfig.ClaimMapping.EmailKey = "mail"
 	testConfig.ClaimMapping.EmailVerifiedKey = "has_verified_email"
 
-	log := logrus.New()
+	log := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	conn, err := testConfig.Open("id", log)
 	if err != nil {
diff --git a/connector/oidc/oidc_test.go b/connector/oidc/oidc_test.go
index e621a55f..07291f7e 100644
--- a/connector/oidc/oidc_test.go
+++ b/connector/oidc/oidc_test.go
@@ -10,6 +10,8 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"io"
+	"log/slog"
 	"net/http"
 	"net/http/httptest"
 	"reflect"
@@ -18,7 +20,6 @@ import (
 	"time"
 
 	"github.com/go-jose/go-jose/v4"
-	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/require"
 
 	"github.com/dexidp/dex/connector"
@@ -765,7 +766,7 @@ func newToken(key *jose.JSONWebKey, claims map[string]interface{}) (string, erro
 }
 
 func newConnector(config Config) (*oidcConnector, error) {
-	logger := logrus.New()
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 	conn, err := config.Open("id", logger)
 	if err != nil {
 		return nil, fmt.Errorf("unable to open: %v", err)
diff --git a/connector/openshift/openshift_test.go b/connector/openshift/openshift_test.go
index 1a2c7a48..89ec0e25 100644
--- a/connector/openshift/openshift_test.go
+++ b/connector/openshift/openshift_test.go
@@ -4,6 +4,8 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"io"
+	"log/slog"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -11,7 +13,6 @@ import (
 	"testing"
 	"time"
 
-	"github.com/sirupsen/logrus"
 	"golang.org/x/oauth2"
 
 	"github.com/dexidp/dex/connector"
@@ -37,7 +38,7 @@ func TestOpen(t *testing.T) {
 		InsecureCA:   true,
 	}
 
-	logger := logrus.New()
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	oconfig, err := c.Open("id", logger)
 
diff --git a/connector/saml/saml_test.go b/connector/saml/saml_test.go
index 68c0cb1a..f67e3e8b 100644
--- a/connector/saml/saml_test.go
+++ b/connector/saml/saml_test.go
@@ -5,6 +5,8 @@ import (
 	"encoding/base64"
 	"encoding/pem"
 	"errors"
+	"io"
+	"log/slog"
 	"os"
 	"sort"
 	"testing"
@@ -12,7 +14,6 @@ import (
 
 	"github.com/kylelemons/godebug/pretty"
 	dsig "github.com/russellhaering/goxmldsig"
-	"github.com/sirupsen/logrus"
 
 	"github.com/dexidp/dex/connector"
 )
@@ -420,7 +421,7 @@ func (r responseTest) run(t *testing.T) {
 		t.Fatalf("parse test time: %v", err)
 	}
 
-	conn, err := c.openConnector(logrus.New())
+	conn, err := c.openConnector(slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})))
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -454,7 +455,7 @@ func (r responseTest) run(t *testing.T) {
 }
 
 func TestConfigCAData(t *testing.T) {
-	logger := logrus.New()
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 	validPEM, err := os.ReadFile("testdata/ca.crt")
 	if err != nil {
 		t.Fatal(err)
diff --git a/server/api_test.go b/server/api_test.go
index 1ed43168..e4150f1f 100644
--- a/server/api_test.go
+++ b/server/api_test.go
@@ -2,17 +2,16 @@ package server
 
 import (
 	"context"
+	"io"
+	"log/slog"
 	"net"
-	"os"
 	"testing"
 	"time"
 
-	"github.com/sirupsen/logrus"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials/insecure"
 
 	"github.com/dexidp/dex/api/v2"
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/server/internal"
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/memory"
@@ -30,7 +29,7 @@ type apiClient struct {
 }
 
 // newAPI constructs a gRCP client connected to a backing server.
-func newAPI(s storage.Storage, logger log.Logger, t *testing.T) *apiClient {
+func newAPI(s storage.Storage, logger *slog.Logger, t *testing.T) *apiClient {
 	l, err := net.Listen("tcp", "127.0.0.1:0")
 	if err != nil {
 		t.Fatal(err)
@@ -59,11 +58,7 @@ func newAPI(s storage.Storage, logger log.Logger, t *testing.T) *apiClient {
 
 // Attempts to create, update and delete a test Password
 func TestPassword(t *testing.T) {
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	s := memory.New(logger)
 	client := newAPI(s, logger, t)
@@ -172,11 +167,7 @@ func TestPassword(t *testing.T) {
 
 // Ensures checkCost returns expected values
 func TestCheckCost(t *testing.T) {
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	s := memory.New(logger)
 	client := newAPI(s, logger, t)
@@ -229,11 +220,7 @@ func TestCheckCost(t *testing.T) {
 
 // Attempts to list and revoke an existing refresh token.
 func TestRefreshToken(t *testing.T) {
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	s := memory.New(logger)
 	client := newAPI(s, logger, t)
@@ -342,11 +329,7 @@ func TestRefreshToken(t *testing.T) {
 }
 
 func TestUpdateClient(t *testing.T) {
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	s := memory.New(logger)
 	client := newAPI(s, logger, t)
diff --git a/server/rotation_test.go b/server/rotation_test.go
index e279bf54..1d0d2f10 100644
--- a/server/rotation_test.go
+++ b/server/rotation_test.go
@@ -1,12 +1,12 @@
 package server
 
 import (
-	"os"
+	"io"
+	"log/slog"
 	"sort"
 	"testing"
 	"time"
 
-	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/require"
 
 	"github.com/dexidp/dex/storage"
@@ -68,11 +68,7 @@ func TestKeyRotator(t *testing.T) {
 	// Only the last 5 verification keys are expected to be kept around.
 	maxVerificationKeys := 5
 
-	l := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	l := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	r := &keyRotator{
 		Storage:  memory.New(l),
@@ -104,11 +100,7 @@ func TestKeyRotator(t *testing.T) {
 
 func TestRefreshTokenPolicy(t *testing.T) {
 	lastTime := time.Now()
-	l := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	l := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	r, err := NewRefreshTokenPolicy(l, true, "1m", "1m", "1m")
 	require.NoError(t, err)
diff --git a/server/server_test.go b/server/server_test.go
index 25d1909d..8936c90a 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -9,11 +9,11 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 	"net/http/httptest"
 	"net/http/httputil"
 	"net/url"
-	"os"
 	"path"
 	"reflect"
 	"sort"
@@ -26,7 +26,6 @@ import (
 	"github.com/go-jose/go-jose/v4"
 	"github.com/kylelemons/godebug/pretty"
 	"github.com/prometheus/client_golang/prometheus"
-	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/require"
 	"golang.org/x/crypto/bcrypt"
 	"golang.org/x/oauth2"
@@ -77,11 +76,7 @@ FDWV28nTP9sqbtsmU8Tem2jzMvZ7C/Q0AuDoKELFUpux8shm8wfIhyaPnXUGZoAZ
 Np4vUwMSYV5mopESLWOg3loBxKyLGFtgGKVCjGiQvy6zISQ4fQo=
 -----END RSA PRIVATE KEY-----`)
 
-var logger = &logrus.Logger{
-	Out:       os.Stderr,
-	Formatter: &logrus.TextFormatter{DisableColors: true},
-	Level:     logrus.DebugLevel,
-}
+var logger = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 func newTestServer(ctx context.Context, t *testing.T, updateConfig func(c *Config)) (*httptest.Server, *Server) {
 	var server *Server
diff --git a/storage/ent/mysql_test.go b/storage/ent/mysql_test.go
index 6c2dfa1d..f3e198aa 100644
--- a/storage/ent/mysql_test.go
+++ b/storage/ent/mysql_test.go
@@ -1,11 +1,12 @@
 package ent
 
 import (
+	"io"
+	"log/slog"
 	"os"
 	"strconv"
 	"testing"
 
-	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/require"
 
 	"github.com/dexidp/dex/storage"
@@ -39,11 +40,7 @@ func mysqlTestConfig(host string, port uint64) *MySQL {
 }
 
 func newMySQLStorage(host string, port uint64) storage.Storage {
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	cfg := mysqlTestConfig(host, port)
 	s, err := cfg.Open(logger)
diff --git a/storage/ent/postgres_test.go b/storage/ent/postgres_test.go
index c8e3a54d..baf0172b 100644
--- a/storage/ent/postgres_test.go
+++ b/storage/ent/postgres_test.go
@@ -1,11 +1,12 @@
 package ent
 
 import (
+	"io"
+	"log/slog"
 	"os"
 	"strconv"
 	"testing"
 
-	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/require"
 
 	"github.com/dexidp/dex/storage"
@@ -36,11 +37,7 @@ func postgresTestConfig(host string, port uint64) *Postgres {
 }
 
 func newPostgresStorage(host string, port uint64) storage.Storage {
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	cfg := postgresTestConfig(host, port)
 	s, err := cfg.Open(logger)
diff --git a/storage/ent/sqlite_test.go b/storage/ent/sqlite_test.go
index 301d769b..d88097c2 100644
--- a/storage/ent/sqlite_test.go
+++ b/storage/ent/sqlite_test.go
@@ -1,21 +1,16 @@
 package ent
 
 import (
-	"os"
+	"io"
+	"log/slog"
 	"testing"
 
-	"github.com/sirupsen/logrus"
-
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/conformance"
 )
 
 func newSQLiteStorage() storage.Storage {
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	cfg := SQLite3{File: ":memory:"}
 	s, err := cfg.Open(logger)
diff --git a/storage/etcd/etcd_test.go b/storage/etcd/etcd_test.go
index 8a9af5c9..5a568e8c 100644
--- a/storage/etcd/etcd_test.go
+++ b/storage/etcd/etcd_test.go
@@ -3,13 +3,14 @@ package etcd
 import (
 	"context"
 	"fmt"
+	"io"
+	"log/slog"
 	"os"
 	"runtime"
 	"strings"
 	"testing"
 	"time"
 
-	"github.com/sirupsen/logrus"
 	clientv3 "go.etcd.io/etcd/client/v3"
 
 	"github.com/dexidp/dex/storage"
@@ -55,11 +56,7 @@ func cleanDB(c *conn) error {
 	return nil
 }
 
-var logger = &logrus.Logger{
-	Out:       os.Stderr,
-	Formatter: &logrus.TextFormatter{DisableColors: true},
-	Level:     logrus.DebugLevel,
-}
+var logger = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 func TestEtcd(t *testing.T) {
 	testEtcdEnv := "DEX_ETCD_ENDPOINTS"
diff --git a/storage/kubernetes/client_test.go b/storage/kubernetes/client_test.go
index cfd04857..c8fc8db1 100644
--- a/storage/kubernetes/client_test.go
+++ b/storage/kubernetes/client_test.go
@@ -3,6 +3,8 @@ package kubernetes
 import (
 	"hash"
 	"hash/fnv"
+	"io"
+	"log/slog"
 	"net/http"
 	"os"
 	"path/filepath"
@@ -10,7 +12,6 @@ import (
 	"testing"
 	"time"
 
-	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/require"
 
 	"github.com/dexidp/dex/storage/kubernetes/k8sapi"
@@ -52,11 +53,7 @@ func TestOfflineTokenName(t *testing.T) {
 }
 
 func TestInClusterTransport(t *testing.T) {
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	user := k8sapi.AuthInfo{Token: "abc"}
 	cli, err := newClient(
diff --git a/storage/kubernetes/storage_test.go b/storage/kubernetes/storage_test.go
index b4b42688..d8bfd1f6 100644
--- a/storage/kubernetes/storage_test.go
+++ b/storage/kubernetes/storage_test.go
@@ -5,6 +5,8 @@ import (
 	"crypto/tls"
 	"errors"
 	"fmt"
+	"io"
+	"log/slog"
 	"net/http"
 	"net/http/httptest"
 	"os"
@@ -13,7 +15,6 @@ import (
 	"testing"
 	"time"
 
-	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/require"
 	"github.com/stretchr/testify/suite"
 
@@ -57,11 +58,7 @@ func (s *StorageTestSuite) SetupTest() {
 		KubeConfigFile: kubeconfigPath,
 	}
 
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	kubeClient, err := config.open(logger, true)
 	s.Require().NoError(err)
@@ -253,11 +250,7 @@ func newStatusCodesResponseTestClient(getResponseCode, actionResponseCode int) *
 	return &client{
 		client:  &http.Client{Transport: tr},
 		baseURL: s.URL,
-		logger: &logrus.Logger{
-			Out:       os.Stderr,
-			Formatter: &logrus.TextFormatter{DisableColors: true},
-			Level:     logrus.DebugLevel,
-		},
+		logger:  slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})),
 	}
 }
 
@@ -314,11 +307,7 @@ func TestRefreshTokenLock(t *testing.T) {
 		KubeConfigFile: kubeconfigPath,
 	}
 
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	kubeClient, err := config.open(logger, true)
 	require.NoError(t, err)
diff --git a/storage/memory/memory_test.go b/storage/memory/memory_test.go
index 84a8826e..75a17ac6 100644
--- a/storage/memory/memory_test.go
+++ b/storage/memory/memory_test.go
@@ -1,21 +1,16 @@
 package memory
 
 import (
-	"os"
+	"io"
+	"log/slog"
 	"testing"
 
-	"github.com/sirupsen/logrus"
-
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/conformance"
 )
 
 func TestStorage(t *testing.T) {
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	newStorage := func() storage.Storage {
 		return New(logger)
diff --git a/storage/memory/static_test.go b/storage/memory/static_test.go
index 4be23a1e..b9138742 100644
--- a/storage/memory/static_test.go
+++ b/storage/memory/static_test.go
@@ -3,22 +3,17 @@ package memory
 import (
 	"context"
 	"fmt"
-	"os"
+	"io"
+	"log/slog"
 	"strings"
 	"testing"
 
-	"github.com/sirupsen/logrus"
-
 	"github.com/dexidp/dex/storage"
 )
 
 func TestStaticClients(t *testing.T) {
 	ctx := context.Background()
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 	backing := New(logger)
 
 	c1 := storage.Client{ID: "foo", Secret: "foo_secret"}
@@ -102,11 +97,7 @@ func TestStaticClients(t *testing.T) {
 
 func TestStaticPasswords(t *testing.T) {
 	ctx := context.Background()
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 	backing := New(logger)
 
 	p1 := storage.Password{Email: "foo@example.com", Username: "foo_secret"}
@@ -215,11 +206,7 @@ func TestStaticPasswords(t *testing.T) {
 
 func TestStaticConnectors(t *testing.T) {
 	ctx := context.Background()
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 	backing := New(logger)
 
 	config1 := []byte(`{"issuer": "https://accounts.google.com"}`)
diff --git a/storage/sql/config_test.go b/storage/sql/config_test.go
index 1178728c..b1037e64 100644
--- a/storage/sql/config_test.go
+++ b/storage/sql/config_test.go
@@ -2,15 +2,14 @@ package sql
 
 import (
 	"fmt"
+	"io"
+	"log/slog"
 	"os"
 	"runtime"
 	"strconv"
 	"testing"
 	"time"
 
-	"github.com/sirupsen/logrus"
-
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/conformance"
 )
@@ -48,14 +47,10 @@ func cleanDB(c *conn) error {
 	return nil
 }
 
-var logger = &logrus.Logger{
-	Out:       os.Stderr,
-	Formatter: &logrus.TextFormatter{DisableColors: true},
-	Level:     logrus.DebugLevel,
-}
+var logger = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 type opener interface {
-	open(logger log.Logger) (*conn, error)
+	open(logger *slog.Logger) (*conn, error)
 }
 
 func testDB(t *testing.T, o opener, withTransactions bool) {
diff --git a/storage/sql/migrate_test.go b/storage/sql/migrate_test.go
index 4b77eb2f..bf9f0ddb 100644
--- a/storage/sql/migrate_test.go
+++ b/storage/sql/migrate_test.go
@@ -5,11 +5,11 @@ package sql
 
 import (
 	"database/sql"
-	"os"
+	"io"
+	"log/slog"
 	"testing"
 
 	sqlite3 "github.com/mattn/go-sqlite3"
-	"github.com/sirupsen/logrus"
 )
 
 func TestMigrate(t *testing.T) {
@@ -19,11 +19,7 @@ func TestMigrate(t *testing.T) {
 	}
 	defer db.Close()
 
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	errCheck := func(err error) bool {
 		sqlErr, ok := err.(sqlite3.Error)