custom-adal 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|