verify_it 0.2.0 → 0.4.0.beta
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 +26 -0
- data/README.md +260 -261
- data/app/controllers/verify_it/application_controller.rb +9 -0
- data/app/controllers/verify_it/verifications_controller.rb +71 -0
- data/config/locales/en.yml +16 -0
- data/config/routes.rb +6 -0
- data/lib/generators/verify_it/install/install_generator.rb +46 -0
- data/lib/{verify_it/templates/migration.rb.erb → generators/verify_it/install/templates/create_verify_it_tables.rb} +1 -3
- data/lib/{verify_it → generators/verify_it/install}/templates/initializer.rb.erb +20 -6
- data/lib/verify_it/code_generator.rb +3 -3
- data/lib/verify_it/code_hasher.rb +25 -0
- data/lib/verify_it/configuration.rb +17 -1
- data/lib/verify_it/engine.rb +19 -0
- data/lib/verify_it/storage/database_storage.rb +7 -6
- data/lib/verify_it/storage/memory_storage.rb +3 -3
- data/lib/verify_it/storage/models/attempt.rb +2 -2
- data/lib/verify_it/storage/models/code.rb +2 -2
- data/lib/verify_it/storage/models/identifier_change.rb +2 -2
- data/lib/verify_it/verifiable.rb +1 -3
- data/lib/verify_it/verifier.rb +171 -121
- data/lib/verify_it/version.rb +1 -1
- data/lib/verify_it.rb +4 -1
- data/verify_it-0.1.0.gem +0 -0
- data/verify_it-0.1.1.gem +0 -0
- data/verify_it-0.2.0.gem +0 -0
- data/verify_it-0.3.0.gem +0 -0
- metadata +57 -36
- data/exe/verify_it +0 -7
- data/lib/verify_it/cli.rb +0 -141
- data/lib/verify_it/railtie.rb +0 -14
data/README.md
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# VerifyIt
|
|
2
2
|
|
|
3
|
+
[](https://badge.fury.io/rb/verify_it)
|
|
4
|
+
|
|
3
5
|
A storage-agnostic verification system for Ruby applications.
|
|
6
|
+
|
|
4
7
|
VerifyIt provides a flexible framework for implementing SMS and email verifications with built-in rate limiting and multiple storage backends.
|
|
5
8
|
|
|
9
|
+
It allows applications to generate and validate verification codes locally, helping reduce the operational cost associated with per-verification billing models.
|
|
10
|
+
|
|
6
11
|
## Features
|
|
7
12
|
|
|
8
13
|
- **Storage Agnostic**: Redis, Memory, or Database storage
|
|
@@ -15,393 +20,387 @@ VerifyIt provides a flexible framework for implementing SMS and email verificati
|
|
|
15
20
|
|
|
16
21
|
## Installation
|
|
17
22
|
|
|
18
|
-
Add
|
|
23
|
+
Add to your Gemfile:
|
|
19
24
|
|
|
20
25
|
```ruby
|
|
21
26
|
gem 'verify_it'
|
|
22
27
|
```
|
|
23
28
|
|
|
24
|
-
And then execute:
|
|
25
|
-
|
|
26
29
|
```bash
|
|
27
30
|
bundle install
|
|
28
31
|
```
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
gem install verify_it
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
### Rails Installation
|
|
33
|
+
## Rails Setup
|
|
37
34
|
|
|
38
|
-
|
|
35
|
+
### 1. Generate configuration files
|
|
39
36
|
|
|
40
37
|
```bash
|
|
41
38
|
bundle exec verify_it install
|
|
42
39
|
```
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
The installer will ask you to choose a storage backend and generate:
|
|
42
|
+
|
|
43
|
+
- `config/initializers/verify_it.rb` — your main configuration
|
|
44
|
+
- A database migration (if you chose Database storage)
|
|
48
45
|
|
|
49
|
-
If you
|
|
46
|
+
If you chose Database storage, run:
|
|
50
47
|
|
|
51
48
|
```bash
|
|
52
49
|
rails db:migrate
|
|
53
50
|
```
|
|
54
51
|
|
|
55
|
-
|
|
52
|
+
### 2. Configure the initializer
|
|
56
53
|
|
|
57
|
-
|
|
54
|
+
Open `config/initializers/verify_it.rb` and fill in your delivery lambdas.
|
|
58
55
|
|
|
59
|
-
|
|
56
|
+
**Redis + Twilio example:**
|
|
60
57
|
|
|
61
|
-
|
|
58
|
+
```ruby
|
|
59
|
+
VerifyIt.configure do |config|
|
|
60
|
+
config.secret_key_base = Rails.application.secret_key_base
|
|
61
|
+
|
|
62
|
+
config.storage = :redis
|
|
63
|
+
config.redis_client = Redis.new(url: ENV["REDIS_URL"])
|
|
64
|
+
|
|
65
|
+
config.sms_sender = ->(to:, code:, context:) {
|
|
66
|
+
Twilio::REST::Client.new.messages.create(
|
|
67
|
+
from: ENV["TWILIO_NUMBER"],
|
|
68
|
+
to: to,
|
|
69
|
+
body: "Your verification code is: #{code}"
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
config.email_sender = ->(to:, code:, context:) {
|
|
74
|
+
EmailClient.new(to:).send("Your verification code is: #{code}")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
config.test_mode = Rails.env.test?
|
|
78
|
+
config.bypass_delivery = Rails.env.test?
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 3. Add `Verifiable` to your model
|
|
62
83
|
|
|
63
84
|
```ruby
|
|
64
85
|
class User < ApplicationRecord
|
|
65
86
|
include VerifyIt::Verifiable
|
|
66
|
-
|
|
67
|
-
verifies :phone_number
|
|
87
|
+
|
|
88
|
+
verifies :phone_number # defaults to channel: :sms
|
|
68
89
|
verifies :email, channel: :email
|
|
69
90
|
end
|
|
91
|
+
```
|
|
70
92
|
|
|
71
|
-
|
|
72
|
-
user = User.find(params[:id])
|
|
93
|
+
This generates the following methods on `User`:
|
|
73
94
|
|
|
74
|
-
|
|
75
|
-
|
|
95
|
+
| Method | Description |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `send_sms_code` | Send a code to `phone_number` via SMS |
|
|
98
|
+
| `send_email_code` | Send a code to `email` via email |
|
|
99
|
+
| `verify_sms_code(code)` | Verify an SMS code |
|
|
100
|
+
| `verify_email_code(code)` | Verify an email code |
|
|
101
|
+
| `cleanup_sms_verification` | Remove stored verification data |
|
|
102
|
+
| `cleanup_email_verification` | Remove stored verification data |
|
|
76
103
|
|
|
77
|
-
|
|
78
|
-
result = user.send_sms_code(context: {
|
|
79
|
-
message_template: "Your #{user.company_name} verification code is: %{code}"
|
|
80
|
-
})
|
|
104
|
+
### 4. Use it in your controllers
|
|
81
105
|
|
|
82
|
-
|
|
83
|
-
result = user.send_email_code(context: {
|
|
84
|
-
ip: request.ip,
|
|
85
|
-
user_agent: request.user_agent,
|
|
86
|
-
action: 'password_reset'
|
|
87
|
-
})
|
|
106
|
+
**Sending a code:**
|
|
88
107
|
|
|
89
|
-
|
|
90
|
-
|
|
108
|
+
```ruby
|
|
109
|
+
class VerificationsController < ApplicationController
|
|
110
|
+
def create
|
|
111
|
+
result = current_user.send_sms_code
|
|
112
|
+
|
|
113
|
+
if result.success?
|
|
114
|
+
render json: { expires_at: result.expires_at }
|
|
115
|
+
elsif result.rate_limited?
|
|
116
|
+
render json: { error: "Too many requests" }, status: :too_many_requests
|
|
117
|
+
else
|
|
118
|
+
render json: { error: result.message }, status: :unprocessable_entity
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
```
|
|
91
123
|
|
|
92
|
-
|
|
93
|
-
|
|
124
|
+
**Verifying a code:**
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
class VerificationsController < ApplicationController
|
|
128
|
+
def update
|
|
129
|
+
result = current_user.verify_sms_code(params[:code])
|
|
130
|
+
|
|
131
|
+
if result.verified?
|
|
132
|
+
session[:phone_verified] = true
|
|
133
|
+
render json: { verified: true }
|
|
134
|
+
elsif result.locked?
|
|
135
|
+
render json: { error: "Too many failed attempts. Try again later." }, status: :forbidden
|
|
136
|
+
else
|
|
137
|
+
remaining = VerifyIt.configuration.max_verification_attempts - result.attempts
|
|
138
|
+
render json: { error: "Invalid code. #{remaining} attempts remaining." }
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
94
142
|
```
|
|
95
143
|
|
|
96
|
-
###
|
|
144
|
+
### Passing context
|
|
97
145
|
|
|
98
|
-
|
|
146
|
+
Both `send_*` methods accept an optional `context:` hash that is forwarded to your sender lambda. Use it for custom message templates or request metadata:
|
|
99
147
|
|
|
100
148
|
```ruby
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
config.bypass_delivery = Rails.env.test?
|
|
111
|
-
end
|
|
149
|
+
current_user.send_sms_code(context: {
|
|
150
|
+
message_template: :code_verification_v1,
|
|
151
|
+
locale: :es
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
current_user.send_email_code(context: {
|
|
155
|
+
ip: request.ip,
|
|
156
|
+
user_agent: request.user_agent
|
|
157
|
+
})
|
|
112
158
|
```
|
|
113
159
|
|
|
114
|
-
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Plain Ruby
|
|
115
163
|
|
|
116
164
|
```ruby
|
|
117
|
-
require
|
|
165
|
+
require "verify_it"
|
|
118
166
|
|
|
119
167
|
VerifyIt.configure do |config|
|
|
120
|
-
|
|
121
|
-
config.storage
|
|
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)
|
|
168
|
+
config.secret_key_base = ENV.fetch("VERIFY_IT_SECRET")
|
|
169
|
+
config.storage = :memory # :memory, :redis, or :database
|
|
170
|
+
|
|
138
171
|
config.sms_sender = ->(to:, code:, context:) {
|
|
139
|
-
|
|
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
|
|
172
|
+
MySmsSender.deliver(to: to, body: "Your code: #{code}")
|
|
157
173
|
}
|
|
158
174
|
end
|
|
159
|
-
```
|
|
160
175
|
|
|
161
|
-
|
|
176
|
+
# Send a code
|
|
177
|
+
result = VerifyIt.send_code(to: "+15551234567", channel: :sms, record: current_user)
|
|
162
178
|
|
|
163
|
-
|
|
164
|
-
result = VerifyIt.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
context: { user_id: current_user.id }
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
if result.success?
|
|
172
|
-
puts "Code sent successfully!"
|
|
173
|
-
puts "Expires at: #{result.expires_at}"
|
|
174
|
-
else
|
|
175
|
-
puts "Error: #{result.message}"
|
|
176
|
-
puts "Rate limited!" if result.rate_limited?
|
|
177
|
-
end
|
|
179
|
+
# Verify a code
|
|
180
|
+
result = VerifyIt.verify_code(to: "+15551234567", code: "123456", record: current_user)
|
|
181
|
+
|
|
182
|
+
# Clean up
|
|
183
|
+
VerifyIt.cleanup(to: "+15551234567", record: current_user)
|
|
178
184
|
```
|
|
179
185
|
|
|
180
|
-
|
|
186
|
+
---
|
|
181
187
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
record: current_user
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
if result.verified?
|
|
190
|
-
# Success! Code is valid
|
|
191
|
-
session[:verified] = true
|
|
192
|
-
redirect_to dashboard_path
|
|
193
|
-
elsif result.locked?
|
|
194
|
-
# Too many failed attempts
|
|
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"
|
|
199
|
-
end
|
|
200
|
-
```
|
|
188
|
+
## Rails Engine (HTTP Endpoints)
|
|
189
|
+
|
|
190
|
+
VerifyIt ships a mountable Rails Engine that exposes two JSON endpoints. This is
|
|
191
|
+
optional, if you prefer to not use the engine, skip to the next section.
|
|
201
192
|
|
|
202
|
-
###
|
|
193
|
+
### Mount the engine
|
|
203
194
|
|
|
204
195
|
```ruby
|
|
205
|
-
#
|
|
206
|
-
VerifyIt
|
|
207
|
-
to: "+15551234567",
|
|
208
|
-
record: current_user
|
|
209
|
-
)
|
|
196
|
+
# config/routes.rb
|
|
197
|
+
mount VerifyIt::Engine, at: "/verify"
|
|
210
198
|
```
|
|
211
199
|
|
|
212
|
-
|
|
200
|
+
This adds two routes:
|
|
213
201
|
|
|
214
|
-
|
|
202
|
+
| Method | Path | Action |
|
|
203
|
+
|---|---|---|
|
|
204
|
+
| POST | `/verify/send` | Send a verification code |
|
|
205
|
+
| POST | `/verify/confirm` | Confirm a verification code |
|
|
215
206
|
|
|
216
|
-
|
|
217
|
-
|
|
207
|
+
### Configure the resolvers
|
|
208
|
+
|
|
209
|
+
Two additional config options are **required** when using the engine endpoints:
|
|
218
210
|
|
|
219
211
|
```ruby
|
|
220
|
-
config.
|
|
221
|
-
|
|
212
|
+
# config/initializers/verify_it.rb
|
|
213
|
+
VerifyIt.configure do |config|
|
|
214
|
+
config.secret_key_base = Rails.application.secret_key_base
|
|
215
|
+
config.storage = :redis
|
|
216
|
+
config.redis_client = Redis.new
|
|
222
217
|
|
|
223
|
-
|
|
224
|
-
|
|
218
|
+
# Resolve the authenticated record from the request (e.g. from a session or JWT).
|
|
219
|
+
config.current_record_resolver = ->(request) {
|
|
220
|
+
User.find_by(id: request.session[:user_id])
|
|
221
|
+
}
|
|
225
222
|
|
|
226
|
-
|
|
227
|
-
config.
|
|
228
|
-
|
|
223
|
+
# Derive the delivery identifier from the record and the requested channel.
|
|
224
|
+
config.identifier_resolver = ->(record, channel) {
|
|
225
|
+
channel == :sms ? record.phone_number : record.email
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
config.sms_sender = ->(to:, code:, context:) {
|
|
229
|
+
Twilio::REST::Client.new.messages.create(
|
|
230
|
+
from: ENV["TWILIO_NUMBER"],
|
|
231
|
+
to: to,
|
|
232
|
+
body: I18n.t("verify_it.sms.default_message", code: code)
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
end
|
|
229
236
|
```
|
|
230
237
|
|
|
231
|
-
|
|
232
|
-
Uses ActiveRecord models for persistent storage:
|
|
238
|
+
### Request/response examples
|
|
233
239
|
|
|
234
|
-
|
|
235
|
-
|
|
240
|
+
**Send a code:**
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
POST /verify/send
|
|
244
|
+
Content-Type: application/json
|
|
245
|
+
|
|
246
|
+
{ "channel": "sms" }
|
|
247
|
+
|
|
248
|
+
# 201 Created
|
|
249
|
+
{ "message": "Verification code sent." }
|
|
250
|
+
|
|
251
|
+
# 429 Too Many Requests
|
|
252
|
+
{ "error": "Too many attempts. Please try again later." }
|
|
236
253
|
```
|
|
237
254
|
|
|
238
|
-
|
|
255
|
+
**Confirm a code:**
|
|
239
256
|
|
|
240
257
|
```bash
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
```
|
|
258
|
+
POST /verify/confirm
|
|
259
|
+
Content-Type: application/json
|
|
244
260
|
|
|
245
|
-
|
|
246
|
-
- `verify_it_codes` - Stores verification codes
|
|
247
|
-
- `verify_it_attempts` - Tracks verification and send attempts
|
|
248
|
-
- `verify_it_identifier_changes` - Tracks identifier change history
|
|
261
|
+
{ "channel": "sms", "code": "123456" }
|
|
249
262
|
|
|
250
|
-
|
|
263
|
+
# 200 OK
|
|
264
|
+
{ "message": "Successfully verified." }
|
|
251
265
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
config.code_ttl = 300 # Time-to-live in seconds (5 minutes)
|
|
255
|
-
config.code_format = :numeric # :numeric, :alphanumeric, or :alpha
|
|
266
|
+
# 422 Unprocessable Entity
|
|
267
|
+
{ "error": "The code you entered is invalid." }
|
|
256
268
|
```
|
|
257
269
|
|
|
258
|
-
###
|
|
270
|
+
### I18n overrides
|
|
259
271
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
272
|
+
All messages are stored in `config/locales/en.yml` and can be overridden in your
|
|
273
|
+
application's locale files:
|
|
274
|
+
|
|
275
|
+
```yaml
|
|
276
|
+
en:
|
|
277
|
+
verify_it:
|
|
278
|
+
sms:
|
|
279
|
+
default_message: "%{code} is your one-time passcode."
|
|
280
|
+
responses:
|
|
281
|
+
verified: "Identity confirmed."
|
|
265
282
|
```
|
|
266
283
|
|
|
267
|
-
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Configuration Reference
|
|
287
|
+
|
|
288
|
+
| Option | Default | Accepted values |
|
|
289
|
+
|---|---|---|
|
|
290
|
+
| `secret_key_base` | `nil` unless using Rails | Any string — **required** |
|
|
291
|
+
| `storage` | `:memory` | `:memory`, `:redis`, `:database` |
|
|
292
|
+
| `redis_client` | `nil` | A `Redis` instance (required when `storage: :redis`) |
|
|
293
|
+
| `code_length` | `6` | Integer |
|
|
294
|
+
| `code_ttl` | `300` | Seconds (integer) |
|
|
295
|
+
| `code_format` | `:numeric` | `:numeric`, `:alphanumeric`, `:alpha` |
|
|
296
|
+
| `max_send_attempts` | `3` | Integer |
|
|
297
|
+
| `max_verification_attempts` | `5` | Integer |
|
|
298
|
+
| `max_identifier_changes` | `5` | Integer |
|
|
299
|
+
| `rate_limit_window` | `3600` | Seconds (integer) |
|
|
300
|
+
| `sms_sender` | `nil` | Lambda `(to:, code:, context:) { }` |
|
|
301
|
+
| `email_sender` | `nil` | Lambda `(to:, code:, context:) { }` |
|
|
302
|
+
| `on_send` | `nil` | Lambda `(record:, identifier:, channel:) { }` |
|
|
303
|
+
| `on_verify_success` | `nil` | Lambda `(record:, identifier:) { }` |
|
|
304
|
+
| `on_verify_failure` | `nil` | Lambda `(record:, identifier:, attempts:) { }` |
|
|
305
|
+
| `namespace` | `nil` | Lambda `(record) { }` returning a string or nil |
|
|
306
|
+
| `test_mode` | `false` | Boolean — exposes `result.code` |
|
|
307
|
+
| `bypass_delivery` | `false` | Boolean — skips actual delivery |
|
|
268
308
|
|
|
269
|
-
|
|
309
|
+
### Callbacks
|
|
270
310
|
|
|
271
311
|
```ruby
|
|
272
312
|
config.on_send = ->(record:, identifier:, channel:) {
|
|
273
|
-
Analytics.track(
|
|
313
|
+
Analytics.track("verification_sent", user_id: record.id, channel: channel)
|
|
274
314
|
}
|
|
275
315
|
|
|
276
316
|
config.on_verify_success = ->(record:, identifier:) {
|
|
277
|
-
Analytics.track(
|
|
317
|
+
Analytics.track("verification_success", user_id: record.id)
|
|
278
318
|
}
|
|
279
319
|
|
|
280
320
|
config.on_verify_failure = ->(record:, identifier:, attempts:) {
|
|
281
|
-
Analytics.track(
|
|
321
|
+
Analytics.track("verification_failure", user_id: record.id, attempts: attempts)
|
|
282
322
|
}
|
|
283
323
|
```
|
|
284
324
|
|
|
285
325
|
### Namespacing
|
|
286
326
|
|
|
287
|
-
Isolate verification data
|
|
327
|
+
Isolate verification data per tenant:
|
|
288
328
|
|
|
289
329
|
```ruby
|
|
290
|
-
config.namespace = ->(record) {
|
|
291
|
-
record.respond_to?(:organization_id) ? "org:#{record.organization_id}" : nil
|
|
292
|
-
}
|
|
330
|
+
config.namespace = ->(record) { "org:#{record.organization_id}" }
|
|
293
331
|
```
|
|
294
332
|
|
|
295
|
-
|
|
333
|
+
---
|
|
296
334
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
```ruby
|
|
300
|
-
config.test_mode = true # Includes code in Result object
|
|
301
|
-
config.bypass_delivery = true # Skip actual delivery
|
|
302
|
-
```
|
|
335
|
+
## Result Object
|
|
303
336
|
|
|
304
|
-
|
|
337
|
+
Every operation returns a `VerifyIt::Result`:
|
|
305
338
|
|
|
306
|
-
|
|
339
|
+
| Method | Type | Description |
|
|
340
|
+
|---|---|---|
|
|
341
|
+
| `success?` | Boolean | Operation completed without error |
|
|
342
|
+
| `verified?` | Boolean | Code matched successfully |
|
|
343
|
+
| `rate_limited?` | Boolean | Rate limit was exceeded |
|
|
344
|
+
| `locked?` | Boolean | Max verification attempts reached |
|
|
345
|
+
| `error` | Symbol | `:invalid_code`, `:rate_limited`, `:code_not_found`, `:locked`, `:delivery_failed` |
|
|
346
|
+
| `message` | String | Human-readable description |
|
|
347
|
+
| `code` | String | The verification code (only when `test_mode: true`) |
|
|
348
|
+
| `expires_at` | Time | When the code expires |
|
|
349
|
+
| `attempts` | Integer | Number of verification attempts so far |
|
|
307
350
|
|
|
308
|
-
|
|
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
|
-
```
|
|
351
|
+
---
|
|
319
352
|
|
|
320
353
|
## Testing
|
|
321
354
|
|
|
322
|
-
|
|
355
|
+
Configure VerifyIt in your spec helper to avoid real delivery and expose codes:
|
|
323
356
|
|
|
324
357
|
```ruby
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
358
|
+
# spec/support/verify_it.rb
|
|
359
|
+
RSpec.configure do |config|
|
|
360
|
+
config.before(:each) do
|
|
361
|
+
VerifyIt.configure do |c|
|
|
362
|
+
c.storage = :memory
|
|
363
|
+
c.test_mode = true
|
|
364
|
+
c.bypass_delivery = true
|
|
365
|
+
c.secret_key_base = "test_secret"
|
|
331
366
|
end
|
|
332
367
|
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
368
|
end
|
|
344
369
|
```
|
|
345
370
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
VerifyIt is designed to be thread-safe:
|
|
349
|
-
|
|
350
|
-
- Memory storage uses `Mutex` for synchronization
|
|
351
|
-
- Redis operations are atomic
|
|
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
|
|
371
|
+
In a request spec:
|
|
360
372
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
373
|
+
```ruby
|
|
374
|
+
it "verifies phone number" do
|
|
375
|
+
post "/verify/send", params: { phone: "+15551234567" }
|
|
376
|
+
code = JSON.parse(response.body)["code"] # available in test_mode
|
|
365
377
|
|
|
366
|
-
|
|
378
|
+
post "/verify/confirm", params: { phone: "+15551234567", code: code }
|
|
379
|
+
expect(response).to have_http_status(:ok)
|
|
380
|
+
end
|
|
381
|
+
```
|
|
367
382
|
|
|
368
|
-
|
|
383
|
+
---
|
|
369
384
|
|
|
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
|
|
385
|
+
## Security
|
|
375
386
|
|
|
376
|
-
|
|
387
|
+
- **`secret_key_base` is required** — codes are hashed with HMAC-SHA256 before storage
|
|
388
|
+
- Do not disable rate limiting
|
|
389
|
+
- Use `:redis` or `:database` storage in production
|
|
390
|
+
- Keep `code_ttl` short.
|
|
391
|
+
- Use `on_verify_failure` to monitor and alert on repeated failures
|
|
377
392
|
|
|
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
|
|
393
|
+
---
|
|
384
394
|
|
|
385
395
|
## Development
|
|
386
396
|
|
|
387
|
-
After checking out the repo:
|
|
388
|
-
|
|
389
397
|
```bash
|
|
390
|
-
bin/setup
|
|
391
|
-
bundle exec rspec
|
|
392
|
-
|
|
393
|
-
|
|
398
|
+
bin/setup # Install dependencies
|
|
399
|
+
bundle exec rspec # Run tests
|
|
400
|
+
bundle exec rubocop # Lint
|
|
401
|
+
bin/console # Interactive console
|
|
394
402
|
```
|
|
395
403
|
|
|
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
404
|
## Contributing
|
|
406
405
|
|
|
407
406
|
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,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VerifyIt
|
|
4
|
+
class ApplicationController < ActionController::API
|
|
5
|
+
rescue_from VerifyIt::ConfigurationError do |_error|
|
|
6
|
+
render json: { error: "Service configuration error" }, status: :internal_server_error
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|