truelayer-signing 0.2.0 → 0.2.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 +5 -0
- data/lib/truelayer-signing/jwt.rb +1 -1
- data/lib/truelayer-signing/signer.rb +3 -0
- data/lib/truelayer-signing/utils.rb +8 -1
- data/lib/truelayer-signing/verifier.rb +13 -5
- data/test/resources/failed-payment-expired-test-payload.json +12 -0
- data/test/test-truelayer-signing.rb +23 -1
- data/truelayer-signing.gemspec +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 751c6c37cee7adedf204491894146d577e70b310d9cd50504c01c29836743271
|
|
4
|
+
data.tar.gz: d2b7de41a966d0e114dd368320f35578714a674da1d76121d500944f0c33596b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 88a7bc727e82df06eccb1992036730e49f6de45a6d38d6df7d2cf8d749441a8d1f5be031e59410e45778da20d84fac03bb07f7bf11103387893baebd4d86af2b
|
|
7
|
+
data.tar.gz: 249a44e6a94171f89f69f1e53aff377bee6ac6de33a3f2383a6c7f4ed94b517e216cd81da450caf1f363f21e1a8f60edcb6dfc71481af0c7fc70336d028ea0de
|
data/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
- ...
|
|
11
11
|
|
|
12
|
+
## [0.2.1] – 2023-07-14
|
|
13
|
+
|
|
14
|
+
- Disable expiration verification
|
|
15
|
+
- Add missing header discovery
|
|
16
|
+
|
|
12
17
|
## [0.2.0] – 2023-06-13
|
|
13
18
|
|
|
14
19
|
- Add support for signature verification using a JWKS: `TrueLayerSigning.verify_with_jwks(jwks)`
|
|
@@ -7,7 +7,9 @@ module TrueLayerSigning
|
|
|
7
7
|
|
|
8
8
|
private_key = OpenSSL::PKey.read(TrueLayerSigning.private_key)
|
|
9
9
|
jws_header_args = { tl_headers: headers }
|
|
10
|
+
|
|
10
11
|
jws_header_args[:jku] = jws_jku if jws_jku
|
|
12
|
+
|
|
11
13
|
jws_header = TrueLayerSigning::JwsHeader.new(jws_header_args).to_h
|
|
12
14
|
jwt = JWT.truelayer_encode(build_signing_payload, private_key, TrueLayerSigning.algorithm,
|
|
13
15
|
jws_header)
|
|
@@ -18,6 +20,7 @@ module TrueLayerSigning
|
|
|
18
20
|
|
|
19
21
|
def set_jku(jku)
|
|
20
22
|
@jws_jku = jku
|
|
23
|
+
|
|
21
24
|
self
|
|
22
25
|
end
|
|
23
26
|
|
|
@@ -16,18 +16,20 @@ module TrueLayerSigning
|
|
|
16
16
|
|
|
17
17
|
def to_h
|
|
18
18
|
hash = instance_variables.map { |var| [var[1..-1].to_sym, instance_variable_get(var)] }.to_h
|
|
19
|
+
|
|
19
20
|
hash.reject { |key, _value| hash[key].nil? }
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def filter_headers(headers)
|
|
23
24
|
required_header_keys = tl_headers.split(",").reject { |key| key.empty? }
|
|
24
25
|
normalised_headers = {}
|
|
26
|
+
|
|
25
27
|
headers.to_a.each { |header| normalised_headers[header.first.downcase] = header.last }
|
|
26
28
|
|
|
27
29
|
ordered_headers = required_header_keys.map do |key|
|
|
28
30
|
value = normalised_headers[key.downcase]
|
|
29
31
|
|
|
30
|
-
raise(Error, "Missing header
|
|
32
|
+
raise(Error, "Missing header declared in signature: #{key.downcase}") unless value
|
|
31
33
|
|
|
32
34
|
[key, value]
|
|
33
35
|
end
|
|
@@ -50,6 +52,7 @@ module TrueLayerSigning
|
|
|
50
52
|
|
|
51
53
|
def set_method(method)
|
|
52
54
|
@method = method.to_s.upcase
|
|
55
|
+
|
|
53
56
|
self
|
|
54
57
|
end
|
|
55
58
|
|
|
@@ -57,21 +60,25 @@ module TrueLayerSigning
|
|
|
57
60
|
raise(Error, "Path must start with '/'") unless path.start_with?("/")
|
|
58
61
|
|
|
59
62
|
@path = path
|
|
63
|
+
|
|
60
64
|
self
|
|
61
65
|
end
|
|
62
66
|
|
|
63
67
|
def add_header(name, value)
|
|
64
68
|
@headers[name.to_s] = value
|
|
69
|
+
|
|
65
70
|
self
|
|
66
71
|
end
|
|
67
72
|
|
|
68
73
|
def set_headers(headers)
|
|
69
74
|
headers.each { |name, value| @headers[name.to_s] = value }
|
|
75
|
+
|
|
70
76
|
self
|
|
71
77
|
end
|
|
72
78
|
|
|
73
79
|
def set_body(body)
|
|
74
80
|
@body = body
|
|
81
|
+
|
|
75
82
|
self
|
|
76
83
|
end
|
|
77
84
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
module TrueLayerSigning
|
|
2
2
|
class Verifier < JwsBase
|
|
3
|
-
|
|
3
|
+
EXPECTED_EC_KEY_COORDS_LENGTH = 66.freeze
|
|
4
4
|
|
|
5
5
|
attr_reader :required_headers, :key_type, :key_value
|
|
6
6
|
|
|
7
7
|
def initialize(args)
|
|
8
8
|
super
|
|
9
|
+
|
|
9
10
|
@key_type = args[:key_type]
|
|
10
11
|
@key_value = args[:key_value]
|
|
11
12
|
end
|
|
@@ -19,6 +20,7 @@ module TrueLayerSigning
|
|
|
19
20
|
|
|
20
21
|
ordered_headers = jws_header.filter_headers(headers)
|
|
21
22
|
normalised_headers = {}
|
|
23
|
+
|
|
22
24
|
ordered_headers.to_a.each { |header| normalised_headers[header.first.downcase] = header.last }
|
|
23
25
|
|
|
24
26
|
raise(Error, "Signature missing required header(s)") if required_headers &&
|
|
@@ -30,11 +32,13 @@ module TrueLayerSigning
|
|
|
30
32
|
def require_header(name)
|
|
31
33
|
@required_headers ||= []
|
|
32
34
|
@required_headers.push(name)
|
|
35
|
+
|
|
33
36
|
self
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
def require_headers(names)
|
|
37
40
|
@required_headers = names
|
|
41
|
+
|
|
38
42
|
self
|
|
39
43
|
end
|
|
40
44
|
|
|
@@ -85,8 +89,13 @@ module TrueLayerSigning
|
|
|
85
89
|
public_key = retrieve_public_key(:jwks, key_value, jws_header)
|
|
86
90
|
end
|
|
87
91
|
|
|
88
|
-
jwt_options = {
|
|
89
|
-
|
|
92
|
+
jwt_options = {
|
|
93
|
+
algorithm: TrueLayerSigning.algorithm,
|
|
94
|
+
verify_expiration: false,
|
|
95
|
+
verify_not_before: false
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
JWT.truelayer_decode(full_signature, public_key, jwt_options)
|
|
90
99
|
end
|
|
91
100
|
|
|
92
101
|
private def retrieve_public_key(key_type, key_value, jws_header)
|
|
@@ -112,8 +121,7 @@ module TrueLayerSigning
|
|
|
112
121
|
|
|
113
122
|
%i(x y).each do |elem|
|
|
114
123
|
coords = Base64.urlsafe_decode64(valid_jwk[elem])
|
|
115
|
-
|
|
116
|
-
diff = EXPECTED_COORDS_LENGTH - length
|
|
124
|
+
diff = EXPECTED_EC_KEY_COORDS_LENGTH - coords.length
|
|
117
125
|
|
|
118
126
|
valid_jwk[elem] = Base64.urlsafe_encode64(("\x00" * diff) + coords) if diff > 0
|
|
119
127
|
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "payment_failed",
|
|
3
|
+
"event_version": 1,
|
|
4
|
+
"event_id": "6f00897f-f8c8-4ffb-93a3-cfbd311396e2",
|
|
5
|
+
"payment_id": "2c4db314-b509-4d68-b883-a34ca5fa7b72",
|
|
6
|
+
"payment_method": {
|
|
7
|
+
"type": "bank_transfer"
|
|
8
|
+
},
|
|
9
|
+
"failed_at": "2023-07-11T17:42:24.123Z",
|
|
10
|
+
"failure_stage": "authorization_required",
|
|
11
|
+
"failure_reason": "expired"
|
|
12
|
+
}
|
|
@@ -314,7 +314,7 @@ class TrueLayerSigningTest < Minitest::Test
|
|
|
314
314
|
.set_body(body)
|
|
315
315
|
|
|
316
316
|
error = assert_raises(TrueLayerSigning::Error) { verifier.verify(tl_signature) }
|
|
317
|
-
assert_equal("Missing header
|
|
317
|
+
assert_equal("Missing header declared in signature: idempotency-key", error.message)
|
|
318
318
|
end
|
|
319
319
|
|
|
320
320
|
def test_full_request_signature_missing_required_header_should_fail
|
|
@@ -455,6 +455,28 @@ class TrueLayerSigningTest < Minitest::Test
|
|
|
455
455
|
assert_equal("Signature verification failed", error.message)
|
|
456
456
|
end
|
|
457
457
|
|
|
458
|
+
# This test reproduces an issue we had with an edge case
|
|
459
|
+
def test_verify_with_failed_payment_expired_webhook_should_succeed
|
|
460
|
+
path = "/tl-webhook"
|
|
461
|
+
payload = read_file("resources/failed-payment-expired-test-payload.json")
|
|
462
|
+
body = JSON.parse(payload).to_json
|
|
463
|
+
|
|
464
|
+
tl_signature = TrueLayerSigning.sign_with_pem
|
|
465
|
+
.set_method(:post)
|
|
466
|
+
.set_path(path)
|
|
467
|
+
.set_body(body)
|
|
468
|
+
.sign
|
|
469
|
+
|
|
470
|
+
result = TrueLayerSigning.verify_with_pem(PUBLIC_KEY)
|
|
471
|
+
.set_method(:post)
|
|
472
|
+
.set_path(path)
|
|
473
|
+
.set_body(body)
|
|
474
|
+
.verify(tl_signature)
|
|
475
|
+
|
|
476
|
+
assert(result.first.start_with?("POST /tl-webhook\n"))
|
|
477
|
+
assert(result.first.include?("\"failure_reason\":\"expired\""))
|
|
478
|
+
end
|
|
479
|
+
|
|
458
480
|
def test_sign_with_pem_and_custom_jku_should_succeed
|
|
459
481
|
body = { currency: "GBP", max_amount_in_minor: 50_000_00 }.to_json
|
|
460
482
|
idempotency_key = "idemp-2076717c-9005-4811-a321-9e0787fa0382"
|
data/truelayer-signing.gemspec
CHANGED
|
@@ -2,7 +2,7 @@ $LOAD_PATH.unshift(::File.join(::File.dirname(__FILE__), "lib"))
|
|
|
2
2
|
|
|
3
3
|
Gem::Specification.new do |s|
|
|
4
4
|
s.name = "truelayer-signing"
|
|
5
|
-
s.version = "0.2.
|
|
5
|
+
s.version = "0.2.1"
|
|
6
6
|
s.summary = "Ruby gem to produce and verify TrueLayer API requests signatures"
|
|
7
7
|
s.description = "TrueLayer provides instant access to open banking to " \
|
|
8
8
|
"easily integrate next-generation payments and financial data into any app." \
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: truelayer-signing
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kevin Plattret
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-
|
|
11
|
+
date: 2023-07-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: jwt
|
|
@@ -51,6 +51,7 @@ files:
|
|
|
51
51
|
- "./lib/truelayer-signing/signer.rb"
|
|
52
52
|
- "./lib/truelayer-signing/utils.rb"
|
|
53
53
|
- "./lib/truelayer-signing/verifier.rb"
|
|
54
|
+
- "./test/resources/failed-payment-expired-test-payload.json"
|
|
54
55
|
- "./test/resources/missing-zero-padding-test-jwks.json"
|
|
55
56
|
- "./test/resources/missing-zero-padding-test-payload.json"
|
|
56
57
|
- "./test/resources/missing-zero-padding-test-signature.txt"
|