veza/veza-backend-api/internal/services/refresh_lock.go

54 lines
1.6 KiB
Go

package services
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
)
const refreshLockTTL = 30 * time.Second
// RefreshLock provides Redis-based locking for refresh token operations
// to prevent race conditions when two concurrent refresh requests use the same token.
type RefreshLock struct {
client *redis.Client
}
// NewRefreshLock creates a new RefreshLock. If client is nil, AcquireRefreshLock
// will always return (true, noop) to allow operation without Redis (graceful degradation).
func NewRefreshLock(client *redis.Client) *RefreshLock {
return &RefreshLock{client: client}
}
// AcquireRefreshLock acquires a Redis lock for the given userID+token combination.
// Returns (true, releaseFn) if lock acquired; (false, nil) if already held by another request.
// releaseFn must be called to release the lock (typically via defer).
// Key format: refresh_lock:{userID}:{tokenHash} to avoid storing full token in Redis.
func (r *RefreshLock) AcquireRefreshLock(ctx context.Context, userID uuid.UUID, token string) (bool, func()) {
if r.client == nil {
return true, func() {}
}
key := fmt.Sprintf("refresh_lock:%s:%s", userID.String(), hashToken(token))
ok, err := r.client.SetNX(ctx, key, "1", refreshLockTTL).Result()
if err != nil {
return false, nil
}
if !ok {
return false, nil
}
release := func() {
_ = r.client.Del(context.Background(), key).Err()
}
return true, release
}
func hashToken(s string) string {
h := sha256.Sum256([]byte(s))
return hex.EncodeToString(h[:8])
}