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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +1 -6
  4. data/Gemfile +2 -12
  5. data/README.md +363 -35
  6. data/Rakefile +14 -0
  7. data/changelog.md +22 -9
  8. data/lib/onelogin/ruby-saml/attribute_service.rb +34 -0
  9. data/lib/onelogin/ruby-saml/attributes.rb +26 -64
  10. data/lib/onelogin/ruby-saml/authrequest.rb +47 -93
  11. data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +87 -0
  12. data/lib/onelogin/ruby-saml/logoutrequest.rb +36 -100
  13. data/lib/onelogin/ruby-saml/logoutresponse.rb +25 -35
  14. data/lib/onelogin/ruby-saml/metadata.rb +46 -16
  15. data/lib/onelogin/ruby-saml/response.rb +63 -373
  16. data/lib/onelogin/ruby-saml/saml_message.rb +78 -0
  17. data/lib/onelogin/ruby-saml/settings.rb +54 -122
  18. data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +25 -71
  19. data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +37 -102
  20. data/lib/onelogin/ruby-saml/utils.rb +32 -199
  21. data/lib/onelogin/ruby-saml/version.rb +1 -1
  22. data/lib/ruby-saml.rb +5 -2
  23. data/lib/schemas/{saml20assertion_schema.xsd → saml-schema-assertion-2.0.xsd} +283 -283
  24. data/lib/schemas/saml-schema-authn-context-2.0.xsd +23 -0
  25. data/lib/schemas/saml-schema-authn-context-types-2.0.xsd +821 -0
  26. data/lib/schemas/saml-schema-metadata-2.0.xsd +339 -0
  27. data/lib/schemas/{saml20protocol_schema.xsd → saml-schema-protocol-2.0.xsd} +302 -302
  28. data/lib/schemas/sstc-metadata-attr.xsd +35 -0
  29. data/lib/schemas/sstc-saml-attribute-ext.xsd +25 -0
  30. data/lib/schemas/sstc-saml-metadata-algsupport-v1.0.xsd +41 -0
  31. data/lib/schemas/sstc-saml-metadata-ui-v1.0.xsd +89 -0
  32. data/lib/schemas/{xenc_schema.xsd → xenc-schema.xsd} +1 -11
  33. data/lib/schemas/xml.xsd +287 -0
  34. data/lib/schemas/{xmldsig_schema.xsd → xmldsig-core-schema.xsd} +0 -9
  35. data/lib/xml_security.rb +83 -235
  36. data/ruby-saml.gemspec +1 -0
  37. data/test/idp_metadata_parser_test.rb +54 -0
  38. data/test/logoutrequest_test.rb +68 -155
  39. data/test/logoutresponse_test.rb +43 -32
  40. data/test/metadata_test.rb +87 -0
  41. data/test/request_test.rb +102 -99
  42. data/test/response_test.rb +181 -495
  43. data/test/responses/idp_descriptor.xml +3 -0
  44. data/test/responses/logoutresponse_fixtures.rb +7 -8
  45. data/test/responses/response_no_cert_and_encrypted_attrs.xml +29 -0
  46. data/test/responses/response_with_multiple_attribute_values.xml +1 -1
  47. data/test/responses/slo_request.xml +4 -0
  48. data/test/settings_test.rb +25 -112
  49. data/test/slo_logoutrequest_test.rb +40 -50
  50. data/test/slo_logoutresponse_test.rb +86 -185
  51. data/test/test_helper.rb +27 -102
  52. data/test/xml_security_test.rb +114 -337
  53. metadata +30 -81
  54. data/lib/onelogin/ruby-saml/setting_error.rb +0 -6
  55. data/test/certificates/certificate.der +0 -0
  56. data/test/certificates/formatted_certificate +0 -14
  57. data/test/certificates/formatted_chained_certificate +0 -42
  58. data/test/certificates/formatted_private_key +0 -12
  59. data/test/certificates/formatted_rsa_private_key +0 -12
  60. data/test/certificates/invalid_certificate1 +0 -1
  61. data/test/certificates/invalid_certificate2 +0 -1
  62. data/test/certificates/invalid_certificate3 +0 -12
  63. data/test/certificates/invalid_chained_certificate1 +0 -1
  64. data/test/certificates/invalid_private_key1 +0 -1
  65. data/test/certificates/invalid_private_key2 +0 -1
  66. data/test/certificates/invalid_private_key3 +0 -10
  67. data/test/certificates/invalid_rsa_private_key1 +0 -1
  68. data/test/certificates/invalid_rsa_private_key2 +0 -1
  69. data/test/certificates/invalid_rsa_private_key3 +0 -10
  70. data/test/certificates/ruby-saml-2.crt +0 -15
  71. data/test/requests/logoutrequest_fixtures.rb +0 -47
  72. data/test/responses/encrypted_new_attack.xml.base64 +0 -1
  73. data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +0 -1
  74. data/test/responses/invalids/invalid_issuer_message.xml.base64 +0 -1
  75. data/test/responses/invalids/multiple_signed.xml.base64 +0 -1
  76. data/test/responses/invalids/no_signature.xml.base64 +0 -1
  77. data/test/responses/invalids/response_with_concealed_signed_assertion.xml +0 -51
  78. data/test/responses/invalids/response_with_doubled_signed_assertion.xml +0 -49
  79. data/test/responses/invalids/signature_wrapping_attack.xml.base64 +0 -1
  80. data/test/responses/response_node_text_attack.xml.base64 +0 -1
  81. data/test/responses/response_with_concealed_signed_assertion.xml +0 -51
  82. data/test/responses/response_with_doubled_signed_assertion.xml +0 -49
  83. data/test/responses/response_with_multiple_attribute_statements.xml +0 -72
  84. data/test/responses/response_with_signed_assertion_3.xml +0 -30
  85. data/test/responses/response_with_signed_message_and_assertion.xml +0 -34
  86. data/test/responses/response_with_undefined_recipient.xml.base64 +0 -1
  87. data/test/responses/response_wrapped.xml.base64 +0 -150
  88. data/test/responses/valid_response.xml.base64 +0 -1
  89. data/test/responses/valid_response_without_x509certificate.xml.base64 +0 -1
  90. data/test/utils_test.rb +0 -231
@@ -1,118 +1,91 @@
1
1
  module OneLogin
2
2
  module RubySaml
3
-
4
- # SAML2 Attributes. Parse the Attributes from the AttributeStatement of the SAML Response.
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
- # @return [Boolean] Get current status of backwards compatibility mode.
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
- # @param attrs [Hash] The +attrs+ must be a Hash with attribute names as keys and **arrays** as values:
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)) || attributes.has_key?(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
- multi(name).first if include?(name)
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
- values = attributes[canonize_name(name)] || attributes[name]
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
- # Retrieve attribute value(s)
79
- # @param name [String] The attribute name
80
- # @return [String|Array] Depending on the single value compatibility status this returns:
81
- # - First value if single_value_compatibility = true
82
- # response.attributes['mail'] # => 'user@example.com'
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
- # @return [Array] Return all attributes as an array
91
- #
71
+ # Return all attributes as an array
92
72
  def all
93
73
  attributes
94
74
  end
95
75
 
96
- # @param name [String] The attribute name
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
- # @param name [String] The attribute name
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 method_missing(name, *args, &block)
139
- if attributes.respond_to?(name)
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 "base64"
2
- require "zlib"
3
- require "cgi"
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
- class Authrequest
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 = OneLogin::RubySaml::Utils.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
- # The method expects :RelayState but sometimes we get 'RelayState' instead.
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 = Zlib::Deflate.deflate(request, 9)[2..-5] if settings.compress_request
62
- if Base64.respond_to?('strict_encode64')
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'] = settings.security[:signature_method]
72
- url_string = OneLogin::RubySaml::Utils.build_query(
73
- :type => 'SAMLRequest',
74
- :data => base64_request,
75
- :relay_state => relay_state,
76
- :sig_alg => params['SigAlg']
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
- document = create_xml_document(settings)
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? or settings.idp_sso_target_url.empty?
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.sp_entity_id != nil
79
+ if settings.issuer != nil
119
80
  issuer = root.add_element "saml:Issuer"
120
- issuer.text = settings.sp_entity_id
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
- # BUG fix here -- if an authn_context is defined, add the tags with an "exact"
144
- # match required for authentication to succeed. If this is not defined,
145
- # the IdP will choose default rules for authentication. (Shibboleth IdP)
146
- if settings.authn_context != nil
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
- "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
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
- class_ref.text = settings.authn_context
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
- def sign_document(document, settings)
160
- # embed signature
161
- if settings.security[:authn_requests_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
162
- private_key = settings.get_sp_key
163
- cert = settings.get_sp_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
- document
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 "base64"
2
- require "zlib"
3
- require "cgi"
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
- class Logoutrequest
12
-
13
- attr_accessor :uuid
9
+ attr_reader :uuid # Can be obtained if neccessary
14
10
 
15
11
  def initialize
16
- @uuid = OneLogin::RubySaml::Utils.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
- # The method expects :RelayState but sometimes we get 'RelayState' instead.
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 = Zlib::Deflate.deflate(request, 9)[2..-5] if settings.compress_request
60
- if Base64.respond_to?('strict_encode64')
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'] = settings.security[:signature_method]
69
- url_string = OneLogin::RubySaml::Utils.build_query(
70
- :type => 'SAMLRequest',
71
- :data => base64_request,
72
- :relay_state => relay_state,
73
- :sig_alg => params['SigAlg']
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
- document = create_xml_document(settings)
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
- if request_doc.nil?
104
- request_doc = XMLSecurity::Document.new
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? or settings.idp_slo_target_url.empty?
68
+ root.attributes['Destination'] = settings.idp_slo_target_url unless settings.idp_slo_target_url.nil?
113
69
 
114
- if settings.sp_entity_id
115
- issuer = root.add_element "saml:Issuer", { "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
116
- issuer.text = settings.sp_entity_id
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 = root.add_element "saml:NameID", { "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
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", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol" }
87
+ sessionindex = root.add_element "samlp:SessionIndex"
129
88
  sessionindex.text = settings.sessionindex
130
89
  end
131
90
 
132
- # BUG fix here -- if an authn_context is defined, add the tags with an "exact"
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
- document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
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
- document
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