saml_idp 0.8.0 → 0.15.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +49 -47
- data/lib/saml_idp/assertion_builder.rb +28 -3
- data/lib/saml_idp/configurator.rb +6 -1
- data/lib/saml_idp/controller.rb +19 -11
- data/lib/saml_idp/encryptor.rb +0 -1
- data/lib/saml_idp/fingerprint.rb +19 -0
- data/lib/saml_idp/incoming_metadata.rb +18 -0
- data/lib/saml_idp/metadata_builder.rb +23 -8
- data/lib/saml_idp/persisted_metadata.rb +4 -0
- data/lib/saml_idp/request.rb +13 -6
- data/lib/saml_idp/response_builder.rb +26 -6
- data/lib/saml_idp/saml_response.rb +62 -28
- data/lib/saml_idp/service_provider.rb +1 -6
- data/lib/saml_idp/signable.rb +1 -2
- data/lib/saml_idp/version.rb +1 -1
- data/lib/saml_idp/xml_security.rb +1 -1
- data/lib/saml_idp.rb +2 -1
- data/saml_idp.gemspec +31 -31
- data/spec/lib/saml_idp/assertion_builder_spec.rb +143 -0
- data/spec/lib/saml_idp/configurator_spec.rb +2 -0
- data/spec/lib/saml_idp/controller_spec.rb +24 -0
- data/spec/lib/saml_idp/fingerprint_spec.rb +14 -0
- data/spec/lib/saml_idp/incoming_metadata_spec.rb +20 -1
- data/spec/lib/saml_idp/metadata_builder_spec.rb +23 -0
- data/spec/lib/saml_idp/request_spec.rb +43 -9
- data/spec/lib/saml_idp/response_builder_spec.rb +3 -1
- data/spec/lib/saml_idp/saml_response_spec.rb +122 -7
- data/spec/rails_app/app/controllers/saml_controller.rb +1 -5
- data/spec/rails_app/app/controllers/saml_idp_controller.rb +55 -3
- data/{app → spec/rails_app/app}/views/saml_idp/idp/new.html.erb +1 -5
- data/{app → spec/rails_app/app}/views/saml_idp/idp/saml_post.html.erb +1 -1
- data/spec/rails_app/config/application.rb +1 -0
- data/spec/rails_app/config/boot.rb +1 -1
- data/spec/rails_app/config/environments/development.rb +2 -0
- data/spec/spec_helper.rb +20 -1
- data/spec/support/certificates/sp_cert_req.csr +12 -0
- data/spec/support/certificates/sp_private_key.pem +16 -0
- data/spec/support/certificates/sp_x509_cert.crt +18 -0
- data/spec/support/saml_request_macros.rb +62 -3
- data/spec/support/security_helpers.rb +10 -0
- metadata +83 -61
- data/app/controllers/saml_idp/idp_controller.rb +0 -59
@@ -1,8 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'saml_idp/assertion_builder'
|
2
4
|
require 'saml_idp/response_builder'
|
3
5
|
module SamlIdp
|
4
6
|
class SamlResponse
|
5
|
-
attr_accessor :assertion_with_signature
|
6
7
|
attr_accessor :reference_id
|
7
8
|
attr_accessor :response_id
|
8
9
|
attr_accessor :issuer_uri
|
@@ -17,20 +18,32 @@ module SamlIdp
|
|
17
18
|
attr_accessor :expiry
|
18
19
|
attr_accessor :encryption_opts
|
19
20
|
attr_accessor :session_expiry
|
21
|
+
attr_accessor :name_id_formats_opts
|
22
|
+
attr_accessor :asserted_attributes_opts
|
23
|
+
attr_accessor :signed_message_opts
|
24
|
+
attr_accessor :signed_assertion_opts
|
25
|
+
attr_accessor :compression_opts
|
26
|
+
|
27
|
+
def initialize(
|
28
|
+
reference_id,
|
29
|
+
response_id,
|
30
|
+
issuer_uri,
|
31
|
+
principal,
|
32
|
+
audience_uri,
|
33
|
+
saml_request_id,
|
34
|
+
saml_acs_url,
|
35
|
+
algorithm,
|
36
|
+
authn_context_classref,
|
37
|
+
expiry = 60 * 60,
|
38
|
+
encryption_opts = nil,
|
39
|
+
session_expiry = 0,
|
40
|
+
name_id_formats_opts = nil,
|
41
|
+
asserted_attributes_opts = nil,
|
42
|
+
signed_message_opts = false,
|
43
|
+
signed_assertion_opts = true,
|
44
|
+
compression_opts = false
|
45
|
+
)
|
20
46
|
|
21
|
-
def initialize(reference_id,
|
22
|
-
response_id,
|
23
|
-
issuer_uri,
|
24
|
-
principal,
|
25
|
-
audience_uri,
|
26
|
-
saml_request_id,
|
27
|
-
saml_acs_url,
|
28
|
-
algorithm,
|
29
|
-
authn_context_classref,
|
30
|
-
expiry=60*60,
|
31
|
-
encryption_opts=nil,
|
32
|
-
session_expiry=0
|
33
|
-
)
|
34
47
|
self.reference_id = reference_id
|
35
48
|
self.response_id = response_id
|
36
49
|
self.issuer_uri = issuer_uri
|
@@ -45,38 +58,59 @@ module SamlIdp
|
|
45
58
|
self.expiry = expiry
|
46
59
|
self.encryption_opts = encryption_opts
|
47
60
|
self.session_expiry = session_expiry
|
61
|
+
self.signed_message_opts = signed_message_opts
|
62
|
+
self.name_id_formats_opts = name_id_formats_opts
|
63
|
+
self.asserted_attributes_opts = asserted_attributes_opts
|
64
|
+
self.signed_assertion_opts = signed_assertion_opts
|
65
|
+
self.name_id_formats_opts = name_id_formats_opts
|
66
|
+
self.asserted_attributes_opts = asserted_attributes_opts
|
67
|
+
self.compression_opts = compression_opts
|
48
68
|
end
|
49
69
|
|
50
70
|
def build
|
51
|
-
@
|
71
|
+
@build ||= encoded_message
|
52
72
|
end
|
53
73
|
|
54
74
|
def signed_assertion
|
55
75
|
if encryption_opts
|
56
76
|
assertion_builder.encrypt(sign: true)
|
57
|
-
|
77
|
+
elsif signed_assertion_opts
|
58
78
|
assertion_builder.signed
|
79
|
+
else
|
80
|
+
assertion_builder.raw
|
59
81
|
end
|
60
82
|
end
|
61
83
|
private :signed_assertion
|
62
84
|
|
85
|
+
def encoded_message
|
86
|
+
if signed_message_opts
|
87
|
+
response_builder.encoded(signed_message: true, compress: compression_opts)
|
88
|
+
else
|
89
|
+
response_builder.encoded(signed_message: false, compress: compression_opts)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
private :encoded_message
|
93
|
+
|
63
94
|
def response_builder
|
64
|
-
ResponseBuilder.new(response_id, issuer_uri, saml_acs_url, saml_request_id, signed_assertion)
|
95
|
+
ResponseBuilder.new(response_id, issuer_uri, saml_acs_url, saml_request_id, signed_assertion, algorithm)
|
65
96
|
end
|
66
97
|
private :response_builder
|
67
98
|
|
68
99
|
def assertion_builder
|
69
|
-
@assertion_builder ||=
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
100
|
+
@assertion_builder ||=
|
101
|
+
AssertionBuilder.new SecureRandom.uuid,
|
102
|
+
issuer_uri,
|
103
|
+
principal,
|
104
|
+
audience_uri,
|
105
|
+
saml_request_id,
|
106
|
+
saml_acs_url,
|
107
|
+
algorithm,
|
108
|
+
authn_context_classref,
|
109
|
+
expiry,
|
110
|
+
encryption_opts,
|
111
|
+
session_expiry,
|
112
|
+
name_id_formats_opts,
|
113
|
+
asserted_attributes_opts
|
80
114
|
end
|
81
115
|
private :assertion_builder
|
82
116
|
end
|
@@ -22,18 +22,13 @@ module SamlIdp
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def valid_signature?(doc, require_signature = false)
|
25
|
-
if require_signature ||
|
25
|
+
if require_signature || attributes[:validate_signature]
|
26
26
|
doc.valid_signature?(fingerprint)
|
27
27
|
else
|
28
28
|
true
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
def should_validate_signature?
|
33
|
-
attributes[:validate_signature] ||
|
34
|
-
current_metadata.respond_to?(:sign_assertions?) && current_metadata.sign_assertions?
|
35
|
-
end
|
36
|
-
|
37
32
|
def refresh_metadata
|
38
33
|
fresh = fresh_incoming_metadata
|
39
34
|
if valid_signature?(fresh.document)
|
data/lib/saml_idp/signable.rb
CHANGED
@@ -108,8 +108,7 @@ module SamlIdp
|
|
108
108
|
canon_algorithm = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
|
109
109
|
canon_hashed_element = noko_raw.canonicalize(canon_algorithm, inclusive_namespaces)
|
110
110
|
digest_algorithm = get_algorithm
|
111
|
-
|
112
|
-
hash = digest_algorithm.digest(canon_hashed_element)
|
111
|
+
hash = digest_algorithm.digest(canon_hashed_element)
|
113
112
|
Base64.strict_encode64(hash).gsub(/\n/, '')
|
114
113
|
end
|
115
114
|
private :digest
|
data/lib/saml_idp/version.rb
CHANGED
@@ -108,7 +108,7 @@ module SamlIdp
|
|
108
108
|
canon_algorithm = canon_algorithm REXML::XPath.first(ref, '//ds:CanonicalizationMethod', 'ds' => DSIG)
|
109
109
|
canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
|
110
110
|
|
111
|
-
digest_algorithm = algorithm(REXML::XPath.first(ref, "//ds:DigestMethod"))
|
111
|
+
digest_algorithm = algorithm(REXML::XPath.first(ref, "//ds:DigestMethod", {'ds' => DSIG}))
|
112
112
|
|
113
113
|
hash = digest_algorithm.digest(canon_hashed_element)
|
114
114
|
digest_value = Base64.decode64(REXML::XPath.first(ref, "//ds:DigestValue", {"ds"=>DSIG}).text)
|
data/lib/saml_idp.rb
CHANGED
@@ -8,7 +8,8 @@ module SamlIdp
|
|
8
8
|
require 'saml_idp/default'
|
9
9
|
require 'saml_idp/metadata_builder'
|
10
10
|
require 'saml_idp/version'
|
11
|
-
require 'saml_idp/
|
11
|
+
require 'saml_idp/fingerprint'
|
12
|
+
require 'saml_idp/engine' if defined?(::Rails)
|
12
13
|
|
13
14
|
def self.config
|
14
15
|
@config ||= SamlIdp::Configurator.new
|
data/saml_idp.gemspec
CHANGED
@@ -1,62 +1,62 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
|
3
|
-
|
2
|
+
|
3
|
+
$LOAD_PATH.push File.expand_path('lib', __dir__)
|
4
|
+
require 'saml_idp/version'
|
4
5
|
|
5
6
|
Gem::Specification.new do |s|
|
6
7
|
s.name = %q{saml_idp}
|
7
8
|
s.version = SamlIdp::VERSION
|
8
9
|
s.platform = Gem::Platform::RUBY
|
9
|
-
s.authors = [
|
10
|
+
s.authors = ['Jon Phenow']
|
10
11
|
s.email = 'jon.phenow@sportngin.com'
|
11
12
|
s.homepage = 'https://github.com/saml-idp/saml_idp'
|
12
13
|
s.summary = 'SAML Indentity Provider for Ruby'
|
13
14
|
s.description = 'SAML IdP (Identity Provider) Library for Ruby'
|
14
|
-
s.date = Time.now.utc.strftime(
|
15
|
-
s.files = Dir['
|
16
|
-
s.required_ruby_version = '>= 2.
|
15
|
+
s.date = Time.now.utc.strftime('%Y-%m-%d')
|
16
|
+
s.files = Dir['lib/**/*', 'LICENSE', 'README.md', 'Gemfile', 'saml_idp.gemspec']
|
17
|
+
s.required_ruby_version = '>= 2.5'
|
17
18
|
s.license = 'MIT'
|
18
19
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
20
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
-
s.require_paths = [
|
21
|
+
s.require_paths = ['lib']
|
21
22
|
s.rdoc_options = ['--charset=UTF-8']
|
22
23
|
s.metadata = {
|
23
|
-
'homepage_uri'
|
24
|
-
'source_code_uri'
|
25
|
-
'bug_tracker_uri'
|
24
|
+
'homepage_uri' => 'https://github.com/saml-idp/saml_idp',
|
25
|
+
'source_code_uri' => 'https://github.com/saml-idp/saml_idp',
|
26
|
+
'bug_tracker_uri' => 'https://github.com/saml-idp/saml_idp/issues',
|
26
27
|
'documentation_uri' => "http://rdoc.info/gems/saml_idp/#{SamlIdp::VERSION}"
|
27
28
|
}
|
28
29
|
|
29
30
|
s.post_install_message = <<-INST
|
30
|
-
If you're just recently updating saml_idp - please be aware we've changed the default
|
31
|
-
certificate. See the PR and a description of why we've done this here:
|
32
|
-
https://github.com/saml-idp/saml_idp/pull/29
|
33
|
-
|
34
|
-
If you just need to see the certificate `bundle open saml_idp` and go to
|
35
|
-
`lib/saml_idp/default.rb`
|
31
|
+
If you're just recently updating saml_idp - please be aware we've changed the default
|
32
|
+
certificate. See the PR and a description of why we've done this here:
|
33
|
+
https://github.com/saml-idp/saml_idp/pull/29
|
36
34
|
|
37
|
-
|
38
|
-
|
35
|
+
If you just need to see the certificate `bundle open saml_idp` and go to
|
36
|
+
`lib/saml_idp/default.rb`
|
39
37
|
|
40
|
-
|
38
|
+
Similarly, please see the README about certificates - you should avoid using the
|
39
|
+
defaults in a Production environment. Post any issues you to github.
|
41
40
|
|
42
|
-
|
43
|
-
|
41
|
+
** New in Version 0.3.0 **
|
42
|
+
Encrypted Assertions require the xmlenc gem. See the example in the Controller
|
43
|
+
section of the README.
|
44
44
|
INST
|
45
45
|
|
46
|
-
s.add_dependency('activesupport', '>=
|
47
|
-
s.add_dependency('uuid', '>= 2.3')
|
46
|
+
s.add_dependency('activesupport', '>= 5.2')
|
48
47
|
s.add_dependency('builder', '>= 3.0')
|
49
48
|
s.add_dependency('nokogiri', '>= 1.6.2')
|
49
|
+
s.add_dependency('rexml')
|
50
|
+
s.add_dependency('xmlenc', '>= 0.7.1')
|
50
51
|
|
52
|
+
s.add_development_dependency('activeresource', '>= 5.1')
|
53
|
+
s.add_development_dependency('appraisal')
|
54
|
+
s.add_development_dependency('byebug')
|
55
|
+
s.add_development_dependency('capybara', '>= 2.16')
|
56
|
+
s.add_development_dependency('rails', '>= 5.2')
|
51
57
|
s.add_development_dependency('rake')
|
52
|
-
s.add_development_dependency('simplecov')
|
53
58
|
s.add_development_dependency('rspec', '>= 3.7.0')
|
54
|
-
s.add_development_dependency('ruby-saml', '>= 1.
|
55
|
-
s.add_development_dependency('
|
56
|
-
s.add_development_dependency('activeresource', '>= 3.2')
|
57
|
-
s.add_development_dependency('capybara', '>= 2.16')
|
59
|
+
s.add_development_dependency('ruby-saml', '>= 1.7.2')
|
60
|
+
s.add_development_dependency('simplecov')
|
58
61
|
s.add_development_dependency('timecop', '>= 0.8')
|
59
|
-
s.add_development_dependency('xmlenc', '>= 0.6.4')
|
60
|
-
s.add_development_dependency('appraisal')
|
61
62
|
end
|
62
|
-
|
@@ -19,6 +19,9 @@ module SamlIdp
|
|
19
19
|
key_transport: 'rsa-oaep-mgf1p',
|
20
20
|
}
|
21
21
|
end
|
22
|
+
let(:session_expiry) { nil }
|
23
|
+
let(:name_id_formats_opt) { nil }
|
24
|
+
let(:asserted_attributes_opt) { nil }
|
22
25
|
subject { described_class.new(
|
23
26
|
reference_id,
|
24
27
|
issuer_uri,
|
@@ -103,6 +106,76 @@ module SamlIdp
|
|
103
106
|
expect(encrypted_xml).to_not match(audience_uri)
|
104
107
|
end
|
105
108
|
|
109
|
+
describe "with name_id_formats_opt" do
|
110
|
+
let(:name_id_formats_opt) {
|
111
|
+
{
|
112
|
+
persistent: -> (principal) {
|
113
|
+
principal.unique_identifier
|
114
|
+
}
|
115
|
+
}
|
116
|
+
}
|
117
|
+
it "delegates name_id_formats to opts" do
|
118
|
+
UserWithUniqueId = Struct.new(:unique_identifier, :email, :asserted_attributes)
|
119
|
+
principal = UserWithUniqueId.new('unique_identifier_123456', 'foo@example.com', { emailAddress: { getter: :email } })
|
120
|
+
builder = described_class.new(
|
121
|
+
reference_id,
|
122
|
+
issuer_uri,
|
123
|
+
principal,
|
124
|
+
audience_uri,
|
125
|
+
saml_request_id,
|
126
|
+
saml_acs_url,
|
127
|
+
algorithm,
|
128
|
+
authn_context_classref,
|
129
|
+
expiry,
|
130
|
+
encryption_opts,
|
131
|
+
session_expiry,
|
132
|
+
name_id_formats_opt,
|
133
|
+
asserted_attributes_opt
|
134
|
+
)
|
135
|
+
Timecop.travel(Time.zone.local(2010, 6, 1, 13, 0, 0)) do
|
136
|
+
expect(builder.raw).to eq("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"_abc\" IssueInstant=\"2010-06-01T13:00:00Z\" Version=\"2.0\"><Issuer>http://sportngin.com</Issuer><Subject><NameID Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent\">unique_identifier_123456</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><SubjectConfirmationData InResponseTo=\"123\" NotOnOrAfter=\"2010-06-01T13:03:00Z\" Recipient=\"http://saml.acs.url\"></SubjectConfirmationData></SubjectConfirmation></Subject><Conditions NotBefore=\"2010-06-01T12:59:55Z\" NotOnOrAfter=\"2010-06-01T16:00:00Z\"><AudienceRestriction><Audience>http://example.com</Audience></AudienceRestriction></Conditions><AuthnStatement AuthnInstant=\"2010-06-01T13:00:00Z\" SessionIndex=\"_abc\"><AuthnContext><AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef></AuthnContext></AuthnStatement><AttributeStatement><Attribute Name=\"emailAddress\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"emailAddress\"><AttributeValue>foo@example.com</AttributeValue></Attribute></AttributeStatement></Assertion>")
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe "with asserted_attributes_opt" do
|
142
|
+
let(:asserted_attributes_opt) {
|
143
|
+
{
|
144
|
+
'GivenName' => {
|
145
|
+
getter: :first_name
|
146
|
+
},
|
147
|
+
'SurName' => {
|
148
|
+
getter: -> (principal) {
|
149
|
+
principal.last_name
|
150
|
+
}
|
151
|
+
}
|
152
|
+
}
|
153
|
+
}
|
154
|
+
|
155
|
+
it "delegates asserted_attributes to opts" do
|
156
|
+
UserWithName = Struct.new(:email, :first_name, :last_name)
|
157
|
+
principal = UserWithName.new('foo@example.com', 'George', 'Washington')
|
158
|
+
builder = described_class.new(
|
159
|
+
reference_id,
|
160
|
+
issuer_uri,
|
161
|
+
principal,
|
162
|
+
audience_uri,
|
163
|
+
saml_request_id,
|
164
|
+
saml_acs_url,
|
165
|
+
algorithm,
|
166
|
+
authn_context_classref,
|
167
|
+
expiry,
|
168
|
+
encryption_opts,
|
169
|
+
session_expiry,
|
170
|
+
name_id_formats_opt,
|
171
|
+
asserted_attributes_opt
|
172
|
+
)
|
173
|
+
Timecop.travel(Time.zone.local(2010, 6, 1, 13, 0, 0)) do
|
174
|
+
expect(builder.raw).to eq("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"_abc\" IssueInstant=\"2010-06-01T13:00:00Z\" Version=\"2.0\"><Issuer>http://sportngin.com</Issuer><Subject><NameID Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\">foo@example.com</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><SubjectConfirmationData InResponseTo=\"123\" NotOnOrAfter=\"2010-06-01T13:03:00Z\" Recipient=\"http://saml.acs.url\"></SubjectConfirmationData></SubjectConfirmation></Subject><Conditions NotBefore=\"2010-06-01T12:59:55Z\" NotOnOrAfter=\"2010-06-01T16:00:00Z\"><AudienceRestriction><Audience>http://example.com</Audience></AudienceRestriction></Conditions><AuthnStatement AuthnInstant=\"2010-06-01T13:00:00Z\" SessionIndex=\"_abc\"><AuthnContext><AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef></AuthnContext></AuthnStatement><AttributeStatement><Attribute Name=\"GivenName\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"GivenName\"><AttributeValue>George</AttributeValue></Attribute><Attribute Name=\"SurName\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"SurName\"><AttributeValue>Washington</AttributeValue></Attribute></AttributeStatement></Assertion>")
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
106
179
|
describe "with custom session_expiry configuration" do
|
107
180
|
let(:config) { SamlIdp::Configurator.new }
|
108
181
|
before do
|
@@ -126,5 +199,75 @@ module SamlIdp
|
|
126
199
|
expect(builder.session_expiry).to eq(8)
|
127
200
|
end
|
128
201
|
end
|
202
|
+
|
203
|
+
describe "with name_id_formats_opt" do
|
204
|
+
let(:name_id_formats_opt) {
|
205
|
+
{
|
206
|
+
persistent: -> (principal) {
|
207
|
+
principal.unique_identifier
|
208
|
+
}
|
209
|
+
}
|
210
|
+
}
|
211
|
+
it "delegates name_id_formats to opts" do
|
212
|
+
UserWithUniqueId = Struct.new(:unique_identifier, :email, :asserted_attributes)
|
213
|
+
principal = UserWithUniqueId.new('unique_identifier_123456', 'foo@example.com', { emailAddress: { getter: :email } })
|
214
|
+
builder = described_class.new(
|
215
|
+
reference_id,
|
216
|
+
issuer_uri,
|
217
|
+
principal,
|
218
|
+
audience_uri,
|
219
|
+
saml_request_id,
|
220
|
+
saml_acs_url,
|
221
|
+
algorithm,
|
222
|
+
authn_context_classref,
|
223
|
+
expiry,
|
224
|
+
encryption_opts,
|
225
|
+
session_expiry,
|
226
|
+
name_id_formats_opt,
|
227
|
+
asserted_attributes_opt
|
228
|
+
)
|
229
|
+
Timecop.travel(Time.zone.local(2010, 6, 1, 13, 0, 0)) do
|
230
|
+
expect(builder.raw).to eq("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"_abc\" IssueInstant=\"2010-06-01T13:00:00Z\" Version=\"2.0\"><Issuer>http://sportngin.com</Issuer><Subject><NameID Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent\">unique_identifier_123456</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><SubjectConfirmationData InResponseTo=\"123\" NotOnOrAfter=\"2010-06-01T13:03:00Z\" Recipient=\"http://saml.acs.url\"></SubjectConfirmationData></SubjectConfirmation></Subject><Conditions NotBefore=\"2010-06-01T12:59:55Z\" NotOnOrAfter=\"2010-06-01T16:00:00Z\"><AudienceRestriction><Audience>http://example.com</Audience></AudienceRestriction></Conditions><AuthnStatement AuthnInstant=\"2010-06-01T13:00:00Z\" SessionIndex=\"_abc\"><AuthnContext><AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef></AuthnContext></AuthnStatement><AttributeStatement><Attribute Name=\"emailAddress\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"emailAddress\"><AttributeValue>foo@example.com</AttributeValue></Attribute></AttributeStatement></Assertion>")
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
describe "with asserted_attributes_opt" do
|
236
|
+
let(:asserted_attributes_opt) {
|
237
|
+
{
|
238
|
+
'GivenName' => {
|
239
|
+
getter: :first_name
|
240
|
+
},
|
241
|
+
'SurName' => {
|
242
|
+
getter: -> (principal) {
|
243
|
+
principal.last_name
|
244
|
+
}
|
245
|
+
}
|
246
|
+
}
|
247
|
+
}
|
248
|
+
|
249
|
+
it "delegates asserted_attributes to opts" do
|
250
|
+
UserWithName = Struct.new(:email, :first_name, :last_name)
|
251
|
+
principal = UserWithName.new('foo@example.com', 'George', 'Washington')
|
252
|
+
builder = described_class.new(
|
253
|
+
reference_id,
|
254
|
+
issuer_uri,
|
255
|
+
principal,
|
256
|
+
audience_uri,
|
257
|
+
saml_request_id,
|
258
|
+
saml_acs_url,
|
259
|
+
algorithm,
|
260
|
+
authn_context_classref,
|
261
|
+
expiry,
|
262
|
+
encryption_opts,
|
263
|
+
session_expiry,
|
264
|
+
name_id_formats_opt,
|
265
|
+
asserted_attributes_opt
|
266
|
+
)
|
267
|
+
Timecop.travel(Time.zone.local(2010, 6, 1, 13, 0, 0)) do
|
268
|
+
expect(builder.raw).to eq("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"_abc\" IssueInstant=\"2010-06-01T13:00:00Z\" Version=\"2.0\"><Issuer>http://sportngin.com</Issuer><Subject><NameID Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\">foo@example.com</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><SubjectConfirmationData InResponseTo=\"123\" NotOnOrAfter=\"2010-06-01T13:03:00Z\" Recipient=\"http://saml.acs.url\"></SubjectConfirmationData></SubjectConfirmation></Subject><Conditions NotBefore=\"2010-06-01T12:59:55Z\" NotOnOrAfter=\"2010-06-01T16:00:00Z\"><AudienceRestriction><Audience>http://example.com</Audience></AudienceRestriction></Conditions><AuthnStatement AuthnInstant=\"2010-06-01T13:00:00Z\" SessionIndex=\"_abc\"><AuthnContext><AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef></AuthnContext></AuthnStatement><AttributeStatement><Attribute Name=\"GivenName\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"GivenName\"><AttributeValue>George</AttributeValue></Attribute><Attribute Name=\"SurName\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"SurName\"><AttributeValue>Washington</AttributeValue></Attribute></AttributeStatement></Assertion>")
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
129
272
|
end
|
130
273
|
end
|
@@ -9,6 +9,7 @@ module SamlIdp
|
|
9
9
|
it { should respond_to :base_saml_location }
|
10
10
|
it { should respond_to :reference_id_generator }
|
11
11
|
it { should respond_to :attribute_service_location }
|
12
|
+
it { should respond_to :single_service_redirect_location }
|
12
13
|
it { should respond_to :single_service_post_location }
|
13
14
|
it { should respond_to :single_logout_service_post_location }
|
14
15
|
it { should respond_to :single_logout_service_redirect_location }
|
@@ -16,6 +17,7 @@ module SamlIdp
|
|
16
17
|
it { should respond_to :attributes }
|
17
18
|
it { should respond_to :service_provider }
|
18
19
|
it { should respond_to :session_expiry }
|
20
|
+
it { should respond_to :logger }
|
19
21
|
|
20
22
|
it "has a valid x509_certificate" do
|
21
23
|
expect(subject.x509_certificate).to eq(Default::X509_CERTIFICATE)
|
@@ -21,6 +21,30 @@ describe SamlIdp::Controller do
|
|
21
21
|
expect(saml_acs_url).to eq(requested_saml_acs_url)
|
22
22
|
end
|
23
23
|
|
24
|
+
context "When SP metadata required to validate auth request signature" do
|
25
|
+
before do
|
26
|
+
idp_configure("https://foo.example.com/saml/consume", true)
|
27
|
+
params[:SAMLRequest] = make_saml_request("https://foo.example.com/saml/consume", true)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'SP metadata sign_authn_request attribute should be true' do
|
31
|
+
# Signed auth request will be true in the metadata
|
32
|
+
expect(SamlIdp.config.service_provider.persisted_metadata_getter.call(nil,nil)[:sign_authn_request]).to eq(true)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should call xml signature validation method' do
|
36
|
+
signed_doc = SamlIdp::XMLSecurity::SignedDocument.new(params[:SAMLRequest])
|
37
|
+
allow(signed_doc).to receive(:validate).and_return(true)
|
38
|
+
allow(SamlIdp::XMLSecurity::SignedDocument).to receive(:new).and_return(signed_doc)
|
39
|
+
validate_saml_request
|
40
|
+
expect(signed_doc).to have_received(:validate).once
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should successfully validate signature' do
|
44
|
+
expect(validate_saml_request).to eq(true)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
24
48
|
context "SAML Responses" do
|
25
49
|
let(:principal) { double email_address: "foo@example.com" }
|
26
50
|
let (:encryption_opts) do
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module SamlIdp
|
4
|
+
describe Fingerprint do
|
5
|
+
describe "certificate_digest" do
|
6
|
+
let(:cert) { sp_x509_cert }
|
7
|
+
let(:fingerprint) { "a2:cb:f6:6b:bc:2a:33:b9:4f:f3:c3:7e:26:a4:21:cd:41:83:ef:26:88:fa:ba:71:37:40:07:3e:d5:76:04:b7" }
|
8
|
+
|
9
|
+
it "returns the fingerprint string" do
|
10
|
+
expect(Fingerprint.certificate_digest(cert, :sha256)).to eq(fingerprint)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -3,7 +3,7 @@ module SamlIdp
|
|
3
3
|
|
4
4
|
metadata_1 = <<-eos
|
5
5
|
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="test" entityID="https://test-saml.com/saml">
|
6
|
-
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol" AuthnRequestsSigned="
|
6
|
+
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol" AuthnRequestsSigned="false" WantAssertionsSigned="false">
|
7
7
|
</md:SPSSODescriptor>
|
8
8
|
</md:EntityDescriptor>
|
9
9
|
eos
|
@@ -22,20 +22,39 @@ module SamlIdp
|
|
22
22
|
</md:EntityDescriptor>
|
23
23
|
eos
|
24
24
|
|
25
|
+
metadata_4 = <<-eos
|
26
|
+
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="test" entityID="https://test-saml.com/saml">
|
27
|
+
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
28
|
+
</md:SPSSODescriptor>
|
29
|
+
</md:EntityDescriptor>
|
30
|
+
eos
|
31
|
+
|
25
32
|
describe IncomingMetadata do
|
26
33
|
it 'should properly set sign_assertions to false' do
|
27
34
|
metadata = SamlIdp::IncomingMetadata.new(metadata_1)
|
28
35
|
expect(metadata.sign_assertions).to eq(false)
|
36
|
+
expect(metadata.sign_authn_request).to eq(false)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should properly set entity_id as https://test-saml.com/saml' do
|
40
|
+
metadata = SamlIdp::IncomingMetadata.new(metadata_1)
|
41
|
+
expect(metadata.entity_id).to eq('https://test-saml.com/saml')
|
29
42
|
end
|
30
43
|
|
31
44
|
it 'should properly set sign_assertions to true' do
|
32
45
|
metadata = SamlIdp::IncomingMetadata.new(metadata_2)
|
33
46
|
expect(metadata.sign_assertions).to eq(true)
|
47
|
+
expect(metadata.sign_authn_request).to eq(true)
|
34
48
|
end
|
35
49
|
|
36
50
|
it 'should properly set sign_assertions to false when WantAssertionsSigned is not included' do
|
37
51
|
metadata = SamlIdp::IncomingMetadata.new(metadata_3)
|
38
52
|
expect(metadata.sign_assertions).to eq(false)
|
39
53
|
end
|
54
|
+
|
55
|
+
it 'should properly set sign_authn_request to false when AuthnRequestsSigned is not included' do
|
56
|
+
metadata = SamlIdp::IncomingMetadata.new(metadata_4)
|
57
|
+
expect(metadata.sign_authn_request).to eq(false)
|
58
|
+
end
|
40
59
|
end
|
41
60
|
end
|
@@ -11,7 +11,30 @@ module SamlIdp
|
|
11
11
|
|
12
12
|
it "includes logout element" do
|
13
13
|
subject.configurator.single_logout_service_post_location = 'https://example.com/saml/logout'
|
14
|
+
subject.configurator.single_logout_service_redirect_location = 'https://example.com/saml/logout'
|
14
15
|
expect(subject.fresh).to match('<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://example.com/saml/logout"/>')
|
16
|
+
expect(subject.fresh).to match('<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://example.com/saml/logout"/>')
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'will not includes empty logout endpoint' do
|
20
|
+
subject.configurator.single_logout_service_post_location = ''
|
21
|
+
subject.configurator.single_logout_service_redirect_location = nil
|
22
|
+
expect(subject.fresh).not_to match('<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"')
|
23
|
+
expect(subject.fresh).not_to match('<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"')
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'will includes sso element' do
|
27
|
+
subject.configurator.single_service_post_location = 'https://example.com/saml/sso'
|
28
|
+
subject.configurator.single_service_redirect_location = 'https://example.com/saml/sso'
|
29
|
+
expect(subject.fresh).to match('<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://example.com/saml/sso"/>')
|
30
|
+
expect(subject.fresh).to match('<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://example.com/saml/sso"/>')
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'will not includes empty sso element' do
|
34
|
+
subject.configurator.single_service_post_location = ''
|
35
|
+
subject.configurator.single_service_redirect_location = nil
|
36
|
+
expect(subject.fresh).not_to match('<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"')
|
37
|
+
expect(subject.fresh).not_to match('<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"')
|
15
38
|
end
|
16
39
|
|
17
40
|
context "technical contact" do
|