saml2 3.1.2 → 3.1.4
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 +6 -4
- data/exe/bulk_verify_responses +94 -0
- data/lib/saml2/assertion.rb +7 -7
- data/lib/saml2/attribute/x500.rb +31 -28
- data/lib/saml2/attribute.rb +53 -49
- data/lib/saml2/attribute_consuming_service.rb +29 -31
- data/lib/saml2/authn_request.rb +54 -47
- data/lib/saml2/authn_statement.rb +31 -20
- data/lib/saml2/base.rb +72 -63
- data/lib/saml2/bindings/http_post.rb +7 -7
- data/lib/saml2/bindings/http_redirect.rb +37 -33
- data/lib/saml2/bindings.rb +1 -1
- data/lib/saml2/conditions.rb +19 -16
- data/lib/saml2/contact.rb +19 -18
- data/lib/saml2/endpoint.rb +14 -11
- data/lib/saml2/entity.rb +27 -27
- data/lib/saml2/identity_provider.rb +13 -10
- data/lib/saml2/indexed_object.rb +15 -12
- data/lib/saml2/key.rb +43 -34
- data/lib/saml2/localized_name.rb +11 -10
- data/lib/saml2/logout_request.rb +8 -8
- data/lib/saml2/logout_response.rb +4 -4
- data/lib/saml2/message.rb +24 -20
- data/lib/saml2/name_id.rb +45 -41
- data/lib/saml2/namespaces.rb +8 -8
- data/lib/saml2/organization.rb +11 -10
- data/lib/saml2/organization_and_contacts.rb +5 -5
- data/lib/saml2/request.rb +3 -3
- data/lib/saml2/requested_authn_context.rb +4 -4
- data/lib/saml2/response.rb +45 -33
- data/lib/saml2/role.rb +11 -11
- data/lib/saml2/schemas.rb +13 -10
- data/lib/saml2/service_provider.rb +11 -12
- data/lib/saml2/signable.rb +23 -18
- data/lib/saml2/sso.rb +5 -5
- data/lib/saml2/status.rb +9 -7
- data/lib/saml2/status_response.rb +5 -5
- data/lib/saml2/subject.rb +28 -28
- data/lib/saml2/version.rb +1 -1
- data/lib/saml2.rb +7 -7
- metadata +78 -122
- data/spec/fixtures/FederationMetadata.xml +0 -670
- data/spec/fixtures/authnrequest.xml +0 -12
- data/spec/fixtures/certificate.pem +0 -24
- data/spec/fixtures/entities.xml +0 -13
- data/spec/fixtures/external-uri-reference-response.xml +0 -48
- data/spec/fixtures/identity_provider.xml +0 -46
- data/spec/fixtures/noconditions_response.xml +0 -1
- data/spec/fixtures/othercertificate.pem +0 -25
- data/spec/fixtures/privatekey.key +0 -27
- data/spec/fixtures/response_assertion_signed_reffed_from_response.xml +0 -6
- data/spec/fixtures/response_signed.xml +0 -46
- data/spec/fixtures/response_tampered_certificate.xml +0 -25
- data/spec/fixtures/response_tampered_signature.xml +0 -46
- data/spec/fixtures/response_with_attribute_signed.xml +0 -46
- data/spec/fixtures/response_with_encrypted_assertion.xml +0 -58
- data/spec/fixtures/response_with_rsa_key_value.xml +0 -1
- data/spec/fixtures/response_with_signed_assertion_and_encrypted_subject.xml +0 -116
- data/spec/fixtures/response_without_keyinfo.xml +0 -1
- data/spec/fixtures/service_provider.xml +0 -79
- data/spec/fixtures/test3-response.xml +0 -9
- data/spec/fixtures/test6-response.xml +0 -10
- data/spec/fixtures/test7-response.xml +0 -10
- data/spec/fixtures/xml_missigned_assertion.xml +0 -84
- data/spec/fixtures/xml_signature_wrapping_attack_duplicate_ids.xml +0 -11
- data/spec/fixtures/xml_signature_wrapping_attack_response_attributes.xml +0 -45
- data/spec/fixtures/xml_signature_wrapping_attack_response_nameid.xml +0 -44
- data/spec/fixtures/xslt-transform-response.xml +0 -57
- data/spec/lib/attribute_consuming_service_spec.rb +0 -129
- data/spec/lib/attribute_spec.rb +0 -149
- data/spec/lib/authn_request_spec.rb +0 -52
- data/spec/lib/bindings/http_redirect_spec.rb +0 -183
- data/spec/lib/conditions_spec.rb +0 -74
- data/spec/lib/entity_spec.rb +0 -58
- data/spec/lib/identity_provider_spec.rb +0 -43
- data/spec/lib/indexed_object_spec.rb +0 -71
- data/spec/lib/key_spec.rb +0 -23
- data/spec/lib/logout_request_spec.rb +0 -33
- data/spec/lib/logout_response_spec.rb +0 -33
- data/spec/lib/message_spec.rb +0 -23
- data/spec/lib/response_spec.rb +0 -293
- data/spec/lib/service_provider_spec.rb +0 -76
- data/spec/lib/signable_spec.rb +0 -15
- data/spec/spec_helper.rb +0 -8
data/spec/lib/attribute_spec.rb
DELETED
@@ -1,149 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative '../spec_helper'
|
4
|
-
|
5
|
-
module SAML2
|
6
|
-
describe Attribute do
|
7
|
-
def serialize(attribute)
|
8
|
-
doc = Nokogiri::XML::Builder.new do |builder|
|
9
|
-
builder['saml'].Root('xmlns:saml' => Namespaces::SAML) do |root|
|
10
|
-
attribute.build(root)
|
11
|
-
root.parent.child['xmlns:saml'] = Namespaces::SAML
|
12
|
-
end
|
13
|
-
end.doc
|
14
|
-
doc.root.child.to_s
|
15
|
-
end
|
16
|
-
|
17
|
-
let(:eduPersonPrincipalNameXML) { <<XML.strip
|
18
|
-
<saml:Attribute xmlns:x500="urn:oasis:names:tc:SAML:2.0:profiles:attribute:X500" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.6" FriendlyName="eduPersonPrincipalName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" x500:Encoding="LDAP" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
|
19
|
-
<saml:AttributeValue xsi:type="xs:string">user@domain</saml:AttributeValue>
|
20
|
-
</saml:Attribute>
|
21
|
-
XML
|
22
|
-
}
|
23
|
-
|
24
|
-
it "should auto-parse X500 attributes" do
|
25
|
-
attr = Attribute.from_xml(Nokogiri::XML(eduPersonPrincipalNameXML).root)
|
26
|
-
expect(attr).to be_instance_of Attribute::X500
|
27
|
-
expect(attr.value).to eq "user@domain"
|
28
|
-
expect(attr.name).to eq Attribute::X500::EduPerson::PRINCIPAL_NAME
|
29
|
-
expect(attr.friendly_name).to eq 'eduPersonPrincipalName'
|
30
|
-
expect(attr.name_format).to eq Attribute::NameFormats::URI
|
31
|
-
end
|
32
|
-
|
33
|
-
it "recognizes and X500 attribute without a NameFormat" do
|
34
|
-
xml = <<-XML
|
35
|
-
<saml:Attribute xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.6"><saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">user@domain</saml:AttributeValue></saml:Attribute>
|
36
|
-
XML
|
37
|
-
attr = Attribute.from_xml(Nokogiri::XML(xml).root)
|
38
|
-
expect(attr).to be_instance_of Attribute::X500
|
39
|
-
expect(attr.value).to eq "user@domain"
|
40
|
-
expect(attr.name).to eq Attribute::X500::EduPerson::PRINCIPAL_NAME
|
41
|
-
expect(attr.friendly_name).to eq 'eduPersonPrincipalName'
|
42
|
-
expect(attr.name_format).to eq nil
|
43
|
-
end
|
44
|
-
|
45
|
-
it "should serialize an X500 attribute correctly" do
|
46
|
-
attr = Attribute.create('eduPersonPrincipalName', 'user@domain')
|
47
|
-
expect(attr).to be_instance_of Attribute::X500
|
48
|
-
expect(attr.value).to eq "user@domain"
|
49
|
-
expect(attr.name).to eq Attribute::X500::EduPerson::PRINCIPAL_NAME
|
50
|
-
expect(attr.friendly_name).to eq 'eduPersonPrincipalName'
|
51
|
-
expect(attr.name_format).to eq Attribute::NameFormats::URI
|
52
|
-
|
53
|
-
expect(serialize(attr)).to eq eduPersonPrincipalNameXML
|
54
|
-
end
|
55
|
-
|
56
|
-
it "should parse and serialize boolean values" do
|
57
|
-
xml = <<XML.strip
|
58
|
-
<saml:AttributeStatement xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
|
59
|
-
<saml:Attribute Name="attr">
|
60
|
-
<saml:AttributeValue xsi:type="xs:boolean">1</saml:AttributeValue>
|
61
|
-
</saml:Attribute>
|
62
|
-
</saml:AttributeStatement>
|
63
|
-
XML
|
64
|
-
|
65
|
-
stmt = AttributeStatement.from_xml(Nokogiri::XML(xml).root)
|
66
|
-
expect(stmt.attributes.first.value).to eq true
|
67
|
-
|
68
|
-
# serializes canonically
|
69
|
-
expect(serialize(stmt)).to eq(xml.sub('>1<', '>true<'))
|
70
|
-
end
|
71
|
-
|
72
|
-
it "should parse and serialize dateTime values" do
|
73
|
-
xml = <<XML.strip
|
74
|
-
<saml:AttributeStatement xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
|
75
|
-
<saml:Attribute Name="attr">
|
76
|
-
<saml:AttributeValue xsi:type="xs:dateTime">2015-06-29T18:37:03Z</saml:AttributeValue>
|
77
|
-
</saml:Attribute>
|
78
|
-
</saml:AttributeStatement>
|
79
|
-
XML
|
80
|
-
|
81
|
-
stmt = AttributeStatement.from_xml(Nokogiri::XML(xml).root)
|
82
|
-
expect(stmt.attributes.first.value).to eq Time.at(1435603023)
|
83
|
-
|
84
|
-
# serializes canonically
|
85
|
-
expect(serialize(stmt)).to eq xml
|
86
|
-
end
|
87
|
-
|
88
|
-
it "should parse values with different namespace prefixes" do
|
89
|
-
xml = <<XML.strip
|
90
|
-
<saml:Attribute Name="attr" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xssi="http://www.w3.org/2001/XMLSchema-instance">
|
91
|
-
<saml:AttributeValue xssi:type="xsd:boolean">0</saml:AttributeValue>
|
92
|
-
</saml:Attribute>
|
93
|
-
XML
|
94
|
-
|
95
|
-
attr = Attribute.from_xml(Nokogiri::XML(xml).root)
|
96
|
-
expect(attr.value).to eq false
|
97
|
-
end
|
98
|
-
|
99
|
-
it "should parse untagged values" do
|
100
|
-
xml = <<XML.strip
|
101
|
-
<saml:Attribute Name="attr" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
|
102
|
-
<saml:AttributeValue>something</saml:AttributeValue>
|
103
|
-
</saml:Attribute>
|
104
|
-
XML
|
105
|
-
|
106
|
-
attr = Attribute.from_xml(Nokogiri::XML(xml).root)
|
107
|
-
expect(attr.value).to eq "something"
|
108
|
-
end
|
109
|
-
|
110
|
-
end
|
111
|
-
|
112
|
-
describe AttributeStatement do
|
113
|
-
describe "#to_h" do
|
114
|
-
it "works" do
|
115
|
-
attr_statement = Response.parse(fixture("response_with_attribute_signed.xml")).assertions.first.attribute_statements.first
|
116
|
-
expect(attr_statement.to_h(:friendly_name)).to eq('givenName' => 'cody')
|
117
|
-
expect(attr_statement.to_h(:name)).to eq("urn:oid:2.5.4.42" => 'cody')
|
118
|
-
expect(attr_statement.to_h(:both)).to eq('givenName' => 'cody', "urn:oid:2.5.4.42" => 'cody')
|
119
|
-
end
|
120
|
-
|
121
|
-
it "infers friendly names if possible" do
|
122
|
-
attr_statement = Response.parse(fixture("test3-response.xml")).assertions.first.attribute_statements.first
|
123
|
-
expect(attr_statement.to_h).to eq({
|
124
|
-
'urn:oid:1.3.6.1.4.1.5923.1.1.1.1' => 'member',
|
125
|
-
'urn:oid:1.3.6.1.4.1.5923.1.1.1.6' => 'student@example.edu',
|
126
|
-
'eduPersonAffiliation' => 'member',
|
127
|
-
'eduPersonPrincipalName' => 'student@example.edu'})
|
128
|
-
end
|
129
|
-
|
130
|
-
it "properly combines repeated attributes" do
|
131
|
-
attr_statement = AttributeStatement.from_xml(Nokogiri::XML(<<-XML).root)
|
132
|
-
<saml2:AttributeStatement xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
|
133
|
-
<saml2:Attribute FriendlyName="eduPersonScopedAffiliation" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.9" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
|
134
|
-
<saml2:AttributeValue>02</saml2:AttributeValue>
|
135
|
-
</saml2:Attribute>
|
136
|
-
<saml2:Attribute FriendlyName="eduPersonScopedAffiliation" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.9" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
|
137
|
-
<saml2:AttributeValue>employee@school.edu</saml2:AttributeValue>
|
138
|
-
<saml2:AttributeValue>students@school.edu</saml2:AttributeValue>
|
139
|
-
</saml2:Attribute>
|
140
|
-
</saml2:AttributeStatement>
|
141
|
-
XML
|
142
|
-
|
143
|
-
expect(attr_statement.to_h(:friendly_name)).to eq({
|
144
|
-
'eduPersonScopedAffiliation' => ['02', 'employee@school.edu', 'students@school.edu']
|
145
|
-
})
|
146
|
-
end
|
147
|
-
end
|
148
|
-
end
|
149
|
-
end
|
@@ -1,52 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative '../spec_helper'
|
4
|
-
|
5
|
-
module SAML2
|
6
|
-
describe AuthnRequest do
|
7
|
-
let(:sp) { Entity.parse(fixture('service_provider.xml')).roles.first }
|
8
|
-
let(:request) { AuthnRequest.parse(fixture('authnrequest.xml')) }
|
9
|
-
|
10
|
-
it "should be valid" do
|
11
|
-
expect(request.valid_schema?).to eq true
|
12
|
-
expect(request.resolve(sp)).to eq true
|
13
|
-
expect(request.assertion_consumer_service.location).to eq "https://siteadmin.test.instructure.com/saml_consume"
|
14
|
-
end
|
15
|
-
|
16
|
-
it "should not be valid if the ACS url is not in the SP" do
|
17
|
-
allow(request).to receive(:assertion_consumer_service_url).and_return("garbage")
|
18
|
-
expect(request.resolve(sp)).to eq false
|
19
|
-
end
|
20
|
-
|
21
|
-
it "should use the default ACS if not specified" do
|
22
|
-
allow(request).to receive(:assertion_consumer_service_url).and_return(nil)
|
23
|
-
expect(request.resolve(sp)).to eq true
|
24
|
-
expect(request.assertion_consumer_service.location).to eq "https://siteadmin.instructure.com/saml_consume"
|
25
|
-
end
|
26
|
-
|
27
|
-
it "should find the ACS by index" do
|
28
|
-
allow(request).to receive(:assertion_consumer_service_url).and_return(nil)
|
29
|
-
allow(request).to receive(:assertion_consumer_service_index).and_return(2)
|
30
|
-
expect(request.resolve(sp)).to eq true
|
31
|
-
expect(request.assertion_consumer_service.location).to eq "https://siteadmin.beta.instructure.com/saml_consume"
|
32
|
-
end
|
33
|
-
|
34
|
-
it "should find the NameID policy" do
|
35
|
-
expect(request.name_id_policy).to eq NameID::Policy.new(true, NameID::Format::PERSISTENT, "moodle.bridge.feide.no")
|
36
|
-
end
|
37
|
-
|
38
|
-
it 'serializes valid XML' do
|
39
|
-
authn_request = AuthnRequest.initiate(NameID.new("entity"),
|
40
|
-
assertion_consumer_service: Endpoint.new('https://somewhere/'))
|
41
|
-
authn_request.requested_authn_context = RequestedAuthnContext.new
|
42
|
-
authn_request.requested_authn_context.class_ref = "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
|
43
|
-
authn_request.requested_authn_context.comparison = :exact
|
44
|
-
authn_request.passive = true
|
45
|
-
xml = authn_request.to_s
|
46
|
-
authn_request = AuthnRequest.parse(xml)
|
47
|
-
expect(authn_request).to be_valid_schema
|
48
|
-
expect(authn_request.force_authn?).to eq nil
|
49
|
-
expect(authn_request.passive?).to eq true
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
@@ -1,183 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative '../../spec_helper'
|
4
|
-
|
5
|
-
require 'openssl'
|
6
|
-
|
7
|
-
module SAML2
|
8
|
-
describe Bindings::HTTPRedirect do
|
9
|
-
describe '.decode' do
|
10
|
-
def check_error(wrapped, cause)
|
11
|
-
error = nil
|
12
|
-
begin
|
13
|
-
yield
|
14
|
-
rescue => e
|
15
|
-
error = e
|
16
|
-
end
|
17
|
-
expect(error).not_to be_nil
|
18
|
-
expect(error).to be_a(wrapped)
|
19
|
-
expect(error.cause).to be_a(cause)
|
20
|
-
end
|
21
|
-
|
22
|
-
it "complains about invalid URIs" do
|
23
|
-
check_error(CorruptMessage, URI::InvalidURIError) { Bindings::HTTPRedirect.decode(" ") }
|
24
|
-
end
|
25
|
-
|
26
|
-
it "complains about missing message" do
|
27
|
-
expect { Bindings::HTTPRedirect.decode("http://somewhere/") }.to raise_error(MissingMessage)
|
28
|
-
expect { Bindings::HTTPRedirect.decode("http://somewhere/?RelayState=bob") }.to raise_error(MissingMessage)
|
29
|
-
end
|
30
|
-
|
31
|
-
it "complains about malformed Base64" do
|
32
|
-
check_error(CorruptMessage, ArgumentError) { Bindings::HTTPRedirect.decode("http://somewhere/?SAMLRequest=%^") }
|
33
|
-
end
|
34
|
-
|
35
|
-
it "doesn't allow deflate bombs" do
|
36
|
-
message = double()
|
37
|
-
allow(message).to receive(:destination).and_return("http://somewhere/")
|
38
|
-
allow(message).to receive(:to_s).and_return("\0" * 2 * 1024 * 1024)
|
39
|
-
url = Bindings::HTTPRedirect.encode(message)
|
40
|
-
|
41
|
-
expect { Bindings::HTTPRedirect.decode(url) }.to raise_error(MessageTooLarge)
|
42
|
-
end
|
43
|
-
|
44
|
-
it "complains about malformed deflated data" do
|
45
|
-
check_error(CorruptMessage, Zlib::BufError) { Bindings::HTTPRedirect.decode("http://somewhere/?SAMLRequest=abcd") }
|
46
|
-
end
|
47
|
-
|
48
|
-
it "complains about zlibbed data" do
|
49
|
-
# SAML uses just Deflate, which has no header/footer; Zlib adds a simple header/footer
|
50
|
-
message = Base64.strict_encode64(Zlib::Deflate.deflate('abcd'))
|
51
|
-
check_error(CorruptMessage, Zlib::DataError) { Bindings::HTTPRedirect.decode("http://somewhere/?SAMLRequest=#{message}") }
|
52
|
-
end
|
53
|
-
|
54
|
-
it "validates encoding" do
|
55
|
-
message = double()
|
56
|
-
allow(message).to receive(:destination).and_return("http://somewhere/")
|
57
|
-
allow(message).to receive(:to_s).and_return("hi")
|
58
|
-
url = Bindings::HTTPRedirect.encode(message, relay_state: "abc")
|
59
|
-
url << "&SAMLEncoding=garbage"
|
60
|
-
expect { Bindings::HTTPRedirect.decode(url) }.to raise_error(UnsupportedEncoding)
|
61
|
-
end
|
62
|
-
|
63
|
-
it "returns relay state" do
|
64
|
-
message = double()
|
65
|
-
allow(message).to receive(:destination).and_return("http://somewhere/")
|
66
|
-
allow(message).to receive(:to_s).and_return("hi")
|
67
|
-
url = Bindings::HTTPRedirect.encode(message, relay_state: "abc")
|
68
|
-
allow(Message).to receive(:parse).with("hi").and_return("parsed")
|
69
|
-
message, relay_state = Bindings::HTTPRedirect.decode(url)
|
70
|
-
expect(message).to eq "parsed"
|
71
|
-
expect(relay_state).to eq "abc"
|
72
|
-
end
|
73
|
-
|
74
|
-
context "signature validation" do
|
75
|
-
# taken from logs of another system. It's a good one to test because the Signature
|
76
|
-
# has to be taken out of the middle, so you can't just use the whole query string
|
77
|
-
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' }
|
78
|
-
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 }
|
79
|
-
let(:incorrect_certificate) { OpenSSL::X509::Certificate.new(fixture('certificate.pem')).public_key }
|
80
|
-
|
81
|
-
it "validates the signature" do
|
82
|
-
# no exception raised
|
83
|
-
Bindings::HTTPRedirect.decode(url, public_key: certificate)
|
84
|
-
end
|
85
|
-
|
86
|
-
it "raises on invalid signature" do
|
87
|
-
expect { Bindings::HTTPRedirect.decode(url, public_key: incorrect_certificate) }.to raise_error(InvalidSignature)
|
88
|
-
end
|
89
|
-
|
90
|
-
it "raises on unsupported signature algorithm" do
|
91
|
-
x = url.dup
|
92
|
-
# SigAlg is now sha10
|
93
|
-
x << "0"
|
94
|
-
expect { Bindings::HTTPRedirect.decode(x, public_key: certificate) }.to raise_error(UnsupportedSignatureAlgorithm)
|
95
|
-
end
|
96
|
-
|
97
|
-
it "allows the caller to detect an unsigned message" do
|
98
|
-
message = double()
|
99
|
-
allow(message).to receive(:destination).and_return("http://somewhere/")
|
100
|
-
allow(message).to receive(:to_s).and_return("hi")
|
101
|
-
url = Bindings::HTTPRedirect.encode(message)
|
102
|
-
allow(Message).to receive(:parse).with("hi").and_return("parsed")
|
103
|
-
|
104
|
-
expect do
|
105
|
-
Bindings::HTTPRedirect.decode(url) do |_message, sig_alg|
|
106
|
-
expect(sig_alg).to be_nil
|
107
|
-
raise "no signature"
|
108
|
-
end
|
109
|
-
end.to raise_error("no signature")
|
110
|
-
end
|
111
|
-
|
112
|
-
it "requires a signature if a key is passed" do
|
113
|
-
message = double()
|
114
|
-
allow(message).to receive(:destination).and_return("http://somewhere/")
|
115
|
-
allow(message).to receive(:to_s).and_return("hi")
|
116
|
-
url = Bindings::HTTPRedirect.encode(message)
|
117
|
-
allow(Message).to receive(:parse).with("hi").and_return("parsed")
|
118
|
-
|
119
|
-
expect { Bindings::HTTPRedirect.decode(url, public_key: certificate) } .to raise_error(UnsignedMessage)
|
120
|
-
end
|
121
|
-
|
122
|
-
it "notifies the caller which key was used" do
|
123
|
-
called = 0
|
124
|
-
key_used = ->(key) do
|
125
|
-
expect(key).to eq certificate
|
126
|
-
called += 1
|
127
|
-
end
|
128
|
-
Bindings::HTTPRedirect.decode(url,
|
129
|
-
public_key: [incorrect_certificate,
|
130
|
-
certificate],
|
131
|
-
public_key_used: key_used)
|
132
|
-
expect(called).to eq 1
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
describe '.encode' do
|
138
|
-
it 'works' do
|
139
|
-
message = double()
|
140
|
-
allow(message).to receive(:destination).and_return("http://somewhere/")
|
141
|
-
allow(message).to receive(:to_s).and_return("hi")
|
142
|
-
url = Bindings::HTTPRedirect.encode(message, relay_state: "abc")
|
143
|
-
expect(url).to match(%r{^http://somewhere/\?SAMLResponse=(?:.*)&RelayState=abc})
|
144
|
-
end
|
145
|
-
|
146
|
-
it 'signs a message' do
|
147
|
-
message = double()
|
148
|
-
allow(message).to receive(:destination).and_return("http://somewhere/")
|
149
|
-
allow(message).to receive(:to_s).and_return("hi")
|
150
|
-
key = OpenSSL::PKey::RSA.new(fixture('privatekey.key'))
|
151
|
-
url = Bindings::HTTPRedirect.encode(message,
|
152
|
-
relay_state: "abc",
|
153
|
-
private_key: key)
|
154
|
-
|
155
|
-
# verify the signature
|
156
|
-
allow(Message).to receive(:parse).with("hi").and_return("parsed")
|
157
|
-
Bindings::HTTPRedirect.decode(url) do |_message, sig_alg|
|
158
|
-
expect(sig_alg).to eq Bindings::HTTPRedirect::SigAlgs::RSA_SHA1
|
159
|
-
OpenSSL::X509::Certificate.new(fixture('certificate.pem')).public_key
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
|
-
it 'signs a message with RSA-SHA256' do
|
164
|
-
message = double()
|
165
|
-
allow(message).to receive(:destination).and_return("http://somewhere/")
|
166
|
-
allow(message).to receive(:to_s).and_return("hi")
|
167
|
-
key = OpenSSL::PKey::RSA.new(fixture('privatekey.key'))
|
168
|
-
url = Bindings::HTTPRedirect.encode(message,
|
169
|
-
relay_state: "abc",
|
170
|
-
private_key: key,
|
171
|
-
sig_alg: Bindings::HTTPRedirect::SigAlgs::RSA_SHA256)
|
172
|
-
|
173
|
-
# verify the signature
|
174
|
-
allow(Message).to receive(:parse).with("hi").and_return("parsed")
|
175
|
-
Bindings::HTTPRedirect.decode(url) do |_message, sig_alg|
|
176
|
-
expect(sig_alg).to eq Bindings::HTTPRedirect::SigAlgs::RSA_SHA256
|
177
|
-
OpenSSL::X509::Certificate.new(fixture('certificate.pem')).public_key
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
end
|
182
|
-
end
|
183
|
-
end
|
data/spec/lib/conditions_spec.rb
DELETED
@@ -1,74 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative '../spec_helper'
|
4
|
-
|
5
|
-
module SAML2
|
6
|
-
describe Conditions do
|
7
|
-
it "empty should be valid" do
|
8
|
-
expect(Conditions.new.valid?).to eq true
|
9
|
-
end
|
10
|
-
|
11
|
-
it "should be invalid with unknown condition" do
|
12
|
-
conditions = Conditions.new
|
13
|
-
conditions << Conditions::Condition.new
|
14
|
-
expect(conditions.valid?).to eq false
|
15
|
-
end
|
16
|
-
|
17
|
-
it "should be valid with timestamps" do
|
18
|
-
conditions = Conditions.new
|
19
|
-
now = Time.now.utc
|
20
|
-
conditions.not_before = now - 5
|
21
|
-
conditions.not_on_or_after = now + 30
|
22
|
-
expect(conditions.valid?).to eq true
|
23
|
-
end
|
24
|
-
|
25
|
-
it "should be invalid with out of range timestamps" do
|
26
|
-
conditions = Conditions.new
|
27
|
-
now = Time.now.utc
|
28
|
-
conditions.not_before = now - 35
|
29
|
-
conditions.not_on_or_after = now - 5
|
30
|
-
expect(conditions.valid?).to eq false
|
31
|
-
end
|
32
|
-
|
33
|
-
it "should allow passing now" do
|
34
|
-
conditions = Conditions.new
|
35
|
-
now = Time.now.utc
|
36
|
-
conditions.not_before = now - 35
|
37
|
-
conditions.not_on_or_after = now - 5
|
38
|
-
expect(conditions.valid?(now: now - 10)).to eq true
|
39
|
-
end
|
40
|
-
|
41
|
-
it "should be invalid before indeterminate" do
|
42
|
-
conditions = Conditions.new
|
43
|
-
now = Time.now.utc
|
44
|
-
conditions.not_before = now + 5
|
45
|
-
conditions << Conditions::Condition.new
|
46
|
-
expect(conditions.valid?).to eq false
|
47
|
-
end
|
48
|
-
|
49
|
-
it "should be invalid before indeterminate (actual conditions)" do
|
50
|
-
conditions = Conditions.new
|
51
|
-
conditions << Conditions::Condition.new
|
52
|
-
conditions << Conditions::AudienceRestriction.new('audience')
|
53
|
-
expect(conditions.valid?).to eq false
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
describe Conditions::AudienceRestriction do
|
58
|
-
it "should be invalid" do
|
59
|
-
expect(Conditions::AudienceRestriction.new('expected').valid?(audience: 'actual')).to eq false
|
60
|
-
end
|
61
|
-
|
62
|
-
it "should be valid" do
|
63
|
-
expect(Conditions::AudienceRestriction.new('expected').valid?(audience: 'expected')).to eq true
|
64
|
-
end
|
65
|
-
|
66
|
-
it "should be valid with an array" do
|
67
|
-
expect(Conditions::AudienceRestriction.new(['expected', 'actual']).valid?(audience: 'actual')).to eq true
|
68
|
-
end
|
69
|
-
|
70
|
-
it "is valid when ignored" do
|
71
|
-
expect(Conditions::AudienceRestriction.new('expected').valid?(audience: 'actual', ignore_audience_condition: true)).to eq true
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
data/spec/lib/entity_spec.rb
DELETED
@@ -1,58 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative '../spec_helper'
|
4
|
-
|
5
|
-
module SAML2
|
6
|
-
describe Entity do
|
7
|
-
it "should parse and validate" do
|
8
|
-
entity = Entity.parse(fixture('service_provider.xml'))
|
9
|
-
expect(entity.valid_schema?).to eq true
|
10
|
-
end
|
11
|
-
|
12
|
-
it "should return nil when not valid schema" do
|
13
|
-
entity = Entity.parse("<xml></xml>")
|
14
|
-
expect(entity).to be_nil
|
15
|
-
end
|
16
|
-
|
17
|
-
it "should return nil on non-XML" do
|
18
|
-
entity = Entity.parse("garbage")
|
19
|
-
expect(entity).to be_nil
|
20
|
-
end
|
21
|
-
|
22
|
-
describe "valid schema" do
|
23
|
-
let(:entity) { Entity.parse(fixture('service_provider.xml')) }
|
24
|
-
|
25
|
-
it "should find the id" do
|
26
|
-
expect(entity.entity_id).to eq "http://siteadmin.instructure.com/saml2"
|
27
|
-
end
|
28
|
-
|
29
|
-
it "should parse the organization" do
|
30
|
-
expect(entity.organization.display_name.to_s).to eq 'Canvas'
|
31
|
-
expect(entity.organization.display_name['en']).to eq 'Canvas'
|
32
|
-
expect(entity.organization.display_name['es']).to be_nil
|
33
|
-
expect(entity.organization.display_name[:all]).to eq en: 'Canvas'
|
34
|
-
end
|
35
|
-
|
36
|
-
it "validates metadata from ADFS containing lots of non-SAML schemas" do
|
37
|
-
expect(Entity.parse(fixture('FederationMetadata.xml')).valid_schema?).to eq true
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
it "should sign correctly" do
|
42
|
-
entity = Entity.parse(fixture('service_provider.xml'))
|
43
|
-
entity.sign(fixture('certificate.pem'), fixture('privatekey.key'))
|
44
|
-
entity2 = Entity.parse(entity.to_s)
|
45
|
-
expect(entity2.valid_schema?).to eq true
|
46
|
-
end
|
47
|
-
|
48
|
-
describe Entity::Group do
|
49
|
-
it "should parse and validate" do
|
50
|
-
group = Entity.parse(fixture('entities.xml'))
|
51
|
-
expect(group).to be_instance_of(Entity::Group)
|
52
|
-
expect(group.valid_schema?).to eq true
|
53
|
-
|
54
|
-
expect(group.map(&:entity_id)).to eq ['urn:entity1', 'urn:entity2']
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
@@ -1,43 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative '../spec_helper'
|
4
|
-
|
5
|
-
module SAML2
|
6
|
-
describe IdentityProvider do
|
7
|
-
it "should serialize valid xml" do
|
8
|
-
entity = Entity.new
|
9
|
-
entity.entity_id = 'http://sso.canvaslms.com/SAML2'
|
10
|
-
entity.organization = Organization.new('Canvas', 'Canvas by Instructure', 'https://www.canvaslms.com/')
|
11
|
-
contact = Contact.new(Contact::Type::TECHNICAL)
|
12
|
-
contact.company = 'Instructure'
|
13
|
-
contact.email_addresses << 'mailto:ops@instructure.com'
|
14
|
-
entity.contacts << contact
|
15
|
-
|
16
|
-
idp = IdentityProvider.new
|
17
|
-
idp.name_id_formats << NameID::Format::PERSISTENT
|
18
|
-
idp.single_sign_on_services << Endpoint.new('https://sso.canvaslms.com/SAML2/Login')
|
19
|
-
idp.keys << KeyDescriptor.new('somedata', KeyDescriptor::Type::SIGNING)
|
20
|
-
|
21
|
-
entity.roles << idp
|
22
|
-
expect(Schemas.metadata.validate(Nokogiri::XML(entity.to_s))).to eq []
|
23
|
-
end
|
24
|
-
|
25
|
-
describe "valid metadata" do
|
26
|
-
let(:entity) { Entity.parse(fixture('identity_provider.xml')) }
|
27
|
-
let(:idp) { entity.roles.first }
|
28
|
-
|
29
|
-
it "should create the single_sign_on_services array" do
|
30
|
-
expect(idp.single_sign_on_services.length).to eq 3
|
31
|
-
expect(idp.single_sign_on_services.first.location).to eq 'https://sso.school.edu/idp/profile/Shibboleth/SSO'
|
32
|
-
end
|
33
|
-
|
34
|
-
it "should find the signing certificate" do
|
35
|
-
expect(idp.keys.first.x509).to match(/MIIE8TCCA9mgAwIBAgIJAITusxON60cKMA0GCSqGSIb3DQEBBQUAMIGrMQswCQYD/)
|
36
|
-
end
|
37
|
-
|
38
|
-
it "loads identity provider attributes" do
|
39
|
-
expect(idp.want_authn_requests_signed?).to be_truthy
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
@@ -1,71 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative '../spec_helper'
|
4
|
-
|
5
|
-
module SAML2
|
6
|
-
describe IndexedObject do
|
7
|
-
describe "#default?" do
|
8
|
-
it "always returns a boolean" do
|
9
|
-
acs = Endpoint::Indexed.new('a', 0)
|
10
|
-
expect(acs.default?).to eq false
|
11
|
-
expect(acs.default_defined?).to eq false
|
12
|
-
end
|
13
|
-
|
14
|
-
it "#default_defined? works" do
|
15
|
-
acs = Endpoint::Indexed.new('a', 0, false)
|
16
|
-
expect(acs.default?).to eq false
|
17
|
-
expect(acs.default_defined?).to eq true
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
context "serialization" do
|
22
|
-
it "doesn't include isDefault when it's nil" do
|
23
|
-
acs = Endpoint::Indexed.new('a', 0)
|
24
|
-
builder = double()
|
25
|
-
expect(builder).to receive(:[]).and_return(builder).ordered
|
26
|
-
expect(builder).to receive(:"AssertionConsumerService").ordered
|
27
|
-
expect(builder).to receive(:parent).and_return(builder).ordered
|
28
|
-
expect(builder).to receive(:children).and_return(builder).ordered
|
29
|
-
expect(builder).to receive(:last).and_return(builder).ordered
|
30
|
-
expect(builder).to receive(:[]=).with("index", 0).ordered
|
31
|
-
|
32
|
-
acs.build(builder,"AssertionConsumerService")
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
describe IndexedObject::Array do
|
38
|
-
it "should sort by index" do
|
39
|
-
acses = Endpoint::Indexed::Array.new(
|
40
|
-
[Endpoint::Indexed.new('b', 1),
|
41
|
-
Endpoint::Indexed.new('a', 0)])
|
42
|
-
expect(acses.map(&:location)).to eq ['a', 'b']
|
43
|
-
end
|
44
|
-
|
45
|
-
it "should be accessible by index" do
|
46
|
-
acses = Endpoint::Indexed::Array.new(
|
47
|
-
[Endpoint::Indexed.new('b', 3),
|
48
|
-
Endpoint::Indexed.new('a', 1)])
|
49
|
-
expect(acses.map(&:location)).to eq ['a', 'b']
|
50
|
-
expect(acses[1].location).to eq 'a'
|
51
|
-
expect(acses[3].location).to eq 'b'
|
52
|
-
expect(acses[0]).to be_nil
|
53
|
-
end
|
54
|
-
|
55
|
-
describe "#default" do
|
56
|
-
it "should default to first entry if not otherwise specified" do
|
57
|
-
acses = Endpoint::Indexed::Array.new(
|
58
|
-
[Endpoint::Indexed.new('a', 0),
|
59
|
-
Endpoint::Indexed.new('b', 1)])
|
60
|
-
expect(acses.default.location).to eq 'a'
|
61
|
-
end
|
62
|
-
|
63
|
-
it "should default to a tagged default" do
|
64
|
-
acses = Endpoint::Indexed::Array.new(
|
65
|
-
[Endpoint::Indexed.new('a', 0),
|
66
|
-
Endpoint::Indexed.new('b', 1, true)])
|
67
|
-
expect(acses.default.location).to eq 'b'
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
data/spec/lib/key_spec.rb
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative '../spec_helper'
|
4
|
-
|
5
|
-
module SAML2
|
6
|
-
describe KeyInfo do
|
7
|
-
describe '.format_fingerprint' do
|
8
|
-
it 'strips non-hexadecimal characters' do
|
9
|
-
expect(KeyInfo.format_fingerprint("\u200F abcdefghijklmnop 1234567890-\n a1")).to eq("ab:cd:ef:12:34:56:78:90:a1")
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
describe '#certificate' do
|
14
|
-
it "doesn't asplode if the keyinfo is just an rsa key value" do
|
15
|
-
response = Nokogiri::XML(fixture("response_with_rsa_key_value.xml"))
|
16
|
-
key = KeyInfo.from_xml(response.at_xpath('//dsig:KeyInfo', Namespaces::ALL))
|
17
|
-
expect(key.certificate).to be_nil
|
18
|
-
expect(key.fingerprint).to be_nil
|
19
|
-
expect(key.key).not_to be_nil
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|