verify_it 0.4.2 → 0.5.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 +33 -0
- data/README.md +49 -36
- data/app/controllers/verify_it/verifications_controller.rb +1 -1
- data/config/locales/en.yml +1 -0
- data/lib/generators/verify_it/install/templates/create_verify_it_tables.rb +2 -2
- data/lib/verify_it/configuration.rb +4 -0
- data/lib/verify_it/rate_limiter.rb +24 -0
- data/lib/verify_it/storage/database_storage.rb +25 -31
- data/lib/verify_it/storage/models/attempt.rb +1 -1
- data/lib/verify_it/verifiable.rb +3 -2
- data/lib/verify_it/verifier.rb +24 -1
- data/lib/verify_it/version.rb +1 -1
- data/lib/verify_it.rb +2 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: be733b2c9a751f9163ffcc41c67a22c88b19a92baf8e8e5d478f0d2cfb3c1c3b
|
|
4
|
+
data.tar.gz: fb2580dd55fda49704fa8c327721eda31d470fff6f8f4177adb3741d24d89553
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d87fdcf7e40044a25259f945151644337b21d6e404e6fc793a3b9d856f745da23fe1014e34a7240d1386ca24a7e096b2233c19548e0ba8a86470faf98eb459db
|
|
7
|
+
data.tar.gz: bf5ef12903fe759e87de6ff9cd6bda4cb53b4abe3fe5c71aaae00c63a0efd3f2290abdff32706e0b7868e276a68580d78af847a15d82beab19531a522703e5dc
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
## [0.5.0] - 2026-03-22
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- **IP-based rate limiting** — new opt-in configuration options `max_ip_send_attempts`
|
|
5
|
+
and `max_ip_verification_attempts` protect against SMS pumping, distributed brute
|
|
6
|
+
force, and IP-based abuse. Both default to `nil` (disabled). When configured, IP
|
|
7
|
+
attempts are tracked using the existing storage backends with no schema changes required.
|
|
8
|
+
- `Verifier#send_code` now accepts an optional `request:` keyword, matching
|
|
9
|
+
`verify_code`. The engine controller and `Verifiable` concern forward it automatically.
|
|
10
|
+
- `RateLimiter` gains `ip_send_rate_limited?`, `ip_verification_rate_limited?`,
|
|
11
|
+
`record_ip_send_attempt`, and `record_ip_verification_attempt` methods.
|
|
12
|
+
- `Verifiable#send_{channel}_code` now accepts an optional `request:` keyword.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **DatabaseStorage race conditions** — `store_code` and `increment_attempt_count`
|
|
16
|
+
now use atomic patterns with `rescue ActiveRecord::RecordNotUnique` + retry,
|
|
17
|
+
preventing duplicate records and rate limit bypass under concurrency.
|
|
18
|
+
- Migration template indexes on `verify_it_codes` and `verify_it_attempts` are now
|
|
19
|
+
`unique: true`, enforcing data integrity at the database level.
|
|
20
|
+
- `Attempt` model validation now accepts `ip_send` and `ip_verification` attempt types.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- README: generator commands now show the bare command first; `--storage` and
|
|
24
|
+
`--engine` are documented as opt-in flags with defaults explained
|
|
25
|
+
- README: moved Rails Engine section right after Rails Setup, before Plain Ruby,
|
|
26
|
+
so all Rails content is grouped together
|
|
27
|
+
- README: Plain Ruby examples use `record: user` instead of `record: current_user`
|
|
28
|
+
to avoid implying a Rails dependency
|
|
29
|
+
- README: Engine section notes that `--engine` already generates mount and resolver
|
|
30
|
+
config, so users don't duplicate setup
|
|
31
|
+
- README: Engine resolver example no longer hardcodes Redis storage; replaced with
|
|
32
|
+
a storage-agnostic comment pointing to the Configuration Reference
|
|
33
|
+
|
|
1
34
|
## [0.4.2] - 2026-03-21
|
|
2
35
|
|
|
3
36
|
### Added
|
data/README.md
CHANGED
|
@@ -35,21 +35,21 @@ bundle install
|
|
|
35
35
|
### 1. Generate configuration files
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
rails generate verify_it:install
|
|
38
|
+
rails generate verify_it:install
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- `
|
|
44
|
-
- A database migration (if you chose `--storage=database`)
|
|
41
|
+
Available options:
|
|
42
|
+
- `--storage=memory|redis|database` (default: `memory`)
|
|
43
|
+
- `--engine` — mount the VerifyIt engine and generate resolver config
|
|
45
44
|
|
|
46
|
-
|
|
45
|
+
Examples:
|
|
47
46
|
|
|
48
47
|
```bash
|
|
49
|
-
rails generate verify_it:install --storage=redis
|
|
48
|
+
rails generate verify_it:install --storage=redis
|
|
49
|
+
rails generate verify_it:install --storage=database --engine
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
If you chose database
|
|
52
|
+
If you chose `--storage=database`, run the migration:
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
55
|
rails db:migrate
|
|
@@ -165,32 +165,6 @@ current_user.send_email_code(context: {
|
|
|
165
165
|
|
|
166
166
|
---
|
|
167
167
|
|
|
168
|
-
## Plain Ruby
|
|
169
|
-
|
|
170
|
-
```ruby
|
|
171
|
-
require "verify_it"
|
|
172
|
-
|
|
173
|
-
VerifyIt.configure do |config|
|
|
174
|
-
config.secret_key_base = ENV.fetch("VERIFY_IT_SECRET")
|
|
175
|
-
config.storage = :memory # :memory, :redis, or :database
|
|
176
|
-
|
|
177
|
-
config.sms_sender = ->(to:, code:, context:) {
|
|
178
|
-
MySmsSender.deliver(to: to, body: "Your code: #{code}")
|
|
179
|
-
}
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# Send a code
|
|
183
|
-
result = VerifyIt.send_code(to: "+15551234567", channel: :sms, record: current_user)
|
|
184
|
-
|
|
185
|
-
# Verify a code
|
|
186
|
-
result = VerifyIt.verify_code(to: "+15551234567", code: "123456", record: current_user)
|
|
187
|
-
|
|
188
|
-
# Clean up
|
|
189
|
-
VerifyIt.cleanup(to: "+15551234567", record: current_user)
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
---
|
|
193
|
-
|
|
194
168
|
## Rails Engine (HTTP Endpoints)
|
|
195
169
|
|
|
196
170
|
VerifyIt ships a mountable Rails Engine that exposes two JSON endpoints. Use the
|
|
@@ -198,6 +172,8 @@ engine when you want drop-in endpoints without writing controller code. This is
|
|
|
198
172
|
optional - skip this section if you prefer to handle verification in your own
|
|
199
173
|
controllers using the `Verifiable` concern or the plain Ruby API.
|
|
200
174
|
|
|
175
|
+
> If you ran the installer with `--engine`, the mount and resolver config are already generated for you. The details below are for reference or manual setup.
|
|
176
|
+
|
|
201
177
|
### Mount the engine
|
|
202
178
|
|
|
203
179
|
```ruby
|
|
@@ -220,8 +196,7 @@ Two additional config options are **required** when using the engine endpoints:
|
|
|
220
196
|
# config/initializers/verify_it.rb
|
|
221
197
|
VerifyIt.configure do |config|
|
|
222
198
|
config.secret_key_base = Rails.application.secret_key_base
|
|
223
|
-
|
|
224
|
-
config.redis_client = Redis.new
|
|
199
|
+
# ... your storage config (see Configuration Reference) ...
|
|
225
200
|
|
|
226
201
|
# Resolve the authenticated record from the request (e.g. from a session or JWT).
|
|
227
202
|
config.current_record_resolver = ->(request) {
|
|
@@ -291,6 +266,32 @@ en:
|
|
|
291
266
|
|
|
292
267
|
---
|
|
293
268
|
|
|
269
|
+
## Plain Ruby
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
require "verify_it"
|
|
273
|
+
|
|
274
|
+
VerifyIt.configure do |config|
|
|
275
|
+
config.secret_key_base = ENV.fetch("VERIFY_IT_SECRET")
|
|
276
|
+
config.storage = :memory # :memory, :redis, or :database
|
|
277
|
+
|
|
278
|
+
config.sms_sender = ->(to:, code:, context:) {
|
|
279
|
+
MySmsSender.deliver(to: to, body: "Your code: #{code}")
|
|
280
|
+
}
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Send a code
|
|
284
|
+
result = VerifyIt.send_code(to: "+15551234567", channel: :sms, record: user)
|
|
285
|
+
|
|
286
|
+
# Verify a code
|
|
287
|
+
result = VerifyIt.verify_code(to: "+15551234567", code: "123456", record: user)
|
|
288
|
+
|
|
289
|
+
# Clean up
|
|
290
|
+
VerifyIt.cleanup(to: "+15551234567", record: user)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
294
295
|
## Configuration Reference
|
|
295
296
|
|
|
296
297
|
| Option | Default | Accepted values |
|
|
@@ -304,6 +305,8 @@ en:
|
|
|
304
305
|
| `max_send_attempts` | `3` | Integer |
|
|
305
306
|
| `max_verification_attempts` | `5` | Integer |
|
|
306
307
|
| `max_identifier_changes` | `5` | Integer |
|
|
308
|
+
| `max_ip_send_attempts` | `nil` | Integer or `nil` (disabled) — max sends per IP per window |
|
|
309
|
+
| `max_ip_verification_attempts` | `nil` | Integer or `nil` (disabled) — max verifications per IP per window |
|
|
307
310
|
| `rate_limit_window` | `3600` | Seconds (integer) |
|
|
308
311
|
| `delivery_channel` | `:sms` | `:sms`, `:email` |
|
|
309
312
|
| `sms_sender` | `nil` | Lambda `(to:, code:, context:) { }` |
|
|
@@ -400,6 +403,16 @@ end
|
|
|
400
403
|
- Use `:redis` or `:database` storage in production
|
|
401
404
|
- Keep `code_ttl` short.
|
|
402
405
|
- Use `on_verify_failure` to monitor and alert on repeated failures
|
|
406
|
+
- **IP-based rate limiting** — enable `max_ip_send_attempts` and `max_ip_verification_attempts` to protect against SMS pumping and distributed brute force attacks:
|
|
407
|
+
|
|
408
|
+
```ruby
|
|
409
|
+
VerifyIt.configure do |config|
|
|
410
|
+
config.max_ip_send_attempts = 10 # max 10 sends per IP per window
|
|
411
|
+
config.max_ip_verification_attempts = 20 # max 20 verifications per IP per window
|
|
412
|
+
end
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
IP rate limiting requires passing `request:` to `send_code`/`verify_code`. The engine controller does this automatically. For custom controllers, pass `request:` explicitly.
|
|
403
416
|
|
|
404
417
|
---
|
|
405
418
|
|
|
@@ -13,7 +13,7 @@ module VerifyIt
|
|
|
13
13
|
|
|
14
14
|
channel = resolve_channel
|
|
15
15
|
identifier = resolve_identifier(record, channel)
|
|
16
|
-
result = VerifyIt.send_code(to: identifier, record: record, channel: channel, context: {})
|
|
16
|
+
result = VerifyIt.send_code(to: identifier, record: record, channel: channel, context: {}, request: request)
|
|
17
17
|
|
|
18
18
|
if result.success?
|
|
19
19
|
payload = { message: I18n.t("verify_it.responses.sent") }
|
data/config/locales/en.yml
CHANGED
|
@@ -9,6 +9,7 @@ en:
|
|
|
9
9
|
code_not_found: "No active verification code found. Please request a new one."
|
|
10
10
|
locked: "Your account is temporarily locked due to too many failed attempts."
|
|
11
11
|
delivery_failed: "Failed to deliver verification code. Please try again."
|
|
12
|
+
ip_rate_limited: "Too many requests from this IP address. Please try again later."
|
|
12
13
|
sms:
|
|
13
14
|
default_message: "%{code} is your verification code."
|
|
14
15
|
email:
|
|
@@ -8,7 +8,7 @@ class CreateVerifyItTables < ActiveRecord::Migration[<%= ActiveRecord::Migration
|
|
|
8
8
|
t.datetime :expires_at, null: false
|
|
9
9
|
t.timestamps
|
|
10
10
|
|
|
11
|
-
t.index [:identifier, :record_type, :record_id], name: "index_verify_it_codes_on_record"
|
|
11
|
+
t.index [:identifier, :record_type, :record_id], name: "index_verify_it_codes_on_record", unique: true
|
|
12
12
|
t.index :expires_at
|
|
13
13
|
end
|
|
14
14
|
|
|
@@ -22,7 +22,7 @@ class CreateVerifyItTables < ActiveRecord::Migration[<%= ActiveRecord::Migration
|
|
|
22
22
|
t.timestamps
|
|
23
23
|
|
|
24
24
|
t.index [:identifier, :record_type, :record_id, :attempt_type],
|
|
25
|
-
name: "index_verify_it_attempts_on_record_and_type"
|
|
25
|
+
name: "index_verify_it_attempts_on_record_and_type", unique: true
|
|
26
26
|
t.index :expires_at
|
|
27
27
|
end
|
|
28
28
|
|
|
@@ -21,6 +21,8 @@ module VerifyIt
|
|
|
21
21
|
:test_mode,
|
|
22
22
|
:bypass_delivery,
|
|
23
23
|
:secret_key_base,
|
|
24
|
+
:max_ip_send_attempts,
|
|
25
|
+
:max_ip_verification_attempts,
|
|
24
26
|
:current_record_resolver,
|
|
25
27
|
:identifier_resolver
|
|
26
28
|
|
|
@@ -55,6 +57,8 @@ module VerifyIt
|
|
|
55
57
|
@test_mode = false
|
|
56
58
|
@bypass_delivery = false
|
|
57
59
|
@secret_key_base = nil
|
|
60
|
+
@max_ip_send_attempts = nil
|
|
61
|
+
@max_ip_verification_attempts = nil
|
|
58
62
|
@current_record_resolver = nil
|
|
59
63
|
@identifier_resolver = nil
|
|
60
64
|
end
|
|
@@ -64,5 +64,29 @@ module VerifyIt
|
|
|
64
64
|
def reset_verification_attempts(identifier:, record:)
|
|
65
65
|
@storage.reset_attempts(identifier: identifier, record: record)
|
|
66
66
|
end
|
|
67
|
+
|
|
68
|
+
def ip_send_rate_limited?(ip:)
|
|
69
|
+
max = VerifyIt.configuration.max_ip_send_attempts
|
|
70
|
+
return false unless max
|
|
71
|
+
|
|
72
|
+
count = @storage.send_count(identifier: ip, record: nil)
|
|
73
|
+
count >= max
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def ip_verification_rate_limited?(ip:)
|
|
77
|
+
max = VerifyIt.configuration.max_ip_verification_attempts
|
|
78
|
+
return false unless max
|
|
79
|
+
|
|
80
|
+
count = @storage.attempts(identifier: ip, record: nil)
|
|
81
|
+
count >= max
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def record_ip_send_attempt(ip:)
|
|
85
|
+
@storage.increment_send_count(identifier: ip, record: nil)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def record_ip_verification_attempt(ip:)
|
|
89
|
+
@storage.increment_attempts(identifier: ip, record: nil)
|
|
90
|
+
end
|
|
67
91
|
end
|
|
68
92
|
end
|
|
@@ -25,13 +25,14 @@ module VerifyIt
|
|
|
25
25
|
attrs[:record_id] = record.id
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
# Find existing or create new
|
|
29
28
|
existing = find_code(identifier: identifier, record: record)
|
|
30
29
|
if existing
|
|
31
|
-
existing.update!(
|
|
30
|
+
existing.update!(code: code, expires_at: expires_at)
|
|
32
31
|
else
|
|
33
32
|
Models::Code.create!(attrs)
|
|
34
33
|
end
|
|
34
|
+
rescue ActiveRecord::RecordNotUnique
|
|
35
|
+
retry
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
def fetch_code(identifier:, record:)
|
|
@@ -146,48 +147,41 @@ module VerifyIt
|
|
|
146
147
|
end
|
|
147
148
|
|
|
148
149
|
def find_attempt(identifier:, record:, attempt_type:)
|
|
150
|
+
build_attempt_scope(identifier:, record:, attempt_type:).first
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def build_attempt_scope(identifier:, record:, attempt_type:)
|
|
149
154
|
scope = Models::Attempt
|
|
150
155
|
.for_identifier(identifier)
|
|
151
156
|
.where(attempt_type: attempt_type)
|
|
152
157
|
|
|
153
158
|
scope = scope.for_record(record) if record
|
|
154
|
-
scope
|
|
159
|
+
scope
|
|
155
160
|
end
|
|
156
161
|
|
|
157
162
|
def increment_attempt_count(identifier:, record:, attempt_type:)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
record: record,
|
|
161
|
-
attempt_type: attempt_type
|
|
162
|
-
)
|
|
163
|
+
# Clean expired attempts first
|
|
164
|
+
build_attempt_scope(identifier:, record:, attempt_type:).expired.delete_all
|
|
163
165
|
|
|
164
166
|
window = VerifyIt.configuration.rate_limit_window
|
|
165
167
|
expires_at = Time.now + window
|
|
168
|
+
attrs = {
|
|
169
|
+
identifier: identifier.to_s,
|
|
170
|
+
attempt_type: attempt_type,
|
|
171
|
+
count: 0,
|
|
172
|
+
expires_at: expires_at
|
|
173
|
+
}
|
|
166
174
|
|
|
167
|
-
if
|
|
168
|
-
|
|
169
|
-
attrs =
|
|
170
|
-
identifier: identifier.to_s,
|
|
171
|
-
attempt_type: attempt_type,
|
|
172
|
-
count: 1,
|
|
173
|
-
expires_at: expires_at
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if record
|
|
177
|
-
attrs[:record_type] = record.class.name
|
|
178
|
-
attrs[:record_id] = record.id
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
# Delete old expired attempt if exists
|
|
182
|
-
attempt&.destroy
|
|
183
|
-
|
|
184
|
-
Models::Attempt.create!(attrs)
|
|
185
|
-
1
|
|
186
|
-
else
|
|
187
|
-
# Increment existing
|
|
188
|
-
attempt.increment!(:count)
|
|
189
|
-
attempt.count
|
|
175
|
+
if record
|
|
176
|
+
attrs[:record_type] = record.class.name
|
|
177
|
+
attrs[:record_id] = record.id
|
|
190
178
|
end
|
|
179
|
+
|
|
180
|
+
attempt = build_attempt_scope(identifier:, record:, attempt_type:).first_or_create!(attrs)
|
|
181
|
+
attempt.increment!(:count)
|
|
182
|
+
attempt.count
|
|
183
|
+
rescue ActiveRecord::RecordNotUnique
|
|
184
|
+
retry
|
|
191
185
|
end
|
|
192
186
|
|
|
193
187
|
def get_attempt_count(identifier:, record:, attempt_type:)
|
|
@@ -7,7 +7,7 @@ module VerifyIt
|
|
|
7
7
|
self.table_name = "verify_it_attempts"
|
|
8
8
|
|
|
9
9
|
validates :identifier, presence: true
|
|
10
|
-
validates :attempt_type, presence: true, inclusion: { in: %w[verification send] }
|
|
10
|
+
validates :attempt_type, presence: true, inclusion: { in: %w[verification send ip_send ip_verification] }
|
|
11
11
|
validates :count, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
|
12
12
|
validates :expires_at, presence: true
|
|
13
13
|
|
data/lib/verify_it/verifiable.rb
CHANGED
|
@@ -20,13 +20,14 @@ module VerifyIt
|
|
|
20
20
|
|
|
21
21
|
raise ArgumentError, "Method #{send_method} is already defined on #{name}" if method_defined?(send_method)
|
|
22
22
|
|
|
23
|
-
define_method(send_method) do |context: {}|
|
|
23
|
+
define_method(send_method) do |context: {}, request: nil|
|
|
24
24
|
identifier = send(attribute)
|
|
25
25
|
VerifyIt.send_code(
|
|
26
26
|
to: identifier,
|
|
27
27
|
record: self,
|
|
28
28
|
channel: channel,
|
|
29
|
-
context: context
|
|
29
|
+
context: context,
|
|
30
|
+
request: request
|
|
30
31
|
)
|
|
31
32
|
end
|
|
32
33
|
|
data/lib/verify_it/verifier.rb
CHANGED
|
@@ -11,9 +11,14 @@ module VerifyIt
|
|
|
11
11
|
@rate_limiter = RateLimiter.new(storage)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def send_code(to:, record:, channel: nil, context: {})
|
|
14
|
+
def send_code(to:, record:, channel: nil, context: {}, request: nil)
|
|
15
15
|
channel ||= config.delivery_channel
|
|
16
16
|
|
|
17
|
+
ip = extract_ip(request)
|
|
18
|
+
if ip && rate_limiter.ip_send_rate_limited?(ip: ip)
|
|
19
|
+
return rate_limited_result("IP address rate limited for sending")
|
|
20
|
+
end
|
|
21
|
+
|
|
17
22
|
rate_limit_result = check_send_rate_limits(to: to, record: record)
|
|
18
23
|
return rate_limit_result if rate_limit_result
|
|
19
24
|
|
|
@@ -27,10 +32,17 @@ module VerifyIt
|
|
|
27
32
|
)
|
|
28
33
|
return delivery_result if delivery_result
|
|
29
34
|
|
|
35
|
+
rate_limiter.record_ip_send_attempt(ip: ip) if ip
|
|
36
|
+
|
|
30
37
|
finalize_send(to: to, record: record, channel: channel, code_data: code_data)
|
|
31
38
|
end
|
|
32
39
|
|
|
33
40
|
def verify_code(to:, code:, record:, request: nil)
|
|
41
|
+
ip = extract_ip(request)
|
|
42
|
+
if ip && rate_limiter.ip_verification_rate_limited?(ip: ip)
|
|
43
|
+
return rate_limited_result("IP address rate limited for verification")
|
|
44
|
+
end
|
|
45
|
+
|
|
34
46
|
rate_limit_result = check_verification_rate_limit(to: to, record: record)
|
|
35
47
|
return rate_limit_result if rate_limit_result
|
|
36
48
|
|
|
@@ -38,6 +50,7 @@ module VerifyIt
|
|
|
38
50
|
return code_not_found_result unless stored_code
|
|
39
51
|
|
|
40
52
|
current_attempts = rate_limiter.record_verification_attempt(identifier: to, record: record)
|
|
53
|
+
rate_limiter.record_ip_verification_attempt(ip: ip) if ip
|
|
41
54
|
|
|
42
55
|
if code_matches?(code, stored_code)
|
|
43
56
|
handle_verification_success(to: to, record: record, attempts: current_attempts, request: request)
|
|
@@ -159,6 +172,16 @@ module VerifyIt
|
|
|
159
172
|
end
|
|
160
173
|
end
|
|
161
174
|
|
|
175
|
+
def extract_ip(request)
|
|
176
|
+
return nil unless request
|
|
177
|
+
|
|
178
|
+
if request.respond_to?(:remote_ip)
|
|
179
|
+
request.remote_ip
|
|
180
|
+
elsif request.respond_to?(:ip)
|
|
181
|
+
request.ip
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
162
185
|
def invoke_callback(callback, **args)
|
|
163
186
|
callback&.call(**args)
|
|
164
187
|
end
|
data/lib/verify_it/version.rb
CHANGED
data/lib/verify_it.rb
CHANGED
|
@@ -33,8 +33,8 @@ module VerifyIt
|
|
|
33
33
|
yield(configuration)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
def send_code(to:, record:, channel: nil, context: {})
|
|
37
|
-
verifier.send_code(to: to, record: record, channel: channel, context: context)
|
|
36
|
+
def send_code(to:, record:, channel: nil, context: {}, request: nil)
|
|
37
|
+
verifier.send_code(to: to, record: record, channel: channel, context: context, request: request)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def verify_code(to:, code:, record:, request: nil)
|