adal 1.0.0

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