truelayer-signing 0.2.1 → 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/Gemfile +5 -0
- data/Rakefile +2 -0
- data/lib/truelayer-signing/config.rb +6 -4
- data/lib/truelayer-signing/errors.rb +2 -0
- data/lib/truelayer-signing/jwt.rb +11 -3
- data/lib/truelayer-signing/signer.rb +21 -12
- data/lib/truelayer-signing/utils.rb +31 -15
- data/lib/truelayer-signing/verifier.rb +26 -15
- data/lib/truelayer-signing.rb +2 -0
- data/test/test-truelayer-signing.rb +29 -27
- data/truelayer-signing.gemspec +17 -6
- metadata +27 -33
- data/examples/sign-request/Gemfile +0 -4
- data/examples/sign-request/README.md +0 -27
- data/examples/sign-request/main.rb +0 -46
- data/examples/webhook-server/Gemfile +0 -5
- data/examples/webhook-server/Gemfile.lock +0 -42
- data/examples/webhook-server/README.md +0 -30
- data/examples/webhook-server/main.rb +0 -95
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2bd92901150104b3d2212252852071a8c3a1ed2e0a8f48ce72cb934686931bb9
|
4
|
+
data.tar.gz: 6ca20861d24c36d2380e070ad9d2cc47d55614cdafbf3f0ba77505b64984ec7c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b1b988b8a58ba51af8f6f4462e1de6871a0d79fa3d12bba9f7d12bb6ee6e14e22acc9c09268b11d622d2a6848f6ba070cc2704e6baa4b501dc89c59eb5870206
|
7
|
+
data.tar.gz: c4afccedb9a1f9b3676b1b69238ce0b9b4016fc883a947a44d9945f2c27944ba97373e4bd01c111cfc23ad5f4120c1075e12e4c25ed6714b1718576a6711dc62
|
data/CHANGELOG.md
CHANGED
@@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
9
9
|
|
10
10
|
- ...
|
11
11
|
|
12
|
+
## [0.2.3] – 2024-07-29
|
13
|
+
|
14
|
+
- Fix `Gemspec/AddRuntimeDependency` linter error
|
15
|
+
- Pin minor version for `jwt 2.7.0`
|
16
|
+
|
17
|
+
## [0.2.2] – 2023-12-07
|
18
|
+
|
19
|
+
- Add code linter
|
20
|
+
|
12
21
|
## [0.2.1] – 2023-07-14
|
13
22
|
|
14
23
|
- Disable expiration verification
|
data/Gemfile
ADDED
data/Rakefile
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module TrueLayerSigning
|
2
4
|
class Config
|
3
5
|
attr_accessor :certificate_id, :private_key
|
@@ -12,10 +14,10 @@ module TrueLayerSigning
|
|
12
14
|
|
13
15
|
# @return [TrueLayerSigning::Config]
|
14
16
|
def initialize
|
15
|
-
@algorithm = "ES512"
|
16
|
-
@certificate_id = ENV.fetch("TRUELAYER_SIGNING_CERTIFICATE_ID", nil)
|
17
|
-
@private_key = ENV.fetch("TRUELAYER_SIGNING_PRIVATE_KEY", nil)&.gsub(
|
18
|
-
@version = "2"
|
17
|
+
@algorithm = "ES512"
|
18
|
+
@certificate_id = ENV.fetch("TRUELAYER_SIGNING_CERTIFICATE_ID", nil)
|
19
|
+
@private_key = ENV.fetch("TRUELAYER_SIGNING_PRIVATE_KEY", nil)&.gsub("\\n", "\n")
|
20
|
+
@version = "2"
|
19
21
|
end
|
20
22
|
end
|
21
23
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# TODO: this is a custom patch of payload-related methods, from the 'jwt' gem.
|
2
4
|
# It prevents the payload from being systematically converted to and from JSON.
|
3
5
|
# To be changed in the 'jwt' gem directly, or hard-coded in this library.
|
@@ -5,18 +7,22 @@ module JWT
|
|
5
7
|
module_function
|
6
8
|
|
7
9
|
class TrueLayerEncode < Encode
|
10
|
+
private
|
11
|
+
|
8
12
|
# See https://github.com/jwt/ruby-jwt/blob/main/lib/jwt/encode.rb#L53-L55
|
9
|
-
|
13
|
+
def encode_payload
|
10
14
|
::JWT::Base64.url_encode(@payload)
|
11
15
|
end
|
12
16
|
end
|
13
17
|
|
14
18
|
class TrueLayerDecode < Decode
|
19
|
+
private
|
20
|
+
|
15
21
|
# See https://github.com/jwt/ruby-jwt/blob/main/lib/jwt/decode.rb#L154-L156
|
16
|
-
|
22
|
+
def payload
|
17
23
|
@payload ||= ::JWT::Base64.url_decode(@segments[1])
|
18
24
|
rescue ::JSON::ParserError
|
19
|
-
raise JWT::DecodeError,
|
25
|
+
raise JWT::DecodeError, "Invalid segment encoding"
|
20
26
|
end
|
21
27
|
end
|
22
28
|
|
@@ -29,6 +35,7 @@ module JWT
|
|
29
35
|
).segments
|
30
36
|
end
|
31
37
|
|
38
|
+
# rubocop:disable Style/OptionalArguments, Style/OptionalBooleanParameter
|
32
39
|
def truelayer_decode(jwt, key, verify = true, options, &keyfinder)
|
33
40
|
TrueLayerDecode.new(
|
34
41
|
jwt,
|
@@ -38,4 +45,5 @@ module JWT
|
|
38
45
|
&keyfinder
|
39
46
|
).decode_segments
|
40
47
|
end
|
48
|
+
# rubocop:enable Style/OptionalArguments, Style/OptionalBooleanParameter
|
41
49
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module TrueLayerSigning
|
2
4
|
class Signer < JwsBase
|
3
5
|
attr_reader :jws_jku
|
@@ -6,13 +8,13 @@ module TrueLayerSigning
|
|
6
8
|
ensure_signer_config!
|
7
9
|
|
8
10
|
private_key = OpenSSL::PKey.read(TrueLayerSigning.private_key)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
11
|
+
jws_header = generate_jws_header!
|
12
|
+
jwt = JWT.truelayer_encode(
|
13
|
+
build_signing_payload,
|
14
|
+
private_key,
|
15
|
+
TrueLayerSigning.algorithm,
|
16
|
+
jws_header
|
17
|
+
)
|
16
18
|
header, _, signature = jwt.split(".")
|
17
19
|
|
18
20
|
"#{header}..#{signature}"
|
@@ -24,13 +26,20 @@ module TrueLayerSigning
|
|
24
26
|
self
|
25
27
|
end
|
26
28
|
|
27
|
-
private
|
29
|
+
private
|
30
|
+
|
31
|
+
def generate_jws_header!
|
32
|
+
jws_header_args = { tl_headers: headers }
|
33
|
+
jws_header_args[:jku] = jws_jku if jws_jku
|
34
|
+
|
35
|
+
TrueLayerSigning::JwsHeader.new(jws_header_args).to_h
|
36
|
+
end
|
37
|
+
|
38
|
+
def ensure_signer_config!
|
28
39
|
raise(Error, "TRUELAYER_SIGNING_CERTIFICATE_ID missing") \
|
29
|
-
if TrueLayerSigning.certificate_id.nil? ||
|
30
|
-
TrueLayerSigning.certificate_id.empty?
|
40
|
+
if TrueLayerSigning.certificate_id.nil? || TrueLayerSigning.certificate_id.empty?
|
31
41
|
raise(Error, "TRUELAYER_SIGNING_PRIVATE_KEY missing") \
|
32
|
-
if TrueLayerSigning.private_key.nil? ||
|
33
|
-
TrueLayerSigning.private_key.empty?
|
42
|
+
if TrueLayerSigning.private_key.nil? || TrueLayerSigning.private_key.empty?
|
34
43
|
raise(Error, "Request path missing") unless path
|
35
44
|
raise(Error, "Request body missing") unless body
|
36
45
|
end
|
@@ -1,11 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module TrueLayerSigning
|
4
|
+
class Utils
|
5
|
+
def self.normalise_headers!(headers)
|
6
|
+
normalised_headers = {}
|
7
|
+
|
8
|
+
headers.to_a.each { |header| normalised_headers[header.first.downcase] = header.last }
|
9
|
+
|
10
|
+
normalised_headers
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
2
14
|
class JwsHeader
|
3
15
|
attr_reader :alg, :kid, :tl_version, :tl_headers, :jku
|
4
16
|
|
5
17
|
def initialize(args = {})
|
6
18
|
raise(Error, "TRUELAYER_SIGNING_CERTIFICATE_ID is missing") \
|
7
|
-
if TrueLayerSigning.certificate_id.nil? ||
|
8
|
-
TrueLayerSigning.certificate_id.empty?
|
19
|
+
if TrueLayerSigning.certificate_id.nil? || TrueLayerSigning.certificate_id.empty?
|
9
20
|
|
10
21
|
@alg = args[:alg] || TrueLayerSigning.algorithm
|
11
22
|
@kid = args[:kid] || TrueLayerSigning.certificate_id
|
@@ -15,37 +26,40 @@ module TrueLayerSigning
|
|
15
26
|
end
|
16
27
|
|
17
28
|
def to_h
|
18
|
-
hash = instance_variables.
|
29
|
+
hash = instance_variables.to_h { |var| [var[1..].to_sym, instance_variable_get(var)] }
|
19
30
|
|
20
31
|
hash.reject { |key, _value| hash[key].nil? }
|
21
32
|
end
|
22
33
|
|
23
34
|
def filter_headers(headers)
|
24
|
-
required_header_keys = tl_headers.split(",").reject
|
25
|
-
normalised_headers =
|
35
|
+
required_header_keys = tl_headers.split(",").reject(&:empty?)
|
36
|
+
normalised_headers = Utils.normalise_headers!(headers)
|
37
|
+
ordered_headers = validate_declared_headers!(required_header_keys, normalised_headers)
|
26
38
|
|
27
|
-
|
39
|
+
ordered_headers.to_h
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
28
43
|
|
29
|
-
|
44
|
+
def validate_declared_headers!(required_header_keys, normalised_headers)
|
45
|
+
required_header_keys.map do |key|
|
30
46
|
value = normalised_headers[key.downcase]
|
31
47
|
|
32
48
|
raise(Error, "Missing header declared in signature: #{key.downcase}") unless value
|
33
49
|
|
34
50
|
[key, value]
|
35
51
|
end
|
36
|
-
|
37
|
-
ordered_headers.to_h
|
38
52
|
end
|
39
53
|
|
40
|
-
|
41
|
-
tl_headers
|
54
|
+
def retrieve_headers(tl_headers)
|
55
|
+
(tl_headers.is_a?(Hash) && tl_headers.keys.join(",")) || tl_headers || ""
|
42
56
|
end
|
43
57
|
end
|
44
58
|
|
45
59
|
class JwsBase
|
46
60
|
attr_reader :method, :path, :headers, :body
|
47
61
|
|
48
|
-
def initialize(
|
62
|
+
def initialize(_args = {})
|
49
63
|
@method = "POST"
|
50
64
|
@headers = {}
|
51
65
|
end
|
@@ -82,15 +96,17 @@ module TrueLayerSigning
|
|
82
96
|
self
|
83
97
|
end
|
84
98
|
|
85
|
-
private
|
99
|
+
private
|
100
|
+
|
101
|
+
def build_signing_payload(custom_headers = nil)
|
86
102
|
parts = []
|
87
103
|
parts.push("#{method.upcase} #{path}")
|
88
|
-
parts.push(custom_headers && format_headers(custom_headers) || format_headers(headers))
|
104
|
+
parts.push((custom_headers && format_headers(custom_headers)) || format_headers(headers))
|
89
105
|
parts.push(body)
|
90
106
|
parts.reject { |elem| elem.nil? || elem.empty? }.join("\n")
|
91
107
|
end
|
92
108
|
|
93
|
-
|
109
|
+
def format_headers(headers)
|
94
110
|
headers.map { |key, value| "#{key}: #{value}" }.join("\n")
|
95
111
|
end
|
96
112
|
end
|
@@ -1,6 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module TrueLayerSigning
|
2
4
|
class Verifier < JwsBase
|
3
|
-
EXPECTED_EC_KEY_COORDS_LENGTH = 66
|
5
|
+
EXPECTED_EC_KEY_COORDS_LENGTH = 66
|
4
6
|
|
5
7
|
attr_reader :required_headers, :key_type, :key_value
|
6
8
|
|
@@ -16,15 +18,12 @@ module TrueLayerSigning
|
|
16
18
|
|
17
19
|
jws_header, jws_header_b64, signature_b64 = self.class.parse_tl_signature(tl_signature)
|
18
20
|
|
19
|
-
|
21
|
+
validate_algorithm!(jws_header.alg)
|
20
22
|
|
21
23
|
ordered_headers = jws_header.filter_headers(headers)
|
22
|
-
normalised_headers =
|
23
|
-
|
24
|
-
ordered_headers.to_a.each { |header| normalised_headers[header.first.downcase] = header.last }
|
24
|
+
normalised_headers = Utils.normalise_headers!(ordered_headers)
|
25
25
|
|
26
|
-
|
27
|
-
required_headers.any? { |key| !normalised_headers.has_key?(key.downcase) }
|
26
|
+
validate_required_headers!(normalised_headers)
|
28
27
|
|
29
28
|
verify_signature_flex(ordered_headers, jws_header, jws_header_b64, signature_b64)
|
30
29
|
end
|
@@ -58,13 +57,15 @@ module TrueLayerSigning
|
|
58
57
|
[jws_header, jws_header_b64, signature_b64]
|
59
58
|
end
|
60
59
|
|
61
|
-
private
|
60
|
+
private
|
61
|
+
|
62
|
+
def verify_signature_flex(ordered_headers, jws_header, jws_header_b64, signature_b64)
|
62
63
|
full_signature = build_full_signature(ordered_headers, jws_header_b64, signature_b64)
|
63
64
|
|
64
65
|
begin
|
65
66
|
verify_signature(jws_header, full_signature)
|
66
67
|
rescue JWT::VerificationError
|
67
|
-
@path = path.end_with?("/")
|
68
|
+
@path = path.end_with?("/") ? path[0...-1] : "#{path}/"
|
68
69
|
full_signature = build_full_signature(ordered_headers, jws_header_b64, signature_b64)
|
69
70
|
|
70
71
|
begin
|
@@ -75,13 +76,13 @@ module TrueLayerSigning
|
|
75
76
|
end
|
76
77
|
end
|
77
78
|
|
78
|
-
|
79
|
+
def build_full_signature(ordered_headers, jws_header_b64, signature_b64)
|
79
80
|
payload_b64 = Base64.urlsafe_encode64(build_signing_payload(ordered_headers), padding: false)
|
80
81
|
|
81
82
|
[jws_header_b64, payload_b64, signature_b64].join(".")
|
82
83
|
end
|
83
84
|
|
84
|
-
|
85
|
+
def verify_signature(jws_header, full_signature)
|
85
86
|
case key_type
|
86
87
|
when :pem
|
87
88
|
public_key = OpenSSL::PKey.read(key_value)
|
@@ -98,7 +99,7 @@ module TrueLayerSigning
|
|
98
99
|
JWT.truelayer_decode(full_signature, public_key, jwt_options)
|
99
100
|
end
|
100
101
|
|
101
|
-
|
102
|
+
def retrieve_public_key(key_type, key_value, jws_header)
|
102
103
|
case key_type
|
103
104
|
when :pem
|
104
105
|
OpenSSL::PKey.read(key_value)
|
@@ -116,20 +117,30 @@ module TrueLayerSigning
|
|
116
117
|
end
|
117
118
|
end
|
118
119
|
|
119
|
-
|
120
|
+
def apply_zero_padding_as_needed(jwk)
|
120
121
|
valid_jwk = jwk.clone
|
121
122
|
|
122
123
|
%i(x y).each do |elem|
|
123
124
|
coords = Base64.urlsafe_decode64(valid_jwk[elem])
|
124
125
|
diff = EXPECTED_EC_KEY_COORDS_LENGTH - coords.length
|
125
126
|
|
126
|
-
valid_jwk[elem] = Base64.urlsafe_encode64(("\x00" * diff) + coords) if diff
|
127
|
+
valid_jwk[elem] = Base64.urlsafe_encode64(("\x00" * diff) + coords) if diff.positive?
|
127
128
|
end
|
128
129
|
|
129
130
|
valid_jwk
|
130
131
|
end
|
131
132
|
|
132
|
-
|
133
|
+
def validate_required_headers!(headers)
|
134
|
+
raise(Error, "Signature missing required header(s)") if required_headers&.any? do |key|
|
135
|
+
!headers.key?(key.downcase)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def validate_algorithm!(algorithm)
|
140
|
+
raise(Error, "Unexpected `alg` header value") if algorithm != TrueLayerSigning.algorithm
|
141
|
+
end
|
142
|
+
|
143
|
+
def ensure_verifier_config!
|
133
144
|
raise(Error, "Key value missing") unless key_value
|
134
145
|
end
|
135
146
|
end
|
data/lib/truelayer-signing.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "minitest/autorun"
|
2
4
|
require "truelayer-signing"
|
3
5
|
|
@@ -5,12 +7,12 @@ def read_file(path)
|
|
5
7
|
File.read(File.expand_path(path, File.dirname(__FILE__)))
|
6
8
|
end
|
7
9
|
|
8
|
-
CERTIFICATE_ID = "45fc75cf-5649-4134-84b3-192c2c78e990"
|
9
|
-
PRIVATE_KEY = read_file("../../test-resources/ec512-private.pem")
|
10
|
-
PUBLIC_KEY = read_file("../../test-resources/ec512-public.pem")
|
10
|
+
CERTIFICATE_ID = "45fc75cf-5649-4134-84b3-192c2c78e990"
|
11
|
+
PRIVATE_KEY = read_file("../../test-resources/ec512-private.pem")
|
12
|
+
PUBLIC_KEY = read_file("../../test-resources/ec512-public.pem")
|
11
13
|
|
12
|
-
TrueLayerSigning.certificate_id = CERTIFICATE_ID
|
13
|
-
TrueLayerSigning.private_key = PRIVATE_KEY
|
14
|
+
TrueLayerSigning.certificate_id = CERTIFICATE_ID
|
15
|
+
TrueLayerSigning.private_key = PRIVATE_KEY
|
14
16
|
|
15
17
|
class TrueLayerSigningTest < Minitest::Test
|
16
18
|
def test_full_request_signature_should_succeed
|
@@ -35,7 +37,7 @@ class TrueLayerSigningTest < Minitest::Test
|
|
35
37
|
.verify(tl_signature)
|
36
38
|
|
37
39
|
refute(result.first.include?("\nX-Whatever: aoitbeh\n"))
|
38
|
-
assert(result.first.include?("\nIdempotency-Key:
|
40
|
+
assert(result.first.include?("\nIdempotency-Key: #{idempotency_key}\n"))
|
39
41
|
assert(result.first
|
40
42
|
.start_with?("POST /merchant_accounts/a61acaef-ee05-4077-92f3-25543a11bd8d/sweeping\n"))
|
41
43
|
end
|
@@ -82,11 +84,11 @@ class TrueLayerSigningTest < Minitest::Test
|
|
82
84
|
def test_mismatched_signature_with_attached_valid_body_should_fail
|
83
85
|
# Signature for `/bar` but with a valid jws-body pre-attached.
|
84
86
|
# If we run a simple jws verify on this unchanged, it'll work!
|
85
|
-
tl_signature = "eyJhbGciOiJFUzUxMiIsImtpZCI6IjQ1ZmM3NWNmLTU2ND"
|
86
|
-
"ktndeZnC04NGIzLTE5MmMyYzc4ZTk5MCIsInRsX3ZlcnNpb24iOiIyIiwidGxfaGV"
|
87
|
-
"hZGVycyI6IiJ9.UE9TVCAvYmFyCnt9.ARLa7Q5b8k5CIhfy1qrS-IkNqCDeE-VFRD"
|
88
|
-
"z7Lb0fXUMOi_Ktck-R7BHDMXFDzbI5TyaxIo5TGHZV_cs0fg96dlSxAERp3UaN2oC"
|
89
|
-
"QHIE5gQ4m5uU3ee69XfwwU_RpEIMFypycxwq1HOf4LzTLXqP_CDT8DdyX8oTwYdUB"
|
87
|
+
tl_signature = "eyJhbGciOiJFUzUxMiIsImtpZCI6IjQ1ZmM3NWNmLTU2ND" \
|
88
|
+
"ktndeZnC04NGIzLTE5MmMyYzc4ZTk5MCIsInRsX3ZlcnNpb24iOiIyIiwidGxfaGV" \
|
89
|
+
"hZGVycyI6IiJ9.UE9TVCAvYmFyCnt9.ARLa7Q5b8k5CIhfy1qrS-IkNqCDeE-VFRD" \
|
90
|
+
"z7Lb0fXUMOi_Ktck-R7BHDMXFDzbI5TyaxIo5TGHZV_cs0fg96dlSxAERp3UaN2oC" \
|
91
|
+
"QHIE5gQ4m5uU3ee69XfwwU_RpEIMFypycxwq1HOf4LzTLXqP_CDT8DdyX8oTwYdUB" \
|
90
92
|
"d2d3D17Wd9UA"
|
91
93
|
|
92
94
|
verifier = TrueLayerSigning.verify_with_pem(PUBLIC_KEY)
|
@@ -101,11 +103,11 @@ class TrueLayerSigningTest < Minitest::Test
|
|
101
103
|
def test_mismatched_signature_with_attached_valid_body_and_trailing_dots_should_fail
|
102
104
|
# Signature for `/bar` but with a valid jws-body pre-attached.
|
103
105
|
# If we run a simple jws verify on this unchanged, it'll work!
|
104
|
-
tl_signature = "eyJhbGciOiJFUzUxMiIsImtpZCI6IjQ1ZmM3NWNmLTU2ND"
|
105
|
-
"ktndeZnC04NGIzLTE5MmMyYzc4ZTk5MCIsInRsX3ZlcnNpb24iOiIyIiwidGxfaGV"
|
106
|
-
"hZGVycyI6IiJ9.UE9TVCAvYmFyCnt9.ARLa7Q5b8k5CIhfy1qrS-IkNqCDeE-VFRD"
|
107
|
-
"z7Lb0fXUMOi_Ktck-R7BHDMXFDzbI5TyaxIo5TGHZV_cs0fg96dlSxAERp3UaN2oC"
|
108
|
-
"QHIE5gQ4m5uU3ee69XfwwU_RpEIMFypycxwq1HOf4LzTLXqP_CDT8DdyX8oTwYdUB"
|
106
|
+
tl_signature = "eyJhbGciOiJFUzUxMiIsImtpZCI6IjQ1ZmM3NWNmLTU2ND" \
|
107
|
+
"ktndeZnC04NGIzLTE5MmMyYzc4ZTk5MCIsInRsX3ZlcnNpb24iOiIyIiwidGxfaGV" \
|
108
|
+
"hZGVycyI6IiJ9.UE9TVCAvYmFyCnt9.ARLa7Q5b8k5CIhfy1qrS-IkNqCDeE-VFRD" \
|
109
|
+
"z7Lb0fXUMOi_Ktck-R7BHDMXFDzbI5TyaxIo5TGHZV_cs0fg96dlSxAERp3UaN2oC" \
|
110
|
+
"QHIE5gQ4m5uU3ee69XfwwU_RpEIMFypycxwq1HOf4LzTLXqP_CDT8DdyX8oTwYdUB" \
|
109
111
|
"d2d3D17Wd9UA...."
|
110
112
|
|
111
113
|
verifier = TrueLayerSigning.verify_with_pem(PUBLIC_KEY)
|
@@ -132,7 +134,7 @@ class TrueLayerSigningTest < Minitest::Test
|
|
132
134
|
.verify(tl_signature)
|
133
135
|
|
134
136
|
refute(result.first.include?("\nX-Whatever-2: t2345d\n"))
|
135
|
-
assert(result.first.include?("\nIdempotency-Key:
|
137
|
+
assert(result.first.include?("\nIdempotency-Key: #{idempotency_key}\n"))
|
136
138
|
assert(result.first
|
137
139
|
.start_with?("POST /merchant_accounts/a61acaef-ee05-4077-92f3-25543a11bd8d/sweeping\n"))
|
138
140
|
end
|
@@ -482,34 +484,34 @@ class TrueLayerSigningTest < Minitest::Test
|
|
482
484
|
idempotency_key = "idemp-2076717c-9005-4811-a321-9e0787fa0382"
|
483
485
|
path = "/merchant_accounts/a61acaef-ee05-4077-92f3-25543a11bd8d/sweeping"
|
484
486
|
|
485
|
-
|
487
|
+
tl_signature_a = TrueLayerSigning.sign_with_pem
|
486
488
|
.set_path(path)
|
487
489
|
.add_header("Idempotency-Key", idempotency_key)
|
488
490
|
.set_body(body)
|
489
491
|
.sign
|
490
492
|
|
491
|
-
|
493
|
+
jws_header_a = TrueLayerSigning.extract_jws_header(tl_signature_a)
|
492
494
|
|
493
|
-
assert_nil(
|
495
|
+
assert_nil(jws_header_a.jku)
|
494
496
|
|
495
|
-
|
497
|
+
tl_signature_b = TrueLayerSigning.sign_with_pem
|
496
498
|
.set_path(path)
|
497
499
|
.add_header("Idempotency-Key", idempotency_key)
|
498
500
|
.set_body(body)
|
499
501
|
.set_jku("https://webhooks.truelayer.com/.well-known/jwks")
|
500
502
|
.sign
|
501
503
|
|
502
|
-
|
504
|
+
jws_header_b = TrueLayerSigning.extract_jws_header(tl_signature_b)
|
503
505
|
|
504
|
-
assert_equal("https://webhooks.truelayer.com/.well-known/jwks",
|
506
|
+
assert_equal("https://webhooks.truelayer.com/.well-known/jwks", jws_header_b.jku)
|
505
507
|
end
|
506
508
|
|
507
509
|
# TODO: remove if/when we get rid of `lib/truelayer-signing/jwt.rb`
|
508
510
|
def test_jwt_encode_and_decode_should_succeed
|
509
511
|
payload_object = { currency: "GBP", max_amount_in_minor: 50_000_00 }
|
510
|
-
token_when_object = "eyJhbGciOiJIUzI1NiJ9.eyJjdXJyZW5jeSI6IkdCUCIsIm1heF9hbW"
|
512
|
+
token_when_object = "eyJhbGciOiJIUzI1NiJ9.eyJjdXJyZW5jeSI6IkdCUCIsIm1heF9hbW" \
|
511
513
|
"91bnRfaW5fbWlub3IiOjUwMDAwMDB9.SjbwZCqTl6G7LQNs_M6oQhwl3a9rbqO7p3cVncLtgZY"
|
512
|
-
token_when_json = "eyJhbGciOiJIUzI1NiJ9.IntcImN1cnJlbmN5XCI6XCJHQlBcIixcIm1h"
|
514
|
+
token_when_json = "eyJhbGciOiJIUzI1NiJ9.IntcImN1cnJlbmN5XCI6XCJHQlBcIixcIm1h" \
|
513
515
|
"eF9hbW91bnRfaW5fbWlub3JcIjo1MDAwMDAwfSI.rvCcgu-JevsNxbjUwJiFOuTd0hzVKvPK5RvGmaoDc7E"
|
514
516
|
|
515
517
|
# succeeds with a hash object
|
@@ -530,7 +532,7 @@ class TrueLayerSigningTest < Minitest::Test
|
|
530
532
|
# TODO: remove if/when we get rid of `lib/truelayer-signing/jwt.rb`
|
531
533
|
def test_jwt_truelayer_encode_and_decode_when_given_json_should_succeed
|
532
534
|
payload_json = { currency: "GBP", max_amount_in_minor: 50_000_00 }.to_json
|
533
|
-
token_when_json = "eyJhbGciOiJIUzI1NiJ9.eyJjdXJyZW5jeSI6IkdCUCIsIm1heF9hbW9"
|
535
|
+
token_when_json = "eyJhbGciOiJIUzI1NiJ9.eyJjdXJyZW5jeSI6IkdCUCIsIm1heF9hbW9" \
|
534
536
|
"1bnRfaW5fbWlub3IiOjUwMDAwMDB9.SjbwZCqTl6G7LQNs_M6oQhwl3a9rbqO7p3cVncLtgZY"
|
535
537
|
|
536
538
|
assert_equal(token_when_json, JWT.truelayer_encode(payload_json, "12345", "HS256", {}))
|
@@ -549,7 +551,7 @@ class TrueLayerSigningTest < Minitest::Test
|
|
549
551
|
|
550
552
|
# TODO: remove if/when we get rid of `lib/truelayer-signing/jwt.rb`
|
551
553
|
def test_jwt_truelayer_decode_when_given_a_hash_should_succeed
|
552
|
-
token_when_object = "eyJhbGciOiJIUzI1NiJ9.eyJjdXJyZW5jeSI6IkdCUCIsIm1heF9hbW"
|
554
|
+
token_when_object = "eyJhbGciOiJIUzI1NiJ9.eyJjdXJyZW5jeSI6IkdCUCIsIm1heF9hbW" \
|
553
555
|
"91bnRfaW5fbWlub3IiOjUwMDAwMDB9.SjbwZCqTl6G7LQNs_M6oQhwl3a9rbqO7p3cVncLtgZY"
|
554
556
|
|
555
557
|
assert_equal(
|
data/truelayer-signing.gemspec
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "lib"))
|
2
4
|
|
3
5
|
Gem::Specification.new do |s|
|
4
6
|
s.name = "truelayer-signing"
|
5
|
-
s.version = "0.2.
|
7
|
+
s.version = "0.2.3"
|
6
8
|
s.summary = "Ruby gem to produce and verify TrueLayer API requests signatures"
|
7
9
|
s.description = "TrueLayer provides instant access to open banking to " \
|
8
10
|
"easily integrate next-generation payments and financial data into any app." \
|
@@ -14,12 +16,21 @@ Gem::Specification.new do |s|
|
|
14
16
|
|
15
17
|
s.metadata = {
|
16
18
|
"bug_tracker_uri" => "https://github.com/TrueLayer/truelayer-signing/issues",
|
17
|
-
"changelog_uri" => "https://github.com/TrueLayer/truelayer-signing/blob/main/ruby/CHANGELOG.md"
|
19
|
+
"changelog_uri" => "https://github.com/TrueLayer/truelayer-signing/blob/main/ruby/CHANGELOG.md"
|
18
20
|
}
|
19
21
|
|
20
|
-
s.files = Dir[
|
21
|
-
|
22
|
+
s.files = Dir[
|
23
|
+
"CHANGELOG.md",
|
24
|
+
"Gemfile",
|
25
|
+
"LICENSE*",
|
26
|
+
"README.md",
|
27
|
+
"Rakefile",
|
28
|
+
"lib/**/*.*",
|
29
|
+
"test/**/*.*",
|
30
|
+
"truelayer-signing.gemspec",
|
31
|
+
]
|
32
|
+
s.require_path = "lib"
|
22
33
|
|
23
34
|
s.required_ruby_version = ">= 2.7"
|
24
|
-
s.
|
35
|
+
s.add_dependency("jwt", "~> 2.7.0")
|
25
36
|
end
|
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.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kevin Plattret
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-07-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: jwt
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 2.7.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 2.7.0
|
27
27
|
description: TrueLayer provides instant access to open banking to easily integrate
|
28
28
|
next-generation payments and financial data into any app.This helps easily sign
|
29
29
|
TrueLayer API requests using a JSON web signature.
|
@@ -32,31 +32,25 @@ executables: []
|
|
32
32
|
extensions: []
|
33
33
|
extra_rdoc_files: []
|
34
34
|
files:
|
35
|
-
-
|
36
|
-
-
|
37
|
-
-
|
38
|
-
-
|
39
|
-
-
|
40
|
-
-
|
41
|
-
-
|
42
|
-
-
|
43
|
-
-
|
44
|
-
-
|
45
|
-
-
|
46
|
-
-
|
47
|
-
-
|
48
|
-
-
|
49
|
-
-
|
50
|
-
-
|
51
|
-
-
|
52
|
-
-
|
53
|
-
-
|
54
|
-
- "./test/resources/failed-payment-expired-test-payload.json"
|
55
|
-
- "./test/resources/missing-zero-padding-test-jwks.json"
|
56
|
-
- "./test/resources/missing-zero-padding-test-payload.json"
|
57
|
-
- "./test/resources/missing-zero-padding-test-signature.txt"
|
58
|
-
- "./test/test-truelayer-signing.rb"
|
59
|
-
- "./truelayer-signing.gemspec"
|
35
|
+
- CHANGELOG.md
|
36
|
+
- Gemfile
|
37
|
+
- LICENSE-APACHE
|
38
|
+
- LICENSE-MIT
|
39
|
+
- README.md
|
40
|
+
- Rakefile
|
41
|
+
- lib/truelayer-signing.rb
|
42
|
+
- lib/truelayer-signing/config.rb
|
43
|
+
- lib/truelayer-signing/errors.rb
|
44
|
+
- lib/truelayer-signing/jwt.rb
|
45
|
+
- lib/truelayer-signing/signer.rb
|
46
|
+
- lib/truelayer-signing/utils.rb
|
47
|
+
- lib/truelayer-signing/verifier.rb
|
48
|
+
- test/resources/failed-payment-expired-test-payload.json
|
49
|
+
- test/resources/missing-zero-padding-test-jwks.json
|
50
|
+
- test/resources/missing-zero-padding-test-payload.json
|
51
|
+
- test/resources/missing-zero-padding-test-signature.txt
|
52
|
+
- test/test-truelayer-signing.rb
|
53
|
+
- truelayer-signing.gemspec
|
60
54
|
homepage: https://github.com/TrueLayer/truelayer-signing/tree/main/ruby
|
61
55
|
licenses:
|
62
56
|
- Apache-2.0
|
@@ -64,7 +58,7 @@ licenses:
|
|
64
58
|
metadata:
|
65
59
|
bug_tracker_uri: https://github.com/TrueLayer/truelayer-signing/issues
|
66
60
|
changelog_uri: https://github.com/TrueLayer/truelayer-signing/blob/main/ruby/CHANGELOG.md
|
67
|
-
post_install_message:
|
61
|
+
post_install_message:
|
68
62
|
rdoc_options: []
|
69
63
|
require_paths:
|
70
64
|
- lib
|
@@ -79,8 +73,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
79
73
|
- !ruby/object:Gem::Version
|
80
74
|
version: '0'
|
81
75
|
requirements: []
|
82
|
-
rubygems_version: 3.
|
83
|
-
signing_key:
|
76
|
+
rubygems_version: 3.5.11
|
77
|
+
signing_key:
|
84
78
|
specification_version: 4
|
85
79
|
summary: Ruby gem to produce and verify TrueLayer API requests signatures
|
86
80
|
test_files: []
|
@@ -1,27 +0,0 @@
|
|
1
|
-
# Ruby request signature example
|
2
|
-
|
3
|
-
Sends a signed request to `https://api.truelayer-sandbox.com/test-signature`.
|
4
|
-
|
5
|
-
## Run
|
6
|
-
|
7
|
-
Set the following environment variables:
|
8
|
-
|
9
|
-
* `TRUELAYER_SIGNING_ACCESS_TOKEN` – a valid JWT access token for the `payments` scope (see our
|
10
|
-
[docs](https://docs.truelayer.com/docs/retrieve-a-token-in-your-server)).
|
11
|
-
* `TRUELAYER_SIGNING_CERTIFICATE_ID` – the certificate/key UUID associated with your public key
|
12
|
-
uploaded at [console.truelayer.com](https://console.truelayer.com).
|
13
|
-
* `TRUELAYER_SIGNING_PRIVATE_KEY` – the private key PEM string that matches the certificate ID of the
|
14
|
-
uploaded public key. Should have the same format as [this example private
|
15
|
-
key](https://github.com/TrueLayer/truelayer-signing/blob/main/test-resources/ec512-private.pem).
|
16
|
-
|
17
|
-
Install the required dependencies:
|
18
|
-
|
19
|
-
```sh
|
20
|
-
$ bundle
|
21
|
-
```
|
22
|
-
|
23
|
-
Execute the request-signing example script:
|
24
|
-
|
25
|
-
```sh
|
26
|
-
$ ruby main.rb
|
27
|
-
```
|
@@ -1,46 +0,0 @@
|
|
1
|
-
require "http"
|
2
|
-
require "securerandom"
|
3
|
-
require "truelayer-signing"
|
4
|
-
|
5
|
-
class TrueLayerSigningExamples
|
6
|
-
# Set required environment variables
|
7
|
-
TRUELAYER_SIGNING_ACCESS_TOKEN = ENV.fetch("TRUELAYER_SIGNING_ACCESS_TOKEN", nil).freeze
|
8
|
-
TRUELAYER_SIGNING_BASE_URL = "https://api.truelayer-sandbox.com".freeze
|
9
|
-
|
10
|
-
raise(StandardError, "TRUELAYER_SIGNING_ACCESS_TOKEN is missing") \
|
11
|
-
if TRUELAYER_SIGNING_ACCESS_TOKEN.nil? || TRUELAYER_SIGNING_ACCESS_TOKEN.empty?
|
12
|
-
|
13
|
-
class << self
|
14
|
-
def test_signature_endpoint
|
15
|
-
url = "#{TRUELAYER_SIGNING_BASE_URL}/test-signature"
|
16
|
-
idempotency_key = SecureRandom.uuid
|
17
|
-
|
18
|
-
# A random body string is enough for this request as the `/test-signature` endpoint does not
|
19
|
-
# require any schema, it simply checks the signature is valid against what's received.
|
20
|
-
body = "body-#{SecureRandom.uuid}"
|
21
|
-
|
22
|
-
# Generate a `Tl-Signature`
|
23
|
-
signature = TrueLayerSigning.sign_with_pem
|
24
|
-
.set_method("POST")
|
25
|
-
.set_path("/test-signature")
|
26
|
-
# Optional: `/test-signature` does not require any headers, but we may sign some anyway.
|
27
|
-
# All signed headers *must* be included unmodified in the request.
|
28
|
-
.add_header("Idempotency-Key", idempotency_key)
|
29
|
-
.add_header("X-Bar-Header", "abc123")
|
30
|
-
.set_body(body)
|
31
|
-
.sign
|
32
|
-
|
33
|
-
response = HTTP.auth("Bearer #{TRUELAYER_SIGNING_ACCESS_TOKEN}")
|
34
|
-
.headers(idempotency_key: idempotency_key)
|
35
|
-
.headers(x_bar_header: "abc123")
|
36
|
-
.headers(tl_signature: signature)
|
37
|
-
.post(url, body: body)
|
38
|
-
|
39
|
-
return puts "✓ Signature is valid" if response.status.success?
|
40
|
-
|
41
|
-
puts JSON.pretty_generate(JSON.parse(response.to_s))
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
TrueLayerSigningExamples.test_signature_endpoint
|
@@ -1,42 +0,0 @@
|
|
1
|
-
GEM
|
2
|
-
remote: https://rubygems.org/
|
3
|
-
specs:
|
4
|
-
addressable (2.8.4)
|
5
|
-
public_suffix (>= 2.0.2, < 6.0)
|
6
|
-
domain_name (0.5.20190701)
|
7
|
-
unf (>= 0.0.5, < 1.0.0)
|
8
|
-
ffi (1.15.5)
|
9
|
-
ffi-compiler (1.0.1)
|
10
|
-
ffi (>= 1.0.0)
|
11
|
-
rake
|
12
|
-
http (5.1.1)
|
13
|
-
addressable (~> 2.8)
|
14
|
-
http-cookie (~> 1.0)
|
15
|
-
http-form_data (~> 2.2)
|
16
|
-
llhttp-ffi (~> 0.4.0)
|
17
|
-
http-cookie (1.0.5)
|
18
|
-
domain_name (~> 0.5)
|
19
|
-
http-form_data (2.3.0)
|
20
|
-
jwt (2.7.0)
|
21
|
-
llhttp-ffi (0.4.0)
|
22
|
-
ffi-compiler (~> 1.0)
|
23
|
-
rake (~> 13.0)
|
24
|
-
public_suffix (5.0.1)
|
25
|
-
rake (13.0.6)
|
26
|
-
truelayer-signing (0.2.0)
|
27
|
-
jwt (~> 2.7)
|
28
|
-
unf (0.1.4)
|
29
|
-
unf_ext
|
30
|
-
unf_ext (0.0.8.2)
|
31
|
-
webrick (1.8.1)
|
32
|
-
|
33
|
-
PLATFORMS
|
34
|
-
arm64-darwin-21
|
35
|
-
|
36
|
-
DEPENDENCIES
|
37
|
-
http
|
38
|
-
truelayer-signing
|
39
|
-
webrick
|
40
|
-
|
41
|
-
BUNDLED WITH
|
42
|
-
2.4.13
|
@@ -1,30 +0,0 @@
|
|
1
|
-
# Ruby webhook server example
|
2
|
-
|
3
|
-
An HTTP server that can receive and verify signed TrueLayer webhooks.
|
4
|
-
|
5
|
-
## Run
|
6
|
-
|
7
|
-
Install the required dependencies:
|
8
|
-
|
9
|
-
```sh
|
10
|
-
$ bundle
|
11
|
-
```
|
12
|
-
|
13
|
-
Run the server locally:
|
14
|
-
|
15
|
-
```sh
|
16
|
-
$ ruby main.rb
|
17
|
-
```
|
18
|
-
|
19
|
-
Send a valid webhook that was signed for the path `/hook/d7a2c49d-110a-4ed2-a07d-8fdb3ea6424b`:
|
20
|
-
|
21
|
-
```sh
|
22
|
-
curl -iX POST -H "Content-Type: application/json" \
|
23
|
-
-H "X-Tl-Webhook-Timestamp: 2022-03-11T14:00:33Z" \
|
24
|
-
-H "Tl-Signature: eyJhbGciOiJFUzUxMiIsImtpZCI6IjFmYzBlNTlmLWIzMzUtNDdjYS05OWE5LTczNzQ5NTc1NmE1OCIsInRsX3ZlcnNpb24iOiIyIiwidGxfaGVhZGVycyI6IngtdGwtd2ViaG9vay10aW1lc3RhbXAiLCJqa3UiOiJodHRwczovL3dlYmhvb2tzLnRydWVsYXllci5jb20vLndlbGwta25vd24vandrcyJ9..AE_QsBRhnsMkcRzd42wvY1e2HruUhkOgjuZKktGH_WmbD7rBzoaEHUuF36IxyyvCbLajd3MBExNmzjbrOQsGaspwAI5DcGVMFLKUhB7ZzUlTP9up3eNUrdwWyyfBWDQb-qmEuLnrhFDJvgCXEqlV5OLrt-O7LaRAJ4f9KHsZLQ_j2vPC" \
|
25
|
-
-d "{\"event_type\":\"payout_settled\",\"event_schema_version\":1,\"event_id\":\"8fb9fb4e-bb2b-400b-af64-59e5dde74bad\",\"event_body\":{\"transaction_id\":\"c34c8721-66a9-49f6-a229-284efcf88a02\",\"settled_at\":\"2022-03-11T14:00:32.933000Z\"}}" \
|
26
|
-
http://localhost:4567/hook/d7a2c49d-110a-4ed2-a07d-8fdb3ea6424b
|
27
|
-
```
|
28
|
-
|
29
|
-
Modifying the `X-Tl-Webhook-Timestamp` header, the body or the path of the request will cause the
|
30
|
-
above signature to be invalid.
|
@@ -1,95 +0,0 @@
|
|
1
|
-
require "http"
|
2
|
-
require "truelayer-signing"
|
3
|
-
require "webrick"
|
4
|
-
|
5
|
-
class TrueLayerSigningExamples
|
6
|
-
# Note: the webhook path can be whatever is configured for your application.
|
7
|
-
# Here a unique path is used, matching the example signature in the README.
|
8
|
-
WEBHOOK_PATH = "/hook/d7a2c49d-110a-4ed2-a07d-8fdb3ea6424b".freeze
|
9
|
-
|
10
|
-
class << self
|
11
|
-
def run_webhook_server
|
12
|
-
server = WEBrick::HTTPServer.new(Port: 4567)
|
13
|
-
|
14
|
-
puts "Server running at http://localhost:4567"
|
15
|
-
|
16
|
-
server.mount_proc('/') do |req, res|
|
17
|
-
request = parse_request(req)
|
18
|
-
status, body = handle_request.call(request)
|
19
|
-
headers = { "Content-Type" => "text/plain" }
|
20
|
-
|
21
|
-
send_response(res, status, headers, body)
|
22
|
-
rescue => error
|
23
|
-
puts error
|
24
|
-
end
|
25
|
-
|
26
|
-
server.start
|
27
|
-
ensure
|
28
|
-
server.shutdown
|
29
|
-
end
|
30
|
-
|
31
|
-
private def parse_request(request)
|
32
|
-
{
|
33
|
-
method: request.request_method,
|
34
|
-
path: request.path,
|
35
|
-
headers: headers_to_hash(request.header),
|
36
|
-
body: request.body
|
37
|
-
}
|
38
|
-
end
|
39
|
-
|
40
|
-
private def handle_request
|
41
|
-
Proc.new do |request|
|
42
|
-
if request[:method] == "POST" && request[:path] == WEBHOOK_PATH
|
43
|
-
verify_webhook(request[:path], request[:headers], request[:body])
|
44
|
-
else
|
45
|
-
["403", "Forbidden"]
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
private def verify_webhook(path, headers, body)
|
51
|
-
tl_signature = headers["tl-signature"]
|
52
|
-
|
53
|
-
return ["400", "Bad Request – Header `Tl-Signature` missing"] unless tl_signature
|
54
|
-
|
55
|
-
jku = TrueLayerSigning.extract_jws_header(tl_signature).jku
|
56
|
-
|
57
|
-
return ["400", "Bad Request – Signature missing `jku`"] unless jku
|
58
|
-
return ["401", "Unauthorized – Unpermitted `jku`"] \
|
59
|
-
unless jku == "https://webhooks.truelayer.com/.well-known/jwks" ||
|
60
|
-
jku == "https://webhooks.truelayer-sandbox.com/.well-known/jwks"
|
61
|
-
|
62
|
-
jwks = HTTP.get(jku)
|
63
|
-
|
64
|
-
return ["401", "Unauthorized – Unavailable `jwks` resource"] unless jwks.status.success?
|
65
|
-
|
66
|
-
begin
|
67
|
-
TrueLayerSigning.verify_with_jwks(jwks.to_s)
|
68
|
-
.set_method(:post)
|
69
|
-
.set_path(path)
|
70
|
-
.set_headers(headers)
|
71
|
-
.set_body(body)
|
72
|
-
.verify(tl_signature)
|
73
|
-
|
74
|
-
["202", "Accepted"]
|
75
|
-
rescue TrueLayerSigning::Error => error
|
76
|
-
puts error
|
77
|
-
|
78
|
-
["401", "Unauthorized"]
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
private def headers_to_hash(headers)
|
83
|
-
headers.transform_keys { |key| key.to_s.strip.downcase }.transform_values(&:first)
|
84
|
-
end
|
85
|
-
|
86
|
-
private def send_response(response, status, headers, body)
|
87
|
-
response.status = status
|
88
|
-
response.header.merge!(headers)
|
89
|
-
response.body = body
|
90
|
-
response
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
TrueLayerSigningExamples.run_webhook_server
|