Commit 051551d
Changed files (9)
lib/saml/kit/authentication_request.rb
@@ -24,6 +24,10 @@ module Saml
to_h[name]['ID']
end
+ def destination
+ to_h[name]['Destination']
+ end
+
def acs_url
to_h[name]['AssertionConsumerServiceURL'] || registered_acs_url
end
@@ -128,8 +132,7 @@ module Saml
end
class Builder
- attr_accessor :id, :now, :issuer, :acs_url, :name_id_format
- attr_reader :sign
+ attr_accessor :id, :now, :issuer, :acs_url, :name_id_format, :sign, :destination
def initialize(user = nil, configuration: Saml::Kit.configuration, sign: true)
@id = SecureRandom.uuid
@@ -162,6 +165,7 @@ module Saml
ID: "_#{id}",
Version: "2.0",
IssueInstant: now.utc.iso8601,
+ Destination: destination,
}
options[:AssertionConsumerServiceURL] = acs_url if acs_url.present?
options
lib/saml/kit/content.rb
@@ -24,7 +24,7 @@ module Saml
end
def self.encode(value)
- Base64.strict_encode64(value).gsub(/\n/, '')
+ Base64.strict_encode64(value)
end
def self.base64_encoded?(value)
@@ -32,11 +32,12 @@ module Saml
end
def self.inflate(value)
- Zlib::Inflate.new.inflate(value)
+ inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
+ inflater.inflate(value)
end
def self.deflate(value, level: Zlib::BEST_COMPRESSION)
- Zlib::Deflate.deflate(value, level)
+ Zlib::Deflate.deflate(value, level)[2..-5]
end
end
end
lib/saml/kit/namespaces.rb
@@ -6,6 +6,7 @@ module Saml
BASIC = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
BEARER = "urn:oasis:names:tc:SAML:2.0:cm:bearer"
EMAIL_ADDRESS = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
+ ENVELOPED_SIG = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
HTTP_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
METADATA = "urn:oasis:names:tc:SAML:2.0:metadata"
PASSWORD = "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
@@ -13,13 +14,21 @@ module Saml
PERSISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
- SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success"
REQUESTER_ERROR = "urn:oasis:names:tc:SAML:2.0:status:Requester"
RESPONDER_ERROR = "urn:oasis:names:tc:SAML:2.0:status:Responder"
- VERSION_MISMATCH_ERROR = "urn:oasis:names:tc:SAML:2.0:status:VersionMismatch"
+ RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
+ RSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
+ RSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
+ RSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"
+ SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1"
+ SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256'
+ SHA384 = "http://www.w3.org/2001/04/xmldsig-more#sha384"
+ SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512'
+ SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success"
TRANSIENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
UNSPECIFIED = "urn:oasis:names:tc:SAML:2.0:consent:unspecified"
URI = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
+ VERSION_MISMATCH_ERROR = "urn:oasis:names:tc:SAML:2.0:status:VersionMismatch"
XMLDSIG = "http://www.w3.org/2000/09/xmldsig#"
def self.binding_for(binding)
lib/saml/kit/url_builder.rb
@@ -0,0 +1,27 @@
+module Saml
+ module Kit
+ class UrlBuilder
+ def build(request, binding:, relay_state: nil)
+ payload = {
+ 'SAMLRequest' => Content.encode_raw_saml(request.to_xml),
+ 'RelayState' => relay_state,
+ 'SigAlg' => Saml::Kit::Namespaces::SHA256,
+ }.map do |(x, y)|
+ "#{x}=#{y}"
+ end.join('&')
+ payload = URI.encode(payload)
+ "#{request.destination}?#{payload}&Signature=#{signature_for(payload)}"
+ end
+
+ private
+
+ def private_key
+ Saml::Kit.configuration.signing_private_key
+ end
+
+ def signature_for(payload)
+ Base64.strict_encode64(private_key.sign(OpenSSL::Digest::SHA256.new, payload))
+ end
+ end
+ end
+end
lib/saml/kit.rb
@@ -29,6 +29,7 @@ require "saml/kit/invalid_request"
require "saml/kit/self_signed_certificate"
require "saml/kit/service_provider_metadata"
require "saml/kit/signature"
+require "saml/kit/url_builder"
require "saml/kit/xml"
I18n.load_path += Dir[File.expand_path("kit/locales/*.yml", File.dirname(__FILE__))]
spec/saml/authentication_request_spec.rb
@@ -5,6 +5,7 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
let(:id) { SecureRandom.uuid }
let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
let(:issuer) { FFaker::Movie.title }
+ let(:destination) { FFaker::Internet.http_url }
let(:name_id_format) { Saml::Kit::Namespaces::EMAIL_ADDRESS }
let(:raw_xml) do
builder = described_class::Builder.new
@@ -13,6 +14,7 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
builder.issuer = issuer
builder.acs_url = acs_url
builder.name_id_format = name_id_format
+ builder.destination = destination
builder.to_xml
end
@@ -20,6 +22,7 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
it { expect(subject.id).to eql("_#{id}") }
it { expect(subject.acs_url).to eql(acs_url) }
it { expect(subject.name_id_format).to eql(name_id_format) }
+ it { expect(subject.destination).to eql(destination) }
describe "#to_xml" do
subject { described_class::Builder.new(configuration: configuration) }
@@ -175,12 +178,10 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
end
describe "#serialize" do
- it 'returns a compressed and base64 encoded document' do
- builder = described_class::Builder.new
- xml = builder.to_xml
- subject = described_class.new(xml)
+ subject { described_class::Builder.new.build }
- expected_value = Base64.encode64(Zlib::Deflate.deflate(xml, 9)).gsub(/\n/, '')
+ it 'returns a compressed and base64 encoded document' do
+ expected_value = Base64.strict_encode64(Zlib::Deflate.deflate(subject.to_xml, 9)[2..-5])
expect(subject.serialize).to eql(expected_value)
end
end
spec/saml/logout_response_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Saml::Kit::LogoutResponse do
subject { builder.build }
it 'returns a compressed and base64 encoded document' do
- expected_value = Base64.encode64(Zlib::Deflate.deflate(subject.to_xml, 9)).gsub(/\n/, '')
+ expected_value = Base64.strict_encode64(Zlib::Deflate.deflate(subject.to_xml, 9)[2..-5])
expect(subject.serialize).to eql(expected_value)
end
end
spec/saml/response_spec.rb
@@ -244,7 +244,7 @@ RSpec.describe Saml::Kit::Response do
xml = builder.to_xml
subject = described_class.new(xml)
- expected_value = Base64.encode64(Zlib::Deflate.deflate(xml, 9)).gsub(/\n/, '')
+ expected_value = Base64.strict_encode64(Zlib::Deflate.deflate(xml, 9)[2..-5])
expect(subject.serialize).to eql(expected_value)
end
end
spec/saml/url_builder_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::UrlBuilder do
+ describe "#build" do
+ let(:request) { instance_double(Saml::Kit::AuthenticationRequest, destination: destination, to_xml: xml) }
+ let(:xml) { "<xml></xml>" }
+ let(:destination) { FFaker::Internet.http_url }
+ let(:relay_state) { FFaker::Movie.title }
+ let(:query_params) { Hash[result_uri.query.split("&").map { |x| x.split('=', 2) }] }
+ let(:result) { subject.build(request, binding: :http_redirect, relay_state: relay_state) }
+ let(:result_uri) { URI.parse(result) }
+
+ it 'returns a url containing the target location' do
+ expect(result_uri.scheme).to eql("http")
+ expect(result_uri.host).to eql(URI.parse(destination).host)
+ end
+
+ it 'includes the message deflated (without header and checksum), base64-encoded, and URL-encoded' do
+ level = Zlib::BEST_COMPRESSION
+ expected = URI.encode(Base64.encode64(Zlib::Deflate.deflate(xml, level)[2..-5]).gsub(/\n/, ''))
+ expect(result).to include("SAMLRequest=#{expected}")
+ expect(query_params['SAMLRequest']).to eql(expected)
+ end
+
+ it 'includes the relay state' do
+ expect(query_params['RelayState']).to eql(URI.encode(relay_state))
+ expect(result).to include("RelayState=#{URI.encode(relay_state)}")
+ end
+
+<<-DOC
+2. To construct the signature, a string consisting of the concatenation of the RelayState
+(if present), SigAlg, and SAMLRequest (or SAMLResponse) query string parameters
+(each one URLencoded) is constructed in one of the following ways (ordered as below):
+
+SAMLRequest=value&RelayState=value&SigAlg=value
+SAMLResponse=value&RelayState=value&SigAlg=value
+
+3. The resulting string of bytes is the octet string to be fed into the signature algorithm.
+Any other content in the original query string is not included and not signed.
+4. The signature value MUST be encoded using the base64 encoding (see RFC 2045 [RFC2045]) with
+any whitespace removed, and included as a query string parameter named Signature.
+DOC
+ it 'includes a signature' do
+ expect(query_params['SigAlg']).to eql(URI.encode(Saml::Kit::Namespaces::SHA256))
+
+ payload = "SAMLRequest=#{query_params['SAMLRequest']}"
+ payload << "&RelayState=#{query_params['RelayState']}"
+ payload << "&SigAlg=#{query_params['SigAlg']}"
+ expected_signature = Base64.strict_encode64(Saml::Kit.configuration.signing_private_key.sign(OpenSSL::Digest::SHA256.new, payload))
+ expect(query_params['Signature']).to eql(expected_signature)
+ end
+ end
+end