main
  1# frozen_string_literal: true
  2
  3RSpec.describe Saml::Kit::Assertion do
  4  subject do
  5    described_class.build(user, request) do |x|
  6      x.issuer = entity_id
  7    end
  8  end
  9
 10  let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::Kit::Id.generate, issuer: FFaker::Internet.uri('https'), assertion_consumer_service_url: FFaker::Internet.uri('https'), name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil, signed?: true, trusted?: true) }
 11  let(:user) { User.new(name_id: SecureRandom.uuid, attributes: { id: SecureRandom.uuid }) }
 12  let(:entity_id) { FFaker::Internet.uri('https') }
 13
 14  specify { expect(subject.issuer).to eql(entity_id) }
 15  specify { expect(subject.name_id).to eql(user.name_id) }
 16  specify { expect(subject.name_id_format).to eql(Saml::Kit::Namespaces::PERSISTENT) }
 17  specify { expect(subject.started_at.to_i).to eql(Time.now.utc.to_i) }
 18  specify { expect(subject.expired_at.to_i).to eql(Saml::Kit.configuration.session_timeout.since(Time.now).utc.to_i) }
 19  specify { expect(subject.attributes).to eql('id' => user.attributes[:id]) }
 20  specify { expect(subject.audiences).to match_array([request.issuer]) }
 21
 22  describe '#active?' do
 23    let(:configuration) do
 24      Saml::Kit::Configuration.new do |config|
 25        config.session_timeout = 30.minutes
 26        config.clock_drift = 30.seconds
 27      end
 28    end
 29
 30    it 'is valid after a valid session window + drift' do
 31      now = Time.current
 32      travel_to now
 33      not_on_or_after = configuration.session_timeout.since(now).iso8601
 34      xml = <<-XML.strip_heredoc
 35      <Response xmlns="#{Saml::Kit::Namespaces::PROTOCOL}">
 36        <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{Xml::Kit::Id.generate}" IssueInstant="#{now.iso8601}" Version="2.0">
 37         <Issuer>#{FFaker::Internet.uri('https')}</Issuer>
 38         <Subject>
 39           <NameID Format="#{Saml::Kit::Namespaces::PERSISTENT}">#{SecureRandom.uuid}</NameID>
 40           <SubjectConfirmation Method="#{Saml::Kit::Namespaces::BEARER}">
 41             <SubjectConfirmationData InResponseTo="#{SecureRandom.uuid}" NotOnOrAfter="#{not_on_or_after}" Recipient="#{FFaker::Internet.uri('https')}"/>
 42           </SubjectConfirmation>
 43         </Subject>
 44         <Conditions NotBefore="#{now.utc.iso8601}" NotOnOrAfter="#{not_on_or_after}">
 45           <AudienceRestriction>
 46             <Audience>#{FFaker::Internet.uri('https')}</Audience>
 47           </AudienceRestriction>
 48         </Conditions>
 49         <AuthnStatement AuthnInstant="#{now.utc.iso8601}" SessionIndex="#{Xml::Kit::Id.generate}" SessionNotOnOrAfter="#{not_on_or_after}">
 50           <AuthnContext>
 51             <AuthnContextClassRef>#{Saml::Kit::Namespaces::PASSWORD}</AuthnContextClassRef>
 52           </AuthnContext>
 53         </AuthnStatement>
 54        </Assertion>
 55      </Response>
 56      XML
 57      document = Nokogiri::XML(xml)
 58      node = document.at_xpath('//saml:Assertion', 'saml' => Saml::Kit::Namespaces::ASSERTION)
 59      subject = described_class.new(node, configuration: configuration)
 60      travel_to((configuration.clock_drift - 1.second).before(now))
 61      expect(subject).to be_active
 62      expect(subject).not_to be_expired
 63    end
 64
 65    it 'interprets integers correctly' do
 66      configuration.clock_drift = 30
 67      now = Time.current
 68      travel_to now
 69      not_before = now.utc.iso8601
 70      not_after = configuration.session_timeout.since(now).iso8601
 71      xml = <<-XML.strip_heredoc
 72      <Response xmlns="#{Saml::Kit::Namespaces::PROTOCOL}">
 73        <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{Xml::Kit::Id.generate}" IssueInstant="#{now.iso8601}" Version="2.0">
 74         <Issuer>#{FFaker::Internet.uri('https')}</Issuer>
 75         <Subject>
 76           <NameID Format="#{Saml::Kit::Namespaces::PERSISTENT}">#{SecureRandom.uuid}</NameID>
 77           <SubjectConfirmation Method="#{Saml::Kit::Namespaces::BEARER}">
 78             <SubjectConfirmationData InResponseTo="#{SecureRandom.uuid}" NotOnOrAfter="#{not_after}" Recipient="#{FFaker::Internet.uri('https')}"/>
 79           </SubjectConfirmation>
 80         </Subject>
 81         <Conditions NotBefore="#{not_before}" NotOnOrAfter="#{not_after}">
 82           <AudienceRestriction>
 83             <Audience>#{FFaker::Internet.uri('https')}</Audience>
 84           </AudienceRestriction>
 85         </Conditions>
 86         <AuthnStatement AuthnInstant="#{now.utc.iso8601}" SessionIndex="#{Xml::Kit::Id.generate}" SessionNotOnOrAfter="#{not_after}">
 87           <AuthnContext>
 88             <AuthnContextClassRef>#{Saml::Kit::Namespaces::PASSWORD}</AuthnContextClassRef>
 89           </AuthnContext>
 90         </AuthnStatement>
 91        </Assertion>
 92      </Response>
 93      XML
 94      document = Nokogiri::XML(xml)
 95      node = document.at_xpath('//saml:Assertion', 'saml' => Saml::Kit::Namespaces::ASSERTION)
 96      subject = described_class.new(node, configuration: configuration)
 97      expect(subject).to be_active
 98      expect(subject).not_to be_expired
 99    end
100  end
101
102  describe '#present?' do
103    it 'returns false when the assertion is empty' do
104      subject = described_class.new(nil)
105      expect(subject).not_to be_present
106    end
107
108    it 'returns true when the assertion is present' do
109      not_before = Time.now.utc.iso8601
110      not_after = 10.minutes.from_now.iso8601
111      xml = <<-XML.strip_heredoc
112        <Response>
113        <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{Xml::Kit::Id.generate}" IssueInstant="#{Time.now.iso8601}" Version="2.0">
114         <Issuer>#{FFaker::Internet.uri('https')}</Issuer>
115         <Subject>
116           <NameID Format="#{Saml::Kit::Namespaces::PERSISTENT}">#{SecureRandom.uuid}</NameID>
117           <SubjectConfirmation Method="#{Saml::Kit::Namespaces::BEARER}">
118             <SubjectConfirmationData InResponseTo="#{SecureRandom.uuid}" NotOnOrAfter="#{not_after}" Recipient="#{FFaker::Internet.uri('https')}"/>
119           </SubjectConfirmation>
120         </Subject>
121         <Conditions NotBefore="#{not_before}" NotOnOrAfter="#{not_after}">
122           <AudienceRestriction>
123             <Audience>#{FFaker::Internet.uri('https')}</Audience>
124           </AudienceRestriction>
125         </Conditions>
126         <AuthnStatement AuthnInstant="#{Time.now.utc.iso8601}" SessionIndex="#{Xml::Kit::Id.generate}" SessionNotOnOrAfter="#{not_after}">
127           <AuthnContext>
128             <AuthnContextClassRef>#{Saml::Kit::Namespaces::PASSWORD}</AuthnContextClassRef>
129           </AuthnContext>
130         </AuthnStatement>
131        </Assertion>
132        </Response>
133      XML
134      subject = described_class.new(Nokogiri::XML(xml))
135      expect(subject).to be_present
136    end
137  end
138
139  describe '#signed?' do
140    let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::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) }
141    let(:user) { User.new(attributes: { id: SecureRandom.uuid }) }
142
143    it 'detects a signature in an encrypted assertion' do
144      encryption_key_pair = Xml::Kit::KeyPair.generate(use: :encryption)
145      response = Saml::Kit::Response.build(user, request) do |x|
146        x.sign_with(Xml::Kit::KeyPair.generate(use: :signing))
147        x.encrypt_with(encryption_key_pair)
148      end
149      assertion = response.assertion([encryption_key_pair.private_key])
150      expect(response).to be_signed
151      expect(assertion).to be_signed
152    end
153  end
154
155  describe '#encrypted?' do
156    it 'returns true when encrypted' do
157      key_pair = Xml::Kit::KeyPair.generate(use: :encryption)
158      response = Saml::Kit::Response.build(user, request) do |x|
159        x.encrypt_with(key_pair)
160      end
161      subject = response.assertion([key_pair.private_key])
162      expect(subject).to be_encrypted
163    end
164
165    it 'returns false when not encrypted' do
166      expect(subject).not_to be_encrypted
167    end
168  end
169
170  describe '#to_xml' do
171    let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::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) }
172    let(:user) { User.new(attributes: { id: SecureRandom.uuid }) }
173
174    it 'returns the decrypted xml' do
175      encryption_key_pair = Xml::Kit::KeyPair.generate(use: :encryption)
176      response = Saml::Kit::Response.build(user, request) do |x|
177        x.sign_with(Xml::Kit::KeyPair.generate(use: :signing))
178        x.encrypt_with(encryption_key_pair)
179      end
180      assertion = response.assertion([encryption_key_pair.private_key])
181      expect(assertion.to_xml).not_to include('EncryptedAssertion')
182      expect(assertion.to_xml).to include('Assertion')
183    end
184  end
185
186  describe '#valid?' do
187    let(:entity_id) { FFaker::Internet.uri('https') }
188    let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::Kit::Id.generate, issuer: entity_id, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil, signed?: true, trusted?: true) }
189    let(:name_id) { SecureRandom.uuid }
190    let(:user) { User.new(name_id: name_id, attributes: { id: SecureRandom.uuid }) }
191    let(:registry) { instance_double(Saml::Kit::DefaultRegistry, metadata_for: idp) }
192    let(:idp) { Saml::Kit::IdentityProviderMetadata.build(configuration: configuration) }
193    let(:configuration) do
194      Saml::Kit::Configuration.new do |x|
195        x.entity_id = entity_id
196        x.generate_key_pair_for(use: :signing)
197      end
198    end
199
200    before do
201      allow(configuration.registry).to receive(:metadata_for).with(entity_id).and_return(idp)
202    end
203
204    it 'is invalid when the encrypted signature is invalid' do
205      xml = Saml::Kit::Response.build_xml(user, request, configuration: configuration)
206      altered = xml.gsub(name_id, 'altered')
207      document = Nokogiri::XML(altered)
208      assertion = document.at_xpath('/samlp:Response/saml:Assertion', Saml::Kit::Document::NAMESPACES)
209      key_pair = Xml::Kit::KeyPair.generate(use: :encryption)
210      encrypted = Xml::Kit::EncryptedData.new(assertion.to_xml, asymmetric_cipher: Xml::Kit::Crypto::RsaCipher.new('', key_pair.private_key)).to_xml
211      response = Saml::Kit::Response.new(encrypted, configuration: configuration)
212      expect(response.assertion([key_pair.private_key])).to be_invalid
213    end
214
215    it 'is valid when the encrypted signature is valid' do
216      key_pair = Xml::Kit::KeyPair.generate(use: :encryption)
217      response = Saml::Kit::Response.build(user, request, configuration: configuration) do |x|
218        x.encrypt_with(key_pair)
219      end
220      expect(response.assertion([key_pair.private_key])).to be_valid
221    end
222
223    it 'is invalid when the assertion signature is invalid' do
224      xml = Saml::Kit::Response.build_xml(user, request, configuration: configuration)
225      altered = xml.gsub(name_id, 'altered')
226      response = Saml::Kit::Response.new(altered, configuration: configuration)
227      expect(response.assertion).to be_invalid
228      expect(response.assertion.errors[:digest_value]).to match_array(['is invalid.'])
229    end
230
231    it 'is invalid when the response signature is invalid' do
232      xml = Saml::Kit::Response.build_xml(user, request, configuration: configuration)
233      altered = xml.gsub('StatusCode', 'ALTERED')
234      response = Saml::Kit::Response.new(altered, configuration: configuration)
235      expect(response).to be_invalid
236    end
237
238    it 'is valid' do
239      response = Saml::Kit::Response.build(user, request, configuration: configuration)
240      expect(response.assertion).to be_valid
241    end
242  end
243
244  describe '.new' do
245    let(:user) { instance_double(User, name_id_for: SecureRandom.uuid, assertion_attributes_for: {}) }
246    let(:saml_request) { instance_double(Saml::Kit::AuthenticationRequest, id: Xml::Kit::Id.generate, issuer: configuration.entity_id) }
247    let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
248    let(:configuration) do
249      Saml::Kit::Configuration.new do |x|
250        x.entity_id = FFaker::Internet.uri('https')
251        x.registry = registry
252        x.generate_key_pair_for(use: :signing)
253      end
254    end
255    let(:metadata) do
256      Saml::Kit::Metadata.build(configuration: configuration, &:build_identity_provider)
257    end
258
259    before { allow(registry).to receive(:metadata_for).with(configuration.entity_id).and_return(metadata) }
260
261    it 'parses a raw xml assertion' do
262      saml = described_class.build_xml(user, saml_request, configuration: configuration)
263      expect(described_class.new(saml, configuration: configuration)).to be_valid
264    end
265  end
266end