jwt_auth_cognito 0.1.1 → 1.0.0.pre.beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redis"
4
- require "digest"
5
- require "openssl"
3
+ require 'redis'
4
+ require 'digest'
5
+ require 'openssl'
6
6
 
7
7
  module JwtAuthCognito
8
8
  class RedisService
9
- BLACKLIST_PREFIX = "jwt_blacklist:"
10
- USER_TOKENS_PREFIX = "user_tokens:"
9
+ BLACKLIST_PREFIX = 'jwt_blacklist:'
10
+ USER_TOKENS_PREFIX = 'user_tokens:'
11
11
 
12
12
  def initialize(config = JwtAuthCognito.configuration)
13
13
  @config = config
@@ -17,13 +17,13 @@ module JwtAuthCognito
17
17
  def save_revoked_token(token_id, ttl = nil)
18
18
  connect_redis
19
19
  key = "#{BLACKLIST_PREFIX}#{token_id}"
20
-
20
+
21
21
  if ttl
22
- @redis.setex(key, ttl, "revoked")
22
+ @redis.setex(key, ttl, 'revoked')
23
23
  else
24
- @redis.set(key, "revoked")
24
+ @redis.set(key, 'revoked')
25
25
  end
26
-
26
+
27
27
  true
28
28
  rescue Redis::BaseError => e
29
29
  raise BlacklistError, "Failed to save revoked token: #{e.message}"
@@ -33,8 +33,8 @@ module JwtAuthCognito
33
33
  connect_redis
34
34
  key = "#{BLACKLIST_PREFIX}#{token_id}"
35
35
  result = @redis.exists?(key)
36
- result.is_a?(Integer) ? result > 0 : result
37
- rescue Redis::BaseError => e
36
+ result.is_a?(Integer) ? result.positive? : result
37
+ rescue Redis::BaseError
38
38
  # Graceful degradation - if Redis is down, don't block validation
39
39
  false
40
40
  end
@@ -50,19 +50,19 @@ module JwtAuthCognito
50
50
 
51
51
  def invalidate_user_tokens(user_id)
52
52
  connect_redis
53
-
53
+
54
54
  # Get all tokens for the user
55
55
  user_key = "#{USER_TOKENS_PREFIX}#{user_id}"
56
56
  token_ids = @redis.smembers(user_key)
57
-
57
+
58
58
  # Add all tokens to blacklist
59
59
  token_ids.each do |token_id|
60
60
  save_revoked_token(token_id)
61
61
  end
62
-
62
+
63
63
  # Clear the user's token set
64
64
  @redis.del(user_key)
65
-
65
+
66
66
  token_ids.length
67
67
  rescue Redis::BaseError => e
68
68
  raise BlacklistError, "Failed to invalidate user tokens: #{e.message}"
@@ -70,28 +70,28 @@ module JwtAuthCognito
70
70
 
71
71
  def track_user_token(user_id, token_id, ttl = nil)
72
72
  connect_redis
73
-
73
+
74
74
  user_key = "#{USER_TOKENS_PREFIX}#{user_id}"
75
75
  @redis.sadd(user_key, token_id)
76
-
76
+
77
77
  # Set expiration on the user's token set
78
78
  @redis.expire(user_key, ttl) if ttl
79
-
79
+
80
80
  true
81
- rescue Redis::BaseError => e
81
+ rescue Redis::BaseError
82
82
  # Non-critical operation, log but don't fail
83
83
  false
84
84
  end
85
85
 
86
86
  def generate_token_id(token)
87
87
  # Try to extract jti from token first
88
- begin
88
+ begin
89
89
  payload = JWT.decode(token, nil, false).first
90
- return payload["jti"] if payload["jti"]
90
+ return payload['jti'] if payload['jti']
91
91
  rescue JWT::DecodeError
92
92
  # Fall back to hash if token can't be decoded
93
93
  end
94
-
94
+
95
95
  # Generate hash-based ID
96
96
  Digest::SHA256.hexdigest(token)[0, 16]
97
97
  end
@@ -102,23 +102,21 @@ module JwtAuthCognito
102
102
  return @redis if @redis
103
103
 
104
104
  redis_options = build_redis_options
105
-
105
+
106
106
  # Retry logic with exponential backoff (similar to Node.js implementation)
107
107
  max_retries = 3
108
108
  retry_count = 0
109
-
109
+
110
110
  begin
111
111
  @redis = Redis.new(redis_options)
112
112
  @redis.ping # Test connection
113
113
  @redis
114
114
  rescue Redis::BaseError => e
115
115
  retry_count += 1
116
- if retry_count <= max_retries
117
- sleep(0.1 * (2 ** retry_count)) # Exponential backoff
118
- retry
119
- else
120
- raise BlacklistError, "Failed to connect to Redis after #{max_retries} retries: #{e.message}"
121
- end
116
+ raise BlacklistError, "Failed to connect to Redis after #{max_retries} retries: #{e.message}" unless retry_count <= max_retries
117
+
118
+ sleep(0.1 * (2**retry_count)) # Exponential backoff
119
+ retry
122
120
  end
123
121
  end
124
122
 
@@ -145,35 +143,76 @@ module JwtAuthCognito
145
143
 
146
144
  def build_ssl_params
147
145
  ssl_params = {}
148
-
146
+
149
147
  # Set TLS version constraints
150
- if @config.redis_tls_min_version
151
- ssl_params[:min_version] = parse_tls_version(@config.redis_tls_min_version)
148
+ ssl_params[:min_version] = parse_tls_version(@config.redis_tls_min_version) if @config.redis_tls_min_version
149
+
150
+ ssl_params[:max_version] = parse_tls_version(@config.redis_tls_max_version) if @config.redis_tls_max_version
151
+
152
+ # CA certificate configuration with multiple sources
153
+ ca_cert_data = load_ca_certificate
154
+ if ca_cert_data
155
+ # Create a temporary file for the CA certificate
156
+ require 'tempfile'
157
+ temp_file = Tempfile.new('redis_ca_cert')
158
+ temp_file.write(ca_cert_data)
159
+ temp_file.close
160
+ ssl_params[:ca_file] = temp_file.path
161
+
162
+ # Store reference to prevent garbage collection
163
+ @temp_ca_file = temp_file
152
164
  end
153
-
154
- if @config.redis_tls_max_version
155
- ssl_params[:max_version] = parse_tls_version(@config.redis_tls_max_version)
165
+
166
+ # Verification mode
167
+ ssl_params[:verify_mode] = case @config.redis_verify_mode
168
+ when 'none'
169
+ OpenSSL::SSL::VERIFY_NONE
170
+ when 'peer'
171
+ OpenSSL::SSL::VERIFY_PEER
172
+ else
173
+ OpenSSL::SSL::VERIFY_PEER
174
+ end
175
+
176
+ ssl_params
177
+ end
178
+
179
+ def load_ca_certificate
180
+ # Priority order for certificate loading (matching Node.js implementation):
181
+ # 1. SSM Parameter Store (for auth-service compatibility)
182
+ # 2. Local file system
183
+ # 3. Environment variable
184
+
185
+ # 1. Try SSM Parameter Store first (for auth-service compatibility)
186
+ if @config.redis_ca_cert_ssm_path && @config.redis_ca_cert_ssm_name
187
+ begin
188
+ puts '🔍 Loading CA certificate from SSM...'
189
+ return JwtAuthCognito::SSMService.get_ca_certificate(
190
+ @config.redis_ca_cert_ssm_path,
191
+ @config.redis_ca_cert_ssm_name
192
+ )
193
+ rescue StandardError => e
194
+ puts "⚠️ Failed to load certificate from SSM: #{e.message}"
195
+ puts '⚠️ Falling back to file system...'
196
+ end
156
197
  end
157
-
158
- # CA certificate configuration
198
+
199
+ # 2. Try local file system
159
200
  if @config.redis_ca_cert_path && @config.redis_ca_cert_name
160
201
  ca_cert_file = File.join(@config.redis_ca_cert_path, @config.redis_ca_cert_name)
161
202
  if File.exist?(ca_cert_file)
162
- ssl_params[:ca_file] = ca_cert_file
203
+ puts "📁 Loading CA certificate from file system: #{ca_cert_file}"
204
+ return File.read(ca_cert_file)
163
205
  end
164
206
  end
165
-
166
- # Verification mode
167
- case @config.redis_verify_mode
168
- when 'none'
169
- ssl_params[:verify_mode] = OpenSSL::SSL::VERIFY_NONE
170
- when 'peer'
171
- ssl_params[:verify_mode] = OpenSSL::SSL::VERIFY_PEER
172
- else
173
- ssl_params[:verify_mode] = OpenSSL::SSL::VERIFY_PEER
207
+
208
+ # 3. Try environment variable
209
+ if ENV['REDIS_CA_CERT']
210
+ puts '🌍 Loading CA certificate from environment variable'
211
+ return ENV['REDIS_CA_CERT']
174
212
  end
175
-
176
- ssl_params
213
+
214
+ puts '⚠️ No CA certificate found, proceeding without certificate validation'
215
+ nil
177
216
  end
178
217
 
179
218
  def parse_tls_version(version_string)
@@ -191,4 +230,4 @@ module JwtAuthCognito
191
230
  end
192
231
  end
193
232
  end
194
- end
233
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+ require 'aws-sdk-ssm'
7
+
8
+ module JwtAuthCognito
9
+ class SSMService
10
+ class << self
11
+ attr_accessor :client, :certificate_cache
12
+ end
13
+
14
+ @client = nil
15
+ @certificate_cache = {}
16
+
17
+ # Initialize the SSM client
18
+ def self.get_client
19
+ @client ||= begin
20
+ require 'aws-sdk-ssm'
21
+ region = ENV['AWS_REGION'] || ENV['AWS_DEFAULT_REGION'] || 'us-east-1'
22
+ Aws::SSM::Client.new(region: region)
23
+ end
24
+ rescue LoadError
25
+ raise ConfigurationError,
26
+ "aws-sdk-ssm gem is required for SSM functionality. Add 'gem \"aws-sdk-ssm\"' to your Gemfile"
27
+ end
28
+
29
+ # Gets a certificate from AWS Parameter Store (compatible with auth-service)
30
+ # Uses the same path pattern: /${cert_path}/${cert_name}
31
+ def self.get_ca_certificate(cert_path, cert_name)
32
+ full_path = "/#{cert_path}/#{cert_name}"
33
+
34
+ # Check cache first
35
+ if @certificate_cache.key?(full_path)
36
+ puts '📋 Using cached certificate from SSM'
37
+ return @certificate_cache[full_path]
38
+ end
39
+
40
+ begin
41
+ puts "📡 Getting certificate from Parameter Store: #{full_path}"
42
+
43
+ client = get_client
44
+ response = client.get_parameter({
45
+ name: full_path,
46
+ with_decryption: true
47
+ })
48
+
49
+ raise ConfigurationError, "Certificate parameter not found or invalid: #{full_path}" unless response.parameter&.value
50
+
51
+ # Cache the certificate
52
+ @certificate_cache[full_path] = response.parameter.value
53
+ puts '✅ Certificate obtained from SSM and cached'
54
+
55
+ response.parameter.value
56
+ rescue Aws::SSM::Errors::ParameterNotFound
57
+ raise ConfigurationError, "Certificate parameter not found: #{full_path}"
58
+ rescue Aws::SSM::Errors::ServiceError => e
59
+ puts "❌ Error getting certificate from SSM (#{full_path}): #{e.message}"
60
+ raise ConfigurationError, "Error accessing SSM: #{e.message}"
61
+ rescue StandardError => e
62
+ puts "❌ Error getting certificate from SSM (#{full_path}): #{e.message}"
63
+ raise e
64
+ end
65
+ end
66
+
67
+ # Gets a parameter from AWS Parameter Store
68
+ def self.get_parameter(parameter_name, with_decryption = true)
69
+ # Check cache first
70
+ return @certificate_cache[parameter_name] if @certificate_cache.key?(parameter_name)
71
+
72
+ begin
73
+ client = get_client
74
+ response = client.get_parameter({
75
+ name: parameter_name,
76
+ with_decryption: with_decryption
77
+ })
78
+
79
+ raise ConfigurationError, "Parameter not found or invalid: #{parameter_name}" unless response.parameter&.value
80
+
81
+ # Cache the parameter
82
+ @certificate_cache[parameter_name] = response.parameter.value
83
+
84
+ response.parameter.value
85
+ rescue Aws::SSM::Errors::ParameterNotFound
86
+ raise ConfigurationError, "Parameter not found: #{parameter_name}"
87
+ rescue Aws::SSM::Errors::ServiceError => e
88
+ puts "❌ Error getting parameter from SSM (#{parameter_name}): #{e.message}"
89
+ raise ConfigurationError, "Error accessing SSM: #{e.message}"
90
+ rescue StandardError => e
91
+ puts "❌ Error getting parameter from SSM (#{parameter_name}): #{e.message}"
92
+ raise e
93
+ end
94
+ end
95
+
96
+ # Clears the certificate cache
97
+ def self.clear_cache
98
+ @certificate_cache.clear
99
+ end
100
+
101
+ # Gets cache stats
102
+ def self.cache_stats
103
+ {
104
+ size: @certificate_cache.size,
105
+ keys: @certificate_cache.keys
106
+ }
107
+ end
108
+ end
109
+ end
@@ -10,14 +10,12 @@ module JwtAuthCognito
10
10
  def add_to_blacklist(token, user_id: nil)
11
11
  token_id = @redis_service.generate_token_id(token)
12
12
  ttl = calculate_ttl(token)
13
-
13
+
14
14
  result = @redis_service.save_revoked_token(token_id, ttl)
15
-
15
+
16
16
  # Track token for user if provided
17
- if user_id
18
- @redis_service.track_user_token(user_id, token_id, ttl)
19
- end
20
-
17
+ @redis_service.track_user_token(user_id, token_id, ttl) if user_id
18
+
21
19
  result
22
20
  end
23
21
 
@@ -39,18 +37,18 @@ module JwtAuthCognito
39
37
  def calculate_ttl(token)
40
38
  begin
41
39
  payload = JWT.decode(token, nil, false).first
42
- exp = payload["exp"]
43
-
40
+ exp = payload['exp']
41
+
44
42
  if exp
45
43
  ttl = exp - Time.now.to_i
46
- return ttl > 0 ? ttl : 1 # At least 1 second TTL
44
+ return ttl.positive? ? ttl : 1 # At least 1 second TTL
47
45
  end
48
46
  rescue JWT::DecodeError
49
47
  # If we can't decode the token, use a default TTL
50
48
  end
51
-
49
+
52
50
  # Default TTL of 1 day if we can't determine expiration
53
- 86400
51
+ 86_400
54
52
  end
55
53
  end
56
- end
54
+ end