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,117 @@
1
+ require 'cgi'
2
+ require 'zlib'
3
+ require 'base64'
4
+ require "nokogiri"
5
+ require "rexml/document"
6
+ require "rexml/xpath"
7
+ require "xmlenc"
8
+ require "thread"
9
+
10
+ module Samlsso
11
+ class SamlMessage
12
+ include REXML
13
+
14
+ ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
15
+ PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
16
+
17
+ def self.schema
18
+ @schema ||= Mutex.new.synchronize do
19
+ Dir.chdir(File.expand_path("../../../schemas", __FILE__)) do
20
+ ::Nokogiri::XML::Schema(File.read("saml-schema-protocol-2.0.xsd"))
21
+ end
22
+ end
23
+ end
24
+
25
+ def valid_saml?(document, soft = true)
26
+ xml = Nokogiri::XML(document.to_s)
27
+
28
+ SamlMessage.schema.validate(xml).map do |error|
29
+ break false if soft
30
+ validation_error("#{error.message}\n\n#{xml.to_s}")
31
+ end
32
+ end
33
+
34
+ def validation_error(message)
35
+ raise ValidationError.new(message)
36
+ end
37
+
38
+ private
39
+
40
+ def decrypt_saml(decoded_saml, private_key_file_path=nil)
41
+ noko_xml = Nokogiri::XML(decoded_saml)
42
+ if (noko_xml.xpath('//saml:EncryptedAssertion', { :saml => ASSERTION }).count > 0)
43
+ raise ArgumentError, "Decryption Key File Path not provided for Encrypted Assertion" if private_key_file_path.nil?
44
+ key_pem = File.read(private_key_file_path)
45
+ encrypted_response = Xmlenc::EncryptedDocument.new(decoded_saml)
46
+ private_key = OpenSSL::PKey::RSA.new(key_pem)
47
+ decrypted_string = encrypted_response.decrypt(private_key)
48
+ decrypted_doc = Nokogiri::XML(decrypted_string) do |config|
49
+ # config.strict.nonet # for an ideal world
50
+ end
51
+ saml_namespace = {:saml => ASSERTION}
52
+ assertion = decrypted_doc.xpath("//saml:EncryptedAssertion/saml:Assertion", saml_namespace)
53
+ assertion = decrypted_doc.xpath("//saml:assertion", saml_namespace) if assertion.empty?
54
+ assertion = decrypted_doc.xpath("//saml:Assertion", saml_namespace) if assertion.empty?
55
+ assertion = decrypted_doc.xpath("//saml:ASSERTION", saml_namespace) if assertion.empty?
56
+
57
+ encrypted_assertion = decrypted_doc.xpath("//saml:EncryptedAssertion", saml_namespace)
58
+ encrypted_assertion = decrypted_doc.xpath("//saml:encryptedassertion", saml_namespace) if encrypted_assertion.empty?
59
+ encrypted_assertion = decrypted_doc.xpath("//saml:Encryptedassertion", saml_namespace) if encrypted_assertion.empty?
60
+ encrypted_assertion = decrypted_doc.xpath("//saml:ENCRYPTEDASSERTION", saml_namespace) if encrypted_assertion.empty?
61
+
62
+ if assertion.empty?
63
+ validation_error("XML document seems to be malformed and does not have correct Nodes")
64
+ else
65
+ encrypted_assertion.remove
66
+ decrypted_doc.root.add_child(assertion.last)
67
+ return decrypted_doc.to_xml.squish
68
+ end
69
+ end
70
+ return decoded_saml
71
+ end
72
+
73
+ def decode_raw_saml(saml)
74
+ if saml =~ /^</
75
+ return saml
76
+ elsif (decoded = decode(saml)) =~ /^</
77
+ return decoded
78
+ elsif (inflated = inflate(decoded)) =~ /^</
79
+ return inflated
80
+ end
81
+
82
+ return nil
83
+ end
84
+
85
+ def encode_raw_saml(saml, settings)
86
+ saml = Zlib::Deflate.deflate(saml, 9)[2..-5] if settings.compress_request
87
+ base64_saml = Base64.encode64(saml)
88
+ return CGI.escape(base64_saml)
89
+ end
90
+
91
+ def decode(encoded)
92
+ Base64.decode64(encoded)
93
+ end
94
+
95
+ def encode(encoded)
96
+ Base64.encode64(encoded).gsub(/\n/, "")
97
+ end
98
+
99
+ def escape(unescaped)
100
+ CGI.escape(unescaped)
101
+ end
102
+
103
+ def unescape(escaped)
104
+ CGI.unescape(escaped)
105
+ end
106
+
107
+ def inflate(deflated)
108
+ zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS)
109
+ zlib.inflate(deflated)
110
+ end
111
+
112
+ def deflate(inflated)
113
+ Zlib::Deflate.deflate(inflated, 9)[2..-5]
114
+ end
115
+
116
+ end
117
+ end
@@ -0,0 +1,115 @@
1
+ module Samlsso
2
+ class Settings
3
+ def initialize(overrides = {})
4
+ config = DEFAULTS.merge(overrides)
5
+ config.each do |k,v|
6
+ acc = "#{k.to_s}=".to_sym
7
+ self.send(acc, v) if self.respond_to? acc
8
+ end
9
+ @attribute_consuming_service = AttributeService.new
10
+ end
11
+
12
+ # IdP Data
13
+ attr_accessor :idp_entity_id
14
+ attr_accessor :idp_sso_target_url
15
+ attr_accessor :idp_slo_target_url
16
+ attr_accessor :idp_cert
17
+ attr_accessor :idp_cert_fingerprint
18
+ attr_accessor :idp_sso_is_encrypted
19
+ # SP Data
20
+ attr_accessor :issuer
21
+ attr_accessor :assertion_consumer_service_url
22
+ attr_accessor :assertion_consumer_service_binding
23
+ attr_accessor :sp_name_qualifier
24
+ attr_accessor :name_identifier_format
25
+ attr_accessor :name_identifier_value
26
+ attr_accessor :sessionindex
27
+ attr_accessor :compress_request
28
+ attr_accessor :compress_response
29
+ attr_accessor :double_quote_xml_attribute_values
30
+ attr_accessor :passive
31
+ attr_accessor :protocol_binding
32
+ attr_accessor :attributes_index
33
+ attr_accessor :force_authn
34
+ attr_accessor :security
35
+ attr_accessor :certificate
36
+ attr_accessor :private_key
37
+ attr_accessor :authn_context
38
+ attr_accessor :authn_context_comparison
39
+ attr_accessor :authn_context_decl_ref
40
+ attr_reader :attribute_consuming_service
41
+ # Compability
42
+ attr_accessor :assertion_consumer_logout_service_url
43
+ attr_accessor :assertion_consumer_logout_service_binding
44
+
45
+ def single_logout_service_url()
46
+ val = nil
47
+ if @single_logout_service_url.nil?
48
+ if @assertion_consumer_logout_service_url
49
+ val = @assertion_consumer_logout_service_url
50
+ end
51
+ else
52
+ val = @single_logout_service_url
53
+ end
54
+ val
55
+ end
56
+
57
+ # setter
58
+ def single_logout_service_url=(val)
59
+ @single_logout_service_url = val
60
+ end
61
+
62
+ def single_logout_service_binding()
63
+ val = nil
64
+ if @single_logout_service_binding.nil?
65
+ if @assertion_consumer_logout_service_binding
66
+ val = @assertion_consumer_logout_service_binding
67
+ end
68
+ else
69
+ val = @single_logout_service_binding
70
+ end
71
+ val
72
+ end
73
+
74
+ # setter
75
+ def single_logout_service_binding=(val)
76
+ @single_logout_service_binding = val
77
+ end
78
+
79
+ def get_sp_cert
80
+ cert = nil
81
+ if self.certificate
82
+ formated_cert = Samlsso::Utils.format_cert(self.certificate)
83
+ cert = OpenSSL::X509::Certificate.new(formated_cert)
84
+ end
85
+ cert
86
+ end
87
+
88
+ def get_sp_key
89
+ private_key = nil
90
+ if self.private_key
91
+ formated_private_key = Samlsso::Utils.format_private_key(self.private_key)
92
+ private_key = OpenSSL::PKey::RSA.new(formated_private_key)
93
+ end
94
+ private_key
95
+ end
96
+
97
+ private
98
+
99
+ DEFAULTS = {
100
+ :assertion_consumer_service_binding => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
101
+ :single_logout_service_binding => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
102
+ :compress_request => true,
103
+ :compress_response => true,
104
+ :security => {
105
+ :authn_requests_signed => false,
106
+ :logout_requests_signed => false,
107
+ :logout_responses_signed => false,
108
+ :embed_sign => false,
109
+ :digest_method => XMLSecurity::Document::SHA1,
110
+ :signature_method => XMLSecurity::Document::SHA1
111
+ },
112
+ :double_quote_xml_attribute_values => false,
113
+ }
114
+ end
115
+ end
@@ -0,0 +1,64 @@
1
+ require 'zlib'
2
+ require 'time'
3
+ require 'nokogiri'
4
+
5
+ # Only supports SAML 2.0
6
+ module Samlsso
7
+ class SloLogoutrequest < SamlMessage
8
+ attr_reader :options
9
+ attr_reader :request
10
+ attr_reader :document
11
+
12
+ def initialize(request, options = {})
13
+ raise ArgumentError.new("Request cannot be nil") if request.nil?
14
+ @options = options
15
+ @request = decode_raw_saml(request)
16
+ @document = REXML::Document.new(@request)
17
+ end
18
+
19
+ def is_valid?
20
+ validate
21
+ end
22
+
23
+ def validate!
24
+ validate(false)
25
+ end
26
+
27
+ # The value of the user identifier as designated by the initialization request response
28
+ def name_id
29
+ @name_id ||= begin
30
+ node = REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
31
+ node.nil? ? nil : node.text
32
+ end
33
+ end
34
+
35
+ def id
36
+ return @id if @id
37
+ element = REXML::XPath.first(document, "/p:LogoutRequest", {
38
+ "p" => PROTOCOL} )
39
+ return nil if element.nil?
40
+ return element.attributes["ID"]
41
+ end
42
+
43
+ def issuer
44
+ @issuer ||= begin
45
+ node = REXML::XPath.first(document, "/p:LogoutRequest/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
46
+ node.nil? ? nil : node.text
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def validate(soft = true)
53
+ valid_saml?(document, soft) && validate_request_state(soft)
54
+ end
55
+
56
+ def validate_request_state(soft = true)
57
+ if request.empty?
58
+ return soft ? false : validation_error("Blank request")
59
+ end
60
+ true
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,99 @@
1
+ require "uuid"
2
+
3
+ require "samlsso/logging"
4
+
5
+ module Samlsso
6
+ class SloLogoutresponse < 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, request_id = nil, logout_message = nil, params = {})
15
+ params = create_params(settings, request_id, logout_message, params)
16
+ params_prefix = (settings.idp_slo_target_url =~ /\?/) ? '&' : '?'
17
+ saml_response = CGI.escape(params.delete("SAMLResponse"))
18
+ response_params = "#{params_prefix}SAMLResponse=#{saml_response}"
19
+ params.each_pair do |key, value|
20
+ response_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
21
+ end
22
+
23
+ @logout_url = settings.idp_slo_target_url + response_params
24
+ end
25
+
26
+ def create_params(settings, request_id = nil, logout_message = nil, params = {})
27
+ params = {} if params.nil?
28
+
29
+ response_doc = create_logout_response_xml_doc(settings, request_id, logout_message)
30
+ response_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
31
+
32
+ response = ""
33
+ response_doc.write(response)
34
+
35
+ Logging.debug "Created SLO Logout Response: #{response}"
36
+
37
+ response = deflate(response) if settings.compress_response
38
+ base64_response = encode(response)
39
+ response_params = {"SAMLResponse" => base64_response}
40
+
41
+ if settings.security[:logout_responses_signed] && !settings.security[:embed_sign] && settings.private_key
42
+ params['SigAlg'] = XMLSecurity::Document::SHA1
43
+ url_string = "SAMLResponse=#{CGI.escape(base64_response)}"
44
+ url_string += "&RelayState=#{CGI.escape(params['RelayState'])}" if params['RelayState']
45
+ url_string += "&SigAlg=#{CGI.escape(params['SigAlg'])}"
46
+ private_key = settings.get_sp_key()
47
+ signature = private_key.sign(XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]).new, url_string)
48
+ params['Signature'] = encode(signature)
49
+ end
50
+
51
+ params.each_pair do |key, value|
52
+ response_params[key] = value.to_s
53
+ end
54
+
55
+ response_params
56
+ end
57
+
58
+ def create_logout_response_xml_doc(settings, request_id = nil, logout_message = nil)
59
+ time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
60
+
61
+ response_doc = XMLSecurity::Document.new
62
+ response_doc.uuid = uuid
63
+
64
+ root = response_doc.add_element 'samlp:LogoutResponse', { 'xmlns:samlp' => 'urn:oasis:names:tc:SAML:2.0:protocol', "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
65
+ root.attributes['ID'] = uuid
66
+ root.attributes['IssueInstant'] = time
67
+ root.attributes['Version'] = '2.0'
68
+ root.attributes['InResponseTo'] = request_id unless request_id.nil?
69
+ root.attributes['Destination'] = settings.idp_slo_target_url unless settings.idp_slo_target_url.nil?
70
+
71
+ # add success message
72
+ status = root.add_element 'samlp:Status'
73
+
74
+ # success status code
75
+ status_code = status.add_element 'samlp:StatusCode'
76
+ status_code.attributes['Value'] = 'urn:oasis:names:tc:SAML:2.0:status:Success'
77
+
78
+ # success status message
79
+ logout_message ||= 'Successfully Signed Out'
80
+ status_message = status.add_element 'samlp:StatusMessage'
81
+ status_message.text = logout_message
82
+
83
+ if settings.issuer != nil
84
+ issuer = root.add_element "saml:Issuer"
85
+ issuer.text = settings.issuer
86
+ end
87
+
88
+ # embebed sign
89
+ if settings.security[:logout_responses_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
90
+ private_key = settings.get_sp_key()
91
+ cert = settings.get_sp_cert()
92
+ response_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
93
+ end
94
+
95
+ response_doc
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,42 @@
1
+ module Samlsso
2
+ class Utils
3
+ def self.format_cert(cert, heads=true)
4
+ cert = cert.delete("\n").delete("\r").delete("\x0D")
5
+ if cert
6
+ cert = cert.gsub('-----BEGIN CERTIFICATE-----', '')
7
+ cert = cert.gsub('-----END CERTIFICATE-----', '')
8
+ cert = cert.gsub(' ', '')
9
+
10
+ if heads
11
+ cert = cert.scan(/.{1,64}/).join("\n")+"\n"
12
+ cert = "-----BEGIN CERTIFICATE-----\n" + cert + "-----END CERTIFICATE-----\n"
13
+ end
14
+ end
15
+ cert
16
+ end
17
+
18
+ def self.format_private_key(key, heads=true)
19
+ key = key.delete("\n").delete("\r").delete("\x0D")
20
+ if key
21
+ if key.index('-----BEGIN PRIVATE KEY-----') != nil
22
+ key = key.gsub('-----BEGIN PRIVATE KEY-----', '')
23
+ key = key.gsub('-----END PRIVATE KEY-----', '')
24
+ key = key.gsub(' ', '')
25
+ if heads
26
+ key = key.scan(/.{1,64}/).join("\n")+"\n"
27
+ key = "-----BEGIN PRIVATE KEY-----\n" + key + "-----END PRIVATE KEY-----\n"
28
+ end
29
+ else
30
+ key = key.gsub('-----BEGIN RSA PRIVATE KEY-----', '')
31
+ key = key.gsub('-----END RSA PRIVATE KEY-----', '')
32
+ key = key.gsub(' ', '')
33
+ if heads
34
+ key = key.scan(/.{1,64}/).join("\n")+"\n"
35
+ key = "-----BEGIN RSA PRIVATE KEY-----\n" + key + "-----END RSA PRIVATE KEY-----\n"
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ module Samlsso
2
+ class ValidationError < StandardError
3
+ end
4
+ end
5
+