Repository: dexidp/dex
Pull request #2679 (2022-09-22 19:57:17)

fix for issue 2670; check for no serviceAccountFilePath and no email
#### Overview

This (hopefully) reverts a bug introduced in #2530 which does not handle the case where application default credentials nor a service account file (containing credentials) are needed to operate a Google connector.

#### What this PR does / why we need it

Fixes: #2670 

#### Special notes for your reviewer

#### Does this PR introduce a user-facing change?
NONE

```release-note
fixes regression for Google connector when used without groups
```
Signed-off-by: Bob Callaway <bcallaway@google.com>

@ichbinfrog 


Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 192a4404..c38ed934 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -73,7 +73,7 @@ jobs:
         run: docker-compose -f docker-compose.test.yaml up -d
 
       - name: Create kind cluster
-        uses: helm/kind-action@v1.3.0
+        uses: helm/kind-action@v1.4.0
         with:
           version: v0.11.1
           node_image: kindest/node:v1.19.11@sha256:07db187ae84b4b7de440a73886f008cf903fcf5764ba8106a9fd5243d6f32729
diff --git a/connector/google/google.go b/connector/google/google.go
index 72cc6a18..313858f6 100644
--- a/connector/google/google.go
+++ b/connector/google/google.go
@@ -71,10 +71,13 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e
 		scopes = append(scopes, "profile", "email")
 	}
 
-	srv, err := createDirectoryService(c.ServiceAccountFilePath, c.AdminEmail, logger)
-	if err != nil {
-		cancel()
-		return nil, fmt.Errorf("could not create directory service: %v", err)
+	var srv *admin.Service
+	if len(c.Groups) > 0 {
+		srv, err = createDirectoryService(c.ServiceAccountFilePath, c.AdminEmail, logger)
+		if err != nil {
+			cancel()
+			return nil, fmt.Errorf("could not create directory service: %v", err)
+		}
 	}
 
 	clientID := c.ClientID
diff --git a/connector/keystone/keystone.go b/connector/keystone/keystone.go
index db97b5a7..03f47331 100644
--- a/connector/keystone/keystone.go
+++ b/connector/keystone/keystone.go
@@ -18,6 +18,7 @@ type conn struct {
 	Host          string
 	AdminUsername string
 	AdminPassword string
+	client        *http.Client
 	Logger        log.Logger
 }
 
@@ -35,6 +36,7 @@ type domainKeystone struct {
 // Config holds the configuration parameters for Keystone connector.
 // Keystone should expose API v3
 // An example config:
+//
 //	connectors:
 //		type: keystone
 //		id: keystone
@@ -111,11 +113,12 @@ var (
 // Open returns an authentication strategy using Keystone.
 func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
 	return &conn{
-		c.Domain,
-		c.Host,
-		c.AdminUsername,
-		c.AdminPassword,
-		logger,
+		Domain:        c.Domain,
+		Host:          c.Host,
+		AdminUsername: c.AdminUsername,
+		AdminPassword: c.AdminPassword,
+		Logger:        logger,
+		client:        http.DefaultClient,
 	}, nil
 }
 
@@ -192,7 +195,6 @@ func (p *conn) Refresh(
 }
 
 func (p *conn) getTokenResponse(ctx context.Context, username, pass string) (response *http.Response, err error) {
-	client := &http.Client{}
 	jsonData := loginRequestData{
 		auth: auth{
 			Identity: identity{
@@ -221,7 +223,7 @@ func (p *conn) getTokenResponse(ctx context.Context, username, pass string) (res
 	req.Header.Set("Content-Type", "application/json")
 	req = req.WithContext(ctx)
 
-	return client.Do(req)
+	return p.client.Do(req)
 }
 
 func (p *conn) getAdminToken(ctx context.Context) (string, error) {
@@ -243,7 +245,6 @@ func (p *conn) checkIfUserExists(ctx context.Context, userID string, token strin
 func (p *conn) getUser(ctx context.Context, userID string, token string) (*userResponse, error) {
 	// https://developer.openstack.org/api-ref/identity/v3/#show-user-details
 	userURL := p.Host + "/v3/users/" + userID
-	client := &http.Client{}
 	req, err := http.NewRequest("GET", userURL, nil)
 	if err != nil {
 		return nil, err
@@ -251,7 +252,7 @@ func (p *conn) getUser(ctx context.Context, userID string, token string) (*userR
 
 	req.Header.Set("X-Auth-Token", token)
 	req = req.WithContext(ctx)
-	resp, err := client.Do(req)
+	resp, err := p.client.Do(req)
 	if err != nil {
 		return nil, err
 	}
@@ -276,7 +277,6 @@ func (p *conn) getUser(ctx context.Context, userID string, token string) (*userR
 }
 
 func (p *conn) getUserGroups(ctx context.Context, userID string, token string) ([]string, error) {
-	client := &http.Client{}
 	// https://developer.openstack.org/api-ref/identity/v3/#list-groups-to-which-a-user-belongs
 	groupsURL := p.Host + "/v3/users/" + userID + "/groups"
 	req, err := http.NewRequest("GET", groupsURL, nil)
@@ -285,7 +285,7 @@ func (p *conn) getUserGroups(ctx context.Context, userID string, token string) (
 	}
 	req.Header.Set("X-Auth-Token", token)
 	req = req.WithContext(ctx)
-	resp, err := client.Do(req)
+	resp, err := p.client.Do(req)
 	if err != nil {
 		p.Logger.Errorf("keystone: error while fetching user %q groups\n", userID)
 		return nil, err
diff --git a/go.mod b/go.mod
index ef825dfb..850dff12 100644
--- a/go.mod
+++ b/go.mod
@@ -28,12 +28,12 @@ require (
 	github.com/sirupsen/logrus v1.9.0
 	github.com/spf13/cobra v1.5.0
 	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
+	go.etcd.io/etcd/client/pkg/v3 v3.5.5
+	go.etcd.io/etcd/client/v3 v3.5.5
 	golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
-	golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b
+	golang.org/x/net v0.0.0-20220909164309-bea034e7d591
 	golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094
-	google.golang.org/api v0.95.0
+	google.golang.org/api v0.98.0
 	google.golang.org/grpc v1.49.0
 	google.golang.org/protobuf v1.28.1
 	gopkg.in/square/go-jose.v2 v2.6.0
@@ -78,7 +78,7 @@ require (
 	github.com/spf13/cast v1.4.1 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/zclconf/go-cty v1.8.0 // indirect
-	go.etcd.io/etcd/api/v3 v3.5.4 // indirect
+	go.etcd.io/etcd/api/v3 v3.5.5 // indirect
 	go.opencensus.io v0.23.0 // indirect
 	go.uber.org/atomic v1.7.0 // indirect
 	go.uber.org/multierr v1.6.0 // indirect
diff --git a/go.sum b/go.sum
index 1a796167..0628c982 100644
--- a/go.sum
+++ b/go.sum
@@ -409,12 +409,12 @@ github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q
 github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA=
 github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
 github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
-go.etcd.io/etcd/api/v3 v3.5.4 h1:OHVyt3TopwtUQ2GKdd5wu3PmmipR4FTwCqoEjSyRdIc=
-go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
-go.etcd.io/etcd/client/pkg/v3 v3.5.4 h1:lrneYvz923dvC14R54XcA7FXoZ3mlGZAgmwhfm7HqOg=
-go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
-go.etcd.io/etcd/client/v3 v3.5.4 h1:p83BUL3tAYS0OT/r0qglgc3M1JjhM0diV8DSWAhVXv4=
-go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
+go.etcd.io/etcd/api/v3 v3.5.5 h1:BX4JIbQ7hl7+jL+g+2j5UAr0o1bctCm6/Ct+ArBGkf0=
+go.etcd.io/etcd/api/v3 v3.5.5/go.mod h1:KFtNaxGDw4Yx/BA4iPPwevUTAuqcsPxzyX8PHydchN8=
+go.etcd.io/etcd/client/pkg/v3 v3.5.5 h1:9S0JUVvmrVl7wCF39iTQthdaaNIiAaQbmK75ogO6GU8=
+go.etcd.io/etcd/client/pkg/v3 v3.5.5/go.mod h1:ggrwbk069qxpKPq8/FKkQ3Xq9y39kbFR4LnKszpRXeQ=
+go.etcd.io/etcd/client/v3 v3.5.5 h1:q++2WTJbUgpQu4B6hCuT7VkdwaTP7Qz6Daak3WzbrlI=
+go.etcd.io/etcd/client/v3 v3.5.5/go.mod h1:aApjR4WGlSumpnJ2kloS75h6aHUmAyaPLjHMxpc7E7c=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -524,8 +524,9 @@ golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su
 golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
 golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
+golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI=
+golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 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=
@@ -629,7 +630,6 @@ golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/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-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
 golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -748,8 +748,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.95.0 h1:d1c24AAS01DYqXreBeuVV7ewY/U8Mnhh47pwtsgVtYg=
-google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI=
+google.golang.org/api v0.98.0 h1:yxZrcxXESimy6r6mdL5Q6EnZwmewDJK2dVg3g75s5Dg=
+google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
 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=
@@ -864,6 +864,7 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD
 google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
 google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
 google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
 google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
 google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
diff --git a/server/handlers.go b/server/handlers.go
index 5f8caf11..11dcdd07 100755
--- a/server/handlers.go
+++ b/server/handlers.go
@@ -1,6 +1,7 @@
 package server
 
 import (
+	"crypto/hmac"
 	"crypto/sha256"
 	"crypto/subtle"
 	"encoding/base64"
@@ -499,7 +500,15 @@ 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)
 
-	returnURL := path.Join(s.issuerURL.Path, "/approval") + "?req=" + authReq.ID
+	// TODO: if s.skipApproval or !authReq.ForceApprovalPrompt, we can skip the redirect to /approval and go ahead and send code
+
+	// 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
+	h := hmac.New(sha256.New, authReq.HMACKey)
+	h.Write([]byte(authReq.ID))
+	mac := h.Sum(nil)
+
+	returnURL := path.Join(s.issuerURL.Path, "/approval") + "?req=" + authReq.ID + "&hmac=" + base64.RawURLEncoding.EncodeToString(mac)
 	_, ok := conn.(connector.RefreshConnector)
 	if !ok {
 		return returnURL, nil
@@ -544,6 +553,17 @@ func (s *Server) finalizeLogin(identity connector.Identity, authReq storage.Auth
 }
 
 func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
+	macEncoded := r.FormValue("hmac")
+	if macEncoded == "" {
+		s.renderError(r, w, http.StatusUnauthorized, "Unauthorized request")
+		return
+	}
+	mac, err := base64.RawURLEncoding.DecodeString(macEncoded)
+	if err != nil {
+		s.renderError(r, w, http.StatusUnauthorized, "Unauthorized request")
+		return
+	}
+
 	authReq, err := s.storage.GetAuthRequest(r.FormValue("req"))
 	if err != nil {
 		s.logger.Errorf("Failed to get auth request: %v", err)
@@ -556,6 +576,16 @@ func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	// build expected hmac with secret key
+	h := hmac.New(sha256.New, authReq.HMACKey)
+	h.Write([]byte(authReq.ID))
+	expectedMAC := h.Sum(nil)
+	// constant time comparison
+	if !hmac.Equal(mac, expectedMAC) {
+		s.renderError(r, w, http.StatusUnauthorized, "Unauthorized request")
+		return
+	}
+
 	switch r.Method {
 	case http.MethodGet:
 		if s.skipApproval {
diff --git a/server/oauth2.go b/server/oauth2.go
index 998bf02a..67223fa1 100644
--- a/server/oauth2.go
+++ b/server/oauth2.go
@@ -2,6 +2,7 @@ package server
 
 import (
 	"context"
+	"crypto"
 	"crypto/ecdsa"
 	"crypto/elliptic"
 	"crypto/rsa"
@@ -576,6 +577,7 @@ func (s *Server) parseAuthorizationRequest(r *http.Request) (*storage.AuthReques
 			CodeChallenge:       codeChallenge,
 			CodeChallengeMethod: codeChallengeMethod,
 		},
+		HMACKey: storage.NewHMACKey(crypto.SHA256),
 	}, nil
 }
 
diff --git a/storage/conformance/conformance.go b/storage/conformance/conformance.go
index 9d9766eb..1b45b76c 100644
--- a/storage/conformance/conformance.go
+++ b/storage/conformance/conformance.go
@@ -104,7 +104,8 @@ func testAuthRequestCRUD(t *testing.T, s storage.Storage) {
 			EmailVerified: true,
 			Groups:        []string{"a", "b"},
 		},
-		PKCE: codeChallenge,
+		PKCE:    codeChallenge,
+		HMACKey: []byte("hmac_key"),
 	}
 
 	identity := storage.Claims{Email: "foobar"}
@@ -137,6 +138,7 @@ func testAuthRequestCRUD(t *testing.T, s storage.Storage) {
 			EmailVerified: true,
 			Groups:        []string{"a"},
 		},
+		HMACKey: []byte("hmac_key"),
 	}
 
 	if err := s.CreateAuthRequest(a2); err != nil {
@@ -817,6 +819,7 @@ func testGC(t *testing.T, s storage.Storage) {
 			EmailVerified: true,
 			Groups:        []string{"a", "b"},
 		},
+		HMACKey: []byte("hmac_key"),
 	}
 
 	if err := s.CreateAuthRequest(a); err != nil {
diff --git a/storage/ent/client/authrequest.go b/storage/ent/client/authrequest.go
index bde37adc..d68fd438 100644
--- a/storage/ent/client/authrequest.go
+++ b/storage/ent/client/authrequest.go
@@ -31,6 +31,7 @@ func (d *Database) CreateAuthRequest(authRequest storage.AuthRequest) error {
 		SetExpiry(authRequest.Expiry.UTC()).
 		SetConnectorID(authRequest.ConnectorID).
 		SetConnectorData(authRequest.ConnectorData).
+		SetHmacKey(authRequest.HMACKey).
 		Save(context.TODO())
 	if err != nil {
 		return convertDBError("create auth request: %w", err)
@@ -94,6 +95,7 @@ func (d *Database) UpdateAuthRequest(id string, updater func(old storage.AuthReq
 		SetExpiry(newAuthRequest.Expiry.UTC()).
 		SetConnectorID(newAuthRequest.ConnectorID).
 		SetConnectorData(newAuthRequest.ConnectorData).
+		SetHmacKey(newAuthRequest.HMACKey).
 		Save(context.TODO())
 	if err != nil {
 		return rollback(tx, "update auth request uploading: %w", err)
diff --git a/storage/ent/client/types.go b/storage/ent/client/types.go
index 256bb73d..397d4d30 100644
--- a/storage/ent/client/types.go
+++ b/storage/ent/client/types.go
@@ -45,6 +45,7 @@ func toStorageAuthRequest(a *db.AuthRequest) storage.AuthRequest {
 			CodeChallenge:       a.CodeChallenge,
 			CodeChallengeMethod: a.CodeChallengeMethod,
 		},
+		HMACKey: a.HmacKey,
 	}
 }
 
diff --git a/storage/ent/db/authrequest.go b/storage/ent/db/authrequest.go
index ecef32d7..095427ae 100644
--- a/storage/ent/db/authrequest.go
+++ b/storage/ent/db/authrequest.go
@@ -55,6 +55,8 @@ type AuthRequest struct {
 	CodeChallenge string `json:"code_challenge,omitempty"`
 	// CodeChallengeMethod holds the value of the "code_challenge_method" field.
 	CodeChallengeMethod string `json:"code_challenge_method,omitempty"`
+	// HmacKey holds the value of the "hmac_key" field.
+	HmacKey []byte `json:"hmac_key,omitempty"`
 }
 
 // scanValues returns the types for scanning values from sql.Rows.
@@ -62,7 +64,7 @@ func (*AuthRequest) scanValues(columns []string) ([]interface{}, error) {
 	values := make([]interface{}, len(columns))
 	for i := range columns {
 		switch columns[i] {
-		case authrequest.FieldScopes, authrequest.FieldResponseTypes, authrequest.FieldClaimsGroups, authrequest.FieldConnectorData:
+		case authrequest.FieldScopes, authrequest.FieldResponseTypes, authrequest.FieldClaimsGroups, authrequest.FieldConnectorData, authrequest.FieldHmacKey:
 			values[i] = new([]byte)
 		case authrequest.FieldForceApprovalPrompt, authrequest.FieldLoggedIn, authrequest.FieldClaimsEmailVerified:
 			values[i] = new(sql.NullBool)
@@ -211,6 +213,12 @@ func (ar *AuthRequest) assignValues(columns []string, values []interface{}) erro
 			} else if value.Valid {
 				ar.CodeChallengeMethod = value.String
 			}
+		case authrequest.FieldHmacKey:
+			if value, ok := values[i].(*[]byte); !ok {
+				return fmt.Errorf("unexpected type %T for field hmac_key", values[i])
+			} else if value != nil {
+				ar.HmacKey = *value
+			}
 		}
 	}
 	return nil
@@ -297,6 +305,8 @@ func (ar *AuthRequest) String() string {
 	builder.WriteString(", ")
 	builder.WriteString("code_challenge_method=")
 	builder.WriteString(ar.CodeChallengeMethod)
+	builder.WriteString(", hmac_key=")
+	builder.WriteString(fmt.Sprintf("%v", ar.HmacKey))
 	builder.WriteByte(')')
 	return builder.String()
 }
diff --git a/storage/ent/db/authrequest/authrequest.go b/storage/ent/db/authrequest/authrequest.go
index 528d26b6..537f631e 100644
--- a/storage/ent/db/authrequest/authrequest.go
+++ b/storage/ent/db/authrequest/authrequest.go
@@ -45,6 +45,8 @@ const (
 	FieldCodeChallenge = "code_challenge"
 	// FieldCodeChallengeMethod holds the string denoting the code_challenge_method field in the database.
 	FieldCodeChallengeMethod = "code_challenge_method"
+	// FieldHmacKey holds the string denoting the hmac_key field in the database.
+	FieldHmacKey = "hmac_key"
 	// Table holds the table name of the authrequest in the database.
 	Table = "auth_requests"
 )
@@ -71,6 +73,7 @@ var Columns = []string{
 	FieldExpiry,
 	FieldCodeChallenge,
 	FieldCodeChallengeMethod,
+	FieldHmacKey,
 }
 
 // ValidColumn reports if the column name is valid (part of the table columns).
diff --git a/storage/ent/db/authrequest/where.go b/storage/ent/db/authrequest/where.go
index e31cdcee..1fd1d4e4 100644
--- a/storage/ent/db/authrequest/where.go
+++ b/storage/ent/db/authrequest/where.go
@@ -192,6 +192,13 @@ func CodeChallengeMethod(v string) predicate.AuthRequest {
 	})
 }
 
+// HmacKey applies equality check predicate on the "hmac_key" field. It's identical to HmacKeyEQ.
+func HmacKey(v []byte) predicate.AuthRequest {
+	return predicate.AuthRequest(func(s *sql.Selector) {
+		s.Where(sql.EQ(s.C(FieldHmacKey), v))
+	})
+}
+
 // ClientIDEQ applies the EQ predicate on the "client_id" field.
 func ClientIDEQ(v string) predicate.AuthRequest {
 	return predicate.AuthRequest(func(s *sql.Selector) {
@@ -1507,6 +1514,82 @@ func CodeChallengeMethodContainsFold(v string) predicate.AuthRequest {
 	})
 }
 
+// HmacKeyEQ applies the EQ predicate on the "hmac_key" field.
+func HmacKeyEQ(v []byte) predicate.AuthRequest {
+	return predicate.AuthRequest(func(s *sql.Selector) {
+		s.Where(sql.EQ(s.C(FieldHmacKey), v))
+	})
+}
+
+// HmacKeyNEQ applies the NEQ predicate on the "hmac_key" field.
+func HmacKeyNEQ(v []byte) predicate.AuthRequest {
+	return predicate.AuthRequest(func(s *sql.Selector) {
+		s.Where(sql.NEQ(s.C(FieldHmacKey), v))
+	})
+}
+
+// HmacKeyIn applies the In predicate on the "hmac_key" field.
+func HmacKeyIn(vs ...[]byte) predicate.AuthRequest {
+	v := make([]interface{}, len(vs))
+	for i := range v {
+		v[i] = vs[i]
+	}
+	return predicate.AuthRequest(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(FieldHmacKey), v...))
+	})
+}
+
+// HmacKeyNotIn applies the NotIn predicate on the "hmac_key" field.
+func HmacKeyNotIn(vs ...[]byte) predicate.AuthRequest {
+	v := make([]interface{}, len(vs))
+	for i := range v {
+		v[i] = vs[i]
+	}
+	return predicate.AuthRequest(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(FieldHmacKey), v...))
+	})
+}
+
+// HmacKeyGT applies the GT predicate on the "hmac_key" field.
+func HmacKeyGT(v []byte) predicate.AuthRequest {
+	return predicate.AuthRequest(func(s *sql.Selector) {
+		s.Where(sql.GT(s.C(FieldHmacKey), v))
+	})
+}
+
+// HmacKeyGTE applies the GTE predicate on the "hmac_key" field.
+func HmacKeyGTE(v []byte) predicate.AuthRequest {
+	return predicate.AuthRequest(func(s *sql.Selector) {
+		s.Where(sql.GTE(s.C(FieldHmacKey), v))
+	})
+}
+
+// HmacKeyLT applies the LT predicate on the "hmac_key" field.
+func HmacKeyLT(v []byte) predicate.AuthRequest {
+	return predicate.AuthRequest(func(s *sql.Selector) {
+		s.Where(sql.LT(s.C(FieldHmacKey), v))
+	})
+}
+
+// HmacKeyLTE applies the LTE predicate on the "hmac_key" field.
+func HmacKeyLTE(v []byte) predicate.AuthRequest {
+	return predicate.AuthRequest(func(s *sql.Selector) {
+		s.Where(sql.LTE(s.C(FieldHmacKey), v))
+	})
+}
+
 // And groups predicates with the AND operator between them.
 func And(predicates ...predicate.AuthRequest) predicate.AuthRequest {
 	return predicate.AuthRequest(func(s *sql.Selector) {
diff --git a/storage/ent/db/authrequest_create.go b/storage/ent/db/authrequest_create.go
index 63e0357d..c353c182 100644
--- a/storage/ent/db/authrequest_create.go
+++ b/storage/ent/db/authrequest_create.go
@@ -158,6 +158,12 @@ func (arc *AuthRequestCreate) SetNillableCodeChallengeMethod(s *string) *AuthReq
 	return arc
 }
 
+// SetHmacKey sets the "hmac_key" field.
+func (arc *AuthRequestCreate) SetHmacKey(b []byte) *AuthRequestCreate {
+	arc.mutation.SetHmacKey(b)
+	return arc
+}
+
 // SetID sets the "id" field.
 func (arc *AuthRequestCreate) SetID(s string) *AuthRequestCreate {
 	arc.mutation.SetID(s)
@@ -302,6 +308,9 @@ func (arc *AuthRequestCreate) check() error {
 	if _, ok := arc.mutation.CodeChallengeMethod(); !ok {
 		return &ValidationError{Name: "code_challenge_method", err: errors.New(`db: missing required field "AuthRequest.code_challenge_method"`)}
 	}
+	if _, ok := arc.mutation.HmacKey(); !ok {
+		return &ValidationError{Name: "hmac_key", err: errors.New(`db: missing required field "AuthRequest.hmac_key"`)}
+	}
 	if v, ok := arc.mutation.ID(); ok {
 		if err := authrequest.IDValidator(v); err != nil {
 			return &ValidationError{Name: "id", err: fmt.Errorf(`db: validator failed for field "AuthRequest.id": %w`, err)}
@@ -495,6 +504,14 @@ func (arc *AuthRequestCreate) createSpec() (*AuthRequest, *sqlgraph.CreateSpec)
 		})
 		_node.CodeChallengeMethod = value
 	}
+	if value, ok := arc.mutation.HmacKey(); ok {
+		_spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
+			Type:   field.TypeBytes,
+			Value:  value,
+			Column: authrequest.FieldHmacKey,
+		})
+		_node.HmacKey = value
+	}
 	return _node, _spec
 }
 
diff --git a/storage/ent/db/authrequest_update.go b/storage/ent/db/authrequest_update.go
index 22dbb24b..f0bf9b34 100644
--- a/storage/ent/db/authrequest_update.go
+++ b/storage/ent/db/authrequest_update.go
@@ -190,6 +190,12 @@ func (aru *AuthRequestUpdate) SetNillableCodeChallengeMethod(s *string) *AuthReq
 	return aru
 }
 
+// SetHmacKey sets the "hmac_key" field.
+func (aru *AuthRequestUpdate) SetHmacKey(b []byte) *AuthRequestUpdate {
+	aru.mutation.SetHmacKey(b)
+	return aru
+}
+
 // Mutation returns the AuthRequestMutation object of the builder.
 func (aru *AuthRequestUpdate) Mutation() *AuthRequestMutation {
 	return aru.mutation
@@ -424,6 +430,13 @@ func (aru *AuthRequestUpdate) sqlSave(ctx context.Context) (n int, err error) {
 			Column: authrequest.FieldCodeChallengeMethod,
 		})
 	}
+	if value, ok := aru.mutation.HmacKey(); ok {
+		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
+			Type:   field.TypeBytes,
+			Value:  value,
+			Column: authrequest.FieldHmacKey,
+		})
+	}
 	if n, err = sqlgraph.UpdateNodes(ctx, aru.driver, _spec); err != nil {
 		if _, ok := err.(*sqlgraph.NotFoundError); ok {
 			err = &NotFoundError{authrequest.Label}
@@ -605,6 +618,12 @@ func (aruo *AuthRequestUpdateOne) SetNillableCodeChallengeMethod(s *string) *Aut
 	return aruo
 }
 
+// SetHmacKey sets the "hmac_key" field.
+func (aruo *AuthRequestUpdateOne) SetHmacKey(b []byte) *AuthRequestUpdateOne {
+	aruo.mutation.SetHmacKey(b)
+	return aruo
+}
+
 // Mutation returns the AuthRequestMutation object of the builder.
 func (aruo *AuthRequestUpdateOne) Mutation() *AuthRequestMutation {
 	return aruo.mutation
@@ -869,6 +888,13 @@ func (aruo *AuthRequestUpdateOne) sqlSave(ctx context.Context) (_node *AuthReque
 			Column: authrequest.FieldCodeChallengeMethod,
 		})
 	}
+	if value, ok := aruo.mutation.HmacKey(); ok {
+		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
+			Type:   field.TypeBytes,
+			Value:  value,
+			Column: authrequest.FieldHmacKey,
+		})
+	}
 	_node = &AuthRequest{config: aruo.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 ced57b32..d3295a0c 100644
--- a/storage/ent/db/migrate/schema.go
+++ b/storage/ent/db/migrate/schema.go
@@ -55,6 +55,7 @@ var (
 		{Name: "expiry", Type: field.TypeTime, SchemaType: map[string]string{"mysql": "datetime(3)", "postgres": "timestamptz", "sqlite3": "timestamp"}},
 		{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"}},
+		{Name: "hmac_key", Type: field.TypeBytes},
 	}
 	// AuthRequestsTable holds the schema information for the "auth_requests" table.
 	AuthRequestsTable = &schema.Table{
diff --git a/storage/ent/db/mutation.go b/storage/ent/db/mutation.go
index af12a242..85e1af72 100644
--- a/storage/ent/db/mutation.go
+++ b/storage/ent/db/mutation.go
@@ -1205,6 +1205,7 @@ type AuthRequestMutation struct {
 	expiry                    *time.Time
 	code_challenge            *string
 	code_challenge_method     *string
+	hmac_key                  *[]byte
 	clearedFields             map[string]struct{}
 	done                      bool
 	oldValue                  func(context.Context) (*AuthRequest, error)
@@ -2051,6 +2052,42 @@ func (m *AuthRequestMutation) ResetCodeChallengeMethod() {
 	m.code_challenge_method = nil
 }
 
+// SetHmacKey sets the "hmac_key" field.
+func (m *AuthRequestMutation) SetHmacKey(b []byte) {
+	m.hmac_key = &b
+}
+
+// HmacKey returns the value of the "hmac_key" field in the mutation.
+func (m *AuthRequestMutation) HmacKey() (r []byte, exists bool) {
+	v := m.hmac_key
+	if v == nil {
+		return
+	}
+	return *v, true
+}
+
+// OldHmacKey returns the old "hmac_key" field's value of the AuthRequest entity.
+// If the AuthRequest 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 *AuthRequestMutation) OldHmacKey(ctx context.Context) (v []byte, err error) {
+	if !m.op.Is(OpUpdateOne) {
+		return v, errors.New("OldHmacKey is only allowed on UpdateOne operations")
+	}
+	if m.id == nil || m.oldValue == nil {
+		return v, errors.New("OldHmacKey requires an ID field in the mutation")
+	}
+	oldValue, err := m.oldValue(ctx)
+	if err != nil {
+		return v, fmt.Errorf("querying old value for OldHmacKey: %w", err)
+	}
+	return oldValue.HmacKey, nil
+}
+
+// ResetHmacKey resets all changes to the "hmac_key" field.
+func (m *AuthRequestMutation) ResetHmacKey() {
+	m.hmac_key = nil
+}
+
 // Where appends a list predicates to the AuthRequestMutation builder.
 func (m *AuthRequestMutation) Where(ps ...predicate.AuthRequest) {
 	m.predicates = append(m.predicates, ps...)
@@ -2070,7 +2107,7 @@ func (m *AuthRequestMutation) Type() string {
 // order to get all numeric fields that were incremented/decremented, call
 // AddedFields().
 func (m *AuthRequestMutation) Fields() []string {
-	fields := make([]string, 0, 19)
+	fields := make([]string, 0, 20)
 	if m.client_id != nil {
 		fields = append(fields, authrequest.FieldClientID)
 	}
@@ -2128,6 +2165,9 @@ func (m *AuthRequestMutation) Fields() []string {
 	if m.code_challenge_method != nil {
 		fields = append(fields, authrequest.FieldCodeChallengeMethod)
 	}
+	if m.hmac_key != nil {
+		fields = append(fields, authrequest.FieldHmacKey)
+	}
 	return fields
 }
 
@@ -2174,6 +2214,8 @@ func (m *AuthRequestMutation) Field(name string) (ent.Value, bool) {
 		return m.CodeChallenge()
 	case authrequest.FieldCodeChallengeMethod:
 		return m.CodeChallengeMethod()
+	case authrequest.FieldHmacKey:
+		return m.HmacKey()
 	}
 	return nil, false
 }
@@ -2221,6 +2263,8 @@ func (m *AuthRequestMutation) OldField(ctx context.Context, name string) (ent.Va
 		return m.OldCodeChallenge(ctx)
 	case authrequest.FieldCodeChallengeMethod:
 		return m.OldCodeChallengeMethod(ctx)
+	case authrequest.FieldHmacKey:
+		return m.OldHmacKey(ctx)
 	}
 	return nil, fmt.Errorf("unknown AuthRequest field %s", name)
 }
@@ -2363,6 +2407,13 @@ func (m *AuthRequestMutation) SetField(name string, value ent.Value) error {
 		}
 		m.SetCodeChallengeMethod(v)
 		return nil
+	case authrequest.FieldHmacKey:
+		v, ok := value.([]byte)
+		if !ok {
+			return fmt.Errorf("unexpected type %T for field %s", value, name)
+		}
+		m.SetHmacKey(v)
+		return nil
 	}
 	return fmt.Errorf("unknown AuthRequest field %s", name)
 }
@@ -2496,6 +2547,9 @@ func (m *AuthRequestMutation) ResetField(name string) error {
 	case authrequest.FieldCodeChallengeMethod:
 		m.ResetCodeChallengeMethod()
 		return nil
+	case authrequest.FieldHmacKey:
+		m.ResetHmacKey()
+		return nil
 	}
 	return fmt.Errorf("unknown AuthRequest field %s", name)
 }
diff --git a/storage/ent/schema/authrequest.go b/storage/ent/schema/authrequest.go
index 7d41e830..2b75927b 100644
--- a/storage/ent/schema/authrequest.go
+++ b/storage/ent/schema/authrequest.go
@@ -27,7 +27,8 @@ create table auth_request
     expiry                    timestamp not null,
     claims_preferred_username text default '' not null,
     code_challenge            text default '' not null,
-    code_challenge_method     text default '' not null
+    code_challenge_method     text default '' not null,
+    hmac_key                  blob
 );
 */
 
@@ -86,6 +87,7 @@ func (AuthRequest) Fields() []ent.Field {
 		field.Text("code_challenge_method").
 			SchemaType(textSchema).
 			Default(""),
+		field.Bytes("hmac_key"),
 	}
 }
 
diff --git a/storage/etcd/types.go b/storage/etcd/types.go
index 1174a2d2..91199ab6 100644
--- a/storage/etcd/types.go
+++ b/storage/etcd/types.go
@@ -84,6 +84,8 @@ type AuthRequest struct {
 
 	CodeChallenge       string `json:"code_challenge,omitempty"`
 	CodeChallengeMethod string `json:"code_challenge_method,omitempty"`
+
+	HMACKey []byte `json:"hmac_key"`
 }
 
 func fromStorageAuthRequest(a storage.AuthRequest) AuthRequest {
@@ -103,6 +105,7 @@ func fromStorageAuthRequest(a storage.AuthRequest) AuthRequest {
 		ConnectorData:       a.ConnectorData,
 		CodeChallenge:       a.PKCE.CodeChallenge,
 		CodeChallengeMethod: a.PKCE.CodeChallengeMethod,
+		HMACKey:             a.HMACKey,
 	}
 }
 
@@ -125,6 +128,7 @@ func toStorageAuthRequest(a AuthRequest) storage.AuthRequest {
 			CodeChallenge:       a.CodeChallenge,
 			CodeChallengeMethod: a.CodeChallengeMethod,
 		},
+		HMACKey: a.HMACKey,
 	}
 }
 
diff --git a/storage/health.go b/storage/health.go
index 8a2f5a3d..1b6e22c6 100644
--- a/storage/health.go
+++ b/storage/health.go
@@ -2,6 +2,7 @@ package storage
 
 import (
 	"context"
+	"crypto"
 	"fmt"
 	"time"
 )
@@ -14,7 +15,8 @@ func NewCustomHealthCheckFunc(s Storage, now func() time.Time) func(context.Cont
 			ClientID: NewID(),
 
 			// Set a short expiry so if the delete fails this will be cleaned up quickly by garbage collection.
-			Expiry: now().Add(time.Minute),
+			Expiry:  now().Add(time.Minute),
+			HMACKey: NewHMACKey(crypto.SHA256),
 		}
 
 		if err := s.CreateAuthRequest(a); err != nil {
diff --git a/storage/kubernetes/types.go b/storage/kubernetes/types.go
index 5149e3ee..a5ec29af 100644
--- a/storage/kubernetes/types.go
+++ b/storage/kubernetes/types.go
@@ -356,6 +356,8 @@ type AuthRequest struct {
 
 	CodeChallenge       string `json:"code_challenge,omitempty"`
 	CodeChallengeMethod string `json:"code_challenge_method,omitempty"`
+
+	HMACKey []byte `json:"hmac_key"`
 }
 
 // AuthRequestList is a list of AuthRequests.
@@ -384,6 +386,7 @@ func toStorageAuthRequest(req AuthRequest) storage.AuthRequest {
 			CodeChallenge:       req.CodeChallenge,
 			CodeChallengeMethod: req.CodeChallengeMethod,
 		},
+		HMACKey: req.HMACKey,
 	}
 	return a
 }
@@ -412,6 +415,7 @@ func (cli *client) fromStorageAuthRequest(a storage.AuthRequest) AuthRequest {
 		Claims:              fromStorageClaims(a.Claims),
 		CodeChallenge:       a.PKCE.CodeChallenge,
 		CodeChallengeMethod: a.PKCE.CodeChallengeMethod,
+		HMACKey:             a.HMACKey,
 	}
 	return req
 }
diff --git a/storage/sql/crud.go b/storage/sql/crud.go
index ac67bf28..1583c177 100644
--- a/storage/sql/crud.go
+++ b/storage/sql/crud.go
@@ -131,10 +131,11 @@ func (c *conn) CreateAuthRequest(a storage.AuthRequest) error {
 			claims_email, claims_email_verified, claims_groups,
 			connector_id, connector_data,
 			expiry,
-			code_challenge, code_challenge_method
+			code_challenge, code_challenge_method,
+			hmac_key
 		)
 		values (
-			$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20
+			$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21
 		);
 	`,
 		a.ID, a.ClientID, encoder(a.ResponseTypes), encoder(a.Scopes), a.RedirectURI, a.Nonce, a.State,
@@ -144,6 +145,7 @@ func (c *conn) CreateAuthRequest(a storage.AuthRequest) error {
 		a.ConnectorID, a.ConnectorData,
 		a.Expiry,
 		a.PKCE.CodeChallenge, a.PKCE.CodeChallengeMethod,
+		a.HMACKey,
 	)
 	if err != nil {
 		if c.alreadyExistsCheck(err) {
@@ -175,8 +177,9 @@ func (c *conn) UpdateAuthRequest(id string, updater func(a storage.AuthRequest)
 				claims_groups = $14,
 				connector_id = $15, connector_data = $16,
 				expiry = $17,
-				code_challenge = $18, code_challenge_method = $19
-			where id = $20;
+				code_challenge = $18, code_challenge_method = $19,
+				hmac_key = $20
+			where id = $21;
 		`,
 			a.ClientID, encoder(a.ResponseTypes), encoder(a.Scopes), a.RedirectURI, a.Nonce, a.State,
 			a.ForceApprovalPrompt, a.LoggedIn,
@@ -185,7 +188,7 @@ func (c *conn) UpdateAuthRequest(id string, updater func(a storage.AuthRequest)
 			encoder(a.Claims.Groups),
 			a.ConnectorID, a.ConnectorData,
 			a.Expiry,
-			a.PKCE.CodeChallenge, a.PKCE.CodeChallengeMethod,
+			a.PKCE.CodeChallenge, a.PKCE.CodeChallengeMethod, a.HMACKey,
 			r.ID,
 		)
 		if err != nil {
@@ -207,7 +210,7 @@ func getAuthRequest(q querier, id string) (a storage.AuthRequest, err error) {
 			claims_user_id, claims_username, claims_preferred_username,
 			claims_email, claims_email_verified, claims_groups,
 			connector_id, connector_data, expiry,
-			code_challenge, code_challenge_method
+			code_challenge, code_challenge_method, hmac_key
 		from auth_request where id = $1;
 	`, id).Scan(
 		&a.ID, &a.ClientID, decoder(&a.ResponseTypes), decoder(&a.Scopes), &a.RedirectURI, &a.Nonce, &a.State,
@@ -216,7 +219,7 @@ func getAuthRequest(q querier, id string) (a storage.AuthRequest, err error) {
 		&a.Claims.Email, &a.Claims.EmailVerified,
 		decoder(&a.Claims.Groups),
 		&a.ConnectorID, &a.ConnectorData, &a.Expiry,
-		&a.PKCE.CodeChallenge, &a.PKCE.CodeChallengeMethod,
+		&a.PKCE.CodeChallenge, &a.PKCE.CodeChallengeMethod, &a.HMACKey,
 	)
 	if err != nil {
 		if err == sql.ErrNoRows {
diff --git a/storage/sql/migrate.go b/storage/sql/migrate.go
index 57720e17..83e9c20d 100644
--- a/storage/sql/migrate.go
+++ b/storage/sql/migrate.go
@@ -291,4 +291,11 @@ var migrations = []migration{
 				add column code_challenge_method text not null default '';`,
 		},
 	},
+	{
+		stmts: []string{
+			`
+			alter table auth_request
+				add column hmac_key bytea;`,
+		},
+	},
 }
diff --git a/storage/storage.go b/storage/storage.go
index af39228a..198a70c8 100644
--- a/storage/storage.go
+++ b/storage/storage.go
@@ -1,6 +1,7 @@
 package storage
 
 import (
+	"crypto"
 	"crypto/rand"
 	"encoding/base32"
 	"errors"
@@ -47,6 +48,11 @@ func newSecureID(len int) string {
 	return string(buff[0]%26+'a') + strings.TrimRight(encoding.EncodeToString(buff[1:]), "=")
 }
 
+// NewHMACKey returns a random key which can be used in the computation of an HMAC
+func NewHMACKey(h crypto.Hash) []byte {
+	return []byte(newSecureID(h.Size()))
+}
+
 // GCResult returns the number of objects deleted by garbage collection.
 type GCResult struct {
 	AuthRequests   int64
@@ -223,6 +229,9 @@ type AuthRequest struct {
 
 	// PKCE CodeChallenge and CodeChallengeMethod
 	PKCE PKCE
+
+	// HMACKey is used when generating an AuthRequest-specific HMAC
+	HMACKey []byte
 }
 
 // AuthCode represents a code which can be exchanged for an OAuth2 token response.


Test patch:
diff --git a/connector/google/google_test.go b/connector/google/google_test.go
index 5cecbec9..26241ab6 100644
--- a/connector/google/google_test.go
+++ b/connector/google/google_test.go
@@ -72,12 +72,22 @@ func TestOpen(t *testing.T) {
 	assert.Nil(t, err)
 
 	for name, reference := range map[string]testCase{
+		"not_requesting_groups": {
+			config: &Config{
+				ClientID:     "testClient",
+				ClientSecret: "testSecret",
+				RedirectURI:  ts.URL + "/callback",
+				Scopes:       []string{"openid"},
+			},
+			expectedErr: "",
+		},
 		"missing_admin_email": {
 			config: &Config{
 				ClientID:     "testClient",
 				ClientSecret: "testSecret",
 				RedirectURI:  ts.URL + "/callback",
 				Scopes:       []string{"openid", "groups"},
+				Groups:       []string{"someGroup"},
 			},
 			expectedErr: "requires adminEmail",
 		},
@@ -89,6 +99,7 @@ func TestOpen(t *testing.T) {
 				Scopes:                 []string{"openid", "groups"},
 				AdminEmail:             "foo@bar.com",
 				ServiceAccountFilePath: "not_found.json",
+				Groups:                 []string{"someGroup"},
 			},
 			expectedErr: "error reading credentials",
 		},
@@ -100,6 +111,7 @@ func TestOpen(t *testing.T) {
 				Scopes:                 []string{"openid", "groups"},
 				AdminEmail:             "foo@bar.com",
 				ServiceAccountFilePath: serviceAccountFilePath,
+				Groups:                 []string{"someGroup"},
 			},
 			expectedErr: "",
 		},
@@ -110,6 +122,7 @@ func TestOpen(t *testing.T) {
 				RedirectURI:  ts.URL + "/callback",
 				Scopes:       []string{"openid", "groups"},
 				AdminEmail:   "foo@bar.com",
+				Groups:       []string{"someGroup"},
 			},
 			adc:         serviceAccountFilePath,
 			expectedErr: "",
@@ -122,6 +135,7 @@ func TestOpen(t *testing.T) {
 				Scopes:                 []string{"openid", "groups"},
 				AdminEmail:             "foo@bar.com",
 				ServiceAccountFilePath: serviceAccountFilePath,
+				Groups:                 []string{"someGroup"},
 			},
 			adc:         "/dev/null",
 			expectedErr: "",
diff --git a/connector/keystone/keystone_test.go b/connector/keystone/keystone_test.go
index fc6c01e2..8f1ea1bb 100644
--- a/connector/keystone/keystone_test.go
+++ b/connector/keystone/keystone_test.go
@@ -42,8 +42,6 @@ type groupResponse struct {
 
 func getAdminToken(t *testing.T, adminName, adminPass string) (token, id string) {
 	t.Helper()
-	client := &http.Client{}
-
 	jsonData := loginRequestData{
 		auth: auth{
 			Identity: identity{
@@ -70,7 +68,7 @@ func getAdminToken(t *testing.T, adminName, adminPass string) (token, id string)
 	}
 
 	req.Header.Set("Content-Type", "application/json")
-	resp, err := client.Do(req)
+	resp, err := http.DefaultClient.Do(req)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -93,7 +91,6 @@ func getAdminToken(t *testing.T, adminName, adminPass string) (token, id string)
 
 func createUser(t *testing.T, token, userName, userEmail, userPass string) string {
 	t.Helper()
-	client := &http.Client{}
 
 	createUserData := map[string]interface{}{
 		"user": map[string]interface{}{
@@ -116,7 +113,7 @@ func createUser(t *testing.T, token, userName, userEmail, userPass string) strin
 	}
 	req.Header.Set("X-Auth-Token", token)
 	req.Header.Add("Content-Type", "application/json")
-	resp, err := client.Do(req)
+	resp, err := http.DefaultClient.Do(req)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -139,7 +136,6 @@ func createUser(t *testing.T, token, userName, userEmail, userPass string) strin
 // delete group or user
 func deleteResource(t *testing.T, token, id, uri string) {
 	t.Helper()
-	client := &http.Client{}
 
 	deleteURI := uri + id
 	req, err := http.NewRequest("DELETE", deleteURI, nil)
@@ -148,7 +144,7 @@ func deleteResource(t *testing.T, token, id, uri string) {
 	}
 	req.Header.Set("X-Auth-Token", token)
 
-	resp, err := client.Do(req)
+	resp, err := http.DefaultClient.Do(req)
 	if err != nil {
 		t.Fatalf("error: %v", err)
 	}
@@ -157,7 +153,6 @@ func deleteResource(t *testing.T, token, id, uri string) {
 
 func createGroup(t *testing.T, token, description, name string) string {
 	t.Helper()
-	client := &http.Client{}
 
 	createGroupData := map[string]interface{}{
 		"group": map[string]interface{}{
@@ -177,7 +172,7 @@ func createGroup(t *testing.T, token, description, name string) string {
 	}
 	req.Header.Set("X-Auth-Token", token)
 	req.Header.Add("Content-Type", "application/json")
-	resp, err := client.Do(req)
+	resp, err := http.DefaultClient.Do(req)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -200,14 +195,13 @@ func createGroup(t *testing.T, token, description, name string) string {
 func addUserToGroup(t *testing.T, token, groupID, userID string) error {
 	t.Helper()
 	uri := groupsURL + groupID + "/users/" + userID
-	client := &http.Client{}
 	req, err := http.NewRequest("PUT", uri, nil)
 	if err != nil {
 		return err
 	}
 	req.Header.Set("X-Auth-Token", token)
 
-	resp, err := client.Do(req)
+	resp, err := http.DefaultClient.Do(req)
 	if err != nil {
 		t.Fatalf("error: %v", err)
 	}
@@ -219,7 +213,8 @@ func addUserToGroup(t *testing.T, token, groupID, userID string) error {
 func TestIncorrectCredentialsLogin(t *testing.T) {
 	setupVariables(t)
 	c := conn{
-		Host: keystoneURL, Domain: testDomain,
+		client: http.DefaultClient,
+		Host:   keystoneURL, Domain: testDomain,
 		AdminUsername: adminUser, AdminPassword: adminPass,
 	}
 	s := connector.Scopes{OfflineAccess: true, Groups: true}
@@ -296,7 +291,8 @@ func TestValidUserLogin(t *testing.T) {
 			defer deleteResource(t, token, userID, usersURL)
 
 			c := conn{
-				Host: keystoneURL, Domain: tt.input.domain,
+				client: http.DefaultClient,
+				Host:   keystoneURL, Domain: tt.input.domain,
 				AdminUsername: adminUser, AdminPassword: adminPass,
 			}
 			s := connector.Scopes{OfflineAccess: true, Groups: true}
@@ -333,7 +329,8 @@ func TestUseRefreshToken(t *testing.T) {
 	defer deleteResource(t, token, groupID, groupsURL)
 
 	c := conn{
-		Host: keystoneURL, Domain: testDomain,
+		client: http.DefaultClient,
+		Host:   keystoneURL, Domain: testDomain,
 		AdminUsername: adminUser, AdminPassword: adminPass,
 	}
 	s := connector.Scopes{OfflineAccess: true, Groups: true}
@@ -358,7 +355,8 @@ func TestUseRefreshTokenUserDeleted(t *testing.T) {
 	userID := createUser(t, token, testUser, testEmail, testPass)
 
 	c := conn{
-		Host: keystoneURL, Domain: testDomain,
+		client: http.DefaultClient,
+		Host:   keystoneURL, Domain: testDomain,
 		AdminUsername: adminUser, AdminPassword: adminPass,
 	}
 	s := connector.Scopes{OfflineAccess: true, Groups: true}
@@ -388,7 +386,8 @@ func TestUseRefreshTokenGroupsChanged(t *testing.T) {
 	defer deleteResource(t, token, userID, usersURL)
 
 	c := conn{
-		Host: keystoneURL, Domain: testDomain,
+		client: http.DefaultClient,
+		Host:   keystoneURL, Domain: testDomain,
 		AdminUsername: adminUser, AdminPassword: adminPass,
 	}
 	s := connector.Scopes{OfflineAccess: true, Groups: true}
@@ -424,7 +423,8 @@ func TestNoGroupsInScope(t *testing.T) {
 	defer deleteResource(t, token, userID, usersURL)
 
 	c := conn{
-		Host: keystoneURL, Domain: testDomain,
+		client: http.DefaultClient,
+		Host:   keystoneURL, Domain: testDomain,
 		AdminUsername: adminUser, AdminPassword: adminPass,
 	}
 	s := connector.Scopes{OfflineAccess: true, Groups: false}