samlsso 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +50 -0
  3. data/CODE_OF_CONDUCT.md +49 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE +21 -0
  6. data/README.md +36 -0
  7. data/Rakefile +2 -0
  8. data/bin/console +14 -0
  9. data/bin/setup +8 -0
  10. data/lib/samlsso.rb +16 -0
  11. data/lib/samlsso/attribute_service.rb +32 -0
  12. data/lib/samlsso/attributes.rb +107 -0
  13. data/lib/samlsso/authrequest.rb +124 -0
  14. data/lib/samlsso/idp_metadata_parser.rb +85 -0
  15. data/lib/samlsso/logging.rb +20 -0
  16. data/lib/samlsso/logoutrequest.rb +100 -0
  17. data/lib/samlsso/logoutresponse.rb +110 -0
  18. data/lib/samlsso/metadata.rb +94 -0
  19. data/lib/samlsso/response.rb +271 -0
  20. data/lib/samlsso/saml_message.rb +117 -0
  21. data/lib/samlsso/settings.rb +115 -0
  22. data/lib/samlsso/slo_logoutrequest.rb +64 -0
  23. data/lib/samlsso/slo_logoutresponse.rb +99 -0
  24. data/lib/samlsso/utils.rb +42 -0
  25. data/lib/samlsso/validation_error.rb +5 -0
  26. data/lib/samlsso/version.rb +3 -0
  27. data/lib/schemas/saml-schema-assertion-2.0.xsd +283 -0
  28. data/lib/schemas/saml-schema-authn-context-2.0.xsd +23 -0
  29. data/lib/schemas/saml-schema-authn-context-types-2.0.xsd +821 -0
  30. data/lib/schemas/saml-schema-metadata-2.0.xsd +339 -0
  31. data/lib/schemas/saml-schema-protocol-2.0.xsd +302 -0
  32. data/lib/schemas/sstc-metadata-attr.xsd +35 -0
  33. data/lib/schemas/sstc-saml-attribute-ext.xsd +25 -0
  34. data/lib/schemas/sstc-saml-metadata-algsupport-v1.0.xsd +41 -0
  35. data/lib/schemas/sstc-saml-metadata-ui-v1.0.xsd +89 -0
  36. data/lib/schemas/xenc-schema.xsd +136 -0
  37. data/lib/schemas/xml.xsd +287 -0
  38. data/lib/schemas/xmldsig-core-schema.xsd +309 -0
  39. data/lib/xml_security.rb +276 -0
  40. data/samlsso.gemspec +44 -0
  41. metadata +168 -0
@@ -0,0 +1,85 @@
1
+ require "base64"
2
+ require "uuid"
3
+ require "zlib"
4
+ require "cgi"
5
+ require "rexml/document"
6
+ require "rexml/xpath"
7
+
8
+ module Samlsso
9
+ include REXML
10
+
11
+ class IdpMetadataParser
12
+
13
+ METADATA = "urn:oasis:names:tc:SAML:2.0:metadata"
14
+ DSIG = "http://www.w3.org/2000/09/xmldsig#"
15
+
16
+ attr_reader :document
17
+
18
+ def parse_remote(url, validate_cert = true)
19
+ idp_metadata = get_idp_metadata(url, validate_cert)
20
+ parse(idp_metadata)
21
+ end
22
+
23
+ def parse(idp_metadata)
24
+ @document = REXML::Document.new(idp_metadata)
25
+
26
+ Samlsso::Settings.new.tap do |settings|
27
+
28
+ settings.idp_sso_target_url = single_signon_service_url
29
+ settings.idp_slo_target_url = single_logout_service_url
30
+ settings.idp_cert_fingerprint = fingerprint
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # Retrieve the remote IdP metadata from the URL or a cached copy
37
+ # # returns a REXML document of the metadata
38
+ def get_idp_metadata(url, validate_cert)
39
+ uri = URI.parse(url)
40
+ if uri.scheme == "http"
41
+ response = Net::HTTP.get_response(uri)
42
+ meta_text = response.body
43
+ elsif uri.scheme == "https"
44
+ http = Net::HTTP.new(uri.host, uri.port)
45
+ http.use_ssl = true
46
+ # Most IdPs will probably use self signed certs
47
+ if validate_cert
48
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
49
+ else
50
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
51
+ end
52
+ get = Net::HTTP::Get.new(uri.request_uri)
53
+ response = http.request(get)
54
+ meta_text = response.body
55
+ end
56
+ meta_text
57
+ end
58
+
59
+ def single_signon_service_url
60
+ node = REXML::XPath.first(document, "/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService/@Location", { "md" => METADATA })
61
+ node.value if node
62
+ end
63
+
64
+ def single_logout_service_url
65
+ node = REXML::XPath.first(document, "/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService/@Location", { "md" => METADATA })
66
+ node.value if node
67
+ end
68
+
69
+ def certificate
70
+ @certificate ||= begin
71
+ node = REXML::XPath.first(document, "/md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate", { "md" => METADATA, "ds" => DSIG })
72
+ Base64.decode64(node.text) if node
73
+ end
74
+ end
75
+
76
+ def fingerprint
77
+ @fingerprint ||= begin
78
+ if certificate
79
+ cert = OpenSSL::X509::Certificate.new(certificate)
80
+ Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(":")
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,20 @@
1
+ # Simplistic log class when we're running in Rails
2
+ module Samlsso
3
+ class Logging
4
+ def self.debug(message)
5
+ if defined? Rails
6
+ Rails.logger.debug message
7
+ else
8
+ puts message
9
+ end
10
+ end
11
+
12
+ def self.info(message)
13
+ if defined? Rails
14
+ Rails.logger.info message
15
+ else
16
+ puts message
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,100 @@
1
+ require "uuid"
2
+
3
+ require "samlsso/logging"
4
+
5
+ module Samlsso
6
+ class Logoutrequest < SamlMessage
7
+
8
+ attr_reader :uuid # Can be obtained if neccessary
9
+
10
+ def initialize
11
+ @uuid = "_" + UUID.new.generate
12
+ end
13
+
14
+ def create(settings, params={})
15
+ params = create_params(settings, params)
16
+ params_prefix = (settings.idp_slo_target_url =~ /\?/) ? '&' : '?'
17
+ saml_request = CGI.escape(params.delete("SAMLRequest"))
18
+ request_params = "#{params_prefix}SAMLRequest=#{saml_request}"
19
+ params.each_pair do |key, value|
20
+ request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
21
+ end
22
+ @logout_url = settings.idp_slo_target_url + request_params
23
+ end
24
+
25
+ def create_params(settings, params={})
26
+ params = {} if params.nil?
27
+
28
+ request_doc = create_logout_request_xml_doc(settings)
29
+ request_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
30
+
31
+ request = ""
32
+ request_doc.write(request)
33
+
34
+ Logging.debug "Created SLO Logout Request: #{request}"
35
+
36
+ request = deflate(request) if settings.compress_request
37
+ base64_request = encode(request)
38
+ request_params = {"SAMLRequest" => base64_request}
39
+
40
+ if settings.security[:logout_requests_signed] && !settings.security[:embed_sign] && settings.private_key
41
+ params['SigAlg'] = XMLSecurity::Document::SHA1
42
+ url_string = "SAMLRequest=#{CGI.escape(base64_request)}"
43
+ url_string += "&RelayState=#{CGI.escape(params['RelayState'])}" if params['RelayState']
44
+ url_string += "&SigAlg=#{CGI.escape(params['SigAlg'])}"
45
+ private_key = settings.get_sp_key()
46
+ signature = private_key.sign(XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]).new, url_string)
47
+ params['Signature'] = encode(signature)
48
+ end
49
+
50
+ params.each_pair do |key, value|
51
+ request_params[key] = value.to_s
52
+ end
53
+
54
+ request_params
55
+ end
56
+
57
+ def create_logout_request_xml_doc(settings)
58
+ time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
59
+
60
+ request_doc = XMLSecurity::Document.new
61
+ request_doc.uuid = uuid
62
+
63
+ 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" }
64
+ root.attributes['ID'] = uuid
65
+ root.attributes['IssueInstant'] = time
66
+ root.attributes['Version'] = "2.0"
67
+ root.attributes['Destination'] = settings.idp_slo_target_url unless settings.idp_slo_target_url.nil?
68
+
69
+ if settings.issuer
70
+ issuer = root.add_element "saml:Issuer"
71
+ issuer.text = settings.issuer
72
+ end
73
+
74
+ name_id = root.add_element "saml:NameID"
75
+ if settings.name_identifier_value
76
+ name_id.attributes['NameQualifier'] = settings.sp_name_qualifier if settings.sp_name_qualifier
77
+ name_id.attributes['Format'] = settings.name_identifier_format if settings.name_identifier_format
78
+ name_id.text = settings.name_identifier_value
79
+ else
80
+ # If no NameID is present in the settings we generate one
81
+ name_id.text = "_" + UUID.new.generate
82
+ name_id.attributes['Format'] = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
83
+ end
84
+
85
+ if settings.sessionindex
86
+ sessionindex = root.add_element "samlp:SessionIndex"
87
+ sessionindex.text = settings.sessionindex
88
+ end
89
+
90
+ # embebed sign
91
+ if settings.security[:logout_requests_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
92
+ private_key = settings.get_sp_key()
93
+ cert = settings.get_sp_cert()
94
+ request_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
95
+ end
96
+
97
+ request_doc
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,110 @@
1
+ require "xml_security"
2
+ require "time"
3
+
4
+ module Samlsso
5
+ class Logoutresponse < SamlMessage
6
+ # For API compability, this is mutable.
7
+ attr_accessor :settings
8
+
9
+ attr_reader :document
10
+ attr_reader :response
11
+ attr_reader :options
12
+
13
+ #
14
+ # In order to validate that the response matches a given request, append
15
+ # the option:
16
+ # :matches_request_id => REQUEST_ID
17
+ #
18
+ # It will validate that the logout response matches the ID of the request.
19
+ # You can also do this yourself through the in_response_to accessor.
20
+ #
21
+ def initialize(response, settings = nil, options = {})
22
+ raise ArgumentError.new("Logoutresponse cannot be nil") if response.nil?
23
+ self.settings = settings
24
+
25
+ @options = options
26
+ @response = decode_raw_saml(response)
27
+ @document = XMLSecurity::SignedDocument.new(@response)
28
+ end
29
+
30
+ def validate!
31
+ validate(false)
32
+ end
33
+
34
+ def validate(soft = true)
35
+ return false unless valid_saml?(document, soft) && valid_state?(soft)
36
+
37
+ valid_in_response_to?(soft) && valid_issuer?(soft) && success?(soft)
38
+ end
39
+
40
+ def success?(soft = true)
41
+ unless status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
42
+ return soft ? false : validation_error("Bad status code. Expected <urn:oasis:names:tc:SAML:2.0:status:Success>, but was: <#@status_code> ")
43
+ end
44
+ true
45
+ end
46
+
47
+ def in_response_to
48
+ @in_response_to ||= begin
49
+ node = REXML::XPath.first(document, "/p:LogoutResponse", { "p" => PROTOCOL, "a" => ASSERTION })
50
+ node.nil? ? nil : node.attributes['InResponseTo']
51
+ end
52
+ end
53
+
54
+ def issuer
55
+ @issuer ||= begin
56
+ node = REXML::XPath.first(document, "/p:LogoutResponse/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
57
+ node ||= REXML::XPath.first(document, "/p:LogoutResponse/a:Assertion/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
58
+ node.nil? ? nil : node.text
59
+ end
60
+ end
61
+
62
+ def status_code
63
+ @status_code ||= begin
64
+ node = REXML::XPath.first(document, "/p:LogoutResponse/p:Status/p:StatusCode", { "p" => PROTOCOL, "a" => ASSERTION })
65
+ node.nil? ? nil : node.attributes["Value"]
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def valid_state?(soft = true)
72
+ if response.empty?
73
+ return soft ? false : validation_error("Blank response")
74
+ end
75
+
76
+ if settings.nil?
77
+ return soft ? false : validation_error("No settings on response")
78
+ end
79
+
80
+ if settings.issuer.nil?
81
+ return soft ? false : validation_error("No issuer in settings")
82
+ end
83
+
84
+ if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
85
+ return soft ? false : validation_error("No fingerprint or certificate on settings")
86
+ end
87
+
88
+ true
89
+ end
90
+
91
+ def valid_in_response_to?(soft = true)
92
+ return true unless self.options.has_key? :matches_request_id
93
+
94
+ unless self.options[:matches_request_id] == in_response_to
95
+ return soft ? false : validation_error("Response does not match the request ID, expected: <#{self.options[:matches_request_id]}>, but was: <#{in_response_to}>")
96
+ end
97
+
98
+ true
99
+ end
100
+
101
+ def valid_issuer?(soft = true)
102
+ return true if self.settings.idp_entity_id.nil? or self.issuer.nil?
103
+
104
+ unless URI.parse(self.issuer) == URI.parse(self.settings.idp_entity_id)
105
+ return soft ? false : validation_error("Doesn't match the issuer, expected: <#{self.settings.issuer}>, but was: <#{issuer}>")
106
+ end
107
+ true
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,94 @@
1
+ require "rexml/document"
2
+ require "rexml/xpath"
3
+ require "uri"
4
+
5
+ require "samlsso/logging"
6
+
7
+ # Class to return SP metadata based on the settings requested.
8
+ # Return this XML in a controller, then give that URL to the the
9
+ # IdP administrator. The IdP will poll the URL and your settings
10
+ # will be updated automatically
11
+ module Samlsso
12
+ include REXML
13
+ class Metadata
14
+ def generate(settings)
15
+ meta_doc = REXML::Document.new
16
+ root = meta_doc.add_element "md:EntityDescriptor", {
17
+ "xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata"
18
+ }
19
+ sp_sso = root.add_element "md:SPSSODescriptor", {
20
+ "protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol",
21
+ "AuthnRequestsSigned" => settings.security[:authn_requests_signed],
22
+ # However we would like assertions signed if idp_cert_fingerprint or idp_cert is set
23
+ "WantAssertionsSigned" => !!(settings.idp_cert_fingerprint || settings.idp_cert)
24
+ }
25
+ if settings.issuer
26
+ root.attributes["entityID"] = settings.issuer
27
+ end
28
+ if settings.single_logout_service_url
29
+ sp_sso.add_element "md:SingleLogoutService", {
30
+ "Binding" => settings.single_logout_service_binding,
31
+ "Location" => settings.single_logout_service_url,
32
+ "ResponseLocation" => settings.single_logout_service_url,
33
+ "isDefault" => true,
34
+ "index" => 0
35
+ }
36
+ end
37
+ if settings.name_identifier_format
38
+ name_id = sp_sso.add_element "md:NameIDFormat"
39
+ name_id.text = settings.name_identifier_format
40
+ end
41
+ if settings.assertion_consumer_service_url
42
+ sp_sso.add_element "md:AssertionConsumerService", {
43
+ "Binding" => settings.assertion_consumer_service_binding,
44
+ "Location" => settings.assertion_consumer_service_url,
45
+ "isDefault" => true,
46
+ "index" => 0
47
+ }
48
+ end
49
+
50
+ # Add KeyDescriptor if messages will be signed
51
+ cert = settings.get_sp_cert()
52
+ if cert
53
+ kd = sp_sso.add_element "md:KeyDescriptor", { "use" => "signing" }
54
+ ki = kd.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
55
+ xd = ki.add_element "ds:X509Data"
56
+ xc = xd.add_element "ds:X509Certificate"
57
+ xc.text = Base64.encode64(cert.to_der).gsub("\n", '')
58
+ end
59
+
60
+ if settings.attribute_consuming_service.configured?
61
+ sp_acs = sp_sso.add_element "md:AttributeConsumingService", {
62
+ "isDefault" => "true",
63
+ "index" => settings.attribute_consuming_service.index
64
+ }
65
+ srv_name = sp_acs.add_element "md:ServiceName", {
66
+ "xml:lang" => "en"
67
+ }
68
+ srv_name.text = settings.attribute_consuming_service.name
69
+ settings.attribute_consuming_service.attributes.each do |attribute|
70
+ sp_req_attr = sp_acs.add_element "md:RequestedAttribute", {
71
+ "NameFormat" => attribute[:name_format],
72
+ "Name" => attribute[:name],
73
+ "FriendlyName" => attribute[:friendly_name]
74
+ }
75
+ unless attribute[:attribute_value].nil?
76
+ sp_attr_val = sp_req_attr.add_element "md:AttributeValue"
77
+ sp_attr_val.text = attribute[:attribute_value]
78
+ end
79
+ end
80
+ end
81
+
82
+ # With OpenSSO, it might be required to also include
83
+ # <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"/>
84
+ # <md:XACMLAuthzDecisionQueryDescriptor WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"/>
85
+
86
+ meta_doc << REXML::XMLDecl.new("1.0", "UTF-8")
87
+ ret = ""
88
+ # pretty print the XML so IdP administrators can easily see what the SP supports
89
+ meta_doc.write(ret, 1)
90
+
91
+ return ret
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,271 @@
1
+ require "xml_security"
2
+ require "time"
3
+ require "nokogiri"
4
+
5
+ # Only supports SAML 2.0
6
+ module Samlsso
7
+
8
+ class Response < SamlMessage
9
+ ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
10
+ PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
11
+ DSIG = "http://www.w3.org/2000/09/xmldsig#"
12
+
13
+ # TODO: This should probably be ctor initialized too... WDYT?
14
+ attr_accessor :settings
15
+ attr_accessor :errors
16
+
17
+ attr_reader :options
18
+ attr_reader :response
19
+ attr_reader :document
20
+ attr_reader :decoded_response
21
+ attr_reader :decrypted_response
22
+ attr_reader :decoded_document
23
+
24
+ def initialize(response, options = {})
25
+ @errors = []
26
+ raise ArgumentError.new("Response cannot be nil") if response.nil?
27
+ @options = options
28
+ @decoded_response = decode_raw_saml(response)
29
+ @decrypted_response = decrypt_saml(@decoded_response, @options[:private_key_file_path])
30
+ @response = @decrypted_response
31
+ @document = XMLSecurity::SignedDocument.new(@response, @errors)
32
+ @decoded_document = XMLSecurity::SignedDocument.new(@decoded_response, @errors)
33
+ end
34
+
35
+ def is_valid?
36
+ validate
37
+ end
38
+
39
+ def validate!
40
+ validate(false)
41
+ end
42
+
43
+ def errors
44
+ @errors
45
+ end
46
+
47
+ # The value of the user identifier as designated by the initialization request response
48
+ def name_id
49
+ @name_id ||= begin
50
+ node = xpath_first_from_signed_assertion('/a:Subject/a:NameID')
51
+ node.nil? ? nil : node.text
52
+ end
53
+ end
54
+
55
+ def sessionindex
56
+ @sessionindex ||= begin
57
+ node = xpath_first_from_signed_assertion('/a:AuthnStatement')
58
+ node.nil? ? nil : (node.attributes['SessionIndex'] ? node.attributes['SessionIndex'] : node.attributes['sessionindex'])
59
+ end
60
+ end
61
+
62
+ # Returns Samlsso::Attributes enumerable collection.
63
+ # All attributes can be iterated over +attributes.each+ or returned as array by +attributes.all+
64
+ #
65
+ # For backwards compatibility samlsso returns by default only the first value for a given attribute with
66
+ # attributes['name']
67
+ # To get all of the attributes, use:
68
+ # attributes.multi('name')
69
+ # Or turn off the compatibility:
70
+ # Samlsso::Attributes.single_value_compatibility = false
71
+ # Now this will return an array:
72
+ # attributes['name']
73
+ def attributes
74
+ @attr_statements ||= begin
75
+ attributes = Attributes.new
76
+
77
+ stmt_element = xpath_first_from_signed_assertion('/a:AttributeStatement')
78
+ return attributes if stmt_element.nil?
79
+
80
+ stmt_element.elements.each do |attr_element|
81
+ name = attr_element.attributes["Name"] ? attr_element.attributes["Name"] : attr_element.attributes["name"]
82
+ values = attr_element.elements.collect{|e|
83
+ # SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1"
84
+ # otherwise the value is to be regarded as empty.
85
+ ["true", "1"].include?(e.attributes['xsi:nil']) ? nil : e.text.to_s
86
+ }
87
+
88
+ attributes.add(name, values)
89
+ end
90
+
91
+ attributes
92
+ end
93
+ end
94
+
95
+ # When this user session should expire at latest
96
+ def session_expires_at
97
+ @expires_at ||= begin
98
+ node = xpath_first_from_signed_assertion('/a:AuthnStatement')
99
+ parse_time(node, "SessionNotOnOrAfter")
100
+ end
101
+ end
102
+
103
+ # Checks the status of the response for a "Success" code
104
+ def success?
105
+ @status_code ||= begin
106
+ node = REXML::XPath.first(document, "/p:Response/p:Status/p:StatusCode", { "p" => PROTOCOL, "a" => ASSERTION })
107
+ node.attributes["Value"] == "urn:oasis:names:tc:SAML:2.0:status:Success" || node.attributes["value"] == "urn:oasis:names:tc:SAML:2.0:status:Success"
108
+ end
109
+ end
110
+
111
+ def status_message
112
+ @status_message ||= begin
113
+ node = REXML::XPath.first(document, "/p:Response/p:Status/p:StatusMessage", { "p" => PROTOCOL, "a" => ASSERTION })
114
+ node.text if node
115
+ end
116
+ end
117
+
118
+ # Conditions (if any) for the assertion to run
119
+ def conditions
120
+ @conditions ||= xpath_first_from_signed_assertion('/a:Conditions')
121
+ end
122
+
123
+ def not_before
124
+ @not_before ||= parse_time(conditions, "NotBefore")
125
+ end
126
+
127
+ def not_on_or_after
128
+ @not_on_or_after ||= parse_time(conditions, "NotOnOrAfter")
129
+ end
130
+
131
+ def issuer
132
+ @issuer ||= begin
133
+ node = REXML::XPath.first(document, "/p:Response/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
134
+ node ||= xpath_first_from_signed_assertion('/a:Issuer')
135
+ node.nil? ? nil : node.text
136
+ end
137
+ end
138
+
139
+ private
140
+
141
+ def validate(soft = true)
142
+ valid_saml?(decoded_document, soft) &&
143
+ validate_response_state(soft) &&
144
+ validate_conditions(soft) &&
145
+ validate_issuer(soft) &&
146
+ decoded_document.validate_document(get_fingerprint, soft) &&
147
+ validate_success_status(soft)
148
+ end
149
+
150
+ def validate_success_status(soft = true)
151
+ if success?
152
+ true
153
+ else
154
+ soft ? false : validation_error(status_message)
155
+ end
156
+ end
157
+
158
+ def validate_structure(soft = true)
159
+ return true #temp
160
+ xml = Nokogiri::XML(self.document.to_s)
161
+
162
+ SamlMessage.schema.validate(xml).map do |error|
163
+ if soft
164
+ @errors << "Schema validation failed"
165
+ break false
166
+ else
167
+ error_message = [error.message, xml.to_s].join("\n\n")
168
+
169
+ @errors << error_message
170
+ validation_error(error_message)
171
+ end
172
+ end
173
+ end
174
+
175
+ def validate_response_state(soft = true)
176
+ if response.empty?
177
+ return soft ? false : validation_error("Blank response")
178
+ end
179
+
180
+ if settings.nil?
181
+ return soft ? false : validation_error("No settings on response")
182
+ end
183
+
184
+ if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
185
+ return soft ? false : validation_error("No fingerprint or certificate on settings")
186
+ end
187
+
188
+ true
189
+ end
190
+
191
+ def xpath_first_from_signed_assertion(subelt=nil)
192
+ id_str = ""
193
+ id_str = "[@ID=$id]" unless document.signed_element_id.blank?
194
+ node = REXML::XPath.first(
195
+ document,
196
+ "/p:Response/a:Assertion#{id_str}#{subelt}",
197
+ { "p" => PROTOCOL, "a" => ASSERTION },
198
+ { 'id' => document.signed_element_id }
199
+ )
200
+ node ||= REXML::XPath.first(
201
+ document,
202
+ "/p:Response#{id_str}/a:Assertion#{subelt}",
203
+ { "p" => PROTOCOL, "a" => ASSERTION },
204
+ { 'id' => document.signed_element_id }
205
+ )
206
+ node ||= REXML::XPath.first(
207
+ document,
208
+ "/p:Response/a:assertion#{id_str}#{subelt}",
209
+ { "p" => PROTOCOL, "a" => ASSERTION },
210
+ { 'id' => document.signed_element_id }
211
+ )
212
+ node ||= REXML::XPath.first(
213
+ document,
214
+ "/p:Response#{id_str}/a:assertion#{subelt}",
215
+ { "p" => PROTOCOL, "a" => ASSERTION },
216
+ { 'id' => document.signed_element_id }
217
+ )
218
+ node ||= REXML::XPath.first(
219
+ document,
220
+ "/p:Response#{id_str}/a:assertion#{subelt.downcase}",
221
+ { "p" => PROTOCOL, "a" => ASSERTION },
222
+ { 'id' => document.signed_element_id }
223
+ )
224
+ node
225
+ end
226
+
227
+ def get_fingerprint
228
+ if settings.idp_cert
229
+ cert = OpenSSL::X509::Certificate.new(settings.idp_cert)
230
+ Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(":")
231
+ else
232
+ settings.idp_cert_fingerprint
233
+ end
234
+ end
235
+
236
+ def validate_conditions(soft = true)
237
+ return true if conditions.nil?
238
+ return true if options[:skip_conditions]
239
+
240
+ now = Time.now.utc
241
+
242
+ if not_before && (now + (options[:allowed_clock_drift] || 0)) < not_before
243
+ @errors << "Current time is earlier than NotBefore condition #{(now + (options[:allowed_clock_drift] || 0))} < #{not_before})"
244
+ return soft ? false : validation_error("Current time is earlier than NotBefore condition")
245
+ end
246
+
247
+ if not_on_or_after && now >= not_on_or_after
248
+ @errors << "Current time is on or after NotOnOrAfter condition (#{now} >= #{not_on_or_after})"
249
+ return soft ? false : validation_error("Current time is on or after NotOnOrAfter condition")
250
+ end
251
+
252
+ true
253
+ end
254
+
255
+ def validate_issuer(soft = true)
256
+ return true if settings.idp_entity_id.nil?
257
+
258
+ unless URI.parse(issuer) == URI.parse(settings.idp_entity_id)
259
+ return soft ? false : validation_error("Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>")
260
+ end
261
+ true
262
+ end
263
+
264
+ def parse_time(node, attribute)
265
+ if node
266
+ attrs = node.attributes[attribute] ? node.attributes[attribute] : node.attributes[attribute.downcase]
267
+ Time.parse(attrs) if attrs
268
+ end
269
+ end
270
+ end
271
+ end