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.
- checksums.yaml +4 -4
- data/Rakefile +2 -7
- data/lib/saml2.rb +2 -0
- data/lib/saml2/attribute.rb +2 -0
- data/lib/saml2/attribute_consuming_service.rb +1 -0
- data/lib/saml2/authn_request.rb +19 -47
- data/lib/saml2/base.rb +5 -2
- data/lib/saml2/bindings.rb +7 -0
- data/lib/saml2/bindings/http_redirect.rb +141 -0
- data/lib/saml2/contact.rb +14 -16
- data/lib/saml2/endpoint.rb +5 -6
- data/lib/saml2/entity.rb +23 -18
- data/lib/saml2/identity_provider.rb +4 -4
- data/lib/saml2/indexed_object.rb +7 -3
- data/lib/saml2/key.rb +19 -1
- data/lib/saml2/logout_request.rb +43 -0
- data/lib/saml2/logout_response.rb +23 -0
- data/lib/saml2/message.rb +109 -0
- data/lib/saml2/name_id.rb +16 -8
- data/lib/saml2/organization_and_contacts.rb +2 -2
- data/lib/saml2/request.rb +8 -0
- data/lib/saml2/response.rb +7 -23
- data/lib/saml2/role.rb +2 -3
- data/lib/saml2/service_provider.rb +24 -2
- data/lib/saml2/sso.rb +2 -2
- data/lib/saml2/status.rb +28 -0
- data/lib/saml2/status_response.rb +33 -0
- data/lib/saml2/version.rb +1 -1
- data/spec/fixtures/identity_provider.xml +1 -0
- data/spec/fixtures/response_signed.xml +1 -1
- data/spec/fixtures/response_with_attribute_signed.xml +1 -1
- data/spec/lib/attribute_consuming_service_spec.rb +37 -37
- data/spec/lib/attribute_spec.rb +17 -17
- data/spec/lib/authn_request_spec.rb +15 -71
- data/spec/lib/bindings/http_redirect_spec.rb +151 -0
- data/spec/lib/conditions_spec.rb +10 -10
- data/spec/lib/entity_spec.rb +12 -12
- data/spec/lib/identity_provider_spec.rb +4 -4
- data/spec/lib/indexed_object_spec.rb +38 -7
- data/spec/lib/logout_request_spec.rb +31 -0
- data/spec/lib/logout_response_spec.rb +31 -0
- data/spec/lib/message_spec.rb +21 -0
- data/spec/lib/response_spec.rb +8 -9
- data/spec/lib/service_provider_spec.rb +29 -8
- data/spec/spec_helper.rb +0 -1
- metadata +41 -11
data/lib/saml2/sso.rb
CHANGED
@@ -15,11 +15,11 @@ module SAML2
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def single_logout_services
|
18
|
-
@single_logout_services ||= load_object_array(
|
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(
|
22
|
+
@name_id_formats ||= load_string_array(xml, 'md:NameIDFormat')
|
23
23
|
end
|
24
24
|
|
25
25
|
protected
|
data/lib/saml2/status.rb
ADDED
@@ -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
|
data/lib/saml2/version.rb
CHANGED
@@ -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
|
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
|
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
|
-
|
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.
|
21
|
-
stmt.attributes.first.name.
|
22
|
-
stmt.attributes.first.value.
|
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.
|
28
|
-
stmt.attributes.first.name.
|
29
|
-
stmt.attributes.first.value.
|
30
|
-
stmt.attributes.last.name.
|
31
|
-
stmt.attributes.last.value.
|
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.
|
37
|
-
stmt.attributes.first.name.
|
38
|
-
stmt.attributes.first.value.
|
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.
|
44
|
-
stmt.attributes.first.name.
|
45
|
-
stmt.attributes.first.value.
|
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.
|
54
|
-
stmt.attributes.first.name.
|
55
|
-
stmt.attributes.first.value.
|
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.
|
62
|
-
stmt.attributes.first.name.
|
63
|
-
stmt.attributes.first.value.
|
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.
|
69
|
-
stmt.attributes.first.name.
|
70
|
-
stmt.attributes.first.value.
|
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
|
-
|
78
|
+
expect { acs.create_statement('attr' => 'something') }.to raise_error InvalidAttributeValue
|
79
79
|
stmt = acs.create_statement('attr' => 'value')
|
80
|
-
stmt.attributes.length.
|
81
|
-
stmt.attributes.first.name.
|
82
|
-
stmt.attributes.first.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
|
-
|
90
|
+
expect { acs.create_statement('attr' => 'something') }.to raise_error InvalidAttributeValue
|
91
91
|
stmt = acs.create_statement('attr' => 'value1')
|
92
|
-
stmt.attributes.length.
|
93
|
-
stmt.attributes.first.name.
|
94
|
-
stmt.attributes.first.value.
|
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.
|
104
|
-
stmt.attributes.first.name.
|
105
|
-
stmt.attributes.first.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
|
-
|
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
|
-
|
122
|
+
expect(stmt).to be_nil
|
123
123
|
end
|
124
124
|
|
125
125
|
end
|
data/spec/lib/attribute_spec.rb
CHANGED
@@ -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.
|
25
|
-
attr.value.
|
26
|
-
attr.name.
|
27
|
-
attr.friendly_name.
|
28
|
-
attr.name_format.
|
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.
|
34
|
-
attr.value.
|
35
|
-
attr.name.
|
36
|
-
attr.friendly_name.
|
37
|
-
attr.name_format.
|
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).
|
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.
|
52
|
+
expect(stmt.attributes.first.value).to eq true
|
53
53
|
|
54
54
|
# serializes canonically
|
55
|
-
serialize(stmt).
|
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.
|
68
|
+
expect(stmt.attributes.first.value).to eq Time.at(1435603023)
|
69
69
|
|
70
70
|
# serializes canonically
|
71
|
-
serialize(stmt).
|
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.
|
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.
|
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
|
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
|
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
|
74
|
-
request.resolve(sp).
|
75
|
-
request.assertion_consumer_service.location.
|
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.
|
80
|
-
|
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.
|
86
|
-
|
87
|
-
|
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.
|
93
|
-
|
94
|
-
|
95
|
-
|
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.
|
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
|