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.
- checksums.yaml +7 -0
 - data/.gitignore +50 -0
 - data/CODE_OF_CONDUCT.md +49 -0
 - data/Gemfile +4 -0
 - data/LICENSE +21 -0
 - data/README.md +36 -0
 - data/Rakefile +2 -0
 - data/bin/console +14 -0
 - data/bin/setup +8 -0
 - data/lib/samlsso.rb +16 -0
 - data/lib/samlsso/attribute_service.rb +32 -0
 - data/lib/samlsso/attributes.rb +107 -0
 - data/lib/samlsso/authrequest.rb +124 -0
 - data/lib/samlsso/idp_metadata_parser.rb +85 -0
 - data/lib/samlsso/logging.rb +20 -0
 - data/lib/samlsso/logoutrequest.rb +100 -0
 - data/lib/samlsso/logoutresponse.rb +110 -0
 - data/lib/samlsso/metadata.rb +94 -0
 - data/lib/samlsso/response.rb +271 -0
 - data/lib/samlsso/saml_message.rb +117 -0
 - data/lib/samlsso/settings.rb +115 -0
 - data/lib/samlsso/slo_logoutrequest.rb +64 -0
 - data/lib/samlsso/slo_logoutresponse.rb +99 -0
 - data/lib/samlsso/utils.rb +42 -0
 - data/lib/samlsso/validation_error.rb +5 -0
 - data/lib/samlsso/version.rb +3 -0
 - data/lib/schemas/saml-schema-assertion-2.0.xsd +283 -0
 - data/lib/schemas/saml-schema-authn-context-2.0.xsd +23 -0
 - data/lib/schemas/saml-schema-authn-context-types-2.0.xsd +821 -0
 - data/lib/schemas/saml-schema-metadata-2.0.xsd +339 -0
 - data/lib/schemas/saml-schema-protocol-2.0.xsd +302 -0
 - data/lib/schemas/sstc-metadata-attr.xsd +35 -0
 - data/lib/schemas/sstc-saml-attribute-ext.xsd +25 -0
 - data/lib/schemas/sstc-saml-metadata-algsupport-v1.0.xsd +41 -0
 - data/lib/schemas/sstc-saml-metadata-ui-v1.0.xsd +89 -0
 - data/lib/schemas/xenc-schema.xsd +136 -0
 - data/lib/schemas/xml.xsd +287 -0
 - data/lib/schemas/xmldsig-core-schema.xsd +309 -0
 - data/lib/xml_security.rb +276 -0
 - data/samlsso.gemspec +44 -0
 - 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
         
     |