otto 2.0.0.pre1 → 2.0.0.pre3

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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -3
  3. data/.github/workflows/claude-code-review.yml +30 -14
  4. data/.github/workflows/claude.yml +1 -1
  5. data/.rubocop.yml +4 -1
  6. data/CHANGELOG.rst +54 -6
  7. data/CLAUDE.md +537 -0
  8. data/Gemfile +3 -2
  9. data/Gemfile.lock +34 -26
  10. data/benchmark_middleware_wrap.rb +163 -0
  11. data/changelog.d/20251014_144317_delano_54_thats_a_wrapper.rst +36 -0
  12. data/changelog.d/20251014_161526_delano_54_thats_a_wrapper.rst +5 -0
  13. data/docs/.gitignore +2 -0
  14. data/docs/ipaddr-encoding-quirk.md +34 -0
  15. data/docs/migrating/v2.0.0-pre2.md +338 -0
  16. data/examples/authentication_strategies/config.ru +0 -1
  17. data/lib/otto/core/configuration.rb +91 -41
  18. data/lib/otto/core/freezable.rb +93 -0
  19. data/lib/otto/core/middleware_stack.rb +103 -16
  20. data/lib/otto/core/router.rb +8 -7
  21. data/lib/otto/core.rb +8 -0
  22. data/lib/otto/env_keys.rb +118 -0
  23. data/lib/otto/helpers/base.rb +2 -21
  24. data/lib/otto/helpers/request.rb +80 -2
  25. data/lib/otto/helpers/response.rb +25 -3
  26. data/lib/otto/helpers.rb +4 -0
  27. data/lib/otto/locale/config.rb +56 -0
  28. data/lib/otto/mcp/{validation.rb → schema_validation.rb} +3 -2
  29. data/lib/otto/mcp/server.rb +26 -13
  30. data/lib/otto/mcp.rb +3 -0
  31. data/lib/otto/privacy/config.rb +199 -0
  32. data/lib/otto/privacy/geo_resolver.rb +115 -0
  33. data/lib/otto/privacy/ip_privacy.rb +175 -0
  34. data/lib/otto/privacy/redacted_fingerprint.rb +136 -0
  35. data/lib/otto/privacy.rb +29 -0
  36. data/lib/otto/response_handlers/json.rb +6 -0
  37. data/lib/otto/route.rb +44 -48
  38. data/lib/otto/route_handlers/base.rb +1 -2
  39. data/lib/otto/route_handlers/factory.rb +24 -9
  40. data/lib/otto/route_handlers/logic_class.rb +2 -2
  41. data/lib/otto/security/authentication/auth_failure.rb +44 -0
  42. data/lib/otto/security/authentication/auth_strategy.rb +3 -3
  43. data/lib/otto/security/authentication/route_auth_wrapper.rb +260 -0
  44. data/lib/otto/security/authentication/strategies/{public_strategy.rb → noauth_strategy.rb} +6 -2
  45. data/lib/otto/security/authentication/strategy_result.rb +129 -15
  46. data/lib/otto/security/authentication.rb +5 -6
  47. data/lib/otto/security/config.rb +51 -18
  48. data/lib/otto/security/configurator.rb +2 -15
  49. data/lib/otto/security/middleware/ip_privacy_middleware.rb +211 -0
  50. data/lib/otto/security/middleware/rate_limit_middleware.rb +19 -3
  51. data/lib/otto/security.rb +9 -0
  52. data/lib/otto/version.rb +1 -1
  53. data/lib/otto.rb +183 -89
  54. data/otto.gemspec +5 -0
  55. metadata +83 -8
  56. data/changelog.d/20250911_235619_delano_next.rst +0 -28
  57. data/changelog.d/20250912_123055_delano_remove_ostruct.rst +0 -21
  58. data/changelog.d/20250912_175625_claude_delano_remove_ostruct.rst +0 -21
  59. data/lib/otto/security/authentication/authentication_middleware.rb +0 -123
  60. data/lib/otto/security/authentication/failure_result.rb +0 -36
@@ -22,40 +22,142 @@ class Otto
22
22
  module Security
23
23
  module Authentication
24
24
  StrategyResult = Data.define(:session, :user, :auth_method, :metadata) do
25
+ # =====================================================================
26
+ # USAGE PATTERNS - READ THIS FIRST
27
+ # =====================================================================
28
+ #
29
+ # StrategyResult represents authentication state for a request.
30
+ # It serves TWO distinct purposes that must not be confused:
31
+ #
32
+ # 1. REQUEST STATE: Current session/user information
33
+ # - Use `authenticated?` to check if session has a user
34
+ # - Available on ALL requests (anonymous or authenticated)
35
+ #
36
+ # 2. AUTH ATTEMPT OUTCOME: Whether authentication just succeeded
37
+ # - Use `auth_attempt_succeeded?` to check if auth strategy ran
38
+ # - Only true when route had auth=... requirement AND succeeded
39
+ #
40
+ # CREATION PATTERNS
41
+ # -----------------
42
+ #
43
+ # StrategyResult should ONLY be created by:
44
+ #
45
+ # 1. Otto's AuthenticationMiddleware (automatic, route-based)
46
+ # - Routes WITH auth=...: Creates result from strategy execution
47
+ # - Routes WITHOUT auth=...: Creates anonymous result
48
+ #
49
+ # 2. Auth app router (manual, for Logic class compatibility)
50
+ # - Manually builds StrategyResult for Roda routes
51
+ # - Maintains same interface as Otto controllers
52
+ #
53
+ # APPLICATION CODE SHOULD NOT manually create StrategyResult!
54
+ # Instead, access session directly or rely on middleware.
55
+ #
56
+ # SESSION CONTRACT
57
+ # ----------------
58
+ #
59
+ # For multi-app architectures with shared session:
60
+ #
61
+ # Required session keys for authenticated state:
62
+ # session['authenticated'] # Boolean flag
63
+ # session['identity_id'] # User/customer ID
64
+ # session['authenticated_at'] # Timestamp
65
+ #
66
+ # Optional session keys:
67
+ # session['email'] # User email
68
+ # session['ip_address'] # Client IP
69
+ # session['user_agent'] # Client UA
70
+ # session['locale'] # User locale
71
+ #
72
+ # Advanced mode adds:
73
+ # session['account_external_id'] # Rodauth external_id
74
+ # session['advanced_account_id'] # Rodauth account ID
75
+ #
76
+ # EXAMPLES
77
+ # --------
78
+ #
79
+ # Check if user in session (registration flow):
80
+ # class CreateAccount
81
+ # def raise_concerns
82
+ # # Block registration if already logged in
83
+ # raise FormError, "Already signed up" if @context.authenticated?
84
+ # end
85
+ # end
86
+ #
87
+ # Check if auth just succeeded (post-login redirect):
88
+ # class LoginHandler
89
+ # def process
90
+ # if @context.auth_attempt_succeeded?
91
+ # redirect_to dashboard_path
92
+ # end
93
+ # end
94
+ # end
95
+ #
96
+ # Distinguish between the two:
97
+ # @context.authenticated? #=> true (user in session)
98
+ # @context.auth_attempt_succeeded? #=> false (no auth route)
99
+ #
100
+ # # vs route with auth=session:
101
+ # @context.authenticated? #=> true (user in session)
102
+ # @context.auth_attempt_succeeded? #=> true (strategy just ran)
103
+ #
104
+ # =====================================================================
105
+
25
106
  # Create an anonymous (unauthenticated) result
26
- # @return [StrategyResult] Anonymous result with empty session and nil user
107
+ #
108
+ # Used by middleware for routes without auth requirements
109
+ # and by PublicStrategy for publicly accessible routes.
110
+ #
111
+ # @param metadata [Hash] Optional metadata (IP, user agent, etc.)
112
+ # @return [StrategyResult] Anonymous result with nil user
27
113
  def self.anonymous(metadata: {})
28
114
  new(
29
115
  session: {},
30
- user: nil, # Changed from {} to nil - clearer semantics
116
+ user: nil,
31
117
  auth_method: 'anonymous',
32
118
  metadata: metadata
33
119
  )
34
120
  end
35
121
 
36
- # Check if the request is authenticated (has a user)
37
- # @return [Boolean] True if user is present, false otherwise
122
+ # Check if the request has an authenticated user in session
123
+ #
124
+ # This checks REQUEST STATE, not auth attempt outcome.
125
+ # Returns true if session contains a user, regardless of
126
+ # whether authentication just occurred or was from a previous request.
127
+ #
128
+ # @return [Boolean] True if user is present in session
129
+ # @example
130
+ # # Block registration if user already logged in
131
+ # raise FormError if @context.authenticated?
38
132
  def authenticated?
39
133
  !user.nil?
40
134
  end
41
135
 
42
- # Check if the request is anonymous (no user)
136
+ # Check if authentication strategy just executed and succeeded
137
+ #
138
+ # This checks AUTH ATTEMPT OUTCOME, not just session state.
139
+ # Returns true only when:
140
+ # 1. Route had an auth=... requirement (not anonymous/public)
141
+ # 2. Auth strategy executed
142
+ # 3. Authentication succeeded (user authenticated)
143
+ #
144
+ # @return [Boolean] True if auth strategy just succeeded
145
+ # @example
146
+ # # Redirect after successful login
147
+ # redirect_to dashboard if @context.auth_attempt_succeeded?
148
+ def auth_attempt_succeeded?
149
+ authenticated? && auth_method.to_s != 'anonymous'
150
+ end
151
+
152
+ # Check if the request is anonymous (no user in session)
153
+ #
43
154
  # @return [Boolean] True if not authenticated
44
155
  def anonymous?
45
156
  user.nil?
46
157
  end
47
158
 
48
- # Success/failure methods for compatibility
49
- def success?
50
- true # If we have a StrategyResult, authentication succeeded
51
- end
52
-
53
- def failure?
54
- false # Failures return nil, not a StrategyResult
55
- end
56
-
57
-
58
159
  # Check if the user has a specific role
160
+ #
59
161
  # @param role [String, Symbol] Role to check
60
162
  # @return [Boolean] True if user has the role
61
163
  def has_role?(role)
@@ -75,6 +177,7 @@ class Otto
75
177
  end
76
178
 
77
179
  # Check if the user has a specific permission
180
+ #
78
181
  # @param permission [String, Symbol] Permission to check
79
182
  # @return [Boolean] True if user has the permission
80
183
  def has_permission?(permission)
@@ -97,6 +200,7 @@ class Otto
97
200
  end
98
201
 
99
202
  # Check if the user has any of the specified roles
203
+ #
100
204
  # @param roles [Array<String, Symbol>] Roles to check
101
205
  # @return [Boolean] True if user has any of the roles
102
206
  def has_any_role?(*roles)
@@ -104,6 +208,7 @@ class Otto
104
208
  end
105
209
 
106
210
  # Check if the user has any of the specified permissions
211
+ #
107
212
  # @param permissions [Array<String, Symbol>] Permissions to check
108
213
  # @return [Boolean] True if user has any of the permissions
109
214
  def has_any_permission?(*permissions)
@@ -111,6 +216,7 @@ class Otto
111
216
  end
112
217
 
113
218
  # Get user ID from various possible locations
219
+ #
114
220
  # @return [String, Integer, nil] User ID or nil
115
221
  def user_id
116
222
  return nil unless authenticated?
@@ -126,6 +232,7 @@ class Otto
126
232
  end
127
233
 
128
234
  # Get user name from various possible locations
235
+ #
129
236
  # @return [String, nil] User name or nil
130
237
  def user_name
131
238
  return nil unless authenticated?
@@ -141,12 +248,14 @@ class Otto
141
248
  end
142
249
 
143
250
  # Get session ID from various possible locations
251
+ #
144
252
  # @return [String, nil] Session ID or nil
145
253
  def session_id
146
254
  session[:id] || session['id'] || session[:session_id] || session['session_id']
147
255
  end
148
256
 
149
257
  # Get all user roles as an array
258
+ #
150
259
  # @return [Array<String>] Array of roles (empty if none)
151
260
  def roles
152
261
  return [] unless authenticated?
@@ -163,6 +272,7 @@ class Otto
163
272
  end
164
273
 
165
274
  # Get all user permissions as an array
275
+ #
166
276
  # @return [Array<String>] Array of permissions (empty if none)
167
277
  def permissions
168
278
  return [] unless authenticated?
@@ -173,6 +283,7 @@ class Otto
173
283
  end
174
284
 
175
285
  # Create a string representation for debugging
286
+ #
176
287
  # @return [String] Debug representation
177
288
  def inspect
178
289
  if authenticated?
@@ -183,6 +294,7 @@ class Otto
183
294
  end
184
295
 
185
296
  # Get user context - a hash containing user-specific information and metadata
297
+ #
186
298
  # @return [Hash] User context hash
187
299
  def user_context
188
300
  if authenticated?
@@ -203,6 +315,7 @@ class Otto
203
315
  end
204
316
 
205
317
  # Create a hash representation
318
+ #
206
319
  # @return [Hash] Hash representation of the context
207
320
  def to_h
208
321
  {
@@ -211,6 +324,7 @@ class Otto
211
324
  auth_method: auth_method,
212
325
  metadata: metadata,
213
326
  authenticated: authenticated?,
327
+ auth_attempt_succeeded: auth_attempt_succeeded?,
214
328
  user_id: user_id,
215
329
  user_name: user_name,
216
330
  roles: roles,
@@ -7,11 +7,11 @@
7
7
 
8
8
  require_relative 'authentication/auth_strategy'
9
9
  require_relative 'authentication/strategy_result'
10
- require_relative 'authentication/failure_result'
11
- require_relative 'authentication/authentication_middleware'
10
+ require_relative 'authentication/auth_failure'
11
+ require_relative 'authentication/route_auth_wrapper'
12
12
 
13
13
  # Load all strategies
14
- require_relative 'authentication/strategies/public_strategy'
14
+ require_relative 'authentication/strategies/noauth_strategy'
15
15
  require_relative 'authentication/strategies/session_strategy'
16
16
  require_relative 'authentication/strategies/role_strategy'
17
17
  require_relative 'authentication/strategies/api_key_strategy'
@@ -21,15 +21,14 @@ class Otto
21
21
  module Security
22
22
  # Backward compatibility aliases for the old namespace
23
23
  AuthStrategy = Authentication::AuthStrategy
24
- PublicStrategy = Authentication::Strategies::PublicStrategy
24
+ NoAuthStrategy = Authentication::Strategies::NoAuthStrategy
25
25
  SessionStrategy = Authentication::Strategies::SessionStrategy
26
26
  RoleStrategy = Authentication::Strategies::RoleStrategy
27
27
  APIKeyStrategy = Authentication::Strategies::APIKeyStrategy
28
28
  PermissionStrategy = Authentication::Strategies::PermissionStrategy
29
- AuthenticationMiddleware = Authentication::AuthenticationMiddleware
30
29
  end
31
30
 
32
31
  # Top-level backward compatibility aliases
33
32
  StrategyResult = Security::Authentication::StrategyResult
34
- FailureResult = Security::Authentication::FailureResult
33
+ AuthFailure = Security::Authentication::AuthFailure
35
34
  end
@@ -4,6 +4,7 @@
4
4
 
5
5
  require 'securerandom'
6
6
  require 'digest'
7
+ require_relative '../core/freezable'
7
8
 
8
9
  class Otto
9
10
  module Security
@@ -23,12 +24,15 @@ class Otto
23
24
  # config.max_request_size = 5 * 1024 * 1024 # 5MB
24
25
  # config.max_param_depth = 16
25
26
  class Config
26
- attr_accessor :csrf_protection, :csrf_token_key, :csrf_header_key, :csrf_session_key,
27
- :max_request_size, :max_param_depth, :max_param_keys,
28
- :trusted_proxies, :require_secure_cookies,
29
- :security_headers, :input_validation,
30
- :csp_nonce_enabled, :debug_csp, :mcp_auth,
31
- :rate_limiting_config
27
+ include Otto::Core::Freezable
28
+
29
+ attr_accessor :input_validation, :max_param_depth, :csrf_token_key, :rate_limiting_config, :csrf_session_key, :max_request_size, :max_param_keys
30
+
31
+ attr_reader :csrf_protection, :csrf_header_key,
32
+ :trusted_proxies, :require_secure_cookies,
33
+ :security_headers,
34
+ :csp_nonce_enabled, :debug_csp, :mcp_auth,
35
+ :ip_privacy_config
32
36
 
33
37
  # Initialize security configuration with safe defaults
34
38
  #
@@ -48,7 +52,8 @@ class Otto
48
52
  @input_validation = true
49
53
  @csp_nonce_enabled = false
50
54
  @debug_csp = false
51
- @rate_limiting_config = {}
55
+ @rate_limiting_config = { custom_rules: {} }
56
+ @ip_privacy_config = Otto::Privacy::Config.new
52
57
  end
53
58
 
54
59
  # Enable CSRF (Cross-Site Request Forgery) protection
@@ -60,14 +65,20 @@ class Otto
60
65
  # - Provide helper methods for forms and AJAX requests
61
66
  #
62
67
  # @return [void]
68
+ # @raise [FrozenError] if configuration is frozen
63
69
  def enable_csrf_protection!
70
+ raise FrozenError, 'Cannot modify frozen configuration' if frozen?
71
+
64
72
  @csrf_protection = true
65
73
  end
66
74
 
67
75
  # Disable CSRF protection
68
76
  #
69
77
  # @return [void]
78
+ # @raise [FrozenError] if configuration is frozen
70
79
  def disable_csrf_protection!
80
+ raise FrozenError, 'Cannot modify frozen configuration' if frozen?
81
+
71
82
  @csrf_protection = false
72
83
  end
73
84
 
@@ -86,6 +97,7 @@ class Otto
86
97
  #
87
98
  # @param proxy [String, Array] IP address, CIDR range, or array of addresses
88
99
  # @raise [ArgumentError] if proxy is not a String or Array
100
+ # @raise [FrozenError] if configuration is frozen
89
101
  # @return [void]
90
102
  #
91
103
  # @example Add single proxy
@@ -97,6 +109,8 @@ class Otto
97
109
  # @example Add multiple proxies
98
110
  # config.add_trusted_proxy(['10.0.0.1', '172.16.0.0/12'])
99
111
  def add_trusted_proxy(proxy)
112
+ raise FrozenError, 'Cannot modify frozen configuration' if frozen?
113
+
100
114
  case proxy
101
115
  when String, Regexp
102
116
  @trusted_proxies << proxy
@@ -149,11 +163,6 @@ class Otto
149
163
  signature = Digest::SHA256.hexdigest(hash_input)
150
164
  csrf_token = "#{token}:#{signature}"
151
165
 
152
- puts '=== CSRF Generation ==='
153
- puts "hash_input: #{hash_input.inspect}"
154
- puts "signature: #{signature}"
155
- puts "csrf_token: #{csrf_token}"
156
-
157
166
  csrf_token
158
167
  end
159
168
 
@@ -168,12 +177,6 @@ class Otto
168
177
  expected_signature = Digest::SHA256.hexdigest(hash_input)
169
178
  comparison_result = secure_compare(signature, expected_signature)
170
179
 
171
- puts '=== CSRF Verification ==='
172
- puts "hash_input: #{hash_input.inspect}"
173
- puts "received_signature: #{signature}"
174
- puts "expected_signature: #{expected_signature}"
175
- puts "match: #{comparison_result}"
176
-
177
180
  comparison_result
178
181
  end
179
182
 
@@ -186,7 +189,10 @@ class Otto
186
189
  # @param max_age [Integer] Maximum age in seconds (default: 1 year)
187
190
  # @param include_subdomains [Boolean] Apply to all subdomains (default: true)
188
191
  # @return [void]
192
+ # @raise [FrozenError] if configuration is frozen
189
193
  def enable_hsts!(max_age: 31_536_000, include_subdomains: true)
194
+ raise FrozenError, 'Cannot modify frozen configuration' if frozen?
195
+
190
196
  hsts_value = "max-age=#{max_age}"
191
197
  hsts_value += '; includeSubDomains' if include_subdomains
192
198
  @security_headers['strict-transport-security'] = hsts_value
@@ -199,10 +205,13 @@ class Otto
199
205
  #
200
206
  # @param policy [String] CSP policy string (default: "default-src 'self'")
201
207
  # @return [void]
208
+ # @raise [FrozenError] if configuration is frozen
202
209
  #
203
210
  # @example Custom policy
204
211
  # config.enable_csp!("default-src 'self'; script-src 'self' 'unsafe-inline'")
205
212
  def enable_csp!(policy = "default-src 'self'")
213
+ raise FrozenError, 'Cannot modify frozen configuration' if frozen?
214
+
206
215
  @security_headers['content-security-policy'] = policy
207
216
  end
208
217
 
@@ -214,10 +223,13 @@ class Otto
214
223
  #
215
224
  # @param debug [Boolean] Enable debug logging for CSP headers (default: false)
216
225
  # @return [void]
226
+ # @raise [FrozenError] if configuration is frozen
217
227
  #
218
228
  # @example
219
229
  # config.enable_csp_with_nonce!(debug: true)
220
230
  def enable_csp_with_nonce!(debug: false)
231
+ raise FrozenError, 'Cannot modify frozen configuration' if frozen?
232
+
221
233
  @csp_nonce_enabled = true
222
234
  @debug_csp = debug
223
235
  end
@@ -225,7 +237,10 @@ class Otto
225
237
  # Disable CSP nonce support
226
238
  #
227
239
  # @return [void]
240
+ # @raise [FrozenError] if configuration is frozen
228
241
  def disable_csp_nonce!
242
+ raise FrozenError, 'Cannot modify frozen configuration' if frozen?
243
+
229
244
  @csp_nonce_enabled = false
230
245
  end
231
246
 
@@ -257,7 +272,10 @@ class Otto
257
272
  #
258
273
  # @param option [String] Frame options: 'DENY', 'SAMEORIGIN', or 'ALLOW-FROM uri'
259
274
  # @return [void]
275
+ # @raise [FrozenError] if configuration is frozen
260
276
  def enable_frame_protection!(option = 'SAMEORIGIN')
277
+ raise FrozenError, 'Cannot modify frozen configuration' if frozen?
278
+
261
279
  @security_headers['x-frame-options'] = option
262
280
  end
263
281
 
@@ -265,6 +283,7 @@ class Otto
265
283
  #
266
284
  # @param headers [Hash] Hash of header name => value pairs
267
285
  # @return [void]
286
+ # @raise [FrozenError] if configuration is frozen
268
287
  #
269
288
  # @example
270
289
  # config.set_custom_headers({
@@ -272,9 +291,23 @@ class Otto
272
291
  # 'cross-origin-opener-policy' => 'same-origin'
273
292
  # })
274
293
  def set_custom_headers(headers)
294
+ raise FrozenError, 'Cannot modify frozen configuration' if frozen?
295
+
275
296
  @security_headers.merge!(headers)
276
297
  end
277
298
 
299
+ # Override deep_freeze! to ensure rate_limiting_config has custom_rules initialized
300
+ #
301
+ # This pre-initializes any lazy values before freezing to prevent FrozenError
302
+ # when accessing configuration after it's frozen.
303
+ #
304
+ # @return [self] The frozen configuration
305
+ def deep_freeze!
306
+ # Ensure custom_rules is initialized (should already be done in constructor)
307
+ @rate_limiting_config[:custom_rules] ||= {}
308
+ super
309
+ end
310
+
278
311
  def get_or_create_session_id(request)
279
312
  # Try existing sources first
280
313
  session_id = extract_existing_session_id(request)
@@ -4,7 +4,6 @@
4
4
 
5
5
  require_relative 'middleware/csrf_middleware'
6
6
  require_relative 'middleware/validation_middleware'
7
- require_relative 'authentication/authentication_middleware'
8
7
  require_relative 'middleware/rate_limit_middleware'
9
8
 
10
9
  # Security configuration facade for Otto framework
@@ -21,7 +20,7 @@ class Otto
21
20
  @security_config = security_config
22
21
  @middleware_stack = middleware_stack
23
22
  # Use provided auth_config or initialize a new one
24
- @auth_config = auth_config || { auth_strategies: {}, default_auth_strategy: 'publicly' }
23
+ @auth_config = auth_config || { auth_strategies: {}, default_auth_strategy: 'noauth' }
25
24
  end
26
25
 
27
26
  # Unified security configuration method with sensible defaults
@@ -75,7 +74,6 @@ class Otto
75
74
  enable_hsts! if hsts
76
75
  enable_csp! if csp
77
76
  enable_frame_protection! if frame_protection
78
- enable_authentication! if authentication
79
77
  end
80
78
 
81
79
  # Enable CSRF protection for POST, PUT, DELETE, and PATCH requests.
@@ -118,7 +116,6 @@ class Otto
118
116
  # @option options [Integer] :period Time period in seconds (default: 60)
119
117
  # @option options [Proc] :condition Optional condition proc that receives request
120
118
  def add_rate_limit_rule(name, options)
121
- @security_config.rate_limiting_config[:custom_rules] ||= {}
122
119
  @security_config.rate_limiting_config[:custom_rules][name.to_s] = options
123
120
  end
124
121
 
@@ -171,32 +168,22 @@ class Otto
171
168
  @security_config.enable_csp_with_nonce!(debug: debug)
172
169
  end
173
170
 
174
- # Enable authentication middleware for route-level access control.
175
- # This will automatically check route auth parameters and enforce authentication.
176
- def enable_authentication!
177
- return if middleware_enabled?(Otto::Security::Authentication::AuthenticationMiddleware)
178
-
179
- @middleware_stack.add(Otto::Security::Authentication::AuthenticationMiddleware, @auth_config)
180
- end
181
-
182
171
  # Add a single authentication strategy
183
172
  #
184
173
  # @param name [String] Strategy name
185
174
  # @param strategy [Otto::Security::Authentication::AuthStrategy] Strategy instance
186
175
  def add_auth_strategy(name, strategy)
187
176
  @auth_config[:auth_strategies][name] = strategy
188
- enable_authentication!
189
177
  end
190
178
 
191
179
  # Configure authentication strategies for route-level access control.
192
180
  #
193
181
  # @param strategies [Hash] Hash mapping strategy names to strategy instances
194
182
  # @param default_strategy [String] Default strategy to use when none specified
195
- def configure_auth_strategies(strategies, default_strategy: 'publicly')
183
+ def configure_auth_strategies(strategies, default_strategy: 'noauth')
196
184
  # Merge new strategies with existing ones, preserving shared state
197
185
  @auth_config[:auth_strategies].merge!(strategies)
198
186
  @auth_config[:default_auth_strategy] = default_strategy
199
- enable_authentication! unless strategies.empty?
200
187
  end
201
188
 
202
189
  # Configure rate limiting settings.