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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +251 -0
- data/README.md +18 -0
- data/docs/ADAPTERS.md +478 -0
- data/docs/ARCHITECTURE.md +222 -0
- data/docs/CONTENT.md +967 -0
- data/docs/PERFORMANCE.md +492 -0
- data/docs/PLUGIN_DEVELOPMENT.md +688 -0
- data/docs/USAGE.md +4998 -0
- data/docs/plugins/API_KEY_AUTH.md +232 -0
- data/docs/plugins/BASIC_AUTH.md +582 -0
- data/docs/plugins/BEARER_AUTH.md +394 -0
- data/docs/plugins/CACHE.md +369 -0
- data/docs/plugins/COMPRESSION.md +216 -0
- data/docs/plugins/COOKIES.md +30 -0
- data/docs/plugins/CORS.md +283 -0
- data/docs/plugins/CSRF.md +751 -0
- data/docs/plugins/ETAG.md +308 -0
- data/docs/plugins/FORM_PARSER.md +193 -0
- data/docs/plugins/HEALTH_CHECK.md +469 -0
- data/docs/plugins/JSON.md +291 -0
- data/docs/plugins/MULTIPART.md +427 -0
- data/docs/plugins/RATE_LIMITER.md +368 -0
- data/docs/plugins/REQUEST_ID.md +369 -0
- data/docs/plugins/REQUEST_LOGGER.md +151 -0
- data/docs/plugins/SECURITY.md +193 -0
- data/docs/plugins/SESSION.md +98 -0
- data/lib/aris/adapters/rack/adapter.rb +17 -2
- data/lib/aris/adapters/rack/request.rb +29 -11
- data/lib/aris/plugins/basic_auth.rb +3 -1
- data/lib/aris/plugins/cookies.rb +4 -32
- data/lib/aris/plugins/cors.rb +8 -1
- data/lib/aris/plugins/csrf.rb +63 -22
- data/lib/aris/plugins/flash.rb +3 -1
- data/lib/aris/plugins/form_parser.rb +52 -31
- data/lib/aris/plugins/multipart.rb +22 -2
- data/lib/aris/plugins/request_logger.rb +8 -1
- data/lib/aris/plugins/security_headers.rb +8 -1
- data/lib/aris/plugins/session.rb +150 -99
- data/lib/aris/response_helpers.rb +41 -0
- data/lib/aris/version.rb +2 -2
- 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).
|