otto 2.0.0.pre8 → 2.0.0.pre10
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 +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/.github/workflows/claude-code-review.yml +1 -1
- data/.github/workflows/claude.yml +1 -1
- data/.github/workflows/code-smells.yml +2 -2
- data/CHANGELOG.rst +73 -35
- data/CLAUDE.md +44 -0
- data/Gemfile.lock +7 -7
- data/README.md +20 -0
- data/docs/.gitignore +2 -0
- data/docs/modern-authentication-authorization-landscape.md +558 -0
- data/docs/multi-strategy-authentication-design.md +1401 -0
- data/lib/otto/core/error_handler.rb +104 -10
- data/lib/otto/core/freezable.rb +0 -2
- data/lib/otto/core/helper_registry.rb +135 -0
- data/lib/otto/core/lifecycle_hooks.rb +63 -0
- data/lib/otto/core/middleware_management.rb +70 -0
- data/lib/otto/core/middleware_stack.rb +12 -8
- data/lib/otto/core/router.rb +25 -31
- data/lib/otto/core.rb +3 -0
- data/lib/otto/errors.rb +92 -0
- data/lib/otto/helpers.rb +2 -2
- data/lib/otto/locale/middleware.rb +1 -1
- data/lib/otto/mcp/core.rb +33 -0
- data/lib/otto/mcp/protocol.rb +1 -1
- data/lib/otto/mcp/rate_limiting.rb +6 -2
- data/lib/otto/mcp/schema_validation.rb +2 -2
- data/lib/otto/mcp.rb +1 -0
- data/lib/otto/privacy/core.rb +82 -0
- data/lib/otto/privacy.rb +1 -0
- data/lib/otto/{helpers/request.rb → request.rb} +17 -6
- data/lib/otto/{helpers/response.rb → response.rb} +20 -7
- data/lib/otto/response_handlers/json.rb +1 -3
- data/lib/otto/response_handlers/view.rb +1 -1
- data/lib/otto/route.rb +2 -4
- data/lib/otto/route_handlers/base.rb +88 -5
- data/lib/otto/route_handlers/class_method.rb +9 -67
- data/lib/otto/route_handlers/instance_method.rb +10 -57
- data/lib/otto/route_handlers/lambda.rb +2 -2
- data/lib/otto/route_handlers/logic_class.rb +85 -90
- data/lib/otto/security/authentication/auth_strategy.rb +2 -2
- data/lib/otto/security/authentication/strategies/api_key_strategy.rb +1 -1
- data/lib/otto/security/authentication/strategy_result.rb +9 -9
- data/lib/otto/security/authorization_error.rb +1 -1
- data/lib/otto/security/config.rb +3 -3
- data/lib/otto/security/core.rb +167 -0
- data/lib/otto/security/middleware/csrf_middleware.rb +1 -1
- data/lib/otto/security/middleware/validation_middleware.rb +1 -1
- data/lib/otto/security/rate_limiter.rb +7 -3
- data/lib/otto/security.rb +1 -0
- data/lib/otto/version.rb +1 -1
- data/lib/otto.rb +29 -404
- metadata +12 -3
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# lib/otto/route_handlers/instance_method.rb
|
|
2
2
|
#
|
|
3
3
|
# frozen_string_literal: true
|
|
4
|
-
require 'securerandom'
|
|
5
4
|
|
|
6
5
|
require_relative 'base'
|
|
7
6
|
|
|
@@ -17,62 +16,16 @@ class Otto
|
|
|
17
16
|
# Use this handler for endpoints requiring request-level control (logout,
|
|
18
17
|
# session management, cookie manipulation, custom header handling).
|
|
19
18
|
class InstanceMethodHandler < BaseHandler
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
instance = target_class.new(req, res)
|
|
31
|
-
result = instance.send(route_definition.method_name)
|
|
32
|
-
|
|
33
|
-
# Only handle response if response_type is not default
|
|
34
|
-
if route_definition.response_type != 'default'
|
|
35
|
-
handle_response(result, res, {
|
|
36
|
-
instance: instance,
|
|
37
|
-
request: req,
|
|
38
|
-
})
|
|
39
|
-
end
|
|
40
|
-
rescue StandardError => e
|
|
41
|
-
# Check if we're being called through Otto's integrated context (vs direct handler testing)
|
|
42
|
-
# In integrated context, let Otto's centralized error handler manage the response
|
|
43
|
-
# In direct testing context, handle errors locally for unit testing
|
|
44
|
-
if otto_instance
|
|
45
|
-
# Store handler context in env for centralized error handler
|
|
46
|
-
handler_name = "#{target_class}##{route_definition.method_name}"
|
|
47
|
-
env['otto.handler'] = handler_name
|
|
48
|
-
env['otto.handler_duration'] = Otto::Utils.now_in_μs - start_time
|
|
49
|
-
|
|
50
|
-
raise e # Re-raise to let Otto's centralized error handler manage the response
|
|
51
|
-
else
|
|
52
|
-
# Direct handler testing context - handle errors locally with security improvements
|
|
53
|
-
error_id = SecureRandom.hex(8)
|
|
54
|
-
Otto.logger.error "[#{error_id}] #{e.class}: #{e.message}"
|
|
55
|
-
Otto.logger.debug "[#{error_id}] Backtrace: #{e.backtrace.join("\n")}" if Otto.debug
|
|
56
|
-
|
|
57
|
-
res.status = 500
|
|
58
|
-
res.headers['content-type'] = 'text/plain'
|
|
59
|
-
|
|
60
|
-
if Otto.env?(:dev, :development)
|
|
61
|
-
res.write "Server error (ID: #{error_id}). Check logs for details."
|
|
62
|
-
else
|
|
63
|
-
res.write 'An error occurred. Please try again later.'
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Add security headers if available
|
|
67
|
-
if otto_instance.respond_to?(:security_config) && otto_instance.security_config
|
|
68
|
-
otto_instance.security_config.security_headers.each do |header, value|
|
|
69
|
-
res.headers[header] = value
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
finalize_response(res)
|
|
19
|
+
protected
|
|
20
|
+
|
|
21
|
+
# Invoke the instance method on the target class
|
|
22
|
+
# @param req [Rack::Request] Request object
|
|
23
|
+
# @param res [Rack::Response] Response object
|
|
24
|
+
# @return [Array] [result, context] for handle_response
|
|
25
|
+
def invoke_target(req, res)
|
|
26
|
+
instance = target_class.new(req, res)
|
|
27
|
+
result = instance.send(route_definition.method_name)
|
|
28
|
+
[result, { instance: instance, request: req }]
|
|
76
29
|
end
|
|
77
30
|
end
|
|
78
31
|
end
|
|
@@ -11,8 +11,8 @@ class Otto
|
|
|
11
11
|
class LambdaHandler < BaseHandler
|
|
12
12
|
def call(env, extra_params = {})
|
|
13
13
|
start_time = Otto::Utils.now_in_μs
|
|
14
|
-
req =
|
|
15
|
-
res =
|
|
14
|
+
req = otto_instance ? otto_instance.request_class.new(env) : Otto::Request.new(env)
|
|
15
|
+
res = otto_instance ? otto_instance.response_class.new : Otto::Response.new
|
|
16
16
|
|
|
17
17
|
begin
|
|
18
18
|
# Security: Lambda handlers require pre-configured procs from Otto instance
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# lib/otto/route_handlers/logic_class.rb
|
|
2
2
|
#
|
|
3
3
|
# frozen_string_literal: true
|
|
4
|
-
require 'json'
|
|
5
|
-
require 'securerandom'
|
|
6
4
|
|
|
7
5
|
require_relative 'base'
|
|
8
6
|
|
|
@@ -20,98 +18,95 @@ class Otto
|
|
|
20
18
|
# For endpoints requiring direct request access (sessions, cookies, headers,
|
|
21
19
|
# or logout flows), use controller handlers (Controller#action or Controller.action).
|
|
22
20
|
class LogicClassHandler < BaseHandler
|
|
23
|
-
|
|
24
|
-
start_time = Otto::Utils.now_in_μs
|
|
25
|
-
req = Rack::Request.new(env)
|
|
26
|
-
res = Rack::Response.new
|
|
21
|
+
protected
|
|
27
22
|
|
|
23
|
+
# Invoke Logic class with constrained signature
|
|
24
|
+
# @param req [Rack::Request] Request object
|
|
25
|
+
# @param res [Rack::Response] Response object
|
|
26
|
+
# @return [Array] [result, context] for handle_response
|
|
27
|
+
def invoke_target(req, _res)
|
|
28
|
+
env = req.env
|
|
29
|
+
|
|
30
|
+
# Get strategy result (guaranteed to exist from RouteAuthWrapper)
|
|
31
|
+
strategy_result = env['otto.strategy_result']
|
|
32
|
+
|
|
33
|
+
# Extract params including JSON body parsing
|
|
34
|
+
logic_params = extract_logic_params(req, env)
|
|
35
|
+
|
|
36
|
+
# Get locale
|
|
37
|
+
locale = env['otto.locale'] || 'en'
|
|
38
|
+
|
|
39
|
+
# Instantiate Logic class
|
|
40
|
+
logic = target_class.new(strategy_result, logic_params, locale)
|
|
41
|
+
|
|
42
|
+
# Execute standard Logic class lifecycle
|
|
43
|
+
logic.raise_concerns if logic.respond_to?(:raise_concerns)
|
|
44
|
+
|
|
45
|
+
result = if logic.respond_to?(:process)
|
|
46
|
+
logic.process
|
|
47
|
+
else
|
|
48
|
+
logic.call || logic
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context = {
|
|
52
|
+
logic_instance: logic,
|
|
53
|
+
request: req,
|
|
54
|
+
status_code: logic.respond_to?(:status_code) ? logic.status_code : nil,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
[result, context]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Extract logic parameters including JSON body parsing
|
|
61
|
+
# @param req [Rack::Request] Request object
|
|
62
|
+
# @param env [Hash] Rack environment
|
|
63
|
+
# @return [Hash] Parameters for Logic class
|
|
64
|
+
def extract_logic_params(req, env)
|
|
65
|
+
# req.params already has extra_params merged and indifferent_params applied
|
|
66
|
+
# by setup_request_response in BaseHandler
|
|
67
|
+
logic_params = req.params.dup
|
|
68
|
+
|
|
69
|
+
# Handle JSON request bodies
|
|
70
|
+
if req.content_type&.include?('application/json') && req.body.size.positive?
|
|
71
|
+
logic_params = parse_json_body(req, env, logic_params)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
logic_params
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Parse JSON request body with error handling
|
|
78
|
+
# @param req [Rack::Request] Request object
|
|
79
|
+
# @param env [Hash] Rack environment
|
|
80
|
+
# @param logic_params [Hash] Current parameters
|
|
81
|
+
# @return [Hash] Parameters with JSON merged (or original if parsing fails)
|
|
82
|
+
def parse_json_body(req, env, logic_params)
|
|
28
83
|
begin
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
base_context.merge(
|
|
47
|
-
handler: "#{target_class}#call",
|
|
48
|
-
error: e.message,
|
|
49
|
-
error_class: e.class.name,
|
|
50
|
-
duration: Otto::Utils.now_in_μs - start_time
|
|
51
|
-
)
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
Otto::LoggingHelpers.log_backtrace(e,
|
|
55
|
-
base_context.merge(handler: "#{target_class}#call")
|
|
56
|
-
)
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
locale = env['otto.locale'] || 'en'
|
|
61
|
-
|
|
62
|
-
logic = target_class.new(strategy_result, logic_params, locale)
|
|
63
|
-
|
|
64
|
-
# Execute standard Logic class lifecycle
|
|
65
|
-
logic.raise_concerns if logic.respond_to?(:raise_concerns)
|
|
66
|
-
|
|
67
|
-
result = if logic.respond_to?(:process)
|
|
68
|
-
logic.process
|
|
69
|
-
else
|
|
70
|
-
logic.call || logic
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Handle response with Logic instance context
|
|
74
|
-
handle_response(result, res, {
|
|
75
|
-
logic_instance: logic,
|
|
76
|
-
request: req,
|
|
77
|
-
status_code: logic.respond_to?(:status_code) ? logic.status_code : nil,
|
|
78
|
-
})
|
|
79
|
-
rescue StandardError => e
|
|
80
|
-
# Check if we're being called through Otto's integrated context (vs direct handler testing)
|
|
81
|
-
# In integrated context, let Otto's centralized error handler manage the response
|
|
82
|
-
# In direct testing context, handle errors locally for unit testing
|
|
83
|
-
if otto_instance
|
|
84
|
-
# Store handler context in env for centralized error handler
|
|
85
|
-
handler_name = "#{target_class}#call"
|
|
86
|
-
env['otto.handler'] = handler_name
|
|
87
|
-
env['otto.handler_duration'] = Otto::Utils.now_in_μs - start_time
|
|
88
|
-
|
|
89
|
-
raise e # Re-raise to let Otto's centralized error handler manage the response
|
|
90
|
-
else
|
|
91
|
-
# Direct handler testing context - handle errors locally with security improvements
|
|
92
|
-
error_id = SecureRandom.hex(8)
|
|
93
|
-
Otto.logger.error "[#{error_id}] #{e.class}: #{e.message}"
|
|
94
|
-
Otto.logger.debug "[#{error_id}] Backtrace: #{e.backtrace.join("\n")}" if Otto.debug
|
|
95
|
-
|
|
96
|
-
res.status = 500
|
|
97
|
-
res.headers['content-type'] = 'text/plain'
|
|
98
|
-
|
|
99
|
-
if Otto.env?(:dev, :development)
|
|
100
|
-
res.write "Server error (ID: #{error_id}). Check logs for details."
|
|
101
|
-
else
|
|
102
|
-
res.write 'An error occurred. Please try again later.'
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# Add security headers if available
|
|
106
|
-
if otto_instance.respond_to?(:security_config) && otto_instance.security_config
|
|
107
|
-
otto_instance.security_config.security_headers.each do |header, value|
|
|
108
|
-
res.headers[header] = value
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
84
|
+
req.body.rewind
|
|
85
|
+
json_data = JSON.parse(req.body.read)
|
|
86
|
+
logic_params = logic_params.merge(json_data) if json_data.is_a?(Hash)
|
|
87
|
+
rescue JSON::ParserError => e
|
|
88
|
+
# Base context pattern: create once, reuse for correlation
|
|
89
|
+
log_context = Otto::LoggingHelpers.request_context(env)
|
|
90
|
+
|
|
91
|
+
Otto.structured_log(:error, 'JSON parsing error',
|
|
92
|
+
log_context.merge(
|
|
93
|
+
handler: handler_name,
|
|
94
|
+
error: e.message,
|
|
95
|
+
error_class: e.class.name,
|
|
96
|
+
duration: Otto::Utils.now_in_μs - @start_time
|
|
97
|
+
))
|
|
98
|
+
|
|
99
|
+
Otto::LoggingHelpers.log_backtrace(e,
|
|
100
|
+
log_context.merge(handler: handler_name))
|
|
112
101
|
end
|
|
113
102
|
|
|
114
|
-
|
|
103
|
+
logic_params
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Format handler name for Logic routes
|
|
107
|
+
# @return [String] Handler name in format "ClassName#call"
|
|
108
|
+
def handler_name
|
|
109
|
+
"#{target_class.name}#call"
|
|
115
110
|
end
|
|
116
111
|
end
|
|
117
112
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# lib/otto/security/authentication/auth_strategy.rb
|
|
2
2
|
#
|
|
3
3
|
# frozen_string_literal: true
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
# Base class for all authentication strategies in Otto framework
|
|
6
6
|
# Provides pluggable authentication patterns that can be customized per application
|
|
7
7
|
|
|
@@ -33,7 +33,7 @@ class Otto
|
|
|
33
33
|
user: user,
|
|
34
34
|
auth_method: auth_method || self.class.name.split('::').last,
|
|
35
35
|
metadata: metadata,
|
|
36
|
-
strategy_name: nil
|
|
36
|
+
strategy_name: nil # Will be set by RouteAuthWrapper
|
|
37
37
|
)
|
|
38
38
|
end
|
|
39
39
|
|
|
@@ -320,16 +320,16 @@ class Otto
|
|
|
320
320
|
# @return [Hash] Hash representation of the context
|
|
321
321
|
def to_h
|
|
322
322
|
{
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
323
|
+
session: session,
|
|
324
|
+
user: user,
|
|
325
|
+
auth_method: auth_method,
|
|
326
|
+
metadata: metadata,
|
|
327
|
+
authenticated: authenticated?,
|
|
328
328
|
auth_attempt_succeeded: auth_attempt_succeeded?,
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
329
|
+
user_id: user_id,
|
|
330
|
+
user_name: user_name,
|
|
331
|
+
roles: roles,
|
|
332
|
+
permissions: permissions,
|
|
333
333
|
}
|
|
334
334
|
end
|
|
335
335
|
end
|
data/lib/otto/security/config.rb
CHANGED
|
@@ -436,12 +436,12 @@ class Otto
|
|
|
436
436
|
end
|
|
437
437
|
|
|
438
438
|
# Raised when a request exceeds the configured size limit
|
|
439
|
-
class RequestTooLargeError <
|
|
439
|
+
class RequestTooLargeError < Otto::PayloadTooLargeError; end
|
|
440
440
|
|
|
441
441
|
# Raised when CSRF token validation fails
|
|
442
|
-
class CSRFError <
|
|
442
|
+
class CSRFError < Otto::ForbiddenError; end
|
|
443
443
|
|
|
444
444
|
# Raised when input validation fails (XSS, SQL injection, etc.)
|
|
445
|
-
class ValidationError <
|
|
445
|
+
class ValidationError < Otto::BadRequestError; end
|
|
446
446
|
end
|
|
447
447
|
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# lib/otto/security/core.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
class Otto
|
|
6
|
+
module Security
|
|
7
|
+
# Core security configuration methods included in the Otto class.
|
|
8
|
+
# Provides the public API for enabling and configuring security features.
|
|
9
|
+
module Core
|
|
10
|
+
# Enable CSRF protection for POST, PUT, DELETE, and PATCH requests.
|
|
11
|
+
# This will automatically add CSRF tokens to HTML forms and validate
|
|
12
|
+
# them on unsafe HTTP methods.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# otto.enable_csrf_protection!
|
|
16
|
+
def enable_csrf_protection!
|
|
17
|
+
ensure_not_frozen!
|
|
18
|
+
return if @middleware.includes?(Otto::Security::Middleware::CSRFMiddleware)
|
|
19
|
+
|
|
20
|
+
@security_config.enable_csrf_protection!
|
|
21
|
+
use Otto::Security::Middleware::CSRFMiddleware
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Enable request validation including input sanitization, size limits,
|
|
25
|
+
# and protection against XSS and SQL injection attacks.
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# otto.enable_request_validation!
|
|
29
|
+
def enable_request_validation!
|
|
30
|
+
ensure_not_frozen!
|
|
31
|
+
return if @middleware.includes?(Otto::Security::Middleware::ValidationMiddleware)
|
|
32
|
+
|
|
33
|
+
@security_config.input_validation = true
|
|
34
|
+
use Otto::Security::Middleware::ValidationMiddleware
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Enable rate limiting to protect against abuse and DDoS attacks.
|
|
38
|
+
# This will automatically add rate limiting rules based on client IP.
|
|
39
|
+
#
|
|
40
|
+
# @param options [Hash] Rate limiting configuration options
|
|
41
|
+
# @option options [Integer] :requests_per_minute Maximum requests per minute per IP (default: 100)
|
|
42
|
+
# @option options [Hash] :custom_rules Custom rate limiting rules
|
|
43
|
+
# @example
|
|
44
|
+
# otto.enable_rate_limiting!(requests_per_minute: 50)
|
|
45
|
+
def enable_rate_limiting!(options = {})
|
|
46
|
+
ensure_not_frozen!
|
|
47
|
+
return if @middleware.includes?(Otto::Security::Middleware::RateLimitMiddleware)
|
|
48
|
+
|
|
49
|
+
@security.configure_rate_limiting(options)
|
|
50
|
+
use Otto::Security::Middleware::RateLimitMiddleware
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Add a custom rate limiting rule.
|
|
54
|
+
#
|
|
55
|
+
# @param name [String, Symbol] Rule name
|
|
56
|
+
# @param options [Hash] Rule configuration
|
|
57
|
+
# @option options [Integer] :limit Maximum requests
|
|
58
|
+
# @option options [Integer] :period Time period in seconds (default: 60)
|
|
59
|
+
# @option options [Proc] :condition Optional condition proc that receives request
|
|
60
|
+
# @example
|
|
61
|
+
# otto.add_rate_limit_rule('uploads', limit: 5, period: 300, condition: ->(req) { req.post? && req.path.include?('upload') })
|
|
62
|
+
def add_rate_limit_rule(name, options)
|
|
63
|
+
ensure_not_frozen!
|
|
64
|
+
@security_config.rate_limiting_config[:custom_rules][name.to_s] = options
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Add a trusted proxy server for accurate client IP detection.
|
|
68
|
+
# Only requests from trusted proxies will have their forwarded headers honored.
|
|
69
|
+
#
|
|
70
|
+
# @param proxy [String, Regexp] IP address, CIDR range, or regex pattern
|
|
71
|
+
# @example
|
|
72
|
+
# otto.add_trusted_proxy('10.0.0.0/8')
|
|
73
|
+
# otto.add_trusted_proxy(/^172\.16\./)
|
|
74
|
+
def add_trusted_proxy(proxy)
|
|
75
|
+
ensure_not_frozen!
|
|
76
|
+
@security_config.add_trusted_proxy(proxy)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Set custom security headers that will be added to all responses.
|
|
80
|
+
# These merge with the default security headers.
|
|
81
|
+
#
|
|
82
|
+
# @param headers [Hash] Hash of header name => value pairs
|
|
83
|
+
# @example
|
|
84
|
+
# otto.set_security_headers({
|
|
85
|
+
# 'content-security-policy' => "default-src 'self'",
|
|
86
|
+
# 'strict-transport-security' => 'max-age=31536000'
|
|
87
|
+
# })
|
|
88
|
+
def set_security_headers(headers)
|
|
89
|
+
ensure_not_frozen!
|
|
90
|
+
@security_config.security_headers.merge!(headers)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Enable HTTP Strict Transport Security (HSTS) header.
|
|
94
|
+
# WARNING: This can make your domain inaccessible if HTTPS is not properly
|
|
95
|
+
# configured. Only enable this when you're certain HTTPS is working correctly.
|
|
96
|
+
#
|
|
97
|
+
# @param max_age [Integer] Maximum age in seconds (default: 1 year)
|
|
98
|
+
# @param include_subdomains [Boolean] Apply to all subdomains (default: true)
|
|
99
|
+
# @example
|
|
100
|
+
# otto.enable_hsts!(max_age: 86400, include_subdomains: false)
|
|
101
|
+
def enable_hsts!(max_age: 31_536_000, include_subdomains: true)
|
|
102
|
+
ensure_not_frozen!
|
|
103
|
+
@security_config.enable_hsts!(max_age: max_age, include_subdomains: include_subdomains)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Enable Content Security Policy (CSP) header to prevent XSS attacks.
|
|
107
|
+
# The default policy only allows resources from the same origin.
|
|
108
|
+
#
|
|
109
|
+
# @param policy [String] CSP policy string (default: "default-src 'self'")
|
|
110
|
+
# @example
|
|
111
|
+
# otto.enable_csp!("default-src 'self'; script-src 'self' 'unsafe-inline'")
|
|
112
|
+
def enable_csp!(policy = "default-src 'self'")
|
|
113
|
+
ensure_not_frozen!
|
|
114
|
+
@security_config.enable_csp!(policy)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Enable X-Frame-Options header to prevent clickjacking attacks.
|
|
118
|
+
#
|
|
119
|
+
# @param option [String] Frame options: 'DENY', 'SAMEORIGIN', or 'ALLOW-FROM uri'
|
|
120
|
+
# @example
|
|
121
|
+
# otto.enable_frame_protection!('DENY')
|
|
122
|
+
def enable_frame_protection!(option = 'SAMEORIGIN')
|
|
123
|
+
ensure_not_frozen!
|
|
124
|
+
@security_config.enable_frame_protection!(option)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Enable Content Security Policy (CSP) with nonce support for dynamic header generation.
|
|
128
|
+
# This enables the res.send_csp_headers response helper method.
|
|
129
|
+
#
|
|
130
|
+
# @param debug [Boolean] Enable debug logging for CSP headers (default: false)
|
|
131
|
+
# @example
|
|
132
|
+
# otto.enable_csp_with_nonce!(debug: true)
|
|
133
|
+
def enable_csp_with_nonce!(debug: false)
|
|
134
|
+
ensure_not_frozen!
|
|
135
|
+
@security_config.enable_csp_with_nonce!(debug: debug)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Add an authentication strategy with a registered name
|
|
139
|
+
#
|
|
140
|
+
# This is the primary public API for registering authentication strategies.
|
|
141
|
+
# The name you provide here will be available as `strategy_result.strategy_name`
|
|
142
|
+
# in your application code, making it easy to identify which strategy authenticated
|
|
143
|
+
# the current request.
|
|
144
|
+
#
|
|
145
|
+
# Also available via Otto::Security::Configurator for consolidated security config.
|
|
146
|
+
#
|
|
147
|
+
# @param name [String, Symbol] Strategy name (e.g., 'session', 'api_key', 'jwt')
|
|
148
|
+
# @param strategy [AuthStrategy] Strategy instance
|
|
149
|
+
# @example
|
|
150
|
+
# otto.add_auth_strategy('session', SessionStrategy.new(session_key: 'user_id'))
|
|
151
|
+
# otto.add_auth_strategy('api_key', APIKeyStrategy.new)
|
|
152
|
+
# @raise [ArgumentError] if strategy name already registered
|
|
153
|
+
def add_auth_strategy(name, strategy)
|
|
154
|
+
ensure_not_frozen!
|
|
155
|
+
# Ensure auth_config is initialized (handles edge case where it might be nil)
|
|
156
|
+
@auth_config = { auth_strategies: {}, default_auth_strategy: 'noauth' } if @auth_config.nil?
|
|
157
|
+
|
|
158
|
+
# Strict mode: Detect strategy name collisions
|
|
159
|
+
if @auth_config[:auth_strategies].key?(name)
|
|
160
|
+
raise ArgumentError, "Authentication strategy '#{name}' is already registered"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
@auth_config[:auth_strategies][name] = strategy
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -55,9 +55,13 @@ class Otto
|
|
|
55
55
|
'retry-after' => (match_data[:period] - (now % match_data[:period])).to_s,
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
# Content negotiation for rate limit response
|
|
59
|
+
# Route's response_type takes precedence over Accept header
|
|
60
|
+
route_def = request.env['otto.route_definition']
|
|
61
|
+
wants_json = (route_def&.response_type == 'json') ||
|
|
62
|
+
request.env['HTTP_ACCEPT'].to_s.include?('application/json')
|
|
63
|
+
|
|
64
|
+
if wants_json
|
|
61
65
|
error_response = {
|
|
62
66
|
error: 'Rate limit exceeded',
|
|
63
67
|
message: 'Too many requests',
|
data/lib/otto/security.rb
CHANGED
data/lib/otto/version.rb
CHANGED