Commit 341d0b3
Changed files (5)
spec
saml
lib/saml/kit/authentication_request.rb
@@ -3,13 +3,13 @@ module Saml
class AuthenticationRequest
PROTOCOL_XSD = File.expand_path("./xsd/saml-schema-protocol-2.0.xsd", File.dirname(__FILE__)).freeze
include XsdValidatable
-
include ActiveModel::Validations
+
validates_presence_of :content
validates_presence_of :acs_url, if: :login_request?
validate :must_be_request
validate :must_have_valid_signature
- validate :must_be_registered_service_provider
+ validate :must_be_registered
validate :must_match_xsd
attr_reader :content, :name
@@ -51,11 +51,11 @@ module Saml
private
def registered_acs_url
- acs_urls = service_provider.assertion_consumer_services
+ acs_urls = provider.assertion_consumer_services
return acs_urls.first[:location] if acs_urls.any?
end
- def service_provider
+ def provider
registry.metadata_for(issuer)
end
@@ -63,9 +63,9 @@ module Saml
Saml::Kit.configuration.registry
end
- def must_be_registered_service_provider
+ def must_be_registered
return unless login_request?
- return if service_provider.matches?(fingerprint, use: "signing")
+ return if provider.matches?(fingerprint, use: "signing")
errors[:base] << error_message(:invalid)
end
lib/saml/kit/metadata.rb
@@ -62,6 +62,15 @@ module Saml
end
end
+ def matches?(fingerprint, use: :signing)
+ if :signing == use
+ sha256 = fingerprint.algorithm(OpenSSL::Digest::SHA256)
+ signing_certificates.find do |signing_certificate|
+ sha256 == signing_certificate[:fingerprint]
+ end
+ end
+ end
+
def to_xml
@xml
end
lib/saml/kit/response.rb
@@ -1,13 +1,29 @@
module Saml
module Kit
class Response
+ PROTOCOL_XSD = File.expand_path("./xsd/saml-schema-protocol-2.0.xsd", File.dirname(__FILE__)).freeze
+ include ActiveModel::Validations
+ include XsdValidatable
+
+ attr_reader :content, :name
+ validates_presence_of :content
+ validate :must_have_valid_signature
+ validate :must_be_response
+ validate :must_be_registered
+ validate :must_match_xsd
+
def initialize(xml)
- @xml = xml
+ @content = xml
@xml_hash = Hash.from_xml(xml)
+ @name = 'Response'
end
def name_id
- @xml_hash['Response']['Assertion']['Subject']['NameID']
+ @xml_hash[name]['Assertion']['Subject']['NameID']
+ end
+
+ def issuer
+ @xml_hash[name]['Issuer']
end
def [](key)
@@ -15,29 +31,80 @@ module Saml
end
def attributes
- @attributes ||= Hash[@xml_hash['Response']['Assertion']['AttributeStatement']['Attribute'].map do |item|
+ @attributes ||= Hash[@xml_hash[name]['Assertion']['AttributeStatement']['Attribute'].map do |item|
[item['Name'].to_sym, item['AttributeValue']]
end].with_indifferent_access
end
def acs_url
- @xml_hash['Response']['Destination']
+ @xml_hash[name]['Destination']
end
def to_xml
- @xml
+ content
end
def encode
Base64.strict_encode64(to_xml)
end
+ def certificate
+ @xml_hash[name]['Signature']['KeyInfo']['X509Data']['X509Certificate']
+ end
+
+ def fingerprint
+ Fingerprint.new(certificate)
+ end
+
def self.parse(saml_response)
new(Base64.decode64(saml_response))
end
+ private
+
+ def provider
+ registry.metadata_for(issuer)
+ end
+
+ def registry
+ Saml::Kit.configuration.registry
+ end
+
+ def must_have_valid_signature
+ return if to_xml.blank?
+
+ xml = Saml::Kit::Xml.new(to_xml)
+ xml.valid?
+ xml.errors.each do |error|
+ errors[:base] << error
+ end
+ end
+
+ def must_be_response
+ return if to_xml.blank?
+
+ errors[:base] << error_message(:invalid) unless login_response?
+ end
+
+ def must_be_registered
+ return unless login_response?
+ return if provider.present? && provider.matches?(fingerprint, use: "signing")
+
+ errors[:base] << error_message(:invalid)
+ end
+
+ def must_match_xsd
+ matches_xsd?(PROTOCOL_XSD)
+ end
+
+ def login_response?
+ return false if to_xml.blank?
+ @xml_hash[name].present?
+ end
+
class Builder
- attr_reader :user, :request, :id, :reference_id, :now, :name_id_format
+ attr_reader :user, :request
+ attr_accessor :id, :reference_id, :now, :name_id_format
def initialize(user, request)
@user = user
@@ -51,11 +118,11 @@ module Saml
def to_xml
signature = Signature.new(id)
xml = ::Builder::XmlMarkup.new
- xml.tag!("samlp:Response", response_options) do
- signature.template(xml)
+ xml.Response response_options do
xml.Issuer(configuration.issuer, xmlns: Namespaces::ASSERTION)
- xml.tag!("samlp:Status") do
- xml.tag!('samlp:StatusCode', Value: Namespaces::SUCCESS)
+ signature.template(xml)
+ xml.Status do
+ xml.StatusCode Value: Namespaces::SUCCESS
end
xml.Assertion(assertion_options) do
xml.Issuer configuration.issuer
@@ -105,7 +172,7 @@ module Saml
Destination: request.acs_url,
Consent: Namespaces::UNSPECIFIED,
InResponseTo: request.id,
- "xmlns:samlp" => Namespaces::PROTOCOL,
+ xmlns: Namespaces::PROTOCOL,
}
end
@@ -114,6 +181,7 @@ module Saml
ID: "_#{reference_id}",
IssueInstant: now.iso8601,
Version: "2.0",
+ xmlns: Namespaces::ASSERTION,
}
end
lib/saml/kit/service_provider_metadata.rb
@@ -14,15 +14,6 @@ module Saml
end
end
- def matches?(fingerprint, use: :signing)
- if :signing == use
- sha256 = fingerprint.algorithm(OpenSSL::Digest::SHA256)
- signing_certificates.find do |signing_certificate|
- sha256 == signing_certificate[:fingerprint]
- end
- end
- end
-
private
class Builder
spec/saml/response_spec.rb
@@ -137,4 +137,62 @@ RSpec.describe Saml::Kit::Response do
expect(result['Response']['Assertion']['AttributeStatement']['Attribute'][0]['AttributeValue']).to eql("ea64c235-e18d-4b9a-8672-06ef84dabdec")
end
end
+
+ describe "#valid?" do
+ let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: "_#{SecureRandom.uuid}", issuer: FFaker::Internet.http_url, acs_url: FFaker::Internet.http_url) }
+ let(:user) { double(:user, uuid: SecureRandom.uuid, assertion_attributes_for: { id: SecureRandom.uuid }) }
+ let(:builder) { described_class::Builder.new(user, request) }
+ let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
+ let(:metadata) { instance_double(Saml::Kit::IdentityProviderMetadata) }
+
+ before :each do
+ allow(Saml::Kit.configuration).to receive(:registry).and_return(registry)
+ end
+
+ it 'is valid' do
+ allow(registry).to receive(:metadata_for).and_return(metadata)
+ allow(metadata).to receive(:matches?).and_return(true)
+ expect(builder.build).to be_valid
+ end
+
+ it 'is invalid when blank' do
+ expect(described_class.new("")).to be_invalid
+ end
+
+ it 'is invalid if the document has been tampered with' do
+ allow(registry).to receive(:metadata_for).and_return(metadata)
+ allow(metadata).to receive(:matches?).and_return(true)
+ name_id_format = Saml::Kit::Namespaces::PERSISTENT
+ builder.name_id_format = name_id_format
+ subject = described_class.new(builder.to_xml.gsub(name_id_format, Saml::Kit::Namespaces::EMAIL_ADDRESS))
+ expect(subject).to_not be_valid
+ end
+
+ it 'is invalid when not a Response' do
+ xml = Saml::Kit::IdentityProviderMetadata::Builder.new.to_xml
+ expect(described_class.new(xml)).to be_invalid
+ end
+
+ it 'is invalid when the fingerprint of the certificate does not match the registered fingerprint' do
+ allow(registry).to receive(:metadata_for).and_return(metadata)
+ allow(metadata).to receive(:matches?).and_return(false)
+ expect(described_class.new(builder.to_xml)).to be_invalid
+ end
+
+ it 'validates the schema of the response' do
+ allow(registry).to receive(:metadata_for).and_return(metadata)
+ allow(metadata).to receive(:matches?).and_return(true)
+ xml = ::Builder::XmlMarkup.new
+ id = SecureRandom.uuid
+ options = { "xmlns:samlp" => Saml::Kit::Namespaces::PROTOCOL, ID: "_#{id}", }
+ signature = Saml::Kit::Signature.new(id)
+ xml.tag!("samlp:Response", options) do
+ signature.template(xml)
+ xml.Fake do
+ xml.NotAllowed "Huh?"
+ end
+ end
+ expect(described_class.new(signature.finalize(xml))).to be_invalid
+ end
+ end
end