custom-adal 1.0.1
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 +6 -0
- data/.rubocop.yml +7 -0
- data/.travis.yml +7 -0
- data/Gemfile +25 -0
- data/LICENSE.txt +21 -0
- data/README.md +106 -0
- data/Rakefile +39 -0
- data/adal.gemspec +52 -0
- data/contributing.md +127 -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/hash.rb +34 -0
- data/lib/adal/core_ext.rb +26 -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/lib/adal.rb +24 -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 +264 -0
|
@@ -0,0 +1,231 @@
|
|
|
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 './cache_driver'
|
|
24
|
+
require_relative './logging'
|
|
25
|
+
require_relative './noop_cache'
|
|
26
|
+
require_relative './oauth_request'
|
|
27
|
+
require_relative './request_parameters'
|
|
28
|
+
|
|
29
|
+
require 'openssl'
|
|
30
|
+
|
|
31
|
+
module ADAL
|
|
32
|
+
# A request for a token that may be fulfilled by a cache or an OAuthRequest
|
|
33
|
+
# to a token endpoint.
|
|
34
|
+
class TokenRequest
|
|
35
|
+
include Logging
|
|
36
|
+
include RequestParameters
|
|
37
|
+
|
|
38
|
+
# An error that signifies an attempt to perform OAuth with a UserIdentifier.
|
|
39
|
+
# UserIdentifiers can only be used to retrieve access tokens from the cache,
|
|
40
|
+
# so if no matching cache token is found, this error is thrown.
|
|
41
|
+
class UserCredentialError < StandardError; end
|
|
42
|
+
|
|
43
|
+
# All accepted grant types. This module can be mixed-in to other classes
|
|
44
|
+
# that require them.
|
|
45
|
+
module GrantType
|
|
46
|
+
AUTHORIZATION_CODE = 'authorization_code'
|
|
47
|
+
CLIENT_CREDENTIALS = 'client_credentials'
|
|
48
|
+
JWT_BEARER = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
|
|
49
|
+
PASSWORD = 'password'
|
|
50
|
+
REFRESH_TOKEN = 'refresh_token'
|
|
51
|
+
SAML1 = 'urn:ietf:params:oauth:grant-type:saml1_1-bearer'
|
|
52
|
+
SAML2 = 'urn:ietf:params:oauth:grant-type:saml2-bearer'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
##
|
|
56
|
+
# Constructs a TokenRequest.
|
|
57
|
+
#
|
|
58
|
+
# @param [Authority] authority
|
|
59
|
+
# The Authority providing authorization and token endpoints.
|
|
60
|
+
# @param ClientCredential|ClientAssertion|ClientAssertionCertificate
|
|
61
|
+
# Used to identify the client. Provides a request_parameters method
|
|
62
|
+
# that yields the relevant client credential parameters.
|
|
63
|
+
# @option [TokenCache] token_cache
|
|
64
|
+
# The cache implementation to store tokens. A NoopCache that stores no
|
|
65
|
+
# tokens will be used by default.
|
|
66
|
+
def initialize(authority, client, token_cache = NoopCache.new)
|
|
67
|
+
@authority = authority
|
|
68
|
+
@cache_driver = CacheDriver.new(authority, client, token_cache)
|
|
69
|
+
@client = client
|
|
70
|
+
@token_cache = token_cache
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
public
|
|
74
|
+
|
|
75
|
+
##
|
|
76
|
+
# Gets a token based solely on the clients credentials that were used to
|
|
77
|
+
# initialize the token request.
|
|
78
|
+
#
|
|
79
|
+
# @param String resource
|
|
80
|
+
# The resource for which the requested access token will provide access.
|
|
81
|
+
# @return TokenResponse
|
|
82
|
+
def get_for_client(resource)
|
|
83
|
+
logger.verbose("TokenRequest getting token for client for #{resource}.")
|
|
84
|
+
request(GRANT_TYPE => GrantType::CLIENT_CREDENTIALS,
|
|
85
|
+
RESOURCE => resource)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
##
|
|
89
|
+
# Gets a token based on a previously acquired authentication code.
|
|
90
|
+
#
|
|
91
|
+
# @param String auth_code
|
|
92
|
+
# An authentication code that was previously acquired from an
|
|
93
|
+
# authentication endpoint.
|
|
94
|
+
# @param String redirect_uri
|
|
95
|
+
# The redirect uri that was passed to the authentication endpoint when the
|
|
96
|
+
# auth code was acquired.
|
|
97
|
+
# @optional String resource
|
|
98
|
+
# The resource for which the requested access token will provide access.
|
|
99
|
+
# @return TokenResponse
|
|
100
|
+
def get_with_authorization_code(auth_code, redirect_uri, resource = nil)
|
|
101
|
+
logger.verbose('TokenRequest getting token with authorization code ' \
|
|
102
|
+
"#{auth_code}, redirect_uri #{redirect_uri} and " \
|
|
103
|
+
"resource #{resource}.")
|
|
104
|
+
request(CODE => auth_code,
|
|
105
|
+
GRANT_TYPE => GrantType::AUTHORIZATION_CODE,
|
|
106
|
+
REDIRECT_URI => URI.parse(redirect_uri.to_s),
|
|
107
|
+
RESOURCE => resource)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
##
|
|
111
|
+
# Gets a token based on a previously acquired refresh token.
|
|
112
|
+
#
|
|
113
|
+
# @param String refresh_token
|
|
114
|
+
# The refresh token that was previously acquired from a token response.
|
|
115
|
+
# @optional String resource
|
|
116
|
+
# The resource for which the requested access token will provide access.
|
|
117
|
+
# @return TokenResponse
|
|
118
|
+
def get_with_refresh_token(refresh_token, resource = nil)
|
|
119
|
+
logger.verbose('TokenRequest getting token with refresh token digest ' \
|
|
120
|
+
"#{Digest::SHA256.hexdigest refresh_token} and resource " \
|
|
121
|
+
"#{resource}.")
|
|
122
|
+
request_no_cache(GRANT_TYPE => GrantType::REFRESH_TOKEN,
|
|
123
|
+
REFRESH_TOKEN => refresh_token,
|
|
124
|
+
RESOURCE => resource)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
##
|
|
128
|
+
# Gets a token based on possessing the users credentials.
|
|
129
|
+
#
|
|
130
|
+
# @param UserCredential|UserIdentifier user_cred
|
|
131
|
+
# Something that can be used to verify the user. Typically a username
|
|
132
|
+
# and password. If it is a UserIdentifier, only the cache will be checked.
|
|
133
|
+
# If a matching token is not there, it will fail.
|
|
134
|
+
# @optional String resource
|
|
135
|
+
# The resource for which the requested access token will provide access.
|
|
136
|
+
# @return TokenResponse
|
|
137
|
+
def get_with_user_credential(user_cred, resource = nil)
|
|
138
|
+
logger.verbose('TokenRequest getting token with user credential ' \
|
|
139
|
+
"#{user_cred} and resource #{resource}.")
|
|
140
|
+
oauth = if user_cred.is_a? UserIdentifier
|
|
141
|
+
lambda do
|
|
142
|
+
fail UserCredentialError,
|
|
143
|
+
'UserIdentifier can only be used once there is a ' \
|
|
144
|
+
'matching token in the cache.'
|
|
145
|
+
end
|
|
146
|
+
end || -> {}
|
|
147
|
+
request(user_cred.request_params.merge(RESOURCE => resource), &oauth)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
##
|
|
153
|
+
# The OAuth parameters that are specific to the client for which tokens will
|
|
154
|
+
# be requested.
|
|
155
|
+
#
|
|
156
|
+
# @return Hash
|
|
157
|
+
def client_params
|
|
158
|
+
@client.request_params
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
##
|
|
162
|
+
# Attempts to fulfill a token request, first via the token cache and then
|
|
163
|
+
# through OAuth.
|
|
164
|
+
#
|
|
165
|
+
# @param Hash params
|
|
166
|
+
# Any additional request parameters that should be used.
|
|
167
|
+
# @return TokenResponse
|
|
168
|
+
def request(params, &block)
|
|
169
|
+
cached_token = check_cache(request_params(params))
|
|
170
|
+
return cached_token if cached_token
|
|
171
|
+
cache_response(request_no_cache(request_params(params), &block))
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
##
|
|
175
|
+
# Executes an OAuth request based on the params and returns it.
|
|
176
|
+
#
|
|
177
|
+
# @param Hash params
|
|
178
|
+
# Any additional request parameters that should be used.
|
|
179
|
+
# @return TokenResponse
|
|
180
|
+
def request_no_cache(params)
|
|
181
|
+
yield if block_given?
|
|
182
|
+
oauth_request(request_params(params)).execute
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
##
|
|
186
|
+
# Adds client params to additional params. If there is a conflict, the value
|
|
187
|
+
# from additional_params is used. It can be called multiple times, because
|
|
188
|
+
# request_params(request_params(x)) == request_params(x).
|
|
189
|
+
#
|
|
190
|
+
# @param Hash
|
|
191
|
+
# @return Hash
|
|
192
|
+
def request_params(additional_params)
|
|
193
|
+
client_params.merge(additional_params).select { |_, v| !v.nil? }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
##
|
|
197
|
+
# Helper method to chain OAuthRequest and cache operation.
|
|
198
|
+
#
|
|
199
|
+
# @param TokenResponse
|
|
200
|
+
# The token response to cache.
|
|
201
|
+
# @return TokenResponse
|
|
202
|
+
def cache_response(token_response)
|
|
203
|
+
@cache_driver.add(token_response)
|
|
204
|
+
token_response
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
##
|
|
208
|
+
# Attempts to fulfill the request from @token_cache.
|
|
209
|
+
#
|
|
210
|
+
# @return TokenResponse
|
|
211
|
+
# If the cache contains a valid response it wil be returned as a
|
|
212
|
+
# SuccessResponse. Otherwise returns nil.
|
|
213
|
+
def check_cache(params)
|
|
214
|
+
logger.verbose("TokenRequest checking cache #{@token_cache} for token.")
|
|
215
|
+
result = @cache_driver.find(params)
|
|
216
|
+
logger.info("#{result ? 'Found' : 'Did not find'} token in cache.")
|
|
217
|
+
result
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
##
|
|
221
|
+
# Constructs an OAuthRequest from the TokenRequest instance.
|
|
222
|
+
#
|
|
223
|
+
# @param Hash params
|
|
224
|
+
# The OAuth parameters specific to the TokenRequest instance.
|
|
225
|
+
# @return OAuthRequest
|
|
226
|
+
def oauth_request(params)
|
|
227
|
+
logger.verbose('Resorting to OAuth to fulfill token request.')
|
|
228
|
+
OAuthRequest.new(@authority.token_endpoint, params)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
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 'digest'
|
|
26
|
+
require 'json'
|
|
27
|
+
require 'jwt'
|
|
28
|
+
require 'securerandom'
|
|
29
|
+
|
|
30
|
+
module ADAL
|
|
31
|
+
# The return type of all of the instance methods that return tokens.
|
|
32
|
+
class TokenResponse
|
|
33
|
+
extend Logging
|
|
34
|
+
|
|
35
|
+
##
|
|
36
|
+
# Constructs a TokenResponse from a raw hash. It will return either a
|
|
37
|
+
# SuccessResponse or an ErrorResponse depending on the fields of the hash.
|
|
38
|
+
#
|
|
39
|
+
# @param Hash raw_response
|
|
40
|
+
# The body of the HTTP response expressed as a raw hash.
|
|
41
|
+
# @return TokenResponse
|
|
42
|
+
def self.parse(raw_response)
|
|
43
|
+
logger.verbose('Attempting to create a TokenResponse from raw response.')
|
|
44
|
+
if raw_response.nil?
|
|
45
|
+
ErrorResponse.new
|
|
46
|
+
elsif raw_response['error']
|
|
47
|
+
ErrorResponse.new(JSON.parse(raw_response))
|
|
48
|
+
else
|
|
49
|
+
SuccessResponse.new(JSON.parse(raw_response))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
public
|
|
54
|
+
|
|
55
|
+
##
|
|
56
|
+
# Shorthand for checking if a token response is successful or failed.
|
|
57
|
+
#
|
|
58
|
+
# @return Boolean
|
|
59
|
+
def error?
|
|
60
|
+
self.respond_to? :error
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# A token response that contains an access token. All fields are read only
|
|
65
|
+
# and may be nil. Some fields are only populated in certain flows.
|
|
66
|
+
class SuccessResponse < TokenResponse
|
|
67
|
+
include Logging
|
|
68
|
+
|
|
69
|
+
# These fields may or may not be included in the response from the token
|
|
70
|
+
# endpoint.
|
|
71
|
+
OAUTH_FIELDS = [:access_token, :expires_in, :expires_on, :id_token,
|
|
72
|
+
:not_before, :refresh_token, :resource, :scope, :token_type]
|
|
73
|
+
OAUTH_FIELDS.each { |field| attr_reader field }
|
|
74
|
+
attr_reader :user_info
|
|
75
|
+
attr_reader :fields
|
|
76
|
+
|
|
77
|
+
##
|
|
78
|
+
# Constructs a SuccessResponse from a collection of fields returned from a
|
|
79
|
+
# token endpoint.
|
|
80
|
+
#
|
|
81
|
+
# @param Hash
|
|
82
|
+
def initialize(fields = {})
|
|
83
|
+
@fields = fields
|
|
84
|
+
fields.each { |k, v| instance_variable_set("@#{k}", v) }
|
|
85
|
+
parse_id_token(id_token)
|
|
86
|
+
@expires_on = @expires_in.to_i + Time.now.to_i
|
|
87
|
+
logger.info('Parsed a SuccessResponse with access token digest ' \
|
|
88
|
+
"#{Digest::SHA256.hexdigest @access_token.to_s} and " \
|
|
89
|
+
'refresh token digest ' \
|
|
90
|
+
"#{Digest::SHA256.hexdigest @refresh_token.to_s}.")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
##
|
|
94
|
+
# Converts the fields that were used to create this token response into
|
|
95
|
+
# a JSON string. This is helpful for storing then in a database.
|
|
96
|
+
#
|
|
97
|
+
# @param JSON::Ext::Generator::State
|
|
98
|
+
# We don't care about this, because the JSON representation of this
|
|
99
|
+
# object does not depend on the fields before it.
|
|
100
|
+
# @return String
|
|
101
|
+
def to_json(_ = nil)
|
|
102
|
+
JSON.unparse(fields)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
##
|
|
106
|
+
# Parses the raw id token into an ADAL::UserInformation.
|
|
107
|
+
# If the id token is missing, an ADAL::UserInformation will still be
|
|
108
|
+
# generated, it just won't contain any displayable information.
|
|
109
|
+
#
|
|
110
|
+
# @param String id_token
|
|
111
|
+
# The id token to parse
|
|
112
|
+
# Adds an id token to the token response if one is not present
|
|
113
|
+
def parse_id_token(id_token)
|
|
114
|
+
if id_token.nil?
|
|
115
|
+
logger.warn('No id token found.')
|
|
116
|
+
@user_info ||= ADAL::UserInformation.new(unique_id: SecureRandom.uuid)
|
|
117
|
+
return
|
|
118
|
+
end
|
|
119
|
+
logger.verbose('Attempting to decode id token in token response.')
|
|
120
|
+
claims = JWT.decode(id_token.to_s, nil, false).first
|
|
121
|
+
@id_token = id_token
|
|
122
|
+
@user_info = ADAL::UserInformation.new(claims)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# A token response that contains an error code.
|
|
127
|
+
class ErrorResponse < TokenResponse
|
|
128
|
+
include Logging
|
|
129
|
+
|
|
130
|
+
OAUTH_FIELDS = [:error, :error_description, :error_codes, :timestamp,
|
|
131
|
+
:trace_id, :correlation_id, :submit_url, :context]
|
|
132
|
+
OAUTH_FIELDS.each { |field| attr_reader field }
|
|
133
|
+
|
|
134
|
+
# Constructs a Error from a collection of fields returned from a
|
|
135
|
+
# token endpoint.
|
|
136
|
+
#
|
|
137
|
+
# @param Hash
|
|
138
|
+
def initialize(fields = {})
|
|
139
|
+
fields.each { |k, v| instance_variable_set("@#{k}", v) }
|
|
140
|
+
logger.error("Parsed an ErrorResponse with error: #{@error} and error " \
|
|
141
|
+
"description: #{@error_description}.")
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
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 './token_request'
|
|
24
|
+
|
|
25
|
+
module ADAL
|
|
26
|
+
# An assertion and its representation type, stored as a JWT for
|
|
27
|
+
# the on-behalf-of flow.
|
|
28
|
+
class UserAssertion
|
|
29
|
+
attr_reader :assertion
|
|
30
|
+
attr_reader :assertion_type
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# Creates a new UserAssertion.
|
|
34
|
+
#
|
|
35
|
+
# @param String assertion
|
|
36
|
+
# An OAuth assertion representing the user.
|
|
37
|
+
# @optional AssertionType assertion_type
|
|
38
|
+
# The type of the assertion being made. Currently only JWT_BEARER is
|
|
39
|
+
# supported.
|
|
40
|
+
def initialize(
|
|
41
|
+
assertion, assertion_type = ADAL::TokenRequest::GrantType::JWT_BEARER)
|
|
42
|
+
@assertion = assertion
|
|
43
|
+
@assertion_type = assertion_type
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
##
|
|
47
|
+
# The relevant OAuth access token request parameters for this object.
|
|
48
|
+
#
|
|
49
|
+
# @return Hash
|
|
50
|
+
def request_params
|
|
51
|
+
{ grant_type: assertion_type,
|
|
52
|
+
assertion: assertion,
|
|
53
|
+
requested_token_use: :on_behalf_of,
|
|
54
|
+
scope: :openid }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
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 './authority'
|
|
24
|
+
require_relative './logger'
|
|
25
|
+
require_relative './mex_request'
|
|
26
|
+
require_relative './token_request'
|
|
27
|
+
require_relative './wstrust_request'
|
|
28
|
+
|
|
29
|
+
require 'base64'
|
|
30
|
+
require 'json'
|
|
31
|
+
require 'net/http'
|
|
32
|
+
require 'uri'
|
|
33
|
+
|
|
34
|
+
module ADAL
|
|
35
|
+
# A convenience class for username and password credentials.
|
|
36
|
+
class UserCredential
|
|
37
|
+
include Logging
|
|
38
|
+
|
|
39
|
+
# Federation response type from the userrealm endpoint.
|
|
40
|
+
module AccountType
|
|
41
|
+
FEDERATED = 'Federated'
|
|
42
|
+
MANAGED = 'Managed'
|
|
43
|
+
UNKNOWN = 'Unknown'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# ADAL only supports flows for managed and federated users.
|
|
47
|
+
class UnsupportedAccountTypeError < StandardError
|
|
48
|
+
def initialize(account_type)
|
|
49
|
+
super("Unsupported account type for authentication: #{account_type}.")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
attr_reader :username
|
|
54
|
+
attr_reader :password
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# Constructs a new UserCredential.
|
|
58
|
+
#
|
|
59
|
+
# @param String username
|
|
60
|
+
# @param String password
|
|
61
|
+
# @optional String authority_host
|
|
62
|
+
# The host name of the authority to verify the user against.
|
|
63
|
+
def initialize(
|
|
64
|
+
username, password, authority_host = Authority::WORLD_WIDE_AUTHORITY)
|
|
65
|
+
@username = username
|
|
66
|
+
@password = password
|
|
67
|
+
@authority_host = authority_host
|
|
68
|
+
@discovery_path = "/common/userrealm/#{URI.escape @username}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# Determines the account type based on a Home Realm Discovery request.
|
|
73
|
+
#
|
|
74
|
+
# @return UserCredential::AccountType
|
|
75
|
+
def account_type
|
|
76
|
+
realm_discovery_response['account_type']
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# The OAuth parameters that respresent this UserCredential.
|
|
81
|
+
#
|
|
82
|
+
# @return Hash
|
|
83
|
+
def request_params
|
|
84
|
+
case account_type
|
|
85
|
+
when AccountType::MANAGED
|
|
86
|
+
managed_request_params
|
|
87
|
+
when AccountType::FEDERATED
|
|
88
|
+
federated_request_params
|
|
89
|
+
else
|
|
90
|
+
fail UnsupportedAccountTypeError, account_type
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# :nocov:
|
|
95
|
+
def to_s
|
|
96
|
+
"UserCredential[Username: #{@username}, AccountType: #{account_type}]"
|
|
97
|
+
end
|
|
98
|
+
# :nocov:
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
# Memoized response from the discovery endpoint. Since a UserCredential is
|
|
103
|
+
# read only, this should only ever need to be called once.
|
|
104
|
+
# @return Hash
|
|
105
|
+
def realm_discovery_response
|
|
106
|
+
@realm_discovery_response ||=
|
|
107
|
+
JSON.parse(Net::HTTP.get(realm_discovery_uri))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @return URI
|
|
111
|
+
def realm_discovery_uri
|
|
112
|
+
URI::HTTPS.build(
|
|
113
|
+
host: @authority_host,
|
|
114
|
+
path: @discovery_path,
|
|
115
|
+
query: URI.encode_www_form('api-version' => '1.0'))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @return Hash
|
|
119
|
+
def federated_request_params
|
|
120
|
+
logger.verbose("Getting OAuth parameters for Federated #{@username}.")
|
|
121
|
+
wstrust_response = wstrust_request.execute(@username, @password)
|
|
122
|
+
{ assertion: Base64.encode64(wstrust_response.token).strip,
|
|
123
|
+
grant_type: wstrust_response.grant_type,
|
|
124
|
+
scope: :openid }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# @return URI
|
|
128
|
+
def federation_metadata_url
|
|
129
|
+
URI.parse(realm_discovery_response['federation_metadata_url'])
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @return Hash
|
|
133
|
+
def managed_request_params
|
|
134
|
+
logger.verbose("Getting OAuth parameters for Managed #{@username}.")
|
|
135
|
+
{ username: @username,
|
|
136
|
+
password: @password,
|
|
137
|
+
grant_type: TokenRequest::GrantType::PASSWORD,
|
|
138
|
+
scope: :openid }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @return MexResponse
|
|
142
|
+
def mex_response
|
|
143
|
+
@mex_response ||= MexRequest.new(federation_metadata_url).execute
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# @return WSTrustRequest
|
|
147
|
+
def wstrust_request
|
|
148
|
+
@wstrust_request ||=
|
|
149
|
+
WSTrustRequest.new(mex_response.wstrust_url, mex_response.action)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
module ADAL
|
|
24
|
+
# Identifier for users in the cache.
|
|
25
|
+
#
|
|
26
|
+
# Ideally, the application will first use a different OAuth flow, such as the
|
|
27
|
+
# Authorization Code flow, to acquire an ADAL::SuccessResponse. Then, it can
|
|
28
|
+
# create ADAL::UserIdentifier to query the cache which will refresh tokens as
|
|
29
|
+
# necessary.
|
|
30
|
+
class UserIdentifier
|
|
31
|
+
attr_reader :id
|
|
32
|
+
attr_reader :type
|
|
33
|
+
|
|
34
|
+
# Displayable IDs are human readable (eg email addresses) while Unique Ids
|
|
35
|
+
# are generally random UUIDs.
|
|
36
|
+
module Type
|
|
37
|
+
UNIQUE_ID = :UNIQUE_ID
|
|
38
|
+
DISPLAYABLE_ID = :DISPLAYABLE_ID
|
|
39
|
+
end
|
|
40
|
+
include Type
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# Creates a UserIdentifier with a specific type. Used for cache lookups.
|
|
44
|
+
# Matches .NET ADAL implementation.
|
|
45
|
+
#
|
|
46
|
+
# @param String id
|
|
47
|
+
# @param UserIdentifier::Type
|
|
48
|
+
# @return ADAL::UserIdentifier
|
|
49
|
+
def initialize(id, type)
|
|
50
|
+
unless [UNIQUE_ID, DISPLAYABLE_ID].include? type
|
|
51
|
+
fail ArgumentError, 'type must be an ADAL::UserIdentifier::Type.'
|
|
52
|
+
end
|
|
53
|
+
@id = id
|
|
54
|
+
@type = type
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# These parameters should only be used for cache lookup. This is enforced
|
|
59
|
+
# by ADAL::TokenRequest.
|
|
60
|
+
#
|
|
61
|
+
# @return Hash
|
|
62
|
+
def request_params
|
|
63
|
+
{ user_info: self }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
##
|
|
67
|
+
# Overrides comparison operator for cache lookups
|
|
68
|
+
#
|
|
69
|
+
# @param UserIdentifier other
|
|
70
|
+
# @return Boolean
|
|
71
|
+
def ==(other)
|
|
72
|
+
case other
|
|
73
|
+
when UserIdentifier
|
|
74
|
+
self.equal? other
|
|
75
|
+
when UserInformation
|
|
76
|
+
(type == UNIQUE_ID && id == other.unique_id) ||
|
|
77
|
+
(type == DISPLAYABLE_ID && id == other.displayable_id)
|
|
78
|
+
when String
|
|
79
|
+
@id == other
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|