didit-sdk 1.0.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 +7 -0
- data/CHANGELOG.md +25 -0
- data/LICENSE +21 -0
- data/README.md +277 -0
- data/lib/didit/authentication/base64_url.rb +34 -0
- data/lib/didit/authentication/cache.rb +201 -0
- data/lib/didit/authentication/http.rb +150 -0
- data/lib/didit/authentication/issuer.rb +115 -0
- data/lib/didit/authentication/jws.rb +231 -0
- data/lib/didit/authentication/key_set.rb +89 -0
- data/lib/didit/authentication/pkce.rb +64 -0
- data/lib/didit/authentication/types.rb +175 -0
- data/lib/didit/authentication.rb +546 -0
- data/lib/didit/client.rb +211 -0
- data/lib/didit/errors.rb +153 -0
- data/lib/didit/file_part.rb +85 -0
- data/lib/didit/internal/config.rb +94 -0
- data/lib/didit/internal/errors.rb +83 -0
- data/lib/didit/internal/log.rb +41 -0
- data/lib/didit/internal/multipart.rb +60 -0
- data/lib/didit/internal/request.rb +322 -0
- data/lib/didit/internal/transport.rb +198 -0
- data/lib/didit/internal/util.rb +76 -0
- data/lib/didit/internal.rb +15 -0
- data/lib/didit/model.rb +199 -0
- data/lib/didit/page.rb +130 -0
- data/lib/didit/raw_response.rb +57 -0
- data/lib/didit/resource.rb +15 -0
- data/lib/didit/resources/account.rb +309 -0
- data/lib/didit/resources/address_validation.rb +73 -0
- data/lib/didit/resources/age_estimation.rb +110 -0
- data/lib/didit/resources/aml.rb +166 -0
- data/lib/didit/resources/aml_monitoring.rb +30 -0
- data/lib/didit/resources/analytics.rb +87 -0
- data/lib/didit/resources/bank.rb +65 -0
- data/lib/didit/resources/billing.rb +61 -0
- data/lib/didit/resources/biometric_templates.rb +234 -0
- data/lib/didit/resources/blocklist.rb +130 -0
- data/lib/didit/resources/business_sessions.rb +29 -0
- data/lib/didit/resources/businesses.rb +471 -0
- data/lib/didit/resources/cases.rb +2116 -0
- data/lib/didit/resources/customization.rb +153 -0
- data/lib/didit/resources/database_validation.rb +327 -0
- data/lib/didit/resources/document_ai.rb +145 -0
- data/lib/didit/resources/email.rb +254 -0
- data/lib/didit/resources/face_match.rb +103 -0
- data/lib/didit/resources/face_search.rb +103 -0
- data/lib/didit/resources/fraud_checks.rb +130 -0
- data/lib/didit/resources/id_verification.rb +163 -0
- data/lib/didit/resources/id_wallets.rb +1468 -0
- data/lib/didit/resources/investigator.rb +419 -0
- data/lib/didit/resources/ip.rb +68 -0
- data/lib/didit/resources/kyb.rb +256 -0
- data/lib/didit/resources/lists.rb +332 -0
- data/lib/didit/resources/monitoring_events.rb +466 -0
- data/lib/didit/resources/networks.rb +427 -0
- data/lib/didit/resources/passive_liveness.rb +103 -0
- data/lib/didit/resources/phone.rb +271 -0
- data/lib/didit/resources/pricing.rb +40 -0
- data/lib/didit/resources/proof_of_address.rb +167 -0
- data/lib/didit/resources/questionnaires.rb +170 -0
- data/lib/didit/resources/regulatory_reports.rb +314 -0
- data/lib/didit/resources/report_templates.rb +146 -0
- data/lib/didit/resources/sessions.rb +2820 -0
- data/lib/didit/resources/transactions.rb +981 -0
- data/lib/didit/resources/travel_rule.rb +1202 -0
- data/lib/didit/resources/users.rb +521 -0
- data/lib/didit/resources/wallet_screening.rb +190 -0
- data/lib/didit/resources/webhooks.rb +396 -0
- data/lib/didit/resources/workflows.rb +214 -0
- data/lib/didit/resources.rb +263 -0
- data/lib/didit/types/account.rb +11 -0
- data/lib/didit/types/account_components.rb +105 -0
- data/lib/didit/types/address_validation.rb +8 -0
- data/lib/didit/types/age_estimation.rb +89 -0
- data/lib/didit/types/aml.rb +193 -0
- data/lib/didit/types/aml_monitoring.rb +73 -0
- data/lib/didit/types/analytics.rb +62 -0
- data/lib/didit/types/bank.rb +8 -0
- data/lib/didit/types/billing.rb +18 -0
- data/lib/didit/types/biometric_templates.rb +292 -0
- data/lib/didit/types/blocklist.rb +31 -0
- data/lib/didit/types/businesses.rb +45 -0
- data/lib/didit/types/cases.rb +368 -0
- data/lib/didit/types/components.rb +12206 -0
- data/lib/didit/types/customization.rb +8 -0
- data/lib/didit/types/database_validation.rb +177 -0
- data/lib/didit/types/document_ai.rb +135 -0
- data/lib/didit/types/email.rb +116 -0
- data/lib/didit/types/face_match.rb +93 -0
- data/lib/didit/types/face_search.rb +129 -0
- data/lib/didit/types/fraud_checks.rb +8 -0
- data/lib/didit/types/id_verification.rb +231 -0
- data/lib/didit/types/id_wallets.rb +8 -0
- data/lib/didit/types/investigator.rb +221 -0
- data/lib/didit/types/ip.rb +8 -0
- data/lib/didit/types/kyb.rb +1391 -0
- data/lib/didit/types/lists.rb +111 -0
- data/lib/didit/types/monitoring_events.rb +235 -0
- data/lib/didit/types/networks.rb +120 -0
- data/lib/didit/types/passive_liveness.rb +95 -0
- data/lib/didit/types/phone.rb +69 -0
- data/lib/didit/types/proof_of_address.rb +166 -0
- data/lib/didit/types/questionnaires.rb +29 -0
- data/lib/didit/types/regulatory_reports.rb +100 -0
- data/lib/didit/types/report_templates.rb +41 -0
- data/lib/didit/types/sessions.rb +1441 -0
- data/lib/didit/types/transactions.rb +245 -0
- data/lib/didit/types/travel_rule.rb +168 -0
- data/lib/didit/types/users.rb +180 -0
- data/lib/didit/types/wallet_screening.rb +49 -0
- data/lib/didit/types/webhooks.rb +355 -0
- data/lib/didit/types/workflows.rb +32 -0
- data/lib/didit/types.rb +1681 -0
- data/lib/didit/version.rb +5 -0
- data/lib/didit/webhooks/canonical_json.rb +187 -0
- data/lib/didit/webhooks/client_methods.rb +23 -0
- data/lib/didit/webhooks/events.rb +221 -0
- data/lib/didit/webhooks/headers.rb +83 -0
- data/lib/didit/webhooks/verifier.rb +260 -0
- data/lib/didit/webhooks.rb +99 -0
- data/lib/didit-sdk.rb +4 -0
- data/lib/didit.rb +31 -0
- data/sig/didit/authentication/base64_url.rbs +10 -0
- data/sig/didit/authentication/cache.rbs +60 -0
- data/sig/didit/authentication/http.rbs +37 -0
- data/sig/didit/authentication/issuer.rbs +17 -0
- data/sig/didit/authentication/jws.rbs +41 -0
- data/sig/didit/authentication/jwt.rbs +14 -0
- data/sig/didit/authentication/key_set.rbs +25 -0
- data/sig/didit/authentication/pkce.rbs +20 -0
- data/sig/didit/authentication/types.rbs +114 -0
- data/sig/didit/authentication.rbs +81 -0
- data/sig/didit/client.rbs +61 -0
- data/sig/didit/errors.rbs +80 -0
- data/sig/didit/file_part.rbs +19 -0
- data/sig/didit/internal.rbs +199 -0
- data/sig/didit/model.rbs +42 -0
- data/sig/didit/page.rbs +34 -0
- data/sig/didit/raw_response.rbs +20 -0
- data/sig/didit/resource.rbs +8 -0
- data/sig/didit/resources/account.rbs +23 -0
- data/sig/didit/resources/address_validation.rbs +9 -0
- data/sig/didit/resources/age_estimation.rbs +9 -0
- data/sig/didit/resources/aml.rbs +10 -0
- data/sig/didit/resources/aml_monitoring.rbs +9 -0
- data/sig/didit/resources/analytics.rbs +9 -0
- data/sig/didit/resources/bank.rbs +9 -0
- data/sig/didit/resources/billing.rbs +10 -0
- data/sig/didit/resources/biometric_templates.rbs +13 -0
- data/sig/didit/resources/blocklist.rbs +11 -0
- data/sig/didit/resources/business_sessions.rbs +9 -0
- data/sig/didit/resources/businesses.rbs +22 -0
- data/sig/didit/resources/cases.rbs +103 -0
- data/sig/didit/resources/customization.rbs +10 -0
- data/sig/didit/resources/database_validation.rbs +9 -0
- data/sig/didit/resources/document_ai.rbs +9 -0
- data/sig/didit/resources/email.rbs +11 -0
- data/sig/didit/resources/face_match.rbs +9 -0
- data/sig/didit/resources/face_search.rbs +9 -0
- data/sig/didit/resources/fraud_checks.rbs +11 -0
- data/sig/didit/resources/id_verification.rbs +9 -0
- data/sig/didit/resources/id_wallets.rbs +37 -0
- data/sig/didit/resources/investigator.rbs +23 -0
- data/sig/didit/resources/ip.rbs +9 -0
- data/sig/didit/resources/kyb.rbs +12 -0
- data/sig/didit/resources/lists.rbs +20 -0
- data/sig/didit/resources/monitoring_events.rbs +16 -0
- data/sig/didit/resources/networks.rbs +25 -0
- data/sig/didit/resources/passive_liveness.rbs +9 -0
- data/sig/didit/resources/phone.rbs +11 -0
- data/sig/didit/resources/pricing.rbs +9 -0
- data/sig/didit/resources/proof_of_address.rbs +9 -0
- data/sig/didit/resources/questionnaires.rbs +13 -0
- data/sig/didit/resources/regulatory_reports.rbs +16 -0
- data/sig/didit/resources/report_templates.rbs +13 -0
- data/sig/didit/resources/sessions.rbs +78 -0
- data/sig/didit/resources/transactions.rbs +43 -0
- data/sig/didit/resources/travel_rule.rbs +62 -0
- data/sig/didit/resources/users.rbs +21 -0
- data/sig/didit/resources/wallet_screening.rbs +11 -0
- data/sig/didit/resources/webhooks.rbs +23 -0
- data/sig/didit/resources/workflows.rbs +13 -0
- data/sig/didit/resources.rbs +50 -0
- data/sig/didit/types/account.rbs +22 -0
- data/sig/didit/types/account_components.rbs +94 -0
- data/sig/didit/types/address_validation.rbs +7 -0
- data/sig/didit/types/age_estimation.rbs +80 -0
- data/sig/didit/types/aml.rbs +174 -0
- data/sig/didit/types/aml_monitoring.rbs +67 -0
- data/sig/didit/types/analytics.rbs +57 -0
- data/sig/didit/types/bank.rbs +9 -0
- data/sig/didit/types/billing.rbs +19 -0
- data/sig/didit/types/biometric_templates.rbs +268 -0
- data/sig/didit/types/blocklist.rbs +33 -0
- data/sig/didit/types/businesses.rbs +54 -0
- data/sig/didit/types/cases.rbs +414 -0
- data/sig/didit/types/components.rbs +10920 -0
- data/sig/didit/types/customization.rbs +7 -0
- data/sig/didit/types/database_validation.rbs +152 -0
- data/sig/didit/types/document_ai.rbs +123 -0
- data/sig/didit/types/email.rbs +112 -0
- data/sig/didit/types/face_match.rbs +86 -0
- data/sig/didit/types/face_search.rbs +112 -0
- data/sig/didit/types/fraud_checks.rbs +9 -0
- data/sig/didit/types/id_verification.rbs +196 -0
- data/sig/didit/types/id_wallets.rbs +55 -0
- data/sig/didit/types/investigator.rbs +212 -0
- data/sig/didit/types/ip.rbs +7 -0
- data/sig/didit/types/kyb.rbs +1254 -0
- data/sig/didit/types/lists.rbs +106 -0
- data/sig/didit/types/monitoring_events.rbs +234 -0
- data/sig/didit/types/networks.rbs +113 -0
- data/sig/didit/types/passive_liveness.rbs +87 -0
- data/sig/didit/types/phone.rbs +65 -0
- data/sig/didit/types/proof_of_address.rbs +144 -0
- data/sig/didit/types/questionnaires.rbs +30 -0
- data/sig/didit/types/regulatory_reports.rbs +101 -0
- data/sig/didit/types/report_templates.rbs +42 -0
- data/sig/didit/types/sessions.rbs +1349 -0
- data/sig/didit/types/transactions.rbs +273 -0
- data/sig/didit/types/travel_rule.rbs +188 -0
- data/sig/didit/types/users.rbs +181 -0
- data/sig/didit/types/wallet_screening.rbs +40 -0
- data/sig/didit/types/webhooks.rbs +311 -0
- data/sig/didit/types/workflows.rbs +30 -0
- data/sig/didit/webhooks/canonical_json.rbs +25 -0
- data/sig/didit/webhooks/client_methods.rbs +11 -0
- data/sig/didit/webhooks/events.rbs +125 -0
- data/sig/didit/webhooks/headers.rbs +22 -0
- data/sig/didit/webhooks/verifier.rbs +54 -0
- data/sig/didit/webhooks.rbs +37 -0
- data/sig/didit.rbs +20 -0
- metadata +300 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
module Didit
|
|
7
|
+
module Webhooks
|
|
8
|
+
# One verification (docs/DESIGN.md section 9.3). {Didit::Webhooks.verify} and
|
|
9
|
+
# {Didit::Webhooks.verify_parsed} are the public entry points.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
class Verifier
|
|
13
|
+
# Signatures over the whole body, in the order they are tried for each secret.
|
|
14
|
+
FULL_BODY = [
|
|
15
|
+
HEADER_SIGNATURE, HEADER_SIGNATURE_V2, HEADER_SIGNATURE_PREVIOUS, HEADER_SIGNATURE_V2_PREVIOUS
|
|
16
|
+
].freeze
|
|
17
|
+
# Signatures of the canonical JSON, the only ones a parsed body can be checked against.
|
|
18
|
+
CANONICAL = [HEADER_SIGNATURE_V2, HEADER_SIGNATURE_V2_PREVIOUS].freeze
|
|
19
|
+
# The deprecated envelope-only signatures.
|
|
20
|
+
SIMPLE = [HEADER_SIGNATURE_SIMPLE, HEADER_SIGNATURE_SIMPLE_PREVIOUS].freeze
|
|
21
|
+
ALL = (FULL_BODY + SIMPLE).freeze
|
|
22
|
+
# The fields X-Signature-Simple signs, in order.
|
|
23
|
+
SIMPLE_FIELDS = %w[timestamp session_id status webhook_type].freeze
|
|
24
|
+
|
|
25
|
+
# Timestamps at or beyond this many seconds from 1970 (16 digits) are never in tolerance.
|
|
26
|
+
MAX_TIMESTAMP = 1_000_000_000_000_000
|
|
27
|
+
# X-Timestamp: an optional sign and digits (leading zeros allowed), at most 32 characters.
|
|
28
|
+
TIMESTAMP_HEADER = /\A[+-]?[0-9]{1,31}\z/
|
|
29
|
+
# Strict JSON (no NaN). A repeated key keeps its last value, like Python. json 2.13 added
|
|
30
|
+
# allow_duplicate_key for that (without it json 2.13+ warns and json 3 raises); older
|
|
31
|
+
# versions do not know the option and keep the last value anyway.
|
|
32
|
+
DUPLICATE_KEY_OPTION = (JSON::VERSION.split(".").map(&:to_i) <=> [2, 13]).to_i >= 0
|
|
33
|
+
PARSE_OPTIONS = (DUPLICATE_KEY_OPTION ? { allow_nan: false, allow_duplicate_key: true } : { allow_nan: false })
|
|
34
|
+
.freeze
|
|
35
|
+
|
|
36
|
+
# @raise [Didit::ConfigurationError] for a missing or empty secret, a bad tolerance or clock,
|
|
37
|
+
# or headers that cannot be read
|
|
38
|
+
def initialize(secret, headers, tolerance:, now:)
|
|
39
|
+
@keys = secret_keys(secret)
|
|
40
|
+
@tolerance = check_tolerance(tolerance)
|
|
41
|
+
@now = check_now(now)
|
|
42
|
+
@headers = Headers.new(headers)
|
|
43
|
+
@raw = nil
|
|
44
|
+
@payload = nil
|
|
45
|
+
@digests = {}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Event]
|
|
49
|
+
def verify(raw_body, allow_simple:)
|
|
50
|
+
# Only true turns the envelope-only signature on: "false" from an environment variable must not.
|
|
51
|
+
unless [true, false].include?(allow_simple)
|
|
52
|
+
raise ConfigurationError, "allow_simple must be true or false, got #{allow_simple.inspect}."
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
raw = body_bytes(raw_body)
|
|
56
|
+
@raw = raw
|
|
57
|
+
@payload = normalize(parse_json(raw))
|
|
58
|
+
verified_by = check_signatures(allow_simple ? FULL_BODY + SIMPLE : FULL_BODY)
|
|
59
|
+
Event.build(payload, verified_by)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Event]
|
|
63
|
+
def verify_parsed(payload)
|
|
64
|
+
unless payload.is_a?(Hash)
|
|
65
|
+
raise failure(:invalid_payload,
|
|
66
|
+
"verify_parsed needs the parsed JSON object (a Hash), not a #{payload.class}; " \
|
|
67
|
+
"pass the raw body String to verify instead.")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@payload = normalize(payload)
|
|
71
|
+
Event.build(self.payload, check_signatures(CANONICAL))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# The parsed body, a Hash with String keys.
|
|
77
|
+
def payload
|
|
78
|
+
@payload || raise(ArgumentError, "no payload")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def secret_keys(secret)
|
|
82
|
+
if secret.nil?
|
|
83
|
+
raise ConfigurationError, "No webhook secret (nil); copy it from the webhook destination in the " \
|
|
84
|
+
"Didit console, for example into DIDIT_WEBHOOK_SECRET."
|
|
85
|
+
end
|
|
86
|
+
list = secret.is_a?(Array) ? secret : [secret]
|
|
87
|
+
raise ConfigurationError, "No webhook secret (an empty list)." if list.empty?
|
|
88
|
+
|
|
89
|
+
list.map { |item| key_bytes(item) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def key_bytes(secret)
|
|
93
|
+
unless secret.is_a?(String)
|
|
94
|
+
raise ConfigurationError, "The webhook secret must be a String or an Array of Strings."
|
|
95
|
+
end
|
|
96
|
+
raise ConfigurationError, "The webhook secret is empty." if secret.empty?
|
|
97
|
+
|
|
98
|
+
(secret.encoding == Encoding::BINARY ? secret : secret.encode(Encoding::UTF_8)).b
|
|
99
|
+
rescue EncodingError
|
|
100
|
+
raise ConfigurationError, "The webhook secret cannot be converted to UTF-8."
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def check_tolerance(tolerance)
|
|
104
|
+
number = tolerance.is_a?(Integer) || tolerance.is_a?(Float)
|
|
105
|
+
# `>= 0` is false for NaN, which would otherwise switch the replay check off.
|
|
106
|
+
return tolerance if number && tolerance >= 0
|
|
107
|
+
|
|
108
|
+
raise ConfigurationError, "tolerance must be a number of seconds, 0 or more."
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def check_now(now)
|
|
112
|
+
return Time.now.to_f if now.nil?
|
|
113
|
+
return now if now.is_a?(Integer) || (now.is_a?(Float) && now.finite?)
|
|
114
|
+
|
|
115
|
+
raise ConfigurationError, "now must be unix seconds, an Integer or a Float."
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def body_bytes(raw_body)
|
|
119
|
+
return raw_body.b if raw_body.is_a?(String)
|
|
120
|
+
|
|
121
|
+
raise failure(:invalid_payload, "The webhook body must be the raw request body as a String, " \
|
|
122
|
+
"not a #{raw_body.class}.")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Every parser failure is invalid_payload, whatever the JSON library raises.
|
|
126
|
+
def parse_json(bytes)
|
|
127
|
+
text = bytes.dup.force_encoding(Encoding::UTF_8)
|
|
128
|
+
raise failure(:invalid_payload, "The webhook body is not valid UTF-8.") unless text.valid_encoding?
|
|
129
|
+
|
|
130
|
+
value = begin
|
|
131
|
+
JSON.parse(text, **PARSE_OPTIONS)
|
|
132
|
+
rescue StandardError # JSON::ParserError, JSON::NestingError, EncodingError, ...
|
|
133
|
+
raise failure(:invalid_payload, "The webhook body is not valid JSON.")
|
|
134
|
+
end
|
|
135
|
+
raise failure(:invalid_payload, "The webhook body is not a JSON object.") unless value.is_a?(Hash)
|
|
136
|
+
|
|
137
|
+
value
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def normalize(value)
|
|
141
|
+
CanonicalJson.normalize(value)
|
|
142
|
+
rescue ArgumentError => e
|
|
143
|
+
raise failure(:invalid_payload, "The webhook payload is not JSON data Didit sends: #{e.message}.")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Steps 3 to 9 of the rules: timestamp, then the signatures in order.
|
|
147
|
+
def check_signatures(checked)
|
|
148
|
+
check_timestamp
|
|
149
|
+
unless ALL.any? { |name| @headers.present?(name) }
|
|
150
|
+
raise failure(:missing_signature,
|
|
151
|
+
"The request has no Didit signature header (X-Signature, X-Signature-V2 or " \
|
|
152
|
+
"X-Signature-Simple).")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
match(checked) || raise(no_match(checked))
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def check_timestamp
|
|
159
|
+
timestamp = payload["timestamp"]
|
|
160
|
+
unless timestamp.is_a?(Integer)
|
|
161
|
+
raise failure(:missing_timestamp, "The webhook body has no integer \"timestamp\".")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
stale = timestamp >= MAX_TIMESTAMP || timestamp <= -MAX_TIMESTAMP
|
|
165
|
+
check_timestamp_header(timestamp) unless stale
|
|
166
|
+
return unless stale || (@now - timestamp).abs > @tolerance
|
|
167
|
+
|
|
168
|
+
raise failure(:timestamp_out_of_tolerance,
|
|
169
|
+
"The webhook timestamp is more than #{@tolerance} seconds away from now: a delayed or " \
|
|
170
|
+
"replayed delivery, or a clock that is off.")
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def check_timestamp_header(timestamp)
|
|
174
|
+
header = @headers.timestamp
|
|
175
|
+
return if header.nil? || (header.match?(TIMESTAMP_HEADER) && Integer(header, 10) == timestamp)
|
|
176
|
+
|
|
177
|
+
raise failure(:timestamp_mismatch, "X-Timestamp does not match the \"timestamp\" in the webhook body.")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# The first header, for the first secret, whose signature matches; nil when none does.
|
|
181
|
+
def match(checked)
|
|
182
|
+
full = checked & FULL_BODY
|
|
183
|
+
simple = checked & SIMPLE
|
|
184
|
+
@keys.each do |key|
|
|
185
|
+
full.each { |name| return name if signed?(key, name) }
|
|
186
|
+
end
|
|
187
|
+
return if simple.empty? || simple_message.nil?
|
|
188
|
+
|
|
189
|
+
@keys.each do |key|
|
|
190
|
+
simple.each { |name| return name if signed?(key, name) }
|
|
191
|
+
end
|
|
192
|
+
nil
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def signed?(key, name)
|
|
196
|
+
candidates = @headers.signatures(name)
|
|
197
|
+
return false if candidates.empty?
|
|
198
|
+
|
|
199
|
+
expected = digest(key, name)
|
|
200
|
+
candidates.any? { |candidate| OpenSSL.fixed_length_secure_compare(expected, candidate) }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# The HMAC of what +name+ signs; a header and its -Previous variant share it.
|
|
204
|
+
def digest(key, name)
|
|
205
|
+
kind = message_kind(name)
|
|
206
|
+
@digests[[key, kind]] ||= OpenSSL::HMAC.hexdigest("SHA256", key, message(kind))
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def message_kind(name)
|
|
210
|
+
return :canonical if CANONICAL.include?(name)
|
|
211
|
+
|
|
212
|
+
SIMPLE.include?(name) ? :simple : :raw
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def message(kind)
|
|
216
|
+
case kind
|
|
217
|
+
when :raw then @raw || raise(ArgumentError, "no raw body")
|
|
218
|
+
when :canonical then canonical
|
|
219
|
+
else simple_message || raise(ArgumentError, "no simple message")
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Built only when an X-Signature-V2 value is well formed and nothing matched before it.
|
|
224
|
+
def canonical
|
|
225
|
+
@canonical ||= CanonicalJson.dump(payload)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# "{timestamp}:{session_id}:{status}:{webhook_type}" with Python's str() of each value;
|
|
229
|
+
# nil when one of them is an object or an array, which never matches.
|
|
230
|
+
def simple_message
|
|
231
|
+
return @simple_message if defined?(@simple_message)
|
|
232
|
+
|
|
233
|
+
parts = SIMPLE_FIELDS.map { |field| payload.key?(field) ? CanonicalJson.python_str(payload[field]) : "" }
|
|
234
|
+
@simple_message = parts.all? ? parts.join(":") : nil
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def no_match(checked)
|
|
238
|
+
present = ALL.select { |name| @headers.present?(name) }
|
|
239
|
+
message =
|
|
240
|
+
if checked == CANONICAL && !present.intersect?(CANONICAL)
|
|
241
|
+
"verify_parsed can check only X-Signature-V2, and the request has none; pass the raw body to verify."
|
|
242
|
+
elsif checked == CANONICAL
|
|
243
|
+
"X-Signature-V2 does not match: check that the secret belongs to this destination and that the " \
|
|
244
|
+
"payload is the body as parsed, unchanged."
|
|
245
|
+
elsif !present.intersect?(checked)
|
|
246
|
+
"The request carries only X-Signature-Simple, which does not cover the body and counts only " \
|
|
247
|
+
"with allow_simple: true."
|
|
248
|
+
else
|
|
249
|
+
"No webhook signature matches the secret: check that the secret belongs to this destination and " \
|
|
250
|
+
"that the raw body reaches verify unchanged."
|
|
251
|
+
end
|
|
252
|
+
failure(:invalid_signature, message)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def failure(reason, message)
|
|
256
|
+
WebhookVerificationError.new(reason, message)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Didit
|
|
4
|
+
# Verify Didit webhook deliveries and read them as typed events (docs/DESIGN.md section 9).
|
|
5
|
+
#
|
|
6
|
+
# event = Didit::Webhooks.verify(request.raw_post, request.headers, ENV.fetch("DIDIT_WEBHOOK_SECRET"))
|
|
7
|
+
# case event
|
|
8
|
+
# in Didit::Webhooks::StatusUpdatedEvent(status: "Approved", session_id:) then approve(session_id)
|
|
9
|
+
# in Didit::Webhooks::UnknownEvent then nil # a type this SDK version does not know yet
|
|
10
|
+
# else nil
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# The verifier needs no client, no API key and no framework; +examples/webhooks/+ has Rails,
|
|
14
|
+
# Sinatra and plain Rack receivers. The same methods are on +client.webhooks+.
|
|
15
|
+
module Webhooks
|
|
16
|
+
# Default maximum distance in seconds between the delivery's timestamp and now.
|
|
17
|
+
DEFAULT_TOLERANCE = 300
|
|
18
|
+
|
|
19
|
+
HEADER_SIGNATURE = "X-Signature"
|
|
20
|
+
HEADER_SIGNATURE_V2 = "X-Signature-V2"
|
|
21
|
+
HEADER_SIGNATURE_SIMPLE = "X-Signature-Simple"
|
|
22
|
+
HEADER_SIGNATURE_PREVIOUS = "X-Signature-Previous"
|
|
23
|
+
HEADER_SIGNATURE_V2_PREVIOUS = "X-Signature-V2-Previous"
|
|
24
|
+
HEADER_SIGNATURE_SIMPLE_PREVIOUS = "X-Signature-Simple-Previous"
|
|
25
|
+
HEADER_TIMESTAMP = "X-Timestamp"
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
# Verifies a webhook delivery and returns its typed event.
|
|
29
|
+
#
|
|
30
|
+
# Pass the exact bytes of the request body (+request.raw_post+, +request.body.read+).
|
|
31
|
+
# The delivery is accepted when, for one of the secrets, +X-Signature+ matches the body,
|
|
32
|
+
# +X-Signature-V2+ matches its canonical JSON (it survives middleware that re-encodes the
|
|
33
|
+
# JSON), or one of their +-Previous+ variants matches (secret rotation). The deprecated
|
|
34
|
+
# +X-Signature-Simple+ counts only with +allow_simple: true+, and then
|
|
35
|
+
# {Event#body_authenticated?} is false: re-fetch the record from the API.
|
|
36
|
+
#
|
|
37
|
+
# @param raw_body [String] the request body as received, in any encoding (read as UTF-8)
|
|
38
|
+
# @param headers [Hash, #each] request headers: a Hash (names in any case), a Rack env,
|
|
39
|
+
# Rails +request.headers+, or [name, value] pairs
|
|
40
|
+
# @param secret [String, Array<String>] the destination's secret, or several during a rotation
|
|
41
|
+
# @param tolerance [Integer, Float] largest accepted distance in seconds between the
|
|
42
|
+
# delivery's timestamp and now
|
|
43
|
+
# @param allow_simple [Boolean] also accept the envelope-only +X-Signature-Simple+
|
|
44
|
+
# @param now [Integer, Float, nil] the current unix time, for tests
|
|
45
|
+
# @return [Event]
|
|
46
|
+
# @raise [Didit::WebhookVerificationError] when the delivery fails verification; +reason+ says why
|
|
47
|
+
# @raise [Didit::ConfigurationError] for a missing or empty secret, a negative tolerance,
|
|
48
|
+
# an +allow_simple+ other than true or false, or headers that cannot be read
|
|
49
|
+
def verify(raw_body, headers, secret, tolerance: DEFAULT_TOLERANCE, allow_simple: false, now: nil)
|
|
50
|
+
Verifier.new(secret, headers, tolerance: tolerance, now: now).verify(raw_body, allow_simple: allow_simple)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Verifies a delivery when the framework only gives the parsed JSON body.
|
|
54
|
+
#
|
|
55
|
+
# Only +X-Signature-V2+ and +X-Signature-V2-Previous+ can be checked: the other signatures
|
|
56
|
+
# cover the exact body bytes. Prefer {verify} with the raw body whenever you have it.
|
|
57
|
+
#
|
|
58
|
+
# @param payload [Hash] the parsed JSON object, String or Symbol keys
|
|
59
|
+
# @return [Event]
|
|
60
|
+
# @raise (see verify)
|
|
61
|
+
def verify_parsed(payload, headers, secret, tolerance: DEFAULT_TOLERANCE, now: nil)
|
|
62
|
+
Verifier.new(secret, headers, tolerance: tolerance, now: now).verify_parsed(payload)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The canonical JSON of a parsed body, the text +X-Signature-V2+ signs: exactly Python's
|
|
66
|
+
# +json.dumps(shorten_floats(value), sort_keys=True, separators=(",", ":"), ensure_ascii=False)+.
|
|
67
|
+
#
|
|
68
|
+
# @param value [Hash, Array, String, Integer, Float, true, false, nil] parsed JSON
|
|
69
|
+
# @return [String] UTF-8
|
|
70
|
+
# @raise [ArgumentError] when +value+ is not JSON data (NaN, Infinity, a lone surrogate, ...)
|
|
71
|
+
def canonical_json(value)
|
|
72
|
+
CanonicalJson.generate(value)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Builds the typed event of a payload WITHOUT verifying anything.
|
|
76
|
+
#
|
|
77
|
+
# For unsigned callbacks only, such as +kyb.registry_search.resolved+: anyone can send
|
|
78
|
+
# such a request, so treat every field as untrusted and fetch the record from the API
|
|
79
|
+
# before acting on it. {Event#verified_by} is nil and {Event#body_authenticated?} false.
|
|
80
|
+
#
|
|
81
|
+
# @param payload [Hash] the parsed JSON object
|
|
82
|
+
# @return [Event]
|
|
83
|
+
# @raise [ArgumentError] when +payload+ is not a Hash
|
|
84
|
+
def parse_event(payload)
|
|
85
|
+
unless payload.is_a?(Hash)
|
|
86
|
+
raise ArgumentError, "parse_event needs the parsed JSON object (a Hash), not a #{payload.class}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
Event.build(payload)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
require_relative "webhooks/canonical_json"
|
|
96
|
+
require_relative "webhooks/headers"
|
|
97
|
+
require_relative "webhooks/verifier"
|
|
98
|
+
require_relative "webhooks/events"
|
|
99
|
+
require_relative "webhooks/client_methods"
|
data/lib/didit-sdk.rb
ADDED
data/lib/didit.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "didit/version"
|
|
4
|
+
require_relative "didit/errors"
|
|
5
|
+
|
|
6
|
+
# The official Didit Ruby library.
|
|
7
|
+
#
|
|
8
|
+
# didit = Didit::Client.new # reads DIDIT_API_KEY
|
|
9
|
+
# session = didit.sessions.create(workflow_id: "...", vendor_data: "user-42")
|
|
10
|
+
#
|
|
11
|
+
# See https://docs.didit.me and docs/DESIGN.md.
|
|
12
|
+
module Didit
|
|
13
|
+
# The value of a keyword argument the caller did not pass. Such arguments are
|
|
14
|
+
# left out of the request; +nil+ sends JSON +null+.
|
|
15
|
+
OMIT = Object.new.tap do |omit|
|
|
16
|
+
def omit.inspect = "Didit::OMIT"
|
|
17
|
+
def omit.to_s = "Didit::OMIT"
|
|
18
|
+
end.freeze
|
|
19
|
+
|
|
20
|
+
autoload :Authentication, "didit/authentication"
|
|
21
|
+
autoload :Client, "didit/client"
|
|
22
|
+
autoload :FilePart, "didit/file_part"
|
|
23
|
+
autoload :Internal, "didit/internal"
|
|
24
|
+
autoload :Model, "didit/model"
|
|
25
|
+
autoload :Page, "didit/page"
|
|
26
|
+
autoload :RawResponse, "didit/raw_response"
|
|
27
|
+
autoload :Resource, "didit/resource"
|
|
28
|
+
autoload :Resources, "didit/resources"
|
|
29
|
+
autoload :Types, "didit/types"
|
|
30
|
+
autoload :Webhooks, "didit/webhooks"
|
|
31
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Didit
|
|
2
|
+
class Authentication
|
|
3
|
+
class Discovery
|
|
4
|
+
attr_reader issuer: String
|
|
5
|
+
attr_reader token_endpoint: String
|
|
6
|
+
attr_reader jwks_uri: String
|
|
7
|
+
attr_reader document: Hash[String, untyped]
|
|
8
|
+
|
|
9
|
+
def initialize: (issuer: String, token_endpoint: String, jwks_uri: String, document: Hash[String, untyped]) -> void
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class IssuerCache
|
|
13
|
+
DEFAULT_MAX_AGE: Integer
|
|
14
|
+
MIN_MAX_AGE: Integer
|
|
15
|
+
MAX_MAX_AGE: Integer
|
|
16
|
+
REFETCH_INTERVAL: Integer
|
|
17
|
+
NO_CACHE: Array[String]
|
|
18
|
+
|
|
19
|
+
class Entry[T]
|
|
20
|
+
attr_reader value: T
|
|
21
|
+
attr_reader expires_at: Float
|
|
22
|
+
|
|
23
|
+
def initialize: (T value, Float expires_at) -> void
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.cache_seconds: (Hash[String, String] headers) -> Integer
|
|
27
|
+
def self.deep_freeze: (untyped value) -> untyped
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def self.max_age: (String cache_control) -> Integer?
|
|
32
|
+
|
|
33
|
+
public
|
|
34
|
+
|
|
35
|
+
@clock: ^() -> (Integer | Float)
|
|
36
|
+
@discovery: Entry[Discovery]?
|
|
37
|
+
@key_set: Entry[KeySet]?
|
|
38
|
+
@key_set_attempted_at: Float?
|
|
39
|
+
@key_set_failed: bool
|
|
40
|
+
@refused: Entry[String]?
|
|
41
|
+
|
|
42
|
+
def initialize: (^() -> (Integer | Float) clock) -> void
|
|
43
|
+
def discovery: () -> Discovery?
|
|
44
|
+
def store_discovery: (Discovery discovery, Hash[String, String] headers) -> Discovery
|
|
45
|
+
def discovery_refused: (String message) -> nil
|
|
46
|
+
def refused_discovery: () -> String?
|
|
47
|
+
def key_set: () -> KeySet?
|
|
48
|
+
def store_key_set: (KeySet key_set, Hash[String, String] headers) -> KeySet
|
|
49
|
+
def key_set_fetch_failed: () -> nil
|
|
50
|
+
def key_set_unavailable?: () -> bool
|
|
51
|
+
def key_set_with: (String kid) -> KeySet?
|
|
52
|
+
def needs_key_set_fetch?: (String? kid) -> bool
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def fresh: [T] (Entry[T]? entry) -> T?
|
|
57
|
+
def now: () -> Float
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Didit
|
|
2
|
+
class Authentication
|
|
3
|
+
class Http
|
|
4
|
+
class Call
|
|
5
|
+
attr_reader http_method: String
|
|
6
|
+
attr_reader uri: URI::Generic
|
|
7
|
+
attr_reader bearer: String?
|
|
8
|
+
attr_reader form: Hash[String, String]?
|
|
9
|
+
attr_reader json: Hash[String, untyped]?
|
|
10
|
+
attr_reader secrets: Array[String]
|
|
11
|
+
|
|
12
|
+
def initialize: (String http_method, URI::Generic uri, ?bearer: String?, ?form: Hash[String, String]?,
|
|
13
|
+
?json: Hash[String, untyped]?, ?secrets: Array[String]) -> void
|
|
14
|
+
def display_url: () -> String
|
|
15
|
+
def describe: () -> String
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
VERBS: Hash[String, Symbol]
|
|
19
|
+
|
|
20
|
+
@timeout: Float
|
|
21
|
+
@transport: Internal::Transport
|
|
22
|
+
|
|
23
|
+
def initialize: (timeout: Float, http_client: Internal::http_client?) -> void
|
|
24
|
+
def perform: (Call call) -> Internal::Transport::Response
|
|
25
|
+
def close: () -> nil
|
|
26
|
+
def read: (Call call, Internal::Transport::Response response) -> untyped
|
|
27
|
+
def read_object: (Call call, Internal::Transport::Response response) -> Hash[String, untyped]
|
|
28
|
+
def validation_error: (Call call, Internal::Transport::Response response, String message) -> APIResponseValidationError
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def headers_for: (Call call) -> Hash[String, String]
|
|
33
|
+
def body_for: (Call call) -> String?
|
|
34
|
+
def text_of: (String body) -> String
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Didit
|
|
2
|
+
class Authentication
|
|
3
|
+
module Issuer
|
|
4
|
+
LOOPBACK_HOSTS: Array[String]
|
|
5
|
+
PREFIXES: Hash[String, bool]
|
|
6
|
+
UNSAFE: Regexp
|
|
7
|
+
|
|
8
|
+
def self?.normalize: (untyped issuer, ?allow_loopback_http: bool) -> String
|
|
9
|
+
def self?.from_publishable_key: (untyped publishable_key) -> String
|
|
10
|
+
def self?.loopback?: (String host) -> bool
|
|
11
|
+
def self?.decode_issuer: (String prefix, String encoded) -> String
|
|
12
|
+
def self?.unpad: (String text) -> String
|
|
13
|
+
def self?.parse: (String text) -> URI::HTTP
|
|
14
|
+
def self?.invalid: (String text) -> bot
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Didit
|
|
2
|
+
class Authentication
|
|
3
|
+
module Jws
|
|
4
|
+
type kind = :access | :id
|
|
5
|
+
|
|
6
|
+
ALGORITHM: String
|
|
7
|
+
MAX_TOKEN_LENGTH: Integer
|
|
8
|
+
ACCESS_TYPES: Array[String]
|
|
9
|
+
ID_TYPES: Array[String]
|
|
10
|
+
ZERO: String
|
|
11
|
+
JSON_OPTIONS: Hash[Symbol, untyped]
|
|
12
|
+
DECODE_OPTIONS: Hash[Symbol, untyped]
|
|
13
|
+
|
|
14
|
+
class Parsed
|
|
15
|
+
attr_reader token: String
|
|
16
|
+
attr_reader header: Hash[String, untyped]
|
|
17
|
+
attr_reader claims: Hash[String, untyped]
|
|
18
|
+
attr_reader signature: String
|
|
19
|
+
attr_reader kid: String
|
|
20
|
+
|
|
21
|
+
def initialize: (token: String, header: Hash[String, untyped], claims: Hash[String, untyped], signature: String,
|
|
22
|
+
kid: String) -> void
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self?.parse: (untyped token, Symbol kind) -> Parsed
|
|
26
|
+
def self?.verify_signature: (Parsed parsed, OpenSSL::PKey::EC key) -> nil
|
|
27
|
+
def self?.check_claims: (Hash[String, untyped] claims, kind: Symbol, issuer: String, audience: Array[String]?,
|
|
28
|
+
nonce: String?, leeway: Float, now: Float) -> nil
|
|
29
|
+
def self?.shown: (untyped value) -> String
|
|
30
|
+
def self?.fail!: (Symbol reason, String message) -> bot
|
|
31
|
+
def self?.json_object: (String segment, String part) -> Hash[String, untyped]
|
|
32
|
+
def self?.check_header: (Hash[String, untyped] header, Symbol kind) -> void
|
|
33
|
+
def self?.check_type: (untyped typ, Symbol kind) -> void
|
|
34
|
+
def self?.check_issuer: (Hash[String, untyped] claims, String issuer) -> void
|
|
35
|
+
def self?.check_times: (Hash[String, untyped] claims, Float leeway, Float now) -> void
|
|
36
|
+
def self?.numeric_date: (Hash[String, untyped] claims, String name) -> Float
|
|
37
|
+
def self?.check_audience: (Hash[String, untyped] claims, Array[String] audience) -> void
|
|
38
|
+
def self?.check_nonce: (Hash[String, untyped] claims, String? nonce) -> void
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# The parts of the jwt gem (>= 2.7, < 4) that Didit::Authentication uses: the gem
|
|
2
|
+
# ships no signatures. Only Didit::Authentication::Jws calls it.
|
|
3
|
+
module JWT
|
|
4
|
+
def self.decode: (String jwt, ?untyped key, ?bool verify, ?Hash[Symbol, untyped] options) -> Array[untyped]
|
|
5
|
+
|
|
6
|
+
class DecodeError < StandardError
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class VerificationError < DecodeError
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class IncorrectAlgorithm < DecodeError
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Didit
|
|
2
|
+
class Authentication
|
|
3
|
+
class KeySet
|
|
4
|
+
SPKI_PREFIX: String
|
|
5
|
+
UNCOMPRESSED: String
|
|
6
|
+
|
|
7
|
+
def self.parse: (untyped data) -> KeySet?
|
|
8
|
+
def self.p256_key: (Hash[String, untyped] jwk) -> OpenSSL::PKey::EC?
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def self.coordinate: (untyped value) -> String?
|
|
13
|
+
|
|
14
|
+
public
|
|
15
|
+
|
|
16
|
+
@keys: Hash[String, OpenSSL::PKey::EC]
|
|
17
|
+
|
|
18
|
+
attr_reader document: Hash[String, untyped]
|
|
19
|
+
|
|
20
|
+
def initialize: (Hash[String, OpenSSL::PKey::EC] keys, Hash[String, untyped] document) -> void
|
|
21
|
+
def []: (String kid) -> OpenSSL::PKey::EC?
|
|
22
|
+
def key?: (String kid) -> bool
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Didit
|
|
2
|
+
class Authentication
|
|
3
|
+
class Pkce
|
|
4
|
+
METHOD: String
|
|
5
|
+
VERIFIER: Regexp
|
|
6
|
+
|
|
7
|
+
attr_reader code_verifier: String
|
|
8
|
+
attr_reader code_challenge: String
|
|
9
|
+
attr_reader code_challenge_method: String
|
|
10
|
+
|
|
11
|
+
def self.generate: () -> Pkce
|
|
12
|
+
def self.s256_challenge: (String verifier) -> String
|
|
13
|
+
|
|
14
|
+
def initialize: (untyped code_verifier) -> void
|
|
15
|
+
def to_h: () -> { code_verifier: String, code_challenge: String, code_challenge_method: String }
|
|
16
|
+
def inspect: () -> String
|
|
17
|
+
def to_s: () -> String
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|