ibanity 2.3 → 2.3.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|