didww-otp_verification 1.1.0 → 1.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 +76 -0
- data/README.md +112 -16
- data/lib/didww/otp_verification/callback_verifier.rb +12 -5
- data/lib/didww/otp_verification/client.rb +32 -1
- data/lib/didww/otp_verification/errors.rb +5 -2
- data/lib/didww/otp_verification/rails.rb +2 -1
- data/lib/didww/otp_verification/verification.rb +52 -3
- data/lib/didww/otp_verification/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2d642797b509a6a3a07315a163ac283086e3877a7d6304f88fb968995ad16f09
|
|
4
|
+
data.tar.gz: 6656b36bd906929a5397887573d1d1b40ed29fb3cecbe8add71ec5cc2355934d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c994241bc5f386def42788c2bc6d7b6fbad3050784de09a396252046343df7fa16cd8c6066df35a2168cbb101fffec56f2dded64d2a5d7ff82204e5c40e1683a
|
|
7
|
+
data.tar.gz: 8e3313f28b2c2327b5e73d31ba4b24d574981f89a4878089e584924dbc395035e1b86ea24c0a3d7622b68733e2bf1da8f26db6ea50e6c86828bf1f659a46cea0
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,82 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
|
|
|
6
6
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html): from 1.0.0 onwards a breaking change
|
|
7
7
|
to the public surface requires a major version.
|
|
8
8
|
|
|
9
|
+
## [1.3.0] — 2026-11
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`destination_prefix_rate_limited` error code**, raised as `RateLimitedError`
|
|
14
|
+
(429) from `start_verification` when there are too many verifications to a
|
|
15
|
+
number range. `#retry_after` carries the `Retry-After` wait in seconds; do not
|
|
16
|
+
retry it automatically.
|
|
17
|
+
- **`channel_blocked` error code.** A start can be refused with a 422 carrying it
|
|
18
|
+
when the account can't use the requested delivery method. Contact DIDWW
|
|
19
|
+
support.
|
|
20
|
+
|
|
21
|
+
## [1.2.0] — 2026-10
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **`custom:` on `start_verification`**, up to 4096 characters of free text sent
|
|
26
|
+
as top-level `data.custom` and delivered unchanged to your callback server in
|
|
27
|
+
the `verification_request` body (the key is absent when unset). It is not
|
|
28
|
+
returned in API responses. A longer value is refused with the new
|
|
29
|
+
`custom_too_long` error code.
|
|
30
|
+
- **`sms: {autofill: ...}`** with type `app_hash`, `domain` or `none`.
|
|
31
|
+
`Verification#sms_autofill` reads back the stored marker, `app_hash` or
|
|
32
|
+
`domain`, whose `value` is the hash or the domain. New error codes
|
|
33
|
+
`autofill_invalid` and `sms_domain_not_configured`.
|
|
34
|
+
- **`template_group` in the `sms:` and `callout:` options**, to pick a template
|
|
35
|
+
group instead of the application's default. New readers
|
|
36
|
+
`Verification#sms_template_group`/`#callout_template_group`,
|
|
37
|
+
`#sms_template_group_used`/`#callout_template_group_used` and
|
|
38
|
+
`#sms_template_fallback_reason`/`#callout_template_fallback_reason`; the last
|
|
39
|
+
is an open set (`language_missing`, `version_pending`, `version_rejected`,
|
|
40
|
+
`audio_not_ready`, `group_missing`). An unknown group is refused with the new
|
|
41
|
+
`template_group_not_found` error code.
|
|
42
|
+
- **Secret rotation support in callback verification.** During a secret
|
|
43
|
+
rotation the service also sends an `x-signature-previous` header, signed with
|
|
44
|
+
the previous secret. `CallbackVerifier#valid?` takes an optional
|
|
45
|
+
`previous_signature:` and accepts the callback when either signature matches
|
|
46
|
+
the configured secret. `RailsCallbackVerifier` reads the header itself.
|
|
47
|
+
Upgrade the SDK before rotating a secret.
|
|
48
|
+
- **New 429 error codes:** `rate_limit_per_ip` (too many requests from this IP
|
|
49
|
+
address), `rate_limit_failed_auth` (too many failed authentication attempts
|
|
50
|
+
from this IP address), `rate_limit_per_account` (too many requests for this
|
|
51
|
+
account), `rate_limit_public_per_account` (too many public requests for this
|
|
52
|
+
account) and `volume_limit_reached` (the application reached its daily or
|
|
53
|
+
monthly verification limit; `Retry-After` can be days, so surface it instead
|
|
54
|
+
of retrying). All raise `RateLimitedError`.
|
|
55
|
+
- **New outcome codes:** `application_paused` and `destination_country_disabled`
|
|
56
|
+
(status `denied`).
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
|
|
60
|
+
- **`destination_not_supported_for_channel` is now an outcome, not a 422.** A
|
|
61
|
+
start for a number with no supported country, no delivery for the requested
|
|
62
|
+
method, or in a blocked range returns `201` with status `failed` and this
|
|
63
|
+
`error_code`. Code that rescued the 422 now gets a failed verification
|
|
64
|
+
instead. The error code stays in the list for compatibility.
|
|
65
|
+
|
|
66
|
+
- **Supported Ruby is now 3.3+** (`required_ruby_version >= 3.3.0`). Ruby 3.1
|
|
67
|
+
went EOL on 2025-03-26 and 3.2 on 2026-03-31. CI tests 3.3, 3.4 and 4.0.
|
|
68
|
+
|
|
69
|
+
### Deprecated
|
|
70
|
+
|
|
71
|
+
- **The `sms: {app_hash: ...}` option and `Verification#sms_app_hash`.** Use
|
|
72
|
+
`autofill`. The option is sent as `autofill: {type: "app_hash", value: ...}`
|
|
73
|
+
with a `:deprecated` warning; passing it together with a non-nil `autofill`
|
|
74
|
+
raises `ArgumentError`.
|
|
75
|
+
- **The `stale_dispatch` outcome code.** The service no longer sends it and
|
|
76
|
+
reports these verifications as `dispatch_failed`. It stays documented so
|
|
77
|
+
existing matches keep working, and will be dropped from the docs in the next
|
|
78
|
+
major version.
|
|
79
|
+
|
|
80
|
+
### Docs
|
|
81
|
+
|
|
82
|
+
- A Testing section on the sandbox environment, which sends nothing and gives
|
|
83
|
+
a predefined result only for the reserved test numbers.
|
|
84
|
+
|
|
9
85
|
## [1.1.0] — 2026-10
|
|
10
86
|
|
|
11
87
|
### Added
|
data/README.md
CHANGED
|
@@ -65,15 +65,23 @@ reads only the block matching `delivery_method` and ignores the others.
|
|
|
65
65
|
| Field | Description |
|
|
66
66
|
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
67
67
|
| `languages` | Preferred template languages as BCP 47 tags, most preferred first. Matched exactly, so the region subtag is required — `"pl"` does not match `pl-PL`. Unmatched tags fall back to `en-US`. |
|
|
68
|
-
| `
|
|
68
|
+
| `autofill` | Auto-fill marker for the delivered message. `{type: "app_hash", value: "..."}` is the Android SMS Retriever hash: exactly 11 characters of `[A-Za-z0-9+/]`. The message is then prefixed with `<#> ` and the hash appended as its last token, so the handset can auto-fill the code. `{type: "domain"}` adds a domain-bound line for the application's configured SMS domain. `{type: "none"}` explicitly asks for no marker. Omit it to use the application default. |
|
|
69
|
+
| `template_group` | Name of one of your template groups, used instead of the application's default. |
|
|
69
70
|
|
|
70
|
-
`
|
|
71
|
+
`autofill` is here because a Ruby backend often starts the verification on behalf
|
|
71
72
|
of an Android app — the hash identifies that app, so only the app can compute it.
|
|
73
|
+
`{type: "domain"}` takes no `value`; on an application with no SMS domain it is
|
|
74
|
+
refused with `sms_domain_not_configured`. A malformed `autofill` is refused with
|
|
75
|
+
`autofill_invalid`.
|
|
76
|
+
The former `app_hash:` key still works as a deprecated alias for
|
|
77
|
+
`autofill: {type: "app_hash", value: ...}` (it prints a deprecation warning
|
|
78
|
+
when Ruby's `:deprecated` warnings are enabled). Passing both with non-nil values
|
|
79
|
+
raises `ArgumentError`; `autofill: nil` counts as absent.
|
|
72
80
|
|
|
73
81
|
```ruby
|
|
74
82
|
client.start_verification(
|
|
75
83
|
destination: "+4915112345678", delivery_method: "sms",
|
|
76
|
-
sms: {languages: ["en-US"], app_hash: "A1b2C3d4E5f"}
|
|
84
|
+
sms: {languages: ["en-US"], autofill: {type: "app_hash", value: "A1b2C3d4E5f"}}
|
|
77
85
|
)
|
|
78
86
|
```
|
|
79
87
|
|
|
@@ -82,6 +90,10 @@ client.start_verification(
|
|
|
82
90
|
| Field | Description |
|
|
83
91
|
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
84
92
|
| `languages` | Preferred announcement languages — the same tags, order and matching rules as the `sms:` ones, so the same list is valid for either method. It is not shared between them, though: each method reads only its own block, so the list goes under `callout:` here and under `sms:` there. The catalogues are separate too, so a tag that has a message template but no recording is accepted and falls back to `en-US`. |
|
|
93
|
+
| `template_group` | Name of one of your template groups, as for `sms:`. |
|
|
94
|
+
|
|
95
|
+
An unknown `template_group` on either method is refused with
|
|
96
|
+
`template_group_not_found`.
|
|
85
97
|
|
|
86
98
|
```ruby
|
|
87
99
|
client.start_verification(
|
|
@@ -96,6 +108,20 @@ API ships it. The cost of that is no typo protection: an unrecognized option is
|
|
|
96
108
|
ignored rather than rejected, and a misspelled key returns `201` with the
|
|
97
109
|
defaults applied, not an error.
|
|
98
110
|
|
|
111
|
+
### `custom:`
|
|
112
|
+
|
|
113
|
+
Optional free text, up to 4096 characters, sent as top-level `data.custom` (an
|
|
114
|
+
empty string counts as absent). It is not returned by the API; it reaches your
|
|
115
|
+
callback server as `data.custom` in the `verification_request` body; the key is
|
|
116
|
+
absent when unset. A longer value is refused with `custom_too_long`.
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
client.start_verification(
|
|
120
|
+
destination: "+4915112345678", delivery_method: "sms",
|
|
121
|
+
custom: "order-42"
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
99
125
|
### Reading a method block back
|
|
100
126
|
|
|
101
127
|
The response carries the block for the method that was used, readable field by
|
|
@@ -106,11 +132,17 @@ v.sms_template # => "Your code is {{CODE}}"
|
|
|
106
132
|
v.sms_language # => "en-US"
|
|
107
133
|
v.sms_interception_timeout # => 300
|
|
108
134
|
v.sms_code_length # => 6
|
|
109
|
-
v.
|
|
135
|
+
v.sms_autofill # => {"type" => "app_hash", "value" => "A1b2C3d4E5f"}, or nil
|
|
136
|
+
v.sms_template_group # => "checkout", or nil if none applied
|
|
137
|
+
v.sms_template_group_used # => true
|
|
138
|
+
v.sms_template_fallback_reason # => nil, or e.g. "language_missing"
|
|
110
139
|
v.sms # => the raw block, or nil on a callout verification
|
|
111
140
|
|
|
112
141
|
v.callout_language # => "de-DE"
|
|
113
142
|
v.callout_code_length # => 6
|
|
143
|
+
v.callout_template_group # => "checkout", or nil if none applied
|
|
144
|
+
v.callout_template_group_used # => true
|
|
145
|
+
v.callout_template_fallback_reason # => nil, or e.g. "audio_not_ready"
|
|
114
146
|
v.callout # => the raw block, or nil on an sms verification
|
|
115
147
|
```
|
|
116
148
|
|
|
@@ -127,8 +159,16 @@ default 300) and is **not** a deadline for the verification itself — manual
|
|
|
127
159
|
entry keeps working until `expires_at`.
|
|
128
160
|
`sms_code_length`/`callout_code_length` is the length of this verification's
|
|
129
161
|
code, 4–8 digits, set per application (default 6).
|
|
130
|
-
`
|
|
131
|
-
persisted rather than what was requested
|
|
162
|
+
`sms_autofill` is echoed back only when a hash or domain marker was stored, so it
|
|
163
|
+
reflects what was persisted rather than what was requested; for `domain`, `value`
|
|
164
|
+
is the domain. `sms_app_hash` is a deprecated reader derived from it: the `value`
|
|
165
|
+
when the type is `app_hash`, otherwise `nil`.
|
|
166
|
+
`*_template_group_used` is true when the group's template was used. When it is
|
|
167
|
+
false, `*_template_fallback_reason` says why: `language_missing`,
|
|
168
|
+
`version_pending`, `version_rejected`, `audio_not_ready` or `group_missing` (the
|
|
169
|
+
group no longer exists; `*_template_group` is then `nil`). Both are false/`nil`
|
|
170
|
+
when no template group applies at all. Like error codes, the reasons are an open
|
|
171
|
+
set of plain strings.
|
|
132
172
|
|
|
133
173
|
### Address by phone number
|
|
134
174
|
|
|
@@ -167,6 +207,15 @@ DIDWW::OTPVerification.configure do |c|
|
|
|
167
207
|
end
|
|
168
208
|
```
|
|
169
209
|
|
|
210
|
+
## Testing
|
|
211
|
+
|
|
212
|
+
The sandbox sends no real SMS and places no real calls. Only the reserved test
|
|
213
|
+
numbers give a predefined result, such as delivered, failed or expired; a
|
|
214
|
+
delivered one has a fixed code. Any other number is never delivered to, and its
|
|
215
|
+
verification ends as failed. The test numbers, their outcomes and the code to
|
|
216
|
+
submit are listed in
|
|
217
|
+
[Testing on sandbox](https://doc.didww.com/otp-verification/testing.html).
|
|
218
|
+
|
|
170
219
|
## Configuration
|
|
171
220
|
|
|
172
221
|
Set global defaults; every field is overridable per `Client`.
|
|
@@ -273,15 +322,35 @@ ok = verifier.valid?(
|
|
|
273
322
|
content_type: request.content_type,
|
|
274
323
|
body: request.raw_post, # RAW body bytes — do not re-serialize
|
|
275
324
|
timestamp: request.headers["x-timestamp"],
|
|
276
|
-
signature: signature
|
|
325
|
+
signature: signature,
|
|
326
|
+
previous_signature: request.headers["x-signature-previous"] # optional
|
|
277
327
|
)
|
|
278
328
|
|
|
279
329
|
render json: { action: ok ? "allow" : "deny" }
|
|
280
330
|
```
|
|
281
331
|
|
|
332
|
+
The `verification_request` body carries the `custom` value you passed to
|
|
333
|
+
`start_verification` as `data.custom` (absent when unset). When the verification
|
|
334
|
+
is started from a mobile app, that value is client-controlled: validate it
|
|
335
|
+
against your own records rather than trusting it.
|
|
336
|
+
|
|
282
337
|
The verifier also enforces a 5-minute timestamp window (configurable via
|
|
283
338
|
`tolerance:`) to reject replays.
|
|
284
339
|
|
|
340
|
+
### Rotating the application secret
|
|
341
|
+
|
|
342
|
+
While a rotated-out secret is still valid, DIDWW also sends an
|
|
343
|
+
`x-signature-previous` header: the same signature computed with the previous
|
|
344
|
+
secret. The verifier accepts a callback when either the `Authorization`
|
|
345
|
+
signature or `previous_signature` matches the secret you configured. A receiver
|
|
346
|
+
can therefore switch from the old secret to the new one at any point during the
|
|
347
|
+
grace period, with no failed callbacks. Without the header the verifier behaves
|
|
348
|
+
as before.
|
|
349
|
+
|
|
350
|
+
Upgrade the SDK before you rotate a secret, because an older version ignores
|
|
351
|
+
`x-signature-previous` and rejects callbacks signed with the new secret while
|
|
352
|
+
the old one is configured.
|
|
353
|
+
|
|
285
354
|
### Rails
|
|
286
355
|
|
|
287
356
|
`RailsCallbackVerifier` pulls every signed field off an `ActionDispatch`
|
|
@@ -335,13 +404,23 @@ carries `error_code` (switch on it) and `error_detail` (display it). These are
|
|
|
335
404
|
| ---------- | -------- | -------------------------------------------------------------------------------------------- |
|
|
336
405
|
| `pending` | no | `nil` — on its way, or awaiting a report |
|
|
337
406
|
| `verified` | yes | `nil` |
|
|
338
|
-
| `failed` | yes | `too_many_attempts`, `dispatch_failed`, `
|
|
407
|
+
| `failed` | yes | `too_many_attempts`, `dispatch_failed`, `superseded`, `application_deleted`, `destination_not_supported_for_channel`, `stale_dispatch` (deprecated) |
|
|
339
408
|
| `expired` | yes | `expired` — past the application's code lifetime (60–600 s, default 300) |
|
|
340
|
-
| `denied` | yes | `denied_by_callback`, `denied_missing_callback_url`, `denied_invalid_callback_response`
|
|
409
|
+
| `denied` | yes | `denied_by_callback`, `denied_missing_callback_url`, `denied_invalid_callback_response`, `application_paused`, `destination_country_disabled` |
|
|
341
410
|
|
|
342
411
|
`superseded` means a newer `start_verification` for the same number retired this
|
|
343
412
|
one — only the newest verification per number stays active.
|
|
344
413
|
|
|
414
|
+
Some starts are refused as an outcome rather than an error: a `201` whose status
|
|
415
|
+
is `denied` (`application_paused`, `destination_country_disabled`) or `failed`
|
|
416
|
+
(`destination_not_supported_for_channel`: the number has no supported country,
|
|
417
|
+
can't be reached by this delivery method, or is in a blocked range). Earlier
|
|
418
|
+
versions returned the last one as a `422`; that code stays in the error list for
|
|
419
|
+
compatibility only.
|
|
420
|
+
|
|
421
|
+
`stale_dispatch` is deprecated: the service no longer sends it and reports these
|
|
422
|
+
verifications as `dispatch_failed`.
|
|
423
|
+
|
|
345
424
|
```ruby
|
|
346
425
|
v = client.get_verification(id)
|
|
347
426
|
|
|
@@ -370,7 +449,7 @@ Non-2xx responses raise a typed error under `DIDWW::OTPVerification::Error`:
|
|
|
370
449
|
| `BalanceInsufficientError` | 402 | `balance_insufficient` |
|
|
371
450
|
| `NotFoundError` | 404 | `not_found` |
|
|
372
451
|
| `ValidationError` | 400 / 422 | `parameter_missing` / per-field codes |
|
|
373
|
-
| `RateLimitedError` | 429 | `destination_in_cooldown`
|
|
452
|
+
| `RateLimitedError` | 429 | `destination_in_cooldown`, `rate_limit_*`, `volume_limit_reached`, `destination_prefix_rate_limited` |
|
|
374
453
|
| `ServerError` | 5xx | `internal_error` |
|
|
375
454
|
|
|
376
455
|
Every error carries the API's coded envelope: `#errors` is an array of
|
|
@@ -394,21 +473,38 @@ rescue DIDWW::OTPVerification::ValidationError => e
|
|
|
394
473
|
end
|
|
395
474
|
```
|
|
396
475
|
|
|
476
|
+
`channel_blocked` (422): the account can't use the requested delivery method.
|
|
477
|
+
No verification is created; contact DIDWW support.
|
|
478
|
+
|
|
397
479
|
## Retries
|
|
398
480
|
|
|
399
481
|
The SDK does **not** auto-retry. If you add `faraday-retry`, exclude `POST`
|
|
400
482
|
(double-charges) and `PATCH` (each report counts against the 3-attempt limit).
|
|
401
483
|
|
|
402
|
-
A `
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
484
|
+
A 429 raises `RateLimitedError`, and its code says which limit was hit:
|
|
485
|
+
|
|
486
|
+
- `destination_in_cooldown`: a verification for this destination was requested
|
|
487
|
+
too recently.
|
|
488
|
+
- `rate_limit_per_ip`: too many requests from this IP address.
|
|
489
|
+
- `rate_limit_failed_auth`: too many failed authentication attempts from this
|
|
490
|
+
IP address.
|
|
491
|
+
- `rate_limit_per_account`: too many requests for this account.
|
|
492
|
+
- `rate_limit_public_per_account`: too many public requests for this account.
|
|
493
|
+
- `volume_limit_reached`: the application reached its daily or monthly
|
|
494
|
+
verification limit. `Retry-After` can be long (days) — surface it to the
|
|
495
|
+
user instead of retrying. Contact support to raise the limit.
|
|
496
|
+
- `destination_prefix_rate_limited`: too many verifications to this number range;
|
|
497
|
+
try again later. Wait `Retry-After` before starting another one, and show the
|
|
498
|
+
user "Too many verification requests right now. Please try again later."
|
|
499
|
+
|
|
500
|
+
`#retry_after` gives the wait in seconds (`nil` if the `Retry-After` header is
|
|
501
|
+
missing or unparseable) — wait that long before trying again yourself; never
|
|
502
|
+
auto-retry it.
|
|
407
503
|
|
|
408
504
|
## Development
|
|
409
505
|
|
|
410
506
|
Development is pinned to the Ruby in `.ruby-version`; the gem itself supports
|
|
411
|
-
3.
|
|
507
|
+
3.3+.
|
|
412
508
|
|
|
413
509
|
```sh
|
|
414
510
|
bundle install
|
|
@@ -19,7 +19,8 @@ module DIDWW
|
|
|
19
19
|
# verifier.valid?(
|
|
20
20
|
# method: "POST", path: "/callbacks/didww",
|
|
21
21
|
# content_type: "application/json", body: raw_body,
|
|
22
|
-
# timestamp: request.headers["x-timestamp"], signature: signature
|
|
22
|
+
# timestamp: request.headers["x-timestamp"], signature: signature,
|
|
23
|
+
# previous_signature: request.headers["x-signature-previous"]
|
|
23
24
|
# )
|
|
24
25
|
class CallbackVerifier
|
|
25
26
|
# @param tolerance [Integer] max allowed clock skew, in seconds (server uses 5 minutes).
|
|
@@ -41,16 +42,21 @@ module DIDWW
|
|
|
41
42
|
[key, signature]
|
|
42
43
|
end
|
|
43
44
|
|
|
44
|
-
# @
|
|
45
|
-
#
|
|
46
|
-
|
|
45
|
+
# @param previous_signature [String, nil] the `x-signature-previous` header, sent
|
|
46
|
+
# during a secret rotation and signed with the previous secret. Ignored when absent.
|
|
47
|
+
# @return [Boolean] true when the timestamp is fresh and the signature, or the
|
|
48
|
+
# previous signature, matches (constant-time comparison).
|
|
49
|
+
def valid?(method:, path:, content_type:, body:, timestamp:, signature:, previous_signature: nil)
|
|
47
50
|
return false if Util.blank?(signature)
|
|
48
51
|
return false unless fresh?(timestamp)
|
|
49
52
|
|
|
50
53
|
expected = @signer.sign(
|
|
51
54
|
method:, path:, content_type:, body:, timestamp: timestamp
|
|
52
55
|
)
|
|
53
|
-
|
|
56
|
+
# Both comparisons always run, so timing does not reveal which header matched.
|
|
57
|
+
current = secure_compare(expected, signature)
|
|
58
|
+
previous = secure_compare(expected, previous_signature)
|
|
59
|
+
current || previous
|
|
54
60
|
end
|
|
55
61
|
|
|
56
62
|
private
|
|
@@ -63,6 +69,7 @@ module DIDWW
|
|
|
63
69
|
|
|
64
70
|
# Constant-time string comparison via the audited stdlib implementation.
|
|
65
71
|
def secure_compare(a, b)
|
|
72
|
+
return false if b.nil?
|
|
66
73
|
return false unless a.bytesize == b.bytesize
|
|
67
74
|
|
|
68
75
|
OpenSSL.fixed_length_secure_compare(a, b)
|
|
@@ -50,10 +50,21 @@ module DIDWW
|
|
|
50
50
|
# reads only the block matching +delivery_method+ and ignores the others.
|
|
51
51
|
# An unrecognized option is ignored rather than rejected, so a typo there
|
|
52
52
|
# fails silently. Methods with no options of their own take no keyword.
|
|
53
|
-
|
|
53
|
+
#
|
|
54
|
+
# +custom+ is free text (up to 4096 characters) delivered to your
|
|
55
|
+
# callback server as +data.custom+; it is not part of the response.
|
|
56
|
+
#
|
|
57
|
+
# The +sms+ key +app_hash+ is deprecated: it is sent as
|
|
58
|
+
# <tt>autofill: {type: "app_hash", value: ...}</tt> with a deprecation
|
|
59
|
+
# warning. Passing it together with +autofill+ raises ArgumentError,
|
|
60
|
+
# because the API rejects that combination. Both apply only when
|
|
61
|
+
# +delivery_method+ is +sms+, the only method that reads +autofill+.
|
|
62
|
+
def start_verification(destination:, delivery_method:, sms: nil, callout: nil, custom: nil)
|
|
54
63
|
data = {destination:, delivery_method:}
|
|
64
|
+
data[:custom] = custom unless custom.nil?
|
|
55
65
|
data[:sms] = sms unless sms.nil?
|
|
56
66
|
data[:callout] = callout unless callout.nil?
|
|
67
|
+
data[:sms] = migrate_app_hash(data[:sms]) if delivery_method.to_s == "sms" && data[:sms].is_a?(Hash)
|
|
57
68
|
request(:post, "#{API_PREFIX}/verifications", data)
|
|
58
69
|
end
|
|
59
70
|
|
|
@@ -85,6 +96,26 @@ module DIDWW
|
|
|
85
96
|
|
|
86
97
|
private
|
|
87
98
|
|
|
99
|
+
# Maps the deprecated sms +app_hash+ onto +autofill+ on a copy, so the
|
|
100
|
+
# caller's hash is left alone.
|
|
101
|
+
def migrate_app_hash(sms)
|
|
102
|
+
keys = [:app_hash, "app_hash"].select { |k| sms.key?(k) }
|
|
103
|
+
return sms if keys.empty?
|
|
104
|
+
|
|
105
|
+
# A nil hash counts as absent, as the API always treated it.
|
|
106
|
+
key = keys.find { |k| !sms[k].nil? }
|
|
107
|
+
return sms.except(*keys) unless key
|
|
108
|
+
# A nil autofill counts as absent too, as the API treats it.
|
|
109
|
+
autofill_keys = [:autofill, "autofill"].select { |k| sms.key?(k) }
|
|
110
|
+
if autofill_keys.any? { |k| !sms[k].nil? }
|
|
111
|
+
raise ArgumentError, "sms: pass either autofill or the deprecated app_hash, not both"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
warn "DIDWW::OTPVerification: sms app_hash is deprecated; " \
|
|
115
|
+
"use sms: {autofill: {type: \"app_hash\", value: ...}}", category: :deprecated, uplevel: 2
|
|
116
|
+
sms.except(*keys, *autofill_keys).merge(autofill: {type: "app_hash", value: sms[key]})
|
|
117
|
+
end
|
|
118
|
+
|
|
88
119
|
# Percent-encode a value for use as a single URL path segment. Notably
|
|
89
120
|
# turns a leading "+" into "%2B" so it survives proxies that would
|
|
90
121
|
# otherwise decode "+" to a space in the path.
|
|
@@ -48,8 +48,11 @@ module DIDWW
|
|
|
48
48
|
# 400 Bad Request / 422 Unprocessable Content (validation errors in +errors+).
|
|
49
49
|
class ValidationError < APIError; end
|
|
50
50
|
|
|
51
|
-
# 429 Too Many Requests
|
|
52
|
-
#
|
|
51
|
+
# 429 Too Many Requests. +code+ says which limit was hit:
|
|
52
|
+
# +destination_in_cooldown+ (this destination was requested too recently),
|
|
53
|
+
# a +rate_limit_*+ request limit, +volume_limit_reached+ (a daily or
|
|
54
|
+
# monthly limit; +retry_after+ can be days), or +destination_prefix_rate_limited+
|
|
55
|
+
# (too many verifications to this number range; wait +retry_after+).
|
|
53
56
|
# Never auto-retry this.
|
|
54
57
|
class RateLimitedError < APIError
|
|
55
58
|
# @return [Integer, nil] seconds to wait, from the Retry-After header.
|
|
@@ -61,10 +61,40 @@ module DIDWW
|
|
|
61
61
|
@sms && @sms["code_length"]
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
-
# @return [
|
|
65
|
-
#
|
|
64
|
+
# @return [Hash, nil] the stored autofill marker, +{"type", "value"}+ with
|
|
65
|
+
# type +app_hash+ (value: the hash) or +domain+ (value: the domain);
|
|
66
|
+
# +nil+ unless one was stored on this verification.
|
|
67
|
+
def sms_autofill
|
|
68
|
+
@sms && @sms["autofill"]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @deprecated Use {#sms_autofill}.
|
|
72
|
+
# @return [String, nil] the SMS Retriever hash, when the stored autofill
|
|
73
|
+
# is of type +app_hash+.
|
|
66
74
|
def sms_app_hash
|
|
67
|
-
|
|
75
|
+
autofill = sms_autofill
|
|
76
|
+
return @sms && @sms["app_hash"] unless autofill
|
|
77
|
+
|
|
78
|
+
autofill["value"] if autofill["type"] == "app_hash"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @return [String, nil] the template group applied; nil when none applies
|
|
82
|
+
# or it no longer exists. Only present for the +sms+ method.
|
|
83
|
+
def sms_template_group
|
|
84
|
+
@sms && @sms["template_group"]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [Boolean, nil] true when the group's template was used. Only
|
|
88
|
+
# present for the +sms+ method.
|
|
89
|
+
def sms_template_group_used
|
|
90
|
+
@sms && @sms["template_group_used"]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @return [String, nil] why the group's template was not used, e.g.
|
|
94
|
+
# +language_missing+ (an open set); nil when it was or when no group
|
|
95
|
+
# applies. Only present for the +sms+ method.
|
|
96
|
+
def sms_template_fallback_reason
|
|
97
|
+
@sms && @sms["template_fallback_reason"]
|
|
68
98
|
end
|
|
69
99
|
|
|
70
100
|
# The tag the announcement is played in: the first requested language the
|
|
@@ -83,6 +113,25 @@ module DIDWW
|
|
|
83
113
|
@callout && @callout["code_length"]
|
|
84
114
|
end
|
|
85
115
|
|
|
116
|
+
# @return [String, nil] the template group applied; nil when none applies
|
|
117
|
+
# or it no longer exists. Only present for the +callout+ method.
|
|
118
|
+
def callout_template_group
|
|
119
|
+
@callout && @callout["template_group"]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @return [Boolean, nil] true when the group's template was used. Only
|
|
123
|
+
# present for the +callout+ method.
|
|
124
|
+
def callout_template_group_used
|
|
125
|
+
@callout && @callout["template_group_used"]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @return [String, nil] why the group's template was not used, e.g.
|
|
129
|
+
# +audio_not_ready+ (an open set); nil when it was or when no group
|
|
130
|
+
# applies. Only present for the +callout+ method.
|
|
131
|
+
def callout_template_fallback_reason
|
|
132
|
+
@callout && @callout["template_fallback_reason"]
|
|
133
|
+
end
|
|
134
|
+
|
|
86
135
|
def pending? = status == "pending"
|
|
87
136
|
def verified? = status == "verified"
|
|
88
137
|
def failed? = status == "failed"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: didww-otp_verification
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- DIDWW
|
|
@@ -85,7 +85,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
85
85
|
requirements:
|
|
86
86
|
- - ">="
|
|
87
87
|
- !ruby/object:Gem::Version
|
|
88
|
-
version: 3.
|
|
88
|
+
version: 3.3.0
|
|
89
89
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
90
90
|
requirements:
|
|
91
91
|
- - ">="
|