Skip to content

Commit 9320c9c

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 9320c9c

File tree

2 files changed

+36
-10
lines changed

2 files changed

+36
-10
lines changed

server/refreshhandlers.go

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -232,15 +232,16 @@ 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) {
237239
var rerr *refreshError
238240

239241
newToken := &internal.RefreshToken{
240242
Token: rCtx.requestToken.Token,
241243
RefreshId: rCtx.requestToken.RefreshId,
242244
}
243-
244245
lastUsed := s.now()
245246

246247
ident := connector.Identity{
@@ -250,6 +251,31 @@ func (s *Server) updateRefreshToken(ctx context.Context, rCtx *refreshContext) (
250251
Email: rCtx.storageToken.Claims.Email,
251252
EmailVerified: rCtx.storageToken.Claims.EmailVerified,
252253
Groups: rCtx.storageToken.Claims.Groups,
254+
ConnectorData: rCtx.connectorData,
255+
}
256+
257+
rotationEnabled := s.refreshTokenPolicy.RotationEnabled()
258+
reusingAllowed := s.refreshTokenPolicy.AllowedToReuse(rCtx.storageToken.LastUsed)
259+
needConnectorRefresh := rotationEnabled && !reusingAllowed
260+
261+
if needConnectorRefresh {
262+
// Serialize concurrent refreshes for the same refresh ID.
263+
lock := s.getRefreshLock(rCtx.storageToken.ID)
264+
lock.Lock()
265+
s.logger.Debug("Acquired refresh lock", "refreshID", rCtx.storageToken.ID)
266+
defer func() {
267+
lock.Unlock()
268+
s.logger.Debug("Released refresh lock", "refreshID", rCtx.storageToken.ID)
269+
}()
270+
271+
// Double-check if another goroutine already refreshed while we waited:
272+
if !s.refreshTokenPolicy.AllowedToReuse(rCtx.storageToken.LastUsed) {
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) {
@@ -293,14 +319,6 @@ func (s *Server) updateRefreshToken(ctx context.Context, rCtx *refreshContext) (
293319
// ConnectorData has been moved to OfflineSession
294320
old.ConnectorData = nil
295321

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-
304322
// Update the claims of the refresh token.
305323
//
306324
// UserID intentionally ignored for now.

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)