diff --git a/backend/internal/provider/leonardo/client.go b/backend/internal/provider/leonardo/client.go index 8c9365c..bd60499 100644 --- a/backend/internal/provider/leonardo/client.go +++ b/backend/internal/provider/leonardo/client.go @@ -66,10 +66,14 @@ type Client struct { // persists it; keeping it here means an unpersisted rotation still works for // the rest of the process's life. rotated map[string]string + // refreshing serialises get-session per cookie. Two concurrent refreshes hand + // Cognito the same refresh token twice and its reuse detection revokes the + // whole session — the account then answers 401 forever. + refreshing map[string]*sync.Mutex } func NewClient(proxy string) *Client { - return &Client{proxy: strings.TrimSpace(proxy), sessions: map[string]*Session{}, rotated: map[string]string{}} + return &Client{proxy: strings.TrimSpace(proxy), sessions: map[string]*Session{}, rotated: map[string]string{}, refreshing: map[string]*sync.Mutex{}} } func (c *Client) SetProxy(proxy string) { @@ -193,6 +197,18 @@ func (c *Client) GetSession(ctx context.Context, cookie string) (*Session, error } c.mu.Unlock() + // Only one refresh per cookie at a time; the others wait and then re-use the + // token it minted. + gate := c.refreshGate(cookie) + gate.Lock() + defer gate.Unlock() + c.mu.Lock() + if cs, ok := c.sessions[cookie]; ok && cs.ExpiresAt-60 > time.Now().Unix() { + c.mu.Unlock() + return cs, nil + } + c.mu.Unlock() + // Use the freshest known value (an earlier response may have rotated the // better-auth cookie cache) rather than the possibly stale stored cookie. send := cookie @@ -327,6 +343,18 @@ func (c *Client) GetSession(ctx context.Context, cookie string) (*Session, error return sess, nil } +// refreshGate returns the per-cookie lock that serialises get-session refreshes. +func (c *Client) refreshGate(cookie string) *sync.Mutex { + c.mu.Lock() + defer c.mu.Unlock() + gate, ok := c.refreshing[cookie] + if !ok { + gate = &sync.Mutex{} + c.refreshing[cookie] = gate + } + return gate +} + // warmSession calls cross-origin-cookie, whose response refreshes better-auth's // cookie cache and CF_Access_Token. It returns the Set-Cookie headers; a // checkpoint answer (403/429) is an error, since get-session would then be cold.