aikotoba 0.1.0 → 0.1.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 +4 -4
- data/README.md +1 -1
- data/app/models/aikotoba/account/confirmation_token.rb +1 -1
- data/app/models/aikotoba/account/password/argon2.rb +25 -0
- data/app/models/aikotoba/account/password.rb +34 -0
- data/app/models/aikotoba/account/recovery_token.rb +1 -1
- data/app/models/aikotoba/account/service/authentication.rb +10 -9
- data/app/models/aikotoba/account/service/registration.rb +4 -4
- data/app/models/aikotoba/account/{value/token.rb → token.rb} +1 -1
- data/app/models/aikotoba/account/unlock_token.rb +1 -1
- data/app/models/aikotoba/account.rb +3 -3
- data/lib/aikotoba/version.rb +1 -1
- metadata +6 -5
- data/app/models/aikotoba/account/value/password.rb +0 -48
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 173aca2780a3086a9276f56c4bd59a1c4aedb6aa2a8f2d3e29068f00e8a5f417
|
4
|
+
data.tar.gz: d96bea3b841912e7586c07a984e58c6b0daea49109a3b73bc3cc90090cf7434f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 050d2f984527212f1a0426fd830c0b0ae1dfff51ea3e75fda1a5596cc702175996f70c3bbf1ca9acf6b21ea2099863ee5dfa705654db8ad338053ce384aeb635
|
7
|
+
data.tar.gz: 7efcedd94cb09d06f9d846d6d43931ded9eb243dd7625f5a6ab74a41682082ca2d2b4c00feb27a1c282006ab04e44b70086d7024554ac4fe7e73cae273369f93
|
data/README.md
CHANGED
@@ -226,7 +226,7 @@ Rails.application.config.to_prepare do
|
|
226
226
|
end
|
227
227
|
|
228
228
|
class Profile < ApplicationRecord
|
229
|
-
has_one :account, class_name: 'Aikotoba::Account'
|
229
|
+
has_one :account, class_name: 'Aikotoba::Account', as: :authenticate_target
|
230
230
|
end
|
231
231
|
|
232
232
|
current_account.profile #=> Profile instance
|
@@ -10,7 +10,7 @@ module Aikotoba
|
|
10
10
|
scope :active, ->(now: Time.current) { where("expired_at >= ?", now) }
|
11
11
|
|
12
12
|
after_initialize do |record|
|
13
|
-
token = Account::
|
13
|
+
token = Account::Token.new(extipry: Aikotoba.confirmation_token_expiry)
|
14
14
|
record.token ||= token.value
|
15
15
|
record.expired_at ||= token.expired_at
|
16
16
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "argon2"
|
4
|
+
|
5
|
+
module Aikotoba
|
6
|
+
class Account::Password::Argon2
|
7
|
+
def initialize(password:)
|
8
|
+
@password = password
|
9
|
+
end
|
10
|
+
|
11
|
+
def verify_password?(digest)
|
12
|
+
Argon2::Password.verify_password(@password, digest)
|
13
|
+
rescue Argon2::ArgonHashFail # NOTE: If an invalid digest is passed, consider it a mismatch.
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
def generate_hash
|
18
|
+
# NOTE: Adjusted to be OWASAP's recommended value by default.
|
19
|
+
# > Use Argon2id with a minimum configuration of 15 MiB of memory, an iteration count of 2, and 1 degree of parallelism.
|
20
|
+
# > https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#introduction
|
21
|
+
argon = Argon2::Password.new(t_cost: 2, m_cost: 14, p_cost: 1)
|
22
|
+
argon.create(@password)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aikotoba
|
4
|
+
class Account::Password
|
5
|
+
LENGTH_RENGE = Aikotoba.password_length_range
|
6
|
+
|
7
|
+
def initialize(
|
8
|
+
value:,
|
9
|
+
pepper: Aikotoba.password_pepper,
|
10
|
+
algorithm_class: Argon2
|
11
|
+
)
|
12
|
+
@value = value
|
13
|
+
@pepper = pepper
|
14
|
+
@algorithm = algorithm_class.new(password: password_with_pepper(@value))
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :value
|
18
|
+
|
19
|
+
def match?(digest:)
|
20
|
+
@algorithm.verify_password?(digest)
|
21
|
+
end
|
22
|
+
|
23
|
+
def digest
|
24
|
+
return "" if value.blank?
|
25
|
+
@algorithm.generate_hash
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def password_with_pepper(password)
|
31
|
+
"#{password}-#{@pepper}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -10,7 +10,7 @@ module Aikotoba
|
|
10
10
|
scope :active, ->(now: Time.current) { where("expired_at >= ?", now) }
|
11
11
|
|
12
12
|
after_initialize do |record|
|
13
|
-
token = Account::
|
13
|
+
token = Account::Token.new(extipry: Aikotoba.recovery_token_expiry)
|
14
14
|
record.token ||= token.value
|
15
15
|
record.expired_at ||= token.expired_at
|
16
16
|
end
|
@@ -6,12 +6,17 @@ module Aikotoba
|
|
6
6
|
new(email: email, password: password).call!
|
7
7
|
end
|
8
8
|
|
9
|
-
def initialize(email:, password
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
def initialize(email:, password:,
|
10
|
+
account_class: Account,
|
11
|
+
lock_service: Account::Service::Lock,
|
12
|
+
lockable: Account.lockable?,
|
13
|
+
prevent_timing_atack: Aikotoba.prevent_timing_atack)
|
14
|
+
@account_class = account_class
|
15
|
+
@lock_service = lock_service
|
16
|
+
@lockable = lockable
|
13
17
|
@email = email
|
14
18
|
@password = password
|
19
|
+
@aikotoba_prevent_timing_atack = prevent_timing_atack
|
15
20
|
end
|
16
21
|
|
17
22
|
def call!
|
@@ -29,16 +34,12 @@ module Aikotoba
|
|
29
34
|
|
30
35
|
# NOTE: Verify passwords even when accounts are not found to prevent timing attacks.
|
31
36
|
def prevent_timing_atack
|
32
|
-
return true unless aikotoba_prevent_timing_atack
|
37
|
+
return true unless @aikotoba_prevent_timing_atack
|
33
38
|
account = @account_class.build_by(attributes: {email: @email, password: @password})
|
34
39
|
account.password_match?(@password)
|
35
40
|
true
|
36
41
|
end
|
37
42
|
|
38
|
-
def aikotoba_prevent_timing_atack
|
39
|
-
Aikotoba.prevent_timing_atack
|
40
|
-
end
|
41
|
-
|
42
43
|
def success_callback(account)
|
43
44
|
account.authentication_success!
|
44
45
|
end
|
@@ -6,10 +6,10 @@ module Aikotoba
|
|
6
6
|
new.call!(account: account)
|
7
7
|
end
|
8
8
|
|
9
|
-
def initialize
|
10
|
-
@account_class =
|
11
|
-
@confirm_service =
|
12
|
-
@confirmable =
|
9
|
+
def initialize(account_class: Account, confirm_service: Account::Service::Confirmation, confirmable: Account.confirmable?)
|
10
|
+
@account_class = account_class
|
11
|
+
@confirm_service = confirm_service
|
12
|
+
@confirmable = confirmable
|
13
13
|
end
|
14
14
|
|
15
15
|
def call!(account:)
|
@@ -10,7 +10,7 @@ module Aikotoba
|
|
10
10
|
scope :active, ->(now: Time.current) { where("expired_at >= ?", now) }
|
11
11
|
|
12
12
|
after_initialize do |record|
|
13
|
-
token = Account::
|
13
|
+
token = Account::Token.new(extipry: Aikotoba.unlock_token_expiry)
|
14
14
|
record.token ||= token.value
|
15
15
|
record.expired_at ||= token.expired_at
|
16
16
|
end
|
@@ -13,7 +13,7 @@ module Aikotoba
|
|
13
13
|
attribute :max_failed_attempts, :integer, default: -> { Aikotoba.max_failed_attempts }
|
14
14
|
|
15
15
|
validates :email, presence: true, uniqueness: true, format: EMAIL_REGEXP, length: {maximum: EMAIL_MAXIMUM_LENGTH}
|
16
|
-
validates :password, presence: true, length: {in:
|
16
|
+
validates :password, presence: true, length: {in: Password::LENGTH_RENGE}, on: [:create, :recover]
|
17
17
|
validates :password_digest, presence: true
|
18
18
|
validates :confirmed, inclusion: [true, false]
|
19
19
|
validates :failed_attempts, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}
|
@@ -30,7 +30,7 @@ module Aikotoba
|
|
30
30
|
attr_reader :password
|
31
31
|
|
32
32
|
def password=(value)
|
33
|
-
new_password =
|
33
|
+
new_password = Password.new(value: value)
|
34
34
|
@password = new_password.value
|
35
35
|
assign_attributes(password_digest: new_password.digest)
|
36
36
|
end
|
@@ -53,7 +53,7 @@ module Aikotoba
|
|
53
53
|
end
|
54
54
|
|
55
55
|
def password_match?(password)
|
56
|
-
|
56
|
+
Password.new(value: password).match?(digest: password_digest)
|
57
57
|
end
|
58
58
|
|
59
59
|
def authentication_failed!
|
data/lib/aikotoba/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aikotoba
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Madogiwa
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-05-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -61,15 +61,16 @@ files:
|
|
61
61
|
- app/mailers/aikotoba/application_mailer.rb
|
62
62
|
- app/models/aikotoba/account.rb
|
63
63
|
- app/models/aikotoba/account/confirmation_token.rb
|
64
|
+
- app/models/aikotoba/account/password.rb
|
65
|
+
- app/models/aikotoba/account/password/argon2.rb
|
64
66
|
- app/models/aikotoba/account/recovery_token.rb
|
65
67
|
- app/models/aikotoba/account/service/authentication.rb
|
66
68
|
- app/models/aikotoba/account/service/confirmation.rb
|
67
69
|
- app/models/aikotoba/account/service/lock.rb
|
68
70
|
- app/models/aikotoba/account/service/recovery.rb
|
69
71
|
- app/models/aikotoba/account/service/registration.rb
|
72
|
+
- app/models/aikotoba/account/token.rb
|
70
73
|
- app/models/aikotoba/account/unlock_token.rb
|
71
|
-
- app/models/aikotoba/account/value/password.rb
|
72
|
-
- app/models/aikotoba/account/value/token.rb
|
73
74
|
- app/models/concerns/aikotoba/enabled_feature_checkable.rb
|
74
75
|
- app/models/concerns/aikotoba/token_encryptable.rb
|
75
76
|
- app/views/aikotoba/account_mailer/confirm.html.erb
|
@@ -120,7 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
120
121
|
- !ruby/object:Gem::Version
|
121
122
|
version: '0'
|
122
123
|
requirements: []
|
123
|
-
rubygems_version: 3.3.
|
124
|
+
rubygems_version: 3.3.7
|
124
125
|
signing_key:
|
125
126
|
specification_version: 4
|
126
127
|
summary: Aikotoba is a Rails engine that makes it easy to implement simple email and
|
@@ -1,48 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "argon2"
|
4
|
-
|
5
|
-
module Aikotoba
|
6
|
-
class Account::Value::Password
|
7
|
-
LENGTH_RENGE = Aikotoba.password_length_range
|
8
|
-
|
9
|
-
def initialize(
|
10
|
-
value:,
|
11
|
-
pepper: Aikotoba.password_pepper
|
12
|
-
)
|
13
|
-
@value = value
|
14
|
-
@pepper = pepper
|
15
|
-
end
|
16
|
-
|
17
|
-
attr_reader :value
|
18
|
-
|
19
|
-
def match?(digest:)
|
20
|
-
verify_password?(password_with_pepper(value), digest)
|
21
|
-
end
|
22
|
-
|
23
|
-
def digest
|
24
|
-
return "" if value.blank?
|
25
|
-
generate_hash(password_with_pepper(value))
|
26
|
-
end
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
def verify_password?(password, digest)
|
31
|
-
Argon2::Password.verify_password(password, digest)
|
32
|
-
rescue Argon2::ArgonHashFail # NOTE: If an invalid digest is passed, consider it a mismatch.
|
33
|
-
false
|
34
|
-
end
|
35
|
-
|
36
|
-
def password_with_pepper(password)
|
37
|
-
"#{password}-#{@pepper}"
|
38
|
-
end
|
39
|
-
|
40
|
-
def generate_hash(password)
|
41
|
-
# NOTE: Adjusted to be OWASAP's recommended value by default.
|
42
|
-
# > Use Argon2id with a minimum configuration of 15 MiB of memory, an iteration count of 2, and 1 degree of parallelism.
|
43
|
-
# > https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#introduction
|
44
|
-
argon = Argon2::Password.new(t_cost: 2, m_cost: 14, p_cost: 1)
|
45
|
-
argon.create(password)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|