Skip to content

Commit 5050ac8

Browse files
committed
Rebase
2 parents 646d8d3 + 05e22f0 commit 5050ac8

6 files changed

Lines changed: 257 additions & 9 deletions

File tree

Gemfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ group :test do
66
if RUBY_VERSION < "1.9"
77
gem "nokogiri", "~> 1.5.0"
88
gem "ruby-debug", "~> 0.10.4"
9+
elsif RUBY_VERSION < "2.0"
10+
gem "debugger", "~> 1.1"
911
else
10-
gem "debugger", "~> 1.1"
12+
gem "byebug", "~> 2.1.1"
1113
end
1214
gem "shoulda", "~> 2.11"
1315
gem "rake", "~> 10"

README.md

Lines changed: 129 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def saml_settings
7373

7474
settings.assertion_consumer_service_url = "http://#{request.host}/saml/finalize"
7575
settings.issuer = request.host
76-
settings.idp_sso_target_url = "https://app.onelogin.com/saml/signon/#{OneLoginAppId}"
76+
settings.idp_sso_target_url = "https://app.onelogin.com/trust/saml2/http-post/sso/#{OneLoginAppId}"
7777
settings.idp_cert_fingerprint = OneLoginAppCertFingerPrint
7878
settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
7979

@@ -126,9 +126,35 @@ class SamlController < ApplicationController
126126
end
127127
end
128128
```
129+
## Metadata Based Configuration
129130

131+
The method above requires a little extra work to manually specify attributes about the IdP. (And your SP application) There's an easier method -- use a metadata exchange. Metadata is just an XML file that defines the capabilities of both the IdP and the SP application. It also contains the X.509 public
132+
key certificates which add to the trusted relationship. The IdP administrator can also configure custom settings for an SP based on the metadata.
130133

131-
If are using saml:AttributeStatement to transfare metadata, like the user name, you can access all the attributes through `response.attributes`. It contains all the saml:AttributeStatement with its 'Name' as a indifferent key and the one saml:AttributeValue as value.
134+
Using ```idp_metadata_parser.parse_remote``` IdP metadata will be added to the settings withouth further ado.
135+
136+
```ruby
137+
def saml_settings
138+
139+
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
140+
# Returns OneLogin::RubySaml::Settings prepopulated with idp metadata
141+
settings = idp_metadata_parser.parse_remote("https://example.com/auth/saml2/idp/metadata")
142+
143+
settings.assertion_consumer_service_url = "http://#{request.host}/saml/consume"
144+
settings.issuer = request.host
145+
settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
146+
# Optional for most SAML IdPs
147+
settings.authn_context = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
148+
149+
settings
150+
end
151+
```
152+
The following attributes are set:
153+
* id_sso_target_url
154+
* idp_slo_target_url
155+
* id_cert_fingerpint
156+
157+
If are using saml:AttributeStatement to transfer metadata, like the user name, you can access all the attributes through `response.attributes`. It contains all the saml:AttributeStatement with its 'Name' as a indifferent key and the one saml:AttributeValue as value.
132158

133159
```ruby
134160
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse])
@@ -137,9 +163,108 @@ response.settings = saml_settings
137163
response.attributes[:username]
138164
```
139165

140-
The saml:AuthnContextClassRef of the AuthNRequest can be provided by `settings.authn_context` , possible values are described at [SAMLAuthnCxt]. The comparison method can be set using the parameter `settings.authn_context_comparison` (the possible values are: 'exact', 'better', 'maximum' and 'minimum'), 'exact' is the default value.
141-
If we want to add a saml:AuthnContextDeclRef, define a `settings.authn_context_decl_ref`.
166+
Imagine this saml:AttributeStatement
167+
168+
```xml
169+
<saml:AttributeStatement>
170+
<saml:Attribute Name="uid">
171+
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">demo</saml:AttributeValue>
172+
</saml:Attribute>
173+
<saml:Attribute Name="another_value">
174+
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">value1</saml:AttributeValue>
175+
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">value2</saml:AttributeValue>
176+
</saml:Attribute>
177+
<saml:Attribute Name="role">
178+
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">role1</saml:AttributeValue>
179+
</saml:Attribute>
180+
<saml:Attribute Name="role">
181+
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">role2</saml:AttributeValue>
182+
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">role3</saml:AttributeValue>
183+
</saml:Attribute>
184+
<saml:Attribute Name="attribute_with_nil_value">
185+
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>
186+
</saml:Attribute>
187+
<saml:Attribute Name="attribute_with_nils_and_empty_strings">
188+
<saml:AttributeValue/>
189+
<saml:AttributeValue>valuePresent</saml:AttributeValue>
190+
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>
191+
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="1"/>
192+
</saml:Attribute>
193+
</saml:AttributeStatement>
194+
```
195+
196+
```ruby
197+
pp(response.attributes) # is an OneLogin::RubySaml::Attributes object
198+
# => @attributes=
199+
{"uid"=>["demo"],
200+
"another_value"=>["value1", "value2"],
201+
"role"=>["role1", "role2", "role3"],
202+
"attribute_with_nil_value"=>[nil],
203+
"attribute_with_nils_and_empty_strings"=>["", "valuePresent", nil, nil]}>
204+
205+
# Active single_value_compatibility
206+
OneLogin::RubySaml::Attributes.single_value_compatibility = true
207+
208+
pp(response.attributes[:uid])
209+
# => "demo"
142210

211+
pp(response.attributes[:role])
212+
# => "role1"
213+
214+
pp(response.attributes.single(:role))
215+
# => "role1"
216+
217+
pp(response.attributes.multi(:role))
218+
# => ["role1", "role2", "role3"]
219+
220+
pp(response.attributes[:attribute_with_nil_value])
221+
# => nil
222+
223+
pp(response.attributes[:attribute_with_nils_and_empty_strings])
224+
# => ""
225+
226+
pp(response.attributes[:not_exists])
227+
# => nil
228+
229+
pp(response.attributes.single(:not_exists))
230+
# => nil
231+
232+
pp(response.attributes.multi(:not_exists))
233+
# => nil
234+
235+
# Deactive single_value_compatibility
236+
OneLogin::RubySaml::Attributes.single_value_compatibility = false
237+
238+
pp(response.attributes[:uid])
239+
# => ["demo"]
240+
241+
pp(response.attributes[:role])
242+
# => ["role1", "role2", "role3"]
243+
244+
pp(response.attributes.single(:role))
245+
# => "role1"
246+
247+
pp(response.attributes.multi(:role))
248+
# => ["role1", "role2", "role3"]
249+
250+
pp(response.attributes[:attribute_with_nil_value])
251+
# => [nil]
252+
253+
pp(response.attributes[:attribute_with_nils_and_empty_strings])
254+
# => ["", "valuePresent", nil, nil]
255+
256+
pp(response.attributes[:not_exists])
257+
# => nil
258+
259+
pp(response.attributes.single(:not_exists))
260+
# => nil
261+
262+
pp(response.attributes.multi(:not_exists))
263+
# => nil
264+
```
265+
266+
The saml:AuthnContextClassRef of the AuthNRequest can be provided by `settings.authn_context` , possible values are described at [SAMLAuthnCxt]. The comparison method can be set using the parameter `settings.authn_context_comparison` (the possible values are: 'exact', 'better', 'maximum' and 'minimum'), 'exact' is the default value.
267+
+If we want to add a saml:AuthnContextDeclRef, define a `settings.authn_context_decl_ref`.
143268

144269
## Service Provider Metadata
145270

lib/onelogin/ruby-saml/attributes.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def include?(name)
4747

4848
# Return first value for an attribute
4949
def single(name)
50-
attributes[canonize_name(name)].first
50+
attributes[canonize_name(name)].first if include?(name)
5151
end
5252

5353
# Return all values for an attribute
@@ -106,4 +106,4 @@ def attributes
106106
end
107107
end
108108
end
109-
end
109+
end

lib/onelogin/ruby-saml/idp_metadata_parser.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ class IdpMetadataParser
1616

1717
attr_reader :document
1818

19+
def parse_remote(url, validate_cert = true)
20+
idp_metadata = get_idp_metadata(url, validate_cert)
21+
parse(idp_metadata)
22+
end
23+
1924
def parse(idp_metadata)
2025
@document = REXML::Document.new(idp_metadata)
2126

@@ -29,6 +34,29 @@ def parse(idp_metadata)
2934

3035
private
3136

37+
# Retrieve the remote IdP metadata from the URL or a cached copy
38+
# # returns a REXML document of the metadata
39+
def get_idp_metadata(url, validate_cert)
40+
uri = URI.parse(url)
41+
if uri.scheme == "http"
42+
response = Net::HTTP.get_response(uri)
43+
meta_text = response.body
44+
elsif uri.scheme == "https"
45+
http = Net::HTTP.new(uri.host, uri.port)
46+
http.use_ssl = true
47+
# Most IdPs will probably use self signed certs
48+
if validate_cert
49+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
50+
else
51+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
52+
end
53+
get = Net::HTTP::Get.new(uri.request_uri)
54+
response = http.request(get)
55+
meta_text = response.body
56+
end
57+
meta_text
58+
end
59+
3260
def single_signon_service_url
3361
node = REXML::XPath.first(document, "/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService/@Location", { "md" => METADATA })
3462
node.value if node

test/idp_metadata_parser_test.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
require File.expand_path(File.join(File.dirname(__FILE__), "test_helper"))
2+
require 'net/http'
3+
require 'net/https'
24

35
class IdpMetadataParserTest < Test::Unit::TestCase
46

7+
class MockResponse
8+
attr_accessor :body
9+
end
10+
511
context "parsing an IdP descriptor file" do
612
should "extract settings details from xml" do
713
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
@@ -14,4 +20,35 @@ class IdpMetadataParserTest < Test::Unit::TestCase
1420
end
1521
end
1622

23+
context "download and parse IdP descriptor file" do
24+
setup do
25+
mock_response = MockResponse.new
26+
mock_response.body = idp_metadata
27+
@url = "https://example.com"
28+
uri = URI(@url)
29+
30+
@http = Net::HTTP.new(uri.host, uri.port)
31+
Net::HTTP.expects(:new).returns(@http)
32+
@http.expects(:request).returns(mock_response)
33+
end
34+
35+
36+
should "extract settings from remote xml" do
37+
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
38+
settings = idp_metadata_parser.parse_remote(@url)
39+
40+
assert_equal "https://example.hello.com/access/saml/login", settings.idp_sso_target_url
41+
assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
42+
assert_equal "https://example.hello.com/access/saml/logout", settings.idp_slo_target_url
43+
assert_equal OpenSSL::SSL::VERIFY_PEER, @http.verify_mode
44+
end
45+
46+
should "accept self signed certificate if insturcted" do
47+
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
48+
settings = idp_metadata_parser.parse_remote(@url, false)
49+
50+
assert_equal OpenSSL::SSL::VERIFY_NONE, @http.verify_mode
51+
end
52+
end
53+
1754
end

test/response_test.rb

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,43 +243,99 @@ class RubySamlTest < Test::Unit::TestCase
243243
assert_equal "demo", response.attributes[:uid]
244244
end
245245

246+
should "extract single value as string in compatibility mode off" do
247+
response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values))
248+
OneLogin::RubySaml::Attributes.single_value_compatibility = false
249+
assert_equal ["demo"], response.attributes[:uid]
250+
# classes are not reloaded between tests so restore default
251+
OneLogin::RubySaml::Attributes.single_value_compatibility = true
252+
end
253+
246254
should "extract first of multiple values as string for b/w compatibility" do
247255
response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values))
248256
assert_equal 'value1', response.attributes[:another_value]
249257
end
250258

259+
should "extract first of multiple values as string for b/w compatibility in compatibility mode off" do
260+
response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values))
261+
OneLogin::RubySaml::Attributes.single_value_compatibility = false
262+
assert_equal ['value1', 'value2'], response.attributes[:another_value]
263+
OneLogin::RubySaml::Attributes.single_value_compatibility = true
264+
end
265+
251266
should "return array with all attributes when asked in XML order" do
252267
response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values))
253268
assert_equal ['value1', 'value2'], response.attributes.multi(:another_value)
254269
end
255270

271+
should "return array with all attributes when asked in XML order in compatibility mode off" do
272+
response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values))
273+
OneLogin::RubySaml::Attributes.single_value_compatibility = false
274+
assert_equal ['value1', 'value2'], response.attributes.multi(:another_value)
275+
OneLogin::RubySaml::Attributes.single_value_compatibility = true
276+
end
277+
256278
should "return first of multiple values when multiple Attribute tags in XML" do
257279
response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values))
258280
assert_equal 'role1', response.attributes[:role]
259281
end
260282

283+
should "return first of multiple values when multiple Attribute tags in XML in compatibility mode off" do
284+
response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values))
285+
OneLogin::RubySaml::Attributes.single_value_compatibility = false
286+
assert_equal ['role1', 'role2', 'role3'], response.attributes[:role]
287+
OneLogin::RubySaml::Attributes.single_value_compatibility = true
288+
end
289+
261290
should "return all of multiple values in reverse order when multiple Attribute tags in XML" do
262291
response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values))
263292
assert_equal ['role1', 'role2', 'role3'], response.attributes.multi(:role)
264293
end
265294

295+
should "return all of multiple values in reverse order when multiple Attribute tags in XML in compatibility mode off" do
296+
response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values))
297+
OneLogin::RubySaml::Attributes.single_value_compatibility = false
298+
assert_equal ['role1', 'role2', 'role3'], response.attributes.multi(:role)
299+
OneLogin::RubySaml::Attributes.single_value_compatibility = true
300+
end
301+
266302
should "return nil value correctly" do
267303
response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values))
268304
assert_nil response.attributes[:attribute_with_nil_value]
269305
end
270306

307+
should "return nil value correctly when not in compatibility mode off" do
308+
response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values))
309+
OneLogin::RubySaml::Attributes.single_value_compatibility = false
310+
assert_equal [nil], response.attributes[:attribute_with_nil_value]
311+
OneLogin::RubySaml::Attributes.single_value_compatibility = true
312+
end
313+
271314
should "return multiple values including nil and empty string" do
272315
response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values))
273316
assert_equal ["", "valuePresent", nil, nil], response.attributes.multi(:attribute_with_nils_and_empty_strings)
274317
end
275318

276-
should "return multiple values from [] when not in compatibility mode" do
319+
should "return multiple values from [] when not in compatibility mode off" do
277320
response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values))
278321
OneLogin::RubySaml::Attributes.single_value_compatibility = false
279322
assert_equal ["", "valuePresent", nil, nil], response.attributes[:attribute_with_nils_and_empty_strings]
280-
# classes are not reloaded between tests so restore default
281323
OneLogin::RubySaml::Attributes.single_value_compatibility = true
282324
end
325+
326+
should "check what happens when trying retrieve attribute that does not exists" do
327+
response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values))
328+
assert_equal nil, response.attributes[:attribute_not_exists]
329+
assert_equal nil, response.attributes.single(:attribute_not_exists)
330+
assert_equal nil, response.attributes.multi(:attribute_not_exists)
331+
332+
OneLogin::RubySaml::Attributes.single_value_compatibility = false
333+
assert_equal nil, response.attributes[:attribute_not_exists]
334+
assert_equal nil, response.attributes.single(:attribute_not_exists)
335+
assert_equal nil, response.attributes.multi(:attribute_not_exists)
336+
OneLogin::RubySaml::Attributes.single_value_compatibility = true
337+
end
338+
283339
end
284340
end
285341

0 commit comments

Comments
 (0)