samlsso 0.1.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.
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