diff --git a/README.md b/README.md
index 7d70b75c..d8c48dd7 100644
--- a/README.md
+++ b/README.md
@@ -24,9 +24,13 @@ Add this to your Gemfile:
Include `SamlIdp::Controller` and see the examples that use rails. It should be straightforward for you.
-Basically you call `decode_request(params[:SAMLRequest])` on an incoming request and then use the value
-`saml_acs_url` to determine the source for which you need to authenticate a user. How you authenticate
-a user is entirely up to you.
+Basically, you call `decode_request(params[:SAMLRequest])` on an incoming request and then use the value
+`saml_acs_url` to determine the source for which you need to authenticate a user.
+If the signature (`Signature`) and signing algorithm (`SigAlg`) are provided as external parameters in the request,
+you can pass those parameters as `decode_request(params[:SAMLRequest], params[:Signature], params[:SigAlg], params[:RelayState])`.
+Then, you can verify the request signature with the `valid?` method.
+
+How you authenticate a user is entirely up to you.
Once a user has successfully authenticated on your system send the Service Provider a SAMLResponse by
posting to `saml_acs_url` the parameter `SAMLResponse` with the return value from a call to
diff --git a/lib/saml_idp.rb b/lib/saml_idp.rb
index d654fe89..543111a3 100644
--- a/lib/saml_idp.rb
+++ b/lib/saml_idp.rb
@@ -70,9 +70,9 @@ def signed?
!!xpath("//ds:Signature", ds: signature_namespace).first
end
- def valid_signature?(fingerprint)
+ def valid_signature?(certificate, fingerprint)
signed? &&
- signed_document.validate(fingerprint, :soft)
+ signed_document.validate(certificate, fingerprint, :soft)
end
def signed_document
diff --git a/lib/saml_idp/controller.rb b/lib/saml_idp/controller.rb
index e2bf25e7..d5bd20c9 100644
--- a/lib/saml_idp/controller.rb
+++ b/lib/saml_idp/controller.rb
@@ -33,15 +33,21 @@ def acs_url
end
def validate_saml_request(raw_saml_request = params[:SAMLRequest])
- decode_request(raw_saml_request)
+ decode_request(raw_saml_request, params[:Signature], params[:SigAlg], params[:RelayState])
return true if valid_saml_request?
head :forbidden if defined?(::Rails)
false
end
- def decode_request(raw_saml_request)
- @saml_request = Request.from_deflated_request(raw_saml_request)
+ def decode_request(raw_saml_request, signature, sig_algorithm, relay_state)
+ @saml_request = Request.from_deflated_request(
+ raw_saml_request,
+ saml_request: raw_saml_request,
+ signature: signature,
+ sig_algorithm: sig_algorithm,
+ relay_state: relay_state
+ )
end
def authn_context_classref
diff --git a/lib/saml_idp/request.rb b/lib/saml_idp/request.rb
index 4b8b891f..eb651145 100644
--- a/lib/saml_idp/request.rb
+++ b/lib/saml_idp/request.rb
@@ -3,7 +3,7 @@
require 'logger'
module SamlIdp
class Request
- def self.from_deflated_request(raw)
+ def self.from_deflated_request(raw, external_attributes = {})
if raw
decoded = Base64.decode64(raw)
zstream = Zlib::Inflate.new(-Zlib::MAX_WBITS)
@@ -18,18 +18,22 @@ def self.from_deflated_request(raw)
else
inflated = ""
end
- new(inflated)
+ new(inflated, external_attributes)
end
- attr_accessor :raw_xml
+ attr_accessor :raw_xml, :saml_request, :signature, :sig_algorithm, :relay_state
delegate :config, to: :SamlIdp
private :config
delegate :xpath, to: :document
private :xpath
- def initialize(raw_xml = "")
+ def initialize(raw_xml = "", external_attributes = {})
self.raw_xml = raw_xml
+ self.saml_request = external_attributes[:saml_request]
+ self.relay_state = external_attributes[:relay_state]
+ self.sig_algorithm = external_attributes[:sig_algorithm]
+ self.signature = external_attributes[:signature]
end
def logout_request?
@@ -85,7 +89,7 @@ def log(msg)
end
end
- def valid?
+ def valid?(external_attributes = {})
unless service_provider?
log "Unable to find service provider for issuer #{issuer}"
return false
@@ -96,8 +100,15 @@ def valid?
return false
end
- unless valid_signature?
- log "Signature is invalid in #{raw_xml}"
+ # XML embedded signature
+ if signature.nil? && !valid_signature?
+ log "Requested document signature is invalid in #{raw_xml}"
+ return false
+ end
+
+ # URI query signature
+ if signature.present? && !valid_external_signature?
+ log "Requested URI signature is invalid in #{raw_xml}"
return false
end
@@ -120,12 +131,29 @@ def valid_signature?
# Validate signature when metadata specify AuthnRequest should be signed
metadata = service_provider.current_metadata
if logout_request? || authn_request? && metadata.respond_to?(:sign_authn_request?) && metadata.sign_authn_request?
- document.valid_signature?(service_provider.fingerprint)
+ document.valid_signature?(service_provider.cert, service_provider.fingerprint)
else
true
end
end
+ def valid_external_signature?
+ cert = OpenSSL::X509::Certificate.new(service_provider.cert)
+
+ sha_version = sig_algorithm =~ /sha(.*?)$/i && $1.to_i
+ raw_signature = Base64.decode64(signature)
+
+ signature_algorithm = case sha_version
+ when 256 then OpenSSL::Digest::SHA256
+ when 384 then OpenSSL::Digest::SHA384
+ when 512 then OpenSSL::Digest::SHA512
+ else
+ OpenSSL::Digest::SHA1
+ end
+
+ cert.public_key.verify(signature_algorithm.new, raw_signature, query_request_string)
+ end
+
def service_provider?
service_provider && service_provider.valid?
end
@@ -148,6 +176,13 @@ def session_index
@_session_index ||= xpath("//samlp:SessionIndex", samlp: samlp).first.try(:content)
end
+ def query_request_string
+ url_string = "SAMLRequest=#{CGI.escape(saml_request)}"
+ url_string << "&RelayState=#{CGI.escape(relay_state)}" if relay_state
+ url_string << "&SigAlg=#{CGI.escape(sig_algorithm)}"
+ end
+ private :query_request_string
+
def response_host
uri = URI(response_url)
if uri
diff --git a/lib/saml_idp/xml_security.rb b/lib/saml_idp/xml_security.rb
index 640c0348..0a223ad2 100644
--- a/lib/saml_idp/xml_security.rb
+++ b/lib/saml_idp/xml_security.rb
@@ -43,24 +43,29 @@ def initialize(response)
extract_signed_element_id
end
- def validate(idp_cert_fingerprint, soft = true)
+ def validate(idp_base64_cert, idp_cert_fingerprint, soft = true)
# get cert from response
cert_element = REXML::XPath.first(self, "//ds:X509Certificate", { "ds"=>DSIG })
- raise ValidationError.new("Certificate element missing in response (ds:X509Certificate)") unless cert_element
- base64_cert = cert_element.text
- cert_text = Base64.decode64(base64_cert)
- cert = OpenSSL::X509::Certificate.new(cert_text)
-
- # check cert matches registered idp cert
- fingerprint = fingerprint_cert(cert)
- sha1_fingerprint = fingerprint_cert_sha1(cert)
- plain_idp_cert_fingerprint = idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
-
- if fingerprint != plain_idp_cert_fingerprint && sha1_fingerprint != plain_idp_cert_fingerprint
- return soft ? false : (raise ValidationError.new("Fingerprint mismatch"))
+ if cert_element
+ idp_base64_cert = cert_element.text
+ cert_text = Base64.decode64(idp_base64_cert)
+ cert = OpenSSL::X509::Certificate.new(cert_text)
+
+ # check cert matches registered idp cert
+ fingerprint = fingerprint_cert(cert)
+ sha1_fingerprint = fingerprint_cert_sha1(cert)
+ plain_idp_cert_fingerprint = idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
+
+ if fingerprint != plain_idp_cert_fingerprint && sha1_fingerprint != plain_idp_cert_fingerprint
+ return soft ? false : (raise ValidationError.new("Fingerprint mismatch"))
+ end
+ end
+
+ if idp_base64_cert.nil? || idp_base64_cert.empty?
+ raise ValidationError.new("Certificate validation is required, but it doesn't exist.")
end
- validate_doc(base64_cert, soft)
+ validate_doc(idp_base64_cert, soft)
end
def fingerprint_cert(cert)
diff --git a/spec/lib/saml_idp/controller_spec.rb b/spec/lib/saml_idp/controller_spec.rb
index 1401b14c..735cdaae 100644
--- a/spec/lib/saml_idp/controller_spec.rb
+++ b/spec/lib/saml_idp/controller_spec.rb
@@ -124,4 +124,19 @@ def params
end
end
end
+
+ context "Single Logout Request" do
+ before do
+ idp_configure("https://foo.example.com/saml/consume", true)
+ slo_request = make_saml_sp_slo_request
+ params[:SAMLRequest] = slo_request['SAMLRequest']
+ params[:RelayState] = slo_request['RelayState']
+ params[:SigAlg] = slo_request['SigAlg']
+ params[:Signature] = slo_request['Signature']
+ end
+
+ it 'should successfully validate signature' do
+ expect(validate_saml_request).to eq(true)
+ end
+ end
end
diff --git a/spec/lib/saml_idp/incoming_metadata_spec.rb b/spec/lib/saml_idp/incoming_metadata_spec.rb
index 7d483e0b..a966e17d 100644
--- a/spec/lib/saml_idp/incoming_metadata_spec.rb
+++ b/spec/lib/saml_idp/incoming_metadata_spec.rb
@@ -33,7 +33,6 @@ module SamlIdp
it 'should properly set sign_assertions to false' do
metadata = SamlIdp::IncomingMetadata.new(metadata_1)
expect(metadata.sign_assertions).to eq(false)
- expect(metadata.sign_authn_request).to eq(false)
end
it 'should properly set entity_id as https://test-saml.com/saml' do
diff --git a/spec/lib/saml_idp/metadata_builder_spec.rb b/spec/lib/saml_idp/metadata_builder_spec.rb
index c8e14765..453a8d81 100644
--- a/spec/lib/saml_idp/metadata_builder_spec.rb
+++ b/spec/lib/saml_idp/metadata_builder_spec.rb
@@ -6,7 +6,7 @@ module SamlIdp
end
it "signs valid xml" do
- expect(Saml::XML::Document.parse(subject.signed).valid_signature?(Default::FINGERPRINT)).to be_truthy
+ expect(Saml::XML::Document.parse(subject.signed).valid_signature?("", Default::FINGERPRINT)).to be_truthy
end
it "includes logout element" do
diff --git a/spec/lib/saml_idp/request_spec.rb b/spec/lib/saml_idp/request_spec.rb
index 47711539..794fc361 100644
--- a/spec/lib/saml_idp/request_spec.rb
+++ b/spec/lib/saml_idp/request_spec.rb
@@ -122,36 +122,68 @@ def info(msg); end
end
describe "logout request" do
- let(:raw_logout_request) { "
<%= label_tag :email %>
<%= email_field_tag :email, params[:email], :autocapitalize => "off", :autocorrect => "off", :autofocus => "autofocus", :spellcheck => "false", :size => 30, :class => "email_pwd txt" %>
diff --git a/spec/support/saml_request_macros.rb b/spec/support/saml_request_macros.rb
index d5629c83..4f3049ad 100644
--- a/spec/support/saml_request_macros.rb
+++ b/spec/support/saml_request_macros.rb
@@ -18,6 +18,17 @@ def make_saml_logout_request(requested_saml_logout_url = 'https://foo.example.co
Base64.strict_encode64(request_builder.signed)
end
+ def make_saml_sp_slo_request(param_type: true, embed_sign: false)
+ logout_request = OneLogin::RubySaml::Logoutrequest.new
+ saml_sp_setting = saml_settings("https://foo.example.com/saml/consume")
+ add_securty_options(saml_sp_setting, embed_sign: embed_sign)
+ if param_type
+ logout_request.create_params(saml_sp_setting, 'RelayState' => 'https://foo.example.com/home')
+ else
+ logout_request.create(saml_sp_setting, 'RelayState' => 'https://foo.example.com/home')
+ end
+ end
+
def generate_sp_metadata(saml_acs_url = "https://foo.example.com/saml/consume", enable_secure_options = false)
sp_metadata = OneLogin::RubySaml::Metadata.new
sp_metadata.generate(saml_settings(saml_acs_url, enable_secure_options), true)
@@ -28,6 +39,7 @@ def saml_settings(saml_acs_url = "https://foo.example.com/saml/consume", enable_
settings.assertion_consumer_service_url = saml_acs_url
settings.issuer = "http://example.com/issuer"
settings.idp_sso_target_url = "http://idp.com/saml/idp"
+ settings.idp_slo_target_url = "http://idp.com/saml/slo"
settings.assertion_consumer_logout_service_url = 'https://foo.example.com/saml/logout'
settings.idp_cert_fingerprint = SamlIdp::Default::FINGERPRINT
settings.name_identifier_format = SamlIdp::Default::NAME_ID_FORMAT
@@ -84,7 +96,8 @@ def idp_configure(saml_acs_url = "https://foo.example.com/saml/consume", enable_
response_hosts: [URI(saml_acs_url).host],
acs_url: saml_acs_url,
cert: sp_x509_cert,
- fingerprint: SamlIdp::Fingerprint.certificate_digest(sp_x509_cert)
+ fingerprint: SamlIdp::Fingerprint.certificate_digest(sp_x509_cert),
+ assertion_consumer_logout_service_url: 'https://foo.example.com/saml/logout'
}
}
end
diff --git a/spec/xml_security_spec.rb b/spec/xml_security_spec.rb
index 7b7bf3c4..276dfcef 100644
--- a/spec/xml_security_spec.rb
+++ b/spec/xml_security_spec.rb
@@ -19,7 +19,7 @@ module SamlIdp
end
it "it raise Fingerprint mismatch" do
- expect { document.validate("no:fi:ng:er:pr:in:t", false) }.to(
+ expect { document.validate("", "no:fi:ng:er:pr:in:t", false) }.to(
raise_error(SamlIdp::XMLSecurity::SignedDocument::ValidationError, "Fingerprint mismatch")
)
end
@@ -45,10 +45,10 @@ module SamlIdp
response = Base64.decode64(response_document)
response.sub!(/