ibanity 2.3 → 2.3.1
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 +4 -0
- data/ibanity.gemspec +1 -1
- data/lib/ibanity/version.rb +1 -1
- data/lib/ibanity/webhook.rb +28 -38
- data/lib/ibanity.rb +1 -1
- data/spec/lib/ibanity/webhook_signature_spec.rb +75 -0
- data/spec/support/fixture.rb +10 -0
- data/spec/support/fixtures/json/webhooks/example_payload.json +1 -0
- data/spec/support/fixtures/signature/webhooks/example_signature.sig +1 -0
- metadata +11 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 319589bf55ec8cd48ca771266da34bc5374f8ebfa9ec4894c6b9b6ef7c8ca1bf
|
4
|
+
data.tar.gz: 5f7c0cc44590541555d51dfc96e1f1e70eabd54b19dbb14b66ffdb40f967a87d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 31ec94ee483bc7926d7a66dbb59a34d8bfcdec5e27daedcd7599a53b54c80611ff4d829a1f634b6d6c3c033592e321ab1faf9638c2e52fa00a7664aeac996250
|
7
|
+
data.tar.gz: 6f8bd1c3ff75e41c7e32178e9f060896de45245ec6fb8261cd406432eae62653071b4babbf5dd93d976978d63d3dab70e51f4bd3ade1afbcfbd22c753a567179
|
data/CHANGELOG.md
CHANGED
data/ibanity.gemspec
CHANGED
data/lib/ibanity/version.rb
CHANGED
data/lib/ibanity/webhook.rb
CHANGED
@@ -26,66 +26,56 @@ module Ibanity
|
|
26
26
|
#
|
27
27
|
# Returns true otherwise
|
28
28
|
def self.verify!(payload, signature_header, tolerance)
|
29
|
-
|
30
|
-
|
31
|
-
raise unless key_details["alg"] == SIGNING_ALGORITHM && key_details["kid"]
|
32
|
-
rescue
|
33
|
-
raise Ibanity::Error.new("Key details could not be parsed from the header", nil)
|
34
|
-
end
|
35
|
-
|
36
|
-
key = Ibanity.webhook_keys.find { |key| key.kid == key_details["kid"] }
|
29
|
+
jwks_loader = ->(options) do
|
30
|
+
raise Ibanity::Error.new("The key id from the header didn't match an available signing key", nil) if options[:kid_not_found]
|
37
31
|
|
38
|
-
|
39
|
-
|
32
|
+
keys = Ibanity.webhook_keys.select { |key| key.use == "sig" }
|
33
|
+
.map { |key| JWT::JWK.new(key.to_h {|key, value| [key.to_s, value] }) }
|
34
|
+
JWT::JWK::Set.new(keys)
|
40
35
|
end
|
41
|
-
signer = JOSE::JWK.from(key.to_h {|key, value| [key.to_s, value] })
|
42
|
-
verified, claims_string, _jws = JOSE::JWT.verify_strict(signer, [SIGNING_ALGORITHM], signature_header)
|
43
|
-
|
44
|
-
raise Ibanity::Error.new("The signature verification failed", nil) unless verified
|
45
36
|
|
46
|
-
|
37
|
+
options = {
|
38
|
+
aud: Ibanity.client.application_id,
|
39
|
+
algorithm: SIGNING_ALGORITHM,
|
40
|
+
exp_leeway: tolerance,
|
41
|
+
iss: Ibanity.client.base_uri,
|
42
|
+
jwks: jwks_loader,
|
43
|
+
verify_aud: true,
|
44
|
+
verify_iss: true
|
45
|
+
}
|
46
|
+
jwts = JWT.decode(signature_header, nil, true, options)
|
47
|
+
jwt = jwts.first
|
47
48
|
|
48
49
|
validate_digest!(jwt, payload)
|
49
50
|
validate_issued_at!(jwt, tolerance)
|
50
|
-
validate_expiration!(jwt, tolerance)
|
51
|
-
validate_issuer!(jwt)
|
52
|
-
validate_audience!(jwt)
|
53
51
|
|
54
52
|
true
|
53
|
+
rescue JWT::ExpiredSignature
|
54
|
+
raise_invalid_signature_error!("exp")
|
55
|
+
rescue JWT::IncorrectAlgorithm
|
56
|
+
raise Ibanity::Error.new("Incorrect algorithm for signature", nil)
|
57
|
+
rescue JWT::InvalidAudError
|
58
|
+
raise_invalid_signature_error!("aud")
|
59
|
+
rescue JWT::InvalidIssuerError
|
60
|
+
raise_invalid_signature_error!("iss")
|
61
|
+
rescue JWT::DecodeError
|
62
|
+
raise Ibanity::Error.new("The signature verification failed", nil)
|
55
63
|
end
|
56
64
|
|
57
65
|
private
|
58
66
|
|
59
67
|
def self.validate_digest!(jwt, payload)
|
60
|
-
unless Digest::SHA512.base64digest(payload) == jwt
|
68
|
+
unless Digest::SHA512.base64digest(payload) == jwt["digest"]
|
61
69
|
raise_invalid_signature_error!("digest")
|
62
70
|
end
|
63
71
|
end
|
64
72
|
|
65
73
|
def self.validate_issued_at!(jwt, tolerance)
|
66
|
-
unless jwt
|
74
|
+
unless jwt["iat"] <= Time.now.to_i + tolerance
|
67
75
|
raise_invalid_signature_error!("iat")
|
68
76
|
end
|
69
77
|
end
|
70
78
|
|
71
|
-
def self.validate_expiration!(jwt, tolerance)
|
72
|
-
unless jwt.fields["exp"] >= Time.now.to_i - tolerance
|
73
|
-
raise_invalid_signature_error!("exp")
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
def self.validate_issuer!(jwt)
|
78
|
-
unless jwt.fields["iss"] == Ibanity.client.base_uri
|
79
|
-
raise_invalid_signature_error!("iss")
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
def self.validate_audience!(jwt)
|
84
|
-
unless jwt.fields["aud"] == Ibanity.client.application_id
|
85
|
-
raise_invalid_signature_error!("aud")
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
79
|
def self.raise_invalid_signature_error!(field)
|
90
80
|
raise Ibanity::Error.new("The signature #{field} is invalid", nil)
|
91
81
|
end
|
data/lib/ibanity.rb
CHANGED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'ibanity'
|
2
|
+
|
3
|
+
RSpec.describe Ibanity::Webhook::Signature do
|
4
|
+
subject(:signature_module) { described_class }
|
5
|
+
|
6
|
+
let(:client) { instance_double("Ibanity::Client") }
|
7
|
+
|
8
|
+
before do
|
9
|
+
allow(Ibanity).to receive(:client).and_return(client)
|
10
|
+
allow(Ibanity).to receive(:webhook_keys).and_return(
|
11
|
+
[
|
12
|
+
Ibanity::Webhooks::Key.new(
|
13
|
+
alg: "RS512",
|
14
|
+
e: "AQAB",
|
15
|
+
kid: "sandbox_events_signature_1",
|
16
|
+
kty: "RSA",
|
17
|
+
n: "v9qVZmotpil47Pw2NOmP11bpE5B_GtG6ICfqtm13Uusa4asf4FWedclr-kQTV2Ly5rSItq2f3RGRNyove_4TiTIbx21rwM5HP0iFhlVaqHjkr1iSmKzCFojOnTM4UwKQNROhDVDC6TWIzSafZkBacUrCX5l0PLSh2aEK8aiopu5ajYpOr8Ipjw_mbKXxBfcxtjgskbXPyEcf6xlB_Dygl9-btAvRTKiuie4qAWANTdVAgSnddjZMJxFnndZMCH1h-z4ISwphBYbwG2aZrZ7RfHnoIROxsdmKeostYtHy3gMR4_poufzFRR8lpvODd3m7lzdXKBTCvzlQYBNpmf6gmG9p08laE-h67F1GoqvuqspcvRlVpGZEzEwRIbPMAaS4_omCSj4HFZyo58PLUsAp--AD8GGFfVMyBdFhTEkr2235O5AP4UMdHuvyP-NPFCsqibqKK1GIl_Hy0UXnqg7-MCGqs4jX1k4IZZ3wDwza30f1O6tUtaOT8YXzZ2ZWnVWyMLcNx6gep8t3A7gTzEXcselrJgO6SLFRhYA0QmtIRtTwnl-8OmjEi5AJVzO0e-yiRj7g_JLEmgG3pDwmvbiXzEqkY5mPqJMB9G5qcd0SWgvvZs02_1tRRhvw0D5BTKfcEcLW9PKm8Nts1_BGSXKOhTSeQgxuw4iC63ST3dtpl-0=",
|
18
|
+
status: "ACTIVE",
|
19
|
+
use: "sig"
|
20
|
+
)
|
21
|
+
]
|
22
|
+
)
|
23
|
+
allow(client).to receive(:application_id).and_return("e643b5be-ccb1-4a38-b7b6-36689853bef5")
|
24
|
+
allow(client).to receive(:base_uri).and_return("https://api.ibanity.com")
|
25
|
+
end
|
26
|
+
|
27
|
+
describe ".verify!" do
|
28
|
+
let(:payload) { Fixture.load_unparsed_json("webhooks/example_payload.json") }
|
29
|
+
let(:signature) { Fixture.load_signature("webhooks/example_signature.sig") }
|
30
|
+
let(:tolerance) { Time.now.to_i + 10 - 1691670985 }
|
31
|
+
|
32
|
+
it "returns true for a correct signature" do
|
33
|
+
expect(signature_module.verify!(payload, signature, tolerance)).to be_truthy
|
34
|
+
end
|
35
|
+
|
36
|
+
it "raises an error if the contents are altered" do
|
37
|
+
expect do
|
38
|
+
signature_module.verify!(payload + "altered", signature, tolerance)
|
39
|
+
end.to raise_error(Ibanity::Error, /The signature digest is invalid/)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "raises an error if the signature is expired" do
|
43
|
+
expect do
|
44
|
+
signature_module.verify!(payload, signature, 0)
|
45
|
+
end.to raise_error(Ibanity::Error, /The signature exp is invalid/)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "raises an error if the audience is invalid" do
|
49
|
+
allow(client).to receive(:application_id).and_return("invalid")
|
50
|
+
expect do
|
51
|
+
signature_module.verify!(payload, signature, tolerance)
|
52
|
+
end.to raise_error(Ibanity::Error, /The signature aud is invalid/)
|
53
|
+
end
|
54
|
+
|
55
|
+
it "raises an error if the issuer is invalid" do
|
56
|
+
allow(client).to receive(:base_uri).and_return("invalid")
|
57
|
+
expect do
|
58
|
+
signature_module.verify!(payload, signature, tolerance)
|
59
|
+
end.to raise_error(Ibanity::Error, /The signature iss is invalid/)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "raises an error if the signing key is unavailable" do
|
63
|
+
allow(Ibanity).to receive(:webhook_keys).and_return([])
|
64
|
+
expect do
|
65
|
+
signature_module.verify!(payload, signature, tolerance)
|
66
|
+
end.to raise_error(Ibanity::Error, /The key id from the header didn't match an available signing key/)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "raises an error if the signature is altered" do
|
70
|
+
expect do
|
71
|
+
signature_module.verify!(payload, signature + "altered", tolerance)
|
72
|
+
end.to raise_error(Ibanity::Error, /The signature verification failed/)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/spec/support/fixture.rb
CHANGED
@@ -5,4 +5,14 @@ module Fixture
|
|
5
5
|
path = Pathname([File.dirname(__FILE__ ), "fixtures", "json", filename].join("/"))
|
6
6
|
JSON.parse(File.read(path))
|
7
7
|
end
|
8
|
+
|
9
|
+
def self.load_unparsed_json(filename)
|
10
|
+
path = Pathname([File.dirname(__FILE__ ), "fixtures", "json", filename].join("/"))
|
11
|
+
File.read(path)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.load_signature(filename)
|
15
|
+
path = Pathname([File.dirname(__FILE__ ), "fixtures", "signature", filename].join("/"))
|
16
|
+
File.read(path)
|
17
|
+
end
|
8
18
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
{"data":{"attributes":{"createdAt":"2023-08-10T12:24:42.224Z","synchronizationSubtype":"accountTransactions"},"id":"3465fd2f-6c9a-484e-971e-56bf44c9181a","relationships":{"account":{"data":{"id":"1d89d411-e310-4802-bbb7-aad7d98371e7","type":"account"}},"organization":{"data":{"id":"9315d6b3-129a-481a-9604-e2a95e0f967d","type":"organization"}},"synchronization":{"data":{"id":"178b59a6-679f-43d4-b5ad-28caea0bc08b","type":"synchronization"}}},"type":"pontoConnect.synchronization.succeededWithoutChange"}}
|
@@ -0,0 +1 @@
|
|
1
|
+
eyJhbGciOiJSUzUxMiIsImtpZCI6InNhbmRib3hfZXZlbnRzX3NpZ25hdHVyZV8xIn0.eyJhdWQiOiJlNjQzYjViZS1jY2IxLTRhMzgtYjdiNi0zNjY4OTg1M2JlZjUiLCJkaWdlc3QiOiJDZEJPWGwzMGsyM0lsVVdkM2JPL1FtazhXek5lN1NERjVCV3FUQnIyVzQ0VjlqRnhjZW4wSEFHVGsra0Nka0NjWVZsRW9PYmtZM0JCc2lPRnNFV0hIUT09IiwiZXhwIjoxNjkxNjcwOTg1LCJpYXQiOjE2OTE2NzA5MjUsImlzcyI6Imh0dHBzOi8vYXBpLmliYW5pdHkuY29tIiwianRpIjoiMWMyOTNkNTYtOTk1Yi00NDg5LTlhODMtMDEzZTg2ZTk4NjQ2In0.ooEZQDTfGmfYr5DmD7wVxdIN_0YXDBiwwL1_fFwSlTiIcr8vehAbDtcsyf3XrycySkUbWvtZYbhYXJw1BH4eDKdhCd2MnrxNX8BzxEjbAKzsGTT4l1YnrbBEIfcL_Z3qCzxJOzWErkSAgIi1XbiNFbw1YTU0c_Y0bBwxY6EEZgQVpG0-OXbzxhT7GRnsdSmYCAEkbLUzhRDj37fOz0oTfuimn04ANqVNJbTrOnQ2md_lOIS1XWl_WaDrk8pExUQ5JQii7dIxnzaHLB5J6U8zAOUchNLYJW1tcJmpVE_3baO_W6MvsWfEG9wY5Jud55WsnSHOOA4ak0KQJx3owqXt3uzlKH0MjPe6UeuJagZo1nXPW8IV6QCyvqtJ6TMwXqN67GHzh2YTS-Ct7qFxgHKFutlSfBfV4PaTTRd0ywqgTIVXY9P_hQmTSL_J0WiTg_2Di0ty10ehCau9rF1Bmpf2ZnSMK2Rr4JRPIMnFQsU-W5jZbZNF_tDaECzzkDoZgMDRrQT1TsdfSWU-XWG2f0ytiHKVcFJE9mMd25b2SJ6rx2e-TZaQwb5jO61uL1Q8zjvPWqofWZ5emiby9FWNFS-0fqF0UUJdlb7XU1fo_cQi0q4pYGgN2_2sgNETXr99zcTT2vpCf9x9RZGUzXne_WhZP_gp49m0kTX1W0s8O_2g2Do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ibanity
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ibanity
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-08-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rest-client
|
@@ -25,19 +25,19 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 1.8.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: jwt
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 2.0.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 2.0.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -136,6 +136,7 @@ files:
|
|
136
136
|
- spec/lib/ibanity/base_resource_spec.rb
|
137
137
|
- spec/lib/ibanity/http_signature_spec.rb
|
138
138
|
- spec/lib/ibanity/util_spec.rb
|
139
|
+
- spec/lib/ibanity/webhook_signature_spec.rb
|
139
140
|
- spec/spec_helper.rb
|
140
141
|
- spec/support/crypto_helper.rb
|
141
142
|
- spec/support/fixture.rb
|
@@ -143,9 +144,11 @@ files:
|
|
143
144
|
- spec/support/fixtures/json/relationships/data_without_type.json
|
144
145
|
- spec/support/fixtures/json/relationships/meta_with_type.json
|
145
146
|
- spec/support/fixtures/json/relationships/no_links_related.json
|
147
|
+
- spec/support/fixtures/json/webhooks/example_payload.json
|
146
148
|
- spec/support/fixtures/signature/test-certificate.pem
|
147
149
|
- spec/support/fixtures/signature/test-private_key.pem
|
148
150
|
- spec/support/fixtures/signature/test-public_key.pem
|
151
|
+
- spec/support/fixtures/signature/webhooks/example_signature.sig
|
149
152
|
homepage: https://documentation.ibanity.com/api/ruby
|
150
153
|
licenses:
|
151
154
|
- MIT
|
@@ -173,6 +176,7 @@ test_files:
|
|
173
176
|
- spec/lib/ibanity/base_resource_spec.rb
|
174
177
|
- spec/lib/ibanity/http_signature_spec.rb
|
175
178
|
- spec/lib/ibanity/util_spec.rb
|
179
|
+
- spec/lib/ibanity/webhook_signature_spec.rb
|
176
180
|
- spec/spec_helper.rb
|
177
181
|
- spec/support/crypto_helper.rb
|
178
182
|
- spec/support/fixture.rb
|
@@ -180,6 +184,8 @@ test_files:
|
|
180
184
|
- spec/support/fixtures/json/relationships/data_without_type.json
|
181
185
|
- spec/support/fixtures/json/relationships/meta_with_type.json
|
182
186
|
- spec/support/fixtures/json/relationships/no_links_related.json
|
187
|
+
- spec/support/fixtures/json/webhooks/example_payload.json
|
183
188
|
- spec/support/fixtures/signature/test-certificate.pem
|
184
189
|
- spec/support/fixtures/signature/test-private_key.pem
|
185
190
|
- spec/support/fixtures/signature/test-public_key.pem
|
191
|
+
- spec/support/fixtures/signature/webhooks/example_signature.sig
|