app_identity 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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