aris 1.4.2 → 1.5.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +251 -0
  3. data/README.md +18 -0
  4. data/docs/ADAPTERS.md +478 -0
  5. data/docs/ARCHITECTURE.md +222 -0
  6. data/docs/CONTENT.md +967 -0
  7. data/docs/PERFORMANCE.md +492 -0
  8. data/docs/PLUGIN_DEVELOPMENT.md +688 -0
  9. data/docs/USAGE.md +4998 -0
  10. data/docs/plugins/API_KEY_AUTH.md +232 -0
  11. data/docs/plugins/BASIC_AUTH.md +582 -0
  12. data/docs/plugins/BEARER_AUTH.md +394 -0
  13. data/docs/plugins/CACHE.md +369 -0
  14. data/docs/plugins/COMPRESSION.md +216 -0
  15. data/docs/plugins/COOKIES.md +30 -0
  16. data/docs/plugins/CORS.md +283 -0
  17. data/docs/plugins/CSRF.md +751 -0
  18. data/docs/plugins/ETAG.md +308 -0
  19. data/docs/plugins/FORM_PARSER.md +193 -0
  20. data/docs/plugins/HEALTH_CHECK.md +469 -0
  21. data/docs/plugins/JSON.md +291 -0
  22. data/docs/plugins/MULTIPART.md +427 -0
  23. data/docs/plugins/RATE_LIMITER.md +368 -0
  24. data/docs/plugins/REQUEST_ID.md +369 -0
  25. data/docs/plugins/REQUEST_LOGGER.md +151 -0
  26. data/docs/plugins/SECURITY.md +193 -0
  27. data/docs/plugins/SESSION.md +98 -0
  28. data/lib/aris/adapters/rack/adapter.rb +17 -2
  29. data/lib/aris/adapters/rack/request.rb +29 -11
  30. data/lib/aris/plugins/basic_auth.rb +3 -1
  31. data/lib/aris/plugins/cookies.rb +4 -32
  32. data/lib/aris/plugins/cors.rb +8 -1
  33. data/lib/aris/plugins/csrf.rb +63 -22
  34. data/lib/aris/plugins/flash.rb +3 -1
  35. data/lib/aris/plugins/form_parser.rb +52 -31
  36. data/lib/aris/plugins/multipart.rb +22 -2
  37. data/lib/aris/plugins/request_logger.rb +8 -1
  38. data/lib/aris/plugins/security_headers.rb +8 -1
  39. data/lib/aris/plugins/session.rb +150 -99
  40. data/lib/aris/response_helpers.rb +41 -0
  41. data/lib/aris/version.rb +2 -2
  42. metadata +31 -3
@@ -0,0 +1,394 @@
1
+ # Bearer Token Authentication Plugin
2
+
3
+ Dead-simple Bearer token authentication for API endpoints.
4
+
5
+ ## Installation
6
+
7
+ ```ruby
8
+ # lib/aris.rb already includes this
9
+ require_relative 'aris/plugins/bearer_auth'
10
+ ```
11
+
12
+ ## Basic Usage
13
+
14
+ ### Simple Token Validation
15
+
16
+ Perfect for internal APIs or development:
17
+
18
+ ```ruby
19
+ # config/routes.rb
20
+ api_auth = Aris::Plugins::BearerAuth.build(
21
+ token: ENV['API_SECRET_TOKEN']
22
+ )
23
+
24
+ Aris.routes({
25
+ "api.example.com": {
26
+ use: [api_auth],
27
+ "/users": { get: { to: UsersHandler } },
28
+ "/posts": { get: { to: PostsHandler } }
29
+ }
30
+ })
31
+ ```
32
+
33
+ **Request:**
34
+ ```bash
35
+ curl -H "Authorization: Bearer your-secret-token" \
36
+ https://api.example.com/users
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Advanced Usage
42
+
43
+ ### Database Token Validation
44
+
45
+ Validate against your user/token database:
46
+
47
+ ```ruby
48
+ api_auth = Aris::Plugins::BearerAuth.build(
49
+ validator: ->(token) {
50
+ # Check if token exists and is valid
51
+ api_key = ApiKey.find_by(token: token, active: true)
52
+
53
+ if api_key && !api_key.expired?
54
+ # Optional: Track usage
55
+ api_key.update(last_used_at: Time.now)
56
+ true
57
+ else
58
+ false
59
+ end
60
+ },
61
+ realm: 'MyApp API v1'
62
+ )
63
+
64
+ Aris.routes({
65
+ "api.example.com": {
66
+ use: [api_auth],
67
+ "/data": { get: { to: DataHandler } }
68
+ }
69
+ })
70
+ ```
71
+
72
+ ---
73
+
74
+ ### Redis Token Store
75
+
76
+ For high-performance token validation:
77
+
78
+ ```ruby
79
+ require 'redis'
80
+ REDIS = Redis.new
81
+
82
+ api_auth = Aris::Plugins::BearerAuth.build(
83
+ validator: ->(token) {
84
+ # Check Redis cache first (fast!)
85
+ cached = REDIS.get("token:#{token}")
86
+ return cached == "valid" if cached
87
+
88
+ # Fallback to database
89
+ user = User.find_by(api_token: token)
90
+ if user
91
+ REDIS.setex("token:#{token}", 3600, "valid") # Cache for 1 hour
92
+ true
93
+ else
94
+ REDIS.setex("token:#{token}", 300, "invalid") # Cache misses too
95
+ false
96
+ end
97
+ }
98
+ )
99
+ ```
100
+
101
+ ---
102
+
103
+ ### JWT Token Validation
104
+
105
+ Integrate with JWT for stateless authentication:
106
+
107
+ ```ruby
108
+ require 'jwt'
109
+
110
+ jwt_auth = Aris::Plugins::BearerAuth.build(
111
+ validator: ->(token) {
112
+ begin
113
+ payload = JWT.decode(token, ENV['JWT_SECRET'], true, algorithm: 'HS256')
114
+
115
+ # Check expiration
116
+ exp = payload[0]['exp']
117
+ exp && exp > Time.now.to_i
118
+ rescue JWT::DecodeError, JWT::ExpiredSignature
119
+ false
120
+ end
121
+ },
122
+ realm: 'JWT API'
123
+ )
124
+
125
+ Aris.routes({
126
+ "api.example.com": {
127
+ use: [jwt_auth],
128
+ "/protected": { get: { to: ProtectedHandler } }
129
+ }
130
+ })
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Multiple Authentication Strategies
136
+
137
+ Different endpoints, different tokens:
138
+
139
+ ```ruby
140
+ # Admin API - super secret token
141
+ admin_auth = Aris::Plugins::BearerAuth.build(
142
+ token: ENV['ADMIN_SECRET'],
143
+ realm: 'Admin Panel'
144
+ )
145
+
146
+ # Public API - database validation
147
+ public_auth = Aris::Plugins::BearerAuth.build(
148
+ validator: ->(token) { ApiKey.valid?(token) },
149
+ realm: 'Public API'
150
+ )
151
+
152
+ # Partner API - partner-specific tokens
153
+ partner_auth = Aris::Plugins::BearerAuth.build(
154
+ validator: ->(token) {
155
+ Partner.find_by(api_key: token, status: 'active')
156
+ },
157
+ realm: 'Partner Integration'
158
+ )
159
+
160
+ Aris.routes({
161
+ "admin.myapp.com": {
162
+ use: [admin_auth],
163
+ "/dashboard": { get: { to: AdminDashboard } }
164
+ },
165
+ "api.myapp.com": {
166
+ use: [public_auth],
167
+ "/users": { get: { to: UsersAPI } }
168
+ },
169
+ "partner.myapp.com": {
170
+ use: [partner_auth],
171
+ "/webhooks": { post: { to: WebhookHandler } }
172
+ }
173
+ })
174
+ ```
175
+
176
+ ---
177
+
178
+ ## Accessing the Token in Handlers
179
+
180
+ The validated token is attached to the request:
181
+
182
+ ```ruby
183
+ class UsersHandler
184
+ def self.call(request, params)
185
+ # Access the bearer token
186
+ token = request.instance_variable_get(:@bearer_token)
187
+
188
+ # Use it to identify the user
189
+ api_key = ApiKey.find_by(token: token)
190
+ user = api_key.user
191
+
192
+ {
193
+ message: "Hello, #{user.name}!",
194
+ token_expires: api_key.expires_at
195
+ }
196
+ end
197
+ end
198
+ ```
199
+
200
+ ---
201
+
202
+ ## Combining with Other Plugins
203
+
204
+ Layer authentication with rate limiting and CORS:
205
+
206
+ ```ruby
207
+ cors = Aris::Plugins::Cors.build(origins: ['https://app.example.com'])
208
+ auth = Aris::Plugins::BearerAuth.build(validator: ->(t) { ApiKey.valid?(t) })
209
+ rate_limit = Aris::Plugins::RateLimiter.build(limit: 1000, window: 3600)
210
+
211
+ Aris.routes({
212
+ "api.example.com": {
213
+ use: [cors, auth, rate_limit], # Execute in order
214
+ "/data": { get: { to: DataHandler } }
215
+ }
216
+ })
217
+ ```
218
+
219
+ **Execution order matters:**
220
+ 1. CORS headers set first (for preflight)
221
+ 2. Auth validates token (fails fast if invalid)
222
+ 3. Rate limiter only runs for authenticated users (saves Redis calls)
223
+
224
+ ---
225
+
226
+ ## Configuration Options
227
+
228
+ | Option | Type | Required | Description |
229
+ |:---|:---|:---|:---|
230
+ | `token` | String | * | Static token to validate against |
231
+ | `validator` | Proc | * | Custom validation logic `(token) -> Boolean` |
232
+ | `realm` | String | No | Realm for WWW-Authenticate header (default: "API") |
233
+
234
+ **Note:** Must provide either `token` OR `validator`, not both.
235
+
236
+ ---
237
+
238
+ ## Error Responses
239
+
240
+ All errors return JSON with consistent format:
241
+
242
+ **401 Unauthorized:**
243
+ ```json
244
+ {
245
+ "error": "Unauthorized",
246
+ "message": "Invalid or expired token"
247
+ }
248
+ ```
249
+
250
+ **Headers:**
251
+ ```
252
+ HTTP/1.1 401 Unauthorized
253
+ content-type: application/json
254
+ WWW-Authenticate: Bearer realm="API"
255
+ ```
256
+
257
+ ---
258
+
259
+ ## Production Tips
260
+
261
+ ### 1. Use Environment Variables
262
+ ```ruby
263
+ # NEVER hardcode tokens in source code
264
+ auth = Aris::Plugins::BearerAuth.build(
265
+ token: ENV.fetch('API_SECRET_TOKEN') # Fails fast if missing
266
+ )
267
+ ```
268
+
269
+ ### 2. Cache Token Lookups
270
+ ```ruby
271
+ # Cache in Redis/Memcached to avoid DB hits
272
+ auth = Aris::Plugins::BearerAuth.build(
273
+ validator: ->(token) {
274
+ Rails.cache.fetch("token:#{token}", expires_in: 5.minutes) do
275
+ ApiKey.exists?(token: token, active: true)
276
+ end
277
+ }
278
+ )
279
+ ```
280
+
281
+ ### 3. Log Failed Attempts
282
+ ```ruby
283
+ auth = Aris::Plugins::BearerAuth.build(
284
+ validator: ->(token) {
285
+ valid = ApiKey.valid?(token)
286
+ unless valid
287
+ Rails.logger.warn("Failed auth attempt with token: #{token[0..8]}...")
288
+ Metrics.increment('api.auth.failed')
289
+ end
290
+ valid
291
+ }
292
+ )
293
+ ```
294
+
295
+ ### 4. Rate Limit by Token
296
+ ```ruby
297
+ # Combine with rate limiter for per-token limits
298
+ token_rate_limit = Aris::Plugins::RateLimiter.build(
299
+ key_extractor: ->(request) {
300
+ request.instance_variable_get(:@bearer_token)
301
+ }
302
+ )
303
+
304
+ Aris.routes({
305
+ "api.example.com": {
306
+ use: [auth, token_rate_limit], # Auth first, then rate limit by token
307
+ "/data": { get: { to: DataHandler } }
308
+ }
309
+ })
310
+ ```
311
+
312
+ ---
313
+
314
+ ## Testing
315
+
316
+ ```ruby
317
+ # test/integration/api_auth_test.rb
318
+ class ApiAuthTest < Minitest::Test
319
+ def test_valid_token_grants_access
320
+ auth = Aris::Plugins::BearerAuth.build(token: 'test-token-123')
321
+
322
+ Aris.routes({
323
+ "api.test": {
324
+ use: [auth],
325
+ "/data": { get: { to: DataHandler } }
326
+ }
327
+ })
328
+
329
+ app = Aris::Adapters::RackApp.new
330
+ env = {
331
+ 'REQUEST_METHOD' => 'GET',
332
+ 'PATH_INFO' => '/data',
333
+ 'HTTP_HOST' => 'api.test',
334
+ 'HTTP_AUTHORIZATION' => 'Bearer test-token-123',
335
+ 'rack.input' => StringIO.new('')
336
+ }
337
+
338
+ status, _, body = app.call(env)
339
+ assert_equal 200, status
340
+ end
341
+ end
342
+ ```
343
+
344
+ ---
345
+
346
+ ## Common Patterns
347
+
348
+ ### Health Check Bypass
349
+ ```ruby
350
+ Aris.routes({
351
+ "api.example.com": {
352
+ use: [auth],
353
+
354
+ "/users": { get: { to: UsersHandler } },
355
+
356
+ "/health": {
357
+ use: nil, # Clear auth for health checks
358
+ get: { to: HealthHandler }
359
+ }
360
+ }
361
+ })
362
+ ```
363
+
364
+ ### Scope-Specific Auth
365
+ ```ruby
366
+ Aris.routes({
367
+ "example.com": {
368
+ "/public": {
369
+ # No auth
370
+ "/blog": { get: { to: BlogHandler } }
371
+ },
372
+ "/api": {
373
+ use: [api_auth], # Auth only for /api/* routes
374
+ "/users": { get: { to: UsersHandler } }
375
+ }
376
+ }
377
+ })
378
+ ```
379
+
380
+ ---
381
+
382
+ ## Security Notes
383
+
384
+ - ✅ Always use HTTPS in production (tokens sent in headers)
385
+ - ✅ Rotate tokens regularly
386
+ - ✅ Use different tokens for different environments
387
+ - ✅ Log failed authentication attempts
388
+ - ✅ Set short expiration times for JWT tokens
389
+ - ❌ Never log full tokens (log only first 8 chars)
390
+ - ❌ Never commit tokens to version control
391
+
392
+ ---
393
+
394
+ Need help? Check out the [full plugin development guide](../docs/plugin-development.md).