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.
- 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
|