saml_idp 0.8.0 → 0.15.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/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
|