veri 2.0.2 → 2.1.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: c9b0db1fbb87c0a3b25d47f2142cab1b9143a0ba7751a05c1676ca6e3b4b6ee3
4
- data.tar.gz: 600d600760a3c0af2fddfece43322193be1ae4255eae0dd1c22d4099a083cb7b
3
+ metadata.gz: 703094a8e90e3fde6888629b9702406548122bed4ab199e1a02eac174c68ee07
4
+ data.tar.gz: 337701b97ed23c8cffd7b8ffb6d649e3e520c09bc61de6ed69e7c9d8b1905462
5
5
  SHA512:
6
- metadata.gz: aaf5ec5aced9ddec1c42367df39be9e4322d3b1b3946bb473bfaee3f668d6fea55d78b48f3f1f0fa1acb8ee4588de8039d054730d3b913ab8036def74054d858
7
- data.tar.gz: b188516f09afc341584f4460d5caa0e552c94e54dfe3a4bc3fe7a4cddb592acbb8c4957092e4763e88bd1be34ebc122934b40876bd2fc23e87d3b207b5fb2479
6
+ metadata.gz: e717d0cf3a4a8d9c673919548c739820ed15b42e7ab7140c9a95cd0f4740cd20a1678cf2a9dfbbcbaeb1257b60f9b8b2a28c2030d9b893fbf382a309e90b1445
7
+ data.tar.gz: 515e5fee62f96f4ddf4982419207565a905faa00246b5d1b483581ba86fd1063e0bbe06eaa253274c6e73d69395b1f8afd296210afd8aed3c067e264d872d7a4
data/CHANGELOG.md CHANGED
@@ -1,3 +1,30 @@
1
+ ## v2.1.0
2
+
3
+ ### Features
4
+
5
+ - Added `Veri::Session#switch_tenant` method to move a session to another tenant
6
+ - Added `password_set?` method to check whether a user has a password
7
+
8
+ ### Bugs
9
+
10
+ - Fixed `log_in` using the previous tenant when `current_tenant` changes within the same request
11
+ - Fixed `Veri::Session.in_tenant` raising an error without a message when given an invalid tenant
12
+
13
+ ## v2.0.3
14
+
15
+ ### Bugs
16
+
17
+ - Fixed authentication raising `NameError` when the configured user model is not named `User`
18
+ - Fixed Rails commands failing when the configured user model class does not exist yet
19
+ - Fixed expired and inactive sessions still appearing as logged in on pages that don't require authentication
20
+ - Fixed locking a user not terminating their sessions until their next request
21
+ - Fixed password verification raising an error for users who have no password set
22
+ - Fixed cross-tenant shapeshifting not working
23
+
24
+ ### Misc
25
+
26
+ - The hashing algorithm can now be changed without breaking existing passwords
27
+
1
28
  ## v2.0.2
2
29
 
3
30
  ### Misc
data/README.md CHANGED
@@ -81,6 +81,8 @@ Veri.configure do |config|
81
81
  end
82
82
  ```
83
83
 
84
+ The hashing algorithm can be changed at any time: existing passwords are verified with the algorithm they were hashed with and are automatically re-hashed with the configured algorithm on the next successful login.
85
+
84
86
  ## Password Management
85
87
 
86
88
  Your user model is automatically extended with password management methods:
@@ -91,6 +93,9 @@ user.update_password("password")
91
93
 
92
94
  # Verify a password
93
95
  user.verify_password("password")
96
+
97
+ # Check if a password is set
98
+ user.password_set?
94
99
  ```
95
100
  Changing a password does not automatically terminate existing sessions. If you want to invalidate the user's sessions after a password change, do so explicitly:
96
101
 
@@ -396,6 +401,40 @@ user.sessions.in_tenant(tenant)
396
401
  user.sessions.in_tenant(tenant).terminate_all
397
402
  ```
398
403
 
404
+ ### Switching Tenants
405
+
406
+ A session belongs to one tenant, and a browser holds one session per host. A user is therefore logged in to one tenant at a time, unless your tenants live on separate subdomains, where each subdomain keeps its own session.
407
+
408
+ To move a user to another tenant, call `switch_tenant` on their session and make `current_tenant` return the new tenant from then on:
409
+
410
+ ```rb
411
+ class ApplicationController < ActionController::Base
412
+ include Veri::Authentication
413
+
414
+ with_authentication
415
+
416
+ private
417
+
418
+ def current_tenant
419
+ # This example keeps the tenant in a cookie; your app can keep it anywhere
420
+ Company.find_by(id: cookies.signed[:company_id])
421
+ end
422
+ end
423
+
424
+ class CompaniesController < ApplicationController
425
+ def switch
426
+ company = current_user.companies.find(params[:id])
427
+
428
+ current_session.switch_tenant(company)
429
+ cookies.signed[:company_id] = company.id
430
+
431
+ redirect_to root_path, notice: "Switched to #{company.name}"
432
+ end
433
+ end
434
+ ```
435
+
436
+ The user stays logged in and the session keeps its expiration.
437
+
399
438
  ### User Impersonation with Tenants
400
439
 
401
440
  When using user impersonation in a multi-tenant setup, Veri allows cross-tenant shapeshifting while preserving the original tenant context:
@@ -408,24 +447,20 @@ session.shapeshift(user, tenant: company)
408
447
  session.true_tenant
409
448
  ```
410
449
 
450
+ The `tenant:` argument always becomes the session's tenant: omitting it means no tenant. In a multi-tenant application, always pass it explicitly.
451
+
411
452
  All other session methods work the same way in multi-tenant applications as in single-tenant applications. However, `to_true_identity` will restore both the original user and tenant.
412
453
 
413
454
  ### Orphaned Sessions
414
455
 
415
456
  When a tenant object is deleted from your database, its associated sessions become orphaned.
416
457
 
417
- To clean up orphaned sessions, use:
458
+ Ideally, the application should be designed so that this doesn't happen: whenever a tenant object is deleted, its sessions are terminated as part of the same operation. However, if that wasn't done, you can clean up the orphaned sessions as a fallback:
418
459
 
419
460
  ```rb
420
461
  Veri::Session.prune
421
462
  ```
422
463
 
423
- Or, for a specific user:
424
-
425
- ```rb
426
- user.sessions.prune
427
- ```
428
-
429
464
  ### Tenant Migrations
430
465
 
431
466
  When you rename or remove models used as tenants, you need to update Veri's stored data accordingly. Use these irreversible data migrations:
@@ -502,24 +537,26 @@ end
502
537
  ## Getting Help and Contributing
503
538
 
504
539
  ### Getting Help
540
+
505
541
  Have a question or need assistance? Open a discussion in the [discussions section](https://github.com/enjaku4/veri/discussions) for:
542
+
506
543
  - Usage questions
507
544
  - Implementation guidance
508
- - Feature suggestions
545
+ - Open-ended ideas or suggestions
546
+
547
+ ### Issues
548
+
549
+ [Issues](https://github.com/enjaku4/veri/issues) track bugs, planned features, and other work. When reporting a bug, please include:
509
550
 
510
- ### Reporting Issues
511
- Found a bug? Please [create an issue](https://github.com/enjaku4/veri/issues) with:
512
551
  - A clear description of the problem
513
552
  - Steps to reproduce the issue
514
- - Your environment details (Rails version, Ruby version, etc.)
553
+ - Your environment details (Rails and Ruby versions, OS, etc.)
515
554
 
516
555
  ### Contributing Code
517
- Ready to contribute? You can:
518
- - Fix bugs by submitting pull requests
519
- - Improve documentation
520
- - Add new features (please discuss first in the [discussions section](https://github.com/enjaku4/veri/discussions))
521
556
 
522
- Before contributing, please read the [contributing guidelines](https://github.com/enjaku4/veri/blob/main/CONTRIBUTING.md)
557
+ Any open issue is free to pick up or discuss. Check the [project board](https://github.com/users/enjaku4/projects/13) or [issues tab](https://github.com/enjaku4/veri/issues).
558
+
559
+ Before contributing, please read the [contributing guidelines](https://github.com/enjaku4/veri/blob/main/CONTRIBUTING.md).
523
560
 
524
561
  ## License
525
562
 
@@ -531,6 +568,6 @@ Everyone interacting in the Veri project is expected to follow the [code of cond
531
568
 
532
569
  ## Old Versions
533
570
 
534
- Only the latest major version is supported. Older versions are obsolete and not maintained, but their READMEs are available here for reference:
571
+ Only the latest major version is actively maintained. READMEs for older versions are available here for reference:
535
572
 
536
573
  [v1.x.x](https://github.com/enjaku4/veri/blob/9c188e16a703141b7cd89dd31d5cd49a557f143d/README.md)
@@ -1,5 +1,3 @@
1
- require "zlib"
2
-
3
1
  module Veri
4
2
  module Authentication
5
3
  extend ActiveSupport::Concern
@@ -29,28 +27,27 @@ module Veri
29
27
  end
30
28
 
31
29
  def current_session
32
- token = cookies.encrypted["#{auth_cookie_prefix}_token"]
30
+ token = cookies.encrypted["veri_token"]
33
31
 
34
- @current_session ||= Session.lookup(token, resolved_tenant)
32
+ @current_session ||= Session.find_active(token, resolved_tenant)
35
33
  end
36
34
 
37
35
  def log_in(authenticatable)
38
- processed_authenticatable = Veri::Inputs::Authenticatable.new(
39
- authenticatable,
40
- message: "Expected an instance of #{Veri::Configuration.user_model_name}, got `#{authenticatable.inspect}`"
41
- ).process
36
+ processed_authenticatable = Veri::Inputs::Authenticatable.new(authenticatable).process
42
37
 
43
38
  return false if processed_authenticatable.locked?
44
39
 
45
40
  token = Veri::Session.establish(processed_authenticatable, request, resolved_tenant)
46
41
 
47
- cookies.encrypted.permanent["#{auth_cookie_prefix}_token"] = { value: token, httponly: true }
42
+ cookies.encrypted.permanent["veri_token"] = { value: token, httponly: true, secure: request.ssl? }
43
+ reset_memoization
48
44
  true
49
45
  end
50
46
 
51
47
  def log_out
52
48
  current_session&.terminate
53
- cookies.delete("#{auth_cookie_prefix}_token")
49
+ cookies.delete("veri_token")
50
+ reset_memoization
54
51
  end
55
52
 
56
53
  def logged_in?
@@ -58,7 +55,7 @@ module Veri
58
55
  end
59
56
 
60
57
  def return_path
61
- cookies.signed["#{auth_cookie_prefix}_return_path"]
58
+ cookies.signed["veri_return_path"]
62
59
  end
63
60
 
64
61
  def shapeshifter?
@@ -68,20 +65,14 @@ module Veri
68
65
  private
69
66
 
70
67
  def with_authentication
71
- if logged_in? && current_session.active?
72
- if current_user.locked?
73
- log_out
74
- when_unauthenticated
75
- else
76
- current_session.update_info(request)
77
- end
78
-
68
+ if logged_in? && !current_user.locked?
69
+ current_session.update_info(request)
79
70
  return
80
71
  end
81
72
 
82
73
  log_out
83
74
 
84
- cookies.signed["#{auth_cookie_prefix}_return_path"] = { value: request.fullpath, expires: 15.minutes.from_now } if request.get? && request.format.html?
75
+ cookies.signed["veri_return_path"] = { value: request.fullpath, expires: 15.minutes.from_now } if request.get? && request.format.html?
85
76
 
86
77
  when_unauthenticated
87
78
  end
@@ -93,15 +84,11 @@ module Veri
93
84
  def current_tenant = nil
94
85
 
95
86
  def resolved_tenant
96
- @resolved_tenant ||= Veri::Inputs::Tenant.new(
97
- current_tenant,
98
- error: Veri::InvalidTenantError,
99
- message: "Expected a string, an ActiveRecord model instance, or nil, got `#{current_tenant.inspect}`"
100
- ).resolve
87
+ Veri::Inputs::Tenant.new(current_tenant).resolve
101
88
  end
102
89
 
103
- def auth_cookie_prefix
104
- @auth_cookie_prefix ||= "auth_#{Zlib.crc32(Marshal.dump(resolved_tenant))}"
90
+ def reset_memoization
91
+ @current_user = @current_session = nil
105
92
  end
106
93
  end
107
94
  end
@@ -3,6 +3,8 @@ module Veri
3
3
  class Authenticatable < Veri::Inputs::Base
4
4
  private
5
5
 
6
+ def default_message = "Expected an instance of #{Veri::Configuration.user_model_name}, got `#{@value.inspect}`"
7
+
6
8
  def processor = -> { @value.is_a?(Veri::Configuration.user_model) ? @value : raise_error }
7
9
  end
8
10
  end
@@ -1,7 +1,7 @@
1
1
  module Veri
2
2
  module Inputs
3
3
  class Base
4
- def initialize(value, optional: false, error: Veri::InvalidArgumentError, message: nil)
4
+ def initialize(value, optional: false, error: nil, message: nil)
5
5
  @value = value
6
6
  @optional = optional
7
7
  @error = error
@@ -20,8 +20,11 @@ module Veri
20
20
  raise NotImplementedError
21
21
  end
22
22
 
23
+ def default_error = Veri::InvalidArgumentError
24
+ def default_message = nil
25
+
23
26
  def raise_error
24
- raise @error, @message
27
+ raise @error || default_error, @message || default_message
25
28
  end
26
29
  end
27
30
  end
@@ -3,6 +3,8 @@ module Veri
3
3
  class NonEmptyString < Veri::Inputs::Base
4
4
  private
5
5
 
6
+ def default_message = "Expected a non-empty string, got `#{@value.inspect}`"
7
+
6
8
  def processor = -> { @value.is_a?(String) && !@value.empty? ? @value : raise_error }
7
9
  end
8
10
  end
@@ -17,6 +17,9 @@ module Veri
17
17
 
18
18
  private
19
19
 
20
+ def default_error = Veri::InvalidTenantError
21
+ def default_message = "Expected a string, an ActiveRecord model instance, or nil, got `#{@value.inspect}`"
22
+
20
23
  def processor
21
24
  -> {
22
25
  return @value if @value.nil?
@@ -16,21 +16,37 @@ module Veri
16
16
  def update_password(password)
17
17
  update!(
18
18
  hashed_password: hasher.create(
19
- Veri::Inputs::NonEmptyString.new(password, message: "Expected a non-empty string, got `#{password.inspect}`").process
19
+ Veri::Inputs::NonEmptyString.new(password).process
20
20
  ),
21
21
  password_updated_at: Time.current
22
22
  )
23
23
  end
24
24
 
25
+ def password_set?
26
+ hashed_password.present?
27
+ end
28
+
25
29
  def verify_password(password)
26
- hasher.verify(
27
- Veri::Inputs::NonEmptyString.new(password, message: "Expected a non-empty string, got `#{password.inspect}`").process,
28
- hashed_password
29
- )
30
+ processed_password = Veri::Inputs::NonEmptyString.new(password).process
31
+
32
+ return false unless password_set?
33
+
34
+ stored_hasher = Veri::Configuration::HASHERS.values.find { _1.match?(hashed_password) }
35
+
36
+ raise Veri::Error, "Unrecognized password hash format" unless stored_hasher
37
+
38
+ return false unless stored_hasher.verify(processed_password, hashed_password)
39
+
40
+ update_column(:hashed_password, hasher.create(processed_password)) unless stored_hasher == hasher
41
+
42
+ true
30
43
  end
31
44
 
32
45
  def lock!
33
- update!(locked: true, locked_at: Time.current)
46
+ transaction do
47
+ update!(locked: true, locked_at: Time.current)
48
+ sessions.terminate_all
49
+ end
34
50
  end
35
51
 
36
52
  def unlock!
@@ -5,8 +5,6 @@ module Veri
5
5
  class Session < ActiveRecord::Base
6
6
  self.table_name = "veri_sessions"
7
7
 
8
- belongs_to :authenticatable, class_name: Veri::Configuration.user_model_name
9
- belongs_to :original_authenticatable, class_name: Veri::Configuration.user_model_name, optional: true
10
8
  belongs_to :tenant, polymorphic: true, optional: true
11
9
  belongs_to :original_tenant, polymorphic: true, optional: true
12
10
 
@@ -62,11 +60,7 @@ module Veri
62
60
  def shapeshift(user, tenant: nil)
63
61
  raise Veri::Error, "Cannot shapeshift from a shapeshifted session" if shapeshifted?
64
62
 
65
- resolved_tenant = Veri::Inputs::Tenant.new(
66
- tenant,
67
- error: Veri::InvalidTenantError,
68
- message: "Expected a string, an ActiveRecord model instance, or nil, got `#{tenant.inspect}`"
69
- ).resolve
63
+ resolved_tenant = Veri::Inputs::Tenant.new(tenant).resolve
70
64
 
71
65
  update!(
72
66
  shapeshifted_at: Time.current,
@@ -74,10 +68,7 @@ module Veri
74
68
  original_tenant_type: tenant_type,
75
69
  original_tenant_id: tenant_id,
76
70
  **resolved_tenant,
77
- authenticatable: Veri::Inputs::Authenticatable.new(
78
- user,
79
- message: "Expected an instance of #{Veri::Configuration.user_model_name}, got `#{user.inspect}`"
80
- ).process
71
+ authenticatable: Veri::Inputs::Authenticatable.new(user).process
81
72
  )
82
73
  end
83
74
 
@@ -93,6 +84,10 @@ module Veri
93
84
  )
94
85
  end
95
86
 
87
+ def switch_tenant(tenant)
88
+ update!(**Veri::Inputs::Tenant.new(tenant).resolve)
89
+ end
90
+
96
91
  def tenant
97
92
  resolve_tenant(tenant_type, tenant_id) { super }
98
93
  end
@@ -128,10 +123,12 @@ module Veri
128
123
 
129
124
  alias terminate_all delete_all
130
125
 
131
- def lookup(token, resolved_tenant)
126
+ def find_active(token, resolved_tenant)
132
127
  return nil if token.blank?
133
128
 
134
- find_by(hashed_token: digest_token(token), **resolved_tenant)
129
+ session = find_by(hashed_token: digest_token(token), **resolved_tenant)
130
+
131
+ session&.active? ? session : nil
135
132
  end
136
133
 
137
134
  private
@@ -12,6 +12,10 @@ module Veri
12
12
  def verify(password, hashed_password)
13
13
  ::Argon2::Password.verify_password(password, hashed_password)
14
14
  end
15
+
16
+ def match?(hashed_password)
17
+ hashed_password.start_with?("$argon2")
18
+ end
15
19
  end
16
20
  end
17
21
  end
@@ -12,6 +12,10 @@ module Veri
12
12
  def verify(password, hashed_password)
13
13
  ::BCrypt::Password.new(hashed_password) == password
14
14
  end
15
+
16
+ def match?(hashed_password)
17
+ hashed_password.start_with?("$2")
18
+ end
15
19
  end
16
20
  end
17
21
  end
@@ -42,6 +42,10 @@ module Veri
42
42
 
43
43
  OpenSSL.fixed_length_secure_compare(recalculated_hash, hash)
44
44
  end
45
+
46
+ def match?(hashed_password)
47
+ hashed_password.start_with?("#{DIGEST}$")
48
+ end
45
49
  end
46
50
  end
47
51
  end
@@ -12,6 +12,10 @@ module Veri
12
12
  def verify(password, hashed_password)
13
13
  ::SCrypt::Password.new(hashed_password) == password
14
14
  end
15
+
16
+ def match?(hashed_password)
17
+ hashed_password.match?(/\A\h+\$\h+\$\h+\$/)
18
+ end
15
19
  end
16
20
  end
17
21
  end
data/lib/veri/railtie.rb CHANGED
@@ -22,8 +22,16 @@ module Veri
22
22
  end
23
23
  end
24
24
 
25
- user_model = Veri::Configuration.user_model
26
- user_model.include Veri::Authenticatable unless user_model < Veri::Authenticatable
25
+ begin
26
+ user_model = Veri::Configuration.user_model
27
+ user_model.include Veri::Authenticatable unless user_model < Veri::Authenticatable
28
+ rescue Veri::ConfigurationError
29
+ raise if Veri::Railtie.server_running?
30
+ end
31
+
32
+ Veri::Session.belongs_to :authenticatable, class_name: Veri::Configuration.user_model_name
33
+ Veri::Session.belongs_to :original_authenticatable, class_name: Veri::Configuration.user_model_name,
34
+ optional: true
27
35
  end
28
36
  end
29
37
 
data/lib/veri/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Veri
2
- VERSION = "2.0.2".freeze
2
+ VERSION = "2.1.0".freeze
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: veri
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.2
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - enjaku4
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-03-28 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: argon2
@@ -147,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
147
  - !ruby/object:Gem::Version
148
148
  version: '0'
149
149
  requirements: []
150
- rubygems_version: 4.0.3
150
+ rubygems_version: 4.0.16
151
151
  specification_version: 4
152
152
  summary: Minimal cookie-based authentication library for Ruby on Rails
153
153
  test_files: []