ruby-saml 0.8.18 → 0.9
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 +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +1 -6
- data/Gemfile +2 -12
- data/README.md +363 -35
- data/Rakefile +14 -0
- data/changelog.md +22 -9
- data/lib/onelogin/ruby-saml/attribute_service.rb +34 -0
- data/lib/onelogin/ruby-saml/attributes.rb +26 -64
- data/lib/onelogin/ruby-saml/authrequest.rb +47 -93
- data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +87 -0
- data/lib/onelogin/ruby-saml/logoutrequest.rb +36 -100
- data/lib/onelogin/ruby-saml/logoutresponse.rb +25 -35
- data/lib/onelogin/ruby-saml/metadata.rb +46 -16
- data/lib/onelogin/ruby-saml/response.rb +63 -373
- data/lib/onelogin/ruby-saml/saml_message.rb +78 -0
- data/lib/onelogin/ruby-saml/settings.rb +54 -122
- data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +25 -71
- data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +37 -102
- data/lib/onelogin/ruby-saml/utils.rb +32 -199
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/ruby-saml.rb +5 -2
- data/lib/schemas/{saml20assertion_schema.xsd → saml-schema-assertion-2.0.xsd} +283 -283
- data/lib/schemas/saml-schema-authn-context-2.0.xsd +23 -0
- data/lib/schemas/saml-schema-authn-context-types-2.0.xsd +821 -0
- data/lib/schemas/saml-schema-metadata-2.0.xsd +339 -0
- data/lib/schemas/{saml20protocol_schema.xsd → saml-schema-protocol-2.0.xsd} +302 -302
- data/lib/schemas/sstc-metadata-attr.xsd +35 -0
- data/lib/schemas/sstc-saml-attribute-ext.xsd +25 -0
- data/lib/schemas/sstc-saml-metadata-algsupport-v1.0.xsd +41 -0
- data/lib/schemas/sstc-saml-metadata-ui-v1.0.xsd +89 -0
- data/lib/schemas/{xenc_schema.xsd → xenc-schema.xsd} +1 -11
- data/lib/schemas/xml.xsd +287 -0
- data/lib/schemas/{xmldsig_schema.xsd → xmldsig-core-schema.xsd} +0 -9
- data/lib/xml_security.rb +83 -235
- data/ruby-saml.gemspec +1 -0
- data/test/idp_metadata_parser_test.rb +54 -0
- data/test/logoutrequest_test.rb +68 -155
- data/test/logoutresponse_test.rb +43 -32
- data/test/metadata_test.rb +87 -0
- data/test/request_test.rb +102 -99
- data/test/response_test.rb +181 -495
- data/test/responses/idp_descriptor.xml +3 -0
- data/test/responses/logoutresponse_fixtures.rb +7 -8
- data/test/responses/response_no_cert_and_encrypted_attrs.xml +29 -0
- data/test/responses/response_with_multiple_attribute_values.xml +1 -1
- data/test/responses/slo_request.xml +4 -0
- data/test/settings_test.rb +25 -112
- data/test/slo_logoutrequest_test.rb +40 -50
- data/test/slo_logoutresponse_test.rb +86 -185
- data/test/test_helper.rb +27 -102
- data/test/xml_security_test.rb +114 -337
- metadata +30 -81
- data/lib/onelogin/ruby-saml/setting_error.rb +0 -6
- data/test/certificates/certificate.der +0 -0
- data/test/certificates/formatted_certificate +0 -14
- data/test/certificates/formatted_chained_certificate +0 -42
- data/test/certificates/formatted_private_key +0 -12
- data/test/certificates/formatted_rsa_private_key +0 -12
- data/test/certificates/invalid_certificate1 +0 -1
- data/test/certificates/invalid_certificate2 +0 -1
- data/test/certificates/invalid_certificate3 +0 -12
- data/test/certificates/invalid_chained_certificate1 +0 -1
- data/test/certificates/invalid_private_key1 +0 -1
- data/test/certificates/invalid_private_key2 +0 -1
- data/test/certificates/invalid_private_key3 +0 -10
- data/test/certificates/invalid_rsa_private_key1 +0 -1
- data/test/certificates/invalid_rsa_private_key2 +0 -1
- data/test/certificates/invalid_rsa_private_key3 +0 -10
- data/test/certificates/ruby-saml-2.crt +0 -15
- data/test/requests/logoutrequest_fixtures.rb +0 -47
- data/test/responses/encrypted_new_attack.xml.base64 +0 -1
- data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +0 -1
- data/test/responses/invalids/invalid_issuer_message.xml.base64 +0 -1
- data/test/responses/invalids/multiple_signed.xml.base64 +0 -1
- data/test/responses/invalids/no_signature.xml.base64 +0 -1
- data/test/responses/invalids/response_with_concealed_signed_assertion.xml +0 -51
- data/test/responses/invalids/response_with_doubled_signed_assertion.xml +0 -49
- data/test/responses/invalids/signature_wrapping_attack.xml.base64 +0 -1
- data/test/responses/response_node_text_attack.xml.base64 +0 -1
- data/test/responses/response_with_concealed_signed_assertion.xml +0 -51
- data/test/responses/response_with_doubled_signed_assertion.xml +0 -49
- data/test/responses/response_with_multiple_attribute_statements.xml +0 -72
- data/test/responses/response_with_signed_assertion_3.xml +0 -30
- data/test/responses/response_with_signed_message_and_assertion.xml +0 -34
- data/test/responses/response_with_undefined_recipient.xml.base64 +0 -1
- data/test/responses/response_wrapped.xml.base64 +0 -150
- data/test/responses/valid_response.xml.base64 +0 -1
- data/test/responses/valid_response_without_x509certificate.xml.base64 +0 -1
- data/test/utils_test.rb +0 -231
|
@@ -1,118 +1,91 @@
|
|
|
1
1
|
module OneLogin
|
|
2
2
|
module RubySaml
|
|
3
|
-
|
|
4
|
-
#
|
|
5
|
-
#
|
|
3
|
+
# Wraps all attributes and provides means to query them for single or multiple values.
|
|
4
|
+
#
|
|
5
|
+
# For backwards compatibility Attributes#[] returns *first* value for the attribute.
|
|
6
|
+
# Turn off compatibility to make it return all values as an array:
|
|
7
|
+
# Attributes.single_value_compatibility = false
|
|
6
8
|
class Attributes
|
|
7
9
|
include Enumerable
|
|
8
10
|
|
|
9
|
-
attr_reader :attributes
|
|
10
|
-
|
|
11
11
|
# By default Attributes#[] is backwards compatible and
|
|
12
12
|
# returns only the first value for the attribute
|
|
13
13
|
# Setting this to `false` returns all values for an attribute
|
|
14
14
|
@@single_value_compatibility = true
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
#
|
|
16
|
+
# Get current status of backwards compatibility mode.
|
|
18
17
|
def self.single_value_compatibility
|
|
19
18
|
@@single_value_compatibility
|
|
20
19
|
end
|
|
21
20
|
|
|
22
21
|
# Sets the backwards compatibility mode on/off.
|
|
23
|
-
# @param value [Boolean]
|
|
24
|
-
#
|
|
25
22
|
def self.single_value_compatibility=(value)
|
|
26
23
|
@@single_value_compatibility = value
|
|
27
24
|
end
|
|
28
25
|
|
|
29
|
-
#
|
|
26
|
+
# Initialize Attributes collection, optionally taking a Hash of attribute names and values.
|
|
27
|
+
#
|
|
28
|
+
# The +attrs+ must be a Hash with attribute names as keys and **arrays** as values:
|
|
30
29
|
# Attributes.new({
|
|
31
30
|
# 'name' => ['value1', 'value2'],
|
|
32
31
|
# 'mail' => ['value1'],
|
|
33
32
|
# })
|
|
34
|
-
#
|
|
35
33
|
def initialize(attrs = {})
|
|
36
34
|
@attributes = attrs
|
|
37
35
|
end
|
|
38
36
|
|
|
39
37
|
|
|
40
38
|
# Iterate over all attributes
|
|
41
|
-
#
|
|
42
39
|
def each
|
|
43
40
|
attributes.each{|name, values| yield name, values}
|
|
44
41
|
end
|
|
45
42
|
|
|
46
|
-
|
|
47
43
|
# Test attribute presence by name
|
|
48
|
-
# @param name [String] The attribute name to be checked
|
|
49
|
-
#
|
|
50
44
|
def include?(name)
|
|
51
|
-
attributes.has_key?(canonize_name(name))
|
|
45
|
+
attributes.has_key?(canonize_name(name))
|
|
52
46
|
end
|
|
53
47
|
|
|
54
48
|
# Return first value for an attribute
|
|
55
|
-
# @param name [String] The attribute name
|
|
56
|
-
# @return [String] The value (First occurrence)
|
|
57
|
-
#
|
|
58
49
|
def single(name)
|
|
59
|
-
|
|
50
|
+
attributes[canonize_name(name)].first if include?(name)
|
|
60
51
|
end
|
|
61
52
|
|
|
62
53
|
# Return all values for an attribute
|
|
63
|
-
# @param name [String] The attribute name
|
|
64
|
-
# @return [Array] Values of the attribute
|
|
65
|
-
#
|
|
66
54
|
def multi(name)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if values.is_a?(Array)
|
|
70
|
-
values
|
|
71
|
-
elsif !values.nil?
|
|
72
|
-
Array(values)
|
|
73
|
-
else
|
|
74
|
-
nil
|
|
75
|
-
end
|
|
55
|
+
attributes[canonize_name(name)]
|
|
76
56
|
end
|
|
77
57
|
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
# - All values if single_value_compatibility = false
|
|
84
|
-
# response.attributes['mail'] # => ['user@example.com','user@example.net']
|
|
58
|
+
# By default returns first value for an attribute.
|
|
59
|
+
#
|
|
60
|
+
# Depending on the single value compatibility status this returns first value
|
|
61
|
+
# Attributes.single_value_compatibility = true # Default
|
|
62
|
+
# response.attributes['mail'] # => 'user@example.com'
|
|
85
63
|
#
|
|
64
|
+
# Or all values:
|
|
65
|
+
# Attributes.single_value_compatibility = false
|
|
66
|
+
# response.attributes['mail'] # => ['user@example.com','user@example.net']
|
|
86
67
|
def [](name)
|
|
87
|
-
self.class.single_value_compatibility ? single(name) : multi(name)
|
|
68
|
+
self.class.single_value_compatibility ? single(canonize_name(name)) : multi(canonize_name(name))
|
|
88
69
|
end
|
|
89
70
|
|
|
90
|
-
#
|
|
91
|
-
#
|
|
71
|
+
# Return all attributes as an array
|
|
92
72
|
def all
|
|
93
73
|
attributes
|
|
94
74
|
end
|
|
95
75
|
|
|
96
|
-
#
|
|
97
|
-
# @param values [Array] The values
|
|
98
|
-
#
|
|
76
|
+
# Set values for an attribute, overwriting all existing values
|
|
99
77
|
def set(name, values)
|
|
100
78
|
attributes[canonize_name(name)] = values
|
|
101
79
|
end
|
|
102
80
|
alias_method :[]=, :set
|
|
103
81
|
|
|
104
|
-
#
|
|
105
|
-
# @param values [Array] The values
|
|
106
|
-
#
|
|
82
|
+
# Add new attribute or new value(s) to an existing attribute
|
|
107
83
|
def add(name, values = [])
|
|
108
84
|
attributes[canonize_name(name)] ||= []
|
|
109
85
|
attributes[canonize_name(name)] += Array(values)
|
|
110
86
|
end
|
|
111
87
|
|
|
112
88
|
# Make comparable to another Attributes collection based on attributes
|
|
113
|
-
# @param other [Attributes] An Attributes object to compare with
|
|
114
|
-
# @return [Boolean] True if are contains the same attributes and values
|
|
115
|
-
#
|
|
116
89
|
def ==(other)
|
|
117
90
|
if other.is_a?(Attributes)
|
|
118
91
|
all == other.all
|
|
@@ -121,26 +94,15 @@ module OneLogin
|
|
|
121
94
|
end
|
|
122
95
|
end
|
|
123
96
|
|
|
124
|
-
def respond_to?(name)
|
|
125
|
-
attributes.respond_to?(name) || super
|
|
126
|
-
end
|
|
127
|
-
|
|
128
97
|
protected
|
|
129
98
|
|
|
130
99
|
# stringifies all names so both 'email' and :email return the same result
|
|
131
|
-
# @param name [String] The attribute name
|
|
132
|
-
# @return [String] stringified name
|
|
133
|
-
#
|
|
134
100
|
def canonize_name(name)
|
|
135
101
|
name.to_s
|
|
136
102
|
end
|
|
137
103
|
|
|
138
|
-
def
|
|
139
|
-
|
|
140
|
-
attributes.send(name, *args, &block)
|
|
141
|
-
else
|
|
142
|
-
super
|
|
143
|
-
end
|
|
104
|
+
def attributes
|
|
105
|
+
@attributes
|
|
144
106
|
end
|
|
145
107
|
end
|
|
146
108
|
end
|
|
@@ -1,25 +1,16 @@
|
|
|
1
|
-
require "
|
|
2
|
-
|
|
3
|
-
require "
|
|
4
|
-
require "onelogin/ruby-saml/utils"
|
|
5
|
-
require "onelogin/ruby-saml/setting_error"
|
|
1
|
+
require "uuid"
|
|
2
|
+
|
|
3
|
+
require "onelogin/ruby-saml/logging"
|
|
6
4
|
|
|
7
5
|
module OneLogin
|
|
8
6
|
module RubySaml
|
|
7
|
+
include REXML
|
|
8
|
+
class Authrequest < SamlMessage
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
# AuthNRequest ID
|
|
12
|
-
attr_accessor :uuid
|
|
10
|
+
attr_reader :uuid # Can be obtained if neccessary
|
|
13
11
|
|
|
14
|
-
# Initializes the AuthNRequest. An Authrequest Object.
|
|
15
|
-
# Asigns an ID, a random uuid.
|
|
16
|
-
#
|
|
17
12
|
def initialize
|
|
18
|
-
@uuid =
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def request_id
|
|
22
|
-
@uuid
|
|
13
|
+
@uuid = "_" + UUID.new.generate
|
|
23
14
|
end
|
|
24
15
|
|
|
25
16
|
def create(settings, params = {})
|
|
@@ -30,25 +21,11 @@ module OneLogin
|
|
|
30
21
|
params.each_pair do |key, value|
|
|
31
22
|
request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
|
|
32
23
|
end
|
|
33
|
-
raise SettingError.new "Invalid settings, idp_sso_target_url is not set!" if settings.idp_sso_target_url.nil? or settings.idp_sso_target_url.empty?
|
|
34
24
|
@login_url = settings.idp_sso_target_url + request_params
|
|
35
25
|
end
|
|
36
26
|
|
|
37
|
-
# Creates the Get parameters for the request.
|
|
38
|
-
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
|
39
|
-
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
|
|
40
|
-
# @return [Hash] Parameters
|
|
41
|
-
#
|
|
42
27
|
def create_params(settings, params={})
|
|
43
|
-
|
|
44
|
-
# Based on the HashWithIndifferentAccess value in Rails we could experience
|
|
45
|
-
# conflicts so this line will solve them.
|
|
46
|
-
relay_state = params[:RelayState] || params['RelayState']
|
|
47
|
-
|
|
48
|
-
if relay_state.nil?
|
|
49
|
-
params.delete(:RelayState)
|
|
50
|
-
params.delete('RelayState')
|
|
51
|
-
end
|
|
28
|
+
params = {} if params.nil?
|
|
52
29
|
|
|
53
30
|
request_doc = create_authentication_xml_doc(settings)
|
|
54
31
|
request_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
|
|
@@ -58,30 +35,18 @@ module OneLogin
|
|
|
58
35
|
|
|
59
36
|
Logging.debug "Created AuthnRequest: #{request}"
|
|
60
37
|
|
|
61
|
-
request =
|
|
62
|
-
|
|
63
|
-
base64_request = Base64.strict_encode64(request)
|
|
64
|
-
else
|
|
65
|
-
base64_request = Base64.encode64(request).gsub(/\n/, "")
|
|
66
|
-
end
|
|
67
|
-
|
|
38
|
+
request = deflate(request) if settings.compress_request
|
|
39
|
+
base64_request = encode(request)
|
|
68
40
|
request_params = {"SAMLRequest" => base64_request}
|
|
69
41
|
|
|
70
42
|
if settings.security[:authn_requests_signed] && !settings.security[:embed_sign] && settings.private_key
|
|
71
|
-
params['SigAlg'] =
|
|
72
|
-
url_string
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
)
|
|
78
|
-
sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method])
|
|
79
|
-
signature = settings.get_sp_key.sign(sign_algorithm.new, url_string)
|
|
80
|
-
if Base64.respond_to?('strict_encode64')
|
|
81
|
-
params['Signature'] = Base64.strict_encode64(signature)
|
|
82
|
-
else
|
|
83
|
-
params['Signature'] = Base64.encode64(signature).gsub(/\n/, "")
|
|
84
|
-
end
|
|
43
|
+
params['SigAlg'] = XMLSecurity::Document::SHA1
|
|
44
|
+
url_string = "SAMLRequest=#{CGI.escape(base64_request)}"
|
|
45
|
+
url_string += "&RelayState=#{CGI.escape(params['RelayState'])}" if params['RelayState']
|
|
46
|
+
url_string += "&SigAlg=#{CGI.escape(params['SigAlg'])}"
|
|
47
|
+
private_key = settings.get_sp_key()
|
|
48
|
+
signature = private_key.sign(XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]).new, url_string)
|
|
49
|
+
params['Signature'] = encode(signature)
|
|
85
50
|
end
|
|
86
51
|
|
|
87
52
|
params.each_pair do |key, value|
|
|
@@ -92,12 +57,7 @@ module OneLogin
|
|
|
92
57
|
end
|
|
93
58
|
|
|
94
59
|
def create_authentication_xml_doc(settings)
|
|
95
|
-
|
|
96
|
-
sign_document(document, settings)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def create_xml_document(settings)
|
|
100
|
-
time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
60
|
+
time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
101
61
|
|
|
102
62
|
request_doc = XMLSecurity::Document.new
|
|
103
63
|
request_doc.uuid = uuid
|
|
@@ -106,65 +66,59 @@ module OneLogin
|
|
|
106
66
|
root.attributes['ID'] = uuid
|
|
107
67
|
root.attributes['IssueInstant'] = time
|
|
108
68
|
root.attributes['Version'] = "2.0"
|
|
109
|
-
root.attributes['Destination'] = settings.idp_sso_target_url unless settings.idp_sso_target_url.nil?
|
|
69
|
+
root.attributes['Destination'] = settings.idp_sso_target_url unless settings.idp_sso_target_url.nil?
|
|
110
70
|
root.attributes['IsPassive'] = settings.passive unless settings.passive.nil?
|
|
111
71
|
root.attributes['ProtocolBinding'] = settings.protocol_binding unless settings.protocol_binding.nil?
|
|
72
|
+
root.attributes["AttributeConsumingServiceIndex"] = settings.attributes_index unless settings.attributes_index.nil?
|
|
112
73
|
root.attributes['ForceAuthn'] = settings.force_authn unless settings.force_authn.nil?
|
|
113
74
|
|
|
114
75
|
# Conditionally defined elements based on settings
|
|
115
76
|
if settings.assertion_consumer_service_url != nil
|
|
116
77
|
root.attributes["AssertionConsumerServiceURL"] = settings.assertion_consumer_service_url
|
|
117
78
|
end
|
|
118
|
-
if settings.
|
|
79
|
+
if settings.issuer != nil
|
|
119
80
|
issuer = root.add_element "saml:Issuer"
|
|
120
|
-
issuer.text = settings.
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
if settings.name_identifier_value_requested != nil
|
|
124
|
-
subject = root.add_element "saml:Subject"
|
|
125
|
-
|
|
126
|
-
nameid = subject.add_element "saml:NameID"
|
|
127
|
-
nameid.attributes['Format'] = settings.name_identifier_format if settings.name_identifier_format
|
|
128
|
-
nameid.text = settings.name_identifier_value_requested
|
|
129
|
-
|
|
130
|
-
subject_confirmation = subject.add_element "saml:SubjectConfirmation"
|
|
131
|
-
subject_confirmation.attributes['Method'] = "urn:oasis:names:tc:SAML:2.0:cm:bearer"
|
|
81
|
+
issuer.text = settings.issuer
|
|
132
82
|
end
|
|
133
|
-
|
|
134
83
|
if settings.name_identifier_format != nil
|
|
135
84
|
root.add_element "samlp:NameIDPolicy", {
|
|
136
|
-
"xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
137
85
|
# Might want to make AllowCreate a setting?
|
|
138
86
|
"AllowCreate" => "true",
|
|
139
87
|
"Format" => settings.name_identifier_format
|
|
140
88
|
}
|
|
141
89
|
end
|
|
142
90
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
91
|
+
if settings.authn_context || settings.authn_context_decl_ref
|
|
92
|
+
|
|
93
|
+
if settings.authn_context_comparison != nil
|
|
94
|
+
comparison = settings.authn_context_comparison
|
|
95
|
+
else
|
|
96
|
+
comparison = 'exact'
|
|
97
|
+
end
|
|
98
|
+
|
|
147
99
|
requested_context = root.add_element "samlp:RequestedAuthnContext", {
|
|
148
|
-
"
|
|
149
|
-
"Comparison" => "exact",
|
|
150
|
-
}
|
|
151
|
-
class_ref = requested_context.add_element "saml:AuthnContextClassRef", {
|
|
152
|
-
"xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
|
|
100
|
+
"Comparison" => comparison,
|
|
153
101
|
}
|
|
154
|
-
|
|
102
|
+
|
|
103
|
+
if settings.authn_context != nil
|
|
104
|
+
class_ref = requested_context.add_element "saml:AuthnContextClassRef"
|
|
105
|
+
class_ref.text = settings.authn_context
|
|
106
|
+
end
|
|
107
|
+
# add saml:AuthnContextDeclRef element
|
|
108
|
+
if settings.authn_context_decl_ref != nil
|
|
109
|
+
class_ref = requested_context.add_element "saml:AuthnContextDeclRef"
|
|
110
|
+
class_ref.text = settings.authn_context_decl_ref
|
|
111
|
+
end
|
|
155
112
|
end
|
|
156
|
-
request_doc
|
|
157
|
-
end
|
|
158
113
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
cert
|
|
164
|
-
document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
|
|
114
|
+
# embebed sign
|
|
115
|
+
if settings.security[:authn_requests_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
|
|
116
|
+
private_key = settings.get_sp_key()
|
|
117
|
+
cert = settings.get_sp_cert()
|
|
118
|
+
request_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
|
|
165
119
|
end
|
|
166
120
|
|
|
167
|
-
|
|
121
|
+
request_doc
|
|
168
122
|
end
|
|
169
123
|
|
|
170
124
|
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require "base64"
|
|
2
|
+
require "uuid"
|
|
3
|
+
require "zlib"
|
|
4
|
+
require "cgi"
|
|
5
|
+
require "rexml/document"
|
|
6
|
+
require "rexml/xpath"
|
|
7
|
+
|
|
8
|
+
module OneLogin
|
|
9
|
+
module RubySaml
|
|
10
|
+
include REXML
|
|
11
|
+
|
|
12
|
+
class IdpMetadataParser
|
|
13
|
+
|
|
14
|
+
METADATA = "urn:oasis:names:tc:SAML:2.0:metadata"
|
|
15
|
+
DSIG = "http://www.w3.org/2000/09/xmldsig#"
|
|
16
|
+
|
|
17
|
+
attr_reader :document
|
|
18
|
+
|
|
19
|
+
def parse_remote(url, validate_cert = true)
|
|
20
|
+
idp_metadata = get_idp_metadata(url, validate_cert)
|
|
21
|
+
parse(idp_metadata)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def parse(idp_metadata)
|
|
25
|
+
@document = REXML::Document.new(idp_metadata)
|
|
26
|
+
|
|
27
|
+
OneLogin::RubySaml::Settings.new.tap do |settings|
|
|
28
|
+
|
|
29
|
+
settings.idp_sso_target_url = single_signon_service_url
|
|
30
|
+
settings.idp_slo_target_url = single_logout_service_url
|
|
31
|
+
settings.idp_cert_fingerprint = fingerprint
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Retrieve the remote IdP metadata from the URL or a cached copy
|
|
38
|
+
# # returns a REXML document of the metadata
|
|
39
|
+
def get_idp_metadata(url, validate_cert)
|
|
40
|
+
uri = URI.parse(url)
|
|
41
|
+
if uri.scheme == "http"
|
|
42
|
+
response = Net::HTTP.get_response(uri)
|
|
43
|
+
meta_text = response.body
|
|
44
|
+
elsif uri.scheme == "https"
|
|
45
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
46
|
+
http.use_ssl = true
|
|
47
|
+
# Most IdPs will probably use self signed certs
|
|
48
|
+
if validate_cert
|
|
49
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
50
|
+
else
|
|
51
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
52
|
+
end
|
|
53
|
+
get = Net::HTTP::Get.new(uri.request_uri)
|
|
54
|
+
response = http.request(get)
|
|
55
|
+
meta_text = response.body
|
|
56
|
+
end
|
|
57
|
+
meta_text
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def single_signon_service_url
|
|
61
|
+
node = REXML::XPath.first(document, "/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService/@Location", { "md" => METADATA })
|
|
62
|
+
node.value if node
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def single_logout_service_url
|
|
66
|
+
node = REXML::XPath.first(document, "/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService/@Location", { "md" => METADATA })
|
|
67
|
+
node.value if node
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def certificate
|
|
71
|
+
@certificate ||= begin
|
|
72
|
+
node = REXML::XPath.first(document, "/md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate", { "md" => METADATA, "ds" => DSIG })
|
|
73
|
+
Base64.decode64(node.text) if node
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def fingerprint
|
|
78
|
+
@fingerprint ||= begin
|
|
79
|
+
if certificate
|
|
80
|
+
cert = OpenSSL::X509::Certificate.new(certificate)
|
|
81
|
+
Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(":")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -1,23 +1,15 @@
|
|
|
1
|
-
require "
|
|
2
|
-
|
|
3
|
-
require "
|
|
4
|
-
require 'rexml/document'
|
|
5
|
-
require "onelogin/ruby-saml/utils"
|
|
6
|
-
require "onelogin/ruby-saml/setting_error"
|
|
1
|
+
require "uuid"
|
|
2
|
+
|
|
3
|
+
require "onelogin/ruby-saml/logging"
|
|
7
4
|
|
|
8
5
|
module OneLogin
|
|
9
6
|
module RubySaml
|
|
7
|
+
class Logoutrequest < SamlMessage
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
attr_accessor :uuid
|
|
9
|
+
attr_reader :uuid # Can be obtained if neccessary
|
|
14
10
|
|
|
15
11
|
def initialize
|
|
16
|
-
@uuid =
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def request_id
|
|
20
|
-
@uuid
|
|
12
|
+
@uuid = "_" + UUID.new.generate
|
|
21
13
|
end
|
|
22
14
|
|
|
23
15
|
def create(settings, params={})
|
|
@@ -28,25 +20,11 @@ module OneLogin
|
|
|
28
20
|
params.each_pair do |key, value|
|
|
29
21
|
request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
|
|
30
22
|
end
|
|
31
|
-
raise SettingError.new "Invalid settings, idp_slo_target_url is not set!" if settings.idp_slo_target_url.nil? or settings.idp_slo_target_url.empty?
|
|
32
23
|
@logout_url = settings.idp_slo_target_url + request_params
|
|
33
24
|
end
|
|
34
25
|
|
|
35
|
-
# Creates the Get parameters for the logout request.
|
|
36
|
-
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
|
37
|
-
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
|
|
38
|
-
# @return [Hash] Parameters
|
|
39
|
-
#
|
|
40
26
|
def create_params(settings, params={})
|
|
41
|
-
|
|
42
|
-
# Based on the HashWithIndifferentAccess value in Rails we could experience
|
|
43
|
-
# conflicts so this line will solve them.
|
|
44
|
-
relay_state = params[:RelayState] || params['RelayState']
|
|
45
|
-
|
|
46
|
-
if relay_state.nil?
|
|
47
|
-
params.delete(:RelayState)
|
|
48
|
-
params.delete('RelayState')
|
|
49
|
-
end
|
|
27
|
+
params = {} if params.nil?
|
|
50
28
|
|
|
51
29
|
request_doc = create_logout_request_xml_doc(settings)
|
|
52
30
|
request_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
|
|
@@ -56,29 +34,18 @@ module OneLogin
|
|
|
56
34
|
|
|
57
35
|
Logging.debug "Created SLO Logout Request: #{request}"
|
|
58
36
|
|
|
59
|
-
request =
|
|
60
|
-
|
|
61
|
-
base64_request = Base64.strict_encode64(request)
|
|
62
|
-
else
|
|
63
|
-
base64_request = Base64.encode64(request).gsub(/\n/, "")
|
|
64
|
-
end
|
|
37
|
+
request = deflate(request) if settings.compress_request
|
|
38
|
+
base64_request = encode(request)
|
|
65
39
|
request_params = {"SAMLRequest" => base64_request}
|
|
66
40
|
|
|
67
41
|
if settings.security[:logout_requests_signed] && !settings.security[:embed_sign] && settings.private_key
|
|
68
|
-
params['SigAlg'] =
|
|
69
|
-
url_string
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
75
|
-
sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method])
|
|
76
|
-
signature = settings.get_sp_key.sign(sign_algorithm.new, url_string)
|
|
77
|
-
if Base64.respond_to?('strict_encode64')
|
|
78
|
-
params['Signature'] = Base64.strict_encode64(signature)
|
|
79
|
-
else
|
|
80
|
-
params['Signature'] = Base64.encode64(signature).gsub(/\n/, "")
|
|
81
|
-
end
|
|
42
|
+
params['SigAlg'] = XMLSecurity::Document::SHA1
|
|
43
|
+
url_string = "SAMLRequest=#{CGI.escape(base64_request)}"
|
|
44
|
+
url_string += "&RelayState=#{CGI.escape(params['RelayState'])}" if params['RelayState']
|
|
45
|
+
url_string += "&SigAlg=#{CGI.escape(params['SigAlg'])}"
|
|
46
|
+
private_key = settings.get_sp_key()
|
|
47
|
+
signature = private_key.sign(XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]).new, url_string)
|
|
48
|
+
params['Signature'] = encode(signature)
|
|
82
49
|
end
|
|
83
50
|
|
|
84
51
|
params.each_pair do |key, value|
|
|
@@ -88,78 +55,47 @@ module OneLogin
|
|
|
88
55
|
request_params
|
|
89
56
|
end
|
|
90
57
|
|
|
91
|
-
# Creates the SAMLRequest String.
|
|
92
|
-
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
|
93
|
-
# @return [String] The SAMLRequest String.
|
|
94
|
-
#
|
|
95
58
|
def create_logout_request_xml_doc(settings)
|
|
96
|
-
|
|
97
|
-
sign_document(document, settings)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def create_xml_document(settings, request_doc=nil)
|
|
101
|
-
time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
59
|
+
time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
102
60
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
request_doc.uuid = uuid
|
|
106
|
-
end
|
|
61
|
+
request_doc = XMLSecurity::Document.new
|
|
62
|
+
request_doc.uuid = uuid
|
|
107
63
|
|
|
108
|
-
root = request_doc.add_element "samlp:LogoutRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol" }
|
|
64
|
+
root = request_doc.add_element "samlp:LogoutRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol", "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
|
|
109
65
|
root.attributes['ID'] = uuid
|
|
110
66
|
root.attributes['IssueInstant'] = time
|
|
111
67
|
root.attributes['Version'] = "2.0"
|
|
112
|
-
root.attributes['Destination'] = settings.idp_slo_target_url unless settings.idp_slo_target_url.nil?
|
|
68
|
+
root.attributes['Destination'] = settings.idp_slo_target_url unless settings.idp_slo_target_url.nil?
|
|
113
69
|
|
|
114
|
-
if settings.
|
|
115
|
-
issuer = root.add_element "saml:Issuer"
|
|
116
|
-
issuer.text = settings.
|
|
70
|
+
if settings.issuer
|
|
71
|
+
issuer = root.add_element "saml:Issuer"
|
|
72
|
+
issuer.text = settings.issuer
|
|
117
73
|
end
|
|
118
74
|
|
|
75
|
+
name_id = root.add_element "saml:NameID"
|
|
119
76
|
if settings.name_identifier_value
|
|
120
|
-
name_id =
|
|
121
|
-
nameid.attributes['NameQualifier'] = settings.idp_name_qualifier if settings.idp_name_qualifier
|
|
122
|
-
nameid.attributes['SPNameQualifier'] = settings.sp_name_qualifier if settings.sp_name_qualifier
|
|
77
|
+
name_id.attributes['NameQualifier'] = settings.sp_name_qualifier if settings.sp_name_qualifier
|
|
123
78
|
name_id.attributes['Format'] = settings.name_identifier_format if settings.name_identifier_format
|
|
124
79
|
name_id.text = settings.name_identifier_value
|
|
80
|
+
else
|
|
81
|
+
# If no NameID is present in the settings we generate one
|
|
82
|
+
name_id.text = "_" + UUID.new.generate
|
|
83
|
+
name_id.attributes['Format'] = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
|
|
125
84
|
end
|
|
126
85
|
|
|
127
86
|
if settings.sessionindex
|
|
128
|
-
sessionindex = root.add_element "samlp:SessionIndex"
|
|
87
|
+
sessionindex = root.add_element "samlp:SessionIndex"
|
|
129
88
|
sessionindex.text = settings.sessionindex
|
|
130
89
|
end
|
|
131
90
|
|
|
132
|
-
#
|
|
133
|
-
# match required for authentication to succeed. If this is not defined,
|
|
134
|
-
# the IdP will choose default rules for authentication. (Shibboleth IdP)
|
|
135
|
-
if settings.authn_context != nil
|
|
136
|
-
requested_context = root.add_element "samlp:RequestedAuthnContext", {
|
|
137
|
-
"xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
138
|
-
"Comparison" => "exact",
|
|
139
|
-
}
|
|
140
|
-
class_ref = requested_context.add_element "saml:AuthnContextClassRef", {
|
|
141
|
-
"xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
|
|
142
|
-
}
|
|
143
|
-
class_ref.text = settings.authn_context
|
|
144
|
-
end
|
|
145
|
-
request_doc
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def sign_document(document, settings)
|
|
149
|
-
# embed signature
|
|
91
|
+
# embebed sign
|
|
150
92
|
if settings.security[:logout_requests_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
|
|
151
|
-
private_key = settings.get_sp_key
|
|
152
|
-
cert = settings.get_sp_cert
|
|
153
|
-
|
|
93
|
+
private_key = settings.get_sp_key()
|
|
94
|
+
cert = settings.get_sp_cert()
|
|
95
|
+
request_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
|
|
154
96
|
end
|
|
155
97
|
|
|
156
|
-
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
# Leave due compatibility
|
|
160
|
-
def create_unauth_xml_doc(settings, params)
|
|
161
|
-
request_doc = ReXML::Document.new
|
|
162
|
-
create_xml_document(settings, request_doc)
|
|
98
|
+
request_doc
|
|
163
99
|
end
|
|
164
100
|
end
|
|
165
101
|
end
|