adal 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rubocop.yml +7 -0
  4. data/.travis.yml +7 -0
  5. data/Gemfile +25 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +97 -0
  8. data/Rakefile +39 -0
  9. data/adal.gemspec +52 -0
  10. data/contributing.md +127 -0
  11. data/lib/adal.rb +24 -0
  12. data/lib/adal/authentication_context.rb +202 -0
  13. data/lib/adal/authentication_parameters.rb +126 -0
  14. data/lib/adal/authority.rb +165 -0
  15. data/lib/adal/cache_driver.rb +171 -0
  16. data/lib/adal/cached_token_response.rb +190 -0
  17. data/lib/adal/client_assertion.rb +63 -0
  18. data/lib/adal/client_assertion_certificate.rb +89 -0
  19. data/lib/adal/client_credential.rb +46 -0
  20. data/lib/adal/core_ext.rb +26 -0
  21. data/lib/adal/core_ext/hash.rb +34 -0
  22. data/lib/adal/jwt_parameters.rb +39 -0
  23. data/lib/adal/logger.rb +90 -0
  24. data/lib/adal/logging.rb +98 -0
  25. data/lib/adal/memory_cache.rb +95 -0
  26. data/lib/adal/mex_request.rb +52 -0
  27. data/lib/adal/mex_response.rb +141 -0
  28. data/lib/adal/noop_cache.rb +38 -0
  29. data/lib/adal/oauth_request.rb +76 -0
  30. data/lib/adal/request_parameters.rb +48 -0
  31. data/lib/adal/self_signed_jwt_factory.rb +96 -0
  32. data/lib/adal/templates/rst.13.xml.erb +35 -0
  33. data/lib/adal/templates/rst.2005.xml.erb +32 -0
  34. data/lib/adal/token_request.rb +231 -0
  35. data/lib/adal/token_response.rb +144 -0
  36. data/lib/adal/user_assertion.rb +57 -0
  37. data/lib/adal/user_credential.rb +152 -0
  38. data/lib/adal/user_identifier.rb +83 -0
  39. data/lib/adal/user_information.rb +49 -0
  40. data/lib/adal/util.rb +49 -0
  41. data/lib/adal/version.rb +36 -0
  42. data/lib/adal/wstrust_request.rb +100 -0
  43. data/lib/adal/wstrust_response.rb +168 -0
  44. data/lib/adal/xml_namespaces.rb +64 -0
  45. data/samples/authorization_code_example/README.md +10 -0
  46. data/samples/authorization_code_example/web_app.rb +139 -0
  47. data/samples/client_assertion_certificate_example/README.md +42 -0
  48. data/samples/client_assertion_certificate_example/app.rb +55 -0
  49. data/samples/on_behalf_of_example/README.md +35 -0
  50. data/samples/on_behalf_of_example/native_app.rb +52 -0
  51. data/samples/on_behalf_of_example/web_api.rb +71 -0
  52. data/samples/user_credentials_example/README.md +7 -0
  53. data/samples/user_credentials_example/app.rb +52 -0
  54. data/spec/adal/authentication_context_spec.rb +186 -0
  55. data/spec/adal/authentication_parameters_spec.rb +107 -0
  56. data/spec/adal/authority_spec.rb +122 -0
  57. data/spec/adal/cache_driver_spec.rb +191 -0
  58. data/spec/adal/cached_token_response_spec.rb +148 -0
  59. data/spec/adal/client_assertion_certificate_spec.rb +113 -0
  60. data/spec/adal/client_assertion_spec.rb +38 -0
  61. data/spec/adal/core_ext/hash_spec.rb +47 -0
  62. data/spec/adal/logging_spec.rb +48 -0
  63. data/spec/adal/memory_cache_spec.rb +107 -0
  64. data/spec/adal/mex_request_spec.rb +57 -0
  65. data/spec/adal/mex_response_spec.rb +143 -0
  66. data/spec/adal/self_signed_jwt_factory_spec.rb +63 -0
  67. data/spec/adal/token_request_spec.rb +150 -0
  68. data/spec/adal/token_response_spec.rb +102 -0
  69. data/spec/adal/user_credential_spec.rb +125 -0
  70. data/spec/adal/user_identifier_spec.rb +115 -0
  71. data/spec/adal/wstrust_request_spec.rb +51 -0
  72. data/spec/adal/wstrust_response_spec.rb +152 -0
  73. data/spec/fixtures/mex/insecureaddress.xml +924 -0
  74. data/spec/fixtures/mex/invalid_namespaces.xml +916 -0
  75. data/spec/fixtures/mex/malformed.xml +914 -0
  76. data/spec/fixtures/mex/microsoft.xml +916 -0
  77. data/spec/fixtures/mex/multiple_endpoints.xml +922 -0
  78. data/spec/fixtures/mex/no_matching_bindings.xml +916 -0
  79. data/spec/fixtures/mex/no_username_token_policies.xml +914 -0
  80. data/spec/fixtures/mex/no_wstrust_endpoints.xml +838 -0
  81. data/spec/fixtures/mex/only_13.xml +842 -0
  82. data/spec/fixtures/mex/only_2005.xml +842 -0
  83. data/spec/fixtures/oauth/error.json +1 -0
  84. data/spec/fixtures/oauth/success.json +1 -0
  85. data/spec/fixtures/oauth/success_with_id_token.json +1 -0
  86. data/spec/fixtures/wstrust/error.xml +24 -0
  87. data/spec/fixtures/wstrust/invalid_namespaces.xml +136 -0
  88. data/spec/fixtures/wstrust/missing_security_tokens.xml +90 -0
  89. data/spec/fixtures/wstrust/success.xml +136 -0
  90. data/spec/fixtures/wstrust/token.xml +1 -0
  91. data/spec/fixtures/wstrust/too_many_security_tokens.xml +219 -0
  92. data/spec/fixtures/wstrust/unrecognized_token_type.xml +136 -0
  93. data/spec/fixtures/wstrust/wstrust.13.xml +1 -0
  94. data/spec/fixtures/wstrust/wstrust.2005.xml +89 -0
  95. data/spec/spec_helper.rb +53 -0
  96. data/spec/support/fake_data.rb +40 -0
  97. data/spec/support/fake_token_endpoint.rb +108 -0
  98. 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