truelayer-signing 0.1.0 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +4 -0
- data/examples/sign-request/Gemfile.lock +3 -4
- data/examples/webhook-server/Gemfile +1 -0
- data/examples/webhook-server/Gemfile.lock +4 -2
- data/examples/webhook-server/main.rb +43 -47
- data/lib/truelayer-signing/jwt.rb +23 -2
- data/lib/truelayer-signing/signer.rb +2 -1
- data/lib/truelayer-signing/verifier.rb +2 -2
- data/test/test-truelayer-signing.rb +54 -0
- data/truelayer-signing.gemspec +4 -4
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8cc4b5beb4b79ccfafd7350d4882927d45abea2580d4d4d869d39faae157498d
|
4
|
+
data.tar.gz: ab43241ee2475b2797ff43fc5c142b778ff4e4ef28d042a123b9ac8179d913aa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6e208e6b3ed16190946cdeceae856ebcddc42b472458b86c527e0dfe52a67fe0d33e954c018f63fcb8502b659081aa35522516f9dacfb12f492b36a7830fe6b6
|
7
|
+
data.tar.gz: 3282b084e0907c8c709dfa2f3d2bef71527bca7181c342a24de63262c70ffa3a9e41f4437eb77f667a1e9c6a45776b8db6549ce709b244128b5fb58006b4afea
|
data/CHANGELOG.md
CHANGED
@@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
9
9
|
|
10
10
|
- ...
|
11
11
|
|
12
|
+
## [0.1.2] – 2023-05-19
|
13
|
+
|
14
|
+
- Fix conflict with JWT library
|
15
|
+
|
16
|
+
## [0.1.1] – 2023-05-17
|
17
|
+
|
18
|
+
- Fix webhook server example
|
19
|
+
|
12
20
|
## [0.1.0] – 2023-01-09
|
13
21
|
|
14
22
|
- Add `TrueLayerSigning.sign_with_pem` and `TrueLayerSigning.verify_with_pem(pem)`.
|
data/README.md
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# truelayer-signing
|
2
2
|
|
3
|
+
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/TrueLayer/truelayer-signing/ruby.yml?branch=main)
|
4
|
+
![Gem](https://img.shields.io/gem/v/truelayer-signing)
|
5
|
+
![Gem](https://img.shields.io/gem/dt/truelayer-signing)
|
6
|
+
|
3
7
|
Ruby gem to produce and verify TrueLayer API requests signatures.
|
4
8
|
|
5
9
|
## Installation
|
@@ -1,7 +1,7 @@
|
|
1
1
|
GEM
|
2
2
|
remote: https://rubygems.org/
|
3
3
|
specs:
|
4
|
-
addressable (2.8.
|
4
|
+
addressable (2.8.4)
|
5
5
|
public_suffix (>= 2.0.2, < 6.0)
|
6
6
|
domain_name (0.5.20190701)
|
7
7
|
unf (>= 0.0.5, < 1.0.0)
|
@@ -23,7 +23,7 @@ GEM
|
|
23
23
|
rake (~> 13.0)
|
24
24
|
public_suffix (5.0.1)
|
25
25
|
rake (13.0.6)
|
26
|
-
truelayer-signing (0.1.
|
26
|
+
truelayer-signing (0.1.2)
|
27
27
|
jwt (= 2.6)
|
28
28
|
unf (0.1.4)
|
29
29
|
unf_ext
|
@@ -31,11 +31,10 @@ GEM
|
|
31
31
|
|
32
32
|
PLATFORMS
|
33
33
|
arm64-darwin-21
|
34
|
-
ruby
|
35
34
|
|
36
35
|
DEPENDENCIES
|
37
36
|
http
|
38
37
|
truelayer-signing
|
39
38
|
|
40
39
|
BUNDLED WITH
|
41
|
-
2.4.
|
40
|
+
2.4.13
|
@@ -2,14 +2,16 @@ GEM
|
|
2
2
|
remote: https://rubygems.org/
|
3
3
|
specs:
|
4
4
|
jwt (2.6.0)
|
5
|
-
truelayer-signing (0.1.
|
5
|
+
truelayer-signing (0.1.1)
|
6
6
|
jwt (= 2.6)
|
7
|
+
webrick (1.8.1)
|
7
8
|
|
8
9
|
PLATFORMS
|
9
10
|
arm64-darwin-21
|
10
11
|
|
11
12
|
DEPENDENCIES
|
12
13
|
truelayer-signing
|
14
|
+
webrick
|
13
15
|
|
14
16
|
BUNDLED WITH
|
15
|
-
2.4.
|
17
|
+
2.4.13
|
@@ -1,57 +1,54 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'securerandom'
|
2
|
+
require 'truelayer-signing'
|
3
|
+
require 'webrick'
|
3
4
|
|
4
5
|
class TrueLayerSigningExamples
|
5
6
|
# Note: the webhook path can be whatever is configured for your application.
|
6
7
|
# Here a unique path is used, matching the example signature in the README.
|
7
8
|
WEBHOOK_PATH = "/hook/d7a2c49d-110a-4ed2-a07d-8fdb3ea6424b".freeze
|
8
|
-
PUBLIC_KEY_PEM =
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
PUBLIC_KEY_PEM = <<~TXT.freeze
|
10
|
+
-----BEGIN PUBLIC KEY-----
|
11
|
+
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBJ6ET9XeVCyMy+yOetZaNNCXPhwr5
|
12
|
+
BlyDDg1CLmyNM5SvqOs8RveL6dYl4lpPur4xrPQl04ggYlVd9wnHkZnp3jcBlXw8
|
13
|
+
Lc5phyYF1q2/QV/5wp2WHIhKDqUiXC0TvlE8d7MdTAN9yolcwrh6aWZ3kesTMZif
|
14
|
+
BgItyT6PXUab8mMdI8k=
|
15
|
+
-----END PUBLIC KEY-----
|
16
|
+
TXT
|
13
17
|
|
14
18
|
class << self
|
15
19
|
def run_webhook_server
|
16
|
-
|
20
|
+
TrueLayerSigning.certificate_id ||= SecureRandom.uuid
|
21
|
+
server = WEBrick::HTTPServer.new(Port: 4567)
|
17
22
|
|
18
23
|
puts "Server running at http://localhost:4567"
|
19
24
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
rescue => error
|
29
|
-
puts error
|
30
|
-
ensure
|
31
|
-
client.close
|
32
|
-
end
|
33
|
-
end
|
25
|
+
server.mount_proc('/') do |req, res|
|
26
|
+
request = parse_request(req)
|
27
|
+
status, body = handle_request.call(request)
|
28
|
+
headers = { "Content-Type" => "text/plain" }
|
29
|
+
|
30
|
+
send_response(res, status, headers, body)
|
31
|
+
rescue => error
|
32
|
+
puts error
|
34
33
|
end
|
35
|
-
end
|
36
34
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
url = remainder.split(" ").first
|
42
|
-
path, _query_strings = url.split("?", 2)
|
35
|
+
server.start
|
36
|
+
ensure
|
37
|
+
server.shutdown
|
38
|
+
end
|
43
39
|
|
40
|
+
private def parse_request(request)
|
44
41
|
{
|
45
|
-
method:
|
46
|
-
path: path,
|
47
|
-
headers: headers_to_hash(
|
48
|
-
body: body
|
42
|
+
method: request.request_method,
|
43
|
+
path: request.path,
|
44
|
+
headers: headers_to_hash(request.header),
|
45
|
+
body: request.body
|
49
46
|
}
|
50
47
|
end
|
51
48
|
|
52
49
|
private def handle_request
|
53
50
|
Proc.new do |request|
|
54
|
-
if request[:method] == "POST"
|
51
|
+
if request[:method] == "POST" && request[:path] == WEBHOOK_PATH
|
55
52
|
verify_webhook(request[:path], request[:headers], request[:body])
|
56
53
|
else
|
57
54
|
["403", "Forbidden"]
|
@@ -65,7 +62,8 @@ class TrueLayerSigningExamples
|
|
65
62
|
return ["400", "Bad Request – Header `Tl-Signature` missing"] unless tl_signature
|
66
63
|
|
67
64
|
begin
|
68
|
-
TrueLayerSigning
|
65
|
+
TrueLayerSigning
|
66
|
+
.verify_with_pem(PUBLIC_KEY_PEM)
|
69
67
|
.set_method(:post)
|
70
68
|
.set_path(path)
|
71
69
|
.set_headers(headers)
|
@@ -73,24 +71,22 @@ class TrueLayerSigningExamples
|
|
73
71
|
.verify(tl_signature)
|
74
72
|
|
75
73
|
["202", "Accepted"]
|
76
|
-
rescue TrueLayerSigning::Error
|
74
|
+
rescue TrueLayerSigning::Error => error
|
75
|
+
puts error
|
76
|
+
|
77
77
|
["401", "Unauthorized"]
|
78
78
|
end
|
79
79
|
end
|
80
80
|
|
81
|
-
private def
|
82
|
-
|
83
|
-
headers.each { |key, value| client.print("#{key}: #{value}\r\n") }
|
84
|
-
client.print("\r\n#{body}")
|
81
|
+
private def headers_to_hash(headers)
|
82
|
+
headers.transform_keys { |key| key.to_s.strip.downcase }.transform_values(&:first)
|
85
83
|
end
|
86
84
|
|
87
|
-
private def
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
hash
|
85
|
+
private def send_response(response, status, headers, body)
|
86
|
+
response.status = status
|
87
|
+
response.header.merge!(headers)
|
88
|
+
response.body = body
|
89
|
+
response
|
94
90
|
end
|
95
91
|
end
|
96
92
|
end
|
@@ -2,14 +2,16 @@
|
|
2
2
|
# It prevents the payload from being systematically converted to and from JSON.
|
3
3
|
# To be changed in the 'jwt' gem directly, or hard-coded in this library.
|
4
4
|
module JWT
|
5
|
-
|
5
|
+
module_function
|
6
|
+
|
7
|
+
class TrueLayerEncode < Encode
|
6
8
|
# See https://github.com/jwt/ruby-jwt/blob/main/lib/jwt/encode.rb#L53-L55
|
7
9
|
private def encode_payload
|
8
10
|
::JWT::Base64.url_encode(@payload)
|
9
11
|
end
|
10
12
|
end
|
11
13
|
|
12
|
-
class Decode
|
14
|
+
class TrueLayerDecode < Decode
|
13
15
|
# See https://github.com/jwt/ruby-jwt/blob/main/lib/jwt/decode.rb#L154-L156
|
14
16
|
private def payload
|
15
17
|
@payload ||= ::JWT::Base64.url_decode(@segments[1])
|
@@ -17,4 +19,23 @@ module JWT
|
|
17
19
|
raise JWT::DecodeError, 'Invalid segment encoding'
|
18
20
|
end
|
19
21
|
end
|
22
|
+
|
23
|
+
def truelayer_encode(payload, key, algorithm, headers)
|
24
|
+
TrueLayerEncode.new(
|
25
|
+
payload: payload,
|
26
|
+
key: key,
|
27
|
+
algorithm: algorithm,
|
28
|
+
headers: headers
|
29
|
+
).segments
|
30
|
+
end
|
31
|
+
|
32
|
+
def truelayer_decode(jwt, key, verify, options, &keyfinder)
|
33
|
+
TrueLayerDecode.new(
|
34
|
+
jwt,
|
35
|
+
key,
|
36
|
+
verify,
|
37
|
+
configuration.decode.to_h.merge(options),
|
38
|
+
&keyfinder
|
39
|
+
).decode_segments
|
40
|
+
end
|
20
41
|
end
|
@@ -9,7 +9,8 @@ module TrueLayerSigning
|
|
9
9
|
jws_header_args = { tl_headers: headers }
|
10
10
|
jws_header_args[:jku] = jws_jku if jws_jku
|
11
11
|
jws_header = TrueLayerSigning::JwsHeader.new(jws_header_args).to_h
|
12
|
-
jwt = JWT.
|
12
|
+
jwt = JWT.truelayer_encode(build_signing_payload, private_key, TrueLayerSigning.algorithm,
|
13
|
+
jws_header)
|
13
14
|
header, _, signature = jwt.split(".")
|
14
15
|
|
15
16
|
"#{header}..#{signature}"
|
@@ -27,7 +27,7 @@ module TrueLayerSigning
|
|
27
27
|
jwt_options = { algorithm: TrueLayerSigning.algorithm }
|
28
28
|
|
29
29
|
begin
|
30
|
-
JWT.
|
30
|
+
JWT.truelayer_decode(full_signature, public_key, true, jwt_options)
|
31
31
|
rescue JWT::VerificationError
|
32
32
|
@path = path.end_with?("/") && path[0...-1] || path + "/"
|
33
33
|
payload_b64 = Base64.urlsafe_encode64(build_signing_payload(ordered_headers),
|
@@ -35,7 +35,7 @@ module TrueLayerSigning
|
|
35
35
|
full_signature = [jws_header_b64, payload_b64, signature_b64].join(".")
|
36
36
|
|
37
37
|
begin
|
38
|
-
JWT.
|
38
|
+
JWT.truelayer_decode(full_signature, public_key, true, jwt_options)
|
39
39
|
rescue
|
40
40
|
raise(Error, "Signature verification failed")
|
41
41
|
end
|
@@ -369,4 +369,58 @@ class TrueLayerSigningTest < Minitest::Test
|
|
369
369
|
assert(result.first
|
370
370
|
.start_with?("POST /merchant_accounts/a61acaef-ee05-4077-92f3-25543a11bd8d/sweeping\n"))
|
371
371
|
end
|
372
|
+
|
373
|
+
# TODO: remove if/when we get rid of `lib/truelayer-signing/jwt.rb`
|
374
|
+
def test_jwt_encode_and_decode_should_succeed
|
375
|
+
payload_object = { currency: "GBP", max_amount_in_minor: 50_000_00 }
|
376
|
+
token_when_object = "eyJhbGciOiJIUzI1NiJ9.eyJjdXJyZW5jeSI6IkdCUCIsIm1heF9hbW" +
|
377
|
+
"91bnRfaW5fbWlub3IiOjUwMDAwMDB9.SjbwZCqTl6G7LQNs_M6oQhwl3a9rbqO7p3cVncLtgZY"
|
378
|
+
token_when_json = "eyJhbGciOiJIUzI1NiJ9.IntcImN1cnJlbmN5XCI6XCJHQlBcIixcIm1h" +
|
379
|
+
"eF9hbW91bnRfaW5fbWlub3JcIjo1MDAwMDAwfSI.rvCcgu-JevsNxbjUwJiFOuTd0hzVKvPK5RvGmaoDc7E"
|
380
|
+
|
381
|
+
# succeeds with a hash object
|
382
|
+
assert_equal(token_when_object, JWT.encode(payload_object, "12345", "HS256", {}))
|
383
|
+
assert_equal(
|
384
|
+
[{ "currency" => "GBP", "max_amount_in_minor" => 50_000_00 }, { "alg" => "HS256" }],
|
385
|
+
JWT.decode(token_when_object, "12345", true, algorithm: "HS256")
|
386
|
+
)
|
387
|
+
|
388
|
+
# succeeds with a JSON string
|
389
|
+
assert_equal(token_when_json, JWT.encode(payload_object.to_json, "12345", "HS256", {}))
|
390
|
+
assert_equal(
|
391
|
+
["{\"currency\":\"GBP\",\"max_amount_in_minor\":5000000}", { "alg" => "HS256" }],
|
392
|
+
JWT.decode(token_when_json, "12345", true, algorithm: "HS256")
|
393
|
+
)
|
394
|
+
end
|
395
|
+
|
396
|
+
# TODO: remove if/when we get rid of `lib/truelayer-signing/jwt.rb`
|
397
|
+
def test_jwt_truelayer_encode_and_decode_when_given_json_should_succeed
|
398
|
+
payload_json = { currency: "GBP", max_amount_in_minor: 50_000_00 }.to_json
|
399
|
+
token_when_json = "eyJhbGciOiJIUzI1NiJ9.eyJjdXJyZW5jeSI6IkdCUCIsIm1heF9hbW9" +
|
400
|
+
"1bnRfaW5fbWlub3IiOjUwMDAwMDB9.SjbwZCqTl6G7LQNs_M6oQhwl3a9rbqO7p3cVncLtgZY"
|
401
|
+
|
402
|
+
assert_equal(token_when_json, JWT.truelayer_encode(payload_json, "12345", "HS256", {}))
|
403
|
+
assert_equal(
|
404
|
+
["{\"currency\":\"GBP\",\"max_amount_in_minor\":5000000}", { "alg" => "HS256" }],
|
405
|
+
JWT.truelayer_decode(token_when_json, "12345", true, algorithm: "HS256")
|
406
|
+
)
|
407
|
+
end
|
408
|
+
|
409
|
+
# TODO: remove if/when we get rid of `lib/truelayer-signing/jwt.rb`
|
410
|
+
def test_jwt_truelayer_encode_when_given_a_hash_should_not_succeed
|
411
|
+
payload_object = { currency: "GBP", max_amount_in_minor: 50_000_00 }
|
412
|
+
error = assert_raises(TypeError) { JWT.truelayer_encode(payload_object, "12345", "HS256", {}) }
|
413
|
+
assert_equal("no implicit conversion of Hash into String", error.message)
|
414
|
+
end
|
415
|
+
|
416
|
+
# TODO: remove if/when we get rid of `lib/truelayer-signing/jwt.rb`
|
417
|
+
def test_jwt_truelayer_decode_when_given_a_hash_should_succeed
|
418
|
+
token_when_object = "eyJhbGciOiJIUzI1NiJ9.eyJjdXJyZW5jeSI6IkdCUCIsIm1heF9hbW" +
|
419
|
+
"91bnRfaW5fbWlub3IiOjUwMDAwMDB9.SjbwZCqTl6G7LQNs_M6oQhwl3a9rbqO7p3cVncLtgZY"
|
420
|
+
|
421
|
+
assert_equal(
|
422
|
+
["{\"currency\":\"GBP\",\"max_amount_in_minor\":5000000}", { "alg" => "HS256" }],
|
423
|
+
JWT.truelayer_decode(token_when_object, "12345", true, algorithm: "HS256")
|
424
|
+
)
|
425
|
+
end
|
372
426
|
end
|
data/truelayer-signing.gemspec
CHANGED
@@ -2,19 +2,19 @@ $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.1.
|
5
|
+
s.version = "0.1.2"
|
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." \
|
9
9
|
"This helps easily sign TrueLayer API requests using a JSON web signature."
|
10
10
|
s.author = "Kevin Plattret"
|
11
11
|
s.email = "kevin@truelayer.com"
|
12
|
-
s.homepage = "https://github.com/
|
12
|
+
s.homepage = "https://github.com/TrueLayer/truelayer-signing/tree/main/ruby"
|
13
13
|
s.licenses = ["Apache-2.0", "MIT"]
|
14
14
|
|
15
15
|
s.metadata = {
|
16
|
-
"bug_tracker_uri" => "https://github.com/
|
17
|
-
"changelog_uri" => "https://github.com/
|
16
|
+
"bug_tracker_uri" => "https://github.com/TrueLayer/truelayer-signing/issues",
|
17
|
+
"changelog_uri" => "https://github.com/TrueLayer/truelayer-signing/blob/main/ruby/CHANGELOG.md",
|
18
18
|
}
|
19
19
|
|
20
20
|
s.files = Dir["./**/*"]
|
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.1.
|
4
|
+
version: 0.1.2
|
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-05-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: jwt
|
@@ -122,13 +122,13 @@ files:
|
|
122
122
|
- "./lib/truelayer-signing/verifier.rb"
|
123
123
|
- "./test/test-truelayer-signing.rb"
|
124
124
|
- "./truelayer-signing.gemspec"
|
125
|
-
homepage: https://github.com/
|
125
|
+
homepage: https://github.com/TrueLayer/truelayer-signing/tree/main/ruby
|
126
126
|
licenses:
|
127
127
|
- Apache-2.0
|
128
128
|
- MIT
|
129
129
|
metadata:
|
130
|
-
bug_tracker_uri: https://github.com/
|
131
|
-
changelog_uri: https://github.com/
|
130
|
+
bug_tracker_uri: https://github.com/TrueLayer/truelayer-signing/issues
|
131
|
+
changelog_uri: https://github.com/TrueLayer/truelayer-signing/blob/main/ruby/CHANGELOG.md
|
132
132
|
post_install_message:
|
133
133
|
rdoc_options: []
|
134
134
|
require_paths:
|
@@ -144,7 +144,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
144
144
|
- !ruby/object:Gem::Version
|
145
145
|
version: '0'
|
146
146
|
requirements: []
|
147
|
-
rubygems_version: 3.4.
|
147
|
+
rubygems_version: 3.4.13
|
148
148
|
signing_key:
|
149
149
|
specification_version: 4
|
150
150
|
summary: Ruby gem to produce and verify TrueLayer API requests signatures
|