Comparing changes

v1.0.31 v1.1.0
12 commits 20 files changed

Commits

2707192 update changelog mo 2019-04-30 21:07:32
022d60b publish 1.1.0 mo 2019-04-30 21:05:39
f6f0c74 drop ruby 2.2, 2.3 mo 2019-04-30 20:52:16
ec6f782 fix linter errors mo 2019-04-30 20:49:29
14713a6 collapse rescue blocks mo 2019-04-23 22:59:55
0d74c5d fix failing test mo 2019-04-23 22:57:33
ec85c17 update gems mo 2019-04-23 22:56:00
b7b5757 fix linter errors mo 2019-04-23 22:37:15
bin/cibuild
@@ -17,5 +17,5 @@ export RUBY_HEAP_SLOTS_INCREMENT=400000
 export RUBY_HEAP_SLOTS_GROWTH_FACTOR=1
 
 ruby -v
-gem install bundler --conservative
+gem install bundler --conservative -v '~> 2.0'
 bin/test
lib/saml/kit/builders/authentication_request.rb
@@ -10,6 +10,7 @@ module Saml
         attr_accessor :id, :now, :issuer, :assertion_consumer_service_url
         attr_accessor :name_id_format, :destination
         attr_accessor :version
+        attr_accessor :force_authn
         attr_reader :configuration
 
         def initialize(configuration: Saml::Kit.configuration)
@@ -36,10 +37,8 @@ module Saml
             IssueInstant: now.utc.iso8601,
             Destination: destination,
           }
-          if assertion_consumer_service_url.present?
-            options[:AssertionConsumerServiceURL] =
-              assertion_consumer_service_url
-          end
+          options[:ForceAuthn] = force_authn unless force_authn.nil?
+          options[:AssertionConsumerServiceURL] = assertion_consumer_service_url if assertion_consumer_service_url.present?
           options
         end
       end
lib/saml/kit/deprecated/metadata.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module Saml
   module Kit
     class Metadata
lib/saml/kit/authentication_request.rb
@@ -47,6 +47,11 @@ module Saml
         at_xpath('./*/@AssertionConsumerServiceURL').try(:value)
       end
 
+      # Returns the ForceAuthn attribute as a boolean.
+      def force_authn
+        at_xpath('./*/@ForceAuthn').try(:value) == 'true'
+      end
+
       def name_id_format
         name_id_policy
       end
lib/saml/kit/bindings.rb
@@ -11,10 +11,10 @@ module Saml
     # the different SAML bindings that are
     # supported by this gem.
     module Bindings
-      BINDINGS_2_0 = 'urn:oasis:names:tc:SAML:2.0:bindings'.freeze
-      HTTP_ARTIFACT = "#{BINDINGS_2_0}:HTTP-Artifact".freeze
-      HTTP_POST = "#{BINDINGS_2_0}:HTTP-POST".freeze
-      HTTP_REDIRECT = "#{BINDINGS_2_0}:HTTP-Redirect".freeze
+      BINDINGS_2_0 = 'urn:oasis:names:tc:SAML:2.0:bindings'
+      HTTP_ARTIFACT = "#{BINDINGS_2_0}:HTTP-Artifact"
+      HTTP_POST = "#{BINDINGS_2_0}:HTTP-POST"
+      HTTP_REDIRECT = "#{BINDINGS_2_0}:HTTP-Redirect"
       ALL = {
         http_post: HTTP_POST,
         http_redirect: HTTP_REDIRECT,
lib/saml/kit/namespaces.rb
@@ -3,33 +3,33 @@
 module Saml
   module Kit
     module Namespaces
-      SAML_2_0 = 'urn:oasis:names:tc:SAML:2.0'.freeze
-      SAML_1_1 = 'urn:oasis:names:tc:SAML:1.1'.freeze
-      ATTR_NAME_FORMAT = "#{SAML_2_0}:attrname-format".freeze
-      NAME_ID_FORMAT_1_1 = "#{SAML_1_1}:nameid-format".freeze
-      NAME_ID_FORMAT_2_0 = "#{SAML_2_0}:nameid-format".freeze
-      STATUS = "#{SAML_2_0}:status".freeze
+      SAML_2_0 = 'urn:oasis:names:tc:SAML:2.0'
+      SAML_1_1 = 'urn:oasis:names:tc:SAML:1.1'
+      ATTR_NAME_FORMAT = "#{SAML_2_0}:attrname-format"
+      NAME_ID_FORMAT_1_1 = "#{SAML_1_1}:nameid-format"
+      NAME_ID_FORMAT_2_0 = "#{SAML_2_0}:nameid-format"
+      STATUS = "#{SAML_2_0}:status"
 
-      ASSERTION = "#{SAML_2_0}:assertion".freeze
-      ATTR_SPLAT = "#{ATTR_NAME_FORMAT}:*".freeze
-      BASIC = "#{ATTR_NAME_FORMAT}:basic".freeze
-      BEARER = "#{SAML_2_0}:cm:bearer".freeze
-      EMAIL_ADDRESS = "#{NAME_ID_FORMAT_1_1}:emailAddress".freeze
-      INVALID_NAME_ID_POLICY = "#{STATUS}:InvalidNameIDPolicy".freeze
-      METADATA = "#{SAML_2_0}:metadata".freeze
-      PASSWORD = "#{SAML_2_0}:ac:classes:Password".freeze
+      ASSERTION = "#{SAML_2_0}:assertion"
+      ATTR_SPLAT = "#{ATTR_NAME_FORMAT}:*"
+      BASIC = "#{ATTR_NAME_FORMAT}:basic"
+      BEARER = "#{SAML_2_0}:cm:bearer"
+      EMAIL_ADDRESS = "#{NAME_ID_FORMAT_1_1}:emailAddress"
+      INVALID_NAME_ID_POLICY = "#{STATUS}:InvalidNameIDPolicy"
+      METADATA = "#{SAML_2_0}:metadata"
+      PASSWORD = "#{SAML_2_0}:ac:classes:Password"
       PASSWORD_PROTECTED =
-        "#{SAML_2_0}:ac:classes:PasswordProtectedTransport".freeze
-      PERSISTENT = "#{NAME_ID_FORMAT_2_0}:persistent".freeze
-      PROTOCOL = "#{SAML_2_0}:protocol".freeze
-      REQUESTER_ERROR = "#{STATUS}:Requester".freeze
-      RESPONDER_ERROR = "#{STATUS}:Responder".freeze
-      SUCCESS = "#{STATUS}:Success".freeze
-      TRANSIENT = "#{NAME_ID_FORMAT_2_0}:transient".freeze
-      UNSPECIFIED = "#{SAML_2_0}:consent:unspecified".freeze
-      UNSPECIFIED_NAMEID = "#{NAME_ID_FORMAT_1_1}:unspecified".freeze
-      URI = "#{ATTR_NAME_FORMAT}:uri".freeze
-      VERSION_MISMATCH_ERROR = "#{STATUS}:VersionMismatch".freeze
+        "#{SAML_2_0}:ac:classes:PasswordProtectedTransport"
+      PERSISTENT = "#{NAME_ID_FORMAT_2_0}:persistent"
+      PROTOCOL = "#{SAML_2_0}:protocol"
+      REQUESTER_ERROR = "#{STATUS}:Requester"
+      RESPONDER_ERROR = "#{STATUS}:Responder"
+      SUCCESS = "#{STATUS}:Success"
+      TRANSIENT = "#{NAME_ID_FORMAT_2_0}:transient"
+      UNSPECIFIED = "#{SAML_2_0}:consent:unspecified"
+      UNSPECIFIED_NAMEID = "#{NAME_ID_FORMAT_1_1}:unspecified"
+      URI = "#{ATTR_NAME_FORMAT}:uri"
+      VERSION_MISMATCH_ERROR = "#{STATUS}:VersionMismatch"
     end
   end
 end
lib/saml/kit/organization.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module Saml
   module Kit
     class Organization
lib/saml/kit/signature.rb
@@ -105,7 +105,7 @@ module Saml
         dsignature.errors.each do |attribute|
           errors.add(attribute, error_message(attribute))
         end
-      rescue Xmldsig::SchemaError => error
+      rescue StandardError => error
         errors.add(:base, error.message)
       end
 
lib/saml/kit/version.rb
@@ -2,6 +2,6 @@
 
 module Saml
   module Kit
-    VERSION = '1.0.31'.freeze
+    VERSION = '1.1.0'
   end
 end
spec/fixtures/unsigned_response_two_assertions.xml
@@ -0,0 +1,113 @@
+<?xml version="1.0"?>
+<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" Destination="https://portal.dev/sessions/acs" ID="_f6a486e0-29e2-0135-23c6-20999b09e5e7" InResponseTo="_0890e87d-1b33-4d0d-8875-776b50bf3359" IssueInstant="2017-06-02T17:00:54Z" Version="2.0">
+  <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">https://portal/sessions/metadata</Issuer>
+  <samlp:Status>
+    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
+  </samlp:Status>
+  <Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="_03c49290-29e5-0135-23c7-20999b09e5e7" IssueInstant="2017-06-02T17:15:35Z" Version="2.0">
+    <Issuer>http://auth.dev/auth/metadata</Issuer>
+    <Subject>
+      <NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">mokha@cisco.com</NameID>
+      <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
+        <SubjectConfirmationData InResponseTo="_e2d943d8-8c0f-4de6-b58a-0ded2d016b85" NotOnOrAfter="2017-06-02T17:18:35Z" Recipient="https://portal.dev/sessions/acs"/>
+      </SubjectConfirmation>
+    </Subject>
+    <Conditions NotBefore="2017-06-02T17:15:30Z" NotOnOrAfter="2017-06-02T18:15:35Z">
+      <AudienceRestriction>
+        <Audience>https://portal.dev/sessions/metadata</Audience>
+      </AudienceRestriction>
+    </Conditions>
+    <AttributeStatement>
+      <Attribute FriendlyName="user_id" Name="user_id" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+        <AttributeValue>a44550cf-9839-49fb-a101-10a741afe16b</AttributeValue>
+      </Attribute>
+      <Attribute FriendlyName="success_notice" Name="success_notice" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"/>
+      <Attribute FriendlyName="business_guid" Name="business_guid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+        <AttributeValue>4198398a-8cd3-4539-a936-5b34e35513ac</AttributeValue>
+      </Attribute>
+      <Attribute FriendlyName="event_intake_url" Name="event_intake_url" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+        <AttributeValue/>
+      </Attribute>
+      <Attribute FriendlyName="console_base_url" Name="console_base_url" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+        <AttributeValue/>
+      </Attribute>
+      <Attribute FriendlyName="auth_token" Name="auth_token" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+        <AttributeValue>8185b86b3e19fe9782fd69c790b2d185627e9b68bff229fb</AttributeValue>
+      </Attribute>
+      <Attribute FriendlyName="amp_user_role" Name="amp_user_admin" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+        <AttributeValue>true</AttributeValue>
+      </Attribute>
+      <Attribute FriendlyName="amp_business_name" Name="amp_business_name" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+        <AttributeValue>Business for mokha@cisco.com</AttributeValue>
+      </Attribute>
+    </AttributeStatement>
+    <AuthnStatement AuthnInstant="2017-06-02T17:15:35Z" SessionIndex="_03c49290-29e5-0135-23c7-20999b09e5e7">
+      <AuthnContext>
+        <AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef>
+      </AuthnContext>
+    </AuthnStatement>
+  </Assertion>
+  <Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="_03c49290-29e5-0135-23c7-20999b09e5e7" IssueInstant="2017-06-02T17:15:35Z" Version="2.0">
+    <Issuer>http://auth.dev/auth/metadata</Issuer>
+    <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="#_03c49290-29e5-0135-23c7-20999b09e5e7">
+          <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#"/>
+          </ds:Transforms>
+          <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+          <ds:DigestValue>00HKo34VqiMWtfJC6V2ZECp/gGCyXpmsoAJ7d1ApBlI=</ds:DigestValue>
+        </ds:Reference>
+      </ds:SignedInfo>
+      <ds:SignatureValue>WsM5KurVpKx9ewETIoWM9hrXKbDGybwCA7mgp0v4bUuq4njpGCDVwfLyOvc7zGbeJ2KIZ3IRF5fra3y97xlXXnEbwUth1b43liXi/SvOawkI38AGyu9CVqu2PgX+tt73in81Z1n8w0esZpy1L1mdgZqLLTpgVee+feEO6fd4TfPqy2VdLJJaSWWdIhyIEsK2pN7sO8476KS+PMcazhy15lGXR8/NEtzSC39t7NpfYg4CHHOypOHLnkiuY3sOC9Y3DLK/vUG/yx/43BCMDksW4mPNXFMQEoRb3+Hc0yEN5liz73oZa02wSwUYioj2FTCU2Ll003pgY/+E0kIV5hIzpg==</ds:SignatureValue>
+      <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+        <ds:X509Data>
+          <ds:X509Certificate>MIID2zCCAsOgAwIBAgIJANEwdtPv2CcfMA0GCSqGSIb3DQEBCwUAMIGDMQswCQYDVQQGEwJDQTEQMA4GA1UECAwHQWxiZXJ0YTEQMA4GA1UEBwwHQ2FsZ2FyeTEOMAwGA1UECgwFQ2lzY28xDDAKBgNVBAsMA0FNUDESMBAGA1UEAwwJY2lzY28uY29tMR4wHAYJKoZIhvcNAQkBFg9wamlzbEBjaXNjby5jb20wHhcNMTYwMjIzMTgyMDEzWhcNMjYwMjIyMTgyMDEzWjCBgzELMAkGA1UEBhMCQ0ExEDAOBgNVBAgMB0FsYmVydGExEDAOBgNVBAcMB0NhbGdhcnkxDjAMBgNVBAoMBUNpc2NvMQwwCgYDVQQLDANBTVAxEjAQBgNVBAMMCWNpc2NvLmNvbTEeMBwGCSqGSIb3DQEJARYPcGppc2xAY2lzY28uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApY6BaL1HZn3OQM0zPV+5+KkpJc90fUKGI0mf4WmbPB3NPF1qKhoFVS9cZclyL4y6vRhL88lZBp87xkz3M1pz14HLTdls4NsOGzw1xKMizFzP/sEBmn5gL2fWHkkb5mFtrWa1PqEM5cTWPYvej/5fKM4qdQtW3kZFuMghacCV9Y9xGObkucZMRov1Yy8Hea3vEH/SQ5a/7NJNFiHzCQ2109OCkxpTV0njcYeIBQxUFkR2UKWID1wbiEqvjAurtA8siq/CPpwMjkAGiqiaRMDroHCRpyc3ZIDiew49es3txwqJrZJPgiXy7HdIdCAaigFsWAvi6YdVEEyNExeb3giObwIDAQABo1AwTjAdBgNVHQ4EFgQUbpy9sDuGyct0uIu3OYkyI7GVd4EwHwYDVR0jBBgwFoAUbpy9sDuGyct0uIu3OYkyI7GVd4EwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAXTUOqaz1a7McsgiU6u09iQ4HUZKk5WgsoIjkMJY/jE1I8GsOlhly1tHgPmOVHe/0QxZg+YB4MV/d83708NpBiK+qGzE9mjCC457tcfzd762VcpexmLo5SR+JWOBFXSFgddrEhBy8eWXk075vdQt/fmB8S4MwWSiICcwY4rTddwy4LBQ4yqsLh7u7y1XptZG20jd1VIBsFP1kcrxlPTDxhulHJkgADWf+N/nyhUOTSOmFlaXK7LJFJDjqL4uslP85iPnEzjCRrNRCrI6eQleTrrTdev1u32RMj5nyrcp1h9G3Jt5/gyJChM7Bar3bwJMZpv7cmdGmYa3Lsx6mRgO6Gg==</ds:X509Certificate>
+        </ds:X509Data>
+      </KeyInfo>
+    </ds:Signature>
+    <Subject>
+      <NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">mokha@cisco.com</NameID>
+      <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
+        <SubjectConfirmationData InResponseTo="_e2d943d8-8c0f-4de6-b58a-0ded2d016b85" NotOnOrAfter="2017-06-02T17:18:35Z" Recipient="https://portal.dev/sessions/acs"/>
+      </SubjectConfirmation>
+    </Subject>
+    <Conditions NotBefore="2017-06-02T17:15:30Z" NotOnOrAfter="2017-06-02T18:15:35Z">
+      <AudienceRestriction>
+        <Audience>https://portal.dev/sessions/metadata</Audience>
+      </AudienceRestriction>
+    </Conditions>
+    <AttributeStatement>
+      <Attribute FriendlyName="user_id" Name="user_id" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+        <AttributeValue>a44550cf-9839-49fb-a101-10a741afe16b</AttributeValue>
+      </Attribute>
+      <Attribute FriendlyName="success_notice" Name="success_notice" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"/>
+      <Attribute FriendlyName="business_guid" Name="business_guid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+        <AttributeValue>4198398a-8cd3-4539-a936-5b34e35513ac</AttributeValue>
+      </Attribute>
+      <Attribute FriendlyName="event_intake_url" Name="event_intake_url" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+        <AttributeValue/>
+      </Attribute>
+      <Attribute FriendlyName="console_base_url" Name="console_base_url" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+        <AttributeValue/>
+      </Attribute>
+      <Attribute FriendlyName="auth_token" Name="auth_token" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+        <AttributeValue>8185b86b3e19fe9782fd69c790b2d185627e9b68bff229fb</AttributeValue>
+      </Attribute>
+      <Attribute FriendlyName="amp_user_role" Name="amp_user_admin" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+        <AttributeValue>true</AttributeValue>
+      </Attribute>
+      <Attribute FriendlyName="amp_business_name" Name="amp_business_name" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+        <AttributeValue>Business for mokha@cisco.com</AttributeValue>
+      </Attribute>
+    </AttributeStatement>
+    <AuthnStatement AuthnInstant="2017-06-02T17:15:35Z" SessionIndex="_03c49290-29e5-0135-23c7-20999b09e5e7">
+      <AuthnContext>
+        <AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef>
+      </AuthnContext>
+    </AuthnStatement>
+  </Assertion>
+</samlp:Response>
spec/saml/kit/bindings/url_builder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 RSpec.describe Saml::Kit::Bindings::UrlBuilder do
   describe '#build' do
     let(:xml) { '<xml></xml>' }
@@ -57,9 +59,9 @@ RSpec.describe Saml::Kit::Bindings::UrlBuilder do
           query_params = to_query_params(result)
           expect(query_params['SigAlg']).to eql(CGI.escape(::Xml::Kit::Namespaces::SHA256))
 
-          payload = "#{query_string_parameter}=#{query_params[query_string_parameter]}"
-          payload << "&RelayState=#{query_params['RelayState']}"
-          payload << "&SigAlg=#{query_params['SigAlg']}"
+          payload = "#{query_string_parameter}=#{query_params[query_string_parameter]}" \
+                    "&RelayState=#{query_params['RelayState']}" \
+                    "&SigAlg=#{query_params['SigAlg']}"
           private_key = configuration.private_keys(use: :signing).last
           expected_signature = Base64.strict_encode64(private_key.sign(OpenSSL::Digest::SHA256.new, payload))
           expect(query_params['Signature']).to eql(expected_signature)
@@ -70,8 +72,8 @@ RSpec.describe Saml::Kit::Bindings::UrlBuilder do
           query_params = to_query_params(result)
           expect(query_params['SigAlg']).to eql(CGI.escape(::Xml::Kit::Namespaces::SHA256))
 
-          payload = "#{query_string_parameter}=#{query_params[query_string_parameter]}"
-          payload << "&SigAlg=#{query_params['SigAlg']}"
+          payload = "#{query_string_parameter}=#{query_params[query_string_parameter]}" \
+                    "&SigAlg=#{query_params['SigAlg']}"
           private_key = configuration.private_keys(use: :signing).last
           expected_signature = Base64.strict_encode64(private_key.sign(OpenSSL::Digest::SHA256.new, payload))
           expect(query_params['Signature']).to eql(expected_signature)
spec/saml/kit/builders/assertion_builder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 RSpec.describe Saml::Kit::Builders::Assertion do
spec/saml/kit/builders/authentication_request_spec.rb
@@ -25,5 +25,13 @@ RSpec.describe Saml::Kit::Builders::AuthenticationRequest do
       expect(result['AuthnRequest']['Issuer']).to eql(issuer)
       expect(result['AuthnRequest']['NameIDPolicy']['Format']).to eql(Saml::Kit::Namespaces::PERSISTENT)
     end
+
+    context 'when force authn is enabled' do
+      before { subject.force_authn = true }
+
+      let(:result) { Hash.from_xml(subject.to_xml) }
+
+      specify { expect(result['AuthnRequest']['ForceAuthn']).to eql('true') }
+    end
   end
 end
spec/saml/kit/authentication_request_spec.rb
@@ -30,6 +30,7 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
   specify { expect(subject.assertion_consumer_service_url).to eql(assertion_consumer_service_url) }
   specify { expect(subject.name_id_format).to eql(name_id_format) }
   specify { expect(subject.destination).to eql(destination) }
+  specify { expect(subject.force_authn).to be(false) }
 
   describe '#valid?' do
     let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
@@ -186,6 +187,26 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
     end
   end
 
+  describe '#force_authn' do
+    context 'when set to true' do
+      subject { described_class.build { |x| x.force_authn = true } }
+
+      specify { expect(subject.force_authn).to be(true) }
+    end
+
+    context 'when set to false' do
+      subject { described_class.build { |x| x.force_authn = false } }
+
+      specify { expect(subject.force_authn).to be(false) }
+    end
+
+    context 'when not specified' do
+      subject { described_class.build { |x| x.force_authn = nil } }
+
+      specify { expect(subject.force_authn).to be(false) }
+    end
+  end
+
   describe '.build' do
     let(:url) { FFaker::Internet.uri('https') }
     let(:entity_id) { FFaker::Internet.uri('https') }
spec/saml/kit/response_spec.rb
@@ -241,6 +241,13 @@ RSpec.describe Saml::Kit::Response do
       expect(subject.errors.full_messages).to include('must contain a single Assertion.')
     end
 
+    it 'is invalid if there are two assertions (one signed and the other unsigned)' do
+      raw_xml = IO.read('spec/fixtures/unsigned_response_two_assertions.xml')
+      subject = described_class.new(raw_xml)
+      expect(subject).not_to be_valid
+      expect(subject.errors.full_messages).to include('must contain a single Assertion.')
+    end
+
     it 'is invalid when the assertion has a signature and has been tampered with' do
       user = User.new(attributes: { token: SecureRandom.uuid })
       request = Saml::Kit::AuthenticationRequest.build
.rubocop.yml
@@ -12,7 +12,7 @@ AllCops:
     - 'spec/examples/**/*'
     - 'tmp/**/*'
     - 'vendor/**/*'
-  TargetRubyVersion: 2.2
+  TargetRubyVersion: 2.4
 
 Layout/AlignParameters:
   Enabled: true
.travis.yml
@@ -3,13 +3,12 @@ env:
   - CC_TEST_REPORTER_ID=256cf27053220ac6b8962d6aef566e28753bc58633348ffef9274d3e1a48b31c
 language: ruby
 rvm:
-  - 2.2.10
-  - 2.3.8
   - 2.4.6
   - 2.5.5
-  - 2.6.2
+  - 2.6.3
 before_install:
-  - "gem install bundler -v '~> 1.0'"
+  - gem update --system
+  - gem install bundler -v '~> 2.0'
 before_script:
   - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
   - chmod +x ./cc-test-reporter
CHANGELOG.md
@@ -1,4 +1,5 @@
-Version 1.0.31
+Version 1.1.0
+
 # Changelog
 All notable changes to this project will be documented in this file.
 
@@ -8,6 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ## [Unreleased]
 - nil
 
+## [1.1.0] - 2019-04-30
+### Added
+- Add support for ForceAuthn attribute on AuthnRequest
+
+### Removed
+- Drop support for ruby 2.2
+- Drop support for ruby 2.3
+
+### Changed
+- Rescue from invalid signature validation
+- Change minimum ruby version to 2.4
+- Change minimum bundler to 2.0
+
 ## [1.0.31] - 2019-04-17
 ### Changed
 - Rescue from all decryption errors
@@ -69,7 +83,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Removed
 - Removed optional SessionNotOnOrAfter attribute from AuthnStatement.
 
-[Unreleased]: https://github.com/saml-kit/saml-kit/compare/v1.0.31...HEAD
+[Unreleased]: https://github.com/saml-kit/saml-kit/compare/v1.1.0...HEAD
+[1.1.0]: https://github.com/saml-kit/saml-kit/compare/v1.0.31...v1.1.0
 [1.0.31]: https://github.com/saml-kit/saml-kit/compare/v1.0.30...v1.0.31
 [1.0.30]: https://github.com/saml-kit/saml-kit/compare/v1.0.29...v1.0.30
 [1.0.29]: https://github.com/saml-kit/saml-kit/compare/v1.0.28...v1.0.29
Gemfile.lock
@@ -1,7 +1,7 @@
 PATH
   remote: .
   specs:
-    saml-kit (1.0.31)
+    saml-kit (1.1.0)
       activemodel (>= 4.2.0)
       net-hippie (~> 0.1)
       xml-kit (>= 0.3.0, < 1.0.0)
@@ -19,8 +19,9 @@ GEM
     addressable (2.6.0)
       public_suffix (>= 2.0.2, < 4.0)
     ast (2.4.0)
-    benchmark-perf (0.4.0)
-    benchmark-trend (0.2.0)
+    benchmark-malloc (0.1.0)
+    benchmark-perf (0.5.0)
+    benchmark-trend (0.3.0)
     builder (3.2.3)
     bundler-audit (0.6.1)
       bundler (>= 1.2.0, < 3)
@@ -31,15 +32,15 @@ GEM
     diff-lcs (1.3)
     docile (1.3.1)
     ffaker (2.11.0)
-    hashdiff (0.3.8)
-    i18n (1.5.1)
+    hashdiff (0.3.9)
+    i18n (1.6.0)
       concurrent-ruby (~> 1.0)
     jaro_winkler (1.5.2)
     json (2.2.0)
     mini_portile2 (2.4.0)
     minitest (5.11.3)
     net-hippie (0.2.5)
-    nokogiri (1.9.1)
+    nokogiri (1.10.3)
       mini_portile2 (~> 2.4.0)
     parallel (1.17.0)
     parser (2.6.2.1)
@@ -52,13 +53,14 @@ GEM
       rspec-core (~> 3.8.0)
       rspec-expectations (~> 3.8.0)
       rspec-mocks (~> 3.8.0)
-    rspec-benchmark (0.4.0)
-      benchmark-perf (~> 0.4.0)
-      benchmark-trend (~> 0.2.0)
+    rspec-benchmark (0.5.0)
+      benchmark-malloc (~> 0.1.0)
+      benchmark-perf (~> 0.5.0)
+      benchmark-trend (~> 0.3.0)
       rspec (>= 3.0.0, < 4.0.0)
     rspec-core (3.8.0)
       rspec-support (~> 3.8.0)
-    rspec-expectations (3.8.2)
+    rspec-expectations (3.8.3)
       diff-lcs (>= 1.2.0, < 2.0)
       rspec-support (~> 3.8.0)
     rspec-mocks (3.8.0)
@@ -106,7 +108,7 @@ PLATFORMS
   ruby
 
 DEPENDENCIES
-  bundler (~> 1.17)
+  bundler (~> 2.0)
   bundler-audit (~> 0.6)
   ffaker (~> 2.7)
   rake (~> 10.0)
@@ -120,4 +122,4 @@ DEPENDENCIES
   webmock (~> 3.1)
 
 BUNDLED WITH
-   1.17.3
+   2.0.1
saml-kit.gemspec
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
   spec.description   = 'A simple toolkit for working with SAML.'
   spec.homepage      = 'https://github.com/saml-kit/saml-kit'
   spec.license       = 'MIT'
-  spec.required_ruby_version = '>= 2.2.0'
+  spec.required_ruby_version = '~> 2.4'
 
   spec.files = `git ls-files -z`.split("\x0").reject do |f|
     (
@@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
   spec.add_dependency 'activemodel', '>= 4.2.0'
   spec.add_dependency 'net-hippie', '~> 0.1'
   spec.add_dependency 'xml-kit', '>= 0.3.0', '< 1.0.0'
-  spec.add_development_dependency 'bundler', '~> 1.17'
+  spec.add_development_dependency 'bundler', '~> 2.0'
   spec.add_development_dependency 'bundler-audit', '~> 0.6'
   spec.add_development_dependency 'ffaker', '~> 2.7'
   spec.add_development_dependency 'rake', '~> 10.0'