saml2 1.0.10 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +2 -7
  3. data/lib/saml2.rb +2 -0
  4. data/lib/saml2/attribute.rb +2 -0
  5. data/lib/saml2/attribute_consuming_service.rb +1 -0
  6. data/lib/saml2/authn_request.rb +19 -47
  7. data/lib/saml2/base.rb +5 -2
  8. data/lib/saml2/bindings.rb +7 -0
  9. data/lib/saml2/bindings/http_redirect.rb +141 -0
  10. data/lib/saml2/contact.rb +14 -16
  11. data/lib/saml2/endpoint.rb +5 -6
  12. data/lib/saml2/entity.rb +23 -18
  13. data/lib/saml2/identity_provider.rb +4 -4
  14. data/lib/saml2/indexed_object.rb +7 -3
  15. data/lib/saml2/key.rb +19 -1
  16. data/lib/saml2/logout_request.rb +43 -0
  17. data/lib/saml2/logout_response.rb +23 -0
  18. data/lib/saml2/message.rb +109 -0
  19. data/lib/saml2/name_id.rb +16 -8
  20. data/lib/saml2/organization_and_contacts.rb +2 -2
  21. data/lib/saml2/request.rb +8 -0
  22. data/lib/saml2/response.rb +7 -23
  23. data/lib/saml2/role.rb +2 -3
  24. data/lib/saml2/service_provider.rb +24 -2
  25. data/lib/saml2/sso.rb +2 -2
  26. data/lib/saml2/status.rb +28 -0
  27. data/lib/saml2/status_response.rb +33 -0
  28. data/lib/saml2/version.rb +1 -1
  29. data/spec/fixtures/identity_provider.xml +1 -0
  30. data/spec/fixtures/response_signed.xml +1 -1
  31. data/spec/fixtures/response_with_attribute_signed.xml +1 -1
  32. data/spec/lib/attribute_consuming_service_spec.rb +37 -37
  33. data/spec/lib/attribute_spec.rb +17 -17
  34. data/spec/lib/authn_request_spec.rb +15 -71
  35. data/spec/lib/bindings/http_redirect_spec.rb +151 -0
  36. data/spec/lib/conditions_spec.rb +10 -10
  37. data/spec/lib/entity_spec.rb +12 -12
  38. data/spec/lib/identity_provider_spec.rb +4 -4
  39. data/spec/lib/indexed_object_spec.rb +38 -7
  40. data/spec/lib/logout_request_spec.rb +31 -0
  41. data/spec/lib/logout_response_spec.rb +31 -0
  42. data/spec/lib/message_spec.rb +21 -0
  43. data/spec/lib/response_spec.rb +8 -9
  44. data/spec/lib/service_provider_spec.rb +29 -8
  45. data/spec/spec_helper.rb +0 -1
  46. metadata +41 -11
@@ -15,11 +15,11 @@ module SAML2
15
15
  end
16
16
 
17
17
  def single_logout_services
18
- @single_logout_services ||= load_object_array(@root, 'md:SingleLogoutService', Endpoint)
18
+ @single_logout_services ||= load_object_array(xml, 'md:SingleLogoutService', Endpoint)
19
19
  end
20
20
 
21
21
  def name_id_formats
22
- @name_id_formats ||= load_string_array(@root, 'md:NameIDFormat')
22
+ @name_id_formats ||= load_string_array(xml, 'md:NameIDFormat')
23
23
  end
24
24
 
25
25
  protected
@@ -0,0 +1,28 @@
1
+ require 'saml2/base'
2
+
3
+ module SAML2
4
+ class Status < Base
5
+ SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success".freeze
6
+
7
+ attr_accessor :code, :message
8
+
9
+ def initialize(code = SUCCESS, message = nil)
10
+ @code, @message = code, message
11
+ end
12
+
13
+ def from_xml(node)
14
+ super
15
+ self.code = node.at_xpath('samlp:StatusCode', Namespaces::ALL)['Value']
16
+ self.message = load_string_array(xml, 'samlp:StatusMessage')
17
+ end
18
+
19
+ def build(builder)
20
+ builder['samlp'].Status do |status|
21
+ status['samlp'].StatusCode(Value: code)
22
+ Array(message).each do |m|
23
+ status['samlp'].StatusMessage(m)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,33 @@
1
+ require 'saml2/message'
2
+ require 'saml2/status'
3
+
4
+ module SAML2
5
+ class StatusResponse < Message
6
+ attr_accessor :in_response_to, :status
7
+
8
+ def initialize
9
+ super
10
+ @status = Status.new
11
+ end
12
+
13
+ def from_xml(node)
14
+ super
15
+ @status = nil
16
+ remove_instance_variable(:@status)
17
+ end
18
+
19
+ def status
20
+ @status ||= Status.from_xml(xml.at_xpath('samlp:Status', Namespaces::ALL))
21
+ end
22
+
23
+ protected
24
+
25
+ def build(status_response)
26
+ super(status_response)
27
+
28
+ status_response.parent['InResponseTo'] = in_response_to if in_response_to
29
+
30
+ status.build(status_response)
31
+ end
32
+ end
33
+ end
@@ -1,3 +1,3 @@
1
1
  module SAML2
2
- VERSION = '1.0.10'
2
+ VERSION = '1.1.0'
3
3
  end
@@ -36,6 +36,7 @@
36
36
  </ds:X509Data>
37
37
  </ds:KeyInfo>
38
38
  </KeyDescriptor>
39
+ <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.school.edu/idp/profile/SAML2/Redirect/Logout"
39
40
  <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.school.edu:8443/idp/profile/SAML2/SOAP/ArtifactResolution" index="1"/>
40
41
  <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding" Location="https://sso.school.edu:8443/idp/profile/SAML1/SOAP/ArtifactResolution" index="2"/>
41
42
  <SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest" Location="https://sso.school.edu/idp/profile/Shibboleth/SSO"/>
@@ -1,5 +1,5 @@
1
1
  <?xml version="1.0"?>
2
- <samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_9a15e699-2d04-4ba7-a521-cfa4dcd21f44" Version="2.0" IssueInstant="2015-02-12T22:51:29Z" Destination="https://siteadmin.test.instructure.com/saml_consume" InResponseTo="_bec424fa5103428909a30ff1e31168327f79474984"><saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">issuer</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status><saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_cdfc3faf-90ad-462f-880d-677483210684" Version="2.0" IssueInstant="2015-02-12T22:51:29Z"><saml:Issuer>issuer</saml:Issuer><Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
2
+ <samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_9a15e699-2d04-4ba7-a521-cfa4dcd21f44" Version="2.0" IssueInstant="2015-02-12T22:51:29Z" Destination="https://siteadmin.test.instructure.com/saml_consume" InResponseTo="_bec424fa5103428909a30ff1e31168327f79474984"><saml:Issuer>issuer</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status><saml:Assertion ID="_cdfc3faf-90ad-462f-880d-677483210684" Version="2.0" IssueInstant="2015-02-12T22:51:29Z"><saml:Issuer>issuer</saml:Issuer><Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
3
3
  <SignedInfo>
4
4
  <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5
5
  <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
@@ -1,5 +1,5 @@
1
1
  <?xml version="1.0"?>
2
- <samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_9a15e699-2d04-4ba7-a521-cfa4dcd21f44" Version="2.0" IssueInstant="2015-02-12T22:51:29Z" Destination="https://siteadmin.test.instructure.com/saml_consume" InResponseTo="_bec424fa5103428909a30ff1e31168327f79474984"><saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">issuer</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status><saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_cdfc3faf-90ad-462f-880d-677483210684" Version="2.0" IssueInstant="2015-02-12T22:51:29Z"><saml:Issuer>issuer</saml:Issuer><Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
2
+ <samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_9a15e699-2d04-4ba7-a521-cfa4dcd21f44" Version="2.0" IssueInstant="2015-02-12T22:51:29Z" Destination="https://siteadmin.test.instructure.com/saml_consume" InResponseTo="_bec424fa5103428909a30ff1e31168327f79474984"><saml:Issuer>issuer</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status><saml:Assertion ID="_cdfc3faf-90ad-462f-880d-677483210684" Version="2.0" IssueInstant="2015-02-12T22:51:29Z"><saml:Issuer>issuer</saml:Issuer><Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
3
3
  <SignedInfo>
4
4
  <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
5
5
  <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
@@ -12,37 +12,37 @@ module SAML2
12
12
  end
13
13
 
14
14
  it "should require name attribute" do
15
- -> { acs.create_statement({}) }.must_raise RequiredAttributeMissing
15
+ expect { acs.create_statement({}) }.to raise_error RequiredAttributeMissing
16
16
  end
17
17
 
18
18
  it "should create a statement" do
19
19
  stmt = acs.create_statement('name' => 'cody')
20
- stmt.attributes.length.must_equal 1
21
- stmt.attributes.first.name.must_equal 'name'
22
- stmt.attributes.first.value.must_equal 'cody'
20
+ expect(stmt.attributes.length).to eq 1
21
+ expect(stmt.attributes.first.name).to eq 'name'
22
+ expect(stmt.attributes.first.value).to eq 'cody'
23
23
  end
24
24
 
25
25
  it "should include optional attributes" do
26
26
  stmt = acs.create_statement('name' => 'cody', 'age' => 29)
27
- stmt.attributes.length.must_equal 2
28
- stmt.attributes.first.name.must_equal 'name'
29
- stmt.attributes.first.value.must_equal 'cody'
30
- stmt.attributes.last.name.must_equal 'age'
31
- stmt.attributes.last.value.must_equal 29
27
+ expect(stmt.attributes.length).to eq 2
28
+ expect(stmt.attributes.first.name).to eq 'name'
29
+ expect(stmt.attributes.first.value).to eq 'cody'
30
+ expect(stmt.attributes.last.name).to eq 'age'
31
+ expect(stmt.attributes.last.value).to eq 29
32
32
  end
33
33
 
34
34
  it "should ignore extra attributes" do
35
35
  stmt = acs.create_statement('name' => 'cody', 'height' => 73)
36
- stmt.attributes.length.must_equal 1
37
- stmt.attributes.first.name.must_equal 'name'
38
- stmt.attributes.first.value.must_equal 'cody'
36
+ expect(stmt.attributes.length).to eq 1
37
+ expect(stmt.attributes.first.name).to eq 'name'
38
+ expect(stmt.attributes.first.value).to eq 'cody'
39
39
  end
40
40
 
41
41
  it "should materialize deferred attributes" do
42
42
  stmt = acs.create_statement('name' => -> { 'cody' })
43
- stmt.attributes.length.must_equal 1
44
- stmt.attributes.first.name.must_equal 'name'
45
- stmt.attributes.first.value.must_equal 'cody'
43
+ expect(stmt.attributes.length).to eq 1
44
+ expect(stmt.attributes.first.name).to eq 'name'
45
+ expect(stmt.attributes.first.value).to eq 'cody'
46
46
  end
47
47
 
48
48
  it "should match explicit name formats" do
@@ -50,24 +50,24 @@ module SAML2
50
50
  stmt = acs.create_statement([Attribute.new('name', 'cody', nil, 'format'),
51
51
  Attribute.new('name', 'unspecified'),
52
52
  Attribute.new('name', 'other', nil, 'otherformat')])
53
- stmt.attributes.length.must_equal 1
54
- stmt.attributes.first.name.must_equal 'name'
55
- stmt.attributes.first.value.must_equal 'cody'
53
+ expect(stmt.attributes.length).to eq 1
54
+ expect(stmt.attributes.first.name).to eq 'name'
55
+ expect(stmt.attributes.first.value).to eq 'cody'
56
56
  end
57
57
 
58
58
  it "should match explicitly requested name formats" do
59
59
  acs.requested_attributes.first.name_format = 'format'
60
60
  stmt = acs.create_statement('name' => 'cody')
61
- stmt.attributes.length.must_equal 1
62
- stmt.attributes.first.name.must_equal 'name'
63
- stmt.attributes.first.value.must_equal 'cody'
61
+ expect(stmt.attributes.length).to eq 1
62
+ expect(stmt.attributes.first.name).to eq 'name'
63
+ expect(stmt.attributes.first.value).to eq 'cody'
64
64
  end
65
65
 
66
66
  it "should match explicitly provided name formats" do
67
67
  stmt = acs.create_statement([Attribute.new('name', 'cody', 'format')])
68
- stmt.attributes.length.must_equal 1
69
- stmt.attributes.first.name.must_equal 'name'
70
- stmt.attributes.first.value.must_equal 'cody'
68
+ expect(stmt.attributes.length).to eq 1
69
+ expect(stmt.attributes.first.name).to eq 'name'
70
+ expect(stmt.attributes.first.value).to eq 'cody'
71
71
  end
72
72
 
73
73
  it "requires that provided attributes match a single default" do
@@ -75,11 +75,11 @@ module SAML2
75
75
  attr = RequestedAttribute.new('attr')
76
76
  attr.value = 'value'
77
77
  acs.requested_attributes << attr
78
- -> { acs.create_statement('attr' => 'something') }.must_raise InvalidAttributeValue
78
+ expect { acs.create_statement('attr' => 'something') }.to raise_error InvalidAttributeValue
79
79
  stmt = acs.create_statement('attr' => 'value')
80
- stmt.attributes.length.must_equal 1
81
- stmt.attributes.first.name.must_equal 'attr'
82
- stmt.attributes.first.value.must_equal 'value'
80
+ expect(stmt.attributes.length).to eq 1
81
+ expect(stmt.attributes.first.name).to eq 'attr'
82
+ expect(stmt.attributes.first.value).to eq 'value'
83
83
  end
84
84
 
85
85
  it "requires that provided attributes be from allowed enumeration" do
@@ -87,11 +87,11 @@ module SAML2
87
87
  attr = RequestedAttribute.new('attr')
88
88
  attr.value = ['value1', 'value2']
89
89
  acs.requested_attributes << attr
90
- -> { acs.create_statement('attr' => 'something') }.must_raise InvalidAttributeValue
90
+ expect { acs.create_statement('attr' => 'something') }.to raise_error InvalidAttributeValue
91
91
  stmt = acs.create_statement('attr' => 'value1')
92
- stmt.attributes.length.must_equal 1
93
- stmt.attributes.first.name.must_equal 'attr'
94
- stmt.attributes.first.value.must_equal 'value1'
92
+ expect(stmt.attributes.length).to eq 1
93
+ expect(stmt.attributes.first.name).to eq 'attr'
94
+ expect(stmt.attributes.first.value).to eq 'value1'
95
95
  end
96
96
 
97
97
  it "auto-provides missing required attribute with a default" do
@@ -100,9 +100,9 @@ module SAML2
100
100
  attr.value = 'value'
101
101
  acs.requested_attributes << attr
102
102
  stmt = acs.create_statement({})
103
- stmt.attributes.length.must_equal 1
104
- stmt.attributes.first.name.must_equal 'attr'
105
- stmt.attributes.first.value.must_equal 'value'
103
+ expect(stmt.attributes.length).to eq 1
104
+ expect(stmt.attributes.first.name).to eq 'attr'
105
+ expect(stmt.attributes.first.value).to eq 'value'
106
106
  end
107
107
 
108
108
  it "doesn't auto-provide missing required attribute with an enumeration" do
@@ -110,7 +110,7 @@ module SAML2
110
110
  attr = RequestedAttribute.new('attr', true)
111
111
  attr.value = ['value1', 'value2']
112
112
  acs.requested_attributes << attr
113
- -> { acs.create_statement({}) }.must_raise RequiredAttributeMissing
113
+ expect { acs.create_statement({}) }.to raise_error RequiredAttributeMissing
114
114
  end
115
115
 
116
116
  it "doesn't auto-provide missing non-required attribute with a default" do
@@ -119,7 +119,7 @@ module SAML2
119
119
  attr.value = 'value'
120
120
  acs.requested_attributes << attr
121
121
  stmt = acs.create_statement({})
122
- assert_nil(stmt)
122
+ expect(stmt).to be_nil
123
123
  end
124
124
 
125
125
  end
@@ -21,22 +21,22 @@ XML
21
21
 
22
22
  it "should auto-parse X500 attributes" do
23
23
  attr = Attribute.from_xml(Nokogiri::XML(eduPersonPrincipalNameXML).root)
24
- attr.must_be_instance_of Attribute::X500
25
- attr.value.must_equal "user@domain"
26
- attr.name.must_equal Attribute::X500::EduPerson::PRINCIPAL_NAME
27
- attr.friendly_name.must_equal 'eduPersonPrincipalName'
28
- attr.name_format.must_equal Attribute::NameFormats::URI
24
+ expect(attr).to be_instance_of Attribute::X500
25
+ expect(attr.value).to eq "user@domain"
26
+ expect(attr.name).to eq Attribute::X500::EduPerson::PRINCIPAL_NAME
27
+ expect(attr.friendly_name).to eq 'eduPersonPrincipalName'
28
+ expect(attr.name_format).to eq Attribute::NameFormats::URI
29
29
  end
30
30
 
31
31
  it "should serialize an X500 attribute correctly" do
32
32
  attr = Attribute.create('eduPersonPrincipalName', 'user@domain')
33
- attr.must_be_instance_of Attribute::X500
34
- attr.value.must_equal "user@domain"
35
- attr.name.must_equal Attribute::X500::EduPerson::PRINCIPAL_NAME
36
- attr.friendly_name.must_equal 'eduPersonPrincipalName'
37
- attr.name_format.must_equal Attribute::NameFormats::URI
33
+ expect(attr).to be_instance_of Attribute::X500
34
+ expect(attr.value).to eq "user@domain"
35
+ expect(attr.name).to eq Attribute::X500::EduPerson::PRINCIPAL_NAME
36
+ expect(attr.friendly_name).to eq 'eduPersonPrincipalName'
37
+ expect(attr.name_format).to eq Attribute::NameFormats::URI
38
38
 
39
- serialize(attr).must_equal eduPersonPrincipalNameXML
39
+ expect(serialize(attr)).to eq eduPersonPrincipalNameXML
40
40
  end
41
41
 
42
42
  it "should parse and serialize boolean values" do
@@ -49,10 +49,10 @@ XML
49
49
  XML
50
50
 
51
51
  stmt = AttributeStatement.from_xml(Nokogiri::XML(xml).root)
52
- stmt.attributes.first.value.must_equal true
52
+ expect(stmt.attributes.first.value).to eq true
53
53
 
54
54
  # serializes canonically
55
- serialize(stmt).must_equal(xml.sub('>1<', '>true<'))
55
+ expect(serialize(stmt)).to eq(xml.sub('>1<', '>true<'))
56
56
  end
57
57
 
58
58
  it "should parse and serialize dateTime values" do
@@ -65,10 +65,10 @@ XML
65
65
  XML
66
66
 
67
67
  stmt = AttributeStatement.from_xml(Nokogiri::XML(xml).root)
68
- stmt.attributes.first.value.must_equal Time.at(1435603023)
68
+ expect(stmt.attributes.first.value).to eq Time.at(1435603023)
69
69
 
70
70
  # serializes canonically
71
- serialize(stmt).must_equal(xml)
71
+ expect(serialize(stmt)).to eq xml
72
72
  end
73
73
 
74
74
  it "should parse values with different namespace prefixes" do
@@ -79,7 +79,7 @@ XML
79
79
  XML
80
80
 
81
81
  attr = Attribute.from_xml(Nokogiri::XML(xml).root)
82
- attr.value.must_equal false
82
+ expect(attr.value).to eq false
83
83
  end
84
84
 
85
85
  it "should parse untagged values" do
@@ -90,7 +90,7 @@ XML
90
90
  XML
91
91
 
92
92
  attr = Attribute.from_xml(Nokogiri::XML(xml).root)
93
- attr.value.must_equal "something"
93
+ expect(attr.value).to eq "something"
94
94
  end
95
95
 
96
96
  end
@@ -8,97 +8,41 @@ module SAML2
8
8
  describe '.decode' do
9
9
  it "should not choke on empty string" do
10
10
  authnrequest = AuthnRequest.decode('')
11
- authnrequest.valid_schema?.must_equal false
11
+ expect(authnrequest.valid_schema?).to eq false
12
12
  end
13
13
 
14
14
  it "should not choke on garbage" do
15
15
  authnrequest = AuthnRequest.decode('abc')
16
- authnrequest.valid_schema?.must_equal false
17
- end
18
-
19
- it "doesn't allow deflate bombs" do
20
- bomb = <<-BOMB
21
- 7cExAQAAAMKg9U9tDB+gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
22
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
23
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
24
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
25
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
26
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
27
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
28
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
29
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
30
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
31
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
32
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
33
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
34
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
35
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
36
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
37
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
38
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
39
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
40
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
41
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
42
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
43
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
44
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
45
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
46
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
47
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
48
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
49
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
50
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
51
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
52
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
53
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
54
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
55
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
56
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
57
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
58
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
59
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
60
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
61
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
62
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
63
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
64
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
65
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
66
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAL4G
67
- BOMB
68
- -> { AuthnRequest.decode(bomb) }.must_raise MessageTooLarge
16
+ expect(authnrequest.valid_schema?).to eq false
69
17
  end
70
18
  end
71
19
 
72
20
  it "should be valid" do
73
- request.valid_schema?.must_equal true
74
- request.resolve(sp).must_equal true
75
- request.assertion_consumer_service.location.must_equal "https://siteadmin.test.instructure.com/saml_consume"
21
+ expect(request.valid_schema?).to eq true
22
+ expect(request.resolve(sp)).to eq true
23
+ expect(request.assertion_consumer_service.location).to eq "https://siteadmin.test.instructure.com/saml_consume"
76
24
  end
77
25
 
78
26
  it "should not be valid if the ACS url is not in the SP" do
79
- request.stub(:assertion_consumer_service_url, "garbage") do
80
- request.resolve(sp).must_equal false
81
- end
27
+ allow(request).to receive(:assertion_consumer_service_url).and_return("garbage")
28
+ expect(request.resolve(sp)).to eq false
82
29
  end
83
30
 
84
31
  it "should use the default ACS if not specified" do
85
- request.stub(:assertion_consumer_service_url, nil) do
86
- request.resolve(sp).must_equal true
87
- request.assertion_consumer_service.location.must_equal "https://siteadmin.instructure.com/saml_consume"
88
- end
32
+ allow(request).to receive(:assertion_consumer_service_url).and_return(nil)
33
+ expect(request.resolve(sp)).to eq true
34
+ expect(request.assertion_consumer_service.location).to eq "https://siteadmin.instructure.com/saml_consume"
89
35
  end
90
36
 
91
37
  it "should find the ACS by index" do
92
- request.stub(:assertion_consumer_service_url, nil) do
93
- request.stub(:assertion_consumer_service_index, 2) do
94
- request.resolve(sp).must_equal true
95
- request.assertion_consumer_service.location.must_equal "https://siteadmin.beta.instructure.com/saml_consume"
96
- end
97
- end
38
+ allow(request).to receive(:assertion_consumer_service_url).and_return(nil)
39
+ allow(request).to receive(:assertion_consumer_service_index).and_return(2)
40
+ expect(request.resolve(sp)).to eq true
41
+ expect(request.assertion_consumer_service.location).to eq "https://siteadmin.beta.instructure.com/saml_consume"
98
42
  end
99
43
 
100
44
  it "should find the NameID policy" do
101
- request.name_id_policy.must_equal NameID::Policy.new(true, NameID::Format::PERSISTENT)
45
+ expect(request.name_id_policy).to eq NameID::Policy.new(true, NameID::Format::PERSISTENT)
102
46
  end
103
47
  end
104
48
  end
@@ -0,0 +1,151 @@
1
+ require_relative '../../spec_helper'
2
+
3
+ module SAML2
4
+ describe Bindings::HTTPRedirect do
5
+ describe '.decode' do
6
+ def check_error(wrapped, cause)
7
+ error = nil
8
+ begin
9
+ yield
10
+ rescue => e
11
+ error = e
12
+ end
13
+ expect(error).not_to be_nil
14
+ expect(error).to be_a(wrapped)
15
+ expect(error.cause).to be_a(cause)
16
+ end
17
+
18
+ it "complains about invalid URIs" do
19
+ check_error(CorruptMessage, URI::InvalidURIError) { Bindings::HTTPRedirect.decode(" ") }
20
+ end
21
+
22
+ it "complains about missing message" do
23
+ expect { Bindings::HTTPRedirect.decode("http://somewhere/") }.to raise_error(MissingMessage)
24
+ expect { Bindings::HTTPRedirect.decode("http://somewhere/?RelayState=bob") }.to raise_error(MissingMessage)
25
+ end
26
+
27
+ it "complains about malformed Base64" do
28
+ check_error(CorruptMessage, ArgumentError) { Bindings::HTTPRedirect.decode("http://somewhere/?SAMLRequest=%^") }
29
+ end
30
+
31
+ it "doesn't allow deflate bombs" do
32
+ message = "\0" * 2 * 1024 * 1024
33
+ allow(message).to receive(:destination).and_return("http://somewhere/")
34
+ url = Bindings::HTTPRedirect.encode(message)
35
+
36
+ expect { Bindings::HTTPRedirect.decode(url) }.to raise_error(MessageTooLarge)
37
+ end
38
+
39
+ it "complains about malformed deflated data" do
40
+ check_error(CorruptMessage, Zlib::BufError) { Bindings::HTTPRedirect.decode("http://somewhere/?SAMLRequest=abcd") }
41
+ end
42
+
43
+ it "complains about zlibbed data" do
44
+ # SAML uses just Deflate, which has no header/footer; Zlib adds a simple header/footer
45
+ message = Base64.strict_encode64(Zlib::Deflate.deflate('abcd'))
46
+ check_error(CorruptMessage, Zlib::DataError) { Bindings::HTTPRedirect.decode("http://somewhere/?SAMLRequest=#{message}") }
47
+ end
48
+
49
+ it "validates encoding" do
50
+ message = "hi"
51
+ allow(message).to receive(:destination).and_return("http://somewhere/")
52
+ url = Bindings::HTTPRedirect.encode(message, relay_state: "abc")
53
+ url << "&SAMLEncoding=garbage"
54
+ expect { Bindings::HTTPRedirect.decode(url) }.to raise_error(UnsupportedEncoding)
55
+ end
56
+
57
+ it "returns relay state" do
58
+ message = "hi"
59
+ allow(message).to receive(:destination).and_return("http://somewhere/")
60
+ url = Bindings::HTTPRedirect.encode(message, relay_state: "abc")
61
+ allow(Message).to receive(:parse).with("hi").and_return("parsed")
62
+ message, relay_state = Bindings::HTTPRedirect.decode(url)
63
+ expect(message).to eq "parsed"
64
+ expect(relay_state).to eq "abc"
65
+ end
66
+
67
+ context "signature validation" do
68
+ # taken from logs of another system. It's a good one to test because the Signature
69
+ # has to be taken out of the middle, so you can't just use the whole query string
70
+ let(:url) { '/login/saml/logout?SAMLResponse=fZLNasMwEIRfxehuy7KVOhaOobSXQHtpSg%2b9FP2sE4EjGa9U2rev7JBDoOQmLTO7s5%2fUoTyPk3jxRx%2fDG%2bDkHUK2f96Rr%2fpBN4pXMtdcq5xXbJurbb3JN1JCZZpSqQ0n2QfMaL3bkaooSbZHjLB3GKQLqVSyJi95zup3xgVvRdkWVdN%2bkuwZMFgnw%2bo8hTChoNR%2b%2fwbQp8Im%2fxx1iDMU2p%2fp6I%2fW0SXockw5Sfa0xFxGxNkJL9GicPIMKIIWh8fXF5HSCH0RiehwAm0HCyYldNct3%2f2OaK3Yw8AkSKVqXrJqYIpVst4ao0xtymZbD21bbXja7ec8OhQrr%2ftzp9kHr%2f1I%2bm7lMV%2bs900SEeaFB%2bkXHgmHNAMWVyZg4lqgSfVtNSBNiDB09DKh7y7veAgyRLy9PXkD2YccI9xPgKtaHKJO7ZFktO%2fobVf632fp%2fwA%3d&Signature=eZejccsvxyLEZvkNdL3shazIPyBIfRwk0Ny5INfrwzhE40N%2bH4neEo7xoHi2ncJ0LQG6A71oxviVkqPWXUaePuAt3fbxTf%2bLkWPiXo0D6GVebSgPSZIMrsU%2fawfou68yX%2bI5dk8KtFiMOPCf5818oJvCdYjszN5o%2fU80UlJdjiDK%2bMF2rtzx6ZEXs04MoMDWKgouTZUFxNZP3KJeFQumLbK99ZfJVl8Wl4XDTs1DKq7eBJc1IgyH13LELxhHgCZMpARrCX65gCYhjnJWhmyTU4YFzdcwKeulYcP0eTbMEqRy9s1sEaOH%2fTx3I46Fl%2bv7j3GbRiRTwqF9%2bqjAKbJjpw%3d%3d&SigAlg=http%3a%2f%2fwww.w3.org%2f2000%2f09%2fxmldsig%23rsa-sha1' }
71
+ let(:certificate) { OpenSSL::X509::Certificate.new(Base64.decode64('MIIFnzCCBIegAwIBAgIQItX5wssh0ecd46K65PkSNDANBgkqhkiG9w0BAQsFADCBkDELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxNjA0BgNVBAMTLUNPTU9ETyBSU0EgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQTAeFw0xNjA5MDgwMDAwMDBaFw0xOTEwMjUyMzU5NTlaMIGeMSEwHwYDVQQLExhEb21haW4gQ29udHJvbCBWYWxpZGF0ZWQxSTBHBgNVBAsTQElzc3VlZCB0aHJvdWdoIEl2eSBUZWNoIENvbW11bml0eSBDb2xsZWdlIG9mIEluZGlhbmEgRS1QS0kgTWFuYWcxEzARBgNVBAsTCkNPTU9ETyBTU0wxGTAXBgNVBAMTEGFkZnMuaXZ5dGVjaC5lZHUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC58zHz7VsV9S2XZMRjgqiWxBZ6M9y6/3zkrbObJ9hZqO7giCoonNDuELUiNt8pBqF8aHef8qbDOecBBXkz8rPAJL1S6lzvbxHIBuvEy+xOpVdUNMoyOaAYHOI5T6ueL1Q4iGMKfnWuXSvVTyB+9wAF/aWVFSoz+alUOiQtqTYyfgIKzHIAmFX7/SjFA9UjKVtqatcvzWsSWZHL4imeTmPosXXjmJVZnl+jaeFsnmW59o66sdGR+NYkhsBcVRnuP3MdxVgr5xSJMN+/BgZwCncX+4LJq5664eeQcJM5Km9kbQ/jMFhYy765ejszcL0vWe/fS7tdXQCfoKjRZ5LzNEb3AgMBAAGjggHjMIIB3zAfBgNVHSMEGDAWgBSQr2o6lFoL2JDqElZz30O0Oija5zAdBgNVHQ4EFgQUdFr6SnHaXUqLAEdOL9qrTJS/3AYwDgYDVR0PAQH/BAQDAgWgMAwGA1UdEwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCME8GA1UdIARIMEYwOgYLKwYBBAGyMQECAgcwKzApBggrBgEFBQcCARYdaHR0cHM6Ly9zZWN1cmUuY29tb2RvLmNvbS9DUFMwCAYGZ4EMAQIBMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0NPTU9ET1JTQURvbWFpblZhbGlkYXRpb25TZWN1cmVTZXJ2ZXJDQS5jcmwwgYUGCCsGAQUFBwEBBHkwdzBPBggrBgEFBQcwAoZDaHR0cDovL2NydC5jb21vZG9jYS5jb20vQ09NT0RPUlNBRG9tYWluVmFsaWRhdGlvblNlY3VyZVNlcnZlckNBLmNydDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMDEGA1UdEQQqMCiCEGFkZnMuaXZ5dGVjaC5lZHWCFHd3dy5hZGZzLml2eXRlY2guZWR1MA0GCSqGSIb3DQEBCwUAA4IBAQA0dXP0leDcdrr/iKk4nDSCofllPAWE8LE3mD9Yb9K+/oVymxpqNIVJesDPLtf1HqWk6S6eafcYvfzl9aTMcvwEkL27g2l9UQuICkQgqSEY5qTsK//u/2S98JqXep2oRyvxo3UHX+3Ouc3i49hQ0v05Faoeap/ZT3JEsMV2Go9UKRJbYBG9Nqq/CDBuTgyopKJ7fvCtsGxwsvlUAz/NMuNoUphPQ2S+O/SjabjR4XsAGU78Hji2tqJyvPyKPanxc0ioDdnL5lvrk4uZ/6Dy159C5FOFeLU2ZfiNLXRR85KFfhtX954qvX6jmM7CPmcidhzEnZV8fQv9G6XYPfrNL7bh')).public_key }
72
+ let(:incorrect_certificate) { OpenSSL::X509::Certificate.new(fixture('certificate.pem')).public_key }
73
+
74
+ it "validates the signature" do
75
+ # no exception raised
76
+ Bindings::HTTPRedirect.decode(url, public_key: certificate)
77
+ end
78
+
79
+ it "raises on invalid signature" do
80
+ expect { Bindings::HTTPRedirect.decode(url, public_key: incorrect_certificate) }.to raise_error(InvalidSignature)
81
+ end
82
+
83
+ it "raises on unsupported signature algorithm" do
84
+ x = url
85
+ # SigAlg is now sha10
86
+ x << "0"
87
+ expect { Bindings::HTTPRedirect.decode(url, public_key: certificate) }.to raise_error(UnsupportedSignatureAlgorithm)
88
+ end
89
+
90
+ it "allows the caller to detect an unsigned message" do
91
+ message = "hi"
92
+ allow(message).to receive(:destination).and_return("http://somewhere/")
93
+ url = Bindings::HTTPRedirect.encode(message)
94
+ allow(Message).to receive(:parse).with("hi").and_return("parsed")
95
+
96
+ expect do
97
+ Bindings::HTTPRedirect.decode(url) do |_message, sig_alg|
98
+ expect(sig_alg).to be_nil
99
+ raise "no signature"
100
+ end
101
+ end.to raise_error("no signature")
102
+ end
103
+
104
+ it "requires a signature if a key is passed" do
105
+ message = "hi"
106
+ allow(message).to receive(:destination).and_return("http://somewhere/")
107
+ url = Bindings::HTTPRedirect.encode(message)
108
+ allow(Message).to receive(:parse).with("hi").and_return("parsed")
109
+
110
+ expect { Bindings::HTTPRedirect.decode(url, public_key: certificate) } .to raise_error(UnsignedMessage)
111
+ end
112
+
113
+ it "notifies the caller which key was used" do
114
+ called = 0
115
+ key_used = ->(key) do
116
+ expect(key).to eq certificate
117
+ called += 1
118
+ end
119
+ Bindings::HTTPRedirect.decode(url,
120
+ public_key: [incorrect_certificate,
121
+ certificate],
122
+ public_key_used: key_used)
123
+ expect(called).to eq 1
124
+ end
125
+ end
126
+ end
127
+
128
+ describe '.encode' do
129
+ it 'works' do
130
+ message = "hi"
131
+ allow(message).to receive(:destination).and_return("http://somewhere/")
132
+ url = Bindings::HTTPRedirect.encode(message, relay_state: "abc")
133
+ expect(url).to match(%r{^http://somewhere/\?SAMLResponse=(?:.*)&RelayState=abc})
134
+ end
135
+
136
+ it 'signs a message' do
137
+ message = "hi"
138
+ allow(message).to receive(:destination).and_return("http://somewhere/")
139
+ key = OpenSSL::PKey::RSA.new(fixture('privatekey.key'))
140
+ url = Bindings::HTTPRedirect.encode(message, relay_state: "abc", private_key: key)
141
+
142
+ # verify the signature
143
+ allow(Message).to receive(:parse).with("hi").and_return("parsed")
144
+ Bindings::HTTPRedirect.decode(url) do |_message, sig_alg|
145
+ expect(sig_alg).not_to be_nil
146
+ OpenSSL::X509::Certificate.new(fixture('certificate.pem')).public_key
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end