Comparing changes
v0.2.1
→
v0.2.2
54 commits
48 files changed
Commits
Changed files (48)
lib
saml
kit
bindings
builders
spec
saml
bindings
builders
exe/saml-kit-create-self-signed-certificate
@@ -0,0 +1,21 @@
+#!/usr/bin/env ruby
+require 'saml/kit'
+
+password = STDIN.read.strip
+certificate, private_key = Saml::Kit::SelfSignedCertificate.new(password).create
+
+puts "** BEGIN File Format **"
+print certificate
+puts private_key
+puts "***********************"
+
+puts
+
+puts "*** BEGIN ENV Format **"
+puts certificate.inspect
+puts private_key.inspect
+puts "***********************"
+
+puts
+puts "Private Key Password:"
+puts password.inspect
lib/saml/kit/bindings/http_post.rb
@@ -9,7 +9,6 @@ module Saml
end
def serialize(builder, relay_state: nil)
- builder.sign = true
builder.destination = location
document = builder.build
saml_params = {
lib/saml/kit/builders/authentication_request.rb
@@ -0,0 +1,48 @@
+module Saml
+ module Kit
+ module Builders
+ class AuthenticationRequest
+ attr_accessor :id, :now, :issuer, :assertion_consumer_service_url, :name_id_format, :sign, :destination
+ attr_accessor :version
+
+ def initialize(configuration: Saml::Kit.configuration, sign: true)
+ @id = Id.generate
+ @issuer = configuration.issuer
+ @name_id_format = Namespaces::PERSISTENT
+ @now = Time.now.utc
+ @version = "2.0"
+ @sign = sign
+ 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
+ end
+
+ def build
+ Saml::Kit::AuthenticationRequest.new(to_xml)
+ end
+
+ private
+
+ def request_options
+ options = {
+ "xmlns:samlp" => Namespaces::PROTOCOL,
+ "xmlns:saml" => Namespaces::ASSERTION,
+ ID: id,
+ Version: version,
+ IssueInstant: now.utc.iso8601,
+ Destination: destination,
+ }
+ options[:AssertionConsumerServiceURL] = assertion_consumer_service_url if assertion_consumer_service_url.present?
+ options
+ end
+ end
+ end
+ end
+end
lib/saml/kit/builders/identity_provider_metadata.rb
@@ -0,0 +1,103 @@
+module Saml
+ module Kit
+ module Builders
+ class IdentityProviderMetadata
+ 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
+
+ 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
+ @sign = true
+ @want_authn_requests_signed = true
+ end
+
+ def add_single_sign_on_service(url, binding: :http_post)
+ @single_sign_on_urls.push(location: url, binding: Bindings.binding_for(binding))
+ end
+
+ def add_single_logout_service(url, binding: :http_post)
+ @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
+
+ private
+
+ def entity_descriptor_options
+ {
+ 'xmlns': Namespaces::METADATA,
+ 'xmlns:ds': Namespaces::XMLDSIG,
+ 'xmlns:saml': Namespaces::ASSERTION,
+ ID: id,
+ entityID: entity_id,
+ }
+ end
+
+ def idp_sso_descriptor_options
+ {
+ WantAuthnRequestsSigned: want_authn_requests_signed,
+ protocolSupportEnumeration: Namespaces::PROTOCOL,
+ }
+ end
+ end
+ end
+ end
+end
lib/saml/kit/builders/logout_request.rb
@@ -0,0 +1,55 @@
+module Saml
+ module Kit
+ module Builders
+ class LogoutRequest
+ attr_accessor :id, :destination, :issuer, :name_id_format, :now
+ attr_accessor :sign, :version
+ attr_reader :user
+
+ def initialize(user, configuration: Saml::Kit.configuration, sign: true)
+ @user = user
+ @id = "_#{SecureRandom.uuid}"
+ @issuer = configuration.issuer
+ @name_id_format = Saml::Kit::Namespaces::PERSISTENT
+ @now = Time.now.utc
+ @version = "2.0"
+ @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
+
+ private
+
+ def logout_request_options
+ {
+ ID: id,
+ Version: version,
+ IssueInstant: now.utc.iso8601,
+ Destination: destination,
+ xmlns: Namespaces::PROTOCOL,
+ }
+ end
+
+ def name_id_options
+ {
+ Format: name_id_format,
+ xmlns: Namespaces::ASSERTION,
+ }
+ end
+ end
+ end
+ end
+end
lib/saml/kit/builders/logout_response.rb
@@ -0,0 +1,50 @@
+module Saml
+ module Kit
+ module Builders
+ class LogoutResponse
+ attr_accessor :id, :issuer, :version, :status_code, :sign, :now, :destination
+ attr_reader :request
+
+ def initialize(user, request, configuration: Saml::Kit.configuration, sign: true)
+ @user = user
+ @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
+ end
+
+ def build
+ Saml::Kit::LogoutResponse.new(to_xml, request_id: request.id)
+ end
+
+ private
+
+ def logout_response_options
+ {
+ xmlns: Namespaces::PROTOCOL,
+ ID: id,
+ Version: version,
+ IssueInstant: now.utc.iso8601,
+ Destination: destination,
+ InResponseTo: request.id,
+ }
+ end
+ end
+ end
+ end
+end
lib/saml/kit/builders/response.rb
@@ -0,0 +1,182 @@
+module Saml
+ module Kit
+ module Builders
+ class Response
+ attr_reader :user, :request
+ attr_accessor :id, :reference_id, :now
+ attr_accessor :version, :status_code
+ attr_accessor :issuer, :sign, :destination, :encrypt
+
+ def initialize(user, request)
+ @user = user
+ @request = request
+ @id = Id.generate
+ @reference_id = Id.generate
+ @now = Time.now.utc
+ @version = "2.0"
+ @status_code = Namespaces::SUCCESS
+ @issuer = configuration.issuer
+ @destination = destination_for(request)
+ @sign = want_assertions_signed
+ @encrypt = false
+ end
+
+ def want_assertions_signed
+ request.provider.want_assertions_signed
+ rescue => error
+ Saml::Kit.logger.error(error)
+ 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
+ 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
+ end
+
+ def destination_for(request)
+ if request.signed? && request.trusted?
+ request.assertion_consumer_service_url || request.provider.assertion_consumer_service_for(binding: :http_post).try(:location)
+ else
+ request.provider.assertion_consumer_service_for(binding: :http_post).try(:location)
+ end
+ end
+
+ def configuration
+ Saml::Kit.configuration
+ end
+
+ def response_options
+ {
+ ID: id,
+ Version: version,
+ IssueInstant: now.iso8601,
+ Destination: destination,
+ Consent: Namespaces::UNSPECIFIED,
+ InResponseTo: request.id,
+ 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
+end
lib/saml/kit/builders/service_provider_metadata.rb
@@ -0,0 +1,89 @@
+module Saml
+ module Kit
+ module Builders
+ class ServiceProviderMetadata
+ attr_accessor :id, :entity_id, :acs_urls, :logout_urls, :name_id_formats, :sign
+ attr_accessor :want_assertions_signed
+
+ def initialize(configuration = Saml::Kit.configuration)
+ @id = Id.generate
+ @configuration = configuration
+ @entity_id = configuration.issuer
+ @acs_urls = []
+ @logout_urls = []
+ @name_id_formats = [Namespaces::PERSISTENT]
+ @sign = true
+ @want_assertions_signed = true
+ end
+
+ def add_assertion_consumer_service(url, binding: :http_post)
+ @acs_urls.push(location: url, binding: Bindings.binding_for(binding))
+ end
+
+ def add_single_logout_service(url, binding: :http_post)
+ @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
+
+ private
+
+ def entity_descriptor_options
+ {
+ 'xmlns': Namespaces::METADATA,
+ ID: id,
+ entityID: entity_id,
+ }
+ end
+
+ def descriptor_options
+ {
+ AuthnRequestsSigned: sign,
+ WantAssertionsSigned: want_assertions_signed,
+ protocolSupportEnumeration: Namespaces::PROTOCOL,
+ }
+ end
+ end
+ end
+ end
+end
lib/saml/kit/authentication_request.rb
@@ -7,7 +7,7 @@ module Saml
super(xml, name: "AuthnRequest")
end
- def acs_url
+ def assertion_consumer_service_url
to_h[name]['AssertionConsumerServiceURL']
end
@@ -15,54 +15,16 @@ module Saml
to_h[name]['NameIDPolicy']['Format']
end
- def response_for(user)
- Response::Builder.new(user, self)
- end
-
- private
-
- class Builder
- attr_accessor :id, :now, :issuer, :acs_url, :name_id_format, :sign, :destination
- attr_accessor :version
-
- def initialize(configuration: Saml::Kit.configuration, sign: true)
- @id = SecureRandom.uuid
- @issuer = configuration.issuer
- @name_id_format = Namespaces::PERSISTENT
- @now = Time.now.utc
- @version = "2.0"
- @sign = sign
- 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
- end
-
- def build
- AuthenticationRequest.new(to_xml)
- end
-
- private
-
- def request_options
- options = {
- "xmlns:samlp" => Namespaces::PROTOCOL,
- "xmlns:saml" => Namespaces::ASSERTION,
- ID: "_#{id}",
- Version: version,
- IssueInstant: now.utc.iso8601,
- Destination: destination,
- }
- options[:AssertionConsumerServiceURL] = acs_url if acs_url.present?
- options
+ def response_for(user, binding:, relay_state: nil)
+ response_binding = provider.assertion_consumer_service_for(binding: binding)
+ builder = Saml::Kit::Response.builder(user, self) do |x|
+ x.sign = provider.want_assertions_signed
+ yield x if block_given?
end
+ response_binding.serialize(builder, relay_state: relay_state)
end
+
+ Builder = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Saml::Kit::AuthenticationRequest::Builder', 'Saml::Kit::Builders::AuthenticationRequest')
end
end
end
lib/saml/kit/buildable.rb
@@ -0,0 +1,21 @@
+module Saml
+ module Kit
+ module Buildable
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def build(*args)
+ builder(*args).tap do |x|
+ yield x if block_given?
+ end.build
+ end
+
+ def builder(*args)
+ builder_class.new(*args).tap do |builder|
+ yield builder if block_given?
+ end
+ end
+ end
+ end
+ end
+end
lib/saml/kit/builders.rb
@@ -0,0 +1,13 @@
+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'
+
+module Saml
+ module Kit
+ module Builders
+ end
+ end
+end
lib/saml/kit/document.rb
@@ -5,6 +5,7 @@ module Saml
include XsdValidatable
include ActiveModel::Validations
include Trustable
+ include Buildable
validates_presence_of :content
validates_presence_of :id
validate :must_match_xsd
@@ -76,6 +77,21 @@ module Saml
Saml::Kit.logger.error(error)
InvalidDocument.new(xml)
end
+
+ def builder_class
+ case name
+ when Saml::Kit::Response.to_s
+ Saml::Kit::Builders::Response
+ when Saml::Kit::LogoutResponse.to_s
+ Saml::Kit::Builders::LogoutResponse
+ when Saml::Kit::AuthenticationRequest.to_s
+ Saml::Kit::Builders::AuthenticationRequest
+ when Saml::Kit::LogoutRequest.to_s
+ Saml::Kit::Builders::LogoutRequest
+ else
+ raise ArgumentError.new("Unknown SAML Document #{name}")
+ end
+ end
end
private
lib/saml/kit/id.rb
@@ -0,0 +1,9 @@
+module Saml
+ module Kit
+ class Id
+ def self.generate
+ "_#{SecureRandom.uuid}"
+ end
+ end
+ end
+end
lib/saml/kit/identity_provider_metadata.rb
@@ -29,105 +29,20 @@ module Saml
end
end
- private
-
- class Builder
- 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
-
- def initialize(configuration = Saml::Kit.configuration)
- @id = SecureRandom.uuid
- @entity_id = configuration.issuer
- @attributes = []
- @name_id_formats = [Namespaces::PERSISTENT]
- @single_sign_on_urls = []
- @logout_urls = []
- @configuration = configuration
- @sign = true
- @want_authn_requests_signed = true
- end
-
- def add_single_sign_on_service(url, binding: :http_post)
- @single_sign_on_urls.push(location: url, binding: Bindings.binding_for(binding))
- end
-
- def add_single_logout_service(url, binding: :http_post)
- @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
- IdentityProviderMetadata.new(to_xml)
- end
-
- private
-
- def entity_descriptor_options
- {
- 'xmlns': Namespaces::METADATA,
- 'xmlns:ds': Namespaces::XMLDSIG,
- 'xmlns:saml': Namespaces::ASSERTION,
- ID: "_#{id}",
- entityID: entity_id,
- }
+ def login_request_for(binding:, relay_state: nil)
+ builder = Saml::Kit::AuthenticationRequest.builder do |x|
+ x.sign = want_authn_requests_signed
+ yield x if block_given?
end
+ request_binding = single_sign_on_service_for(binding: binding)
+ request_binding.serialize(builder, relay_state: relay_state)
+ end
- def idp_sso_descriptor_options
- {
- WantAuthnRequestsSigned: want_authn_requests_signed,
- protocolSupportEnumeration: Namespaces::PROTOCOL,
- }
- end
+ def self.builder_class
+ Saml::Kit::Builders::IdentityProviderMetadata
end
+
+ Builder = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Saml::Kit::IdentityProviderMetadata::Builder', 'Saml::Kit::Builders::IdentityProviderMetadata')
end
end
end
lib/saml/kit/logout_request.rb
@@ -18,61 +18,15 @@ module Saml
urls.first
end
- def response_for(user)
- LogoutResponse::Builder.new(user, self)
- end
-
- private
-
- class Builder
- attr_accessor :id, :destination, :issuer, :name_id_format, :now
- attr_accessor :sign, :version
- attr_reader :user
-
- def initialize(user, configuration: Saml::Kit.configuration, sign: true)
- @user = user
- @id = SecureRandom.uuid
- @issuer = configuration.issuer
- @name_id_format = Saml::Kit::Namespaces::PERSISTENT
- @now = Time.now.utc
- @version = "2.0"
- @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
-
- private
-
- def logout_request_options
- {
- ID: "_#{id}",
- Version: version,
- IssueInstant: now.utc.iso8601,
- Destination: destination,
- xmlns: Namespaces::PROTOCOL,
- }
- end
-
- def name_id_options
- {
- Format: name_id_format,
- xmlns: Namespaces::ASSERTION,
- }
+ def response_for(user, binding:, relay_state: nil)
+ builder = Saml::Kit::LogoutResponse.builder(user, self) do |x|
+ yield x if block_given?
end
+ response_binding = provider.single_logout_service_for(binding: binding)
+ response_binding.serialize(builder, relay_state: relay_state)
end
+
+ Builder = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Saml::Kit::LogoutRequest::Builder', 'Saml::Kit::Builders::LogoutRequest')
end
end
end
lib/saml/kit/logout_response.rb
@@ -8,56 +8,7 @@ module Saml
super(xml, name: "LogoutResponse")
end
- private
-
- class Builder
- attr_accessor :id, :issuer, :version, :status_code, :sign, :now, :destination
- attr_reader :request
-
- def initialize(user, request, configuration: Saml::Kit.configuration, sign: true)
- @user = user
- @now = Time.now.utc
- @request = request
- @id = SecureRandom.uuid
- @version = "2.0"
- @status_code = Namespaces::SUCCESS
- @sign = sign
- @issuer = configuration.issuer
- provider = configuration.registry.metadata_for(@issuer)
- if provider
- @destination = provider.single_logout_service_for(binding: :http_post).try(:location)
- end
- 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
- end
-
- def build
- LogoutResponse.new(to_xml, request_id: request.id)
- end
-
- private
-
- def logout_response_options
- {
- xmlns: Namespaces::PROTOCOL,
- ID: "_#{id}",
- Version: version,
- IssueInstant: now.utc.iso8601,
- Destination: destination,
- InResponseTo: request.id,
- }
- end
- end
+ Builder = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Saml::Kit::LogoutResponse::Builder', 'Saml::Kit::Builders::LogoutResponse')
end
end
end
lib/saml/kit/metadata.rb
@@ -3,6 +3,7 @@ module Saml
class Metadata
include ActiveModel::Validations
include XsdValidatable
+ include Buildable
METADATA_XSD = File.expand_path("./xsd/saml-schema-metadata-2.0.xsd", File.dirname(__FILE__)).freeze
validates_presence_of :metadata
@@ -63,6 +64,14 @@ module Saml
service_for(binding: binding, type: 'SingleLogoutService')
end
+ def logout_request_for(user, binding: :http_post, relay_state: nil)
+ builder = Saml::Kit::LogoutRequest.builder(user) do |x|
+ yield x if block_given?
+ end
+ request_binding = single_logout_service_for(binding: binding)
+ request_binding.serialize(builder, relay_state: relay_state)
+ end
+
def matches?(fingerprint, use: :signing)
certificates.find do |certificate|
certificate.for?(use) && certificate.fingerprint == fingerprint
lib/saml/kit/response.rb
@@ -20,11 +20,16 @@ module Saml
end
def attributes
- @attributes ||= Hash[
- assertion.fetch('AttributeStatement', {}).fetch('Attribute', []).map do |item|
- [item['Name'].to_sym, item['AttributeValue']]
+ @attributes ||=
+ begin
+ attrs = assertion.fetch('AttributeStatement', {}).fetch('Attribute', [])
+ items = if attrs.is_a? Hash
+ [[attrs["Name"], attrs["AttributeValue"]]]
+ else
+ attrs.map { |item| [item['Name'], item['AttributeValue']] }
+ end
+ Hash[items].with_indifferent_access
end
- ].with_indifferent_access
end
def started_at
@@ -51,7 +56,7 @@ module Saml
@assertion =
begin
if encrypted?
- decrypted = Cryptography.new.decrypt(to_h.fetch(name, {}).fetch('EncryptedAssertion', {}))
+ decrypted = XmlDecryption.new.decrypt(to_h.fetch(name, {}).fetch('EncryptedAssertion', {}))
Saml::Kit.logger.debug(decrypted)
Hash.from_xml(decrypted)['Assertion']
else
@@ -99,182 +104,7 @@ module Saml
Time.at(0).to_datetime
end
- class Builder
- attr_reader :user, :request
- attr_accessor :id, :reference_id, :now
- attr_accessor :version, :status_code
- attr_accessor :issuer, :sign, :destination, :encrypt
-
- def initialize(user, request)
- @user = user
- @request = request
- @id = SecureRandom.uuid
- @reference_id = SecureRandom.uuid
- @now = Time.now.utc
- @version = "2.0"
- @status_code = Namespaces::SUCCESS
- @issuer = configuration.issuer
- @destination = destination_for(request)
- @sign = want_assertions_signed
- @encrypt = false
- end
-
- def want_assertions_signed
- request.provider.want_assertions_signed
- rescue => error
- Saml::Kit.logger.error(error)
- 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
- 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
- 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
- end
-
- def destination_for(request)
- if request.signed? && request.trusted?
- request.acs_url || request.provider.assertion_consumer_service_for(binding: :http_post).try(:location)
- else
- request.provider.assertion_consumer_service_for(binding: :http_post).try(:location)
- end
- end
-
- def configuration
- Saml::Kit.configuration
- end
-
- def response_options
- {
- ID: id.present? ? "_#{id}" : nil,
- Version: version,
- IssueInstant: now.iso8601,
- Destination: destination,
- Consent: Namespaces::UNSPECIFIED,
- InResponseTo: request.id,
- 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.acs_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
+ Builder = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Saml::Kit::Response::Builder', 'Saml::Kit::Builders::Response')
end
end
end
lib/saml/kit/self_signed_certificate.rb
@@ -1,6 +1,8 @@
module Saml
module Kit
class SelfSignedCertificate
+ SUBJECT="/C=CA/ST=Alberta/L=Calgary/O=SamlKit/OU=SamlKit/CN=SamlKit"
+
def initialize(password)
@password = password
end
@@ -9,20 +11,25 @@ module Saml
rsa_key = OpenSSL::PKey::RSA.new(2048)
public_key = rsa_key.public_key
certificate = OpenSSL::X509::Certificate.new
- certificate.subject = certificate.issuer = OpenSSL::X509::Name.parse("/C=CA/ST=Alberta/L=Calgary/O=Xsig/OU=Xsig/CN=Xsig")
+ certificate.subject = certificate.issuer = OpenSSL::X509::Name.parse(SUBJECT)
certificate.not_before = DateTime.now.beginning_of_day
- certificate.not_after = 1.year.from_now.end_of_day
+ certificate.not_after = 30.days.from_now
certificate.public_key = public_key
certificate.serial = 0x0
certificate.version = 2
factory = OpenSSL::X509::ExtensionFactory.new
factory.subject_certificate = factory.issuer_certificate = certificate
- certificate.extensions = [ factory.create_extension("basicConstraints","CA:TRUE", true), factory.create_extension("subjectKeyIdentifier", "hash"), ]
- certificate.add_extension(factory.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always"))
+ certificate.extensions = [
+ factory.create_extension("basicConstraints","CA:TRUE", true),
+ factory.create_extension("subjectKeyIdentifier", "hash"),
+ ]
+ certificate.add_extension(
+ factory.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always")
+ )
certificate.sign(rsa_key, OpenSSL::Digest::SHA256.new)
[
certificate.to_pem,
- rsa_key.to_pem(OpenSSL::Cipher::Cipher.new('des3'), @password)
+ rsa_key.to_pem(OpenSSL::Cipher.new('AES-256-CBC'), @password)
]
end
end
lib/saml/kit/service_provider_metadata.rb
@@ -15,92 +15,15 @@ module Saml
def want_assertions_signed
attribute = document.find_by("/md:EntityDescriptor/md:#{name}").attribute("WantAssertionsSigned")
+ return true if attribute.nil?
attribute.text.downcase == "true"
end
- class Builder
- attr_accessor :id, :entity_id, :acs_urls, :logout_urls, :name_id_formats, :sign
- attr_accessor :want_assertions_signed
-
- def initialize(configuration = Saml::Kit.configuration)
- @id = SecureRandom.uuid
- @configuration = configuration
- @entity_id = configuration.issuer
- @acs_urls = []
- @logout_urls = []
- @name_id_formats = [Namespaces::PERSISTENT]
- @sign = true
- @want_assertions_signed = true
- end
-
- def add_assertion_consumer_service(url, binding: :http_post)
- @acs_urls.push(location: url, binding: Bindings.binding_for(binding))
- end
-
- def add_single_logout_service(url, binding: :http_post)
- @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
- ServiceProviderMetadata.new(to_xml)
- end
-
- private
-
- def entity_descriptor_options
- {
- 'xmlns': Namespaces::METADATA,
- ID: "_#{id}",
- entityID: entity_id,
- }
- end
-
- def descriptor_options
- {
- AuthnRequestsSigned: sign,
- WantAssertionsSigned: want_assertions_signed,
- protocolSupportEnumeration: Namespaces::PROTOCOL,
- }
- end
+ def self.builder_class
+ Saml::Kit::Builders::ServiceProviderMetadata
end
+
+ Builder = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Saml::Kit::ServiceProviderMetadata::Builder', 'Saml::Kit::Builders::ServiceProviderMetadata')
end
end
end
lib/saml/kit/signature.rb
@@ -34,7 +34,7 @@ module Saml
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.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#"
lib/saml/kit/trustable.rb
@@ -30,11 +30,7 @@ module Saml
end
def provider
- registry.metadata_for(issuer)
- end
-
- def registry
- Saml::Kit.configuration.registry
+ Saml::Kit.registry.metadata_for(issuer)
end
def signature_verified!
lib/saml/kit/version.rb
@@ -1,5 +1,5 @@
module Saml
module Kit
- VERSION = "0.2.1"
+ VERSION = "0.2.2"
end
end
lib/saml/kit/cryptography.rb → lib/saml/kit/xml_decryption.rb
@@ -1,6 +1,6 @@
module Saml
module Kit
- class Cryptography
+ class XmlDecryption
attr_reader :private_key
def initialize(private_key = Saml::Kit.configuration.encryption_private_key)
lib/saml/kit.rb
@@ -5,6 +5,7 @@ require "active_support/core_ext/date/calculations"
require "active_support/core_ext/hash/conversions"
require "active_support/core_ext/hash/indifferent_access"
require "active_support/core_ext/numeric/time"
+require "active_support/deprecation"
require "active_support/duration"
require "builder"
require "logger"
@@ -13,6 +14,8 @@ require "nokogiri"
require "securerandom"
require "xmldsig"
+require "saml/kit/buildable"
+require "saml/kit/builders"
require "saml/kit/namespaces"
require "saml/kit/serializable"
require "saml/kit/xsd_validatable"
@@ -26,19 +29,20 @@ require "saml/kit/bindings"
require "saml/kit/certificate"
require "saml/kit/configuration"
require "saml/kit/crypto"
-require "saml/kit/cryptography"
require "saml/kit/default_registry"
require "saml/kit/fingerprint"
require "saml/kit/logout_response"
require "saml/kit/logout_request"
require "saml/kit/metadata"
require "saml/kit/response"
+require "saml/kit/id"
require "saml/kit/identity_provider_metadata"
require "saml/kit/invalid_document"
require "saml/kit/self_signed_certificate"
require "saml/kit/service_provider_metadata"
require "saml/kit/signature"
require "saml/kit/xml"
+require "saml/kit/xml_decryption"
I18n.load_path += Dir[File.expand_path("kit/locales/*.yml", File.dirname(__FILE__))]
@@ -56,6 +60,10 @@ module Saml
def logger
configuration.logger
end
+
+ def registry
+ configuration.registry
+ end
end
end
end
spec/saml/bindings/http_post_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
let(:relay_state) { "ECHO" }
it 'encodes the request using the HTTP-POST encoding for a AuthenticationRequest' do
- builder = Saml::Kit::AuthenticationRequest::Builder.new
+ builder = Saml::Kit::AuthenticationRequest.builder_class.new
url, saml_params = subject.serialize(builder, relay_state: relay_state)
expect(url).to eql(location)
@@ -22,7 +22,7 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
it 'returns a SAMLRequest for a LogoutRequest' do
user = double(:user, name_id_for: SecureRandom.uuid)
- builder = Saml::Kit::LogoutRequest::Builder.new(user)
+ builder = Saml::Kit::LogoutRequest.builder_class.new(user)
url, saml_params = subject.serialize(builder, relay_state: relay_state)
expect(url).to eql(location)
@@ -37,7 +37,7 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
it 'returns a SAMLResponse for a LogoutResponse' do
user = double(:user, name_id_for: SecureRandom.uuid)
request = instance_double(Saml::Kit::AuthenticationRequest, id: SecureRandom.uuid)
- builder = Saml::Kit::LogoutResponse::Builder.new(user, request)
+ builder = Saml::Kit::LogoutResponse.builder_class.new(user, request)
url, saml_params = subject.serialize(builder, relay_state: relay_state)
expect(url).to eql(location)
@@ -50,7 +50,7 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
end
it 'excludes the RelayState when blank' do
- builder = Saml::Kit::AuthenticationRequest::Builder.new
+ builder = Saml::Kit::AuthenticationRequest.builder_class.new
url, saml_params = subject.serialize(builder)
expect(url).to eql(location)
@@ -60,7 +60,7 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
describe "#deserialize" do
it 'deserializes to an AuthnRequest' do
- builder = Saml::Kit::AuthenticationRequest::Builder.new
+ builder = Saml::Kit::AuthenticationRequest.builder_class.new
_, params = subject.serialize(builder)
result = subject.deserialize(params)
expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
@@ -68,7 +68,7 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
it 'deserializes to a LogoutRequest' do
user = double(:user, name_id_for: SecureRandom.uuid)
- builder = Saml::Kit::LogoutRequest::Builder.new(user)
+ builder = Saml::Kit::LogoutRequest.builder_class.new(user)
_, params = subject.serialize(builder)
result = subject.deserialize(params)
expect(result).to be_instance_of(Saml::Kit::LogoutRequest)
@@ -76,8 +76,8 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
it 'deserializes to a Response' do
user = double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: [])
- request = double(:request, id: SecureRandom.uuid, provider: nil, acs_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: FFaker::Internet.http_url, signed?: true, trusted?: true)
- builder = Saml::Kit::Response::Builder.new(user, request)
+ request = double(:request, id: SecureRandom.uuid, provider: nil, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: FFaker::Internet.http_url, signed?: true, trusted?: true)
+ builder = Saml::Kit::Response.builder_class.new(user, request)
_, params = subject.serialize(builder)
result = subject.deserialize(params)
expect(result).to be_instance_of(Saml::Kit::Response)
spec/saml/bindings/http_redirect_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
let(:relay_state) { "ECHO" }
it 'encodes the request using the HTTP-Redirect encoding' do
- builder = Saml::Kit::AuthenticationRequest::Builder.new
+ builder = Saml::Kit::AuthenticationRequest.builder_class.new
url, _ = subject.serialize(builder, relay_state: relay_state)
expect(url).to start_with(location)
expect(url).to have_query_param('SAMLRequest')
@@ -19,7 +19,7 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
describe "#deserialize" do
let(:issuer) { FFaker::Internet.http_url }
- let(:provider) { Saml::Kit::IdentityProviderMetadata::Builder.new.build }
+ let(:provider) { Saml::Kit::IdentityProviderMetadata.build }
before :each do
allow(Saml::Kit.configuration.registry).to receive(:metadata_for).with(issuer).and_return(provider)
@@ -27,14 +27,14 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
end
it 'deserializes the SAMLRequest to an AuthnRequest' do
- url, _ = subject.serialize(Saml::Kit::AuthenticationRequest::Builder.new)
+ url, _ = subject.serialize(Saml::Kit::AuthenticationRequest.builder_class.new)
result = subject.deserialize(query_params_from(url))
expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
end
it 'deserializes the SAMLRequest to a LogoutRequest' do
user = double(:user, name_id_for: SecureRandom.uuid)
- url, _ = subject.serialize(Saml::Kit::LogoutRequest::Builder.new(user))
+ url, _ = subject.serialize(Saml::Kit::LogoutRequest.builder_class.new(user))
result = subject.deserialize(query_params_from(url))
expect(result).to be_instance_of(Saml::Kit::LogoutRequest)
end
@@ -47,16 +47,16 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
it 'deserializes the SAMLResponse to a Response' do
user = double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: [])
- request = double(:request, id: SecureRandom.uuid, provider: nil, acs_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: issuer, signed?: true, trusted?: true)
- url, _ = subject.serialize(Saml::Kit::Response::Builder.new(user, request))
+ request = double(:request, id: SecureRandom.uuid, provider: nil, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: issuer, signed?: true, trusted?: true)
+ url, _ = subject.serialize(Saml::Kit::Response.builder_class.new(user, request))
result = subject.deserialize(query_params_from(url))
expect(result).to be_instance_of(Saml::Kit::Response)
end
it 'deserializes the SAMLResponse to a LogoutResponse' do
user = double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: [])
- request = double(:request, id: SecureRandom.uuid, provider: provider, acs_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: FFaker::Internet.http_url)
- url, _ = subject.serialize(Saml::Kit::LogoutResponse::Builder.new(user, request))
+ request = double(:request, id: SecureRandom.uuid, provider: provider, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: FFaker::Internet.http_url)
+ url, _ = subject.serialize(Saml::Kit::LogoutResponse.builder_class.new(user, request))
result = subject.deserialize(query_params_from(url))
expect(result).to be_instance_of(Saml::Kit::LogoutResponse)
end
@@ -74,7 +74,7 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
end
it 'raises an error when the signature does not match' do
- url, _ = subject.serialize(Saml::Kit::AuthenticationRequest::Builder.new)
+ url, _ = subject.serialize(Saml::Kit::AuthenticationRequest.builder_class.new)
query_params = query_params_from(url)
query_params['Signature'] = 'invalid'
expect do
@@ -83,12 +83,12 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
end
it 'returns a signed document, when a signature is missing' do
- builder = Saml::Kit::ServiceProviderMetadata::Builder.new
- builder.add_assertion_consumer_service(FFaker::Internet.http_url, binding: :http_post)
- provider = builder.build
+ provider = Saml::Kit::ServiceProviderMetadata.build do |builder|
+ builder.add_assertion_consumer_service(FFaker::Internet.http_url, binding: :http_post)
+ end
allow(Saml::Kit.configuration.registry).to receive(:metadata_for).with(issuer).and_return(provider)
- url, _ = subject.serialize(Saml::Kit::AuthenticationRequest::Builder.new)
+ url, _ = subject.serialize(Saml::Kit::AuthenticationRequest.builder_class.new)
result = subject.deserialize(query_params_from(url))
expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
expect(result).to be_valid
spec/saml/builders/authentication_request_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Builders::AuthenticationRequest do
+ subject { described_class.new(configuration: configuration) }
+ let(:configuration) do
+ config = Saml::Kit::Configuration.new
+ config.issuer = issuer
+ config
+ end
+
+ describe "#to_xml" do
+ let(:issuer) { FFaker::Movie.title }
+ let(:assertion_consumer_service_url) { "https://airport.dev/session/acs" }
+
+ it 'returns a valid authentication request' do
+ travel_to 1.second.from_now
+ subject.assertion_consumer_service_url = assertion_consumer_service_url
+ result = Hash.from_xml(subject.to_xml)
+
+ expect(result['AuthnRequest']['ID']).to be_present
+ expect(result['AuthnRequest']['Version']).to eql('2.0')
+ expect(result['AuthnRequest']['IssueInstant']).to eql(Time.now.utc.iso8601)
+ expect(result['AuthnRequest']['AssertionConsumerServiceURL']).to eql(assertion_consumer_service_url)
+ expect(result['AuthnRequest']['Issuer']).to eql(issuer)
+ expect(result['AuthnRequest']['NameIDPolicy']['Format']).to eql(Saml::Kit::Namespaces::PERSISTENT)
+ end
+ end
+end
spec/saml/builders/identity_provider_metadata_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Builders::IdentityProviderMetadata do
+ subject { described_class.new }
+ let(:email) { FFaker::Internet.email }
+ let(:org_name) { FFaker::Movie.title }
+ let(:url) { FFaker::Internet.uri("https") }
+ let(:entity_id) { FFaker::Movie.title }
+
+ it 'builds a proper metadata' do
+ subject.contact_email = email
+ subject.entity_id = entity_id
+ subject.organization_name = org_name
+ subject.organization_url = url
+ subject.name_id_formats = [
+ Saml::Kit::Namespaces::PERSISTENT,
+ Saml::Kit::Namespaces::TRANSIENT,
+ Saml::Kit::Namespaces::EMAIL_ADDRESS,
+ ]
+ subject.add_single_sign_on_service("https://www.example.com/login", binding: :http_redirect)
+ subject.add_single_logout_service("https://www.example.com/logout", binding: :http_post)
+ subject.attributes << "id"
+
+ result = Hash.from_xml(subject.build.to_xml)
+
+ expect(result['EntityDescriptor']['ID']).to be_present
+ expect(result['EntityDescriptor']['entityID']).to eql(entity_id)
+ expect(result['EntityDescriptor']['IDPSSODescriptor']['protocolSupportEnumeration']).to eql(Saml::Kit::Namespaces::PROTOCOL)
+ expect(result['EntityDescriptor']['IDPSSODescriptor']['WantAuthnRequestsSigned']).to eql('true')
+ expect(result['EntityDescriptor']['IDPSSODescriptor']['NameIDFormat']).to match_array([
+ Saml::Kit::Namespaces::PERSISTENT,
+ Saml::Kit::Namespaces::TRANSIENT,
+ Saml::Kit::Namespaces::EMAIL_ADDRESS,
+ ])
+ expect(result['EntityDescriptor']['IDPSSODescriptor']['SingleSignOnService']['Binding']).to eql(Saml::Kit::Bindings::HTTP_REDIRECT)
+ expect(result['EntityDescriptor']['IDPSSODescriptor']['SingleSignOnService']['Location']).to eql("https://www.example.com/login")
+ expect(result['EntityDescriptor']['IDPSSODescriptor']['SingleLogoutService']['Binding']).to eql(Saml::Kit::Bindings::HTTP_POST)
+ expect(result['EntityDescriptor']['IDPSSODescriptor']['SingleLogoutService']['Location']).to eql("https://www.example.com/logout")
+ 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,
+ ])
+ 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/builders/logout_request_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Builders::LogoutRequest do
+ subject { described_class.new(user) }
+ let(:user) { double(:user, name_id_for: name_id) }
+ let(:name_id) { SecureRandom.uuid }
+
+ it 'produces the expected xml' do
+ travel_to 1.second.from_now
+ subject.id = Saml::Kit::Id.generate
+ subject.destination = FFaker::Internet.http_url
+ subject.issuer = FFaker::Internet.http_url
+ subject.name_id_format = Saml::Kit::Namespaces::TRANSIENT
+
+ result = subject.to_xml
+ xml_hash = Hash.from_xml(result)
+
+ expect(xml_hash['LogoutRequest']['ID']).to eql(subject.id)
+ expect(xml_hash['LogoutRequest']['Version']).to eql("2.0")
+ expect(xml_hash['LogoutRequest']['IssueInstant']).to eql(Time.now.utc.iso8601)
+ expect(xml_hash['LogoutRequest']['Destination']).to eql(subject.destination)
+
+ expect(xml_hash['LogoutRequest']['Issuer']).to eql(subject.issuer)
+ expect(xml_hash['LogoutRequest']['NameID']).to eql(name_id)
+ expect(result).to have_xpath("//samlp:LogoutRequest//saml:NameID[@Format=\"#{subject.name_id_format}\"]")
+ end
+
+ it 'includes a signature by default' do
+ xml_hash = Hash.from_xml(subject.to_xml)
+ expect(xml_hash['LogoutRequest']['Signature']).to be_present
+ end
+
+ it 'excludes a signature' do
+ subject.sign = false
+ xml_hash = Hash.from_xml(subject.to_xml)
+ expect(xml_hash['LogoutRequest']['Signature']).to be_nil
+ end
+
+ it 'builds a LogoutRequest' do
+ travel_to 1.second.from_now
+ result = subject.build
+ expect(result).to be_instance_of(Saml::Kit::LogoutRequest)
+ expect(result.to_xml).to eql(subject.to_xml)
+ end
+end
spec/saml/builders/logout_response_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Builders::LogoutResponse do
+ subject { described_class.new(user, request, configuration: configuration) }
+ let(:configuration) { double(issuer: issuer) }
+ let(:user) { double(:user, name_id_for: SecureRandom.uuid) }
+ let(:request) { Saml::Kit::Builders::LogoutRequest.new(user).build }
+ let(:issuer) { FFaker::Internet.http_url }
+ let(:destination) { FFaker::Internet.http_url }
+
+ describe "#build" do
+ it 'builds a logout response' do
+ travel_to 1.second.from_now
+
+ subject.destination = destination
+ result = subject.build
+ expect(result.id).to be_present
+ expect(result.issue_instant).to eql(Time.now.utc.iso8601)
+ expect(result.version).to eql("2.0")
+ expect(result.issuer).to eql(issuer)
+ expect(result.status_code).to eql(Saml::Kit::Namespaces::SUCCESS)
+ expect(result.in_response_to).to eql(request.id)
+ expect(result.destination).to eql(destination)
+ end
+ end
+end
spec/saml/builders/response_spec.rb
@@ -0,0 +1,132 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Builders::Response do
+ subject { described_class.new(user, request) }
+ let(:email) { FFaker::Internet.email }
+ let(:assertion_consumer_service_url) { FFaker::Internet.uri("https") }
+ 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(:issuer) { FFaker::Internet.uri("https") }
+
+ before :each do
+ allow(Saml::Kit.configuration).to receive(:issuer).and_return(issuer)
+ end
+
+ describe "#build" do
+ it 'builds a response with the request_id' do
+ expect(subject.build.request_id).to eql(request.id)
+ end
+
+ it 'builds a valid encrypted assertion' do
+ allow(Saml::Kit.configuration.registry).to receive(:metadata_for).with(issuer).and_return(provider)
+ allow(provider).to receive(:matches?).and_return(true)
+
+ subject.sign = true
+ subject.encrypt = true
+ result = subject.build
+ expect(result).to be_valid
+ end
+ end
+
+ describe "#to_xml" do
+ it 'returns a proper response for the user' do
+ travel_to 1.second.from_now
+ allow(Saml::Kit.configuration).to receive(:issuer).and_return(issuer)
+ hash = Hash.from_xml(subject.to_xml)
+
+ expect(hash['Response']['ID']).to be_present
+ expect(hash['Response']['Version']).to eql('2.0')
+ expect(hash['Response']['IssueInstant']).to eql(Time.now.utc.iso8601)
+ expect(hash['Response']['Destination']).to eql(assertion_consumer_service_url)
+ expect(hash['Response']['InResponseTo']).to eql(request.id)
+ expect(hash['Response']['Issuer']).to eql(issuer)
+ expect(hash['Response']['Status']['StatusCode']['Value']).to eql("urn:oasis:names:tc:SAML:2.0:status:Success")
+
+ expect(hash['Response']['Assertion']['ID']).to be_present
+ expect(hash['Response']['Assertion']['IssueInstant']).to eql(Time.now.utc.iso8601)
+ expect(hash['Response']['Assertion']['Version']).to eql("2.0")
+ expect(hash['Response']['Assertion']['Issuer']).to eql(issuer)
+
+ expect(hash['Response']['Assertion']['Subject']['NameID']).to eql(user.name_id_for)
+ expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['Method']).to eql("urn:oasis:names:tc:SAML:2.0:cm:bearer")
+ expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['NotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
+ expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['Recipient']).to eql(assertion_consumer_service_url)
+ expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['InResponseTo']).to eql(request.id)
+
+ expect(hash['Response']['Assertion']['Conditions']['NotBefore']).to eql(0.seconds.ago.utc.iso8601)
+ expect(hash['Response']['Assertion']['Conditions']['NotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
+ expect(hash['Response']['Assertion']['Conditions']['AudienceRestriction']['Audience']).to eql(request.issuer)
+
+ expect(hash['Response']['Assertion']['AuthnStatement']['AuthnInstant']).to eql(Time.now.utc.iso8601)
+ expect(hash['Response']['Assertion']['AuthnStatement']['SessionNotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
+ expect(hash['Response']['Assertion']['AuthnStatement']['SessionIndex']).to eql(hash['Response']['Assertion']['ID'])
+ expect(hash['Response']['Assertion']['AuthnStatement']['AuthnContext']['AuthnContextClassRef']).to eql('urn:oasis:names:tc:SAML:2.0:ac:classes:Password')
+
+ expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['Name']).to eql('email')
+ expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['FriendlyName']).to eql('email')
+ expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['NameFormat']).to eql('urn:oasis:names:tc:SAML:2.0:attrname-format:uri')
+ expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['AttributeValue']).to eql(email)
+
+ expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['Name']).to eql('created_at')
+ expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['FriendlyName']).to eql('created_at')
+ expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['NameFormat']).to eql('urn:oasis:names:tc:SAML:2.0:attrname-format:uri')
+ expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['AttributeValue']).to be_present
+ end
+
+ it 'does not add a signature when the SP does not want assertions signed' do
+ builder = Saml::Kit::Builders::ServiceProviderMetadata.new
+ builder.want_assertions_signed = false
+ metadata = builder.build
+ allow(request).to receive(:provider).and_return(metadata)
+
+ hash = Hash.from_xml(subject.to_xml)
+ expect(hash['Response']['Signature']).to be_nil
+ end
+
+ it 'generates an EncryptedAssertion' do
+ subject.encrypt = true
+ result = Hash.from_xml(subject.to_xml)
+ expect(result['Response']['EncryptedAssertion']).to be_present
+ encrypted_assertion = result['Response']['EncryptedAssertion']
+ decrypted_assertion = Saml::Kit::XmlDecryption.new.decrypt(encrypted_assertion)
+ decrypted_hash = Hash.from_xml(decrypted_assertion)
+ expect(decrypted_hash['Assertion']).to be_present
+ expect(decrypted_hash['Assertion']['Issuer']).to be_present
+ expect(decrypted_hash['Assertion']['Subject']).to be_present
+ expect(decrypted_hash['Assertion']['Subject']['NameID']).to be_present
+ expect(decrypted_hash['Assertion']['Subject']['SubjectConfirmation']).to be_present
+ expect(decrypted_hash['Assertion']['Conditions']).to be_present
+ expect(decrypted_hash['Assertion']['Conditions']['AudienceRestriction']).to be_present
+ expect(decrypted_hash['Assertion']['AuthnStatement']).to be_present
+ expect(decrypted_hash['Assertion']['AuthnStatement']['AuthnContext']).to be_present
+ expect(decrypted_hash['Assertion']['AuthnStatement']['AuthnContext']['AuthnContextClassRef']).to be_present
+ end
+ end
+
+ describe "#destination" do
+ let(:assertion_consumer_service_url) { "https://#{FFaker::Internet.domain_name}/acs" }
+ let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: []) }
+ subject { described_class.new(user, request).build }
+
+ describe "when the request is signed and trusted" do
+ let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: SecureRandom.uuid, assertion_consumer_service_url: assertion_consumer_service_url, issuer: FFaker::Movie.title, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: nil, signed?: true, trusted?: true) }
+
+ it 'returns the ACS embedded in the request' do
+ expect(subject.destination).to eql(assertion_consumer_service_url)
+ end
+ end
+
+ describe "when the request is not trusted" do
+ let(:registered_acs_url) { FFaker::Internet.uri("https") }
+ let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: SecureRandom.uuid, assertion_consumer_service_url: assertion_consumer_service_url, issuer: FFaker::Movie.title, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: provider, signed?: true, trusted?: false) }
+ let(:provider) { instance_double(Saml::Kit::ServiceProviderMetadata, want_assertions_signed: false) }
+
+ it 'returns the registered ACS embedded in the metadata' do
+ allow(provider).to receive(:assertion_consumer_service_for).and_return(double(location: registered_acs_url))
+ expect(subject.destination).to eql(registered_acs_url)
+ end
+ end
+ end
+end
spec/saml/builders/service_provider_metadata_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Builders::ServiceProviderMetadata do
+ let(:assertion_consumer_service_url) { FFaker::Internet.http_url }
+ let(:entity_id) { FFaker::Internet.uri("https") }
+
+ it 'builds the service provider metadata' do
+ subject.entity_id = entity_id
+ subject.add_assertion_consumer_service(assertion_consumer_service_url, binding: :http_post)
+ subject.name_id_formats = [
+ Saml::Kit::Namespaces::PERSISTENT,
+ Saml::Kit::Namespaces::TRANSIENT,
+ Saml::Kit::Namespaces::EMAIL_ADDRESS,
+ ]
+ result = Hash.from_xml(subject.build.to_xml)
+
+ expect(result['EntityDescriptor']['xmlns']).to eql("urn:oasis:names:tc:SAML:2.0:metadata")
+ expect(result['EntityDescriptor']['ID']).to be_present
+ expect(result['EntityDescriptor']['entityID']).to eql(entity_id)
+ expect(result['EntityDescriptor']['SPSSODescriptor']['AuthnRequestsSigned']).to eql('true')
+ expect(result['EntityDescriptor']['SPSSODescriptor']['WantAssertionsSigned']).to eql('true')
+ expect(result['EntityDescriptor']['SPSSODescriptor']['protocolSupportEnumeration']).to eql('urn:oasis:names:tc:SAML:2.0:protocol')
+ expect(result['EntityDescriptor']['SPSSODescriptor']['NameIDFormat']).to match_array([
+ Saml::Kit::Namespaces::PERSISTENT,
+ Saml::Kit::Namespaces::TRANSIENT,
+ Saml::Kit::Namespaces::EMAIL_ADDRESS,
+ ])
+ expect(result['EntityDescriptor']['SPSSODescriptor']['AssertionConsumerService']['Binding']).to eql("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST")
+ expect(result['EntityDescriptor']['SPSSODescriptor']['AssertionConsumerService']['Location']).to eql(assertion_consumer_service_url)
+ expect(result['EntityDescriptor']['SPSSODescriptor']['AssertionConsumerService']['isDefault']).to eql('true')
+ expect(result['EntityDescriptor']['SPSSODescriptor']['AssertionConsumerService']['index']).to eql('0')
+ 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,
+ ])
+ end
+end
spec/saml/authentication_request_spec.rb
@@ -2,52 +2,28 @@ require 'spec_helper'
RSpec.describe Saml::Kit::AuthenticationRequest do
subject { described_class.new(raw_xml) }
- let(:id) { SecureRandom.uuid }
- let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
+ let(:id) { Saml::Kit::Id.generate }
+ let(:assertion_consumer_service_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
- builder.id = id
- builder.now = Time.now.utc
- builder.issuer = issuer
- builder.acs_url = acs_url
- builder.name_id_format = name_id_format
- builder.destination = destination
- builder.to_xml
+ described_class.build do |builder|
+ builder.id = id
+ builder.now = Time.now.utc
+ builder.issuer = issuer
+ builder.assertion_consumer_service_url = assertion_consumer_service_url
+ builder.name_id_format = name_id_format
+ builder.destination = destination
+ end.to_xml
end
it { expect(subject.issuer).to eql(issuer) }
- it { expect(subject.id).to eql("_#{id}") }
- it { expect(subject.acs_url).to eql(acs_url) }
+ it { expect(subject.id).to eql(id) }
+ it { expect(subject.assertion_consumer_service_url).to eql(assertion_consumer_service_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) }
- let(:configuration) do
- config = Saml::Kit::Configuration.new
- config.issuer = issuer
- config
- end
- let(:issuer) { FFaker::Movie.title }
- let(:acs_url) { "https://airport.dev/session/acs" }
-
- it 'returns a valid authentication request' do
- travel_to 1.second.from_now
- subject.acs_url = acs_url
- result = Hash.from_xml(subject.to_xml)
-
- expect(result['AuthnRequest']['ID']).to be_present
- expect(result['AuthnRequest']['Version']).to eql('2.0')
- expect(result['AuthnRequest']['IssueInstant']).to eql(Time.now.utc.iso8601)
- expect(result['AuthnRequest']['AssertionConsumerServiceURL']).to eql(acs_url)
- expect(result['AuthnRequest']['Issuer']).to eql(issuer)
- expect(result['AuthnRequest']['NameIDPolicy']['Format']).to eql(Saml::Kit::Namespaces::PERSISTENT)
- end
- end
-
describe "#valid?" do
let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
let(:metadata) { instance_double(Saml::Kit::ServiceProviderMetadata) }
@@ -76,36 +52,34 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
end
it 'is invalid when not an AuthnRequest' do
- xml = Saml::Kit::IdentityProviderMetadata::Builder.new.to_xml
+ xml = Saml::Kit::IdentityProviderMetadata.build.to_xml
subject = described_class.new(xml)
expect(subject).to be_invalid
expect(subject.errors[:base]).to include(subject.error_message(:invalid))
end
it 'is invalid when the fingerprint of the certificate does not match the registered fingerprint' do
- builder = described_class::Builder.new
- builder.issuer = issuer
- builder.acs_url = acs_url
- xml = builder.to_xml
-
allow(metadata).to receive(:matches?).and_return(false)
- subject = described_class.new(xml)
+ subject = described_class.build do |builder|
+ builder.issuer = issuer
+ builder.assertion_consumer_service_url = assertion_consumer_service_url
+ end
+
expect(subject).to be_invalid
expect(subject.errors[:fingerprint]).to be_present
end
it 'is invalid when the service provider is not known' do
allow(registry).to receive(:metadata_for).and_return(nil)
- builder = described_class::Builder.new
- subject = described_class.new(builder.to_xml)
+ subject = described_class.build
expect(subject).to be_invalid
expect(subject.errors[:provider]).to be_present
end
it 'validates the schema of the request' do
- id = SecureRandom.uuid
+ id = Saml::Kit::Id.generate
signed_xml = Saml::Kit::Signature.sign(sign: true) do |xml, signature|
- xml.tag!('samlp:AuthnRequest', "xmlns:samlp" => Saml::Kit::Namespaces::PROTOCOL, AssertionConsumerServiceURL: acs_url, ID: "_#{id}") do
+ xml.tag!('samlp:AuthnRequest', "xmlns:samlp" => Saml::Kit::Namespaces::PROTOCOL, AssertionConsumerServiceURL: assertion_consumer_service_url, ID: id) do
signature.template(id)
xml.Fake do
xml.NotAllowed "Huh?"
@@ -117,12 +91,12 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
it 'validates a request without a signature' do
now = Time.now.utc
-raw_xml = <<-XML
-<samlp:AuthnRequest AssertionConsumerServiceURL='#{acs_url}' ID='_#{SecureRandom.uuid}' IssueInstant='#{now.iso8601}' Version='2.0' xmlns:saml='#{Saml::Kit::Namespaces::ASSERTION}' xmlns:samlp='#{Saml::Kit::Namespaces::PROTOCOL}'>
+ raw_xml = <<-XML
+<samlp:AuthnRequest AssertionConsumerServiceURL='#{assertion_consumer_service_url}' ID='#{Saml::Kit::Id.generate}' IssueInstant='#{now.iso8601}' Version='2.0' xmlns:saml='#{Saml::Kit::Namespaces::ASSERTION}' xmlns:samlp='#{Saml::Kit::Namespaces::PROTOCOL}'>
<saml:Issuer>#{issuer}</saml:Issuer>
<samlp:NameIDPolicy AllowCreate='true' Format='#{Saml::Kit::Namespaces::EMAIL_ADDRESS}'/>
</samlp:AuthnRequest>
-XML
+ XML
subject = described_class.new(raw_xml)
subject.signature_verified!
@@ -130,24 +104,56 @@ XML
end
end
- describe "#acs_url" do
+ describe "#assertion_consumer_service_url" do
let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
let(:metadata) { instance_double(Saml::Kit::ServiceProviderMetadata) }
it 'returns the ACS in the request' do
- builder = described_class::Builder.new
- builder.acs_url = acs_url
- subject = builder.build
- expect(subject.acs_url).to eql(acs_url)
+ subject = described_class.build do |builder|
+ builder.assertion_consumer_service_url = assertion_consumer_service_url
+ end
+ expect(subject.assertion_consumer_service_url).to eql(assertion_consumer_service_url)
end
it 'returns nil' do
- builder = described_class::Builder.new
- builder.issuer = issuer
- builder.acs_url = nil
- subject = builder.build
+ subject = described_class.build do |builder|
+ builder.assertion_consumer_service_url = nil
+ end
+
+ expect(subject.assertion_consumer_service_url).to be_nil
+ end
+ end
+
+ describe ".build" do
+ let(:url) { FFaker::Internet.uri("https") }
+ let(:entity_id) { FFaker::Internet.uri("https") }
+
+ it 'provides a nice API for building metadata' do
+ result = described_class.build do |builder|
+ builder.issuer = entity_id
+ builder.assertion_consumer_service_url = url
+ end
+
+ expect(result).to be_instance_of(described_class)
+ expect(result.issuer).to eql(entity_id)
+ expect(result.assertion_consumer_service_url).to eql(url)
+ end
+ end
+
+ describe "#response_for" do
+ let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: []) }
+ let(:provider) do
+ Saml::Kit::ServiceProviderMetadata.build do |x|
+ x.add_assertion_consumer_service(FFaker::Internet.uri("https"), binding: :http_post)
+ end
+ end
+
+ it 'serializes a response' do
+ allow(subject).to receive(:provider).and_return(provider)
+ url, saml_params = subject.response_for(user, binding: :http_post, relay_state: FFaker::Movie.title)
- expect(subject.acs_url).to be_nil
+ response = provider.assertion_consumer_service_for(binding: :http_post).deserialize(saml_params)
+ expect(response).to be_instance_of(Saml::Kit::Response)
end
end
end
spec/saml/bindings_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Bindings do
+ describe ".to_symbol" do
+ subject { described_class }
+
+ it { expect(subject.to_symbol(Saml::Kit::Bindings::HTTP_POST)).to eql(:http_post) }
+ it { expect(subject.to_symbol(Saml::Kit::Bindings::HTTP_REDIRECT)).to eql(:http_redirect) }
+ it { expect(subject.to_symbol('unknown')).to eql('unknown') }
+ end
+
+ describe ".create_for" do
+ subject { described_class }
+ let(:location) { FFaker::Internet.uri("https") }
+
+ it 'returns an HTTP redirect binding' do
+ expect(
+ subject.create_for(Saml::Kit::Bindings::HTTP_REDIRECT, location)
+ ).to be_instance_of(Saml::Kit::Bindings::HttpRedirect)
+ end
+
+ it 'returns an HTTP Post binding' do
+ expect(
+ subject.create_for(Saml::Kit::Bindings::HTTP_POST, location)
+ ).to be_instance_of(Saml::Kit::Bindings::HttpPost)
+ end
+
+ it 'returns an unknown binding' do
+ expect(
+ subject.create_for(Saml::Kit::Bindings::HTTP_ARTIFACT, location)
+ ).to be_instance_of(Saml::Kit::Bindings::Binding)
+ end
+ end
+end
spec/saml/default_registry_spec.rb
@@ -4,14 +4,14 @@ RSpec.describe Saml::Kit::DefaultRegistry do
subject { described_class.new }
let(:entity_id) { FFaker::Internet.http_url }
let(:service_provider_metadata) do
- builder = Saml::Kit::ServiceProviderMetadata::Builder.new
- builder.entity_id = entity_id
- builder.build
+ Saml::Kit::ServiceProviderMetadata.build do |builder|
+ builder.entity_id = entity_id
+ end
end
let(:identity_provider_metadata) do
- builder = Saml::Kit::IdentityProviderMetadata::Builder.new
- builder.entity_id = entity_id
- builder.build
+ Saml::Kit::IdentityProviderMetadata.build do |builder|
+ builder.entity_id = entity_id
+ end
end
describe "#metadata_for" do
spec/saml/identity_provider_metadata_spec.rb
@@ -70,17 +70,18 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
describe "#validate" do
it 'valid when given valid identity provider metadata' do
- builder = described_class::Builder.new
- builder.attributes = [:email]
- builder.add_single_sign_on_service(FFaker::Internet.http_url, binding: :http_post)
- builder.add_single_sign_on_service(FFaker::Internet.http_url, binding: :http_redirect)
- builder.add_single_logout_service(FFaker::Internet.http_url, binding: :http_post)
- builder.add_single_logout_service(FFaker::Internet.http_url, binding: :http_redirect)
- expect(builder.build).to be_valid
+ subject = described_class.build do |builder|
+ builder.attributes = [:email]
+ builder.add_single_sign_on_service(FFaker::Internet.http_url, binding: :http_post)
+ builder.add_single_sign_on_service(FFaker::Internet.http_url, binding: :http_redirect)
+ builder.add_single_logout_service(FFaker::Internet.http_url, binding: :http_post)
+ builder.add_single_logout_service(FFaker::Internet.http_url, binding: :http_redirect)
+ end
+ expect(subject).to be_valid
end
it 'is invalid, when given service provider metadata' do
- service_provider_metadata = Saml::Kit::ServiceProviderMetadata::Builder.new.to_xml
+ service_provider_metadata = Saml::Kit::ServiceProviderMetadata.build.to_xml
subject = described_class.new(service_provider_metadata)
expect(subject).to_not be_valid
expect(subject.errors[:base]).to include(I18n.translate("saml/kit.errors.IDPSSODescriptor.invalid"))
@@ -121,10 +122,10 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
let(:redirect_url) { FFaker::Internet.http_url }
subject do
- builder = Saml::Kit::IdentityProviderMetadata::Builder.new
- builder.add_single_sign_on_service(redirect_url, binding: :http_redirect)
- builder.add_single_sign_on_service(post_url, binding: :http_post)
- builder.build
+ described_class.build do |builder|
+ builder.add_single_sign_on_service(redirect_url, binding: :http_redirect)
+ builder.add_single_sign_on_service(post_url, binding: :http_post)
+ end
end
it 'returns the POST binding' do
@@ -145,37 +146,37 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
end
describe "#want_authn_requests_signed" do
- let(:builder) { described_class::Builder.new }
-
it 'returns true when enabled' do
- builder.want_authn_requests_signed = true
- subject = builder.build
+ subject = described_class.build do |builder|
+ builder.want_authn_requests_signed = true
+ end
expect(subject.want_authn_requests_signed).to be(true)
end
it 'returns false when disabled' do
- builder.want_authn_requests_signed = false
- subject = builder.build
+ subject = described_class.build do |builder|
+ builder.want_authn_requests_signed = false
+ end
expect(subject.want_authn_requests_signed).to be(false)
end
it 'returns true when the attribute is missing' do
- builder.want_authn_requests_signed = false
- xml = builder.to_xml.gsub("WantAuthnRequestsSigned=\"false\"", "")
+ xml = described_class.build do |builder|
+ builder.want_authn_requests_signed = false
+ end.to_xml.gsub("WantAuthnRequestsSigned=\"false\"", "")
subject = described_class.new(xml)
expect(subject.want_authn_requests_signed).to be(true)
end
end
describe "#single_logout_service_for" do
- let(:builder) { described_class::Builder.new }
- let(:redirect_url) { FFaker::Internet.http_url }
- let(:post_url) { FFaker::Internet.http_url }
- let(:subject) { builder.build }
-
- before :each do
- builder.add_single_logout_service(redirect_url, binding: :http_redirect)
- builder.add_single_logout_service(post_url, binding: :http_post)
+ let(:redirect_url) { FFaker::Internet.uri("https") }
+ let(:post_url) { FFaker::Internet.uri("https") }
+ let(:subject) do
+ described_class.build do |builder|
+ builder.add_single_logout_service(redirect_url, binding: :http_redirect)
+ builder.add_single_logout_service(post_url, binding: :http_post)
+ end
end
it 'returns the location for the matching binding' do
@@ -188,53 +189,30 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
end
end
- describe described_class::Builder do
- subject { described_class.new }
- let(:email) { FFaker::Internet.email }
- let(:org_name) { FFaker::Movie.title }
- let(:url) { "https://#{FFaker::Internet.domain_name}" }
- let(:entity_id) { FFaker::Movie.title }
-
- it 'builds a proper metadata' do
- subject.contact_email = email
- subject.entity_id = entity_id
- subject.organization_name = org_name
- subject.organization_url = url
- subject.name_id_formats = [
- Saml::Kit::Namespaces::PERSISTENT,
- Saml::Kit::Namespaces::TRANSIENT,
- Saml::Kit::Namespaces::EMAIL_ADDRESS,
- ]
- subject.add_single_sign_on_service("https://www.example.com/login", binding: :http_redirect)
- subject.add_single_logout_service("https://www.example.com/logout", binding: :http_post)
- subject.attributes << "id"
-
- result = Hash.from_xml(subject.build.to_xml)
-
- expect(result['EntityDescriptor']['ID']).to be_present
- expect(result['EntityDescriptor']['entityID']).to eql(entity_id)
- expect(result['EntityDescriptor']['IDPSSODescriptor']['protocolSupportEnumeration']).to eql(Saml::Kit::Namespaces::PROTOCOL)
- expect(result['EntityDescriptor']['IDPSSODescriptor']['WantAuthnRequestsSigned']).to eql('true')
- expect(result['EntityDescriptor']['IDPSSODescriptor']['NameIDFormat']).to match_array([
- Saml::Kit::Namespaces::PERSISTENT,
- Saml::Kit::Namespaces::TRANSIENT,
- Saml::Kit::Namespaces::EMAIL_ADDRESS,
- ])
- expect(result['EntityDescriptor']['IDPSSODescriptor']['SingleSignOnService']['Binding']).to eql(Saml::Kit::Bindings::HTTP_REDIRECT)
- expect(result['EntityDescriptor']['IDPSSODescriptor']['SingleSignOnService']['Location']).to eql("https://www.example.com/login")
- expect(result['EntityDescriptor']['IDPSSODescriptor']['SingleLogoutService']['Binding']).to eql(Saml::Kit::Bindings::HTTP_POST)
- expect(result['EntityDescriptor']['IDPSSODescriptor']['SingleLogoutService']['Location']).to eql("https://www.example.com/logout")
- 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,
- ])
- 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}")
+ describe ".build" do
+ let(:url) { FFaker::Internet.uri("https") }
+ let(:entity_id) { FFaker::Internet.uri("https") }
+
+ it 'provides a nice API for building metadata' do
+ result = described_class.build do |builder|
+ builder.entity_id = entity_id
+ builder.add_single_sign_on_service(url, binding: :http_post)
+ end
+
+ expect(result).to be_instance_of(described_class)
+ expect(result.entity_id).to eql(entity_id)
+ expect(result.single_sign_on_service_for(binding: :http_post).location).to eql(url)
+ end
+ end
+
+ describe "#login_request_for" do
+ it 'returns a serialized login request' do
+ subject = described_class.build do |x|
+ x.add_single_sign_on_service(FFaker::Internet.uri("https"), binding: :http_post)
+ end
+ url, saml_params = subject.login_request_for(binding: :http_post, relay_state: FFaker::Movie.title)
+ result = subject.single_sign_on_service_for(binding: :http_post).deserialize(saml_params)
+ expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
end
end
end
spec/saml/invalid_document_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::InvalidDocument do
+ subject { described_class.new(xml) }
+ let(:xml) { "<xml></xml>" }
+
+ it 'is invalid' do
+ expect(subject).to be_invalid
+ expect(subject.errors[:base]).to be_present
+ end
+end
+
spec/saml/logout_request_spec.rb
@@ -1,14 +1,16 @@
require 'spec_helper'
RSpec.describe Saml::Kit::LogoutRequest do
- subject { builder.build }
- let(:builder) { described_class::Builder.new(user) }
+ subject { described_class.build(user) }
let(:user) { double(:user, name_id_for: name_id) }
let(:name_id) { SecureRandom.uuid }
it 'parses the issuer' do
- builder.issuer = FFaker::Internet.http_url
- expect(subject.issuer).to eql(builder.issuer)
+ issuer = FFaker::Internet.uri("https")
+ subject = described_class.build(user) do |builder|
+ builder.issuer = issuer
+ end
+ expect(subject.issuer).to eql(issuer)
end
it 'parses the issue instant' do
@@ -21,8 +23,11 @@ RSpec.describe Saml::Kit::LogoutRequest do
end
it 'parses the destination' do
- builder.destination = FFaker::Internet.http_url
- expect(subject.destination).to eql(builder.destination)
+ destination = FFaker::Internet.uri("https")
+ subject = described_class.build(user) do |builder|
+ builder.destination = destination
+ end
+ expect(subject.destination).to eql(destination)
end
it 'parses the name_id' do
@@ -43,14 +48,16 @@ RSpec.describe Saml::Kit::LogoutRequest do
end
it 'is valid when left untampered' do
- expect(builder.build).to be_valid
+ expect(subject).to be_valid
end
it 'is invalid if the document has been tampered with' do
- builder.issuer = FFaker::Internet.http_url
- raw_xml = builder.to_xml.gsub(builder.issuer, 'corrupt')
- subject = described_class.new(raw_xml)
- expect(subject).to be_invalid
+ issuer = FFaker::Internet.uri("https")
+ raw_xml = described_class.build(user) do |builder|
+ builder.issuer = issuer
+ end.to_xml.gsub(issuer, 'corrupt')
+
+ expect(described_class.new(raw_xml)).to be_invalid
end
it 'is invalid when blank' do
@@ -60,22 +67,19 @@ RSpec.describe Saml::Kit::LogoutRequest do
end
it 'is invalid when not a LogoutRequest' do
- xml = Saml::Kit::IdentityProviderMetadata::Builder.new.to_xml
- subject = described_class.new(xml)
+ subject = described_class.new(Saml::Kit::IdentityProviderMetadata.build.to_xml)
expect(subject).to be_invalid
expect(subject.errors[:base]).to include(subject.error_message(:invalid))
end
it 'is invalid when the fingerprint of the certificate does not match the registered fingerprint' do
allow(metadata).to receive(:matches?).and_return(false)
- subject = builder.build
expect(subject).to be_invalid
expect(subject.errors[:fingerprint]).to be_present
end
it 'is invalid when the provider is not known' do
allow(registry).to receive(:metadata_for).and_return(nil)
- subject = builder.build
expect(subject).to be_invalid
expect(subject.errors[:provider]).to be_present
end
@@ -84,26 +88,28 @@ RSpec.describe Saml::Kit::LogoutRequest do
allow(metadata).to receive(:matches?).and_return(true)
allow(metadata).to receive(:single_logout_services).and_return([])
- subject = builder.build
expect(subject).to be_invalid
expect(subject.errors[:single_logout_service]).to be_present
end
it 'is valid when a single lgout service url is available via the registry' do
- builder.issuer = FFaker::Internet.http_url
- allow(registry).to receive(:metadata_for).with(builder.issuer).and_return(metadata)
+ issuer = FFaker::Internet.uri("https")
+ allow(registry).to receive(:metadata_for).with(issuer).and_return(metadata)
allow(metadata).to receive(:matches?).and_return(true)
allow(metadata).to receive(:single_logout_services).and_return([
- Saml::Kit::Bindings::HttpPost.new(location: FFaker::Internet.http_url)
+ Saml::Kit::Bindings::HttpPost.new(location: FFaker::Internet.uri("https"))
])
- expect(builder.build).to be_valid
+ subject = described_class.build(user) do |builder|
+ builder.issuer = issuer
+ end
+ expect(subject).to be_valid
end
it 'validates the schema of the request' do
- id = SecureRandom.uuid
+ id = Saml::Kit::Id.generate
signed_xml = Saml::Kit::Signature.sign(sign: true) do |xml, signature|
- xml.LogoutRequest ID: "_#{id}" do
+ xml.LogoutRequest ID: id do
signature.template(id)
xml.Fake do
xml.NotAllowed "Huh?"
@@ -114,54 +120,21 @@ RSpec.describe Saml::Kit::LogoutRequest do
end
end
- describe described_class::Builder do
- subject { described_class.new(user) }
- let(:user) { double(:user, name_id_for: name_id) }
- let(:name_id) { SecureRandom.uuid }
-
- it 'produces the expected xml' do
- travel_to 1.second.from_now
- subject.id = SecureRandom.uuid
- subject.destination = FFaker::Internet.http_url
- subject.issuer = FFaker::Internet.http_url
- subject.name_id_format = Saml::Kit::Namespaces::TRANSIENT
-
- result = subject.to_xml
- xml_hash = Hash.from_xml(result)
-
- expect(xml_hash['LogoutRequest']['ID']).to eql("_#{subject.id}")
- expect(xml_hash['LogoutRequest']['Version']).to eql("2.0")
- expect(xml_hash['LogoutRequest']['IssueInstant']).to eql(Time.now.utc.iso8601)
- expect(xml_hash['LogoutRequest']['Destination']).to eql(subject.destination)
-
- expect(xml_hash['LogoutRequest']['Issuer']).to eql(subject.issuer)
- expect(xml_hash['LogoutRequest']['NameID']).to eql(name_id)
- expect(result).to have_xpath("//samlp:LogoutRequest//saml:NameID[@Format=\"#{subject.name_id_format}\"]")
- end
-
- it 'includes a signature by default' do
- xml_hash = Hash.from_xml(subject.to_xml)
- expect(xml_hash['LogoutRequest']['Signature']).to be_present
+ describe "#response_for" do
+ let(:user) { double(:user, name_id_for: SecureRandom.uuid) }
+ let(:provider) do
+ Saml::Kit::IdentityProviderMetadata.build do |builder|
+ builder.add_single_logout_service(FFaker::Internet.uri("https"), binding: :http_post)
+ end
end
- it 'excludes a signature' do
- subject.sign = false
- xml_hash = Hash.from_xml(subject.to_xml)
- expect(xml_hash['LogoutRequest']['Signature']).to be_nil
- end
+ it 'serializes a logout response for a particular user' do
+ allow(subject).to receive(:provider).and_return(provider)
- it 'builds a LogoutRequest' do
- travel_to 1.second.from_now
- result = subject.build
- expect(result).to be_instance_of(Saml::Kit::LogoutRequest)
- expect(result.to_xml).to eql(subject.to_xml)
- end
- end
-
- describe "#response_for" do
- it 'returns a logout response for a particular user' do
- user = double(:user)
- expect(subject.response_for(user)).to be_instance_of(Saml::Kit::LogoutResponse::Builder)
+ _, saml_params = subject.response_for(user, binding: :http_post)
+ response_binding = provider.single_logout_service_for(binding: :http_post)
+ result = response_binding.deserialize(saml_params)
+ expect(result).to be_instance_of(Saml::Kit::LogoutResponse)
end
end
end
spec/saml/logout_response_spec.rb
@@ -1,34 +1,4 @@
require 'spec_helper'
RSpec.describe Saml::Kit::LogoutResponse do
- describe described_class::Builder do
- subject { described_class.new(user, request, configuration: configuration) }
- let(:configuration) { double(issuer: issuer) }
- let(:user) { double(:user, name_id_for: SecureRandom.uuid) }
- let(:request) { Saml::Kit::LogoutRequest::Builder.new(user).build }
- let(:issuer) { FFaker::Internet.http_url }
- let(:destination) { FFaker::Internet.http_url }
- let(:registry) { double(:registry) }
- let(:provider) { double(:provider) }
- let(:binding) { double(:binding, location: destination) }
-
- describe "#build" do
- it 'builds a logout response' do
- allow(configuration).to receive(:registry).and_return(registry)
- allow(registry).to receive(:metadata_for).with(issuer).and_return(provider)
- allow(provider).to receive(:single_logout_service_for).and_return(binding)
-
- travel_to 1.second.from_now
-
- result = subject.build
- expect(result.id).to be_present
- expect(result.issue_instant).to eql(Time.now.utc.iso8601)
- expect(result.version).to eql("2.0")
- expect(result.issuer).to eql(issuer)
- expect(result.status_code).to eql(Saml::Kit::Namespaces::SUCCESS)
- expect(result.in_response_to).to eql(request.id)
- expect(result.destination).to eql(destination)
- end
- end
- end
end
spec/saml/response_spec.rb
@@ -1,100 +1,12 @@
require 'spec_helper'
RSpec.describe Saml::Kit::Response do
- describe "#destination" do
- let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
- let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: []) }
- subject { described_class::Builder.new(user, request).build }
-
- describe "when the request is signed and trusted" do
- let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: SecureRandom.uuid, acs_url: acs_url, issuer: FFaker::Movie.title, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: nil, signed?: true, trusted?: true) }
-
- it 'returns the ACS embedded in the request' do
- expect(subject.destination).to eql(acs_url)
- end
- end
-
- describe "when the request is not trusted" do
- let(:registered_acs_url) { FFaker::Internet.uri("https") }
- let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: SecureRandom.uuid, acs_url: acs_url, issuer: FFaker::Movie.title, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: provider, signed?: true, trusted?: false) }
- let(:provider) { instance_double(Saml::Kit::ServiceProviderMetadata, want_assertions_signed: false) }
-
- it 'returns the registered ACS embedded in the metadata' do
- allow(provider).to receive(:assertion_consumer_service_for).and_return(double(location: registered_acs_url))
- expect(subject.destination).to eql(registered_acs_url)
- end
- end
- end
-
- describe "#to_xml" do
- subject { described_class::Builder.new(user, request) }
- let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: { email: email, created_at: Time.now.utc.iso8601 }) }
- let(:request) { double(id: SecureRandom.uuid, acs_url: acs_url, issuer: FFaker::Movie.title, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: nil, trusted?: true, signed?: true) }
- let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
- let(:issuer) { FFaker::Movie.title }
- let(:email) { FFaker::Internet.email }
-
- it 'returns a proper response for the user' do
- travel_to 1.second.from_now
- allow(Saml::Kit.configuration).to receive(:issuer).and_return(issuer)
- hash = Hash.from_xml(subject.to_xml)
-
- expect(hash['Response']['ID']).to be_present
- expect(hash['Response']['Version']).to eql('2.0')
- expect(hash['Response']['IssueInstant']).to eql(Time.now.utc.iso8601)
- expect(hash['Response']['Destination']).to eql(acs_url)
- expect(hash['Response']['InResponseTo']).to eql(request.id)
- expect(hash['Response']['Issuer']).to eql(issuer)
- expect(hash['Response']['Status']['StatusCode']['Value']).to eql("urn:oasis:names:tc:SAML:2.0:status:Success")
-
- expect(hash['Response']['Assertion']['ID']).to be_present
- expect(hash['Response']['Assertion']['IssueInstant']).to eql(Time.now.utc.iso8601)
- expect(hash['Response']['Assertion']['Version']).to eql("2.0")
- expect(hash['Response']['Assertion']['Issuer']).to eql(issuer)
-
- expect(hash['Response']['Assertion']['Subject']['NameID']).to eql(user.name_id_for)
- expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['Method']).to eql("urn:oasis:names:tc:SAML:2.0:cm:bearer")
- expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['NotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
- expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['Recipient']).to eql(acs_url)
- expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['InResponseTo']).to eql(request.id)
-
- expect(hash['Response']['Assertion']['Conditions']['NotBefore']).to eql(0.seconds.ago.utc.iso8601)
- expect(hash['Response']['Assertion']['Conditions']['NotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
- expect(hash['Response']['Assertion']['Conditions']['AudienceRestriction']['Audience']).to eql(request.issuer)
-
- expect(hash['Response']['Assertion']['AuthnStatement']['AuthnInstant']).to eql(Time.now.utc.iso8601)
- expect(hash['Response']['Assertion']['AuthnStatement']['SessionNotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
- expect(hash['Response']['Assertion']['AuthnStatement']['SessionIndex']).to eql(hash['Response']['Assertion']['ID'])
- expect(hash['Response']['Assertion']['AuthnStatement']['AuthnContext']['AuthnContextClassRef']).to eql('urn:oasis:names:tc:SAML:2.0:ac:classes:Password')
-
- expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['Name']).to eql('email')
- expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['FriendlyName']).to eql('email')
- expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['NameFormat']).to eql('urn:oasis:names:tc:SAML:2.0:attrname-format:uri')
- expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['AttributeValue']).to eql(email)
-
- expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['Name']).to eql('created_at')
- expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['FriendlyName']).to eql('created_at')
- expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['NameFormat']).to eql('urn:oasis:names:tc:SAML:2.0:attrname-format:uri')
- expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['AttributeValue']).to be_present
- end
-
- it 'does not add a signature when the SP does not want assertions signed' do
- builder = Saml::Kit::ServiceProviderMetadata::Builder.new
- builder.want_assertions_signed = false
- metadata = builder.build
- allow(request).to receive(:provider).and_return(metadata)
-
- hash = Hash.from_xml(subject.to_xml)
- expect(hash['Response']['Signature']).to be_nil
- end
- end
-
describe "#valid?" do
- let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: "_#{SecureRandom.uuid}", issuer: FFaker::Internet.http_url, acs_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil, signed?: true, trusted?: true) }
+ let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: Saml::Kit::Id.generate, issuer: FFaker::Internet.http_url, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil, signed?: true, trusted?: true) }
let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: { id: SecureRandom.uuid }) }
- let(:builder) { described_class::Builder.new(user, request) }
let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
let(:metadata) { instance_double(Saml::Kit::IdentityProviderMetadata) }
+ subject { described_class.build(user, request) }
before :each do
allow(Saml::Kit.configuration).to receive(:registry).and_return(registry)
@@ -104,7 +16,7 @@ RSpec.describe Saml::Kit::Response do
it 'is valid' do
allow(registry).to receive(:metadata_for).and_return(metadata)
allow(metadata).to receive(:matches?).and_return(true)
- expect(builder.build).to be_valid
+ expect(subject).to be_valid
end
it 'is invalid when blank' do
@@ -118,15 +30,16 @@ RSpec.describe Saml::Kit::Response do
allow(registry).to receive(:metadata_for).and_return(metadata)
allow(metadata).to receive(:matches?).and_return(true)
status_code = FFaker::Movie.title
- builder.status_code = status_code
- subject = described_class.new(builder.to_xml.gsub(status_code, "TAMPERED"))
+ xml = described_class.build(user, request) do |builder|
+ builder.status_code = status_code
+ end.to_xml.gsub(status_code, "TAMPERED")
+ subject = described_class.new(xml)
expect(subject).to be_invalid
end
it 'is invalid when not a Response' do
allow(registry).to receive(:metadata_for).and_return(nil)
- xml = Saml::Kit::IdentityProviderMetadata::Builder.new.to_xml
- subject = described_class.new(xml)
+ subject = described_class.new(Saml::Kit::IdentityProviderMetadata.build.to_xml)
expect(subject).to be_invalid
expect(subject.errors[:base]).to include(subject.error_message(:invalid))
end
@@ -134,7 +47,6 @@ RSpec.describe Saml::Kit::Response do
it 'is invalid when the fingerprint of the certificate does not match the registered fingerprint' do
allow(registry).to receive(:metadata_for).and_return(metadata)
allow(metadata).to receive(:matches?).and_return(false)
- subject = described_class.new(builder.to_xml)
expect(subject).to be_invalid
expect(subject.errors[:fingerprint]).to be_present
end
@@ -142,9 +54,9 @@ RSpec.describe Saml::Kit::Response do
it 'validates the schema of the response' do
allow(registry).to receive(:metadata_for).and_return(metadata)
allow(metadata).to receive(:matches?).and_return(true)
- id = SecureRandom.uuid
+ id = Saml::Kit::Id.generate
signed_xml = Saml::Kit::Signature.sign(sign: true) do |xml, signature|
- xml.tag! "samlp:Response", "xmlns:samlp" => Saml::Kit::Namespaces::PROTOCOL, ID: "_#{id}" do
+ xml.tag! "samlp:Response", "xmlns:samlp" => Saml::Kit::Namespaces::PROTOCOL, ID: id do
signature.template(id)
xml.Fake do
xml.NotAllowed "Huh?"
@@ -159,8 +71,9 @@ RSpec.describe Saml::Kit::Response do
it 'validates the version' do
allow(registry).to receive(:metadata_for).and_return(metadata)
allow(metadata).to receive(:matches?).and_return(true)
- builder.version = "1.1"
- subject = described_class.new(builder.to_xml)
+ subject = described_class.build(user, request) do |builder|
+ builder.version = "1.1"
+ end
expect(subject).to be_invalid
expect(subject.errors[:version]).to be_present
end
@@ -168,8 +81,9 @@ RSpec.describe Saml::Kit::Response do
it 'validates the id' do
allow(registry).to receive(:metadata_for).and_return(metadata)
allow(metadata).to receive(:matches?).and_return(true)
- builder.id = nil
- subject = described_class.new(builder.to_xml)
+ subject = described_class.build(user, request) do |builder|
+ builder.id = nil
+ end
expect(subject).to be_invalid
expect(subject.errors[:id]).to be_present
end
@@ -177,8 +91,9 @@ RSpec.describe Saml::Kit::Response do
it 'validates the status code' do
allow(registry).to receive(:metadata_for).and_return(metadata)
allow(metadata).to receive(:matches?).and_return(true)
- builder.status_code = Saml::Kit::Namespaces::REQUESTER_ERROR
- subject = described_class.new(builder.to_xml)
+ subject = described_class.build(user, request) do |builder|
+ builder.status_code = Saml::Kit::Namespaces::REQUESTER_ERROR
+ end
expect(subject).to be_invalid
expect(subject.errors[:status_code]).to be_present
end
@@ -186,7 +101,9 @@ RSpec.describe Saml::Kit::Response do
it 'validates the InResponseTo' do
allow(registry).to receive(:metadata_for).and_return(metadata)
allow(metadata).to receive(:matches?).and_return(true)
- subject = described_class.new(builder.to_xml, request_id: SecureRandom.uuid)
+ xml = described_class.build(user, request).to_xml
+ subject = described_class.new(xml, request_id: SecureRandom.uuid)
+
expect(subject).to be_invalid
expect(subject.errors[:in_response_to]).to be_present
end
@@ -195,7 +112,7 @@ RSpec.describe Saml::Kit::Response do
allow(registry).to receive(:metadata_for).and_return(metadata)
allow(metadata).to receive(:matches?).and_return(true)
- subject = described_class.new(builder.to_xml)
+ subject = described_class.build(user, request)
travel_to Saml::Kit.configuration.session_timeout.from_now + 5.seconds
expect(subject).to_not be_valid
expect(subject.errors[:base]).to be_present
@@ -205,7 +122,7 @@ RSpec.describe Saml::Kit::Response do
allow(registry).to receive(:metadata_for).and_return(metadata)
allow(metadata).to receive(:matches?).and_return(true)
- subject = described_class.new(builder.to_xml)
+ subject = described_class.build(user, request)
travel_to 5.seconds.ago
expect(subject).to be_invalid
expect(subject.errors[:base]).to be_present
@@ -215,9 +132,8 @@ RSpec.describe Saml::Kit::Response do
allow(registry).to receive(:metadata_for).and_return(metadata)
allow(metadata).to receive(:matches?).and_return(true)
- allow(Saml::Kit.configuration).to receive(:issuer).and_return(FFaker::Internet.http_url)
- allow(request).to receive(:issuer).and_return(FFaker::Internet.http_url)
- subject = described_class.new(builder.to_xml)
+ allow(Saml::Kit.configuration).to receive(:issuer).and_return(FFaker::Internet.uri("https"))
+ allow(request).to receive(:issuer).and_return(FFaker::Internet.uri("https"))
expect(subject).to be_invalid
expect(subject.errors[:audience]).to be_present
@@ -225,10 +141,10 @@ RSpec.describe Saml::Kit::Response do
it 'is invalid' do
now = Time.now.utc
- destination = FFaker::Internet.http_url
+ destination = FFaker::Internet.uri("https")
raw_xml = <<-XML
<?xml version="1.0"?>
-<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="_#{SecureRandom.uuid}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{destination}" Consent="#{Saml::Kit::Namespaces::UNSPECIFIED}" InResponseTo="#{request.id}">
+<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{Saml::Kit::Id.generate}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{destination}" Consent="#{Saml::Kit::Namespaces::UNSPECIFIED}" InResponseTo="#{request.id}">
<Issuer xmlns="#{Saml::Kit::Namespaces::ASSERTION}">#{request.issuer}</Issuer>
<samlp:Status>
<samlp:StatusCode Value="#{Saml::Kit::Namespaces::RESPONDER_ERROR}"/>
@@ -244,19 +160,19 @@ RSpec.describe Saml::Kit::Response do
describe "#signed?" do
let(:now) { Time.now.utc }
- let(:id) { SecureRandom.uuid }
+ let(:id) { Saml::Kit::Id.generate }
let(:url) { FFaker::Internet.uri("https") }
it 'returns true when the Assertion is signed' do
xml = <<-XML
<?xml version="1.0"?>
-<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="_#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
- <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="_#{id}" IssueInstant="#{now.iso8601}" Version="2.0">
+<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
+ <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0">
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
- <ds:Reference URI="#_#{id}">
+ <ds:Reference URI="##{id}">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
@@ -282,12 +198,12 @@ RSpec.describe Saml::Kit::Response do
it 'returns true when the Response is signed' do
xml = <<-XML
<?xml version="1.0"?>
-<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="_#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
+<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
- <ds:Reference URI="#_#{id}">
+ <ds:Reference URI="##{id}">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
@@ -303,7 +219,7 @@ RSpec.describe Saml::Kit::Response do
</ds:X509Data>
</KeyInfo>
</ds:Signature>
- <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="_#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
+ <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
</samlp:Response>
XML
subject = described_class.new(xml)
@@ -313,8 +229,8 @@ RSpec.describe Saml::Kit::Response do
it 'returns false when there is no signature' do
xml = <<-XML
<?xml version="1.0"?>
-<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="_#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
- <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="_#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
+<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
+ <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
</samlp:Response>
XML
subject = described_class.new(xml)
@@ -324,20 +240,20 @@ RSpec.describe Saml::Kit::Response do
describe "#certificate" do
let(:now) { Time.now.utc }
- let(:id) { SecureRandom.uuid }
+ let(:id) { Saml::Kit::Id.generate }
let(:url) { FFaker::Internet.uri("https") }
let(:certificate) { FFaker::Movie.title }
it 'returns the certificate when the Assertion is signed' do
xml = <<-XML
<?xml version="1.0"?>
-<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="_#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
- <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="_#{id}" IssueInstant="#{now.iso8601}" Version="2.0">
+<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
+ <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0">
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
- <ds:Reference URI="#_#{id}">
+ <ds:Reference URI="##{id}">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
@@ -363,12 +279,12 @@ RSpec.describe Saml::Kit::Response do
it 'returns the certificate when the Response is signed' do
xml = <<-XML
<?xml version="1.0"?>
-<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="_#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
+<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
- <ds:Reference URI="#_#{id}">
+ <ds:Reference URI="##{id}">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
@@ -384,7 +300,7 @@ RSpec.describe Saml::Kit::Response do
</ds:X509Data>
</KeyInfo>
</ds:Signature>
- <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="_#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
+ <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
</samlp:Response>
XML
subject = described_class.new(xml)
@@ -394,8 +310,8 @@ RSpec.describe Saml::Kit::Response do
it 'returns nil when there is no signature' do
xml = <<-XML
<?xml version="1.0"?>
-<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="_#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
- <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="_#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
+<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
+ <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
</samlp:Response>
XML
subject = described_class.new(xml)
@@ -403,67 +319,15 @@ RSpec.describe Saml::Kit::Response do
end
end
- describe described_class::Builder do
- subject { described_class.new(user, request) }
- let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: []) }
- let(:request) { double(:request, id: "_#{SecureRandom.uuid}", acs_url: FFaker::Internet.http_url, provider: provider, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: issuer, signed?: true, trusted?: true) }
- let(:provider) { double(want_assertions_signed: false, encryption_certificates: [Saml::Kit::Certificate.new(encryption_pem, use: :encryption)]) }
- let(:encryption_pem) do
- Saml::Kit.configuration.stripped_encryption_certificate
- end
- let(:issuer) { FFaker::Internet.uri("https") }
-
- before :each do
- allow(Saml::Kit.configuration).to receive(:issuer).and_return(issuer)
- end
-
- describe "#build" do
- it 'builds a response with the request_id' do
- expect(subject.build.request_id).to eql(request.id)
- end
-
- it 'builds a valid encrypted assertion' do
- allow(Saml::Kit.configuration.registry).to receive(:metadata_for).with(issuer).and_return(provider)
- allow(provider).to receive(:matches?).and_return(true)
-
- subject.sign = true
- subject.encrypt = true
- result = subject.build
- expect(result).to be_valid
- end
- end
-
- describe "#to_xml" do
- it 'generates an EncryptedAssertion' do
- subject.encrypt = true
- result = Hash.from_xml(subject.to_xml)
- expect(result['Response']['EncryptedAssertion']).to be_present
- encrypted_assertion = result['Response']['EncryptedAssertion']
- decrypted_assertion = Saml::Kit::Cryptography.new.decrypt(encrypted_assertion)
- decrypted_hash = Hash.from_xml(decrypted_assertion)
- expect(decrypted_hash['Assertion']).to be_present
- expect(decrypted_hash['Assertion']['Issuer']).to be_present
- expect(decrypted_hash['Assertion']['Subject']).to be_present
- expect(decrypted_hash['Assertion']['Subject']['NameID']).to be_present
- expect(decrypted_hash['Assertion']['Subject']['SubjectConfirmation']).to be_present
- expect(decrypted_hash['Assertion']['Conditions']).to be_present
- expect(decrypted_hash['Assertion']['Conditions']['AudienceRestriction']).to be_present
- expect(decrypted_hash['Assertion']['AuthnStatement']).to be_present
- expect(decrypted_hash['Assertion']['AuthnStatement']['AuthnContext']).to be_present
- expect(decrypted_hash['Assertion']['AuthnStatement']['AuthnContext']['AuthnContextClassRef']).to be_present
- end
- end
- end
-
describe "encrypted assertion" do
- let(:id) { SecureRandom.uuid }
+ let(:id) { Saml::Kit::Id.generate }
let(:now) { Time.now.utc }
- let(:acs_url) { FFaker::Internet.uri("https") }
+ let(:assertion_consumer_service_url) { FFaker::Internet.uri("https") }
let(:password) { FFaker::Movie.title }
let(:assertion) do
FFaker::Movie.title
<<-XML
-<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="_11d39a7f-1b86-43ed-90d7-68090a857ca8" IssueInstant="2017-11-23T04:33:58Z" Version="2.0">
+<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="#{id}" IssueInstant="2017-11-23T04:33:58Z" Version="2.0">
<Issuer>#{FFaker::Internet.uri("https")}</Issuer>
<Subject>
<NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">fdddf7ad-c4a4-443c-b96d-c953913b7b4e</NameID>
@@ -507,7 +371,7 @@ XML
encrypted = cipher.update(assertion) + cipher.final
xml = <<-XML
-<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" xmlns:saml="#{Saml::Kit::Namespaces::ASSERTION}" ID="_#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{acs_url}" InResponseTo="_#{SecureRandom.uuid}">
+<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" xmlns:saml="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{assertion_consumer_service_url}" InResponseTo="#{Saml::Kit::Id.generate}">
<saml:Issuer>#{FFaker::Internet.uri("https")}</saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="#{Saml::Kit::Namespaces::SUCCESS}"/>
@@ -536,4 +400,26 @@ XML
expect(subject.attributes).to be_present
end
end
+
+ describe "parsing" do
+ let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: attributes) }
+ let(:request) { double(:request, id: Saml::Kit::Id.generate, signed?: true, trusted?: true, provider: nil, assertion_consumer_service_url: FFaker::Internet.uri("https"), name_id_format: '', issuer: FFaker::Internet.uri("https")) }
+ let(:attributes) { { name: 'mo' } }
+
+ it 'returns the name id' do
+ subject = described_class.build(user, request)
+ expect(subject.name_id).to eql(user.name_id_for)
+ end
+
+ it 'returns the single attributes' do
+ subject = described_class.build(user, request)
+ expect(subject.attributes).to eql('name' => 'mo')
+ end
+
+ it 'returns the multiple attributes' do
+ attributes[:age] = 33
+ subject = described_class.build(user, request)
+ expect(subject.attributes).to eql('name' => 'mo', 'age' => '33')
+ end
+ end
end
spec/saml/service_provider_metadata_spec.rb
@@ -7,52 +7,15 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
let(:logout_post_url) { FFaker::Internet.uri("https") }
let(:logout_redirect_url) { FFaker::Internet.uri("https") }
- describe described_class::Builder do
- let(:acs_url) { FFaker::Internet.http_url }
-
- it 'builds the service provider metadata' do
- subject.entity_id = entity_id
- subject.add_assertion_consumer_service(acs_url, binding: :http_post)
- subject.name_id_formats = [
- Saml::Kit::Namespaces::PERSISTENT,
- Saml::Kit::Namespaces::TRANSIENT,
- Saml::Kit::Namespaces::EMAIL_ADDRESS,
- ]
- result = Hash.from_xml(subject.build.to_xml)
-
- expect(result['EntityDescriptor']['xmlns']).to eql("urn:oasis:names:tc:SAML:2.0:metadata")
- expect(result['EntityDescriptor']['ID']).to be_present
- expect(result['EntityDescriptor']['entityID']).to eql(entity_id)
- expect(result['EntityDescriptor']['SPSSODescriptor']['AuthnRequestsSigned']).to eql('true')
- expect(result['EntityDescriptor']['SPSSODescriptor']['WantAssertionsSigned']).to eql('true')
- expect(result['EntityDescriptor']['SPSSODescriptor']['protocolSupportEnumeration']).to eql('urn:oasis:names:tc:SAML:2.0:protocol')
- expect(result['EntityDescriptor']['SPSSODescriptor']['NameIDFormat']).to match_array([
- Saml::Kit::Namespaces::PERSISTENT,
- Saml::Kit::Namespaces::TRANSIENT,
- Saml::Kit::Namespaces::EMAIL_ADDRESS,
- ])
- expect(result['EntityDescriptor']['SPSSODescriptor']['AssertionConsumerService']['Binding']).to eql("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST")
- expect(result['EntityDescriptor']['SPSSODescriptor']['AssertionConsumerService']['Location']).to eql(acs_url)
- expect(result['EntityDescriptor']['SPSSODescriptor']['AssertionConsumerService']['isDefault']).to eql('true')
- expect(result['EntityDescriptor']['SPSSODescriptor']['AssertionConsumerService']['index']).to eql('0')
- 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,
- ])
- end
- end
-
describe described_class do
- let(:builder) { described_class::Builder.new }
subject do
- builder.entity_id = entity_id
- builder.add_assertion_consumer_service(acs_post_url, binding: :http_post)
- builder.add_assertion_consumer_service(acs_redirect_url, binding: :http_redirect)
- builder.add_single_logout_service(logout_post_url, binding: :http_post)
- builder.add_single_logout_service(logout_redirect_url, binding: :http_redirect)
- builder.build
+ described_class.build do |builder|
+ builder.entity_id = entity_id
+ builder.add_assertion_consumer_service(acs_post_url, binding: :http_post)
+ builder.add_assertion_consumer_service(acs_redirect_url, binding: :http_redirect)
+ builder.add_single_logout_service(logout_post_url, binding: :http_post)
+ builder.add_single_logout_service(logout_redirect_url, binding: :http_redirect)
+ end
end
it 'returns each of the certificates' do
@@ -89,13 +52,13 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
describe "#validate" do
let(:service_provider_metadata) do
- builder = described_class::Builder.new
- builder.entity_id = entity_id
- builder.add_assertion_consumer_service(acs_post_url, binding: :http_post)
- builder.add_assertion_consumer_service(acs_redirect_url, binding: :http_redirect)
- builder.add_single_logout_service(logout_post_url, binding: :http_post)
- builder.add_single_logout_service(logout_redirect_url, binding: :http_redirect)
- builder.to_xml
+ described_class.build do |builder|
+ builder.entity_id = entity_id
+ builder.add_assertion_consumer_service(acs_post_url, binding: :http_post)
+ builder.add_assertion_consumer_service(acs_redirect_url, binding: :http_redirect)
+ builder.add_single_logout_service(logout_post_url, binding: :http_post)
+ builder.add_single_logout_service(logout_redirect_url, binding: :http_redirect)
+ end.to_xml
end
it 'valid when given valid service provider metadata' do
@@ -138,7 +101,7 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
it 'is invalid when 0 ACS endpoints are specified' do
xml = <<-XML
<?xml version="1.0" encoding="UTF-8"?>
-<EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="_#{SecureRandom.uuid}" entityID="#{entity_id}">
+<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>
@@ -150,7 +113,7 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
end
describe "#matches?" do
- subject { described_class::Builder.new.build }
+ subject { Saml::Kit::ServiceProviderMetadata.build }
it 'returns true when the fingerprint matches one of the signing certificates' do
certificate = Hash.from_xml(subject.to_xml)['EntityDescriptor']['Signature']['KeyInfo']['X509Data']['X509Certificate']
@@ -164,4 +127,26 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
expect(subject.matches?(fingerprint)).to be_falsey
end
end
+
+ describe ".build" do
+ let(:assertion_consumer_service_url) { FFaker::Internet.uri("https") }
+
+ it 'provides a nice API for building metadata' do
+ result = described_class.build do |builder|
+ builder.entity_id = entity_id
+ builder.add_assertion_consumer_service(assertion_consumer_service_url, binding: :http_post)
+ end
+
+ expect(result).to be_instance_of(described_class)
+ expect(result.entity_id).to eql(entity_id)
+ expect(result.assertion_consumer_service_for(binding: :http_post).location).to eql(assertion_consumer_service_url)
+ end
+ end
+
+ describe "deprecations" do
+ it 'resolves the old builder constant' do
+ subject = Saml::Kit::ServiceProviderMetadata::Builder.new
+ expect(subject).to be_present
+ end
+ end
end
spec/saml/signature_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Saml::Kit::Signature do
config
end
- let(:reference_id) { SecureRandom.uuid }
+ let(:reference_id) { Saml::Kit::Id.generate }
let(:rsa_key) { OpenSSL::PKey::RSA.new(2048) }
let(:public_key) { rsa_key.public_key }
let(:certificate) do
@@ -34,7 +34,7 @@ RSpec.describe Saml::Kit::Signature do
options = {
"xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
"xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
- ID: "_#{reference_id}",
+ ID: reference_id,
}
signed_xml = described_class.sign(sign: true, configuration: configuration) do |xml, signature|
xml.tag!('samlp:AuthnRequest', options) do
@@ -49,7 +49,7 @@ RSpec.describe Saml::Kit::Signature do
expect(signature['SignedInfo']['CanonicalizationMethod']['Algorithm']).to eql('http://www.w3.org/2001/10/xml-exc-c14n#')
expect(signature['SignedInfo']['SignatureMethod']['Algorithm']).to eql("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256")
- expect(signature['SignedInfo']['Reference']['URI']).to eql("#_#{reference_id}")
+ expect(signature['SignedInfo']['Reference']['URI']).to eql("##{reference_id}")
expect(signature['SignedInfo']['Reference']['Transforms']['Transform']).to match_array([
{ "Algorithm" => "http://www.w3.org/2000/09/xmldsig#enveloped-signature" },
{ "Algorithm" => "http://www.w3.org/2001/10/xml-exc-c14n#" }
spec/saml/cryptography_spec.rb → spec/saml/xml_decryption_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-RSpec.describe Saml::Kit::Cryptography do
+RSpec.describe Saml::Kit::XmlDecryption do
describe "#decrypt" do
let(:secret) { FFaker::Movie.title }
let(:password) { FFaker::Movie.title }
spec/saml/xml_spec.rb
@@ -6,13 +6,13 @@ RSpec.describe Saml::Kit::Xml do
let(:logout_url) { "https://#{FFaker::Internet.domain_name}/logout" }
let(:signed_xml) do
- builder = Saml::Kit::ServiceProviderMetadata::Builder.new
- builder.entity_id = FFaker::Movie.title
- builder.add_assertion_consumer_service(login_url, binding: :http_post)
- builder.add_assertion_consumer_service(login_url, binding: :http_redirect)
- builder.add_single_logout_service(logout_url, binding: :http_post)
- builder.add_single_logout_service(logout_url, binding: :http_redirect)
- builder.to_xml
+ Saml::Kit::ServiceProviderMetadata.build do |builder|
+ builder.entity_id = FFaker::Movie.title
+ builder.add_assertion_consumer_service(login_url, binding: :http_post)
+ builder.add_assertion_consumer_service(login_url, binding: :http_redirect)
+ builder.add_single_logout_service(logout_url, binding: :http_post)
+ builder.add_single_logout_service(logout_url, binding: :http_redirect)
+ end.to_xml
end
it 'returns true, when the digest and signature is valid' do
spec/spec_helper.rb
@@ -1,3 +1,7 @@
+require 'simplecov'
+SimpleCov.start do
+ add_filter '/spec/'
+end
require "bundler/setup"
require "saml/kit"
require "active_support/testing/time_helpers"
README.md
@@ -32,7 +32,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
## Contributing
-Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/saml-kit.
+Bug reports and pull requests are welcome on GitHub at https://github.com/mokhan/saml-kit.
## License
saml-kit.gemspec
@@ -23,13 +23,13 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]
spec.add_dependency "activemodel", ">= 4.2.0"
- spec.add_dependency "activesupport", ">= 4.2.0"
spec.add_dependency "builder", "~> 3.2"
spec.add_dependency "nokogiri", "~> 1.8"
spec.add_dependency "xmldsig", "~> 0.6"
spec.add_development_dependency "bundler", "~> 1.15"
+ spec.add_development_dependency "ffaker", "~> 2.7"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "rspec", "~> 3.0"
- spec.add_development_dependency "ffaker", "~> 2.7"
+ spec.add_development_dependency "simplecov", "~> 0.15.1"
spec.add_development_dependency "webmock", "~> 3.1"
end