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,582 @@
1
+ # Basic Authentication Plugin
2
+
3
+ HTTP Basic Authentication for protecting admin panels, staging environments, and simple protected resources.
4
+
5
+ ## Installation
6
+
7
+ ```ruby
8
+ # lib/aris.rb already includes this
9
+ require_relative 'aris/plugins/basic_auth'
10
+ ```
11
+
12
+ ## Basic Usage
13
+
14
+ ### Simple Username/Password
15
+
16
+ Perfect for admin panels or staging environments:
17
+
18
+ ```ruby
19
+ # config/routes.rb
20
+ admin_auth = Aris::Plugins::BasicAuth.build(
21
+ username: ENV['ADMIN_USERNAME'],
22
+ password: ENV['ADMIN_PASSWORD'],
23
+ realm: 'Admin Area'
24
+ )
25
+
26
+ Aris.routes({
27
+ "admin.example.com": {
28
+ use: [admin_auth],
29
+ "/dashboard": { get: { to: DashboardHandler } },
30
+ "/users": { get: { to: AdminUsersHandler } }
31
+ }
32
+ })
33
+ ```
34
+
35
+ **Browser behavior:** Users will see a login prompt automatically.
36
+
37
+ **cURL:**
38
+ ```bash
39
+ curl -u admin:secret123 https://admin.example.com/dashboard
40
+ # or
41
+ curl -H "Authorization: Basic YWRtaW46c2VjcmV0MTIz" \
42
+ https://admin.example.com/dashboard
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Advanced Usage
48
+
49
+ ### Database User Validation
50
+
51
+ Validate against your user database:
52
+
53
+ ```ruby
54
+ admin_auth = Aris::Plugins::BasicAuth.build(
55
+ validator: ->(username, password) {
56
+ user = User.find_by(username: username, role: 'admin')
57
+
58
+ if user && user.authenticate(password)
59
+ # Optional: Log successful login
60
+ LoginLog.create(user: user, timestamp: Time.now)
61
+ true
62
+ else
63
+ # Optional: Log failed attempt
64
+ Rails.logger.warn("Failed login attempt for: #{username}")
65
+ false
66
+ end
67
+ },
68
+ realm: 'Admin Panel'
69
+ )
70
+
71
+ Aris.routes({
72
+ "admin.example.com": {
73
+ use: [admin_auth],
74
+ "/dashboard": { get: { to: DashboardHandler } }
75
+ }
76
+ })
77
+ ```
78
+
79
+ ---
80
+
81
+ ### Bcrypt Password Hashing
82
+
83
+ Secure password validation with bcrypt:
84
+
85
+ ```ruby
86
+ require 'bcrypt'
87
+
88
+ # Store hashed passwords in your database
89
+ # User.create(username: 'admin', password_hash: BCrypt::Password.create('secret'))
90
+
91
+ admin_auth = Aris::Plugins::BasicAuth.build(
92
+ validator: ->(username, password) {
93
+ user = User.find_by(username: username)
94
+ return false unless user
95
+
96
+ # BCrypt comparison (constant-time)
97
+ BCrypt::Password.new(user.password_hash) == password
98
+ },
99
+ realm: 'Secure Admin'
100
+ )
101
+ ```
102
+
103
+ ---
104
+
105
+ ### LDAP/Active Directory Integration
106
+
107
+ Authenticate against LDAP:
108
+
109
+ ```ruby
110
+ require 'net/ldap'
111
+
112
+ ldap_auth = Aris::Plugins::BasicAuth.build(
113
+ validator: ->(username, password) {
114
+ ldap = Net::LDAP.new(
115
+ host: ENV['LDAP_HOST'],
116
+ port: 389,
117
+ auth: {
118
+ method: :simple,
119
+ username: "cn=#{username},dc=example,dc=com",
120
+ password: password
121
+ }
122
+ )
123
+
124
+ ldap.bind # Returns true if credentials valid
125
+ },
126
+ realm: 'Corporate Login'
127
+ )
128
+
129
+ Aris.routes({
130
+ "intranet.company.com": {
131
+ use: [ldap_auth],
132
+ "/": { get: { to: IntranetHome } }
133
+ }
134
+ })
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Multiple Authentication Strategies
140
+
141
+ Different areas, different credentials:
142
+
143
+ ```ruby
144
+ # Admin panel - super admin only
145
+ admin_auth = Aris::Plugins::BasicAuth.build(
146
+ username: ENV['ADMIN_USER'],
147
+ password: ENV['ADMIN_PASS'],
148
+ realm: 'Admin Panel'
149
+ )
150
+
151
+ # Staging environment - team access
152
+ staging_auth = Aris::Plugins::BasicAuth.build(
153
+ validator: ->(username, password) {
154
+ # Multiple valid username/password pairs
155
+ credentials = {
156
+ 'dev1' => 'devpass1',
157
+ 'dev2' => 'devpass2',
158
+ 'qa' => 'qapass'
159
+ }
160
+ credentials[username] == password
161
+ },
162
+ realm: 'Staging Environment'
163
+ )
164
+
165
+ # Partner API - partner credentials
166
+ partner_auth = Aris::Plugins::BasicAuth.build(
167
+ validator: ->(username, password) {
168
+ Partner.authenticate(username, password)
169
+ },
170
+ realm: 'Partner Portal'
171
+ )
172
+
173
+ Aris.routes({
174
+ "admin.myapp.com": {
175
+ use: [admin_auth],
176
+ "/dashboard": { get: { to: AdminDashboard } }
177
+ },
178
+ "staging.myapp.com": {
179
+ use: [staging_auth],
180
+ "/": { get: { to: StagingHome } }
181
+ },
182
+ "partners.myapp.com": {
183
+ use: [partner_auth],
184
+ "/api": { get: { to: PartnerAPI } }
185
+ }
186
+ })
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Accessing Credentials in Handlers
192
+
193
+ The authenticated username is attached to the request:
194
+
195
+ ```ruby
196
+ class DashboardHandler
197
+ def self.call(request, params)
198
+ # Access the authenticated username
199
+ username = request.instance_variable_get(:@current_user)
200
+
201
+ # Load user data
202
+ user = User.find_by(username: username)
203
+
204
+ {
205
+ message: "Welcome to your dashboard, #{user.full_name}!",
206
+ last_login: user.last_login_at
207
+ }
208
+ end
209
+ end
210
+ ```
211
+
212
+ ---
213
+
214
+ ## Combining with Other Plugins
215
+
216
+ Layer Basic Auth with other security measures:
217
+
218
+ ```ruby
219
+ basic_auth = Aris::Plugins::BasicAuth.build(
220
+ username: ENV['ADMIN_USER'],
221
+ password: ENV['ADMIN_PASS']
222
+ )
223
+
224
+ csrf = Aris::Plugins::CsrfTokenGenerator.new
225
+ rate_limit = Aris::Plugins::RateLimiter.build(limit: 100, window: 3600)
226
+
227
+ Aris.routes({
228
+ "admin.example.com": {
229
+ use: [basic_auth, csrf, rate_limit], # Execute in order
230
+ "/dashboard": { get: { to: DashboardHandler } }
231
+ }
232
+ })
233
+ ```
234
+
235
+ **Execution order:**
236
+ 1. Basic Auth validates credentials (fails fast if invalid)
237
+ 2. CSRF generates token for forms
238
+ 3. Rate limiter prevents brute force attacks
239
+
240
+ ---
241
+
242
+ ## Configuration Options
243
+
244
+ | Option | Type | Required | Description |
245
+ |:---|:---|:---|:---|
246
+ | `username` | String | * | Static username to validate against |
247
+ | `password` | String | * | Static password to validate against |
248
+ | `validator` | Proc | * | Custom validation logic `(username, password) -> Boolean` |
249
+ | `realm` | String | No | Realm for WWW-Authenticate header (default: "Restricted Area") |
250
+
251
+ **Note:** Must provide either (`username` AND `password`) OR `validator`, not both.
252
+
253
+ ---
254
+
255
+ ## Error Responses
256
+
257
+ **401 Unauthorized:**
258
+ ```
259
+ HTTP/1.1 401 Unauthorized
260
+ content-type: text/plain
261
+ WWW-Authenticate: Basic realm="Admin Area"
262
+
263
+ Invalid username or password
264
+ ```
265
+
266
+ **Browsers display a login dialog automatically** when they receive a 401 with `WWW-Authenticate: Basic`.
267
+
268
+ ---
269
+
270
+ ## Production Tips
271
+
272
+ ### 1. Always Use HTTPS
273
+
274
+ Basic Auth sends credentials in Base64 (easily decoded). **Always use HTTPS in production.**
275
+
276
+ ```ruby
277
+ # In production config
278
+ config.force_ssl = true # Rails
279
+ # or configure your web server to enforce HTTPS
280
+ ```
281
+
282
+ ### 2. Use Environment Variables
283
+
284
+ ```ruby
285
+ # NEVER hardcode credentials
286
+ auth = Aris::Plugins::BasicAuth.build(
287
+ username: ENV.fetch('BASIC_AUTH_USER'),
288
+ password: ENV.fetch('BASIC_AUTH_PASS')
289
+ )
290
+ ```
291
+
292
+ ### 3. Rate Limit Login Attempts
293
+
294
+ ```ruby
295
+ # Prevent brute force attacks
296
+ login_rate_limit = Aris::Plugins::RateLimiter.build(
297
+ limit: 5,
298
+ window: 300, # 5 attempts per 5 minutes
299
+ key_extractor: ->(request) {
300
+ # Rate limit by IP address
301
+ request.headers['REMOTE_ADDR']
302
+ }
303
+ )
304
+
305
+ Aris.routes({
306
+ "admin.example.com": {
307
+ use: [login_rate_limit, admin_auth], # Rate limit BEFORE auth
308
+ "/dashboard": { get: { to: DashboardHandler } }
309
+ }
310
+ })
311
+ ```
312
+
313
+ ### 4. Log Failed Attempts
314
+
315
+ ```ruby
316
+ auth = Aris::Plugins::BasicAuth.build(
317
+ validator: ->(username, password) {
318
+ valid = validate_credentials(username, password)
319
+
320
+ unless valid
321
+ Rails.logger.warn("Failed login: #{username} from #{request.ip}")
322
+ Metrics.increment('admin.login.failed', tags: ["username:#{username}"])
323
+ end
324
+
325
+ valid
326
+ }
327
+ )
328
+ ```
329
+
330
+ ### 5. Use Strong Passwords
331
+
332
+ ```ruby
333
+ # Validate password strength
334
+ auth = Aris::Plugins::BasicAuth.build(
335
+ validator: ->(username, password) {
336
+ user = User.find_by(username: username)
337
+ return false unless user
338
+
339
+ # Check password AND ensure it meets complexity requirements
340
+ user.authenticate(password) && user.password_meets_requirements?
341
+ }
342
+ )
343
+ ```
344
+
345
+ ### 6. Different Credentials Per Environment
346
+
347
+ ```ruby
348
+ # config/environments/staging.rb
349
+ STAGING_AUTH = Aris::Plugins::BasicAuth.build(
350
+ username: ENV['STAGING_USER'],
351
+ password: ENV['STAGING_PASS']
352
+ )
353
+
354
+ # config/environments/production.rb
355
+ ADMIN_AUTH = Aris::Plugins::BasicAuth.build(
356
+ validator: ->(u, p) { AdminUser.authenticate(u, p) }
357
+ )
358
+ ```
359
+
360
+ ---
361
+
362
+ ## Testing
363
+
364
+ ```ruby
365
+ # test/integration/basic_auth_test.rb
366
+ require 'base64'
367
+
368
+ class BasicAuthTest < Minitest::Test
369
+ def test_valid_credentials_grant_access
370
+ auth = Aris::Plugins::BasicAuth.build(
371
+ username: 'admin',
372
+ password: 'secret123'
373
+ )
374
+
375
+ Aris.routes({
376
+ "admin.test": {
377
+ use: [auth],
378
+ "/dashboard": { get: { to: DashboardHandler } }
379
+ }
380
+ })
381
+
382
+ app = Aris::Adapters::RackApp.new
383
+
384
+ # Encode credentials
385
+ credentials = Base64.strict_encode64("admin:secret123")
386
+
387
+ env = {
388
+ 'REQUEST_METHOD' => 'GET',
389
+ 'PATH_INFO' => '/dashboard',
390
+ 'HTTP_HOST' => 'admin.test',
391
+ 'HTTP_AUTHORIZATION' => "Basic #{credentials}",
392
+ 'rack.input' => StringIO.new('')
393
+ }
394
+
395
+ status, _, body = app.call(env)
396
+ assert_equal 200, status
397
+ end
398
+
399
+ def test_invalid_credentials_denied
400
+ auth = Aris::Plugins::BasicAuth.build(
401
+ username: 'admin',
402
+ password: 'secret123'
403
+ )
404
+
405
+ Aris.routes({
406
+ "admin.test": {
407
+ use: [auth],
408
+ "/dashboard": { get: { to: DashboardHandler } }
409
+ }
410
+ })
411
+
412
+ app = Aris::Adapters::RackApp.new
413
+ credentials = Base64.strict_encode64("admin:wrongpass")
414
+
415
+ env = {
416
+ 'REQUEST_METHOD' => 'GET',
417
+ 'PATH_INFO' => '/dashboard',
418
+ 'HTTP_HOST' => 'admin.test',
419
+ 'HTTP_AUTHORIZATION' => "Basic #{credentials}",
420
+ 'rack.input' => StringIO.new('')
421
+ }
422
+
423
+ status, _, _ = app.call(env)
424
+ assert_equal 401, status
425
+ end
426
+ end
427
+ ```
428
+
429
+ ---
430
+
431
+ ## Common Patterns
432
+
433
+ ### Protect Entire Staging Environment
434
+
435
+ ```ruby
436
+ # Only protect in staging
437
+ if ENV['RACK_ENV'] == 'staging'
438
+ staging_auth = Aris::Plugins::BasicAuth.build(
439
+ username: ENV['STAGING_USER'],
440
+ password: ENV['STAGING_PASS'],
441
+ realm: 'Staging Environment'
442
+ )
443
+
444
+ Aris.routes({
445
+ "staging.myapp.com": {
446
+ use: [staging_auth], # Everything behind auth
447
+ "/": { get: { to: HomeHandler } },
448
+ "/api": { get: { to: ApiHandler } }
449
+ }
450
+ })
451
+ end
452
+ ```
453
+
454
+ ### Public Health Check
455
+
456
+ ```ruby
457
+ Aris.routes({
458
+ "admin.example.com": {
459
+ use: [admin_auth],
460
+
461
+ "/dashboard": { get: { to: DashboardHandler } },
462
+
463
+ "/health": {
464
+ use: nil, # Clear auth for health checks
465
+ get: { to: HealthHandler }
466
+ }
467
+ }
468
+ })
469
+ ```
470
+
471
+ ### Layered Authentication
472
+
473
+ ```ruby
474
+ # Basic Auth for staging, then Bearer for API
475
+ staging_auth = Aris::Plugins::BasicAuth.build(
476
+ username: ENV['STAGING_USER'],
477
+ password: ENV['STAGING_PASS']
478
+ )
479
+
480
+ api_auth = Aris::Plugins::BearerAuth.build(
481
+ validator: ->(token) { ApiKey.valid?(token) }
482
+ )
483
+
484
+ Aris.routes({
485
+ "staging-api.example.com": {
486
+ use: [staging_auth, api_auth], # Both required!
487
+ "/data": { get: { to: DataHandler } }
488
+ }
489
+ })
490
+ ```
491
+
492
+ ### Admin Panel with Role Check
493
+
494
+ ```ruby
495
+ admin_auth = Aris::Plugins::BasicAuth.build(
496
+ validator: ->(username, password) {
497
+ user = User.find_by(username: username)
498
+
499
+ # Check password AND role
500
+ user &&
501
+ user.authenticate(password) &&
502
+ user.role == 'admin'
503
+ }
504
+ )
505
+ ```
506
+
507
+ ---
508
+
509
+ ## Limitations
510
+
511
+ ### Colons in Usernames
512
+
513
+ Basic Auth uses `:` as the delimiter between username and password. **Usernames containing colons are not supported** and will be truncated at the first colon character.
514
+
515
+ ```ruby
516
+ # ❌ Won't work correctly
517
+ username: "user:name" # Splits into username="user", password="name"
518
+
519
+ # ✅ Use these alternatives instead
520
+ username: "user_name"
521
+ username: "username"
522
+ ```
523
+
524
+ If you need special characters in usernames, consider using **Bearer Token Auth** instead.
525
+
526
+ ### Browser Logout
527
+
528
+ Browsers cache Basic Auth credentials and don't provide a standard "logout" mechanism. Users must:
529
+ - Close all browser windows/tabs
530
+ - Clear browser cache
531
+ - Use browser's "forget this password" feature
532
+
533
+ For better UX with logout functionality, consider **cookie-based session auth** or **Bearer tokens**.
534
+
535
+ ### Credential Exposure
536
+
537
+ Credentials are sent with **every request** in the Authorization header. While Base64 encoded, they're easily decoded. **Always use HTTPS** in production.
538
+
539
+ ---
540
+
541
+ ## Security Checklist
542
+
543
+ - ✅ Use HTTPS everywhere (Basic Auth is insecure over HTTP)
544
+ - ✅ Use environment variables for credentials
545
+ - ✅ Use bcrypt or similar for password hashing
546
+ - ✅ Rate limit authentication attempts
547
+ - ✅ Log failed login attempts
548
+ - ✅ Use strong passwords (12+ characters, mixed case, numbers, symbols)
549
+ - ✅ Rotate passwords regularly
550
+ - ✅ Different credentials per environment
551
+ - ❌ Never commit credentials to version control
552
+ - ❌ Never log full credentials (only log username)
553
+ - ❌ Never use Basic Auth as your primary user authentication system
554
+
555
+ ---
556
+
557
+ ## When to Use Basic Auth
558
+
559
+ **✅ Good for:**
560
+ - Admin panels with limited users
561
+ - Staging/preview environments
562
+ - Internal tools and dashboards
563
+ - CI/CD webhook endpoints
564
+ - Quick prototyping
565
+ - Adding a second layer of security
566
+
567
+ **❌ Not ideal for:**
568
+ - Primary user authentication
569
+ - Public-facing applications
570
+ - Mobile apps (credentials stored on device)
571
+ - APIs with many users
572
+ - Applications requiring granular permissions
573
+
574
+ **Better alternatives:**
575
+ - **Bearer Token Auth** - For APIs and mobile apps
576
+ - **JWT** - For stateless authentication
577
+ - **OAuth2** - For third-party integrations
578
+ - **Session Cookies** - For traditional web apps
579
+
580
+ ---
581
+
582
+ Need help? Check out the [full plugin development guide](../docs/plugin-development.md).