otto 2.0.0.pre8 → 2.0.0.pre9

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.
@@ -0,0 +1,558 @@
1
+ # Modern Authentication/Authorization Landscape
2
+
3
+ ## Overview
4
+
5
+ This document analyzes modern authentication and authorization patterns across web frameworks and libraries to inform Otto's design decisions regarding multiple authentication strategies per route.
6
+
7
+ **Research Date:** November 2025
8
+ **Frameworks Analyzed:** Warden (Ruby/Rack), Django REST Framework (Python), Passport.js (Node.js/Express)
9
+
10
+ ---
11
+
12
+ ## Industry-Standard Pattern: Multiple Strategies with OR Logic
13
+
14
+ **All major frameworks support multiple authentication strategies per route**, and they follow the same pattern:
15
+
16
+ ### 1. Warden (Ruby/Rack) - The Reference Implementation
17
+
18
+ ```ruby
19
+ # Warden explicitly supports cascading strategies
20
+ manager.default_strategies :session, :token, :basic
21
+
22
+ # "Warden looks through all valid strategies and attempts to
23
+ # authenticate until one works"
24
+ ```
25
+
26
+ **Behavior:**
27
+ - Tries each strategy in sequence until one succeeds
28
+ - First success wins, remaining strategies skipped
29
+ - Fails only if ALL strategies fail
30
+
31
+ **Key Insight:** Warden is the foundational Rack authentication framework that both Devise (Rails) and other Ruby frameworks build upon. It was specifically designed with multiple strategy support from day one.
32
+
33
+ ### 2. Django REST Framework (Python)
34
+
35
+ ```python
36
+ # Multiple authentication classes tried in order
37
+ authentication_classes = [SessionAuthentication, TokenAuthentication, BasicAuthentication]
38
+
39
+ # "If one authentication class authenticates the user,
40
+ # all other classes are skipped"
41
+ ```
42
+
43
+ **Behavior:**
44
+ - Order matters (first success wins)
45
+ - Use case: Support web browsers (session) AND API clients (token)
46
+
47
+ **Implementation Note:** Django REST Framework processes authentication classes sequentially. If any class successfully authenticates, the remaining classes are not processed. This is OR logic, not AND.
48
+
49
+ ### 3. Passport.js (Node/Express)
50
+
51
+ ```javascript
52
+ // Array of strategies with OR logic
53
+ app.post('/api/data',
54
+ passport.authenticate(['local', 'bearer', 'oauth2']))
55
+
56
+ // "Will only fail if NONE of the strategies returned success"
57
+ ```
58
+
59
+ **Behavior:**
60
+ - Explicit array syntax
61
+ - Commonly used for: session OR API key OR OAuth token
62
+
63
+ **Documentation:** Passport's official documentation emphasizes that passing multiple strategies creates an OR relationship, allowing requests to authenticate via any of the provided methods.
64
+
65
+ ---
66
+
67
+ ## Common Use Cases
68
+
69
+ ### API Endpoints Supporting Multiple Client Types
70
+
71
+ ```
72
+ GET /api/users auth=session,apikey,oauth
73
+ ```
74
+
75
+ **Scenario:**
76
+ - **Web browser:** Uses session cookies
77
+ - **Mobile app:** Uses API key
78
+ - **Third-party integration:** Uses OAuth token
79
+
80
+ **Why This Matters:** Modern applications often need to support multiple client types accessing the same resources. Requiring separate endpoints for each auth method breaks REST principles and creates maintenance overhead.
81
+
82
+ ### Gradual Migration Patterns
83
+
84
+ ```
85
+ GET /protected auth=legacy,modern
86
+ ```
87
+
88
+ **Scenario:**
89
+ - Support old auth method while migrating to new one
90
+ - Remove legacy strategy after migration complete
91
+
92
+ **Real-World Example:** Migrating from custom token auth to OAuth 2.0 without breaking existing integrations.
93
+
94
+ ### Tiered Authentication
95
+
96
+ ```
97
+ GET /public-data auth=anonymous,session
98
+ ```
99
+
100
+ **Scenario:**
101
+ - Anonymous users get rate-limited access
102
+ - Authenticated users get full access
103
+
104
+ **Use Case:** Public APIs that offer limited anonymous access but enhanced features for authenticated users.
105
+
106
+ ---
107
+
108
+ ## Best Practices vs Anti-Patterns
109
+
110
+ ### ✅ Best Practices
111
+
112
+ #### 1. OR Logic, Not AND
113
+
114
+ Multiple strategies = "authenticate via ANY of these methods"
115
+
116
+ ```
117
+ auth=session,apikey # Session OR API key (not both)
118
+ ```
119
+
120
+ **Rationale:** Authentication answers "WHO are you?" A user cannot be authenticated via multiple methods simultaneously in a meaningful way. They authenticate via ONE method that successfully proves their identity.
121
+
122
+ #### 2. Order Matters - Most Preferred First
123
+
124
+ ```
125
+ auth=session,apikey,basic # Try session first (stateful, faster)
126
+ ```
127
+
128
+ **Performance Considerations:**
129
+ - **Session auth:** Cheapest (just session lookup in memory/Redis)
130
+ - **API key:** Medium cost (database lookup)
131
+ - **OAuth token:** Most expensive (signature verification, possible external validation)
132
+
133
+ **Security Note:** Put more secure/trusted methods first. If session authentication succeeds, you don't need to validate the API key.
134
+
135
+ #### 3. Separation of Authentication vs Authorization
136
+
137
+ - **Authentication:** WHO are you? (session, API key, OAuth)
138
+ - **Authorization:** WHAT can you do? (role, permission)
139
+
140
+ ```
141
+ # Good: Clear separation
142
+ auth=session,apikey # WHO (authentication)
143
+ role=admin # WHAT (authorization)
144
+
145
+ # Bad: Mixing concerns
146
+ auth=admin_session,user_apikey # Conflates WHO and WHAT
147
+ ```
148
+
149
+ **Why This Matters:** Authentication and authorization are orthogonal concerns. Mixing them creates maintenance problems and violates the Single Responsibility Principle.
150
+
151
+ #### 4. Explicit Failure Messages
152
+
153
+ ```ruby
154
+ # Log which strategies were tried
155
+ "Authentication failed: tried [session, apikey, oauth], all failed"
156
+ ```
157
+
158
+ **Debugging Value:** When authentication fails in production, knowing which strategies were attempted helps diagnose issues (expired tokens, missing headers, etc.).
159
+
160
+ #### 5. Content Negotiation
161
+
162
+ Some frameworks automatically choose strategy based on request headers:
163
+
164
+ ```ruby
165
+ # If request has Authorization header → try token first
166
+ # If request has session cookie → try session first
167
+ ```
168
+
169
+ **Advanced Pattern:** While not required, some implementations optimize by reordering strategies based on request characteristics.
170
+
171
+ ### ❌ Anti-Patterns
172
+
173
+ #### 1. Requiring ALL Strategies to Pass (AND Logic)
174
+
175
+ ```
176
+ # WRONG: Requiring both session AND API key
177
+ auth=session&apikey # Makes no sense - that's not authentication
178
+ ```
179
+
180
+ **Why This is Wrong:** This conflates authentication with authorization. If you need multiple verification factors, use Multi-Factor Authentication (MFA) within a single strategy, not multiple strategies.
181
+
182
+ **Correct Approach for MFA:**
183
+ ```ruby
184
+ # ONE strategy that implements MFA internally
185
+ class MFAStrategy
186
+ def authenticate(env, req)
187
+ # Verify password (factor 1)
188
+ # Verify TOTP code (factor 2)
189
+ # Both must pass within this single strategy
190
+ end
191
+ end
192
+ ```
193
+
194
+ #### 2. Different Strategies Per Route for Same Resource
195
+
196
+ ```
197
+ # WRONG: Inconsistent authentication
198
+ GET /users auth=session
199
+ POST /users auth=apikey
200
+
201
+ # RIGHT: Consistent authentication
202
+ GET /users auth=session,apikey
203
+ POST /users auth=session,apikey
204
+ ```
205
+
206
+ **Why This is Wrong:** Different auth requirements for different HTTP methods on the same resource breaks client expectations and creates security confusion.
207
+
208
+ #### 3. Creating Composite Strategies Instead of Native Support
209
+
210
+ ```ruby
211
+ # WORKAROUND (not ideal):
212
+ class SessionOrAPIKeyStrategy
213
+ def authenticate(env, req)
214
+ session_result = SessionStrategy.new.authenticate(env, req)
215
+ return session_result if session_result.success?
216
+
217
+ APIKeyStrategy.new.authenticate(env, req)
218
+ end
219
+ end
220
+
221
+ otto.add_auth_strategy('session_or_apikey', SessionOrAPIKeyStrategy.new)
222
+
223
+ # Routes file
224
+ GET /api/data auth=session_or_apikey
225
+
226
+ # BETTER: Native framework support
227
+ GET /api/data auth=session,apikey
228
+ ```
229
+
230
+ **Why This is Suboptimal:**
231
+ - Doesn't scale (need composite for each combination)
232
+ - Less clear in routes file (what strategies are included?)
233
+ - Harder to maintain (changing strategy order requires code changes)
234
+ - Not standard (other developers expect framework-level support)
235
+
236
+ #### 4. Mixing Authentication Strategies with Authorization Rules
237
+
238
+ ```ruby
239
+ # WRONG: Conflating auth and authz
240
+ auth=admin_session # This is authorization, not authentication!
241
+
242
+ # RIGHT: Separate concerns
243
+ auth=session
244
+ role=admin
245
+ ```
246
+
247
+ **Why This is Wrong:** `admin_session` suggests that authentication depends on authorization. In reality, authentication establishes WHO you are, then authorization checks WHAT you can do.
248
+
249
+ **Correct Pattern:**
250
+ ```
251
+ # Authenticate via session, then authorize admin role
252
+ GET /admin/dashboard auth=session role=admin
253
+ ```
254
+
255
+ ---
256
+
257
+ ## OWASP & Security Considerations
258
+
259
+ ### From OWASP Authentication Cheat Sheet
260
+
261
+ 1. **Fail Securely:** If all strategies fail → deny access (401/403)
262
+ 2. **Log Authentication Events:** Log which strategy succeeded
263
+ 3. **Least Privilege:** Authentication ≠ Authorization
264
+ 4. **Defense in Depth:** Multiple auth methods don't weaken security if implemented correctly
265
+
266
+ ### Security Notes for Multiple Strategies
267
+
268
+ - ✅ **SAFE:** `auth=session,apikey` - Different auth methods for different clients
269
+ - ⚠️ **CAREFUL:** Order matters - put more secure methods first
270
+ - ❌ **DANGEROUS:** Fallback to weaker auth if stronger fails (e.g., `auth=mfa,basic` could bypass MFA)
271
+
272
+ ### Dangerous Pattern Example
273
+
274
+ ```
275
+ # DANGEROUS: Could allow MFA bypass
276
+ auth=mfa,basic
277
+
278
+ # If MFA fails, falls back to basic auth → defeats MFA purpose
279
+ ```
280
+
281
+ **Safe Alternative:**
282
+ ```
283
+ # Use MFA strategy only (no fallback)
284
+ auth=mfa
285
+
286
+ # Or separate routes for different security levels
287
+ GET /high-security auth=mfa
288
+ GET /low-security auth=basic
289
+ ```
290
+
291
+ ### Logging and Monitoring
292
+
293
+ **Essential Security Logs:**
294
+ ```ruby
295
+ # Log successful authentication
296
+ Otto.structured_log(:info, "Authentication succeeded",
297
+ strategy: 'apikey',
298
+ strategies_tried: ['session', 'apikey'],
299
+ user_id: result.user_id
300
+ )
301
+
302
+ # Log authentication failures
303
+ Otto.structured_log(:warn, "Authentication failed",
304
+ strategies_tried: ['session', 'apikey', 'oauth'],
305
+ ip: env['REMOTE_ADDR'] # Already masked by IPPrivacyMiddleware
306
+ )
307
+ ```
308
+
309
+ **Why This Matters:**
310
+ - Detect authentication bypass attempts
311
+ - Identify broken integrations (clients using wrong auth method)
312
+ - Monitor for credential stuffing attacks
313
+
314
+ ---
315
+
316
+ ## Recommendation for Otto
317
+
318
+ ### YES, Multiple Strategies is Reasonable and Recommended
319
+
320
+ Based on industry standards (Warden, DRF, Passport), Otto SHOULD support:
321
+
322
+ ```
323
+ GET /api/data auth=session,apikey,oauth
324
+ ```
325
+
326
+ ### Semantics
327
+
328
+ - **OR logic:** Authenticate via session OR API key OR OAuth
329
+ - **First success wins:** Once authenticated, stop trying strategies
330
+ - **Fail if all fail:** Return 401 only if ALL strategies fail
331
+ - **Order matters:** Try left-to-right (most preferred first)
332
+
333
+ ### Why This is Better Than Alternatives
334
+
335
+ #### Alternative 1: Composite Strategies
336
+
337
+ ```ruby
338
+ otto.add_auth_strategy('session_or_apikey', CompositeStrategy.new(session, apikey))
339
+ ```
340
+
341
+ **Drawbacks:**
342
+ - ❌ Doesn't scale (need composite for each combination)
343
+ - ❌ Less clear in routes file
344
+ - ❌ Harder to maintain
345
+ - ❌ Not industry standard
346
+
347
+ #### Alternative 2: Multiple Routes
348
+
349
+ ```
350
+ GET /api/data/session auth=session
351
+ GET /api/data/apikey auth=apikey
352
+ ```
353
+
354
+ **Drawbacks:**
355
+ - ❌ Route explosion
356
+ - ❌ Breaks REST principles
357
+ - ❌ Client needs to know which endpoint to use
358
+ - ❌ Cache invalidation complexity (same resource, multiple URLs)
359
+
360
+ #### Recommended: Native Multi-Strategy Support
361
+
362
+ ```
363
+ GET /api/data auth=session,apikey
364
+ ```
365
+
366
+ **Benefits:**
367
+ - ✅ Matches industry standards (Warden, DRF, Passport)
368
+ - ✅ Clear, concise syntax
369
+ - ✅ Flexible for clients (use whatever auth method they have)
370
+ - ✅ Easy to add/remove strategies
371
+ - ✅ Single endpoint per resource (REST compliant)
372
+
373
+ ---
374
+
375
+ ## Implementation Pattern (Following Warden)
376
+
377
+ ### Proposed Route Parsing
378
+
379
+ ```ruby
380
+ # lib/otto/route_definition.rb
381
+ def auth_requirements
382
+ auth = option(:auth)
383
+ return [] unless auth
384
+
385
+ # Split on comma and strip whitespace
386
+ auth.split(',').map(&:strip)
387
+ end
388
+
389
+ # Keep backward compatibility
390
+ def auth_requirement
391
+ reqs = auth_requirements
392
+ reqs.empty? ? nil : reqs.first
393
+ end
394
+ ```
395
+
396
+ ### Proposed Authentication Flow
397
+
398
+ ```ruby
399
+ # lib/otto/security/authentication/route_auth_wrapper.rb
400
+ def call(env, extra_params = {})
401
+ auth_requirements = route_definition.auth_requirements # ['session', 'apikey']
402
+
403
+ return anonymous_result if auth_requirements.empty?
404
+
405
+ # Try each strategy in order (OR logic)
406
+ auth_requirements.each do |requirement|
407
+ strategy, strategy_name = get_strategy(requirement)
408
+ next unless strategy # Skip if not found
409
+
410
+ result = strategy.authenticate(env, requirement)
411
+
412
+ if result.success?
413
+ # First success wins
414
+ Otto.structured_log(:info, "Authentication succeeded",
415
+ Otto::LoggingHelpers.request_context(env).merge(
416
+ strategy: strategy_name,
417
+ tried: auth_requirements,
418
+ succeeded_with: strategy_name
419
+ )
420
+ )
421
+
422
+ # Set env and return successful result
423
+ env['otto.strategy_result'] = result
424
+ env['otto.user'] = result.user_context
425
+ env['rack.session'] = result.session_data if result.session_data
426
+ return wrapped_handler.call(env, extra_params)
427
+ end
428
+ end
429
+
430
+ # All strategies failed
431
+ Otto.structured_log(:warn, "Authentication failed",
432
+ Otto::LoggingHelpers.request_context(env).merge(
433
+ strategies_tried: auth_requirements
434
+ )
435
+ )
436
+
437
+ unauthorized_response(env, "Authentication required")
438
+ end
439
+ ```
440
+
441
+ ### Example Routes File
442
+
443
+ ```
444
+ # Organization Management API
445
+ # Supports both browser sessions and API keys
446
+
447
+ GET /orgs OrgAPI::ListOrgs auth=session,apikey response=json
448
+ POST /orgs OrgAPI::CreateOrg auth=session,apikey response=json
449
+ GET /orgs/:id OrgAPI::GetOrg auth=session,apikey response=json
450
+ PUT /orgs/:id OrgAPI::UpdateOrg auth=session,apikey response=json
451
+ DELETE /orgs/:id OrgAPI::DeleteOrg auth=session,apikey response=json
452
+
453
+ # Admin-only endpoints (session required for CSRF protection)
454
+ GET /admin/dashboard Admin::Dashboard auth=session role=admin
455
+ POST /admin/users/:id/ban Admin::BanUser auth=session role=admin
456
+
457
+ # Public API (anonymous or authenticated)
458
+ GET /public/stats Public::Stats auth=anonymous,session response=json
459
+ ```
460
+
461
+ ### Testing Strategy
462
+
463
+ ```ruby
464
+ # spec/otto/security/route_auth_wrapper_spec.rb
465
+ describe 'multiple auth strategies' do
466
+ it 'succeeds if first strategy succeeds' do
467
+ # auth=session,apikey - session succeeds
468
+ expect(response.status).to eq(200)
469
+ expect(logs).to include(strategy: 'session', succeeded_with: 'session')
470
+ end
471
+
472
+ it 'tries second strategy if first fails' do
473
+ # auth=session,apikey - session fails, apikey succeeds
474
+ expect(response.status).to eq(200)
475
+ expect(logs).to include(strategy: 'apikey', tried: ['session', 'apikey'])
476
+ end
477
+
478
+ it 'fails if all strategies fail' do
479
+ # auth=session,apikey - both fail
480
+ expect(response.status).to eq(401)
481
+ expect(logs).to include(strategies_tried: ['session', 'apikey'])
482
+ end
483
+
484
+ it 'respects strategy order' do
485
+ # auth=apikey,session - tries apikey first
486
+ expect(auth_attempts.first).to eq('apikey')
487
+ end
488
+
489
+ it 'handles missing strategies gracefully' do
490
+ # auth=session,unknown - unknown strategy doesn't exist
491
+ expect(response.status).to eq(200) # session succeeds
492
+ expect(logs).to include(strategy: 'session')
493
+ end
494
+ end
495
+ ```
496
+
497
+ ---
498
+
499
+ ## Summary
500
+
501
+ ### Answer: YES, supporting multiple strategies is the industry-standard approach.
502
+
503
+ - ✅ Warden (Ruby/Rack reference impl) supports it
504
+ - ✅ Django REST Framework supports it
505
+ - ✅ Passport.js supports it
506
+ - ✅ OWASP doesn't flag concerns with proper implementation
507
+ - ✅ Common use case: Supporting multiple client types (web + mobile + API)
508
+ - ✅ Otto's architecture (strategy pattern) is already well-positioned for this
509
+
510
+ ### Recommended Syntax
511
+
512
+ ```
513
+ auth=session,apikey,oauth # OR logic, left-to-right priority
514
+ ```
515
+
516
+ ### NOT Recommended
517
+
518
+ - Creating composite strategies for each combination
519
+ - Using route duplication
520
+ - Mixing authentication and authorization
521
+ - AND logic for multiple strategies
522
+ - Fallback to weaker auth methods
523
+
524
+ ### Implementation Effort
525
+
526
+ **Files to Modify:**
527
+ 1. `lib/otto/route_definition.rb` - Add `auth_requirements` method
528
+ 2. `lib/otto/security/authentication/route_auth_wrapper.rb` - Update `call` to try multiple strategies
529
+ 3. `spec/otto/security/route_auth_wrapper_spec.rb` - Add comprehensive tests
530
+
531
+ **Estimated Effort:** 4-6 hours including:
532
+ - Code changes (2 hours)
533
+ - Tests (2 hours)
534
+ - Edge case handling and documentation (1-2 hours)
535
+
536
+ **Complexity:** Medium - touches core authentication flow but well-isolated
537
+
538
+ ---
539
+
540
+ ## References
541
+
542
+ ### Official Documentation
543
+ - [Warden README](https://github.com/wardencommunity/warden/wiki) - Ruby Rack authentication framework
544
+ - [Django REST Framework - Authentication](https://www.django-rest-framework.org/api-guide/authentication/)
545
+ - [Passport.js Documentation](https://www.passportjs.org/concepts/authentication/strategies/)
546
+ - [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)
547
+
548
+ ### Key Insights from Research
549
+ - Devise (Rails' most popular auth library) is built on Warden
550
+ - Passport.js has 500+ authentication strategies available
551
+ - Django REST Framework's approach matches Warden's cascade pattern
552
+ - Industry consensus: Multiple strategies = OR logic, not AND
553
+
554
+ ### Related Topics
555
+ - Multi-Factor Authentication (MFA) - Multiple factors within ONE strategy
556
+ - Content Negotiation - Automatic strategy selection based on headers
557
+ - Rate Limiting - Different limits per authentication method
558
+ - CORS - Pre-flight requests and authentication