verify_it 0.2.0 → 0.3.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 +10 -0
- data/README.md +168 -276
- data/lib/verify_it/code_hasher.rb +25 -0
- data/lib/verify_it/configuration.rb +3 -1
- data/lib/verify_it/templates/initializer.rb.erb +5 -1
- data/lib/verify_it/verifier.rb +4 -3
- data/lib/verify_it/version.rb +1 -1
- data/lib/verify_it.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9c975ad8c3016c930d9b8071d4f6c21fef89eece05332a48c5b1ebfebebf7386
|
|
4
|
+
data.tar.gz: 40adb101cba36f8cb6b1c89f81977558e4d21ae0de87a93333a70e3bf92006d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e4b862df0a1be371deb7fa9ea1d4eb2ed85da2f83852771a3efd52156187cf0c7a5057c162cd5e6e04ab5f6c88571c3fa6bc9c4d0427f901a7f4dbf5f23b6fe9
|
|
7
|
+
data.tar.gz: af94994c116729b2f0670b1664618ac1c01877755996ae00bd2b97069292fba1c0b2ef48d73f91a48d2c5653c2b49d5fc500a40c3a37e1cf304c77d7cf9d852c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-03-03
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
- Verification codes are now hashed with HMAC-SHA256 before storage; a compromised storage backend no longer exposes usable codes
|
|
7
|
+
- New required configuration: `secret_key_base` — must be set to a random secret in your initializer
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `VerifyIt::CodeHasher` module for HMAC-SHA256 code digesting (stdlib `openssl` only — no new dependencies)
|
|
11
|
+
- `config.secret_key_base` configuration option
|
|
12
|
+
|
|
3
13
|
## [0.1.1] - 2026-03-02
|
|
4
14
|
|
|
5
15
|
### Fixed
|
data/README.md
CHANGED
|
@@ -15,393 +15,285 @@ VerifyIt provides a flexible framework for implementing SMS and email verificati
|
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
18
|
-
Add
|
|
18
|
+
Add to your Gemfile:
|
|
19
19
|
|
|
20
20
|
```ruby
|
|
21
21
|
gem 'verify_it'
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
And then execute:
|
|
25
|
-
|
|
26
24
|
```bash
|
|
27
25
|
bundle install
|
|
28
26
|
```
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
gem install verify_it
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
### Rails Installation
|
|
28
|
+
## Rails Setup
|
|
37
29
|
|
|
38
|
-
|
|
30
|
+
### 1. Generate configuration files
|
|
39
31
|
|
|
40
32
|
```bash
|
|
41
33
|
bundle exec verify_it install
|
|
42
34
|
```
|
|
43
35
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
The installer will ask you to choose a storage backend and generate:
|
|
37
|
+
|
|
38
|
+
- `config/initializers/verify_it.rb` — your main configuration
|
|
39
|
+
- A database migration (if you chose Database storage)
|
|
48
40
|
|
|
49
|
-
If you
|
|
41
|
+
If you chose Database storage, run:
|
|
50
42
|
|
|
51
43
|
```bash
|
|
52
44
|
rails db:migrate
|
|
53
45
|
```
|
|
54
46
|
|
|
55
|
-
|
|
47
|
+
### 2. Configure the initializer
|
|
56
48
|
|
|
57
|
-
|
|
49
|
+
Open `config/initializers/verify_it.rb` and fill in your delivery lambdas.
|
|
58
50
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
### Model Integration
|
|
51
|
+
**Redis + Twilio example:**
|
|
62
52
|
|
|
63
53
|
```ruby
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
verifies :phone_number, channel: :sms
|
|
68
|
-
verifies :email, channel: :email
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Usage
|
|
72
|
-
user = User.find(params[:id])
|
|
73
|
-
|
|
74
|
-
# Send code (no context needed)
|
|
75
|
-
result = user.send_sms_code
|
|
76
|
-
|
|
77
|
-
# Send with custom message template
|
|
78
|
-
result = user.send_sms_code(context: {
|
|
79
|
-
message_template: "Your #{user.company_name} verification code is: %{code}"
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
# Send with tracking metadata
|
|
83
|
-
result = user.send_email_code(context: {
|
|
84
|
-
ip: request.ip,
|
|
85
|
-
user_agent: request.user_agent,
|
|
86
|
-
action: 'password_reset'
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
# Verify code
|
|
90
|
-
result = user.verify_sms_code(params[:code])
|
|
91
|
-
|
|
92
|
-
# Cleanup
|
|
93
|
-
user.cleanup_sms_verification
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### Rails Configuration
|
|
54
|
+
VerifyIt.configure do |config|
|
|
55
|
+
config.secret_key_base = Rails.application.secret_key_base
|
|
97
56
|
|
|
98
|
-
|
|
57
|
+
config.storage = :redis
|
|
58
|
+
config.redis_client = Redis.new(url: ENV["REDIS_URL"])
|
|
99
59
|
|
|
100
|
-
```ruby
|
|
101
|
-
VerifyIt.configure do |config|
|
|
102
|
-
config.storage = :redis
|
|
103
|
-
config.redis_client = Redis.new(url: ENV['REDIS_URL'])
|
|
104
|
-
|
|
105
60
|
config.sms_sender = ->(to:, code:, context:) {
|
|
106
|
-
|
|
61
|
+
Twilio::REST::Client.new.messages.create(
|
|
62
|
+
from: ENV["TWILIO_NUMBER"],
|
|
63
|
+
to: to,
|
|
64
|
+
body: "Your verification code is: #{code}"
|
|
65
|
+
)
|
|
107
66
|
}
|
|
108
|
-
|
|
109
|
-
config.test_mode
|
|
67
|
+
|
|
68
|
+
config.test_mode = Rails.env.test?
|
|
110
69
|
config.bypass_delivery = Rails.env.test?
|
|
111
70
|
end
|
|
112
71
|
```
|
|
113
72
|
|
|
114
|
-
|
|
73
|
+
### 3. Add `Verifiable` to your model
|
|
115
74
|
|
|
116
75
|
```ruby
|
|
117
|
-
|
|
76
|
+
class User < ApplicationRecord
|
|
77
|
+
include VerifyIt::Verifiable
|
|
118
78
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
config.storage = :memory # or :redis, :database
|
|
122
|
-
|
|
123
|
-
# For Redis storage
|
|
124
|
-
# config.redis_client = Redis.new(url: ENV['REDIS_URL'])
|
|
125
|
-
|
|
126
|
-
# Code settings
|
|
127
|
-
config.code_length = 6
|
|
128
|
-
config.code_ttl = 300 # 5 minutes
|
|
129
|
-
config.code_format = :numeric # or :alphanumeric, :alpha
|
|
130
|
-
|
|
131
|
-
# Rate limiting
|
|
132
|
-
config.max_send_attempts = 3
|
|
133
|
-
config.max_verification_attempts = 5
|
|
134
|
-
config.max_identifier_changes = 5
|
|
135
|
-
config.rate_limit_window = 3600 # 1 hour
|
|
136
|
-
|
|
137
|
-
# Delivery (SMS example with Twilio)
|
|
138
|
-
config.sms_sender = ->(to:, code:, context:) {
|
|
139
|
-
# Use custom template from context, or default
|
|
140
|
-
message = if context[:message_template]
|
|
141
|
-
context[:message_template] % { code: code }
|
|
142
|
-
else
|
|
143
|
-
"Your verification code is: #{code}"
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
twilio_client.messages.create(
|
|
147
|
-
to: to,
|
|
148
|
-
from: '+15551234567',
|
|
149
|
-
body: message
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
# Email delivery
|
|
154
|
-
config.email_sender = ->(to:, code:, context:) {
|
|
155
|
-
# Context available for customization
|
|
156
|
-
UserMailer.verification_code(to, code, context).deliver_now
|
|
157
|
-
}
|
|
79
|
+
verifies :phone_number # defaults to channel: :sms
|
|
80
|
+
verifies :email, channel: :email
|
|
158
81
|
end
|
|
159
82
|
```
|
|
160
83
|
|
|
161
|
-
|
|
84
|
+
This generates the following methods on `User`:
|
|
162
85
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
puts "Expires at: #{result.expires_at}"
|
|
174
|
-
else
|
|
175
|
-
puts "Error: #{result.message}"
|
|
176
|
-
puts "Rate limited!" if result.rate_limited?
|
|
177
|
-
end
|
|
178
|
-
```
|
|
86
|
+
| Method | Description |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `send_sms_code` | Send a code to `phone_number` via SMS |
|
|
89
|
+
| `send_email_code` | Send a code to `email` via email |
|
|
90
|
+
| `verify_sms_code(code)` | Verify an SMS code |
|
|
91
|
+
| `verify_email_code(code)` | Verify an email code |
|
|
92
|
+
| `cleanup_sms_verification` | Remove stored verification data |
|
|
93
|
+
| `cleanup_email_verification` | Remove stored verification data |
|
|
94
|
+
|
|
95
|
+
### 4. Use it in your controllers
|
|
179
96
|
|
|
180
|
-
|
|
97
|
+
**Sending a code:**
|
|
181
98
|
|
|
182
99
|
```ruby
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
flash[:error] = "Account locked due to too many attempts"
|
|
196
|
-
elsif result.success? == false
|
|
197
|
-
# Invalid code
|
|
198
|
-
flash[:error] = "Invalid verification code. #{5 - result.attempts} attempts remaining"
|
|
100
|
+
class VerificationsController < ApplicationController
|
|
101
|
+
def create
|
|
102
|
+
result = current_user.send_sms_code
|
|
103
|
+
|
|
104
|
+
if result.success?
|
|
105
|
+
render json: { expires_at: result.expires_at }
|
|
106
|
+
elsif result.rate_limited?
|
|
107
|
+
render json: { error: "Too many requests" }, status: :too_many_requests
|
|
108
|
+
else
|
|
109
|
+
render json: { error: result.message }, status: :unprocessable_entity
|
|
110
|
+
end
|
|
111
|
+
end
|
|
199
112
|
end
|
|
200
113
|
```
|
|
201
114
|
|
|
202
|
-
|
|
115
|
+
**Verifying a code:**
|
|
203
116
|
|
|
204
117
|
```ruby
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
118
|
+
class VerificationsController < ApplicationController
|
|
119
|
+
def update
|
|
120
|
+
result = current_user.verify_sms_code(params[:code])
|
|
121
|
+
|
|
122
|
+
if result.verified?
|
|
123
|
+
session[:phone_verified] = true
|
|
124
|
+
render json: { verified: true }
|
|
125
|
+
elsif result.locked?
|
|
126
|
+
render json: { error: "Too many failed attempts. Try again later." }, status: :locked
|
|
127
|
+
else
|
|
128
|
+
remaining = VerifyIt.configuration.max_verification_attempts - result.attempts
|
|
129
|
+
render json: { error: "Invalid code. #{remaining} attempts remaining." }, status: :unprocessable_entity
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
210
133
|
```
|
|
211
134
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
### Storage Backends
|
|
135
|
+
### Passing context
|
|
215
136
|
|
|
216
|
-
|
|
217
|
-
Perfect for testing and development:
|
|
137
|
+
Both `send_*` methods accept an optional `context:` hash that is forwarded to your sender lambda. Use it for custom message templates or request metadata:
|
|
218
138
|
|
|
219
139
|
```ruby
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
#### Redis Storage
|
|
224
|
-
Recommended for production:
|
|
140
|
+
current_user.send_sms_code(context: {
|
|
141
|
+
message_template: "Your Acme login code: %{code}"
|
|
142
|
+
})
|
|
225
143
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
144
|
+
current_user.send_email_code(context: {
|
|
145
|
+
ip: request.ip,
|
|
146
|
+
user_agent: request.user_agent
|
|
147
|
+
})
|
|
229
148
|
```
|
|
230
149
|
|
|
231
|
-
|
|
232
|
-
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Plain Ruby
|
|
233
153
|
|
|
234
154
|
```ruby
|
|
235
|
-
|
|
236
|
-
```
|
|
155
|
+
require "verify_it"
|
|
237
156
|
|
|
238
|
-
|
|
157
|
+
VerifyIt.configure do |config|
|
|
158
|
+
config.secret_key_base = ENV.fetch("VERIFY_IT_SECRET")
|
|
159
|
+
config.storage = :memory # :memory, :redis, or :database
|
|
239
160
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
161
|
+
config.sms_sender = ->(to:, code:, context:) {
|
|
162
|
+
MySmsSender.deliver(to: to, body: "Your code: #{code}")
|
|
163
|
+
}
|
|
164
|
+
end
|
|
244
165
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
- `verify_it_attempts` - Tracks verification and send attempts
|
|
248
|
-
- `verify_it_identifier_changes` - Tracks identifier change history
|
|
166
|
+
# Send a code
|
|
167
|
+
result = VerifyIt.send_code(to: "+15551234567", channel: :sms, record: current_user)
|
|
249
168
|
|
|
250
|
-
|
|
169
|
+
# Verify a code
|
|
170
|
+
result = VerifyIt.verify_code(to: "+15551234567", code: "123456", record: current_user)
|
|
251
171
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
config.code_ttl = 300 # Time-to-live in seconds (5 minutes)
|
|
255
|
-
config.code_format = :numeric # :numeric, :alphanumeric, or :alpha
|
|
172
|
+
# Clean up
|
|
173
|
+
VerifyIt.cleanup(to: "+15551234567", record: current_user)
|
|
256
174
|
```
|
|
257
175
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Configuration Reference
|
|
179
|
+
|
|
180
|
+
| Option | Default | Accepted values |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| `secret_key_base` | `nil` | Any string — **required** |
|
|
183
|
+
| `storage` | `:memory` | `:memory`, `:redis`, `:database` |
|
|
184
|
+
| `redis_client` | `nil` | A `Redis` instance (required when `storage: :redis`) |
|
|
185
|
+
| `code_length` | `6` | Integer |
|
|
186
|
+
| `code_ttl` | `300` | Seconds (integer) |
|
|
187
|
+
| `code_format` | `:numeric` | `:numeric`, `:alphanumeric`, `:alpha` |
|
|
188
|
+
| `max_send_attempts` | `3` | Integer |
|
|
189
|
+
| `max_verification_attempts` | `5` | Integer |
|
|
190
|
+
| `max_identifier_changes` | `5` | Integer |
|
|
191
|
+
| `rate_limit_window` | `3600` | Seconds (integer) |
|
|
192
|
+
| `sms_sender` | `nil` | Lambda `(to:, code:, context:) { }` |
|
|
193
|
+
| `email_sender` | `nil` | Lambda `(to:, code:, context:) { }` |
|
|
194
|
+
| `on_send` | `nil` | Lambda `(record:, identifier:, channel:) { }` |
|
|
195
|
+
| `on_verify_success` | `nil` | Lambda `(record:, identifier:) { }` |
|
|
196
|
+
| `on_verify_failure` | `nil` | Lambda `(record:, identifier:, attempts:) { }` |
|
|
197
|
+
| `namespace` | `nil` | Lambda `(record) { }` returning a string or nil |
|
|
198
|
+
| `test_mode` | `false` | Boolean — exposes `result.code` |
|
|
199
|
+
| `bypass_delivery` | `false` | Boolean — skips actual delivery |
|
|
266
200
|
|
|
267
201
|
### Callbacks
|
|
268
202
|
|
|
269
|
-
Hook into the verification lifecycle:
|
|
270
|
-
|
|
271
203
|
```ruby
|
|
272
204
|
config.on_send = ->(record:, identifier:, channel:) {
|
|
273
|
-
Analytics.track(
|
|
205
|
+
Analytics.track("verification_sent", user_id: record.id, channel: channel)
|
|
274
206
|
}
|
|
275
207
|
|
|
276
208
|
config.on_verify_success = ->(record:, identifier:) {
|
|
277
|
-
Analytics.track(
|
|
209
|
+
Analytics.track("verification_success", user_id: record.id)
|
|
278
210
|
}
|
|
279
211
|
|
|
280
212
|
config.on_verify_failure = ->(record:, identifier:, attempts:) {
|
|
281
|
-
Analytics.track(
|
|
213
|
+
Analytics.track("verification_failure", user_id: record.id, attempts: attempts)
|
|
282
214
|
}
|
|
283
215
|
```
|
|
284
216
|
|
|
285
217
|
### Namespacing
|
|
286
218
|
|
|
287
|
-
Isolate verification data
|
|
219
|
+
Isolate verification data per tenant:
|
|
288
220
|
|
|
289
221
|
```ruby
|
|
290
|
-
config.namespace = ->(record) {
|
|
291
|
-
record.respond_to?(:organization_id) ? "org:#{record.organization_id}" : nil
|
|
292
|
-
}
|
|
222
|
+
config.namespace = ->(record) { "org:#{record.organization_id}" }
|
|
293
223
|
```
|
|
294
224
|
|
|
295
|
-
|
|
225
|
+
---
|
|
296
226
|
|
|
297
|
-
|
|
227
|
+
## Result Object
|
|
298
228
|
|
|
299
|
-
|
|
300
|
-
config.test_mode = true # Includes code in Result object
|
|
301
|
-
config.bypass_delivery = true # Skip actual delivery
|
|
302
|
-
```
|
|
229
|
+
Every operation returns a `VerifyIt::Result`:
|
|
303
230
|
|
|
304
|
-
|
|
231
|
+
| Method | Type | Description |
|
|
232
|
+
|---|---|---|
|
|
233
|
+
| `success?` | Boolean | Operation completed without error |
|
|
234
|
+
| `verified?` | Boolean | Code matched successfully |
|
|
235
|
+
| `rate_limited?` | Boolean | Rate limit was exceeded |
|
|
236
|
+
| `locked?` | Boolean | Max verification attempts reached |
|
|
237
|
+
| `error` | Symbol | `:invalid_code`, `:rate_limited`, `:code_not_found`, `:locked`, `:delivery_failed` |
|
|
238
|
+
| `message` | String | Human-readable description |
|
|
239
|
+
| `code` | String | The verification code (only when `test_mode: true`) |
|
|
240
|
+
| `expires_at` | Time | When the code expires |
|
|
241
|
+
| `attempts` | Integer | Number of verification attempts so far |
|
|
305
242
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
```ruby
|
|
309
|
-
result.success? # true if operation succeeded
|
|
310
|
-
result.verified? # true if code was verified successfully
|
|
311
|
-
result.locked? # true if max attempts exceeded
|
|
312
|
-
result.rate_limited? # true if rate limit hit
|
|
313
|
-
result.error # Symbol error code (:invalid_code, :rate_limited, etc.)
|
|
314
|
-
result.message # Human-readable message
|
|
315
|
-
result.code # Verification code (only in test_mode)
|
|
316
|
-
result.expires_at # Time when code expires
|
|
317
|
-
result.attempts # Number of verification attempts
|
|
318
|
-
```
|
|
243
|
+
---
|
|
319
244
|
|
|
320
245
|
## Testing
|
|
321
246
|
|
|
322
|
-
|
|
247
|
+
Configure VerifyIt in your spec helper to avoid real delivery and expose codes:
|
|
323
248
|
|
|
324
249
|
```ruby
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
250
|
+
# spec/support/verify_it.rb
|
|
251
|
+
RSpec.configure do |config|
|
|
252
|
+
config.before(:each) do
|
|
253
|
+
VerifyIt.configure do |c|
|
|
254
|
+
c.storage = :memory
|
|
255
|
+
c.test_mode = true
|
|
256
|
+
c.bypass_delivery = true
|
|
257
|
+
c.secret_key_base = "test_secret"
|
|
331
258
|
end
|
|
332
259
|
end
|
|
333
|
-
|
|
334
|
-
it "verifies phone number" do
|
|
335
|
-
post '/verify/send', params: { phone: '+15551234567' }
|
|
336
|
-
|
|
337
|
-
# Get code from response (test mode)
|
|
338
|
-
code = JSON.parse(response.body)['code']
|
|
339
|
-
|
|
340
|
-
post '/verify/confirm', params: { phone: '+15551234567', code: code }
|
|
341
|
-
expect(response).to have_http_status(:success)
|
|
342
|
-
end
|
|
343
260
|
end
|
|
344
261
|
```
|
|
345
262
|
|
|
346
|
-
|
|
263
|
+
In a request spec:
|
|
347
264
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
- No shared mutable state in core logic
|
|
353
|
-
|
|
354
|
-
## Performance Considerations
|
|
355
|
-
|
|
356
|
-
### Redis Storage
|
|
357
|
-
- Uses key expiration for TTL
|
|
358
|
-
- Sorted sets for identifier tracking
|
|
359
|
-
- Atomic operations for counters
|
|
360
|
-
|
|
361
|
-
### Memory Storage
|
|
362
|
-
- Fast for testing and development
|
|
363
|
-
- Not recommended for production
|
|
364
|
-
- Data lost on restart
|
|
265
|
+
```ruby
|
|
266
|
+
it "verifies phone number" do
|
|
267
|
+
post "/verify/send", params: { phone: "+15551234567" }
|
|
268
|
+
code = JSON.parse(response.body)["code"] # available in test_mode
|
|
365
269
|
|
|
366
|
-
|
|
270
|
+
post "/verify/confirm", params: { phone: "+15551234567", code: code }
|
|
271
|
+
expect(response).to have_http_status(:ok)
|
|
272
|
+
end
|
|
273
|
+
```
|
|
367
274
|
|
|
368
|
-
|
|
275
|
+
---
|
|
369
276
|
|
|
370
|
-
|
|
371
|
-
- `:invalid_code` - Code doesn't match
|
|
372
|
-
- `:code_not_found` - No code stored or expired
|
|
373
|
-
- `:locked` - Max attempts reached
|
|
374
|
-
- `:delivery_failed` - Delivery provider error
|
|
277
|
+
## Security
|
|
375
278
|
|
|
376
|
-
|
|
279
|
+
- **`secret_key_base` is required** — codes are hashed with HMAC-SHA256 before storage
|
|
280
|
+
- Enable rate limiting (on by default) in all environments
|
|
281
|
+
- Use `:redis` or `:database` storage in production (`:memory` does not survive restarts)
|
|
282
|
+
- Keep `code_ttl` short — 5 minutes is a sensible default
|
|
283
|
+
- Use `on_verify_failure` to monitor and alert on repeated failures
|
|
284
|
+
- Always serve verification endpoints over HTTPS
|
|
377
285
|
|
|
378
|
-
|
|
379
|
-
2. **Use Redis storage** for distributed systems
|
|
380
|
-
3. **Set short TTLs** (5 minutes recommended)
|
|
381
|
-
4. **Monitor failed attempts** via callbacks
|
|
382
|
-
5. **Use HTTPS** for all verification endpoints
|
|
383
|
-
6. **Sanitize phone numbers** before storage
|
|
286
|
+
---
|
|
384
287
|
|
|
385
288
|
## Development
|
|
386
289
|
|
|
387
|
-
After checking out the repo:
|
|
388
|
-
|
|
389
290
|
```bash
|
|
390
|
-
bin/setup
|
|
391
|
-
bundle exec rspec
|
|
392
|
-
|
|
393
|
-
|
|
291
|
+
bin/setup # Install dependencies
|
|
292
|
+
bundle exec rspec # Run tests
|
|
293
|
+
bundle exec rubocop # Lint
|
|
294
|
+
bin/console # Interactive console
|
|
394
295
|
```
|
|
395
296
|
|
|
396
|
-
## Roadmap
|
|
397
|
-
|
|
398
|
-
- [x] Database storage adapter
|
|
399
|
-
- [ ] Voice verification support
|
|
400
|
-
- [ ] WebAuthn integration
|
|
401
|
-
- [ ] Backup code generation
|
|
402
|
-
- [ ] Multi-factor authentication helpers
|
|
403
|
-
- [x] Rails installer (CLI)
|
|
404
|
-
|
|
405
297
|
## Contributing
|
|
406
298
|
|
|
407
299
|
Bug reports and pull requests are welcome on GitHub at https://github.com/JeremasPosta/verify_it. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/JeremasPosta/verify_it/blob/main/CODE_OF_CONDUCT.md).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module VerifyIt
|
|
6
|
+
# Hashes verification codes with HMAC-SHA256 before storage.
|
|
7
|
+
#
|
|
8
|
+
# Using a secret key makes offline brute-force of the small OTP keyspace
|
|
9
|
+
# infeasible even if the storage layer is compromised.
|
|
10
|
+
module CodeHasher
|
|
11
|
+
# @param code [String] the plaintext verification code
|
|
12
|
+
# @param secret [String] the HMAC secret (config.secret_key_base)
|
|
13
|
+
# @return [String] hex-encoded HMAC-SHA256 digest
|
|
14
|
+
# @raise [ArgumentError] if secret is nil or empty
|
|
15
|
+
def self.digest(code, secret:)
|
|
16
|
+
if secret.nil? || secret.to_s.empty?
|
|
17
|
+
raise ArgumentError,
|
|
18
|
+
"VerifyIt requires secret_key_base to be configured. " \
|
|
19
|
+
"Set config.secret_key_base in your initializer."
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
OpenSSL::HMAC.hexdigest("SHA256", secret.to_s, code.to_s)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -19,7 +19,8 @@ module VerifyIt
|
|
|
19
19
|
:on_verify_failure,
|
|
20
20
|
:namespace,
|
|
21
21
|
:test_mode,
|
|
22
|
-
:bypass_delivery
|
|
22
|
+
:bypass_delivery,
|
|
23
|
+
:secret_key_base
|
|
23
24
|
|
|
24
25
|
def initialize
|
|
25
26
|
# Default values
|
|
@@ -41,6 +42,7 @@ module VerifyIt
|
|
|
41
42
|
@namespace = nil
|
|
42
43
|
@test_mode = false
|
|
43
44
|
@bypass_delivery = false
|
|
45
|
+
@secret_key_base = nil
|
|
44
46
|
end
|
|
45
47
|
end
|
|
46
48
|
end
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
VerifyIt.configure do |config|
|
|
4
|
+
# Required: set a secret key to hash verification codes before storage.
|
|
5
|
+
# It is not required to use the Rails secret key base.
|
|
6
|
+
config.secret_key_base = Rails.application.secret_key_base
|
|
7
|
+
|
|
4
8
|
<% if storage_type == "redis" %>
|
|
5
9
|
config.storage = :redis
|
|
6
10
|
config.redis_client = Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0"))
|
|
@@ -19,7 +23,7 @@ VerifyIt.configure do |config|
|
|
|
19
23
|
# e.g. Twilio, Vonage, etc.
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
#
|
|
26
|
+
# Some other config options (with defaults):
|
|
23
27
|
# config.code_length = 6
|
|
24
28
|
# config.code_ttl = 300
|
|
25
29
|
# config.code_format = :numeric
|
data/lib/verify_it/verifier.rb
CHANGED
|
@@ -41,11 +41,12 @@ module VerifyIt
|
|
|
41
41
|
)
|
|
42
42
|
expires_at = Time.now + VerifyIt.configuration.code_ttl
|
|
43
43
|
|
|
44
|
-
# Store code
|
|
44
|
+
# Store hashed code
|
|
45
|
+
hashed_code = CodeHasher.digest(code, secret: VerifyIt.configuration.secret_key_base)
|
|
45
46
|
storage.store_code(
|
|
46
47
|
identifier: to,
|
|
47
48
|
record: record,
|
|
48
|
-
code:
|
|
49
|
+
code: hashed_code,
|
|
49
50
|
expires_at: expires_at
|
|
50
51
|
)
|
|
51
52
|
|
|
@@ -115,7 +116,7 @@ module VerifyIt
|
|
|
115
116
|
current_attempts = rate_limiter.record_verification_attempt(identifier: to, record: record)
|
|
116
117
|
|
|
117
118
|
# Verify code
|
|
118
|
-
if stored_code == code.
|
|
119
|
+
if stored_code == CodeHasher.digest(code, secret: VerifyIt.configuration.secret_key_base)
|
|
119
120
|
# Success - cleanup
|
|
120
121
|
storage.delete_code(identifier: to, record: record)
|
|
121
122
|
storage.reset_attempts(identifier: to, record: record)
|
data/lib/verify_it/version.rb
CHANGED
data/lib/verify_it.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative "verify_it/version"
|
|
|
4
4
|
require_relative "verify_it/configuration"
|
|
5
5
|
require_relative "verify_it/result"
|
|
6
6
|
require_relative "verify_it/code_generator"
|
|
7
|
+
require_relative "verify_it/code_hasher"
|
|
7
8
|
require_relative "verify_it/rate_limiter"
|
|
8
9
|
require_relative "verify_it/storage/base"
|
|
9
10
|
require_relative "verify_it/storage/memory_storage"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: verify_it
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jeremias Ramirez
|
|
@@ -157,6 +157,7 @@ files:
|
|
|
157
157
|
- lib/verify_it.rb
|
|
158
158
|
- lib/verify_it/cli.rb
|
|
159
159
|
- lib/verify_it/code_generator.rb
|
|
160
|
+
- lib/verify_it/code_hasher.rb
|
|
160
161
|
- lib/verify_it/configuration.rb
|
|
161
162
|
- lib/verify_it/delivery/base.rb
|
|
162
163
|
- lib/verify_it/delivery/email_delivery.rb
|