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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a4fcb8a0d60277a1b18a9c23114d96c19472794ee47c02d62445248ff30f16f0
4
- data.tar.gz: fa8eb3f99daf00c2c7c338350f14d3014ab855923e6977581d60e376c9dc3222
3
+ metadata.gz: c4434399cb0f0e3081ff2ccb8e72df12623e74f6f2cd43f318d50ae483845be6
4
+ data.tar.gz: 194dec0866793d8f2bc906d2830f8c9a4fb37e7d8a5c7f5c58bec7d0631abc51
5
5
  SHA512:
6
- metadata.gz: d21bba8f857717fd6e9c54b1444c23feb2f5dea73dccda0ca3d661155b6a79d2901b8f4a58717bdb28527d6e1576222688fd802da86c18e86055d301e6c2003f
7
- data.tar.gz: a3547cee5d2bf4190fc95f923177da126e4f478ef8dc052d49bbdc524515be211d05ab21a0636fd373edb9b7d24b76049e7ef40bf826d667e85226da0b4ab7b1
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
  [![Gem Version](https://badge.fury.io/rb/veri.svg)](http://badge.fury.io/rb/veri)
4
- [![Github Actions badge](https://github.com/brownboxdev/veri/actions/workflows/ci.yml/badge.svg)](https://github.com/brownboxdev/veri/actions/workflows/ci.yml)
4
+ [![Github Actions badge](https://github.com/enjaku4/veri/actions/workflows/ci.yml/badge.svg)](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("new_password")
86
+ user.update_password("password")
88
87
 
89
88
  # Verify a password
90
- user.verify_password("submitted_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: :unprocessable_entity
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
- - `current_user` - Returns authenticated user or `nil`
141
- - `logged_in?` - Returns `true` if user is authenticated
142
- - `log_in(user)` - Authenticates user and creates session, returns `true` on success or `false` if account is locked
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.revert_to_true_identity
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
- - `shapeshift(user)` - Assume another user's identity (maintains original identity)
172
- - `revert_to_true_identity` - Return to original identity
173
- - `shapeshifted?` - Returns true if currently shapeshifted
174
- - `true_identity` - Returns original user when shapeshifted, otherwise current user
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
- - `shapeshifter?` - Returns true if the current session is shapeshifted
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.veri_sessions
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
- session.active? # Session is active (neither expired nor inactive)
236
- session.inactive? # Session exceeded inactivity timeout
237
- session.expired? # Session exceeded maximum lifetime
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 for a user
247
- Veri::Session.terminate_all(user)
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 # All sessions
251
- Veri::Session.prune(user) # Specific user's sessions
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, users cannot log in. If they're already logged in, their sessions will be terminated and they'll be treated as unauthenticated users.
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/brownboxdev/veri/discussions) for:
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/brownboxdev/veri/issues) with:
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/brownboxdev/veri/discussions))
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/brownboxdev/veri/blob/main/CONTRIBUTING.md)
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/brownboxdev/veri/blob/main/LICENSE.txt).
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/brownboxdev/veri/blob/main/CODE_OF_CONDUCT.md).
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
@@ -52,6 +52,7 @@ module Veri
52
52
  HASHERS = {
53
53
  argon2: Veri::Password::Argon2,
54
54
  bcrypt: Veri::Password::BCrypt,
55
+ pbkdf2: Veri::Password::Pbkdf2,
55
56
  scrypt: Veri::Password::SCrypt
56
57
  }.freeze
57
58
 
@@ -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[:veri_token]
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
- cookies.encrypted.permanent[:veri_token] = { value: token, httponly: true }
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(:veri_token)
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[:veri_return_path]
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[:veri_return_path] = { value: request.fullpath, expires: 15.minutes.from_now } if request.get? && request.format.html?
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
@@ -3,7 +3,7 @@ module Veri
3
3
  class HashingAlgorithm < Veri::Inputs::Base
4
4
  private
5
5
 
6
- def type = -> { self.class::Strict::Symbol.enum(:argon2, :bcrypt, :scrypt) }
6
+ def type = -> { self.class::Strict::Symbol.enum(:argon2, :bcrypt, :pbkdf2, :scrypt) }
7
7
  end
8
8
  end
9
9
  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 :veri_sessions, class_name: "Veri::Session", foreign_key: :authenticatable_id, dependent: :destroy
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)
@@ -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
- # rubocop:enable Rails/ReflectionClassName
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 revert_to_true_identity
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: Veri::Inputs::Authenticatable.new(user, error: Veri::Error).process
95
+ authenticatable: user,
96
+ **resolved_tenant
80
97
  ).update_info(request)
81
98
 
82
99
  token
83
100
  end
84
101
 
85
- def prune(user = nil)
86
- scope = if user
87
- where(
88
- authenticatable: Veri::Inputs::Authenticatable.new(
89
- user,
90
- optional: true,
91
- message: "Expected an instance of #{Veri::Configuration.user_model_name} or nil, got `#{user.inspect}`"
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
- expired_scope.delete_all
111
+ where(id: orphaned_tenant_sessions).delete_all if orphaned_tenant_sessions.any?
106
112
  end
107
113
 
108
- def terminate_all(user)
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
@@ -1,3 +1,3 @@
1
1
  module Veri
2
- VERSION = "0.3.1".freeze
2
+ VERSION = "1.0.0".freeze
3
3
  end
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/brownboxdev/veri"
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.1", "< 8.1"
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.3.1
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.1'
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.1'
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/brownboxdev/veri
145
+ homepage: https://github.com/enjaku4/veri
143
146
  licenses:
144
147
  - MIT
145
148
  metadata:
146
- homepage_uri: https://github.com/brownboxdev/veri
147
- source_code_uri: https://github.com/brownboxdev/veri
148
- changelog_uri: https://github.com/brownboxdev/veri/blob/main/CHANGELOG.md
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: