omniauth-saml 2.2.4 → 2.2.5
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/CHANGELOG.md +9 -0
- data/README.md +16 -3
- data/lib/omniauth/strategies/saml.rb +76 -13
- data/lib/omniauth-saml/version.rb +1 -1
- data/spec/omniauth/strategies/saml_spec.rb +271 -65
- metadata +3 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d346f41f4069110547ddd73e4d9f7b23196d8519871da44f0eb42f2176c5fe6
|
|
4
|
+
data.tar.gz: 7451c766a513d52948fc07e0ac971e6e5d1b4392d822991444db38d72a66c835
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f93c043e99ccd8521877c45d2627cc87a150c42fb086b394172be4a12d02c28b816fec390810f934bdf10b07f68049ee222d15dc2a1e50e623e155b90951fca1
|
|
7
|
+
data.tar.gz: ae4440bfcb758760cd1f15b797d2f9b0b339a4ecfebd16e80834359c247cb335c15459fd509acf71364971ed5ef9d8d7009224f2996c3fbfd1ad54a92b26282d
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -101,7 +101,18 @@ Note that when [integrating with Devise](#devise-integration), the URL path will
|
|
|
101
101
|
* `:slo_default_relay_state` - The value to use as default `RelayState` for single log outs. The
|
|
102
102
|
value can be a string, or a `Proc` (or other object responding to `call`). The `request`
|
|
103
103
|
instance will be passed to this callable if it has an arity of 1. If the value is a string,
|
|
104
|
-
the string will be returned, when the `RelayState` is called.
|
|
104
|
+
the string will be returned, when the `RelayState` is called.
|
|
105
|
+
The value is assumed to be safe and is not validated by `:slo_relay_state_validator`.
|
|
106
|
+
Optional.
|
|
107
|
+
|
|
108
|
+
* `:slo_enabled` - Enables or disables Single Logout (SLO). Set to `false` to disable SLO. Defaults to `true`. Optional.
|
|
109
|
+
|
|
110
|
+
* `:slo_relay_state_validator` - A callable used to validate any `RelayState` before performing the redirect
|
|
111
|
+
in Single Logout flows. The callable receives the RelayState value and the current Rack request.
|
|
112
|
+
If unset, the default validator is used. The default validator allows only relative paths beginning
|
|
113
|
+
with `/` and rejects absolute URLs, invalid URIs, protocol-relative URLs, and other schemes.
|
|
114
|
+
If the given `RelayState` is considered invalid then the `slo_default_relay_state` value is used for the SLO redirect.
|
|
115
|
+
Optional.
|
|
105
116
|
|
|
106
117
|
* `:idp_sso_service_url_runtime_params` - A dynamic mapping of request params that exist
|
|
107
118
|
during the request phase of OmniAuth that should to be sent to the IdP after a specific
|
|
@@ -112,7 +123,7 @@ Note that when [integrating with Devise](#devise-integration), the URL path will
|
|
|
112
123
|
* `:idp_cert` - The identity provider's certificate in PEM format. Takes precedence
|
|
113
124
|
over the fingerprint option below. This option or `:idp_cert_multi` or `:idp_cert_fingerprint` must
|
|
114
125
|
be present.
|
|
115
|
-
|
|
126
|
+
|
|
116
127
|
* `:idp_cert_multi` - Multiple identity provider certificates in PEM format. Takes precedence
|
|
117
128
|
over the fingerprint option below. This option `:idp_cert` or `:idp_cert_fingerprint` must
|
|
118
129
|
be present.
|
|
@@ -192,7 +203,9 @@ Single Logout can be Service Provider initiated or Identity Provider initiated.
|
|
|
192
203
|
For SP initiated logout, the `idp_slo_service_url` option must be set to the logout url on the IdP,
|
|
193
204
|
and users directed to `user_saml_omniauth_authorize_path + '/spslo'` after logging out locally. For
|
|
194
205
|
IdP initiated logout, logout requests from the IdP should go to `/auth/saml/slo` (this can be
|
|
195
|
-
advertised in metadata by setting the `single_logout_service_url` config option).
|
|
206
|
+
advertised in metadata by setting the `single_logout_service_url` config option). If you wish to
|
|
207
|
+
disable Single Logout entirely (both SP and IdP initiated), set `:slo_enabled => false`; the `/auth/saml/slo`
|
|
208
|
+
and `/auth/saml/spslo` endpoints will then respond with HTTP 501 Not Implemented.
|
|
196
209
|
|
|
197
210
|
When using Devise as an authentication solution, the SP initiated flow can be integrated
|
|
198
211
|
in the `SessionsController#destroy` action.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'omniauth'
|
|
2
2
|
require 'ruby-saml'
|
|
3
|
+
require 'uri'
|
|
3
4
|
|
|
4
5
|
module OmniAuth
|
|
5
6
|
module Strategies
|
|
@@ -13,7 +14,7 @@ module OmniAuth
|
|
|
13
14
|
RUBYSAML_RESPONSE_OPTIONS = OneLogin::RubySaml::Response::AVAILABLE_OPTIONS
|
|
14
15
|
|
|
15
16
|
option :name_identifier_format, nil
|
|
16
|
-
option :idp_sso_service_url_runtime_params, {}
|
|
17
|
+
option :idp_sso_service_url_runtime_params, { RelayState: 'RelayState' }
|
|
17
18
|
option :request_attributes, [
|
|
18
19
|
{ :name => 'email', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Email address' },
|
|
19
20
|
{ :name => 'name', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Full name' },
|
|
@@ -27,7 +28,26 @@ module OmniAuth
|
|
|
27
28
|
first_name: ["first_name", "firstname", "firstName"],
|
|
28
29
|
last_name: ["last_name", "lastname", "lastName"]
|
|
29
30
|
}
|
|
31
|
+
DEFAULT_SLO_RELAY_STATE_VALIDATOR = lambda do |relay_state, _request|
|
|
32
|
+
return true if relay_state.nil? || relay_state == ""
|
|
33
|
+
|
|
34
|
+
return false if relay_state.start_with?("//")
|
|
35
|
+
|
|
36
|
+
begin
|
|
37
|
+
uri = URI.parse(relay_state)
|
|
38
|
+
rescue URI::Error
|
|
39
|
+
return false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
return false unless uri.relative?
|
|
43
|
+
|
|
44
|
+
path = uri.path
|
|
45
|
+
path && path.start_with?("/")
|
|
46
|
+
end
|
|
47
|
+
|
|
30
48
|
option :slo_default_relay_state
|
|
49
|
+
option :slo_enabled, true
|
|
50
|
+
option :slo_relay_state_validator, DEFAULT_SLO_RELAY_STATE_VALIDATOR
|
|
31
51
|
option :uid_attribute
|
|
32
52
|
option :idp_slo_session_destroy, proc { |_env, session| session.clear }
|
|
33
53
|
|
|
@@ -73,8 +93,12 @@ module OmniAuth
|
|
|
73
93
|
if on_subpath?(:metadata)
|
|
74
94
|
other_phase_for_metadata
|
|
75
95
|
elsif on_subpath?(:slo)
|
|
96
|
+
return slo_disabled_response unless slo_enabled?
|
|
97
|
+
|
|
76
98
|
other_phase_for_slo
|
|
77
99
|
elsif on_subpath?(:spslo)
|
|
100
|
+
return slo_disabled_response unless slo_enabled?
|
|
101
|
+
|
|
78
102
|
other_phase_for_spslo
|
|
79
103
|
else
|
|
80
104
|
call_app!
|
|
@@ -115,6 +139,22 @@ module OmniAuth
|
|
|
115
139
|
nil
|
|
116
140
|
end
|
|
117
141
|
|
|
142
|
+
def mock_request_call
|
|
143
|
+
# Per SAML 2.0, if a RelayState param is passed, IDPs "MUST place the exact RelayState
|
|
144
|
+
# data it received with the request into the corresponding RelayState parameter in the response."
|
|
145
|
+
#
|
|
146
|
+
# By default, the "mock" `OmniAuth::Strategy` implementation will forward along any URL params,
|
|
147
|
+
# so we can in turn take any POSTed RelayState params and put them in the GET query string:
|
|
148
|
+
query_hash = request.GET.merge!(additional_params_for_authn_request.slice('RelayState'))
|
|
149
|
+
query_string = Rack::Utils.build_query(query_hash)
|
|
150
|
+
|
|
151
|
+
request.set_header(Rack::QUERY_STRING, query_string)
|
|
152
|
+
request.set_header(Rack::RACK_REQUEST_QUERY_STRING, query_string)
|
|
153
|
+
request.set_header(Rack::RACK_REQUEST_QUERY_HASH, query_hash)
|
|
154
|
+
|
|
155
|
+
super
|
|
156
|
+
end
|
|
157
|
+
|
|
118
158
|
private
|
|
119
159
|
|
|
120
160
|
def request_path_pattern
|
|
@@ -143,19 +183,34 @@ module OmniAuth
|
|
|
143
183
|
|
|
144
184
|
def slo_relay_state
|
|
145
185
|
if request.params.has_key?("RelayState") && request.params["RelayState"] != ""
|
|
146
|
-
request.params["RelayState"]
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if slo_default_relay_state.respond_to?(:call)
|
|
150
|
-
if slo_default_relay_state.arity == 1
|
|
151
|
-
slo_default_relay_state.call(request)
|
|
152
|
-
else
|
|
153
|
-
slo_default_relay_state.call
|
|
154
|
-
end
|
|
155
|
-
else
|
|
156
|
-
slo_default_relay_state
|
|
157
|
-
end
|
|
186
|
+
relay_state = request.params["RelayState"]
|
|
187
|
+
|
|
188
|
+
return relay_state if valid_slo_relay_state?(relay_state)
|
|
158
189
|
end
|
|
190
|
+
|
|
191
|
+
default_slo_relay_state
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def valid_slo_relay_state?(relay_state)
|
|
195
|
+
validator = options.slo_relay_state_validator
|
|
196
|
+
|
|
197
|
+
return !!call_slo_relay_state_validator(validator, relay_state) if validator.respond_to?(:call)
|
|
198
|
+
|
|
199
|
+
!!validator
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def call_slo_relay_state_validator(validator, relay_state)
|
|
203
|
+
return validator.call if validator.arity.zero?
|
|
204
|
+
return validator.call(relay_state) if validator.arity == 1
|
|
205
|
+
validator.call(relay_state, request)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def default_slo_relay_state
|
|
209
|
+
slo_default_relay_state = options.slo_default_relay_state
|
|
210
|
+
|
|
211
|
+
return slo_default_relay_state unless slo_default_relay_state.respond_to?(:call)
|
|
212
|
+
return slo_default_relay_state.call if slo_default_relay_state.arity.zero?
|
|
213
|
+
slo_default_relay_state.call(request)
|
|
159
214
|
end
|
|
160
215
|
|
|
161
216
|
def handle_logout_response(raw_response, settings)
|
|
@@ -259,6 +314,14 @@ module OmniAuth
|
|
|
259
314
|
end
|
|
260
315
|
end
|
|
261
316
|
|
|
317
|
+
def slo_enabled?
|
|
318
|
+
!!options[:slo_enabled]
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def slo_disabled_response
|
|
322
|
+
Rack::Response.new("Not Implemented", 501, { "Content-Type" => "text/html" }).finish
|
|
323
|
+
end
|
|
324
|
+
|
|
262
325
|
def add_request_attributes_to(settings)
|
|
263
326
|
settings.attribute_consuming_service.service_name options.attribute_service_name
|
|
264
327
|
settings.sp_entity_id = options.sp_entity_id
|
|
@@ -6,10 +6,6 @@ RSpec::Matchers.define :fail_with do |message|
|
|
|
6
6
|
end
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
def post_xml(xml = :example_response, opts = {})
|
|
10
|
-
post "/auth/saml/callback", opts.merge({'SAMLResponse' => load_xml(xml)})
|
|
11
|
-
end
|
|
12
|
-
|
|
13
9
|
describe OmniAuth::Strategies::SAML, :type => :strategy do
|
|
14
10
|
include OmniAuth::Test::StrategyTestCase
|
|
15
11
|
|
|
@@ -34,6 +30,55 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
|
|
|
34
30
|
end
|
|
35
31
|
let(:strategy) { [OmniAuth::Strategies::SAML, saml_options] }
|
|
36
32
|
|
|
33
|
+
shared_examples 'validating RelayState param' do
|
|
34
|
+
context 'when slo_relay_state_validator is not defined and default' do
|
|
35
|
+
[
|
|
36
|
+
['/signed-out', '//attacker.test', '%2Fsigned-out'],
|
|
37
|
+
['/signed-out', 'javascript:alert(1)', '%2Fsigned-out'],
|
|
38
|
+
['/signed-out', 'https://example.com/logout', '%2Fsigned-out'],
|
|
39
|
+
['/signed-out', 'https://example.com/logout?param=1&two=two', '%2Fsigned-out'],
|
|
40
|
+
['/signed-out', '/', '%2F'],
|
|
41
|
+
['', '//attacker.test', ''],
|
|
42
|
+
['', '/team/logout', '%2Fteam%2Flogout'],
|
|
43
|
+
].each do |slo_default_relay_state, relay_state_param, expected_relay_state|
|
|
44
|
+
context "when slo_default_relay_state: #{slo_default_relay_state.inspect}, relay_state_param: #{relay_state_param.inspect}" do
|
|
45
|
+
let(:saml_options) { super().merge(slo_default_relay_state: slo_default_relay_state) }
|
|
46
|
+
let(:params) { super().merge('RelayState' => relay_state_param) }
|
|
47
|
+
|
|
48
|
+
it { is_expected.to be_redirect.and have_attributes(location: a_string_including("RelayState=#{expected_relay_state}")) }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
context 'when slo_relay_state_validator is overridden' do
|
|
54
|
+
[
|
|
55
|
+
['/signed-out', proc { |state| state.start_with?('https://trusted.example.com') }, 'https://trusted.example.com/logout', 'https%3A%2F%2Ftrusted.example.com%2Flogout'],
|
|
56
|
+
['/signed-out', proc { |state| state.start_with?('https://trusted.example.com') }, 'https://attacker.test/logout', '%2Fsigned-out'],
|
|
57
|
+
['/signed-out', proc { |state| state.start_with?('https://trusted.example.com') }, '/safe/path', '%2Fsigned-out'],
|
|
58
|
+
['/signed-out', proc { |state, req| state == req.params['RelayState'] }, '/team/logout', '%2Fteam%2Flogout'],
|
|
59
|
+
['/signed-out', nil, '//attacker.test', '%2Fsigned-out'],
|
|
60
|
+
['/signed-out', false, '//attacker.test', '%2Fsigned-out'],
|
|
61
|
+
['/signed-out', proc { |_| false }, '//attacker.test', '%2Fsigned-out'],
|
|
62
|
+
['/signed-out', proc { |_| true }, 'javascript:alert(1)', 'javascript%3Aalert%281%29'],
|
|
63
|
+
[nil, true, 'https://example.com/logout', 'https%3A%2F%2Fexample.com%2Flogout'],
|
|
64
|
+
[nil, true, 'javascript:alert(1)', 'javascript%3Aalert%281%29'],
|
|
65
|
+
[nil, true, '/', '%2F'],
|
|
66
|
+
].each do |slo_default_relay_state, slo_relay_state_validator, relay_state_param, expected_relay_state|
|
|
67
|
+
context "when slo_default_relay_state: #{slo_default_relay_state.inspect}, slo_relay_state_validator: #{slo_relay_state_validator.inspect}, relay_state_param: #{relay_state_param.inspect}" do
|
|
68
|
+
let(:saml_options) do
|
|
69
|
+
super().merge(
|
|
70
|
+
slo_default_relay_state: slo_default_relay_state,
|
|
71
|
+
slo_relay_state_validator: slo_relay_state_validator,
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
let(:params) { super().merge('RelayState' => relay_state_param) }
|
|
75
|
+
|
|
76
|
+
it { is_expected.to be_redirect.and have_attributes(location: a_string_including("RelayState=#{expected_relay_state}")) }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
37
82
|
describe 'POST /auth/saml' do
|
|
38
83
|
context 'without idp runtime params present' do
|
|
39
84
|
before do
|
|
@@ -63,6 +108,33 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
|
|
|
63
108
|
end
|
|
64
109
|
end
|
|
65
110
|
|
|
111
|
+
context 'with RelayState param' do
|
|
112
|
+
before do
|
|
113
|
+
post '/auth/saml', 'RelayState' => 'RELAY_STATE_VALUE'
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'should get authentication page' do
|
|
117
|
+
expect(last_response).to be_redirect
|
|
118
|
+
expect(last_response.location).to match(
|
|
119
|
+
/\Ahttps:\/\/idp.sso.example.com\/signon\/29490\?SAMLRequest=.*&RelayState=RELAY_STATE_VALUE\z/,
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
context 'when test_mode is enabled' do
|
|
124
|
+
around do |example|
|
|
125
|
+
OmniAuth.config.test_mode = true
|
|
126
|
+
example.run
|
|
127
|
+
ensure
|
|
128
|
+
OmniAuth.config.test_mode = false
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'should redirect to local saml callback page' do
|
|
132
|
+
expect(last_response).to be_redirect
|
|
133
|
+
expect(last_response.location).to eq('http://example.org/auth/saml/callback?RelayState=RELAY_STATE_VALUE')
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
66
138
|
context "when the assertion_consumer_service_url is the default" do
|
|
67
139
|
before :each do
|
|
68
140
|
saml_options[:compress_request] = false
|
|
@@ -118,24 +190,27 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
|
|
|
118
190
|
end
|
|
119
191
|
|
|
120
192
|
describe 'POST /auth/saml/callback' do
|
|
121
|
-
subject { last_response }
|
|
122
|
-
|
|
123
193
|
let(:xml) { :example_response }
|
|
194
|
+
let(:params) { { 'SAMLResponse' => load_xml(xml) } }
|
|
195
|
+
|
|
196
|
+
subject(:post_callback_response) do
|
|
197
|
+
post "/auth/saml/callback", params
|
|
198
|
+
end
|
|
124
199
|
|
|
125
200
|
before :each do
|
|
126
201
|
allow(Time).to receive(:now).and_return(Time.utc(2012, 11, 8, 20, 40, 00))
|
|
127
202
|
end
|
|
128
203
|
|
|
129
204
|
context "when the response is valid" do
|
|
130
|
-
before :each do
|
|
131
|
-
post_xml
|
|
132
|
-
end
|
|
133
|
-
|
|
134
205
|
it "should set the uid to the nameID in the SAML response" do
|
|
206
|
+
post_callback_response
|
|
207
|
+
|
|
135
208
|
expect(auth_hash['uid']).to eq '_1f6fcf6be5e13b08b1e3610e7ff59f205fbd814f23'
|
|
136
209
|
end
|
|
137
210
|
|
|
138
211
|
it "should set the raw info to all attributes" do
|
|
212
|
+
post_callback_response
|
|
213
|
+
|
|
139
214
|
expect(auth_hash['extra']['raw_info'].all.to_hash).to eq(
|
|
140
215
|
'first_name' => ['Rajiv'],
|
|
141
216
|
'last_name' => ['Manglani'],
|
|
@@ -146,6 +221,8 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
|
|
|
146
221
|
end
|
|
147
222
|
|
|
148
223
|
it "should set the response_object to the response object from ruby_saml response" do
|
|
224
|
+
post_callback_response
|
|
225
|
+
|
|
149
226
|
expect(auth_hash['extra']['response_object']).to be_kind_of(OneLogin::RubySaml::Response)
|
|
150
227
|
end
|
|
151
228
|
end
|
|
@@ -154,24 +231,22 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
|
|
|
154
231
|
before :each do
|
|
155
232
|
saml_options.delete(:assertion_consumer_service_url)
|
|
156
233
|
OmniAuth.config.full_host = 'http://localhost:9080'
|
|
157
|
-
post_xml
|
|
158
234
|
end
|
|
159
235
|
|
|
160
236
|
it { is_expected.not_to fail_with(:invalid_ticket) }
|
|
161
237
|
end
|
|
162
238
|
|
|
163
239
|
context "when there is no SAMLResponse parameter" do
|
|
164
|
-
|
|
165
|
-
post '/auth/saml/callback'
|
|
166
|
-
end
|
|
240
|
+
let(:params) { {} }
|
|
167
241
|
|
|
168
242
|
it { is_expected.to fail_with(:invalid_ticket) }
|
|
169
243
|
end
|
|
170
244
|
|
|
171
245
|
context "when there is no name id in the XML" do
|
|
246
|
+
let(:xml) { :no_name_id }
|
|
247
|
+
|
|
172
248
|
before :each do
|
|
173
249
|
allow(Time).to receive(:now).and_return(Time.utc(2012, 11, 8, 23, 55, 00))
|
|
174
|
-
post_xml :no_name_id
|
|
175
250
|
end
|
|
176
251
|
|
|
177
252
|
it { is_expected.to fail_with(:invalid_ticket) }
|
|
@@ -180,43 +255,37 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
|
|
|
180
255
|
context "when the fingerprint is invalid" do
|
|
181
256
|
before :each do
|
|
182
257
|
saml_options[:idp_cert_fingerprint] = "00:00:00:00:00:0C:6C:A9:41:0F:6E:83:F6:D1:52:25:45:58:89:FB"
|
|
183
|
-
post_xml
|
|
184
258
|
end
|
|
185
259
|
|
|
186
260
|
it { is_expected.to fail_with(:invalid_ticket) }
|
|
187
261
|
end
|
|
188
262
|
|
|
189
263
|
context "when the digest is invalid" do
|
|
190
|
-
|
|
191
|
-
post_xml :digest_mismatch
|
|
192
|
-
end
|
|
264
|
+
let(:xml) { :digest_mismatch }
|
|
193
265
|
|
|
194
266
|
it { is_expected.to fail_with(:invalid_ticket) }
|
|
195
267
|
end
|
|
196
268
|
|
|
197
269
|
context "when the signature is invalid" do
|
|
198
|
-
|
|
199
|
-
post_xml :invalid_signature
|
|
200
|
-
end
|
|
270
|
+
let(:xml) { :invalid_signature }
|
|
201
271
|
|
|
202
272
|
it { is_expected.to fail_with(:invalid_ticket) }
|
|
203
273
|
end
|
|
204
274
|
|
|
205
275
|
context "when the response is stale" do
|
|
276
|
+
let(:xml) { :example_response }
|
|
277
|
+
|
|
206
278
|
before :each do
|
|
207
279
|
allow(Time).to receive(:now).and_return(Time.utc(2012, 11, 8, 20, 45, 00))
|
|
208
280
|
end
|
|
209
281
|
|
|
210
282
|
context "without :allowed_clock_drift option" do
|
|
211
|
-
before { post_xml :example_response }
|
|
212
|
-
|
|
213
283
|
it { is_expected.to fail_with(:invalid_ticket) }
|
|
214
284
|
end
|
|
215
285
|
|
|
216
286
|
context "with :allowed_clock_drift option" do
|
|
217
287
|
before :each do
|
|
218
288
|
saml_options[:allowed_clock_drift] = 60
|
|
219
|
-
post_xml :example_response
|
|
220
289
|
end
|
|
221
290
|
|
|
222
291
|
it { is_expected.to_not fail_with(:invalid_ticket) }
|
|
@@ -224,14 +293,16 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
|
|
|
224
293
|
end
|
|
225
294
|
|
|
226
295
|
context "when response has custom attributes" do
|
|
296
|
+
let(:xml) { :custom_attributes }
|
|
297
|
+
|
|
227
298
|
before :each do
|
|
228
|
-
saml_options[:idp_cert_fingerprint] = "3B:82:F1:F5:54:FC:A8:FF:12:B8:4B:B8:16:61:1D:E4:8E:9B:E2:3C"
|
|
229
299
|
saml_options[:attribute_statements] = {
|
|
230
300
|
email: ["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
|
|
231
301
|
first_name: ["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"],
|
|
232
302
|
last_name: ["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"]
|
|
233
303
|
}
|
|
234
|
-
|
|
304
|
+
|
|
305
|
+
post_callback_response
|
|
235
306
|
end
|
|
236
307
|
|
|
237
308
|
it "should obey attribute statements mapping" do
|
|
@@ -245,10 +316,12 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
|
|
|
245
316
|
end
|
|
246
317
|
|
|
247
318
|
context "when using custom user id attribute" do
|
|
319
|
+
let(:xml) { :custom_attributes }
|
|
320
|
+
|
|
248
321
|
before :each do
|
|
249
|
-
saml_options[:idp_cert_fingerprint] = "3B:82:F1:F5:54:FC:A8:FF:12:B8:4B:B8:16:61:1D:E4:8E:9B:E2:3C"
|
|
250
322
|
saml_options[:uid_attribute] = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
|
|
251
|
-
|
|
323
|
+
|
|
324
|
+
post_callback_response
|
|
252
325
|
end
|
|
253
326
|
|
|
254
327
|
it "should return user id attribute" do
|
|
@@ -259,55 +332,146 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
|
|
|
259
332
|
context "when using custom user id attribute, but it is missing" do
|
|
260
333
|
before :each do
|
|
261
334
|
saml_options[:uid_attribute] = "missing_attribute"
|
|
262
|
-
post_xml
|
|
263
335
|
end
|
|
264
336
|
|
|
265
337
|
it "should fail to authenticate" do
|
|
266
|
-
|
|
338
|
+
expect(post_callback_response).to fail_with(:invalid_ticket)
|
|
267
339
|
expect(last_request.env['omniauth.error']).to be_instance_of(OmniAuth::Strategies::SAML::ValidationError)
|
|
268
340
|
expect(last_request.env['omniauth.error'].message).to eq("SAML response missing 'missing_attribute' attribute")
|
|
269
341
|
end
|
|
270
342
|
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
describe 'POST /auth/saml/slo' do
|
|
346
|
+
before do
|
|
347
|
+
saml_options[:sp_entity_id] = "https://idp.sso.example.com/metadata/29490"
|
|
348
|
+
end
|
|
271
349
|
|
|
272
350
|
context "when response is a logout response" do
|
|
273
|
-
|
|
274
|
-
|
|
351
|
+
let(:opts) do
|
|
352
|
+
{ "rack.session" => { "saml_transaction_id" => "_3fef1069-d0c6-418a-b68d-6f008a4787e9" } }
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
let(:params) { { SAMLResponse: load_xml(:example_logout_response) } }
|
|
275
356
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
357
|
+
subject(:post_slo_response) { post "/auth/saml/slo", params, opts }
|
|
358
|
+
|
|
359
|
+
context "when relay state is relative" do
|
|
360
|
+
let(:params) { super().merge(RelayState: "/signed-out") }
|
|
361
|
+
|
|
362
|
+
it "redirects to the relaystate" do
|
|
363
|
+
post_slo_response
|
|
364
|
+
|
|
365
|
+
expect(last_response).to be_redirect
|
|
366
|
+
expect(last_response.location).to eq "/signed-out"
|
|
367
|
+
end
|
|
280
368
|
end
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
369
|
+
|
|
370
|
+
context "when relay state is an absolute https URL" do
|
|
371
|
+
let(:params) { super().merge(RelayState: "https://example.com/") }
|
|
372
|
+
|
|
373
|
+
it "redirects without a location header" do
|
|
374
|
+
post_slo_response
|
|
375
|
+
|
|
376
|
+
expect(last_response).to be_redirect
|
|
377
|
+
expect(last_response.headers.fetch("Location")).to be_nil
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
context 'when slo_default_relay_state is present' do
|
|
382
|
+
let(:saml_options) { super().merge(slo_default_relay_state: '/signed-out') }
|
|
383
|
+
|
|
384
|
+
context "when response relay state is valid" do
|
|
385
|
+
let(:params) { super().merge(RelayState: "/safe/logout") }
|
|
386
|
+
|
|
387
|
+
it { is_expected.to be_redirect.and have_attributes(location: '/safe/logout') }
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
context "when response relay state is invalid" do
|
|
391
|
+
let(:params) { super().merge(RelayState: "javascript:alert(1)") }
|
|
392
|
+
|
|
393
|
+
it { is_expected.to be_redirect.and have_attributes(location: '/signed-out') }
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
context 'when slo_default_relay_state is blank' do
|
|
398
|
+
let(:saml_options) { super().merge(slo_default_relay_state: nil) }
|
|
399
|
+
|
|
400
|
+
context "when response relay state is valid" do
|
|
401
|
+
let(:params) { super().merge(RelayState: "/safe/logout") }
|
|
402
|
+
|
|
403
|
+
it { is_expected.to be_redirect.and have_attributes(location: '/safe/logout') }
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
context "when response relay state is invalid" do
|
|
407
|
+
let(:params) { super().merge(RelayState: "javascript:alert(1)") }
|
|
408
|
+
|
|
409
|
+
it { is_expected.to be_redirect.and have_attributes(location: nil) }
|
|
410
|
+
end
|
|
284
411
|
end
|
|
285
412
|
end
|
|
286
413
|
|
|
287
414
|
context "when request is a logout request" do
|
|
288
415
|
subject { post "/auth/saml/slo", params, "rack.session" => { "saml_uid" => "username@example.com" } }
|
|
289
416
|
|
|
290
|
-
|
|
291
|
-
saml_options[:sp_entity_id] = "https://idp.sso.example.com/metadata/29490"
|
|
292
|
-
end
|
|
417
|
+
let(:relay_state) { "https://example.com/" }
|
|
293
418
|
|
|
294
419
|
let(:params) do
|
|
295
420
|
{
|
|
296
421
|
"SAMLRequest" => load_xml(:example_logout_request),
|
|
297
|
-
"RelayState" =>
|
|
422
|
+
"RelayState" => relay_state,
|
|
298
423
|
}
|
|
299
424
|
end
|
|
300
425
|
|
|
301
426
|
context "when logout request is valid" do
|
|
427
|
+
let(:relay_state) { "/logout" }
|
|
428
|
+
|
|
302
429
|
before { subject }
|
|
303
430
|
|
|
304
431
|
it "should redirect to logout response" do
|
|
305
432
|
expect(last_response).to be_redirect
|
|
306
433
|
expect(last_response.location).to match /https:\/\/idp.sso.example.com\/signoff\/29490/
|
|
307
|
-
expect(last_response.location).to match /RelayState
|
|
434
|
+
expect(last_response.location).to match /RelayState=%2Flogout/
|
|
308
435
|
end
|
|
309
436
|
end
|
|
310
437
|
|
|
438
|
+
it_behaves_like 'validating RelayState param'
|
|
439
|
+
|
|
440
|
+
context 'when slo_default_relay_state is blank' do
|
|
441
|
+
let(:saml_options) { super().merge(slo_default_relay_state: nil) }
|
|
442
|
+
|
|
443
|
+
context "when request relay state is invalid" do
|
|
444
|
+
let(:params) do
|
|
445
|
+
{
|
|
446
|
+
"SAMLRequest" => load_xml(:example_logout_request),
|
|
447
|
+
"RelayState" => "javascript:alert(1)",
|
|
448
|
+
}
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
it "redirects without including a RelayState parameter" do
|
|
452
|
+
subject
|
|
453
|
+
|
|
454
|
+
expect(last_response).to be_redirect
|
|
455
|
+
expect(last_response.location).to match %r{https://idp\.sso\.example\.com/signoff/29490}
|
|
456
|
+
expect(last_response.location).not_to match(/RelayState=/)
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
context "with a custom relay state validator" do
|
|
462
|
+
let(:saml_options) do
|
|
463
|
+
super().merge(
|
|
464
|
+
slo_relay_state_validator: proc do |relay_state, rack_request|
|
|
465
|
+
expect(rack_request).to respond_to(:params)
|
|
466
|
+
relay_state == "custom-state"
|
|
467
|
+
end,
|
|
468
|
+
)
|
|
469
|
+
end
|
|
470
|
+
let(:params) { super().merge("RelayState" => "custom-state") }
|
|
471
|
+
|
|
472
|
+
it { is_expected.to be_redirect.and have_attributes(location: a_string_matching(/RelayState=custom-state/)) }
|
|
473
|
+
end
|
|
474
|
+
|
|
311
475
|
context "when request is an invalid logout request" do
|
|
312
476
|
before :each do
|
|
313
477
|
allow_any_instance_of(OneLogin::RubySaml::SloLogoutrequest).to receive(:is_valid?).and_return(false)
|
|
@@ -332,38 +496,80 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
|
|
|
332
496
|
end
|
|
333
497
|
end
|
|
334
498
|
|
|
335
|
-
context "when
|
|
336
|
-
|
|
337
|
-
saml_options[
|
|
338
|
-
post "/auth/saml/
|
|
499
|
+
context "when SLO is disabled" do
|
|
500
|
+
before do
|
|
501
|
+
saml_options[:slo_enabled] = false
|
|
502
|
+
post "/auth/saml/slo"
|
|
503
|
+
end
|
|
339
504
|
|
|
340
|
-
|
|
341
|
-
expect(last_response.
|
|
342
|
-
expect(last_response.
|
|
505
|
+
it "should return not implemented" do
|
|
506
|
+
expect(last_response.status).to eq 501
|
|
507
|
+
expect(last_response.body).to eq "Not Implemented"
|
|
343
508
|
end
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
describe 'POST /auth/saml/spslo' do
|
|
513
|
+
let(:params) { {} }
|
|
514
|
+
subject { post "/auth/saml/spslo", params }
|
|
515
|
+
|
|
516
|
+
def test_default_relay_state(static_default_relay_state = nil, &block_default_relay_state)
|
|
517
|
+
saml_options["slo_default_relay_state"] = static_default_relay_state || block_default_relay_state
|
|
518
|
+
post "/auth/saml/spslo"
|
|
519
|
+
|
|
520
|
+
expect(last_response).to be_redirect
|
|
521
|
+
expect(last_response.location).to match /https:\/\/idp.sso.example.com\/signoff\/29490/
|
|
522
|
+
expect(last_response.location).to match /RelayState=https%3A%2F%2Fexample.com%2F/
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
it "should redirect to logout request" do
|
|
526
|
+
test_default_relay_state("https://example.com/")
|
|
527
|
+
end
|
|
344
528
|
|
|
345
|
-
|
|
346
|
-
|
|
529
|
+
it "should redirect to logout request with a block" do
|
|
530
|
+
test_default_relay_state do
|
|
531
|
+
"https://example.com/"
|
|
347
532
|
end
|
|
533
|
+
end
|
|
348
534
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
end
|
|
535
|
+
it "should redirect to logout request with a block with a request parameter" do
|
|
536
|
+
test_default_relay_state do |request|
|
|
537
|
+
"https://example.com/"
|
|
353
538
|
end
|
|
539
|
+
end
|
|
354
540
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
541
|
+
it_behaves_like 'validating RelayState param'
|
|
542
|
+
|
|
543
|
+
context 'when slo_default_relay_state is blank' do
|
|
544
|
+
let(:saml_options) { super().merge(slo_default_relay_state: nil) }
|
|
545
|
+
let(:params) { { RelayState: "//example.com" } }
|
|
546
|
+
|
|
547
|
+
it "redirects without including a RelayState parameter" do
|
|
548
|
+
subject
|
|
549
|
+
|
|
550
|
+
expect(last_response).to be_redirect
|
|
551
|
+
expect(last_response.location).to match %r{https://idp\.sso\.example\.com/signoff/29490}
|
|
552
|
+
expect(last_response.location).not_to match(/RelayState=/)
|
|
359
553
|
end
|
|
554
|
+
end
|
|
360
555
|
|
|
361
|
-
|
|
362
|
-
|
|
556
|
+
it "should give not implemented without an idp_slo_service_url" do
|
|
557
|
+
saml_options.delete(:idp_slo_service_url)
|
|
558
|
+
post "/auth/saml/spslo"
|
|
559
|
+
|
|
560
|
+
expect(last_response.status).to eq 501
|
|
561
|
+
expect(last_response.body).to match /Not Implemented/
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
context "when SLO is disabled" do
|
|
565
|
+
before do
|
|
566
|
+
saml_options[:slo_enabled] = false
|
|
363
567
|
post "/auth/saml/spslo"
|
|
568
|
+
end
|
|
364
569
|
|
|
570
|
+
it "should return not implemented" do
|
|
365
571
|
expect(last_response.status).to eq 501
|
|
366
|
-
expect(last_response.body).to
|
|
572
|
+
expect(last_response.body).to eq "Not Implemented"
|
|
367
573
|
end
|
|
368
574
|
end
|
|
369
575
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: omniauth-saml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.2.
|
|
4
|
+
version: 2.2.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Raecoo Cao
|
|
@@ -11,10 +11,9 @@ authors:
|
|
|
11
11
|
- Nikos Dimitrakopoulos
|
|
12
12
|
- Rudolf Vriend
|
|
13
13
|
- Bruno Pedro
|
|
14
|
-
autorequire:
|
|
15
14
|
bindir: bin
|
|
16
15
|
cert_chain: []
|
|
17
|
-
date:
|
|
16
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
18
17
|
dependencies:
|
|
19
18
|
- !ruby/object:Gem::Dependency
|
|
20
19
|
name: omniauth
|
|
@@ -129,7 +128,6 @@ dependencies:
|
|
|
129
128
|
- !ruby/object:Gem::Version
|
|
130
129
|
version: '0.8'
|
|
131
130
|
description: A generic SAML strategy for OmniAuth.
|
|
132
|
-
email:
|
|
133
131
|
executables: []
|
|
134
132
|
extensions: []
|
|
135
133
|
extra_rdoc_files: []
|
|
@@ -147,7 +145,6 @@ homepage: https://github.com/omniauth/omniauth-saml
|
|
|
147
145
|
licenses:
|
|
148
146
|
- MIT
|
|
149
147
|
metadata: {}
|
|
150
|
-
post_install_message:
|
|
151
148
|
rdoc_options: []
|
|
152
149
|
require_paths:
|
|
153
150
|
- lib
|
|
@@ -162,8 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
162
159
|
- !ruby/object:Gem::Version
|
|
163
160
|
version: '0'
|
|
164
161
|
requirements: []
|
|
165
|
-
rubygems_version: 3.
|
|
166
|
-
signing_key:
|
|
162
|
+
rubygems_version: 3.6.9
|
|
167
163
|
specification_version: 4
|
|
168
164
|
summary: A generic SAML strategy for OmniAuth.
|
|
169
165
|
test_files:
|