omniauth-saml 1.1.0 → 2.1.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.
Potentially problematic release.
This version of omniauth-saml might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/CHANGELOG.md +155 -8
- data/LICENSE.md +25 -0
- data/README.md +173 -38
- data/lib/omniauth/strategies/saml.rb +265 -32
- data/lib/omniauth-saml/version.rb +1 -1
- data/spec/omniauth/strategies/saml_spec.rb +358 -33
- data/spec/spec_helper.rb +11 -0
- metadata +87 -48
@@ -6,60 +6,293 @@ module OmniAuth
|
|
6
6
|
class SAML
|
7
7
|
include OmniAuth::Strategy
|
8
8
|
|
9
|
+
def self.inherited(subclass)
|
10
|
+
OmniAuth::Strategy.included(subclass)
|
11
|
+
end
|
12
|
+
|
13
|
+
RUBYSAML_RESPONSE_OPTIONS = OneLogin::RubySaml::Response::AVAILABLE_OPTIONS
|
14
|
+
|
9
15
|
option :name_identifier_format, nil
|
10
|
-
option :
|
16
|
+
option :idp_sso_service_url_runtime_params, {}
|
17
|
+
option :request_attributes, [
|
18
|
+
{ :name => 'email', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Email address' },
|
19
|
+
{ :name => 'name', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Full name' },
|
20
|
+
{ :name => 'first_name', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Given name' },
|
21
|
+
{ :name => 'last_name', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Family name' }
|
22
|
+
]
|
23
|
+
option :attribute_service_name, 'Required attributes'
|
24
|
+
option :attribute_statements, {
|
25
|
+
name: ["name"],
|
26
|
+
email: ["email", "mail"],
|
27
|
+
first_name: ["first_name", "firstname", "firstName"],
|
28
|
+
last_name: ["last_name", "lastname", "lastName"]
|
29
|
+
}
|
30
|
+
option :slo_default_relay_state
|
31
|
+
option :uid_attribute
|
32
|
+
option :idp_slo_session_destroy, proc { |_env, session| session.clear }
|
11
33
|
|
12
34
|
def request_phase
|
13
|
-
|
14
|
-
|
35
|
+
authn_request = OneLogin::RubySaml::Authrequest.new
|
36
|
+
|
37
|
+
with_settings do |settings|
|
38
|
+
redirect(authn_request.create(settings, additional_params_for_authn_request))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def callback_phase
|
43
|
+
raise OmniAuth::Strategies::SAML::ValidationError.new("SAML response missing") unless request.params["SAMLResponse"]
|
44
|
+
|
45
|
+
with_settings do |settings|
|
46
|
+
# Call a fingerprint validation method if there's one
|
47
|
+
validate_fingerprint(settings) if options.idp_cert_fingerprint_validator
|
48
|
+
|
49
|
+
handle_response(request.params["SAMLResponse"], options_for_response_object, settings) do
|
50
|
+
super
|
51
|
+
end
|
52
|
+
end
|
53
|
+
rescue OmniAuth::Strategies::SAML::ValidationError
|
54
|
+
fail!(:invalid_ticket, $!)
|
55
|
+
rescue OneLogin::RubySaml::ValidationError
|
56
|
+
fail!(:invalid_ticket, $!)
|
57
|
+
end
|
15
58
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
59
|
+
# Obtain an idp certificate fingerprint from the response.
|
60
|
+
def response_fingerprint
|
61
|
+
response = request.params["SAMLResponse"]
|
62
|
+
response = (response =~ /^</) ? response : Base64.decode64(response)
|
63
|
+
document = XMLSecurity::SignedDocument::new(response)
|
64
|
+
cert_element = REXML::XPath.first(document, "//ds:X509Certificate", { "ds"=> 'http://www.w3.org/2000/09/xmldsig#' })
|
65
|
+
base64_cert = cert_element.text
|
66
|
+
cert_text = Base64.decode64(base64_cert)
|
67
|
+
cert = OpenSSL::X509::Certificate.new(cert_text)
|
68
|
+
Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(':')
|
69
|
+
end
|
20
70
|
|
21
|
-
|
22
|
-
|
71
|
+
def other_phase
|
72
|
+
if request_path_pattern.match(current_path)
|
73
|
+
@env['omniauth.strategy'] ||= self
|
74
|
+
setup_phase
|
23
75
|
|
24
|
-
|
76
|
+
if on_subpath?(:metadata)
|
77
|
+
other_phase_for_metadata
|
78
|
+
elsif on_subpath?(:slo)
|
79
|
+
other_phase_for_slo
|
80
|
+
elsif on_subpath?(:spslo)
|
81
|
+
other_phase_for_spslo
|
82
|
+
else
|
83
|
+
call_app!
|
84
|
+
end
|
85
|
+
else
|
86
|
+
call_app!
|
87
|
+
end
|
25
88
|
end
|
26
89
|
|
27
|
-
|
28
|
-
|
29
|
-
|
90
|
+
uid do
|
91
|
+
if options.uid_attribute
|
92
|
+
ret = find_attribute_by([options.uid_attribute])
|
93
|
+
if ret.nil?
|
94
|
+
raise OmniAuth::Strategies::SAML::ValidationError.new("SAML response missing '#{options.uid_attribute}' attribute")
|
95
|
+
end
|
96
|
+
ret
|
97
|
+
else
|
98
|
+
@name_id
|
30
99
|
end
|
100
|
+
end
|
31
101
|
|
32
|
-
|
33
|
-
|
102
|
+
info do
|
103
|
+
found_attributes = options.attribute_statements.map do |key, values|
|
104
|
+
attribute = find_attribute_by(values)
|
105
|
+
[key, attribute]
|
106
|
+
end
|
34
107
|
|
108
|
+
Hash[found_attributes]
|
109
|
+
end
|
110
|
+
|
111
|
+
extra { { :raw_info => @attributes, :session_index => @session_index, :response_object => @response_object } }
|
112
|
+
|
113
|
+
def find_attribute_by(keys)
|
114
|
+
keys.each do |key|
|
115
|
+
return @attributes[key] if @attributes[key]
|
116
|
+
end
|
117
|
+
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def request_path_pattern
|
124
|
+
@request_path_pattern ||= %r{\A#{Regexp.quote(request_path)}(/|\z)}
|
125
|
+
end
|
126
|
+
|
127
|
+
def on_subpath?(subpath)
|
128
|
+
on_path?("#{request_path}/#{subpath}")
|
129
|
+
end
|
130
|
+
|
131
|
+
def handle_response(raw_response, opts, settings)
|
132
|
+
response = OneLogin::RubySaml::Response.new(raw_response, opts.merge(settings: settings))
|
133
|
+
response.attributes["fingerprint"] = settings.idp_cert_fingerprint
|
134
|
+
response.soft = false
|
135
|
+
|
136
|
+
response.is_valid?
|
35
137
|
@name_id = response.name_id
|
138
|
+
@session_index = response.sessionindex
|
36
139
|
@attributes = response.attributes
|
140
|
+
@response_object = response
|
37
141
|
|
38
|
-
|
39
|
-
|
142
|
+
session["saml_uid"] = @name_id
|
143
|
+
session["saml_session_index"] = @session_index
|
144
|
+
yield
|
145
|
+
end
|
146
|
+
|
147
|
+
def slo_relay_state
|
148
|
+
if request.params.has_key?("RelayState") && request.params["RelayState"] != ""
|
149
|
+
request.params["RelayState"]
|
150
|
+
else
|
151
|
+
slo_default_relay_state = options.slo_default_relay_state
|
152
|
+
if slo_default_relay_state.respond_to?(:call)
|
153
|
+
if slo_default_relay_state.arity == 1
|
154
|
+
slo_default_relay_state.call(request)
|
155
|
+
else
|
156
|
+
slo_default_relay_state.call
|
157
|
+
end
|
158
|
+
else
|
159
|
+
slo_default_relay_state
|
160
|
+
end
|
40
161
|
end
|
162
|
+
end
|
41
163
|
|
42
|
-
|
164
|
+
def handle_logout_response(raw_response, settings)
|
165
|
+
# After sending an SP initiated LogoutRequest to the IdP, we need to accept
|
166
|
+
# the LogoutResponse, verify it, then actually delete our session.
|
43
167
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
168
|
+
logout_response = OneLogin::RubySaml::Logoutresponse.new(raw_response, settings, :matches_request_id => session["saml_transaction_id"])
|
169
|
+
logout_response.soft = false
|
170
|
+
logout_response.validate
|
171
|
+
|
172
|
+
session.delete("saml_uid")
|
173
|
+
session.delete("saml_transaction_id")
|
174
|
+
session.delete("saml_session_index")
|
175
|
+
|
176
|
+
redirect(slo_relay_state)
|
49
177
|
end
|
50
178
|
|
51
|
-
|
179
|
+
def handle_logout_request(raw_request, settings)
|
180
|
+
logout_request = OneLogin::RubySaml::SloLogoutrequest.new(raw_request, {}.merge(settings: settings).merge(get_params: @request.params))
|
52
181
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
182
|
+
if logout_request.is_valid? &&
|
183
|
+
logout_request.name_id == session["saml_uid"]
|
184
|
+
|
185
|
+
# Actually log out this session
|
186
|
+
options[:idp_slo_session_destroy].call @env, session
|
187
|
+
|
188
|
+
# Generate a response to the IdP.
|
189
|
+
logout_request_id = logout_request.id
|
190
|
+
logout_response = OneLogin::RubySaml::SloLogoutresponse.new.create(settings, logout_request_id, nil, RelayState: slo_relay_state)
|
191
|
+
redirect(logout_response)
|
192
|
+
else
|
193
|
+
raise OmniAuth::Strategies::SAML::ValidationError.new("SAML failed to process LogoutRequest")
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Create a SP initiated SLO: https://github.com/onelogin/ruby-saml#single-log-out
|
198
|
+
def generate_logout_request(settings)
|
199
|
+
logout_request = OneLogin::RubySaml::Logoutrequest.new()
|
200
|
+
|
201
|
+
# Since we created a new SAML request, save the transaction_id
|
202
|
+
# to compare it with the response we get back
|
203
|
+
session["saml_transaction_id"] = logout_request.uuid
|
204
|
+
|
205
|
+
if settings.name_identifier_value.nil?
|
206
|
+
settings.name_identifier_value = session["saml_uid"]
|
207
|
+
end
|
208
|
+
|
209
|
+
if settings.sessionindex.nil?
|
210
|
+
settings.sessionindex = session["saml_session_index"]
|
211
|
+
end
|
212
|
+
|
213
|
+
logout_request.create(settings, RelayState: slo_relay_state)
|
214
|
+
end
|
215
|
+
|
216
|
+
def with_settings
|
217
|
+
options[:assertion_consumer_service_url] ||= callback_url
|
218
|
+
yield OneLogin::RubySaml::Settings.new(options)
|
219
|
+
end
|
220
|
+
|
221
|
+
def validate_fingerprint(settings)
|
222
|
+
fingerprint_exists = options.idp_cert_fingerprint_validator[response_fingerprint]
|
223
|
+
|
224
|
+
unless fingerprint_exists
|
225
|
+
raise OmniAuth::Strategies::SAML::ValidationError.new("Non-existent fingerprint")
|
226
|
+
end
|
227
|
+
|
228
|
+
# id_cert_fingerprint becomes the given fingerprint if it exists
|
229
|
+
settings.idp_cert_fingerprint = fingerprint_exists
|
230
|
+
end
|
231
|
+
|
232
|
+
def options_for_response_object
|
233
|
+
# filter options to select only extra parameters
|
234
|
+
opts = options.select {|k,_| RUBYSAML_RESPONSE_OPTIONS.include?(k.to_sym)}
|
235
|
+
|
236
|
+
# symbolize keys without activeSupport/symbolize_keys (ruby-saml use symbols)
|
237
|
+
opts.inject({}) do |new_hash, (key, value)|
|
238
|
+
new_hash[key.to_sym] = value
|
239
|
+
new_hash
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def other_phase_for_metadata
|
244
|
+
with_settings do |settings|
|
245
|
+
# omniauth does not set the strategy on the other_phase
|
246
|
+
response = OneLogin::RubySaml::Metadata.new
|
247
|
+
|
248
|
+
add_request_attributes_to(settings) if options.request_attributes.length > 0
|
249
|
+
|
250
|
+
Rack::Response.new(response.generate(settings), 200, { "Content-Type" => "application/xml" }).finish
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def other_phase_for_slo
|
255
|
+
with_settings do |settings|
|
256
|
+
if request.params["SAMLResponse"]
|
257
|
+
handle_logout_response(request.params["SAMLResponse"], settings)
|
258
|
+
elsif request.params["SAMLRequest"]
|
259
|
+
handle_logout_request(request.params["SAMLRequest"], settings)
|
260
|
+
else
|
261
|
+
raise OmniAuth::Strategies::SAML::ValidationError.new("SAML logout response/request missing")
|
262
|
+
end
|
263
|
+
end
|
60
264
|
end
|
61
265
|
|
62
|
-
|
266
|
+
def other_phase_for_spslo
|
267
|
+
if options.idp_slo_service_url
|
268
|
+
with_settings do |settings|
|
269
|
+
redirect(generate_logout_request(settings))
|
270
|
+
end
|
271
|
+
else
|
272
|
+
Rack::Response.new("Not Implemented", 501, { "Content-Type" => "text/html" }).finish
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def add_request_attributes_to(settings)
|
277
|
+
settings.attribute_consuming_service.service_name options.attribute_service_name
|
278
|
+
settings.sp_entity_id = options.sp_entity_id
|
279
|
+
|
280
|
+
options.request_attributes.each do |attribute|
|
281
|
+
settings.attribute_consuming_service.add_attribute attribute
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def additional_params_for_authn_request
|
286
|
+
{}.tap do |additional_params|
|
287
|
+
runtime_request_parameters = options.delete(:idp_sso_service_url_runtime_params)
|
288
|
+
|
289
|
+
if runtime_request_parameters
|
290
|
+
runtime_request_parameters.each_pair do |request_param_key, mapped_param_key|
|
291
|
+
additional_params[mapped_param_key] = request.params[request_param_key.to_s] if request.params.has_key?(request_param_key.to_s)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
63
296
|
end
|
64
297
|
end
|
65
298
|
end
|