From 4e450c765bc804c0b796d58f30f37f1544c09b50 Mon Sep 17 00:00:00 2001 From: John Lynch Date: Mon, 24 Nov 2014 09:45:26 -0800 Subject: [PATCH] Implement signed AuthnRequests --- README.md | 21 +++++++++++++++++ lib/samlr/request.rb | 31 ++++++++++++++++++++------ lib/samlr/tools/certificate_builder.rb | 7 ++++++ lib/samlr/tools/request_builder.rb | 5 +++++ test/unit/test_request.rb | 9 ++++++++ 5 files changed, 66 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e49b071..2789e66 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,27 @@ redirect_to saml_request.url( ) ``` +### Initiating a signed authentication request + +The signed SAML request must have a Destination node, specified as `destination_url` + +```ruby +saml_request = Samlr::Request.new( + :issuer => request.host, + :name_identity_format => Samlr::EMAIL_FORMAT, + :consumer_service_url => "https://#{request.host}/auth/saml", + :sign_requests => true, + :destination_url => "https://adfs.company.com/adfs/ls/", + :signing_certificate => Samlr::Tools::CertificateBuilder.read(private_key_pem, certifictate_pem) +) +``` +This will sign the URL with your private key and look something like this: + +``` +https://foo.com/?SAMLRequest=...Base64 Encoded SAML Request...&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha1&Signature=tvY57vi6IXHP1gHAMRQoRP5CZQlUniPwSeuwOUypqbjim04svTkk72njvbxzUE27U5PhK0Cwzq4ZdZ08i%2BuVAw%3D%3D +``` + + Once the IdP receives the request, it prompts the user to authenticate, after which it sends the SAML response to your application. ### Verifying a SAML response diff --git a/lib/samlr/request.rb b/lib/samlr/request.rb index d1ea90e..edf8373 100644 --- a/lib/samlr/request.rb +++ b/lib/samlr/request.rb @@ -20,18 +20,35 @@ def body # Utility method to get the full redirect destination, Request#url("https://idp.example.com/saml", { :RelayState => "https://sp.example.com/saml" }) def url(root, params = {}) - dest = root.dup - if dest.include?("?") - dest << "&SAMLRequest=#{param}" - else - dest << "?SAMLRequest=#{param}" + params = params.dup + buffer = root.dup + buffer << (buffer.include?("?") ? "&" : "?") + + signable = "SAMLRequest=#{param}" + + if params[:RelayState] + signable << "&RelayState=#{CGI.escape(params.delete(:RelayState))}" + end + + if options[:sign_requests] + signable << "&SigAlg=#{CGI.escape('http://www.w3.org/2000/09/xmldsig#rsa-sha1')}" + signable << "&Signature=#{CGI.escape(compute_signature(signable))}" end + buffer << signable + params.each_pair do |key, value| - dest << "&#{key}=#{CGI.escape(value.to_s)}" + buffer << "&#{key}=#{CGI.escape(value.to_s)}" end - dest + buffer + end + + private + def compute_signature(signable) + certificate = options[:signing_certificate] #instance of Samlr::Tools::CertificateBuilder + certificate.sign(signable) end + end end diff --git a/lib/samlr/tools/certificate_builder.rb b/lib/samlr/tools/certificate_builder.rb index 97d0ee7..e8e7dee 100644 --- a/lib/samlr/tools/certificate_builder.rb +++ b/lib/samlr/tools/certificate_builder.rb @@ -69,6 +69,13 @@ def self.load(path, id = "samlr") new(:key_pair => key_pair, :x509 => x509_cert) end + + def self.read(private_key_pem, certificate_pem) + key_pair = OpenSSL::PKey::RSA.new(private_key_pem) + x509_cert = OpenSSL::X509::Certificate.new(certificate_pem) + + new(:key_pair => key_pair, :x509 => x509_cert) + end end end end diff --git a/lib/samlr/tools/request_builder.rb b/lib/samlr/tools/request_builder.rb index ede226d..af2c5a2 100644 --- a/lib/samlr/tools/request_builder.rb +++ b/lib/samlr/tools/request_builder.rb @@ -7,6 +7,7 @@ module Tools module RequestBuilder def self.build(options = {}) consumer_service_url = options[:consumer_service_url] + destination_url = options[:destination_url] issuer = options[:issuer] name_identity_format = options[:name_identity_format] allow_create = options[:allow_create] || "true" @@ -20,6 +21,10 @@ def self.build(options = {}) xml.doc.root["AssertionConsumerServiceURL"] = consumer_service_url end + unless destination_url.nil? + xml.doc.root["Destination"] = destination_url + end + unless issuer.nil? xml["saml"].Issuer(issuer) end diff --git a/test/unit/test_request.rb b/test/unit/test_request.rb index accd350..a658c75 100644 --- a/test/unit/test_request.rb +++ b/test/unit/test_request.rb @@ -31,4 +31,13 @@ end end end + + describe "#signed_url" do + let(:signed_request) {Samlr::Request.new(:sign_requests => true, :signing_certificate => TEST_CERTIFICATE)} + it "returns a signed URL" do + signed_request.stub(:param, "hello") do + assert_equal("https://foo.com/?SAMLRequest=hello&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha1&Signature=tvY57vi6IXHP1gHAMRQoRP5CZQlUniPwSeuwOUypqbjim04svTkk72njvbxzUE27U5PhK0Cwzq4ZdZ08i%2BuVAw%3D%3D&foo=bar", signed_request.url("https://foo.com/", :foo => "bar")) + end + end + end end