adal 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rubocop.yml +7 -0
- data/.travis.yml +7 -0
- data/Gemfile +25 -0
- data/LICENSE.txt +21 -0
- data/README.md +97 -0
- data/Rakefile +39 -0
- data/adal.gemspec +52 -0
- data/contributing.md +127 -0
- data/lib/adal.rb +24 -0
- data/lib/adal/authentication_context.rb +202 -0
- data/lib/adal/authentication_parameters.rb +126 -0
- data/lib/adal/authority.rb +165 -0
- data/lib/adal/cache_driver.rb +171 -0
- data/lib/adal/cached_token_response.rb +190 -0
- data/lib/adal/client_assertion.rb +63 -0
- data/lib/adal/client_assertion_certificate.rb +89 -0
- data/lib/adal/client_credential.rb +46 -0
- data/lib/adal/core_ext.rb +26 -0
- data/lib/adal/core_ext/hash.rb +34 -0
- data/lib/adal/jwt_parameters.rb +39 -0
- data/lib/adal/logger.rb +90 -0
- data/lib/adal/logging.rb +98 -0
- data/lib/adal/memory_cache.rb +95 -0
- data/lib/adal/mex_request.rb +52 -0
- data/lib/adal/mex_response.rb +141 -0
- data/lib/adal/noop_cache.rb +38 -0
- data/lib/adal/oauth_request.rb +76 -0
- data/lib/adal/request_parameters.rb +48 -0
- data/lib/adal/self_signed_jwt_factory.rb +96 -0
- data/lib/adal/templates/rst.13.xml.erb +35 -0
- data/lib/adal/templates/rst.2005.xml.erb +32 -0
- data/lib/adal/token_request.rb +231 -0
- data/lib/adal/token_response.rb +144 -0
- data/lib/adal/user_assertion.rb +57 -0
- data/lib/adal/user_credential.rb +152 -0
- data/lib/adal/user_identifier.rb +83 -0
- data/lib/adal/user_information.rb +49 -0
- data/lib/adal/util.rb +49 -0
- data/lib/adal/version.rb +36 -0
- data/lib/adal/wstrust_request.rb +100 -0
- data/lib/adal/wstrust_response.rb +168 -0
- data/lib/adal/xml_namespaces.rb +64 -0
- data/samples/authorization_code_example/README.md +10 -0
- data/samples/authorization_code_example/web_app.rb +139 -0
- data/samples/client_assertion_certificate_example/README.md +42 -0
- data/samples/client_assertion_certificate_example/app.rb +55 -0
- data/samples/on_behalf_of_example/README.md +35 -0
- data/samples/on_behalf_of_example/native_app.rb +52 -0
- data/samples/on_behalf_of_example/web_api.rb +71 -0
- data/samples/user_credentials_example/README.md +7 -0
- data/samples/user_credentials_example/app.rb +52 -0
- data/spec/adal/authentication_context_spec.rb +186 -0
- data/spec/adal/authentication_parameters_spec.rb +107 -0
- data/spec/adal/authority_spec.rb +122 -0
- data/spec/adal/cache_driver_spec.rb +191 -0
- data/spec/adal/cached_token_response_spec.rb +148 -0
- data/spec/adal/client_assertion_certificate_spec.rb +113 -0
- data/spec/adal/client_assertion_spec.rb +38 -0
- data/spec/adal/core_ext/hash_spec.rb +47 -0
- data/spec/adal/logging_spec.rb +48 -0
- data/spec/adal/memory_cache_spec.rb +107 -0
- data/spec/adal/mex_request_spec.rb +57 -0
- data/spec/adal/mex_response_spec.rb +143 -0
- data/spec/adal/self_signed_jwt_factory_spec.rb +63 -0
- data/spec/adal/token_request_spec.rb +150 -0
- data/spec/adal/token_response_spec.rb +102 -0
- data/spec/adal/user_credential_spec.rb +125 -0
- data/spec/adal/user_identifier_spec.rb +115 -0
- data/spec/adal/wstrust_request_spec.rb +51 -0
- data/spec/adal/wstrust_response_spec.rb +152 -0
- data/spec/fixtures/mex/insecureaddress.xml +924 -0
- data/spec/fixtures/mex/invalid_namespaces.xml +916 -0
- data/spec/fixtures/mex/malformed.xml +914 -0
- data/spec/fixtures/mex/microsoft.xml +916 -0
- data/spec/fixtures/mex/multiple_endpoints.xml +922 -0
- data/spec/fixtures/mex/no_matching_bindings.xml +916 -0
- data/spec/fixtures/mex/no_username_token_policies.xml +914 -0
- data/spec/fixtures/mex/no_wstrust_endpoints.xml +838 -0
- data/spec/fixtures/mex/only_13.xml +842 -0
- data/spec/fixtures/mex/only_2005.xml +842 -0
- data/spec/fixtures/oauth/error.json +1 -0
- data/spec/fixtures/oauth/success.json +1 -0
- data/spec/fixtures/oauth/success_with_id_token.json +1 -0
- data/spec/fixtures/wstrust/error.xml +24 -0
- data/spec/fixtures/wstrust/invalid_namespaces.xml +136 -0
- data/spec/fixtures/wstrust/missing_security_tokens.xml +90 -0
- data/spec/fixtures/wstrust/success.xml +136 -0
- data/spec/fixtures/wstrust/token.xml +1 -0
- data/spec/fixtures/wstrust/too_many_security_tokens.xml +219 -0
- data/spec/fixtures/wstrust/unrecognized_token_type.xml +136 -0
- data/spec/fixtures/wstrust/wstrust.13.xml +1 -0
- data/spec/fixtures/wstrust/wstrust.2005.xml +89 -0
- data/spec/spec_helper.rb +53 -0
- data/spec/support/fake_data.rb +40 -0
- data/spec/support/fake_token_endpoint.rb +108 -0
- metadata +265 -0
@@ -0,0 +1,126 @@
|
|
1
|
+
#-------------------------------------------------------------------------------
|
2
|
+
# Copyright (c) 2015 Micorosft Corporation
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
# of this software and associated documentation files (the "Software"), to deal
|
6
|
+
# in the Software without restriction, including without limitation the rights
|
7
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
# copies of the Software, and to permit persons to whom the Software is
|
9
|
+
# furnished to do so, subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in
|
12
|
+
# all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
# THE SOFTWARE.
|
21
|
+
#-------------------------------------------------------------------------------
|
22
|
+
|
23
|
+
require_relative './logging'
|
24
|
+
require_relative './util'
|
25
|
+
|
26
|
+
require 'net/http'
|
27
|
+
require 'uri'
|
28
|
+
|
29
|
+
module ADAL
|
30
|
+
# Authentication parameters from an unauthorized 401 response from a resource
|
31
|
+
# server that can be used to create an AuthenticationContext.
|
32
|
+
class AuthenticationParameters
|
33
|
+
extend Logging
|
34
|
+
include Util
|
35
|
+
|
36
|
+
AUTHENTICATE_HEADER = 'www-authenticate'
|
37
|
+
AUTHORITY_KEY = 'authorization_uri'
|
38
|
+
RESOURCE_KEY = 'resource'
|
39
|
+
|
40
|
+
BEARER_CHALLENGE_VALIDATION = /^\s*Bearer\s+([^,\s="]+?)="?([^"]*?)"?\s*
|
41
|
+
(,\s*([^,\s="]+?)="([^:]*?)"\s*)*$/x
|
42
|
+
private_constant :BEARER_CHALLENGE_VALIDATION
|
43
|
+
FIRST_KEY_VALUE = /^\s*Bearer\s+([^, \s="]+?)="([^"]*?)"\s*/
|
44
|
+
private_constant :FIRST_KEY_VALUE
|
45
|
+
OTHER_KEY_VALUE = /(?:,\s*([^,\s="]+?)="([^"]*?)"\s*)/
|
46
|
+
private_constant :OTHER_KEY_VALUE
|
47
|
+
|
48
|
+
attr_reader :authority_uri
|
49
|
+
attr_reader :resource
|
50
|
+
|
51
|
+
##
|
52
|
+
# Creates authentication parameters from the address of the resource. The
|
53
|
+
# resource server must respond with 401 unauthorized response with a
|
54
|
+
# www-authenticate header containing the authentication parameters.
|
55
|
+
#
|
56
|
+
# @param URI resource_url
|
57
|
+
# The address of the desired resource.
|
58
|
+
# @return AuthenticationParameters
|
59
|
+
def self.create_from_resource_url(resource_url)
|
60
|
+
logger.verbose('Attempting to retrieve authentication parameters from ' \
|
61
|
+
"#{resource_url}.")
|
62
|
+
response = Net::HTTP.post_form(URI.parse(resource_url.to_s), {})
|
63
|
+
unless response.key? AUTHENTICATE_HEADER
|
64
|
+
fail ArgumentError, 'The specified resource uri does not support ' \
|
65
|
+
'OAuth challenges.'
|
66
|
+
end
|
67
|
+
create_from_authenticate_header(response[AUTHENTICATE_HEADER])
|
68
|
+
end
|
69
|
+
|
70
|
+
##
|
71
|
+
# Creates an AuthenticationParameters object from a www-authenticate
|
72
|
+
# response header.
|
73
|
+
#
|
74
|
+
# @param String challenge
|
75
|
+
# The raw www-authenticate header.
|
76
|
+
# @return AuthenticationParameters
|
77
|
+
def self.create_from_authenticate_header(challenge)
|
78
|
+
params = parse_challenge(challenge)
|
79
|
+
if params.nil? || !params.key?(AUTHORITY_KEY)
|
80
|
+
logger.warn('Unable to create AuthenticationParameters from header ' \
|
81
|
+
"#{challenge}.")
|
82
|
+
return
|
83
|
+
end
|
84
|
+
logger.verbose("Authentication header #{challenge} was successfully " \
|
85
|
+
'parsed as an OAuth challenge into a parameters hash.')
|
86
|
+
AuthenticationParameters.new(
|
87
|
+
params[AUTHORITY_KEY], params[RESOURCE_KEY])
|
88
|
+
end
|
89
|
+
|
90
|
+
##
|
91
|
+
# Parses a challenge from the www-authenticate header into a hash of
|
92
|
+
# parameters.
|
93
|
+
#
|
94
|
+
# @param String challenge
|
95
|
+
# @return Hash
|
96
|
+
def self.parse_challenge(challenge)
|
97
|
+
if challenge !~ BEARER_CHALLENGE_VALIDATION
|
98
|
+
logger.warn("#{challenge} is not parseable as an RFC6750 OAuth2 " \
|
99
|
+
'challenge.')
|
100
|
+
return
|
101
|
+
end
|
102
|
+
Hash[challenge.scan(FIRST_KEY_VALUE) + challenge.scan(OTHER_KEY_VALUE)]
|
103
|
+
end
|
104
|
+
private_class_method :parse_challenge
|
105
|
+
|
106
|
+
##
|
107
|
+
# Constructs a new AuthenticationParameters.
|
108
|
+
#
|
109
|
+
# @param String|URI authority_uri
|
110
|
+
# The uri of the authority server, including both host and tenant.
|
111
|
+
# @param String
|
112
|
+
def initialize(authority_uri, resource = nil)
|
113
|
+
fail_if_arguments_nil(authority_uri)
|
114
|
+
@authority_uri = URI.parse(authority_uri.to_s)
|
115
|
+
@resource = resource
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# Creates an AuthenticationContext based on the parameters.
|
120
|
+
#
|
121
|
+
# @return AuthenticationContext
|
122
|
+
def create_context
|
123
|
+
AuthenticationContext.new(@authority_uri.host, @authority_uri.path[1..-1])
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
#-------------------------------------------------------------------------------
|
2
|
+
# Copyright (c) 2015 Micorosft Corporation
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
# of this software and associated documentation files (the "Software"), to deal
|
6
|
+
# in the Software without restriction, including without limitation the rights
|
7
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
# copies of the Software, and to permit persons to whom the Software is
|
9
|
+
# furnished to do so, subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in
|
12
|
+
# all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
# THE SOFTWARE.
|
21
|
+
#-------------------------------------------------------------------------------
|
22
|
+
|
23
|
+
require_relative './logging'
|
24
|
+
|
25
|
+
require 'json'
|
26
|
+
require 'net/http'
|
27
|
+
require 'uri'
|
28
|
+
require 'uri_template'
|
29
|
+
|
30
|
+
module ADAL
|
31
|
+
# An authentication and token server with the ability to self validate.
|
32
|
+
class Authority
|
33
|
+
include Logging
|
34
|
+
|
35
|
+
AUTHORIZE_PATH = '/oauth2/authorize'
|
36
|
+
COMMON_TENANT = 'common'
|
37
|
+
DISCOVERY_TEMPLATE = URITemplate.new('https://{host}/common/discovery/' \
|
38
|
+
'instance?authorization_endpoint={endpoint}&api-version=1.0')
|
39
|
+
TENANT_DISCOVERY_ENDPOINT_KEY = 'tenant_discovery_endpoint'
|
40
|
+
TOKEN_PATH = '/oauth2/token'
|
41
|
+
WELL_KNOWN_AUTHORITY_HOSTS = [
|
42
|
+
'login.windows.net',
|
43
|
+
'login.microsoftonline.com',
|
44
|
+
'login.chinacloudapi.cn',
|
45
|
+
'login.cloudgovapi.us'
|
46
|
+
]
|
47
|
+
WORLD_WIDE_AUTHORITY = 'login.microsoftonline.com'
|
48
|
+
|
49
|
+
attr_reader :host
|
50
|
+
attr_reader :tenant
|
51
|
+
|
52
|
+
##
|
53
|
+
# Creates a new Authority.
|
54
|
+
#
|
55
|
+
# @param [String] host
|
56
|
+
# The host name of the authority server.
|
57
|
+
# @param [String] tenant
|
58
|
+
# The name of the tenant for the Authority to access.
|
59
|
+
# @option [Boolean] validate_authority (false)
|
60
|
+
# The setting that controls whether the Authority instance will check that
|
61
|
+
# it matches a set of know authorities or can dynamically retrieve an
|
62
|
+
# identifying response.
|
63
|
+
def initialize(host = WORLD_WIDE_AUTHORITY,
|
64
|
+
tenant = COMMON_TENANT,
|
65
|
+
validate_authority = false)
|
66
|
+
@host = host
|
67
|
+
@tenant = tenant
|
68
|
+
@validated = !validate_authority
|
69
|
+
end
|
70
|
+
|
71
|
+
public
|
72
|
+
|
73
|
+
##
|
74
|
+
# URI that can be used to acquire authorization codes.
|
75
|
+
#
|
76
|
+
# @optional Hash params
|
77
|
+
# Query parameters that will added to the endpoint.
|
78
|
+
# @return [URI]
|
79
|
+
def authorize_endpoint(params = nil)
|
80
|
+
params = params.select { |_, v| !v.nil? } if params.respond_to? :select
|
81
|
+
if params.nil? || params.empty?
|
82
|
+
URI::HTTPS.build(host: @host, path: '/' + @tenant + AUTHORIZE_PATH)
|
83
|
+
else
|
84
|
+
URI::HTTPS.build(host: @host,
|
85
|
+
path: '/' + @tenant + AUTHORIZE_PATH,
|
86
|
+
query: URI.encode_www_form(params))
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
##
|
91
|
+
# URI that can be used to acquire tokens.
|
92
|
+
#
|
93
|
+
# @return [URI]
|
94
|
+
def token_endpoint
|
95
|
+
URI::HTTPS.build(host: @host, path: '/' + @tenant + TOKEN_PATH)
|
96
|
+
end
|
97
|
+
|
98
|
+
##
|
99
|
+
# Checks if the authority matches a set list of known authorities or if it
|
100
|
+
# can be resolved by the discovery endpoint.
|
101
|
+
#
|
102
|
+
# @return [Boolean]
|
103
|
+
# True if the Authority was successfully validated.
|
104
|
+
def validate
|
105
|
+
@validated = validated_statically? unless validated?
|
106
|
+
@validated = validated_dynamically? unless validated?
|
107
|
+
@validated
|
108
|
+
end
|
109
|
+
|
110
|
+
# @return [Boolean]
|
111
|
+
def validated?
|
112
|
+
@validated
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
##
|
118
|
+
# Creates an instance discovery endpoint url for authority that this object
|
119
|
+
# represents.
|
120
|
+
#
|
121
|
+
# @return [URI]
|
122
|
+
def discovery_uri(host = WORLD_WIDE_AUTHORITY)
|
123
|
+
URI(DISCOVERY_TEMPLATE.expand(host: host, endpoint: authorize_endpoint))
|
124
|
+
end
|
125
|
+
|
126
|
+
##
|
127
|
+
# Performs instance discovery via a network call to well known authorities.
|
128
|
+
#
|
129
|
+
# @return [String]
|
130
|
+
# The tenant discovery endpoint, if found. Otherwise nil.
|
131
|
+
def validated_dynamically?
|
132
|
+
logger.verbose("Attempting instance discovery at: #{discovery_uri}.")
|
133
|
+
http_response = Net::HTTP.get(discovery_uri)
|
134
|
+
if http_response.nil?
|
135
|
+
logger.error('Dynamic validation received no response from endpoint.')
|
136
|
+
return false
|
137
|
+
end
|
138
|
+
parse_dynamic_validation(JSON.parse(http_response))
|
139
|
+
end
|
140
|
+
|
141
|
+
# @return [Boolean]
|
142
|
+
def validated_statically?
|
143
|
+
logger.verbose('Performing static instance discovery.')
|
144
|
+
found_it = WELL_KNOWN_AUTHORITY_HOSTS.include? @host
|
145
|
+
if found_it
|
146
|
+
logger.verbose('Authority validated via static instance discovery.')
|
147
|
+
end
|
148
|
+
found_it
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
# @param Hash
|
154
|
+
# @return Boolean
|
155
|
+
def parse_dynamic_validation(response)
|
156
|
+
unless response.key? TENANT_DISCOVERY_ENDPOINT_KEY
|
157
|
+
logger.error('Received unexpected response from instance discovery ' \
|
158
|
+
"endpoint: #{response}. Unable to validate dynamically.")
|
159
|
+
return false
|
160
|
+
end
|
161
|
+
logger.verbose('Authority validated via dynamic instance discovery.')
|
162
|
+
response[TENANT_DISCOVERY_ENDPOINT_KEY]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
#-------------------------------------------------------------------------------
|
2
|
+
# Copyright (c) 2015 Micorosft Corporation
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
# of this software and associated documentation files (the "Software"), to deal
|
6
|
+
# in the Software without restriction, including without limitation the rights
|
7
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
# copies of the Software, and to permit persons to whom the Software is
|
9
|
+
# furnished to do so, subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in
|
12
|
+
# all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
# THE SOFTWARE.
|
21
|
+
#-------------------------------------------------------------------------------
|
22
|
+
|
23
|
+
require_relative './core_ext'
|
24
|
+
require_relative './logging'
|
25
|
+
require_relative './request_parameters'
|
26
|
+
require_relative './util'
|
27
|
+
|
28
|
+
using ADAL::CoreExt
|
29
|
+
|
30
|
+
module ADAL
|
31
|
+
# Performs logical operations on the TokenCache in the context of one token
|
32
|
+
# request.
|
33
|
+
class CacheDriver
|
34
|
+
include Logging
|
35
|
+
include RequestParameters
|
36
|
+
|
37
|
+
FIELDS = { user_info: USER_INFO,
|
38
|
+
username: USERNAME,
|
39
|
+
resource: RESOURCE }
|
40
|
+
|
41
|
+
##
|
42
|
+
# Constructs a CacheDriver to interact with a token cache.
|
43
|
+
#
|
44
|
+
# @param String authority
|
45
|
+
# The URL of the authority endpoint.
|
46
|
+
# @param ClientAssertion|ClientCredential|etc client
|
47
|
+
# The credentials representing the calling application. We need this
|
48
|
+
# instead of just the client id so that the tokens can be refreshed if
|
49
|
+
# necessary.
|
50
|
+
# @param TokenCache token_cache
|
51
|
+
# The cache implementation to store tokens.
|
52
|
+
# @optional Fixnum expiration_buffer_sec
|
53
|
+
# The number of seconds to use as a leeway when dealing with cache expiry.
|
54
|
+
def initialize(
|
55
|
+
authority, client, token_cache = NoopCache.new, expiration_buffer_sec = 0)
|
56
|
+
@authority = authority
|
57
|
+
@client = client
|
58
|
+
@expiration_buffer_sec = expiration_buffer_sec
|
59
|
+
@token_cache = token_cache
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Checks if a TokenResponse is successful and if so adds it to the token
|
64
|
+
# cache for future retrieval.
|
65
|
+
#
|
66
|
+
# @param SuccessResponse token_response
|
67
|
+
# The successful token response to be cached. If it is not successful, it
|
68
|
+
# fails silently.
|
69
|
+
def add(token_response)
|
70
|
+
return unless token_response.instance_of? SuccessResponse
|
71
|
+
logger.verbose('Adding successful TokenResponse to cache.')
|
72
|
+
entry = CachedTokenResponse.new(@client, @authority, token_response)
|
73
|
+
update_refresh_tokens(entry) if entry.mrrt?
|
74
|
+
@token_cache.add(entry)
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Searches the cache for a token matching a specific query of fields.
|
79
|
+
#
|
80
|
+
# @param Hash query
|
81
|
+
# The fields to match against.
|
82
|
+
# @return TokenResponse
|
83
|
+
def find(query = {})
|
84
|
+
query = query.map { |k, v| [FIELDS[k], v] if FIELDS[k] }.compact.to_h
|
85
|
+
resource = query.delete(RESOURCE)
|
86
|
+
matches = validate(
|
87
|
+
find_all_cached_entries(
|
88
|
+
query.reverse_merge(
|
89
|
+
authority: @authority, client_id: @client.client_id))
|
90
|
+
)
|
91
|
+
resource_specific(matches, resource) || refresh_mrrt(matches, resource)
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
##
|
97
|
+
# All cache entries that match a query. This matches keys in values against
|
98
|
+
# a hash to method calls on an object.
|
99
|
+
#
|
100
|
+
# @param Hash query
|
101
|
+
# The fields to be matched and the values to match them to.
|
102
|
+
# @return Array<CachedTokenResponse>
|
103
|
+
def find_all_cached_entries(query)
|
104
|
+
logger.verbose("Searching cache for tokens by keys: #{query.keys}.")
|
105
|
+
@token_cache.find do |entry|
|
106
|
+
query.map do |k, v|
|
107
|
+
(entry.respond_to? k.to_sym) && (v == entry.send(k.to_sym))
|
108
|
+
end.reduce(:&)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
##
|
113
|
+
# Attempts to obtain an access token for a resource with refresh tokens from
|
114
|
+
# a list of MRRTs.
|
115
|
+
#
|
116
|
+
# @param Array[CachedTokenResponse]
|
117
|
+
# @return SuccessResponse|nil
|
118
|
+
def refresh_mrrt(responses, resource)
|
119
|
+
logger.verbose("Attempting to obtain access token for #{resource} by " \
|
120
|
+
"refreshing 1 of #{responses.count(&:mrrt?)} matching " \
|
121
|
+
'MRRTs.')
|
122
|
+
responses.each do |response|
|
123
|
+
if response.mrrt?
|
124
|
+
refresh_response = response.refresh(resource)
|
125
|
+
return refresh_response if add(refresh_response)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
nil
|
129
|
+
end
|
130
|
+
|
131
|
+
##
|
132
|
+
# Searches a list of CachedTokenResponses for one that matches the resource.
|
133
|
+
#
|
134
|
+
# @param Array[CachedTokenResponse]
|
135
|
+
# @return SuccessResponse|nil
|
136
|
+
def resource_specific(responses, resource)
|
137
|
+
logger.verbose("Looking through #{responses.size} matching cache " \
|
138
|
+
"entries for resource #{resource}.")
|
139
|
+
responses.select { |response| response.resource == resource }
|
140
|
+
.map(&:token_response).first
|
141
|
+
end
|
142
|
+
|
143
|
+
##
|
144
|
+
# Updates the refresh tokens of all tokens in the cache that match a given
|
145
|
+
# MRRT.
|
146
|
+
#
|
147
|
+
# @param CachedTokenResponse mrrt
|
148
|
+
# A new MRRT containing a refresh token to update other matching cache
|
149
|
+
# entries with.
|
150
|
+
def update_refresh_tokens(mrrt)
|
151
|
+
fail ArgumentError, 'Token must contain an MRRT.' unless mrrt.mrrt?
|
152
|
+
@token_cache.find.each do |entry|
|
153
|
+
entry.refresh_token = mrrt.refresh_token if mrrt.can_refresh?(entry)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
##
|
158
|
+
# Checks if an array of current cache entries are still valid, attempts to
|
159
|
+
# refresh those that have expired and discards those that cannot be.
|
160
|
+
#
|
161
|
+
# @param Array[CachedTokenResponse] entries
|
162
|
+
# The tokens to validate.
|
163
|
+
# @return Array[CachedTokenResponse]
|
164
|
+
def validate(entries)
|
165
|
+
logger.verbose("Validating #{entries.size} possible cache matches.")
|
166
|
+
valid_entries = entries.group_by(&:validate)
|
167
|
+
@token_cache.remove(valid_entries[false] || [])
|
168
|
+
valid_entries[true] || []
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|