Commit 6a6a768
Changed files (5)
lib
saml
kit
spec
saml
builders
lib/saml/kit/builders/response.rb
@@ -0,0 +1,182 @@
+module Saml
+ module Kit
+ class Response < Document
+ 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
+ end
+ end
+end
lib/saml/kit/builders.rb
@@ -2,3 +2,4 @@ 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'
lib/saml/kit/response.rb
@@ -98,183 +98,6 @@ module Saml
Saml::Kit.logger.error(error)
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
end
end
end
spec/saml/builders/response_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Response::Builder do
+ describe "#to_xml" do
+ subject { described_class.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
+end
spec/saml/response_spec.rb
@@ -26,69 +26,6 @@ RSpec.describe Saml::Kit::Response do
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(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: { id: SecureRandom.uuid }) }