soar_authentication_token 3.0.9 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +12 -12
- data/lib/soar_authentication_token.rb +5 -0
- data/lib/soar_authentication_token/providers/jwt_token_generator.rb +66 -0
- data/lib/soar_authentication_token/providers/jwt_token_validator.rb +89 -0
- data/lib/soar_authentication_token/providers/remote_token_generator.rb +44 -0
- data/lib/soar_authentication_token/providers/remote_token_validator.rb +57 -0
- data/lib/soar_authentication_token/providers/static_token_validator.rb +64 -0
- data/lib/soar_authentication_token/token_generator.rb +8 -87
- data/lib/soar_authentication_token/token_validator.rb +8 -149
- data/lib/soar_authentication_token/version.rb +1 -1
- data/spec/rack_middleware_spec.rb +3 -4
- data/spec/token_generator_spec.rb +10 -7
- data/spec/token_validator_spec.rb +7 -7
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4020cefab79d1ffcef0ea47c4787412cf4c520e4
|
4
|
+
data.tar.gz: dff3cc2d0e722cfbfbd9ae761694417350bfe9ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a9f8caae8f97bc3b7954b9c70848f605b6b7c6ba32b90800e42955811ccdfb34cc2d9bc7632aca2ec1c5b0872146a4a2ca761340fd228afe44a238f37b2998fd
|
7
|
+
data.tar.gz: 2a9aaf075ccae76090499d7bca7a7d88b663f75aecbd40393c783071a48591c2bcf842c64b86e9b04c436e6c5c36947442eadd34b0314ced9660030a5f5dc55b
|
data/README.md
CHANGED
@@ -24,16 +24,16 @@ gem install soar_authentication_token
|
|
24
24
|
|
25
25
|
## Configuration of generators and validators
|
26
26
|
|
27
|
-
There are three
|
27
|
+
There are three provider groups for operation: JwtToken, RemoteToken or StaticToken
|
28
28
|
|
29
|
-
###
|
30
|
-
|
29
|
+
### JwtTokenGenerator/JwtTokenValidator
|
30
|
+
With the JwtTokenValidator provider the tokens are decoded, verified and meta extracted locally using configured key material. In practice the the token generation and validation services run in this mode since their roles are to generate and validate tokens.
|
31
31
|
|
32
|
-
###
|
33
|
-
|
32
|
+
### RemoteTokenGenerator/RemoteTokenValidator
|
33
|
+
With the RemoteTokenValidator provider the tokens are passed to a token validation service for validation. This allows for a centralized management of tokens. The key material are therefore managed on the validation service and . In this mode you only have to provide the url of the validation service.
|
34
34
|
|
35
|
-
###
|
36
|
-
In this mode the
|
35
|
+
### StaticTokenValidator
|
36
|
+
In this mode the StaticTokenValidator is configured with a list of preconfigured static tokens. Incoming tokens are simply checked against this list. No extraction of meta is performed on the tokens but retrieved from the configuration. Rotation of tokens is simple since many tokens can be configured. This mode is to be used in only two scenarios:
|
37
37
|
* Between the various authentication token services that requires authentication between themselves. These services themselves do not the benefit of another centralized authentication service to rely on.
|
38
38
|
* In test scenarios where you do not want to pull in the authentication services to perform testing of your services.
|
39
39
|
|
@@ -77,7 +77,7 @@ First step is to configure the middleware. Actually you are not configuring the
|
|
77
77
|
|
78
78
|
```ruby
|
79
79
|
@iut_configuration = {
|
80
|
-
'
|
80
|
+
'provider' => 'RemoteTokenValidator',
|
81
81
|
'validator-url' => 'http://authentication-token-validator-service:9393/validate'
|
82
82
|
}
|
83
83
|
@iut = SoarAuthenticationToken::RackMiddleware.new(@test_app, @iut_configuration)
|
@@ -94,7 +94,7 @@ The class generates tokens or requests a token from a generator service as confi
|
|
94
94
|
For remote token generation, configure as such:
|
95
95
|
```ruby
|
96
96
|
@configuration_remote = {
|
97
|
-
'
|
97
|
+
'provider' => 'RemoteTokenGenerator',
|
98
98
|
'generator-url' => 'http://authentication-token-generator-service:9393/generate',
|
99
99
|
'generator-client-auth-token' => 'xxxx'
|
100
100
|
}
|
@@ -105,7 +105,7 @@ For local token generation, configure as such:
|
|
105
105
|
generator = SoarAuthenticationToken::KeypairGenerator.new
|
106
106
|
private_key, public_key = generator.generate
|
107
107
|
@configuration_local = {
|
108
|
-
'
|
108
|
+
'provider' => 'JwtTokenGenerator',
|
109
109
|
'private_key' => private_key
|
110
110
|
}
|
111
111
|
```
|
@@ -131,7 +131,7 @@ The class validates tokens or requests validation of a token from a validation s
|
|
131
131
|
For remote token validation, configure as such:
|
132
132
|
```ruby
|
133
133
|
@configuration_remote = {
|
134
|
-
'
|
134
|
+
'provider' => 'RemoteTokenGenerator',
|
135
135
|
'validator-url' => 'http://authentication-token-validator-service:9393/validate',
|
136
136
|
}
|
137
137
|
```
|
@@ -141,7 +141,7 @@ For local token validation, configure as such:
|
|
141
141
|
generator = SoarAuthenticationToken::KeypairGenerator.new
|
142
142
|
private_key, public_key = generator.generate
|
143
143
|
@configuration_local = {
|
144
|
-
'
|
144
|
+
'provider' => 'JwtTokenValidator',
|
145
145
|
'public_key' => public_key
|
146
146
|
}
|
147
147
|
```
|
@@ -1,6 +1,11 @@
|
|
1
1
|
module SoarAuthenticationToken
|
2
2
|
end
|
3
3
|
|
4
|
+
require 'soar_authentication_token/providers/jwt_token_generator'
|
5
|
+
require 'soar_authentication_token/providers/jwt_token_validator'
|
6
|
+
require 'soar_authentication_token/providers/remote_token_generator'
|
7
|
+
require 'soar_authentication_token/providers/remote_token_validator'
|
8
|
+
require 'soar_authentication_token/providers/static_token_validator'
|
4
9
|
require 'soar_authentication_token/keypair_generator'
|
5
10
|
require 'soar_authentication_token/token_generator'
|
6
11
|
require 'soar_authentication_token/token_validator'
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'soar_xt'
|
2
|
+
require 'jwt'
|
3
|
+
require 'securerandom'
|
4
|
+
require 'time'
|
5
|
+
require 'json'
|
6
|
+
require 'authenticated_client'
|
7
|
+
|
8
|
+
module SoarAuthenticationToken
|
9
|
+
class JwtTokenGenerator
|
10
|
+
DEFAULT_CONFIGURATION = {
|
11
|
+
'expiry' => 604800 #a days worth of seconds
|
12
|
+
} unless defined? DEFAULT_CONFIGURATION; DEFAULT_CONFIGURATION.freeze
|
13
|
+
|
14
|
+
def initialize(configuration)
|
15
|
+
@configuration = merge_with_default_configuration(configuration)
|
16
|
+
validate_local_mode_configuration
|
17
|
+
@private_key = OpenSSL::PKey::EC.new(@configuration['private_key'])
|
18
|
+
end
|
19
|
+
|
20
|
+
def inject_store_provider(store_provider)
|
21
|
+
@store_provider = store_provider
|
22
|
+
end
|
23
|
+
|
24
|
+
def generate(authenticated_identifier:, flow_identifier: nil)
|
25
|
+
token_meta = generate_meta(authenticated_identifier)
|
26
|
+
token = encode(token_meta)
|
27
|
+
add_token_to_store(token_meta,flow_identifier)
|
28
|
+
[token, token_meta]
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def generate_meta(authenticated_identifier)
|
34
|
+
current_time = Time.now
|
35
|
+
{ 'authenticated_identifier' => authenticated_identifier,
|
36
|
+
'token_issue_time' => current_time.utc.iso8601(3),
|
37
|
+
'token_expiry_time' => (current_time + @configuration['expiry']).utc.iso8601(3),
|
38
|
+
'token_identifier' => SecureRandom.hex(32)
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def encode(meta)
|
43
|
+
JWT.encode(meta, @private_key, 'ES512')
|
44
|
+
end
|
45
|
+
|
46
|
+
def validate_local_mode_configuration
|
47
|
+
raise "'private_key' must be configured in local mode" unless @configuration['private_key']
|
48
|
+
raise "'expiry' must be configured in local mode" unless @configuration['expiry']
|
49
|
+
raise "'expiry' must be an integer" unless Integer(@configuration['expiry'])
|
50
|
+
end
|
51
|
+
|
52
|
+
def merge_with_default_configuration(configuration)
|
53
|
+
configuration = {} unless configuration
|
54
|
+
Hash.deep_merge(DEFAULT_CONFIGURATION,configuration)
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_token_to_store(meta,flow_identifier)
|
58
|
+
@store_provider.add(
|
59
|
+
token_identifier: meta['token_identifier'],
|
60
|
+
authenticated_identifier: meta['authenticated_identifier'],
|
61
|
+
token_issue_time: meta['token_issue_time'],
|
62
|
+
token_expiry_time: meta['token_expiry_time'],
|
63
|
+
flow_identifier: flow_identifier)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'jwt'
|
2
|
+
require 'authenticated_client'
|
3
|
+
|
4
|
+
module SoarAuthenticationToken
|
5
|
+
class JwtTokenValidator
|
6
|
+
def initialize(configuration)
|
7
|
+
@configuration = configuration
|
8
|
+
set_configuration_defaults
|
9
|
+
validate_configuration
|
10
|
+
@public_key = OpenSSL::PKey::EC.new(@configuration['public_key'])
|
11
|
+
@public_key.private_key = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def inject_store_provider(store_provider)
|
15
|
+
@store_provider = store_provider
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate(authentication_token:,flow_identifier: nil)
|
19
|
+
meta = decode_token_meta(authentication_token)
|
20
|
+
return rejection_result(reason: "Expired token <#{meta['token_expiry_time']}> for <#{meta['authenticated_identifier']}>") if token_expired?(meta)
|
21
|
+
return rejection_result(reason: "Unknown token for <#{meta['authenticated_identifier']}>") unless token_exist_in_store?(meta,flow_identifier)
|
22
|
+
success_result(token_meta: meta)
|
23
|
+
rescue JWT::VerificationError, JWT::DecodeError
|
24
|
+
rejection_result(reason: 'Token decode/verification failure')
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def set_configuration_defaults
|
30
|
+
@configuration['expiry'] ||= 86400
|
31
|
+
end
|
32
|
+
|
33
|
+
def decode_token_meta(authentication_token)
|
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:)
|
45
|
+
{
|
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)
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def token_age(token_issue_time)
|
55
|
+
Time.now - Time.parse(token_issue_time.to_s)
|
56
|
+
end
|
57
|
+
|
58
|
+
def validate_configuration
|
59
|
+
raise "'public_key' must be configured in local mode" unless @configuration['public_key']
|
60
|
+
raise "'expiry' must be configured in local mode" unless @configuration['expiry']
|
61
|
+
raise "'expiry' must be an integer" unless Integer(@configuration['expiry'])
|
62
|
+
end
|
63
|
+
|
64
|
+
def decode(authentication_token)
|
65
|
+
JWT.decode(authentication_token, @public_key, true, { :algorithm => 'ES512' })
|
66
|
+
end
|
67
|
+
|
68
|
+
def token_expired?(meta)
|
69
|
+
Time.parse(meta['token_expiry_time'].to_s) < Time.now
|
70
|
+
end
|
71
|
+
|
72
|
+
def token_exist_in_store?(meta,flow_identifier)
|
73
|
+
@store_provider.token_exist?(
|
74
|
+
token_identifier: meta['token_identifier'],
|
75
|
+
authenticated_identifier: meta['authenticated_identifier'],
|
76
|
+
token_issue_time: meta['token_issue_time'],
|
77
|
+
token_expiry_time: meta['token_expiry_time'],
|
78
|
+
flow_identifier: flow_identifier)
|
79
|
+
end
|
80
|
+
|
81
|
+
def rejection_result(reason:)
|
82
|
+
[false, nil, reason]
|
83
|
+
end
|
84
|
+
|
85
|
+
def success_result(token_meta:)
|
86
|
+
[true, token_meta, "Valid token for <#{token_meta['authenticated_identifier']}>" ]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'authenticated_client'
|
3
|
+
|
4
|
+
module SoarAuthenticationToken
|
5
|
+
class RemoteTokenGenerator
|
6
|
+
def initialize(configuration)
|
7
|
+
@configuration = configuration
|
8
|
+
validate_configuration
|
9
|
+
end
|
10
|
+
|
11
|
+
def inject_store_provider(store_provider)
|
12
|
+
#ignore the store provider since this generator does not use a store
|
13
|
+
end
|
14
|
+
|
15
|
+
def generate(authenticated_identifier:, flow_identifier: nil)
|
16
|
+
client = authenticated_client(authenticated_identifier,flow_identifier)
|
17
|
+
validate_and_extract_token_from_response(client.request)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def authenticated_client(authenticated_identifier,flow_identifier)
|
23
|
+
client = AuthenticatedClient::Client.new
|
24
|
+
client.url = @configuration['generator-url']
|
25
|
+
client.token = @configuration['generator-client-auth-token']
|
26
|
+
client.verb = :post
|
27
|
+
client.parameters = {'flow_identifier' => flow_identifier}
|
28
|
+
client.body = { 'authenticated_identifier' => authenticated_identifier }
|
29
|
+
client
|
30
|
+
end
|
31
|
+
|
32
|
+
def validate_and_extract_token_from_response(response)
|
33
|
+
raise "Failure generating token with token generation service. Code #{response.code}" if '200' != response.code
|
34
|
+
body = JSON.parse(response.body)
|
35
|
+
raise 'Failure generating token by token service' if 'success' != body['status']
|
36
|
+
raise 'Token service did not provide token' if body['data'].nil? or body['data']['token'].nil?
|
37
|
+
body['data']['token']
|
38
|
+
end
|
39
|
+
|
40
|
+
def validate_configuration
|
41
|
+
raise "'generator-url' must be configured" if @configuration['generator-url'].nil?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'authenticated_client'
|
2
|
+
|
3
|
+
module SoarAuthenticationToken
|
4
|
+
class RemoteTokenValidator
|
5
|
+
def initialize(configuration)
|
6
|
+
@configuration = configuration
|
7
|
+
validate_configuration
|
8
|
+
end
|
9
|
+
|
10
|
+
def inject_store_provider(store_provider)
|
11
|
+
#ignore the store provider since this validator does not use a store
|
12
|
+
end
|
13
|
+
|
14
|
+
def validate(authentication_token:,flow_identifier: nil)
|
15
|
+
response = send_request(authentication_token,flow_identifier)
|
16
|
+
validate_and_extract_information_from_response(response)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def send_request(authentication_token,flow_identifier)
|
22
|
+
uri = URI.parse(@configuration['validator-url'])
|
23
|
+
uri.query = URI.encode_www_form( {'flow_identifier' => flow_identifier} )
|
24
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
25
|
+
http.use_ssl = true if uri.is_a?(URI::HTTPS)
|
26
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
27
|
+
request.body = { 'authentication_token' => authentication_token }.to_json
|
28
|
+
http.request(request)
|
29
|
+
end
|
30
|
+
|
31
|
+
def validate_and_extract_information_from_response(response)
|
32
|
+
raise "Failure validating token with token validation service. Code #{response.code} received" if '200' != response.code
|
33
|
+
body = JSON.parse(response.body)
|
34
|
+
if ('success' == body['status']) and body['data']
|
35
|
+
token_validity = body['data']['token_validity']
|
36
|
+
token_meta = body['data']['token_meta']
|
37
|
+
message = body['data']['message']
|
38
|
+
raise 'Token validation service did not provide token_validity' if token_validity.nil?
|
39
|
+
raise 'Token validation service did not provide token_meta' if token_validity and token_meta.nil?
|
40
|
+
raise 'Token validation service did not provide message' if message.nil?
|
41
|
+
return [token_validity, token_meta, message]
|
42
|
+
end
|
43
|
+
if 'fail' == body['status']
|
44
|
+
return rejection_result(reason: 'remote validation failed')
|
45
|
+
end
|
46
|
+
raise "Failure validating token with token validation service. Status '#{body['status']}' received"
|
47
|
+
end
|
48
|
+
|
49
|
+
def validate_configuration
|
50
|
+
raise "'validator-url' must be configured in remote mode" unless @configuration['validator-url']
|
51
|
+
end
|
52
|
+
|
53
|
+
def rejection_result(reason:)
|
54
|
+
[false, nil, reason]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module SoarAuthenticationToken
|
2
|
+
class StaticTokenValidator
|
3
|
+
def initialize(configuration)
|
4
|
+
@configuration = configuration
|
5
|
+
validate_configuration
|
6
|
+
end
|
7
|
+
|
8
|
+
def inject_store_provider(store_provider)
|
9
|
+
#ignore the store provider since this validator does not use a store
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate(authentication_token:,flow_identifier: nil)
|
13
|
+
found_static_token = find_configured_static_token(authentication_token)
|
14
|
+
return rejection_result(reason: 'Unknown static token') if found_static_token.nil?
|
15
|
+
meta = compile_meta(token_identifier: 'static_token',
|
16
|
+
authenticated_identifier: found_static_token['authenticated_identifier'],
|
17
|
+
token_issue_time: found_static_token['token_issue_time'],
|
18
|
+
token_expiry_time: found_static_token['token_expiry_time'])
|
19
|
+
return rejection_result(reason: "Expired token <#{meta['token_expiry_time']}> for <#{meta['authenticated_identifier']}>") if token_expired?(meta)
|
20
|
+
return success_result(token_meta: meta)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def find_configured_static_token(authentication_token)
|
26
|
+
@configuration['static_tokens'].each { |static_token|
|
27
|
+
return static_token if authentication_token == static_token['token']
|
28
|
+
}
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def compile_meta(token_identifier:,
|
33
|
+
authenticated_identifier:,
|
34
|
+
token_issue_time:,
|
35
|
+
token_expiry_time:)
|
36
|
+
{
|
37
|
+
'token_identifier' => token_identifier,
|
38
|
+
'authenticated_identifier' => authenticated_identifier,
|
39
|
+
'token_issue_time' => token_issue_time,
|
40
|
+
'token_expiry_time' => token_expiry_time,
|
41
|
+
'token_age' => token_age(token_issue_time)
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def token_age(token_issue_time)
|
46
|
+
Time.now - Time.parse(token_issue_time.to_s)
|
47
|
+
end
|
48
|
+
|
49
|
+
def validate_configuration
|
50
|
+
raise "array of 'static_tokens' must be configured" unless @configuration['static_tokens']
|
51
|
+
end
|
52
|
+
|
53
|
+
def token_expired?(meta)
|
54
|
+
Time.parse(meta['token_expiry_time'].to_s) < Time.now
|
55
|
+
end
|
56
|
+
|
57
|
+
def rejection_result(reason:)
|
58
|
+
[false, nil, reason]
|
59
|
+
end
|
60
|
+
def success_result(token_meta:)
|
61
|
+
[true, token_meta, "Valid token for <#{token_meta['authenticated_identifier']}>" ]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -1,107 +1,28 @@
|
|
1
|
-
require 'soar_xt'
|
2
|
-
require 'jwt'
|
3
|
-
require 'securerandom'
|
4
|
-
require 'time'
|
5
|
-
require 'net/http'
|
6
|
-
require 'uri'
|
7
|
-
require 'json'
|
8
|
-
require 'authenticated_client'
|
9
|
-
|
10
1
|
module SoarAuthenticationToken
|
11
2
|
class TokenGenerator
|
12
|
-
DEFAULT_CONFIGURATION = {
|
13
|
-
'expiry' => 604800 #a days worth of seconds
|
14
|
-
} unless defined? DEFAULT_CONFIGURATION; DEFAULT_CONFIGURATION.freeze
|
15
|
-
|
16
3
|
def initialize(configuration)
|
17
|
-
@configuration =
|
4
|
+
@configuration = configuration
|
18
5
|
validate_configuration
|
19
|
-
|
6
|
+
instantiate_provider
|
20
7
|
end
|
21
8
|
|
22
9
|
def inject_store_provider(store_provider)
|
23
|
-
@store_provider
|
10
|
+
@provider.inject_store_provider(store_provider)
|
24
11
|
end
|
25
12
|
|
26
13
|
def generate(authenticated_identifier:, flow_identifier: nil)
|
27
|
-
|
28
|
-
|
14
|
+
@provider.generate(authenticated_identifier: authenticated_identifier,
|
15
|
+
flow_identifier: flow_identifier)
|
29
16
|
end
|
30
17
|
|
31
18
|
private
|
32
19
|
|
33
|
-
def
|
34
|
-
|
35
|
-
token = encode(token_meta)
|
36
|
-
add_token_to_store(token_meta,flow_identifier)
|
37
|
-
[token, token_meta]
|
38
|
-
end
|
39
|
-
|
40
|
-
def generate_remotely(authenticated_identifier,flow_identifier)
|
41
|
-
client = authenticated_client(authenticated_identifier,flow_identifier)
|
42
|
-
validate_and_extract_token_from_response(client.request)
|
43
|
-
end
|
44
|
-
|
45
|
-
def authenticated_client(authenticated_identifier,flow_identifier)
|
46
|
-
client = AuthenticatedClient::Client.new
|
47
|
-
client.url = @configuration['generator-url']
|
48
|
-
client.token = @configuration['generator-client-auth-token']
|
49
|
-
client.verb = :post
|
50
|
-
client.parameters = {'flow_identifier' => flow_identifier}
|
51
|
-
client.body = { 'authenticated_identifier' => authenticated_identifier }
|
52
|
-
client
|
53
|
-
end
|
54
|
-
|
55
|
-
def validate_and_extract_token_from_response(response)
|
56
|
-
raise "Failure generating token with token generation service. Code #{response.code}" if '200' != response.code
|
57
|
-
body = JSON.parse(response.body)
|
58
|
-
raise 'Failure generating token by token service' if 'success' != body['status']
|
59
|
-
raise 'Token service did not provide token' if body['data'].nil? or body['data']['token'].nil?
|
60
|
-
body['data']['token']
|
61
|
-
end
|
62
|
-
|
63
|
-
def generate_meta(authenticated_identifier)
|
64
|
-
current_time = Time.now
|
65
|
-
{ 'authenticated_identifier' => authenticated_identifier,
|
66
|
-
'token_issue_time' => current_time.utc.iso8601(3),
|
67
|
-
'token_expiry_time' => (current_time + @configuration['expiry']).utc.iso8601(3),
|
68
|
-
'token_identifier' => SecureRandom.hex(32)
|
69
|
-
}
|
70
|
-
end
|
71
|
-
|
72
|
-
def encode(meta)
|
73
|
-
JWT.encode(meta, @private_key, 'ES512')
|
20
|
+
def instantiate_provider
|
21
|
+
@provider = Object::const_get("SoarAuthenticationToken::#{@configuration['provider']}").new(@configuration)
|
74
22
|
end
|
75
23
|
|
76
24
|
def validate_configuration
|
77
|
-
raise "'
|
78
|
-
raise "'mode' must be configured as either 'local' or 'remote'" unless ['local','remote'].include?(@configuration['mode'])
|
79
|
-
validate_local_mode_configuration if 'local' == @configuration['mode']
|
80
|
-
validate_remote_mode_configuration if 'remote' == @configuration['mode']
|
81
|
-
end
|
82
|
-
|
83
|
-
def validate_remote_mode_configuration
|
84
|
-
raise "'generator-url' must be configured in remote mode" if @configuration['generator-url'].nil?
|
85
|
-
end
|
86
|
-
|
87
|
-
def validate_local_mode_configuration
|
88
|
-
raise "'private_key' must be configured in local mode" unless @configuration['private_key']
|
89
|
-
raise "'expiry' must be configured in local mode" unless @configuration['expiry']
|
90
|
-
raise "'expiry' must be an integer" unless Integer(@configuration['expiry'])
|
91
|
-
end
|
92
|
-
|
93
|
-
def merge_with_default_configuration(configuration)
|
94
|
-
configuration = {} unless configuration
|
95
|
-
Hash.deep_merge(DEFAULT_CONFIGURATION,configuration)
|
96
|
-
end
|
97
|
-
|
98
|
-
def add_token_to_store(meta,flow_identifier)
|
99
|
-
@store_provider.add(
|
100
|
-
token_identifier: meta['token_identifier'],
|
101
|
-
authenticated_identifier: meta['authenticated_identifier'],
|
102
|
-
token_issue_time: meta['token_issue_time'],
|
103
|
-
token_expiry_time: meta['token_expiry_time'],
|
104
|
-
flow_identifier: flow_identifier)
|
25
|
+
raise "'provider' must be configured" unless @configuration['provider']
|
105
26
|
end
|
106
27
|
end
|
107
28
|
end
|
@@ -1,169 +1,28 @@
|
|
1
|
-
require 'soar_xt'
|
2
|
-
require 'jwt'
|
3
|
-
require 'authenticated_client'
|
4
|
-
|
5
1
|
module SoarAuthenticationToken
|
6
2
|
class TokenValidator
|
7
|
-
DEFAULT_CONFIGURATION = {
|
8
|
-
'expiry' => 604800 #a days worth of seconds
|
9
|
-
} unless defined? DEFAULT_CONFIGURATION; DEFAULT_CONFIGURATION.freeze
|
10
|
-
|
11
3
|
def initialize(configuration)
|
12
|
-
@configuration =
|
4
|
+
@configuration = configuration
|
13
5
|
validate_configuration
|
14
|
-
|
15
|
-
@public_key.private_key = nil
|
6
|
+
instantiate_provider
|
16
7
|
end
|
17
8
|
|
18
9
|
def inject_store_provider(store_provider)
|
19
|
-
@store_provider
|
10
|
+
@provider.inject_store_provider(store_provider)
|
20
11
|
end
|
21
12
|
|
22
13
|
def validate(authentication_token:,flow_identifier: nil)
|
23
|
-
|
24
|
-
|
25
|
-
return validate_remotely(authentication_token,flow_identifier) if 'remote' == @configuration['mode']
|
26
|
-
raise 'invalid validation mode configured'
|
14
|
+
@provider.validate(authentication_token: authentication_token,
|
15
|
+
flow_identifier: flow_identifier)
|
27
16
|
end
|
28
17
|
|
29
18
|
private
|
30
19
|
|
31
|
-
def
|
32
|
-
|
33
|
-
return rejection_result(reason: 'Unknown static token') if found_static_token.nil?
|
34
|
-
meta = compile_meta(token_identifier: 'static_token',
|
35
|
-
authenticated_identifier: found_static_token['authenticated_identifier'],
|
36
|
-
token_issue_time: found_static_token['token_issue_time'],
|
37
|
-
token_expiry_time: found_static_token['token_expiry_time'])
|
38
|
-
return rejection_result(reason: "Expired token <#{meta['token_expiry_time']}> for <#{meta['authenticated_identifier']}>") if token_expired?(meta)
|
39
|
-
return success_result(token_meta: meta)
|
40
|
-
end
|
41
|
-
|
42
|
-
def find_configured_static_token(authentication_token)
|
43
|
-
@configuration['static_tokens'].each { |static_token|
|
44
|
-
if authentication_token == static_token['token']
|
45
|
-
return static_token
|
46
|
-
end
|
47
|
-
}
|
48
|
-
nil
|
49
|
-
end
|
50
|
-
|
51
|
-
def validate_locally(authentication_token,flow_identifier)
|
52
|
-
meta = decode_token_meta(authentication_token)
|
53
|
-
return rejection_result(reason: "Expired token <#{meta['token_expiry_time']}> for <#{meta['authenticated_identifier']}>") if token_expired?(meta)
|
54
|
-
return rejection_result(reason: "Unknown token for <#{meta['authenticated_identifier']}>") unless token_exist_in_store?(meta,flow_identifier)
|
55
|
-
success_result(token_meta: meta)
|
56
|
-
rescue JWT::VerificationError, JWT::DecodeError
|
57
|
-
rejection_result(reason: 'Token decode/verification failure')
|
58
|
-
end
|
59
|
-
|
60
|
-
def decode_token_meta(authentication_token)
|
61
|
-
decoded_token_payload = decode(authentication_token)
|
62
|
-
compile_meta(token_identifier: decoded_token_payload[0]['token_identifier'],
|
63
|
-
authenticated_identifier: decoded_token_payload[0]['authenticated_identifier'],
|
64
|
-
token_issue_time: decoded_token_payload[0]['token_issue_time'],
|
65
|
-
token_expiry_time: decoded_token_payload[0]['token_expiry_time'])
|
66
|
-
end
|
67
|
-
|
68
|
-
def compile_meta(token_identifier:,
|
69
|
-
authenticated_identifier:,
|
70
|
-
token_issue_time:,
|
71
|
-
token_expiry_time:)
|
72
|
-
{
|
73
|
-
'token_identifier' => token_identifier,
|
74
|
-
'authenticated_identifier' => authenticated_identifier,
|
75
|
-
'token_issue_time' => token_issue_time,
|
76
|
-
'token_expiry_time' => token_expiry_time,
|
77
|
-
'token_age' => token_age(token_issue_time)
|
78
|
-
}
|
79
|
-
end
|
80
|
-
|
81
|
-
def token_age(token_issue_time)
|
82
|
-
Time.now - Time.parse(token_issue_time.to_s)
|
83
|
-
end
|
84
|
-
|
85
|
-
def validate_remotely(authentication_token,flow_identifier)
|
86
|
-
response = send_request(authentication_token,flow_identifier)
|
87
|
-
validate_and_extract_information_from_response(response)
|
88
|
-
end
|
89
|
-
|
90
|
-
def send_request(authentication_token,flow_identifier)
|
91
|
-
uri = URI.parse(@configuration['validator-url'])
|
92
|
-
uri.query = URI.encode_www_form( {'flow_identifier' => flow_identifier} )
|
93
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
94
|
-
http.use_ssl = true if uri.is_a?(URI::HTTPS)
|
95
|
-
request = Net::HTTP::Post.new(uri.request_uri)
|
96
|
-
request.body = { 'authentication_token' => authentication_token }.to_json
|
97
|
-
http.request(request)
|
98
|
-
end
|
99
|
-
|
100
|
-
def validate_and_extract_information_from_response(response)
|
101
|
-
raise "Failure validating token with token validation service. Code #{response.code} received" if '200' != response.code
|
102
|
-
body = JSON.parse(response.body)
|
103
|
-
if ('success' == body['status']) and body['data']
|
104
|
-
token_validity = body['data']['token_validity']
|
105
|
-
token_meta = body['data']['token_meta']
|
106
|
-
message = body['data']['message']
|
107
|
-
raise 'Token validation service did not provide token_validity' if token_validity.nil?
|
108
|
-
raise 'Token validation service did not provide token_meta' if token_validity and token_meta.nil?
|
109
|
-
raise 'Token validation service did not provide message' if message.nil?
|
110
|
-
return [token_validity, token_meta, message]
|
111
|
-
end
|
112
|
-
if 'fail' == body['status']
|
113
|
-
return rejection_result(reason: 'remote validation failed')
|
114
|
-
end
|
115
|
-
raise "Failure validating token with token validation service. Status '#{body['status']}' received"
|
20
|
+
def instantiate_provider
|
21
|
+
@provider = Object::const_get("SoarAuthenticationToken::#{@configuration['provider']}").new(@configuration)
|
116
22
|
end
|
117
23
|
|
118
24
|
def validate_configuration
|
119
|
-
raise "'
|
120
|
-
raise "'mode' must be configured as either 'local', 'remote' or 'static'" unless ['local','remote', 'static'].include?(@configuration['mode'])
|
121
|
-
validate_remote_configuration if 'remote' == @configuration['mode']
|
122
|
-
validate_local_configuration if 'local' == @configuration['mode']
|
123
|
-
validate_static_configuration if 'static' == @configuration['mode']
|
124
|
-
end
|
125
|
-
|
126
|
-
def validate_remote_configuration
|
127
|
-
raise "'validator-url' must be configured in remote mode" unless @configuration['validator-url']
|
128
|
-
end
|
129
|
-
|
130
|
-
def validate_local_configuration
|
131
|
-
raise "'public_key' must be configured in local mode" unless @configuration['public_key']
|
132
|
-
raise "'expiry' must be configured in local mode" unless @configuration['expiry']
|
133
|
-
raise "'expiry' must be an integer" unless Integer(@configuration['expiry'])
|
134
|
-
end
|
135
|
-
|
136
|
-
def validate_static_configuration
|
137
|
-
raise "'static_tokens' must be configured in local mode" unless @configuration['static_tokens']
|
138
|
-
end
|
139
|
-
|
140
|
-
def merge_with_default_configuration(configuration)
|
141
|
-
Hash.deep_merge(DEFAULT_CONFIGURATION,configuration)
|
142
|
-
end
|
143
|
-
|
144
|
-
def decode(authentication_token)
|
145
|
-
JWT.decode(authentication_token, @public_key, true, { :algorithm => 'ES512' })
|
146
|
-
end
|
147
|
-
|
148
|
-
def token_expired?(meta)
|
149
|
-
Time.parse(meta['token_expiry_time'].to_s) < Time.now
|
150
|
-
end
|
151
|
-
|
152
|
-
def token_exist_in_store?(meta,flow_identifier)
|
153
|
-
@store_provider.token_exist?(
|
154
|
-
token_identifier: meta['token_identifier'],
|
155
|
-
authenticated_identifier: meta['authenticated_identifier'],
|
156
|
-
token_issue_time: meta['token_issue_time'],
|
157
|
-
token_expiry_time: meta['token_expiry_time'],
|
158
|
-
flow_identifier: flow_identifier)
|
159
|
-
end
|
160
|
-
|
161
|
-
def rejection_result(reason:)
|
162
|
-
[false, nil, reason]
|
163
|
-
end
|
164
|
-
|
165
|
-
def success_result(token_meta:)
|
166
|
-
[true, token_meta, "Valid token for <#{token_meta['authenticated_identifier']}>" ]
|
25
|
+
raise "'provider' must be configured" unless @configuration['provider']
|
167
26
|
end
|
168
27
|
end
|
169
28
|
end
|
@@ -9,7 +9,7 @@ describe SoarAuthenticationToken::RackMiddleware do
|
|
9
9
|
keypair_generator = SoarAuthenticationToken::KeypairGenerator.new
|
10
10
|
private_key, public_key = keypair_generator.generate
|
11
11
|
configuration = {
|
12
|
-
'
|
12
|
+
'provider' => 'RemoteTokenGenerator',
|
13
13
|
'generator-url' => 'http://authentication-token-generator-service:9393/generate',
|
14
14
|
'generator-client-auth-token' => 'test_ecosystem_token_for_auth_token_aaapi_authenticator_service'
|
15
15
|
}
|
@@ -22,7 +22,7 @@ describe SoarAuthenticationToken::RackMiddleware do
|
|
22
22
|
keypair_generator = SoarAuthenticationToken::KeypairGenerator.new
|
23
23
|
private_key, public_key = keypair_generator.generate
|
24
24
|
configuration = {
|
25
|
-
'
|
25
|
+
'provider' => 'JwtTokenGenerator',
|
26
26
|
'private_key' => private_key,
|
27
27
|
'public_key' => public_key
|
28
28
|
}
|
@@ -52,8 +52,7 @@ describe SoarAuthenticationToken::RackMiddleware do
|
|
52
52
|
[200, {"Content-Type"=>"text/html"}, test_app_response_data ]
|
53
53
|
end
|
54
54
|
@iut_configuration = {
|
55
|
-
'
|
56
|
-
'generator-url' => 'http://authentication-token-generator-service:9393/generate',
|
55
|
+
'provider' => 'RemoteTokenValidator',
|
57
56
|
'validator-url' => 'http://authentication-token-validator-service:9393/validate'
|
58
57
|
}
|
59
58
|
@iut = SoarAuthenticationToken::RackMiddleware.new(@test_app, @iut_configuration)
|
@@ -9,19 +9,22 @@ describe SoarAuthenticationToken::TokenGenerator do
|
|
9
9
|
|
10
10
|
before :each do
|
11
11
|
@generator_configuration_local = {
|
12
|
-
'
|
12
|
+
'provider' => 'JwtTokenGenerator',
|
13
13
|
'private_key' => @private_key
|
14
14
|
}
|
15
15
|
@validator_configuration_local = {
|
16
|
-
'
|
16
|
+
'provider' => 'JwtTokenValidator',
|
17
17
|
'public_key' => @public_key
|
18
18
|
}
|
19
|
-
@
|
20
|
-
'
|
19
|
+
@configuration_remote_generator = {
|
20
|
+
'provider' => 'RemoteTokenGenerator',
|
21
21
|
'generator-url' => 'http://authentication-token-generator-service:9393/generate',
|
22
|
-
'validator-url' => 'http://authentication-token-validator-service:9393/validate',
|
23
22
|
'generator-client-auth-token' => 'test_ecosystem_token_for_auth_token_aaapi_authenticator_service'
|
24
23
|
}
|
24
|
+
@configuration_remote_validator = {
|
25
|
+
'provider' => 'RemoteTokenValidator',
|
26
|
+
'validator-url' => 'http://authentication-token-validator-service:9393/validate',
|
27
|
+
}
|
25
28
|
|
26
29
|
@test_store = AuthTokenStoreProvider::StubClient.new
|
27
30
|
end
|
@@ -52,12 +55,12 @@ describe SoarAuthenticationToken::TokenGenerator do
|
|
52
55
|
|
53
56
|
context "when generating a new token remotely" do
|
54
57
|
it 'should request the token from the configured remote service' do
|
55
|
-
@iut = SoarAuthenticationToken::TokenGenerator.new(@
|
58
|
+
@iut = SoarAuthenticationToken::TokenGenerator.new(@configuration_remote_generator)
|
56
59
|
@iut.inject_store_provider(@test_store)
|
57
60
|
|
58
61
|
token, token_generator_meta = @iut.generate(authenticated_identifier: @test_authenticated_identifier, flow_identifier: 'test-flow-id')
|
59
62
|
|
60
|
-
@validator = SoarAuthenticationToken::TokenValidator.new(@
|
63
|
+
@validator = SoarAuthenticationToken::TokenValidator.new(@configuration_remote_validator)
|
61
64
|
@iut.inject_store_provider(@test_store)
|
62
65
|
token_validity, token_validator_meta, messages = @validator.validate(authentication_token: token, flow_identifier: 'test-flow-id')
|
63
66
|
|
@@ -10,11 +10,11 @@ describe SoarAuthenticationToken::TokenValidator do
|
|
10
10
|
@invalid_private_key, @invalid_public_key = keypair_generator.generate
|
11
11
|
@test_identifier = 'a@b.co.za'
|
12
12
|
@local_valid_generator_configuration = {
|
13
|
-
'
|
13
|
+
'provider' => 'JwtTokenGenerator',
|
14
14
|
'private_key' => @valid_private_key
|
15
15
|
}
|
16
16
|
@local_invalid_generator_configuration = {
|
17
|
-
'
|
17
|
+
'provider' => 'JwtTokenGenerator',
|
18
18
|
'private_key' => @invalid_private_key
|
19
19
|
}
|
20
20
|
|
@@ -25,7 +25,7 @@ describe SoarAuthenticationToken::TokenValidator do
|
|
25
25
|
|
26
26
|
current_time = Time.now
|
27
27
|
@static_validator_configuration = {
|
28
|
-
'
|
28
|
+
'provider' => 'StaticTokenValidator',
|
29
29
|
'static_tokens' => [
|
30
30
|
{
|
31
31
|
'token' => 'some_secret_token_string_1111',
|
@@ -48,16 +48,16 @@ describe SoarAuthenticationToken::TokenValidator do
|
|
48
48
|
]
|
49
49
|
}
|
50
50
|
@local_validator_configuration = {
|
51
|
-
'
|
51
|
+
'provider' => 'JwtTokenValidator',
|
52
52
|
'public_key' => @valid_public_key
|
53
53
|
}
|
54
54
|
@remote_generator_configuration = {
|
55
|
-
'
|
55
|
+
'provider' => 'RemoteTokenGenerator',
|
56
56
|
'generator-url' => 'http://authentication-token-generator-service:9393/generate',
|
57
57
|
'generator-client-auth-token' => 'test_ecosystem_token_for_auth_token_aaapi_authenticator_service'
|
58
58
|
}
|
59
59
|
@remote_validator_configuration = {
|
60
|
-
'
|
60
|
+
'provider' => 'RemoteTokenValidator',
|
61
61
|
'validator-url' => 'http://authentication-token-validator-service:9393/validate',
|
62
62
|
'generator-client-auth-token' => 'test_ecosystem_token_for_auth_token_aaapi_authenticator_service'
|
63
63
|
}
|
@@ -97,7 +97,7 @@ describe SoarAuthenticationToken::TokenValidator do
|
|
97
97
|
it 'indicate token is valid' do
|
98
98
|
expect(token_validity).to eq true
|
99
99
|
end
|
100
|
-
|
100
|
+
|
101
101
|
it 'provide the token meta' do
|
102
102
|
expect(token_meta['authenticated_identifier']).to eq @test_identifier
|
103
103
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: soar_authentication_token
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 4.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Barney de Villiers
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-02-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: soar_xt
|
@@ -223,6 +223,11 @@ files:
|
|
223
223
|
- docker-compose.yml
|
224
224
|
- lib/soar_authentication_token.rb
|
225
225
|
- lib/soar_authentication_token/keypair_generator.rb
|
226
|
+
- lib/soar_authentication_token/providers/jwt_token_generator.rb
|
227
|
+
- lib/soar_authentication_token/providers/jwt_token_validator.rb
|
228
|
+
- lib/soar_authentication_token/providers/remote_token_generator.rb
|
229
|
+
- lib/soar_authentication_token/providers/remote_token_validator.rb
|
230
|
+
- lib/soar_authentication_token/providers/static_token_validator.rb
|
226
231
|
- lib/soar_authentication_token/rack_middleware.rb
|
227
232
|
- lib/soar_authentication_token/token_generator.rb
|
228
233
|
- lib/soar_authentication_token/token_validator.rb
|