ruby-saml 0.6.0 → 0.7.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.

Potentially problematic release.


This version of ruby-saml might be problematic. Click here for more details.

data/.gitignore CHANGED
@@ -8,3 +8,4 @@ Gemfile.lock
8
8
  lib/Lib.iml
9
9
  test/Test.iml
10
10
  .rvmrc
11
+ *.gem
@@ -10,6 +10,8 @@ module Onelogin
10
10
  include REXML
11
11
  class Authrequest
12
12
  def create(settings, params = {})
13
+ params = {} if params.nil?
14
+
13
15
  request_doc = create_authentication_xml_doc(settings)
14
16
 
15
17
  request = ""
@@ -17,14 +19,14 @@ module Onelogin
17
19
 
18
20
  Logging.debug "Created AuthnRequest: #{request}"
19
21
 
20
- deflated_request = Zlib::Deflate.deflate(request, 9)[2..-5]
21
- base64_request = Base64.encode64(deflated_request)
22
+ request = Zlib::Deflate.deflate(request, 9)[2..-5] if settings.compress_request
23
+ base64_request = Base64.encode64(request)
22
24
  encoded_request = CGI.escape(base64_request)
23
25
  params_prefix = (settings.idp_sso_target_url =~ /\?/) ? '&' : '?'
24
26
  request_params = "#{params_prefix}SAMLRequest=#{encoded_request}"
25
27
 
26
28
  params.each_pair do |key, value|
27
- request_params << "&#{key}=#{CGI.escape(value.to_s)}"
29
+ request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
28
30
  end
29
31
 
30
32
  settings.idp_sso_target_url + request_params
@@ -35,7 +35,7 @@ module Onelogin
35
35
 
36
36
  def create_unauth_xml_doc(settings, params)
37
37
 
38
- time = Time.new().strftime("%Y-%m-%dT%H:%M:%SZ")
38
+ time = Time.new().strftime("%Y-%m-%dT%H:%M:%S")
39
39
 
40
40
  request_doc = REXML::Document.new
41
41
  root = request_doc.add_element "samlp:LogoutRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol" }
@@ -53,6 +53,8 @@ module Onelogin
53
53
  name_id.attributes['NameQualifier'] = settings.sp_name_qualifier if settings.sp_name_qualifier
54
54
  name_id.attributes['Format'] = settings.name_identifier_format if settings.name_identifier_format
55
55
  name_id.text = settings.name_identifier_value
56
+ else
57
+ raise ValidationError.new("Missing required name identifier")
56
58
  end
57
59
 
58
60
  if settings.sessionindex
@@ -0,0 +1,154 @@
1
+ require "xml_security"
2
+ require "time"
3
+ require "base64"
4
+ require "zlib"
5
+ require "open-uri"
6
+
7
+ module Onelogin
8
+ module Saml
9
+ class Logoutresponse
10
+
11
+ ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
12
+ PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
13
+
14
+ # For API compability, this is mutable.
15
+ attr_accessor :settings
16
+
17
+ attr_reader :document
18
+ attr_reader :response
19
+ attr_reader :options
20
+
21
+ #
22
+ # In order to validate that the response matches a given request, append
23
+ # the option:
24
+ # :matches_request_id => REQUEST_ID
25
+ #
26
+ # It will validate that the logout response matches the ID of the request.
27
+ # You can also do this yourself through the in_response_to accessor.
28
+ #
29
+ def initialize(response, settings = nil, options = {})
30
+ raise ArgumentError.new("Logoutresponse cannot be nil") if response.nil?
31
+ self.settings = settings
32
+
33
+ @options = options
34
+ @response = decode_raw_response(response)
35
+ @document = XMLSecurity::SignedDocument.new(response)
36
+ end
37
+
38
+ def validate!
39
+ validate(false)
40
+ end
41
+
42
+ def validate(soft = true)
43
+ return false unless valid_saml?(soft) && valid_state?(soft)
44
+
45
+ valid_in_response_to?(soft) && valid_issuer?(soft) && success?(soft)
46
+ end
47
+
48
+ def success?(soft = true)
49
+ unless status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
50
+ return soft ? false : validation_error("Bad status code. Expected <urn:oasis:names:tc:SAML:2.0:status:Success>, but was: <#@status_code> ")
51
+ end
52
+ true
53
+ end
54
+
55
+ def in_response_to
56
+ @in_response_to ||= begin
57
+ node = REXML::XPath.first(document, "/p:LogoutResponse", { "p" => PROTOCOL, "a" => ASSERTION })
58
+ node.nil? ? nil : node.attributes['InResponseTo']
59
+ end
60
+ end
61
+
62
+ def issuer
63
+ @issuer ||= begin
64
+ node = REXML::XPath.first(document, "/p:LogoutResponse/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
65
+ node ||= REXML::XPath.first(document, "/p:LogoutResponse/a:Assertion/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
66
+ node.nil? ? nil : node.text
67
+ end
68
+ end
69
+
70
+ def status_code
71
+ @status_code ||= begin
72
+ node = REXML::XPath.first(document, "/p:LogoutResponse/p:Status/p:StatusCode", { "p" => PROTOCOL, "a" => ASSERTION })
73
+ node.nil? ? nil : node.attributes["Value"]
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def decode(encoded)
80
+ Base64.decode64(encoded)
81
+ end
82
+
83
+ def inflate(deflated)
84
+ zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS)
85
+ zlib.inflate(deflated)
86
+ end
87
+
88
+ def decode_raw_response(response)
89
+ if response =~ /^</
90
+ return response
91
+ elsif (decoded = decode(response)) =~ /^</
92
+ return decoded
93
+ elsif (inflated = inflate(decoded)) =~ /^</
94
+ return inflated
95
+ end
96
+
97
+ raise "Couldn't decode SAMLResponse"
98
+ end
99
+
100
+ def valid_saml?(soft = true)
101
+ Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
102
+ @schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
103
+ @xml = Nokogiri::XML(self.document.to_s)
104
+ end
105
+ if soft
106
+ @schema.validate(@xml).map{ return false }
107
+ else
108
+ @schema.validate(@xml).map{ |error| raise(Exception.new("#{error.message}\n\n#{@xml.to_s}")) }
109
+ end
110
+ end
111
+
112
+ def valid_state?(soft = true)
113
+ if response.empty?
114
+ return soft ? false : validation_error("Blank response")
115
+ end
116
+
117
+ if settings.nil?
118
+ return soft ? false : validation_error("No settings on response")
119
+ end
120
+
121
+ if settings.issuer.nil?
122
+ return soft ? false : validation_error("No issuer in settings")
123
+ end
124
+
125
+ if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
126
+ return soft ? false : validation_error("No fingerprint or certificate on settings")
127
+ end
128
+
129
+ true
130
+ end
131
+
132
+ def valid_in_response_to?(soft = true)
133
+ return true unless self.options.has_key? :matches_request_id
134
+
135
+ unless self.options[:matches_request_id] == in_response_to
136
+ return soft ? false : validation_error("Response does not match the request ID, expected: <#{self.options[:matches_request_id]}>, but was: <#{in_response_to}>")
137
+ end
138
+
139
+ true
140
+ end
141
+
142
+ def valid_issuer?(soft = true)
143
+ unless URI.parse(issuer) == URI.parse(self.settings.issuer)
144
+ return soft ? false : validation_error("Doesn't match the issuer, expected: <#{self.settings.issuer}>, but was: <#{issuer}>")
145
+ end
146
+ true
147
+ end
148
+
149
+ def validation_error(message)
150
+ raise ValidationError.new(message)
151
+ end
152
+ end
153
+ end
154
+ end
@@ -12,15 +12,29 @@ module Onelogin
12
12
  class Metadata
13
13
  def generate(settings)
14
14
  meta_doc = REXML::Document.new
15
- root = meta_doc.add_element "md:EntityDescriptor", {
16
- "xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata"
15
+ root = meta_doc.add_element "md:EntityDescriptor", {
16
+ "xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata"
17
17
  }
18
- sp_sso = root.add_element "md:SPSSODescriptor", {
19
- "protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol"
18
+ sp_sso = root.add_element "md:SPSSODescriptor", {
19
+ "protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol",
20
+ # Metadata request need not be signed (as we don't publish our cert)
21
+ "AuthnRequestsSigned" => false,
22
+ # However we would like assertions signed if idp_cert_fingerprint or idp_cert is set
23
+ "WantAssertionsSigned" => (!settings.idp_cert_fingerprint.nil? || !settings.idp_cert.nil?)
20
24
  }
21
25
  if settings.issuer != nil
22
26
  root.attributes["entityID"] = settings.issuer
23
27
  end
28
+ if settings.assertion_consumer_logout_service_url != nil
29
+ sp_sso.add_element "md:SingleLogoutService", {
30
+ # Add this as a setting to create different bindings?
31
+ "Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
32
+ "Location" => settings.assertion_consumer_logout_service_url,
33
+ "ResponseLocation" => settings.assertion_consumer_logout_service_url,
34
+ "isDefault" => true,
35
+ "index" => 0
36
+ }
37
+ end
24
38
  if settings.name_identifier_format != nil
25
39
  name_id = sp_sso.add_element "md:NameIDFormat"
26
40
  name_id.text = settings.name_identifier_format
@@ -29,9 +43,15 @@ module Onelogin
29
43
  sp_sso.add_element "md:AssertionConsumerService", {
30
44
  # Add this as a setting to create different bindings?
31
45
  "Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
32
- "Location" => settings.assertion_consumer_service_url
46
+ "Location" => settings.assertion_consumer_service_url,
47
+ "isDefault" => true,
48
+ "index" => 0
33
49
  }
34
50
  end
51
+ # With OpenSSO, it might be required to also include
52
+ # <md:RoleDescriptor xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:query="urn:oasis:names:tc:SAML:metadata:ext:query" xsi:type="query:AttributeQueryDescriptorType" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"/>
53
+ # <md:XACMLAuthzDecisionQueryDescriptor WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"/>
54
+
35
55
  meta_doc << REXML::XMLDecl.new
36
56
  ret = ""
37
57
  # pretty print the XML so IdP administrators can easily see what the SP supports
@@ -39,8 +59,7 @@ module Onelogin
39
59
 
40
60
  Logging.debug "Generated metadata:\n#{ret}"
41
61
 
42
- return ret
43
-
62
+ ret
44
63
  end
45
64
  end
46
65
  end
@@ -11,22 +11,18 @@ module Onelogin
11
11
  PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
12
12
  DSIG = "http://www.w3.org/2000/09/xmldsig#"
13
13
 
14
- attr_accessor :options, :response, :document, :settings
14
+ # TODO: This should probably be ctor initialized too... WDYT?
15
+ attr_accessor :settings
16
+
17
+ attr_reader :options
18
+ attr_reader :response
19
+ attr_reader :document
15
20
 
16
21
  def initialize(response, options = {})
17
22
  raise ArgumentError.new("Response cannot be nil") if response.nil?
18
- self.options = options
19
- self.response = response
20
-
21
- begin
22
- self.document = XMLSecurity::SignedDocument.new(Base64.decode64(response))
23
- rescue REXML::ParseException => e
24
- if response =~ /</
25
- self.document = XMLSecurity::SignedDocument.new(response)
26
- else
27
- raise e
28
- end
29
- end
23
+ @options = options
24
+ @response = (response =~ /^</) ? response : Base64.decode64(response)
25
+ @document = XMLSecurity::SignedDocument.new(@response)
30
26
  end
31
27
 
32
28
  def is_valid?
@@ -46,6 +42,14 @@ module Onelogin
46
42
  end
47
43
  end
48
44
 
45
+ def sessionindex
46
+ @sessionindex ||= begin
47
+ node = REXML::XPath.first(document, "/p:Response/a:Assertion[@ID='#{document.signed_element_id}']/a:AuthnStatement", { "p" => PROTOCOL, "a" => ASSERTION })
48
+ node ||= REXML::XPath.first(document, "/p:Response[@ID='#{document.signed_element_id}']/a:Assertion/a:AuthnStatement", { "p" => PROTOCOL, "a" => ASSERTION })
49
+ node.nil? ? nil : node.attributes['SessionIndex']
50
+ end
51
+ end
52
+
49
53
  # A hash of alle the attributes with the response. Assuming there is only one value for each key
50
54
  def attributes
51
55
  @attr_statements ||= begin
@@ -155,13 +159,13 @@ module Onelogin
155
159
  return true if conditions.nil?
156
160
  return true if options[:skip_conditions]
157
161
 
158
- if not_before = parse_time(conditions, "NotBefore")
162
+ if (not_before = parse_time(conditions, "NotBefore"))
159
163
  if Time.now.utc < not_before
160
164
  return soft ? false : validation_error("Current time is earlier than NotBefore condition")
161
165
  end
162
166
  end
163
167
 
164
- if not_on_or_after = parse_time(conditions, "NotOnOrAfter")
168
+ if (not_on_or_after = parse_time(conditions, "NotOnOrAfter"))
165
169
  if Time.now.utc >= not_on_or_after
166
170
  return soft ? false : validation_error("Current time is on or after NotOnOrAfter condition")
167
171
  end
@@ -1,7 +1,8 @@
1
1
  module Onelogin
2
2
  module Saml
3
3
  class Settings
4
- def initialize(config = {})
4
+ def initialize(overrides = {})
5
+ config = DEFAULTS.merge(overrides)
5
6
  config.each do |k,v|
6
7
  acc = "#{k.to_s}=".to_sym
7
8
  self.send(acc, v) if self.respond_to? acc
@@ -13,6 +14,12 @@ module Onelogin
13
14
  attr_accessor :idp_slo_target_url
14
15
  attr_accessor :name_identifier_value
15
16
  attr_accessor :sessionindex
17
+ attr_accessor :assertion_consumer_logout_service_url
18
+ attr_accessor :compress_request
19
+
20
+ private
21
+
22
+ DEFAULTS = {:compress_request => true}
16
23
  end
17
24
  end
18
25
  end
@@ -1,5 +1,5 @@
1
1
  module Onelogin
2
2
  module Saml
3
- VERSION = '0.6.0'
3
+ VERSION = '0.7.0'
4
4
  end
5
5
  end
@@ -1,6 +1,7 @@
1
1
  require 'onelogin/ruby-saml/logging'
2
2
  require 'onelogin/ruby-saml/authrequest'
3
3
  require 'onelogin/ruby-saml/logoutrequest'
4
+ require 'onelogin/ruby-saml/logoutresponse'
4
5
  require 'onelogin/ruby-saml/response'
5
6
  require 'onelogin/ruby-saml/settings'
6
7
  require 'onelogin/ruby-saml/validation_error'
@@ -80,7 +80,7 @@ module XMLSecurity
80
80
  signed_info_element = REXML::XPath.first(sig_element, "//ds:SignedInfo", {"ds"=>DSIG})
81
81
  self.noko_sig_element ||= document.at_xpath('//ds:Signature', 'ds' => DSIG)
82
82
  noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG)
83
- canon_algorithm = canon_algorithm REXML::XPath.first(sig_element, '//ds:CanonicalizationMethod')
83
+ canon_algorithm = canon_algorithm REXML::XPath.first(sig_element, '//ds:CanonicalizationMethod', 'ds' => DSIG)
84
84
  canon_string = noko_signed_info_element.canonicalize(canon_algorithm)
85
85
  noko_sig_element.remove
86
86
 
@@ -89,7 +89,7 @@ module XMLSecurity
89
89
  uri = ref.attributes.get_attribute("URI").value
90
90
 
91
91
  hashed_element = document.at_xpath("//*[@ID='#{uri[1..-1]}']")
92
- canon_algorithm = canon_algorithm REXML::XPath.first(ref, '//ds:CanonicalizationMethod')
92
+ canon_algorithm = canon_algorithm REXML::XPath.first(ref, '//ds:CanonicalizationMethod', 'ds' => DSIG)
93
93
  canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces).gsub('&','&amp;')
94
94
 
95
95
  digest_algorithm = algorithm(REXML::XPath.first(ref, "//ds:DigestMethod"))
@@ -7,6 +7,8 @@ class RequestTest < Test::Unit::TestCase
7
7
 
8
8
  should "create the deflated SAMLRequest URL parameter" do
9
9
  settings.idp_slo_target_url = "http://unauth.com/logout"
10
+ settings.name_identifier_value = "f00f00"
11
+
10
12
  unauth_url = Onelogin::Saml::Logoutrequest.new.create(settings)
11
13
  assert unauth_url =~ /^http:\/\/unauth\.com\/logout\?SAMLRequest=/
12
14
 
@@ -50,10 +52,19 @@ class RequestTest < Test::Unit::TestCase
50
52
  assert_match %r(#{name_identifier_value}</saml:NameID>), inflated
51
53
  end
52
54
 
55
+ should "require name_identifier_value" do
56
+ settings = Onelogin::Saml::Settings.new
57
+ settings.idp_slo_target_url = "http://example.com"
58
+ settings.name_identifier_format = nil
59
+
60
+ assert_raises(Onelogin::Saml::ValidationError) { Onelogin::Saml::Logoutrequest.new.create(settings) }
61
+ end
62
+
53
63
  context "when the target url doesn't contain a query string" do
54
64
  should "create the SAMLRequest parameter correctly" do
55
65
  settings = Onelogin::Saml::Settings.new
56
66
  settings.idp_slo_target_url = "http://example.com"
67
+ settings.name_identifier_value = "f00f00"
57
68
 
58
69
  unauth_url = Onelogin::Saml::Logoutrequest.new.create(settings)
59
70
  assert unauth_url =~ /^http:\/\/example.com\?SAMLRequest/
@@ -64,6 +75,7 @@ class RequestTest < Test::Unit::TestCase
64
75
  should "create the SAMLRequest parameter correctly" do
65
76
  settings = Onelogin::Saml::Settings.new
66
77
  settings.idp_slo_target_url = "http://example.com?field=value"
78
+ settings.name_identifier_value = "f00f00"
67
79
 
68
80
  unauth_url = Onelogin::Saml::Logoutrequest.new.create(settings)
69
81
  assert unauth_url =~ /^http:\/\/example.com\?field=value&SAMLRequest/
@@ -74,6 +86,7 @@ class RequestTest < Test::Unit::TestCase
74
86
  should "have access to the request uuid" do
75
87
  settings = Onelogin::Saml::Settings.new
76
88
  settings.idp_slo_target_url = "http://example.com?field=value"
89
+ settings.name_identifier_value = "f00f00"
77
90
 
78
91
  unauth_req = Onelogin::Saml::Logoutrequest.new
79
92
  unauth_url = unauth_req.create(settings)
@@ -0,0 +1,116 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "test_helper"))
2
+ require 'rexml/document'
3
+ require 'responses/logoutresponse_fixtures'
4
+ class RubySamlTest < Test::Unit::TestCase
5
+
6
+ context "Logoutresponse" do
7
+ context "#new" do
8
+ should "raise an exception when response is initialized with nil" do
9
+ assert_raises(ArgumentError) { Onelogin::Saml::Logoutresponse.new(nil) }
10
+ end
11
+ should "default to empty settings" do
12
+ logoutresponse = Onelogin::Saml::Logoutresponse.new( valid_response)
13
+ assert logoutresponse.settings.nil?
14
+ end
15
+ should "accept constructor-injected settings" do
16
+ logoutresponse = Onelogin::Saml::Logoutresponse.new(valid_response, settings)
17
+ assert !logoutresponse.settings.nil?
18
+ end
19
+ should "accept constructor-injected options" do
20
+ logoutresponse = Onelogin::Saml::Logoutresponse.new(valid_response, nil, { :foo => :bar} )
21
+ assert !logoutresponse.options.empty?
22
+ end
23
+ should "support base64 encoded responses" do
24
+ expected_response = valid_response
25
+ logoutresponse = Onelogin::Saml::Logoutresponse.new(Base64.encode64(expected_response), settings)
26
+
27
+ assert_equal expected_response, logoutresponse.response
28
+ end
29
+ end
30
+
31
+ context "#validate" do
32
+ should "validate the response" do
33
+ in_relation_to_request_id = random_id
34
+
35
+ logoutresponse = Onelogin::Saml::Logoutresponse.new(valid_response({:uuid => in_relation_to_request_id}), settings)
36
+
37
+ assert logoutresponse.validate
38
+
39
+ assert_equal settings.issuer, logoutresponse.issuer
40
+ assert_equal in_relation_to_request_id, logoutresponse.in_response_to
41
+
42
+ assert logoutresponse.success?
43
+ end
44
+
45
+ should "invalidate responses with wrong id when given option :matches_uuid" do
46
+
47
+ expected_request_id = "_some_other_expected_uuid"
48
+ opts = { :matches_request_id => expected_request_id}
49
+
50
+ logoutresponse = Onelogin::Saml::Logoutresponse.new(valid_response, settings, opts)
51
+
52
+ assert !logoutresponse.validate
53
+ assert_not_equal expected_request_id, logoutresponse.in_response_to
54
+ end
55
+
56
+ should "invalidate responses with wrong request status" do
57
+ logoutresponse = Onelogin::Saml::Logoutresponse.new(unsuccessful_response, settings)
58
+
59
+ assert !logoutresponse.validate
60
+ assert !logoutresponse.success?
61
+ end
62
+ end
63
+
64
+ context "#validate!" do
65
+ should "validates good responses" do
66
+ in_relation_to_request_id = random_id
67
+
68
+ logoutresponse = Onelogin::Saml::Logoutresponse.new(valid_response({:uuid => in_relation_to_request_id}), settings)
69
+
70
+ logoutresponse.validate!
71
+ end
72
+
73
+ should "raises validation error when matching for wrong request id" do
74
+
75
+ expected_request_id = "_some_other_expected_id"
76
+ opts = { :matches_request_id => expected_request_id}
77
+
78
+ logoutresponse = Onelogin::Saml::Logoutresponse.new(valid_response, settings, opts)
79
+
80
+ assert_raises(Onelogin::Saml::ValidationError) { logoutresponse.validate! }
81
+ end
82
+
83
+ should "raise validation error for wrong request status" do
84
+ logoutresponse = Onelogin::Saml::Logoutresponse.new(unsuccessful_response, settings)
85
+
86
+ assert_raises(Onelogin::Saml::ValidationError) { logoutresponse.validate! }
87
+ end
88
+
89
+ should "raise validation error when in bad state" do
90
+ # no settings
91
+ logoutresponse = Onelogin::Saml::Logoutresponse.new(unsuccessful_response)
92
+ assert_raises(Onelogin::Saml::ValidationError) { logoutresponse.validate! }
93
+ end
94
+
95
+ should "raise validation error when in lack of issuer setting" do
96
+ bad_settings = settings
97
+ bad_settings.issuer = nil
98
+ logoutresponse = Onelogin::Saml::Logoutresponse.new(unsuccessful_response, bad_settings)
99
+ assert_raises(Onelogin::Saml::ValidationError) { logoutresponse.validate! }
100
+ end
101
+
102
+ should "raise error for invalid xml" do
103
+ logoutresponse = Onelogin::Saml::Logoutresponse.new(invalid_xml_response, settings)
104
+
105
+ assert_raises(Exception) { logoutresponse.validate! }
106
+ end
107
+ end
108
+
109
+ end
110
+
111
+ # logoutresponse fixtures
112
+ def random_id
113
+ "_#{UUID.new.generate}"
114
+ end
115
+
116
+ end
@@ -19,6 +19,18 @@ class RequestTest < Test::Unit::TestCase
19
19
  assert_match /^<samlp:AuthnRequest/, inflated
20
20
  end
21
21
 
22
+ should "create the SAMLRequest URL parameter without deflating" do
23
+ settings = Onelogin::Saml::Settings.new
24
+ settings.compress_request = false
25
+ settings.idp_sso_target_url = "http://example.com"
26
+ auth_url = Onelogin::Saml::Authrequest.new.create(settings)
27
+ assert auth_url =~ /^http:\/\/example\.com\?SAMLRequest=/
28
+ payload = CGI.unescape(auth_url.split("=").last)
29
+ decoded = Base64.decode64(payload)
30
+
31
+ assert_match /^<samlp:AuthnRequest/, decoded
32
+ end
33
+
22
34
  should "accept extra parameters" do
23
35
  settings = Onelogin::Saml::Settings.new
24
36
  settings.idp_sso_target_url = "http://example.com"
@@ -0,0 +1,67 @@
1
+ #encoding: utf-8
2
+
3
+ def default_response_opts
4
+ {
5
+ :uuid => "_28024690-000e-0130-b6d2-38f6b112be8b",
6
+ :issue_instant => Time.now.strftime('%Y-%m-%dT%H:%M:%SZ'),
7
+ :settings => settings
8
+ }
9
+ end
10
+
11
+ def valid_response(opts = {})
12
+ opts = default_response_opts.merge!(opts)
13
+
14
+ "<samlp:LogoutResponse
15
+ xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"
16
+ ID=\"#{random_id}\" Version=\"2.0\"
17
+ IssueInstant=\"#{opts[:issue_instant]}\"
18
+ Destination=\"#{opts[:settings].assertion_consumer_logout_service_url}\"
19
+ InResponseTo=\"#{opts[:uuid]}\">
20
+ <saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">#{opts[:settings].issuer}</saml:Issuer>
21
+ <samlp:Status xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\">
22
+ <samlp:StatusCode xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"
23
+ Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\">
24
+ </samlp:StatusCode>
25
+ </samlp:Status>
26
+ </samlp:LogoutResponse>"
27
+ end
28
+
29
+ def unsuccessful_response(opts = {})
30
+ opts = default_response_opts.merge!(opts)
31
+
32
+ "<samlp:LogoutResponse
33
+ xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"
34
+ ID=\"#{random_id}\" Version=\"2.0\"
35
+ IssueInstant=\"#{opts[:issue_instant]}\"
36
+ Destination=\"#{opts[:settings].assertion_consumer_logout_service_url}\"
37
+ InResponseTo=\"#{opts[:uuid]}\">
38
+ <saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">#{opts[:settings].issuer}</saml:Issuer>
39
+ <samlp:Status xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\">
40
+ <samlp:StatusCode xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"
41
+ Value=\"urn:oasis:names:tc:SAML:2.0:status:Requester\">
42
+ </samlp:StatusCode>
43
+ </samlp:Status>
44
+ </samlp:LogoutResponse>"
45
+ end
46
+
47
+ def invalid_xml_response
48
+ "<samlp:SomethingAwful
49
+ xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"
50
+ ID=\"#{random_id}\" Version=\"2.0\">
51
+ </samlp:SomethingAwful>"
52
+ end
53
+
54
+ def settings
55
+ @settings ||= Onelogin::Saml::Settings.new(
56
+ {
57
+ :assertion_consumer_service_url => "http://app.muda.no/sso/consume",
58
+ :assertion_consumer_logout_service_url => "http://app.muda.no/sso/consume_logout",
59
+ :issuer => "http://app.muda.no",
60
+ :sp_name_qualifier => "http://sso.muda.no",
61
+ :idp_sso_target_url => "http://sso.muda.no/sso",
62
+ :idp_slo_target_url => "http://sso.muda.no/slo",
63
+ :idp_cert_fingerprint => "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00",
64
+ :name_identifier_format => "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
65
+ }
66
+ )
67
+ end
@@ -0,0 +1 @@
1
+ PHNhbWxwOlJlc3BvbnNlIElEPSJCZWViMzkyYjc1Ny02ZGM3LTRlYjktYmI1Yy03NmU1MTFmZDZiZWIiIElzc3VlSW5zdGFudD0iMjAxMi0xMS0yOFQxODoxMzo0NVoiIFZlcnNpb249IjIuMCIgeG1sbnM9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI+PFNpZ25hdHVyZSB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PFNpZ25lZEluZm8+PENhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy9UUi8yMDAxL1JFQy14bWwtYzE0bi0yMDAxMDMxNSIgLz48U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIiAvPjxSZWZlcmVuY2UgVVJJPSIjQmVlYjM5MmI3NTctNmRjNy00ZWI5LWJiNWMtNzZlNTExZmQ2YmViIj48VHJhbnNmb3Jtcz48VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiIC8+PC9UcmFuc2Zvcm1zPjxEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIgLz48RGlnZXN0VmFsdWU+S0RqNHlxK0RDaVVJZVpDZUcyRFhFZWxWVURNPTwvRGlnZXN0VmFsdWU+PC9SZWZlcmVuY2U+PC9TaWduZWRJbmZvPjxTaWduYXR1cmVWYWx1ZT5zTDZjakVJTkt0U2l3THpRamk3RzYvRXVXMytZVXUzOHNUa3A1WnJva2pmUUxxZlZacVE0MDltYXowSU9neDVwWW44TmxrdWpIZUtTQ1N6ME9uRjlLRUxUNEpINi82Sklob0JuSjdiY1JFMG0vSFdEbFhCRGU2V3hkd2w4M1Y2aENKMTZtM25tTHhLTldDRThPU3BuaEdqK3d3UFRxRU56NVRTdGgrbkU1WVcyLzNBU3ZYK3ZENjFBUFNUeWkxWm5CR0huWWRiZVQ5Yk9LUUFVck1kQ1ZwWFlYZlVrK1I5Qkh6U3grR1VaL3RWU01mUjQ0ZE01dlpjTXFnSHhtd1c0eUQrQUVYNVNNQlZEYkdRd1o5dUFnQk14YWtQYjU4VHM1YlVtVUVFUzFDbFV6Zm95S3lvdHR3WmIwSVVsQVh4bSs3RHNpT2tkQ1BTQjhyUDRnMXIvNHc9PTwvU2lnbmF0dXJlVmFsdWU+PEtleUluZm8+PFg1MDlEYXRhPjxYNTA5Q2VydGlmaWNhdGU+TUlJQ1dUQ0NBZ2VnQXdJQkFnSVF5K0lPeGE5UkZZcE1mZjlPYkJMNThqQUpCZ1VyRGdNQ0hRVUFNQll4RkRBU0JnTlZCQU1UQzFKdmIzUWdRV2RsYm1ONU1CNFhEVEV3TURFeU1URTFNREl4TUZvWERUTTVNVEl6TVRJek5UazFPVm93T0RFMk1EUUdBMVVFQXhNdFUwRk5URlJsYzNRdE5qSTVOalF4T0dRdFlUWmpOeTAwTVRoa0xUZzBaV0V0WmpSak1EUmlPV1JrTVdJMk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1JtMW9oaEcwdE5XdVRIdnpNYWJaWUgwclJSeUFGQ012Wk9EczBBNVArVjJSYVU2b3IwcmNpREhLQUlhaG5FVGRNcUl0RzhzbEtxeUhRRVAzbXFVTk1adjMrd3ByMVBlV0I2REJZMnBBUmoxM2pQaTUyTjVHSWF2UUtCS0E1OEJBcmtJK0pzQjZmMmpXWXM4c0YyekZ5TUg5aEEvZDJVRm50L1BDWjNBLytUU1FJY3lqTldiUVByZUNDdW1hRUJDVWkvbGRhQ2lZSXM2SUJ5aGdFeXRKYWhTejdPTWVPdWNKanFmUGRHbWlHVzZ1dzRmZmZaR3l1U0k0TC9memFKQTZGdU9XZlZCTUdLb3NoZzQxUnB3U3V1dDRXWU05dXlLZzdYQ1dZbndSL1F3YTJDQmgzdGZHd3E3ME5uMzcyWVdqYkszdU1OU2JzOHlhUGtqb0psLzJRSURBUUFCbzBzd1NUQkhCZ05WSFFFRVFEQStnQkFTNUFrdEJoMGRUd0NOWVNIY0ZtUmpvUmd3RmpFVU1CSUdBMVVFQXhNTFVtOXZkQ0JCWjJWdVkzbUNFQVkzYkFDcUFHU0tFYys0MUtwY05mUXdDUVlGS3c0REFoMEZBQU5CQUM1K2RFaElTbFB4bEZLeDIvQTVXcU9WTWZVVG96N2F6c0k3VDVmMXJkdWhrWXlDVnlvc3RUb3BSQWQrL3JSdzhxYmVGVXhYTVZHK3VHaG5RVlR3N0ljPTwvWDUwOUNlcnRpZmljYXRlPjwvWDUwOURhdGE+PC9LZXlJbmZvPjwvU2lnbmF0dXJlPjxzYW1scDpTdGF0dXM+PHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIgLz48L3NhbWxwOlN0YXR1cz48QXNzZXJ0aW9uIElEPSJCZWVhYjUwOTk1My01ZDE0LTQwMDctYTY0NC1mOWFjMmRlOWNlMjIiIElzc3VlSW5zdGFudD0iMjAwMy0wNC0xN1QwMDo0NjowMloiIFZlcnNpb249IjIuMCIgeG1sbnM9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPjxJc3N1ZXI+QmVlbGluZS5jb20NCiAgICAgICAgICAgICAgICA8L0lzc3Vlcj48U3ViamVjdD48TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdHJldm9yLmxpdHRsZUBzdGFyZmllbGR0bXMuY29tDQogICAgICAgICAgICAgICAgICAgICAgICA8L05hbWVJRD48U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiIC8+PC9TdWJqZWN0PjxDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMi0xMS0yOFQxNzo1Mzo0NVoiIE5vdE9uT3JBZnRlcj0iMjAxMi0xMS0yOFQxODozMzo0NVoiPjwvQ29uZGl0aW9ucz48QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDEyLTExLTI4VDE4OjEzOjQ1WiI+PEF1dGhuQ29udGV4dD48QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9BdXRobkNvbnRleHRDbGFzc1JlZj48L0F1dGhuQ29udGV4dD48L0F1dGhuU3RhdGVtZW50PjwvQXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+
@@ -10,7 +10,8 @@ class SettingsTest < Test::Unit::TestCase
10
10
  accessors = [
11
11
  :assertion_consumer_service_url, :issuer, :sp_name_qualifier,
12
12
  :idp_sso_target_url, :idp_cert_fingerprint, :name_identifier_format,
13
- :idp_slo_target_url, :name_identifier_value, :sessionindex
13
+ :idp_slo_target_url, :name_identifier_value, :sessionindex,
14
+ :assertion_consumer_logout_service_url
14
15
  ]
15
16
 
16
17
  accessors.each do |accessor|
@@ -117,7 +117,17 @@ class XmlSecurityTest < Test::Unit::TestCase
117
117
  assert inclusive_namespaces.empty?
118
118
  end
119
119
  end
120
-
120
+
121
+ context "StarfieldTMS" do
122
+ should "be able to validate a response" do
123
+ response = Onelogin::Saml::Response.new(fixture(:starfield_response))
124
+ response.settings = Onelogin::Saml::Settings.new(
125
+ :idp_cert_fingerprint => "8D:BA:53:8E:A3:B6:F9:F1:69:6C:BB:D9:D8:BD:41:B3:AC:4F:9D:4D"
126
+ )
127
+ assert response.validate!
128
+ end
129
+ end
130
+
121
131
  end
122
132
 
123
133
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-saml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-02 00:00:00.000000000 Z
12
+ date: 2013-01-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: canonix
@@ -77,6 +77,7 @@ files:
77
77
  - lib/onelogin/ruby-saml/authrequest.rb
78
78
  - lib/onelogin/ruby-saml/logging.rb
79
79
  - lib/onelogin/ruby-saml/logoutrequest.rb
80
+ - lib/onelogin/ruby-saml/logoutresponse.rb
80
81
  - lib/onelogin/ruby-saml/metadata.rb
81
82
  - lib/onelogin/ruby-saml/response.rb
82
83
  - lib/onelogin/ruby-saml/settings.rb
@@ -91,12 +92,14 @@ files:
91
92
  - ruby-saml.gemspec
92
93
  - test/certificates/certificate1
93
94
  - test/logoutrequest_test.rb
95
+ - test/logoutresponse_test.rb
94
96
  - test/request_test.rb
95
97
  - test/response_test.rb
96
98
  - test/responses/adfs_response_sha1.xml
97
99
  - test/responses/adfs_response_sha256.xml
98
100
  - test/responses/adfs_response_sha384.xml
99
101
  - test/responses/adfs_response_sha512.xml
102
+ - test/responses/logoutresponse_fixtures.rb
100
103
  - test/responses/no_signature_ns.xml
101
104
  - test/responses/open_saml_response.xml
102
105
  - test/responses/response1.xml.base64
@@ -107,6 +110,7 @@ files:
107
110
  - test/responses/response_with_ampersands.xml
108
111
  - test/responses/response_with_ampersands.xml.base64
109
112
  - test/responses/simple_saml_php.xml
113
+ - test/responses/starfield_response.xml.base64
110
114
  - test/responses/wrapped_response_2.xml.base64
111
115
  - test/settings_test.rb
112
116
  - test/test_helper.rb
@@ -132,19 +136,21 @@ required_rubygems_version: !ruby/object:Gem::Requirement
132
136
  version: '0'
133
137
  requirements: []
134
138
  rubyforge_project: http://www.rubygems.org/gems/ruby-saml
135
- rubygems_version: 1.8.24
139
+ rubygems_version: 1.8.23
136
140
  signing_key:
137
141
  specification_version: 3
138
142
  summary: SAML Ruby Tookit
139
143
  test_files:
140
144
  - test/certificates/certificate1
141
145
  - test/logoutrequest_test.rb
146
+ - test/logoutresponse_test.rb
142
147
  - test/request_test.rb
143
148
  - test/response_test.rb
144
149
  - test/responses/adfs_response_sha1.xml
145
150
  - test/responses/adfs_response_sha256.xml
146
151
  - test/responses/adfs_response_sha384.xml
147
152
  - test/responses/adfs_response_sha512.xml
153
+ - test/responses/logoutresponse_fixtures.rb
148
154
  - test/responses/no_signature_ns.xml
149
155
  - test/responses/open_saml_response.xml
150
156
  - test/responses/response1.xml.base64
@@ -155,6 +161,7 @@ test_files:
155
161
  - test/responses/response_with_ampersands.xml
156
162
  - test/responses/response_with_ampersands.xml.base64
157
163
  - test/responses/simple_saml_php.xml
164
+ - test/responses/starfield_response.xml.base64
158
165
  - test/responses/wrapped_response_2.xml.base64
159
166
  - test/settings_test.rb
160
167
  - test/test_helper.rb