Comparing changes
v0.2.2
→
v0.2.3
35 commits
46 files changed
Commits
Changed files (46)
lib
saml
kit
bindings
builders
templates
spec
saml
builders
lib/saml/kit/bindings/binding.rb
@@ -25,6 +25,26 @@ module Saml
{ binding: binding, location: location }
end
+ def ==(other)
+ self.to_s == other.to_s
+ end
+
+ def eql?(other)
+ self == other
+ end
+
+ def hash
+ to_s.hash
+ end
+
+ def to_s
+ "#{location}#{binding}"
+ end
+
+ def inspect
+ to_h.inspect
+ end
+
protected
def saml_param_from(params)
lib/saml/kit/builders/templates/assertion.builder
@@ -0,0 +1,29 @@
+xml.Assertion(assertion_options) do
+ xml.Issuer issuer
+ signature_for(reference_id: reference_id, xml: xml) unless encrypt
+ xml.Subject do
+ xml.NameID name_id, Format: name_id_format
+ xml.SubjectConfirmation Method: Saml::Kit::Namespaces::BEARER do
+ xml.SubjectConfirmationData "", subject_confirmation_data_options
+ end
+ end
+ xml.Conditions conditions_options do
+ xml.AudienceRestriction do
+ xml.Audience request.issuer
+ end
+ end
+ xml.AuthnStatement authn_statement_options do
+ xml.AuthnContext do
+ xml.AuthnContextClassRef Saml::Kit::Namespaces::PASSWORD
+ end
+ end
+ if assertion_attributes.any?
+ xml.AttributeStatement do
+ assertion_attributes.each do |key, value|
+ xml.Attribute Name: key, NameFormat: Saml::Kit::Namespaces::URI, FriendlyName: key do
+ xml.AttributeValue value.to_s
+ end
+ end
+ end
+ end
+end
lib/saml/kit/builders/templates/authentication_request.builder
@@ -0,0 +1,6 @@
+xml.instruct!
+xml.tag!('samlp:AuthnRequest', request_options) do
+ xml.tag!('saml:Issuer', issuer)
+ signature_for(reference_id: id, xml: xml)
+ xml.tag!('samlp:NameIDPolicy', Format: name_id_format)
+end
lib/saml/kit/builders/templates/certificate.builder
@@ -0,0 +1,7 @@
+xml.KeyDescriptor use: use do
+ xml.KeyInfo "xmlns": Saml::Kit::Namespaces::XMLDSIG do
+ xml.X509Data do
+ xml.X509Certificate stripped
+ end
+ end
+end
lib/saml/kit/builders/templates/identity_provider_metadata.builder
@@ -0,0 +1,32 @@
+xml.instruct!
+xml.EntityDescriptor entity_descriptor_options do
+ signature_for(reference_id: id, xml: xml)
+ xml.IDPSSODescriptor idp_sso_descriptor_options do
+ if configuration.signing_certificate_pem.present?
+ render configuration.signing_certificate, xml: xml
+ end
+ if configuration.encryption_certificate_pem.present?
+ render configuration.encryption_certificate, xml: xml
+ end
+ logout_urls.each do |item|
+ xml.SingleLogoutService Binding: item[:binding], Location: item[:location]
+ end
+ name_id_formats.each do |format|
+ xml.NameIDFormat format
+ end
+ single_sign_on_urls.each do |item|
+ xml.SingleSignOnService Binding: item[:binding], Location: item[:location]
+ end
+ attributes.each do |attribute|
+ xml.tag! 'saml:Attribute', Name: attribute
+ end
+ end
+ xml.Organization do
+ xml.OrganizationName organization_name, 'xml:lang': "en"
+ xml.OrganizationDisplayName organization_name, 'xml:lang': "en"
+ xml.OrganizationURL organization_url, 'xml:lang': "en"
+ end
+ xml.ContactPerson contactType: "technical" do
+ xml.Company "mailto:#{contact_email}"
+ end
+end
lib/saml/kit/builders/templates/logout_request.builder
@@ -0,0 +1,6 @@
+xml.instruct!
+xml.LogoutRequest logout_request_options do
+ xml.Issuer({ xmlns: Saml::Kit::Namespaces::ASSERTION }, issuer)
+ signature_for(reference_id: id, xml: xml)
+ xml.NameID name_id_options, user.name_id_for(name_id_format)
+end
lib/saml/kit/builders/templates/logout_response.builder
@@ -0,0 +1,8 @@
+xml.instruct!
+xml.LogoutResponse logout_response_options do
+ xml.Issuer(issuer, xmlns: Saml::Kit::Namespaces::ASSERTION)
+ signature_for(reference_id: id, xml: xml)
+ xml.Status do
+ xml.StatusCode Value: status_code
+ end
+end
lib/saml/kit/builders/templates/response.builder
@@ -0,0 +1,11 @@
+xml.instruct!
+xml.Response response_options do
+ xml.Issuer(issuer, xmlns: Saml::Kit::Namespaces::ASSERTION)
+ signature_for(reference_id: id, xml: xml)
+ xml.Status do
+ xml.StatusCode Value: status_code
+ end
+ encryption_for(xml: xml) do |xml|
+ render assertion, xml: xml
+ end
+end
lib/saml/kit/builders/templates/service_provider_metadata.builder
@@ -0,0 +1,29 @@
+xml.instruct!
+xml.EntityDescriptor entity_descriptor_options do
+ signature_for(reference_id: id, xml: xml)
+ xml.SPSSODescriptor descriptor_options do
+ if configuration.signing_certificate_pem.present?
+ render configuration.signing_certificate, xml: xml
+ end
+ if configuration.encryption_certificate_pem.present?
+ render configuration.encryption_certificate, xml: xml
+ end
+ logout_urls.each do |item|
+ xml.SingleLogoutService Binding: item[:binding], Location: item[:location]
+ end
+ name_id_formats.each do |format|
+ xml.NameIDFormat format
+ end
+ acs_urls.each_with_index do |item, index|
+ xml.AssertionConsumerService Binding: item[:binding], Location: item[:location], index: index, isDefault: index == 0 ? true : false
+ end
+ end
+ xml.Organization do
+ xml.OrganizationName organization_name, 'xml:lang': "en"
+ xml.OrganizationDisplayName organization_name, 'xml:lang': "en"
+ xml.OrganizationURL organization_url, 'xml:lang': "en"
+ end
+ xml.ContactPerson contactType: "technical" do
+ xml.Company "mailto:#{contact_email}"
+ end
+end
lib/saml/kit/builders/templates/xml_encryption.builder
@@ -0,0 +1,16 @@
+xml.EncryptedAssertion xmlns: Saml::Kit::Namespaces::ASSERTION do
+ xml.EncryptedData xmlns: Saml::Kit::Namespaces::XMLENC do
+ xml.EncryptionMethod Algorithm: "http://www.w3.org/2001/04/xmlenc#aes256-cbc"
+ xml.KeyInfo xmlns: Saml::Kit::Namespaces::XMLDSIG do
+ xml.EncryptedKey xmlns: Saml::Kit::Namespaces::XMLENC do
+ xml.EncryptionMethod Algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-1_5"
+ xml.CipherData do
+ xml.CipherValue Base64.encode64(public_key.public_encrypt(key))
+ end
+ end
+ end
+ xml.CipherData do
+ xml.CipherValue Base64.encode64(iv + encrypted)
+ end
+ end
+end
lib/saml/kit/builders/templates/xml_signature.builder
@@ -0,0 +1,20 @@
+xml.Signature "xmlns" => Saml::Kit::Namespaces::XMLDSIG do
+ xml.SignedInfo do
+ xml.CanonicalizationMethod Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#"
+ xml.SignatureMethod Algorithm: signature_method
+ xml.Reference URI: "##{reference_id}" do
+ xml.Transforms do
+ xml.Transform Algorithm: "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
+ xml.Transform Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#"
+ end
+ xml.DigestMethod Algorithm: digest_method
+ xml.DigestValue ""
+ end
+ end
+ xml.SignatureValue ""
+ xml.KeyInfo do
+ xml.X509Data do
+ xml.X509Certificate x509_certificate
+ end
+ end
+end
lib/saml/kit/builders/assertion.rb
@@ -0,0 +1,62 @@
+module Saml
+ module Kit
+ module Builders
+ class Assertion
+ include Templatable
+ extend Forwardable
+
+ def_delegators :@response_builder, :encrypt, :sign, :request, :issuer, :reference_id, :now, :configuration, :user, :version
+
+ def initialize(response_builder)
+ @response_builder = response_builder
+ end
+
+ def name_id_format
+ request.name_id_format
+ end
+
+ def name_id
+ user.name_id_for(name_id_format)
+ end
+
+ def assertion_attributes
+ user.assertion_attributes_for(request)
+ end
+
+ private
+
+ def assertion_options
+ {
+ ID: reference_id,
+ IssueInstant: now.iso8601,
+ Version: version,
+ xmlns: Namespaces::ASSERTION,
+ }
+ end
+
+ def subject_confirmation_data_options
+ {
+ InResponseTo: request.id,
+ NotOnOrAfter: 3.hours.since(now).utc.iso8601,
+ Recipient: request.assertion_consumer_service_url,
+ }
+ end
+
+ def conditions_options
+ {
+ NotBefore: now.utc.iso8601,
+ NotOnOrAfter: configuration.session_timeout.from_now.utc.iso8601,
+ }
+ end
+
+ def authn_statement_options
+ {
+ AuthnInstant: now.iso8601,
+ SessionIndex: reference_id,
+ SessionNotOnOrAfter: 3.hours.since(now).utc.iso8601,
+ }
+ end
+ end
+ end
+ end
+end
lib/saml/kit/builders/authentication_request.rb
@@ -2,26 +2,29 @@ module Saml
module Kit
module Builders
class AuthenticationRequest
+ include Saml::Kit::Templatable
attr_accessor :id, :now, :issuer, :assertion_consumer_service_url, :name_id_format, :sign, :destination
attr_accessor :version
+ attr_reader :configuration
def initialize(configuration: Saml::Kit.configuration, sign: true)
+ @configuration = configuration
@id = Id.generate
@issuer = configuration.issuer
@name_id_format = Namespaces::PERSISTENT
@now = Time.now.utc
- @version = "2.0"
@sign = sign
+ @version = "2.0"
end
- def to_xml
- Signature.sign(sign: sign) do |xml, signature|
- xml.tag!('samlp:AuthnRequest', request_options) do
- xml.tag!('saml:Issuer', issuer)
- signature.template(id)
- xml.tag!('samlp:NameIDPolicy', Format: name_id_format)
- end
- end
+ def acs_url
+ Saml::Kit.deprecate("acs_url is deprecated. Use assertion_consumer_service_url instead")
+ self.assertion_consumer_service_url
+ end
+
+ def acs_url=(value)
+ Saml::Kit.deprecate("acs_url= is deprecated. Use assertion_consumer_service_url= instead")
+ self.assertion_consumer_service_url = value
end
def build
@@ -39,7 +42,9 @@ module Saml
IssueInstant: now.utc.iso8601,
Destination: destination,
}
- options[:AssertionConsumerServiceURL] = assertion_consumer_service_url if assertion_consumer_service_url.present?
+ if assertion_consumer_service_url.present?
+ options[:AssertionConsumerServiceURL] = assertion_consumer_service_url
+ end
options
end
end
lib/saml/kit/builders/identity_provider_metadata.rb
@@ -2,19 +2,21 @@ module Saml
module Kit
module Builders
class IdentityProviderMetadata
+ include Saml::Kit::Templatable
attr_accessor :id, :organization_name, :organization_url, :contact_email, :entity_id, :attributes, :name_id_formats
attr_accessor :want_authn_requests_signed, :sign
attr_reader :logout_urls, :single_sign_on_urls
+ attr_reader :configuration
def initialize(configuration = Saml::Kit.configuration)
- @id = Id.generate
- @entity_id = configuration.issuer
@attributes = []
- @name_id_formats = [Namespaces::PERSISTENT]
- @single_sign_on_urls = []
- @logout_urls = []
@configuration = configuration
+ @entity_id = configuration.issuer
+ @id = Id.generate
+ @logout_urls = []
+ @name_id_formats = [Namespaces::PERSISTENT]
@sign = true
+ @single_sign_on_urls = []
@want_authn_requests_signed = true
end
@@ -26,55 +28,6 @@ module Saml
@logout_urls.push(location: url, binding: Bindings.binding_for(binding))
end
- def to_xml
- Signature.sign(sign: sign) do |xml, signature|
- xml.instruct!
- xml.EntityDescriptor entity_descriptor_options do
- signature.template(id)
- xml.IDPSSODescriptor idp_sso_descriptor_options do
- if @configuration.signing_certificate_pem.present?
- xml.KeyDescriptor use: "signing" do
- xml.KeyInfo "xmlns": Namespaces::XMLDSIG do
- xml.X509Data do
- xml.X509Certificate @configuration.stripped_signing_certificate
- end
- end
- end
- end
- if @configuration.encryption_certificate_pem.present?
- xml.KeyDescriptor use: "encryption" do
- xml.KeyInfo "xmlns": Namespaces::XMLDSIG do
- xml.X509Data do
- xml.X509Certificate @configuration.stripped_encryption_certificate
- end
- end
- end
- end
- logout_urls.each do |item|
- xml.SingleLogoutService Binding: item[:binding], Location: item[:location]
- end
- name_id_formats.each do |format|
- xml.NameIDFormat format
- end
- single_sign_on_urls.each do |item|
- xml.SingleSignOnService Binding: item[:binding], Location: item[:location]
- end
- attributes.each do |attribute|
- xml.tag! 'saml:Attribute', Name: attribute
- end
- end
- xml.Organization do
- xml.OrganizationName organization_name, 'xml:lang': "en"
- xml.OrganizationDisplayName organization_name, 'xml:lang': "en"
- xml.OrganizationURL organization_url, 'xml:lang': "en"
- end
- xml.ContactPerson contactType: "technical" do
- xml.Company "mailto:#{contact_email}"
- end
- end
- end
- end
-
def build
Saml::Kit::IdentityProviderMetadata.new(to_xml)
end
lib/saml/kit/builders/logout_request.rb
@@ -2,11 +2,13 @@ module Saml
module Kit
module Builders
class LogoutRequest
+ include Saml::Kit::Templatable
attr_accessor :id, :destination, :issuer, :name_id_format, :now
attr_accessor :sign, :version
- attr_reader :user
+ attr_reader :user, :configuration
def initialize(user, configuration: Saml::Kit.configuration, sign: true)
+ @configuration = configuration
@user = user
@id = "_#{SecureRandom.uuid}"
@issuer = configuration.issuer
@@ -16,17 +18,6 @@ module Saml
@sign = sign
end
- def to_xml
- Signature.sign(sign: sign) do |xml, signature|
- xml.instruct!
- xml.LogoutRequest logout_request_options do
- xml.Issuer({ xmlns: Namespaces::ASSERTION }, issuer)
- signature.template(id)
- xml.NameID name_id_options, user.name_id_for(name_id_format)
- end
- end
- end
-
def build
Saml::Kit::LogoutRequest.new(to_xml)
end
lib/saml/kit/builders/logout_response.rb
@@ -2,30 +2,21 @@ module Saml
module Kit
module Builders
class LogoutResponse
+ include Saml::Kit::Templatable
attr_accessor :id, :issuer, :version, :status_code, :sign, :now, :destination
attr_reader :request
+ attr_reader :configuration
def initialize(user, request, configuration: Saml::Kit.configuration, sign: true)
- @user = user
+ @configuration = configuration
+ @id = Id.generate
+ @issuer = configuration.issuer
@now = Time.now.utc
@request = request
- @id = Id.generate
- @version = "2.0"
- @status_code = Namespaces::SUCCESS
@sign = sign
- @issuer = configuration.issuer
- end
-
- def to_xml
- Signature.sign(sign: sign) do |xml, signature|
- xml.LogoutResponse logout_response_options do
- xml.Issuer(issuer, xmlns: Namespaces::ASSERTION)
- signature.template(id)
- xml.Status do
- xml.StatusCode Value: status_code
- end
- end
- end
+ @status_code = Namespaces::SUCCESS
+ @user = user
+ @version = "2.0"
end
def build
lib/saml/kit/builders/response.rb
@@ -2,12 +2,14 @@ module Saml
module Kit
module Builders
class Response
+ include Templatable
attr_reader :user, :request
attr_accessor :id, :reference_id, :now
attr_accessor :version, :status_code
attr_accessor :issuer, :sign, :destination, :encrypt
+ attr_reader :configuration
- def initialize(user, request)
+ def initialize(user, request, configuration: Saml::Kit.configuration)
@user = user
@request = request
@id = Id.generate
@@ -19,6 +21,7 @@ module Saml
@destination = destination_for(request)
@sign = want_assertions_signed
@encrypt = false
+ @configuration = configuration
end
def want_assertions_signed
@@ -28,97 +31,18 @@ module Saml
true
end
- def to_xml
- Signature.sign(sign: sign) do |xml, signature|
- xml.Response response_options do
- xml.Issuer(issuer, xmlns: Namespaces::ASSERTION)
- signature.template(id)
- xml.Status do
- xml.StatusCode Value: status_code
- end
- assertion(xml, signature)
- end
- end
- end
-
def build
Saml::Kit::Response.new(to_xml, request_id: request.id)
end
private
- def assertion(xml, signature)
- with_encryption(xml) do |xml|
- xml.Assertion(assertion_options) do
- xml.Issuer issuer
- signature.template(reference_id) unless encrypt
- xml.Subject do
- xml.NameID user.name_id_for(request.name_id_format), Format: request.name_id_format
- xml.SubjectConfirmation Method: Namespaces::BEARER do
- xml.SubjectConfirmationData "", subject_confirmation_data_options
- end
- end
- xml.Conditions conditions_options do
- xml.AudienceRestriction do
- xml.Audience request.issuer
- end
- end
- xml.AuthnStatement authn_statement_options do
- xml.AuthnContext do
- xml.AuthnContextClassRef Namespaces::PASSWORD
- end
- end
- assertion_attributes = user.assertion_attributes_for(request)
- if assertion_attributes.any?
- xml.AttributeStatement do
- assertion_attributes.each do |key, value|
- xml.Attribute Name: key, NameFormat: Namespaces::URI, FriendlyName: key do
- xml.AttributeValue value.to_s
- end
- end
- end
- end
- end
- end
+ def assertion
+ @assertion ||= Saml::Kit::Builders::Assertion.new(self)
end
- def with_encryption(xml)
- if encrypt
- temp = ::Builder::XmlMarkup.new
- yield temp
- raw_xml_to_encrypt = temp.target!
-
- encryption_certificate = request.provider.encryption_certificates.first
- public_key = encryption_certificate.public_key
-
- cipher = OpenSSL::Cipher.new('AES-256-CBC')
- cipher.encrypt
- key = cipher.random_key
- iv = cipher.random_iv
- encrypted = cipher.update(raw_xml_to_encrypt) + cipher.final
-
- Saml::Kit.logger.debug ['+iv', iv].inspect
- Saml::Kit.logger.debug ['+key', key].inspect
-
- xml.EncryptedAssertion xmlns: Namespaces::ASSERTION do
- xml.EncryptedData xmlns: Namespaces::XMLENC do
- xml.EncryptionMethod Algorithm: "http://www.w3.org/2001/04/xmlenc#aes256-cbc"
- xml.KeyInfo xmlns: Namespaces::XMLDSIG do
- xml.EncryptedKey xmlns: Namespaces::XMLENC do
- xml.EncryptionMethod Algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-1_5"
- xml.CipherData do
- xml.CipherValue Base64.encode64(public_key.public_encrypt(key))
- end
- end
- end
- xml.CipherData do
- xml.CipherValue Base64.encode64(iv + encrypted)
- end
- end
- end
- else
- yield xml
- end
+ def encryption_certificate
+ request.provider.encryption_certificates.first
end
def destination_for(request)
@@ -129,10 +53,6 @@ module Saml
end
end
- def configuration
- Saml::Kit.configuration
- end
-
def response_options
{
ID: id,
@@ -144,38 +64,6 @@ module Saml
xmlns: Namespaces::PROTOCOL,
}
end
-
- def assertion_options
- {
- ID: reference_id,
- IssueInstant: now.iso8601,
- Version: "2.0",
- xmlns: Namespaces::ASSERTION,
- }
- end
-
- def subject_confirmation_data_options
- {
- InResponseTo: request.id,
- NotOnOrAfter: 3.hours.since(now).utc.iso8601,
- Recipient: request.assertion_consumer_service_url,
- }
- end
-
- def conditions_options
- {
- NotBefore: now.utc.iso8601,
- NotOnOrAfter: Saml::Kit.configuration.session_timeout.from_now.utc.iso8601,
- }
- end
-
- def authn_statement_options
- {
- AuthnInstant: now.iso8601,
- SessionIndex: assertion_options[:ID],
- SessionNotOnOrAfter: 3.hours.since(now).utc.iso8601,
- }
- end
end
end
end
lib/saml/kit/builders/service_provider_metadata.rb
@@ -2,14 +2,17 @@ module Saml
module Kit
module Builders
class ServiceProviderMetadata
+ include Saml::Kit::Templatable
attr_accessor :id, :entity_id, :acs_urls, :logout_urls, :name_id_formats, :sign
+ attr_accessor :organization_name, :organization_url, :contact_email
attr_accessor :want_assertions_signed
+ attr_reader :configuration
def initialize(configuration = Saml::Kit.configuration)
- @id = Id.generate
+ @acs_urls = []
@configuration = configuration
@entity_id = configuration.issuer
- @acs_urls = []
+ @id = Id.generate
@logout_urls = []
@name_id_formats = [Namespaces::PERSISTENT]
@sign = true
@@ -24,44 +27,6 @@ module Saml
@logout_urls.push(location: url, binding: Bindings.binding_for(binding))
end
- def to_xml
- Signature.sign(sign: sign) do |xml, signature|
- xml.instruct!
- xml.EntityDescriptor entity_descriptor_options do
- signature.template(id)
- xml.SPSSODescriptor descriptor_options do
- if @configuration.signing_certificate_pem.present?
- xml.KeyDescriptor use: "signing" do
- xml.KeyInfo "xmlns": Namespaces::XMLDSIG do
- xml.X509Data do
- xml.X509Certificate @configuration.stripped_signing_certificate
- end
- end
- end
- end
- if @configuration.encryption_certificate_pem.present?
- xml.KeyDescriptor use: "encryption" do
- xml.KeyInfo "xmlns": Namespaces::XMLDSIG do
- xml.X509Data do
- xml.X509Certificate @configuration.stripped_encryption_certificate
- end
- end
- end
- end
- logout_urls.each do |item|
- xml.SingleLogoutService Binding: item[:binding], Location: item[:location]
- end
- name_id_formats.each do |format|
- xml.NameIDFormat format
- end
- acs_urls.each_with_index do |item, index|
- xml.AssertionConsumerService Binding: item[:binding], Location: item[:location], index: index, isDefault: index == 0 ? true : false
- end
- end
- end
- end
- end
-
def build
Saml::Kit::ServiceProviderMetadata.new(to_xml)
end
lib/saml/kit/builders/xml_encryption.rb
@@ -0,0 +1,20 @@
+module Saml
+ module Kit
+ module Builders
+ class XmlEncryption
+ attr_reader :public_key
+ attr_reader :key, :iv, :encrypted
+
+ def initialize(raw_xml, public_key)
+ @public_key = public_key
+ cipher = OpenSSL::Cipher.new('AES-256-CBC')
+ cipher.encrypt
+ @key = cipher.random_key
+ @iv = cipher.random_iv
+ @encrypted = cipher.update(raw_xml) + cipher.final
+ end
+ end
+ end
+ end
+end
+
lib/saml/kit/builders/xml_signature.rb
@@ -0,0 +1,41 @@
+module Saml
+ module Kit
+ module Builders
+ class XmlSignature
+ SIGNATURE_METHODS = {
+ SHA1: "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
+ SHA224: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha224",
+ SHA256: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
+ SHA384: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
+ SHA512: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
+ }.freeze
+ DIGEST_METHODS = {
+ SHA1: "http://www.w3.org/2000/09/xmldsig#SHA1",
+ SHA224: "http://www.w3.org/2001/04/xmldsig-more#sha224",
+ 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",
+ }.freeze
+
+ attr_reader :sign, :configuration
+ attr_reader :reference_id
+ attr_reader :x509_certificate
+
+ def initialize(reference_id, configuration:, sign: true)
+ @configuration = configuration
+ @reference_id = reference_id
+ @sign = sign
+ @x509_certificate = configuration.signing_certificate.stripped
+ end
+
+ def signature_method
+ SIGNATURE_METHODS[configuration.signature_method]
+ end
+
+ def digest_method
+ DIGEST_METHODS[configuration.digest_method]
+ end
+ end
+ end
+ end
+end
lib/saml/kit/builders.rb
@@ -1,9 +1,12 @@
+require 'saml/kit/builders/assertion'
require 'saml/kit/builders/authentication_request'
require 'saml/kit/builders/identity_provider_metadata'
require 'saml/kit/builders/logout_request'
require 'saml/kit/builders/logout_response'
require 'saml/kit/builders/response'
require 'saml/kit/builders/service_provider_metadata'
+require 'saml/kit/builders/xml_encryption'
+require 'saml/kit/builders/xml_signature'
module Saml
module Kit
lib/saml/kit/certificate.rb
@@ -1,6 +1,8 @@
module Saml
module Kit
class Certificate
+ BEGIN_CERT=/-----BEGIN CERTIFICATE-----/
+ END_CERT=/-----END CERTIFICATE-----/
attr_reader :value, :use
def initialize(value, use:)
@@ -48,6 +50,18 @@ module Saml
value
end
+ def to_h
+ { use: @use, x509: @value }
+ end
+
+ def inspect
+ to_h.inspect
+ end
+
+ def stripped
+ value.to_s.gsub(BEGIN_CERT, '').gsub(END_CERT, '').gsub(/\n/, '')
+ end
+
def self.to_x509(value)
OpenSSL::X509::Certificate.new(Base64.decode64(value))
rescue OpenSSL::X509::CertificateError => error
lib/saml/kit/composite_metadata.rb
@@ -0,0 +1,36 @@
+module Saml
+ module Kit
+ class CompositeMetadata < Metadata
+ attr_reader :service_provider, :identity_provider
+
+ def initialize(xml)
+ super("IDPSSODescriptor", xml)
+ @service_provider = Saml::Kit::ServiceProviderMetadata.new(xml)
+ @identity_provider = Saml::Kit::IdentityProviderMetadata.new(xml)
+ end
+
+ def services(type)
+ xpath = "//md:EntityDescriptor/md:SPSSODescriptor/md:#{type}|//md:EntityDescriptor/md:IDPSSODescriptor/md:#{type}"
+ document.find_all(xpath).map do |item|
+ binding = item.attribute("Binding").value
+ location = item.attribute("Location").value
+ Saml::Kit::Bindings.create_for(binding, location)
+ end
+ end
+
+ def certificates
+ identity_provider.certificates + service_provider.certificates
+ end
+
+ def method_missing(name, *args)
+ if identity_provider.respond_to?(name)
+ identity_provider.public_send(name, *args)
+ elsif service_provider.respond_to?(name)
+ service_provider.public_send(name, *args)
+ else
+ super
+ end
+ end
+ end
+ end
+end
lib/saml/kit/configuration.rb
@@ -1,9 +1,6 @@
module Saml
module Kit
class Configuration
- BEGIN_CERT=/-----BEGIN CERTIFICATE-----/
- END_CERT=/-----END CERTIFICATE-----/
-
attr_accessor :issuer
attr_accessor :signature_method, :digest_method
attr_accessor :signing_certificate_pem, :signing_private_key_pem, :signing_private_key_password
@@ -23,20 +20,12 @@ module Saml
@logger = Logger.new(STDOUT)
end
- def stripped_signing_certificate
- normalize(signing_certificate_pem)
- end
-
- def stripped_encryption_certificate
- normalize(encryption_certificate_pem)
- end
-
- def signing_x509
- Certificate.to_x509(signing_certificate_pem)
+ def signing_certificate
+ Saml::Kit::Certificate.new(signing_certificate_pem, use: :signing)
end
- def encryption_x509
- Certificate.to_x509(encryption_certificate_pem)
+ def encryption_certificate
+ Saml::Kit::Certificate.new(encryption_certificate_pem, use: :encryption)
end
def signing_private_key
@@ -46,12 +35,6 @@ module Saml
def encryption_private_key
OpenSSL::PKey::RSA.new(encryption_private_key_pem, encryption_private_key_password)
end
-
- private
-
- def normalize(certificate)
- certificate.to_s.gsub(BEGIN_CERT, '').gsub(END_CERT, '').gsub(/\n/, '')
- end
end
end
end
lib/saml/kit/metadata.rb
@@ -99,7 +99,9 @@ module Saml
def self.from(content)
hash = Hash.from_xml(content)
entity_descriptor = hash["EntityDescriptor"]
- if entity_descriptor.keys.include?("SPSSODescriptor")
+ if entity_descriptor.key?("SPSSODescriptor") && entity_descriptor.key?("IDPSSODescriptor")
+ Saml::Kit::CompositeMetadata.new(content)
+ elsif entity_descriptor.keys.include?("SPSSODescriptor")
Saml::Kit::ServiceProviderMetadata.new(content)
elsif entity_descriptor.keys.include?("IDPSSODescriptor")
Saml::Kit::IdentityProviderMetadata.new(content)
lib/saml/kit/requestable.rb
@@ -3,9 +3,6 @@ module Saml
module Requestable
extend ActiveSupport::Concern
- included do
- end
-
def query_string_parameter
'SAMLRequest'
end
lib/saml/kit/signature.rb
@@ -1,65 +1,23 @@
module Saml
module Kit
class Signature
- SIGNATURE_METHODS = {
- SHA1: "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
- SHA224: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha224",
- SHA256: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
- SHA384: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
- SHA512: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
- }.freeze
- DIGEST_METHODS = {
- SHA1: "http://www.w3.org/2000/09/xmldsig#SHA1",
- SHA224: "http://www.w3.org/2001/04/xmldsig-more#sha224",
- 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",
- }.freeze
-
- attr_reader :configuration, :sign, :xml
+ attr_reader :sign, :xml
+ attr_reader :configuration
def initialize(xml, configuration:, sign: true)
- @xml = xml
@configuration = configuration
@sign = sign
- @reference_ids = []
+ @xml = xml
end
def template(reference_id)
return unless sign
- return if reference_id.blank?
- @reference_ids << reference_id
-
- xml.Signature "xmlns" => Namespaces::XMLDSIG do
- xml.SignedInfo do
- xml.CanonicalizationMethod Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#"
- xml.SignatureMethod Algorithm: SIGNATURE_METHODS[configuration.signature_method]
- xml.Reference URI: "##{reference_id}" do
- xml.Transforms do
- xml.Transform Algorithm: "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
- xml.Transform Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#"
- end
- xml.DigestMethod Algorithm: DIGEST_METHODS[configuration.digest_method]
- xml.DigestValue ""
- end
- end
- xml.SignatureValue ""
- xml.KeyInfo do
- xml.X509Data do
- xml.X509Certificate configuration.stripped_signing_certificate
- end
- end
- end
+ signature = signatures.build(reference_id)
+ Template.new(signature).to_xml(xml: xml)
end
def finalize
- return xml.target! unless sign
-
- raw_xml = xml.target!
- @reference_ids.each do |reference_id|
- raw_xml = Xmldsig::SignedDocument.new(raw_xml).sign(private_key)
- end
- raw_xml
+ signatures.complete(xml.target!)
end
def self.sign(sign: true, xml: ::Builder::XmlMarkup.new, configuration: Saml::Kit.configuration)
@@ -70,8 +28,8 @@ module Saml
private
- def private_key
- configuration.signing_private_key
+ def signatures
+ @signatures ||= Saml::Kit::Signatures.new(configuration: configuration, sign: sign)
end
end
end
lib/saml/kit/signatures.rb
@@ -0,0 +1,28 @@
+module Saml
+ module Kit
+ class Signatures
+ attr_reader :sign, :configuration
+
+ def initialize(configuration:, sign: true)
+ @configuration = configuration
+ @sign = sign
+ end
+
+ def build(reference_id)
+ Saml::Kit::Builders::XmlSignature.new(reference_id, configuration: configuration, sign: sign)
+ end
+
+ def complete(raw_xml)
+ return raw_xml unless sign
+
+ Xmldsig::SignedDocument.new(raw_xml).sign(private_key)
+ end
+
+ private
+
+ def private_key
+ configuration.signing_private_key
+ end
+ end
+ end
+end
lib/saml/kit/templatable.rb
@@ -0,0 +1,33 @@
+module Saml
+ module Kit
+ module Templatable
+ def to_xml(xml: ::Builder::XmlMarkup.new)
+ signatures.complete(render(self, xml: xml))
+ end
+
+ def signature_for(reference_id:, xml:)
+ return unless sign
+ render(signatures.build(reference_id), xml: xml)
+ end
+
+ def signatures
+ @signatures ||= Saml::Kit::Signatures.new(configuration: configuration, sign: sign)
+ end
+
+ def encryption_for(xml:)
+ if encrypt && encryption_certificate
+ temp = ::Builder::XmlMarkup.new
+ yield temp
+ xml_encryption = Saml::Kit::Builders::XmlEncryption.new(temp.target!, encryption_certificate.public_key)
+ render(xml_encryption, xml: xml)
+ else
+ yield xml
+ end
+ end
+
+ def render(model, options)
+ Saml::Kit::Template.new(model).to_xml(options)
+ end
+ end
+ end
+end
lib/saml/kit/template.rb
@@ -0,0 +1,29 @@
+module Saml
+ module Kit
+ class Template
+ attr_reader :target
+
+ def initialize(target)
+ @target = target
+ end
+
+ def to_xml(options)
+ template.render(target, options)
+ end
+
+ private
+
+ def template_name
+ "#{target.class.name.split("::").last.underscore}.builder"
+ end
+
+ def template_path
+ File.join(File.expand_path(File.dirname(__FILE__)), "builders/templates/#{template_name}")
+ end
+
+ def template
+ Tilt.new(template_path)
+ end
+ end
+ end
+end
lib/saml/kit/version.rb
@@ -1,5 +1,5 @@
module Saml
module Kit
- VERSION = "0.2.2"
+ VERSION = "0.2.3"
end
end
lib/saml/kit/xml_decryption.rb
@@ -8,10 +8,10 @@ module Saml
end
def decrypt(data)
- encrypt_data = data['EncryptedData']
- symmetric_key = symmetric_key_from(encrypt_data)
- cipher_text = Base64.decode64(encrypt_data["CipherData"]["CipherValue"])
- to_plaintext(cipher_text, symmetric_key, encrypt_data["EncryptionMethod"]['Algorithm'])
+ encrypted_data = data['EncryptedData']
+ symmetric_key = symmetric_key_from(encrypted_data)
+ cipher_text = Base64.decode64(encrypted_data["CipherData"]["CipherValue"])
+ to_plaintext(cipher_text, symmetric_key, encrypted_data["EncryptionMethod"]['Algorithm'])
end
private
@@ -23,7 +23,7 @@ module Saml
end
def to_plaintext(cipher_text, symmetric_key, algorithm)
- return Crypto.decryptor_for(algorithm, symmetric_key).decrypt(cipher_text)
+ Crypto.decryptor_for(algorithm, symmetric_key).decrypt(cipher_text)
end
end
end
lib/saml/kit.rb
@@ -12,9 +12,11 @@ require "logger"
require "net/http"
require "nokogiri"
require "securerandom"
+require "tilt"
require "xmldsig"
require "saml/kit/buildable"
+require "saml/kit/templatable"
require "saml/kit/builders"
require "saml/kit/namespaces"
require "saml/kit/serializable"
@@ -34,6 +36,7 @@ require "saml/kit/fingerprint"
require "saml/kit/logout_response"
require "saml/kit/logout_request"
require "saml/kit/metadata"
+require "saml/kit/composite_metadata"
require "saml/kit/response"
require "saml/kit/id"
require "saml/kit/identity_provider_metadata"
@@ -41,6 +44,8 @@ require "saml/kit/invalid_document"
require "saml/kit/self_signed_certificate"
require "saml/kit/service_provider_metadata"
require "saml/kit/signature"
+require "saml/kit/signatures"
+require "saml/kit/template"
require "saml/kit/xml"
require "saml/kit/xml_decryption"
@@ -64,6 +69,11 @@ module Saml
def registry
configuration.registry
end
+
+ def deprecate(message)
+ @deprecation ||= ActiveSupport::Deprecation.new('next-release', 'saml-kit')
+ @deprecation.deprecation_warning(message)
+ end
end
end
end
spec/saml/builders/authentication_request_spec.rb
@@ -24,5 +24,10 @@ RSpec.describe Saml::Kit::Builders::AuthenticationRequest do
expect(result['AuthnRequest']['Issuer']).to eql(issuer)
expect(result['AuthnRequest']['NameIDPolicy']['Format']).to eql(Saml::Kit::Namespaces::PERSISTENT)
end
+
+ it 'marks acs_url as deprecated' do
+ subject.acs_url = FFaker::Internet.uri('https')
+ expect(subject.assertion_consumer_service_url).to eql(subject.acs_url)
+ end
end
end
spec/saml/builders/identity_provider_metadata_spec.rb
@@ -39,8 +39,8 @@ RSpec.describe Saml::Kit::Builders::IdentityProviderMetadata do
expect(result['EntityDescriptor']['IDPSSODescriptor']['Attribute']['Name']).to eql("id")
certificates = result['EntityDescriptor']['IDPSSODescriptor']['KeyDescriptor'].map { |x| x['KeyInfo']['X509Data']['X509Certificate'] }
expect(certificates).to match_array([
- Saml::Kit.configuration.stripped_signing_certificate,
- Saml::Kit.configuration.stripped_encryption_certificate,
+ Saml::Kit.configuration.signing_certificate.stripped,
+ Saml::Kit.configuration.encryption_certificate.stripped,
])
expect(result['EntityDescriptor']['Organization']['OrganizationName']).to eql(org_name)
expect(result['EntityDescriptor']['Organization']['OrganizationDisplayName']).to eql(org_name)
spec/saml/builders/logout_response_spec.rb
@@ -1,8 +1,7 @@
require 'spec_helper'
RSpec.describe Saml::Kit::Builders::LogoutResponse do
- subject { described_class.new(user, request, configuration: configuration) }
- let(:configuration) { double(issuer: issuer) }
+ subject { described_class.new(user, request) }
let(:user) { double(:user, name_id_for: SecureRandom.uuid) }
let(:request) { Saml::Kit::Builders::LogoutRequest.new(user).build }
let(:issuer) { FFaker::Internet.http_url }
@@ -12,6 +11,7 @@ RSpec.describe Saml::Kit::Builders::LogoutResponse do
it 'builds a logout response' do
travel_to 1.second.from_now
+ subject.issuer = issuer
subject.destination = destination
result = subject.build
expect(result.id).to be_present
spec/saml/builders/response_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Saml::Kit::Builders::Response do
let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: { email: email, created_at: Time.now.utc.iso8601 }) }
let(:request) { double(:request, id: Saml::Kit::Id.generate, assertion_consumer_service_url: assertion_consumer_service_url, issuer: issuer, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: provider, trusted?: true, signed?: true) }
let(:provider) { double(want_assertions_signed: false, encryption_certificates: [Saml::Kit::Certificate.new(encryption_pem, use: :encryption)]) }
- let(:encryption_pem) { Saml::Kit.configuration.stripped_encryption_certificate }
+ let(:encryption_pem) { Saml::Kit.configuration.encryption_certificate.stripped }
let(:issuer) { FFaker::Internet.uri("https") }
before :each do
@@ -103,6 +103,22 @@ RSpec.describe Saml::Kit::Builders::Response do
expect(decrypted_hash['Assertion']['AuthnStatement']['AuthnContext']).to be_present
expect(decrypted_hash['Assertion']['AuthnStatement']['AuthnContext']['AuthnContextClassRef']).to be_present
end
+
+ it 'generates a signed response and encrypted assertion' do
+ subject.encrypt = true
+ subject.sign = true
+ result = Hash.from_xml(subject.to_xml)
+ expect(result['Response']['Signature']).to be_present
+ expect(result['Response']['EncryptedAssertion']).to be_present
+ end
+
+ it 'generates a signed response and assertion' do
+ subject.encrypt = false
+ subject.sign = true
+ result = Hash.from_xml(subject.to_xml)
+ expect(result['Response']['Signature']).to be_present
+ expect(result['Response']['Assertion']['Signature']).to be_present
+ end
end
describe "#destination" do
spec/saml/builders/service_provider_metadata_spec.rb
@@ -2,10 +2,16 @@ require 'spec_helper'
RSpec.describe Saml::Kit::Builders::ServiceProviderMetadata do
let(:assertion_consumer_service_url) { FFaker::Internet.http_url }
+ let(:email) { FFaker::Internet.email }
+ let(:org_name) { FFaker::Movie.title }
+ let(:url) { FFaker::Internet.uri("https") }
let(:entity_id) { FFaker::Internet.uri("https") }
it 'builds the service provider metadata' do
+ subject.contact_email = email
subject.entity_id = entity_id
+ subject.organization_name = org_name
+ subject.organization_url = url
subject.add_assertion_consumer_service(assertion_consumer_service_url, binding: :http_post)
subject.name_id_formats = [
Saml::Kit::Namespaces::PERSISTENT,
@@ -32,8 +38,13 @@ RSpec.describe Saml::Kit::Builders::ServiceProviderMetadata do
expect(result['EntityDescriptor']['Signature']).to be_present
expect(result['EntityDescriptor']['SPSSODescriptor']['KeyDescriptor'].map { |x| x['use'] }).to match_array(['signing', 'encryption'])
expect(result['EntityDescriptor']['SPSSODescriptor']['KeyDescriptor'].map { |x| x['KeyInfo']['X509Data']['X509Certificate'] }).to match_array([
- Saml::Kit.configuration.stripped_signing_certificate,
- Saml::Kit.configuration.stripped_encryption_certificate,
+ Saml::Kit.configuration.signing_certificate.stripped,
+ Saml::Kit.configuration.encryption_certificate.stripped,
])
+ expect(result['EntityDescriptor']['Organization']['OrganizationName']).to eql(org_name)
+ expect(result['EntityDescriptor']['Organization']['OrganizationDisplayName']).to eql(org_name)
+ expect(result['EntityDescriptor']['Organization']['OrganizationURL']).to eql(url)
+ expect(result['EntityDescriptor']['ContactPerson']['contactType']).to eql("technical")
+ expect(result['EntityDescriptor']['ContactPerson']['Company']).to eql("mailto:#{email}")
end
end
spec/saml/certificate_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
RSpec.describe Saml::Kit::Certificate do
- subject { described_class.new(Saml::Kit.configuration.stripped_signing_certificate, use: :signing) }
+ subject { Saml::Kit.configuration.signing_certificate }
describe "#fingerprint" do
it 'returns a fingerprint' do
spec/saml/composite_metadata_spec.rb
@@ -0,0 +1,171 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::CompositeMetadata do
+ subject { described_class.new(xml) }
+ let(:post_binding) { Saml::Kit::Bindings::HTTP_POST }
+ let(:redirect_binding) { Saml::Kit::Bindings::HTTP_REDIRECT }
+ let(:sign_on_service) { FFaker::Internet.uri("https") }
+ let(:assertion_consumer_service) { FFaker::Internet.uri("https") }
+ let(:sp_logout_service) { FFaker::Internet.uri("https") }
+ let(:idp_logout_service) { FFaker::Internet.uri("https") }
+ let(:entity_id) { FFaker::Internet.uri("https") }
+ let(:xml) do
+ <<-XML
+<EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{Saml::Kit::Id.generate}" entityID="#{entity_id}">
+ <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
+ <KeyDescriptor use="signing">
+ <KeyInfo xmlns="#{Saml::Kit::Namespaces::XMLDSIG}">
+ <X509Data>
+ <X509Certificate>SP-Signing-Certificate</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+ <KeyDescriptor use="encryption">
+ <KeyInfo xmlns="#{Saml::Kit::Namespaces::XMLDSIG}">
+ <X509Data>
+ <X509Certificate>SP-Encryption-Certificate</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+ <SingleLogoutService Binding="#{post_binding}" Location="#{sp_logout_service}"/>
+ <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
+ <AssertionConsumerService Binding="#{post_binding}" Location="#{assertion_consumer_service}" index="0" isDefault="true"/>
+ </SPSSODescriptor>
+ <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
+ <KeyDescriptor use="signing">
+ <KeyInfo xmlns="#{Saml::Kit::Namespaces::XMLDSIG}">
+ <X509Data>
+ <X509Certificate>IDP-Signing-Certificate</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+ <KeyDescriptor use="encryption">
+ <KeyInfo xmlns="#{Saml::Kit::Namespaces::XMLDSIG}">
+ <X509Data>
+ <X509Certificate>IDP-Encryption-Certificate</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+ <SingleLogoutService Binding="#{post_binding}" Location="#{idp_logout_service}"/>
+ <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
+ <SingleSignOnService Binding="#{post_binding}" Location="#{sign_on_service}"/>
+ <SingleSignOnService Binding="#{redirect_binding}" Location="#{sign_on_service}"/>
+ <Attribute xmlns="#{Saml::Kit::Namespaces::ASSERTION}" Name="id" ></Attribute>
+ </IDPSSODescriptor>
+ <Organization>
+ <OrganizationName xml:lang="en">Acme, Inc</OrganizationName>
+ <OrganizationDisplayName xml:lang="en">Acme, Inc</OrganizationDisplayName>
+ <OrganizationURL xml:lang="en">http://localhost:5000/</OrganizationURL>
+ </Organization>
+ <ContactPerson contactType="technical">
+ <Company>mailto:hi@example.com</Company>
+ </ContactPerson>
+</EntityDescriptor>
+ XML
+ end
+
+ describe "#single_sign_on_services" do
+ it 'returns the single sign on services from the idp' do
+ expect(subject.single_sign_on_services).to match_array([
+ Saml::Kit::Bindings::HttpPost.new(location: sign_on_service),
+ Saml::Kit::Bindings::HttpRedirect.new(location: sign_on_service),
+ ])
+ end
+ end
+
+ describe "#single_sign_on_service_for" do
+ it 'returns the post binding' do
+ expect(subject.single_sign_on_service_for(binding: :http_post)).to eql(
+ Saml::Kit::Bindings::HttpPost.new(location: sign_on_service)
+ )
+ end
+ end
+
+ it { expect(subject.want_authn_requests_signed).to be_truthy }
+ it { expect(subject.attributes).to match_array([name: 'id', format: nil]) }
+ it { expect(subject.login_request_for(binding: :http_post)).to be_present }
+ it do
+ expect(subject.assertion_consumer_services).to match_array([
+ Saml::Kit::Bindings::HttpPost.new(location: assertion_consumer_service)
+ ])
+ end
+ it do
+ expect(subject.assertion_consumer_service_for(binding: :http_post)).to eql(
+ Saml::Kit::Bindings::HttpPost.new(location: assertion_consumer_service)
+ )
+ end
+ it { expect(subject.want_assertions_signed).to be_truthy }
+ it { expect(subject.entity_id).to eql(entity_id) }
+ it { expect(subject.name_id_formats).to match_array([Saml::Kit::Namespaces::PERSISTENT]) }
+ it do
+ expect(subject.certificates).to match_array([
+ Saml::Kit::Certificate.new('SP-Signing-Certificate', use: :signing),
+ Saml::Kit::Certificate.new('SP-Encryption-Certificate', use: :encryption),
+ Saml::Kit::Certificate.new('IDP-Signing-Certificate', use: :signing),
+ Saml::Kit::Certificate.new('IDP-Encryption-Certificate', use: :encryption),
+ ])
+ end
+
+ it do
+ expect(subject.encryption_certificates).to match_array([
+ Saml::Kit::Certificate.new('SP-Encryption-Certificate', use: :encryption),
+ Saml::Kit::Certificate.new('IDP-Encryption-Certificate', use: :encryption),
+ ])
+ end
+ it do
+ expect(subject.signing_certificates).to match_array([
+ Saml::Kit::Certificate.new('SP-Signing-Certificate', use: :signing),
+ Saml::Kit::Certificate.new('IDP-Signing-Certificate', use: :signing),
+ ])
+ end
+ it do
+ expect(subject.services('SingleLogoutService')).to match_array([
+ Saml::Kit::Bindings::HttpPost.new(location: sp_logout_service),
+ Saml::Kit::Bindings::HttpPost.new(location: idp_logout_service),
+ ])
+ end
+ it do
+ expect(subject.service_for(type: 'SingleLogoutService', binding: :http_post)).to eql(
+ Saml::Kit::Bindings::HttpPost.new(location: sp_logout_service)
+ )
+ end
+ it do
+ expect(subject.services('AssertionConsumerService')).to match_array([
+ Saml::Kit::Bindings::HttpPost.new(location: assertion_consumer_service),
+ ])
+ end
+ it do
+ expect(subject.service_for(type: 'AssertionConsumerService', binding: :http_post)).to eql(
+ Saml::Kit::Bindings::HttpPost.new(location: assertion_consumer_service),
+ )
+ end
+ it do
+ expect(subject.services('SingleSignOnService')).to match_array([
+ Saml::Kit::Bindings::HttpPost.new(location: sign_on_service),
+ Saml::Kit::Bindings::HttpRedirect.new(location: sign_on_service),
+ ])
+ end
+ it do
+ expect(subject.service_for(type: 'SingleSignOnService', binding: :http_post)).to eql(
+ Saml::Kit::Bindings::HttpPost.new(location: sign_on_service),
+ )
+ end
+
+ it do
+ expect(subject.single_logout_services).to match_array([
+ Saml::Kit::Bindings::HttpPost.new(location: sp_logout_service),
+ Saml::Kit::Bindings::HttpPost.new(location: idp_logout_service),
+ ])
+ end
+
+ it do
+ expect(subject.single_logout_service_for(binding: :http_post)).to eql(
+ Saml::Kit::Bindings::HttpPost.new(location: sp_logout_service)
+ )
+ end
+
+ it do
+ user = double(:user, name_id_for: SecureRandom.uuid)
+ expect(subject.logout_request_for(user)).to be_present
+ end
+end
spec/saml/composite_metadata_spec.rb.orig
@@ -0,0 +1,187 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::CompositeMetadata do
+ subject { described_class.new(xml) }
+ let(:post_binding) { Saml::Kit::Bindings::HTTP_POST }
+ let(:redirect_binding) { Saml::Kit::Bindings::HTTP_REDIRECT }
+ let(:sign_on_service) { FFaker::Internet.uri("https") }
+ let(:assertion_consumer_service) { FFaker::Internet.uri("https") }
+ let(:sp_logout_service) { FFaker::Internet.uri("https") }
+ let(:idp_logout_service) { FFaker::Internet.uri("https") }
+ let(:entity_id) { FFaker::Internet.uri("https") }
+ let(:xml) do
+ <<-XML
+<EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{Saml::Kit::Id.generate}" entityID="#{entity_id}">
+ <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
+ <KeyDescriptor use="signing">
+ <KeyInfo xmlns="#{Saml::Kit::Namespaces::XMLDSIG}">
+ <X509Data>
+ <X509Certificate>SP-Signing-Certificate</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+ <KeyDescriptor use="encryption">
+ <KeyInfo xmlns="#{Saml::Kit::Namespaces::XMLDSIG}">
+ <X509Data>
+ <X509Certificate>SP-Encryption-Certificate</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+ <SingleLogoutService Binding="#{post_binding}" Location="#{sp_logout_service}"/>
+ <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
+ <AssertionConsumerService Binding="#{post_binding}" Location="#{assertion_consumer_service}" index="0" isDefault="true"/>
+ </SPSSODescriptor>
+ <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
+ <KeyDescriptor use="signing">
+ <KeyInfo xmlns="#{Saml::Kit::Namespaces::XMLDSIG}">
+ <X509Data>
+ <X509Certificate>IDP-Signing-Certificate</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+ <KeyDescriptor use="encryption">
+ <KeyInfo xmlns="#{Saml::Kit::Namespaces::XMLDSIG}">
+ <X509Data>
+ <X509Certificate>IDP-Encryption-Certificate</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+ <SingleLogoutService Binding="#{post_binding}" Location="#{idp_logout_service}"/>
+ <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
+ <SingleSignOnService Binding="#{post_binding}" Location="#{sign_on_service}"/>
+ <SingleSignOnService Binding="#{redirect_binding}" Location="#{sign_on_service}"/>
+ <Attribute xmlns="#{Saml::Kit::Namespaces::ASSERTION}" Name="id" ></Attribute>
+ </IDPSSODescriptor>
+ <Organization>
+ <OrganizationName xml:lang="en">Acme, Inc</OrganizationName>
+ <OrganizationDisplayName xml:lang="en">Acme, Inc</OrganizationDisplayName>
+ <OrganizationURL xml:lang="en">http://localhost:5000/</OrganizationURL>
+ </Organization>
+ <ContactPerson contactType="technical">
+ <Company>mailto:hi@example.com</Company>
+ </ContactPerson>
+</EntityDescriptor>
+ XML
+ end
+
+ describe "#single_sign_on_services" do
+ it 'returns the single sign on services from the idp' do
+ expect(subject.single_sign_on_services).to match_array([
+ Saml::Kit::Bindings::HttpPost.new(location: sign_on_service),
+ Saml::Kit::Bindings::HttpRedirect.new(location: sign_on_service),
+ ])
+ end
+ end
+
+ describe "#single_sign_on_service_for" do
+<<<<<<< HEAD
+ it 'returns the http post binding' do
+=======
+ it 'returns the post binding' do
+>>>>>>> 14771fc562487f9c80443197a89e776d256f47c2
+ expect(subject.single_sign_on_service_for(binding: :http_post)).to eql(
+ Saml::Kit::Bindings::HttpPost.new(location: sign_on_service)
+ )
+ end
+<<<<<<< HEAD
+
+ it 'returns the http redirect binding' do
+ expect(subject.single_sign_on_service_for(binding: :http_redirect)).to eql(
+ Saml::Kit::Bindings::HttpRedirect.new(location: sign_on_service)
+ )
+ end
+ end
+
+ describe "#assertion_consumer_services" do
+=======
+ end
+
+ it { expect(subject.want_authn_requests_signed).to be_truthy }
+ it { expect(subject.attributes).to match_array([name: 'id', format: nil]) }
+ it { expect(subject.login_request_for(binding: :http_post)).to be_present }
+ it do
+ expect(subject.assertion_consumer_services).to match_array([
+ Saml::Kit::Bindings::HttpPost.new(location: assertion_consumer_service)
+ ])
+ end
+ it do
+ expect(subject.assertion_consumer_service_for(binding: :http_post)).to eql(
+ Saml::Kit::Bindings::HttpPost.new(location: assertion_consumer_service)
+ )
+ end
+ it { expect(subject.want_assertions_signed).to be_truthy }
+ it { expect(subject.entity_id).to eql(entity_id) }
+ it { expect(subject.name_id_formats).to match_array([Saml::Kit::Namespaces::PERSISTENT]) }
+ it do
+ expect(subject.certificates).to match_array([
+ Saml::Kit::Certificate.new('SP-Signing-Certificate', use: :signing),
+ Saml::Kit::Certificate.new('SP-Encryption-Certificate', use: :encryption),
+ Saml::Kit::Certificate.new('IDP-Signing-Certificate', use: :signing),
+ Saml::Kit::Certificate.new('IDP-Encryption-Certificate', use: :encryption),
+ ])
+ end
+
+ it do
+ expect(subject.encryption_certificates).to match_array([
+ Saml::Kit::Certificate.new('SP-Encryption-Certificate', use: :encryption),
+ Saml::Kit::Certificate.new('IDP-Encryption-Certificate', use: :encryption),
+ ])
+ end
+ it do
+ expect(subject.signing_certificates).to match_array([
+ Saml::Kit::Certificate.new('SP-Signing-Certificate', use: :signing),
+ Saml::Kit::Certificate.new('IDP-Signing-Certificate', use: :signing),
+ ])
+ end
+ it do
+ expect(subject.services('SingleLogoutService')).to match_array([
+ Saml::Kit::Bindings::HttpPost.new(location: sp_logout_service),
+ Saml::Kit::Bindings::HttpPost.new(location: idp_logout_service),
+ ])
+ end
+ it do
+ expect(subject.service_for(type: 'SingleLogoutService', binding: :http_post)).to eql(
+ Saml::Kit::Bindings::HttpPost.new(location: sp_logout_service)
+ )
+ end
+ it do
+ expect(subject.services('AssertionConsumerService')).to match_array([
+ Saml::Kit::Bindings::HttpPost.new(location: assertion_consumer_service),
+ ])
+ end
+ it do
+ expect(subject.service_for(type: 'AssertionConsumerService', binding: :http_post)).to eql(
+ Saml::Kit::Bindings::HttpPost.new(location: assertion_consumer_service),
+ )
+ end
+ it do
+ expect(subject.services('SingleSignOnService')).to match_array([
+ Saml::Kit::Bindings::HttpPost.new(location: sign_on_service),
+ Saml::Kit::Bindings::HttpRedirect.new(location: sign_on_service),
+ ])
+ end
+ it do
+ expect(subject.service_for(type: 'SingleSignOnService', binding: :http_post)).to eql(
+ Saml::Kit::Bindings::HttpPost.new(location: sign_on_service),
+ )
+ end
+
+ it do
+ expect(subject.single_logout_services).to match_array([
+ Saml::Kit::Bindings::HttpPost.new(location: sp_logout_service),
+ Saml::Kit::Bindings::HttpPost.new(location: idp_logout_service),
+ ])
+ end
+
+ it do
+ expect(subject.single_logout_service_for(binding: :http_post)).to eql(
+ Saml::Kit::Bindings::HttpPost.new(location: sp_logout_service)
+ )
+ end
+
+ it do
+ user = double(:user, name_id_for: SecureRandom.uuid)
+ expect(subject.logout_request_for(user)).to be_present
+>>>>>>> 14771fc562487f9c80443197a89e776d256f47c2
+ end
+end
spec/saml/default_registry_spec.rb
@@ -43,7 +43,37 @@ RSpec.describe Saml::Kit::DefaultRegistry do
expect(result).to be_present
expect(result).to be_instance_of(Saml::Kit::IdentityProviderMetadata)
end
- end
- xit 'registers metadata that serves as both an IDP and SP'
+ it 'registers metadata that serves as both an IDP and SP' do
+ xml = <<-XML
+<EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{Saml::Kit::Id.generate}" entityID="#{entity_id}">
+ <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
+ <SingleLogoutService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}"/>
+ <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
+ <AssertionConsumerService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}" index="0" isDefault="true"/>
+ </SPSSODescriptor>
+ <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
+ <SingleLogoutService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}"/>
+ <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
+ <SingleSignOnService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}"/>
+ <SingleSignOnService Binding="#{Saml::Kit::Bindings::HTTP_REDIRECT}" Location="#{FFaker::Internet.uri("https")}"/>
+ </IDPSSODescriptor>
+ <Organization>
+ <OrganizationName xml:lang="en">Acme, Inc</OrganizationName>
+ <OrganizationDisplayName xml:lang="en">Acme, Inc</OrganizationDisplayName>
+ <OrganizationURL xml:lang="en">http://localhost:5000/</OrganizationURL>
+ </Organization>
+ <ContactPerson contactType="technical">
+ <Company>mailto:hi@example.com</Company>
+ </ContactPerson>
+</EntityDescriptor>
+ XML
+ stub_request(:get, url).to_return(status: 200, body: xml)
+ subject.register_url(url)
+
+ result = subject.metadata_for(entity_id)
+ expect(result).to be_present
+ expect(result).to be_instance_of(Saml::Kit::CompositeMetadata)
+ end
+ end
end
spec/saml/http_post_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Bindings::HttpPost do
+ describe "equality" do
+ let(:location) { FFaker::Internet.uri("https") }
+ subject { Saml::Kit::Bindings::HttpPost.new(location: location) }
+
+ it 'is referentially equal' do
+ expect(subject).to eql(subject)
+ end
+
+ it 'is equal by value' do
+ expect(subject).to eql(
+ Saml::Kit::Bindings::HttpPost.new(location: location)
+ )
+ end
+
+ it 'is not equal' do
+ expect(subject).to_not eql(
+ Saml::Kit::Bindings::HttpPost.new(
+ location: FFaker::Internet.uri("https")
+ )
+ )
+ end
+ end
+end
spec/saml/metadata_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Metadata do
+ describe ".from" do
+ subject { described_class }
+
+ it 'returns an identity provider metadata' do
+ xml = Saml::Kit::IdentityProviderMetadata.build.to_xml
+ expect(subject.from(xml)).to be_instance_of(Saml::Kit::IdentityProviderMetadata)
+ end
+
+ it 'returns a service provider metadata' do
+ xml = Saml::Kit::ServiceProviderMetadata.build.to_xml
+ expect(subject.from(xml)).to be_instance_of(Saml::Kit::ServiceProviderMetadata)
+ end
+
+ it 'returns a composite' do
+ xml = <<-XML
+<EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{Saml::Kit::Id.generate}" entityID="#{FFaker::Internet.uri("https")}">
+ <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
+ <SingleLogoutService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}"/>
+ <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
+ <AssertionConsumerService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}" index="0" isDefault="true"/>
+ </SPSSODescriptor>
+ <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
+ <SingleLogoutService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}"/>
+ <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
+ <SingleSignOnService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}"/>
+ <SingleSignOnService Binding="#{Saml::Kit::Bindings::HTTP_REDIRECT}" Location="#{FFaker::Internet.uri("https")}"/>
+ </IDPSSODescriptor>
+ <Organization>
+ <OrganizationName xml:lang="en">Acme, Inc</OrganizationName>
+ <OrganizationDisplayName xml:lang="en">Acme, Inc</OrganizationDisplayName>
+ <OrganizationURL xml:lang="en">http://localhost:5000/</OrganizationURL>
+ </Organization>
+ <ContactPerson contactType="technical">
+ <Company>mailto:hi@example.com</Company>
+ </ContactPerson>
+</EntityDescriptor>
+ XML
+ result = subject.from(xml)
+ expect(result).to be_present
+
+ expect(result.single_sign_on_services.count).to eql(2)
+ expect(result.assertion_consumer_services.count).to eql(1)
+ expect(result.single_logout_services.count).to eql(2)
+ end
+ end
+end
spec/saml/service_provider_metadata_spec.rb
@@ -20,8 +20,8 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
it 'returns each of the certificates' do
expect(subject.certificates).to match_array([
- Saml::Kit::Certificate.new(Saml::Kit.configuration.stripped_signing_certificate, use: :signing),
- Saml::Kit::Certificate.new(Saml::Kit.configuration.stripped_encryption_certificate, use: :encryption),
+ Saml::Kit::Certificate.new(Saml::Kit.configuration.signing_certificate.stripped, use: :signing),
+ Saml::Kit::Certificate.new(Saml::Kit.configuration.encryption_certificate.stripped, use: :encryption),
])
end
saml-kit.gemspec
@@ -25,6 +25,7 @@ Gem::Specification.new do |spec|
spec.add_dependency "activemodel", ">= 4.2.0"
spec.add_dependency "builder", "~> 3.2"
spec.add_dependency "nokogiri", "~> 1.8"
+ spec.add_dependency "tilt", "~> 2.0"
spec.add_dependency "xmldsig", "~> 0.6"
spec.add_development_dependency "bundler", "~> 1.15"
spec.add_development_dependency "ffaker", "~> 2.7"