main
1# frozen_string_literal: true
2
3RSpec.describe Saml::Kit::IdentityProviderMetadata do
4 subject { described_class.new(raw_metadata) }
5
6 describe 'okta metadata' do
7 let(:raw_metadata) { IO.read('spec/fixtures/metadata/okta.xml') }
8 let(:certificate) do
9 Hash.from_xml(raw_metadata)['EntityDescriptor']['IDPSSODescriptor']['KeyDescriptor']['KeyInfo']['X509Data']['X509Certificate']
10 end
11
12 it { expect(subject.entity_id).to eql('http://www.okta.com/1') }
13 it { expect(subject.name_id_formats).to match_array([Saml::Kit::Namespaces::EMAIL_ADDRESS, Saml::Kit::Namespaces::UNSPECIFIED_NAMEID]) }
14
15 it do
16 location = 'https://dev.oktapreview.com/app/example/1/sso/saml'
17 expect(subject.single_sign_on_services.map(&:to_h)).to match_array([
18 { binding: Saml::Kit::Bindings::HTTP_POST, location: location },
19 { binding: Saml::Kit::Bindings::HTTP_REDIRECT, location: location },
20 ])
21 end
22
23 it { expect(subject.single_logout_services).to be_empty }
24
25 it do
26 fingerprint = '9F:74:13:3B:BC:5A:7B:8B:2D:4F:8B:EF:1E:88:EB:D1:AE:BC:19:BF:CA:19:C6:2F:0F:4B:31:1D:68:98:B0:1B'
27 expect(subject.certificates).to match_array([::Xml::Kit::Certificate.new(certificate, use: :signing)])
28 expect(subject.certificates.first.fingerprint.to_s).to eql(fingerprint)
29 end
30
31 it { expect(subject.attributes).to be_empty }
32 end
33
34 describe 'active directory' do
35 let(:raw_metadata) { IO.read('spec/fixtures/metadata/ad_2012.xml') }
36 let(:xml_hash) { Hash.from_xml(raw_metadata) }
37 let(:signing_certificate) do
38 xml_hash['EntityDescriptor']['IDPSSODescriptor']['KeyDescriptor'].find { |x| x['use'] == 'signing' }['KeyInfo']['X509Data']['X509Certificate']
39 end
40 let(:encryption_certificate) do
41 xml_hash['EntityDescriptor']['IDPSSODescriptor']['KeyDescriptor'].find { |x| x['use'] == 'encryption' }['KeyInfo']['X509Data']['X509Certificate']
42 end
43
44 it { expect(subject.entity_id).to eql('http://www.example.com/adfs/services/trust') }
45
46 it do
47 expect(subject.name_id_formats).to match_array([
48 Saml::Kit::Namespaces::EMAIL_ADDRESS,
49 Saml::Kit::Namespaces::PERSISTENT,
50 Saml::Kit::Namespaces::TRANSIENT,
51 ])
52 end
53
54 it do
55 location = 'https://www.example.com/adfs/ls/'
56 expect(subject.single_sign_on_services.map(&:to_h)).to match_array([
57 { location: location, binding: Saml::Kit::Bindings::HTTP_REDIRECT },
58 { location: location, binding: Saml::Kit::Bindings::HTTP_POST },
59 ])
60 end
61
62 it do
63 location = 'https://www.example.com/adfs/ls/'
64 expect(subject.single_logout_services.map(&:to_h)).to match_array([
65 { location: location, binding: Saml::Kit::Bindings::HTTP_REDIRECT },
66 { location: location, binding: Saml::Kit::Bindings::HTTP_POST },
67 ])
68 end
69
70 it do
71 expect(subject.certificates).to match_array([
72 ::Xml::Kit::Certificate.new(signing_certificate, use: :signing),
73 ::Xml::Kit::Certificate.new(encryption_certificate, use: :encryption),
74 ])
75 end
76
77 it { expect(subject.attributes).to be_present }
78 end
79
80 describe '#validate' do
81 it 'valid when given valid identity provider metadata' do
82 subject = described_class.build do |builder|
83 builder.attributes = [:email]
84 builder.add_single_sign_on_service(FFaker::Internet.http_url, binding: :http_post)
85 builder.add_single_sign_on_service(FFaker::Internet.http_url, binding: :http_redirect)
86 builder.add_single_logout_service(FFaker::Internet.http_url, binding: :http_post)
87 builder.add_single_logout_service(FFaker::Internet.http_url, binding: :http_redirect)
88 end
89 expect(subject).to be_valid
90 end
91
92 it 'is invalid, when given service provider metadata' do
93 service_provider_metadata = Saml::Kit::ServiceProviderMetadata.build.to_xml
94 subject = described_class.new(service_provider_metadata)
95 expect(subject).not_to be_valid
96 expect(subject.errors[:base]).to include(I18n.translate('saml/kit.errors.IDPSSODescriptor.invalid'))
97 end
98
99 it 'is invalid, when the metadata is nil' do
100 subject = described_class.new(nil)
101 expect(subject).not_to be_valid
102 expect(subject.errors[:metadata]).to include("can't be blank")
103 end
104
105 it 'is invalid, when the metadata does not validate against the xsd schema' do
106 xml = ::Builder::XmlMarkup.new
107 xml.instruct!
108 xml.EntityDescriptor 'xmlns': Saml::Kit::Namespaces::METADATA do
109 xml.IDPSSODescriptor do
110 xml.Fake foo: :bar
111 end
112 end
113 subject = described_class.new(xml.target!)
114 expect(subject).not_to be_valid
115 expect(subject.errors[:base][0]).to include("1:0: ERROR: Element '{urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor'")
116 end
117
118 it 'is invalid, when the signature is invalid' do
119 old_url = 'https://www.example.com/adfs/ls/'
120 new_url = 'https://myserver.com/hacked'
121 metadata_xml = IO.read('spec/fixtures/metadata/ad_2012.xml').gsub(old_url, new_url)
122
123 subject = described_class.new(metadata_xml)
124 expect(subject).to be_invalid
125 expect(subject.errors[:base]).to be_empty
126 expect(subject.errors[:digest_value]).to match_array(['is invalid.'])
127 expect(subject.errors[:signature]).to match_array(['is invalid.'])
128 end
129 end
130
131 describe '#single_sign_on_service_for' do
132 subject do
133 described_class.build do |builder|
134 builder.add_single_sign_on_service(redirect_url, binding: :http_redirect)
135 builder.add_single_sign_on_service(post_url, binding: :http_post)
136 end
137 end
138
139 let(:post_url) { FFaker::Internet.http_url }
140 let(:redirect_url) { FFaker::Internet.http_url }
141
142 it 'returns the POST binding' do
143 result = subject.single_sign_on_service_for(binding: :http_post)
144 expect(result.location).to eql(post_url)
145 expect(result.binding).to eql(Saml::Kit::Bindings::HTTP_POST)
146 end
147
148 it 'returns the HTTP_REDIRECT binding' do
149 result = subject.single_sign_on_service_for(binding: :http_redirect)
150 expect(result.location).to eql(redirect_url)
151 expect(result.binding).to eql(Saml::Kit::Bindings::HTTP_REDIRECT)
152 end
153
154 it 'returns nil if the binding cannot be found' do
155 expect(subject.single_sign_on_service_for(binding: :soap)).to be_nil
156 end
157 end
158
159 describe '#want_authn_requests_signed' do
160 it 'returns true when enabled' do
161 subject = described_class.build do |builder|
162 builder.want_authn_requests_signed = true
163 end
164 expect(subject.want_authn_requests_signed).to be(true)
165 end
166
167 it 'returns false when disabled' do
168 subject = described_class.build do |builder|
169 builder.want_authn_requests_signed = false
170 end
171 expect(subject.want_authn_requests_signed).to be(false)
172 end
173
174 it 'returns true when the attribute is missing' do
175 xml = described_class.build do |builder|
176 builder.want_authn_requests_signed = false
177 end.to_xml.gsub('WantAuthnRequestsSigned="false"', '')
178 subject = described_class.new(xml)
179 expect(subject.want_authn_requests_signed).to be(true)
180 end
181 end
182
183 describe '#single_logout_service_for' do
184 let(:redirect_url) { FFaker::Internet.uri('https') }
185 let(:post_url) { FFaker::Internet.uri('https') }
186 let(:subject) do
187 described_class.build do |builder|
188 builder.add_single_logout_service(redirect_url, binding: :http_redirect)
189 builder.add_single_logout_service(post_url, binding: :http_post)
190 end
191 end
192
193 it 'returns the location for the matching binding' do
194 expect(subject.single_logout_service_for(binding: :http_post).location).to eql(post_url)
195 expect(subject.single_logout_service_for(binding: :http_redirect).location).to eql(redirect_url)
196 end
197
198 it 'returns nil if the binding is not available' do
199 expect(subject.single_logout_service_for(binding: :soap)).to be_nil
200 end
201 end
202
203 describe '.build' do
204 let(:url) { FFaker::Internet.uri('https') }
205 let(:entity_id) { FFaker::Internet.uri('https') }
206
207 it 'provides a nice API for building metadata' do
208 result = described_class.build do |builder|
209 builder.entity_id = entity_id
210 builder.add_single_sign_on_service(url, binding: :http_post)
211 end
212
213 expect(result).to be_instance_of(described_class)
214 expect(result.entity_id).to eql(entity_id)
215 expect(result.single_sign_on_service_for(binding: :http_post).location).to eql(url)
216 end
217 end
218
219 describe '#login_request_for' do
220 it 'returns a serialized login request' do
221 subject = described_class.build do |x|
222 x.add_single_sign_on_service(FFaker::Internet.uri('https'), binding: :http_post)
223 end
224 _url, saml_params = subject.login_request_for(binding: :http_post, relay_state: FFaker::Movie.title)
225 result = subject.single_sign_on_service_for(binding: :http_post).deserialize(saml_params)
226 expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
227 end
228 end
229end