Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 24 additions & 7 deletions lib/samlr/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need to dup the params

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

params is mutated a few lines down

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.delete, yep. good call.

buffer = root.dup
buffer << (buffer.include?("?") ? "&" : "?")

signable = "SAMLRequest=#{param}"

if params[:RelayState]
signable << "&RelayState=#{CGI.escape(params.delete(:RelayState))}"
end

if options[:sign_requests]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:sign_request or :request_signing makes more sense to me

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make sure this exists in initialize, so that we can show a nicer error message than NoMethodError on nil?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, Ill see what I can do.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even an options.fetch in the initializer would be fine

certificate.sign(signable)
end

end
end
7 changes: 7 additions & 0 deletions lib/samlr/tools/certificate_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions lib/samlr/tools/request_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions test/unit/test_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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