saml_idp 0.15.0 → 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 +4 -4
- data/README.md +12 -4
- data/lib/saml_idp/configurator.rb +3 -3
- data/lib/saml_idp/controller.rb +12 -9
- data/lib/saml_idp/incoming_metadata.rb +9 -0
- data/lib/saml_idp/metadata_builder.rb +2 -1
- data/lib/saml_idp/request.rb +84 -14
- data/lib/saml_idp/saml_response.rb +2 -2
- data/lib/saml_idp/service_provider.rb +1 -0
- 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 +19 -14
- data/lib/saml_idp.rb +3 -3
- data/saml_idp.gemspec +4 -3
- data/spec/lib/saml_idp/configurator_spec.rb +38 -2
- data/spec/lib/saml_idp/controller_spec.rb +43 -9
- data/spec/lib/saml_idp/incoming_metadata_spec.rb +75 -1
- data/spec/lib/saml_idp/metadata_builder_spec.rb +1 -1
- data/spec/lib/saml_idp/request_spec.rb +152 -97
- data/spec/lib/saml_idp/saml_response_spec.rb +19 -0
- data/spec/rails_app/app/views/saml_idp/idp/new.html.erb +3 -0
- data/spec/support/saml_request_macros.rb +60 -18
- data/spec/support/security_helpers.rb +2 -2
- data/spec/xml_security_spec.rb +11 -7
- metadata +32 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4a0fd46437c04823ad86f269552282b0252cb888294345984c5e16b88bcfa9cf
|
4
|
+
data.tar.gz: b7c56c2516f7aaf392fe5a999e98f67af0b79f799374757f3724724605616fc4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 558df23c6ffefed06a205df4ea72cc530f7a963c64741e0bb86cae16f7ffd26a822299793a6b4d7a587addf49428a71e3449f46650d1a26990ec563b690a6cac
|
7
|
+
data.tar.gz: ee6eec4ee019c3e01443bbc6d383e4eb6dcd422e3eb5dddcbefd10666799d9604e094e9dec22b213780a415f9efaaebfe54f8b512fef90322f6ce3cc89e4d751
|
data/README.md
CHANGED
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
Forked from <https://github.com/lawrencepit/ruby-saml-idp>
|
4
4
|
|
5
|
-
[](https://travis-ci.org/saml-idp/saml_idp)
|
6
5
|
[](http://badge.fury.io/rb/saml_idp)
|
7
6
|
|
8
7
|
The ruby SAML Identity Provider library is for implementing the server side of SAML authentication. It allows
|
@@ -25,9 +24,13 @@ Add this to your Gemfile:
|
|
25
24
|
|
26
25
|
Include `SamlIdp::Controller` and see the examples that use rails. It should be straightforward for you.
|
27
26
|
|
28
|
-
Basically you call `decode_request(params[:SAMLRequest])` on an incoming request and then use the value
|
29
|
-
`saml_acs_url` to determine the source for which you need to authenticate a user.
|
30
|
-
|
27
|
+
Basically, you call `decode_request(params[:SAMLRequest])` on an incoming request and then use the value
|
28
|
+
`saml_acs_url` to determine the source for which you need to authenticate a user.
|
29
|
+
If the signature (`Signature`) and signing algorithm (`SigAlg`) are provided as external parameters in the request,
|
30
|
+
you can pass those parameters as `decode_request(params[:SAMLRequest], params[:Signature], params[:SigAlg], params[:RelayState])`.
|
31
|
+
Then, you can verify the request signature with the `valid?` method.
|
32
|
+
|
33
|
+
How you authenticate a user is entirely up to you.
|
31
34
|
|
32
35
|
Once a user has successfully authenticated on your system send the Service Provider a SAMLResponse by
|
33
36
|
posting to `saml_acs_url` the parameter `SAMLResponse` with the return value from a call to
|
@@ -75,6 +78,11 @@ KEY DATA
|
|
75
78
|
-----END RSA PRIVATE KEY-----
|
76
79
|
CERT
|
77
80
|
|
81
|
+
# x509_certificate, secret_key, and password may also be set from within a proc, for example:
|
82
|
+
# config.x509_certificate = -> { File.read("cert.pem") }
|
83
|
+
# config.secret_key = -> { SecretKeyFinder.key_for(id: 1) }
|
84
|
+
# config.password = -> { "password" }
|
85
|
+
|
78
86
|
# config.password = "secret_key_password"
|
79
87
|
# config.algorithm = :sha256 # Default: sha1 only for development.
|
80
88
|
# config.organization_name = "Your Organization"
|
@@ -25,8 +25,8 @@ module SamlIdp
|
|
25
25
|
attr_accessor :logger
|
26
26
|
|
27
27
|
def initialize
|
28
|
-
self.x509_certificate = Default::X509_CERTIFICATE
|
29
|
-
self.secret_key = Default::SECRET_KEY
|
28
|
+
self.x509_certificate = -> { Default::X509_CERTIFICATE }
|
29
|
+
self.secret_key = -> { Default::SECRET_KEY }
|
30
30
|
self.algorithm = :sha1
|
31
31
|
self.reference_id_generator = ->() { SecureRandom.uuid }
|
32
32
|
self.service_provider = OpenStruct.new
|
@@ -35,7 +35,7 @@ module SamlIdp
|
|
35
35
|
self.service_provider.persisted_metadata_getter = ->(id, service_provider) { }
|
36
36
|
self.session_expiry = 0
|
37
37
|
self.attributes = {}
|
38
|
-
self.logger = defined?(::Rails) ? Rails.logger : ->(msg) { puts msg }
|
38
|
+
self.logger = (defined?(::Rails) && Rails.respond_to?(:logger)) ? Rails.logger : ->(msg) { puts msg }
|
39
39
|
end
|
40
40
|
|
41
41
|
# formats
|
data/lib/saml_idp/controller.rb
CHANGED
@@ -33,15 +33,18 @@ module SamlIdp
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def validate_saml_request(raw_saml_request = params[:SAMLRequest])
|
36
|
-
decode_request(raw_saml_request)
|
37
|
-
|
38
|
-
|
39
|
-
head :forbidden if defined?(::Rails)
|
40
|
-
false
|
36
|
+
decode_request(raw_saml_request, params[:Signature], params[:SigAlg], params[:RelayState])
|
37
|
+
valid_saml_request?
|
41
38
|
end
|
42
39
|
|
43
|
-
def decode_request(raw_saml_request)
|
44
|
-
@saml_request = Request.from_deflated_request(
|
40
|
+
def decode_request(raw_saml_request, signature, sig_algorithm, relay_state)
|
41
|
+
@saml_request = Request.from_deflated_request(
|
42
|
+
raw_saml_request,
|
43
|
+
saml_request: raw_saml_request,
|
44
|
+
signature: signature,
|
45
|
+
sig_algorithm: sig_algorithm,
|
46
|
+
relay_state: relay_state
|
47
|
+
)
|
45
48
|
end
|
46
49
|
|
47
50
|
def authn_context_classref
|
@@ -63,7 +66,7 @@ module SamlIdp
|
|
63
66
|
signed_message_opts = opts[:signed_message] || false
|
64
67
|
name_id_formats_opts = opts[:name_id_formats] || nil
|
65
68
|
asserted_attributes_opts = opts[:attributes] || nil
|
66
|
-
signed_assertion_opts = opts[:signed_assertion]
|
69
|
+
signed_assertion_opts = opts[:signed_assertion].nil? ? true : opts[:signed_assertion]
|
67
70
|
compress_opts = opts[:compress] || false
|
68
71
|
|
69
72
|
SamlResponse.new(
|
@@ -81,8 +84,8 @@ module SamlIdp
|
|
81
84
|
session_expiry,
|
82
85
|
name_id_formats_opts,
|
83
86
|
asserted_attributes_opts,
|
84
|
-
signed_assertion_opts,
|
85
87
|
signed_message_opts,
|
88
|
+
signed_assertion_opts,
|
86
89
|
compress_opts
|
87
90
|
).build
|
88
91
|
end
|
@@ -63,6 +63,15 @@ module SamlIdp
|
|
63
63
|
end
|
64
64
|
hashable :contact_person
|
65
65
|
|
66
|
+
def unspecified_certificate
|
67
|
+
xpath(
|
68
|
+
"//md:SPSSODescriptor/md:KeyDescriptor[not(@use)]/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
|
69
|
+
ds: signature_namespace,
|
70
|
+
md: metadata_namespace
|
71
|
+
).first.try(:content).to_s
|
72
|
+
end
|
73
|
+
hashable :unspecified_certificate
|
74
|
+
|
66
75
|
def signing_certificate
|
67
76
|
xpath(
|
68
77
|
"//md:SPSSODescriptor/md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
|
@@ -152,7 +152,8 @@ module SamlIdp
|
|
152
152
|
private :raw_algorithm
|
153
153
|
|
154
154
|
def x509_certificate
|
155
|
-
SamlIdp.config.x509_certificate
|
155
|
+
certificate = SamlIdp.config.x509_certificate.is_a?(Proc) ? SamlIdp.config.x509_certificate.call : SamlIdp.config.x509_certificate
|
156
|
+
certificate
|
156
157
|
.to_s
|
157
158
|
.gsub(/-----BEGIN CERTIFICATE-----/,"")
|
158
159
|
.gsub(/-----END CERTIFICATE-----/,"")
|
data/lib/saml_idp/request.rb
CHANGED
@@ -3,7 +3,9 @@ require 'saml_idp/service_provider'
|
|
3
3
|
require 'logger'
|
4
4
|
module SamlIdp
|
5
5
|
class Request
|
6
|
-
|
6
|
+
attr_accessor :errors
|
7
|
+
|
8
|
+
def self.from_deflated_request(raw, external_attributes = {})
|
7
9
|
if raw
|
8
10
|
decoded = Base64.decode64(raw)
|
9
11
|
zstream = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
@@ -18,18 +20,23 @@ module SamlIdp
|
|
18
20
|
else
|
19
21
|
inflated = ""
|
20
22
|
end
|
21
|
-
new(inflated)
|
23
|
+
new(inflated, external_attributes)
|
22
24
|
end
|
23
25
|
|
24
|
-
attr_accessor :raw_xml
|
26
|
+
attr_accessor :raw_xml, :saml_request, :signature, :sig_algorithm, :relay_state
|
25
27
|
|
26
28
|
delegate :config, to: :SamlIdp
|
27
29
|
private :config
|
28
30
|
delegate :xpath, to: :document
|
29
31
|
private :xpath
|
30
32
|
|
31
|
-
def initialize(raw_xml = "")
|
33
|
+
def initialize(raw_xml = "", external_attributes = {})
|
32
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 = []
|
33
40
|
end
|
34
41
|
|
35
42
|
def logout_request?
|
@@ -78,37 +85,60 @@ module SamlIdp
|
|
78
85
|
end
|
79
86
|
|
80
87
|
def log(msg)
|
81
|
-
if config.logger.
|
82
|
-
config.logger.info msg
|
83
|
-
else
|
88
|
+
if config.logger.respond_to?(:call)
|
84
89
|
config.logger.call msg
|
90
|
+
else
|
91
|
+
config.logger.info msg
|
85
92
|
end
|
86
93
|
end
|
87
94
|
|
88
|
-
def
|
95
|
+
def collect_errors(error_type)
|
96
|
+
errors.push(error_type)
|
97
|
+
end
|
98
|
+
|
99
|
+
def valid?(external_attributes = {})
|
89
100
|
unless service_provider?
|
90
101
|
log "Unable to find service provider for issuer #{issuer}"
|
102
|
+
collect_errors(:sp_not_found)
|
91
103
|
return false
|
92
104
|
end
|
93
105
|
|
94
106
|
unless (authn_request? ^ logout_request?)
|
95
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)
|
96
109
|
return false
|
97
110
|
end
|
98
111
|
|
99
|
-
|
100
|
-
log "
|
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)
|
115
|
+
return false
|
116
|
+
end
|
117
|
+
|
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)
|
101
129
|
return false
|
102
130
|
end
|
103
131
|
|
104
132
|
if response_url.nil?
|
105
133
|
log "Unable to find response url for #{issuer}: #{raw_xml}"
|
134
|
+
collect_errors(:empty_response_url)
|
106
135
|
return false
|
107
136
|
end
|
108
137
|
|
109
138
|
if !service_provider.acceptable_response_hosts.include?(response_host)
|
110
139
|
log "#{service_provider.acceptable_response_hosts} compare to #{response_host}"
|
111
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)
|
112
142
|
return false
|
113
143
|
end
|
114
144
|
|
@@ -117,15 +147,39 @@ module SamlIdp
|
|
117
147
|
|
118
148
|
def valid_signature?
|
119
149
|
# Force signatures for logout requests because there is no other protection against a cross-site DoS.
|
120
|
-
|
121
|
-
|
122
|
-
if logout_request? || authn_request? && metadata.respond_to?(:sign_authn_request?) && metadata.sign_authn_request?
|
123
|
-
document.valid_signature?(service_provider.fingerprint)
|
150
|
+
if logout_request? || authn_request? && validate_auth_request_signature?
|
151
|
+
document.valid_signature?(service_provider.cert, service_provider.fingerprint)
|
124
152
|
else
|
125
153
|
true
|
126
154
|
end
|
127
155
|
end
|
128
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)
|
181
|
+
end
|
182
|
+
|
129
183
|
def service_provider?
|
130
184
|
service_provider && service_provider.valid?
|
131
185
|
end
|
@@ -148,6 +202,13 @@ module SamlIdp
|
|
148
202
|
@_session_index ||= xpath("//samlp:SessionIndex", samlp: samlp).first.try(:content)
|
149
203
|
end
|
150
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
|
+
|
151
212
|
def response_host
|
152
213
|
uri = URI(response_url)
|
153
214
|
if uri
|
@@ -197,5 +258,14 @@ module SamlIdp
|
|
197
258
|
config.service_provider.finder
|
198
259
|
end
|
199
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?
|
200
270
|
end
|
201
271
|
end
|
@@ -98,7 +98,7 @@ module SamlIdp
|
|
98
98
|
|
99
99
|
def assertion_builder
|
100
100
|
@assertion_builder ||=
|
101
|
-
AssertionBuilder.new SecureRandom.uuid,
|
101
|
+
AssertionBuilder.new(reference_id || SecureRandom.uuid,
|
102
102
|
issuer_uri,
|
103
103
|
principal,
|
104
104
|
audience_uri,
|
@@ -110,7 +110,7 @@ module SamlIdp
|
|
110
110
|
encryption_opts,
|
111
111
|
session_expiry,
|
112
112
|
name_id_formats_opts,
|
113
|
-
asserted_attributes_opts
|
113
|
+
asserted_attributes_opts)
|
114
114
|
end
|
115
115
|
private :assertion_builder
|
116
116
|
end
|
@@ -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)
|
data/lib/saml_idp.rb
CHANGED
@@ -9,7 +9,7 @@ module SamlIdp
|
|
9
9
|
require 'saml_idp/metadata_builder'
|
10
10
|
require 'saml_idp/version'
|
11
11
|
require 'saml_idp/fingerprint'
|
12
|
-
require 'saml_idp/engine' if defined?(::Rails)
|
12
|
+
require 'saml_idp/engine' if defined?(::Rails::Engine)
|
13
13
|
|
14
14
|
def self.config
|
15
15
|
@config ||= SamlIdp::Configurator.new
|
@@ -70,9 +70,9 @@ module Saml
|
|
70
70
|
!!xpath("//ds:Signature", ds: signature_namespace).first
|
71
71
|
end
|
72
72
|
|
73
|
-
def valid_signature?(fingerprint)
|
73
|
+
def valid_signature?(certificate, fingerprint)
|
74
74
|
signed? &&
|
75
|
-
signed_document.validate(fingerprint, :soft)
|
75
|
+
signed_document.validate(certificate, fingerprint, :soft)
|
76
76
|
end
|
77
77
|
|
78
78
|
def signed_document
|
data/saml_idp.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |s|
|
|
10
10
|
s.authors = ['Jon Phenow']
|
11
11
|
s.email = 'jon.phenow@sportngin.com'
|
12
12
|
s.homepage = 'https://github.com/saml-idp/saml_idp'
|
13
|
-
s.summary = 'SAML
|
13
|
+
s.summary = 'SAML Identity Provider for Ruby'
|
14
14
|
s.description = 'SAML IdP (Identity Provider) Library for Ruby'
|
15
15
|
s.date = Time.now.utc.strftime('%Y-%m-%d')
|
16
16
|
s.files = Dir['lib/**/*', 'LICENSE', 'README.md', 'Gemfile', 'saml_idp.gemspec']
|
@@ -46,14 +46,15 @@ Gem::Specification.new do |s|
|
|
46
46
|
s.add_dependency('activesupport', '>= 5.2')
|
47
47
|
s.add_dependency('builder', '>= 3.0')
|
48
48
|
s.add_dependency('nokogiri', '>= 1.6.2')
|
49
|
+
s.add_dependency('ostruct')
|
49
50
|
s.add_dependency('rexml')
|
50
51
|
s.add_dependency('xmlenc', '>= 0.7.1')
|
51
52
|
|
52
|
-
s.add_development_dependency('activeresource', '
|
53
|
+
s.add_development_dependency('activeresource', '~> 6.1')
|
53
54
|
s.add_development_dependency('appraisal')
|
54
|
-
s.add_development_dependency('byebug')
|
55
55
|
s.add_development_dependency('capybara', '>= 2.16')
|
56
56
|
s.add_development_dependency('rails', '>= 5.2')
|
57
|
+
s.add_development_dependency('debug')
|
57
58
|
s.add_development_dependency('rake')
|
58
59
|
s.add_development_dependency('rspec', '>= 3.7.0')
|
59
60
|
s.add_development_dependency('ruby-saml', '>= 1.7.2')
|
@@ -20,11 +20,11 @@ module SamlIdp
|
|
20
20
|
it { should respond_to :logger }
|
21
21
|
|
22
22
|
it "has a valid x509_certificate" do
|
23
|
-
expect(subject.x509_certificate).to eq(Default::X509_CERTIFICATE)
|
23
|
+
expect(subject.x509_certificate.call).to eq(Default::X509_CERTIFICATE)
|
24
24
|
end
|
25
25
|
|
26
26
|
it "has a valid secret_key" do
|
27
|
-
expect(subject.secret_key).to eq(Default::SECRET_KEY)
|
27
|
+
expect(subject.secret_key.call).to eq(Default::SECRET_KEY)
|
28
28
|
end
|
29
29
|
|
30
30
|
it "has a valid algorithm" do
|
@@ -47,5 +47,41 @@ module SamlIdp
|
|
47
47
|
it 'has a valid session_expiry' do
|
48
48
|
expect(subject.session_expiry).to eq(0)
|
49
49
|
end
|
50
|
+
|
51
|
+
context "logger initialization" do
|
52
|
+
context 'when Rails has been properly initialized' do
|
53
|
+
before do
|
54
|
+
stub_const("Rails", double(logger: double("Rails.logger")))
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'sets logger to Rails.logger' do
|
58
|
+
expect(subject.logger).to eq(Rails.logger)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'when Rails is not fully initialized' do
|
63
|
+
before do
|
64
|
+
stub_const("Rails", Class.new)
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'sets logger to a lambda' do
|
68
|
+
expect(subject.logger).to be_a(Proc)
|
69
|
+
expect { subject.logger.call("test") }.to output("test\n").to_stdout
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'when Rails is not defined' do
|
74
|
+
it 'sets logger to a lambda' do
|
75
|
+
hide_const("Rails")
|
76
|
+
|
77
|
+
expect(subject.logger).to be_a(Proc)
|
78
|
+
expect { subject.logger.call("test") }.to output("test\n").to_stdout
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
after do
|
83
|
+
hide_const("Rails")
|
84
|
+
end
|
85
|
+
end
|
50
86
|
end
|
51
87
|
end
|
@@ -33,7 +33,7 @@ describe SamlIdp::Controller do
|
|
33
33
|
end
|
34
34
|
|
35
35
|
it 'should call xml signature validation method' do
|
36
|
-
signed_doc = SamlIdp::XMLSecurity::SignedDocument.new(params[:SAMLRequest])
|
36
|
+
signed_doc = SamlIdp::XMLSecurity::SignedDocument.new(decode_saml_request(params[:SAMLRequest]))
|
37
37
|
allow(signed_doc).to receive(:validate).and_return(true)
|
38
38
|
allow(SamlIdp::XMLSecurity::SignedDocument).to receive(:new).and_return(signed_doc)
|
39
39
|
validate_saml_request
|
@@ -66,6 +66,16 @@ describe SamlIdp::Controller do
|
|
66
66
|
end
|
67
67
|
end
|
68
68
|
|
69
|
+
context '#encode_authn_response' do
|
70
|
+
it 'uses default values when opts are not provided' do
|
71
|
+
saml_response = encode_authn_response(principal, { audience_uri: 'http://example.com/issuer', issuer_uri: 'http://example.com', acs_url: 'https://foo.example.com/saml/consume', signed_assertion: false })
|
72
|
+
|
73
|
+
response = OneLogin::RubySaml::Response.new(saml_response)
|
74
|
+
response.settings = saml_settings
|
75
|
+
expect(response.document.to_s).to_not include("<ds:Signature>")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
69
79
|
context "solicited Response" do
|
70
80
|
before(:each) do
|
71
81
|
params[:SAMLRequest] = make_saml_request
|
@@ -81,17 +91,13 @@ describe SamlIdp::Controller do
|
|
81
91
|
expect(response.is_valid?).to be_truthy
|
82
92
|
end
|
83
93
|
|
84
|
-
it "should create a SAML
|
85
|
-
params[:SAMLRequest] = make_saml_logout_request
|
86
|
-
expect(validate_saml_request).to eq(true)
|
87
|
-
expect(saml_request.logout_request?).to eq true
|
94
|
+
it "should by default create a SAML Response with a signed assertion" do
|
88
95
|
saml_response = encode_response(principal)
|
89
|
-
response = OneLogin::RubySaml::
|
90
|
-
|
91
|
-
expect(response.
|
96
|
+
response = OneLogin::RubySaml::Response.new(saml_response)
|
97
|
+
response.settings = saml_settings("https://foo.example.com/saml/consume", true)
|
98
|
+
expect(response.is_valid?).to be_truthy
|
92
99
|
end
|
93
100
|
|
94
|
-
|
95
101
|
[:sha1, :sha256, :sha384, :sha512].each do |algorithm_name|
|
96
102
|
it "should create a SAML Response using the #{algorithm_name} algorithm" do
|
97
103
|
self.algorithm = algorithm_name
|
@@ -118,4 +124,32 @@ describe SamlIdp::Controller do
|
|
118
124
|
end
|
119
125
|
end
|
120
126
|
end
|
127
|
+
|
128
|
+
context "Single Logout Request" do
|
129
|
+
before do
|
130
|
+
idp_configure("https://foo.example.com/saml/consume", true)
|
131
|
+
slo_request = make_saml_sp_slo_request(security_options: { embed_sign: false })
|
132
|
+
params[:SAMLRequest] = slo_request['SAMLRequest']
|
133
|
+
params[:RelayState] = slo_request['RelayState']
|
134
|
+
params[:SigAlg] = slo_request['SigAlg']
|
135
|
+
params[:Signature] = slo_request['Signature']
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'should successfully validate signature' do
|
139
|
+
expect(validate_saml_request).to eq(true)
|
140
|
+
end
|
141
|
+
|
142
|
+
context "solicited Response" do
|
143
|
+
let(:principal) { double email_address: "foo@example.com" }
|
144
|
+
|
145
|
+
it "should create a SAML Logout Response" do
|
146
|
+
expect(validate_saml_request).to eq(true)
|
147
|
+
expect(saml_request.logout_request?).to eq true
|
148
|
+
saml_response = encode_response(principal)
|
149
|
+
response = OneLogin::RubySaml::Logoutresponse.new(saml_response, saml_settings)
|
150
|
+
expect(response.validate).to eq(true)
|
151
|
+
expect(response.issuer).to eq("http://idp.com/saml/idp")
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
121
155
|
end
|