verify_it 0.1.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a41100a91ec8bb6febdc4f760c8b1e2217353e9f606fe95efeae2b8e4ddac027
4
- data.tar.gz: 2afcdf2a1588b1e74510a7f1a3c24f61f679a47f9e9ed5e488d98e43f5971f3c
3
+ metadata.gz: 9c975ad8c3016c930d9b8071d4f6c21fef89eece05332a48c5b1ebfebebf7386
4
+ data.tar.gz: 40adb101cba36f8cb6b1c89f81977558e4d21ae0de87a93333a70e3bf92006d1
5
5
  SHA512:
6
- metadata.gz: df9fbe710460044df86b666b7b04258d09ec05afba41eaffb6db80de641a3b6989a224dd67e3aa38a6a4c4a76b52fc98ff99e8fe08b10dd9a5756b5f28542e33
7
- data.tar.gz: 88497353ebc57ab8076b38c16d499f7f05052cbdc61f2e2423eb61eecbb555b454adfcebac341add0e3180d879b0856273b7e2a973ba128443d69d5e9f7b9217
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
@@ -1,6 +1,7 @@
1
1
  # VerifyIt
2
2
 
3
- A production-grade, storage-agnostic verification system for Ruby applications. VerifyIt provides a flexible framework for implementing SMS, email, and other verification workflows with built-in rate limiting and multiple storage backends.
3
+ A storage-agnostic verification system for Ruby applications.
4
+ VerifyIt provides a flexible framework for implementing SMS and email verifications with built-in rate limiting and multiple storage backends.
4
5
 
5
6
  ## Features
6
7
 
@@ -10,397 +11,289 @@ A production-grade, storage-agnostic verification system for Ruby applications.
10
11
  - **Thread-Safe**: Designed for concurrent access
11
12
  - **Test Mode**: Expose codes for testing without delivery
12
13
  - **Rails Optional**: Works standalone or with Rails
13
- - **Zero Runtime Dependencies**: Only ActiveSupport required
14
+ - **Little Runtime Dependencies**: Only ActiveSupport required
14
15
 
15
16
  ## Installation
16
17
 
17
- Add this line to your application's Gemfile:
18
+ Add to your Gemfile:
18
19
 
19
20
  ```ruby
20
21
  gem 'verify_it'
21
22
  ```
22
23
 
23
- And then execute:
24
-
25
24
  ```bash
26
25
  bundle install
27
26
  ```
28
27
 
29
- Or install it yourself as:
30
-
31
- ```bash
32
- gem install verify_it
33
- ```
34
-
35
- ### Rails Installation
28
+ ## Rails Setup
36
29
 
37
- For Rails applications, use the installer to generate configuration files:
30
+ ### 1. Generate configuration files
38
31
 
39
32
  ```bash
40
33
  bundle exec verify_it install
41
34
  ```
42
35
 
43
- This will:
44
- 1. Prompt you to select a storage backend (Memory, Redis, or Database)
45
- 2. Generate an initializer at `config/initializers/verify_it.rb`
46
- 3. Generate a migration (if Database storage is selected)
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)
47
40
 
48
- If you selected Database storage, run the migration:
41
+ If you chose Database storage, run:
49
42
 
50
43
  ```bash
51
44
  rails db:migrate
52
45
  ```
53
46
 
54
- ## Quick Start
47
+ ### 2. Configure the initializer
55
48
 
56
- ### Basic Configuration
49
+ Open `config/initializers/verify_it.rb` and fill in your delivery lambdas.
57
50
 
58
- ```ruby
59
- require 'verify_it'
51
+ **Redis + Twilio example:**
60
52
 
53
+ ```ruby
61
54
  VerifyIt.configure do |config|
62
- # Storage backend
63
- config.storage = :memory # or :redis, :database
64
-
65
- # For Redis storage
66
- # config.redis_client = Redis.new(url: ENV['REDIS_URL'])
67
-
68
- # Code settings
69
- config.code_length = 6
70
- config.code_ttl = 300 # 5 minutes
71
- config.code_format = :numeric # or :alphanumeric, :alpha
72
-
73
- # Rate limiting
74
- config.max_send_attempts = 3
75
- config.max_verification_attempts = 5
76
- config.max_identifier_changes = 5
77
- config.rate_limit_window = 3600 # 1 hour
78
-
79
- # Delivery (SMS example with Twilio)
55
+ config.secret_key_base = Rails.application.secret_key_base
56
+
57
+ config.storage = :redis
58
+ config.redis_client = Redis.new(url: ENV["REDIS_URL"])
59
+
80
60
  config.sms_sender = ->(to:, code:, context:) {
81
- # Use custom template from context, or default
82
- message = if context[:message_template]
83
- context[:message_template] % { code: code }
84
- else
85
- "Your verification code is: #{code}"
86
- end
87
-
88
- twilio_client.messages.create(
89
- to: to,
90
- from: '+15551234567',
91
- body: message
61
+ Twilio::REST::Client.new.messages.create(
62
+ from: ENV["TWILIO_NUMBER"],
63
+ to: to,
64
+ body: "Your verification code is: #{code}"
92
65
  )
93
66
  }
94
-
95
- # Email delivery
96
- config.email_sender = ->(to:, code:, context:) {
97
- # Context available for customization
98
- UserMailer.verification_code(to, code, context).deliver_now
99
- }
67
+
68
+ config.test_mode = Rails.env.test?
69
+ config.bypass_delivery = Rails.env.test?
100
70
  end
101
71
  ```
102
72
 
103
- ### Sending a Verification Code
73
+ ### 3. Add `Verifiable` to your model
104
74
 
105
75
  ```ruby
106
- result = VerifyIt.send_code(
107
- to: "+15551234567",
108
- record: current_user,
109
- channel: :sms,
110
- context: { user_id: current_user.id }
111
- )
112
-
113
- if result.success?
114
- puts "Code sent successfully!"
115
- puts "Expires at: #{result.expires_at}"
116
- else
117
- puts "Error: #{result.message}"
118
- puts "Rate limited!" if result.rate_limited?
76
+ class User < ApplicationRecord
77
+ include VerifyIt::Verifiable
78
+
79
+ verifies :phone_number # defaults to channel: :sms
80
+ verifies :email, channel: :email
119
81
  end
120
82
  ```
121
83
 
122
- ### Verifying a Code
84
+ This generates the following methods on `User`:
85
+
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
96
+
97
+ **Sending a code:**
123
98
 
124
99
  ```ruby
125
- result = VerifyIt.verify_code(
126
- to: "+15551234567",
127
- code: params[:code],
128
- record: current_user
129
- )
130
-
131
- if result.verified?
132
- # Success! Code is valid
133
- session[:verified] = true
134
- redirect_to dashboard_path
135
- elsif result.locked?
136
- # Too many failed attempts
137
- flash[:error] = "Account locked due to too many attempts"
138
- elsif result.success? == false
139
- # Invalid code
140
- 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
141
112
  end
142
113
  ```
143
114
 
144
- ### Cleanup
115
+ **Verifying a code:**
145
116
 
146
117
  ```ruby
147
- # Clean up verification data for a user
148
- VerifyIt.cleanup(
149
- to: "+15551234567",
150
- record: current_user
151
- )
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
152
133
  ```
153
134
 
154
- ## Configuration Options
155
-
156
- ### Storage Backends
135
+ ### Passing context
157
136
 
158
- #### Memory Storage (Default)
159
- 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:
160
138
 
161
139
  ```ruby
162
- config.storage = :memory
163
- ```
164
-
165
- #### Redis Storage
166
- Recommended for production:
140
+ current_user.send_sms_code(context: {
141
+ message_template: "Your Acme login code: %{code}"
142
+ })
167
143
 
168
- ```ruby
169
- config.storage = :redis
170
- config.redis_client = Redis.new(url: ENV['REDIS_URL'])
144
+ current_user.send_email_code(context: {
145
+ ip: request.ip,
146
+ user_agent: request.user_agent
147
+ })
171
148
  ```
172
149
 
173
- #### Database Storage
174
- Uses ActiveRecord models for persistent storage:
150
+ ---
151
+
152
+ ## Plain Ruby
175
153
 
176
154
  ```ruby
177
- config.storage = :database
178
- ```
155
+ require "verify_it"
179
156
 
180
- Requires running the migration:
157
+ VerifyIt.configure do |config|
158
+ config.secret_key_base = ENV.fetch("VERIFY_IT_SECRET")
159
+ config.storage = :memory # :memory, :redis, or :database
181
160
 
182
- ```bash
183
- rails generate verify_it:install
184
- rails db:migrate
185
- ```
161
+ config.sms_sender = ->(to:, code:, context:) {
162
+ MySmsSender.deliver(to: to, body: "Your code: #{code}")
163
+ }
164
+ end
186
165
 
187
- Creates three tables:
188
- - `verify_it_codes` - Stores verification codes
189
- - `verify_it_attempts` - Tracks verification and send attempts
190
- - `verify_it_identifier_changes` - Tracks identifier change history
166
+ # Send a code
167
+ result = VerifyIt.send_code(to: "+15551234567", channel: :sms, record: current_user)
191
168
 
192
- ### Code Settings
169
+ # Verify a code
170
+ result = VerifyIt.verify_code(to: "+15551234567", code: "123456", record: current_user)
193
171
 
194
- ```ruby
195
- config.code_length = 6 # Length of verification code
196
- config.code_ttl = 300 # Time-to-live in seconds (5 minutes)
197
- config.code_format = :numeric # :numeric, :alphanumeric, or :alpha
172
+ # Clean up
173
+ VerifyIt.cleanup(to: "+15551234567", record: current_user)
198
174
  ```
199
175
 
200
- ### Rate Limiting
201
-
202
- ```ruby
203
- config.max_send_attempts = 3 # Max sends per window
204
- config.max_verification_attempts = 5 # Max verification tries per window
205
- config.max_identifier_changes = 5 # Max identifier changes per window
206
- config.rate_limit_window = 3600 # Window duration in seconds
207
- ```
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 |
208
200
 
209
201
  ### Callbacks
210
202
 
211
- Hook into the verification lifecycle:
212
-
213
203
  ```ruby
214
204
  config.on_send = ->(record:, identifier:, channel:) {
215
- Analytics.track('verification_sent', user_id: record.id)
205
+ Analytics.track("verification_sent", user_id: record.id, channel: channel)
216
206
  }
217
207
 
218
208
  config.on_verify_success = ->(record:, identifier:) {
219
- Analytics.track('verification_success', user_id: record.id)
209
+ Analytics.track("verification_success", user_id: record.id)
220
210
  }
221
211
 
222
212
  config.on_verify_failure = ->(record:, identifier:, attempts:) {
223
- Analytics.track('verification_failure', user_id: record.id, attempts: attempts)
213
+ Analytics.track("verification_failure", user_id: record.id, attempts: attempts)
224
214
  }
225
215
  ```
226
216
 
227
217
  ### Namespacing
228
218
 
229
- Isolate verification data by tenant or organization:
230
-
231
- ```ruby
232
- config.namespace = ->(record) {
233
- record.respond_to?(:organization_id) ? "org:#{record.organization_id}" : nil
234
- }
235
- ```
236
-
237
- ### Test Mode
238
-
239
- Expose codes in test environment:
219
+ Isolate verification data per tenant:
240
220
 
241
221
  ```ruby
242
- config.test_mode = true # Includes code in Result object
243
- config.bypass_delivery = true # Skip actual delivery
222
+ config.namespace = ->(record) { "org:#{record.organization_id}" }
244
223
  ```
245
224
 
246
- ## Rails Integration
247
-
248
- VerifyIt automatically integrates with Rails when detected.
249
-
250
- ### Model Integration
251
-
252
- ```ruby
253
- class User < ApplicationRecord
254
- include VerifyIt::Verifiable
255
-
256
- verifies :phone_number, channel: :sms
257
- verifies :email, channel: :email
258
- end
259
-
260
- # Usage
261
- user = User.find(params[:id])
262
-
263
- # Send code (no context needed)
264
- result = user.send_sms_code
225
+ ---
265
226
 
266
- # Send with custom message template
267
- result = user.send_sms_code(context: {
268
- message_template: "Your #{user.company_name} verification code is: %{code}"
269
- })
270
-
271
- # Send with tracking metadata
272
- result = user.send_email_code(context: {
273
- ip: request.ip,
274
- user_agent: request.user_agent,
275
- action: 'password_reset'
276
- })
227
+ ## Result Object
277
228
 
278
- # Verify code
279
- result = user.verify_sms_code(params[:code])
229
+ Every operation returns a `VerifyIt::Result`:
280
230
 
281
- # Cleanup
282
- user.cleanup_sms_verification
283
- ```
284
-
285
- ### Rails Configuration
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 |
286
242
 
287
- Create an initializer `config/initializers/verify_it.rb`:
288
-
289
- ```ruby
290
- VerifyIt.configure do |config|
291
- config.storage = :redis
292
- config.redis_client = Redis.new(url: ENV['REDIS_URL'])
293
-
294
- config.sms_sender = ->(to:, code:, context:) {
295
- TwilioService.send_sms(to: to, body: "Your code is: #{code}")
296
- }
297
-
298
- config.test_mode = Rails.env.test?
299
- config.bypass_delivery = Rails.env.test?
300
- end
301
- ```
302
-
303
- ## Result Object API
304
-
305
- All operations return a `VerifyIt::Result` object:
306
-
307
- ```ruby
308
- result.success? # true if operation succeeded
309
- result.verified? # true if code was verified successfully
310
- result.locked? # true if max attempts exceeded
311
- result.rate_limited? # true if rate limit hit
312
- result.error # Symbol error code (:invalid_code, :rate_limited, etc.)
313
- result.message # Human-readable message
314
- result.code # Verification code (only in test_mode)
315
- result.expires_at # Time when code expires
316
- result.attempts # Number of verification attempts
317
- ```
243
+ ---
318
244
 
319
245
  ## Testing
320
246
 
321
- ### RSpec Example
247
+ Configure VerifyIt in your spec helper to avoid real delivery and expose codes:
322
248
 
323
249
  ```ruby
324
- RSpec.describe "Phone Verification", type: :request do
325
- before do
326
- VerifyIt.configure do |config|
327
- config.storage = :memory
328
- config.test_mode = true
329
- config.bypass_delivery = true
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"
330
258
  end
331
259
  end
332
-
333
- it "verifies phone number" do
334
- post '/verify/send', params: { phone: '+15551234567' }
335
-
336
- # Get code from response (test mode)
337
- code = JSON.parse(response.body)['code']
338
-
339
- post '/verify/confirm', params: { phone: '+15551234567', code: code }
340
- expect(response).to have_http_status(:success)
341
- end
342
260
  end
343
261
  ```
344
262
 
345
- ## Thread Safety
346
-
347
- VerifyIt is designed to be thread-safe:
348
-
349
- - Memory storage uses `Mutex` for synchronization
350
- - Redis operations are atomic
351
- - No shared mutable state in core logic
263
+ In a request spec:
352
264
 
353
- ## Performance Considerations
354
-
355
- ### Redis Storage
356
- - Uses key expiration for TTL
357
- - Sorted sets for identifier tracking
358
- - Atomic operations for counters
359
-
360
- ### Memory Storage
361
- - Fast for testing and development
362
- - Not recommended for production
363
- - 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
364
269
 
365
- ## Error Handling
270
+ post "/verify/confirm", params: { phone: "+15551234567", code: code }
271
+ expect(response).to have_http_status(:ok)
272
+ end
273
+ ```
366
274
 
367
- VerifyIt uses explicit error states in Result objects:
275
+ ---
368
276
 
369
- - `:rate_limited` - Rate limit exceeded
370
- - `:invalid_code` - Code doesn't match
371
- - `:code_not_found` - No code stored or expired
372
- - `:locked` - Max attempts reached
373
- - `:delivery_failed` - Delivery provider error
277
+ ## Security
374
278
 
375
- ## Security Best Practices
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
376
285
 
377
- 1. **Always use rate limiting** in production
378
- 2. **Use Redis storage** for distributed systems
379
- 3. **Set short TTLs** (5 minutes recommended)
380
- 4. **Monitor failed attempts** via callbacks
381
- 5. **Use HTTPS** for all verification endpoints
382
- 6. **Sanitize phone numbers** before storage
286
+ ---
383
287
 
384
288
  ## Development
385
289
 
386
- After checking out the repo:
387
-
388
290
  ```bash
389
- bin/setup # Install dependencies
390
- bundle exec rspec # Run tests
391
- bin/console # Interactive console
392
- bundle exec rake build # Build gem
291
+ bin/setup # Install dependencies
292
+ bundle exec rspec # Run tests
293
+ bundle exec rubocop # Lint
294
+ bin/console # Interactive console
393
295
  ```
394
296
 
395
- ## Roadmap
396
-
397
- - [x] Database storage adapter
398
- - [ ] Voice verification support
399
- - [ ] WebAuthn integration
400
- - [ ] Backup code generation
401
- - [ ] Multi-factor authentication helpers
402
- - [x] Rails installer (CLI)
403
-
404
297
  ## Contributing
405
298
 
406
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).
data/lib/verify_it/cli.rb CHANGED
@@ -37,6 +37,7 @@ module VerifyIt
37
37
  puts "=" * 50
38
38
  puts
39
39
 
40
+ load_rails_environment
40
41
  check_rails_environment
41
42
 
42
43
  storage_type = prompt_storage_type
@@ -57,6 +58,11 @@ module VerifyIt
57
58
  end
58
59
  end
59
60
 
61
+ def load_rails_environment
62
+ env_file = File.join(Dir.pwd, "config", "environment.rb")
63
+ require env_file if File.exist?(env_file)
64
+ end
65
+
60
66
  def check_rails_environment
61
67
  unless defined?(Rails)
62
68
  puts "Error: Rails environment not detected."
@@ -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,79 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  VerifyIt.configure do |config|
4
- <% if storage_type == "memory" %>
5
- # Memory storage (for development/testing only)
6
- config.storage = :memory
7
- <% elsif storage_type == "redis" %>
8
- # Redis storage (recommended for production)
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
+
8
+ <% if storage_type == "redis" %>
9
9
  config.storage = :redis
10
- config.redis_client = Redis.new(
11
- url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0")
12
- )
10
+ config.redis_client = Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0"))
13
11
  <% elsif storage_type == "database" %>
14
- # Database storage (uses ActiveRecord)
15
12
  config.storage = :database
13
+ <% else %>
14
+ # config.storage = :memory # default
16
15
  <% end %>
17
16
 
18
- # Code settings
19
- config.code_length = 6
20
- config.code_ttl = 300 # 5 minutes
21
- config.code_format = :numeric # :numeric, :alphanumeric, or :alpha
22
-
23
- # Rate limiting
24
- config.max_send_attempts = 3
25
- config.max_verification_attempts = 5
26
- config.max_identifier_changes = 5
27
- config.rate_limit_window = 3600 # 1 hour
28
-
29
- # Delivery channels
30
- config.delivery_channel = :sms # :sms or :email
31
-
32
- # SMS delivery (configure with your provider)
33
- config.sms_sender = ->(to:, code:, context:) {
34
- # Example with Twilio:
35
- # twilio_client = Twilio::REST::Client.new(
36
- # ENV['TWILIO_ACCOUNT_SID'],
37
- # ENV['TWILIO_AUTH_TOKEN']
38
- # )
39
- # twilio_client.messages.create(
40
- # to: to,
41
- # from: ENV['TWILIO_PHONE_NUMBER'],
42
- # body: "Your verification code is: #{code}"
43
- # )
44
-
45
- # For development, just log it:
46
- Rails.logger.info("SMS to #{to}: Your verification code is #{code}")
47
- }
48
-
49
- # Email delivery (configure with your mailer)
17
+ # Required: configure your delivery channel(s)
50
18
  config.email_sender = ->(to:, code:, context:) {
51
- # Example with ActionMailer:
52
19
  # VerificationMailer.send_code(to, code, context).deliver_now
53
-
54
- # For development, just log it:
55
- Rails.logger.info("Email to #{to}: Your verification code is #{code}")
56
20
  }
57
21
 
58
- # Lifecycle callbacks (optional)
59
- # config.on_send = ->(record:, identifier:, channel:) {
60
- # Rails.logger.info("Verification sent to #{identifier} via #{channel}")
61
- # }
62
-
63
- # config.on_verify_success = ->(record:, identifier:) {
64
- # Rails.logger.info("Verification successful for #{identifier}")
65
- # }
66
-
67
- # config.on_verify_failure = ->(record:, identifier:, attempts:) {
68
- # Rails.logger.warn("Verification failed for #{identifier} (#{attempts} attempts)")
69
- # }
70
-
71
- # Namespace (for multi-tenant apps)
72
- # config.namespace = ->(record) {
73
- # record.respond_to?(:organization_id) ? "org:#{record.organization_id}" : nil
74
- # }
22
+ config.sms_sender = ->(to:, code:, context:) {
23
+ # e.g. Twilio, Vonage, etc.
24
+ }
75
25
 
76
- # Test mode (expose codes for testing)
77
- config.test_mode = Rails.env.test?
78
- config.bypass_delivery = Rails.env.test?
26
+ # Some other config options (with defaults):
27
+ # config.code_length = 6
28
+ # config.code_ttl = 300
29
+ # config.code_format = :numeric
30
+ # config.max_send_attempts = 3
31
+ # config.max_verification_attempts = 5
32
+ # config.delivery_channel = :email
33
+ # config.on_send = ->(record:, identifier:, channel:) {}
34
+ # config.on_verify_success = ->(record:, identifier:) {}
35
+ # config.on_verify_failure = ->(record:, identifier:, attempts:) {}
79
36
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/concern"
4
+
3
5
  module VerifyIt
4
6
  module Verifiable
5
7
  extend ActiveSupport::Concern
@@ -11,11 +13,6 @@ module VerifyIt
11
13
  raise ArgumentError, "Invalid channel: #{channel}. Must be :sms or :email"
12
14
  end
13
15
 
14
- # Validate attribute exists (check at definition time)
15
- unless method_defined?(attribute) || private_method_defined?(attribute)
16
- raise ArgumentError, "Attribute :#{attribute} does not respond to #{name}"
17
- end
18
-
19
16
  # Check for method conflicts
20
17
  send_method = "send_#{channel}_code"
21
18
  verify_method = "verify_#{channel}_code"
@@ -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: 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.to_s
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module VerifyIt
4
- VERSION = "0.1.1"
4
+ VERSION = "0.3.0"
5
5
  end
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.1.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremias Ramirez
@@ -135,8 +135,9 @@ dependencies:
135
135
  - - "~>"
136
136
  - !ruby/object:Gem::Version
137
137
  version: '1.5'
138
- description: VerifyIt is a storage-agnostic, delivery-agnostic verification library
139
- supporting Redis, database, and memory backends with configurable rate limiting.
138
+ description: A storage-agnostic verification system for Ruby applications. VerifyIt
139
+ provides a flexible framework for implementing SMS and email verifications with
140
+ built-in rate limiting and multiple storage backends.
140
141
  email:
141
142
  - iguanadejere4@googlemail.com
142
143
  executables:
@@ -156,6 +157,7 @@ files:
156
157
  - lib/verify_it.rb
157
158
  - lib/verify_it/cli.rb
158
159
  - lib/verify_it/code_generator.rb
160
+ - lib/verify_it/code_hasher.rb
159
161
  - lib/verify_it/configuration.rb
160
162
  - lib/verify_it/delivery/base.rb
161
163
  - lib/verify_it/delivery/email_delivery.rb
@@ -181,7 +183,6 @@ homepage: https://github.com/JeremasPosta/verify_it
181
183
  licenses:
182
184
  - MIT
183
185
  metadata:
184
- homepage_uri: https://github.com/JeremasPosta/verify_it
185
186
  source_code_uri: https://github.com/JeremasPosta/verify_it
186
187
  changelog_uri: https://github.com/JeremasPosta/verify_it/blob/main/CHANGELOG.md
187
188
  bug_tracker_uri: https://github.com/JeremasPosta/verify_it/issues