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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7675d871fb8de41624e5f92fb6b0252fac90900f2f52c6bd9b182ea0f80cde44
4
- data.tar.gz: 2345bf7cfc1619ef8c5e1bb20ed56f966ec9a71263798bde713aca0ba92b0811
3
+ metadata.gz: 751c6c37cee7adedf204491894146d577e70b310d9cd50504c01c29836743271
4
+ data.tar.gz: d2b7de41a966d0e114dd368320f35578714a674da1d76121d500944f0c33596b
5
5
  SHA512:
6
- metadata.gz: df411a9c02a97ab165f5a47063ec0ca3fae67fe437f96af20b1f71eab4f88105bd8259a3c9685056c91210c3dca0134721f5c3b94e9cc39691756f6b64c17279
7
- data.tar.gz: a39b865f5c4a1a3226cc8071f3c5971ff8c32c536d4b7b88ddab8cf7ffcc71fa2e36c3d3409b5fa7f3d25647f8ea8fb202ef6f74ac51661e85c2b089f6a4e2c4
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)`
@@ -29,7 +29,7 @@ module JWT
29
29
  ).segments
30
30
  end
31
31
 
32
- def truelayer_decode(jwt, key, verify, options, &keyfinder)
32
+ def truelayer_decode(jwt, key, verify = true, options, &keyfinder)
33
33
  TrueLayerDecode.new(
34
34
  jwt,
35
35
  key,
@@ -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(s) declared in signature") unless value
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
- EXPECTED_COORDS_LENGTH = 66.freeze
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 = { algorithm: TrueLayerSigning.algorithm }
89
- JWT.truelayer_decode(full_signature, public_key, true, jwt_options)
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
- length = coords.length
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(s) declared in signature", error.message)
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"
@@ -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.0"
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.0
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-06-13 00:00:00.000000000 Z
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"