saml2 1.1.5 → 2.0.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 +4 -4
- data/lib/saml2/assertion.rb +33 -30
- data/lib/saml2/attribute.rb +9 -2
- data/lib/saml2/attribute_consuming_service.rb +37 -5
- data/lib/saml2/authn_request.rb +0 -9
- data/lib/saml2/authn_statement.rb +14 -2
- data/lib/saml2/base.rb +25 -6
- data/lib/saml2/bindings/http_post.rb +28 -0
- data/lib/saml2/bindings/http_redirect.rb +1 -1
- data/lib/saml2/conditions.rb +27 -3
- data/lib/saml2/endpoint.rb +2 -8
- data/lib/saml2/entity.rb +16 -17
- data/lib/saml2/indexed_object.rb +7 -2
- data/lib/saml2/key.rb +5 -1
- data/lib/saml2/localized_name.rb +48 -0
- data/lib/saml2/message.rb +33 -4
- data/lib/saml2/organization.rb +15 -60
- data/lib/saml2/response.rb +16 -1
- data/lib/saml2/role.rb +3 -1
- data/lib/saml2/service_provider.rb +4 -0
- data/lib/saml2/signable.rb +69 -0
- data/lib/saml2/status.rb +5 -1
- data/lib/saml2/subject.rb +38 -3
- data/lib/saml2/version.rb +1 -1
- data/spec/fixtures/service_provider.xml +1 -1
- data/spec/lib/authn_request_spec.rb +0 -18
- data/spec/lib/bindings/http_redirect_spec.rb +14 -7
- data/spec/lib/entity_spec.rb +11 -4
- data/spec/lib/response_spec.rb +8 -2
- data/spec/lib/service_provider_spec.rb +16 -3
- metadata +5 -3
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 49552e3c1623dc97bebb250e582f14925c243a01
         | 
| 4 | 
            +
              data.tar.gz: ec92142eee88b9f0bb7b53cb747ca7c9c874ea52
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 22a08e29c00544c48402463bf5615829348abffc0ca79b823f8d594480a422af4ecef8e49cd934a6898d16c5f6dcb890b3183c6fc8d62d0796f21532d87a6778
         | 
| 7 | 
            +
              data.tar.gz: a000f59983fff2fc9c2b6385d5a2f94076f15fee45503c8b58de2c2647b112fa954503c3023ca2baf5c049e493f023e386524b8646ff7b254090eb2119f7503c
         | 
    
        data/lib/saml2/assertion.rb
    CHANGED
    
    | @@ -1,45 +1,48 @@ | |
| 1 1 | 
             
            require 'saml2/conditions'
         | 
| 2 2 |  | 
| 3 3 | 
             
            module SAML2
         | 
| 4 | 
            -
              class Assertion
         | 
| 5 | 
            -
                 | 
| 6 | 
            -
                attr_accessor :issuer, :subject
         | 
| 4 | 
            +
              class Assertion < Message
         | 
| 5 | 
            +
                attr_writer :statements, :subject
         | 
| 7 6 |  | 
| 8 7 | 
             
                def initialize
         | 
| 9 | 
            -
                   | 
| 10 | 
            -
                  @issue_instant = Time.now.utc
         | 
| 8 | 
            +
                  super
         | 
| 11 9 | 
             
                  @statements = []
         | 
| 12 10 | 
             
                  @conditions = Conditions.new
         | 
| 13 11 | 
             
                end
         | 
| 14 12 |  | 
| 15 | 
            -
                def  | 
| 16 | 
            -
                   | 
| 13 | 
            +
                def from_xml(node)
         | 
| 14 | 
            +
                  super
         | 
| 15 | 
            +
                  @conditions = nil
         | 
| 16 | 
            +
                  @statements = nil
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def subject
         | 
| 20 | 
            +
                  if xml && !instance_variable_defined?(:@subject)
         | 
| 21 | 
            +
                    @subject = Subject.from_xml(xml.at_xpath('saml:Subject', Namespaces::ALL))
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                  @subject
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def conditions
         | 
| 27 | 
            +
                  @conditions ||= Conditions.from_xml(xml.at_xpath('saml:Conditions', Namespaces::ALL))
         | 
| 28 | 
            +
                end
         | 
| 17 29 |  | 
| 18 | 
            -
             | 
| 19 | 
            -
                  @ | 
| 20 | 
            -
                  # the Signature element must be right after the Issuer, so put it there
         | 
| 21 | 
            -
                  issuer = @xml.at_xpath("saml:Issuer", Namespaces::ALL)
         | 
| 22 | 
            -
                  signature = @xml.at_xpath("dsig:Signature", Namespaces::ALL)
         | 
| 23 | 
            -
                  issuer.add_next_sibling(signature)
         | 
| 24 | 
            -
                  self
         | 
| 30 | 
            +
                def statements
         | 
| 31 | 
            +
                  @statements ||= load_object_array(xml, 'saml:AuthnStatement|saml:AttributeStatement')
         | 
| 25 32 | 
             
                end
         | 
| 26 33 |  | 
| 27 | 
            -
                def  | 
| 28 | 
            -
                   | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 39 | 
            -
                      conditions.build(assertion)
         | 
| 40 | 
            -
                      statements.each { |stmt| stmt.build(assertion) }
         | 
| 41 | 
            -
                    end
         | 
| 42 | 
            -
                  end.doc.root
         | 
| 34 | 
            +
                def build(builder)
         | 
| 35 | 
            +
                  builder['saml'].Assertion(
         | 
| 36 | 
            +
                      'xmlns:saml' => Namespaces::SAML
         | 
| 37 | 
            +
                  ) do |assertion|
         | 
| 38 | 
            +
                    super(assertion)
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    subject.build(assertion)
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                    conditions.build(assertion)
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                    statements.each { |stmt| stmt.build(assertion) }
         | 
| 45 | 
            +
                  end
         | 
| 43 46 | 
             
                end
         | 
| 44 47 | 
             
              end
         | 
| 45 48 | 
             
            end
         | 
    
        data/lib/saml2/attribute.rb
    CHANGED
    
    | @@ -32,10 +32,17 @@ module SAML2 | |
| 32 32 | 
             
                  end
         | 
| 33 33 |  | 
| 34 34 | 
             
                  def create(name, value = nil)
         | 
| 35 | 
            -
             | 
| 36 35 | 
             
                    (class_for(name) || self).new(name, value)
         | 
| 37 36 | 
             
                  end
         | 
| 38 37 |  | 
| 38 | 
            +
                  def namespace
         | 
| 39 | 
            +
                    'saml'
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  def element
         | 
| 43 | 
            +
                    'Attribute'
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
             | 
| 39 46 | 
             
                  protected
         | 
| 40 47 |  | 
| 41 48 | 
             
                  def class_for(name_or_node)
         | 
| @@ -52,7 +59,7 @@ module SAML2 | |
| 52 59 | 
             
                end
         | 
| 53 60 |  | 
| 54 61 | 
             
                def build(builder)
         | 
| 55 | 
            -
                  builder[ | 
| 62 | 
            +
                  builder[self.class.namespace].__send__(self.class.element, 'Name' => name) do |attribute|
         | 
| 56 63 | 
             
                    attribute.parent['FriendlyName'] = friendly_name if friendly_name
         | 
| 57 64 | 
             
                    attribute.parent['NameFormat'] = name_format if name_format
         | 
| 58 65 | 
             
                    Array.wrap(value).each do |value|
         | 
| @@ -2,12 +2,30 @@ require 'active_support/core_ext/array/wrap' | |
| 2 2 |  | 
| 3 3 | 
             
            require 'saml2/attribute'
         | 
| 4 4 | 
             
            require 'saml2/indexed_object'
         | 
| 5 | 
            +
            require 'saml2/localized_name'
         | 
| 5 6 | 
             
            require 'saml2/namespaces'
         | 
| 6 7 |  | 
| 7 8 | 
             
            module SAML2
         | 
| 8 9 | 
             
              class RequestedAttribute < Attribute
         | 
| 9 | 
            -
                 | 
| 10 | 
            -
                   | 
| 10 | 
            +
                class << self
         | 
| 11 | 
            +
                  def namespace
         | 
| 12 | 
            +
                    'md'
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def element
         | 
| 16 | 
            +
                    'RequestedAttribute'
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  def create(name, is_required = nil)
         | 
| 20 | 
            +
                    # use Attribute.create to get other subclasses to automatically fill out friendly_name
         | 
| 21 | 
            +
                    # and name_format, but still return a RequestedAttribute object
         | 
| 22 | 
            +
                    attribute = Attribute.create(name)
         | 
| 23 | 
            +
                    new(attribute.name, is_required, attribute.friendly_name, attribute.name_format)
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def initialize(name = nil, is_required = nil, friendly_name = nil, name_format = nil)
         | 
| 28 | 
            +
                  super(name, nil, friendly_name, name_format)
         | 
| 11 29 | 
             
                  @is_required = is_required
         | 
| 12 30 | 
             
                end
         | 
| 13 31 |  | 
| @@ -44,16 +62,19 @@ module SAML2 | |
| 44 62 | 
             
              class AttributeConsumingService < Base
         | 
| 45 63 | 
             
                include IndexedObject
         | 
| 46 64 |  | 
| 47 | 
            -
                attr_reader :name, :requested_attributes
         | 
| 65 | 
            +
                attr_reader :name, :description, :requested_attributes
         | 
| 48 66 |  | 
| 49 67 | 
             
                def initialize(name = nil, requested_attributes = [])
         | 
| 50 68 | 
             
                  super()
         | 
| 51 | 
            -
                  @name | 
| 69 | 
            +
                  @name = LocalizedName.new('ServiceName', name)
         | 
| 70 | 
            +
                  @description = LocalizedName.new('ServiceDescription')
         | 
| 71 | 
            +
                  @requested_attributes = requested_attributes
         | 
| 52 72 | 
             
                end
         | 
| 53 73 |  | 
| 54 74 | 
             
                def from_xml(node)
         | 
| 55 75 | 
             
                  super
         | 
| 56 | 
            -
                   | 
| 76 | 
            +
                  name.from_xml(node.xpath('md:ServiceName', Namespaces::ALL))
         | 
| 77 | 
            +
                  description.from_xml(node.xpath('md:ServiceDescription', Namespaces::ALL))
         | 
| 57 78 | 
             
                  @requested_attributes = load_object_array(node, "md:RequestedAttribute", RequestedAttribute)
         | 
| 58 79 | 
             
                end
         | 
| 59 80 |  | 
| @@ -97,5 +118,16 @@ module SAML2 | |
| 97 118 | 
             
                  return nil if attributes.empty?
         | 
| 98 119 | 
             
                  AttributeStatement.new(attributes)
         | 
| 99 120 | 
             
                end
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                def build(builder)
         | 
| 123 | 
            +
                  builder['md'].AttributeConsumingService do |attribute_consuming_service|
         | 
| 124 | 
            +
                    name.build(attribute_consuming_service)
         | 
| 125 | 
            +
                    description.build(attribute_consuming_service)
         | 
| 126 | 
            +
                    requested_attributes.each do |requested_attribute|
         | 
| 127 | 
            +
                      requested_attribute.build(attribute_consuming_service)
         | 
| 128 | 
            +
                    end
         | 
| 129 | 
            +
                  end
         | 
| 130 | 
            +
                  super
         | 
| 131 | 
            +
                end
         | 
| 100 132 | 
             
              end
         | 
| 101 133 | 
             
            end
         | 
    
        data/lib/saml2/authn_request.rb
    CHANGED
    
    | @@ -13,15 +13,6 @@ require 'saml2/subject' | |
| 13 13 |  | 
| 14 14 | 
             
            module SAML2
         | 
| 15 15 | 
             
              class AuthnRequest < Request
         | 
| 16 | 
            -
                # deprecated; takes _just_ the SAMLRequest parameter's value
         | 
| 17 | 
            -
                def self.decode(authnrequest)
         | 
| 18 | 
            -
                  result, _relay_state = Bindings::HTTPRedirect.decode("http://host/?SAMLRequest=#{CGI.escape(authnrequest)}")
         | 
| 19 | 
            -
                  return nil unless result.is_a?(AuthnRequest)
         | 
| 20 | 
            -
                  result
         | 
| 21 | 
            -
                rescue CorruptMessage
         | 
| 22 | 
            -
                  AuthnRequest.from_xml(Nokogiri::XML('<xml></xml>').root)
         | 
| 23 | 
            -
                end
         | 
| 24 | 
            -
             | 
| 25 16 | 
             
                attr_writer :assertion_consumer_service_index,
         | 
| 26 17 | 
             
                            :assertion_consumer_service_url,
         | 
| 27 18 | 
             
                            :attribute_consuming_service_index,
         | 
| @@ -1,5 +1,7 @@ | |
| 1 | 
            +
            require 'saml2/base'
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module SAML2
         | 
| 2 | 
            -
              class AuthnStatement
         | 
| 4 | 
            +
              class AuthnStatement < Base
         | 
| 3 5 | 
             
                module Classes
         | 
| 4 6 | 
             
                  INTERNET_PROTOCOL            = "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocol".freeze # IP address
         | 
| 5 7 | 
             
                  INTERNET_PROTOCOL_PASSWORD   = "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword".freeze # IP address, as well as username/password
         | 
| @@ -13,10 +15,20 @@ module SAML2 | |
| 13 15 | 
             
                  UNSPECIFIED                  = "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified".freeze
         | 
| 14 16 | 
             
                end
         | 
| 15 17 |  | 
| 16 | 
            -
                attr_accessor :authn_instant, :authn_context_class_ref
         | 
| 18 | 
            +
                attr_accessor :authn_instant, :authn_context_class_ref, :session_index, :session_not_on_or_after
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def from_xml(node)
         | 
| 21 | 
            +
                  super
         | 
| 22 | 
            +
                  @authn_instant = Time.parse(node['AuthnInstant'])
         | 
| 23 | 
            +
                  @session_index = node['SessionIndex']
         | 
| 24 | 
            +
                  @session_not_on_or_after = Time.parse(node['SessionNotOnOrAfter']) if node['SessionNotOnOrAfter']
         | 
| 25 | 
            +
                  @authn_context_class_ref = node.at_xpath('saml:AuthnContext/saml:AuthnContextClassRef', Namespaces::ALL)&.content&.strip
         | 
| 26 | 
            +
                end
         | 
| 17 27 |  | 
| 18 28 | 
             
                def build(builder)
         | 
| 19 29 | 
             
                  builder['saml'].AuthnStatement('AuthnInstant' => authn_instant.iso8601) do |authn_statement|
         | 
| 30 | 
            +
                    authn_statement.parent['SessionIndex'] = session_index if session_index
         | 
| 31 | 
            +
                    authn_statement.parent['SessionNotOnOrAfter'] = session_not_on_or_after.iso8601 if session_not_on_or_after
         | 
| 20 32 | 
             
                    authn_statement['saml'].AuthnContext do |authn_context|
         | 
| 21 33 | 
             
                      authn_context['saml'].AuthnContextClassRef(authn_context_class_ref) if authn_context_class_ref
         | 
| 22 34 | 
             
                    end
         | 
    
        data/lib/saml2/base.rb
    CHANGED
    
    | @@ -15,9 +15,19 @@ module SAML2 | |
| 15 15 | 
             
                  @xml = node
         | 
| 16 16 | 
             
                end
         | 
| 17 17 |  | 
| 18 | 
            -
                def to_s
         | 
| 19 | 
            -
                   | 
| 20 | 
            -
             | 
| 18 | 
            +
                def to_s(pretty: true)
         | 
| 19 | 
            +
                  if xml
         | 
| 20 | 
            +
                    xml.to_s
         | 
| 21 | 
            +
                  elsif pretty
         | 
| 22 | 
            +
                    to_xml.to_s
         | 
| 23 | 
            +
                  else
         | 
| 24 | 
            +
                    # make sure to not FORMAT it - it breaks signatures!
         | 
| 25 | 
            +
                    to_xml.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML | Nokogiri::XML::Node::SaveOptions::NO_DECLARATION)
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def inspect
         | 
| 30 | 
            +
                  "#<#{self.class.name} #{instance_variables.map { |iv| next if iv == :@xml; "#{iv}=#{instance_variable_get(iv).inspect}" }.compact.join(", ") }>"
         | 
| 21 31 | 
             
                end
         | 
| 22 32 |  | 
| 23 33 | 
             
                def to_xml
         | 
| @@ -25,19 +35,27 @@ module SAML2 | |
| 25 35 | 
             
                    builder = Nokogiri::XML::Builder.new
         | 
| 26 36 | 
             
                    build(builder)
         | 
| 27 37 | 
             
                    @document = builder.doc
         | 
| 38 | 
            +
                    # if we're re-serializing a parsed document (i.e. after mutating/parsing it),
         | 
| 39 | 
            +
                    # forget the original document we parsed
         | 
| 40 | 
            +
                    @xml = nil
         | 
| 28 41 | 
             
                  end
         | 
| 29 42 | 
             
                  @document
         | 
| 30 43 | 
             
                end
         | 
| 31 44 |  | 
| 45 | 
            +
                def build(builder)
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 32 48 | 
             
                def self.load_string_array(node, element)
         | 
| 33 49 | 
             
                  node.xpath(element, Namespaces::ALL).map do |element_node|
         | 
| 34 50 | 
             
                    element_node.content&.strip
         | 
| 35 51 | 
             
                  end
         | 
| 36 52 | 
             
                end
         | 
| 37 53 |  | 
| 38 | 
            -
                def self.load_object_array(node, element, klass)
         | 
| 54 | 
            +
                def self.load_object_array(node, element, klass = nil)
         | 
| 39 55 | 
             
                  node.xpath(element, Namespaces::ALL).map do |element_node|
         | 
| 40 | 
            -
                    if klass. | 
| 56 | 
            +
                    if klass.nil?
         | 
| 57 | 
            +
                      SAML2.const_get(element_node.name, false).from_xml(element_node)
         | 
| 58 | 
            +
                    elsif klass.is_a?(Hash)
         | 
| 41 59 | 
             
                      klass[element_node.name].from_xml(element_node)
         | 
| 42 60 | 
             
                    else
         | 
| 43 61 | 
             
                      klass.from_xml(element_node)
         | 
| @@ -51,11 +69,12 @@ module SAML2 | |
| 51 69 | 
             
                end
         | 
| 52 70 |  | 
| 53 71 | 
             
                protected
         | 
| 72 | 
            +
             | 
| 54 73 | 
             
                def load_string_array(node, element)
         | 
| 55 74 | 
             
                  self.class.load_string_array(node, element)
         | 
| 56 75 | 
             
                end
         | 
| 57 76 |  | 
| 58 | 
            -
                def load_object_array(node, element, klass)
         | 
| 77 | 
            +
                def load_object_array(node, element, klass = nil)
         | 
| 59 78 | 
             
                  self.class.load_object_array(node, element, klass)
         | 
| 60 79 | 
             
                end
         | 
| 61 80 |  | 
| @@ -1,7 +1,35 @@ | |
| 1 | 
            +
            require 'base64'
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module SAML2
         | 
| 2 4 | 
             
              module Bindings
         | 
| 3 5 | 
             
                module HTTP_POST
         | 
| 4 6 | 
             
                  URN ="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST".freeze
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  class << self
         | 
| 9 | 
            +
                    def decode(post_params)
         | 
| 10 | 
            +
                      base64 = post_params['SAMLRequest'] || post_params['SAMLResponse']
         | 
| 11 | 
            +
                      raise MissingMessage unless base64
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                      raise MessageTooLarge if base64.bytesize > SAML2.config[:max_message_size]
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                      xml = begin
         | 
| 16 | 
            +
                        Base64.decode64(base64)
         | 
| 17 | 
            +
                      rescue ArgumentError
         | 
| 18 | 
            +
                        raise CorruptMessage
         | 
| 19 | 
            +
                      end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                      message = Message.parse(xml)
         | 
| 22 | 
            +
                      [message, post_params['RelayState']]
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    def encode(message, relay_state: nil)
         | 
| 26 | 
            +
                      xml = message.to_s(pretty: false)
         | 
| 27 | 
            +
                      key = message.is_a?(Request) ? 'SAMLRequest' : 'SAMLResponse'
         | 
| 28 | 
            +
                      post_params = { key => Base64.encode64(xml) }
         | 
| 29 | 
            +
                      post_params['RelayState'] = relay_state if relay_state
         | 
| 30 | 
            +
                      post_params
         | 
| 31 | 
            +
                    end
         | 
| 32 | 
            +
                  end
         | 
| 5 33 | 
             
                end
         | 
| 6 34 | 
             
              end
         | 
| 7 35 | 
             
            end
         | 
| @@ -107,7 +107,7 @@ module SAML2 | |
| 107 107 | 
             
                        original_query.delete_if { |(k, v)| k == param }
         | 
| 108 108 | 
             
                      end
         | 
| 109 109 |  | 
| 110 | 
            -
                      xml = message.to_s
         | 
| 110 | 
            +
                      xml = message.to_s(pretty: false)
         | 
| 111 111 | 
             
                      zstream = Zlib::Deflate.new(Zlib::BEST_COMPRESSION, -Zlib::MAX_WBITS)
         | 
| 112 112 | 
             
                      deflated = zstream.deflate(xml, Zlib::FINISH)
         | 
| 113 113 | 
             
                      zstream.close
         | 
    
        data/lib/saml2/conditions.rb
    CHANGED
    
    | @@ -3,6 +3,21 @@ require 'active_support/core_ext/array/wrap' | |
| 3 3 | 
             
            module SAML2
         | 
| 4 4 | 
             
              class Conditions < Array
         | 
| 5 5 | 
             
                attr_accessor :not_before, :not_on_or_after
         | 
| 6 | 
            +
                attr_reader :xml
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def self.from_xml(node)
         | 
| 9 | 
            +
                  result = new
         | 
| 10 | 
            +
                  result.from_xml(node)
         | 
| 11 | 
            +
                  result
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def from_xml(node)
         | 
| 15 | 
            +
                  @xml = node
         | 
| 16 | 
            +
                  @not_before = Time.parse(node['NotBefore']) if node['NotBefore']
         | 
| 17 | 
            +
                  @not_on_or_after = Time.parse(node['NotOnOrAfter']) if node['NotOnOrAfter']
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  replace(node.children.map { |restriction| self.class.const_get(restriction.name, false).from_xml(restriction) })
         | 
| 20 | 
            +
                end
         | 
| 6 21 |  | 
| 7 22 | 
             
                def valid?(options = {})
         | 
| 8 23 | 
             
                  now = options[:now] || Time.now
         | 
| @@ -37,19 +52,28 @@ module SAML2 | |
| 37 52 | 
             
                end
         | 
| 38 53 |  | 
| 39 54 | 
             
                # Any unknown condition
         | 
| 40 | 
            -
                class Condition
         | 
| 55 | 
            +
                class Condition < Base
         | 
| 41 56 | 
             
                  def valid?(_)
         | 
| 42 57 | 
             
                    :indeterminate
         | 
| 43 58 | 
             
                  end
         | 
| 44 59 | 
             
                end
         | 
| 45 60 |  | 
| 46 61 | 
             
                class AudienceRestriction < Condition
         | 
| 47 | 
            -
                   | 
| 62 | 
            +
                  attr_writer :audience
         | 
| 48 63 |  | 
| 49 | 
            -
                  def initialize(audience)
         | 
| 64 | 
            +
                  def initialize(audience = [])
         | 
| 50 65 | 
             
                    @audience = audience
         | 
| 51 66 | 
             
                  end
         | 
| 52 67 |  | 
| 68 | 
            +
                  def from_xml(node)
         | 
| 69 | 
            +
                    super
         | 
| 70 | 
            +
                    @audience = nil
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                  def audience
         | 
| 74 | 
            +
                    @audience ||= load_string_array(xml, 'saml:Audience')
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
             | 
| 53 77 | 
             
                  def valid?(options)
         | 
| 54 78 | 
             
                    Array.wrap(audience).include?(options[:audience]) ? :valid : :invalid
         | 
| 55 79 | 
             
                  end
         | 
    
        data/lib/saml2/endpoint.rb
    CHANGED
    
    | @@ -1,16 +1,10 @@ | |
| 1 | 
            -
            require 'saml2/bindings/http_redirect'
         | 
| 2 1 | 
             
            require 'saml2/bindings/http_post'
         | 
| 3 2 |  | 
| 4 3 | 
             
            module SAML2
         | 
| 5 4 | 
             
              class Endpoint < Base
         | 
| 6 | 
            -
                module Bindings
         | 
| 7 | 
            -
                  HTTP_POST     = ::SAML2::Bindings::HTTP_POST::URN
         | 
| 8 | 
            -
                  HTTP_REDIRECT = ::SAML2::Bindings::HTTPRedirect::URN
         | 
| 9 | 
            -
                end
         | 
| 10 | 
            -
             | 
| 11 5 | 
             
                attr_reader :location, :binding
         | 
| 12 6 |  | 
| 13 | 
            -
                def initialize(location = nil, binding =  | 
| 7 | 
            +
                def initialize(location = nil, binding = Bindings::HTTP_POST::URN)
         | 
| 14 8 | 
             
                  @location, @binding = location, binding
         | 
| 15 9 | 
             
                end
         | 
| 16 10 |  | 
| @@ -31,7 +25,7 @@ module SAML2 | |
| 31 25 | 
             
                class Indexed < Endpoint
         | 
| 32 26 | 
             
                  include IndexedObject
         | 
| 33 27 |  | 
| 34 | 
            -
                  def initialize(location = nil, index = nil, is_default = nil, binding =  | 
| 28 | 
            +
                  def initialize(location = nil, index = nil, is_default = nil, binding = Bindings::HTTP_POST::URN)
         | 
| 35 29 | 
             
                    super(location, binding)
         | 
| 36 30 | 
             
                    @index, @is_default = index, is_default
         | 
| 37 31 | 
             
                  end
         | 
    
        data/lib/saml2/entity.rb
    CHANGED
    
    | @@ -4,10 +4,12 @@ require 'saml2/base' | |
| 4 4 | 
             
            require 'saml2/identity_provider'
         | 
| 5 5 | 
             
            require 'saml2/organization_and_contacts'
         | 
| 6 6 | 
             
            require 'saml2/service_provider'
         | 
| 7 | 
            +
            require 'saml2/signable'
         | 
| 7 8 |  | 
| 8 9 | 
             
            module SAML2
         | 
| 9 10 | 
             
              class Entity < Base
         | 
| 10 11 | 
             
                include OrganizationAndContacts
         | 
| 12 | 
            +
                include Signable
         | 
| 11 13 |  | 
| 12 14 | 
             
                attr_writer :entity_id
         | 
| 13 15 |  | 
| @@ -28,6 +30,8 @@ module SAML2 | |
| 28 30 |  | 
| 29 31 | 
             
                class Group < Base
         | 
| 30 32 | 
             
                  include Enumerable
         | 
| 33 | 
            +
                  include Signable
         | 
| 34 | 
            +
             | 
| 31 35 | 
             
                  [:each, :[]].each do |method|
         | 
| 32 36 | 
             
                    class_eval <<-RUBY, __FILE__, __LINE__ + 1
         | 
| 33 37 | 
             
                      def #{method}(*args, &block)
         | 
| @@ -38,11 +42,13 @@ module SAML2 | |
| 38 42 |  | 
| 39 43 | 
             
                  def initialize
         | 
| 40 44 | 
             
                    @entities = []
         | 
| 45 | 
            +
                    @id = "_#{SecureRandom.uuid}"
         | 
| 41 46 | 
             
                    @valid_until = nil
         | 
| 42 47 | 
             
                  end
         | 
| 43 48 |  | 
| 44 49 | 
             
                  def from_xml(node)
         | 
| 45 50 | 
             
                    super
         | 
| 51 | 
            +
                    @id = nil
         | 
| 46 52 | 
             
                    remove_instance_variable(:@valid_until)
         | 
| 47 53 | 
             
                    @entities = Base.load_object_array(xml, "md:EntityDescriptor|md:EntitiesDescriptor",
         | 
| 48 54 | 
             
                            'EntityDescriptor' => Entity,
         | 
| @@ -53,23 +59,8 @@ module SAML2 | |
| 53 59 | 
             
                    Schemas.federation.valid?(xml.document)
         | 
| 54 60 | 
             
                  end
         | 
| 55 61 |  | 
| 56 | 
            -
                  def  | 
| 57 | 
            -
                     | 
| 58 | 
            -
                      @signature = xml.at_xpath('dsig:Signature', Namespaces::ALL)
         | 
| 59 | 
            -
                      signed_node = @signature.at_xpath('dsig:SignedInfo/dsig:Reference', Namespaces::ALL)['URI']
         | 
| 60 | 
            -
                      # validating the schema will automatically add ID attributes, so check that first
         | 
| 61 | 
            -
                      xml.set_id_attribute('ID') unless xml.document.get_id(xml['ID'])
         | 
| 62 | 
            -
                      @signature = nil unless signed_node == "##{xml['ID']}"
         | 
| 63 | 
            -
                    end
         | 
| 64 | 
            -
                    @signature
         | 
| 65 | 
            -
                  end
         | 
| 66 | 
            -
             | 
| 67 | 
            -
                  def signed?
         | 
| 68 | 
            -
                    !!signature
         | 
| 69 | 
            -
                  end
         | 
| 70 | 
            -
             | 
| 71 | 
            -
                  def valid_signature?(*args)
         | 
| 72 | 
            -
                    signature.verify_with(*args)
         | 
| 62 | 
            +
                  def id
         | 
| 63 | 
            +
                    @id ||= xml['ID']
         | 
| 73 64 | 
             
                  end
         | 
| 74 65 |  | 
| 75 66 | 
             
                  def valid_until
         | 
| @@ -85,10 +76,12 @@ module SAML2 | |
| 85 76 | 
             
                  @valid_until = nil
         | 
| 86 77 | 
             
                  @entity_id = nil
         | 
| 87 78 | 
             
                  @roles = []
         | 
| 79 | 
            +
                  @id = "_#{SecureRandom.uuid}"
         | 
| 88 80 | 
             
                end
         | 
| 89 81 |  | 
| 90 82 | 
             
                def from_xml(node)
         | 
| 91 83 | 
             
                  super
         | 
| 84 | 
            +
                  @id = nil
         | 
| 92 85 | 
             
                  remove_instance_variable(:@valid_until)
         | 
| 93 86 | 
             
                  @roles = nil
         | 
| 94 87 | 
             
                end
         | 
| @@ -101,6 +94,10 @@ module SAML2 | |
| 101 94 | 
             
                  @entity_id || xml && xml['entityID']
         | 
| 102 95 | 
             
                end
         | 
| 103 96 |  | 
| 97 | 
            +
                def id
         | 
| 98 | 
            +
                  @id ||= xml['ID']
         | 
| 99 | 
            +
                end
         | 
| 100 | 
            +
             | 
| 104 101 | 
             
                def valid_until
         | 
| 105 102 | 
             
                  unless instance_variable_defined?(:@valid_until)
         | 
| 106 103 | 
             
                    @valid_until = xml['validUntil'] && Time.parse(xml['validUntil'])
         | 
| @@ -126,6 +123,8 @@ module SAML2 | |
| 126 123 | 
             
                                                 'xmlns:md' => Namespaces::METADATA,
         | 
| 127 124 | 
             
                                                 'xmlns:dsig' => Namespaces::DSIG,
         | 
| 128 125 | 
             
                                                 'xmlns:xenc' => Namespaces::XENC) do |entity_descriptor|
         | 
| 126 | 
            +
                    entity_descriptor.parent['ID'] = id if id
         | 
| 127 | 
            +
             | 
| 129 128 | 
             
                    roles.each do |role|
         | 
| 130 129 | 
             
                      role.build(entity_descriptor)
         | 
| 131 130 | 
             
                    end
         | 
    
        data/lib/saml2/indexed_object.rb
    CHANGED
    
    | @@ -2,7 +2,7 @@ require 'saml2/base' | |
| 2 2 |  | 
| 3 3 | 
             
            module SAML2
         | 
| 4 4 | 
             
              module IndexedObject
         | 
| 5 | 
            -
                 | 
| 5 | 
            +
                attr_accessor :index
         | 
| 6 6 |  | 
| 7 7 | 
             
                def initialize(*args)
         | 
| 8 8 | 
             
                  @is_default = nil
         | 
| @@ -57,8 +57,13 @@ module SAML2 | |
| 57 57 | 
             
                  protected
         | 
| 58 58 |  | 
| 59 59 | 
             
                  def re_index
         | 
| 60 | 
            +
                    last_index = -1
         | 
| 60 61 | 
             
                    @index = {}
         | 
| 61 | 
            -
                    each  | 
| 62 | 
            +
                    each do |object|
         | 
| 63 | 
            +
                      object.index ||= last_index + 1
         | 
| 64 | 
            +
                      last_index = object.index
         | 
| 65 | 
            +
                      @index[object.index] = object
         | 
| 66 | 
            +
                    end
         | 
| 62 67 | 
             
                    @default = find { |object| object.default? } || first
         | 
| 63 68 | 
             
                  end
         | 
| 64 69 | 
             
                end
         | 
    
        data/lib/saml2/key.rb
    CHANGED
    
    | @@ -51,8 +51,12 @@ module SAML2 | |
| 51 51 | 
             
                  @certificate ||= OpenSSL::X509::Certificate.new(Base64.decode64(x509))
         | 
| 52 52 | 
             
                end
         | 
| 53 53 |  | 
| 54 | 
            +
                def self.format_fingerprint(fingerprint)
         | 
| 55 | 
            +
                  fingerprint.downcase.gsub(/(\h{2})(?=\h)/, '\1:')
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 54 58 | 
             
                def fingerprint
         | 
| 55 | 
            -
                  @fingerprint ||= Digest::SHA1.hexdigest(certificate.to_der) | 
| 59 | 
            +
                  @fingerprint ||= self.class.format_fingerprint(Digest::SHA1.hexdigest(certificate.to_der))
         | 
| 56 60 | 
             
                end
         | 
| 57 61 |  | 
| 58 62 | 
             
                def build(builder)
         | 
| @@ -0,0 +1,48 @@ | |
| 1 | 
            +
            require 'saml2/base'
         | 
| 2 | 
            +
            require 'saml2/namespaces'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module SAML2
         | 
| 5 | 
            +
              class LocalizedName < Hash
         | 
| 6 | 
            +
                attr_reader :element
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def initialize(element, name = nil)
         | 
| 9 | 
            +
                  @element = element
         | 
| 10 | 
            +
                  unless name.nil?
         | 
| 11 | 
            +
                    if name.is_a?(Hash)
         | 
| 12 | 
            +
                      replace(name)
         | 
| 13 | 
            +
                    else
         | 
| 14 | 
            +
                      self[nil] = name
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def [](lang)
         | 
| 20 | 
            +
                  case lang
         | 
| 21 | 
            +
                  when :all
         | 
| 22 | 
            +
                    self
         | 
| 23 | 
            +
                  when nil
         | 
| 24 | 
            +
                    !empty? && first.last
         | 
| 25 | 
            +
                  else
         | 
| 26 | 
            +
                    super(lang.to_sym)
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def to_s
         | 
| 31 | 
            +
                  self[nil].to_s
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def from_xml(nodes)
         | 
| 35 | 
            +
                  clear
         | 
| 36 | 
            +
                  nodes.each do |node|
         | 
| 37 | 
            +
                    self[node['xml:lang'].to_sym] = node.content && node.content.strip
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
                  self
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def build(builder)
         | 
| 43 | 
            +
                  each do |lang, value|
         | 
| 44 | 
            +
                    builder['md'].__send__(element, value, 'xml:lang' => lang)
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
              end
         | 
| 48 | 
            +
            end
         | 
    
        data/lib/saml2/message.rb
    CHANGED
    
    | @@ -2,6 +2,7 @@ require 'securerandom' | |
| 2 2 | 
             
            require 'time'
         | 
| 3 3 |  | 
| 4 4 | 
             
            require 'saml2/base'
         | 
| 5 | 
            +
            require 'saml2/signable'
         | 
| 5 6 |  | 
| 6 7 | 
             
            module SAML2
         | 
| 7 8 | 
             
              class InvalidMessage < RuntimeError
         | 
| @@ -38,8 +39,9 @@ module SAML2 | |
| 38 39 | 
             
              # ancestor, but they have several things in common so it's useful to represent
         | 
| 39 40 | 
             
              # that here
         | 
| 40 41 | 
             
              class Message < Base
         | 
| 41 | 
            -
                 | 
| 42 | 
            -
             | 
| 42 | 
            +
                include Signable
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                attr_writer :issuer, :destination
         | 
| 43 45 |  | 
| 44 46 | 
             
                class << self
         | 
| 45 47 | 
             
                  def inherited(klass)
         | 
| @@ -86,14 +88,41 @@ module SAML2 | |
| 86 88 | 
             
                  true
         | 
| 87 89 | 
             
                end
         | 
| 88 90 |  | 
| 89 | 
            -
                def  | 
| 90 | 
            -
                   | 
| 91 | 
            +
                def validate_signature(fingerprint: nil, cert: nil, verification_time: nil)
         | 
| 92 | 
            +
                  # verify the signature (certificate's validity) as of the time the message was generated
         | 
| 93 | 
            +
                  super(fingerprint: fingerprint, cert: cert, verification_time: issue_instant)
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                def sign(x509_certificate, private_key, algorithm_name = :sha256)
         | 
| 97 | 
            +
                  super
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                  xml = @document.root
         | 
| 100 | 
            +
                  # the Signature element must be right after the Issuer, so put it there
         | 
| 101 | 
            +
                  issuer = xml.at_xpath("saml:Issuer", Namespaces::ALL)
         | 
| 102 | 
            +
                  signature = xml.at_xpath("dsig:Signature", Namespaces::ALL)
         | 
| 103 | 
            +
                  issuer.add_next_sibling(signature)
         | 
| 104 | 
            +
                  self
         | 
| 91 105 | 
             
                end
         | 
| 92 106 |  | 
| 93 107 | 
             
                def id
         | 
| 94 108 | 
             
                  @id ||= xml['ID']
         | 
| 95 109 | 
             
                end
         | 
| 96 110 |  | 
| 111 | 
            +
                def issue_instant
         | 
| 112 | 
            +
                  @issue_instant ||= Time.parse(xml['IssueInstant'])
         | 
| 113 | 
            +
                end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                def destination
         | 
| 116 | 
            +
                  if xml && !instance_variable_defined?(:@destination)
         | 
| 117 | 
            +
                    @destination = xml['Destination']
         | 
| 118 | 
            +
                  end
         | 
| 119 | 
            +
                  @destination
         | 
| 120 | 
            +
                end
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                def issuer
         | 
| 123 | 
            +
                  @issuer ||= NameID.from_xml(xml.at_xpath('saml:Issuer', Namespaces::ALL))
         | 
| 124 | 
            +
                end
         | 
| 125 | 
            +
             | 
| 97 126 | 
             
                protected
         | 
| 98 127 |  | 
| 99 128 | 
             
                # should be called from inside the specific request element
         | 
    
        data/lib/saml2/organization.rb
    CHANGED
    
    | @@ -1,73 +1,28 @@ | |
| 1 | 
            +
            require 'saml2/base'
         | 
| 2 | 
            +
            require 'saml2/localized_name'
         | 
| 1 3 | 
             
            require 'saml2/namespaces'
         | 
| 2 4 |  | 
| 3 5 | 
             
            module SAML2
         | 
| 4 | 
            -
              class Organization
         | 
| 5 | 
            -
                 | 
| 6 | 
            -
                  return nil unless node
         | 
| 6 | 
            +
              class Organization < Base
         | 
| 7 | 
            +
                attr_reader :name, :display_name, :url
         | 
| 7 8 |  | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 9 | 
            +
                def from_xml(node)
         | 
| 10 | 
            +
                  name.from_xml(node.xpath('md:OrganizationName', Namespaces::ALL))
         | 
| 11 | 
            +
                  display_name.from_xml(node.xpath('md:OrganizationDisplayName', Namespaces::ALL))
         | 
| 12 | 
            +
                  url.from_xml(node.xpath('md:OrganizationURL', Namespaces::ALL))
         | 
| 11 13 | 
             
                end
         | 
| 12 14 |  | 
| 13 | 
            -
                def initialize(name, display_name, url)
         | 
| 14 | 
            -
                   | 
| 15 | 
            -
             | 
| 16 | 
            -
                   | 
| 17 | 
            -
                  if !display_name.is_a?(Hash)
         | 
| 18 | 
            -
                    display_name = { nil => display_name }
         | 
| 19 | 
            -
                  end
         | 
| 20 | 
            -
                  if !url.is_a?(Hash)
         | 
| 21 | 
            -
                    url = { nil => url }
         | 
| 22 | 
            -
                  end
         | 
| 23 | 
            -
             | 
| 24 | 
            -
                  @name, @display_name, @url = name, display_name, url
         | 
| 25 | 
            -
                end
         | 
| 26 | 
            -
             | 
| 27 | 
            -
                def name(lang = nil)
         | 
| 28 | 
            -
                  self.class.localized_name(@name, lang)
         | 
| 29 | 
            -
                end
         | 
| 30 | 
            -
             | 
| 31 | 
            -
                def display_name(lang = nil)
         | 
| 32 | 
            -
                  self.class.localized_name(@display_name, lang)
         | 
| 33 | 
            -
                end
         | 
| 34 | 
            -
             | 
| 35 | 
            -
                def url(lang = nil)
         | 
| 36 | 
            -
                  self.class.localized_name(@url, lang)
         | 
| 15 | 
            +
                def initialize(name = nil, display_name = nil, url = nil)
         | 
| 16 | 
            +
                  @name = LocalizedName.new('OrganizationName', name)
         | 
| 17 | 
            +
                  @display_name = LocalizedName.new('OrganizationDisplayName', display_name)
         | 
| 18 | 
            +
                  @url = LocalizedName.new('OrganizationURL', url)
         | 
| 37 19 | 
             
                end
         | 
| 38 20 |  | 
| 39 21 | 
             
                def build(builder)
         | 
| 40 22 | 
             
                  builder['md'].Organization do |organization|
         | 
| 41 | 
            -
                     | 
| 42 | 
            -
                     | 
| 43 | 
            -
                     | 
| 44 | 
            -
                  end
         | 
| 45 | 
            -
                end
         | 
| 46 | 
            -
             | 
| 47 | 
            -
                private
         | 
| 48 | 
            -
             | 
| 49 | 
            -
                def self.build(builder, hash, element)
         | 
| 50 | 
            -
                  hash.each do |lang, value|
         | 
| 51 | 
            -
                    builder['md'].__send__(element, value, 'xml:lang' => lang)
         | 
| 52 | 
            -
                  end
         | 
| 53 | 
            -
                end
         | 
| 54 | 
            -
             | 
| 55 | 
            -
                def self.nodes_to_hash(nodes)
         | 
| 56 | 
            -
                  hash = {}
         | 
| 57 | 
            -
                  nodes.each do |node|
         | 
| 58 | 
            -
                    hash[node['xml:lang'].to_sym] = node.content && node.content.strip
         | 
| 59 | 
            -
                  end
         | 
| 60 | 
            -
                  hash
         | 
| 61 | 
            -
                end
         | 
| 62 | 
            -
             | 
| 63 | 
            -
                def self.localized_name(hash, lang)
         | 
| 64 | 
            -
                  case lang
         | 
| 65 | 
            -
                    when :all
         | 
| 66 | 
            -
                      hash
         | 
| 67 | 
            -
                    when nil
         | 
| 68 | 
            -
                      !hash.empty? && hash.first.last
         | 
| 69 | 
            -
                    else
         | 
| 70 | 
            -
                      hash[lang.to_sym]
         | 
| 23 | 
            +
                    @name.build(organization)
         | 
| 24 | 
            +
                    @display_name.build(organization)
         | 
| 25 | 
            +
                    @url.build(organization)
         | 
| 71 26 | 
             
                  end
         | 
| 72 27 | 
             
                end
         | 
| 73 28 | 
             
              end
         | 
    
        data/lib/saml2/response.rb
    CHANGED
    
    | @@ -57,6 +57,18 @@ module SAML2 | |
| 57 57 | 
             
                  @assertions = []
         | 
| 58 58 | 
             
                end
         | 
| 59 59 |  | 
| 60 | 
            +
                def from_xml(node)
         | 
| 61 | 
            +
                  super
         | 
| 62 | 
            +
                  remove_instance_variable(:@assertions)
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                def assertions
         | 
| 66 | 
            +
                  unless instance_variable_defined?(:@assertions)
         | 
| 67 | 
            +
                    @assertions = load_object_array(xml, 'saml:Assertion', Assertion)
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
                  @assertions
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
             | 
| 60 72 | 
             
                def sign(*args)
         | 
| 61 73 | 
             
                  assertions.each { |assertion| assertion.sign(*args) }
         | 
| 62 74 | 
             
                end
         | 
| @@ -71,7 +83,10 @@ module SAML2 | |
| 71 83 | 
             
                    super(response)
         | 
| 72 84 |  | 
| 73 85 | 
             
                    assertions.each do |assertion|
         | 
| 74 | 
            -
                       | 
| 86 | 
            +
                      # we can't just call build, because it may already
         | 
| 87 | 
            +
                      # be signed as a separate message, so call to_xml to
         | 
| 88 | 
            +
                      # get the cached signed result
         | 
| 89 | 
            +
                      response.parent << assertion.to_xml.root
         | 
| 75 90 | 
             
                    end
         | 
| 76 91 | 
             
                  end
         | 
| 77 92 | 
             
                end
         | 
    
        data/lib/saml2/role.rb
    CHANGED
    
    | @@ -1,8 +1,9 @@ | |
| 1 1 | 
             
            require 'set'
         | 
| 2 2 |  | 
| 3 3 | 
             
            require 'saml2/base'
         | 
| 4 | 
            -
            require 'saml2/organization_and_contacts'
         | 
| 5 4 | 
             
            require 'saml2/key'
         | 
| 5 | 
            +
            require 'saml2/organization_and_contacts'
         | 
| 6 | 
            +
            require 'saml2/signable'
         | 
| 6 7 |  | 
| 7 8 | 
             
            module SAML2
         | 
| 8 9 | 
             
              class Role < Base
         | 
| @@ -11,6 +12,7 @@ module SAML2 | |
| 11 12 | 
             
                end
         | 
| 12 13 |  | 
| 13 14 | 
             
                include OrganizationAndContacts
         | 
| 15 | 
            +
                include Signable
         | 
| 14 16 |  | 
| 15 17 | 
             
                attr_writer :supported_protocols, :keys
         | 
| 16 18 |  | 
| @@ -0,0 +1,69 @@ | |
| 1 | 
            +
            require 'saml2/key'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module SAML2
         | 
| 4 | 
            +
              module Signable
         | 
| 5 | 
            +
                def signature
         | 
| 6 | 
            +
                  unless instance_variable_defined?(:@signature)
         | 
| 7 | 
            +
                    @signature = xml.at_xpath('dsig:Signature', Namespaces::ALL)
         | 
| 8 | 
            +
                    if @signature
         | 
| 9 | 
            +
                      signed_node = @signature.at_xpath('dsig:SignedInfo/dsig:Reference', Namespaces::ALL)['URI']
         | 
| 10 | 
            +
                      if signed_node == ''
         | 
| 11 | 
            +
                        @signature = nil unless xml == xml.document.root
         | 
| 12 | 
            +
                      elsif signed_node != "##{xml['ID']}"
         | 
| 13 | 
            +
                        # validating the schema will automatically add ID attributes, so check that first
         | 
| 14 | 
            +
                        xml.set_id_attribute('ID') unless xml.document.get_id(xml['ID'])
         | 
| 15 | 
            +
                        @signature = nil
         | 
| 16 | 
            +
                      end
         | 
| 17 | 
            +
                    end
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
                  @signature
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def signing_key
         | 
| 23 | 
            +
                  @signing_key ||= Key.from_xml(signature)
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def signed?
         | 
| 27 | 
            +
                  !!signature
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def validate_signature(fingerprint: nil, cert: nil, verification_time: nil)
         | 
| 31 | 
            +
                  return ["not signed"] unless signed?
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  certs = Array(cert)
         | 
| 34 | 
            +
                  # see if any given fingerprints match the certificate embedded in the XML;
         | 
| 35 | 
            +
                  # if so, extract the certificate, and add it to the allowed certificates list
         | 
| 36 | 
            +
                  Array(fingerprint)&.each do |fp|
         | 
| 37 | 
            +
                    certs << signing_key.certificate if signing_key&.fingerprint == Key.format_fingerprint(fp)
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
                  certs = certs.uniq
         | 
| 40 | 
            +
                  return ["no certificate found"] if certs.empty?
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  begin
         | 
| 43 | 
            +
                    # verify_certificates being false is hopefully a temporary thing, until I can figure
         | 
| 44 | 
            +
                    # out how to get xmlsec to root a trust chain in a non-root certificate
         | 
| 45 | 
            +
                    result = signature.verify_with(certs: certs, verification_time: verification_time, verify_certificates: false)
         | 
| 46 | 
            +
                    result ? [] : ["signature does not match"]
         | 
| 47 | 
            +
                  rescue XMLSec::VerificationError => e
         | 
| 48 | 
            +
                    [e.message]
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                def valid_signature?(fingerprint: nil, cert: nil, verification_time: nil)
         | 
| 53 | 
            +
                  validate_signature(fingerprint: fingerprint, cert: cert, verification_time: verification_time).empty?
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def sign(x509_certificate, private_key, algorithm_name = :sha256)
         | 
| 57 | 
            +
                  to_xml
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  xml = @document.root
         | 
| 60 | 
            +
                  xml.set_id_attribute('ID')
         | 
| 61 | 
            +
                  xml.sign!(cert: x509_certificate, key: private_key, digest_alg: algorithm_name.to_s, signature_alg: "rsa-#{algorithm_name}", uri: "##{id}")
         | 
| 62 | 
            +
                  # the Signature element must be the first element
         | 
| 63 | 
            +
                  signature = xml.at_xpath("dsig:Signature", Namespaces::ALL)
         | 
| 64 | 
            +
                  xml.children.first.add_previous_sibling(signature)
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  self
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
              end
         | 
| 69 | 
            +
            end
         | 
    
        data/lib/saml2/status.rb
    CHANGED
    
    | @@ -2,7 +2,7 @@ require 'saml2/base' | |
| 2 2 |  | 
| 3 3 | 
             
            module SAML2
         | 
| 4 4 | 
             
              class Status < Base
         | 
| 5 | 
            -
                SUCCESS | 
| 5 | 
            +
                SUCCESS      = "urn:oasis:names:tc:SAML:2.0:status:Success".freeze
         | 
| 6 6 |  | 
| 7 7 | 
             
                attr_accessor :code, :message
         | 
| 8 8 |  | 
| @@ -16,6 +16,10 @@ module SAML2 | |
| 16 16 | 
             
                  self.message = load_string_array(xml, 'samlp:StatusMessage')
         | 
| 17 17 | 
             
                end
         | 
| 18 18 |  | 
| 19 | 
            +
                def success?
         | 
| 20 | 
            +
                  code == SUCCESS
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 19 23 | 
             
                def build(builder)
         | 
| 20 24 | 
             
                  builder['samlp'].Status do |status|
         | 
| 21 25 | 
             
                    status['samlp'].StatusCode(Value: code)
         | 
    
        data/lib/saml2/subject.rb
    CHANGED
    
    | @@ -4,19 +4,42 @@ require 'saml2/namespaces' | |
| 4 4 | 
             
            module SAML2
         | 
| 5 5 | 
             
              class Subject < Base
         | 
| 6 6 | 
             
                attr_writer :name_id
         | 
| 7 | 
            -
                 | 
| 7 | 
            +
                attr_writer :confirmations
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def initialize
         | 
| 10 | 
            +
                  @confirmations = []
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def from_xml(node)
         | 
| 14 | 
            +
                  super
         | 
| 15 | 
            +
                  @confirmations = nil
         | 
| 16 | 
            +
                end
         | 
| 8 17 |  | 
| 9 18 | 
             
                def name_id
         | 
| 10 19 | 
             
                  if xml && !instance_variable_defined?(:@name_id)
         | 
| 11 | 
            -
                    @name_id = NameID.from_xml( | 
| 20 | 
            +
                    @name_id = NameID.from_xml(xml.at_xpath('saml:NameID', Namespaces::ALL))
         | 
| 12 21 | 
             
                  end
         | 
| 13 22 | 
             
                  @name_id
         | 
| 14 23 | 
             
                end
         | 
| 15 24 |  | 
| 25 | 
            +
                def confirmation
         | 
| 26 | 
            +
                  Array.wrap(confirmations).first
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def confirmation=(value)
         | 
| 30 | 
            +
                  @confirmations = [value]
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def confirmations
         | 
| 34 | 
            +
                  @confirmations ||= load_object_array(xml, 'saml:SubjectConfirmation', Confirmation)
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 16 37 | 
             
                def build(builder)
         | 
| 17 38 | 
             
                  builder['saml'].Subject do |subject|
         | 
| 18 39 | 
             
                    name_id.build(subject) if name_id
         | 
| 19 | 
            -
                     | 
| 40 | 
            +
                    Array(confirmations).each do |confirmation|
         | 
| 41 | 
            +
                      confirmation.build(subject)
         | 
| 42 | 
            +
                    end
         | 
| 20 43 | 
             
                  end
         | 
| 21 44 | 
             
                end
         | 
| 22 45 |  | 
| @@ -29,6 +52,18 @@ module SAML2 | |
| 29 52 |  | 
| 30 53 | 
             
                  attr_accessor :method, :not_before, :not_on_or_after, :recipient, :in_response_to
         | 
| 31 54 |  | 
| 55 | 
            +
                  def from_xml(node)
         | 
| 56 | 
            +
                    super
         | 
| 57 | 
            +
                    self.method = node['Method']
         | 
| 58 | 
            +
                    confirmation_data = node.at_xpath('saml:SubjectConfirmationData', Namespaces::ALL)
         | 
| 59 | 
            +
                    if confirmation_data
         | 
| 60 | 
            +
                      self.not_before = Time.parse(confirmation_data['NotBefore']) if confirmation_data['NotBefore']
         | 
| 61 | 
            +
                      self.not_on_or_after = Time.parse(confirmation_data['NotOnOrAfter']) if confirmation_data['NotOnOrAfter']
         | 
| 62 | 
            +
                      self.recipient = confirmation_data['Recipient']
         | 
| 63 | 
            +
                      self.in_response_to = confirmation_data['InResponseTo']
         | 
| 64 | 
            +
                    end
         | 
| 65 | 
            +
                  end
         | 
| 66 | 
            +
             | 
| 32 67 | 
             
                  def build(builder)
         | 
| 33 68 | 
             
                    builder['saml'].SubjectConfirmation('Method' => method) do |subject_confirmation|
         | 
| 34 69 | 
             
                      if in_response_to ||
         | 
    
        data/lib/saml2/version.rb
    CHANGED
    
    
| @@ -1,5 +1,5 @@ | |
| 1 1 | 
             
            <?xml version="1.0"?>
         | 
| 2 | 
            -
            <EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://siteadmin.instructure.com/saml2">
         | 
| 2 | 
            +
            <EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://siteadmin.instructure.com/saml2" ID="unique">
         | 
| 3 3 | 
             
              <SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
         | 
| 4 4 |  | 
| 5 5 | 
             
                <KeyDescriptor use="encryption">
         | 
| @@ -5,24 +5,6 @@ module SAML2 | |
| 5 5 | 
             
                let(:sp) { Entity.parse(fixture('service_provider.xml')).roles.first }
         | 
| 6 6 | 
             
                let(:request) { AuthnRequest.parse(fixture('authnrequest.xml')) }
         | 
| 7 7 |  | 
| 8 | 
            -
                describe '.decode' do
         | 
| 9 | 
            -
                  it "should not choke on empty string" do
         | 
| 10 | 
            -
                    authnrequest = AuthnRequest.decode('')
         | 
| 11 | 
            -
                    expect(authnrequest.valid_schema?).to eq false
         | 
| 12 | 
            -
                  end
         | 
| 13 | 
            -
             | 
| 14 | 
            -
                  it "should not choke on garbage" do
         | 
| 15 | 
            -
                    authnrequest = AuthnRequest.decode('abc')
         | 
| 16 | 
            -
                    expect(authnrequest.valid_schema?).to eq false
         | 
| 17 | 
            -
                  end
         | 
| 18 | 
            -
             | 
| 19 | 
            -
                  it "properly handles authnrequests that have pluses in them" do
         | 
| 20 | 
            -
                    samlrequest = "hZJbU8IwEIX/Smbfe6H1mqE4COPIDGoHqg++hXShmWkTzKao/95QQNEHfN09J2f32/RvPpqabdCSMjqDXhgDQy1NqfQqg+fiLriCm0GfRFMnaz5sXaVn+NYiOeaNmviuk0FrNTeCFHEtGiTuJJ8PH6Y8CWO+tsYZaWpgQyK0zkeNjKa2QTtHu1ESn2fTDCrn1sSjSJqmabVyn6EUeiOobij0tWgbFREZYGOfr7Rw3cwHm+/8MWwHSKKpWSkN7M5Yid0CGSxFTQhsMs5ApCotqzKRWEmxqha91VVVxvIMy1TGl8qLKBdEaoM/NqIWJ5qc0C6DJO5dBvFFkJwVvXOepDy9DtPr+BVYvl/7VukdzlOMFjsR8fuiyIP8aV4AezmcxQtgfwTepdtj+qcfFgfkMPgHcD86Tvg++qN/cjLOTa3kJxvWtXkfWRTO83C2xQ5sI9zpIbYVVQbLTsrX273IoXYQDfapvz/X4As="
         | 
| 21 | 
            -
                    authnrequest = AuthnRequest.decode(samlrequest)
         | 
| 22 | 
            -
                    expect(authnrequest.valid_schema?).to eq true
         | 
| 23 | 
            -
                  end
         | 
| 24 | 
            -
                end
         | 
| 25 | 
            -
             | 
| 26 8 | 
             
                it "should be valid" do
         | 
| 27 9 | 
             
                  expect(request.valid_schema?).to eq true
         | 
| 28 10 | 
             
                  expect(request.resolve(sp)).to eq true
         | 
| @@ -29,8 +29,9 @@ module SAML2 | |
| 29 29 | 
             
                  end
         | 
| 30 30 |  | 
| 31 31 | 
             
                  it "doesn't allow deflate bombs" do
         | 
| 32 | 
            -
                    message =  | 
| 32 | 
            +
                    message = double()
         | 
| 33 33 | 
             
                    allow(message).to receive(:destination).and_return("http://somewhere/")
         | 
| 34 | 
            +
                    allow(message).to receive(:to_s).and_return("\0" * 2 * 1024 * 1024)
         | 
| 34 35 | 
             
                    url = Bindings::HTTPRedirect.encode(message)
         | 
| 35 36 |  | 
| 36 37 | 
             
                    expect { Bindings::HTTPRedirect.decode(url) }.to raise_error(MessageTooLarge)
         | 
| @@ -47,16 +48,18 @@ module SAML2 | |
| 47 48 | 
             
                  end
         | 
| 48 49 |  | 
| 49 50 | 
             
                  it "validates encoding" do
         | 
| 50 | 
            -
                    message =  | 
| 51 | 
            +
                    message = double()
         | 
| 51 52 | 
             
                    allow(message).to receive(:destination).and_return("http://somewhere/")
         | 
| 53 | 
            +
                    allow(message).to receive(:to_s).and_return("hi")
         | 
| 52 54 | 
             
                    url = Bindings::HTTPRedirect.encode(message, relay_state: "abc")
         | 
| 53 55 | 
             
                    url << "&SAMLEncoding=garbage"
         | 
| 54 56 | 
             
                    expect { Bindings::HTTPRedirect.decode(url) }.to raise_error(UnsupportedEncoding)
         | 
| 55 57 | 
             
                  end
         | 
| 56 58 |  | 
| 57 59 | 
             
                  it "returns relay state" do
         | 
| 58 | 
            -
                    message =  | 
| 60 | 
            +
                    message = double()
         | 
| 59 61 | 
             
                    allow(message).to receive(:destination).and_return("http://somewhere/")
         | 
| 62 | 
            +
                    allow(message).to receive(:to_s).and_return("hi")
         | 
| 60 63 | 
             
                    url = Bindings::HTTPRedirect.encode(message, relay_state: "abc")
         | 
| 61 64 | 
             
                    allow(Message).to receive(:parse).with("hi").and_return("parsed")
         | 
| 62 65 | 
             
                    message, relay_state = Bindings::HTTPRedirect.decode(url)
         | 
| @@ -88,8 +91,9 @@ module SAML2 | |
| 88 91 | 
             
                    end
         | 
| 89 92 |  | 
| 90 93 | 
             
                    it "allows the caller to detect an unsigned message" do
         | 
| 91 | 
            -
                      message =  | 
| 94 | 
            +
                      message = double()
         | 
| 92 95 | 
             
                      allow(message).to receive(:destination).and_return("http://somewhere/")
         | 
| 96 | 
            +
                      allow(message).to receive(:to_s).and_return("hi")
         | 
| 93 97 | 
             
                      url = Bindings::HTTPRedirect.encode(message)
         | 
| 94 98 | 
             
                      allow(Message).to receive(:parse).with("hi").and_return("parsed")
         | 
| 95 99 |  | 
| @@ -102,8 +106,9 @@ module SAML2 | |
| 102 106 | 
             
                    end
         | 
| 103 107 |  | 
| 104 108 | 
             
                    it "requires a signature if a key is passed" do
         | 
| 105 | 
            -
                      message =  | 
| 109 | 
            +
                      message = double()
         | 
| 106 110 | 
             
                      allow(message).to receive(:destination).and_return("http://somewhere/")
         | 
| 111 | 
            +
                      allow(message).to receive(:to_s).and_return("hi")
         | 
| 107 112 | 
             
                      url = Bindings::HTTPRedirect.encode(message)
         | 
| 108 113 | 
             
                      allow(Message).to receive(:parse).with("hi").and_return("parsed")
         | 
| 109 114 |  | 
| @@ -127,15 +132,17 @@ module SAML2 | |
| 127 132 |  | 
| 128 133 | 
             
                describe '.encode' do
         | 
| 129 134 | 
             
                  it 'works' do
         | 
| 130 | 
            -
                    message =  | 
| 135 | 
            +
                    message = double()
         | 
| 131 136 | 
             
                    allow(message).to receive(:destination).and_return("http://somewhere/")
         | 
| 137 | 
            +
                    allow(message).to receive(:to_s).and_return("hi")
         | 
| 132 138 | 
             
                    url = Bindings::HTTPRedirect.encode(message, relay_state: "abc")
         | 
| 133 139 | 
             
                    expect(url).to match(%r{^http://somewhere/\?SAMLResponse=(?:.*)&RelayState=abc})
         | 
| 134 140 | 
             
                  end
         | 
| 135 141 |  | 
| 136 142 | 
             
                  it 'signs a message' do
         | 
| 137 | 
            -
                    message =  | 
| 143 | 
            +
                    message = double()
         | 
| 138 144 | 
             
                    allow(message).to receive(:destination).and_return("http://somewhere/")
         | 
| 145 | 
            +
                    allow(message).to receive(:to_s).and_return("hi")
         | 
| 139 146 | 
             
                    key = OpenSSL::PKey::RSA.new(fixture('privatekey.key'))
         | 
| 140 147 | 
             
                    url = Bindings::HTTPRedirect.encode(message, relay_state: "abc", private_key: key)
         | 
| 141 148 |  | 
    
        data/spec/lib/entity_spec.rb
    CHANGED
    
    | @@ -25,10 +25,10 @@ module SAML2 | |
| 25 25 | 
             
                  end
         | 
| 26 26 |  | 
| 27 27 | 
             
                  it "should parse the organization" do
         | 
| 28 | 
            -
                    expect(entity.organization.display_name).to eq 'Canvas'
         | 
| 29 | 
            -
                    expect(entity.organization.display_name | 
| 30 | 
            -
                    expect(entity.organization.display_name | 
| 31 | 
            -
                    expect(entity.organization.display_name | 
| 28 | 
            +
                    expect(entity.organization.display_name.to_s).to eq 'Canvas'
         | 
| 29 | 
            +
                    expect(entity.organization.display_name['en']).to eq 'Canvas'
         | 
| 30 | 
            +
                    expect(entity.organization.display_name['es']).to be_nil
         | 
| 31 | 
            +
                    expect(entity.organization.display_name[:all]).to eq en: 'Canvas'
         | 
| 32 32 | 
             
                  end
         | 
| 33 33 |  | 
| 34 34 | 
             
                  it "validates metadata from ADFS containing lots of non-SAML schemas" do
         | 
| @@ -36,6 +36,13 @@ module SAML2 | |
| 36 36 | 
             
                  end
         | 
| 37 37 | 
             
                end
         | 
| 38 38 |  | 
| 39 | 
            +
                it "should sign correctly" do
         | 
| 40 | 
            +
                  entity = Entity.parse(fixture('service_provider.xml'))
         | 
| 41 | 
            +
                  entity.sign(fixture('certificate.pem'), fixture('privatekey.key'))
         | 
| 42 | 
            +
                  entity2 = Entity.parse(entity.to_s)
         | 
| 43 | 
            +
                  expect(entity2.valid_schema?).to eq true
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 39 46 | 
             
                describe Entity::Group do
         | 
| 40 47 | 
             
                  it "should parse and validate" do
         | 
| 41 48 | 
             
                    group = Entity.parse(fixture('entities.xml'))
         | 
    
        data/spec/lib/response_spec.rb
    CHANGED
    
    | @@ -42,14 +42,14 @@ module SAML2 | |
| 42 42 | 
             
                  expect(Schemas.protocol.validate(response.to_xml)).to eq []
         | 
| 43 43 | 
             
                  # verifiable on the command line with:
         | 
| 44 44 | 
             
                  # xmlsec1 --verify --pubkey-cert-pem certificate.pem --privkey-pem privatekey.key --id-attr:ID urn:oasis:names:tc:SAML:2.0:assertion:Assertion response_signed.xml
         | 
| 45 | 
            -
                  expect(response.to_s).to eq fixture('response_signed.xml')
         | 
| 45 | 
            +
                  expect(response.to_s(pretty: false)).to eq fixture('response_signed.xml')
         | 
| 46 46 | 
             
                end
         | 
| 47 47 |  | 
| 48 48 | 
             
                it "should generate a valid signature when attributes are present" do
         | 
| 49 49 | 
             
                  freeze_response
         | 
| 50 50 | 
             
                  response.assertions.first.statements << sp.attribute_consuming_services.default.create_statement('givenName' => 'cody')
         | 
| 51 51 | 
             
                  response.sign(fixture('certificate.pem'), fixture('privatekey.key'))
         | 
| 52 | 
            -
                  expect(response.to_s).to eq fixture('response_with_attribute_signed.xml')
         | 
| 52 | 
            +
                  expect(response.to_s(pretty: false)).to eq fixture('response_with_attribute_signed.xml')
         | 
| 53 53 | 
             
                end
         | 
| 54 54 |  | 
| 55 55 | 
             
                it "should generate valid XML for IdP initiated response" do
         | 
| @@ -57,5 +57,11 @@ module SAML2 | |
| 57 57 | 
             
                                          NameID.new('jacob', NameID::Format::PERSISTENT))
         | 
| 58 58 | 
             
                  expect(Schemas.protocol.validate(Nokogiri::XML(response.to_s))).to eq []
         | 
| 59 59 | 
             
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                it "parses a serialized assertion" do
         | 
| 62 | 
            +
                  response2 = Message.parse(response.to_s)
         | 
| 63 | 
            +
                  expect(response2.assertions.length).to eq 1
         | 
| 64 | 
            +
                  expect(response2.assertions.first.subject.name_id.id).to eq 'jacob'
         | 
| 65 | 
            +
                end
         | 
| 60 66 | 
             
              end
         | 
| 61 67 | 
             
            end
         | 
| @@ -14,10 +14,14 @@ module SAML2 | |
| 14 14 | 
             
                  sp = ServiceProvider.new
         | 
| 15 15 | 
             
                  sp.single_logout_services << Endpoint.new('https://sso.canvaslms.com/SAML2/Logout',
         | 
| 16 16 | 
             
                                                             Bindings::HTTPRedirect::URN)
         | 
| 17 | 
            -
                  sp.assertion_consumer_services << Endpoint::Indexed.new('https://sso.canvaslms.com/SAML2/Login1' | 
| 18 | 
            -
                  sp.assertion_consumer_services << Endpoint::Indexed.new('https://sso.canvaslms.com/SAML2/Login2' | 
| 17 | 
            +
                  sp.assertion_consumer_services << Endpoint::Indexed.new('https://sso.canvaslms.com/SAML2/Login1')
         | 
| 18 | 
            +
                  sp.assertion_consumer_services << Endpoint::Indexed.new('https://sso.canvaslms.com/SAML2/Login2')
         | 
| 19 19 | 
             
                  sp.keys << Key.new('somedata', Key::Type::ENCRYPTION, [Key::EncryptionMethod.new])
         | 
| 20 20 | 
             
                  sp.keys << Key.new('somedata', Key::Type::SIGNING)
         | 
| 21 | 
            +
                  acs = AttributeConsumingService.new
         | 
| 22 | 
            +
                  acs.name[:en] = 'service'
         | 
| 23 | 
            +
                  acs.requested_attributes << RequestedAttribute.create('uid')
         | 
| 24 | 
            +
                  sp.attribute_consuming_services << acs
         | 
| 21 25 |  | 
| 22 26 | 
             
                  entity.roles << sp
         | 
| 23 27 | 
             
                  expect(Schemas.metadata.validate(Nokogiri::XML(entity.to_s))).to eq []
         | 
| @@ -38,7 +42,7 @@ module SAML2 | |
| 38 42 | 
             
                  end
         | 
| 39 43 |  | 
| 40 44 | 
             
                  it "should load the organization" do
         | 
| 41 | 
            -
                    expect(entity.organization.display_name).to eq 'Canvas'
         | 
| 45 | 
            +
                    expect(entity.organization.display_name.to_s).to eq 'Canvas'
         | 
| 42 46 | 
             
                  end
         | 
| 43 47 |  | 
| 44 48 | 
             
                  it "should load contacts" do
         | 
| @@ -46,6 +50,15 @@ module SAML2 | |
| 46 50 | 
             
                    expect(entity.contacts.first.type).to eq Contact::Type::TECHNICAL
         | 
| 47 51 | 
             
                    expect(entity.contacts.first.surname).to eq 'Administrator'
         | 
| 48 52 | 
             
                  end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  it "loads attribute_consuming_services" do
         | 
| 55 | 
            +
                    expect(sp.attribute_consuming_services.length).to eq 1
         | 
| 56 | 
            +
                    acs = sp.attribute_consuming_services.first
         | 
| 57 | 
            +
                    expect(acs.index).to eq 0
         | 
| 58 | 
            +
                    expect(acs.name.to_s).to eq 'service'
         | 
| 59 | 
            +
                    expect(acs.requested_attributes.length).to eq 1
         | 
| 60 | 
            +
                    expect(acs.requested_attributes.first.name).to eq 'urn:oid:2.5.4.42'
         | 
| 61 | 
            +
                  end
         | 
| 49 62 | 
             
                end
         | 
| 50 63 | 
             
              end
         | 
| 51 64 | 
             
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: saml2
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version:  | 
| 4 | 
            +
              version: 2.0.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Cody Cutrer
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2017- | 
| 11 | 
            +
            date: 2017-12-19 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: nokogiri
         | 
| @@ -147,6 +147,7 @@ files: | |
| 147 147 | 
             
            - lib/saml2/identity_provider.rb
         | 
| 148 148 | 
             
            - lib/saml2/indexed_object.rb
         | 
| 149 149 | 
             
            - lib/saml2/key.rb
         | 
| 150 | 
            +
            - lib/saml2/localized_name.rb
         | 
| 150 151 | 
             
            - lib/saml2/logout_request.rb
         | 
| 151 152 | 
             
            - lib/saml2/logout_response.rb
         | 
| 152 153 | 
             
            - lib/saml2/message.rb
         | 
| @@ -160,6 +161,7 @@ files: | |
| 160 161 | 
             
            - lib/saml2/role.rb
         | 
| 161 162 | 
             
            - lib/saml2/schemas.rb
         | 
| 162 163 | 
             
            - lib/saml2/service_provider.rb
         | 
| 164 | 
            +
            - lib/saml2/signable.rb
         | 
| 163 165 | 
             
            - lib/saml2/sso.rb
         | 
| 164 166 | 
             
            - lib/saml2/status.rb
         | 
| 165 167 | 
             
            - lib/saml2/status_response.rb
         | 
| @@ -221,7 +223,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 221 223 | 
             
                  version: '0'
         | 
| 222 224 | 
             
            requirements: []
         | 
| 223 225 | 
             
            rubyforge_project: 
         | 
| 224 | 
            -
            rubygems_version: 2. | 
| 226 | 
            +
            rubygems_version: 2.6.12
         | 
| 225 227 | 
             
            signing_key: 
         | 
| 226 228 | 
             
            specification_version: 4
         | 
| 227 229 | 
             
            summary: SAML 2.0 Library
         |