From d1d8f2ea9208bc5b0dd222d2ca1e0a357e3ca8d5 Mon Sep 17 00:00:00 2001 From: Matteo Panzeri Date: Sun, 10 May 2026 15:32:53 +0200 Subject: [PATCH] fix(server): enforce client.AllowedConnectors in handleTokenExchange The token-exchange grant did not check client.AllowedConnectors before invoking the connector, while handleConnectorLogin (handlers.go:377) and parseAuthorizationRequest (oauth2.go:535) both call isConnectorAllowed. This left a confidential client able to exchange a subject token via a connector not in its AllowedConnectors list, provided the connector existed in the dex configuration and supported the token-exchange grant. Insert isConnectorAllowed before getConnector in handleTokenExchange so a disallowed request cannot probe connector existence. Mirrors the placement and error shape of the two existing call sites. Tests: TestHandleTokenExchangeAllowedConnectors covers allowed, allowed-non-first-entry, denied, and empty (permit-any) cases. Addresses GHSA-7qjx-gp9h-65qj. Signed-off-by: Matteo Panzeri --- server/handlers.go | 7 +++++ server/handlers_test.go | 61 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/server/handlers.go b/server/handlers.go index 6225fff09b..c4d757dd5f 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -1832,6 +1832,13 @@ func (s *Server) handleTokenExchange(w http.ResponseWriter, r *http.Request, cli return } + if !isConnectorAllowed(client.AllowedConnectors, connID) { + s.logger.ErrorContext(r.Context(), "connector not allowed for client", + "connector_id", connID, "client_id", client.ID) + s.tokenErrHelper(w, errInvalidRequest, "Connector not allowed for this client.", http.StatusBadRequest) + return + } + conn, err := s.getConnector(ctx, connID) if err != nil { s.logger.ErrorContext(r.Context(), "failed to get connector", "err", err) diff --git a/server/handlers_test.go b/server/handlers_test.go index c64bb15a0d..b6f8fb2665 100644 --- a/server/handlers_test.go +++ b/server/handlers_test.go @@ -1832,6 +1832,67 @@ func TestHandleTokenExchangeConnectorGrantTypeRestriction(t *testing.T) { require.Equal(t, http.StatusBadRequest, rr.Code, rr.Body.String()) } +func TestHandleTokenExchangeAllowedConnectors(t *testing.T) { + tests := []struct { + name string + allowedConnectors []string + expectedCode int + }{ + { + name: "connector in allowed list", + allowedConnectors: []string{"mock"}, + expectedCode: http.StatusOK, + }, + { + name: "connector matches non-first entry in allowed list", + allowedConnectors: []string{"other", "mock"}, + expectedCode: http.StatusOK, + }, + { + name: "connector not in allowed list", + allowedConnectors: []string{"other"}, + expectedCode: http.StatusBadRequest, + }, + { + name: "empty allowed list permits any connector", + allowedConnectors: nil, + expectedCode: http.StatusOK, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx := t.Context() + httpServer, s := newTestServer(t, func(c *Config) { + c.Storage.CreateClient(ctx, storage.Client{ + ID: "client_1", + Secret: "secret_1", + AllowedConnectors: tc.allowedConnectors, + }) + }) + defer httpServer.Close() + + vals := make(url.Values) + vals.Set("grant_type", grantTypeTokenExchange) + vals.Set("connector_id", "mock") + vals.Set("scope", "openid") + vals.Set("requested_token_type", tokenTypeAccess) + vals.Set("subject_token_type", tokenTypeID) + vals.Set("subject_token", "foobar") + vals.Set("client_id", "client_1") + vals.Set("client_secret", "secret_1") + + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, httpServer.URL+"/token", strings.NewReader(vals.Encode())) + req.Header.Set("content-type", "application/x-www-form-urlencoded") + + s.handleToken(rr, req) + + require.Equal(t, tc.expectedCode, rr.Code, rr.Body.String()) + }) + } +} + func TestHandleAuthorizationConnectorGrantTypeFiltering(t *testing.T) { tests := []struct { name string