Repository: dexidp/dex
Pull request #2604 (2022-07-27 04:07:31)

build(deps): bump aquasecurity/trivy-action from 0.6.0 to 0.6.1
Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.6.0 to 0.6.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/aquasecurity/trivy-action/releases">aquasecurity/trivy-action's releases</a>.</em></p>
<blockquote>
<h2>v0.6.1</h2>
<h2>What's Changed</h2>
<ul>
<li>chore(deps): Update trivy version by <a href="https://github.com/simar7"><code>@​simar7</code></a> in <a href="https://github-redirect.dependabot.com/aquasecurity/trivy-action/pull/152">aquasecurity/trivy-action#152</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/aquasecurity/trivy-action/compare/0.6.0...0.6.1">https://github.com/aquasecurity/trivy-action/compare/0.6.0...0.6.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/aquasecurity/trivy-action/commit/81b9a6f5abb1047d697af7a3ca18c13f55a97315"><code>81b9a6f</code></a> Update Dockerfile (<a href="https://github-redirect.dependabot.com/aquasecurity/trivy-action/issues/152">#152</a>)</li>
<li>See full diff in <a href="https://github.com/aquasecurity/trivy-action/compare/0.6.0...0.6.1">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=aquasecurity/trivy-action&package-manager=github_actions&previous-version=0.6.0&new-version=0.6.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/.github/workflows/artifacts.yaml b/.github/workflows/artifacts.yaml
index fdf9bee2..311c740c 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.6.0
+        uses: aquasecurity/trivy-action@0.6.1
         with:
           image-ref: "ghcr.io/dexidp/dex:${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}"
           format: "sarif"
diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml
index ab970267..fe30d0cb 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.6.0
+        uses: aquasecurity/trivy-action@0.6.1
         with:
           image-ref: "ghcr.io/dexidp/dex:${{ steps.tags.outputs.version }}"
           format: "template"
diff --git a/connector/microsoft/microsoft.go b/connector/microsoft/microsoft.go
index 2a8bbcfb..3952c94b 100644
--- a/connector/microsoft/microsoft.go
+++ b/connector/microsoft/microsoft.go
@@ -58,6 +58,8 @@ type Config struct {
 	// For valid values, see https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code.
 	PromptType string `json:"promptType"`
 	DomainHint string `json:"domainHint"`
+
+	Scopes []string `json:"scopes"` // defaults to scopeUser (user.read)
 }
 
 // Open returns a strategy for logging in through Microsoft.
@@ -77,6 +79,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
 		emailToLowercase:     c.EmailToLowercase,
 		promptType:           c.PromptType,
 		domainHint:           c.DomainHint,
+		scopes:               c.Scopes,
 	}
 	// By default allow logins from both personal and business/school
 	// accounts.
@@ -122,6 +125,7 @@ type microsoftConnector struct {
 	emailToLowercase     bool
 	promptType           string
 	domainHint           string
+	scopes               []string
 }
 
 func (c *microsoftConnector) isOrgTenant() bool {
@@ -133,7 +137,12 @@ func (c *microsoftConnector) groupsRequired(groupScope bool) bool {
 }
 
 func (c *microsoftConnector) oauth2Config(scopes connector.Scopes) *oauth2.Config {
-	microsoftScopes := []string{scopeUser}
+	var microsoftScopes []string
+	if len(c.scopes) > 0 {
+		microsoftScopes = c.scopes
+	} else {
+		microsoftScopes = append(microsoftScopes, scopeUser)
+	}
 	if c.groupsRequired(scopes.Groups) {
 		microsoftScopes = append(microsoftScopes, scopeGroups)
 	}
diff --git a/go.mod b/go.mod
index 5bd7c568..58d5233e 100644
--- a/go.mod
+++ b/go.mod
@@ -12,7 +12,7 @@ require (
 	github.com/dexidp/dex/api/v2 v2.1.0
 	github.com/felixge/httpsnoop v1.0.3
 	github.com/ghodss/yaml v1.0.0
-	github.com/go-ldap/ldap/v3 v3.4.2
+	github.com/go-ldap/ldap/v3 v3.4.4
 	github.com/go-sql-driver/mysql v1.6.0
 	github.com/gorilla/handlers v1.5.1
 	github.com/gorilla/mux v1.8.0
@@ -30,10 +30,10 @@ require (
 	github.com/stretchr/testify v1.8.0
 	go.etcd.io/etcd/client/pkg/v3 v3.5.4
 	go.etcd.io/etcd/client/v3 v3.5.4
-	golang.org/x/crypto v0.0.0-20220208050332-20e1d8d225ab
+	golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
 	golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e
 	golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2
-	google.golang.org/api v0.86.0
+	google.golang.org/api v0.89.0
 	google.golang.org/grpc v1.47.0
 	google.golang.org/protobuf v1.28.0
 	gopkg.in/square/go-jose.v2 v2.6.0
@@ -42,7 +42,7 @@ require (
 require (
 	ariga.io/atlas v0.3.7-0.20220303204946-787354f533c3 // indirect
 	cloud.google.com/go/compute v1.7.0 // indirect
-	github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect
+	github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
 	github.com/Masterminds/semver/v3 v3.1.1 // indirect
 	github.com/agext/levenshtein v1.2.1 // indirect
@@ -52,7 +52,7 @@ require (
 	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
-	github.com/go-asn1-ber/asn1-ber v1.5.1 // indirect
+	github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
 	github.com/go-openapi/inflect v0.19.0 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
diff --git a/go.sum b/go.sum
index 2aece845..8cdb7bd9 100644
--- a/go.sum
+++ b/go.sum
@@ -61,8 +61,8 @@ entgo.io/ent v0.10.1 h1:dM5h4Zk6yHGIgw4dCqVzGw3nWgpGYJiV4/kyHEF6PFo=
 entgo.io/ent v0.10.1/go.mod h1:YPgxeLnoQ/YdpVORRtqjBF+wCy9NX9IR7veTv3Bffus=
 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-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
-github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
+github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU=
+github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
@@ -140,16 +140,16 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8
 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
 github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
-github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
+github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
+github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
-github.com/go-ldap/ldap/v3 v3.4.2 h1:zFZKcXKLqZpFMrMQGHeHWKXbDTdNCmhGY9AK41zPh+8=
-github.com/go-ldap/ldap/v3 v3.4.2/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
+github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs=
+github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
@@ -389,6 +389,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 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 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
@@ -431,10 +432,9 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20220208050332-20e1d8d225ab h1:lnZ4LoV0UMdibeCUfIB2a4uFwRu491WX/VB2reB8xNc=
-golang.org/x/crypto v0.0.0-20220208050332-20e1d8d225ab/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 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=
@@ -512,6 +512,7 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
@@ -741,8 +742,8 @@ google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69
 google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
 google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
 google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
-google.golang.org/api v0.86.0 h1:ZAnyOHQFIuWso1BodVfSaRyffD74T9ERGFa3k1fNk/U=
-google.golang.org/api v0.86.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
+google.golang.org/api v0.89.0 h1:OUywo5UEEZ8H1eMy55mFpkL9Sy59mQ5TzYGWa+td8zo=
+google.golang.org/api v0.89.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
diff --git a/server/deviceflowhandlers.go b/server/deviceflowhandlers.go
index 0efe3b2b..95fed3b3 100644
--- a/server/deviceflowhandlers.go
+++ b/server/deviceflowhandlers.go
@@ -73,6 +73,17 @@ func (s *Server) handleDeviceCode(w http.ResponseWriter, r *http.Request) {
 		clientID := r.Form.Get("client_id")
 		clientSecret := r.Form.Get("client_secret")
 		scopes := strings.Fields(r.Form.Get("scope"))
+		codeChallenge := r.Form.Get("code_challenge")
+		codeChallengeMethod := r.Form.Get("code_challenge_method")
+
+		if codeChallengeMethod == "" {
+			codeChallengeMethod = codeChallengeMethodPlain
+		}
+		if codeChallengeMethod != codeChallengeMethodS256 && codeChallengeMethod != codeChallengeMethodPlain {
+			description := fmt.Sprintf("Unsupported PKCE challenge method (%q).", codeChallengeMethod)
+			s.tokenErrHelper(w, errInvalidRequest, description, http.StatusBadRequest)
+			return
+		}
 
 		s.logger.Infof("Received device request for client %v with scopes %v", clientID, scopes)
 
@@ -108,6 +119,10 @@ func (s *Server) handleDeviceCode(w http.ResponseWriter, r *http.Request) {
 			Expiry:              expireTime,
 			LastRequestTime:     s.now(),
 			PollIntervalSeconds: 0,
+			PKCE: storage.PKCE{
+				CodeChallenge:       codeChallenge,
+				CodeChallengeMethod: codeChallengeMethod,
+			},
 		}
 
 		if err := s.storage.CreateDeviceToken(deviceToken); err != nil {
@@ -236,6 +251,30 @@ func (s *Server) handleDeviceToken(w http.ResponseWriter, r *http.Request) {
 			s.tokenErrHelper(w, deviceTokenPending, "", http.StatusUnauthorized)
 		}
 	case deviceTokenComplete:
+		codeChallengeFromStorage := deviceToken.PKCE.CodeChallenge
+		providedCodeVerifier := r.Form.Get("code_verifier")
+
+		switch {
+		case providedCodeVerifier != "" && codeChallengeFromStorage != "":
+			calculatedCodeChallenge, err := s.calculateCodeChallenge(providedCodeVerifier, deviceToken.PKCE.CodeChallengeMethod)
+			if err != nil {
+				s.logger.Error(err)
+				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
+				return
+			}
+			if codeChallengeFromStorage != calculatedCodeChallenge {
+				s.tokenErrHelper(w, errInvalidGrant, "Invalid code_verifier.", http.StatusBadRequest)
+				return
+			}
+		case providedCodeVerifier != "":
+			// Received no code_challenge on /auth, but a code_verifier on /token
+			s.tokenErrHelper(w, errInvalidRequest, "No PKCE flow started. Cannot check code_verifier.", http.StatusBadRequest)
+			return
+		case codeChallengeFromStorage != "":
+			// Received PKCE request on /auth, but no code_verifier on /token
+			s.tokenErrHelper(w, errInvalidGrant, "Expecting parameter code_verifier in PKCE flow.", http.StatusBadRequest)
+			return
+		}
 		w.Write([]byte(deviceToken.Token))
 	}
 }
diff --git a/storage/conformance/conformance.go b/storage/conformance/conformance.go
index dde369c4..9d9766eb 100644
--- a/storage/conformance/conformance.go
+++ b/storage/conformance/conformance.go
@@ -890,6 +890,10 @@ func testGC(t *testing.T, s storage.Storage) {
 		Expiry:              expiry,
 		LastRequestTime:     time.Now(),
 		PollIntervalSeconds: 0,
+		PKCE: storage.PKCE{
+			CodeChallenge:       "challenge",
+			CodeChallengeMethod: "S256",
+		},
 	}
 
 	if err := s.CreateDeviceToken(dt); err != nil {
@@ -989,6 +993,11 @@ func testDeviceRequestCRUD(t *testing.T, s storage.Storage) {
 }
 
 func testDeviceTokenCRUD(t *testing.T, s storage.Storage) {
+	codeChallenge := storage.PKCE{
+		CodeChallenge:       "code_challenge_test",
+		CodeChallengeMethod: "plain",
+	}
+
 	// Create a Token
 	d1 := storage.DeviceToken{
 		DeviceCode:          storage.NewID(),
@@ -997,6 +1006,7 @@ func testDeviceTokenCRUD(t *testing.T, s storage.Storage) {
 		Expiry:              neverExpire,
 		LastRequestTime:     time.Now(),
 		PollIntervalSeconds: 0,
+		PKCE:                codeChallenge,
 	}
 
 	if err := s.CreateDeviceToken(d1); err != nil {
@@ -1029,4 +1039,7 @@ func testDeviceTokenCRUD(t *testing.T, s storage.Storage) {
 	if got.Token != "token data" {
 		t.Fatalf("update failed, wanted token %v got %v", "token data", got.Token)
 	}
+	if !reflect.DeepEqual(got.PKCE, codeChallenge) {
+		t.Fatalf("storage does not support PKCE, wanted challenge=%#v got %#v", codeChallenge, got.PKCE)
+	}
 }
diff --git a/storage/ent/client/devicetoken.go b/storage/ent/client/devicetoken.go
index d8870787..99cf077d 100644
--- a/storage/ent/client/devicetoken.go
+++ b/storage/ent/client/devicetoken.go
@@ -17,6 +17,8 @@ func (d *Database) CreateDeviceToken(token storage.DeviceToken) error {
 		SetExpiry(token.Expiry.UTC()).
 		SetLastRequest(token.LastRequestTime.UTC()).
 		SetStatus(token.Status).
+		SetCodeChallenge(token.PKCE.CodeChallenge).
+		SetCodeChallengeMethod(token.PKCE.CodeChallengeMethod).
 		Save(context.TODO())
 	if err != nil {
 		return convertDBError("create device token: %w", err)
@@ -63,6 +65,8 @@ func (d *Database) UpdateDeviceToken(deviceCode string, updater func(old storage
 		SetExpiry(newToken.Expiry.UTC()).
 		SetLastRequest(newToken.LastRequestTime.UTC()).
 		SetStatus(newToken.Status).
+		SetCodeChallenge(newToken.PKCE.CodeChallenge).
+		SetCodeChallengeMethod(newToken.PKCE.CodeChallengeMethod).
 		Save(context.TODO())
 	if err != nil {
 		return rollback(tx, "update device token uploading: %w", err)
diff --git a/storage/ent/client/types.go b/storage/ent/client/types.go
index 57f1c0a7..256bb73d 100644
--- a/storage/ent/client/types.go
+++ b/storage/ent/client/types.go
@@ -164,5 +164,9 @@ func toStorageDeviceToken(t *db.DeviceToken) storage.DeviceToken {
 		Expiry:              t.Expiry,
 		LastRequestTime:     t.LastRequest,
 		PollIntervalSeconds: t.PollInterval,
+		PKCE: storage.PKCE{
+			CodeChallenge:       t.CodeChallenge,
+			CodeChallengeMethod: t.CodeChallengeMethod,
+		},
 	}
 }
diff --git a/storage/ent/db/devicetoken.go b/storage/ent/db/devicetoken.go
index 1731d1f0..5155df9a 100644
--- a/storage/ent/db/devicetoken.go
+++ b/storage/ent/db/devicetoken.go
@@ -28,6 +28,10 @@ type DeviceToken struct {
 	LastRequest time.Time `json:"last_request,omitempty"`
 	// PollInterval holds the value of the "poll_interval" field.
 	PollInterval int `json:"poll_interval,omitempty"`
+	// CodeChallenge holds the value of the "code_challenge" field.
+	CodeChallenge string `json:"code_challenge,omitempty"`
+	// CodeChallengeMethod holds the value of the "code_challenge_method" field.
+	CodeChallengeMethod string `json:"code_challenge_method,omitempty"`
 }
 
 // scanValues returns the types for scanning values from sql.Rows.
@@ -39,7 +43,7 @@ func (*DeviceToken) scanValues(columns []string) ([]interface{}, error) {
 			values[i] = new([]byte)
 		case devicetoken.FieldID, devicetoken.FieldPollInterval:
 			values[i] = new(sql.NullInt64)
-		case devicetoken.FieldDeviceCode, devicetoken.FieldStatus:
+		case devicetoken.FieldDeviceCode, devicetoken.FieldStatus, devicetoken.FieldCodeChallenge, devicetoken.FieldCodeChallengeMethod:
 			values[i] = new(sql.NullString)
 		case devicetoken.FieldExpiry, devicetoken.FieldLastRequest:
 			values[i] = new(sql.NullTime)
@@ -100,6 +104,18 @@ func (dt *DeviceToken) assignValues(columns []string, values []interface{}) erro
 			} else if value.Valid {
 				dt.PollInterval = int(value.Int64)
 			}
+		case devicetoken.FieldCodeChallenge:
+			if value, ok := values[i].(*sql.NullString); !ok {
+				return fmt.Errorf("unexpected type %T for field code_challenge", values[i])
+			} else if value.Valid {
+				dt.CodeChallenge = value.String
+			}
+		case devicetoken.FieldCodeChallengeMethod:
+			if value, ok := values[i].(*sql.NullString); !ok {
+				return fmt.Errorf("unexpected type %T for field code_challenge_method", values[i])
+			} else if value.Valid {
+				dt.CodeChallengeMethod = value.String
+			}
 		}
 	}
 	return nil
@@ -142,6 +158,10 @@ func (dt *DeviceToken) String() string {
 	builder.WriteString(dt.LastRequest.Format(time.ANSIC))
 	builder.WriteString(", poll_interval=")
 	builder.WriteString(fmt.Sprintf("%v", dt.PollInterval))
+	builder.WriteString(", code_challenge=")
+	builder.WriteString(dt.CodeChallenge)
+	builder.WriteString(", code_challenge_method=")
+	builder.WriteString(dt.CodeChallengeMethod)
 	builder.WriteByte(')')
 	return builder.String()
 }
diff --git a/storage/ent/db/devicetoken/devicetoken.go b/storage/ent/db/devicetoken/devicetoken.go
index 7af65799..8f3ae361 100644
--- a/storage/ent/db/devicetoken/devicetoken.go
+++ b/storage/ent/db/devicetoken/devicetoken.go
@@ -19,6 +19,10 @@ const (
 	FieldLastRequest = "last_request"
 	// FieldPollInterval holds the string denoting the poll_interval field in the database.
 	FieldPollInterval = "poll_interval"
+	// FieldCodeChallenge holds the string denoting the code_challenge field in the database.
+	FieldCodeChallenge = "code_challenge"
+	// FieldCodeChallengeMethod holds the string denoting the code_challenge_method field in the database.
+	FieldCodeChallengeMethod = "code_challenge_method"
 	// Table holds the table name of the devicetoken in the database.
 	Table = "device_tokens"
 )
@@ -32,6 +36,8 @@ var Columns = []string{
 	FieldExpiry,
 	FieldLastRequest,
 	FieldPollInterval,
+	FieldCodeChallenge,
+	FieldCodeChallengeMethod,
 }
 
 // ValidColumn reports if the column name is valid (part of the table columns).
@@ -49,4 +55,8 @@ var (
 	DeviceCodeValidator func(string) error
 	// StatusValidator is a validator for the "status" field. It is called by the builders before save.
 	StatusValidator func(string) error
+	// DefaultCodeChallenge holds the default value on creation for the "code_challenge" field.
+	DefaultCodeChallenge string
+	// DefaultCodeChallengeMethod holds the default value on creation for the "code_challenge_method" field.
+	DefaultCodeChallengeMethod string
 )
diff --git a/storage/ent/db/devicetoken/where.go b/storage/ent/db/devicetoken/where.go
index b22879a6..779badbe 100644
--- a/storage/ent/db/devicetoken/where.go
+++ b/storage/ent/db/devicetoken/where.go
@@ -134,6 +134,20 @@ func PollInterval(v int) predicate.DeviceToken {
 	})
 }
 
+// CodeChallenge applies equality check predicate on the "code_challenge" field. It's identical to CodeChallengeEQ.
+func CodeChallenge(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.EQ(s.C(FieldCodeChallenge), v))
+	})
+}
+
+// CodeChallengeMethod applies equality check predicate on the "code_challenge_method" field. It's identical to CodeChallengeMethodEQ.
+func CodeChallengeMethod(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.EQ(s.C(FieldCodeChallengeMethod), v))
+	})
+}
+
 // DeviceCodeEQ applies the EQ predicate on the "device_code" field.
 func DeviceCodeEQ(v string) predicate.DeviceToken {
 	return predicate.DeviceToken(func(s *sql.Selector) {
@@ -674,6 +688,228 @@ func PollIntervalLTE(v int) predicate.DeviceToken {
 	})
 }
 
+// CodeChallengeEQ applies the EQ predicate on the "code_challenge" field.
+func CodeChallengeEQ(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.EQ(s.C(FieldCodeChallenge), v))
+	})
+}
+
+// CodeChallengeNEQ applies the NEQ predicate on the "code_challenge" field.
+func CodeChallengeNEQ(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.NEQ(s.C(FieldCodeChallenge), v))
+	})
+}
+
+// CodeChallengeIn applies the In predicate on the "code_challenge" field.
+func CodeChallengeIn(vs ...string) predicate.DeviceToken {
+	v := make([]interface{}, len(vs))
+	for i := range v {
+		v[i] = vs[i]
+	}
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		// if not arguments were provided, append the FALSE constants,
+		// since we can't apply "IN ()". This will make this predicate falsy.
+		if len(v) == 0 {
+			s.Where(sql.False())
+			return
+		}
+		s.Where(sql.In(s.C(FieldCodeChallenge), v...))
+	})
+}
+
+// CodeChallengeNotIn applies the NotIn predicate on the "code_challenge" field.
+func CodeChallengeNotIn(vs ...string) predicate.DeviceToken {
+	v := make([]interface{}, len(vs))
+	for i := range v {
+		v[i] = vs[i]
+	}
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		// if not arguments were provided, append the FALSE constants,
+		// since we can't apply "IN ()". This will make this predicate falsy.
+		if len(v) == 0 {
+			s.Where(sql.False())
+			return
+		}
+		s.Where(sql.NotIn(s.C(FieldCodeChallenge), v...))
+	})
+}
+
+// CodeChallengeGT applies the GT predicate on the "code_challenge" field.
+func CodeChallengeGT(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.GT(s.C(FieldCodeChallenge), v))
+	})
+}
+
+// CodeChallengeGTE applies the GTE predicate on the "code_challenge" field.
+func CodeChallengeGTE(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.GTE(s.C(FieldCodeChallenge), v))
+	})
+}
+
+// CodeChallengeLT applies the LT predicate on the "code_challenge" field.
+func CodeChallengeLT(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.LT(s.C(FieldCodeChallenge), v))
+	})
+}
+
+// CodeChallengeLTE applies the LTE predicate on the "code_challenge" field.
+func CodeChallengeLTE(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.LTE(s.C(FieldCodeChallenge), v))
+	})
+}
+
+// CodeChallengeContains applies the Contains predicate on the "code_challenge" field.
+func CodeChallengeContains(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.Contains(s.C(FieldCodeChallenge), v))
+	})
+}
+
+// CodeChallengeHasPrefix applies the HasPrefix predicate on the "code_challenge" field.
+func CodeChallengeHasPrefix(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.HasPrefix(s.C(FieldCodeChallenge), v))
+	})
+}
+
+// CodeChallengeHasSuffix applies the HasSuffix predicate on the "code_challenge" field.
+func CodeChallengeHasSuffix(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.HasSuffix(s.C(FieldCodeChallenge), v))
+	})
+}
+
+// CodeChallengeEqualFold applies the EqualFold predicate on the "code_challenge" field.
+func CodeChallengeEqualFold(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.EqualFold(s.C(FieldCodeChallenge), v))
+	})
+}
+
+// CodeChallengeContainsFold applies the ContainsFold predicate on the "code_challenge" field.
+func CodeChallengeContainsFold(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.ContainsFold(s.C(FieldCodeChallenge), v))
+	})
+}
+
+// CodeChallengeMethodEQ applies the EQ predicate on the "code_challenge_method" field.
+func CodeChallengeMethodEQ(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.EQ(s.C(FieldCodeChallengeMethod), v))
+	})
+}
+
+// CodeChallengeMethodNEQ applies the NEQ predicate on the "code_challenge_method" field.
+func CodeChallengeMethodNEQ(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.NEQ(s.C(FieldCodeChallengeMethod), v))
+	})
+}
+
+// CodeChallengeMethodIn applies the In predicate on the "code_challenge_method" field.
+func CodeChallengeMethodIn(vs ...string) predicate.DeviceToken {
+	v := make([]interface{}, len(vs))
+	for i := range v {
+		v[i] = vs[i]
+	}
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		// if not arguments were provided, append the FALSE constants,
+		// since we can't apply "IN ()". This will make this predicate falsy.
+		if len(v) == 0 {
+			s.Where(sql.False())
+			return
+		}
+		s.Where(sql.In(s.C(FieldCodeChallengeMethod), v...))
+	})
+}
+
+// CodeChallengeMethodNotIn applies the NotIn predicate on the "code_challenge_method" field.
+func CodeChallengeMethodNotIn(vs ...string) predicate.DeviceToken {
+	v := make([]interface{}, len(vs))
+	for i := range v {
+		v[i] = vs[i]
+	}
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		// if not arguments were provided, append the FALSE constants,
+		// since we can't apply "IN ()". This will make this predicate falsy.
+		if len(v) == 0 {
+			s.Where(sql.False())
+			return
+		}
+		s.Where(sql.NotIn(s.C(FieldCodeChallengeMethod), v...))
+	})
+}
+
+// CodeChallengeMethodGT applies the GT predicate on the "code_challenge_method" field.
+func CodeChallengeMethodGT(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.GT(s.C(FieldCodeChallengeMethod), v))
+	})
+}
+
+// CodeChallengeMethodGTE applies the GTE predicate on the "code_challenge_method" field.
+func CodeChallengeMethodGTE(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.GTE(s.C(FieldCodeChallengeMethod), v))
+	})
+}
+
+// CodeChallengeMethodLT applies the LT predicate on the "code_challenge_method" field.
+func CodeChallengeMethodLT(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.LT(s.C(FieldCodeChallengeMethod), v))
+	})
+}
+
+// CodeChallengeMethodLTE applies the LTE predicate on the "code_challenge_method" field.
+func CodeChallengeMethodLTE(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.LTE(s.C(FieldCodeChallengeMethod), v))
+	})
+}
+
+// CodeChallengeMethodContains applies the Contains predicate on the "code_challenge_method" field.
+func CodeChallengeMethodContains(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.Contains(s.C(FieldCodeChallengeMethod), v))
+	})
+}
+
+// CodeChallengeMethodHasPrefix applies the HasPrefix predicate on the "code_challenge_method" field.
+func CodeChallengeMethodHasPrefix(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.HasPrefix(s.C(FieldCodeChallengeMethod), v))
+	})
+}
+
+// CodeChallengeMethodHasSuffix applies the HasSuffix predicate on the "code_challenge_method" field.
+func CodeChallengeMethodHasSuffix(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.HasSuffix(s.C(FieldCodeChallengeMethod), v))
+	})
+}
+
+// CodeChallengeMethodEqualFold applies the EqualFold predicate on the "code_challenge_method" field.
+func CodeChallengeMethodEqualFold(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.EqualFold(s.C(FieldCodeChallengeMethod), v))
+	})
+}
+
+// CodeChallengeMethodContainsFold applies the ContainsFold predicate on the "code_challenge_method" field.
+func CodeChallengeMethodContainsFold(v string) predicate.DeviceToken {
+	return predicate.DeviceToken(func(s *sql.Selector) {
+		s.Where(sql.ContainsFold(s.C(FieldCodeChallengeMethod), v))
+	})
+}
+
 // And groups predicates with the AND operator between them.
 func And(predicates ...predicate.DeviceToken) predicate.DeviceToken {
 	return predicate.DeviceToken(func(s *sql.Selector) {
diff --git a/storage/ent/db/devicetoken_create.go b/storage/ent/db/devicetoken_create.go
index 42c86755..023f7185 100644
--- a/storage/ent/db/devicetoken_create.go
+++ b/storage/ent/db/devicetoken_create.go
@@ -56,6 +56,34 @@ func (dtc *DeviceTokenCreate) SetPollInterval(i int) *DeviceTokenCreate {
 	return dtc
 }
 
+// SetCodeChallenge sets the "code_challenge" field.
+func (dtc *DeviceTokenCreate) SetCodeChallenge(s string) *DeviceTokenCreate {
+	dtc.mutation.SetCodeChallenge(s)
+	return dtc
+}
+
+// SetNillableCodeChallenge sets the "code_challenge" field if the given value is not nil.
+func (dtc *DeviceTokenCreate) SetNillableCodeChallenge(s *string) *DeviceTokenCreate {
+	if s != nil {
+		dtc.SetCodeChallenge(*s)
+	}
+	return dtc
+}
+
+// SetCodeChallengeMethod sets the "code_challenge_method" field.
+func (dtc *DeviceTokenCreate) SetCodeChallengeMethod(s string) *DeviceTokenCreate {
+	dtc.mutation.SetCodeChallengeMethod(s)
+	return dtc
+}
+
+// SetNillableCodeChallengeMethod sets the "code_challenge_method" field if the given value is not nil.
+func (dtc *DeviceTokenCreate) SetNillableCodeChallengeMethod(s *string) *DeviceTokenCreate {
+	if s != nil {
+		dtc.SetCodeChallengeMethod(*s)
+	}
+	return dtc
+}
+
 // Mutation returns the DeviceTokenMutation object of the builder.
 func (dtc *DeviceTokenCreate) Mutation() *DeviceTokenMutation {
 	return dtc.mutation
@@ -67,6 +95,7 @@ func (dtc *DeviceTokenCreate) Save(ctx context.Context) (*DeviceToken, error) {
 		err  error
 		node *DeviceToken
 	)
+	dtc.defaults()
 	if len(dtc.hooks) == 0 {
 		if err = dtc.check(); err != nil {
 			return nil, err
@@ -124,6 +153,18 @@ func (dtc *DeviceTokenCreate) ExecX(ctx context.Context) {
 	}
 }
 
+// defaults sets the default values of the builder before save.
+func (dtc *DeviceTokenCreate) defaults() {
+	if _, ok := dtc.mutation.CodeChallenge(); !ok {
+		v := devicetoken.DefaultCodeChallenge
+		dtc.mutation.SetCodeChallenge(v)
+	}
+	if _, ok := dtc.mutation.CodeChallengeMethod(); !ok {
+		v := devicetoken.DefaultCodeChallengeMethod
+		dtc.mutation.SetCodeChallengeMethod(v)
+	}
+}
+
 // check runs all checks and user-defined validators on the builder.
 func (dtc *DeviceTokenCreate) check() error {
 	if _, ok := dtc.mutation.DeviceCode(); !ok {
@@ -151,6 +192,12 @@ func (dtc *DeviceTokenCreate) check() error {
 	if _, ok := dtc.mutation.PollInterval(); !ok {
 		return &ValidationError{Name: "poll_interval", err: errors.New(`db: missing required field "DeviceToken.poll_interval"`)}
 	}
+	if _, ok := dtc.mutation.CodeChallenge(); !ok {
+		return &ValidationError{Name: "code_challenge", err: errors.New(`db: missing required field "DeviceToken.code_challenge"`)}
+	}
+	if _, ok := dtc.mutation.CodeChallengeMethod(); !ok {
+		return &ValidationError{Name: "code_challenge_method", err: errors.New(`db: missing required field "DeviceToken.code_challenge_method"`)}
+	}
 	return nil
 }
 
@@ -226,6 +273,22 @@ func (dtc *DeviceTokenCreate) createSpec() (*DeviceToken, *sqlgraph.CreateSpec)
 		})
 		_node.PollInterval = value
 	}
+	if value, ok := dtc.mutation.CodeChallenge(); ok {
+		_spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
+			Type:   field.TypeString,
+			Value:  value,
+			Column: devicetoken.FieldCodeChallenge,
+		})
+		_node.CodeChallenge = value
+	}
+	if value, ok := dtc.mutation.CodeChallengeMethod(); ok {
+		_spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
+			Type:   field.TypeString,
+			Value:  value,
+			Column: devicetoken.FieldCodeChallengeMethod,
+		})
+		_node.CodeChallengeMethod = value
+	}
 	return _node, _spec
 }
 
@@ -243,6 +306,7 @@ func (dtcb *DeviceTokenCreateBulk) Save(ctx context.Context) ([]*DeviceToken, er
 	for i := range dtcb.builders {
 		func(i int, root context.Context) {
 			builder := dtcb.builders[i]
+			builder.defaults()
 			var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
 				mutation, ok := m.(*DeviceTokenMutation)
 				if !ok {
diff --git a/storage/ent/db/devicetoken_update.go b/storage/ent/db/devicetoken_update.go
index bf9d2993..8f95e7a0 100644
--- a/storage/ent/db/devicetoken_update.go
+++ b/storage/ent/db/devicetoken_update.go
@@ -77,6 +77,34 @@ func (dtu *DeviceTokenUpdate) AddPollInterval(i int) *DeviceTokenUpdate {
 	return dtu
 }
 
+// SetCodeChallenge sets the "code_challenge" field.
+func (dtu *DeviceTokenUpdate) SetCodeChallenge(s string) *DeviceTokenUpdate {
+	dtu.mutation.SetCodeChallenge(s)
+	return dtu
+}
+
+// SetNillableCodeChallenge sets the "code_challenge" field if the given value is not nil.
+func (dtu *DeviceTokenUpdate) SetNillableCodeChallenge(s *string) *DeviceTokenUpdate {
+	if s != nil {
+		dtu.SetCodeChallenge(*s)
+	}
+	return dtu
+}
+
+// SetCodeChallengeMethod sets the "code_challenge_method" field.
+func (dtu *DeviceTokenUpdate) SetCodeChallengeMethod(s string) *DeviceTokenUpdate {
+	dtu.mutation.SetCodeChallengeMethod(s)
+	return dtu
+}
+
+// SetNillableCodeChallengeMethod sets the "code_challenge_method" field if the given value is not nil.
+func (dtu *DeviceTokenUpdate) SetNillableCodeChallengeMethod(s *string) *DeviceTokenUpdate {
+	if s != nil {
+		dtu.SetCodeChallengeMethod(*s)
+	}
+	return dtu
+}
+
 // Mutation returns the DeviceTokenMutation object of the builder.
 func (dtu *DeviceTokenUpdate) Mutation() *DeviceTokenMutation {
 	return dtu.mutation
@@ -230,6 +258,20 @@ func (dtu *DeviceTokenUpdate) sqlSave(ctx context.Context) (n int, err error) {
 			Column: devicetoken.FieldPollInterval,
 		})
 	}
+	if value, ok := dtu.mutation.CodeChallenge(); ok {
+		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
+			Type:   field.TypeString,
+			Value:  value,
+			Column: devicetoken.FieldCodeChallenge,
+		})
+	}
+	if value, ok := dtu.mutation.CodeChallengeMethod(); ok {
+		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
+			Type:   field.TypeString,
+			Value:  value,
+			Column: devicetoken.FieldCodeChallengeMethod,
+		})
+	}
 	if n, err = sqlgraph.UpdateNodes(ctx, dtu.driver, _spec); err != nil {
 		if _, ok := err.(*sqlgraph.NotFoundError); ok {
 			err = &NotFoundError{devicetoken.Label}
@@ -298,6 +340,34 @@ func (dtuo *DeviceTokenUpdateOne) AddPollInterval(i int) *DeviceTokenUpdateOne {
 	return dtuo
 }
 
+// SetCodeChallenge sets the "code_challenge" field.
+func (dtuo *DeviceTokenUpdateOne) SetCodeChallenge(s string) *DeviceTokenUpdateOne {
+	dtuo.mutation.SetCodeChallenge(s)
+	return dtuo
+}
+
+// SetNillableCodeChallenge sets the "code_challenge" field if the given value is not nil.
+func (dtuo *DeviceTokenUpdateOne) SetNillableCodeChallenge(s *string) *DeviceTokenUpdateOne {
+	if s != nil {
+		dtuo.SetCodeChallenge(*s)
+	}
+	return dtuo
+}
+
+// SetCodeChallengeMethod sets the "code_challenge_method" field.
+func (dtuo *DeviceTokenUpdateOne) SetCodeChallengeMethod(s string) *DeviceTokenUpdateOne {
+	dtuo.mutation.SetCodeChallengeMethod(s)
+	return dtuo
+}
+
+// SetNillableCodeChallengeMethod sets the "code_challenge_method" field if the given value is not nil.
+func (dtuo *DeviceTokenUpdateOne) SetNillableCodeChallengeMethod(s *string) *DeviceTokenUpdateOne {
+	if s != nil {
+		dtuo.SetCodeChallengeMethod(*s)
+	}
+	return dtuo
+}
+
 // Mutation returns the DeviceTokenMutation object of the builder.
 func (dtuo *DeviceTokenUpdateOne) Mutation() *DeviceTokenMutation {
 	return dtuo.mutation
@@ -475,6 +545,20 @@ func (dtuo *DeviceTokenUpdateOne) sqlSave(ctx context.Context) (_node *DeviceTok
 			Column: devicetoken.FieldPollInterval,
 		})
 	}
+	if value, ok := dtuo.mutation.CodeChallenge(); ok {
+		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
+			Type:   field.TypeString,
+			Value:  value,
+			Column: devicetoken.FieldCodeChallenge,
+		})
+	}
+	if value, ok := dtuo.mutation.CodeChallengeMethod(); ok {
+		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
+			Type:   field.TypeString,
+			Value:  value,
+			Column: devicetoken.FieldCodeChallengeMethod,
+		})
+	}
 	_node = &DeviceToken{config: dtuo.config}
 	_spec.Assign = _node.assignValues
 	_spec.ScanValues = _node.scanValues
diff --git a/storage/ent/db/migrate/schema.go b/storage/ent/db/migrate/schema.go
index d8b8b62c..c31e0a1e 100644
--- a/storage/ent/db/migrate/schema.go
+++ b/storage/ent/db/migrate/schema.go
@@ -101,6 +101,8 @@ var (
 		{Name: "expiry", Type: field.TypeTime, SchemaType: map[string]string{"mysql": "datetime(3)", "postgres": "timestamptz", "sqlite3": "timestamp"}},
 		{Name: "last_request", Type: field.TypeTime, SchemaType: map[string]string{"mysql": "datetime(3)", "postgres": "timestamptz", "sqlite3": "timestamp"}},
 		{Name: "poll_interval", Type: field.TypeInt},
+		{Name: "code_challenge", Type: field.TypeString, Size: 2147483647, Default: "", SchemaType: map[string]string{"mysql": "varchar(384)", "postgres": "text", "sqlite3": "text"}},
+		{Name: "code_challenge_method", Type: field.TypeString, Size: 2147483647, Default: "", SchemaType: map[string]string{"mysql": "varchar(384)", "postgres": "text", "sqlite3": "text"}},
 	}
 	// DeviceTokensTable holds the schema information for the "device_tokens" table.
 	DeviceTokensTable = &schema.Table{
diff --git a/storage/ent/db/mutation.go b/storage/ent/db/mutation.go
index d7d4e423..53a4a9fd 100644
--- a/storage/ent/db/mutation.go
+++ b/storage/ent/db/mutation.go
@@ -3633,20 +3633,22 @@ func (m *DeviceRequestMutation) ResetEdge(name string) error {
 // DeviceTokenMutation represents an operation that mutates the DeviceToken nodes in the graph.
 type DeviceTokenMutation struct {
 	config
-	op               Op
-	typ              string
-	id               *int
-	device_code      *string
-	status           *string
-	token            *[]byte
-	expiry           *time.Time
-	last_request     *time.Time
-	poll_interval    *int
-	addpoll_interval *int
-	clearedFields    map[string]struct{}
-	done             bool
-	oldValue         func(context.Context) (*DeviceToken, error)
-	predicates       []predicate.DeviceToken
+	op                    Op
+	typ                   string
+	id                    *int
+	device_code           *string
+	status                *string
+	token                 *[]byte
+	expiry                *time.Time
+	last_request          *time.Time
+	poll_interval         *int
+	addpoll_interval      *int
+	code_challenge        *string
+	code_challenge_method *string
+	clearedFields         map[string]struct{}
+	done                  bool
+	oldValue              func(context.Context) (*DeviceToken, error)
+	predicates            []predicate.DeviceToken
 }
 
 var _ ent.Mutation = (*DeviceTokenMutation)(nil)
@@ -3996,6 +3998,78 @@ func (m *DeviceTokenMutation) ResetPollInterval() {
 	m.addpoll_interval = nil
 }
 
+// SetCodeChallenge sets the "code_challenge" field.
+func (m *DeviceTokenMutation) SetCodeChallenge(s string) {
+	m.code_challenge = &s
+}
+
+// CodeChallenge returns the value of the "code_challenge" field in the mutation.
+func (m *DeviceTokenMutation) CodeChallenge() (r string, exists bool) {
+	v := m.code_challenge
+	if v == nil {
+		return
+	}
+	return *v, true
+}
+
+// OldCodeChallenge returns the old "code_challenge" field's value of the DeviceToken entity.
+// If the DeviceToken object wasn't provided to the builder, the object is fetched from the database.
+// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
+func (m *DeviceTokenMutation) OldCodeChallenge(ctx context.Context) (v string, err error) {
+	if !m.op.Is(OpUpdateOne) {
+		return v, errors.New("OldCodeChallenge is only allowed on UpdateOne operations")
+	}
+	if m.id == nil || m.oldValue == nil {
+		return v, errors.New("OldCodeChallenge requires an ID field in the mutation")
+	}
+	oldValue, err := m.oldValue(ctx)
+	if err != nil {
+		return v, fmt.Errorf("querying old value for OldCodeChallenge: %w", err)
+	}
+	return oldValue.CodeChallenge, nil
+}
+
+// ResetCodeChallenge resets all changes to the "code_challenge" field.
+func (m *DeviceTokenMutation) ResetCodeChallenge() {
+	m.code_challenge = nil
+}
+
+// SetCodeChallengeMethod sets the "code_challenge_method" field.
+func (m *DeviceTokenMutation) SetCodeChallengeMethod(s string) {
+	m.code_challenge_method = &s
+}
+
+// CodeChallengeMethod returns the value of the "code_challenge_method" field in the mutation.
+func (m *DeviceTokenMutation) CodeChallengeMethod() (r string, exists bool) {
+	v := m.code_challenge_method
+	if v == nil {
+		return
+	}
+	return *v, true
+}
+
+// OldCodeChallengeMethod returns the old "code_challenge_method" field's value of the DeviceToken entity.
+// If the DeviceToken object wasn't provided to the builder, the object is fetched from the database.
+// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
+func (m *DeviceTokenMutation) OldCodeChallengeMethod(ctx context.Context) (v string, err error) {
+	if !m.op.Is(OpUpdateOne) {
+		return v, errors.New("OldCodeChallengeMethod is only allowed on UpdateOne operations")
+	}
+	if m.id == nil || m.oldValue == nil {
+		return v, errors.New("OldCodeChallengeMethod requires an ID field in the mutation")
+	}
+	oldValue, err := m.oldValue(ctx)
+	if err != nil {
+		return v, fmt.Errorf("querying old value for OldCodeChallengeMethod: %w", err)
+	}
+	return oldValue.CodeChallengeMethod, nil
+}
+
+// ResetCodeChallengeMethod resets all changes to the "code_challenge_method" field.
+func (m *DeviceTokenMutation) ResetCodeChallengeMethod() {
+	m.code_challenge_method = nil
+}
+
 // Where appends a list predicates to the DeviceTokenMutation builder.
 func (m *DeviceTokenMutation) Where(ps ...predicate.DeviceToken) {
 	m.predicates = append(m.predicates, ps...)
@@ -4015,7 +4089,7 @@ func (m *DeviceTokenMutation) Type() string {
 // order to get all numeric fields that were incremented/decremented, call
 // AddedFields().
 func (m *DeviceTokenMutation) Fields() []string {
-	fields := make([]string, 0, 6)
+	fields := make([]string, 0, 8)
 	if m.device_code != nil {
 		fields = append(fields, devicetoken.FieldDeviceCode)
 	}
@@ -4034,6 +4108,12 @@ func (m *DeviceTokenMutation) Fields() []string {
 	if m.poll_interval != nil {
 		fields = append(fields, devicetoken.FieldPollInterval)
 	}
+	if m.code_challenge != nil {
+		fields = append(fields, devicetoken.FieldCodeChallenge)
+	}
+	if m.code_challenge_method != nil {
+		fields = append(fields, devicetoken.FieldCodeChallengeMethod)
+	}
 	return fields
 }
 
@@ -4054,6 +4134,10 @@ func (m *DeviceTokenMutation) Field(name string) (ent.Value, bool) {
 		return m.LastRequest()
 	case devicetoken.FieldPollInterval:
 		return m.PollInterval()
+	case devicetoken.FieldCodeChallenge:
+		return m.CodeChallenge()
+	case devicetoken.FieldCodeChallengeMethod:
+		return m.CodeChallengeMethod()
 	}
 	return nil, false
 }
@@ -4075,6 +4159,10 @@ func (m *DeviceTokenMutation) OldField(ctx context.Context, name string) (ent.Va
 		return m.OldLastRequest(ctx)
 	case devicetoken.FieldPollInterval:
 		return m.OldPollInterval(ctx)
+	case devicetoken.FieldCodeChallenge:
+		return m.OldCodeChallenge(ctx)
+	case devicetoken.FieldCodeChallengeMethod:
+		return m.OldCodeChallengeMethod(ctx)
 	}
 	return nil, fmt.Errorf("unknown DeviceToken field %s", name)
 }
@@ -4126,6 +4214,20 @@ func (m *DeviceTokenMutation) SetField(name string, value ent.Value) error {
 		}
 		m.SetPollInterval(v)
 		return nil
+	case devicetoken.FieldCodeChallenge:
+		v, ok := value.(string)
+		if !ok {
+			return fmt.Errorf("unexpected type %T for field %s", value, name)
+		}
+		m.SetCodeChallenge(v)
+		return nil
+	case devicetoken.FieldCodeChallengeMethod:
+		v, ok := value.(string)
+		if !ok {
+			return fmt.Errorf("unexpected type %T for field %s", value, name)
+		}
+		m.SetCodeChallengeMethod(v)
+		return nil
 	}
 	return fmt.Errorf("unknown DeviceToken field %s", name)
 }
@@ -4217,6 +4319,12 @@ func (m *DeviceTokenMutation) ResetField(name string) error {
 	case devicetoken.FieldPollInterval:
 		m.ResetPollInterval()
 		return nil
+	case devicetoken.FieldCodeChallenge:
+		m.ResetCodeChallenge()
+		return nil
+	case devicetoken.FieldCodeChallengeMethod:
+		m.ResetCodeChallengeMethod()
+		return nil
 	}
 	return fmt.Errorf("unknown DeviceToken field %s", name)
 }
diff --git a/storage/ent/db/runtime.go b/storage/ent/db/runtime.go
index d3123b3f..4be5fbfb 100644
--- a/storage/ent/db/runtime.go
+++ b/storage/ent/db/runtime.go
@@ -142,6 +142,14 @@ func init() {
 	devicetokenDescStatus := devicetokenFields[1].Descriptor()
 	// devicetoken.StatusValidator is a validator for the "status" field. It is called by the builders before save.
 	devicetoken.StatusValidator = devicetokenDescStatus.Validators[0].(func(string) error)
+	// devicetokenDescCodeChallenge is the schema descriptor for code_challenge field.
+	devicetokenDescCodeChallenge := devicetokenFields[6].Descriptor()
+	// devicetoken.DefaultCodeChallenge holds the default value on creation for the code_challenge field.
+	devicetoken.DefaultCodeChallenge = devicetokenDescCodeChallenge.Default.(string)
+	// devicetokenDescCodeChallengeMethod is the schema descriptor for code_challenge_method field.
+	devicetokenDescCodeChallengeMethod := devicetokenFields[7].Descriptor()
+	// devicetoken.DefaultCodeChallengeMethod holds the default value on creation for the code_challenge_method field.
+	devicetoken.DefaultCodeChallengeMethod = devicetokenDescCodeChallengeMethod.Default.(string)
 	keysFields := schema.Keys{}.Fields()
 	_ = keysFields
 	// keysDescID is the schema descriptor for id field.
diff --git a/storage/ent/schema/devicetoken.go b/storage/ent/schema/devicetoken.go
index 29927e2b..dc0e7b8e 100644
--- a/storage/ent/schema/devicetoken.go
+++ b/storage/ent/schema/devicetoken.go
@@ -8,12 +8,14 @@ import (
 /* Original SQL table:
 create table device_token
 (
-    device_code   text      not null primary key,
-    status        text      not null,
-    token         blob,
-    expiry        timestamp not null,
-    last_request  timestamp not null,
-    poll_interval integer   not null
+    device_code           text            not null primary key,
+    status                text            not null,
+    token                 blob,
+    expiry                timestamp       not null,
+    last_request          timestamp       not null,
+    poll_interval         integer         not null,
+    code_challenge        text default '' not null,
+    code_challenge_method text default '' not null
 );
 */
 
@@ -38,6 +40,12 @@ func (DeviceToken) Fields() []ent.Field {
 		field.Time("last_request").
 			SchemaType(timeSchema),
 		field.Int("poll_interval"),
+		field.Text("code_challenge").
+			SchemaType(textSchema).
+			Default(""),
+		field.Text("code_challenge_method").
+			SchemaType(textSchema).
+			Default(""),
 	}
 }
 
diff --git a/storage/etcd/etcd.go b/storage/etcd/etcd.go
index 63fa7bc2..13e815ec 100644
--- a/storage/etcd/etcd.go
+++ b/storage/etcd/etcd.go
@@ -605,8 +605,11 @@ func (c *conn) CreateDeviceToken(t storage.DeviceToken) error {
 func (c *conn) GetDeviceToken(deviceCode string) (t storage.DeviceToken, err error) {
 	ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout)
 	defer cancel()
-	err = c.getKey(ctx, keyID(deviceTokenPrefix, deviceCode), &t)
-	return t, err
+	var dt DeviceToken
+	if err = c.getKey(ctx, keyID(deviceTokenPrefix, deviceCode), &dt); err == nil {
+		t = toStorageDeviceToken(dt)
+	}
+	return
 }
 
 func (c *conn) listDeviceTokens(ctx context.Context) (deviceTokens []DeviceToken, err error) {
diff --git a/storage/etcd/types.go b/storage/etcd/types.go
index 9390608a..1174a2d2 100644
--- a/storage/etcd/types.go
+++ b/storage/etcd/types.go
@@ -281,6 +281,8 @@ type DeviceToken struct {
 	Expiry              time.Time `json:"expiry"`
 	LastRequestTime     time.Time `json:"last_request"`
 	PollIntervalSeconds int       `json:"poll_interval"`
+	CodeChallenge       string    `json:"code_challenge,omitempty"`
+	CodeChallengeMethod string    `json:"code_challenge_method,omitempty"`
 }
 
 func fromStorageDeviceToken(t storage.DeviceToken) DeviceToken {
@@ -291,6 +293,8 @@ func fromStorageDeviceToken(t storage.DeviceToken) DeviceToken {
 		Expiry:              t.Expiry,
 		LastRequestTime:     t.LastRequestTime,
 		PollIntervalSeconds: t.PollIntervalSeconds,
+		CodeChallenge:       t.PKCE.CodeChallenge,
+		CodeChallengeMethod: t.PKCE.CodeChallengeMethod,
 	}
 }
 
@@ -302,5 +306,9 @@ func toStorageDeviceToken(t DeviceToken) storage.DeviceToken {
 		Expiry:              t.Expiry,
 		LastRequestTime:     t.LastRequestTime,
 		PollIntervalSeconds: t.PollIntervalSeconds,
+		PKCE: storage.PKCE{
+			CodeChallenge:       t.CodeChallenge,
+			CodeChallengeMethod: t.CodeChallengeMethod,
+		},
 	}
 }
diff --git a/storage/kubernetes/types.go b/storage/kubernetes/types.go
index faf4ac57..5149e3ee 100644
--- a/storage/kubernetes/types.go
+++ b/storage/kubernetes/types.go
@@ -802,6 +802,8 @@ type DeviceToken struct {
 	Expiry              time.Time `json:"expiry"`
 	LastRequestTime     time.Time `json:"last_request"`
 	PollIntervalSeconds int       `json:"poll_interval"`
+	CodeChallenge       string    `json:"code_challenge,omitempty"`
+	CodeChallengeMethod string    `json:"code_challenge_method,omitempty"`
 }
 
 // DeviceTokenList is a list of DeviceTokens.
@@ -826,6 +828,8 @@ func (cli *client) fromStorageDeviceToken(t storage.DeviceToken) DeviceToken {
 		Expiry:              t.Expiry,
 		LastRequestTime:     t.LastRequestTime,
 		PollIntervalSeconds: t.PollIntervalSeconds,
+		CodeChallenge:       t.PKCE.CodeChallenge,
+		CodeChallengeMethod: t.PKCE.CodeChallengeMethod,
 	}
 	return req
 }
@@ -838,5 +842,9 @@ func toStorageDeviceToken(t DeviceToken) storage.DeviceToken {
 		Expiry:              t.Expiry,
 		LastRequestTime:     t.LastRequestTime,
 		PollIntervalSeconds: t.PollIntervalSeconds,
+		PKCE: storage.PKCE{
+			CodeChallenge:       t.CodeChallenge,
+			CodeChallengeMethod: t.CodeChallengeMethod,
+		},
 	}
 }
diff --git a/storage/sql/crud.go b/storage/sql/crud.go
index 5a234f9d..ac67bf28 100644
--- a/storage/sql/crud.go
+++ b/storage/sql/crud.go
@@ -927,12 +927,12 @@ func (c *conn) CreateDeviceRequest(d storage.DeviceRequest) error {
 func (c *conn) CreateDeviceToken(t storage.DeviceToken) error {
 	_, err := c.Exec(`
 		insert into device_token (
-			device_code, status, token, expiry, last_request, poll_interval
+			device_code, status, token, expiry, last_request, poll_interval, code_challenge, code_challenge_method
 		)
 		values (
-			$1, $2, $3, $4, $5, $6
+			$1, $2, $3, $4, $5, $6, $7, $8
 		);`,
-		t.DeviceCode, t.Status, t.Token, t.Expiry, t.LastRequestTime, t.PollIntervalSeconds,
+		t.DeviceCode, t.Status, t.Token, t.Expiry, t.LastRequestTime, t.PollIntervalSeconds, t.PKCE.CodeChallenge, t.PKCE.CodeChallengeMethod,
 	)
 	if err != nil {
 		if c.alreadyExistsCheck(err) {
@@ -972,10 +972,10 @@ func (c *conn) GetDeviceToken(deviceCode string) (storage.DeviceToken, error) {
 func getDeviceToken(q querier, deviceCode string) (a storage.DeviceToken, err error) {
 	err = q.QueryRow(`
 		select
-            status, token, expiry, last_request, poll_interval
+            status, token, expiry, last_request, poll_interval, code_challenge, code_challenge_method
 		from device_token where device_code = $1;
 	`, deviceCode).Scan(
-		&a.Status, &a.Token, &a.Expiry, &a.LastRequestTime, &a.PollIntervalSeconds,
+		&a.Status, &a.Token, &a.Expiry, &a.LastRequestTime, &a.PollIntervalSeconds, &a.PKCE.CodeChallenge, &a.PKCE.CodeChallengeMethod,
 	)
 	if err != nil {
 		if err == sql.ErrNoRows {
@@ -1002,11 +1002,13 @@ func (c *conn) UpdateDeviceToken(deviceCode string, updater func(old storage.Dev
 				status = $1, 
 				token = $2,
 				last_request = $3,
-				poll_interval = $4
+				poll_interval = $4,
+				code_challenge = $5,
+				code_challenge_method = $6
 			where
-				device_code = $5
+				device_code = $7
 		`,
-			r.Status, r.Token, r.LastRequestTime, r.PollIntervalSeconds, r.DeviceCode,
+			r.Status, r.Token, r.LastRequestTime, r.PollIntervalSeconds, r.PKCE.CodeChallenge, r.PKCE.CodeChallengeMethod, r.DeviceCode,
 		)
 		if err != nil {
 			return fmt.Errorf("update device token: %v", err)
diff --git a/storage/sql/migrate.go b/storage/sql/migrate.go
index 498db252..57720e17 100644
--- a/storage/sql/migrate.go
+++ b/storage/sql/migrate.go
@@ -281,4 +281,14 @@ var migrations = []migration{
 				add column obsolete_token text default '';`,
 		},
 	},
+	{
+		stmts: []string{
+			`
+			alter table device_token
+				add column code_challenge text not null default '';`,
+			`
+			alter table device_token
+				add column code_challenge_method text not null default '';`,
+		},
+	},
 }
diff --git a/storage/storage.go b/storage/storage.go
index cdd83ca6..af39228a 100644
--- a/storage/storage.go
+++ b/storage/storage.go
@@ -427,4 +427,5 @@ type DeviceToken struct {
 	Expiry              time.Time
 	LastRequestTime     time.Time
 	PollIntervalSeconds int
+	PKCE                PKCE
 }


Test patch:
diff --git a/server/deviceflowhandlers_test.go b/server/deviceflowhandlers_test.go
index 225703a4..9a9f2858 100644
--- a/server/deviceflowhandlers_test.go
+++ b/server/deviceflowhandlers_test.go
@@ -49,6 +49,7 @@ func TestHandleDeviceCode(t *testing.T) {
 	tests := []struct {
 		testName               string
 		clientID               string
+		codeChallengeMethod    string
 		requestType            string
 		scopes                 []string
 		expectedResponseCode   int
@@ -71,6 +72,24 @@ func TestHandleDeviceCode(t *testing.T) {
 			expectedResponseCode: http.StatusBadRequest,
 			expectedContentType:  "application/json",
 		},
+		{
+			testName:             "New Code with valid PKCE",
+			clientID:             "test",
+			requestType:          "POST",
+			scopes:               []string{"openid", "profile", "email"},
+			codeChallengeMethod:  "S256",
+			expectedResponseCode: http.StatusOK,
+			expectedContentType:  "application/json",
+		},
+		{
+			testName:             "Invalid code challenge method",
+			clientID:             "test",
+			requestType:          "POST",
+			codeChallengeMethod:  "invalid",
+			scopes:               []string{"openid", "profile", "email"},
+			expectedResponseCode: http.StatusBadRequest,
+			expectedContentType:  "application/json",
+		},
 	}
 	for _, tc := range tests {
 		t.Run(tc.testName, func(t *testing.T) {
@@ -92,6 +111,7 @@ func TestHandleDeviceCode(t *testing.T) {
 
 			data := url.Values{}
 			data.Set("client_id", tc.clientID)
+			data.Set("code_challenge_method", tc.codeChallengeMethod)
 			for _, scope := range tc.scopes {
 				data.Add("scope", scope)
 			}
@@ -401,6 +421,13 @@ func TestDeviceTokenResponse(t *testing.T) {
 
 	now := func() time.Time { return t0 }
 
+	// Base PKCE values
+	// base64-urlencoded, sha256 digest of code_verifier
+	codeChallenge := "L7ZqsT_zNwvrH8E7J0CqPHx1wgBaFiaE-fAZcKUUAbc"
+	codeChallengeMethod := "S256"
+	// "random" string between 43 & 128 ASCII characters
+	codeVerifier := "66114650f56cc45dee7ee03c49f048ddf9aa53cbf5b09985832fa4f790ff2604"
+
 	baseDeviceRequest := storage.DeviceRequest{
 		UserCode:   "ABCD-WXYZ",
 		DeviceCode: "foo",
@@ -415,6 +442,7 @@ func TestDeviceTokenResponse(t *testing.T) {
 		testDeviceToken        storage.DeviceToken
 		testGrantType          string
 		testDeviceCode         string
+		testCodeVerifier       string
 		expectedServerResponse string
 		expectedResponseCode   int
 	}{
@@ -524,6 +552,101 @@ func TestDeviceTokenResponse(t *testing.T) {
 			expectedServerResponse: "{\"access_token\": \"foobar\"}",
 			expectedResponseCode:   http.StatusOK,
 		},
+		{
+			testName: "Successful Exchange with PKCE",
+			testDeviceToken: storage.DeviceToken{
+				DeviceCode:          "foo",
+				Status:              deviceTokenComplete,
+				Token:               "{\"access_token\": \"foobar\"}",
+				Expiry:              now().Add(5 * time.Minute),
+				LastRequestTime:     time.Time{},
+				PollIntervalSeconds: 0,
+				PKCE: storage.PKCE{
+					CodeChallenge:       codeChallenge,
+					CodeChallengeMethod: codeChallengeMethod,
+				},
+			},
+			testDeviceCode:         "foo",
+			testCodeVerifier:       codeVerifier,
+			testDeviceRequest:      baseDeviceRequest,
+			expectedServerResponse: "{\"access_token\": \"foobar\"}",
+			expectedResponseCode:   http.StatusOK,
+		},
+		{
+			testName: "Test Exchange started with PKCE but without verifier provided",
+			testDeviceToken: storage.DeviceToken{
+				DeviceCode:          "foo",
+				Status:              deviceTokenComplete,
+				Token:               "{\"access_token\": \"foobar\"}",
+				Expiry:              now().Add(5 * time.Minute),
+				LastRequestTime:     time.Time{},
+				PollIntervalSeconds: 0,
+				PKCE: storage.PKCE{
+					CodeChallenge:       codeChallenge,
+					CodeChallengeMethod: codeChallengeMethod,
+				},
+			},
+			testDeviceCode:         "foo",
+			testDeviceRequest:      baseDeviceRequest,
+			expectedServerResponse: errInvalidGrant,
+			expectedResponseCode:   http.StatusBadRequest,
+		},
+		{
+			testName: "Test Exchange not started with PKCE but verifier provided",
+			testDeviceToken: storage.DeviceToken{
+				DeviceCode:          "foo",
+				Status:              deviceTokenComplete,
+				Token:               "{\"access_token\": \"foobar\"}",
+				Expiry:              now().Add(5 * time.Minute),
+				LastRequestTime:     time.Time{},
+				PollIntervalSeconds: 0,
+			},
+			testDeviceCode:         "foo",
+			testCodeVerifier:       codeVerifier,
+			testDeviceRequest:      baseDeviceRequest,
+			expectedServerResponse: errInvalidRequest,
+			expectedResponseCode:   http.StatusBadRequest,
+		},
+		{
+			testName: "Test with PKCE but incorrect verifier provided",
+			testDeviceToken: storage.DeviceToken{
+				DeviceCode:          "foo",
+				Status:              deviceTokenComplete,
+				Token:               "{\"access_token\": \"foobar\"}",
+				Expiry:              now().Add(5 * time.Minute),
+				LastRequestTime:     time.Time{},
+				PollIntervalSeconds: 0,
+				PKCE: storage.PKCE{
+					CodeChallenge:       codeChallenge,
+					CodeChallengeMethod: codeChallengeMethod,
+				},
+			},
+			testDeviceCode:         "foo",
+			testCodeVerifier:       "invalid",
+			testDeviceRequest:      baseDeviceRequest,
+			expectedServerResponse: errInvalidGrant,
+			expectedResponseCode:   http.StatusBadRequest,
+		},
+		{
+			testName: "Test with PKCE but incorrect challenge provided",
+			testDeviceToken: storage.DeviceToken{
+				DeviceCode:          "foo",
+				Status:              deviceTokenComplete,
+				Token:               "{\"access_token\": \"foobar\"}",
+				Expiry:              now().Add(5 * time.Minute),
+				LastRequestTime:     time.Time{},
+				PollIntervalSeconds: 0,
+				PKCE: storage.PKCE{
+					CodeChallenge:       "invalid",
+					CodeChallengeMethod: codeChallengeMethod,
+				},
+			},
+			testDeviceCode:         "foo",
+			testCodeVerifier:       codeVerifier,
+			testDeviceRequest:      baseDeviceRequest,
+			expectedServerResponse: errInvalidGrant,
+			expectedResponseCode:   http.StatusBadRequest,
+		},
 	}
 	for _, tc := range tests {
 		t.Run(tc.testName, func(t *testing.T) {
@@ -558,6 +681,9 @@ func TestDeviceTokenResponse(t *testing.T) {
 			}
 			data.Set("grant_type", grantType)
 			data.Set("device_code", tc.testDeviceCode)
+			if tc.testCodeVerifier != "" {
+				data.Set("code_verifier", tc.testCodeVerifier)
+			}
 			req, _ := http.NewRequest("POST", u.String(), bytes.NewBufferString(data.Encode()))
 			req.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")