adal 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -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
|
data/lib/adal/logger.rb
ADDED
@@ -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
|
data/lib/adal/logging.rb
ADDED
@@ -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
|