ruby-saml 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.

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