store.go 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. // Package redissession provides a multi-instance OAuth session backend.
  2. package redissession
  3. import (
  4. "context"
  5. "encoding/json"
  6. "errors"
  7. "strings"
  8. "time"
  9. "github.com/redis/go-redis/v9"
  10. )
  11. var ErrNotConfigured = errors.New("redis session store not configured")
  12. // Store persists JSON sessions and single-use markers under one namespace.
  13. type Store struct {
  14. rdb *redis.Client
  15. prefix string
  16. ttl time.Duration
  17. }
  18. func New(rdb *redis.Client, prefix string, ttl time.Duration) *Store {
  19. if ttl <= 0 {
  20. ttl = 30 * time.Minute
  21. }
  22. prefix = strings.TrimSpace(prefix)
  23. if prefix == "" {
  24. prefix = "oauth:session"
  25. }
  26. if !strings.HasSuffix(prefix, ":") {
  27. prefix += ":"
  28. }
  29. return &Store{rdb: rdb, prefix: prefix, ttl: ttl}
  30. }
  31. func (s *Store) dataKey(id string) string { return s.prefix + strings.TrimSpace(id) }
  32. func (s *Store) usedKey(id string) string { return s.prefix + "used:" + strings.TrimSpace(id) }
  33. func (s *Store) Set(ctx context.Context, id string, value any) error {
  34. if s == nil || s.rdb == nil {
  35. return ErrNotConfigured
  36. }
  37. id = strings.TrimSpace(id)
  38. if id == "" {
  39. return errors.New("session id is required")
  40. }
  41. if ctx == nil {
  42. ctx = context.Background()
  43. }
  44. raw, err := json.Marshal(value)
  45. if err != nil {
  46. return err
  47. }
  48. return s.rdb.Set(ctx, s.dataKey(id), raw, s.ttl).Err()
  49. }
  50. func (s *Store) Get(ctx context.Context, id string, dest any) (bool, error) {
  51. if s == nil || s.rdb == nil {
  52. return false, ErrNotConfigured
  53. }
  54. id = strings.TrimSpace(id)
  55. if id == "" {
  56. return false, nil
  57. }
  58. if ctx == nil {
  59. ctx = context.Background()
  60. }
  61. raw, err := s.rdb.Get(ctx, s.dataKey(id)).Bytes()
  62. if errors.Is(err, redis.Nil) {
  63. return false, nil
  64. }
  65. if err != nil {
  66. return false, err
  67. }
  68. if err := json.Unmarshal(raw, dest); err != nil {
  69. return false, err
  70. }
  71. return true, nil
  72. }
  73. func (s *Store) Delete(ctx context.Context, id string) error {
  74. if s == nil || s.rdb == nil {
  75. return ErrNotConfigured
  76. }
  77. id = strings.TrimSpace(id)
  78. if id == "" {
  79. return nil
  80. }
  81. if ctx == nil {
  82. ctx = context.Background()
  83. }
  84. return s.rdb.Del(ctx, s.dataKey(id), s.usedKey(id)).Err()
  85. }
  86. // TryConsume returns true only for the first claim while the session exists.
  87. func (s *Store) TryConsume(ctx context.Context, id string) (bool, error) {
  88. if s == nil || s.rdb == nil {
  89. return false, ErrNotConfigured
  90. }
  91. id = strings.TrimSpace(id)
  92. if id == "" {
  93. return false, nil
  94. }
  95. if ctx == nil {
  96. ctx = context.Background()
  97. }
  98. ttl := s.ttl
  99. if remaining, err := s.rdb.TTL(ctx, s.dataKey(id)).Result(); err == nil && remaining > 0 {
  100. ttl = remaining
  101. }
  102. ok, err := s.rdb.SetNX(ctx, s.usedKey(id), "1", ttl).Result()
  103. if err != nil || !ok {
  104. return ok, err
  105. }
  106. exists, err := s.rdb.Exists(ctx, s.dataKey(id)).Result()
  107. if err != nil {
  108. return false, err
  109. }
  110. if exists == 0 {
  111. _ = s.rdb.Del(ctx, s.usedKey(id)).Err()
  112. return false, nil
  113. }
  114. return true, nil
  115. }