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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/.github/workflows/claude-code-review.yml +1 -1
- data/.github/workflows/claude.yml +1 -1
- data/.github/workflows/code-smells.yml +2 -2
- data/CHANGELOG.rst +54 -35
- data/Gemfile.lock +6 -6
- data/README.md +20 -0
- data/docs/.gitignore +2 -0
- data/docs/modern-authentication-authorization-landscape.md +558 -0
- data/docs/multi-strategy-authentication-design.md +1401 -0
- data/lib/otto/core/error_handler.rb +19 -8
- data/lib/otto/core/freezable.rb +0 -2
- data/lib/otto/core/middleware_stack.rb +12 -8
- data/lib/otto/core/router.rb +25 -31
- data/lib/otto/errors.rb +92 -0
- data/lib/otto/mcp/rate_limiting.rb +6 -2
- data/lib/otto/mcp/schema_validation.rb +1 -1
- data/lib/otto/response_handlers/json.rb +1 -3
- data/lib/otto/response_handlers/view.rb +1 -1
- data/lib/otto/route_handlers/base.rb +86 -1
- data/lib/otto/route_handlers/class_method.rb +9 -67
- data/lib/otto/route_handlers/instance_method.rb +10 -57
- data/lib/otto/route_handlers/logic_class.rb +85 -90
- data/lib/otto/security/authentication/auth_strategy.rb +2 -2
- data/lib/otto/security/authentication/strategy_result.rb +9 -9
- data/lib/otto/security/authorization_error.rb +1 -1
- data/lib/otto/security/config.rb +3 -3
- data/lib/otto/security/rate_limiter.rb +7 -3
- data/lib/otto/version.rb +1 -1
- data/lib/otto.rb +47 -3
- metadata +4 -1
|
@@ -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
|