saml_idp 0.7.2 → 1.0.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 +5 -5
- data/Gemfile +1 -1
- data/README.md +71 -55
- data/lib/saml_idp/assertion_builder.rb +28 -3
- data/lib/saml_idp/configurator.rb +9 -3
- data/lib/saml_idp/controller.rb +27 -16
- data/lib/saml_idp/encryptor.rb +0 -1
- data/lib/saml_idp/fingerprint.rb +19 -0
- data/lib/saml_idp/incoming_metadata.rb +31 -1
- data/lib/saml_idp/metadata_builder.rb +25 -9
- data/lib/saml_idp/persisted_metadata.rb +4 -0
- data/lib/saml_idp/request.rb +103 -13
- 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 +16 -6
- data/lib/saml_idp/signable.rb +1 -2
- data/lib/saml_idp/signature_builder.rb +2 -1
- data/lib/saml_idp/signed_info_builder.rb +2 -2
- data/lib/saml_idp/version.rb +1 -1
- data/lib/saml_idp/xml_security.rb +20 -15
- data/lib/saml_idp.rb +4 -3
- data/saml_idp.gemspec +46 -42
- data/spec/acceptance/idp_controller_spec.rb +5 -4
- data/spec/lib/saml_idp/algorithmable_spec.rb +6 -6
- data/spec/lib/saml_idp/assertion_builder_spec.rb +151 -8
- data/spec/lib/saml_idp/attribute_decorator_spec.rb +8 -8
- data/spec/lib/saml_idp/configurator_spec.rb +45 -7
- data/spec/lib/saml_idp/controller_spec.rb +86 -25
- data/spec/lib/saml_idp/encryptor_spec.rb +4 -4
- data/spec/lib/saml_idp/fingerprint_spec.rb +14 -0
- data/spec/lib/saml_idp/incoming_metadata_spec.rb +134 -0
- data/spec/lib/saml_idp/metadata_builder_spec.rb +30 -17
- data/spec/lib/saml_idp/name_id_formatter_spec.rb +3 -3
- data/spec/lib/saml_idp/request_spec.rb +153 -64
- data/spec/lib/saml_idp/response_builder_spec.rb +5 -3
- data/spec/lib/saml_idp/saml_response_spec.rb +146 -12
- data/spec/lib/saml_idp/service_provider_spec.rb +2 -2
- data/spec/lib/saml_idp/signable_spec.rb +1 -1
- data/spec/lib/saml_idp/signature_builder_spec.rb +2 -2
- data/spec/lib/saml_idp/signed_info_builder_spec.rb +3 -3
- data/spec/rails_app/app/controllers/saml_controller.rb +1 -1
- 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 +3 -4
- data/{app → spec/rails_app/app}/views/saml_idp/idp/saml_post.html.erb +1 -1
- data/spec/rails_app/config/application.rb +1 -6
- data/spec/rails_app/config/boot.rb +1 -1
- data/spec/rails_app/config/environments/development.rb +2 -5
- data/spec/rails_app/config/environments/production.rb +1 -0
- data/spec/rails_app/config/environments/test.rb +1 -0
- data/spec/spec_helper.rb +23 -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 +107 -5
- data/spec/support/security_helpers.rb +12 -2
- data/spec/xml_security_spec.rb +19 -15
- metadata +146 -80
- data/app/controllers/saml_idp/idp_controller.rb +0 -59
- data/spec/lib/saml_idp/.assertion_builder_spec.rb.swp +0 -0
data/lib/saml_idp/request.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
require 'saml_idp/xml_security'
|
|
2
2
|
require 'saml_idp/service_provider'
|
|
3
|
+
require 'logger'
|
|
3
4
|
module SamlIdp
|
|
4
5
|
class Request
|
|
5
|
-
|
|
6
|
+
attr_accessor :errors
|
|
7
|
+
|
|
8
|
+
def self.from_deflated_request(raw, external_attributes = {})
|
|
6
9
|
if raw
|
|
7
10
|
decoded = Base64.decode64(raw)
|
|
8
11
|
zstream = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
|
@@ -17,18 +20,23 @@ module SamlIdp
|
|
|
17
20
|
else
|
|
18
21
|
inflated = ""
|
|
19
22
|
end
|
|
20
|
-
new(inflated)
|
|
23
|
+
new(inflated, external_attributes)
|
|
21
24
|
end
|
|
22
25
|
|
|
23
|
-
attr_accessor :raw_xml
|
|
26
|
+
attr_accessor :raw_xml, :saml_request, :signature, :sig_algorithm, :relay_state
|
|
24
27
|
|
|
25
28
|
delegate :config, to: :SamlIdp
|
|
26
29
|
private :config
|
|
27
30
|
delegate :xpath, to: :document
|
|
28
31
|
private :xpath
|
|
29
32
|
|
|
30
|
-
def initialize(raw_xml = "")
|
|
33
|
+
def initialize(raw_xml = "", external_attributes = {})
|
|
31
34
|
self.raw_xml = raw_xml
|
|
35
|
+
self.saml_request = external_attributes[:saml_request]
|
|
36
|
+
self.relay_state = external_attributes[:relay_state]
|
|
37
|
+
self.sig_algorithm = external_attributes[:sig_algorithm]
|
|
38
|
+
self.signature = external_attributes[:signature]
|
|
39
|
+
self.errors = []
|
|
32
40
|
end
|
|
33
41
|
|
|
34
42
|
def logout_request?
|
|
@@ -77,31 +85,60 @@ module SamlIdp
|
|
|
77
85
|
end
|
|
78
86
|
|
|
79
87
|
def log(msg)
|
|
80
|
-
if
|
|
81
|
-
|
|
88
|
+
if config.logger.respond_to?(:call)
|
|
89
|
+
config.logger.call msg
|
|
82
90
|
else
|
|
83
|
-
|
|
91
|
+
config.logger.info msg
|
|
84
92
|
end
|
|
85
93
|
end
|
|
86
94
|
|
|
87
|
-
def
|
|
95
|
+
def collect_errors(error_type)
|
|
96
|
+
errors.push(error_type)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def valid?(external_attributes = {})
|
|
88
100
|
unless service_provider?
|
|
89
101
|
log "Unable to find service provider for issuer #{issuer}"
|
|
102
|
+
collect_errors(:sp_not_found)
|
|
90
103
|
return false
|
|
91
104
|
end
|
|
92
105
|
|
|
93
106
|
unless (authn_request? ^ logout_request?)
|
|
94
107
|
log "One and only one of authnrequest and logout request is required. authnrequest: #{authn_request?} logout_request: #{logout_request?} "
|
|
108
|
+
collect_errors(:unaccepted_request)
|
|
109
|
+
return false
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if (logout_request? || validate_auth_request_signature?) && (service_provider.cert.to_s.empty? || !!service_provider.fingerprint.to_s.empty?)
|
|
113
|
+
log "Verifying request signature is required. But certificate and fingerprint was empty."
|
|
114
|
+
collect_errors(:empty_certificate)
|
|
95
115
|
return false
|
|
96
116
|
end
|
|
97
117
|
|
|
98
|
-
|
|
99
|
-
|
|
118
|
+
# XML embedded signature
|
|
119
|
+
if signature.nil? && !valid_signature?
|
|
120
|
+
log "Requested document signature is invalid in #{raw_xml}"
|
|
121
|
+
collect_errors(:invalid_embedded_signature)
|
|
122
|
+
return false
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# URI query signature
|
|
126
|
+
if signature.present? && !valid_external_signature?
|
|
127
|
+
log "Requested URI signature is invalid in #{raw_xml}"
|
|
128
|
+
collect_errors(:invalid_external_signature)
|
|
100
129
|
return false
|
|
101
130
|
end
|
|
102
131
|
|
|
103
132
|
if response_url.nil?
|
|
104
133
|
log "Unable to find response url for #{issuer}: #{raw_xml}"
|
|
134
|
+
collect_errors(:empty_response_url)
|
|
135
|
+
return false
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
if !service_provider.acceptable_response_hosts.include?(response_host)
|
|
139
|
+
log "#{service_provider.acceptable_response_hosts} compare to #{response_host}"
|
|
140
|
+
log "No acceptable AssertionConsumerServiceURL, either configure them via config.service_provider.response_hosts or match to your metadata_url host"
|
|
141
|
+
collect_errors(:not_allowed_host)
|
|
105
142
|
return false
|
|
106
143
|
end
|
|
107
144
|
|
|
@@ -109,9 +146,38 @@ module SamlIdp
|
|
|
109
146
|
end
|
|
110
147
|
|
|
111
148
|
def valid_signature?
|
|
112
|
-
# Force signatures for logout requests because there is no other
|
|
113
|
-
|
|
114
|
-
|
|
149
|
+
# Force signatures for logout requests because there is no other protection against a cross-site DoS.
|
|
150
|
+
if logout_request? || authn_request? && validate_auth_request_signature?
|
|
151
|
+
document.valid_signature?(service_provider.cert, service_provider.fingerprint)
|
|
152
|
+
else
|
|
153
|
+
true
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def valid_external_signature?
|
|
158
|
+
return true if authn_request? && !validate_auth_request_signature?
|
|
159
|
+
|
|
160
|
+
cert = OpenSSL::X509::Certificate.new(service_provider.cert)
|
|
161
|
+
|
|
162
|
+
sha_version = sig_algorithm =~ /sha(.*?)$/i && $1.to_i
|
|
163
|
+
raw_signature = Base64.decode64(signature)
|
|
164
|
+
|
|
165
|
+
signature_algorithm = case sha_version
|
|
166
|
+
when 256 then OpenSSL::Digest::SHA256
|
|
167
|
+
when 384 then OpenSSL::Digest::SHA384
|
|
168
|
+
when 512 then OpenSSL::Digest::SHA512
|
|
169
|
+
else
|
|
170
|
+
OpenSSL::Digest::SHA1
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
result = cert.public_key.verify(signature_algorithm.new, raw_signature, query_request_string)
|
|
174
|
+
# Match all percent-encoded sequences (e.g., %20, %2B) and convert them to lowercase
|
|
175
|
+
# Upper case is recommended for consistency but some services such as MS Entra Id not follows it
|
|
176
|
+
# https://datatracker.ietf.org/doc/html/rfc3986#section-2.1
|
|
177
|
+
result || cert.public_key.verify(signature_algorithm.new, raw_signature, query_request_string.gsub(/%[A-F0-9]{2}/) { |match| match.downcase })
|
|
178
|
+
rescue OpenSSL::X509::CertificateError => e
|
|
179
|
+
log e.message
|
|
180
|
+
collect_errors(:cert_format_error)
|
|
115
181
|
end
|
|
116
182
|
|
|
117
183
|
def service_provider?
|
|
@@ -136,6 +202,21 @@ module SamlIdp
|
|
|
136
202
|
@_session_index ||= xpath("//samlp:SessionIndex", samlp: samlp).first.try(:content)
|
|
137
203
|
end
|
|
138
204
|
|
|
205
|
+
def query_request_string
|
|
206
|
+
url_string = "SAMLRequest=#{CGI.escape(saml_request)}"
|
|
207
|
+
url_string << "&RelayState=#{CGI.escape(relay_state)}" if relay_state
|
|
208
|
+
url_string << "&SigAlg=#{CGI.escape(sig_algorithm)}"
|
|
209
|
+
end
|
|
210
|
+
private :query_request_string
|
|
211
|
+
|
|
212
|
+
def response_host
|
|
213
|
+
uri = URI(response_url)
|
|
214
|
+
if uri
|
|
215
|
+
uri.host
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
private :response_host
|
|
219
|
+
|
|
139
220
|
def document
|
|
140
221
|
@_document ||= Saml::XML::Document.parse(raw_xml)
|
|
141
222
|
end
|
|
@@ -177,5 +258,14 @@ module SamlIdp
|
|
|
177
258
|
config.service_provider.finder
|
|
178
259
|
end
|
|
179
260
|
private :service_provider_finder
|
|
261
|
+
|
|
262
|
+
def validate_auth_request_signature?
|
|
263
|
+
# Validate signature when metadata specify AuthnRequest should be signed
|
|
264
|
+
metadata = service_provider.current_metadata
|
|
265
|
+
sign_authn_request = metadata.respond_to?(:sign_authn_request?) && metadata.sign_authn_request?
|
|
266
|
+
sign_authn_request = service_provider.sign_authn_request unless service_provider.sign_authn_request.nil?
|
|
267
|
+
sign_authn_request
|
|
268
|
+
end
|
|
269
|
+
private :validate_auth_request_signature?
|
|
180
270
|
end
|
|
181
271
|
end
|
|
@@ -1,32 +1,45 @@
|
|
|
1
1
|
require 'builder'
|
|
2
|
+
require 'saml_idp/algorithmable'
|
|
3
|
+
require 'saml_idp/signable'
|
|
2
4
|
module SamlIdp
|
|
3
5
|
class ResponseBuilder
|
|
6
|
+
include Algorithmable
|
|
7
|
+
include Signable
|
|
4
8
|
attr_accessor :response_id
|
|
5
9
|
attr_accessor :issuer_uri
|
|
6
10
|
attr_accessor :saml_acs_url
|
|
7
11
|
attr_accessor :saml_request_id
|
|
8
12
|
attr_accessor :assertion_and_signature
|
|
13
|
+
attr_accessor :raw_algorithm
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
alias_method :reference_id, :response_id
|
|
16
|
+
|
|
17
|
+
def initialize(response_id, issuer_uri, saml_acs_url, saml_request_id, assertion_and_signature, raw_algorithm)
|
|
11
18
|
self.response_id = response_id
|
|
12
19
|
self.issuer_uri = issuer_uri
|
|
13
20
|
self.saml_acs_url = saml_acs_url
|
|
14
21
|
self.saml_request_id = saml_request_id
|
|
15
22
|
self.assertion_and_signature = assertion_and_signature
|
|
23
|
+
self.raw_algorithm = raw_algorithm
|
|
16
24
|
end
|
|
17
25
|
|
|
18
|
-
def encoded
|
|
19
|
-
@encoded ||=
|
|
26
|
+
def encoded(signed_message: false, compress: false)
|
|
27
|
+
@encoded ||= signed_message ? encode_signed_message(compress) : encode_raw_message(compress)
|
|
20
28
|
end
|
|
21
29
|
|
|
22
30
|
def raw
|
|
23
31
|
build
|
|
24
32
|
end
|
|
25
33
|
|
|
26
|
-
def
|
|
27
|
-
Base64.strict_encode64(raw)
|
|
34
|
+
def encode_raw_message(compress)
|
|
35
|
+
Base64.strict_encode64(compress ? deflate(raw) : raw)
|
|
36
|
+
end
|
|
37
|
+
private :encode_raw_message
|
|
38
|
+
|
|
39
|
+
def encode_signed_message(compress)
|
|
40
|
+
Base64.strict_encode64(compress ? deflate(signed) : signed)
|
|
28
41
|
end
|
|
29
|
-
private :
|
|
42
|
+
private :encode_signed_message
|
|
30
43
|
|
|
31
44
|
def build
|
|
32
45
|
resp_options = {}
|
|
@@ -41,6 +54,7 @@ module SamlIdp
|
|
|
41
54
|
builder = Builder::XmlMarkup.new
|
|
42
55
|
builder.tag! "samlp:Response", resp_options do |response|
|
|
43
56
|
response.Issuer issuer_uri, xmlns: Saml::XML::Namespaces::ASSERTION
|
|
57
|
+
sign response
|
|
44
58
|
response.tag! "samlp:Status" do |status|
|
|
45
59
|
status.tag! "samlp:StatusCode", Value: Saml::XML::Namespaces::Statuses::SUCCESS
|
|
46
60
|
end
|
|
@@ -52,11 +66,17 @@ module SamlIdp
|
|
|
52
66
|
def response_id_string
|
|
53
67
|
"_#{response_id}"
|
|
54
68
|
end
|
|
69
|
+
alias_method :reference_id, :response_id
|
|
55
70
|
private :response_id_string
|
|
56
71
|
|
|
57
72
|
def now_iso
|
|
58
73
|
Time.now.utc.iso8601
|
|
59
74
|
end
|
|
60
75
|
private :now_iso
|
|
76
|
+
|
|
77
|
+
def deflate(inflated)
|
|
78
|
+
Zlib::Deflate.deflate(inflated, 9)[2..-5]
|
|
79
|
+
end
|
|
80
|
+
private :deflate
|
|
61
81
|
end
|
|
62
82
|
end
|
|
@@ -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(reference_id || 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
|
|
@@ -11,8 +11,10 @@ module SamlIdp
|
|
|
11
11
|
attribute :fingerprint
|
|
12
12
|
attribute :metadata_url
|
|
13
13
|
attribute :validate_signature
|
|
14
|
+
attribute :sign_authn_request
|
|
14
15
|
attribute :acs_url
|
|
15
16
|
attribute :assertion_consumer_logout_service_url
|
|
17
|
+
attribute :response_hosts
|
|
16
18
|
|
|
17
19
|
delegate :config, to: :SamlIdp
|
|
18
20
|
|
|
@@ -21,18 +23,13 @@ module SamlIdp
|
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def valid_signature?(doc, require_signature = false)
|
|
24
|
-
if require_signature ||
|
|
26
|
+
if require_signature || attributes[:validate_signature]
|
|
25
27
|
doc.valid_signature?(fingerprint)
|
|
26
28
|
else
|
|
27
29
|
true
|
|
28
30
|
end
|
|
29
31
|
end
|
|
30
32
|
|
|
31
|
-
def should_validate_signature?
|
|
32
|
-
attributes[:validate_signature] ||
|
|
33
|
-
current_metadata.respond_to?(:sign_assertions?) && current_metadata.sign_assertions?
|
|
34
|
-
end
|
|
35
|
-
|
|
36
33
|
def refresh_metadata
|
|
37
34
|
fresh = fresh_incoming_metadata
|
|
38
35
|
if valid_signature?(fresh.document)
|
|
@@ -46,6 +43,19 @@ module SamlIdp
|
|
|
46
43
|
@current_metadata ||= get_current_or_build
|
|
47
44
|
end
|
|
48
45
|
|
|
46
|
+
def acceptable_response_hosts
|
|
47
|
+
hosts = Array(self.response_hosts)
|
|
48
|
+
hosts.push(metadata_url_host) if metadata_url_host
|
|
49
|
+
|
|
50
|
+
hosts
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def metadata_url_host
|
|
54
|
+
if metadata_url.present?
|
|
55
|
+
URI(metadata_url).host
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
49
59
|
def get_current_or_build
|
|
50
60
|
persisted = metadata_getter[identifier, self]
|
|
51
61
|
if persisted.is_a? Hash
|
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
|
|
@@ -21,7 +21,8 @@ module SamlIdp
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def x509_certificate
|
|
24
|
-
SamlIdp.config.x509_certificate
|
|
24
|
+
certificate = SamlIdp.config.x509_certificate.is_a?(Proc) ? SamlIdp.config.x509_certificate.call : SamlIdp.config.x509_certificate
|
|
25
|
+
certificate
|
|
25
26
|
.to_s
|
|
26
27
|
.gsub(/-----BEGIN CERTIFICATE-----/,"")
|
|
27
28
|
.gsub(/-----END CERTIFICATE-----/,"")
|
|
@@ -65,12 +65,12 @@ module SamlIdp
|
|
|
65
65
|
private :clean_algorithm_name
|
|
66
66
|
|
|
67
67
|
def secret_key
|
|
68
|
-
SamlIdp.config.secret_key
|
|
68
|
+
SamlIdp.config.secret_key.is_a?(Proc) ? SamlIdp.config.secret_key.call : SamlIdp.config.secret_key
|
|
69
69
|
end
|
|
70
70
|
private :secret_key
|
|
71
71
|
|
|
72
72
|
def password
|
|
73
|
-
SamlIdp.config.password
|
|
73
|
+
SamlIdp.config.password.is_a?(Proc) ? SamlIdp.config.password.call : SamlIdp.config.password
|
|
74
74
|
end
|
|
75
75
|
private :password
|
|
76
76
|
|
data/lib/saml_idp/version.rb
CHANGED
|
@@ -43,24 +43,29 @@ module SamlIdp
|
|
|
43
43
|
extract_signed_element_id
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
def validate(idp_cert_fingerprint, soft = true)
|
|
46
|
+
def validate(idp_base64_cert, idp_cert_fingerprint, soft = true)
|
|
47
47
|
# get cert from response
|
|
48
48
|
cert_element = REXML::XPath.first(self, "//ds:X509Certificate", { "ds"=>DSIG })
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
if cert_element
|
|
50
|
+
idp_base64_cert = cert_element.text
|
|
51
|
+
cert_text = Base64.decode64(idp_base64_cert)
|
|
52
|
+
cert = OpenSSL::X509::Certificate.new(cert_text)
|
|
53
|
+
|
|
54
|
+
# check cert matches registered idp cert
|
|
55
|
+
fingerprint = fingerprint_cert(cert)
|
|
56
|
+
sha1_fingerprint = fingerprint_cert_sha1(cert)
|
|
57
|
+
plain_idp_cert_fingerprint = idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
|
|
58
|
+
|
|
59
|
+
if fingerprint != plain_idp_cert_fingerprint && sha1_fingerprint != plain_idp_cert_fingerprint
|
|
60
|
+
return soft ? false : (raise ValidationError.new("Fingerprint mismatch"))
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if idp_base64_cert.nil? || idp_base64_cert.empty?
|
|
65
|
+
raise ValidationError.new("Certificate validation is required, but it doesn't exist.")
|
|
61
66
|
end
|
|
62
67
|
|
|
63
|
-
validate_doc(
|
|
68
|
+
validate_doc(idp_base64_cert, soft)
|
|
64
69
|
end
|
|
65
70
|
|
|
66
71
|
def fingerprint_cert(cert)
|
|
@@ -108,7 +113,7 @@ module SamlIdp
|
|
|
108
113
|
canon_algorithm = canon_algorithm REXML::XPath.first(ref, '//ds:CanonicalizationMethod', 'ds' => DSIG)
|
|
109
114
|
canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
|
|
110
115
|
|
|
111
|
-
digest_algorithm = algorithm(REXML::XPath.first(ref, "//ds:DigestMethod"))
|
|
116
|
+
digest_algorithm = algorithm(REXML::XPath.first(ref, "//ds:DigestMethod", {'ds' => DSIG}))
|
|
112
117
|
|
|
113
118
|
hash = digest_algorithm.digest(canon_hashed_element)
|
|
114
119
|
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::Engine)
|
|
12
13
|
|
|
13
14
|
def self.config
|
|
14
15
|
@config ||= SamlIdp::Configurator.new
|
|
@@ -69,9 +70,9 @@ module Saml
|
|
|
69
70
|
!!xpath("//ds:Signature", ds: signature_namespace).first
|
|
70
71
|
end
|
|
71
72
|
|
|
72
|
-
def valid_signature?(fingerprint)
|
|
73
|
+
def valid_signature?(certificate, fingerprint)
|
|
73
74
|
signed? &&
|
|
74
|
-
signed_document.validate(fingerprint, :soft)
|
|
75
|
+
signed_document.validate(certificate, fingerprint, :soft)
|
|
75
76
|
end
|
|
76
77
|
|
|
77
78
|
def signed_document
|
data/saml_idp.gemspec
CHANGED
|
@@ -1,59 +1,63 @@
|
|
|
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.email =
|
|
11
|
-
s.homepage =
|
|
12
|
-
s.summary =
|
|
13
|
-
s.description =
|
|
14
|
-
s.date = Time.now.utc.strftime(
|
|
15
|
-
s.files = Dir
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"Gemfile",
|
|
19
|
-
"saml_idp.gemspec"
|
|
20
|
-
]
|
|
21
|
-
s.required_ruby_version = '>= 2.2'
|
|
22
|
-
s.license = "LICENSE"
|
|
10
|
+
s.authors = ['Jon Phenow']
|
|
11
|
+
s.email = 'jon.phenow@sportngin.com'
|
|
12
|
+
s.homepage = 'https://github.com/saml-idp/saml_idp'
|
|
13
|
+
s.summary = 'SAML Identity Provider for Ruby'
|
|
14
|
+
s.description = 'SAML IdP (Identity Provider) Library for Ruby'
|
|
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'
|
|
18
|
+
s.license = 'MIT'
|
|
23
19
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
24
20
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
25
|
-
s.require_paths = [
|
|
26
|
-
s.rdoc_options = [
|
|
21
|
+
s.require_paths = ['lib']
|
|
22
|
+
s.rdoc_options = ['--charset=UTF-8']
|
|
23
|
+
s.metadata = {
|
|
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',
|
|
27
|
+
'documentation_uri' => "http://rdoc.info/gems/saml_idp/#{SamlIdp::VERSION}"
|
|
28
|
+
}
|
|
27
29
|
|
|
28
30
|
s.post_install_message = <<-INST
|
|
29
|
-
If you're just recently updating saml_idp - please be aware we've changed the default
|
|
30
|
-
certificate. See the PR and a description of why we've done this here:
|
|
31
|
-
https://github.com/
|
|
32
|
-
|
|
33
|
-
If you just need to see the certificate `bundle open saml_idp` and go to
|
|
34
|
-
`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
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
If you just need to see the certificate `bundle open saml_idp` and go to
|
|
36
|
+
`lib/saml_idp/default.rb`
|
|
38
37
|
|
|
39
|
-
|
|
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.
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
43
44
|
INST
|
|
44
45
|
|
|
45
|
-
s.add_dependency('activesupport', '>=
|
|
46
|
-
s.add_dependency('
|
|
47
|
-
s.add_dependency('builder', '~> 3.0')
|
|
46
|
+
s.add_dependency('activesupport', '>= 5.2')
|
|
47
|
+
s.add_dependency('builder', '>= 3.0')
|
|
48
48
|
s.add_dependency('nokogiri', '>= 1.6.2')
|
|
49
|
-
|
|
50
|
-
s.
|
|
51
|
-
s.
|
|
52
|
-
|
|
53
|
-
s.add_development_dependency('
|
|
54
|
-
s.add_development_dependency('
|
|
55
|
-
s.add_development_dependency('capybara', '
|
|
56
|
-
s.add_development_dependency('
|
|
57
|
-
s.add_development_dependency('
|
|
49
|
+
s.add_dependency('ostruct')
|
|
50
|
+
s.add_dependency('rexml')
|
|
51
|
+
s.add_dependency('xmlenc', '>= 0.7.1')
|
|
52
|
+
|
|
53
|
+
s.add_development_dependency('activeresource', '~> 6.1')
|
|
54
|
+
s.add_development_dependency('appraisal')
|
|
55
|
+
s.add_development_dependency('capybara', '>= 2.16')
|
|
56
|
+
s.add_development_dependency('rails', '>= 5.2')
|
|
57
|
+
s.add_development_dependency('debug')
|
|
58
|
+
s.add_development_dependency('rake')
|
|
59
|
+
s.add_development_dependency('rspec', '>= 3.7.0')
|
|
60
|
+
s.add_development_dependency('ruby-saml', '>= 1.7.2')
|
|
61
|
+
s.add_development_dependency('simplecov')
|
|
62
|
+
s.add_development_dependency('timecop', '>= 0.8')
|
|
58
63
|
end
|
|
59
|
-
|