soar_authentication_token 4.0.1 → 5.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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/README.md +8 -8
- data/bin/rotate-configs +23 -0
- data/docker-compose.yml +2 -1
- data/lib/soar_authentication_token.rb +1 -0
- data/lib/soar_authentication_token/config_rotator.rb +119 -0
- data/lib/soar_authentication_token/providers/jwt_token_generator.rb +4 -6
- data/lib/soar_authentication_token/providers/jwt_token_validator.rb +24 -26
- data/lib/soar_authentication_token/token_validator.rb +1 -1
- data/lib/soar_authentication_token/version.rb +1 -1
- data/spec/config_rotator_spec.rb +283 -0
- data/spec/jwt_token_validator_spec.rb +291 -0
- data/spec/rack_middleware_spec.rb +3 -3
- data/spec/remote_token_validator_spec.rb +78 -0
- data/spec/static_token_validator_spec.rb +104 -0
- data/spec/token_generator_spec.rb +9 -5
- data/spec/token_validator_spec.rb +1 -272
- metadata +13 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a52f85114800057ab0311580dcaa924d9abb73cb
|
4
|
+
data.tar.gz: 147f69366980200dc8e695621a1884099ab3b3dd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 05a4b09349427a6dc587f0ec6c968ef3b25fbed863e801c5148f3ff98499dfb1950e01f17905778dfbf05641cc5361e3fb17b4efcb47915a5bcda6e3f2129699
|
7
|
+
data.tar.gz: 2d436a9426bbb03364391785061c30742c8d62b3283895629a2269017a1c37adfe5132f72f485bad54870f4ef332de9744d2b56a19067f8e1918a0546a4e2d4f
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -43,9 +43,9 @@ In this mode the StaticTokenValidator is configured with a list of preconfigured
|
|
43
43
|
Run the rspec test tests using docker compose:
|
44
44
|
|
45
45
|
```bash
|
46
|
-
docker-compose build --force-rm --no-cache
|
46
|
+
export UID; docker-compose build --force-rm --no-cache
|
47
47
|
docker-compose down
|
48
|
-
docker-compose run --rm soar-authentication-token
|
48
|
+
export UID; docker-compose run --rm soar-authentication-token
|
49
49
|
docker-compose down
|
50
50
|
```
|
51
51
|
|
@@ -56,7 +56,7 @@ For test purposes this repo relies on various other repos with services. This i
|
|
56
56
|
```bash
|
57
57
|
git pull && git submodule foreach 'git fetch origin --tags; git checkout master; git pull'
|
58
58
|
docker-compose down
|
59
|
-
docker-compose build
|
59
|
+
docker-compose build --force-rm --no-cache
|
60
60
|
```
|
61
61
|
|
62
62
|
## Usage
|
@@ -78,7 +78,7 @@ First step is to configure the middleware. Actually you are not configuring the
|
|
78
78
|
|
79
79
|
```ruby
|
80
80
|
@iut_configuration = {
|
81
|
-
'provider' => 'RemoteTokenValidator',
|
81
|
+
'provider' => 'SoarAuthenticationToken::RemoteTokenValidator',
|
82
82
|
'validator-url' => 'http://authentication-token-validator-service:9393/validate'
|
83
83
|
}
|
84
84
|
@iut = SoarAuthenticationToken::RackMiddleware.new(@test_app, @iut_configuration)
|
@@ -95,7 +95,7 @@ The class generates tokens or requests a token from a generator service as confi
|
|
95
95
|
For remote token generation, configure as such:
|
96
96
|
```ruby
|
97
97
|
@configuration_remote = {
|
98
|
-
'provider' => 'RemoteTokenGenerator',
|
98
|
+
'provider' => 'SoarAuthenticationToken::RemoteTokenGenerator',
|
99
99
|
'generator-url' => 'http://authentication-token-generator-service:9393/generate',
|
100
100
|
'generator-client-auth-token' => 'xxxx'
|
101
101
|
}
|
@@ -106,7 +106,7 @@ For local token generation, configure as such:
|
|
106
106
|
generator = SoarAuthenticationToken::KeypairGenerator.new
|
107
107
|
private_key, public_key = generator.generate
|
108
108
|
@configuration_local = {
|
109
|
-
'provider' => '
|
109
|
+
'provider' => 'SoarAuthenticationToken::JwtTokenValidator',
|
110
110
|
'private_key' => private_key
|
111
111
|
}
|
112
112
|
```
|
@@ -132,7 +132,7 @@ The class validates tokens or requests validation of a token from a validation s
|
|
132
132
|
For remote token validation, configure as such:
|
133
133
|
```ruby
|
134
134
|
@configuration_remote = {
|
135
|
-
'provider' => 'RemoteTokenGenerator',
|
135
|
+
'provider' => 'SoarAuthenticationToken::RemoteTokenGenerator',
|
136
136
|
'validator-url' => 'http://authentication-token-validator-service:9393/validate',
|
137
137
|
}
|
138
138
|
```
|
@@ -142,7 +142,7 @@ For local token validation, configure as such:
|
|
142
142
|
generator = SoarAuthenticationToken::KeypairGenerator.new
|
143
143
|
private_key, public_key = generator.generate
|
144
144
|
@configuration_local = {
|
145
|
-
'provider' => 'JwtTokenValidator',
|
145
|
+
'provider' => 'SoarAuthenticationToken::JwtTokenValidator',
|
146
146
|
'public_key' => public_key
|
147
147
|
}
|
148
148
|
```
|
data/bin/rotate-configs
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "pathname"
|
3
|
+
bin_file = Pathname.new(__FILE__).realpath
|
4
|
+
$:.unshift File.expand_path("../../lib", bin_file)
|
5
|
+
|
6
|
+
require 'soar_authentication_token'
|
7
|
+
require 'thor'
|
8
|
+
|
9
|
+
class RotateConfigCLI < Thor
|
10
|
+
desc "rotate [OPTIONS]", "rotate configurations"
|
11
|
+
option :generator_config_file, :aliases => '-g', :desc => 'Configuration file of the generator'
|
12
|
+
option :validator_config_file, :aliases => '-v', :desc => 'Configuration file of the validator'
|
13
|
+
def rotate
|
14
|
+
raise 'generator_config_file must be specified' unless options['generator_config_file']
|
15
|
+
|
16
|
+
rotator = SoarAuthenticationToken::ConfigRotator.new
|
17
|
+
rotator.rotate_json_config_files(generator_file_name: options['generator_config_file'],
|
18
|
+
validator_file_name: options['validator_config_file'])
|
19
|
+
end
|
20
|
+
default_task :rotate
|
21
|
+
end
|
22
|
+
|
23
|
+
RotateConfigCLI.start(ARGV)
|
data/docker-compose.yml
CHANGED
@@ -7,6 +7,7 @@ require 'soar_authentication_token/providers/remote_token_generator'
|
|
7
7
|
require 'soar_authentication_token/providers/remote_token_validator'
|
8
8
|
require 'soar_authentication_token/providers/static_token_validator'
|
9
9
|
require 'soar_authentication_token/keypair_generator'
|
10
|
+
require 'soar_authentication_token/config_rotator'
|
10
11
|
require 'soar_authentication_token/token_generator'
|
11
12
|
require 'soar_authentication_token/token_validator'
|
12
13
|
require 'soar_authentication_token/rack_middleware'
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module SoarAuthenticationToken
|
4
|
+
class ConfigRotator
|
5
|
+
attr_accessor :maximum_number_of_public_keys
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@maximum_number_of_public_keys = 3
|
9
|
+
end
|
10
|
+
|
11
|
+
def rotate_json_config_files(generator_file_name:, validator_file_name:)
|
12
|
+
generator_config = JSON.parse(File.read(generator_file_name))
|
13
|
+
validator_config = JSON.parse(File.read(validator_file_name))
|
14
|
+
generator_config, validator_config = rotate_configs(generator_config: generator_config,
|
15
|
+
validator_config: validator_config)
|
16
|
+
File.open(generator_file_name,"w") do |f|
|
17
|
+
f.write(JSON.pretty_generate generator_config)
|
18
|
+
f.close
|
19
|
+
end
|
20
|
+
File.open(validator_file_name,"w") do |f|
|
21
|
+
f.write(JSON.pretty_generate validator_config)
|
22
|
+
f.close
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def rotate_configs(generator_config:, validator_config:)
|
27
|
+
private_key, public_key = KeypairGenerator.new.generate
|
28
|
+
key_description = generate_keypair_description
|
29
|
+
updated_generator_config = rotate_generator_config(config: generator_config,new_private_key: private_key,new_key_description: key_description)
|
30
|
+
updated_validator_config = rotate_validator_config(config: validator_config,new_public_key: public_key, new_key_description: key_description)
|
31
|
+
raise 'generated configuration does not match' unless configurations_match_and_valid?(generator_config: updated_generator_config,validator_config: updated_validator_config)
|
32
|
+
[updated_generator_config, updated_validator_config]
|
33
|
+
end
|
34
|
+
|
35
|
+
def rotate_generator_config(config: ,new_private_key: ,new_key_description:)
|
36
|
+
validate_generator_config(config)
|
37
|
+
new_config = config.dup
|
38
|
+
new_config['private_key'] = new_private_key
|
39
|
+
new_config['key_description'] = new_key_description
|
40
|
+
new_config
|
41
|
+
end
|
42
|
+
|
43
|
+
def rotate_validator_config(config: ,new_public_key: ,new_key_description:)
|
44
|
+
validate_validator_config(config)
|
45
|
+
new_config = config.dup
|
46
|
+
trim_public_keys(new_config)
|
47
|
+
new_config['keys'][new_key_description] = { 'public_key' => new_public_key }
|
48
|
+
new_config
|
49
|
+
end
|
50
|
+
|
51
|
+
def configurations_match_and_valid?(generator_config:,validator_config:)
|
52
|
+
validate_generator_config(generator_config)
|
53
|
+
validate_validator_config(validator_config)
|
54
|
+
test_token = generate_test_token(generator_config)
|
55
|
+
validate_test_token(validator_config,test_token)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def trim_public_keys(config)
|
61
|
+
traversed_keys = 0
|
62
|
+
config['keys'].sort.reverse_each do |key_name, key_data|
|
63
|
+
traversed_keys += 1
|
64
|
+
config['keys'].delete(key_name) if traversed_keys >= @maximum_number_of_public_keys
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def generate_keypair_description
|
69
|
+
"KEYPAIR_#{Time.now.strftime("%Y%m%dT%H%M%S")}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def generate_test_token(generator_config)
|
73
|
+
generator = SoarAuthenticationToken::TokenGenerator.new(generator_config)
|
74
|
+
generator.inject_store_provider(DummyStorageClient.new)
|
75
|
+
token, meta = generator.generate(authenticated_identifier: 'test')
|
76
|
+
token
|
77
|
+
end
|
78
|
+
|
79
|
+
def validate_test_token(validator_config,test_token)
|
80
|
+
validator = SoarAuthenticationToken::TokenValidator.new(validator_config)
|
81
|
+
validator.inject_store_provider(DummyStorageClient.new)
|
82
|
+
validity, meta, message = validator.validate(authentication_token: test_token)
|
83
|
+
validity
|
84
|
+
end
|
85
|
+
|
86
|
+
def validate_generator_config(config)
|
87
|
+
raise ArgumentError, 'private_key not in config' unless config['private_key']
|
88
|
+
end
|
89
|
+
|
90
|
+
def validate_validator_config(config)
|
91
|
+
raise ArgumentError, 'keys not in config' unless config['keys']
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
class DummyStorageClient
|
97
|
+
def initialize(configuration = {})
|
98
|
+
end
|
99
|
+
def store_failure(state = true)
|
100
|
+
end
|
101
|
+
def store_failure?
|
102
|
+
false
|
103
|
+
end
|
104
|
+
def add(token_identifier:, authenticated_identifier:, token_issue_time:, token_expiry_time:, flow_identifier: nil)
|
105
|
+
end
|
106
|
+
def remove(token_identifier:, flow_identifier: nil)
|
107
|
+
true
|
108
|
+
end
|
109
|
+
def token_exist?(token_identifier:, authenticated_identifier:, token_issue_time:, token_expiry_time:, flow_identifier: nil)
|
110
|
+
true
|
111
|
+
end
|
112
|
+
def remove_tokens_for(authenticated_identifier:, flow_identifier: nil)
|
113
|
+
true
|
114
|
+
end
|
115
|
+
def list_tokens_for(authenticated_identifier:, flow_identifier: nil)
|
116
|
+
nil
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -2,8 +2,6 @@ require 'soar_xt'
|
|
2
2
|
require 'jwt'
|
3
3
|
require 'securerandom'
|
4
4
|
require 'time'
|
5
|
-
require 'json'
|
6
|
-
require 'authenticated_client'
|
7
5
|
|
8
6
|
module SoarAuthenticationToken
|
9
7
|
class JwtTokenGenerator
|
@@ -13,7 +11,7 @@ module SoarAuthenticationToken
|
|
13
11
|
|
14
12
|
def initialize(configuration)
|
15
13
|
@configuration = merge_with_default_configuration(configuration)
|
16
|
-
|
14
|
+
validate_configuration
|
17
15
|
@private_key = OpenSSL::PKey::EC.new(@configuration['private_key'])
|
18
16
|
end
|
19
17
|
|
@@ -43,9 +41,9 @@ module SoarAuthenticationToken
|
|
43
41
|
JWT.encode(meta, @private_key, 'ES512')
|
44
42
|
end
|
45
43
|
|
46
|
-
def
|
47
|
-
raise "'private_key' must be configured
|
48
|
-
raise "'expiry' must be configured
|
44
|
+
def validate_configuration
|
45
|
+
raise "'private_key' must be configured" unless @configuration['private_key']
|
46
|
+
raise "'expiry' must be configured" unless @configuration['expiry']
|
49
47
|
raise "'expiry' must be an integer" unless Integer(@configuration['expiry'])
|
50
48
|
end
|
51
49
|
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'jwt'
|
2
|
-
require 'authenticated_client'
|
3
2
|
|
4
3
|
module SoarAuthenticationToken
|
5
4
|
class JwtTokenValidator
|
@@ -7,8 +6,6 @@ module SoarAuthenticationToken
|
|
7
6
|
@configuration = configuration
|
8
7
|
set_configuration_defaults
|
9
8
|
validate_configuration
|
10
|
-
@public_key = OpenSSL::PKey::EC.new(@configuration['public_key'])
|
11
|
-
@public_key.private_key = nil
|
12
9
|
end
|
13
10
|
|
14
11
|
def inject_store_provider(store_provider)
|
@@ -16,12 +13,12 @@ module SoarAuthenticationToken
|
|
16
13
|
end
|
17
14
|
|
18
15
|
def validate(authentication_token:,flow_identifier: nil)
|
19
|
-
|
16
|
+
decoded_token_payload = decode(authentication_token)
|
17
|
+
return rejection_result(reason: 'Token decode/verification failure') if decoded_token_payload.nil?
|
18
|
+
meta = compile_meta_from_payload(decoded_token_payload)
|
20
19
|
return rejection_result(reason: "Expired token <#{meta['token_expiry_time']}> for <#{meta['authenticated_identifier']}>") if token_expired?(meta)
|
21
20
|
return rejection_result(reason: "Unknown token for <#{meta['authenticated_identifier']}>") unless token_exist_in_store?(meta,flow_identifier)
|
22
21
|
success_result(token_meta: meta)
|
23
|
-
rescue JWT::VerificationError, JWT::DecodeError
|
24
|
-
rejection_result(reason: 'Token decode/verification failure')
|
25
22
|
end
|
26
23
|
|
27
24
|
private
|
@@ -30,24 +27,13 @@ module SoarAuthenticationToken
|
|
30
27
|
@configuration['expiry'] ||= 86400
|
31
28
|
end
|
32
29
|
|
33
|
-
def
|
34
|
-
decoded_token_payload = decode(authentication_token)
|
35
|
-
compile_meta(token_identifier: decoded_token_payload[0]['token_identifier'],
|
36
|
-
authenticated_identifier: decoded_token_payload[0]['authenticated_identifier'],
|
37
|
-
token_issue_time: decoded_token_payload[0]['token_issue_time'],
|
38
|
-
token_expiry_time: decoded_token_payload[0]['token_expiry_time'])
|
39
|
-
end
|
40
|
-
|
41
|
-
def compile_meta(token_identifier:,
|
42
|
-
authenticated_identifier:,
|
43
|
-
token_issue_time:,
|
44
|
-
token_expiry_time:)
|
30
|
+
def compile_meta_from_payload(decoded_token_payload)
|
45
31
|
{
|
46
|
-
'token_identifier' => token_identifier,
|
47
|
-
'authenticated_identifier' => authenticated_identifier,
|
48
|
-
'token_issue_time' => token_issue_time,
|
49
|
-
'token_expiry_time' => token_expiry_time,
|
50
|
-
'token_age' => token_age(token_issue_time)
|
32
|
+
'token_identifier' => decoded_token_payload[0]['token_identifier'],
|
33
|
+
'authenticated_identifier' => decoded_token_payload[0]['authenticated_identifier'],
|
34
|
+
'token_issue_time' => decoded_token_payload[0]['token_issue_time'],
|
35
|
+
'token_expiry_time' => decoded_token_payload[0]['token_expiry_time'],
|
36
|
+
'token_age' => token_age(decoded_token_payload[0]['token_issue_time'])
|
51
37
|
}
|
52
38
|
end
|
53
39
|
|
@@ -56,13 +42,25 @@ module SoarAuthenticationToken
|
|
56
42
|
end
|
57
43
|
|
58
44
|
def validate_configuration
|
59
|
-
raise "'
|
60
|
-
raise "'expiry' must be configured
|
45
|
+
raise "'keys' must be configured" unless @configuration['keys']
|
46
|
+
raise "'expiry' must be configured" unless @configuration['expiry']
|
61
47
|
raise "'expiry' must be an integer" unless Integer(@configuration['expiry'])
|
62
48
|
end
|
63
49
|
|
64
50
|
def decode(authentication_token)
|
65
|
-
|
51
|
+
@configuration['keys'].sort.reverse_each do |key_name, key_data|
|
52
|
+
payload = attempt_decode_using_a_key(authentication_token,key_data)
|
53
|
+
return payload if payload
|
54
|
+
end
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
|
58
|
+
def attempt_decode_using_a_key(authentication_token,key_data)
|
59
|
+
public_key = OpenSSL::PKey::EC.new(key_data['public_key'])
|
60
|
+
public_key.private_key = nil
|
61
|
+
JWT.decode(authentication_token, public_key, true, { :algorithm => 'ES512' })
|
62
|
+
rescue JWT::VerificationError, JWT::DecodeError
|
63
|
+
nil
|
66
64
|
end
|
67
65
|
|
68
66
|
def token_expired?(meta)
|
@@ -18,7 +18,7 @@ module SoarAuthenticationToken
|
|
18
18
|
private
|
19
19
|
|
20
20
|
def instantiate_provider
|
21
|
-
@provider = Object::const_get(
|
21
|
+
@provider = Object::const_get(@configuration['provider']).new(@configuration)
|
22
22
|
end
|
23
23
|
|
24
24
|
def validate_configuration
|
@@ -0,0 +1,283 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SoarAuthenticationToken::ConfigRotator do
|
4
|
+
subject { SoarAuthenticationToken::ConfigRotator.new }
|
5
|
+
|
6
|
+
before :all do
|
7
|
+
keypair_generator = SoarAuthenticationToken::KeypairGenerator.new
|
8
|
+
@private_key_1, @public_key_1 = keypair_generator.generate
|
9
|
+
@private_key_2, @public_key_2 = keypair_generator.generate
|
10
|
+
@private_key_3, @public_key_3 = keypair_generator.generate
|
11
|
+
@private_key_4, @public_key_4 = keypair_generator.generate
|
12
|
+
|
13
|
+
@valid_validator_config = {
|
14
|
+
'provider' => 'SoarAuthenticationToken::JwtTokenValidator',
|
15
|
+
'keys' => {
|
16
|
+
'KEYPAIR_20160107T230101' => { 'public_key' => @public_key_1 },
|
17
|
+
'KEYPAIR_20160107T230201' => { 'public_key' => @public_key_2 },
|
18
|
+
'KEYPAIR_20160107T230301' => { 'public_key' => @public_key_3 }
|
19
|
+
}
|
20
|
+
}
|
21
|
+
@valid_generator_config = {
|
22
|
+
'provider' => 'SoarAuthenticationToken::JwtTokenGenerator',
|
23
|
+
'private_key' => @private_key_3,
|
24
|
+
'key_description' => 'original key'
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'has a version number' do
|
29
|
+
expect(SoarAuthenticationToken::VERSION).not_to be nil
|
30
|
+
end
|
31
|
+
|
32
|
+
context "when trimming public keys in the validator configuration to one less than maximum allowed" do
|
33
|
+
|
34
|
+
context "with key list containing no keys" do
|
35
|
+
let!(:validator_configuration) {
|
36
|
+
{
|
37
|
+
'keys' => { }
|
38
|
+
}
|
39
|
+
}
|
40
|
+
it 'the resulting list is kept intact with no keys' do
|
41
|
+
test_configuration = validator_configuration.dup
|
42
|
+
subject.send(:trim_public_keys, test_configuration)
|
43
|
+
expect(test_configuration).to eq(
|
44
|
+
{
|
45
|
+
'keys' => { }
|
46
|
+
})
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "with key list containing lower than maximum allowed number of keys" do
|
51
|
+
let!(:validator_configuration) {
|
52
|
+
{
|
53
|
+
'keys' => {
|
54
|
+
'KEYPAIR_20160107T230001' => [],
|
55
|
+
'KEYPAIR_20160107T230101' => []
|
56
|
+
}
|
57
|
+
}
|
58
|
+
}
|
59
|
+
it 'the resulting list is kept intact' do
|
60
|
+
test_configuration = validator_configuration.dup
|
61
|
+
subject.send(:trim_public_keys, test_configuration)
|
62
|
+
expect(test_configuration).to eq( {
|
63
|
+
'keys' => {
|
64
|
+
'KEYPAIR_20160107T230001' => [],
|
65
|
+
'KEYPAIR_20160107T230101' => []
|
66
|
+
}
|
67
|
+
})
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "with key list containing the maximum allowed number of keys" do
|
72
|
+
let!(:validator_configuration) {
|
73
|
+
{
|
74
|
+
'keys' => {
|
75
|
+
'KEYPAIR_20160107T230001' => [],
|
76
|
+
'KEYPAIR_20160107T230101' => [],
|
77
|
+
'KEYPAIR_20160107T230201' => []
|
78
|
+
}
|
79
|
+
}
|
80
|
+
}
|
81
|
+
it 'the key named with the lowest string value (ordered alphabetically) is removed' do
|
82
|
+
test_configuration = validator_configuration.dup
|
83
|
+
subject.send(:trim_public_keys, test_configuration)
|
84
|
+
expect(test_configuration).to eq( {
|
85
|
+
'keys' => {
|
86
|
+
'KEYPAIR_20160107T230101' => [],
|
87
|
+
'KEYPAIR_20160107T230201' => []
|
88
|
+
}
|
89
|
+
})
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context "with key list containing more than the maximum allowed number of keys" do
|
94
|
+
let!(:validator_configuration) {
|
95
|
+
{
|
96
|
+
'keys' => {
|
97
|
+
'KEYPAIR_20160107T230001' => [],
|
98
|
+
'KEYPAIR_20160107T230401' => [],
|
99
|
+
'KEYPAIR_20160107T230201' => [],
|
100
|
+
'KEYPAIR_20160107T230101' => []
|
101
|
+
}
|
102
|
+
}
|
103
|
+
}
|
104
|
+
it 'the keys named with the lowest string values (ordered alphabetically) is removed until the it contains one less than the maximum allowed' do
|
105
|
+
test_configuration = validator_configuration.dup
|
106
|
+
subject.send(:trim_public_keys, test_configuration)
|
107
|
+
expect(test_configuration).to eq( {
|
108
|
+
'keys' => {
|
109
|
+
'KEYPAIR_20160107T230201' => [],
|
110
|
+
'KEYPAIR_20160107T230401' => []
|
111
|
+
}
|
112
|
+
})
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context "when rotating key pairs" do
|
118
|
+
context "with invalid generator configuration in a json formated file" do
|
119
|
+
let!(:validator_config_file_name) {
|
120
|
+
filename = "validator_config.json"
|
121
|
+
File.open(filename,"w") do |f|
|
122
|
+
f.write(@valid_validator_config.to_json)
|
123
|
+
end
|
124
|
+
filename
|
125
|
+
}
|
126
|
+
let!(:generator_config_file_name) {
|
127
|
+
filename = "generator_config.json"
|
128
|
+
File.open(filename,"w") do |f|
|
129
|
+
f.write({}.to_json) #Empty hash is an Invalid hash
|
130
|
+
end
|
131
|
+
filename
|
132
|
+
}
|
133
|
+
it 'raises an error indicating the generator configuration is invalid' do
|
134
|
+
expect { subject.rotate_json_config_files(generator_file_name: generator_config_file_name,
|
135
|
+
validator_file_name: validator_config_file_name)
|
136
|
+
}.to raise_error(ArgumentError, /private_key not in config/)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
context "with invalid validator configuration in a json formated file" do
|
141
|
+
let!(:validator_config_file_name) {
|
142
|
+
filename = "validator_config.json"
|
143
|
+
File.open(filename,"w") do |f|
|
144
|
+
f.write({}.to_json) #Empty hash is an invalid hash
|
145
|
+
end
|
146
|
+
filename
|
147
|
+
}
|
148
|
+
let!(:generator_config_file_name) {
|
149
|
+
filename = "generator_config.json"
|
150
|
+
File.open(filename,"w") do |f|
|
151
|
+
f.write(@valid_generator_config.to_json)
|
152
|
+
end
|
153
|
+
filename
|
154
|
+
}
|
155
|
+
it 'raises an error indicating the validator configuration is invalid' do
|
156
|
+
expect { subject.rotate_json_config_files(generator_file_name: generator_config_file_name,
|
157
|
+
validator_file_name: validator_config_file_name)
|
158
|
+
}.to raise_error(ArgumentError, /keys not in config/)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
context "with valid generator/validator configurations in a json formated file" do
|
163
|
+
let!(:validator_config_file_name) {
|
164
|
+
filename = "validator_config.json"
|
165
|
+
File.open(filename,"w") do |f|
|
166
|
+
f.write(@valid_validator_config.to_json)
|
167
|
+
end
|
168
|
+
filename
|
169
|
+
}
|
170
|
+
let!(:generator_config_file_name) {
|
171
|
+
filename = "generator_config.json"
|
172
|
+
File.open(filename,"w") do |f|
|
173
|
+
f.write(@valid_generator_config.to_json)
|
174
|
+
end
|
175
|
+
filename
|
176
|
+
}
|
177
|
+
it 'replaces the newly generated private key to the generator configuration' do
|
178
|
+
subject.rotate_json_config_files(generator_file_name: generator_config_file_name,
|
179
|
+
validator_file_name: validator_config_file_name)
|
180
|
+
generator_config = JSON.parse(File.read(generator_config_file_name))
|
181
|
+
expect(generator_config['private_key']).to_not eq(@valid_generator_config['private_key'])
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'adds the newly generated public key to the validator configuration' do
|
185
|
+
subject.rotate_json_config_files(generator_file_name: generator_config_file_name,
|
186
|
+
validator_file_name: validator_config_file_name)
|
187
|
+
generator_config = JSON.parse(File.read(generator_config_file_name))
|
188
|
+
validator_config = JSON.parse(File.read(validator_config_file_name))
|
189
|
+
expect(validator_config['keys'][generator_config['key_description']]).to_not be nil
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'removes the oldest public key from the validator configuration in keeping with maximum number of keys' do
|
193
|
+
subject.rotate_json_config_files(generator_file_name: generator_config_file_name,
|
194
|
+
validator_file_name: validator_config_file_name)
|
195
|
+
generator_config = JSON.parse(File.read(generator_config_file_name))
|
196
|
+
validator_config = JSON.parse(File.read(validator_config_file_name))
|
197
|
+
expect(validator_config['keys'].size).to be 3
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
context "when confirming that configurations are valid" do
|
203
|
+
context "given validator (with single public key) configuration that includes generator key" do
|
204
|
+
let!(:validator_config) {{
|
205
|
+
'provider' => 'SoarAuthenticationToken::JwtTokenValidator',
|
206
|
+
'keys' => {
|
207
|
+
'KEYPAIR_20160107T230101' => { 'public_key' => @public_key_1 }
|
208
|
+
}
|
209
|
+
}}
|
210
|
+
let!(:generator_config) {{
|
211
|
+
'provider' => 'SoarAuthenticationToken::JwtTokenGenerator',
|
212
|
+
'private_key' => @private_key_1,
|
213
|
+
'key_description' => 'original key'
|
214
|
+
}}
|
215
|
+
it 'responds that the configuration combination is valid' do
|
216
|
+
valid = subject.configurations_match_and_valid?(generator_config: generator_config,
|
217
|
+
validator_config: validator_config)
|
218
|
+
expect(valid).to be true
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
context "given validator (with single public key) configuration that does not include generator key" do
|
223
|
+
let!(:validator_config) {{
|
224
|
+
'provider' => 'SoarAuthenticationToken::JwtTokenValidator',
|
225
|
+
'keys' => {
|
226
|
+
'KEYPAIR_20160107T230101' => { 'public_key' => @public_key_1 }
|
227
|
+
}
|
228
|
+
}}
|
229
|
+
let!(:generator_config) {{
|
230
|
+
'provider' => 'SoarAuthenticationToken::JwtTokenGenerator',
|
231
|
+
'private_key' => @private_key_2,
|
232
|
+
'key_description' => 'original key'
|
233
|
+
}}
|
234
|
+
it 'responds that the configuration combination is not valid' do
|
235
|
+
valid = subject.configurations_match_and_valid?(generator_config: generator_config,
|
236
|
+
validator_config: validator_config)
|
237
|
+
expect(valid).to be false
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
context "given validator (with multiple public keys) configuration that include generator key" do
|
242
|
+
let!(:validator_config) {{
|
243
|
+
'provider' => 'SoarAuthenticationToken::JwtTokenValidator',
|
244
|
+
'keys' => {
|
245
|
+
'KEYPAIR_20160107T230101' => { 'public_key' => @public_key_1 },
|
246
|
+
'KEYPAIR_20160107T230201' => { 'public_key' => @public_key_2 },
|
247
|
+
'KEYPAIR_20160107T230301' => { 'public_key' => @public_key_3 },
|
248
|
+
}
|
249
|
+
}}
|
250
|
+
let!(:generator_config) {{
|
251
|
+
'provider' => 'SoarAuthenticationToken::JwtTokenGenerator',
|
252
|
+
'private_key' => @private_key_2,
|
253
|
+
'key_description' => 'original key'
|
254
|
+
}}
|
255
|
+
it 'responds that the configuration combination is valid' do
|
256
|
+
valid = subject.configurations_match_and_valid?(generator_config: generator_config,
|
257
|
+
validator_config: validator_config)
|
258
|
+
expect(valid).to be true
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
context "given validator (with multiple public keys) configuration that does not include generator key" do
|
263
|
+
let!(:validator_config) {{
|
264
|
+
'provider' => 'SoarAuthenticationToken::JwtTokenValidator',
|
265
|
+
'keys' => {
|
266
|
+
'KEYPAIR_20160107T230101' => { 'public_key' => @public_key_1 },
|
267
|
+
'KEYPAIR_20160107T230201' => { 'public_key' => @public_key_2 },
|
268
|
+
'KEYPAIR_20160107T230301' => { 'public_key' => @public_key_3 },
|
269
|
+
}
|
270
|
+
}}
|
271
|
+
let!(:generator_config) {{
|
272
|
+
'provider' => 'SoarAuthenticationToken::JwtTokenGenerator',
|
273
|
+
'private_key' => @private_key_4,
|
274
|
+
'key_description' => 'original key'
|
275
|
+
}}
|
276
|
+
it 'responds that the configuration combination is not valid' do
|
277
|
+
valid = subject.configurations_match_and_valid?(generator_config: generator_config,
|
278
|
+
validator_config: validator_config)
|
279
|
+
expect(valid).to be false
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|