rapitapir 0.1.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/.rspec +3 -0
- data/.rubocop.yml +57 -0
- data/CHANGELOG.md +94 -0
- data/CLEANUP_SUMMARY.md +155 -0
- data/CONTRIBUTING.md +280 -0
- data/LICENSE +21 -0
- data/README.md +485 -0
- data/debug_hash.rb +20 -0
- data/docs/EXTENSION_COMPARISON.md +388 -0
- data/docs/SINATRA_EXTENSION.md +467 -0
- data/docs/archive/PHASE_1_2_COMPLETE.md +77 -0
- data/docs/archive/PHASE_1_3_COMPLETE.md +152 -0
- data/docs/archive/PHASE_2_1_OBSERVABILITY_COMPLETED.md +203 -0
- data/docs/archive/PHASE_2_SUMMARY.md +209 -0
- data/docs/archive/REFACTORING_SUMMARY.md +184 -0
- data/docs/archive/phase_1_3_plan.md +136 -0
- data/docs/archive/sinatra_extension_summary.md +188 -0
- data/docs/archive/sinatra_working_solution.md +113 -0
- data/docs/archive/typescript-client-generator-summary.md +259 -0
- data/docs/auto-derivation.md +146 -0
- data/docs/blueprint.md +1091 -0
- data/docs/endpoint-definition.md +211 -0
- data/docs/github_pages_fix.md +52 -0
- data/docs/github_pages_setup.md +49 -0
- data/docs/implementation-status.md +357 -0
- data/docs/observability.md +647 -0
- data/docs/phase3-plan.md +108 -0
- data/docs/sinatra_rapitapir.md +87 -0
- data/docs/type_shortcuts.md +146 -0
- data/examples/README_ENTERPRISE.md +202 -0
- data/examples/authentication_example.rb +192 -0
- data/examples/auto_derivation_ruby_friendly.rb +163 -0
- data/examples/cli/user_api_endpoints.rb +56 -0
- data/examples/client/typescript_client_example.rb +102 -0
- data/examples/client/user-api-client.ts +193 -0
- data/examples/demo_api.rb +41 -0
- data/examples/docs/documentation_example.rb +112 -0
- data/examples/docs/user-api-docs.html +789 -0
- data/examples/docs/user-api-docs.md +403 -0
- data/examples/enhanced_auto_derivation_test.rb +83 -0
- data/examples/enterprise_extension_demo.rb +417 -0
- data/examples/enterprise_rapitapir_api.rb +662 -0
- data/examples/getting_started_extension.rb +218 -0
- data/examples/hello_world.rb +74 -0
- data/examples/oauth2/.env.example +19 -0
- data/examples/oauth2/README.md +205 -0
- data/examples/oauth2/generic_oauth2_api.rb +226 -0
- data/examples/oauth2/get_token.rb +72 -0
- data/examples/oauth2/songs_api_with_auth0.rb +320 -0
- data/examples/oauth2/test_api.sh +16 -0
- data/examples/oauth2/test_songs_api.sh +110 -0
- data/examples/observability/.env.example +35 -0
- data/examples/observability/README.md +230 -0
- data/examples/observability/README_HONEYCOMB.md +332 -0
- data/examples/observability/advanced_setup.rb +384 -0
- data/examples/observability/basic_setup.rb +192 -0
- data/examples/observability/complete_test.rb +121 -0
- data/examples/observability/honeycomb_example.rb +523 -0
- data/examples/observability/honeycomb_rapitapir_clean.rb +488 -0
- data/examples/observability/honeycomb_rapitapir_example.rb +523 -0
- data/examples/observability/honeycomb_working_example.rb +489 -0
- data/examples/observability/quick_test.rb +78 -0
- data/examples/observability/simple_test.rb +14 -0
- data/examples/observability/test_honeycomb_demo.rb +354 -0
- data/examples/observability/test_live_honeycomb.rb +111 -0
- data/examples/observability/test_validation.rb +78 -0
- data/examples/observability/test_working_validation.rb +66 -0
- data/examples/openapi/user_api_schema.rb +132 -0
- data/examples/production_ready_example.rb +105 -0
- data/examples/rails/users_controller.rb +146 -0
- data/examples/readme/basic_sinatra_example.rb +128 -0
- data/examples/server/user_api.rb +179 -0
- data/examples/simple_auto_derivation_demo.rb +44 -0
- data/examples/simple_demo_api.rb +18 -0
- data/examples/sinatra/user_app.rb +127 -0
- data/examples/t_shortcut_demo.rb +59 -0
- data/examples/user_api.rb +190 -0
- data/examples/working_getting_started.rb +184 -0
- data/examples/working_simple_example.rb +195 -0
- data/lib/rapitapir/auth/configuration.rb +129 -0
- data/lib/rapitapir/auth/context.rb +122 -0
- data/lib/rapitapir/auth/errors.rb +104 -0
- data/lib/rapitapir/auth/middleware.rb +324 -0
- data/lib/rapitapir/auth/oauth2.rb +350 -0
- data/lib/rapitapir/auth/schemes.rb +420 -0
- data/lib/rapitapir/auth.rb +113 -0
- data/lib/rapitapir/cli/command.rb +535 -0
- data/lib/rapitapir/cli/server.rb +243 -0
- data/lib/rapitapir/cli/validator.rb +373 -0
- data/lib/rapitapir/client/generator_base.rb +272 -0
- data/lib/rapitapir/client/typescript_generator.rb +350 -0
- data/lib/rapitapir/core/endpoint.rb +158 -0
- data/lib/rapitapir/core/enhanced_endpoint.rb +235 -0
- data/lib/rapitapir/core/input.rb +182 -0
- data/lib/rapitapir/core/output.rb +164 -0
- data/lib/rapitapir/core/request.rb +19 -0
- data/lib/rapitapir/core/response.rb +17 -0
- data/lib/rapitapir/docs/html_generator.rb +780 -0
- data/lib/rapitapir/docs/markdown_generator.rb +464 -0
- data/lib/rapitapir/dsl/endpoint_dsl.rb +116 -0
- data/lib/rapitapir/dsl/enhanced_endpoint_dsl.rb +62 -0
- data/lib/rapitapir/dsl/enhanced_input.rb +73 -0
- data/lib/rapitapir/dsl/enhanced_output.rb +63 -0
- data/lib/rapitapir/dsl/enhanced_structures.rb +393 -0
- data/lib/rapitapir/dsl/fluent_dsl.rb +72 -0
- data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +316 -0
- data/lib/rapitapir/dsl/http_verbs.rb +77 -0
- data/lib/rapitapir/dsl/input_methods.rb +47 -0
- data/lib/rapitapir/dsl/observability_methods.rb +81 -0
- data/lib/rapitapir/dsl/output_methods.rb +43 -0
- data/lib/rapitapir/dsl/type_resolution.rb +43 -0
- data/lib/rapitapir/observability/configuration.rb +108 -0
- data/lib/rapitapir/observability/health_check.rb +236 -0
- data/lib/rapitapir/observability/logging.rb +270 -0
- data/lib/rapitapir/observability/metrics.rb +203 -0
- data/lib/rapitapir/observability/middleware.rb +243 -0
- data/lib/rapitapir/observability/tracing.rb +143 -0
- data/lib/rapitapir/observability.rb +28 -0
- data/lib/rapitapir/openapi/schema_generator.rb +403 -0
- data/lib/rapitapir/schema.rb +136 -0
- data/lib/rapitapir/server/enhanced_rack_adapter.rb +379 -0
- data/lib/rapitapir/server/middleware.rb +120 -0
- data/lib/rapitapir/server/path_matcher.rb +45 -0
- data/lib/rapitapir/server/rack_adapter.rb +215 -0
- data/lib/rapitapir/server/rails_adapter.rb +17 -0
- data/lib/rapitapir/server/rails_adapter_class.rb +53 -0
- data/lib/rapitapir/server/rails_controller.rb +72 -0
- data/lib/rapitapir/server/rails_input_processor.rb +73 -0
- data/lib/rapitapir/server/rails_response_handler.rb +29 -0
- data/lib/rapitapir/server/sinatra_adapter.rb +200 -0
- data/lib/rapitapir/server/sinatra_integration.rb +93 -0
- data/lib/rapitapir/sinatra/configuration.rb +91 -0
- data/lib/rapitapir/sinatra/extension.rb +214 -0
- data/lib/rapitapir/sinatra/oauth2_helpers.rb +236 -0
- data/lib/rapitapir/sinatra/resource_builder.rb +152 -0
- data/lib/rapitapir/sinatra/swagger_ui_generator.rb +166 -0
- data/lib/rapitapir/sinatra_rapitapir.rb +40 -0
- data/lib/rapitapir/types/array.rb +163 -0
- data/lib/rapitapir/types/auto_derivation.rb +265 -0
- data/lib/rapitapir/types/base.rb +146 -0
- data/lib/rapitapir/types/boolean.rb +46 -0
- data/lib/rapitapir/types/date.rb +92 -0
- data/lib/rapitapir/types/datetime.rb +98 -0
- data/lib/rapitapir/types/email.rb +32 -0
- data/lib/rapitapir/types/float.rb +134 -0
- data/lib/rapitapir/types/hash.rb +161 -0
- data/lib/rapitapir/types/integer.rb +143 -0
- data/lib/rapitapir/types/object.rb +156 -0
- data/lib/rapitapir/types/optional.rb +65 -0
- data/lib/rapitapir/types/string.rb +185 -0
- data/lib/rapitapir/types/uuid.rb +32 -0
- data/lib/rapitapir/types.rb +155 -0
- data/lib/rapitapir/version.rb +5 -0
- data/lib/rapitapir.rb +173 -0
- data/rapitapir.gemspec +66 -0
- metadata +387 -0
@@ -0,0 +1,420 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'json'
|
5
|
+
require 'openssl'
|
6
|
+
require_relative 'configuration'
|
7
|
+
require_relative 'errors'
|
8
|
+
require_relative 'context'
|
9
|
+
|
10
|
+
module RapiTapir
|
11
|
+
module Auth
|
12
|
+
module Schemes
|
13
|
+
# Base class for all authentication schemes
|
14
|
+
# Defines the interface that all authentication schemes must implement
|
15
|
+
class Base
|
16
|
+
attr_reader :name, :config
|
17
|
+
|
18
|
+
def initialize(name, config = {})
|
19
|
+
@name = name
|
20
|
+
@config = config
|
21
|
+
end
|
22
|
+
|
23
|
+
def authenticate(request)
|
24
|
+
raise NotImplementedError, 'Subclasses must implement #authenticate'
|
25
|
+
end
|
26
|
+
|
27
|
+
def challenge
|
28
|
+
raise NotImplementedError, 'Subclasses must implement #challenge'
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
def create_context(user: nil, scopes: [], token: nil, metadata: {})
|
34
|
+
Context.new(
|
35
|
+
user: user,
|
36
|
+
scopes: scopes,
|
37
|
+
token: token,
|
38
|
+
metadata: metadata.merge(scheme: @name)
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Bearer token authentication scheme
|
44
|
+
# Authenticates using tokens in the Authorization header
|
45
|
+
class BearerToken < Base
|
46
|
+
def initialize(name, config = {})
|
47
|
+
super
|
48
|
+
@token_validator = config[:token_validator] || method(:default_token_validator)
|
49
|
+
@realm = config[:realm] || 'API'
|
50
|
+
end
|
51
|
+
|
52
|
+
def authenticate(request)
|
53
|
+
auth_header = request.env['HTTP_AUTHORIZATION']
|
54
|
+
return nil unless auth_header
|
55
|
+
|
56
|
+
token = extract_bearer_token(auth_header)
|
57
|
+
return nil unless token
|
58
|
+
|
59
|
+
user_data = @token_validator.call(token)
|
60
|
+
return nil unless user_data
|
61
|
+
|
62
|
+
create_context(
|
63
|
+
user: user_data[:user],
|
64
|
+
scopes: user_data[:scopes] || [],
|
65
|
+
token: token,
|
66
|
+
metadata: { token_type: 'bearer' }
|
67
|
+
)
|
68
|
+
rescue InvalidTokenError
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
|
72
|
+
def challenge
|
73
|
+
"Bearer realm=\"#{@realm}\""
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def extract_bearer_token(auth_header)
|
79
|
+
match = auth_header.match(/\ABearer\s+(.+)\z/i)
|
80
|
+
match ? match[1] : nil
|
81
|
+
end
|
82
|
+
|
83
|
+
def default_token_validator(token)
|
84
|
+
# Default implementation - should be overridden
|
85
|
+
return nil if token.nil? || token.empty?
|
86
|
+
|
87
|
+
{
|
88
|
+
user: { id: 'default_user', name: 'Default User' },
|
89
|
+
scopes: ['read']
|
90
|
+
}
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# API key authentication scheme
|
95
|
+
# Authenticates using API keys in headers or query parameters
|
96
|
+
class ApiKey < Base
|
97
|
+
def initialize(name, config = {})
|
98
|
+
super
|
99
|
+
@key_validator = config[:key_validator] || method(:default_key_validator)
|
100
|
+
@header_name = config[:header_name] || 'X-API-Key'
|
101
|
+
@query_param = config[:query_param] || 'api_key'
|
102
|
+
@location = config[:location] || :header # :header, :query, or :both
|
103
|
+
end
|
104
|
+
|
105
|
+
def authenticate(request)
|
106
|
+
api_key = extract_api_key(request)
|
107
|
+
return nil unless api_key
|
108
|
+
|
109
|
+
user_data = @key_validator.call(api_key)
|
110
|
+
return nil unless user_data
|
111
|
+
|
112
|
+
create_context(
|
113
|
+
user: user_data[:user],
|
114
|
+
scopes: user_data[:scopes] || [],
|
115
|
+
token: api_key,
|
116
|
+
metadata: {
|
117
|
+
token_type: 'api_key',
|
118
|
+
location: @location
|
119
|
+
}
|
120
|
+
)
|
121
|
+
rescue InvalidTokenError
|
122
|
+
nil
|
123
|
+
end
|
124
|
+
|
125
|
+
def challenge
|
126
|
+
'ApiKey'
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def extract_api_key(request)
|
132
|
+
case @location
|
133
|
+
when :header
|
134
|
+
request.env["HTTP_#{@header_name.upcase.tr('-', '_')}"]
|
135
|
+
when :query
|
136
|
+
request.params[@query_param]
|
137
|
+
when :both
|
138
|
+
request.env["HTTP_#{@header_name.upcase.tr('-', '_')}"] ||
|
139
|
+
request.params[@query_param]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def default_key_validator(key)
|
144
|
+
# Default implementation - should be overridden
|
145
|
+
return nil if key.nil? || key.empty?
|
146
|
+
|
147
|
+
{
|
148
|
+
user: { id: 'api_user', name: 'API User' },
|
149
|
+
scopes: ['api']
|
150
|
+
}
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# HTTP Basic authentication scheme
|
155
|
+
# Authenticates using username and password in the Authorization header
|
156
|
+
class BasicAuth < Base
|
157
|
+
def initialize(name, config = {})
|
158
|
+
super
|
159
|
+
@credential_validator = config[:credential_validator] || method(:default_credential_validator)
|
160
|
+
@realm = config[:realm] || 'API'
|
161
|
+
end
|
162
|
+
|
163
|
+
def authenticate(request)
|
164
|
+
auth_header = request.env['HTTP_AUTHORIZATION']
|
165
|
+
return nil unless auth_header
|
166
|
+
|
167
|
+
credentials = extract_basic_credentials(auth_header)
|
168
|
+
return nil unless credentials
|
169
|
+
|
170
|
+
user_data = @credential_validator.call(credentials[:username], credentials[:password])
|
171
|
+
return nil unless user_data
|
172
|
+
|
173
|
+
create_context(
|
174
|
+
user: user_data[:user],
|
175
|
+
scopes: user_data[:scopes] || [],
|
176
|
+
metadata: {
|
177
|
+
token_type: 'basic',
|
178
|
+
username: credentials[:username]
|
179
|
+
}
|
180
|
+
)
|
181
|
+
rescue AuthenticationError
|
182
|
+
nil
|
183
|
+
end
|
184
|
+
|
185
|
+
def challenge
|
186
|
+
"Basic realm=\"#{@realm}\""
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
def extract_basic_credentials(auth_header)
|
192
|
+
match = auth_header.match(/\ABasic\s+(.+)\z/i)
|
193
|
+
return nil unless match
|
194
|
+
|
195
|
+
decoded = Base64.decode64(match[1])
|
196
|
+
username, password = decoded.split(':', 2)
|
197
|
+
|
198
|
+
return nil if username.nil? || password.nil?
|
199
|
+
|
200
|
+
{ username: username, password: password }
|
201
|
+
rescue ArgumentError
|
202
|
+
nil
|
203
|
+
end
|
204
|
+
|
205
|
+
def default_credential_validator(username, password)
|
206
|
+
# Default implementation - should be overridden
|
207
|
+
return nil if username.nil? || password.nil? || username.empty? || password.empty?
|
208
|
+
|
209
|
+
{
|
210
|
+
user: { id: username, name: username.capitalize },
|
211
|
+
scopes: ['basic']
|
212
|
+
}
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# OAuth2 authentication scheme
|
217
|
+
# Authenticates using OAuth2 tokens with optional token introspection
|
218
|
+
class OAuth2 < Base
|
219
|
+
def initialize(name, config = {})
|
220
|
+
super
|
221
|
+
@token_validator = config[:token_validator] || method(:default_oauth2_validator)
|
222
|
+
@introspection_endpoint = config[:introspection_endpoint]
|
223
|
+
@client_id = config[:client_id]
|
224
|
+
@client_secret = config[:client_secret]
|
225
|
+
@realm = config[:realm] || 'API'
|
226
|
+
end
|
227
|
+
|
228
|
+
def authenticate(request)
|
229
|
+
auth_header = request.env['HTTP_AUTHORIZATION']
|
230
|
+
return nil unless auth_header
|
231
|
+
|
232
|
+
token = extract_bearer_token(auth_header)
|
233
|
+
return nil unless token
|
234
|
+
|
235
|
+
user_data = validate_oauth2_token(token)
|
236
|
+
return nil unless user_data
|
237
|
+
|
238
|
+
create_context(
|
239
|
+
user: user_data[:user],
|
240
|
+
scopes: user_data[:scopes] || [],
|
241
|
+
token: token,
|
242
|
+
metadata: {
|
243
|
+
token_type: 'oauth2',
|
244
|
+
client_id: user_data[:client_id],
|
245
|
+
expires_at: user_data[:expires_at]
|
246
|
+
}
|
247
|
+
)
|
248
|
+
rescue InvalidTokenError, TokenExpiredError
|
249
|
+
nil
|
250
|
+
end
|
251
|
+
|
252
|
+
def challenge
|
253
|
+
"Bearer realm=\"#{@realm}\""
|
254
|
+
end
|
255
|
+
|
256
|
+
private
|
257
|
+
|
258
|
+
def extract_bearer_token(auth_header)
|
259
|
+
match = auth_header.match(/\ABearer\s+(.+)\z/i)
|
260
|
+
match ? match[1] : nil
|
261
|
+
end
|
262
|
+
|
263
|
+
def validate_oauth2_token(token)
|
264
|
+
if @introspection_endpoint
|
265
|
+
introspect_token(token)
|
266
|
+
else
|
267
|
+
@token_validator.call(token)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def introspect_token(token)
|
272
|
+
# OAuth2 token introspection (RFC 7662)
|
273
|
+
# This would typically make an HTTP request to the introspection endpoint
|
274
|
+
# For now, we'll use the configured validator
|
275
|
+
@token_validator.call(token)
|
276
|
+
end
|
277
|
+
|
278
|
+
def default_oauth2_validator(token)
|
279
|
+
# Default implementation - should be overridden
|
280
|
+
return nil if token.nil? || token.empty?
|
281
|
+
|
282
|
+
{
|
283
|
+
user: { id: 'oauth_user', name: 'OAuth User' },
|
284
|
+
scopes: %w[read write],
|
285
|
+
client_id: 'default_client',
|
286
|
+
expires_at: Time.now + 3600
|
287
|
+
}
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# JWT (JSON Web Token) authentication scheme
|
292
|
+
# Authenticates using signed JWT tokens with verification
|
293
|
+
class JWT < Base
|
294
|
+
def initialize(name, config = {})
|
295
|
+
super
|
296
|
+
@secret = config[:secret] || raise(ArgumentError, 'JWT secret is required')
|
297
|
+
@algorithm = config[:algorithm] || 'HS256'
|
298
|
+
@verify_expiration = config.fetch(:verify_expiration, true)
|
299
|
+
@verify_issuer = config[:verify_issuer]
|
300
|
+
@verify_audience = config[:verify_audience]
|
301
|
+
@realm = config[:realm] || 'API'
|
302
|
+
end
|
303
|
+
|
304
|
+
def authenticate(request)
|
305
|
+
auth_header = request.env['HTTP_AUTHORIZATION']
|
306
|
+
return nil unless auth_header
|
307
|
+
|
308
|
+
token = extract_bearer_token(auth_header)
|
309
|
+
return nil unless token
|
310
|
+
|
311
|
+
payload = decode_jwt_token(token)
|
312
|
+
return nil unless payload
|
313
|
+
|
314
|
+
create_context(
|
315
|
+
user: extract_user_from_payload(payload),
|
316
|
+
scopes: extract_scopes_from_payload(payload),
|
317
|
+
token: token,
|
318
|
+
metadata: {
|
319
|
+
token_type: 'jwt',
|
320
|
+
payload: payload
|
321
|
+
}
|
322
|
+
)
|
323
|
+
rescue InvalidTokenError, TokenExpiredError
|
324
|
+
nil
|
325
|
+
end
|
326
|
+
|
327
|
+
def challenge
|
328
|
+
"Bearer realm=\"#{@realm}\""
|
329
|
+
end
|
330
|
+
|
331
|
+
private
|
332
|
+
|
333
|
+
def extract_bearer_token(auth_header)
|
334
|
+
match = auth_header.match(/\ABearer\s+(.+)\z/i)
|
335
|
+
match ? match[1] : nil
|
336
|
+
end
|
337
|
+
|
338
|
+
def decode_jwt_token(token)
|
339
|
+
# This is a simplified JWT decoder
|
340
|
+
# In a real implementation, you'd use a library like ruby-jwt
|
341
|
+
parts = split_jwt_token(token)
|
342
|
+
return nil unless parts
|
343
|
+
|
344
|
+
begin
|
345
|
+
_, payload, signature = parse_jwt_parts(parts)
|
346
|
+
return nil unless valid_jwt_signature?(parts, signature)
|
347
|
+
return nil unless valid_jwt_claims?(payload)
|
348
|
+
|
349
|
+
payload
|
350
|
+
rescue JSON::ParserError, ArgumentError
|
351
|
+
nil
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
def split_jwt_token(token)
|
356
|
+
parts = token.split('.')
|
357
|
+
parts.length == 3 ? parts : nil
|
358
|
+
end
|
359
|
+
|
360
|
+
def parse_jwt_parts(parts)
|
361
|
+
header_b64 = add_base64_padding(parts[0])
|
362
|
+
payload_b64 = add_base64_padding(parts[1])
|
363
|
+
|
364
|
+
header = JSON.parse(Base64.urlsafe_decode64(header_b64))
|
365
|
+
payload = JSON.parse(Base64.urlsafe_decode64(payload_b64))
|
366
|
+
signature = parts[2]
|
367
|
+
|
368
|
+
[header, payload, signature]
|
369
|
+
end
|
370
|
+
|
371
|
+
def add_base64_padding(base64_string)
|
372
|
+
missing_padding = 4 - (base64_string.length % 4)
|
373
|
+
base64_string += '=' * missing_padding if missing_padding != 4
|
374
|
+
base64_string
|
375
|
+
end
|
376
|
+
|
377
|
+
def valid_jwt_signature?(parts, signature)
|
378
|
+
expected_signature = Base64.urlsafe_encode64(
|
379
|
+
OpenSSL::HMAC.digest('SHA256', @secret, "#{parts[0]}.#{parts[1]}")
|
380
|
+
).tr('=', '')
|
381
|
+
|
382
|
+
signature == expected_signature
|
383
|
+
end
|
384
|
+
|
385
|
+
def valid_jwt_claims?(payload)
|
386
|
+
return false if @verify_expiration && jwt_expired?(payload)
|
387
|
+
return false if @verify_issuer && payload['iss'] != @verify_issuer
|
388
|
+
return false if @verify_audience && payload['aud'] != @verify_audience
|
389
|
+
|
390
|
+
true
|
391
|
+
end
|
392
|
+
|
393
|
+
def jwt_expired?(payload)
|
394
|
+
payload['exp'] && (Time.at(payload['exp']) < Time.now)
|
395
|
+
end
|
396
|
+
|
397
|
+
def extract_user_from_payload(payload)
|
398
|
+
user_data = payload['user'] || payload['sub']
|
399
|
+
return { id: user_data, name: user_data } if user_data.is_a?(String)
|
400
|
+
|
401
|
+
user_data || { id: payload['sub'], name: payload['name'] || payload['sub'] }
|
402
|
+
end
|
403
|
+
|
404
|
+
def extract_scopes_from_payload(payload)
|
405
|
+
scopes = payload['scopes'] || payload['scope']
|
406
|
+
return [] unless scopes
|
407
|
+
|
408
|
+
case scopes
|
409
|
+
when Array
|
410
|
+
scopes
|
411
|
+
when String
|
412
|
+
scopes.split
|
413
|
+
else
|
414
|
+
[]
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'auth/configuration'
|
4
|
+
require_relative 'auth/errors'
|
5
|
+
require_relative 'auth/context'
|
6
|
+
require_relative 'auth/schemes'
|
7
|
+
require_relative 'auth/middleware'
|
8
|
+
require_relative 'auth/oauth2'
|
9
|
+
|
10
|
+
module RapiTapir
|
11
|
+
# Authentication and authorization module for RapiTapir
|
12
|
+
#
|
13
|
+
# Provides comprehensive authentication schemes including Bearer tokens, API keys,
|
14
|
+
# JWT, OAuth2, and basic authentication. Also includes authorization middleware,
|
15
|
+
# rate limiting, CORS support, and security features.
|
16
|
+
#
|
17
|
+
# @example Configure authentication
|
18
|
+
# RapiTapir::Auth.configure do |config|
|
19
|
+
# config.jwt_secret = 'your-secret'
|
20
|
+
# config.rate_limiting.requests_per_minute = 100
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# @example Create authentication schemes
|
24
|
+
# bearer_auth = RapiTapir::Auth.bearer_token(:bearer, { token_validator: proc { |token| ... } })
|
25
|
+
module Auth
|
26
|
+
class << self
|
27
|
+
attr_accessor :configuration
|
28
|
+
|
29
|
+
def configure
|
30
|
+
self.configuration ||= Configuration.new
|
31
|
+
yield(configuration) if block_given?
|
32
|
+
configuration
|
33
|
+
end
|
34
|
+
|
35
|
+
def config
|
36
|
+
self.configuration ||= Configuration.new
|
37
|
+
end
|
38
|
+
|
39
|
+
# DSL methods for creating authentication schemes
|
40
|
+
def bearer_token(name = :bearer, config = {})
|
41
|
+
Schemes::BearerToken.new(name, config)
|
42
|
+
end
|
43
|
+
|
44
|
+
def api_key(name = :api_key, config = {})
|
45
|
+
Schemes::ApiKey.new(name, config)
|
46
|
+
end
|
47
|
+
|
48
|
+
def basic_auth(name = :basic, config = {})
|
49
|
+
Schemes::BasicAuth.new(name, config)
|
50
|
+
end
|
51
|
+
|
52
|
+
def oauth2(name = :oauth2, config = {})
|
53
|
+
OAuth2::GenericScheme.new(name, config)
|
54
|
+
end
|
55
|
+
|
56
|
+
def oauth2_auth0(name = :oauth2_auth0, config = {})
|
57
|
+
OAuth2::Auth0Scheme.new(name, config)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Alias methods for consistency with Sinatra helpers
|
61
|
+
def auth0_oauth2(name = :oauth2, config = {})
|
62
|
+
OAuth2::Auth0Scheme.new(name, config)
|
63
|
+
end
|
64
|
+
|
65
|
+
def oauth2_introspection(name = :oauth2, config = {})
|
66
|
+
OAuth2::GenericScheme.new(name, config)
|
67
|
+
end
|
68
|
+
|
69
|
+
def jwt(name = :jwt, config = {})
|
70
|
+
Schemes::JWT.new(name, config)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Middleware factory methods
|
74
|
+
def authentication_middleware(auth_schemes = {})
|
75
|
+
Middleware::AuthenticationMiddleware.new(nil, auth_schemes)
|
76
|
+
end
|
77
|
+
|
78
|
+
def authorization_middleware(required_scopes: [], require_all: true)
|
79
|
+
Middleware::AuthorizationMiddleware.new(nil, required_scopes: required_scopes, require_all: require_all)
|
80
|
+
end
|
81
|
+
|
82
|
+
def rate_limiting_middleware(config = {})
|
83
|
+
Middleware::RateLimitingMiddleware.new(nil, config)
|
84
|
+
end
|
85
|
+
|
86
|
+
def cors_middleware(config = {})
|
87
|
+
Middleware::CorsMiddleware.new(nil, config)
|
88
|
+
end
|
89
|
+
|
90
|
+
def security_headers_middleware(config = {})
|
91
|
+
Middleware::SecurityHeadersMiddleware.new(nil, config)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Context access
|
95
|
+
def current_context
|
96
|
+
ContextStore.current
|
97
|
+
end
|
98
|
+
|
99
|
+
def current_user
|
100
|
+
current_context&.user
|
101
|
+
end
|
102
|
+
|
103
|
+
def authenticated?
|
104
|
+
current_context&.authenticated? || false
|
105
|
+
end
|
106
|
+
|
107
|
+
def scope?(scope)
|
108
|
+
current_context&.scope?(scope) || false
|
109
|
+
end
|
110
|
+
alias has_scope? scope?
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|