Skip to content

Commit 5d05562

Browse files
committed
refactor(server): resolve every grant's connector in the pipeline
Address review: the refresh grant re-implemented the connector-authorization invariant (ConnectorAllowed + GrantTypeAllowed) inline and held its own connector cache, duplicating the endpoint's resolve step. ConnectorID now takes the context and client and may look the id up in storage, so grants whose connector is recorded on a stored token — refresh reads it off the refresh token — still source it through the same hook. The endpoint resolves the connector and enforces the invariant once, in one place, and hands it to Authorize. The refresh grant drops its connector cache and inline checks; it validates the token in ConnectorID, stashes it on the request, and reuses it in Authorize. Also replace the hand-rolled contains helper with slices.Contains. Signed-off-by: maksim.nabokikh <max.nabokih@gmail.com>
1 parent 2e24e5d commit 5d05562

6 files changed

Lines changed: 57 additions & 61 deletions

File tree

server/grants/authcode.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ func (g *authorizationCode) ScopePolicy() ScopePolicy {
3535
}
3636

3737
// ConnectorID is empty: the connector is recorded on the stored auth code and
38-
// was already authorized at /auth, so it is resolved inside Authorize rather than
39-
// through the endpoint's connector step.
40-
func (g *authorizationCode) ConnectorID(req *Request) string {
41-
return ""
38+
// was already authorized at /auth. The grant resolves it (without re-running the
39+
// invariant) only to decide on a refresh token, inside ExchangeAuthCode.
40+
func (g *authorizationCode) ConnectorID(ctx context.Context, req *Request, client storage.Client) (string, *oauth2.Error) {
41+
return "", nil
4242
}
4343

4444
// handle an access token request https://tools.ietf.org/html/rfc6749#section-4.1.3

server/grants/clientcredentials.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ func (g *clientCredentials) ScopePolicy() ScopePolicy {
3939
}
4040

4141
// ConnectorID is empty: client_credentials involves no connector.
42-
func (g *clientCredentials) ConnectorID(req *Request) string {
43-
return ""
42+
func (g *clientCredentials) ConnectorID(ctx context.Context, req *Request, client storage.Client) (string, *oauth2.Error) {
43+
return "", nil
4444
}
4545

4646
func (g *clientCredentials) Authorize(ctx context.Context, req *Request, client storage.Client, conn connectors.Connector) (*Result, error) {

server/grants/grants.go

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ type Request struct {
4242
SubjectToken string
4343
SubjectTokenType string
4444
RequestedTokenType string
45+
46+
// resolvedRefresh is the validated refresh token, looked up while resolving
47+
// the connector and reused by the refresh grant's Authorize.
48+
resolvedRefresh *storage.RefreshToken
4549
}
4650

4751
// parseRequest reads the whole token request form once. Client credentials come
@@ -96,10 +100,11 @@ type Grant interface {
96100
// this grant.
97101
ScopePolicy() ScopePolicy
98102
// ConnectorID is the connector this grant authenticates against, or "" when
99-
// it uses none (client_credentials). The endpoint resolves it and enforces
100-
// the connector-authorization invariant before Authorize, so a grant cannot
101-
// forget the check.
102-
ConnectorID(req *Request) string
103+
// it uses none (client_credentials). The grant may read it from the request
104+
// or look it up in storage. The endpoint then resolves it and enforces the
105+
// connector-authorization invariant before Authorize, so a grant cannot
106+
// forget the check. Returning an error rejects the request.
107+
ConnectorID(ctx context.Context, req *Request, client storage.Client) (string, *oauth2.Error)
103108
// Authorize turns the validated request into the authorization to issue
104109
// tokens for, proving the resource owner's identity against conn (the zero
105110
// Connector when ConnectorID is ""). Returning an *oauth2.Error makes the
@@ -163,7 +168,7 @@ func NewEndpoint(issuer *tokens.Issuer, s storage.Storage, conns *connectors.Cac
163168
&password{logger: logger, connectorID: passwordConnector},
164169
&tokenExchange{issuer: issuer, logger: logger},
165170
&authorizationCode{storage: s, connectors: conns, now: now, logger: logger},
166-
&refresh{storage: s, connectors: conns, issuer: issuer, policy: refreshPolicy, sessionsEnabled: sessionsEnabled, now: now, logger: logger},
171+
&refresh{storage: s, issuer: issuer, policy: refreshPolicy, sessionsEnabled: sessionsEnabled, now: now, logger: logger},
167172
)
168173
return e
169174
}
@@ -207,7 +212,12 @@ func (e *Endpoint) Dispatch(w http.ResponseWriter, r *http.Request, grantType st
207212

208213
// 3. Resolve the grant's connector and enforce the connector-authorization
209214
// invariant. A grant that uses no connector resolves to the zero Connector.
210-
conn, oerr := e.resolveConnector(ctx, grant, req, client)
215+
connID, oerr := grant.ConnectorID(ctx, req, client)
216+
if oerr != nil {
217+
e.writeError(ctx, w, oerr)
218+
return true
219+
}
220+
conn, oerr := e.resolveConnector(ctx, connID, client, grant.GrantType())
211221
if oerr != nil {
212222
e.writeError(ctx, w, oerr)
213223
return true
@@ -293,13 +303,12 @@ func (e *Endpoint) validateScopes(ctx context.Context, client storage.Client, re
293303
return nil
294304
}
295305

296-
// resolveConnector enforces the connector-authorization invariant for the grant
297-
// and returns the opened connector: the client must allow the connector, and the
298-
// connector must permit the grant type. A grant that uses no connector
299-
// (ConnectorID == "") resolves to the zero Connector. Running here, before
300-
// Authorize, means no grant can forget the check.
301-
func (e *Endpoint) resolveConnector(ctx context.Context, grant Grant, req *Request, client storage.Client) (connectors.Connector, *oauth2.Error) {
302-
connID := grant.ConnectorID(req)
306+
// resolveConnector enforces the connector-authorization invariant and returns the
307+
// opened connector: the client must allow the connector, and the connector must
308+
// permit the grant type. connID == "" (a grant that uses no connector) resolves
309+
// to the zero Connector. Running here, before Authorize, means no grant can
310+
// forget the check.
311+
func (e *Endpoint) resolveConnector(ctx context.Context, connID string, client storage.Client, grantType string) (connectors.Connector, *oauth2.Error) {
303312
if connID == "" {
304313
return connectors.Connector{}, nil
305314
}
@@ -313,8 +322,8 @@ func (e *Endpoint) resolveConnector(ctx context.Context, grant Grant, req *Reque
313322
e.logger.ErrorContext(ctx, "failed to get connector", "connector_id", connID, "err", err)
314323
return connectors.Connector{}, &oauth2.Error{Type: oauth2.InvalidRequest, Description: "Requested connector does not exist.", Status: http.StatusBadRequest}
315324
}
316-
if !connectors.GrantTypeAllowed(conn.GrantTypes, grant.GrantType()) {
317-
e.logger.ErrorContext(ctx, "connector does not allow grant", "connector_id", connID, "grant_type", grant.GrantType())
325+
if !connectors.GrantTypeAllowed(conn.GrantTypes, grantType) {
326+
e.logger.ErrorContext(ctx, "connector does not allow grant", "connector_id", connID, "grant_type", grantType)
318327
return connectors.Connector{}, &oauth2.Error{Type: oauth2.InvalidRequest, Description: "Requested connector does not support this grant type.", Status: http.StatusBadRequest}
319328
}
320329
return conn, nil

server/grants/password.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ func (g *password) ScopePolicy() ScopePolicy {
4343
}
4444

4545
// ConnectorID is the connector the password grant is configured to use.
46-
func (g *password) ConnectorID(req *Request) string {
47-
return g.connectorID
46+
func (g *password) ConnectorID(ctx context.Context, req *Request, client storage.Client) (string, *oauth2.Error) {
47+
return g.connectorID, nil
4848
}
4949

5050
func (g *password) Authorize(ctx context.Context, req *Request, client storage.Client, conn connectors.Connector) (*Result, error) {

server/grants/refresh.go

Lines changed: 23 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"log/slog"
77
"net/http"
8+
"slices"
89
"time"
910

1011
"github.com/dexidp/dex/connector"
@@ -21,7 +22,6 @@ import (
2122
// than minting a new one, so it mints its own instead of the standard Issue.
2223
type refresh struct {
2324
storage storage.Storage
24-
connectors *connectors.Cache
2525
issuer *tokens.Issuer
2626
policy *tokens.RefreshStrategy
2727
sessionsEnabled bool
@@ -43,38 +43,34 @@ func (g *refresh) ScopePolicy() ScopePolicy {
4343
return ScopePolicy{}
4444
}
4545

46-
// ConnectorID is empty: the connector is recorded on the stored refresh token, so
47-
// it is resolved and re-checked inside Authorize on every refresh.
48-
func (g *refresh) ConnectorID(req *Request) string {
49-
return ""
50-
}
51-
52-
func (g *refresh) Authorize(ctx context.Context, req *Request, client storage.Client, conn connectors.Connector) (*Result, error) {
46+
// ConnectorID validates the refresh token and reports the connector recorded on
47+
// it, so the endpoint resolves and re-checks that connector on every refresh: a
48+
// client's allowed connectors, or a connector's grant types, may have been
49+
// tightened after the token was issued. The validated token is stashed for
50+
// Authorize.
51+
func (g *refresh) ConnectorID(ctx context.Context, req *Request, client storage.Client) (string, *oauth2.Error) {
5352
token, oerr := parseRefreshToken(req.RefreshToken)
5453
if oerr != nil {
55-
return nil, oerr
54+
return "", oerr
5655
}
5756

5857
refreshToken, err := tokens.LookupRefreshToken(ctx, g.storage, g.policy, g.logger, &client.ID, token)
5958
if err != nil {
60-
return nil, refreshLookupError(err)
59+
return "", refreshLookupError(err)
6160
}
6261

63-
// Resolve the connector and re-check authorization on every refresh: the
64-
// connector may have been removed from the client's allowed list, or had the
65-
// refresh grant revoked, after this token was issued.
66-
if !connectors.ConnectorAllowed(client.AllowedConnectors, refreshToken.ConnectorID) {
67-
g.logger.WarnContext(ctx, "connector not allowed for client", "client_id", client.ID, "connector_id", refreshToken.ConnectorID)
68-
return nil, &oauth2.Error{Type: oauth2.InvalidGrant, Description: "Connector not allowed for this client.", Status: http.StatusBadRequest}
69-
}
70-
upstream, err := g.connectors.Get(ctx, refreshToken.ConnectorID)
71-
if err != nil {
72-
g.logger.ErrorContext(ctx, "connector not found", "connector_id", refreshToken.ConnectorID, "err", err)
73-
return nil, &oauth2.Error{Type: oauth2.InvalidRequest, Status: http.StatusInternalServerError}
74-
}
75-
if !connectors.GrantTypeAllowed(upstream.GrantTypes, oauth2.GrantTypeRefreshToken) {
76-
g.logger.ErrorContext(ctx, "connector does not allow refresh token grant", "connector_id", refreshToken.ConnectorID)
77-
return nil, &oauth2.Error{Type: oauth2.InvalidRequest, Description: "Connector does not support refresh tokens.", Status: http.StatusBadRequest}
62+
req.resolvedRefresh = refreshToken
63+
return refreshToken.ConnectorID, nil
64+
}
65+
66+
// Authorize rotates the refresh token, re-reads the identity against the resolved
67+
// connector, and returns the token set to issue (with the rotated refresh token).
68+
func (g *refresh) Authorize(ctx context.Context, req *Request, client storage.Client, conn connectors.Connector) (*Result, error) {
69+
refreshToken := req.resolvedRefresh
70+
// Already validated in ConnectorID; re-parse only to hand Rotate the raw token.
71+
token, oerr := parseRefreshToken(req.RefreshToken)
72+
if oerr != nil {
73+
return nil, oerr
7874
}
7975

8076
scopes, oerr := g.refreshScopes(req, refreshToken)
@@ -109,7 +105,7 @@ func (g *refresh) Authorize(ctx context.Context, req *Request, client storage.Cl
109105
if err != nil {
110106
return connector.Identity{}, err
111107
}
112-
return g.refreshWithConnector(ctx, upstream, connectorData, scopes, tokens.IdentityFromClaims(refreshToken.Claims))
108+
return g.refreshWithConnector(ctx, conn, connectorData, scopes, tokens.IdentityFromClaims(refreshToken.Claims))
113109
}
114110

115111
rawNewToken, ident, err := g.issuer.Refresh.Rotate(ctx, refreshToken, token, g.policy, freshIdentity)
@@ -167,7 +163,7 @@ func (g *refresh) refreshScopes(req *Request, refreshToken *storage.RefreshToken
167163

168164
var unauthorized []string
169165
for _, scope := range req.Scopes {
170-
if !contains(refreshToken.Scopes, scope) {
166+
if !slices.Contains(refreshToken.Scopes, scope) {
171167
unauthorized = append(unauthorized, scope)
172168
}
173169
}
@@ -246,12 +242,3 @@ func refreshLookupError(err error) *oauth2.Error {
246242
return &oauth2.Error{Type: oauth2.InvalidRequest, Status: http.StatusInternalServerError}
247243
}
248244
}
249-
250-
func contains(arr []string, item string) bool {
251-
for _, v := range arr {
252-
if v == item {
253-
return true
254-
}
255-
}
256-
return false
257-
}

server/grants/tokenexchange.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ func (g *tokenExchange) ScopePolicy() ScopePolicy {
3838
}
3939

4040
// ConnectorID reads the required connector_id parameter (an RFC 8693 extension).
41-
func (g *tokenExchange) ConnectorID(req *Request) string {
42-
return req.ConnectorID
41+
func (g *tokenExchange) ConnectorID(ctx context.Context, req *Request, client storage.Client) (string, *oauth2.Error) {
42+
return req.ConnectorID, nil
4343
}
4444

4545
func (g *tokenExchange) Authorize(ctx context.Context, req *Request, client storage.Client, conn connectors.Connector) (*Result, error) {

0 commit comments

Comments
 (0)