Skip to content

Commit fc00334

Browse files
committed
fix(refresh): avoid deadlock by moving connector refresh outside txn and adding per-token mutex
Previously, `updateRefreshToken` executed `refreshWithConnector` inside the `UpdateRefreshToken` transaction. With SQL backends that enforce strict connection limits (e.g. SQLite), this blocked the only available connection while the connector call could indirectly trigger further storage access (e.g. when using PasswordDB), causing the system to hang. This patch moves connector refresh calls outside of the storage transaction and introduces a per-refresh-ID mutex to ensure only one concurrent request per token hits the external IdP. Other concurrent requests wait for the mutex and reuse the updated identity. Signed-off-by: Tommaso Sardelli <[email protected]>
1 parent 9a9a900 commit fc00334

File tree

2 files changed

+41
-29
lines changed

2 files changed

+41
-29
lines changed

server/refreshhandlers.go

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -232,24 +232,50 @@ func (s *Server) updateOfflineSession(ctx context.Context, refresh *storage.Refr
232232
return nil
233233
}
234234

235-
// updateRefreshToken updates refresh token and offline session in the storage
235+
// updateRefreshToken updates refresh token and offline session in the storage.
236+
// Connector refresh is guarded by a per-refresh-ID mutex so only one concurrent
237+
// caller hits the IdP.
236238
func (s *Server) updateRefreshToken(ctx context.Context, rCtx *refreshContext) (*internal.RefreshToken, connector.Identity, *refreshError) {
237-
var rerr *refreshError
238-
239239
newToken := &internal.RefreshToken{
240240
Token: rCtx.requestToken.Token,
241241
RefreshId: rCtx.requestToken.RefreshId,
242242
}
243-
244243
lastUsed := s.now()
245244

245+
// Base identity
246246
ident := connector.Identity{
247247
UserID: rCtx.storageToken.Claims.UserID,
248248
Username: rCtx.storageToken.Claims.Username,
249249
PreferredUsername: rCtx.storageToken.Claims.PreferredUsername,
250250
Email: rCtx.storageToken.Claims.Email,
251251
EmailVerified: rCtx.storageToken.Claims.EmailVerified,
252252
Groups: rCtx.storageToken.Claims.Groups,
253+
ConnectorData: rCtx.connectorData,
254+
}
255+
256+
rotationEnabled := s.refreshTokenPolicy.RotationEnabled()
257+
reusingAllowed := s.refreshTokenPolicy.AllowedToReuse(rCtx.storageToken.LastUsed)
258+
needConnectorRefresh := rotationEnabled && !reusingAllowed
259+
260+
if needConnectorRefresh {
261+
// Serialize concurrent refreshes for the same refresh ID.
262+
lock := s.getRefreshLock(rCtx.storageToken.ID)
263+
lock.Lock()
264+
s.logger.Debug("Acquired refresh lock", "refreshID", rCtx.storageToken.ID)
265+
defer func() {
266+
lock.Unlock()
267+
s.logger.Debug("Released refresh lock", "refreshID", rCtx.storageToken.ID)
268+
}()
269+
270+
// Double-check if another goroutine already refreshed while we waited:
271+
if !s.refreshTokenPolicy.AllowedToReuse(rCtx.storageToken.LastUsed) {
272+
s.logger.Debug("Calling refreshWithConnector", "ident", ident)
273+
var rerr *refreshError
274+
ident, rerr = s.refreshWithConnector(ctx, rCtx, ident)
275+
if rerr != nil {
276+
return nil, ident, rerr
277+
}
278+
}
253279
}
254280

255281
refreshTokenUpdater := func(old storage.RefreshToken) (storage.RefreshToken, error) {
@@ -258,21 +284,16 @@ func (s *Server) updateRefreshToken(ctx context.Context, rCtx *refreshContext) (
258284

259285
switch {
260286
case !rotationEnabled && reusingAllowed:
261-
// If rotation is disabled and the offline session was updated not so long ago - skip further actions.
262287
old.ConnectorData = nil
263288
return old, nil
264289

265290
case rotationEnabled && reusingAllowed:
266291
if old.Token != rCtx.requestToken.Token && old.ObsoleteToken != rCtx.requestToken.Token {
267292
return old, errors.New("refresh token claimed twice")
268293
}
269-
270-
// Return previously generated token for all requests with an obsolete tokens
271294
if old.ObsoleteToken == rCtx.requestToken.Token {
272295
newToken.Token = old.Token
273296
}
274-
275-
// Do not update last used time for offline session if token is allowed to be reused
276297
lastUsed = old.LastUsed
277298
old.ConnectorData = nil
278299
return old, nil
@@ -281,29 +302,15 @@ func (s *Server) updateRefreshToken(ctx context.Context, rCtx *refreshContext) (
281302
if old.Token != rCtx.requestToken.Token {
282303
return old, errors.New("refresh token claimed twice")
283304
}
284-
285-
// Issue new refresh token
286305
old.ObsoleteToken = old.Token
287306
newToken.Token = storage.NewID()
288307
}
289308

290309
old.Token = newToken.Token
291310
old.LastUsed = lastUsed
292-
293-
// ConnectorData has been moved to OfflineSession
294311
old.ConnectorData = nil
295312

296-
// Call only once if there is a request which is not in the reuse interval.
297-
// This is required to avoid multiple calls to the external IdP for concurrent requests.
298-
// Dex will call the connector's Refresh method only once if request is not in reuse interval.
299-
ident, rerr = s.refreshWithConnector(ctx, rCtx, ident)
300-
if rerr != nil {
301-
return old, rerr
302-
}
303-
304-
// Update the claims of the refresh token.
305-
//
306-
// UserID intentionally ignored for now.
313+
// Always update claims
307314
old.Claims.Username = ident.Username
308315
old.Claims.PreferredUsername = ident.PreferredUsername
309316
old.Claims.Email = ident.Email
@@ -313,15 +320,12 @@ func (s *Server) updateRefreshToken(ctx context.Context, rCtx *refreshContext) (
313320
return old, nil
314321
}
315322

316-
// Update refresh token in the storage.
317-
err := s.storage.UpdateRefreshToken(ctx, rCtx.storageToken.ID, refreshTokenUpdater)
318-
if err != nil {
323+
if err := s.storage.UpdateRefreshToken(ctx, rCtx.storageToken.ID, refreshTokenUpdater); err != nil {
319324
s.logger.ErrorContext(ctx, "failed to update refresh token", "err", err)
320325
return nil, ident, newInternalServerError()
321326
}
322327

323-
rerr = s.updateOfflineSession(ctx, rCtx.storageToken, ident, lastUsed)
324-
if rerr != nil {
328+
if rerr := s.updateOfflineSession(ctx, rCtx.storageToken, ident, lastUsed); rerr != nil {
325329
return nil, ident, rerr
326330
}
327331

server/server.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ type Server struct {
198198
deviceRequestsValidFor time.Duration
199199

200200
refreshTokenPolicy *RefreshTokenPolicy
201+
// mutex to refresh the same token only once for concurrent requests
202+
refreshLocks sync.Map
201203

202204
logger *slog.Logger
203205
}
@@ -758,6 +760,12 @@ func (s *Server) getConnector(ctx context.Context, id string) (Connector, error)
758760
return conn, nil
759761
}
760762

763+
// getRefreshLock returns a per-refresh-ID mutex.
764+
func (s *Server) getRefreshLock(refreshID string) *sync.Mutex {
765+
m, _ := s.refreshLocks.LoadOrStore(refreshID, &sync.Mutex{})
766+
return m.(*sync.Mutex)
767+
}
768+
761769
type logRequestKey string
762770

763771
const (

0 commit comments

Comments
 (0)