Repository: dexidp/dex
Pull request #2812 (2023-02-03 15:53:45)

DEP for RFC 8693 OAuth 2 Token Exchange
<!--
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

DEP for RFC 8693 OAuth 2.0 Token Exchange / grant_type urn:ietf:params:oauth:grant-type:token-exchange

#### What this PR does / why we need it

Discussion / design requested before implementation

#### Special notes for your reviewer

#### Does this PR introduce a user-facing change?

<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".
-->

```release-note
NONE
```



Patch:
diff --git a/.github/workflows/artifacts.yaml b/.github/workflows/artifacts.yaml
index f0c5fde6..c8b2e0db 100644
--- a/.github/workflows/artifacts.yaml
+++ b/.github/workflows/artifacts.yaml
@@ -83,7 +83,7 @@ jobs:
           labels: ${{ steps.meta.outputs.labels }}
 
       - name: Run Trivy vulnerability scanner
-        uses: aquasecurity/trivy-action@0.9.1
+        uses: aquasecurity/trivy-action@0.9.2
         with:
           image-ref: "ghcr.io/dexidp/dex:${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}"
           format: "sarif"
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 9b136d1a..1da2420f 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -64,22 +64,28 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v3
         with:
-          go-version: 1.19
+          go-version: "1.20"
 
       - name: Checkout code
         uses: actions/checkout@v3
 
+      - name: Download tool dependencies
+        run: make deps
+
+      # Ensure that generated files were committed.
+      # It can help us determine, that the code is in the intermediate state, which should not be tested.
+      # Thus, heavy jobs like creating a kind cluster and testing / linting will be skipped.
+      - name: Verify
+        run: make verify
+
       - name: Start services
         run: docker-compose -f docker-compose.test.yaml up -d
 
       - name: Create kind cluster
         uses: helm/kind-action@v1.5.0
         with:
-          version: v0.11.1
-          node_image: kindest/node:v1.19.11@sha256:07db187ae84b4b7de440a73886f008cf903fcf5764ba8106a9fd5243d6f32729
-
-      - name: Download tool dependencies
-        run: make deps
+          version: "v0.17.0"
+          node_image: "kindest/node:v1.25.3@sha256:cd248d1438192f7814fbca8fede13cfe5b9918746dfa12583976158a834fd5c5"
 
       - name: Test
         run: make testall
@@ -123,7 +129,3 @@ jobs:
 
       - name: Lint
         run: make lint
-
-      # Ensure that generated files were committed.
-      - name: Verify
-        run: make verify
diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml
index b3e36395..d6f24b73 100644
--- a/.github/workflows/docker.yaml
+++ b/.github/workflows/docker.yaml
@@ -96,7 +96,7 @@ jobs:
             org.opencontainers.image.documentation=https://dexidp.io/docs/
 
       - name: Run Trivy vulnerability scanner
-        uses: aquasecurity/trivy-action@0.9.1
+        uses: aquasecurity/trivy-action@0.9.2
         with:
           image-ref: "ghcr.io/dexidp/dex:${{ steps.tags.outputs.version }}"
           format: "template"
diff --git a/.golangci.yml b/.golangci.yml
index 8d602489..4ed48c7c 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,5 +1,7 @@
 run:
     timeout: 4m
+    skip-dirs:
+        - storage/ent/db # generated ent code
 
 linters-settings:
     depguard:
@@ -18,7 +20,6 @@ linters-settings:
 linters:
     disable-all: true
     enable:
-        - bodyclose
         - depguard
         - dogsled
         - exhaustive
@@ -38,16 +39,19 @@ linters:
         - nolintlint
         - prealloc
         - revive
-        - rowserrcheck
         - sqlclosecheck
         - staticcheck
         - stylecheck
-        - tparallel
         - unconvert
-        - unparam
         - unused
         - whitespace
 
+        # Disable temporarily until everything works with Go 1.20
+        # - bodyclose
+        # - rowserrcheck
+        # - tparallel
+        # - unparam
+
         # Disable temporarily until everything works with Go 1.18
         - typecheck
 
diff --git a/Dockerfile b/Dockerfile
index 67e40850..49871636 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
 ARG BASE_IMAGE=alpine
 
-FROM golang:1.20.0-alpine3.16 AS builder
+FROM golang:1.20.2-alpine3.16 AS builder
 
 WORKDIR /usr/local/src/dex
 
@@ -34,7 +34,7 @@ ARG TARGETOS
 ARG TARGETARCH
 ARG TARGETVARIANT
 
-ENV GOMPLATE_VERSION=v3.11.3
+ENV GOMPLATE_VERSION=v3.11.4
 
 RUN wget -O /usr/local/bin/gomplate \
     "https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${TARGETOS:-linux}-${TARGETARCH:-amd64}${TARGETVARIANT}" \
diff --git a/Makefile b/Makefile
index 89dbf8c6..c1aea638 100644
--- a/Makefile
+++ b/Makefile
@@ -1,47 +1,48 @@
 OS = $(shell uname | tr A-Z a-z)
-
 export PATH := $(abspath bin/protoc/bin/):$(abspath bin/):${PATH}
 
-PROJ=dex
-ORG_PATH=github.com/dexidp
-REPO_PATH=$(ORG_PATH)/$(PROJ)
-
-VERSION ?= $(shell ./scripts/git-version)
-
-DOCKER_REPO=quay.io/dexidp/dex
-DOCKER_IMAGE=$(DOCKER_REPO):$(VERSION)
+user=$(shell id -u -n)
+group=$(shell id -g -n)
 
 $( shell mkdir -p bin )
 
-user=$(shell id -u -n)
-group=$(shell id -g -n)
+PROJ      = dex
+ORG_PATH  = github.com/dexidp
+REPO_PATH = $(ORG_PATH)/$(PROJ)
+VERSION  ?= $(shell ./scripts/git-version)
 
-export GOBIN=$(PWD)/bin
 
+export GOBIN=$(PWD)/bin
 LD_FLAGS="-w -X main.version=$(VERSION)"
 
 # Dependency versions
+GOLANGCI_VERSION   = 1.51.2
+GOTESTSUM_VERSION ?= 1.7.0
 
-KIND_NODE_IMAGE = "kindest/node:v1.19.11@sha256:07db187ae84b4b7de440a73886f008cf903fcf5764ba8106a9fd5243d6f32729"
-KIND_TMP_DIR = "$(PWD)/bin/test/dex-kind-kubeconfig"
+PROTOC_VERSION             = 3.15.6
+PROTOC_GEN_GO_VERSION      = 1.26.0
+PROTOC_GEN_GO_GRPC_VERSION = 1.1.0
 
-build: bin/dex
+KIND_VERSION    = 0.17.0
+KIND_NODE_IMAGE = "kindest/node:v1.25.3@sha256:cd248d1438192f7814fbca8fede13cfe5b9918746dfa12583976158a834fd5c5"
+KIND_TMP_DIR    = "$(PWD)/bin/test/dex-kind-kubeconfig"
 
-verify: verify-proto verify-ent
 
-bin/dex:
-	@mkdir -p bin/
-	@go install -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex
+##@ Build
 
-.PHONY: ent-generate
-ent-generate:
-	@go generate $(REPO_PATH)/storage/ent/
+build: bin/dex ## Build Dex binaries.
 
-.PHONY: verify-ent
-verify-ent: ent-generate
-	@./scripts/git-diff
+examples: bin/grpc-client bin/example-app ## Build example app.
 
-examples: bin/grpc-client bin/example-app
+.PHONY: release-binary
+release-binary: LD_FLAGS = "-w -X main.version=$(VERSION) -extldflags \"-static\""
+release-binary: ## Build release binaries (used to build a final container image).
+	@go build -o /go/bin/dex -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex
+	@go build -o /go/bin/docker-entrypoint -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/docker-entrypoint
+
+bin/dex:
+	@mkdir -p bin/
+	@go install -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex
 
 bin/grpc-client:
 	@mkdir -p bin/
@@ -51,95 +52,29 @@ bin/example-app:
 	@mkdir -p bin/
 	@cd examples/ && go install -v -ldflags $(LD_FLAGS) $(REPO_PATH)/examples/example-app
 
-.PHONY: release-binary
-release-binary: LD_FLAGS = "-w -X main.version=$(VERSION) -extldflags \"-static\""
-release-binary:
-	@go build -o /go/bin/dex -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex
-	@go build -o /go/bin/docker-entrypoint -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/docker-entrypoint
-
-docker-compose.override.yaml:
-	cp docker-compose.override.yaml.dist docker-compose.override.yaml
-
-.PHONY: up
-up: docker-compose.override.yaml ## Launch the development environment
-	@ if [ docker-compose.override.yaml -ot docker-compose.override.yaml.dist ]; then diff -u docker-compose.override.yaml docker-compose.override.yaml.dist || (echo "!!! The distributed docker-compose.override.yaml example changed. Please update your file accordingly (or at least touch it). !!!" && false); fi
-	docker-compose up -d
 
-.PHONY: down
-down: clear ## Destroy the development environment
-	docker-compose down --volumes --remove-orphans --rmi local
-
-test:
-	@go test -v ./...
-
-testrace:
-	@go test -v --race ./...
+##@ Generate
 
-.PHONY: kind-up kind-down kind-tests
-kind-up:
-	@mkdir -p bin/test
-	@kind create cluster --image ${KIND_NODE_IMAGE} --kubeconfig ${KIND_TMP_DIR}
-
-kind-down:
-	@kind delete cluster
-	rm ${KIND_TMP_DIR}
-
-kind-tests: export DEX_KUBERNETES_CONFIG_PATH=${KIND_TMP_DIR}
-kind-tests: testall
-
-.PHONY: lint lint-fix
-lint: ## Run linter
-	golangci-lint run
+.PHONY: generate
+generate: generate-proto generate-proto-internal generate-ent go-mod-tidy ## Run all generators.
 
-.PHONY: fix
-fix: ## Fix lint violations
-	golangci-lint run --fix
-
-.PHONY: docker-image
-docker-image:
-	@sudo docker build -t $(DOCKER_IMAGE) .
-
-.PHONY: verify-proto
-verify-proto: proto
-	@./scripts/git-diff
-
-clean:
-	@rm -rf bin/
-
-testall: testrace
-
-FORCE:
-
-.PHONY: test testrace testall
+.PHONY: generate-ent
+generate-ent: ## Generate code for database ORM.
+	@go generate $(REPO_PATH)/storage/ent/
 
-.PHONY: proto
-proto:
+.PHONY: generate-proto
+generate-proto: ## Generate the Dex client's protobuf code.
 	@protoc --go_out=paths=source_relative:. --go-grpc_out=paths=source_relative:. api/v2/*.proto
 	@protoc --go_out=paths=source_relative:. --go-grpc_out=paths=source_relative:. api/*.proto
-	#@cp api/v2/*.proto api/
 
-.PHONY: proto-internal
-proto-internal:
+.PHONY: generate-proto-internal
+generate-proto-internal: ## Generate protobuf code for token encoding.
 	@protoc --go_out=paths=source_relative:. server/internal/*.proto
 
-# Dependency versions
-GOLANGCI_VERSION = 1.50.1
-GOTESTSUM_VERSION ?= 1.7.0
-PROTOC_VERSION = 3.15.6
-PROTOC_GEN_GO_VERSION = 1.26.0
-PROTOC_GEN_GO_GRPC_VERSION = 1.1.0
-KIND_VERSION = 0.11.1
-
-deps: bin/gotestsum bin/golangci-lint bin/protoc bin/protoc-gen-go bin/protoc-gen-go-grpc bin/kind
-
-bin/gotestsum:
-	@mkdir -p bin
-	curl -L https://github.com/gotestyourself/gotestsum/releases/download/v${GOTESTSUM_VERSION}/gotestsum_${GOTESTSUM_VERSION}_$(shell uname | tr A-Z a-z)_amd64.tar.gz | tar -zOxf - gotestsum > ./bin/gotestsum
-	@chmod +x ./bin/gotestsum
-
-bin/golangci-lint:
-	@mkdir -p bin
-	curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | BINARY=golangci-lint bash -s -- v${GOLANGCI_VERSION}
+go-mod-tidy: ## Run go mod tidy for all targets.
+	@go mod tidy
+	@cd examples/ && go mod tidy
+	@cd api/v2/ && go mod tidy
 
 bin/protoc:
 	@mkdir -p bin/protoc
@@ -162,7 +97,112 @@ bin/protoc-gen-go-grpc:
 	curl -L https://github.com/grpc/grpc-go/releases/download/cmd/protoc-gen-go-grpc/v${PROTOC_GEN_GO_GRPC_VERSION}/protoc-gen-go-grpc.v${PROTOC_GEN_GO_GRPC_VERSION}.$(shell uname | tr A-Z a-z).amd64.tar.gz | tar -zOxf - ./protoc-gen-go-grpc > ./bin/protoc-gen-go-grpc
 	@chmod +x ./bin/protoc-gen-go-grpc
 
+##@ Verify
+
+verify: generate ## Verify that all the code was generated and committed to repository.
+	@./scripts/git-diff
+
+.PHONY: verify-proto
+verify-proto: generate-proto ## Verify that the Dex client's protobuf code was generated.
+	@./scripts/git-diff
+
+.PHONY: verify-proto
+verify-proto-internal: generate-proto-internal ## Verify internal protobuf code for token encoding was generated.
+	@./scripts/git-diff
+
+.PHONY: verify-ent
+verify-ent: generate-ent ## Verify code for database ORM was generated.
+	@./scripts/git-diff
+
+.PHONY: verify-go-mod
+verify-go-mod: go-mod-tidy ## Check that go.mod and go.sum formatted according to the changes.
+	@./scripts/git-diff
+
+##@ Test and Lint
+
+deps: bin/gotestsum bin/golangci-lint bin/protoc bin/protoc-gen-go bin/protoc-gen-go-grpc bin/kind ## Install dev dependencies.
+
+.PHONY: test testrace testall
+test: ## Test go code.
+	@go test -v ./...
+
+testrace: ## Test go code and check for possible race conditions.
+	@go test -v --race ./...
+
+testall: testrace ## Run all tests for go code.
+
+.PHONY: lint lint-fix
+lint: ## Run linter.
+	@golangci-lint version
+	@golangci-lint run
+
+.PHONY: fix
+fix: ## Fix lint violations.
+	@golangci-lint version
+	@golangci-lint run --fix
+
+docker-compose.override.yaml:
+	cp docker-compose.override.yaml.dist docker-compose.override.yaml
+
+.PHONY: up
+up: docker-compose.override.yaml ## Launch the development environment.
+	@ if [ docker-compose.override.yaml -ot docker-compose.override.yaml.dist ]; then diff -u docker-compose.override.yaml docker-compose.override.yaml.dist || (echo "!!! The distributed docker-compose.override.yaml example changed. Please update your file accordingly (or at least touch it). !!!" && false); fi
+	docker-compose up -d
+
+.PHONY: down
+down: clear ## Destroy the development environment.
+	docker-compose down --volumes --remove-orphans --rmi local
+
+.PHONY: kind-up kind-down kind-tests
+kind-up: ## Create a kind cluster.
+	@mkdir -p bin/test
+	@kind create cluster --image ${KIND_NODE_IMAGE} --kubeconfig ${KIND_TMP_DIR} --name dex-tests
+
+kind-tests: export DEX_KUBERNETES_CONFIG_PATH=${KIND_TMP_DIR}
+kind-tests: testall ## Run test on kind cluster (kind cluster must be created).
+
+kind-down: ## Delete the kind cluster.
+	@kind delete cluster --name dex-tests
+	rm ${KIND_TMP_DIR}
+
+bin/golangci-lint:
+	@mkdir -p bin
+	curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | BINARY=golangci-lint bash -s -- v${GOLANGCI_VERSION}
+
+bin/gotestsum:
+	@mkdir -p bin
+	curl -L https://github.com/gotestyourself/gotestsum/releases/download/v${GOTESTSUM_VERSION}/gotestsum_${GOTESTSUM_VERSION}_$(shell uname | tr A-Z a-z)_amd64.tar.gz | tar -zOxf - gotestsum > ./bin/gotestsum
+	@chmod +x ./bin/gotestsum
+
 bin/kind:
 	@mkdir -p bin
 	curl -L https://github.com/kubernetes-sigs/kind/releases/download/v${KIND_VERSION}/kind-$(shell uname | tr A-Z a-z)-amd64 > ./bin/kind
 	@chmod +x ./bin/kind
+
+##@ Clean
+clean: ## Delete all builds and downloaded dependencies.
+	@rm -rf bin/
+
+
+FORMATTING_BEGIN_YELLOW = \033[0;33m
+FORMATTING_BEGIN_BLUE = \033[36m
+FORMATTING_END = \033[0m
+
+.PHONY: help
+help:
+	@printf -- "${FORMATTING_BEGIN_BLUE}%s${FORMATTING_END}\n" \
+	"" \
+	"       ___             " \
+	"      / _ \_____ __    " \
+	"     / // / -_) \ /    " \
+	"    /____/\__/_\_\     " \
+	"" \
+	"-----------------------" \
+	""
+	@awk 'BEGIN {\
+	    FS = ":.*##"; \
+	    printf                "Usage: ${FORMATTING_BEGIN_BLUE}OPTION${FORMATTING_END}=<value> make ${FORMATTING_BEGIN_YELLOW}<target>${FORMATTING_END}\n"\
+	  } \
+	  /^[a-zA-Z0-9_-]+:.*?##/ { printf "  ${FORMATTING_BEGIN_BLUE}%-46s${FORMATTING_END} %s\n", $$1, $$2 } \
+	  /^.?.?##~/              { printf "   %-46s${FORMATTING_BEGIN_YELLOW}%-46s${FORMATTING_END}\n", "", substr($$1, 6) } \
+	  /^##@/                  { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
diff --git a/connector/openshift/openshift.go b/connector/openshift/openshift.go
index 35ee33af..99d1b5b2 100644
--- a/connector/openshift/openshift.go
+++ b/connector/openshift/openshift.go
@@ -82,9 +82,13 @@ func (c *Config) OpenWithHTTPClient(id string, logger log.Logger,
 	httpClient *http.Client,
 ) (conn connector.Connector, err error) {
 	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
 
 	wellKnownURL := strings.TrimSuffix(c.Issuer, "/") + wellKnownURLPath
 	req, err := http.NewRequest(http.MethodGet, wellKnownURL, nil)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create a request to OpenShift endpoint %w", err)
+	}
 
 	openshiftConnector := openshiftConnector{
 		apiURL:       c.Issuer,
@@ -106,14 +110,12 @@ func (c *Config) OpenWithHTTPClient(id string, logger log.Logger,
 
 	resp, err := openshiftConnector.httpClient.Do(req.WithContext(ctx))
 	if err != nil {
-		cancel()
 		return nil, fmt.Errorf("failed to query OpenShift endpoint %w", err)
 	}
 
 	defer resp.Body.Close()
 
 	if err := json.NewDecoder(resp.Body).Decode(&metadata); err != nil {
-		cancel()
 		return nil, fmt.Errorf("discovery through endpoint %s failed to decode body: %w",
 			wellKnownURL, err)
 	}
diff --git a/docs/enhancements/token-exchange-2023-02-03-#2812.md b/docs/enhancements/token-exchange-2023-02-03-#2812.md
new file mode 100644
index 00000000..33d80b8e
--- /dev/null
+++ b/docs/enhancements/token-exchange-2023-02-03-#2812.md
@@ -0,0 +1,175 @@
+# Dex Enhancement Proposal (DEP) 2812 - 2023-02-03 - Token Exchange
+
+## Table of Contents
+
+- [Summary](#summary)
+- [Motivation](#motivation)
+    - [Goals/Pain](#goals)
+    - [Non-Goals](#non-goals)
+- [Proposal](#proposal)
+    - [User Experience](#user-experience)
+    - [Implementation Details/Notes/Constraints](#implementation-detailsnotesconstraints)
+    - [Risks and Mitigations](#risks-and-mitigations)
+    - [Alternatives](#alternatives)
+- [Future Improvements](#future-improvements)
+
+## Summary
+
+[RFC 8693] specifies a new OAuth2 `grant_type` of `urn:ietf:params:oauth:grant-type:token-exchange`.
+Using this grant type, when clients start an authentication flow with Dex,
+in lieu of being redirected to their upstream IDP for authentication on demand,
+clients can present an independently obtained, valid token from their IDP to Dex.
+This is primarily useful in fully automated environments with job/machine identities,
+where there is no human in the loop to handle browser-based login flows.
+This DEP proposes to implement the new grant type for Dex.
+
+[RFC 8693]: https://www.rfc-editor.org/rfc/rfc8693.html
+
+## Context
+
+- [#1668 Question: non-web based clients?]
+  was closed with no real resolution
+- [#1484 Token exchange for external tokens]
+  mentions that Keycloak has a similar capability
+- [#2657 Get OIDC token issued by Dex using a token issued by one of the connectors] 
+  is similar to the previous issue, but this time links to the new (January 2020) [RFC 8693].
+
+I believe the context for all of these are similar:
+a downstream project using Dex as its only IDP wants to grant access to programmatic clients
+without issuing long lived API tokens.
+
+Examples of downstream issues:
+
+- [argoproj/argo-cd#11632 Argocd SSO login via Azure AD Auth using OIDC not work for cli sso login]
+
+Other related Dex issues:
+
+- [#2450 Non-OIDC JWT Connector] is a functionally similar request, but expanded to arbitrary JWTs
+- [#1225 GitHub Non-Web application flow support] also asks for an exchange, but for an opaque Github PAT
+
+More broadly, this fits into recent movements to issue machine identities:
+
+- [GCP Service Identity](https://cloud.google.com/run/docs/securing/service-identity)
+- [AWS Execution Role](https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html)
+- [Github Actions OIDC](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
+- [CircleCI OIDC](https://circleci.com/docs/openid-connect-tokens/)
+- [Kubernetes Service Accounts](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/)
+- [SPIFFE](https://spiffe.io/)
+
+and granting access to resources based on trusting federated identities:
+
+- [GCP Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation)
+- [AWS STS AssumeRoleWithWebIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html)
+
+[#1484 Token exchange for external tokens]: https://github.com/dexidp/dex/issues/1484
+[#1668 Question: non-web based clients?]: https://github.com/dexidp/dex/issues/1668
+[#2657 Get OIDC token issued by Dex using a token issued by one of the connectors]: https://github.com/dexidp/dex/issues/2657
+[argoproj/argo-cd#11632 Argocd SSO login via Azure AD Auth using OIDC not work for cli sso login]: https://github.com/argoproj/argo-cd/issues/11632
+[#2450 Non-OIDC JWT Connector]: https://github.com/dexidp/dex/issues/2450
+[#1225 GitHub Non-Web application flow support]: https://github.com/dexidp/dex/issues/1225
+
+An initial attempt is at [#2806](https://github.com/dexidp/dex/pull/2806)
+
+## Motivation
+
+### Goals/Pain
+
+The goal is to allow programmatic access to Dex-protected resources 
+without the use of static/long-lived secret tokens (API keys, username/password)
+or web-based redirect flows.
+Such scenarios are common in CI/CD workflows,
+and in general automation of common tasks.
+
+### Non-goals
+
+- Work will be scoped to just the OIDC connector
+- [RFC 8693 Section 2.1.1. Relationship between Resource, Audience, and Scope]
+  details more complex authorization checks based on targeted resources.
+  This is considered out of scope.
+
+[RFC 8693 Section 2.1.1. Relationship between Resource, Audience, and Scope]: https://www.rfc-editor.org/rfc/rfc8693.html#name-relationship-between-resour
+
+## Proposal
+
+### User Experience
+
+Clients can make `POST` requests with `application/x-www-form-urlencoded` 
+parameters as specified by [RFC 8693] to Dex's `/token` endpoint.
+If successful, an access token will be returned,
+allowing direct authentication with Dex.
+No refresh tokens will be issued,
+perform a new exchange (possibly with refreshed upstream tokens) to obtain a new access token.
+
+The request parameters from [RFC 8693 Section 2.1](https://www.rfc-editor.org/rfc/rfc8693.html#name-request):
+
+- `grant_type`: REQUIRED - `urn:ietf:params:oauth:grant-type:token-exchange`
+- `resource`: OPTIONAL - the `audience` in the issued Dex token
+- `audience`: REQUIRED (RFC OPTIONAL) - the connector to verify the provided token against
+- `scope`: OPTIONAL - the `scope` in the issued Dex token
+- `requested_token_type`: OPTIONAL - one of `urn:ietf:params:oauth:token-type:access_token` or `urn:ietf:params:oauth:token-type:id_token`, defaulting to access token
+- `subject_token`: REQUIRED - the token issued by the upstream IDP
+- `subject_token_type`: REQUIRED - `urn:ietf:params:oauth:token-type:id_token` or `urn:ietf:params:oauth:token-type:access_token` if `getUserInfo` is `true`.
+- `actor_token`: OPTIONAL - unused
+- `actor_token_type`: OPTIONAL - unused
+
+The response parameters from [RFC 8693 Section 2.2](https://www.rfc-editor.org/rfc/rfc8693.html#name-response):
+
+- `access_token`: the issued token, the field is called `access_token` for legacy reasons
+- `issued_token_type`: the actual type of the issued token
+- `token_type`: the value `Bearer`
+- `expires_in`: validity lifetime in seconds
+- `scope`: the requested scope
+- `refresh_token`: unused
+
+The connector only needs to be configured with an issuer,
+no client ID / client secrets are necessary
+
+```yaml
+connectors:
+- type: oidc
+  id: my-platform
+  name: My Platform
+  config:
+    issuer: https://oidc.my-platform.example/
+```
+
+We expose a global and connector setting, 
+`allowedGrantTypes: []string` defaulting to all implemented types.
+
+### Implementation Details/Notes/Constraints
+
+- Connectors expose a new interface `TokenIdentity` that will verify the given token and return the associated identity.
+  A Dex access/id token is then minted for the given identity.
+
+- `actor_token` and `actor_token_type` are "MUST ... if the actor token is present, 
+  also perform the appropriate validation procedures for its indicated token type".
+  We will ignore these fields for the initial implementation.
+
+
+### Risks and Mitigations
+
+With token exchanges (sometimes known as identity impersonation), 
+is they allow for easier lateral movement if an attacker gains access to an upstream token.
+We limit the potential impact by not issuing refresh tokens, preventing persistent access.
+Combined with short token lifetimes, it should limit the period of time between authentication to upstream IDPs.
+Additionally, a new `allowedGrantTypes` would allow for disabling exchanges if the functionality isn't needed.
+
+### Alternatives
+
+- Continue to use static keys - 
+  this is a secret management nightmare 
+  and quite painful when client storage of keys is [breached](https://circleci.com/blog/january-4-2023-security-alert/)
+
+## Future Improvements
+
+- Other connectors may wish to implement the same capability under Oauth
+- The password connector could be switch to support this new endpoint, submitting passwords as access tokens,
+  allowing for multiple password connectors to be configured
+- The `audience` field could be made optional if there is a single connector or the id token is inspected for issuer url
+- The `actor_token` and `actor_token_type` can be checked / validated if a suitable usecase is determined.
+- A policy language like [cel] or [rego] as mentioned on [#1635 Connector Middleware] 
+  would allow for stronger assertions of the provided identity against requested resource access.
+
+[cel]: https://github.com/google/cel-go
+[rego]: https://www.openpolicyagent.org/docs/latest/policy-language/
+[#1635 Connector Middleware]: https://github.com/dexidp/dex/issues/1635
diff --git a/examples/go.mod b/examples/go.mod
index d66c118a..0b6a90da 100644
--- a/examples/go.mod
+++ b/examples/go.mod
@@ -15,9 +15,9 @@ require (
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce // indirect
-	golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d // indirect
-	golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
-	golang.org/x/text v0.3.7 // indirect
+	golang.org/x/net v0.7.0 // indirect
+	golang.org/x/sys v0.5.0 // indirect
+	golang.org/x/text v0.7.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0 // indirect
 	google.golang.org/protobuf v1.27.1 // indirect
diff --git a/examples/go.sum b/examples/go.sum
index 7907afde..bb153ce6 100644
--- a/examples/go.sum
+++ b/examples/go.sum
@@ -332,6 +332,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
 go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
 go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs=
@@ -356,6 +357,7 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce h1:Roh6XWxHFKrPgC/EQhVubSAGQ6Ozk6IdxHSzt1mR0EI=
 golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -394,6 +396,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 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-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -437,8 +440,9 @@ golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy
 golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d h1:1n1fc535VhN8SYtD4cDUyNlfpAF2ROMM9+11equK3hs=
-golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -468,6 +472,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -533,9 +538,13 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0=
-golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -544,8 +553,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -603,6 +613,7 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/go.mod b/go.mod
index 0b51fe18..4e8a3d36 100644
--- a/go.mod
+++ b/go.mod
@@ -1,9 +1,9 @@
 module github.com/dexidp/dex
 
-go 1.19
+go 1.20
 
 require (
-	entgo.io/ent v0.11.8
+	entgo.io/ent v0.11.9
 	github.com/AppsFlyer/go-sundheit v0.5.0
 	github.com/Masterminds/semver v1.5.0
 	github.com/Masterminds/sprig/v3 v3.2.3
@@ -27,12 +27,12 @@ require (
 	github.com/russellhaering/goxmldsig v1.2.0
 	github.com/sirupsen/logrus v1.9.0
 	github.com/spf13/cobra v1.6.1
-	github.com/stretchr/testify v1.8.1
+	github.com/stretchr/testify v1.8.2
 	go.etcd.io/etcd/client/pkg/v3 v3.5.7
 	go.etcd.io/etcd/client/v3 v3.5.7
-	golang.org/x/crypto v0.6.0
+	golang.org/x/crypto v0.7.0
 	golang.org/x/exp v0.0.0-20221004215720-b9f4876ce741
-	golang.org/x/net v0.7.0
+	golang.org/x/net v0.8.0
 	golang.org/x/oauth2 v0.4.0
 	google.golang.org/api v0.108.0
 	google.golang.org/grpc v1.52.0
@@ -41,7 +41,7 @@ require (
 )
 
 require (
-	ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb // indirect
+	ariga.io/atlas v0.9.1 // indirect
 	cloud.google.com/go/compute v1.14.0 // indirect
 	cloud.google.com/go/compute/metadata v0.2.3 // indirect
 	github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect
@@ -86,9 +86,9 @@ 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.7.0 // indirect
-	golang.org/x/sys v0.5.0 // indirect
-	golang.org/x/text v0.7.0 // indirect
+	golang.org/x/mod v0.8.0 // indirect
+	golang.org/x/sys v0.6.0 // indirect
+	golang.org/x/text v0.8.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
diff --git a/go.sum b/go.sum
index 696983f6..d8525660 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb h1:mbsFtavDqGdYwdDpP50LGOOZ2hgyGoJcZeOpbgKMyu4=
-ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb/go.mod h1:T230JFcENj4ZZzMkZrXFDSkv+2kXkUgpJ5FQQ5hMcKU=
+ariga.io/atlas v0.9.1 h1:EpoPMnwsQG0vn9c0sYExpwSYtr7bvuSUXzQclU2pMjc=
+ariga.io/atlas v0.9.1/go.mod h1:T230JFcENj4ZZzMkZrXFDSkv+2kXkUgpJ5FQQ5hMcKU=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -40,8 +40,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-entgo.io/ent v0.11.8 h1:M/M0QL1CYCUSdqGRXUrXhFYSDRJPsOOrr+RLEej/gyQ=
-entgo.io/ent v0.11.8/go.mod h1:ericBi6Q8l3wBH1wEIDfKxw7rcQEuRPyBfbIzjtxJ18=
+entgo.io/ent v0.11.9 h1:dbbCkAiPVTRBIJwoZctiSYjB7zxQIBOzVSU5H9VYIQI=
+entgo.io/ent v0.11.9/go.mod h1:KWHOcDZn1xk3mz3ipWdKrQpMvwqa/9B69TUuAPP9W6g=
 github.com/AppsFlyer/go-sundheit v0.5.0 h1:/VxpyigCfJrq1r97mn9HPiAB2qrhcTFHwNIIDr15CZM=
 github.com/AppsFlyer/go-sundheit v0.5.0/go.mod h1:2ZM0BnfqT/mljBQO224VbL5XH06TgWuQ6Cn+cTtCpTY=
 github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU=
@@ -243,7 +243,6 @@ github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
 github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
 github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
-github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
 github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
 github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
@@ -263,7 +262,6 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
 github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
-github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -334,8 +332,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
 github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -374,8 +373,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.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
-golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
-golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -409,8 +408,8 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
 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.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
-golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -450,8 +449,8 @@ 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.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
 golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
-golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
-golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -517,8 +516,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
 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.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 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=
@@ -533,8 +532,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -581,7 +580,6 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc
 golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57 h1:/X0t/E4VxbZE7MLS7auvE7YICHeVvbIa9vkOVvYW/24=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/server/handlers.go b/server/handlers.go
index 11dcdd07..f25d52f5 100755
--- a/server/handlers.go
+++ b/server/handlers.go
@@ -372,13 +372,24 @@ func (s *Server) handlePasswordLogin(w http.ResponseWriter, r *http.Request) {
 			}
 			return
 		}
-		redirectURL, err := s.finalizeLogin(identity, authReq, conn.Connector)
+		redirectURL, canSkipApproval, err := s.finalizeLogin(identity, authReq, conn.Connector)
 		if err != nil {
 			s.logger.Errorf("Failed to finalize login: %v", err)
 			s.renderError(r, w, http.StatusInternalServerError, "Login error.")
 			return
 		}
 
+		if canSkipApproval {
+			authReq, err = s.storage.GetAuthRequest(authReq.ID)
+			if err != nil {
+				s.logger.Errorf("Failed to get finalized auth request: %v", err)
+				s.renderError(r, w, http.StatusInternalServerError, "Login error.")
+				return
+			}
+			s.sendCodeResponse(w, r, authReq)
+			return
+		}
+
 		http.Redirect(w, r, redirectURL, http.StatusSeeOther)
 	default:
 		s.renderError(r, w, http.StatusBadRequest, "Unsupported request method.")
@@ -460,19 +471,30 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	redirectURL, err := s.finalizeLogin(identity, authReq, conn.Connector)
+	redirectURL, canSkipApproval, err := s.finalizeLogin(identity, authReq, conn.Connector)
 	if err != nil {
 		s.logger.Errorf("Failed to finalize login: %v", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Login error.")
 		return
 	}
 
+	if canSkipApproval {
+		authReq, err = s.storage.GetAuthRequest(authReq.ID)
+		if err != nil {
+			s.logger.Errorf("Failed to get finalized auth request: %v", err)
+			s.renderError(r, w, http.StatusInternalServerError, "Login error.")
+			return
+		}
+		s.sendCodeResponse(w, r, authReq)
+		return
+	}
+
 	http.Redirect(w, r, redirectURL, http.StatusSeeOther)
 }
 
 // finalizeLogin associates the user's identity with the current AuthRequest, then returns
 // the approval page's path.
-func (s *Server) finalizeLogin(identity connector.Identity, authReq storage.AuthRequest, conn connector.Connector) (string, error) {
+func (s *Server) finalizeLogin(identity connector.Identity, authReq storage.AuthRequest, conn connector.Connector) (string, bool, error) {
 	claims := storage.Claims{
 		UserID:            identity.UserID,
 		Username:          identity.Username,
@@ -489,7 +511,7 @@ func (s *Server) finalizeLogin(identity connector.Identity, authReq storage.Auth
 		return a, nil
 	}
 	if err := s.storage.UpdateAuthRequest(authReq.ID, updater); err != nil {
-		return "", fmt.Errorf("failed to update auth request: %v", err)
+		return "", false, fmt.Errorf("failed to update auth request: %v", err)
 	}
 
 	email := claims.Email
@@ -500,7 +522,10 @@ func (s *Server) finalizeLogin(identity connector.Identity, authReq storage.Auth
 	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)
 
-	// TODO: if s.skipApproval or !authReq.ForceApprovalPrompt, we can skip the redirect to /approval and go ahead and send code
+	// we can skip the redirect to /approval and go ahead and send code if it's not required
+	if s.skipApproval || !authReq.ForceApprovalPrompt {
+		return "", true, nil
+	}
 
 	// an HMAC is used here to ensure that the request ID is unpredictable, ensuring that an attacker who intercepted the original
 	// flow would be unable to poll for the result at the /approval endpoint
@@ -511,7 +536,7 @@ func (s *Server) finalizeLogin(identity connector.Identity, authReq storage.Auth
 	returnURL := path.Join(s.issuerURL.Path, "/approval") + "?req=" + authReq.ID + "&hmac=" + base64.RawURLEncoding.EncodeToString(mac)
 	_, ok := conn.(connector.RefreshConnector)
 	if !ok {
-		return returnURL, nil
+		return returnURL, false, nil
 	}
 
 	// Try to retrieve an existing OfflineSession object for the corresponding user.
@@ -519,7 +544,7 @@ func (s *Server) finalizeLogin(identity connector.Identity, authReq storage.Auth
 	if err != nil {
 		if err != storage.ErrNotFound {
 			s.logger.Errorf("failed to get offline session: %v", err)
-			return "", err
+			return "", false, err
 		}
 		offlineSessions := storage.OfflineSessions{
 			UserID:        identity.UserID,
@@ -532,10 +557,10 @@ func (s *Server) finalizeLogin(identity connector.Identity, authReq storage.Auth
 		// the newly received refreshtoken.
 		if err := s.storage.CreateOfflineSessions(offlineSessions); err != nil {
 			s.logger.Errorf("failed to create offline session: %v", err)
-			return "", err
+			return "", false, err
 		}
 
-		return returnURL, nil
+		return returnURL, false, nil
 	}
 
 	// Update existing OfflineSession obj with new RefreshTokenRef.
@@ -546,10 +571,10 @@ func (s *Server) finalizeLogin(identity connector.Identity, authReq storage.Auth
 		return old, nil
 	}); err != nil {
 		s.logger.Errorf("failed to update offline session: %v", err)
-		return "", err
+		return "", false, err
 	}
 
-	return returnURL, nil
+	return returnURL, false, nil
 }
 
 func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
@@ -588,6 +613,8 @@ func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
 
 	switch r.Method {
 	case http.MethodGet:
+		// TODO: `finalizeLogin()` now sends code directly to client without going through this endpoint,
+		//		 the `if skipApproval { ... }` block needs to be removed after a grace period.
 		if s.skipApproval {
 			s.sendCodeResponse(w, r, authReq)
 			return
diff --git a/storage/ent/db/client.go b/storage/ent/db/client.go
index b3a2bd30..ddbab826 100644
--- a/storage/ent/db/client.go
+++ b/storage/ent/db/client.go
@@ -10,6 +10,9 @@ import (
 
 	"github.com/dexidp/dex/storage/ent/db/migrate"
 
+	"entgo.io/ent"
+	"entgo.io/ent/dialect"
+	"entgo.io/ent/dialect/sql"
 	"github.com/dexidp/dex/storage/ent/db/authcode"
 	"github.com/dexidp/dex/storage/ent/db/authrequest"
 	"github.com/dexidp/dex/storage/ent/db/connector"
@@ -20,9 +23,6 @@ import (
 	"github.com/dexidp/dex/storage/ent/db/offlinesession"
 	"github.com/dexidp/dex/storage/ent/db/password"
 	"github.com/dexidp/dex/storage/ent/db/refreshtoken"
-
-	"entgo.io/ent/dialect"
-	"entgo.io/ent/dialect/sql"
 )
 
 // Client is the client that holds all ent builders.
@@ -75,6 +75,55 @@ func (c *Client) init() {
 	c.RefreshToken = NewRefreshTokenClient(c.config)
 }
 
+type (
+	// config is the configuration for the client and its builder.
+	config struct {
+		// driver used for executing database requests.
+		driver dialect.Driver
+		// debug enable a debug logging.
+		debug bool
+		// log used for logging on debug mode.
+		log func(...any)
+		// hooks to execute on mutations.
+		hooks *hooks
+		// interceptors to execute on queries.
+		inters *inters
+	}
+	// Option function to configure the client.
+	Option func(*config)
+)
+
+// options applies the options on the config object.
+func (c *config) options(opts ...Option) {
+	for _, opt := range opts {
+		opt(c)
+	}
+	if c.debug {
+		c.driver = dialect.Debug(c.driver, c.log)
+	}
+}
+
+// Debug enables debug logging on the ent.Driver.
+func Debug() Option {
+	return func(c *config) {
+		c.debug = true
+	}
+}
+
+// Log sets the logging function for debug mode.
+func Log(fn func(...any)) Option {
+	return func(c *config) {
+		c.log = fn
+	}
+}
+
+// Driver configures the client driver.
+func Driver(driver dialect.Driver) Option {
+	return func(c *config) {
+		c.driver = driver
+	}
+}
+
 // Open opens a database/sql.DB specified by the driver name and
 // the data source name, and returns a new client attached to it.
 // Optional parameters can be added for configuring the client.
@@ -173,31 +222,23 @@ func (c *Client) Close() error {
 // Use adds the mutation hooks to all the entity clients.
 // In order to add hooks to a specific client, call: `client.Node.Use(...)`.
 func (c *Client) Use(hooks ...Hook) {
-	c.AuthCode.Use(hooks...)
-	c.AuthRequest.Use(hooks...)
-	c.Connector.Use(hooks...)
-	c.DeviceRequest.Use(hooks...)
-	c.DeviceToken.Use(hooks...)
-	c.Keys.Use(hooks...)
-	c.OAuth2Client.Use(hooks...)
-	c.OfflineSession.Use(hooks...)
-	c.Password.Use(hooks...)
-	c.RefreshToken.Use(hooks...)
+	for _, n := range []interface{ Use(...Hook) }{
+		c.AuthCode, c.AuthRequest, c.Connector, c.DeviceRequest, c.DeviceToken, c.Keys,
+		c.OAuth2Client, c.OfflineSession, c.Password, c.RefreshToken,
+	} {
+		n.Use(hooks...)
+	}
 }
 
 // Intercept adds the query interceptors to all the entity clients.
 // In order to add interceptors to a specific client, call: `client.Node.Intercept(...)`.
 func (c *Client) Intercept(interceptors ...Interceptor) {
-	c.AuthCode.Intercept(interceptors...)
-	c.AuthRequest.Intercept(interceptors...)
-	c.Connector.Intercept(interceptors...)
-	c.DeviceRequest.Intercept(interceptors...)
-	c.DeviceToken.Intercept(interceptors...)
-	c.Keys.Intercept(interceptors...)
-	c.OAuth2Client.Intercept(interceptors...)
-	c.OfflineSession.Intercept(interceptors...)
-	c.Password.Intercept(interceptors...)
-	c.RefreshToken.Intercept(interceptors...)
+	for _, n := range []interface{ Intercept(...Interceptor) }{
+		c.AuthCode, c.AuthRequest, c.Connector, c.DeviceRequest, c.DeviceToken, c.Keys,
+		c.OAuth2Client, c.OfflineSession, c.Password, c.RefreshToken,
+	} {
+		n.Intercept(interceptors...)
+	}
 }
 
 // Mutate implements the ent.Mutator interface.
@@ -1407,3 +1448,15 @@ func (c *RefreshTokenClient) mutate(ctx context.Context, m *RefreshTokenMutation
 		return nil, fmt.Errorf("db: unknown RefreshToken mutation op: %q", m.Op())
 	}
 }
+
+// hooks and interceptors per client, for fast access.
+type (
+	hooks struct {
+		AuthCode, AuthRequest, Connector, DeviceRequest, DeviceToken, Keys,
+		OAuth2Client, OfflineSession, Password, RefreshToken []ent.Hook
+	}
+	inters struct {
+		AuthCode, AuthRequest, Connector, DeviceRequest, DeviceToken, Keys,
+		OAuth2Client, OfflineSession, Password, RefreshToken []ent.Interceptor
+	}
+)
diff --git a/storage/ent/db/config.go b/storage/ent/db/config.go
deleted file mode 100644
index fc7832c5..00000000
--- a/storage/ent/db/config.go
+++ /dev/null
@@ -1,84 +0,0 @@
-// Code generated by ent, DO NOT EDIT.
-
-package db
-
-import (
-	"entgo.io/ent"
-	"entgo.io/ent/dialect"
-)
-
-// Option function to configure the client.
-type Option func(*config)
-
-// Config is the configuration for the client and its builder.
-type config struct {
-	// driver used for executing database requests.
-	driver dialect.Driver
-	// debug enable a debug logging.
-	debug bool
-	// log used for logging on debug mode.
-	log func(...any)
-	// hooks to execute on mutations.
-	hooks *hooks
-	// interceptors to execute on queries.
-	inters *inters
-}
-
-// hooks and interceptors per client, for fast access.
-type (
-	hooks struct {
-		AuthCode       []ent.Hook
-		AuthRequest    []ent.Hook
-		Connector      []ent.Hook
-		DeviceRequest  []ent.Hook
-		DeviceToken    []ent.Hook
-		Keys           []ent.Hook
-		OAuth2Client   []ent.Hook
-		OfflineSession []ent.Hook
-		Password       []ent.Hook
-		RefreshToken   []ent.Hook
-	}
-	inters struct {
-		AuthCode       []ent.Interceptor
-		AuthRequest    []ent.Interceptor
-		Connector      []ent.Interceptor
-		DeviceRequest  []ent.Interceptor
-		DeviceToken    []ent.Interceptor
-		Keys           []ent.Interceptor
-		OAuth2Client   []ent.Interceptor
-		OfflineSession []ent.Interceptor
-		Password       []ent.Interceptor
-		RefreshToken   []ent.Interceptor
-	}
-)
-
-// Options applies the options on the config object.
-func (c *config) options(opts ...Option) {
-	for _, opt := range opts {
-		opt(c)
-	}
-	if c.debug {
-		c.driver = dialect.Debug(c.driver, c.log)
-	}
-}
-
-// Debug enables debug logging on the ent.Driver.
-func Debug() Option {
-	return func(c *config) {
-		c.debug = true
-	}
-}
-
-// Log sets the logging function for debug mode.
-func Log(fn func(...any)) Option {
-	return func(c *config) {
-		c.log = fn
-	}
-}
-
-// Driver configures the client driver.
-func Driver(driver dialect.Driver) Option {
-	return func(c *config) {
-		c.driver = driver
-	}
-}
diff --git a/storage/ent/db/context.go b/storage/ent/db/context.go
deleted file mode 100644
index 95e1ad8b..00000000
--- a/storage/ent/db/context.go
+++ /dev/null
@@ -1,33 +0,0 @@
-// Code generated by ent, DO NOT EDIT.
-
-package db
-
-import (
-	"context"
-)
-
-type clientCtxKey struct{}
-
-// FromContext returns a Client stored inside a context, or nil if there isn't one.
-func FromContext(ctx context.Context) *Client {
-	c, _ := ctx.Value(clientCtxKey{}).(*Client)
-	return c
-}
-
-// NewContext returns a new context with the given Client attached.
-func NewContext(parent context.Context, c *Client) context.Context {
-	return context.WithValue(parent, clientCtxKey{}, c)
-}
-
-type txCtxKey struct{}
-
-// TxFromContext returns a Tx stored inside a context, or nil if there isn't one.
-func TxFromContext(ctx context.Context) *Tx {
-	tx, _ := ctx.Value(txCtxKey{}).(*Tx)
-	return tx
-}
-
-// NewTxContext returns a new context with the given Tx attached.
-func NewTxContext(parent context.Context, tx *Tx) context.Context {
-	return context.WithValue(parent, txCtxKey{}, tx)
-}
diff --git a/storage/ent/db/ent.go b/storage/ent/db/ent.go
index 128c464b..11f23341 100644
--- a/storage/ent/db/ent.go
+++ b/storage/ent/db/ent.go
@@ -42,6 +42,32 @@ type (
 	MutateFunc    = ent.MutateFunc
 )
 
+type clientCtxKey struct{}
+
+// FromContext returns a Client stored inside a context, or nil if there isn't one.
+func FromContext(ctx context.Context) *Client {
+	c, _ := ctx.Value(clientCtxKey{}).(*Client)
+	return c
+}
+
+// NewContext returns a new context with the given Client attached.
+func NewContext(parent context.Context, c *Client) context.Context {
+	return context.WithValue(parent, clientCtxKey{}, c)
+}
+
+type txCtxKey struct{}
+
+// TxFromContext returns a Tx stored inside a context, or nil if there isn't one.
+func TxFromContext(ctx context.Context) *Tx {
+	tx, _ := ctx.Value(txCtxKey{}).(*Tx)
+	return tx
+}
+
+// NewTxContext returns a new context with the given Tx attached.
+func NewTxContext(parent context.Context, tx *Tx) context.Context {
+	return context.WithValue(parent, txCtxKey{}, tx)
+}
+
 // OrderFunc applies an ordering on the sql selector.
 type OrderFunc func(*sql.Selector)
 
diff --git a/storage/ent/db/mutation.go b/storage/ent/db/mutation.go
index ca854af4..aec11425 100644
--- a/storage/ent/db/mutation.go
+++ b/storage/ent/db/mutation.go
@@ -9,6 +9,8 @@ import (
 	"sync"
 	"time"
 
+	"entgo.io/ent"
+	"entgo.io/ent/dialect/sql"
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/ent/db/authcode"
 	"github.com/dexidp/dex/storage/ent/db/authrequest"
@@ -22,9 +24,6 @@ import (
 	"github.com/dexidp/dex/storage/ent/db/predicate"
 	"github.com/dexidp/dex/storage/ent/db/refreshtoken"
 	jose "gopkg.in/square/go-jose.v2"
-
-	"entgo.io/ent"
-	"entgo.io/ent/dialect/sql"
 )
 
 const (
diff --git a/storage/ent/db/runtime/runtime.go b/storage/ent/db/runtime/runtime.go
index 0b87a8ea..56b7cf08 100644
--- a/storage/ent/db/runtime/runtime.go
+++ b/storage/ent/db/runtime/runtime.go
@@ -5,6 +5,6 @@ package runtime
 // The schema-stitching logic is generated in github.com/dexidp/dex/storage/ent/db/runtime.go
 
 const (
-	Version = "v0.11.8"                                         // Version of ent codegen.
-	Sum     = "h1:M/M0QL1CYCUSdqGRXUrXhFYSDRJPsOOrr+RLEej/gyQ=" // Sum of ent codegen.
+	Version = "v0.11.9"                                         // Version of ent codegen.
+	Sum     = "h1:dbbCkAiPVTRBIJwoZctiSYjB7zxQIBOzVSU5H9VYIQI=" // Sum of ent codegen.
 )


Test patch:
diff --git a/server/handlers_test.go b/server/handlers_test.go
index 5e2a588d..d49e7241 100644
--- a/server/handlers_test.go
+++ b/server/handlers_test.go
@@ -5,10 +5,12 @@ import (
 	"context"
 	"encoding/json"
 	"errors"
+	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
 	"path"
+	"strings"
 	"testing"
 	"time"
 
@@ -323,3 +325,181 @@ func TestHandlePassword(t *testing.T) {
 		require.Equal(t, `{"test": "true"}`, string(newSess.ConnectorData))
 	}
 }
+
+func TestHandlePasswordLoginWithSkipApproval(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	connID := "mockPw"
+	authReqID := "test"
+	expiry := time.Now().Add(100 * time.Second)
+	resTypes := []string{"code"}
+
+	tests := []struct {
+		name         string
+		skipApproval bool
+		authReq      storage.AuthRequest
+	}{
+		{
+			name:         "Force approval",
+			skipApproval: false,
+			authReq: storage.AuthRequest{
+				ID:                  authReqID,
+				ConnectorID:         connID,
+				RedirectURI:         "cb",
+				Expiry:              expiry,
+				ResponseTypes:       resTypes,
+				ForceApprovalPrompt: true,
+			},
+		},
+		{
+			name:         "Skip approval by server config",
+			skipApproval: true,
+			authReq: storage.AuthRequest{
+				ID:                  authReqID,
+				ConnectorID:         connID,
+				RedirectURI:         "cb",
+				Expiry:              expiry,
+				ResponseTypes:       resTypes,
+				ForceApprovalPrompt: true,
+			},
+		},
+		{
+			name:         "Skip approval by auth request",
+			skipApproval: false,
+			authReq: storage.AuthRequest{
+				ID:                  authReqID,
+				ConnectorID:         connID,
+				RedirectURI:         "cb",
+				Expiry:              expiry,
+				ResponseTypes:       resTypes,
+				ForceApprovalPrompt: false,
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		httpServer, s := newTestServer(ctx, t, func(c *Config) {
+			c.SkipApprovalScreen = tc.skipApproval
+			c.Now = time.Now
+		})
+		defer httpServer.Close()
+
+		sc := storage.Connector{
+			ID:              connID,
+			Type:            "mockPassword",
+			Name:            "MockPassword",
+			ResourceVersion: "1",
+			Config:          []byte("{\"username\": \"foo\", \"password\": \"password\"}"),
+		}
+		if err := s.storage.CreateConnector(sc); err != nil {
+			t.Fatalf("create connector: %v", err)
+		}
+		if _, err := s.OpenConnector(sc); err != nil {
+			t.Fatalf("open connector: %v", err)
+		}
+		if err := s.storage.CreateAuthRequest(tc.authReq); err != nil {
+			t.Fatalf("failed to create AuthRequest: %v", err)
+		}
+
+		rr := httptest.NewRecorder()
+
+		path := fmt.Sprintf("/auth/%s/login?state=%s&back=&login=foo&password=password", connID, authReqID)
+		s.handlePasswordLogin(rr, httptest.NewRequest("POST", path, nil))
+
+		require.Equal(t, 303, rr.Code)
+
+		resp := rr.Result()
+		cbPath := strings.Split(resp.Header.Get("Location"), "?")[0]
+
+		if tc.skipApproval || !tc.authReq.ForceApprovalPrompt {
+			require.Equal(t, "/auth/mockPw/cb", cbPath)
+		} else {
+			require.Equal(t, "/approval", cbPath)
+		}
+
+		resp.Body.Close()
+	}
+}
+
+func TestHandleConnectorCallbackWithSkipApproval(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	connID := "mock"
+	authReqID := "test"
+	expiry := time.Now().Add(100 * time.Second)
+	resTypes := []string{"code"}
+
+	tests := []struct {
+		name         string
+		skipApproval bool
+		authReq      storage.AuthRequest
+	}{
+		{
+			name:         "Force approval",
+			skipApproval: false,
+			authReq: storage.AuthRequest{
+				ID:                  authReqID,
+				ConnectorID:         connID,
+				RedirectURI:         "cb",
+				Expiry:              expiry,
+				ResponseTypes:       resTypes,
+				ForceApprovalPrompt: true,
+			},
+		},
+		{
+			name:         "Skip approval by server config",
+			skipApproval: true,
+			authReq: storage.AuthRequest{
+				ID:                  authReqID,
+				ConnectorID:         connID,
+				RedirectURI:         "cb",
+				Expiry:              expiry,
+				ResponseTypes:       resTypes,
+				ForceApprovalPrompt: true,
+			},
+		},
+		{
+			name:         "Skip approval by auth request",
+			skipApproval: false,
+			authReq: storage.AuthRequest{
+				ID:                  authReqID,
+				ConnectorID:         connID,
+				RedirectURI:         "cb",
+				Expiry:              expiry,
+				ResponseTypes:       resTypes,
+				ForceApprovalPrompt: false,
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		httpServer, s := newTestServer(ctx, t, func(c *Config) {
+			c.SkipApprovalScreen = tc.skipApproval
+			c.Now = time.Now
+		})
+		defer httpServer.Close()
+
+		if err := s.storage.CreateAuthRequest(tc.authReq); err != nil {
+			t.Fatalf("failed to create AuthRequest: %v", err)
+		}
+		rr := httptest.NewRecorder()
+
+		path := fmt.Sprintf("/callback/%s?state=%s", connID, authReqID)
+		s.handleConnectorCallback(rr, httptest.NewRequest("GET", path, nil))
+
+		require.Equal(t, 303, rr.Code)
+
+		resp := rr.Result()
+		cbPath := strings.Split(resp.Header.Get("Location"), "?")[0]
+
+		if tc.skipApproval || !tc.authReq.ForceApprovalPrompt {
+			require.Equal(t, "/callback/cb", cbPath)
+		} else {
+			require.Equal(t, "/approval", cbPath)
+		}
+
+		resp.Body.Close()
+	}
+}
diff --git a/server/server_test.go b/server/server_test.go
index e54e80af..aa34be8c 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -98,6 +98,7 @@ func newTestServer(ctx context.Context, t *testing.T, updateConfig func(c *Confi
 		Logger:             logger,
 		PrometheusRegistry: prometheus.NewRegistry(),
 		HealthChecker:      gosundheit.New(),
+		SkipApprovalScreen: true, // Don't prompt for approval, just immediately redirect with code.
 	}
 	if updateConfig != nil {
 		updateConfig(&config)
@@ -118,7 +119,6 @@ func newTestServer(ctx context.Context, t *testing.T, updateConfig func(c *Confi
 	if server, err = newServer(ctx, config, staticRotationStrategy(testKey)); err != nil {
 		t.Fatal(err)
 	}
-	server.skipApproval = true // Don't prompt for approval, just immediately redirect with code.
 
 	// Default rotation policy
 	if server.refreshTokenPolicy == nil {