diff --git a/CHANGELOG.md b/CHANGELOG.md
index 91d13a2..48fee93 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,12 @@
+### 3.0.9
+* Added `Scoping` element to an `AuthnRequest`
+
+### 3.0.8
+* Backward compatibility fix. (#147)
+
+### 3.0.7
+* Added signature config and response location
+
### 3.0.6
* Fix the encryption of an EncryptedID element with multiple recipients.
diff --git a/lib/saml.rb b/lib/saml.rb
index b117228..f944c64 100644
--- a/lib/saml.rb
+++ b/lib/saml.rb
@@ -197,6 +197,9 @@ module Elements
require 'saml/elements/entities_descriptor'
require 'saml/elements/attribute_query'
require 'saml/elements/evidence'
+ require 'saml/elements/idp_entry'
+ require 'saml/elements/idp_list'
+ require 'saml/elements/scoping'
end
module Rails
diff --git a/lib/saml/authn_request.rb b/lib/saml/authn_request.rb
index f29c53d..694e2fc 100644
--- a/lib/saml/authn_request.rb
+++ b/lib/saml/authn_request.rb
@@ -14,6 +14,7 @@ class AuthnRequest
attribute :provider_name, String, :tag => "ProviderName"
has_one :requested_authn_context, Saml::Elements::RequestedAuthnContext
+ has_one :scoping, Saml::Elements::Scoping
validates :force_authn, :inclusion => [true, false, nil]
validates :assertion_consumer_service_index, :numericality => true, :if => lambda { |val|
diff --git a/lib/saml/elements/idp_entry.rb b/lib/saml/elements/idp_entry.rb
new file mode 100644
index 0000000..766f2ad
--- /dev/null
+++ b/lib/saml/elements/idp_entry.rb
@@ -0,0 +1,16 @@
+module Saml
+ module Elements
+ class IdpEntry
+ include Saml::Base
+
+ tag 'IDPEntry'
+ namespace 'samlp'
+
+ attribute :provider_id, String, :tag => 'ProviderID'
+ attribute :name, String, :tag => 'Name'
+ attribute :loc, String, :tag => 'Loc'
+
+ validates :provider_id, :presence => true
+ end
+ end
+end
diff --git a/lib/saml/elements/idp_list.rb b/lib/saml/elements/idp_list.rb
new file mode 100644
index 0000000..615eee8
--- /dev/null
+++ b/lib/saml/elements/idp_list.rb
@@ -0,0 +1,19 @@
+module Saml
+ module Elements
+ class IdpList
+ include Saml::Base
+
+ tag 'IDPList'
+ namespace 'samlp'
+
+ has_many :idp_entries, Saml::Elements::IdpEntry
+
+ validates :idp_entries, :presence => true
+
+ def initialize(*args)
+ super(*args)
+ self.idp_entries ||= []
+ end
+ end
+ end
+end
diff --git a/lib/saml/elements/scoping.rb b/lib/saml/elements/scoping.rb
new file mode 100644
index 0000000..4d96a23
--- /dev/null
+++ b/lib/saml/elements/scoping.rb
@@ -0,0 +1,12 @@
+module Saml
+ module Elements
+ class Scoping
+ include Saml::Base
+
+ tag 'Scoping'
+ namespace 'samlp'
+
+ has_one :idp_list, Saml::Elements::IdpList
+ end
+ end
+end
diff --git a/lib/saml/version.rb b/lib/saml/version.rb
index 1e67ce6..3aa5064 100644
--- a/lib/saml/version.rb
+++ b/lib/saml/version.rb
@@ -1,3 +1,3 @@
module Saml
- VERSION = '3.0.8'
+ VERSION = '3.0.9'
end
diff --git a/spec/factories/all.rb b/spec/factories/all.rb
index 98be907..faba50d 100644
--- a/spec/factories/all.rb
+++ b/spec/factories/all.rb
@@ -245,4 +245,15 @@ class StatementDummy
factory :session_index, class: Saml::Elements::SessionIndex do
value 'SessionIndex'
end
+
+ factory :scoping, :class => Saml::Elements::Scoping do
+ end
+
+ factory :idp_list, :class => Saml::Elements::IdpList do
+ end
+
+ factory :idp_entry, :class => Saml::Elements::IdpEntry do
+ provider_id 'ProviderID'
+ end
+
end
diff --git a/spec/fixtures/authn_request.xml b/spec/fixtures/authn_request.xml
index 8966597..396a2ba 100644
--- a/spec/fixtures/authn_request.xml
+++ b/spec/fixtures/authn_request.xml
@@ -8,4 +8,10 @@
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
+
+
+
+
+
+
diff --git a/spec/lib/saml/authn_request_spec.rb b/spec/lib/saml/authn_request_spec.rb
index 232fae1..18d1d79 100644
--- a/spec/lib/saml/authn_request_spec.rb
+++ b/spec/lib/saml/authn_request_spec.rb
@@ -12,7 +12,7 @@
Saml::AuthnRequest.tag_name.should == "AuthnRequest"
end
- [:force_authn, :assertion_consumer_service_index, :assertion_consumer_service_url, :protocol_binding, :provider_name, :requested_authn_context, :is_passive].each do |attribute|
+ [:force_authn, :assertion_consumer_service_index, :assertion_consumer_service_url, :protocol_binding, :provider_name, :requested_authn_context, :is_passive, :scoping].each do |attribute|
it "should accept the #{attribute} attribute" do
authn_request.should respond_to(attribute)
end
@@ -78,6 +78,10 @@
authn_request.requested_authn_context.should be_a(Saml::Elements::RequestedAuthnContext)
end
+ it 'should create a Saml::Elements::Scoping' do
+ expect(authn_request.scoping).to be_a(Saml::Elements::Scoping)
+ end
+
context "with two requested_authn_context_class_refs" do
let(:authn_request_xml) { File.read(File.join('spec', 'fixtures', 'authn_request_with_two_authn_contexts.xml'))}
diff --git a/spec/lib/saml/elements/idp_entry_spec.rb b/spec/lib/saml/elements/idp_entry_spec.rb
new file mode 100644
index 0000000..1bca621
--- /dev/null
+++ b/spec/lib/saml/elements/idp_entry_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Saml::Elements::IdpEntry do
+ let(:idp_entry) { FactoryGirl.build :idp_entry }
+
+ describe 'optional fields' do
+ [:name, :loc].each do |field|
+ it "should respond to the '#{field}' field" do
+ expect(subject).to respond_to(field)
+ end
+ end
+ end
+
+ describe 'required fields' do
+ [:provider_id].each do |field|
+ it "should have the #{field} field" do
+ subject.should respond_to(field)
+ end
+
+ it "should check the presence of #{field}" do
+ subject.send("#{field}=", nil)
+ subject.should_not be_valid
+ end
+ end
+ end
+
+end
diff --git a/spec/lib/saml/elements/idp_list_spec.rb b/spec/lib/saml/elements/idp_list_spec.rb
new file mode 100644
index 0000000..79f05fb
--- /dev/null
+++ b/spec/lib/saml/elements/idp_list_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe Saml::Elements::IdpList do
+ let(:idp_list) { FactoryGirl.build :idp_list }
+
+ describe 'required fields' do
+ [:idp_entries].each do |field|
+ it "should have the #{field} field" do
+ subject.should respond_to(field)
+ end
+
+ it "should check the presence of #{field}" do
+ subject.send("#{field}=", nil)
+ subject.should_not be_valid
+ end
+ end
+ end
+
+ describe '#idp_entries' do
+ it 'returns an empty array if no IDP entries have been registered' do
+ expect(subject.idp_entries).to eq []
+ end
+ end
+
+ describe '#parse_xml' do
+ let(:authn_request_xml) { File.read(File.join('spec', 'fixtures', 'authn_request.xml')) }
+ let(:idp_list) { Saml::Elements::IdpList.parse(authn_request_xml, single: true) }
+
+ it 'should create a new Saml::Elements::IdpList' do
+ expect(idp_list).to be_a(Saml::Elements::IdpList)
+ end
+
+ it 'should parse the IDP entries' do
+ aggregate_failures do
+ expect(idp_list.idp_entries.count).to eq 2
+
+ expect(idp_list.idp_entries.first).to be_a(Saml::Elements::IdpEntry)
+ expect(idp_list.idp_entries.first.provider_id).to eq 'provider-id-1'
+ expect(idp_list.idp_entries.first.name).to eq 'Provider name 1'
+ expect(idp_list.idp_entries.first.loc).to eq 'https://idp1.example.com'
+
+ expect(idp_list.idp_entries.second).to be_a(Saml::Elements::IdpEntry)
+ expect(idp_list.idp_entries.second.provider_id).to eq 'provider-id-2'
+ expect(idp_list.idp_entries.second.name).to eq 'Provider name 2'
+ expect(idp_list.idp_entries.second.loc).to eq 'https://idp2.example.com'
+ end
+ end
+ end
+
+end
diff --git a/spec/lib/saml/elements/scoping_spec.rb b/spec/lib/saml/elements/scoping_spec.rb
new file mode 100644
index 0000000..b287ca8
--- /dev/null
+++ b/spec/lib/saml/elements/scoping_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Saml::Elements::Scoping do
+ let(:idp_entry) { FactoryGirl.build :scoping }
+
+ describe 'optional fields' do
+ [:idp_list].each do |field|
+ it "should respond to the '#{field}' field" do
+ expect(subject).to respond_to(field)
+ end
+ end
+ end
+
+ describe '#parse_xml' do
+ let(:authn_request_xml) { File.read(File.join('spec', 'fixtures', 'authn_request.xml')) }
+ let(:scoping) { Saml::Elements::Scoping.parse(authn_request_xml, single: true) }
+
+ it 'should create a new Saml::Elements::Scoping' do
+ expect(scoping).to be_a(Saml::Elements::Scoping)
+ end
+
+ it 'should parse the IDP list' do
+ expect(scoping.idp_list).to be_a(Saml::Elements::IdpList)
+ end
+ end
+
+end