Otto v2.0.0-pre2 Migration Guide
Overview
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).
Breaking Changes
1. StrategyResult Methods Removed
Removed Methods:
- StrategyResult#success? - Always returned true, meaningless
- StrategyResult#failure? - Always returned false, meaningless
- FailureResult#success? - Always returned false
- FailureResult#failure? - Always returned true
Migration:
```ruby # Before - checking success/failure if strategy_result&.success? # handle success end
After - type checking
if strategy_result.is_a?(Otto::Security::Authentication::StrategyResult) # handle success end
```
2. New Semantic Distinction
New Method:
- StrategyResult#auth_attempt_succeeded? - Returns true only when auth strategy just executed successfully
Key Semantic Difference:
| Method | Meaning | Use Case |
|---|---|---|
authenticated? |
User in session (request state) | Check if session has user |
auth_attempt_succeeded? |
Auth strategy just succeeded (auth outcome) | Post-login redirects, analytics |
Migration Examples:
Registration Flow (IMPORTANT)
```ruby # Before - BROKEN - blocks legitimate registration class CreateAccount < Logic::Base def raise_concerns # This was always true if user in session, blocking registration raise OT::FormError, “Already signed up” if @strategy_result.success? end end
After - CORRECT
class CreateAccount < Logic::Base def raise_concerns # Check if user already in session raise OT::FormError, “Already signed up” if @strategy_result.authenticated? end end ```
Post-Login Redirect
```ruby # Before - unreliable class AuthController def authenticate if @strategy_result.success? # Always true, not helpful redirect_to dashboard_path end end end
After - correct semantic
class AuthController def authenticate if @strategy_result.auth_attempt_succeeded? # Only redirect when auth route just succeeded redirect_to dashboard_path end end end ```
Non-Breaking Enhancements
1. Comprehensive Documentation
StrategyResult now includes extensive inline documentation:
- Usage patterns and creation guidelines
- Session contract for multi-app architectures
- Examples for common scenarios
- Clear distinction between request state and auth outcomes
2. Session Contract (Multi-App Architectures)
For shared session architectures (Auth app + Core app):
Required session keys for authenticated state:
ruby
session['authenticated'] # Boolean flag
session['identity_id'] # User/customer ID
session['authenticated_at'] # Timestamp
Optional session keys:
ruby
session['email'] # User email
session['ip_address'] # Client IP (masked by default via IPPrivacyMiddleware)
session['user_agent'] # Client UA
session['locale'] # User locale
Advanced mode adds:
ruby
session['account_external_id'] # Rodauth external_id
session['advanced_account_id'] # Rodauth account ID
Application Code Updates Required
1. Remove Manual StrategyResult Creation
Anti-pattern identified:
ruby
# BAD - Bypasses Otto's auth_method tracking
class Controller::Base
def _strategy_result
Otto::Security::Authentication::StrategyResult.new(
session: session,
user: cust,
auth_method: 'session', # Hardcoded - loses semantic meaning
metadata: { ip: req.masked_ip } # Uses masked IP (privacy by default)
)
end
end
Correct approach: ```ruby # GOOD - Use RouteAuthWrapper-provided result class Controller::Base def strategy_result req.env[‘otto.strategy_result’] # Created by RouteAuthWrapper end end
Or for non-auth checks, use session directly
class Controller::Base def current_user return nil unless session[‘authenticated’] Customer.find(session[‘identity_id’]) end end ```
2. Update Logic Classes
Pattern to check for:
ruby
# Search your codebase for these patterns:
grep -r "@strategy_result.success?" apps/
grep -r "@context.success?" apps/
grep -r "strategy_result&.success?" apps/
Update to:
- Use authenticated? for “user in session” checks (registration, profile access, etc.)
- Use auth_attempt_succeeded? for “just logged in” checks (redirects, welcome messages, etc.)
3. Test Updates
RSpec matchers: ```ruby # Before expect(result).to be_success expect(result).to be_failure
After
expect(result).to be_a(Otto::Security::Authentication::StrategyResult) ```
Architecture Clarifications
When StrategyResult is Created
- Routes WITH
auth=...requirement:- RouteAuthWrapper executes strategy
- Always returns
StrategyResult(success or failure) - RouteAuthWrapper returns 401/302 response on
AuthFailure
- Routes WITHOUT
auth=...requirement:- No RouteAuthWrapper wrapping
- No
StrategyResultcreated (routes without auth don’t need it)
- Auth app (Roda) routes:
- Manually creates
StrategyResultfor Logic class compatibility - Same interface as Otto controllers
- Manually creates
Integration Boundaries
Multi-app setup (Auth + Core + API): - Shared: Session middleware, Redis session, Logic classes, Customer model - Auth app: Creates StrategyResult manually, uses Roda routing - Core/API apps: StrategyResult from RouteAuthWrapper - Integration: Pure session-based, no direct code calls between apps
Testing Your Migration
1. Registration Flow Test
```ruby describe “CreateAccount” do it “blocks registration when user already authenticated” do strategy_result = Otto::Security::Authentication::StrategyResult.new( session: { user_id: 123 }, user: { id: 123 }, auth_method: ‘anonymous’, # No auth route, but user in session metadata: {} )
logic = CreateAccount.new(strategy_result, params, 'en')
expect { logic.raise_concerns }.to raise_error(OT::FormError, /Already signed up/) end end ```
2. Auth Attempt Test
```ruby describe “LoginHandler” do it “redirects after successful authentication” do strategy_result = Otto::Security::Authentication::StrategyResult.new( session: { user_id: 123 }, user: { id: 123 }, auth_method: ‘session’, # Auth route succeeded metadata: {} )
expect(strategy_result.authenticated?).to be true
expect(strategy_result.auth_attempt_succeeded?).to be true end end ```
Checklist
- [ ] Remove all usage of
success?andfailure?methods - [ ] Update registration flows to use
authenticated? - [ ] Update post-login flows to use
auth_attempt_succeeded?if needed - [ ] Remove manual
StrategyResultcreation in controllers - [ ] Update test matchers from
be_success/be_failureto type checks - [ ] Verify session contract keys match across apps
- [ ] Run full test suite:
bundle exec rspec - [ ] Test registration while logged in (should be blocked)
- [ ] Test login redirect flow (should work correctly)
Configuration Updates
Authentication Login Path Configuration
When authentication fails for HTML requests, Otto redirects to a login page. You can configure this path:
```ruby # Initialize with login_path configuration otto = Otto.new do auth_config[:login_path] = ‘/auth/login’ # Default: ‘/signin’ end
Or configure after initialization
otto.auth_config[:login_path] = ‘/custom/login’ ```
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.
Additional Improvements in v2.0.0-pre2
Middleware Architecture Enhancements
1. Renamed MCP ValidationMiddleware → SchemaValidationMiddleware
- Resolves naming collision with Otto::Security::ValidationMiddleware
- Otto::MCP::SchemaValidationMiddleware now clearly indicates JSON schema validation
- Otto::Security::ValidationMiddleware remains for input sanitization
Migration:
ruby
# File renamed: lib/otto/mcp/validation.rb → lib/otto/mcp/schema_validation.rb
# Class renamed automatically if using Otto's MCP server
# No action needed for most users
2. Centralized Env Keys Documentation
- New file: lib/otto/env_keys.rb
- Documents all env['otto.*'] keys with types, setters, and users
- Includes usage examples and multi-app integration patterns
- Essential reference for custom middleware development
3. RateLimitMiddleware Clarity
- Added documentation clarifying it’s a CONFIGURATOR, not enforcer
- Actual rate limiting happens in Rack::Attack middleware
- call method is explicitly a pass-through
4. Middleware Order Enforcement
- New method: MiddlewareStack#validate_mcp_middleware_order
- New method: MiddlewareStack#add_with_position for explicit ordering
- MCP Server uses explicit positioning: position: :first and position: :last
- Validates middleware order and warns if suboptimal
- Optimal: RateLimitMiddleware → TokenMiddleware → SchemaValidationMiddleware
- Validation runs automatically when MCP is enabled
Usage Example: ```ruby # Explicit positioning for clarity middleware.add_with_position( Otto::MCP::RateLimitMiddleware, security_config, position: :first # Ensures rate limiting runs first )
middleware.add_with_position( Otto::MCP::SchemaValidationMiddleware, position: :last # Ensures validation runs last ) ```
Questions?
Review the comprehensive inline documentation in:
- lib/otto/security/authentication/strategy_result.rb (lines 1-90) - Auth semantics
- lib/otto/security/authentication/route_auth_wrapper.rb - Auth handler wrapper
- lib/otto/env_keys.rb - Complete env key registry
The documentation includes detailed usage patterns, session contracts, and examples for common scenarios.