totp_cache.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. package repository
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "time"
  7. "github.com/redis/go-redis/v9"
  8. "github.com/Wei-Shaw/sub2api/internal/service"
  9. )
  10. const (
  11. totpSetupKeyPrefix = "totp:setup:"
  12. totpLoginKeyPrefix = "totp:login:"
  13. totpAttemptsKeyPrefix = "totp:attempts:"
  14. totpStepUpKeyPrefix = "totp:stepup:"
  15. totpAttemptsTTL = 15 * time.Minute
  16. )
  17. // TotpCache implements service.TotpCache using Redis
  18. type TotpCache struct {
  19. rdb *redis.Client
  20. }
  21. // NewTotpCache creates a new TOTP cache
  22. func NewTotpCache(rdb *redis.Client) service.TotpCache {
  23. return &TotpCache{rdb: rdb}
  24. }
  25. // GetSetupSession retrieves a TOTP setup session
  26. func (c *TotpCache) GetSetupSession(ctx context.Context, userID int64) (*service.TotpSetupSession, error) {
  27. key := fmt.Sprintf("%s%d", totpSetupKeyPrefix, userID)
  28. data, err := c.rdb.Get(ctx, key).Bytes()
  29. if err != nil {
  30. if err == redis.Nil {
  31. return nil, nil
  32. }
  33. return nil, fmt.Errorf("get setup session: %w", err)
  34. }
  35. var session service.TotpSetupSession
  36. if err := json.Unmarshal(data, &session); err != nil {
  37. return nil, fmt.Errorf("unmarshal setup session: %w", err)
  38. }
  39. return &session, nil
  40. }
  41. // SetSetupSession stores a TOTP setup session
  42. func (c *TotpCache) SetSetupSession(ctx context.Context, userID int64, session *service.TotpSetupSession, ttl time.Duration) error {
  43. key := fmt.Sprintf("%s%d", totpSetupKeyPrefix, userID)
  44. data, err := json.Marshal(session)
  45. if err != nil {
  46. return fmt.Errorf("marshal setup session: %w", err)
  47. }
  48. if err := c.rdb.Set(ctx, key, data, ttl).Err(); err != nil {
  49. return fmt.Errorf("set setup session: %w", err)
  50. }
  51. return nil
  52. }
  53. // DeleteSetupSession deletes a TOTP setup session
  54. func (c *TotpCache) DeleteSetupSession(ctx context.Context, userID int64) error {
  55. key := fmt.Sprintf("%s%d", totpSetupKeyPrefix, userID)
  56. return c.rdb.Del(ctx, key).Err()
  57. }
  58. // GetLoginSession retrieves a TOTP login session
  59. func (c *TotpCache) GetLoginSession(ctx context.Context, tempToken string) (*service.TotpLoginSession, error) {
  60. key := totpLoginKeyPrefix + tempToken
  61. data, err := c.rdb.Get(ctx, key).Bytes()
  62. if err != nil {
  63. if err == redis.Nil {
  64. return nil, nil
  65. }
  66. return nil, fmt.Errorf("get login session: %w", err)
  67. }
  68. var session service.TotpLoginSession
  69. if err := json.Unmarshal(data, &session); err != nil {
  70. return nil, fmt.Errorf("unmarshal login session: %w", err)
  71. }
  72. return &session, nil
  73. }
  74. // SetLoginSession stores a TOTP login session
  75. func (c *TotpCache) SetLoginSession(ctx context.Context, tempToken string, session *service.TotpLoginSession, ttl time.Duration) error {
  76. key := totpLoginKeyPrefix + tempToken
  77. data, err := json.Marshal(session)
  78. if err != nil {
  79. return fmt.Errorf("marshal login session: %w", err)
  80. }
  81. if err := c.rdb.Set(ctx, key, data, ttl).Err(); err != nil {
  82. return fmt.Errorf("set login session: %w", err)
  83. }
  84. return nil
  85. }
  86. // DeleteLoginSession deletes a TOTP login session
  87. func (c *TotpCache) DeleteLoginSession(ctx context.Context, tempToken string) error {
  88. key := totpLoginKeyPrefix + tempToken
  89. return c.rdb.Del(ctx, key).Err()
  90. }
  91. // IncrementVerifyAttempts increments the verify attempt counter
  92. func (c *TotpCache) IncrementVerifyAttempts(ctx context.Context, userID int64) (int, error) {
  93. key := fmt.Sprintf("%s%d", totpAttemptsKeyPrefix, userID)
  94. // Use pipeline for atomic increment and set TTL
  95. pipe := c.rdb.Pipeline()
  96. incrCmd := pipe.Incr(ctx, key)
  97. pipe.Expire(ctx, key, totpAttemptsTTL)
  98. if _, err := pipe.Exec(ctx); err != nil {
  99. return 0, fmt.Errorf("increment verify attempts: %w", err)
  100. }
  101. count, err := incrCmd.Result()
  102. if err != nil {
  103. return 0, fmt.Errorf("get increment result: %w", err)
  104. }
  105. return int(count), nil
  106. }
  107. // GetVerifyAttempts gets the current verify attempt count
  108. func (c *TotpCache) GetVerifyAttempts(ctx context.Context, userID int64) (int, error) {
  109. key := fmt.Sprintf("%s%d", totpAttemptsKeyPrefix, userID)
  110. count, err := c.rdb.Get(ctx, key).Int()
  111. if err != nil {
  112. if err == redis.Nil {
  113. return 0, nil
  114. }
  115. return 0, fmt.Errorf("get verify attempts: %w", err)
  116. }
  117. return count, nil
  118. }
  119. // ClearVerifyAttempts clears the verify attempt counter
  120. func (c *TotpCache) ClearVerifyAttempts(ctx context.Context, userID int64) error {
  121. key := fmt.Sprintf("%s%d", totpAttemptsKeyPrefix, userID)
  122. return c.rdb.Del(ctx, key).Err()
  123. }
  124. func totpStepUpKey(userID int64, sessionKey string) string {
  125. return fmt.Sprintf("%s%d:%s", totpStepUpKeyPrefix, userID, sessionKey)
  126. }
  127. // SetStepUpGrant 记录一次 step-up 验证通过(sudo 窗口),绑定用户+会话。
  128. func (c *TotpCache) SetStepUpGrant(ctx context.Context, userID int64, sessionKey string, ttl time.Duration) error {
  129. return c.rdb.Set(ctx, totpStepUpKey(userID, sessionKey), "1", ttl).Err()
  130. }
  131. // HasStepUpGrant 检查 step-up 授权是否仍在有效期内。
  132. func (c *TotpCache) HasStepUpGrant(ctx context.Context, userID int64, sessionKey string) (bool, error) {
  133. _, err := c.rdb.Get(ctx, totpStepUpKey(userID, sessionKey)).Result()
  134. if err != nil {
  135. if err == redis.Nil {
  136. return false, nil
  137. }
  138. return false, fmt.Errorf("get step-up grant: %w", err)
  139. }
  140. return true, nil
  141. }