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,39 @@
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
+ # Literals used in JWT header and payload.
25
+ module JwtParameters
26
+ ALGORITHM = 'alg'
27
+ AUDIENCE = 'aud'
28
+ EXPIRES_ON = 'exp'
29
+ ISSUER = 'iss'
30
+ JWT_ID = 'jti'
31
+ NOT_BEFORE = 'nbf'
32
+ RS256 = 'RS256'
33
+ SELF_SIGNED_JWT_LIFETIME = 10
34
+ SUBJECT = 'sub'
35
+ THUMBPRINT = 'x5t'
36
+ TYPE = 'typ'
37
+ TYPE_JWT = 'JWT'
38
+ end
39
+ end
@@ -0,0 +1,90 @@
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 'logger'
24
+
25
+ module ADAL
26
+ # Extended version of Ruby's base logger class to support VERBOSE logging.
27
+ # This is consistent with ADAL logging across platforms.
28
+ #
29
+ # The format of a log message is described in the Ruby docs at
30
+ # http://ruby-doc.org/stdlib-2.2.2/libdoc/logger/rdoc/Logger.html as
31
+ # SeverityID, [DateTime #pid] SeverityLabel -- ProgName: message.
32
+ # SeverityID is the first letter of the severity. In the case of ADAL::Logger
33
+ # that means one of {V, I, W, E, F}. The DateTime object uses the to_s method
34
+ # of DateTime from stdlib which is ISO-8601. The ProgName will be the
35
+ # correlation id if one is sent or absent otherwise.
36
+ class Logger < Logger
37
+ SEVS = %w(VERBOSE INFO WARN ERROR FATAL)
38
+ VERBOSE = SEVS.index('VERBOSE')
39
+
40
+ ##
41
+ # Constructs a new Logger.
42
+ #
43
+ # @param String|IO logdev
44
+ # A filename (String) or IO object (STDOUT, STDERR).
45
+ # @param String correlation_id
46
+ # The UUID of the request context.
47
+ def initialize(logdev, correlation_id)
48
+ super(logdev)
49
+ @correlation_id = correlation_id
50
+ end
51
+
52
+ def format_severity(severity)
53
+ SEVS[severity] || 'ANY'
54
+ end
55
+
56
+ ##
57
+ # For some reason, the default logger implementations of #error, #fatal,
58
+ # etc. pass message = nil and progname = <the message> to #add, which
59
+ # interprets that as using the progname as the message. Instead, we will
60
+ # use the message as the message and the progname as the correlation_id.
61
+ #
62
+ # This is purely an internal change, the calling mechanism is exactly the
63
+ # same and it only affects ADAL::Logger, not Logger.
64
+
65
+ # These methods are skipped by the SimpleCov, because it is not our
66
+ # responsibility to test the standard library's logging framework.
67
+
68
+ #:nocov:
69
+ def error(message = nil, &block)
70
+ add(ERROR, message, @correlation_id, &block)
71
+ end
72
+
73
+ def fatal(message = nil, &block)
74
+ add(FATAL, message, @correlation_id, &block)
75
+ end
76
+
77
+ def info(message = nil, &block)
78
+ add(INFO, message, @correlation_id, &block)
79
+ end
80
+
81
+ def verbose(message = nil, &block)
82
+ add(VERBOSE, message, @correlation_id, &block)
83
+ end
84
+
85
+ def warn(message = nil, &block)
86
+ add(WARN, message, @correlation_id, &block)
87
+ end
88
+ #:nocov:
89
+ end
90
+ end
@@ -0,0 +1,98 @@
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 './logger'
24
+
25
+ require 'securerandom'
26
+
27
+ module ADAL
28
+ # Mix-in module for the ADAL logger. To obtain a logger in class methods the
29
+ # calling class will need to extend this module. To obtain a logger in
30
+ # instance methods the calling will need to include this Module.
31
+ module Logging
32
+ DEFAULT_LOG_LEVEL = Logger::ERROR
33
+ DEFAULT_LOG_OUTPUT = STDOUT
34
+
35
+ @correlation_id = SecureRandom.uuid
36
+ @log_level = DEFAULT_LOG_LEVEL
37
+ @log_output = DEFAULT_LOG_OUTPUT
38
+
39
+ # According to the style guide, class instance variables are preferable to
40
+ # class variables.
41
+ class << self
42
+ attr_accessor :correlation_id
43
+ attr_accessor :log_level
44
+ attr_accessor :log_output
45
+ end
46
+
47
+ ##
48
+ # Sets the ADAL log level.
49
+ #
50
+ # Example usage:
51
+ #
52
+ # ADAL::Logging.log_level = ADAL::Logger::VERBOSE
53
+ #
54
+ def self.log_level=(level)
55
+ unless Logger::SEVS.map.with_index { |_, i| i }.include? level
56
+ fail ArgumentError, "Invalid log level: #{level}."
57
+ end
58
+ @log_level = level
59
+ end
60
+
61
+ ##
62
+ # Sets the ADAL log output. All future logs generated by ADAL will be sent
63
+ # to this location. It is not retroactive.
64
+ #
65
+ # @param IO|String output
66
+ # This can either be STDERR, STDOUT or a String containing a file path.
67
+ def self.log_output=(output)
68
+ output = output.to_s unless output.is_a? IO
69
+ @log_output = output
70
+ end
71
+
72
+ ##
73
+ # Creates one ADAL logger per calling class/module with a specified output.
74
+ # This is to be used within ADAL. Clients will have no use for it.
75
+ #
76
+ # Examples usage:
77
+ #
78
+ # require_relative './logging'
79
+ #
80
+ # module ADAL
81
+ # module SomeModule
82
+ # include Logging
83
+ #
84
+ # def something_bad
85
+ # logger.error('An error message')
86
+ # end
87
+ # end
88
+ # end
89
+ #
90
+ # @param output
91
+ # STDERR, STDOUT or the file name as a string.
92
+ def logger
93
+ @logger ||= ADAL::Logger.new(Logging.log_output, Logging.correlation_id)
94
+ @logger.level = Logging.log_level || DEFAULT_LOG_LEVEL
95
+ @logger
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,95 @@
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
+ module ADAL
26
+ # A simple cache implementation that is not persisted across application runs.
27
+ class MemoryCache
28
+ include Logging
29
+
30
+ def initialize
31
+ @entries = []
32
+ end
33
+
34
+ attr_accessor :entries
35
+
36
+ ##
37
+ # Adds an array of objects to the cache.
38
+ #
39
+ # @param Array
40
+ # The entries to add.
41
+ # @return Array
42
+ # The entries after the addition.
43
+ def add(entries)
44
+ entries = Array(entries) # If entries is an array, this is a no-op.
45
+ old_size = @entries.size
46
+ @entries |= entries
47
+ logger.verbose("Added #{entries.size - old_size} new entries to cache.")
48
+ end
49
+
50
+ ##
51
+ # By default, matches all entries.
52
+ #
53
+ # @param Block
54
+ # A matcher on the token list.
55
+ # @return Array
56
+ # The matching tokens.
57
+ def find(&query)
58
+ query ||= proc { true }
59
+ @entries.select(&query)
60
+ end
61
+
62
+ ##
63
+ # Removes an array of objects from the cache.
64
+ #
65
+ # @param Array
66
+ # The entries to remove.
67
+ # @return Array
68
+ # The remaining entries.
69
+ def remove(entries)
70
+ @entries -= Array(entries)
71
+ end
72
+
73
+ ##
74
+ # Converts the cache entries into one JSON string.
75
+ #
76
+ # @param JSON::Ext::Generator::State
77
+ # @return String
78
+ def to_json(_ = nil)
79
+ JSON.unparse(entries)
80
+ end
81
+
82
+ ##
83
+ # Reconstructs the cache from JSON that was previously serialized.
84
+ #
85
+ # @param JSON json
86
+ # @return MemoryCache
87
+ def self.from_json(json)
88
+ cache = MemoryCache.new
89
+ cache.entries = JSON.parse(json).map do |e|
90
+ CachedTokenResponse.from_json(e)
91
+ end
92
+ cache
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,52 @@
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 './mex_response'
24
+ require_relative './util'
25
+
26
+ require 'net/http'
27
+ require 'uri'
28
+
29
+ module ADAL
30
+ # A request to a Metadata Exchange endpoint of an ADFS server. Used to obtain
31
+ # the WSTrust endpoint for username and password authentication of federated
32
+ # users.
33
+ class MexRequest
34
+ include Util
35
+
36
+ ##
37
+ # Constructs a MexRequest object for a specific URL endpoint.
38
+ #
39
+ # @param String|URI endpoint
40
+ # The Metadata Exchange endpoint.
41
+ def initialize(endpoint)
42
+ @endpoint = URI.parse(endpoint.to_s)
43
+ end
44
+
45
+ # @return MexResponse
46
+ def execute
47
+ request = Net::HTTP::Get.new(@endpoint.path)
48
+ request.add_field('Content-Type', 'application/soap+xml')
49
+ MexResponse.parse(http(@endpoint).request(request).body)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,141 @@
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 './xml_namespaces'
25
+
26
+ require 'nokogiri'
27
+ require 'uri'
28
+
29
+ module ADAL
30
+ # Relevant fields from a Mex response.
31
+ class MexResponse
32
+ include XmlNamespaces
33
+
34
+ class << self
35
+ include Logging
36
+ end
37
+
38
+ class MexError < StandardError; end
39
+
40
+ POLICY_ID_XPATH =
41
+ '//wsdl:definitions/wsp:Policy[./wsp:ExactlyOne/wsp:All/sp:SignedSuppor' \
42
+ 'tingTokens/wsp:Policy/sp:UsernameToken/wsp:Policy/sp:WssUsernameToken1' \
43
+ '0]/@u:Id|//wsdl:definitions/wsp:Policy[./wsp:ExactlyOne/wsp:All/ssp:Si' \
44
+ 'gnedEncryptedSupportingTokens/wsp:Policy/ssp:UsernameToken/wsp:Policy/' \
45
+ 'ssp:WssUsernameToken10]/@u:Id'
46
+ BINDING_XPATH = '//wsdl:definitions/wsdl:binding[./wsp:PolicyReference]'
47
+ PORT_XPATH = '//wsdl:definitions/wsdl:service/wsdl:port'
48
+ ADDRESS_XPATH = './soap12:address/@location'
49
+
50
+ ##
51
+ # Parses the XML string response from the Metadata Exchange endpoint into
52
+ # a MexResponse object.
53
+ #
54
+ # @param String response
55
+ # @return MexResponse
56
+ def self.parse(response)
57
+ xml = Nokogiri::XML(response)
58
+ policy_ids = parse_policy_ids(xml)
59
+ bindings = parse_bindings(xml, policy_ids)
60
+ endpoint, binding = parse_endpoint_and_binding(xml, bindings)
61
+ MexResponse.new(endpoint, binding)
62
+ end
63
+
64
+ # @param Nokogiri::XML::Document xml
65
+ # @param Array[String] policy_ids
66
+ # @return Array[String]
67
+ def self.parse_bindings(xml, policy_ids)
68
+ matching_bindings = xml.xpath(BINDING_XPATH, NAMESPACES).map do |node|
69
+ reference_uri = node.xpath('./wsp:PolicyReference/@URI', NAMESPACES)
70
+ node.xpath('./@name').to_s if policy_ids.include? reference_uri.to_s
71
+ end.compact
72
+ fail MexError, 'No matching bindings found.' if matching_bindings.empty?
73
+ matching_bindings
74
+ end
75
+ private_class_method :parse_bindings
76
+
77
+ # @param Nokogiri::XML::Document xml
78
+ # @param Array[String] bindings
79
+ # @return Array[[String, String]]
80
+ def self.parse_all_endpoints(xml, bindings)
81
+ endpoints = xml.xpath(PORT_XPATH, NAMESPACES).map do |node|
82
+ binding = node.attr('binding').split(':').last
83
+ if bindings.include? binding
84
+ [node.xpath(ADDRESS_XPATH, NAMESPACES).to_s, binding]
85
+ end
86
+ end.compact
87
+ endpoints
88
+ end
89
+ private_class_method :parse_all_endpoints
90
+
91
+ # @param Nokogiri::XML::Document xml
92
+ # @param Array[String] bindings
93
+ # @return [String, String]
94
+ def self.parse_endpoint_and_binding(xml, bindings)
95
+ endpoints = parse_all_endpoints(xml, bindings)
96
+ case endpoints.size
97
+ when 0
98
+ fail MexError, 'No valid WS-Trust endpoints found.'
99
+ when 1
100
+ else
101
+ logger.info('Multiple WS-Trust endpoints were found in the mex ' \
102
+ 'response. Only one was used.')
103
+ end
104
+ prefer_13(endpoints).first
105
+ end
106
+ private_class_method :parse_endpoint_and_binding
107
+
108
+ # @param Nokogiri::XML::Document xml
109
+ # @return Array[String]
110
+ def self.parse_policy_ids(xml)
111
+ policy_ids = xml.xpath(POLICY_ID_XPATH, NAMESPACES)
112
+ .map { |attr| "\##{attr.value}" }
113
+ fail MexError, 'No username token policy nodes.' if policy_ids.empty?
114
+ policy_ids
115
+ end
116
+ private_class_method :parse_policy_ids
117
+
118
+ # @param Array[String, String] endpoints
119
+ # @return Array[String, String] endpoints
120
+ def self.prefer_13(endpoints)
121
+ only13 = endpoints.select { |_, b| BINDING_TO_ACTION[b] == WSTRUST_13 }
122
+ only13.empty? ? endpoints : only13
123
+ end
124
+ private_class_method :prefer_13
125
+
126
+ attr_reader :action
127
+ attr_reader :wstrust_url
128
+
129
+ ##
130
+ # Constructs a new MexResponse.
131
+ #
132
+ # @param String|URI wstrust_url
133
+ # @param String action
134
+ def initialize(wstrust_url, binding)
135
+ @action = BINDING_TO_ACTION[binding]
136
+ @wstrust_url = URI.parse(wstrust_url.to_s)
137
+ return if @wstrust_url.instance_of? URI::HTTPS
138
+ fail ArgumentError, 'Mex is only done over HTTPS.'
139
+ end
140
+ end
141
+ end