veri 0.3.1 → 1.0.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 +29 -0
- data/README.md +151 -38
- data/lib/generators/veri/templates/add_veri_authentication.rb.erb +1 -0
- data/lib/veri/configuration.rb +1 -0
- data/lib/veri/controllers/concerns/authentication.rb +24 -7
- data/lib/veri/helpers/migration_helpers.rb +20 -0
- data/lib/veri/inputs/hashing_algorithm.rb +1 -1
- data/lib/veri/inputs/tenant.rb +33 -0
- data/lib/veri/models/concerns/authenticatable.rb +4 -1
- data/lib/veri/models/session.rb +31 -30
- data/lib/veri/password/pbkdf2.rb +47 -0
- data/lib/veri/railtie.rb +20 -0
- data/lib/veri/version.rb +1 -1
- data/lib/veri.rb +4 -0
- data/veri.gemspec +2 -2
- metadata +10 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c4434399cb0f0e3081ff2ccb8e72df12623e74f6f2cd43f318d50ae483845be6
|
4
|
+
data.tar.gz: 194dec0866793d8f2bc906d2830f8c9a4fb37e7d8a5c7f5c58bec7d0631abc51
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2e5dbe8a582d077ef157f8c16d3ec5c21ac24b501e323ed4bf11f577fe436900dffdc1a40814011c78f969962c97ca0b931cd5684253578ecb4804b994016dca
|
7
|
+
data.tar.gz: ba78fb8d71fd4dffdb527add9759f56d84ca9772535897d356f4a4a9f1302f149d3533355e8a86a518b09f179c638313dcdf2b9932c82fd7e8a427457a1b3111
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,32 @@
|
|
1
|
+
## v1.0.0
|
2
|
+
|
3
|
+
### Breaking
|
4
|
+
|
5
|
+
- Dropped support for Rails 7.1
|
6
|
+
|
7
|
+
### Features
|
8
|
+
|
9
|
+
- Added support for pbkdf2 password hashing algorithm
|
10
|
+
|
11
|
+
### Bugs
|
12
|
+
|
13
|
+
- Fixed error raised on Rails console commands when the database was not yet set up
|
14
|
+
|
15
|
+
## v0.4.0
|
16
|
+
|
17
|
+
### Breaking
|
18
|
+
|
19
|
+
- Changed `veri_sessions` table to support multi-tenancy
|
20
|
+
- Renamed `revert_to_true_identity` session method to `to_true_identity`
|
21
|
+
- Session method `prune` no longer accepts a user argument
|
22
|
+
- Session method `terminate_all` no longer accepts a user argument
|
23
|
+
|
24
|
+
### Features
|
25
|
+
|
26
|
+
- Added multi-tenancy support
|
27
|
+
- Added session scopes to fetch active, expired, and inactive sessions
|
28
|
+
- Added user scopes to fetch locked and unlocked users
|
29
|
+
|
1
30
|
## v0.3.1
|
2
31
|
|
3
32
|
### Misc
|
data/README.md
CHANGED
@@ -1,21 +1,19 @@
|
|
1
1
|
# Veri: Minimal Authentication Framework for Rails
|
2
2
|
|
3
3
|
[](http://badge.fury.io/rb/veri)
|
4
|
-
[](https://github.com/enjaku4/veri/actions/workflows/ci.yml)
|
5
5
|
|
6
6
|
Veri is a cookie-based authentication library for Ruby on Rails that provides essential authentication building blocks without imposing business logic. Unlike full-featured solutions, Veri gives you complete control over your authentication flow while handling the complex underlying mechanics of secure password storage and session management.
|
7
7
|
|
8
8
|
**Key Features:**
|
9
9
|
|
10
10
|
- Cookie-based authentication with database-stored sessions
|
11
|
-
- Multiple password hashing algorithms (argon2, bcrypt, scrypt)
|
11
|
+
- Multiple password hashing algorithms (argon2, bcrypt, pbkdf2, scrypt)
|
12
12
|
- Granular session management and control
|
13
13
|
- Return path handling
|
14
14
|
- User impersonation feature
|
15
15
|
- Account lockout functionality
|
16
|
-
|
17
|
-
> ⚠️ **Development Notice**<br>
|
18
|
-
> Veri is functional but in early development. Breaking changes may occur in minor releases until v1.0!
|
16
|
+
- Multi-tenancy support
|
19
17
|
|
20
18
|
## Table of Contents
|
21
19
|
|
@@ -26,6 +24,7 @@ Veri is a cookie-based authentication library for Ruby on Rails that provides es
|
|
26
24
|
- [Controller Integration](#controller-integration)
|
27
25
|
- [Authentication Sessions](#authentication-sessions)
|
28
26
|
- [Account Lockout](#account-lockout)
|
27
|
+
- [Multi-Tenancy](#multi-tenancy)
|
29
28
|
- [View Helpers](#view-helpers)
|
30
29
|
- [Testing](#testing)
|
31
30
|
|
@@ -71,7 +70,7 @@ If customization is required, configure Veri in an initializer:
|
|
71
70
|
```rb
|
72
71
|
# These are the default values; you can change them as needed
|
73
72
|
Veri.configure do |config|
|
74
|
-
config.hashing_algorithm = :argon2 # Password hashing algorithm (:argon2, :bcrypt, or :scrypt)
|
73
|
+
config.hashing_algorithm = :argon2 # Password hashing algorithm (:argon2, :bcrypt, :pbkdf2, or :scrypt)
|
75
74
|
config.inactive_session_lifetime = nil # Session inactivity timeout (nil means sessions never expire due to inactivity)
|
76
75
|
config.total_session_lifetime = 14.days # Maximum session duration regardless of activity
|
77
76
|
config.user_model_name = "User" # Your user model name
|
@@ -84,10 +83,10 @@ Your user model is automatically extended with password management methods:
|
|
84
83
|
|
85
84
|
```rb
|
86
85
|
# Set or update a password
|
87
|
-
user.update_password("
|
86
|
+
user.update_password("password")
|
88
87
|
|
89
88
|
# Verify a password
|
90
|
-
user.verify_password("
|
89
|
+
user.verify_password("password")
|
91
90
|
```
|
92
91
|
|
93
92
|
## Controller Integration
|
@@ -113,6 +112,22 @@ end
|
|
113
112
|
This is a simplified example of how to use Veri's authentication methods:
|
114
113
|
|
115
114
|
```rb
|
115
|
+
class RegistrationsController < ApplicationController
|
116
|
+
skip_authentication
|
117
|
+
|
118
|
+
def create
|
119
|
+
user = User.new(user_params)
|
120
|
+
|
121
|
+
if user.valid?
|
122
|
+
user.update_password(user_params[:password])
|
123
|
+
log_in(user)
|
124
|
+
redirect_to dashboard_path
|
125
|
+
else
|
126
|
+
render :new, status: :unprocessable_content
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
116
131
|
class SessionsController < ApplicationController
|
117
132
|
skip_authentication except: [:destroy]
|
118
133
|
|
@@ -124,7 +139,7 @@ class SessionsController < ApplicationController
|
|
124
139
|
redirect_to return_path || dashboard_path
|
125
140
|
else
|
126
141
|
flash.now[:alert] = "Invalid credentials"
|
127
|
-
render :new, status: :
|
142
|
+
render :new, status: :unprocessable_content
|
128
143
|
end
|
129
144
|
end
|
130
145
|
|
@@ -135,14 +150,27 @@ class SessionsController < ApplicationController
|
|
135
150
|
end
|
136
151
|
```
|
137
152
|
|
138
|
-
Available methods:
|
153
|
+
Available controller methods:
|
154
|
+
|
155
|
+
```rb
|
156
|
+
# Returns authenticated user or nil
|
157
|
+
current_user
|
158
|
+
|
159
|
+
# Returns true if user is authenticated
|
160
|
+
logged_in?
|
161
|
+
|
162
|
+
# Authenticates user and creates session, returns true on success or false if account is locked
|
163
|
+
log_in(user)
|
164
|
+
|
165
|
+
# Terminates current session
|
166
|
+
log_out
|
167
|
+
|
168
|
+
# Returns path user was trying to access before authentication, if any
|
169
|
+
return_path
|
139
170
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
- `log_out` - Terminates current session
|
144
|
-
- `return_path` - Returns path user was accessing before authentication
|
145
|
-
- `current_session` - Returns current authentication session
|
171
|
+
# Returns current authentication session
|
172
|
+
current_session
|
173
|
+
```
|
146
174
|
|
147
175
|
### User Impersonation (Shapeshifting)
|
148
176
|
|
@@ -159,7 +187,7 @@ module Admin
|
|
159
187
|
|
160
188
|
def destroy
|
161
189
|
original_user = current_session.true_identity
|
162
|
-
current_session.
|
190
|
+
current_session.to_true_identity
|
163
191
|
redirect_to admin_dashboard_path, notice: "Returned to #{original_user.name}"
|
164
192
|
end
|
165
193
|
end
|
@@ -168,14 +196,26 @@ end
|
|
168
196
|
|
169
197
|
Available session methods:
|
170
198
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
199
|
+
```rb
|
200
|
+
# Assume another user's identity (maintains original identity)
|
201
|
+
session.shapeshift(user)
|
202
|
+
|
203
|
+
# Return to original identity
|
204
|
+
session.to_true_identity
|
205
|
+
|
206
|
+
# Returns true if currently shapeshifted
|
207
|
+
session.shapeshifted?
|
208
|
+
|
209
|
+
# Returns original user when shapeshifted, otherwise current user
|
210
|
+
session.true_identity
|
211
|
+
```
|
175
212
|
|
176
213
|
Controller helper:
|
177
214
|
|
178
|
-
|
215
|
+
```rb
|
216
|
+
# Returns true if the current session is shapeshifted
|
217
|
+
shapeshifter?
|
218
|
+
```
|
179
219
|
|
180
220
|
### When unauthenticated
|
181
221
|
|
@@ -208,7 +248,7 @@ Veri stores authentication sessions in the database, providing session managemen
|
|
208
248
|
|
209
249
|
```rb
|
210
250
|
# Get all sessions for a user
|
211
|
-
user.
|
251
|
+
user.sessions
|
212
252
|
|
213
253
|
# Get current session in controller
|
214
254
|
current_session
|
@@ -219,6 +259,7 @@ current_session
|
|
219
259
|
```rb
|
220
260
|
session.identity
|
221
261
|
# => authenticated user
|
262
|
+
|
222
263
|
session.info
|
223
264
|
# => {
|
224
265
|
# device: "Desktop",
|
@@ -232,9 +273,23 @@ session.info
|
|
232
273
|
### Session Status
|
233
274
|
|
234
275
|
```rb
|
235
|
-
|
236
|
-
session.
|
237
|
-
|
276
|
+
# Session is active (neither expired nor inactive)
|
277
|
+
session.active?
|
278
|
+
|
279
|
+
# Session exceeded inactivity timeout
|
280
|
+
session.inactive?
|
281
|
+
|
282
|
+
# Session exceeded maximum lifetime
|
283
|
+
session.expired?
|
284
|
+
|
285
|
+
# Fetch active sessions
|
286
|
+
Veri::Session.active
|
287
|
+
|
288
|
+
# Fetch inactive sessions
|
289
|
+
Veri::Session.inactive
|
290
|
+
|
291
|
+
# Fetch expired sessions
|
292
|
+
Veri::Session.expired
|
238
293
|
```
|
239
294
|
|
240
295
|
### Session Management
|
@@ -243,12 +298,17 @@ session.expired? # Session exceeded maximum lifetime
|
|
243
298
|
# Terminate a specific session
|
244
299
|
session.terminate
|
245
300
|
|
246
|
-
# Terminate all sessions
|
247
|
-
Veri::Session.terminate_all
|
301
|
+
# Terminate all sessions
|
302
|
+
Veri::Session.terminate_all
|
303
|
+
|
304
|
+
# Terminate all sessions for a specific user
|
305
|
+
user.sessions.terminate_all
|
248
306
|
|
249
|
-
# Clean up expired/inactive sessions
|
250
|
-
Veri::Session.prune
|
251
|
-
|
307
|
+
# Clean up expired/inactive sessions, and sessions with deleted tenant
|
308
|
+
Veri::Session.prune
|
309
|
+
|
310
|
+
# Clean up expired/inactive sessions for a specific user
|
311
|
+
user.sessions.prune
|
252
312
|
```
|
253
313
|
|
254
314
|
## Account Lockout
|
@@ -264,9 +324,62 @@ user.unlock!
|
|
264
324
|
|
265
325
|
# Check if account is locked
|
266
326
|
user.locked?
|
327
|
+
|
328
|
+
# Fetch locked users
|
329
|
+
User.locked
|
330
|
+
|
331
|
+
# Fetch unlocked users
|
332
|
+
User.unlocked
|
267
333
|
```
|
268
334
|
|
269
|
-
When an account is locked,
|
335
|
+
When an account is locked, the user cannot log in. If the user is already logged in, their sessions will be terminated, and they will be treated as an unauthenticated user.
|
336
|
+
|
337
|
+
## Multi-Tenancy
|
338
|
+
|
339
|
+
Veri supports multi-tenancy, allowing you to isolate authentication sessions between different tenants (e.g., organizations, clients, or subdomains).
|
340
|
+
|
341
|
+
### Setting Up Multi-Tenancy
|
342
|
+
|
343
|
+
To enable multi-tenancy, override `current_tenant` method:
|
344
|
+
|
345
|
+
```rb
|
346
|
+
class ApplicationController < ActionController::Base
|
347
|
+
include Veri::Authentication
|
348
|
+
|
349
|
+
with_authentication
|
350
|
+
|
351
|
+
private
|
352
|
+
|
353
|
+
def current_tenant
|
354
|
+
# Option 1: String-based tenancy (e.g., subdomain)
|
355
|
+
request.subdomain
|
356
|
+
|
357
|
+
# Option 2: Model-based tenancy (e.g., organization)
|
358
|
+
# Company.find_by(subdomain: request.subdomain)
|
359
|
+
end
|
360
|
+
end
|
361
|
+
```
|
362
|
+
|
363
|
+
### Session Tenant Access
|
364
|
+
|
365
|
+
Sessions expose their tenant through `tenant` method:
|
366
|
+
|
367
|
+
```rb
|
368
|
+
# Returns the tenant (string, model instance, or nil in single-tenant applications)
|
369
|
+
session.tenant
|
370
|
+
```
|
371
|
+
|
372
|
+
### Migration Helpers
|
373
|
+
|
374
|
+
Handle tenant changes when models are renamed or removed. These are irreversible data migrations.
|
375
|
+
|
376
|
+
```rb
|
377
|
+
# Rename a tenant class (e.g., when you rename your Organization model to Company)
|
378
|
+
migrate_authentication_tenant!("Organization", "Company")
|
379
|
+
|
380
|
+
# Remove orphaned tenant data (e.g., when you delete the Organization model entirely)
|
381
|
+
delete_authentication_tenant!("Organization")
|
382
|
+
```
|
270
383
|
|
271
384
|
## View Helpers
|
272
385
|
|
@@ -332,13 +445,13 @@ end
|
|
332
445
|
## Getting Help and Contributing
|
333
446
|
|
334
447
|
### Getting Help
|
335
|
-
Have a question or need assistance? Open a discussion in our [discussions section](https://github.com/
|
448
|
+
Have a question or need assistance? Open a discussion in our [discussions section](https://github.com/enjaku4/veri/discussions) for:
|
336
449
|
- Usage questions
|
337
450
|
- Implementation guidance
|
338
451
|
- Feature suggestions
|
339
452
|
|
340
453
|
### Reporting Issues
|
341
|
-
Found a bug? Please [create an issue](https://github.com/
|
454
|
+
Found a bug? Please [create an issue](https://github.com/enjaku4/veri/issues) with:
|
342
455
|
- A clear description of the problem
|
343
456
|
- Steps to reproduce the issue
|
344
457
|
- Your environment details (Rails version, Ruby version, etc.)
|
@@ -347,14 +460,14 @@ Found a bug? Please [create an issue](https://github.com/brownboxdev/veri/issues
|
|
347
460
|
Ready to contribute? You can:
|
348
461
|
- Fix bugs by submitting pull requests
|
349
462
|
- Improve documentation
|
350
|
-
- Add new features (please discuss first in our [discussions section](https://github.com/
|
463
|
+
- Add new features (please discuss first in our [discussions section](https://github.com/enjaku4/veri/discussions))
|
351
464
|
|
352
|
-
Before contributing, please read the [contributing guidelines](https://github.com/
|
465
|
+
Before contributing, please read the [contributing guidelines](https://github.com/enjaku4/veri/blob/main/CONTRIBUTING.md)
|
353
466
|
|
354
467
|
## License
|
355
468
|
|
356
|
-
The gem is available as open source under the terms of the [MIT License](https://github.com/
|
469
|
+
The gem is available as open source under the terms of the [MIT License](https://github.com/enjaku4/veri/blob/main/LICENSE.txt).
|
357
470
|
|
358
471
|
## Code of Conduct
|
359
472
|
|
360
|
-
Everyone interacting in the Veri project is expected to follow the [code of conduct](https://github.com/
|
473
|
+
Everyone interacting in the Veri project is expected to follow the [code of conduct](https://github.com/enjaku4/veri/blob/main/CODE_OF_CONDUCT.md).
|
@@ -10,6 +10,7 @@ class AddVeriAuthentication < ActiveRecord::Migration[<%= ActiveRecord::Migratio
|
|
10
10
|
t.datetime :expires_at, null: false
|
11
11
|
t.belongs_to :authenticatable, null: false, foreign_key: { to_table: <%= table_name.to_sym.inspect %> }, index: true<%= ", type: :uuid" if options[:uuid] %>
|
12
12
|
t.belongs_to :original_authenticatable, foreign_key: { to_table: <%= table_name.to_sym.inspect %> }, index: true<%= ", type: :uuid" if options[:uuid] %>
|
13
|
+
t.belongs_to :tenant, polymorphic: true, index: true<%= ", type: :uuid" if options[:uuid] %>
|
13
14
|
t.datetime :shapeshifted_at
|
14
15
|
t.datetime :last_seen_at, null: false
|
15
16
|
t.string :ip_address
|
data/lib/veri/configuration.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require "digest/sha2"
|
2
|
+
|
1
3
|
module Veri
|
2
4
|
module Authentication
|
3
5
|
extend ActiveSupport::Concern
|
@@ -29,8 +31,8 @@ module Veri
|
|
29
31
|
end
|
30
32
|
|
31
33
|
def current_session
|
32
|
-
token = cookies.encrypted[
|
33
|
-
@current_session ||= token ? Session.find_by(hashed_token: Digest::SHA256.hexdigest(token)) : nil
|
34
|
+
token = cookies.encrypted["#{auth_cookie_prefix}_token"]
|
35
|
+
@current_session ||= token ? Session.find_by(hashed_token: Digest::SHA256.hexdigest(token), **resolved_tenant) : nil
|
34
36
|
end
|
35
37
|
|
36
38
|
def log_in(authenticatable)
|
@@ -41,14 +43,15 @@ module Veri
|
|
41
43
|
|
42
44
|
return false if processed_authenticatable.locked?
|
43
45
|
|
44
|
-
token = Veri::Session.establish(processed_authenticatable, request)
|
45
|
-
|
46
|
+
token = Veri::Session.establish(processed_authenticatable, request, resolved_tenant)
|
47
|
+
|
48
|
+
cookies.encrypted.permanent["#{auth_cookie_prefix}_token"] = { value: token, httponly: true }
|
46
49
|
true
|
47
50
|
end
|
48
51
|
|
49
52
|
def log_out
|
50
53
|
current_session&.terminate
|
51
|
-
cookies.delete(
|
54
|
+
cookies.delete("#{auth_cookie_prefix}_token")
|
52
55
|
end
|
53
56
|
|
54
57
|
def logged_in?
|
@@ -56,7 +59,7 @@ module Veri
|
|
56
59
|
end
|
57
60
|
|
58
61
|
def return_path
|
59
|
-
cookies.signed[
|
62
|
+
cookies.signed["#{auth_cookie_prefix}_return_path"]
|
60
63
|
end
|
61
64
|
|
62
65
|
def shapeshifter?
|
@@ -79,7 +82,7 @@ module Veri
|
|
79
82
|
|
80
83
|
log_out
|
81
84
|
|
82
|
-
cookies.signed[
|
85
|
+
cookies.signed["#{auth_cookie_prefix}_return_path"] = { value: request.fullpath, expires: 15.minutes.from_now } if request.get? && request.format.html?
|
83
86
|
|
84
87
|
when_unauthenticated
|
85
88
|
end
|
@@ -87,5 +90,19 @@ module Veri
|
|
87
90
|
def when_unauthenticated
|
88
91
|
request.format.html? ? redirect_back(fallback_location: root_path) : head(:unauthorized)
|
89
92
|
end
|
93
|
+
|
94
|
+
def current_tenant = nil
|
95
|
+
|
96
|
+
def resolved_tenant
|
97
|
+
@resolved_tenant ||= Veri::Inputs::Tenant.new(
|
98
|
+
current_tenant,
|
99
|
+
error: Veri::InvalidTenantError,
|
100
|
+
message: "Expected a string, an ActiveRecord model instance, or nil, got `#{current_tenant.inspect}`"
|
101
|
+
).resolve
|
102
|
+
end
|
103
|
+
|
104
|
+
def auth_cookie_prefix
|
105
|
+
@auth_cookie_prefix ||= "auth_#{Digest::SHA2.hexdigest(Marshal.dump(resolved_tenant))[0..7]}"
|
106
|
+
end
|
90
107
|
end
|
91
108
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Veri
|
2
|
+
module MigrationHelpers
|
3
|
+
def migrate_authentication_tenant!(old_tenant, new_tenant)
|
4
|
+
sessions = Veri::Session.where(tenant_type: old_tenant.to_s)
|
5
|
+
|
6
|
+
raise Veri::InvalidArgumentError, "No sessions exist in tenant #{old_tenant.inspect}" unless sessions.exists?
|
7
|
+
raise Veri::InvalidArgumentError, "Cannot migrate tenant to #{new_tenant.inspect}: class does not exist" unless new_tenant.to_s.safe_constantize
|
8
|
+
|
9
|
+
sessions.update_all(tenant_type: new_tenant.to_s)
|
10
|
+
end
|
11
|
+
|
12
|
+
def delete_authentication_tenant!(tenant)
|
13
|
+
sessions = Veri::Session.where(tenant_type: tenant.to_s)
|
14
|
+
|
15
|
+
raise Veri::InvalidArgumentError, "No sessions exist in tenant #{tenant.inspect}" unless sessions.exists?
|
16
|
+
|
17
|
+
sessions.delete_all
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Veri
|
2
|
+
module Inputs
|
3
|
+
class Tenant < Base
|
4
|
+
def resolve
|
5
|
+
case tenant = process
|
6
|
+
when nil
|
7
|
+
{ tenant_type: nil, tenant_id: nil }
|
8
|
+
when String
|
9
|
+
{ tenant_type: tenant.to_s, tenant_id: nil }
|
10
|
+
when ActiveRecord::Base
|
11
|
+
raise_error unless tenant.persisted?
|
12
|
+
{ tenant_type: tenant.class.to_s, tenant_id: tenant.public_send(tenant.class.primary_key) }
|
13
|
+
else
|
14
|
+
tenant
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def type
|
21
|
+
-> {
|
22
|
+
self.class::Strict::String.constrained(min_size: 1) |
|
23
|
+
self.class::Instance(ActiveRecord::Base) |
|
24
|
+
self.class::Hash.schema(
|
25
|
+
tenant_type: self.class::Strict::String | self.class::Nil,
|
26
|
+
tenant_id: self.class::Strict::String | self.class::Strict::Integer | self.class::Nil
|
27
|
+
) |
|
28
|
+
self.class::Nil
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -7,7 +7,10 @@ module Veri
|
|
7
7
|
|
8
8
|
@@included = name
|
9
9
|
|
10
|
-
has_many :
|
10
|
+
has_many :sessions, class_name: "Veri::Session", foreign_key: :authenticatable_id, dependent: :destroy
|
11
|
+
|
12
|
+
scope :locked, -> { where(locked: true) }
|
13
|
+
scope :unlocked, -> { where(locked: false) }
|
11
14
|
end
|
12
15
|
|
13
16
|
def update_password(password)
|
data/lib/veri/models/session.rb
CHANGED
@@ -4,10 +4,16 @@ module Veri
|
|
4
4
|
class Session < ActiveRecord::Base
|
5
5
|
self.table_name = "veri_sessions"
|
6
6
|
|
7
|
-
# rubocop:disable Rails/ReflectionClassName
|
8
7
|
belongs_to :authenticatable, class_name: Veri::Configuration.user_model_name
|
9
8
|
belongs_to :original_authenticatable, class_name: Veri::Configuration.user_model_name, optional: true
|
10
|
-
|
9
|
+
belongs_to :tenant, polymorphic: true, optional: true
|
10
|
+
|
11
|
+
scope :active, -> { where.not(id: expired.select(:id)).where.not(id: inactive.select(:id)) }
|
12
|
+
scope :expired, -> { where(expires_at: ...Time.current) }
|
13
|
+
scope :inactive, -> do
|
14
|
+
inactive_session_lifetime = Veri::Configuration.inactive_session_lifetime
|
15
|
+
inactive_session_lifetime ? where(last_seen_at: ...(Time.current - inactive_session_lifetime)) : none
|
16
|
+
end
|
11
17
|
|
12
18
|
def active? = !expired? && !inactive?
|
13
19
|
|
@@ -60,7 +66,7 @@ module Veri
|
|
60
66
|
)
|
61
67
|
end
|
62
68
|
|
63
|
-
def
|
69
|
+
def to_true_identity
|
64
70
|
update!(
|
65
71
|
shapeshifted_at: nil,
|
66
72
|
authenticatable: original_authenticatable,
|
@@ -68,49 +74,44 @@ module Veri
|
|
68
74
|
)
|
69
75
|
end
|
70
76
|
|
77
|
+
def tenant
|
78
|
+
return tenant_type if tenant_type.present? && tenant_id.blank?
|
79
|
+
|
80
|
+
record = super
|
81
|
+
|
82
|
+
raise ActiveRecord::RecordNotFound.new(nil, tenant_type, nil, tenant_id) if tenant_id.present? && !record
|
83
|
+
|
84
|
+
record
|
85
|
+
end
|
86
|
+
|
71
87
|
class << self
|
72
|
-
def establish(user, request)
|
88
|
+
def establish(user, request, resolved_tenant)
|
73
89
|
token = SecureRandom.hex(32)
|
74
90
|
expires_at = Time.current + Veri::Configuration.total_session_lifetime
|
75
91
|
|
76
92
|
new(
|
77
93
|
hashed_token: Digest::SHA256.hexdigest(token),
|
78
94
|
expires_at:,
|
79
|
-
authenticatable:
|
95
|
+
authenticatable: user,
|
96
|
+
**resolved_tenant
|
80
97
|
).update_info(request)
|
81
98
|
|
82
99
|
token
|
83
100
|
end
|
84
101
|
|
85
|
-
def prune
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
).process
|
93
|
-
)
|
94
|
-
else
|
95
|
-
all
|
96
|
-
end
|
97
|
-
|
98
|
-
expired_scope = scope.where(expires_at: ...Time.current)
|
99
|
-
|
100
|
-
if Veri::Configuration.inactive_session_lifetime
|
101
|
-
inactive_cutoff = Time.current - Veri::Configuration.inactive_session_lifetime
|
102
|
-
expired_scope = expired_scope.or(scope.where(last_seen_at: ...inactive_cutoff))
|
102
|
+
def prune
|
103
|
+
expired.or(inactive).delete_all
|
104
|
+
|
105
|
+
orphaned_tenant_sessions = where.not(tenant_id: nil).includes(:tenant).filter_map do |session|
|
106
|
+
!session.tenant
|
107
|
+
rescue ActiveRecord::RecordNotFound
|
108
|
+
session.id
|
103
109
|
end
|
104
110
|
|
105
|
-
|
111
|
+
where(id: orphaned_tenant_sessions).delete_all if orphaned_tenant_sessions.any?
|
106
112
|
end
|
107
113
|
|
108
|
-
|
109
|
-
Veri::Inputs::Authenticatable.new(
|
110
|
-
user,
|
111
|
-
message: "Expected an instance of #{Veri::Configuration.user_model_name}, got `#{user.inspect}`"
|
112
|
-
).process.veri_sessions.delete_all
|
113
|
-
end
|
114
|
+
alias terminate_all delete_all
|
114
115
|
end
|
115
116
|
end
|
116
117
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "openssl"
|
2
|
+
require "base64"
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
module Veri
|
6
|
+
module Password
|
7
|
+
module Pbkdf2
|
8
|
+
module_function
|
9
|
+
|
10
|
+
ITERATIONS = 210_000
|
11
|
+
SALT_BYTES = 64
|
12
|
+
HASH_BYTES = 64
|
13
|
+
DIGEST = "sha512"
|
14
|
+
|
15
|
+
def create(password)
|
16
|
+
salt = SecureRandom.random_bytes(SALT_BYTES)
|
17
|
+
hash = OpenSSL::KDF.pbkdf2_hmac(
|
18
|
+
password,
|
19
|
+
salt:,
|
20
|
+
iterations: ITERATIONS,
|
21
|
+
length: HASH_BYTES,
|
22
|
+
hash: DIGEST
|
23
|
+
)
|
24
|
+
|
25
|
+
"#{DIGEST}$#{ITERATIONS}$#{HASH_BYTES}$#{Base64.strict_encode64(salt)}$#{Base64.strict_encode64(hash)}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def verify(password, hashed_password)
|
29
|
+
parts = hashed_password.split("$")
|
30
|
+
digest, iterations, hash_bytes, encoded_salt, encoded_hash = parts[0], parts[1], parts[2], parts[3], parts[4]
|
31
|
+
|
32
|
+
salt = Base64.strict_decode64(encoded_salt)
|
33
|
+
hash = Base64.strict_decode64(encoded_hash)
|
34
|
+
|
35
|
+
recalculated_hash = OpenSSL::KDF.pbkdf2_hmac(
|
36
|
+
password,
|
37
|
+
salt:,
|
38
|
+
iterations: iterations.to_i,
|
39
|
+
length: hash_bytes.to_i,
|
40
|
+
hash: digest
|
41
|
+
)
|
42
|
+
|
43
|
+
OpenSSL.fixed_length_secure_compare(recalculated_hash, hash)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/veri/railtie.rb
CHANGED
@@ -2,11 +2,31 @@ require "rails/railtie"
|
|
2
2
|
|
3
3
|
module Veri
|
4
4
|
class Railtie < Rails::Railtie
|
5
|
+
def self.table_exists?
|
6
|
+
ActiveRecord::Base.connection.data_source_exists?("veri_sessions")
|
7
|
+
rescue ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished
|
8
|
+
false
|
9
|
+
end
|
10
|
+
|
5
11
|
initializer "veri.to_prepare" do |app|
|
6
12
|
app.config.to_prepare do
|
13
|
+
if Veri::Railtie.table_exists?
|
14
|
+
Veri::Session.where.not(tenant_id: nil).distinct.pluck(:tenant_type).each do |tenant_class|
|
15
|
+
tenant_class.constantize
|
16
|
+
rescue NameError => e
|
17
|
+
raise Veri::Error, "Tenant not found: class `#{e.name}` may have been renamed or deleted"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
7
21
|
user_model = Veri::Configuration.user_model
|
8
22
|
user_model.include Veri::Authenticatable unless user_model < Veri::Authenticatable
|
9
23
|
end
|
10
24
|
end
|
25
|
+
|
26
|
+
initializer "veri.extend_migration_helpers" do
|
27
|
+
ActiveSupport.on_load :active_record do
|
28
|
+
ActiveRecord::Migration.include Veri::MigrationHelpers
|
29
|
+
end
|
30
|
+
end
|
11
31
|
end
|
12
32
|
end
|
data/lib/veri/version.rb
CHANGED
data/lib/veri.rb
CHANGED
@@ -5,6 +5,7 @@ require "active_support"
|
|
5
5
|
|
6
6
|
require_relative "veri/password/argon2"
|
7
7
|
require_relative "veri/password/bcrypt"
|
8
|
+
require_relative "veri/password/pbkdf2"
|
8
9
|
require_relative "veri/password/scrypt"
|
9
10
|
|
10
11
|
require_relative "veri/inputs/base"
|
@@ -13,12 +14,14 @@ require_relative "veri/inputs/duration"
|
|
13
14
|
require_relative "veri/inputs/hashing_algorithm"
|
14
15
|
require_relative "veri/inputs/model"
|
15
16
|
require_relative "veri/inputs/non_empty_string"
|
17
|
+
require_relative "veri/inputs/tenant"
|
16
18
|
require_relative "veri/configuration"
|
17
19
|
|
18
20
|
module Veri
|
19
21
|
class Error < StandardError; end
|
20
22
|
class InvalidArgumentError < Veri::Error; end
|
21
23
|
class ConfigurationError < Veri::InvalidArgumentError; end
|
24
|
+
class InvalidTenantError < Veri::InvalidArgumentError; end
|
22
25
|
|
23
26
|
delegate :configure, to: Veri::Configuration
|
24
27
|
module_function :configure
|
@@ -27,5 +30,6 @@ end
|
|
27
30
|
require_relative "veri/models/session"
|
28
31
|
require_relative "veri/controllers/concerns/authentication"
|
29
32
|
require_relative "veri/models/concerns/authenticatable"
|
33
|
+
require_relative "veri/helpers/migration_helpers"
|
30
34
|
|
31
35
|
require_relative "veri/railtie"
|
data/veri.gemspec
CHANGED
@@ -4,7 +4,7 @@ Gem::Specification.new do |spec|
|
|
4
4
|
spec.name = "veri"
|
5
5
|
spec.version = Veri::VERSION
|
6
6
|
spec.authors = ["enjaku4"]
|
7
|
-
spec.homepage = "https://github.com/
|
7
|
+
spec.homepage = "https://github.com/enjaku4/veri"
|
8
8
|
spec.metadata["homepage_uri"] = spec.homepage
|
9
9
|
spec.metadata["source_code_uri"] = spec.homepage
|
10
10
|
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.add_dependency "bcrypt", "~> 3.0"
|
24
24
|
spec.add_dependency "dry-configurable", "~> 1.1"
|
25
25
|
spec.add_dependency "dry-types", "~> 1.7"
|
26
|
-
spec.add_dependency "rails", ">= 7.
|
26
|
+
spec.add_dependency "rails", ">= 7.2", "< 8.1"
|
27
27
|
spec.add_dependency "scrypt", "~> 3.0"
|
28
28
|
spec.add_dependency "user_agent_parser", "~> 2.0"
|
29
29
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: veri
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- enjaku4
|
@@ -71,7 +71,7 @@ dependencies:
|
|
71
71
|
requirements:
|
72
72
|
- - ">="
|
73
73
|
- !ruby/object:Gem::Version
|
74
|
-
version: '7.
|
74
|
+
version: '7.2'
|
75
75
|
- - "<"
|
76
76
|
- !ruby/object:Gem::Version
|
77
77
|
version: '8.1'
|
@@ -81,7 +81,7 @@ dependencies:
|
|
81
81
|
requirements:
|
82
82
|
- - ">="
|
83
83
|
- !ruby/object:Gem::Version
|
84
|
-
version: '7.
|
84
|
+
version: '7.2'
|
85
85
|
- - "<"
|
86
86
|
- !ruby/object:Gem::Version
|
87
87
|
version: '8.1'
|
@@ -125,27 +125,30 @@ files:
|
|
125
125
|
- lib/veri.rb
|
126
126
|
- lib/veri/configuration.rb
|
127
127
|
- lib/veri/controllers/concerns/authentication.rb
|
128
|
+
- lib/veri/helpers/migration_helpers.rb
|
128
129
|
- lib/veri/inputs/authenticatable.rb
|
129
130
|
- lib/veri/inputs/base.rb
|
130
131
|
- lib/veri/inputs/duration.rb
|
131
132
|
- lib/veri/inputs/hashing_algorithm.rb
|
132
133
|
- lib/veri/inputs/model.rb
|
133
134
|
- lib/veri/inputs/non_empty_string.rb
|
135
|
+
- lib/veri/inputs/tenant.rb
|
134
136
|
- lib/veri/models/concerns/authenticatable.rb
|
135
137
|
- lib/veri/models/session.rb
|
136
138
|
- lib/veri/password/argon2.rb
|
137
139
|
- lib/veri/password/bcrypt.rb
|
140
|
+
- lib/veri/password/pbkdf2.rb
|
138
141
|
- lib/veri/password/scrypt.rb
|
139
142
|
- lib/veri/railtie.rb
|
140
143
|
- lib/veri/version.rb
|
141
144
|
- veri.gemspec
|
142
|
-
homepage: https://github.com/
|
145
|
+
homepage: https://github.com/enjaku4/veri
|
143
146
|
licenses:
|
144
147
|
- MIT
|
145
148
|
metadata:
|
146
|
-
homepage_uri: https://github.com/
|
147
|
-
source_code_uri: https://github.com/
|
148
|
-
changelog_uri: https://github.com/
|
149
|
+
homepage_uri: https://github.com/enjaku4/veri
|
150
|
+
source_code_uri: https://github.com/enjaku4/veri
|
151
|
+
changelog_uri: https://github.com/enjaku4/veri/blob/main/CHANGELOG.md
|
149
152
|
rubygems_mfa_required: 'true'
|
150
153
|
rdoc_options: []
|
151
154
|
require_paths:
|