rack_jwt_aegis 1.0.0 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 775e606ab0475ef440f8d5e0b0cf4fa7834fdcadb9565c6123197437103b2e1f
4
- data.tar.gz: 8dbf8f35c54e16ee86cc31b07a24d8ced023d7aa7684a3f1841cc811ceeb7c97
3
+ metadata.gz: 6bc9e89c08947641810cb3c0a5aef91c56554dcf44e593abc10cb7b75ebe6478
4
+ data.tar.gz: bcbd0711caf2bd74d65ede714cf4cf24eb016146fc69e20bfdf76a165d8a009c
5
5
  SHA512:
6
- metadata.gz: 758002b6d87d06d508eb2a548970ef81a16690db287362054927d4833c1200ded1f179fd573f505201d0d10204a9345bbc9f97c83af4c557757a3b39ed6eee8a
7
- data.tar.gz: cfa522bc3373b26e1180507d04808132e4a3ea8aabfb4ac2a1661b62ec4953fc5014b93a89df9ae2d46f9c4af5b89893c298db7c37a9680d1f82bae1608e75ac
6
+ metadata.gz: 7d279d25565397a64e45be0b157a01e63ca7c8e688fd1bb3592649cef8a2037380f5c9c1ddad645ad46f2326d5cd8cc279661207371825d26fe0c6cc036dd272
7
+ data.tar.gz: fe7e42dc37a5dca3a4fe443fc14e0a64178994b20406471ab6139be4e0b8917a663bcc45d29a819749875a9ef4cecd7d85cd8e13ef9cf5b6054ace0be090d540
data/CHANGELOG.md CHANGED
@@ -5,6 +5,44 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.1] - 2025-08-13
9
+
10
+ ### 🔧 Fixed
11
+
12
+ #### Code Quality & Maintenance
13
+
14
+ - **DRY Refactoring**: Eliminated duplicate `debug_log` method implementations by creating a shared `DebugLogger` module
15
+ - Created `lib/rack_jwt_aegis/debug_logger.rb` with consistent debug logging functionality
16
+ - Updated `Middleware` and `RbacManager` classes to include the shared module
17
+ - Improved code maintainability by centralizing debug logging logic
18
+ - Maintains all existing functionality and logging behavior
19
+ - **RBAC Cache Validation**: Enhanced wildcard permission validation in `validate_rbac_cache_format` to support `admin/*` patterns
20
+ - **JWT Payload Resolution**: Fixed JWT payload key resolution to handle string keys consistently across components
21
+ - **Test Coverage**: Maintained high test coverage (98.17% line coverage) after refactoring
22
+
23
+ #### Developer Experience
24
+
25
+ - **Consistent Logging**: Unified debug log format across all components with automatic timestamp formatting
26
+ - **Component Identification**: Automatic component name inference for better log traceability
27
+ - **Configurable Log Levels**: Support for info, warn, and error log levels with appropriate output streams
28
+
29
+ ### 🏗️ Technical Details
30
+
31
+ #### Architecture Improvements
32
+
33
+ - **Shared Module Pattern**: Introduced consistent module inclusion pattern for cross-cutting concerns
34
+ - **Code Organization**: Better separation of concerns with dedicated debug logging module
35
+ - **Maintainability**: Reduced code duplication from ~40 lines to a single shared implementation
36
+
37
+ #### Testing & Quality
38
+
39
+ - **Test Suite**: All 340 tests pass with 975 assertions
40
+ - **Coverage Maintained**: 98.17% line coverage, 92.83% branch coverage
41
+ - **RBAC Integration**: Verified all role-based authorization tests pass after refactoring
42
+ - **Zero Regression**: No functional changes, only structural improvements
43
+
44
+ ---
45
+
8
46
  ## [1.0.0] - 2025-08-13
9
47
 
10
48
  ### 🎉 Initial Release
@@ -201,4 +239,5 @@ This 1.0.0 release represents a production-ready JWT authentication middleware w
201
239
 
202
240
  **Deprecations**: None (initial release).
203
241
 
242
+ [1.0.1]: https://github.com/kanutocd/rack_jwt_aegis/releases/tag/v1.0.1
204
243
  [1.0.0]: https://github.com/kanutocd/rack_jwt_aegis/releases/tag/v1.0.0
data/README.md CHANGED
@@ -14,7 +14,9 @@ JWT authentication middleware for hierarchical multi-tenant Rack applications wi
14
14
  - 2-level multi-tenant support (Example: Company-Group → Company, Organization → Department, etc.)
15
15
  - Subdomain-based tenant isolation for top-level tenants
16
16
  - URL pathname slug access control for sub-level tenants
17
+ - **RBAC (Role-Based Access Control)** with flexible role extraction from JWT payloads
17
18
  - Configurable path exclusions for public endpoints
19
+ - **Flexible payload mapping** for custom JWT claim names
18
20
  - Custom payload validation
19
21
  - Debug mode for development
20
22
 
@@ -175,14 +177,8 @@ RackJwtAegis::Middleware.new(app, {
175
177
  user_id: :sub, # Map 'sub' claim to user_id
176
178
  tenant_id: :company_group_id, # Map 'company_group_id' claim
177
179
  subdomain: :company_group_domain_name, # Map 'company_group_domain_name' claim
178
- pathname_slugs: :accessible_company_slugs # Map array of accessible companies
179
- },
180
-
181
- # Custom Tenant Extraction
182
- tenant_strategy: :custom,
183
- tenant_extractor: ->(request) {
184
- # Extract tenant from custom header or logic
185
- request.get_header('HTTP_X_TENANT_ID')
180
+ pathname_slugs: :accessible_company_slugs, # Map array of accessible companies
181
+ role_ids: :user_roles # Map 'user_roles' claim for RBAC authorization
186
182
  }
187
183
  })
188
184
  ```
@@ -285,7 +281,8 @@ The middleware expects JWT payloads with the following structure:
285
281
  "tenant_id": 67890,
286
282
  "subdomain": "acme-group-of-companies", # the subdomain part of the host of the request url, e.g. `http://acme-group-of-companies.example.com`
287
283
  "pathname_slugs": ["an-acme-company-subsidiary", "another-acme-company-the-user-has-access"], # the user has access to these kind of request urls: https://acme-group-of-companies.example.com/api/v1/an-acme-company-subsidiary/* or https://acme-group-of-companies.example.com/api/v1/another-acme-company-the-user-has-access/
288
- "roles": ["admin", "user"],
284
+ "role_ids": ["123", "456"], # Role IDs for RBAC authorization (can also be integers)
285
+ "roles": ["admin", "user"], # Legacy role names (kept for backward compatibility)
289
286
  "exp": 1640995200,
290
287
  "iat": 1640991600
291
288
  }
@@ -293,6 +290,69 @@ The middleware expects JWT payloads with the following structure:
293
290
 
294
291
  You can customize the payload mapping using the `payload_mapping` configuration option.
295
292
 
293
+ ### RBAC Role Extraction
294
+
295
+ When RBAC is enabled, the middleware extracts user roles from the JWT payload for authorization. The default payload mapping includes:
296
+
297
+ ```ruby
298
+ payload_mapping: {
299
+ user_id: :user_id,
300
+ tenant_id: :tenant_id,
301
+ subdomain: :subdomain,
302
+ pathname_slugs: :pathname_slugs,
303
+ role_ids: :role_ids # Default field for user roles
304
+ }
305
+ ```
306
+
307
+ #### Role Field Resolution
308
+
309
+ The middleware looks for roles in the following priority order:
310
+
311
+ 1. **Configured Field**: Uses the `role_ids` mapping (e.g., if mapped to `:user_roles`, looks for `user_roles` field)
312
+ 2. **Fallback Fields**: If the mapped field is not found, tries these common alternatives:
313
+ - `roles` - Array of role identifiers
314
+ - `role` - Single role identifier
315
+ - `user_roles` - Array of user role identifiers
316
+ - `role_ids` - Array of role IDs (numeric or string)
317
+
318
+ #### Custom Role Field Mapping
319
+
320
+ You can customize the role field using payload mapping:
321
+
322
+ ```ruby
323
+ # Use a custom field name for roles
324
+ payload_mapping: {
325
+ role_ids: :user_permissions # Look for roles in 'user_permissions' field
326
+ }
327
+
328
+ # JWT payload would contain:
329
+ {
330
+ "user_id": 123,
331
+ "user_permissions": ["admin", "manager"],
332
+ ...
333
+ }
334
+ ```
335
+
336
+ #### Role Format Support
337
+
338
+ The middleware supports flexible role formats:
339
+
340
+ ```ruby
341
+ # Array of strings (recommended)
342
+ "role_ids": ["123", "456", "admin"]
343
+
344
+ # Array of integers
345
+ "role_ids": [123, 456]
346
+
347
+ # Single string
348
+ "role_ids": "admin"
349
+
350
+ # Single integer
351
+ "role_ids": 123
352
+ ```
353
+
354
+ All role values are normalized to strings internally for consistent matching against RBAC cache permissions.
355
+
296
356
  ## Security Features
297
357
 
298
358
  - JWT signature verification
@@ -410,7 +470,8 @@ When RBAC is enabled, the middleware expects permissions to be stored in the cac
410
470
 
411
471
  2. **RBAC Permissions Validation**: Full permission evaluation
412
472
 
413
- - Extract user roles from JWT payload (`roles`, `role`, `user_roles`, or `role_ids` field)
473
+ - Extract user roles from JWT payload using configurable field mapping (default: `role_ids`)
474
+ - Fallback to common fields: `roles`, `role`, `user_roles`, or `role_ids` if mapped field not found
414
475
  - Load RBAC permissions collection and validate format
415
476
  - For each user role, check if any permission matches:
416
477
  - Extract resource path from request URL (removes subdomain/pathname slug)
@@ -491,8 +552,8 @@ To install this gem onto your local machine, run `bundle exec rake install`.
491
552
 
492
553
  This project maintains high test coverage:
493
554
 
494
- - **Line Coverage**: 97.8% (668/683 lines)
495
- - **Branch Coverage**: 86.62% (259/299 branches)
555
+ - **Line Coverage**: 97.81% (670/685 lines)
556
+ - **Branch Coverage**: 87.13% (264/303 branches)
496
557
 
497
558
  Run tests with coverage: `bundle exec rake test`
498
559
 
@@ -273,6 +273,7 @@ module RackJwtAegis
273
273
  tenant_id: :tenant_id,
274
274
  subdomain: :subdomain,
275
275
  pathname_slugs: :pathname_slugs,
276
+ role_ids: :role_ids,
276
277
  }
277
278
  @unauthorized_response = { error: 'Authentication required' }
278
279
  @forbidden_response = { error: 'Access denied' }
@@ -280,6 +281,7 @@ module RackJwtAegis
280
281
 
281
282
  def validate!
282
283
  validate_jwt_settings!
284
+ validate_payload_mapping!
283
285
  validate_cache_settings!
284
286
  validate_multi_tenant_settings!
285
287
  end
@@ -293,6 +295,24 @@ module RackJwtAegis
293
295
  raise ConfigurationError, "Unsupported JWT algorithm: #{jwt_algorithm}"
294
296
  end
295
297
 
298
+ def validate_payload_mapping!
299
+ # Allow nil payload_mapping (will use defaults)
300
+ return if payload_mapping.nil?
301
+
302
+ raise ConfigurationError, 'payload_mapping must be a Hash' unless payload_mapping.is_a?(Hash)
303
+
304
+ # Validate all values are symbols
305
+ invalid_values = payload_mapping.reject { |_key, value| value.is_a?(Symbol) }
306
+ return if invalid_values.empty?
307
+
308
+ raise ConfigurationError, "payload_mapping values must be symbols, invalid: #{invalid_values.inspect}"
309
+
310
+ # NOTE: We don't validate required keys because users may provide
311
+ # partial mappings that are intended to override defaults. The payload_key method
312
+ # handles missing keys by returning the standard key as fallback.
313
+ # This includes RBAC keys - if :role_ids is not mapped, it falls back to 'role_ids'.
314
+ end
315
+
296
316
  def validate_cache_settings!
297
317
  return unless rbac_enabled?
298
318
 
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RackJwtAegis
4
+ # Shared debug logging functionality
5
+ #
6
+ # Provides consistent debug logging across all RackJwtAegis components
7
+ # with configurable log levels and automatic timestamp formatting.
8
+ #
9
+ # @author Ken Camajalan Demanawa
10
+ # @since 1.0.0
11
+ module DebugLogger
12
+ # Log debug message if debug mode is enabled
13
+ #
14
+ # @param message [String] the message to log
15
+ # @param level [Symbol] the log level (:info, :warn, :error) (default: :info)
16
+ # @param component [String] the component name for log prefixing (optional)
17
+ def debug_log(message, level = :info, component = nil)
18
+ return unless @config.debug_mode?
19
+
20
+ timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S.%L')
21
+
22
+ # Determine component name for log prefix
23
+ component_name = component || infer_component_name
24
+
25
+ formatted_message = "[#{timestamp}] #{component_name}: #{message}"
26
+
27
+ case level
28
+ when :error, :warn
29
+ warn formatted_message
30
+ else
31
+ puts formatted_message
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ # Infer component name from class name
38
+ #
39
+ # @return [String] the inferred component name
40
+ def infer_component_name
41
+ case self.class.name
42
+ when /Middleware/
43
+ 'RackJwtAegis'
44
+ when /RbacManager/
45
+ 'RbacManager'
46
+ else
47
+ self.class.name.split('::').last || 'RackJwtAegis'
48
+ end
49
+ end
50
+ end
51
+ end
@@ -25,6 +25,8 @@ module RackJwtAegis
25
25
  # skip_paths: ['/health', '/api/public/*']
26
26
  # }
27
27
  class Middleware
28
+ include DebugLogger
29
+
28
30
  # Initialize the middleware
29
31
  #
30
32
  # @param app [#call] the Rack application
@@ -65,7 +67,7 @@ module RackJwtAegis
65
67
  token = extract_jwt_token(request)
66
68
  payload = @jwt_validator.validate(token)
67
69
 
68
- debug_log("JWT validation successful for user: #{payload[@config.payload_key(:user_id)]}")
70
+ debug_log("JWT validation successful for user: #{payload[@config.payload_key(:user_id).to_s]}")
69
71
 
70
72
  # Step 3: Multi-tenant validation (if enabled)
71
73
  if multi_tenant_enabled?
@@ -159,8 +161,12 @@ module RackJwtAegis
159
161
  # @param payload [Hash] the JWT payload
160
162
  # @return [Array] array of user role IDs
161
163
  def extract_user_roles(payload)
162
- # Try multiple common role field names
163
- roles = payload['roles'] || payload['role'] || payload['user_roles'] || payload['role_ids']
164
+ # Use configured payload mapping for role_ids, with fallback to common field names
165
+ role_key = @config.payload_key(:role_ids).to_s
166
+ roles = payload[role_key]
167
+
168
+ # If mapped key doesn't exist, try common fallback field names
169
+ roles = payload['roles'] || payload['role'] || payload['user_roles'] || payload['role_ids'] if roles.nil?
164
170
 
165
171
  case roles
166
172
  when Array
@@ -168,19 +174,10 @@ module RackJwtAegis
168
174
  when String, Integer
169
175
  [roles.to_s] # Single role as array
170
176
  else
171
- debug_log("Warning: No valid roles found in JWT payload. Available fields: #{payload.keys}")
177
+ debug_log("Warning: No valid roles found in JWT payload. Looking for '#{role_key}' field. \
178
+ Available fields: #{payload.keys}".squeeze)
172
179
  []
173
180
  end
174
181
  end
175
-
176
- # Log debug message if debug mode is enabled
177
- #
178
- # @param message [String] the message to log
179
- def debug_log(message)
180
- return unless @config.debug_mode?
181
-
182
- timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S.%L')
183
- puts "[#{timestamp}] RackJwtAegis: #{message}"
184
- end
185
182
  end
186
183
  end
@@ -15,6 +15,8 @@ module RackJwtAegis
15
15
  # manager = RbacManager.new(config)
16
16
  # manager.authorize(request, jwt_payload)
17
17
  class RbacManager
18
+ include DebugLogger
19
+
18
20
  CACHE_TTL = 300 # 5 minutes default cache TTL
19
21
 
20
22
  # Initialize the RBAC manager
@@ -122,14 +124,12 @@ module RackJwtAegis
122
124
  end
123
125
 
124
126
  # Permission is fresh
125
- if @config.debug_mode?
126
- debug_log("Cache hit: #{permission_key} (permission age: \
127
- #{permission_age}s, RBAC age: #{rbac_update_age || 'unknown'}s)".squeeze)
128
- end
127
+ debug_log("Cache hit: #{permission_key} (permission age: \
128
+ #{permission_age}s, RBAC age: #{rbac_update_age || 'unknown'}s)".squeeze)
129
129
  true
130
130
  rescue CacheError => e
131
131
  # Log cache error but don't fail the request
132
- warn "RbacManager cache read error: #{e.message}" if @config.debug_mode?
132
+ debug_log("RbacManager cache read error: #{e.message}", :warn)
133
133
  nil
134
134
  end
135
135
  end
@@ -146,7 +146,7 @@ module RackJwtAegis
146
146
  false
147
147
  rescue CacheError => e
148
148
  # Cache error - fail secure (deny access)
149
- warn "RbacManager RBAC cache error: #{e.message}" if @config.debug_mode?
149
+ debug_log("RbacManager RBAC cache error: #{e.message}", :warn)
150
150
  false
151
151
  end
152
152
 
@@ -166,10 +166,10 @@ module RackJwtAegis
166
166
  # Write back to cache
167
167
  @permission_cache.write('user_permissions', user_permissions, expires_in: CACHE_TTL)
168
168
 
169
- debug_log("Cached permission: #{permission_key} => #{current_time}") if @config.debug_mode?
169
+ debug_log("Cached permission: #{permission_key} => #{current_time}")
170
170
  rescue CacheError => e
171
171
  # Log cache error but don't fail the request
172
- warn "RbacManager permission cache write error: #{e.message}" if @config.debug_mode?
172
+ debug_log("RbacManager permission cache write error: #{e.message}", :warn)
173
173
  end
174
174
  end
175
175
 
@@ -177,7 +177,7 @@ module RackJwtAegis
177
177
  # Extract user roles from JWT payload
178
178
  user_roles = extract_user_roles_from_request(request)
179
179
  if user_roles.nil? || user_roles.empty?
180
- warn 'RbacManager: No user roles found in request context' if @config.debug_mode?
180
+ debug_log('RbacManager: No user roles found in request context', :warn)
181
181
  return false
182
182
  end
183
183
 
@@ -214,7 +214,7 @@ module RackJwtAegis
214
214
 
215
215
  nil
216
216
  rescue CacheError => e
217
- warn "RbacManager RBAC last-update read error: #{e.message}" if @config.debug_mode?
217
+ debug_log("RbacManager RBAC last-update read error: #{e.message}", :warn)
218
218
  nil
219
219
  end
220
220
  end
@@ -233,14 +233,14 @@ module RackJwtAegis
233
233
  # If no permissions remain, remove the entire cache
234
234
  if user_permissions.empty?
235
235
  @permission_cache.delete('user_permissions')
236
- debug_log("Removed last permission, cleared entire cache: #{reason}") if @config.debug_mode?
236
+ debug_log("Removed last permission, cleared entire cache: #{reason}")
237
237
  else
238
238
  # Update the cache with the modified permissions
239
239
  @permission_cache.write('user_permissions', user_permissions, expires_in: CACHE_TTL)
240
- debug_log("Removed stale permission #{permission_key}: #{reason}") if @config.debug_mode?
240
+ debug_log("Removed stale permission #{permission_key}: #{reason}")
241
241
  end
242
242
  rescue CacheError => e
243
- warn "RbacManager stale permission removal error: #{e.message}" if @config.debug_mode?
243
+ debug_log("RbacManager stale permission removal error: #{e.message}", :warn)
244
244
  end
245
245
  end
246
246
 
@@ -250,9 +250,9 @@ module RackJwtAegis
250
250
 
251
251
  begin
252
252
  @permission_cache.delete('user_permissions')
253
- debug_log("Nuked user permissions cache: #{reason}") if @config.debug_mode?
253
+ debug_log("Nuked user permissions cache: #{reason}")
254
254
  rescue CacheError => e
255
- warn "RbacManager cache nuke error: #{e.message}" if @config.debug_mode?
255
+ debug_log("RbacManager cache nuke error: #{e.message}", :warn)
256
256
  end
257
257
  end
258
258
 
@@ -313,10 +313,10 @@ module RackJwtAegis
313
313
  # Write back to cache
314
314
  @permission_cache.write('user_permissions', user_permissions, expires_in: CACHE_TTL)
315
315
 
316
- debug_log("Cached user permission: #{permission_key} => #{current_time}") if @config.debug_mode?
316
+ debug_log("Cached user permission: #{permission_key} => #{current_time}")
317
317
  rescue CacheError => e
318
318
  # Log cache error but don't fail the request
319
- warn "RbacManager permission cache write error: #{e.message}" if @config.debug_mode?
319
+ debug_log("RbacManager permission cache write error: #{e.message}", :warn)
320
320
  end
321
321
  end
322
322
 
@@ -380,7 +380,7 @@ module RackJwtAegis
380
380
  regex = Regexp.new(regex_pattern)
381
381
  return regex.match?(resource_path)
382
382
  rescue RegexpError => e
383
- warn "RbacManager: Invalid regex pattern '#{regex_pattern}': #{e.message}" if @config.debug_mode?
383
+ debug_log("RbacManager: Invalid regex pattern '#{regex_pattern}': #{e.message}", :warn)
384
384
  return false
385
385
  end
386
386
  end
@@ -422,6 +422,7 @@ module RackJwtAegis
422
422
  # Each permission should be a string in format "endpoint:method"
423
423
  role_permissions.each do |permission|
424
424
  return false unless permission.is_a?(String)
425
+ # Permission must include ':' (resource:method format)
425
426
  return false unless permission.include?(':')
426
427
  end
427
428
  end
@@ -429,16 +430,8 @@ module RackJwtAegis
429
430
 
430
431
  true
431
432
  rescue StandardError => e
432
- warn "RbacManager: Cache format validation error: #{e.message}" if @config.debug_mode?
433
+ debug_log("RbacManager: Cache format validation error: #{e.message}", :warn)
433
434
  false
434
435
  end
435
-
436
- # Log debug message if debug mode is enabled
437
- def debug_log(message)
438
- return unless @config.debug_mode?
439
-
440
- timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S.%L')
441
- puts "[#{timestamp}] RbacManager: #{message}"
442
- end
443
436
  end
444
437
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RackJwtAegis
4
- VERSION = '1.0.0'
4
+ VERSION = '1.0.1'
5
5
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'rack_jwt_aegis/version'
4
4
  require_relative 'rack_jwt_aegis/configuration'
5
+ require_relative 'rack_jwt_aegis/debug_logger'
5
6
  require_relative 'rack_jwt_aegis/middleware'
6
7
  require_relative 'rack_jwt_aegis/jwt_validator'
7
8
  require_relative 'rack_jwt_aegis/multi_tenant_validator'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack_jwt_aegis
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken C. Demanawa
@@ -68,6 +68,7 @@ files:
68
68
  - lib/rack_jwt_aegis.rb
69
69
  - lib/rack_jwt_aegis/cache_adapter.rb
70
70
  - lib/rack_jwt_aegis/configuration.rb
71
+ - lib/rack_jwt_aegis/debug_logger.rb
71
72
  - lib/rack_jwt_aegis/jwt_validator.rb
72
73
  - lib/rack_jwt_aegis/middleware.rb
73
74
  - lib/rack_jwt_aegis/multi_tenant_validator.rb