Class: JwtAuthCognito::RedisService

Inherits:
Object
  • Object
show all
Defined in:
lib/jwt_auth_cognito/redis_service.rb

Constant Summary collapse

BLACKLIST_PREFIX =

auth-service revokes tokens under ‘revoked:32 chars of token)` (see tokenBlacklist.ts#getTokenHash). This prefix and the id derivation in #generate_token_id MUST match it exactly, otherwise revocations performed by auth-service (/auth/logout) are looked up under a key that never exists and are silently missed. The Node validator uses this same scheme.

'revoked:'
USER_TOKENS_PREFIX =
'user_tokens:'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config = JwtAuthCognito.configuration) ⇒ RedisService

Returns a new instance of RedisService.



42
43
44
# File 'lib/jwt_auth_cognito/redis_service.rb', line 42

def initialize(config = JwtAuthCognito.configuration)
  @config = config
end

Class Method Details

.pool(config, factory) ⇒ Object

Builds (once) and returns the shared pool. The factory is a callable that produces a connected Redis client; it runs lazily as the pool grows.



27
28
29
30
31
32
# File 'lib/jwt_auth_cognito/redis_service.rb', line 27

def pool(config, factory)
  @pool ||= ConnectionPool.new(
    size: config.redis_pool_size,
    timeout: config.redis_pool_timeout
  ) { factory.call }
end

.reset_pool!Object

Drops the shared pool. Call after forking (Puma/Unicorn ‘after_fork`, Sidekiq) so each child builds its own connections instead of reusing parent sockets. Also used to isolate tests.



37
38
39
# File 'lib/jwt_auth_cognito/redis_service.rb', line 37

def reset_pool!
  @pool = nil
end

Instance Method Details

#clear_revoked_tokensObject



74
75
76
77
78
79
80
81
82
# File 'lib/jwt_auth_cognito/redis_service.rb', line 74

def clear_revoked_tokens
  with_redis do |redis|
    keys = redis.keys("#{BLACKLIST_PREFIX}*")
    redis.del(*keys) if keys.any?
    keys.length
  end
rescue Redis::BaseError => e
  raise BlacklistError, "Failed to clear revoked tokens: #{e.message}"
end

#generate_token_id(token) ⇒ Object



117
118
119
120
121
122
123
124
125
# File 'lib/jwt_auth_cognito/redis_service.rb', line 117

def generate_token_id(token)
  # Must match auth-service's key derivation (tokenBlacklist.ts#getTokenHash):
  # base64 of the last 32 characters of the raw token. Using the JTI or a SHA256
  # here would look up a key auth-service never writes, so revocations would be
  # missed. The Node validator (token-blacklist-service.ts#getTokenHash) is identical.
  suffix = token.to_s
  suffix = suffix[-32..] || suffix
  Base64.strict_encode64(suffix)
end

#get(key) ⇒ Object



127
128
129
130
131
# File 'lib/jwt_auth_cognito/redis_service.rb', line 127

def get(key)
  with_redis { |redis| redis.get(key) }
rescue Redis::BaseError => e
  raise BlacklistError, "Failed to get key '#{key}': #{e.message}"
end

#invalidate_user_tokens(user_id) ⇒ Object



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/jwt_auth_cognito/redis_service.rb', line 84

def invalidate_user_tokens(user_id)
  user_key = "#{USER_TOKENS_PREFIX}#{user_id}"

  with_redis do |redis|
    token_ids = redis.smembers(user_key)

    # Persist a revocation marker for each tracked token, then clear the set.
    # Inlined (instead of calling #save_revoked_token) to reuse this single
    # pooled connection and avoid a nested pool checkout.
    token_ids.each { |token_id| redis.set("#{BLACKLIST_PREFIX}#{token_id}", 'revoked') }
    redis.del(user_key)

    token_ids.length
  end
rescue Redis::BaseError => e
  raise BlacklistError, "Failed to invalidate user tokens: #{e.message}"
end

#is_token_revoked?(token_id) ⇒ Boolean

Returns:

  • (Boolean)


62
63
64
65
66
67
68
69
70
71
72
# File 'lib/jwt_auth_cognito/redis_service.rb', line 62

def is_token_revoked?(token_id)
  key = "#{BLACKLIST_PREFIX}#{token_id}"
  result = with_redis { |redis| redis.exists?(key) }
  result.is_a?(Integer) ? result.positive? : result
rescue StandardError
  # Fail-open graceful degradation: if Redis is unreachable or the pool is
  # exhausted, do not block validation. Mirrors the Node validator and
  # auth-service (RedisService.isTokenRevoked), which also treat errors as
  # "not revoked" so an infrastructure issue never locks every user out.
  false
end

#save_revoked_token(token_id, ttl = nil) ⇒ Object



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/jwt_auth_cognito/redis_service.rb', line 46

def save_revoked_token(token_id, ttl = nil)
  key = "#{BLACKLIST_PREFIX}#{token_id}"

  with_redis do |redis|
    if ttl
      redis.setex(key, ttl, 'revoked')
    else
      redis.set(key, 'revoked')
    end
  end

  true
rescue Redis::BaseError => e
  raise BlacklistError, "Failed to save revoked token: #{e.message}"
end

#set(key, value, ttl = nil) ⇒ Object



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/jwt_auth_cognito/redis_service.rb', line 133

def set(key, value, ttl = nil)
  with_redis do |redis|
    # Only attach an expiry for a positive TTL. A nil or non-positive ttl (note:
    # 0 is truthy in Ruby) means a persistent key — `SETEX key 0` is rejected by
    # Redis with "ERR invalid expire time", so fall back to a plain SET.
    if ttl&.positive?
      redis.setex(key, ttl, value)
    else
      redis.set(key, value)
    end
  end
  true
rescue Redis::BaseError => e
  raise BlacklistError, "Failed to set key '#{key}': #{e.message}"
end

#track_user_token(user_id, token_id, ttl = nil) ⇒ Object



102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/jwt_auth_cognito/redis_service.rb', line 102

def track_user_token(user_id, token_id, ttl = nil)
  user_key = "#{USER_TOKENS_PREFIX}#{user_id}"

  with_redis do |redis|
    redis.sadd(user_key, token_id)
    # Set expiration on the user's token set
    redis.expire(user_key, ttl) if ttl
  end

  true
rescue Redis::BaseError
  # Non-critical operation, log but don't fail
  false
end