rack_jwt_aegis 0.0.0
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 +7 -0
- data/.rubocop.yml +160 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +215 -0
- data/Rakefile +12 -0
- data/examples/basic_usage.rb +85 -0
- data/lib/rack_jwt_aegis/cache_adapter.rb +264 -0
- data/lib/rack_jwt_aegis/configuration.rb +173 -0
- data/lib/rack_jwt_aegis/jwt_validator.rb +113 -0
- data/lib/rack_jwt_aegis/middleware.rb +116 -0
- data/lib/rack_jwt_aegis/multi_tenant_validator.rb +129 -0
- data/lib/rack_jwt_aegis/rbac_manager.rb +197 -0
- data/lib/rack_jwt_aegis/request_context.rb +100 -0
- data/lib/rack_jwt_aegis/response_builder.rb +59 -0
- data/lib/rack_jwt_aegis/version.rb +5 -0
- data/lib/rack_jwt_aegis.rb +19 -0
- data/sig/rack_jwt_bastion.rbs +4 -0
- metadata +93 -0
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RackJwtAegis
|
4
|
+
class MultiTenantValidator
|
5
|
+
def initialize(config)
|
6
|
+
@config = config
|
7
|
+
end
|
8
|
+
|
9
|
+
def validate(request, payload)
|
10
|
+
validate_subdomain(request, payload) if @config.validate_subdomain?
|
11
|
+
validate_company_slug(request, payload) if @config.validate_company_slug?
|
12
|
+
validate_company_header(request, payload) if @config.company_header_name
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
# Level 1 Multi-Tenant: Top-level tenant (Company-Group) validation via subdomain
|
18
|
+
def validate_subdomain(request, payload)
|
19
|
+
request_host = request.host
|
20
|
+
return if request_host.nil? || request_host.empty?
|
21
|
+
|
22
|
+
# Extract subdomain from request host
|
23
|
+
request_subdomain = extract_subdomain(request_host)
|
24
|
+
|
25
|
+
# Get JWT domain claim
|
26
|
+
jwt_domain_key = @config.payload_key(:company_group_domain).to_s
|
27
|
+
jwt_domain = payload[jwt_domain_key]
|
28
|
+
|
29
|
+
if jwt_domain.nil? || jwt_domain.empty?
|
30
|
+
raise AuthorizationError, 'JWT payload missing company_group_domain for subdomain validation'
|
31
|
+
end
|
32
|
+
|
33
|
+
# Extract subdomain from JWT domain
|
34
|
+
jwt_subdomain = extract_subdomain(jwt_domain)
|
35
|
+
|
36
|
+
# Compare subdomains
|
37
|
+
return if subdomains_match?(request_subdomain, jwt_subdomain)
|
38
|
+
|
39
|
+
raise AuthorizationError,
|
40
|
+
"Subdomain access denied: request subdomain '#{request_subdomain}' does not match JWT subdomain '#{jwt_subdomain}'"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Level 2 Multi-Tenant: Sub-level tenant (Company) validation via URL path
|
44
|
+
def validate_company_slug(request, payload)
|
45
|
+
# Extract company slug from URL path
|
46
|
+
company_slug = extract_company_slug_from_path(request.path)
|
47
|
+
|
48
|
+
return if company_slug.nil? # No company slug in path
|
49
|
+
|
50
|
+
# Get accessible company slugs from JWT
|
51
|
+
jwt_slugs_key = @config.payload_key(:company_slugs).to_s
|
52
|
+
accessible_slugs = payload[jwt_slugs_key]
|
53
|
+
|
54
|
+
if accessible_slugs.nil? || !accessible_slugs.is_a?(Array) || accessible_slugs.empty?
|
55
|
+
raise AuthorizationError, 'JWT payload missing or invalid company_slugs for company access validation'
|
56
|
+
end
|
57
|
+
|
58
|
+
# Check if requested company slug is in user's accessible list
|
59
|
+
return if accessible_slugs.include?(company_slug)
|
60
|
+
|
61
|
+
raise AuthorizationError,
|
62
|
+
"Company access denied: '#{company_slug}' not in accessible companies #{accessible_slugs}"
|
63
|
+
end
|
64
|
+
|
65
|
+
# Company Group header validation (additional security layer)
|
66
|
+
def validate_company_header(request, payload)
|
67
|
+
header_name = "HTTP_#{@config.company_header_name.upcase.tr('-', '_')}"
|
68
|
+
header_value = request.get_header(header_name)
|
69
|
+
|
70
|
+
return if header_value.nil? # Header not present, skip validation
|
71
|
+
|
72
|
+
# Get company group ID from JWT
|
73
|
+
jwt_company_group_key = @config.payload_key(:company_group_id).to_s
|
74
|
+
jwt_company_group_id = payload[jwt_company_group_key]
|
75
|
+
|
76
|
+
if jwt_company_group_id.nil?
|
77
|
+
raise AuthorizationError, 'JWT payload missing company_group_id for header validation'
|
78
|
+
end
|
79
|
+
|
80
|
+
# Normalize values for comparison (both as strings)
|
81
|
+
header_value_str = header_value.to_s
|
82
|
+
jwt_value_str = jwt_company_group_id.to_s
|
83
|
+
|
84
|
+
return if header_value_str == jwt_value_str
|
85
|
+
|
86
|
+
raise AuthorizationError,
|
87
|
+
"Company group header mismatch: header '#{header_value_str}' does not match JWT '#{jwt_value_str}'"
|
88
|
+
end
|
89
|
+
|
90
|
+
def extract_subdomain(host)
|
91
|
+
return nil if host.nil? || host.empty?
|
92
|
+
|
93
|
+
# Handle different host formats:
|
94
|
+
# - subdomain.domain.com -> subdomain
|
95
|
+
# - subdomain.domain.co.uk -> subdomain
|
96
|
+
# - domain.com -> nil (no subdomain)
|
97
|
+
# - localhost:3000 -> nil (no subdomain)
|
98
|
+
|
99
|
+
parts = host.split('.')
|
100
|
+
|
101
|
+
# Need at least 3 parts for subdomain (subdomain.domain.tld)
|
102
|
+
# or 4 parts for country domains (subdomain.domain.co.uk)
|
103
|
+
return nil if parts.length < 3
|
104
|
+
|
105
|
+
# Return first part as subdomain
|
106
|
+
parts.first
|
107
|
+
end
|
108
|
+
|
109
|
+
def extract_company_slug_from_path(path)
|
110
|
+
return nil if path.nil? || path.empty?
|
111
|
+
|
112
|
+
# Use configured pattern to extract company slug
|
113
|
+
match = @config.company_slug_pattern.match(path)
|
114
|
+
return nil unless match && match[1]
|
115
|
+
|
116
|
+
# Return captured group (company slug)
|
117
|
+
match[1]
|
118
|
+
end
|
119
|
+
|
120
|
+
def subdomains_match?(first_subdomain, second_subdomain)
|
121
|
+
# Handle nil cases
|
122
|
+
return true if first_subdomain.nil? && second_subdomain.nil?
|
123
|
+
return false if first_subdomain.nil? || second_subdomain.nil?
|
124
|
+
|
125
|
+
# Case-insensitive comparison
|
126
|
+
first_subdomain.downcase == second_subdomain.downcase
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RackJwtAegis
|
4
|
+
class RbacManager
|
5
|
+
CACHE_TTL = 300 # 5 minutes default cache TTL
|
6
|
+
LAST_UPDATE_KEY = 'last-update'
|
7
|
+
|
8
|
+
def initialize(config)
|
9
|
+
@config = config
|
10
|
+
setup_cache_adapters
|
11
|
+
end
|
12
|
+
|
13
|
+
def authorize(request, payload)
|
14
|
+
user_id = payload[@config.payload_key(:user_id).to_s]
|
15
|
+
raise AuthorizationError, 'User ID missing from JWT payload' if user_id.nil?
|
16
|
+
|
17
|
+
# Build permission key
|
18
|
+
permission_key = build_permission_key(user_id, request)
|
19
|
+
|
20
|
+
# Check cached permission first (if middleware can write to cache)
|
21
|
+
if @permission_cache && @config.cache_write_enabled?
|
22
|
+
cached_permission = check_cached_permission(permission_key)
|
23
|
+
return if cached_permission == true
|
24
|
+
|
25
|
+
raise AuthorizationError, 'Access denied - cached permission' if cached_permission == false
|
26
|
+
end
|
27
|
+
|
28
|
+
# Permission not cached or cache miss - check RBAC store
|
29
|
+
has_permission = check_rbac_permission(user_id, request)
|
30
|
+
|
31
|
+
# Cache the result if middleware has write access
|
32
|
+
cache_permission_result(permission_key, has_permission) if @permission_cache && @config.cache_write_enabled?
|
33
|
+
|
34
|
+
return if has_permission
|
35
|
+
|
36
|
+
raise AuthorizationError, 'Access denied - insufficient permissions'
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def setup_cache_adapters
|
42
|
+
if @config.cache_write_enabled? && @config.cache_store
|
43
|
+
# Shared cache mode - both RBAC and permission cache use same store
|
44
|
+
@rbac_cache = CacheAdapter.build(@config.cache_store, @config.cache_options || {})
|
45
|
+
@permission_cache = @rbac_cache
|
46
|
+
else
|
47
|
+
# Separate cache mode - different stores for RBAC and permissions
|
48
|
+
if @config.rbac_cache_store
|
49
|
+
@rbac_cache = CacheAdapter.build(@config.rbac_cache_store, @config.rbac_cache_options || {})
|
50
|
+
end
|
51
|
+
|
52
|
+
if @config.permission_cache_store
|
53
|
+
@permission_cache = CacheAdapter.build(@config.permission_cache_store, @config.permission_cache_options || {})
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Ensure we have at least RBAC cache for permission lookups
|
58
|
+
return if @rbac_cache
|
59
|
+
|
60
|
+
raise ConfigurationError, 'RBAC cache store not configured'
|
61
|
+
end
|
62
|
+
|
63
|
+
def build_permission_key(user_id, request)
|
64
|
+
"#{user_id}:#{request.host}:#{request.path}:#{request.request_method}"
|
65
|
+
end
|
66
|
+
|
67
|
+
def check_cached_permission(permission_key)
|
68
|
+
return nil unless @permission_cache
|
69
|
+
|
70
|
+
begin
|
71
|
+
# Get cached permission entry
|
72
|
+
cached_entry = @permission_cache.read(permission_key)
|
73
|
+
return nil if cached_entry.nil?
|
74
|
+
|
75
|
+
# Check if cached entry is still valid based on last-update timestamp
|
76
|
+
if cached_entry.is_a?(Hash) && cached_entry['timestamp'] && cached_entry['permission']
|
77
|
+
last_update_time = last_update_timestamp
|
78
|
+
|
79
|
+
return cached_entry['permission'] if last_update_time && cached_entry['timestamp'] >= last_update_time
|
80
|
+
|
81
|
+
# Cached entry is stale, remove it
|
82
|
+
@permission_cache.delete(permission_key)
|
83
|
+
return nil
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
# Invalid cached entry format
|
88
|
+
@permission_cache.delete(permission_key)
|
89
|
+
nil
|
90
|
+
rescue CacheError => e
|
91
|
+
# Log cache error but don't fail the request
|
92
|
+
warn "RbacManager cache read error: #{e.message}" if @config.debug_mode?
|
93
|
+
nil
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def check_rbac_permission(user_id, request)
|
98
|
+
# Build RBAC lookup key
|
99
|
+
rbac_key = build_rbac_key(user_id, request.host, request.path, request.request_method)
|
100
|
+
|
101
|
+
# Check RBAC cache store for permission
|
102
|
+
permission_data = @rbac_cache.read(rbac_key)
|
103
|
+
|
104
|
+
if permission_data.nil?
|
105
|
+
# No explicit permission found - default to deny
|
106
|
+
false
|
107
|
+
else
|
108
|
+
# Permission data found - check if it grants access
|
109
|
+
case permission_data
|
110
|
+
when true, 'true', 1, '1'
|
111
|
+
true
|
112
|
+
when false, 'false', 0, '0'
|
113
|
+
false
|
114
|
+
else
|
115
|
+
# Complex permission data - delegate to custom logic if available
|
116
|
+
evaluate_complex_permission?(permission_data, user_id, request)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
rescue CacheError => e
|
120
|
+
# Cache error - fail secure (deny access)
|
121
|
+
warn "RbacManager RBAC cache error: #{e.message}" if @config.debug_mode?
|
122
|
+
false
|
123
|
+
end
|
124
|
+
|
125
|
+
def cache_permission_result(permission_key, has_permission)
|
126
|
+
return unless @permission_cache
|
127
|
+
|
128
|
+
begin
|
129
|
+
current_time = Time.now.to_i
|
130
|
+
cache_entry = {
|
131
|
+
'permission' => has_permission,
|
132
|
+
'timestamp' => current_time,
|
133
|
+
}
|
134
|
+
|
135
|
+
@permission_cache.write(permission_key, cache_entry, expires_in: CACHE_TTL)
|
136
|
+
rescue CacheError => e
|
137
|
+
# Log cache error but don't fail the request
|
138
|
+
warn "RbacManager permission cache write error: #{e.message}" if @config.debug_mode?
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def last_update_timestamp
|
143
|
+
@rbac_cache.read(LAST_UPDATE_KEY)
|
144
|
+
rescue CacheError => e
|
145
|
+
warn "RbacManager last-update read error: #{e.message}" if @config.debug_mode?
|
146
|
+
nil
|
147
|
+
end
|
148
|
+
|
149
|
+
def build_rbac_key(user_id, host, path, method)
|
150
|
+
# Standard RBAC key format as defined in architecture
|
151
|
+
"#{user_id}:#{host}:#{path}:#{method}"
|
152
|
+
end
|
153
|
+
|
154
|
+
def evaluate_complex_permission?(permission_data, user_id, request)
|
155
|
+
# Handle complex permission data structures
|
156
|
+
case permission_data
|
157
|
+
when Hash
|
158
|
+
# Permission data is a hash - could contain role-based rules
|
159
|
+
evaluate_hash_permission?(permission_data, user_id, request)
|
160
|
+
when Array
|
161
|
+
# Permission data is an array - could be list of allowed actions
|
162
|
+
evaluate_array_permission?(permission_data, request.request_method)
|
163
|
+
else
|
164
|
+
# Unknown format - default to deny
|
165
|
+
false
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def evaluate_hash_permission?(permission_hash, _user_id, request)
|
170
|
+
# Example: {"allowed_methods": ["GET", "POST"], "roles": ["admin"]}
|
171
|
+
|
172
|
+
# Check allowed methods
|
173
|
+
if permission_hash['allowed_methods']
|
174
|
+
allowed_methods = Array(permission_hash['allowed_methods'])
|
175
|
+
return allowed_methods.include?(request.request_method)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Check roles (would need role information from JWT payload)
|
179
|
+
if permission_hash['roles']
|
180
|
+
# This would require additional JWT payload inspection
|
181
|
+
# For now, default to allowing if roles are specified
|
182
|
+
return true
|
183
|
+
end
|
184
|
+
|
185
|
+
# Check boolean permission field
|
186
|
+
return !!permission_hash['allowed'] if permission_hash.key?('allowed')
|
187
|
+
|
188
|
+
# Default deny for unknown hash structure
|
189
|
+
false
|
190
|
+
end
|
191
|
+
|
192
|
+
def evaluate_array_permission?(permission_array, request_method)
|
193
|
+
# Array of allowed HTTP methods
|
194
|
+
permission_array.include?(request_method)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RackJwtAegis
|
4
|
+
class RequestContext
|
5
|
+
# Standard environment keys for JWT data
|
6
|
+
JWT_PAYLOAD_KEY = 'rack_jwt_aegis.payload'
|
7
|
+
USER_ID_KEY = 'rack_jwt_aegis.user_id'
|
8
|
+
COMPANY_GROUP_ID_KEY = 'rack_jwt_aegis.company_group_id'
|
9
|
+
COMPANY_GROUP_DOMAIN_KEY = 'rack_jwt_aegis.company_group_domain'
|
10
|
+
COMPANY_SLUGS_KEY = 'rack_jwt_aegis.company_slugs'
|
11
|
+
AUTHENTICATED_KEY = 'rack_jwt_aegis.authenticated'
|
12
|
+
|
13
|
+
def initialize(config)
|
14
|
+
@config = config
|
15
|
+
end
|
16
|
+
|
17
|
+
def set_context(env, payload)
|
18
|
+
# Set the full payload
|
19
|
+
env[JWT_PAYLOAD_KEY] = payload
|
20
|
+
|
21
|
+
# Set authentication flag
|
22
|
+
env[AUTHENTICATED_KEY] = true
|
23
|
+
|
24
|
+
# Extract and set commonly used values for easy access
|
25
|
+
set_user_context(env, payload)
|
26
|
+
set_tenant_context(env, payload)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Class methods for easy access from application code
|
30
|
+
def self.authenticated?(env)
|
31
|
+
!!env[AUTHENTICATED_KEY]
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.payload(env)
|
35
|
+
env[JWT_PAYLOAD_KEY]
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.user_id(env)
|
39
|
+
env[USER_ID_KEY]
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.company_group_id(env)
|
43
|
+
env[COMPANY_GROUP_ID_KEY]
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.company_group_domain(env)
|
47
|
+
env[COMPANY_GROUP_DOMAIN_KEY]
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.company_slugs(env)
|
51
|
+
env[COMPANY_SLUGS_KEY] || []
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.current_user_id(request)
|
55
|
+
user_id(request.env)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.current_company_group_id(request)
|
59
|
+
company_group_id(request.env)
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.has_company_access?(env, company_slug)
|
63
|
+
company_slugs(env).include?(company_slug)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def set_user_context(env, payload)
|
69
|
+
user_id_key = @config.payload_key(:user_id).to_s
|
70
|
+
user_id = payload[user_id_key]
|
71
|
+
|
72
|
+
env[USER_ID_KEY] = user_id
|
73
|
+
end
|
74
|
+
|
75
|
+
def set_tenant_context(env, payload)
|
76
|
+
# Set company group information
|
77
|
+
if @config.validate_subdomain? || @config.payload_mapping.key?(:company_group_id)
|
78
|
+
company_group_id_key = @config.payload_key(:company_group_id).to_s
|
79
|
+
company_group_id = payload[company_group_id_key]
|
80
|
+
env[COMPANY_GROUP_ID_KEY] = company_group_id
|
81
|
+
end
|
82
|
+
|
83
|
+
if @config.validate_subdomain?
|
84
|
+
company_domain_key = @config.payload_key(:company_group_domain).to_s
|
85
|
+
company_domain = payload[company_domain_key]
|
86
|
+
env[COMPANY_GROUP_DOMAIN_KEY] = company_domain
|
87
|
+
end
|
88
|
+
|
89
|
+
# Set company slugs for sub-level tenant access
|
90
|
+
return unless @config.validate_company_slug? || @config.payload_mapping.key?(:company_slugs)
|
91
|
+
|
92
|
+
company_slugs_key = @config.payload_key(:company_slugs).to_s
|
93
|
+
company_slugs = payload[company_slugs_key]
|
94
|
+
|
95
|
+
# Ensure it's an array
|
96
|
+
company_slugs = Array(company_slugs) if company_slugs
|
97
|
+
env[COMPANY_SLUGS_KEY] = company_slugs || []
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module RackJwtAegis
|
6
|
+
class ResponseBuilder
|
7
|
+
def initialize(config)
|
8
|
+
@config = config
|
9
|
+
end
|
10
|
+
|
11
|
+
def unauthorized_response(message = nil)
|
12
|
+
error_response(
|
13
|
+
message || @config.unauthorized_response[:error] || 'Authentication required',
|
14
|
+
401,
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def forbidden_response(message = nil)
|
19
|
+
error_response(
|
20
|
+
message || @config.forbidden_response[:error] || 'Access denied',
|
21
|
+
403,
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def error_response(message, status_code)
|
26
|
+
response_body = build_error_body(message, status_code)
|
27
|
+
|
28
|
+
[
|
29
|
+
status_code,
|
30
|
+
{
|
31
|
+
'Content-Type' => 'application/json',
|
32
|
+
'Content-Length' => response_body.bytesize.to_s,
|
33
|
+
'Cache-Control' => 'no-cache, no-store, must-revalidate',
|
34
|
+
'Pragma' => 'no-cache',
|
35
|
+
'Expires' => '0',
|
36
|
+
},
|
37
|
+
[response_body],
|
38
|
+
]
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def build_error_body(message, status_code)
|
44
|
+
error_data = {
|
45
|
+
error: message,
|
46
|
+
status: status_code,
|
47
|
+
timestamp: Time.now.iso8601,
|
48
|
+
}
|
49
|
+
|
50
|
+
# Add additional context in debug mode
|
51
|
+
if @config.debug_mode?
|
52
|
+
error_data[:middleware] = 'rack_jwt_aegis'
|
53
|
+
error_data[:version] = RackJwtAegis::VERSION
|
54
|
+
end
|
55
|
+
|
56
|
+
JSON.generate(error_data)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'rack_jwt_aegis/version'
|
4
|
+
require_relative 'rack_jwt_aegis/configuration'
|
5
|
+
require_relative 'rack_jwt_aegis/middleware'
|
6
|
+
require_relative 'rack_jwt_aegis/jwt_validator'
|
7
|
+
require_relative 'rack_jwt_aegis/multi_tenant_validator'
|
8
|
+
require_relative 'rack_jwt_aegis/rbac_manager'
|
9
|
+
require_relative 'rack_jwt_aegis/cache_adapter'
|
10
|
+
require_relative 'rack_jwt_aegis/request_context'
|
11
|
+
require_relative 'rack_jwt_aegis/response_builder'
|
12
|
+
|
13
|
+
module RackJwtAegis
|
14
|
+
class Error < StandardError; end
|
15
|
+
class ConfigurationError < Error; end
|
16
|
+
class AuthenticationError < Error; end
|
17
|
+
class AuthorizationError < Error; end
|
18
|
+
class CacheError < Error; end
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack_jwt_aegis
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ken C. Demanawa
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-08-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: jwt
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.10'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rack
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.2'
|
41
|
+
description: JWT authentication middleware with multi-tenant support, company validation,
|
42
|
+
and subdomain isolation.
|
43
|
+
email:
|
44
|
+
- kenneth.c.demanawa@gmail.com
|
45
|
+
executables: []
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- ".rubocop.yml"
|
50
|
+
- CODE_OF_CONDUCT.md
|
51
|
+
- LICENSE.txt
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- examples/basic_usage.rb
|
55
|
+
- lib/rack_jwt_aegis.rb
|
56
|
+
- lib/rack_jwt_aegis/cache_adapter.rb
|
57
|
+
- lib/rack_jwt_aegis/configuration.rb
|
58
|
+
- lib/rack_jwt_aegis/jwt_validator.rb
|
59
|
+
- lib/rack_jwt_aegis/middleware.rb
|
60
|
+
- lib/rack_jwt_aegis/multi_tenant_validator.rb
|
61
|
+
- lib/rack_jwt_aegis/rbac_manager.rb
|
62
|
+
- lib/rack_jwt_aegis/request_context.rb
|
63
|
+
- lib/rack_jwt_aegis/response_builder.rb
|
64
|
+
- lib/rack_jwt_aegis/version.rb
|
65
|
+
- sig/rack_jwt_bastion.rbs
|
66
|
+
homepage: https://github.com/kanutocd/rack_jwt_aegis
|
67
|
+
licenses:
|
68
|
+
- MIT
|
69
|
+
metadata:
|
70
|
+
allowed_push_host: https://rubygems.org
|
71
|
+
homepage_uri: https://github.com/kanutocd/rack_jwt_aegis
|
72
|
+
source_code_uri: https://github.com/kanutocd/rack_jwt_aegis
|
73
|
+
rubygems_mfa_required: 'true'
|
74
|
+
post_install_message:
|
75
|
+
rdoc_options: []
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 3.2.0
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
requirements: []
|
89
|
+
rubygems_version: 3.4.19
|
90
|
+
signing_key:
|
91
|
+
specification_version: 4
|
92
|
+
summary: JWT authentication middleware for multi-tenant Rack applications
|
93
|
+
test_files: []
|