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
data/lib/adal/util.rb
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
# Various helper methods that are useful across several classes and do not fit
|
|
25
|
+
# into the class hierarchy.
|
|
26
|
+
module Util
|
|
27
|
+
def fail_if_arguments_nil(*args)
|
|
28
|
+
fail ArgumentError, 'Arguments cannot be nil.' if args.any?(&:nil?)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param URI|String
|
|
32
|
+
# @return Net::HTTP
|
|
33
|
+
def http(uri)
|
|
34
|
+
uri = URI.parse(uri.to_s)
|
|
35
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
36
|
+
http.use_ssl = uri.scheme == 'https'
|
|
37
|
+
http
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
##
|
|
41
|
+
# Converts every key and value of a hash to string.
|
|
42
|
+
#
|
|
43
|
+
# @param Hash
|
|
44
|
+
# @return Hash
|
|
45
|
+
def string_hash(hash)
|
|
46
|
+
hash.map { |k, v| [k.to_s, v.to_s] }.to_h
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/adal/version.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
# The current ADAL version.
|
|
25
|
+
class Version
|
|
26
|
+
MAJOR = 1 unless defined? MAJOR
|
|
27
|
+
MINOR = 0 unless defined? MINOR
|
|
28
|
+
UPDATE = 0 unless defined? UPDATE
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
def to_s
|
|
32
|
+
[MAJOR, MINOR, UPDATE].compact.join('.')
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
require_relative './wstrust_response'
|
|
26
|
+
require_relative './xml_namespaces'
|
|
27
|
+
|
|
28
|
+
require 'erb'
|
|
29
|
+
require 'securerandom'
|
|
30
|
+
|
|
31
|
+
module ADAL
|
|
32
|
+
# A request to a WS-Trust endpoint of an ADFS server. Used to obtain a SAML
|
|
33
|
+
# token that can be exchanged for an access token at a token endpoint.
|
|
34
|
+
class WSTrustRequest
|
|
35
|
+
include Logging
|
|
36
|
+
include Util
|
|
37
|
+
include XmlNamespaces
|
|
38
|
+
|
|
39
|
+
DEFAULT_APPLIES_TO = 'urn:federation:MicrosoftOnline'
|
|
40
|
+
|
|
41
|
+
ACTION_TO_RST_TEMPLATE = {
|
|
42
|
+
WSTRUST_13 =>
|
|
43
|
+
File.expand_path('../templates/rst.13.xml.erb', __FILE__),
|
|
44
|
+
WSTRUST_2005 =>
|
|
45
|
+
File.expand_path('../templates/rst.2005.xml.erb', __FILE__)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
# Constructs a new WSTrustRequest.
|
|
50
|
+
#
|
|
51
|
+
# @param String|URI endpoint
|
|
52
|
+
# @param String action
|
|
53
|
+
# @param String applies_to
|
|
54
|
+
def initialize(
|
|
55
|
+
endpoint, action = WSTRUST_13, applies_to = DEFAULT_APPLIES_TO)
|
|
56
|
+
@applies_to = applies_to
|
|
57
|
+
@endpoint = URI.parse(endpoint.to_s)
|
|
58
|
+
@action = action
|
|
59
|
+
@render = ERB.new(File.read(ACTION_TO_RST_TEMPLATE[action]))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
##
|
|
63
|
+
# Performs a WS-Trust RequestSecurityToken request with a username and
|
|
64
|
+
# password to obtain a federated token.
|
|
65
|
+
#
|
|
66
|
+
# @param String username
|
|
67
|
+
# @param String password
|
|
68
|
+
# @return WSTrustResponse
|
|
69
|
+
def execute(username, password)
|
|
70
|
+
logger.verbose("Making a WSTrust request with action #{@action}.")
|
|
71
|
+
request = Net::HTTP::Get.new(@endpoint.path)
|
|
72
|
+
add_headers(request)
|
|
73
|
+
request.body = rst(username, password)
|
|
74
|
+
response = http(@endpoint).request(request)
|
|
75
|
+
if response.code == '200'
|
|
76
|
+
WSTrustResponse.parse(response.body)
|
|
77
|
+
else
|
|
78
|
+
fail WSTrustResponse::WSTrustError, "Failed request: code #{response.code}."
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# @param Net::HTTP::Get request
|
|
85
|
+
def add_headers(request)
|
|
86
|
+
request.add_field('Content-Type', 'application/soap+xml; charset=utf-8')
|
|
87
|
+
request.add_field('SOAPAction', @action)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @param String username
|
|
91
|
+
# @param String password
|
|
92
|
+
# @param String message_id
|
|
93
|
+
# @return String
|
|
94
|
+
def rst(username, password, message_id = SecureRandom.uuid)
|
|
95
|
+
created = Time.now
|
|
96
|
+
expires = created + 10 * 60 # 10 minute expiration
|
|
97
|
+
@render.result(binding)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
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 './token_request'
|
|
25
|
+
require_relative './util'
|
|
26
|
+
require_relative './xml_namespaces'
|
|
27
|
+
|
|
28
|
+
require 'nokogiri'
|
|
29
|
+
|
|
30
|
+
module ADAL
|
|
31
|
+
# Relevant fields from a WS-Trust response.
|
|
32
|
+
class WSTrustResponse
|
|
33
|
+
include XmlNamespaces
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
include Logging
|
|
37
|
+
include Util
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# All recognized SAML token types.
|
|
41
|
+
module TokenType
|
|
42
|
+
V1 = 'urn:oasis:names:tc:SAML:1.0:assertion'
|
|
43
|
+
V2 = 'urn:oasis:names:tc:SAML:2.0:assertion'
|
|
44
|
+
|
|
45
|
+
ALL_TYPES = [V1, V2]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class WSTrustError < StandardError; end
|
|
49
|
+
class UnrecognizedTokenTypeError < WSTrustError; end
|
|
50
|
+
|
|
51
|
+
ACTION_XPATH = '//s:Envelope/s:Header/a:Action/text()'
|
|
52
|
+
ERROR_XPATH = '//s:Envelope/s:Body/s:Fault/s:Code/s:Subcode/s:Value/text()'
|
|
53
|
+
FAULT_XPATH = '//s:Envelope/s:Body/s:Fault/s:Reason'
|
|
54
|
+
SECURITY_TOKEN_XPATH = './trust:RequestedSecurityToken'
|
|
55
|
+
TOKEN_RESPONSE_XPATH =
|
|
56
|
+
'//s:Envelope/s:Body/trust:RequestSecurityTokenResponse|//s:Envelope/s:' \
|
|
57
|
+
'Body/trust:RequestSecurityTokenResponseCollection/trust:RequestSecurit' \
|
|
58
|
+
'yTokenResponse'
|
|
59
|
+
TOKEN_TYPE_XPATH = "./*[local-name() = 'TokenType']/text()"
|
|
60
|
+
TOKEN_XPATH = "./*[local-name() = 'Assertion']"
|
|
61
|
+
|
|
62
|
+
##
|
|
63
|
+
# Parses a WS-Trust response from raw XML into an ADAL::WSTrustResponse
|
|
64
|
+
# object. Throws an error if the response contains an error.
|
|
65
|
+
#
|
|
66
|
+
# @param String|Nokogiri::XML raw_xml
|
|
67
|
+
# @return ADAL::WSTrustResponse
|
|
68
|
+
def self.parse(raw_xml)
|
|
69
|
+
fail_if_arguments_nil(raw_xml)
|
|
70
|
+
xml = Nokogiri::XML(raw_xml.to_s)
|
|
71
|
+
parse_error(xml)
|
|
72
|
+
namespace = ACTION_TO_NAMESPACE[parse_action(xml)]
|
|
73
|
+
token, token_type = parse_token(xml, namespace)
|
|
74
|
+
if token && token_type
|
|
75
|
+
WSTrustResponse.new(format_xml(token), format_xml(token_type))
|
|
76
|
+
else
|
|
77
|
+
fail WSTrustError, 'Unable to parse token from response.'
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
##
|
|
82
|
+
# Determines whether the response uses WS-Trust 2005 or WS-Trust 1.3.
|
|
83
|
+
#
|
|
84
|
+
# @param Nokogiri::XML::Document xml
|
|
85
|
+
# @return String
|
|
86
|
+
def self.parse_action(xml)
|
|
87
|
+
xml.xpath(ACTION_XPATH, NAMESPACES).to_s
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
##
|
|
91
|
+
# Checks a WS-Trust response for properly formatted error codes and
|
|
92
|
+
# descriptions. If found, raises an appropriate exception.
|
|
93
|
+
#
|
|
94
|
+
# @param Nokogiri::XML::Document xml
|
|
95
|
+
def self.parse_error(xml)
|
|
96
|
+
fault = xml.xpath(FAULT_XPATH, NAMESPACES).first
|
|
97
|
+
error = xml.xpath(ERROR_XPATH, NAMESPACES).first
|
|
98
|
+
error = format_xml(error).split(':')[1] || error if error
|
|
99
|
+
fail WSTrustError, "Fault: #{fault}. Error: #{error}." if fault || error
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @param Nokogiri::XML::Document xml
|
|
103
|
+
# @return String
|
|
104
|
+
def self.format_xml(xml)
|
|
105
|
+
xml.to_s.split("\n").map(&:strip).join
|
|
106
|
+
end
|
|
107
|
+
private_class_method :format_xml
|
|
108
|
+
|
|
109
|
+
# @param Nokogiri::XML::Document
|
|
110
|
+
# @return [Nokogiri::XML::Element, Nokogiri::XML::Text]
|
|
111
|
+
def self.parse_token(xml, namespace)
|
|
112
|
+
xml.xpath(TOKEN_RESPONSE_XPATH, namespace).select do |node|
|
|
113
|
+
requested_token = node.xpath(SECURITY_TOKEN_XPATH, namespace)
|
|
114
|
+
case requested_token.size
|
|
115
|
+
when 0
|
|
116
|
+
logger.warn('No security token in token response.')
|
|
117
|
+
next
|
|
118
|
+
when 1
|
|
119
|
+
token = requested_token.xpath(TOKEN_XPATH, namespace).first
|
|
120
|
+
next if token.nil?
|
|
121
|
+
return token, parse_token_type(node)
|
|
122
|
+
else
|
|
123
|
+
fail WSTrustError, 'Found too many RequestedSecurityTokens.'
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
private_class_method :parse_token
|
|
128
|
+
|
|
129
|
+
# @param Nokogiri::XML::Element token_response_node
|
|
130
|
+
# @return Nokogiri::XML::Text
|
|
131
|
+
def self.parse_token_type(token_response_node)
|
|
132
|
+
type = token_response_node.xpath(TOKEN_TYPE_XPATH, NAMESPACES).first
|
|
133
|
+
logger.warn('No type in token response node.') if type.nil?
|
|
134
|
+
type
|
|
135
|
+
end
|
|
136
|
+
private_class_method :parse_token_type
|
|
137
|
+
|
|
138
|
+
attr_reader :token
|
|
139
|
+
|
|
140
|
+
##
|
|
141
|
+
# Constructs a WSTrustResponse.
|
|
142
|
+
#
|
|
143
|
+
# @param String token
|
|
144
|
+
# The content of the returned token.
|
|
145
|
+
# @param WSTrustResponse::TokenType token_type
|
|
146
|
+
# The type of the token contained within the WS-Trust response.
|
|
147
|
+
def initialize(token, token_type)
|
|
148
|
+
unless TokenType::ALL_TYPES.include? token_type
|
|
149
|
+
fail UnrecognizedTokenTypeError, token_type
|
|
150
|
+
end
|
|
151
|
+
@token = token
|
|
152
|
+
@token_type = token_type
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
##
|
|
156
|
+
# Gets the OAuth grant type for the SAML token type of the response.
|
|
157
|
+
#
|
|
158
|
+
# @return TokenRequest::GrantType
|
|
159
|
+
def grant_type
|
|
160
|
+
case @token_type
|
|
161
|
+
when TokenType::V1
|
|
162
|
+
TokenRequest::GrantType::SAML1
|
|
163
|
+
when TokenType::V2
|
|
164
|
+
TokenRequest::GrantType::SAML2
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
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 'nokogiri'
|
|
24
|
+
require 'uri'
|
|
25
|
+
|
|
26
|
+
module ADAL
|
|
27
|
+
# WSTrust namespaces for the username/password OAuth flow as well as action
|
|
28
|
+
# constants and maps to go between bindings, actions and namespaces.
|
|
29
|
+
module XmlNamespaces
|
|
30
|
+
NAMESPACES = {
|
|
31
|
+
'a' => 'http://www.w3.org/2005/08/addressing',
|
|
32
|
+
'o' => 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd',
|
|
33
|
+
's' => 'http://www.w3.org/2003/05/soap-envelope',
|
|
34
|
+
'u' => 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd',
|
|
35
|
+
'wsa' => 'http://www.w3.org/2005/08/addressing',
|
|
36
|
+
'wsdl' => 'http://schemas.xmlsoap.org/wsdl/',
|
|
37
|
+
'wsp' => 'http://schemas.xmlsoap.org/ws/2004/09/policy',
|
|
38
|
+
'soap12' => 'http://schemas.xmlsoap.org/wsdl/soap12/',
|
|
39
|
+
'sp' => 'http://schemas.xmlsoap.org/ws/2005/07/securitypolicy',
|
|
40
|
+
'ssp' => 'http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
NAMESPACES_2005 =
|
|
44
|
+
NAMESPACES.merge(
|
|
45
|
+
'trust' => 'http://schemas.xmlsoap.org/ws/2005/02/trust')
|
|
46
|
+
|
|
47
|
+
NAMESPACES_13 =
|
|
48
|
+
NAMESPACES.merge(
|
|
49
|
+
'trust' => 'http://docs.oasis-open.org/ws-sx/ws-trust/200512')
|
|
50
|
+
|
|
51
|
+
WSTRUST_2005 = 'http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/Issue'
|
|
52
|
+
WSTRUST_13 = 'http://docs.oasis-open.org/ws-sx/ws-trust/200512/RSTRC/IssueFinal'
|
|
53
|
+
|
|
54
|
+
ACTION_TO_NAMESPACE = { WSTRUST_13 => NAMESPACES_13,
|
|
55
|
+
WSTRUST_2005 => NAMESPACES_2005 }
|
|
56
|
+
|
|
57
|
+
BINDING_TO_ACTION = {
|
|
58
|
+
'UserNameWSTrustBinding_IWSTrustFeb2005Async' => WSTRUST_2005,
|
|
59
|
+
'UserNameWSTrustBinding_IWSTrustFeb2005Async1' => WSTRUST_2005,
|
|
60
|
+
'UserNameWSTrustBinding_IWSTrust13Async' => WSTRUST_13,
|
|
61
|
+
'UserNameWSTrustBinding_IWSTrust13Async1' => WSTRUST_13
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
This web app demonstrates the authorization code flow of obtaining access tokens
|
|
2
|
+
with ADAL.
|
|
3
|
+
|
|
4
|
+
To run this web app, you need to:
|
|
5
|
+
|
|
6
|
+
1. Register a web application under your Azure Active Directory account.
|
|
7
|
+
2. Replace `CLIENT_ID`, `CLIENT_SECRET` and `TENANT` with your values.
|
|
8
|
+
3. Ensure that Ruby and Sinatra are installed on your system.
|
|
9
|
+
4. Install ADAL with `gem install adal`.
|
|
10
|
+
5. Start Sinatra with `ruby web_app.rb`.
|
|
@@ -0,0 +1,139 @@
|
|
|
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 'adal'
|
|
24
|
+
require 'json'
|
|
25
|
+
require 'net/http'
|
|
26
|
+
require 'securerandom'
|
|
27
|
+
require 'sinatra'
|
|
28
|
+
require 'uri'
|
|
29
|
+
|
|
30
|
+
AUTHORITY = 'login.windows.net'
|
|
31
|
+
CLIENT_ID = 'your_client_id_here'
|
|
32
|
+
CLIENT_SECRET = 'your_client_secret_here'
|
|
33
|
+
RESOURCE = 'https://graph.windows.net'
|
|
34
|
+
TENANT = 'your_tenant_here.onmicrosoft.com'
|
|
35
|
+
|
|
36
|
+
# AuthenticationContext is specific to a tenant. Our application only cares
|
|
37
|
+
# about one tenant, so we only need one AuthenticationContext.
|
|
38
|
+
auth_ctx = ADAL::AuthenticationContext.new(AUTHORITY, TENANT)
|
|
39
|
+
|
|
40
|
+
# ADAL::ClientCredential is one of several ways that you can identify your
|
|
41
|
+
# client application. It contains a client id and a client secret, which are
|
|
42
|
+
# assigned to you by Azure when you register your application.
|
|
43
|
+
client_cred = ADAL::ClientCredential.new(CLIENT_ID, CLIENT_SECRET)
|
|
44
|
+
|
|
45
|
+
# ADAL supports four logging options: VERBOSE, INFO, WARN and ERROR.
|
|
46
|
+
# They are defined as constants in ADAL::Logger and are used in ADAL::Logging,
|
|
47
|
+
# the mix-in factory module that provides Loggers to the various ADAL classes.
|
|
48
|
+
# By default, log_level = ADAL::Logger::ERROR so only error messages will be
|
|
49
|
+
# displayed.
|
|
50
|
+
ADAL::Logging.log_level = ADAL::Logger::VERBOSE
|
|
51
|
+
|
|
52
|
+
# ADAL allows you to redirect log outputs to a file or any Ruby object
|
|
53
|
+
# that implements IO. By default they are sent to STDOUT.
|
|
54
|
+
ADAL::Logging.log_output = 'my_adal_logs.log'
|
|
55
|
+
|
|
56
|
+
# This is Sinatra specific code that allows storing data as a browser cookie.
|
|
57
|
+
configure do
|
|
58
|
+
enable :sessions
|
|
59
|
+
set :session_secret, 'secret'
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Public landing page for the web app.
|
|
63
|
+
get '/' do
|
|
64
|
+
'This is a public page. Anyone can see it.<br/>' \
|
|
65
|
+
'If you have credentials, you can view the protected phone book ' \
|
|
66
|
+
"for your organization <a href='/secure'>here</a>."
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# In order to access /secure, the user needs an access token for the resource
|
|
70
|
+
# (https://graph.windows.net) that /secure will be displaying.
|
|
71
|
+
before '/secure' do
|
|
72
|
+
redirect to('/auth') unless session[:access_token]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Now that we have an access token, we use it to access the resource.
|
|
76
|
+
# The details here are specific to your resource.
|
|
77
|
+
get '/secure' do
|
|
78
|
+
resource_uri = URI(RESOURCE + '/' + TENANT + '/users?api-version=2013-04-05')
|
|
79
|
+
headers = { 'authorization' => session[:access_token] }
|
|
80
|
+
http = Net::HTTP.new(resource_uri.hostname, resource_uri.port)
|
|
81
|
+
http.use_ssl = true
|
|
82
|
+
graph = JSON.parse(http.get(resource_uri, headers).body)
|
|
83
|
+
|
|
84
|
+
# The resource is displayed to the user.
|
|
85
|
+
'Here is your phone book.<br/><br/>' +
|
|
86
|
+
graph['value'].map do |user|
|
|
87
|
+
%(Given Name: #{user['givenName']}<br/>
|
|
88
|
+
Surname: #{user['surname']}<br/>
|
|
89
|
+
Address: #{user['streetAddress']}, #{user['city']}, #{user['country']}
|
|
90
|
+
<br/><br/>)
|
|
91
|
+
end.join(' ') +
|
|
92
|
+
"You can log out <a href='/logout'>here</a>.<br/>" \
|
|
93
|
+
"Or refresh the token <a href='/refresh'>here.</a>"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Removes the access token from the session. The user will have to authenticate
|
|
97
|
+
# again if they wish to revisit their phone book.
|
|
98
|
+
get '/logout' do
|
|
99
|
+
session.clear
|
|
100
|
+
redirect to('/')
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
get '/auth' do
|
|
104
|
+
# Request authorization by redirecting the user. ADAL will help create the
|
|
105
|
+
# URL, but it will not make the actual request for you. In this case, the
|
|
106
|
+
# request is made by Sinatra's redirect function.
|
|
107
|
+
redirect auth_ctx.authorization_request_url(RESOURCE, CLIENT_ID, uri), 303
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The authorization code is returned from the authorization server as
|
|
111
|
+
# params[:code]. We pass this to ADAL to acquire an access_token.
|
|
112
|
+
post '/auth' do
|
|
113
|
+
token_response = auth_ctx.acquire_token_with_authorization_code(
|
|
114
|
+
params[:code], uri, client_cred, RESOURCE)
|
|
115
|
+
case token_response
|
|
116
|
+
when ADAL::SuccessResponse
|
|
117
|
+
# ADAL successfully exchanged the authorization code for an access_token.
|
|
118
|
+
# The token_response includes other information but we only care about the
|
|
119
|
+
# access token and the refresh token.
|
|
120
|
+
session[:access_token] = token_response.access_token
|
|
121
|
+
session[:refresh_token] = token_response.refresh_token
|
|
122
|
+
redirect to('/secure')
|
|
123
|
+
when ADAL::ErrorResponse
|
|
124
|
+
# ADAL failed to exchange the authorization code for an access_token.
|
|
125
|
+
redirect to('/')
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
before '/refresh' do
|
|
130
|
+
redirect to('/auth') unless session[:refresh_token]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
get '/refresh' do
|
|
134
|
+
token_response = auth_ctx.acquire_token_with_refresh_token(
|
|
135
|
+
session[:refresh_token], client_cred, RESOURCE)
|
|
136
|
+
session[:access_token] = token_response.access_token
|
|
137
|
+
session[:refresh_token] = token_response.refresh_token
|
|
138
|
+
redirect to('/secure')
|
|
139
|
+
end
|