Skip to content

Commit f7b7170

Browse files
committed
refactor(server): migrate password and token-exchange to the Grant abstraction
Turn the token endpoint into a fixed pipeline of shared control points so a grant cannot skip a security-sensitive step. Dispatch always runs, in order: 1. authenticate the client 2. validate the requested scopes (cross-client trust included) 3. resolve the connector and enforce the connector-authorization invariant 4. Authorize — the grant proves the identity against the resolved connector 5. mint the token response 6. write the response or error A Grant only fills the parts unique to it — a scope policy, its connector id, and Authorize — while the Endpoint owns every shared phase. The cross-client trust check and the connector-authorization invariant (client allows the connector, connector allows the grant type) now run in the pipeline, not inside each grant where one could be forgotten: a grant supplies its connector id and the endpoint resolves and checks it. A grant that uses no connector, like client_credentials, returns an empty id and the step is skipped. Token minting is unified: tokens.Issuer.Issue is the single mint every standard grant uses, now issuing an ID token exactly when the openid scope was requested, which also covers client_credentials. A grant whose response is non-standard implements Minter; token-exchange uses it to mint the single RFC 8693 token, the only response shape the standard mint does not produce. A grant never touches the ResponseWriter: it returns a tokens.Response or an *oauth2.Error, and the Endpoint writes it. checkConnectorAllowed stays in the server package for the refresh grant, which has not moved yet. Signed-off-by: maksim.nabokikh <max.nabokih@gmail.com>
1 parent f225b47 commit f7b7170

13 files changed

Lines changed: 591 additions & 478 deletions

server/connectorauth.go

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,25 @@
11
package server
22

3-
// This file is the single home for connector authorization: deciding whether a
4-
// given client may use a given connector, and whether a connector permits a given
5-
// grant type.
3+
// This file handles connector authorization for the token grants that still live
4+
// in the server package: deciding whether a given client may use a given
5+
// connector, and whether a connector permits a given grant type.
66
//
77
// INVARIANT: every token grant that resolves a connector MUST enforce both
88
// policies before issuing tokens:
9-
// 1. checkConnectorAllowed(client, connID) — the connector is in the client's
10-
// AllowedConnectors.
11-
// 2. connectors.GrantTypeAllowed(conn.GrantTypes, grantType) — the connector permits this
12-
// grant type.
9+
// 1. connectors.ConnectorAllowed(client.AllowedConnectors, connID) — the connector
10+
// is in the client's AllowedConnectors.
11+
// 2. connectors.GrantTypeAllowed(conn.GrantTypes, grantType) — the connector permits
12+
// this grant type.
1313
//
14-
// The password and token-exchange grants run #1 before resolving the connector
15-
// (so a disallowed connector is never instantiated) and #2 right after. The
16-
// refresh grant resolves the connector and runs #2 inside getRefreshTokenFromStorage
17-
// (shared with token introspection), then runs #1 in handleRefreshToken — a
18-
// different order, but both still gate token issuance.
14+
// Grants that have moved to the grants package enforce the invariant through
15+
// grants.Endpoint.resolveConnector, which runs both checks in one call. The
16+
// refresh grant, still here, resolves the connector and runs #2 inside
17+
// getRefreshTokenFromStorage (shared with token introspection), then runs #1 via
18+
// checkConnectorAllowed in handleRefreshToken — a different order, but both still
19+
// gate token issuance.
1920
//
20-
// Both checks live here / are referenced from here so a new grant handler can find
21-
// them in one place and not silently forget one — the omission that let the
22-
// password and refresh grants skip check #1. Browser/auth-code paths enforce the
23-
// same policies with their own HTML/redirect error surface (login.go, authorize.go).
21+
// Browser/auth-code paths enforce the same policies with their own HTML/redirect
22+
// error surface (login.go, authorize.go).
2423

2524
import (
2625
"net/http"

server/grant_password.go

Lines changed: 0 additions & 145 deletions
This file was deleted.

server/grant_tokenexchange.go

Lines changed: 0 additions & 128 deletions
This file was deleted.

server/grant_tokenexchange_test.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212

1313
"github.com/stretchr/testify/require"
1414

15-
"github.com/dexidp/dex/server/grants"
1615
"github.com/dexidp/dex/server/oauth2"
1716
"github.com/dexidp/dex/server/tokens"
1817
"github.com/dexidp/dex/storage"
@@ -108,7 +107,7 @@ func TestHandleTokenExchange(t *testing.T) {
108107
req := httptest.NewRequest(http.MethodPost, httpServer.URL+"/token", strings.NewReader(vals.Encode()))
109108
req.Header.Set("content-type", "application/x-www-form-urlencoded")
110109

111-
s.handleToken(rr, req, &grants.Endpoint{})
110+
s.handleToken(rr, req, s.newTokenEndpoint())
112111

113112
require.Equal(t, tc.expectedCode, rr.Code, rr.Body.String())
114113
require.Equal(t, "application/json", rr.Result().Header.Get("content-type"))
@@ -148,7 +147,7 @@ func TestHandleTokenExchangeLogsSuccess(t *testing.T) {
148147
req := httptest.NewRequest(http.MethodPost, httpServer.URL+"/token", strings.NewReader(vals.Encode()))
149148
req.Header.Set("content-type", "application/x-www-form-urlencoded")
150149

151-
s.handleToken(rr, req, &grants.Endpoint{})
150+
s.handleToken(rr, req, s.newTokenEndpoint())
152151
require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
153152

154153
var found map[string]any
@@ -205,7 +204,7 @@ func TestHandleTokenExchangeConnectorGrantTypeRestriction(t *testing.T) {
205204
req := httptest.NewRequest(http.MethodPost, httpServer.URL+"/token", strings.NewReader(vals.Encode()))
206205
req.Header.Set("content-type", "application/x-www-form-urlencoded")
207206

208-
s.handleToken(rr, req, &grants.Endpoint{})
207+
s.handleToken(rr, req, s.newTokenEndpoint())
209208

210209
require.Equal(t, http.StatusBadRequest, rr.Code, rr.Body.String())
211210
}
@@ -264,7 +263,7 @@ func TestHandleTokenExchangeAllowedConnectors(t *testing.T) {
264263
req := httptest.NewRequest(http.MethodPost, httpServer.URL+"/token", strings.NewReader(vals.Encode()))
265264
req.Header.Set("content-type", "application/x-www-form-urlencoded")
266265

267-
s.handleToken(rr, req, &grants.Endpoint{})
266+
s.handleToken(rr, req, s.newTokenEndpoint())
268267

269268
require.Equal(t, tc.expectedCode, rr.Code, rr.Body.String())
270269
if tc.expectedCode == http.StatusBadRequest {

0 commit comments

Comments
 (0)