app_identity 1.0.0

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.
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "digest/sha2"
5
+
6
+ # This module should only be used by the unit tests and the test suite
7
+ # generator.
8
+ module AppIdentity::Support # :nodoc:
9
+ private
10
+
11
+ def make_app(version, fuzz = nil)
12
+ case version
13
+ when 1 then AppIdentity::App.new(v1_input(fuzz))
14
+ when 2 then AppIdentity::App.new(v2_input(fuzz))
15
+ when 3 then AppIdentity::App.new(v3_input(fuzz))
16
+ when 4 then AppIdentity::App.new(v4_input(fuzz))
17
+ end
18
+ end
19
+
20
+ def v1_input(fuzz = nil)
21
+ {version: 1, id: SecureRandom.uuid, secret: SecureRandom.hex(32)}.tap { |app|
22
+ app[:config] = {fuzz: fuzz} if fuzz
23
+ }
24
+ end
25
+
26
+ def v2_input(fuzz = nil)
27
+ v1_input(fuzz).merge(version: 2)
28
+ end
29
+
30
+ def v3_input(fuzz = nil)
31
+ v1_input(fuzz).merge(version: 3)
32
+ end
33
+
34
+ def v4_input(fuzz = nil)
35
+ v1_input(fuzz).merge(version: 4)
36
+ end
37
+
38
+ def v1(fuzz = nil)
39
+ @v1 ||= v1_input(fuzz)
40
+ end
41
+
42
+ def v1_app(fuzz = nil)
43
+ AppIdentity::App.new(v1(fuzz))
44
+ end
45
+
46
+ def v2(fuzz = nil)
47
+ @v2 ||= v2_input(fuzz)
48
+ end
49
+
50
+ def v2_app(fuzz = nil)
51
+ AppIdentity::App.new(v2)
52
+ end
53
+
54
+ def v3(fuzz = nil)
55
+ @v3 ||= v3_input(fuzz)
56
+ end
57
+
58
+ def v3_app(fuzz = nil)
59
+ AppIdentity::App.new(v3)
60
+ end
61
+
62
+ def v4(fuzz = nil)
63
+ @v4 ||= v4_input(fuzz)
64
+ end
65
+
66
+ def v4_app(fuzz = nil)
67
+ AppIdentity::App.new(v4)
68
+ end
69
+
70
+ def build_padlock(app, opts = {})
71
+ app_id = opts.delete(:id) { app.id }
72
+ nonce = opts.delete(:nonce) { "nonce" }
73
+ secret = opts.delete(:secret) { app.secret }
74
+ secret = secret.call if secret.respond_to?(:call)
75
+ version = opts.delete(:version) { app.version }
76
+
77
+ case version
78
+ when 1, 2
79
+ Digest::SHA256.hexdigest([app_id, nonce, secret].join(":")).upcase
80
+ when 3
81
+ Digest::SHA384.hexdigest([app_id, nonce, secret].join(":")).upcase
82
+ when 4
83
+ Digest::SHA512.hexdigest([app_id, nonce, secret].join(":")).upcase
84
+ end
85
+ end
86
+
87
+ def build_proof(app, padlock, opts = {})
88
+ app_id = opts.delete(:id) { app.id }
89
+ nonce = opts.delete(:nonce) { "nonce" }
90
+ version = opts.delete(:version) { 1 }
91
+
92
+ proof = version == 1 ? "#{app_id}:#{nonce}:#{padlock}" : "#{version}:#{app_id}:#{nonce}:#{padlock}"
93
+
94
+ Base64.urlsafe_encode64(proof)
95
+ end
96
+
97
+ def decode_to_parts(header)
98
+ Base64.decode64(header).split(":")
99
+ end
100
+
101
+ def timestamp_nonce(diff = nil, scale = :minutes)
102
+ ts = adjust(Time.now.utc, diff, scale)
103
+
104
+ ts.strftime("%Y%m%dT%H%M%S.%6NZ")
105
+ end
106
+
107
+ def adjust(ts, diff, scale)
108
+ return ts if diff.nil?
109
+
110
+ case scale
111
+ when :second, :seconds
112
+ ts + diff
113
+ when :minute, :minutes
114
+ ts + (diff * 60)
115
+ when :hour, :hours
116
+ ts + (diff * 60 * 60)
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(File.expand_path("../../support", __FILE__))
4
+
5
+ gem "minitest"
6
+
7
+ require "minitest/autorun"
8
+
9
+ require "app_identity"
10
+ require "app_identity/support"
11
+
12
+ module Minitest::AppIdentityExtensions
13
+ def assert_exception_message(message, *types, &block)
14
+ msg = types.last if types.last.is_a?(String)
15
+
16
+ types = [AppIdentity::Error] if types.empty?
17
+
18
+ result = assert_raises(*types, &block)
19
+ assert_equal message, result.message, msg
20
+ end
21
+
22
+ Minitest::Test.send(:include, self)
23
+ Minitest::Test.send(:include, AppIdentity::Support)
24
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest_helper"
4
+
5
+ class TestAppIdentity < Minitest::Test
6
+ def subject
7
+ @subject ||= AppIdentity::Internal.send(:new)
8
+ end
9
+
10
+ def test_generate_v1_proof
11
+ proof = subject.generate_proof!(v1)
12
+ refute_nil proof
13
+ assert_equal 3, decode_to_parts(proof).length
14
+ end
15
+
16
+ def test_generate_valid_v1_proof
17
+ proof = subject.generate_proof!(v1)
18
+ assert_equal v1_app.verify, subject.verify_proof!(proof, v1)
19
+ end
20
+
21
+ def test_generate_v2_proof
22
+ proof = subject.generate_proof!(v2_app)
23
+ refute_nil proof
24
+ assert_equal 4, decode_to_parts(proof).length
25
+ end
26
+
27
+ def test_generate_valid_v2_proof
28
+ proof = subject.generate_proof!(v2)
29
+ assert_equal v2_app.verify, subject.verify_proof!(proof, v2)
30
+ end
31
+
32
+ def test_generate_valid_v3_proof
33
+ proof = subject.generate_proof!(v3)
34
+ assert_equal v3_app.verify, subject.verify_proof!(proof, v3)
35
+ end
36
+
37
+ def test_verify_fails_if_not_base64
38
+ assert_exception_message("proof must have 3 parts (version 1) or 4 parts (any version)") {
39
+ subject.verify_proof!("not base64", v1)
40
+ }
41
+ end
42
+
43
+ def test_verify_fail_on_insufficent_parts
44
+ assert_exception_message("proof must have 3 parts (version 1) or 4 parts (any version)") {
45
+ subject.verify_proof!(Base64.urlsafe_encode64("a:b"), v1)
46
+ }
47
+ end
48
+
49
+ def test_verify_fail_on_bad_v1_nonce
50
+ padlock = build_padlock(v1_app, nonce: "n:once")
51
+ proof = build_proof(v1_app, padlock, nonce: "n:once")
52
+
53
+ assert_exception_message("version cannot be converted to an integer") {
54
+ subject.verify_proof!(proof, v1)
55
+ }
56
+ end
57
+
58
+ def test_verify_fail_on_bad_v2_nonce_format
59
+ padlock = build_padlock(v1_app)
60
+ proof = build_proof(v1_app, padlock, version: 2)
61
+
62
+ assert_exception_message("nonce does not look like a timestamp") {
63
+ subject.verify_proof!(proof, v1)
64
+ }
65
+ end
66
+
67
+ def test_verify_fail_on_v2_nonce_out_of_fuzz
68
+ nonce = timestamp_nonce(-11, :minutes)
69
+ padlock = build_padlock(v1_app, nonce: nonce, version: 2)
70
+ proof = build_proof(v1_app, padlock, version: 2, nonce: nonce)
71
+
72
+ assert_exception_message("nonce is invalid") {
73
+ subject.verify_proof!(proof, v1)
74
+ }
75
+ end
76
+
77
+ def test_verify_fail_on_v1_nonce_for_v2_app
78
+ padlock = build_padlock(v2_app, version: 1)
79
+ proof = build_proof(v2_app, padlock, version: 1)
80
+
81
+ assert_exception_message("proof and app version mismatch") {
82
+ subject.verify_proof!(proof, v2)
83
+ }
84
+ end
85
+
86
+ def test_verify_success_v1
87
+ padlock = build_padlock(v1_app)
88
+ proof = build_proof(v1_app, padlock)
89
+
90
+ assert_equal v1_app.verify, subject.verify_proof!(proof, v1)
91
+ end
92
+
93
+ def test_verify_success_v2_default_fuzz
94
+ nonce = timestamp_nonce(-6, :minutes)
95
+ padlock = build_padlock(v2_app, nonce: nonce)
96
+ proof = build_proof(v2_app, padlock, version: 2, nonce: nonce)
97
+
98
+ assert_equal v2_app.verify, subject.verify_proof!(proof, v2)
99
+ end
100
+
101
+ def test_verify_success_v2_custom_fuzz
102
+ nonce = timestamp_nonce(-2, :minutes)
103
+ padlock = build_padlock(v2_app(300), nonce: nonce)
104
+ proof = build_proof(v2_app, padlock, version: 2, nonce: nonce)
105
+
106
+ assert_equal v2_app.verify, subject.verify_proof!(proof, v2)
107
+ end
108
+
109
+ def test_verify_fail_on_different_app_ids
110
+ padlock = build_padlock(v1_app, id: "00000000-0000-0000-0000-000000000000")
111
+ proof = build_proof(v1_app, padlock, id: "00000000-0000-0000-0000-000000000000")
112
+
113
+ assert_exception_message("proof and app do not match") {
114
+ subject.verify_proof!(proof, v1)
115
+ }
116
+ end
117
+
118
+ def test_verify_fail_on_bad_padlock
119
+ padlock = build_padlock(v1_app, nonce: "foo")
120
+ proof = build_proof(v1_app, padlock)
121
+
122
+ assert_nil subject.verify_proof!(proof, v1)
123
+ end
124
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest_helper"
4
+
5
+ class TestAppIdentityApp < Minitest::Test
6
+ def subject
7
+ AppIdentity::App
8
+ end
9
+
10
+ def test_fails_with_no_app_and_no_block
11
+ assert_exception_message("app cannot be created from input (missing value :id)") {
12
+ subject.new(Object.new)
13
+ }
14
+ end
15
+
16
+ def test_id_validation
17
+ assert_exception_message("id must not be nil") {
18
+ subject.new({id: nil})
19
+ }
20
+ assert_exception_message("id must not be an empty string") {
21
+ subject.new({id: ""})
22
+ }
23
+ assert_exception_message("id must not contain colons") {
24
+ subject.new({id: "1:1"})
25
+ }
26
+ end
27
+
28
+ def test_secret_validation
29
+ assert_exception_message("secret must not be nil") {
30
+ subject.new({id: 1, secret: nil})
31
+ }
32
+ assert_exception_message("secret must not be an empty string") {
33
+ subject.new({id: 1, secret: ""})
34
+ }
35
+ assert_exception_message("secret must be a binary string value") {
36
+ subject.new({id: 1, secret: 3})
37
+ }
38
+ end
39
+
40
+ def test_version_validation
41
+ assert_exception_message("version must not be nil") {
42
+ subject.new({id: 1, secret: "a", version: nil})
43
+ }
44
+ assert_exception_message("version cannot be converted to an integer") {
45
+ subject.new({id: 1, secret: "a", version: ""})
46
+ }
47
+ assert_exception_message("version cannot be converted to an integer") {
48
+ subject.new({id: 1, secret: "a", version: "3.5"})
49
+ }
50
+ assert_exception_message("version must be a positive integer") {
51
+ subject.new({id: 1, secret: "a", version: 3.5})
52
+ }
53
+ end
54
+
55
+ def test_config_validation
56
+ assert_exception_message("config must be nil or a map") {
57
+ subject.new({id: 1, secret: "a", version: 1, config: 3})
58
+ }
59
+ end
60
+
61
+ def test_create_from_block
62
+ assert subject.new(-> { {id: 1, secret: "a", version: 1} }).frozen?
63
+ end
64
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest_helper"
4
+
5
+ require "app_identity/rack_middleware"
6
+ require "rack/test"
7
+ require "rack/mock"
8
+
9
+ class TestKineticApplicationRackMiddleware < Minitest::Test
10
+ include Rack::Test::Methods
11
+
12
+ HEADER = "HTTP_X_APP_IDENTITY"
13
+
14
+ attr_reader :options
15
+
16
+ def setup
17
+ @options = {headers: ["x-app-identity"], apps: [v1]}
18
+ end
19
+
20
+ def app
21
+ options = @options
22
+ @_app ||= Rack::Builder.new do
23
+ use AppIdentity::RackMiddleware, options
24
+
25
+ run ->(_) { [200, {"Content-Type" => "text/plain"}, ["OK"]] }
26
+ end
27
+ end
28
+
29
+ def test_empty_headers
30
+ get "/"
31
+ refute last_response.successful?
32
+ end
33
+
34
+ def test_invalid_proof
35
+ get "/", {}, {HEADER => "invalid proof"}
36
+ refute last_response.successful?
37
+ end
38
+
39
+ def test_invalid_app_header
40
+ get "/", {}, {HEADER => AppIdentity.generate_proof(v2)}
41
+ refute last_response.successful?
42
+ end
43
+
44
+ def test_valid_app_header
45
+ get "/", {}, {HEADER => AppIdentity.generate_proof(v1)}
46
+ assert last_response.successful?
47
+ end
48
+
49
+ def test_valid_app_v2_header
50
+ options[:apps] = [v2]
51
+ get "/", {}, {HEADER => AppIdentity.generate_proof(v2)}
52
+ assert last_response.successful?
53
+ end
54
+
55
+ def test_valid_app_v3_header
56
+ options[:apps] = [v3]
57
+ get "/", {}, {HEADER => AppIdentity.generate_proof(v3)}
58
+ assert last_response.successful?
59
+ end
60
+
61
+ def test_valid_app_v4_header
62
+ options[:apps] = [v4]
63
+ get "/", {}, {HEADER => AppIdentity.generate_proof(v4)}
64
+ assert last_response.successful?
65
+ end
66
+
67
+ def test_valid_app_header_from_list
68
+ options[:apps] = [v1_app, v2_app]
69
+ get "/", {}, {HEADER => AppIdentity.generate_proof(v2)}
70
+ assert last_response.successful?
71
+ end
72
+
73
+ def test_valid_app_multiple_headers
74
+ options[:apps] = [v1_app, v2_app]
75
+ options[:headers] = %w[x-app-identity app-identity]
76
+
77
+ get "/", {}, {
78
+ HEADER => AppIdentity.generate_proof(v2),
79
+ "HTTP_APP_IDENTITY" => AppIdentity.generate_proof(v1)
80
+ }
81
+
82
+ assert last_response.successful?
83
+ end
84
+
85
+ def test_alternate_header
86
+ options[:headers] = ["identity-widget"]
87
+ get "/", {}, {"HTTP_IDENTITY_WIDGET" => AppIdentity.generate_proof(v1)}
88
+ assert last_response.successful?
89
+ end
90
+ end