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

  1. Routes WITH auth=... requirement:
    • RouteAuthWrapper executes strategy
    • Always returns StrategyResult (success or failure)
    • RouteAuthWrapper returns 401/302 response on AuthFailure
  2. Routes WITHOUT auth=... requirement:
    • No RouteAuthWrapper wrapping
    • No StrategyResult created (routes without auth don’t need it)
  3. Auth app (Roda) routes:
    • Manually creates StrategyResult for Logic class compatibility
    • Same interface as Otto controllers

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? and failure? methods
  • [ ] Update registration flows to use authenticated?
  • [ ] Update post-login flows to use auth_attempt_succeeded? if needed
  • [ ] Remove manual StrategyResult creation in controllers
  • [ ] Update test matchers from be_success/be_failure to 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.