otto 2.9.0 → 2.10.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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +5 -0
  3. data/.github/workflows/ci.yml +11 -8
  4. data/.github/workflows/claude-code-review.yml +38 -13
  5. data/.github/workflows/claude.yml +10 -8
  6. data/.github/workflows/code-smells.yml +5 -5
  7. data/.github/workflows/release-gem.yml +2 -2
  8. data/.github/workflows/ruby-lint.yml +3 -3
  9. data/.github/workflows/yardoc.yml +5 -5
  10. data/.gitignore +1 -5
  11. data/.rubocop_todo.yml +7 -5
  12. data/AGENTS.md +22 -1
  13. data/CHANGELOG.rst +203 -0
  14. data/Gemfile +3 -3
  15. data/Gemfile.lock +11 -15
  16. data/README.md +75 -37
  17. data/docs/README.md +166 -0
  18. data/docs/adr/README.md +16 -0
  19. data/docs/adr/adr-001-route-authentication-at-handler-boundary.md +38 -0
  20. data/docs/adr/adr-002-multi-strategy-authentication-and-authorization.md +50 -0
  21. data/docs/adr/adr-003-caddy-tls-route-based-integration.md +48 -0
  22. data/docs/adr/adr-004-separate-compatibility-from-security-maintenance.md +58 -0
  23. data/docs/guides/authentication.md +377 -0
  24. data/docs/guides/caddy-tls.md +205 -0
  25. data/docs/guides/configuration_freezing.md +146 -0
  26. data/docs/guides/enrichment.md +161 -0
  27. data/docs/guides/forwarded-authority.md +249 -0
  28. data/docs/guides/geo-country.md +168 -0
  29. data/docs/guides/ip_privacy.md +39 -0
  30. data/docs/guides/ipaddr-encoding-quirk.md +56 -0
  31. data/docs/guides/mcp.md +282 -0
  32. data/docs/guides/privacy.md +193 -0
  33. data/docs/guides/routing.md +181 -0
  34. data/docs/guides/structured_logging.md +281 -0
  35. data/docs/guides/testing-guide.md +391 -0
  36. data/docs/maintainers/github-actions.md +41 -0
  37. data/docs/maintainers/investigations/.gitignore +2 -0
  38. data/docs/migrating/v2.0.0.md +337 -0
  39. data/docs/reference/authentication.md +290 -0
  40. data/docs/reference/route-syntax.md +181 -0
  41. data/docs/reference/runtime-and-dependency-security.md +86 -0
  42. data/examples/advanced_routes/README.md +43 -57
  43. data/examples/authentication_strategies/README.md +37 -196
  44. data/examples/basic/README.md +24 -39
  45. data/examples/basic/config.ru +0 -1
  46. data/examples/caddy_tls_demo/README.md +8 -2
  47. data/examples/lambda_handlers/README.md +11 -2
  48. data/examples/mcp_demo/README.md +75 -161
  49. data/examples/mcp_demo/config.ru +1 -0
  50. data/examples/security_features/README.md +36 -234
  51. data/lib/otto/caddy_tls/localhost_guard.rb +22 -24
  52. data/lib/otto/core/configuration.rb +36 -22
  53. data/lib/otto/core/file_safety.rb +89 -31
  54. data/lib/otto/core/middleware_stack.rb +36 -36
  55. data/lib/otto/core/router.rb +62 -19
  56. data/lib/otto/env_keys.rb +20 -2
  57. data/lib/otto/mcp/auth/token.rb +10 -4
  58. data/lib/otto/mcp/core.rb +23 -5
  59. data/lib/otto/mcp/endpoint.rb +41 -0
  60. data/lib/otto/mcp/errors.rb +15 -0
  61. data/lib/otto/mcp/options.rb +292 -0
  62. data/lib/otto/mcp/protocol.rb +52 -22
  63. data/lib/otto/mcp/rate_limiting.rb +175 -97
  64. data/lib/otto/mcp/registry.rb +14 -14
  65. data/lib/otto/mcp/schema_validation.rb +20 -12
  66. data/lib/otto/mcp/server.rb +131 -32
  67. data/lib/otto/optional_dependency.rb +57 -0
  68. data/lib/otto/privacy/config.rb +10 -8
  69. data/lib/otto/security/authentication/route_auth_wrapper/role_authorization.rb +22 -5
  70. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +181 -18
  71. data/lib/otto/security/authentication/strategies/permission_strategy.rb +1 -0
  72. data/lib/otto/security/authentication/strategies/role_strategy.rb +1 -0
  73. data/lib/otto/security/authentication/strategies/session_strategy.rb +1 -0
  74. data/lib/otto/security/authentication/strategy_result.rb +58 -28
  75. data/lib/otto/security/config.rb +328 -14
  76. data/lib/otto/security/configurator.rb +36 -6
  77. data/lib/otto/security/core.rb +19 -1
  78. data/lib/otto/security/middleware/ip_privacy_middleware.rb +105 -4
  79. data/lib/otto/security/middleware/rate_limit_middleware.rb +1 -6
  80. data/lib/otto/security/rate_limiter.rb +81 -46
  81. data/lib/otto/utils.rb +50 -0
  82. data/lib/otto/version.rb +1 -1
  83. data/lib/otto.rb +9 -11
  84. data/otto.gemspec +0 -2
  85. metadata +31 -41
  86. data/docs/.gitignore +0 -10
  87. data/docs/1108-STREAMING_ARCHITECTURE_ANALYSIS.md +0 -1105
  88. data/docs/1108-STREAMING_SUPPORT_SUMMARY.md +0 -376
  89. data/docs/enrichment.md +0 -128
  90. data/docs/geo-country.md +0 -181
  91. data/docs/ipaddr-encoding-quirk.md +0 -34
  92. data/docs/migrating/v2.0.0-pre1.md +0 -276
  93. data/docs/migrating/v2.0.0-pre2.md +0 -338
  94. data/docs/modern-authentication-authorization-landscape.md +0 -558
  95. data/docs/multi-strategy-authentication-design.md +0 -1401
  96. data/docs/reverse-proxy-network-services.md +0 -371
@@ -1,34 +0,0 @@
1
- # IPAddr#to_s Encoding Quirk in Ruby 3
2
-
3
- Ruby's `IPAddr#to_s` returns inconsistent encodings: IPv4 addresses use US-ASCII, IPv6 addresses use UTF-8.
4
-
5
- ## Behavior
6
-
7
- ```ruby
8
- IPAddr.new('192.168.1.1').to_s.encoding # => #<Encoding:US-ASCII>
9
- IPAddr.new('::1').to_s.encoding # => #<Encoding:UTF-8>
10
- ```
11
-
12
- ## Cause
13
-
14
- Different string construction in IPAddr's `_to_string` method:
15
-
16
- - **IPv4**: `Array#join('.')` → US-ASCII optimization
17
- - **IPv6**: `String#%` → UTF-8 default
18
-
19
- ## Impact
20
-
21
- - Rack expects UTF-8 strings
22
- - Mixed encodings cause `Encoding::CompatibilityError`
23
- - String operations fail on encoding mismatches
24
-
25
- ## Solution
26
-
27
- Use `force_encoding('UTF-8')` instead of `encode('UTF-8')`:
28
-
29
- - IP addresses contain only ASCII characters
30
- - ASCII bytes are identical in US-ASCII and UTF-8
31
- - `force_encoding` changes label only (O(1))
32
- - `encode` creates new string (O(n))
33
-
34
- This ensures consistent UTF-8 encoding across all IP strings.
@@ -1,276 +0,0 @@
1
- # Migrating to Otto v2.0.0-pre1
2
-
3
- ## What's New in v2.0.0-pre1
4
-
5
- This pre-release includes extensive test coverage improvements (76 new test cases), core module refactoring, middleware stack unification, and a major update to Logic class authentication patterns. See the [full changelog](../../CHANGELOG.rst#changelog-2.0.0-pre1) for complete details.
6
-
7
- The main breaking changes affect:
8
- 1. Applications that directly manipulate the middleware stack
9
- 2. Logic classes using the old authentication signature
10
-
11
- ## Middleware Stack Unified API
12
-
13
- Otto v2.0.0-pre1 introduces a significant refactoring of the middleware stack management, providing a more consistent and efficient approach to middleware configuration.
14
-
15
- ### Key Changes
16
-
17
- #### Unified Middleware Registration
18
-
19
- Previous versions had separate legacy and new middleware stacks. In v2.0.0-pre1, we've consolidated these into a single, more powerful middleware stack.
20
-
21
- **Before:**
22
- ```ruby
23
- # Old approach with separate stacks
24
- otto.middleware_stack << SomeMiddleware
25
- otto.middleware.add(AnotherMiddleware)
26
- ```
27
-
28
- **After:**
29
- ```ruby
30
- # Unified middleware registration
31
- otto.use(SomeMiddleware)
32
- otto.use(AnotherMiddleware)
33
- ```
34
-
35
- #### Performance Improvements
36
-
37
- - Middleware lookup is now O(1) using a Set
38
- - Memoized middleware list reduces repeated array creation
39
- - Prevent duplicate middleware registrations with identical configurations
40
-
41
- ### Migration Steps
42
-
43
- 1. Replace all `otto.middleware_stack <<` calls with `otto.use()`
44
- 2. Remove any direct references to `otto.middleware_stack`
45
- 3. Use `otto.middleware.includes?()` instead of manual stack checks
46
-
47
- ### Authentication and Security Methods
48
-
49
- Authentication and security methods now consistently use the new unified middleware stack:
50
-
51
- ```ruby
52
- # Before
53
- otto.middleware_stack << Otto::Security::CSRFMiddleware
54
-
55
- # After (no change needed)
56
- otto.enable_csrf_protection!
57
- ```
58
-
59
- ### Performance Considerations
60
-
61
- The new implementation is more memory-efficient and provides faster middleware lookups, especially for applications with many middleware components.
62
-
63
- ### Potential Breaking Changes
64
-
65
- - Code relying on direct manipulation of `middleware_stack` will need updates
66
- - Method signatures for middleware configuration remain the same
67
-
68
- ### Example Migration
69
-
70
- ```ruby
71
- # Old code
72
- class MyApp
73
- def initialize
74
- @otto = Otto.new
75
- @otto.middleware_stack << CustomMiddleware
76
- @otto.middleware.add(AnotherMiddleware)
77
- end
78
- end
79
-
80
- # New code
81
- class MyApp
82
- def initialize
83
- @otto = Otto.new
84
- @otto.use(CustomMiddleware)
85
- @otto.use(AnotherMiddleware)
86
- end
87
- end
88
- ```
89
-
90
- ### Troubleshooting
91
-
92
- If you encounter any issues with middleware registration or configuration, please file an issue at [Otto GitHub Repository](https://github.com/delano/otto/issues).
93
-
94
- ## Logic Class RequestContext Pattern
95
-
96
- Otto v2.0.0-pre1 introduces a major improvement to Logic class authentication with the new RequestContext pattern.
97
-
98
- ### Key Changes
99
-
100
- #### New Constructor Signature
101
-
102
- Logic classes now use a cleaner, more powerful constructor signature that provides immutable context.
103
-
104
- **Before:**
105
- ```ruby
106
- class MyLogic
107
- attr_reader :session, :user, :params, :locale
108
-
109
- def initialize(session, user, params, locale)
110
- @session = session
111
- @user = user
112
- @params = params
113
- @locale = locale
114
- end
115
-
116
- def process
117
- return { error: 'not authenticated' } unless @user
118
- { result: 'success', user_name: @user['name'] }
119
- end
120
- end
121
- ```
122
-
123
- **After:**
124
- ```ruby
125
- class MyLogic
126
- attr_reader :context, :params, :locale
127
-
128
- def initialize(context, params, locale)
129
- @context = context
130
- @params = params
131
- @locale = locale
132
- end
133
-
134
- def process
135
- return { error: 'not authenticated' } unless @context.authenticated?
136
- {
137
- result: 'success',
138
- user_name: @context.user_name,
139
- roles: @context.roles,
140
- permissions: @context.permissions
141
- }
142
- end
143
- end
144
- ```
145
-
146
- #### RequestContext Benefits
147
-
148
- 1. **Immutable Structure** - RequestContext is a Ruby Data class that can't be accidentally modified
149
- 2. **Helper Methods** - Built-in methods like `authenticated?`, `has_role?`, `has_permission?`
150
- 3. **Cleaner Interface** - Single context parameter instead of separate session/user parameters
151
- 4. **Type Safety** - Better IDE support and documentation
152
- 5. **Future-proof** - New authentication data automatically available
153
-
154
- #### RequestContext API
155
-
156
- ```ruby
157
- # Authentication status
158
- context.authenticated? # Boolean: has non-empty user data
159
- context.anonymous? # Boolean: not authenticated
160
-
161
- # User information
162
- context.user_id # User ID from various possible locations
163
- context.user_name # User name/username
164
- context.session_id # Session identifier
165
-
166
- # Role and permission checks
167
- context.has_role?('admin') # Single role check
168
- context.has_permission?('write') # Single permission check
169
- context.has_any_role?('admin', 'mod') # Multiple role check
170
- context.roles # Array of all user roles
171
- context.permissions # Array of all user permissions
172
-
173
- # Raw data access
174
- context.session # Session hash
175
- context.user # User hash
176
- context.auth_method # Authentication method used
177
- context.metadata # Additional context data
178
- ```
179
-
180
- ### Migration Steps
181
-
182
- 1. **Update Logic class constructor signatures** from 4 parameters to 3:
183
- ```ruby
184
- # Change this:
185
- def initialize(session, user, params, locale)
186
-
187
- # To this:
188
- def initialize(context, params, locale)
189
- ```
190
-
191
- 2. **Update instance variables**:
192
- ```ruby
193
- # Change this:
194
- @session = session
195
- @user = user
196
-
197
- # To this:
198
- @context = context
199
- ```
200
-
201
- 3. **Update authentication checks**:
202
- ```ruby
203
- # Change this:
204
- return error unless @user
205
-
206
- # To this:
207
- return error unless @context.authenticated?
208
- ```
209
-
210
- 4. **Update data access**:
211
- ```ruby
212
- # Change this:
213
- user_name = @user&.dig('name')
214
- user_role = @user&.dig('role')
215
-
216
- # To this:
217
- user_name = @context.user_name
218
- user_role = @context.roles.first
219
- ```
220
-
221
- ### Example Migration
222
-
223
- **Before (v1.x):**
224
- ```ruby
225
- class AdminPanel
226
- attr_reader :session, :user, :params, :locale
227
-
228
- def initialize(session, user, params, locale)
229
- @session = session
230
- @user = user
231
- @params = params
232
- @locale = locale
233
- end
234
-
235
- def raise_concerns
236
- raise 'Access denied' unless @user&.dig('role') == 'admin'
237
- end
238
-
239
- def process
240
- {
241
- panel: 'admin',
242
- user: @user&.dig('name') || 'unknown',
243
- session_id: @session&.dig('id')
244
- }
245
- end
246
- end
247
- ```
248
-
249
- **After (v2.0):**
250
- ```ruby
251
- class AdminPanel
252
- attr_reader :context, :params, :locale
253
-
254
- def initialize(context, params, locale)
255
- @context = context
256
- @params = params
257
- @locale = locale
258
- end
259
-
260
- def raise_concerns
261
- raise 'Access denied' unless @context.has_role?('admin')
262
- end
263
-
264
- def process
265
- {
266
- panel: 'admin',
267
- user: @context.user_name || 'unknown',
268
- session_id: @context.session_id,
269
- authenticated: @context.authenticated?,
270
- permissions: @context.permissions
271
- }
272
- end
273
- end
274
- ```
275
-
276
- This provides a cleaner, more maintainable interface while giving Logic classes access to rich authentication context.
@@ -1,338 +0,0 @@
1
- # Otto v2.0.0-pre2 Migration Guide
2
-
3
- ## Overview
4
-
5
- This release resolves critical architectural issues with `StrategyResult` semantics and removes deprecated methods. The changes clarify the distinction between "request state" (user in session) and "authentication outcomes" (auth attempt just succeeded).
6
-
7
- ## Breaking Changes
8
-
9
- ### 1. StrategyResult Methods Removed
10
-
11
- **Removed Methods:**
12
- - `StrategyResult#success?` - Always returned `true`, meaningless
13
- - `StrategyResult#failure?` - Always returned `false`, meaningless
14
- - `FailureResult#success?` - Always returned `false`
15
- - `FailureResult#failure?` - Always returned `true`
16
-
17
- **Migration:**
18
-
19
- ```ruby
20
- # Before - checking success/failure
21
- if strategy_result&.success?
22
- # handle success
23
- end
24
-
25
- # After - type checking
26
- if strategy_result.is_a?(Otto::Security::Authentication::StrategyResult)
27
- # handle success
28
- end
29
-
30
- ```
31
-
32
- ### 2. New Semantic Distinction
33
-
34
- **New Method:**
35
- - `StrategyResult#auth_attempt_succeeded?` - Returns `true` only when auth strategy just executed successfully
36
-
37
- **Key Semantic Difference:**
38
-
39
- | Method | Meaning | Use Case |
40
- |--------|---------|----------|
41
- | `authenticated?` | User in session (request state) | Check if session has user |
42
- | `auth_attempt_succeeded?` | Auth strategy just succeeded (auth outcome) | Post-login redirects, analytics |
43
-
44
- **Migration Examples:**
45
-
46
- #### Registration Flow (IMPORTANT)
47
-
48
- ```ruby
49
- # Before - BROKEN - blocks legitimate registration
50
- class CreateAccount < Logic::Base
51
- def raise_concerns
52
- # This was always true if user in session, blocking registration
53
- raise OT::FormError, "Already signed up" if @strategy_result.success?
54
- end
55
- end
56
-
57
- # After - CORRECT
58
- class CreateAccount < Logic::Base
59
- def raise_concerns
60
- # Check if user already in session
61
- raise OT::FormError, "Already signed up" if @strategy_result.authenticated?
62
- end
63
- end
64
- ```
65
-
66
- #### Post-Login Redirect
67
-
68
- ```ruby
69
- # Before - unreliable
70
- class AuthController
71
- def authenticate
72
- if @strategy_result.success? # Always true, not helpful
73
- redirect_to dashboard_path
74
- end
75
- end
76
- end
77
-
78
- # After - correct semantic
79
- class AuthController
80
- def authenticate
81
- if @strategy_result.auth_attempt_succeeded?
82
- # Only redirect when auth route just succeeded
83
- redirect_to dashboard_path
84
- end
85
- end
86
- end
87
- ```
88
-
89
- ## Non-Breaking Enhancements
90
-
91
- ### 1. Comprehensive Documentation
92
-
93
- `StrategyResult` now includes extensive inline documentation:
94
- - Usage patterns and creation guidelines
95
- - Session contract for multi-app architectures
96
- - Examples for common scenarios
97
- - Clear distinction between request state and auth outcomes
98
-
99
- ### 2. Session Contract (Multi-App Architectures)
100
-
101
- For shared session architectures (Auth app + Core app):
102
-
103
- **Required session keys for authenticated state:**
104
- ```ruby
105
- session['authenticated'] # Boolean flag
106
- session['identity_id'] # User/customer ID
107
- session['authenticated_at'] # Timestamp
108
- ```
109
-
110
- **Optional session keys:**
111
- ```ruby
112
- session['email'] # User email
113
- session['ip_address'] # Client IP (masked by default via IPPrivacyMiddleware)
114
- session['user_agent'] # Client UA
115
- session['locale'] # User locale
116
- ```
117
-
118
- **Advanced mode adds:**
119
- ```ruby
120
- session['account_external_id'] # Rodauth external_id
121
- session['advanced_account_id'] # Rodauth account ID
122
- ```
123
-
124
- ## Application Code Updates Required
125
-
126
- ### 1. Remove Manual StrategyResult Creation
127
-
128
- **Anti-pattern identified:**
129
- ```ruby
130
- # BAD - Bypasses Otto's auth_method tracking
131
- class Controller::Base
132
- def _strategy_result
133
- Otto::Security::Authentication::StrategyResult.new(
134
- session: session,
135
- user: cust,
136
- auth_method: 'session', # Hardcoded - loses semantic meaning
137
- metadata: { ip: req.masked_ip } # Uses masked IP (privacy by default)
138
- )
139
- end
140
- end
141
- ```
142
-
143
- **Correct approach:**
144
- ```ruby
145
- # GOOD - Use RouteAuthWrapper-provided result
146
- class Controller::Base
147
- def strategy_result
148
- req.env['otto.strategy_result'] # Created by RouteAuthWrapper
149
- end
150
- end
151
-
152
- # Or for non-auth checks, use session directly
153
- class Controller::Base
154
- def current_user
155
- return nil unless session['authenticated']
156
- Customer.find(session['identity_id'])
157
- end
158
- end
159
- ```
160
-
161
- ### 2. Update Logic Classes
162
-
163
- **Pattern to check for:**
164
- ```ruby
165
- # Search your codebase for these patterns:
166
- grep -r "@strategy_result.success?" apps/
167
- grep -r "@context.success?" apps/
168
- grep -r "strategy_result&.success?" apps/
169
- ```
170
-
171
- **Update to:**
172
- - Use `authenticated?` for "user in session" checks (registration, profile access, etc.)
173
- - Use `auth_attempt_succeeded?` for "just logged in" checks (redirects, welcome messages, etc.)
174
-
175
- ### 3. Test Updates
176
-
177
- **RSpec matchers:**
178
- ```ruby
179
- # Before
180
- expect(result).to be_success
181
- expect(result).to be_failure
182
-
183
- # After
184
- expect(result).to be_a(Otto::Security::Authentication::StrategyResult)
185
- ```
186
-
187
- ## Architecture Clarifications
188
-
189
- ### When StrategyResult is Created
190
-
191
- 1. **Routes WITH `auth=...` requirement:**
192
- - RouteAuthWrapper executes strategy
193
- - Always returns `StrategyResult` (success or failure)
194
- - RouteAuthWrapper returns 401/302 response on `AuthFailure`
195
-
196
- 2. **Routes WITHOUT `auth=...` requirement:**
197
- - No RouteAuthWrapper wrapping
198
- - No `StrategyResult` created (routes without auth don't need it)
199
-
200
- 3. **Auth app (Roda) routes:**
201
- - Manually creates `StrategyResult` for Logic class compatibility
202
- - Same interface as Otto controllers
203
-
204
- ### Integration Boundaries
205
-
206
- **Multi-app setup (Auth + Core + API):**
207
- - **Shared:** Session middleware, Redis session, Logic classes, Customer model
208
- - **Auth app:** Creates StrategyResult manually, uses Roda routing
209
- - **Core/API apps:** StrategyResult from RouteAuthWrapper
210
- - **Integration:** Pure session-based, no direct code calls between apps
211
-
212
- ## Testing Your Migration
213
-
214
- ### 1. Registration Flow Test
215
-
216
- ```ruby
217
- describe "CreateAccount" do
218
- it "blocks registration when user already authenticated" do
219
- strategy_result = Otto::Security::Authentication::StrategyResult.new(
220
- session: { user_id: 123 },
221
- user: { id: 123 },
222
- auth_method: 'anonymous', # No auth route, but user in session
223
- metadata: {}
224
- )
225
-
226
- logic = CreateAccount.new(strategy_result, params, 'en')
227
-
228
- expect { logic.raise_concerns }.to raise_error(OT::FormError, /Already signed up/)
229
- end
230
- end
231
- ```
232
-
233
- ### 2. Auth Attempt Test
234
-
235
- ```ruby
236
- describe "LoginHandler" do
237
- it "redirects after successful authentication" do
238
- strategy_result = Otto::Security::Authentication::StrategyResult.new(
239
- session: { user_id: 123 },
240
- user: { id: 123 },
241
- auth_method: 'session', # Auth route succeeded
242
- metadata: {}
243
- )
244
-
245
- expect(strategy_result.authenticated?).to be true
246
- expect(strategy_result.auth_attempt_succeeded?).to be true
247
- end
248
- end
249
- ```
250
-
251
- ## Checklist
252
-
253
- - [ ] Remove all usage of `success?` and `failure?` methods
254
- - [ ] Update registration flows to use `authenticated?`
255
- - [ ] Update post-login flows to use `auth_attempt_succeeded?` if needed
256
- - [ ] Remove manual `StrategyResult` creation in controllers
257
- - [ ] Update test matchers from `be_success`/`be_failure` to type checks
258
- - [ ] Verify session contract keys match across apps
259
- - [ ] Run full test suite: `bundle exec rspec`
260
- - [ ] Test registration while logged in (should be blocked)
261
- - [ ] Test login redirect flow (should work correctly)
262
-
263
- ## Configuration Updates
264
-
265
- ### Authentication Login Path Configuration
266
-
267
- When authentication fails for HTML requests, Otto redirects to a login page. You can configure this path:
268
-
269
- ```ruby
270
- # Initialize with login_path configuration
271
- otto = Otto.new do
272
- auth_config[:login_path] = '/auth/login' # Default: '/signin'
273
- end
274
-
275
- # Or configure after initialization
276
- otto.auth_config[:login_path] = '/custom/login'
277
- ```
278
-
279
- **Note:** If not configured, the default fallback is `/signin`. Ensure this route exists or configure your actual login path to avoid 404 errors on authentication failures.
280
-
281
- ## Additional Improvements in v2.0.0-pre2
282
-
283
- ### Middleware Architecture Enhancements
284
-
285
- **1. Renamed MCP ValidationMiddleware → SchemaValidationMiddleware**
286
- - Resolves naming collision with `Otto::Security::ValidationMiddleware`
287
- - `Otto::MCP::SchemaValidationMiddleware` now clearly indicates JSON schema validation
288
- - `Otto::Security::ValidationMiddleware` remains for input sanitization
289
-
290
- **Migration:**
291
- ```ruby
292
- # File renamed: lib/otto/mcp/validation.rb → lib/otto/mcp/schema_validation.rb
293
- # Class renamed automatically if using Otto's MCP server
294
- # No action needed for most users
295
- ```
296
-
297
- **2. Centralized Env Keys Documentation**
298
- - New file: `lib/otto/env_keys.rb`
299
- - Documents all `env['otto.*']` keys with types, setters, and users
300
- - Includes usage examples and multi-app integration patterns
301
- - Essential reference for custom middleware development
302
-
303
- **3. RateLimitMiddleware Clarity**
304
- - Added documentation clarifying it's a CONFIGURATOR, not enforcer
305
- - Actual rate limiting happens in Rack::Attack middleware
306
- - `call` method is explicitly a pass-through
307
-
308
- **4. Middleware Order Enforcement**
309
- - New method: `MiddlewareStack#validate_mcp_middleware_order`
310
- - New method: `MiddlewareStack#add_with_position` for explicit ordering
311
- - MCP Server uses explicit positioning: `position: :first` and `position: :last`
312
- - Validates middleware order and warns if suboptimal
313
- - Optimal: RateLimitMiddleware → TokenMiddleware → SchemaValidationMiddleware
314
- - Validation runs automatically when MCP is enabled
315
-
316
- **Usage Example:**
317
- ```ruby
318
- # Explicit positioning for clarity
319
- middleware.add_with_position(
320
- Otto::MCP::RateLimitMiddleware,
321
- security_config,
322
- position: :first # Ensures rate limiting runs first
323
- )
324
-
325
- middleware.add_with_position(
326
- Otto::MCP::SchemaValidationMiddleware,
327
- position: :last # Ensures validation runs last
328
- )
329
- ```
330
-
331
- ## Questions?
332
-
333
- Review the comprehensive inline documentation in:
334
- - `lib/otto/security/authentication/strategy_result.rb` (lines 1-90) - Auth semantics
335
- - `lib/otto/security/authentication/route_auth_wrapper.rb` - Auth handler wrapper
336
- - `lib/otto/env_keys.rb` - Complete env key registry
337
-
338
- The documentation includes detailed usage patterns, session contracts, and examples for common scenarios.