package auth import ( "context" "errors" "fmt" "time" "github.com/redis/go-redis/v9" ) var ( ErrRefreshSessionNotFound = errors.New("refresh session not found") ErrRefreshTokenMismatch = errors.New("refresh token mismatch") ) var rotateRefreshScript = redis.NewScript(` local current = redis.call("GET", KEYS[1]) if not current then return 0 end if current ~= ARGV[1] then return -1 end redis.call("DEL", KEYS[1]) redis.call("PSETEX", KEYS[2], ARGV[3], ARGV[2]) return 1 `) type SessionStore struct { rdb *redis.Client } func NewSessionStore(rdb *redis.Client) *SessionStore { return &SessionStore{rdb: rdb} } func (s *SessionStore) SaveRefresh(ctx context.Context, jti string, tokenHash string, ttl time.Duration) error { key := fmt.Sprintf("refresh:%s", jti) return s.rdb.Set(ctx, key, tokenHash, ttl).Err() } func (s *SessionStore) GetRefresh(ctx context.Context, jti string) (string, error) { key := fmt.Sprintf("refresh:%s", jti) return s.rdb.Get(ctx, key).Result() } func (s *SessionStore) DeleteRefresh(ctx context.Context, jti string) error { key := fmt.Sprintf("refresh:%s", jti) return s.rdb.Del(ctx, key).Err() } func (s *SessionStore) RotateRefresh(ctx context.Context, oldJTI, oldHash, newJTI, newHash string, ttl time.Duration) error { oldKey := fmt.Sprintf("refresh:%s", oldJTI) newKey := fmt.Sprintf("refresh:%s", newJTI) result, err := rotateRefreshScript.Run( ctx, s.rdb, []string{oldKey, newKey}, oldHash, newHash, ttl.Milliseconds(), ).Int() if err != nil { return err } switch result { case 1: return nil case 0: return ErrRefreshSessionNotFound case -1: return ErrRefreshTokenMismatch default: return fmt.Errorf("unexpected rotate refresh result: %d", result) } } func (s *SessionStore) Blacklist(ctx context.Context, accessJTI string, ttl time.Duration) error { key := fmt.Sprintf("blacklist:%s", accessJTI) return s.rdb.Set(ctx, key, "1", ttl).Err() } func (s *SessionStore) IsBlacklisted(ctx context.Context, accessJTI string) (bool, error) { key := fmt.Sprintf("blacklist:%s", accessJTI) n, err := s.rdb.Exists(ctx, key).Result() if err != nil { return false, err } return n > 0, nil }