veri 0.2.2 → 0.3.1

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: 33fc3ba81b0ff4f44df7e5bf8e6f6e25c5c318d8b1ca02988f200bf0039d19b9
4
- data.tar.gz: 1433809afa4da2090dac9c75d8e1d6a14579c3ad77ff8ae6db49a5291e708b68
3
+ metadata.gz: a4fcb8a0d60277a1b18a9c23114d96c19472794ee47c02d62445248ff30f16f0
4
+ data.tar.gz: fa8eb3f99daf00c2c7c338350f14d3014ab855923e6977581d60e376c9dc3222
5
5
  SHA512:
6
- metadata.gz: 86fcd9a468d1f4fcb0c7fbe820c4e67d40a6c5e96a50bb2804141c4ad9960c5f42e0678bdd6bdf8284e9403c7d77e41fb0f756c78cf4acd019d6094aef95212a
7
- data.tar.gz: e50a7bd3673bc9d806e2ed8f3e50820b75373df52720407ccf400073de0ff726ec1e8e89694369878f06b83f8cf8516fdad5ece1645dd62f0afd6ff74e36df95
6
+ metadata.gz: d21bba8f857717fd6e9c54b1444c23feb2f5dea73dccda0ca3d661155b6a79d2901b8f4a58717bdb28527d6e1576222688fd802da86c18e86055d301e6c2003f
7
+ data.tar.gz: a3547cee5d2bf4190fc95f923177da126e4f478ef8dc052d49bbdc524515be211d05ab21a0636fd373edb9b7d24b76049e7ef40bf826d667e85226da0b4ab7b1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## v0.3.1
2
+
3
+ ### Misc
4
+
5
+ - Minor improvements and code cleanup
6
+ - Relaxed dependency versions
7
+
8
+ ## v0.3.0
9
+
10
+ ### Breaking
11
+
12
+ - Added account lockout feature
13
+
1
14
  ## v0.2.2
2
15
 
3
16
  ### Bugs
data/README.md CHANGED
@@ -8,10 +8,11 @@ Veri is a cookie-based authentication library for Ruby on Rails that provides es
8
8
  **Key Features:**
9
9
 
10
10
  - Cookie-based authentication with database-stored sessions
11
- - Supports multiple password hashing algorithms (argon2, bcrypt, scrypt)
11
+ - Multiple password hashing algorithms (argon2, bcrypt, scrypt)
12
12
  - Granular session management and control
13
- - Built-in return path handling
13
+ - Return path handling
14
14
  - User impersonation feature
15
+ - Account lockout functionality
15
16
 
16
17
  > ⚠️ **Development Notice**<br>
17
18
  > Veri is functional but in early development. Breaking changes may occur in minor releases until v1.0!
@@ -24,11 +25,12 @@ Veri is a cookie-based authentication library for Ruby on Rails that provides es
24
25
  - [Password Management](#password-management)
25
26
  - [Controller Integration](#controller-integration)
26
27
  - [Authentication Sessions](#authentication-sessions)
28
+ - [Account Lockout](#account-lockout)
27
29
  - [View Helpers](#view-helpers)
28
30
  - [Testing](#testing)
29
31
 
30
32
  **Community Resources:**
31
- - [Contributing](#contributing)
33
+ - [Getting Help and Contributing](#getting-help-and-contributing)
32
34
  - [License](#license)
33
35
  - [Code of Conduct](#code-of-conduct)
34
36
 
@@ -108,7 +110,7 @@ end
108
110
 
109
111
  ### Authentication Methods
110
112
 
111
- This is a simplified example of how to use Veri's authentication methods in your controllers:
113
+ This is a simplified example of how to use Veri's authentication methods:
112
114
 
113
115
  ```rb
114
116
  class SessionsController < ApplicationController
@@ -137,7 +139,7 @@ Available methods:
137
139
 
138
140
  - `current_user` - Returns authenticated user or `nil`
139
141
  - `logged_in?` - Returns `true` if user is authenticated
140
- - `log_in(user)` - Authenticates user and creates session
142
+ - `log_in(user)` - Authenticates user and creates session, returns `true` on success or `false` if account is locked
141
143
  - `log_out` - Terminates current session
142
144
  - `return_path` - Returns path user was accessing before authentication
143
145
  - `current_session` - Returns current authentication session
@@ -249,6 +251,23 @@ Veri::Session.prune # All sessions
249
251
  Veri::Session.prune(user) # Specific user's sessions
250
252
  ```
251
253
 
254
+ ## Account Lockout
255
+
256
+ Veri provides account lockout functionality to temporarily disable user accounts (for example, after too many failed login attempts or for security reasons).
257
+
258
+ ```rb
259
+ # Lock a user account
260
+ user.lock!
261
+
262
+ # Unlock a user account
263
+ user.unlock!
264
+
265
+ # Check if account is locked
266
+ user.locked?
267
+ ```
268
+
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.
270
+
252
271
  ## View Helpers
253
272
 
254
273
  Access authentication state in your views:
@@ -310,7 +329,7 @@ RSpec.configure do |config|
310
329
  end
311
330
  ```
312
331
 
313
- ## Contributing
332
+ ## Getting Help and Contributing
314
333
 
315
334
  ### Getting Help
316
335
  Have a question or need assistance? Open a discussion in our [discussions section](https://github.com/brownboxdev/veri/discussions) for:
@@ -330,7 +349,7 @@ Ready to contribute? You can:
330
349
  - Improve documentation
331
350
  - Add new features (please discuss first in our [discussions section](https://github.com/brownboxdev/veri/discussions))
332
351
 
333
- Before contributing, please read the [contributing guidelines](https://github.com/brownboxdev/veri/blob/master/CONTRIBUTING.md)
352
+ Before contributing, please read the [contributing guidelines](https://github.com/brownboxdev/veri/blob/main/CONTRIBUTING.md)
334
353
 
335
354
  ## License
336
355
 
@@ -2,6 +2,8 @@ class AddVeriAuthentication < ActiveRecord::Migration[<%= ActiveRecord::Migratio
2
2
  def change
3
3
  add_column <%= table_name.to_sym.inspect %>, :hashed_password, :text
4
4
  add_column <%= table_name.to_sym.inspect %>, :password_updated_at, :datetime
5
+ add_column <%= table_name.to_sym.inspect %>, :locked, :boolean, default: false, null: false
6
+ add_column <%= table_name.to_sym.inspect %>, :locked_at, :datetime
5
7
 
6
8
  create_table :veri_sessions<%= ", id: :uuid" if options[:uuid] %> do |t|
7
9
  t.string :hashed_token, null: false, index: { unique: true }
@@ -11,46 +11,42 @@ module Veri
11
11
  default: :argon2,
12
12
  reader: true,
13
13
  constructor: -> (value) do
14
- Veri::Inputs.process(
14
+ Veri::Inputs::HashingAlgorithm.new(
15
15
  value,
16
- as: :hashing_algorithm,
17
16
  error: Veri::ConfigurationError,
18
17
  message: "Invalid hashing algorithm `#{value.inspect}`, supported algorithms are: #{Veri::Configuration::HASHERS.keys.join(", ")}"
19
- )
18
+ ).process
20
19
  end
21
20
  setting :inactive_session_lifetime,
22
21
  default: nil,
23
22
  reader: true,
24
23
  constructor: -> (value) do
25
- Veri::Inputs.process(
24
+ Veri::Inputs::Duration.new(
26
25
  value,
27
- as: :duration,
28
26
  optional: true,
29
27
  error: Veri::ConfigurationError,
30
28
  message: "Invalid inactive session lifetime `#{value.inspect}`, expected an instance of ActiveSupport::Duration or nil"
31
- )
29
+ ).process
32
30
  end
33
31
  setting :total_session_lifetime,
34
32
  default: 14.days,
35
33
  reader: true,
36
34
  constructor: -> (value) do
37
- Veri::Inputs.process(
35
+ Veri::Inputs::Duration.new(
38
36
  value,
39
- as: :duration,
40
37
  error: Veri::ConfigurationError,
41
38
  message: "Invalid total session lifetime `#{value.inspect}`, expected an instance of ActiveSupport::Duration"
42
- )
39
+ ).process
43
40
  end
44
41
  setting :user_model_name,
45
42
  default: "User",
46
43
  reader: true,
47
44
  constructor: -> (value) do
48
- Veri::Inputs.process(
45
+ Veri::Inputs::NonEmptyString.new(
49
46
  value,
50
- as: :non_empty_string,
51
47
  error: Veri::ConfigurationError,
52
48
  message: "Invalid user model name `#{value.inspect}`, expected an ActiveRecord model name as a string"
53
- )
49
+ ).process
54
50
  end
55
51
 
56
52
  HASHERS = {
@@ -64,12 +60,11 @@ module Veri
64
60
  end
65
61
 
66
62
  def user_model
67
- Veri::Inputs.process(
63
+ Veri::Inputs::Model.new(
68
64
  user_model_name,
69
- as: :model,
70
65
  error: Veri::ConfigurationError,
71
66
  message: "Invalid user model name `#{user_model_name}`, expected an ActiveRecord model name as a string"
72
- )
67
+ ).process
73
68
  end
74
69
  end
75
70
  end
@@ -34,13 +34,16 @@ module Veri
34
34
  end
35
35
 
36
36
  def log_in(authenticatable)
37
- processed_authenticatable = Veri::Inputs.process(
37
+ processed_authenticatable = Veri::Inputs::Authenticatable.new(
38
38
  authenticatable,
39
- as: :authenticatable,
40
39
  message: "Expected an instance of #{Veri::Configuration.user_model_name}, got `#{authenticatable.inspect}`"
41
- )
40
+ ).process
41
+
42
+ return false if processed_authenticatable.locked?
43
+
42
44
  token = Veri::Session.establish(processed_authenticatable, request)
43
45
  cookies.encrypted.permanent[:veri_token] = { value: token, httponly: true }
46
+ true
44
47
  end
45
48
 
46
49
  def log_out
@@ -63,9 +66,18 @@ module Veri
63
66
  private
64
67
 
65
68
  def with_authentication
66
- current_session.update_info(request) and return if logged_in? && current_session.active?
69
+ if logged_in? && current_session.active?
70
+ if current_user.locked?
71
+ log_out
72
+ when_unauthenticated
73
+ else
74
+ current_session.update_info(request)
75
+ end
76
+
77
+ return
78
+ end
67
79
 
68
- current_session&.terminate
80
+ log_out
69
81
 
70
82
  cookies.signed[:veri_return_path] = { value: request.fullpath, expires: 15.minutes.from_now } if request.get? && request.format.html?
71
83
 
@@ -0,0 +1,9 @@
1
+ module Veri
2
+ module Inputs
3
+ class Authenticatable < Veri::Inputs::Base
4
+ private
5
+
6
+ def type = -> { self.class::Instance(Veri::Configuration.user_model) }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,33 @@
1
+ require "dry-types"
2
+
3
+ module Veri
4
+ module Inputs
5
+ class Base
6
+ include Dry.Types()
7
+
8
+ def initialize(value, optional: false, error: Veri::InvalidArgumentError, message: nil)
9
+ @value = value
10
+ @optional = optional
11
+ @error = error
12
+ @message = message
13
+ end
14
+
15
+ def process
16
+ type_checker = @optional ? type.call.optional : type.call
17
+ type_checker[@value]
18
+ rescue Dry::Types::CoercionError
19
+ raise_error
20
+ end
21
+
22
+ private
23
+
24
+ def type
25
+ raise NotImplementedError
26
+ end
27
+
28
+ def raise_error
29
+ raise @error, @message
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ module Veri
2
+ module Inputs
3
+ class Duration < Veri::Inputs::Base
4
+ private
5
+
6
+ def type = -> { self.class::Instance(ActiveSupport::Duration) }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Veri
2
+ module Inputs
3
+ class HashingAlgorithm < Veri::Inputs::Base
4
+ private
5
+
6
+ def type = -> { self.class::Strict::Symbol.enum(:argon2, :bcrypt, :scrypt) }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Veri
2
+ module Inputs
3
+ class Model < Veri::Inputs::Base
4
+ private
5
+
6
+ def type = -> { self.class::Strict::Class.constructor { _1.try(:safe_constantize) }.constrained(lt: ActiveRecord::Base) }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Veri
2
+ module Inputs
3
+ class NonEmptyString < Veri::Inputs::Base
4
+ private
5
+
6
+ def type = -> { self.class::Strict::String.constrained(min_size: 1) }
7
+ end
8
+ end
9
+ end
@@ -13,11 +13,7 @@ module Veri
13
13
  def update_password(password)
14
14
  update!(
15
15
  hashed_password: hasher.create(
16
- Veri::Inputs.process(
17
- password,
18
- as: :non_empty_string,
19
- message: "Expected a non-empty string, got `#{password.inspect}`"
20
- )
16
+ Veri::Inputs::NonEmptyString.new(password, message: "Expected a non-empty string, got `#{password.inspect}`").process
21
17
  ),
22
18
  password_updated_at: Time.current
23
19
  )
@@ -25,15 +21,19 @@ module Veri
25
21
 
26
22
  def verify_password(password)
27
23
  hasher.verify(
28
- Veri::Inputs.process(
29
- password,
30
- as: :non_empty_string,
31
- message: "Expected a non-empty string, got `#{password.inspect}`"
32
- ),
24
+ Veri::Inputs::NonEmptyString.new(password, message: "Expected a non-empty string, got `#{password.inspect}`").process,
33
25
  hashed_password
34
26
  )
35
27
  end
36
28
 
29
+ def lock!
30
+ update!(locked: true, locked_at: Time.current)
31
+ end
32
+
33
+ def unlock!
34
+ update!(locked: false, locked_at: nil)
35
+ end
36
+
37
37
  private
38
38
 
39
39
  def hasher
@@ -26,12 +26,10 @@ module Veri
26
26
  alias terminate delete
27
27
 
28
28
  def update_info(request)
29
- processed_request = Veri::Inputs.process(request, as: :request, error: Veri::Error)
30
-
31
29
  update!(
32
30
  last_seen_at: Time.current,
33
- ip_address: processed_request.remote_ip,
34
- user_agent: processed_request.user_agent
31
+ ip_address: request.remote_ip,
32
+ user_agent: request.user_agent
35
33
  )
36
34
  end
37
35
 
@@ -55,11 +53,10 @@ module Veri
55
53
  update!(
56
54
  shapeshifted_at: Time.current,
57
55
  original_authenticatable: authenticatable,
58
- authenticatable: Veri::Inputs.process(
56
+ authenticatable: Veri::Inputs::Authenticatable.new(
59
57
  user,
60
- as: :authenticatable,
61
58
  message: "Expected an instance of #{Veri::Configuration.user_model_name}, got `#{user.inspect}`"
62
- )
59
+ ).process
63
60
  )
64
61
  end
65
62
 
@@ -79,10 +76,8 @@ module Veri
79
76
  new(
80
77
  hashed_token: Digest::SHA256.hexdigest(token),
81
78
  expires_at:,
82
- authenticatable: Veri::Inputs.process(user, as: :authenticatable, error: Veri::Error)
83
- ).update_info(
84
- Veri::Inputs.process(request, as: :request, error: Veri::Error)
85
- )
79
+ authenticatable: Veri::Inputs::Authenticatable.new(user, error: Veri::Error).process
80
+ ).update_info(request)
86
81
 
87
82
  token
88
83
  end
@@ -90,12 +85,11 @@ module Veri
90
85
  def prune(user = nil)
91
86
  scope = if user
92
87
  where(
93
- authenticatable: Veri::Inputs.process(
88
+ authenticatable: Veri::Inputs::Authenticatable.new(
94
89
  user,
95
- as: :authenticatable,
96
90
  optional: true,
97
91
  message: "Expected an instance of #{Veri::Configuration.user_model_name} or nil, got `#{user.inspect}`"
98
- )
92
+ ).process
99
93
  )
100
94
  else
101
95
  all
@@ -112,11 +106,10 @@ module Veri
112
106
  end
113
107
 
114
108
  def terminate_all(user)
115
- Veri::Inputs.process(
109
+ Veri::Inputs::Authenticatable.new(
116
110
  user,
117
- as: :authenticatable,
118
111
  message: "Expected an instance of #{Veri::Configuration.user_model_name}, got `#{user.inspect}`"
119
- ).veri_sessions.delete_all
112
+ ).process.veri_sessions.delete_all
120
113
  end
121
114
  end
122
115
  end
data/lib/veri/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Veri
2
- VERSION = "0.2.2".freeze
2
+ VERSION = "0.3.1".freeze
3
3
  end
data/lib/veri.rb CHANGED
@@ -7,13 +7,18 @@ require_relative "veri/password/argon2"
7
7
  require_relative "veri/password/bcrypt"
8
8
  require_relative "veri/password/scrypt"
9
9
 
10
- require_relative "veri/inputs"
10
+ require_relative "veri/inputs/base"
11
+ require_relative "veri/inputs/authenticatable"
12
+ require_relative "veri/inputs/duration"
13
+ require_relative "veri/inputs/hashing_algorithm"
14
+ require_relative "veri/inputs/model"
15
+ require_relative "veri/inputs/non_empty_string"
11
16
  require_relative "veri/configuration"
12
17
 
13
18
  module Veri
14
19
  class Error < StandardError; end
15
- class ConfigurationError < Veri::Error; end
16
20
  class InvalidArgumentError < Veri::Error; end
21
+ class ConfigurationError < Veri::InvalidArgumentError; end
17
22
 
18
23
  delegate :configure, to: Veri::Configuration
19
24
  module_function :configure
data/veri.gemspec CHANGED
@@ -21,8 +21,8 @@ Gem::Specification.new do |spec|
21
21
 
22
22
  spec.add_dependency "argon2", "~> 2.0"
23
23
  spec.add_dependency "bcrypt", "~> 3.0"
24
- spec.add_dependency "dry-configurable", "~> 1.3"
25
- spec.add_dependency "dry-types", "~> 1.8"
24
+ spec.add_dependency "dry-configurable", "~> 1.1"
25
+ spec.add_dependency "dry-types", "~> 1.7"
26
26
  spec.add_dependency "rails", ">= 7.1", "< 8.1"
27
27
  spec.add_dependency "scrypt", "~> 3.0"
28
28
  spec.add_dependency "user_agent_parser", "~> 2.0"
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.2.2
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - enjaku4
@@ -43,28 +43,28 @@ dependencies:
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '1.3'
46
+ version: '1.1'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '1.3'
53
+ version: '1.1'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: dry-types
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '1.8'
60
+ version: '1.7'
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '1.8'
67
+ version: '1.7'
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: rails
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -125,7 +125,12 @@ files:
125
125
  - lib/veri.rb
126
126
  - lib/veri/configuration.rb
127
127
  - lib/veri/controllers/concerns/authentication.rb
128
- - lib/veri/inputs.rb
128
+ - lib/veri/inputs/authenticatable.rb
129
+ - lib/veri/inputs/base.rb
130
+ - lib/veri/inputs/duration.rb
131
+ - lib/veri/inputs/hashing_algorithm.rb
132
+ - lib/veri/inputs/model.rb
133
+ - lib/veri/inputs/non_empty_string.rb
129
134
  - lib/veri/models/concerns/authenticatable.rb
130
135
  - lib/veri/models/session.rb
131
136
  - lib/veri/password/argon2.rb
@@ -159,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
159
164
  - !ruby/object:Gem::Version
160
165
  version: '0'
161
166
  requirements: []
162
- rubygems_version: 3.6.7
167
+ rubygems_version: 3.7.1
163
168
  specification_version: 4
164
169
  summary: Minimal cookie-based authentication library for Ruby on Rails
165
170
  test_files: []
data/lib/veri/inputs.rb DELETED
@@ -1,31 +0,0 @@
1
- require "dry-types"
2
-
3
- module Veri
4
- module Inputs
5
- extend self
6
-
7
- include Dry.Types()
8
-
9
- TYPES = {
10
- hashing_algorithm: -> { self::Strict::Symbol.enum(:argon2, :bcrypt, :scrypt) },
11
- duration: -> { self::Instance(ActiveSupport::Duration) },
12
- non_empty_string: -> { self::Strict::String.constrained(min_size: 1) },
13
- model: -> { self::Strict::Class.constructor { _1.try(:safe_constantize) || _1 }.constrained(lt: ActiveRecord::Base) },
14
- authenticatable: -> { self::Instance(Veri::Configuration.user_model) },
15
- request: -> { self::Instance(ActionDispatch::Request) }
16
- }.freeze
17
-
18
- def process(value, as:, optional: false, error: Veri::InvalidArgumentError, message: nil)
19
- checker = type_for(as)
20
- checker = checker.optional if optional
21
-
22
- checker[value]
23
- rescue Dry::Types::CoercionError => e
24
- raise error, message || e.message
25
- end
26
-
27
- private
28
-
29
- def type_for(name) = Veri::Inputs::TYPES.fetch(name).call
30
- end
31
- end