Skip to content

Commit ed3e1c3

Browse files
committed
refactor(server): extract the token issuer into server/tokens
Move the whole token-issuance domain out of the god object into server/tokens: - Authorization / TokenSet value objects. - Signer (access/id token minting) with the OIDC claim vocabulary it owns: the id-token claims, Audience, at_hash, cross-client trust, GenSubject, GetAudience, ParseCrossClientScope and the claim scope constants. - Issuer composing the signer and the refresh store. - RefreshStrategy (rotation policy) and RefreshStore (create/rotate/revoke), with the config-reading adapter (NewRefreshTokenPolicy) kept in the server package so config parsing stays separate from the strategy object. The server package now builds tokens.Authorization values and calls the issuer; introspection, logout and the grants reference the tokens package for the shared OIDC helpers. writeTokenResponse stays as a transport helper. Request-flow scope constants (openid, offline_access) stay in the server package. No behaviour change; public server.NewRefreshTokenPolicy / Config.RefreshTokenPolicy keep working. Signed-off-by: maksim.nabokikh <max.nabokih@gmail.com>
1 parent 57fefd3 commit ed3e1c3

32 files changed

Lines changed: 1025 additions & 948 deletions

server/api.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/dexidp/dex/api/v2"
1717
"github.com/dexidp/dex/pkg/featureflags"
1818
"github.com/dexidp/dex/server/internal"
19+
"github.com/dexidp/dex/server/tokens"
1920
"github.com/dexidp/dex/storage"
2021
)
2122

@@ -42,7 +43,7 @@ func NewAPI(s storage.Storage, logger *slog.Logger, version string, server *Serv
4243
logger: apiLogger,
4344
version: version,
4445
server: server,
45-
refresh: &refreshTokens{storage: s, now: time.Now, logger: apiLogger},
46+
refresh: tokens.NewRefreshStore(s, time.Now, apiLogger),
4647
}
4748
}
4849

@@ -53,7 +54,7 @@ type dexAPI struct {
5354
logger *slog.Logger
5455
version string
5556
server *Server
56-
refresh *refreshTokens
57+
refresh *tokens.RefreshStore
5758
}
5859

5960
func (d dexAPI) GetClient(ctx context.Context, req *api.GetClientReq) (*api.GetClientResp, error) {
@@ -628,7 +629,7 @@ func defaultTo[T comparable](v, def T) T {
628629
// and cleans up offline session references. Errors are logged but not returned
629630
// (best-effort).
630631
func (d dexAPI) revokeUserRefreshTokens(ctx context.Context, userID, connectorID string) {
631-
d.refresh.revoke(ctx, userID, connectorID)
632+
d.refresh.Revoke(ctx, userID, connectorID)
632633
}
633634

634635
// unixOrZero returns the Unix timestamp for t, or 0 when t is the zero value.

server/approval.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"time"
1111

1212
"github.com/dexidp/dex/pkg/featureflags"
13+
"github.com/dexidp/dex/server/tokens"
1314
"github.com/dexidp/dex/storage"
1415
)
1516

@@ -182,7 +183,7 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
182183
implicitOrHybrid = true
183184
var err error
184185

185-
accessToken, _, err = s.issuer.signer.signAccessToken(r.Context(), Authorization{
186+
accessToken, _, err = s.issuer.SignAccessToken(r.Context(), tokens.Authorization{
186187
Client: storage.Client{ID: authReq.ClientID},
187188
Claims: authReq.Claims,
188189
Scopes: authReq.Scopes,
@@ -199,7 +200,7 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
199200
implicitOrHybrid = true
200201
var err error
201202

202-
idToken, idTokenExpiry, err = s.issuer.signer.signIDToken(r.Context(), Authorization{
203+
idToken, idTokenExpiry, err = s.issuer.SignIDToken(r.Context(), tokens.Authorization{
203204
Client: storage.Client{ID: authReq.ClientID},
204205
Claims: authReq.Claims,
205206
Scopes: authReq.Scopes,

server/grant_authcode.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/http"
88

99
"github.com/dexidp/dex/connector"
10+
"github.com/dexidp/dex/server/tokens"
1011
"github.com/dexidp/dex/storage"
1112
)
1213

@@ -71,8 +72,8 @@ func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client s
7172
s.writeAccessToken(w, tokenResponse)
7273
}
7374

74-
func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, authCode storage.AuthCode, client storage.Client) (*accessTokenResponse, error) {
75-
auth := Authorization{
75+
func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, authCode storage.AuthCode, client storage.Client) (tokens.Response, error) {
76+
auth := tokens.Authorization{
7677
Client: client,
7778
Claims: authCode.Claims,
7879
Scopes: authCode.Scopes,
@@ -82,24 +83,24 @@ func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, au
8283
ConnectorData: authCode.ConnectorData,
8384
}
8485

85-
accessToken, _, err := s.issuer.signer.signAccessToken(ctx, auth)
86+
accessToken, _, err := s.issuer.SignAccessToken(ctx, auth)
8687
if err != nil {
8788
s.logger.ErrorContext(ctx, "failed to create new access token", "err", err)
8889
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
89-
return nil, err
90+
return tokens.Response{}, err
9091
}
9192

92-
idToken, expiry, err := s.issuer.signer.signIDToken(ctx, auth, accessToken, authCode.ID)
93+
idToken, expiry, err := s.issuer.SignIDToken(ctx, auth, accessToken, authCode.ID)
9394
if err != nil {
9495
s.logger.ErrorContext(ctx, "failed to create ID token", "err", err)
9596
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
96-
return nil, err
97+
return tokens.Response{}, err
9798
}
9899

99100
if err := s.storage.DeleteAuthCode(ctx, authCode.ID); err != nil {
100101
s.logger.ErrorContext(ctx, "failed to delete auth code", "err", err)
101102
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
102-
return nil, err
103+
return tokens.Response{}, err
103104
}
104105

105106
reqRefresh := func() bool {
@@ -139,11 +140,11 @@ func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, au
139140

140141
var refreshToken string
141142
if reqRefresh {
142-
refreshToken, err = s.issuer.refresh.create(ctx, auth)
143+
refreshToken, err = s.issuer.Refresh.Create(ctx, auth)
143144
if err != nil {
144145
s.logger.ErrorContext(ctx, "failed to create refresh token", "err", err)
145146
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
146-
return nil, err
147+
return tokens.Response{}, err
147148
}
148149
}
149150

server/grant_clientcredentials.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/http"
88
"strings"
99

10+
"github.com/dexidp/dex/server/tokens"
1011
"github.com/dexidp/dex/storage"
1112
)
1213

@@ -36,16 +37,16 @@ func (s *Server) handleClientCredentialsGrant(w http.ResponseWriter, r *http.Req
3637
switch scope {
3738
case scopeOpenID:
3839
hasOpenIDScope = true
39-
case scopeEmail, scopeProfile, scopeGroups:
40+
case tokens.ScopeEmail, tokens.ScopeProfile, tokens.ScopeGroups:
4041
// allowed
4142
case scopeOfflineAccess:
4243
s.tokenErrHelper(w, errInvalidScope, "client_credentials grant does not support offline_access scope.", http.StatusBadRequest)
4344
return
44-
case scopeFederatedID:
45+
case tokens.ScopeFederatedID:
4546
s.tokenErrHelper(w, errInvalidScope, "client_credentials grant does not support federated:id scope.", http.StatusBadRequest)
4647
return
4748
default:
48-
peerID, ok := parseCrossClientScope(scope)
49+
peerID, ok := tokens.ParseCrossClientScope(scope)
4950
if !ok {
5051
unrecognized = append(unrecognized, scope)
5152
continue
@@ -79,10 +80,10 @@ func (s *Server) handleClientCredentialsGrant(w http.ResponseWriter, r *http.Req
7980
// Populate optional claims based on requested scopes.
8081
for _, scope := range scopes {
8182
switch scope {
82-
case scopeProfile:
83+
case tokens.ScopeProfile:
8384
claims.Username = client.Name
8485
claims.PreferredUsername = client.Name
85-
case scopeGroups:
86+
case tokens.ScopeGroups:
8687
if client.ClientCredentialsClaims != nil {
8788
claims.Groups = client.ClientCredentialsClaims.Groups
8889
}
@@ -95,15 +96,15 @@ func (s *Server) handleClientCredentialsGrant(w http.ResponseWriter, r *http.Req
9596
// Creating connectors with an empty ID with the config and API is prohibited
9697
connID := ""
9798

98-
auth := Authorization{
99+
auth := tokens.Authorization{
99100
Client: client,
100101
Claims: claims,
101102
Scopes: scopes,
102103
ConnectorID: connID,
103104
Nonce: nonce,
104105
}
105106

106-
accessToken, expiry, err := s.issuer.signer.signAccessToken(ctx, auth)
107+
accessToken, expiry, err := s.issuer.SignAccessToken(ctx, auth)
107108
if err != nil {
108109
s.logger.ErrorContext(ctx, "client_credentials grant failed to create new access token", "err", err)
109110
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
@@ -112,15 +113,15 @@ func (s *Server) handleClientCredentialsGrant(w http.ResponseWriter, r *http.Req
112113

113114
var idToken string
114115
if hasOpenIDScope {
115-
idToken, expiry, err = s.issuer.signer.signIDToken(ctx, auth, accessToken, "")
116+
idToken, expiry, err = s.issuer.SignIDToken(ctx, auth, accessToken, "")
116117
if err != nil {
117118
s.logger.ErrorContext(ctx, "client_credentials grant failed to create new ID token", "err", err)
118119
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
119120
return
120121
}
121122
}
122123

123-
if err := writeTokenResponse(w, TokenSet{AccessToken: accessToken, IDToken: idToken, Expiry: expiry}, s.now()); err != nil {
124+
if err := writeTokenResponse(w, tokens.TokenSet{AccessToken: accessToken, IDToken: idToken, Expiry: expiry}, s.now()); err != nil {
124125
s.logger.ErrorContext(ctx, "failed to write token response", "err", err)
125126
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
126127
return

server/grant_password.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010

1111
"github.com/dexidp/dex/connector"
12+
"github.com/dexidp/dex/server/tokens"
1213
"github.com/dexidp/dex/storage"
1314
)
1415

@@ -35,9 +36,9 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli
3536
switch scope {
3637
case scopeOpenID:
3738
hasOpenIDScope = true
38-
case scopeOfflineAccess, scopeEmail, scopeProfile, scopeGroups, scopeFederatedID:
39+
case scopeOfflineAccess, tokens.ScopeEmail, tokens.ScopeProfile, tokens.ScopeGroups, tokens.ScopeFederatedID:
3940
default:
40-
peerID, ok := parseCrossClientScope(scope)
41+
peerID, ok := tokens.ParseCrossClientScope(scope)
4142
if !ok {
4243
unrecognized = append(unrecognized, scope)
4344
continue
@@ -120,7 +121,7 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli
120121
wantRefresh = slices.Contains(scopes, scopeOfflineAccess)
121122
}
122123

123-
tokens, err := s.issuer.Issue(ctx, Authorization{
124+
tokens, err := s.issuer.Issue(ctx, tokens.Authorization{
124125
Client: client,
125126
Claims: claims,
126127
Scopes: scopes,

server/grant_refresh.go

Lines changed: 19 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -13,77 +13,44 @@ import (
1313
"github.com/dexidp/dex/connector"
1414
"github.com/dexidp/dex/server/connectors"
1515
"github.com/dexidp/dex/server/internal"
16+
"github.com/dexidp/dex/server/tokens"
1617
"github.com/dexidp/dex/storage"
1718
)
1819

19-
type RefreshTokenPolicy struct {
20-
rotateRefreshTokens bool // enable rotation
21-
22-
absoluteLifetime time.Duration // interval from token creation to the end of its life
23-
validIfNotUsedFor time.Duration // interval from last token update to the end of its life
24-
reuseInterval time.Duration // interval within which old refresh token is allowed to be reused
25-
26-
now func() time.Time
27-
28-
logger *slog.Logger
29-
}
30-
31-
func NewRefreshTokenPolicy(logger *slog.Logger, rotation bool, validIfNotUsedFor, absoluteLifetime, reuseInterval string) (*RefreshTokenPolicy, error) {
32-
r := RefreshTokenPolicy{now: time.Now, logger: logger}
20+
// NewRefreshTokenPolicy parses the refresh-token configuration into a rotation
21+
// strategy. It is the config-reading adapter; the strategy object itself lives
22+
// in the refresh package, independent of how its intervals are configured.
23+
func NewRefreshTokenPolicy(logger *slog.Logger, rotation bool, validIfNotUsedFor, absoluteLifetime, reuseInterval string) (*tokens.RefreshStrategy, error) {
24+
var validDur, absoluteDur, reuseDur time.Duration
3325
var err error
3426

3527
if validIfNotUsedFor != "" {
36-
r.validIfNotUsedFor, err = time.ParseDuration(validIfNotUsedFor)
28+
validDur, err = time.ParseDuration(validIfNotUsedFor)
3729
if err != nil {
3830
return nil, fmt.Errorf("invalid config value %q for refresh token valid if not used for: %v", validIfNotUsedFor, err)
3931
}
4032
logger.Info("config refresh tokens", "valid_if_not_used_for", validIfNotUsedFor)
4133
}
4234

4335
if absoluteLifetime != "" {
44-
r.absoluteLifetime, err = time.ParseDuration(absoluteLifetime)
36+
absoluteDur, err = time.ParseDuration(absoluteLifetime)
4537
if err != nil {
4638
return nil, fmt.Errorf("invalid config value %q for refresh tokens absolute lifetime: %v", absoluteLifetime, err)
4739
}
4840
logger.Info("config refresh tokens", "absolute_lifetime", absoluteLifetime)
4941
}
5042

5143
if reuseInterval != "" {
52-
r.reuseInterval, err = time.ParseDuration(reuseInterval)
44+
reuseDur, err = time.ParseDuration(reuseInterval)
5345
if err != nil {
5446
return nil, fmt.Errorf("invalid config value %q for refresh tokens reuse interval: %v", reuseInterval, err)
5547
}
5648
logger.Info("config refresh tokens", "reuse_interval", reuseInterval)
5749
}
5850

59-
r.rotateRefreshTokens = !rotation
60-
logger.Info("config refresh tokens rotation", "enabled", r.rotateRefreshTokens)
61-
return &r, nil
62-
}
63-
64-
func (r *RefreshTokenPolicy) RotationEnabled() bool {
65-
return r.rotateRefreshTokens
66-
}
67-
68-
func (r *RefreshTokenPolicy) CompletelyExpired(lastUsed time.Time) bool {
69-
if r.absoluteLifetime == 0 {
70-
return false // expiration disabled
71-
}
72-
return r.now().After(lastUsed.Add(r.absoluteLifetime))
73-
}
74-
75-
func (r *RefreshTokenPolicy) ExpiredBecauseUnused(lastUsed time.Time) bool {
76-
if r.validIfNotUsedFor == 0 {
77-
return false // expiration disabled
78-
}
79-
return r.now().After(lastUsed.Add(r.validIfNotUsedFor))
80-
}
81-
82-
func (r *RefreshTokenPolicy) AllowedToReuse(lastUsed time.Time) bool {
83-
if r.reuseInterval == 0 {
84-
return false // expiration disabled
85-
}
86-
return !r.now().After(lastUsed.Add(r.reuseInterval))
51+
rotate := !rotation
52+
logger.Info("config refresh tokens rotation", "enabled", rotate)
53+
return tokens.NewRefreshStrategy(rotate, absoluteDur, validDur, reuseDur, time.Now), nil
8754
}
8855

8956
func contains(arr []string, item string) bool {
@@ -339,27 +306,27 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie
339306
// is only needed on the connector-refresh path.
340307
freshIdentity := func(ctx context.Context) (connector.Identity, error) {
341308
if userIdent != nil {
342-
return identityFromClaims(userIdent.Claims), nil
309+
return tokens.IdentityFromClaims(userIdent.Claims), nil
343310
}
344311
connectorData, rerr := s.refreshConnectorData(ctx, refresh)
345312
if rerr != nil {
346313
return connector.Identity{}, rerr
347314
}
348-
ident, rerr := s.refreshWithConnector(ctx, conn, connectorData, scopes, identityFromClaims(refresh.Claims))
315+
ident, rerr := s.refreshWithConnector(ctx, conn, connectorData, scopes, tokens.IdentityFromClaims(refresh.Claims))
349316
if rerr != nil {
350317
return ident, rerr
351318
}
352319
return ident, nil
353320
}
354321

355-
rawNewToken, ident, err := s.issuer.refresh.rotate(r.Context(), refresh, token, s.refreshTokenPolicy, freshIdentity)
322+
rawNewToken, ident, err := s.issuer.Refresh.Rotate(r.Context(), refresh, token, s.refreshTokenPolicy, freshIdentity)
356323
if err != nil {
357324
s.logger.ErrorContext(r.Context(), "failed to rotate refresh token", "err", err)
358325
s.refreshTokenErrHelper(w, newInternalServerError())
359326
return
360327
}
361328

362-
auth := Authorization{
329+
auth := tokens.Authorization{
363330
Client: client,
364331
Claims: storage.Claims{
365332
UserID: ident.UserID,
@@ -375,21 +342,21 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie
375342
AuthTime: authTime,
376343
}
377344

378-
accessToken, _, err := s.issuer.signer.signAccessToken(r.Context(), auth)
345+
accessToken, _, err := s.issuer.SignAccessToken(r.Context(), auth)
379346
if err != nil {
380347
s.logger.ErrorContext(r.Context(), "failed to create new access token", "err", err)
381348
s.refreshTokenErrHelper(w, newInternalServerError())
382349
return
383350
}
384351

385-
idToken, expiry, err := s.issuer.signer.signIDToken(r.Context(), auth, accessToken, "")
352+
idToken, expiry, err := s.issuer.SignIDToken(r.Context(), auth, accessToken, "")
386353
if err != nil {
387354
s.logger.ErrorContext(r.Context(), "failed to create ID token", "err", err)
388355
s.refreshTokenErrHelper(w, newInternalServerError())
389356
return
390357
}
391358

392-
tokens := TokenSet{AccessToken: accessToken, IDToken: idToken, RefreshToken: rawNewToken, Expiry: expiry}
359+
tokens := tokens.TokenSet{AccessToken: accessToken, IDToken: idToken, RefreshToken: rawNewToken, Expiry: expiry}
393360
if err := writeTokenResponse(w, tokens, s.now()); err != nil {
394361
s.logger.ErrorContext(r.Context(), "failed to write token response", "err", err)
395362
s.refreshTokenErrHelper(w, newInternalServerError())

0 commit comments

Comments
 (0)