devise_saml_authenticatable 1.5.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 388799cd1bf9c14ad21b7a042d63957d965ec080
4
- data.tar.gz: b5e153ee444879be849e6ddda43e2762a3905092
2
+ SHA256:
3
+ metadata.gz: 8f372d3d801220ab4ce4a7e83501f0e5d7a9cc2590eb64aa1c91a0a5ad053d7d
4
+ data.tar.gz: bcdfc0370f0926a542164ea577030e9ff17666adb9e0d5ae8367a605e4966866
5
5
  SHA512:
6
- metadata.gz: 2c304efa473b057a46895b96db3d6d076ebdaa332de7c7d6a3480b86a3ae3e33d17123b5f565d229c5412a9496cc9c549e34960aa119f8a1c18180eea87eb05e
7
- data.tar.gz: 40a2dd033fd20cccc2803dc587309be272005a5d84c8e80a0325b038079fcd2deb9b6aad78f8afb30c2462f9952efe1e49ffa100b9187480c5234a6565aaf566
6
+ metadata.gz: d9bd026db0bb199aaa9b72d1c03fccef49a905dc7270739b48e682623036f945b74f16c99f1d457086df47ff03f8e71aee5c4a412d01cd9f9c80b4f84e79d58a
7
+ data.tar.gz: c2f32e5297c0e9a4aadce6069282d8c17e91d95550a777e2f5755745effc0fda5eaa958e4656d5776e2a507251d230fdaba3d944189874d0166d3f2cefbe153c
data/.gitignore CHANGED
@@ -13,6 +13,4 @@ lib/bundler/man
13
13
  pkg
14
14
  rdoc
15
15
  spec/reports
16
- spec/support/idp/
17
- spec/support/sp/
18
16
  tmp
@@ -1,53 +1,52 @@
1
1
  language: ruby
2
2
  rvm:
3
- - "1.9.3"
4
3
  - "2.0.0"
5
4
  - "2.1.10"
6
5
  - "2.2.10"
7
6
  - "2.3.8"
8
- - "2.4.5"
9
- - "2.5.3"
10
- - "2.6.0"
7
+ - "2.4.10"
8
+ - "2.5.8"
9
+ - "2.6.6"
10
+ - "2.7.1"
11
11
  gemfile:
12
12
  - Gemfile
13
+ - spec/support/Gemfile.rails5.2
13
14
  - spec/support/Gemfile.rails5.1
14
15
  - spec/support/Gemfile.rails5
15
16
  - spec/support/Gemfile.rails4
16
17
  matrix:
17
18
  allow_failures:
18
- - rvm: "1.9.3"
19
- gemfile: Gemfile
20
- - rvm: "1.9.3"
21
- gemfile: spec/support/Gemfile.rails5
22
- - rvm: "1.9.3"
23
- gemfile: spec/support/Gemfile.rails5.1
24
19
  - rvm: "2.0.0"
25
20
  gemfile: Gemfile
26
21
  - rvm: "2.0.0"
27
22
  gemfile: spec/support/Gemfile.rails5
28
23
  - rvm: "2.0.0"
29
24
  gemfile: spec/support/Gemfile.rails5.1
25
+ - rvm: "2.0.0"
26
+ gemfile: spec/support/Gemfile.rails5.2
30
27
  - rvm: "2.1.10"
31
28
  gemfile: Gemfile
32
29
  - rvm: "2.1.10"
33
30
  gemfile: spec/support/Gemfile.rails5
34
31
  - rvm: "2.1.10"
35
32
  gemfile: spec/support/Gemfile.rails5.1
36
- - rvm: "2.6.0"
33
+ - rvm: "2.1.10"
34
+ gemfile: spec/support/Gemfile.rails5.2
35
+ - rvm: "2.2.10"
36
+ gemfile: Gemfile
37
+ - rvm: "2.2.10"
38
+ gemfile: spec/support/Gemfile.rails5.2
39
+ - rvm: "2.3.8"
40
+ gemfile: Gemfile
41
+ - rvm: "2.4.10"
42
+ gemfile: Gemfile
43
+ - rvm: "2.6.6"
44
+ gemfile: spec/support/Gemfile.rails4
45
+ - rvm: "2.7.1"
37
46
  gemfile: spec/support/Gemfile.rails4
38
47
 
39
48
  before_install:
40
- # update bundler to avoid https://github.com/travis-ci/travis-ci/issues/5239
41
49
  - command -v bundle || gem install bundler -v '~> 1.17.3'
42
50
 
43
51
  script:
44
52
  - bundle exec rake
45
-
46
- notifications:
47
- hipchat:
48
- rooms:
49
- secure: cuDak5a6fBeg+sp61COqxQfzdcFEsjwCqtwvCISso0RNh5SR8v+uVYKcA8rlK+GE1l9uR7tLRHeHF3ZmzvFSOat07NvpScvjZXi+OSpWlc6rwQ6Pl6bBP6gu6sREiKVe0eT/uGrvJloyWKZaXIhiiBzQ+ZERx/ssGA9WMmNkhlwy1OgGnPNurNNHZLBjEZn1V6kdyxiXx6QPASNpjNEgN1G8dUh3qzcWUGVQGNZSJk65A6ie1MveNyecTjDhw+ADBU8nS28Ja4y6ohRm4FzofSgespYrvfygIZ5rYF0HPMj5FW1ZDWtM5355ojCk8RLT+ZkuhssCn1OJk7ogaOVjnYcOFRxEfpu3eIbjtMmUz3j4umatFqbgas+6SXMVIPkr5HUoTrP8HNFssIpcEBOnPwAF8QCpx+daHc0r2cc8lGuXhtJfpW0P2F0dmwJNiQ7//nz2y2xs84x4Gb7MV9tEDYp0FqEClMuFBkPNizBljarm04PkiLSrqvR52aMDfQz7YAX2oXAvFjPzI1GC0K8x7xX8TuHT9yuHy7fI+rUSNivZYLKO+IEZqPPDdJpXISUbVwanZoNvmQYk5PZV21MfDSGwQrz8eO/uFiAblj18yIlNbAfb2hdZDVYsm4EvWxELJtfaTxgrj6M3Y3m/KbCbCoDp+2jE307M2rxL0Gum2gk=
50
- template:
51
- - '%{repository}<a href="%{build_url}">#%{build_number}</a> (%{branch} - <a href="%{compare_url}">%{commit}</a> : %{author}): %{message}'
52
- format: html
53
- on_pull_requests: true
data/Gemfile CHANGED
@@ -6,9 +6,9 @@ gemspec
6
6
  group :test do
7
7
  gem 'rake'
8
8
  gem 'rspec', '~> 3.0'
9
- gem 'rails', '~> 5.1'
9
+ gem 'rails', '~> 6.0'
10
10
  gem 'rspec-rails'
11
- gem 'sqlite3'
11
+ gem 'sqlite3', '~> 1.4.0'
12
12
  gem 'capybara'
13
13
  gem 'poltergeist'
14
14
  end
data/README.md CHANGED
@@ -6,18 +6,15 @@ It uses [ruby-saml][] to handle all SAML-related stuff.
6
6
 
7
7
  ## Installation
8
8
 
9
- Add this line to your application's Gemfile:
9
+ Add this gem to your application's Gemfile:
10
10
 
11
- gem 'devise_saml_authenticatable'
11
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
12
+ gem "devise_saml_authenticatable", github: "apokalipto/devise_saml_authenticatable"
12
13
 
13
14
  And then execute:
14
15
 
15
16
  $ bundle
16
17
 
17
- Or install it yourself as:
18
-
19
- $ gem install devise_saml_authenticatable
20
-
21
18
  ## Usage
22
19
 
23
20
  Follow the [normal devise installation process](https://github.com/plataformatec/devise/tree/master#getting-started). The controller filters and helpers are unchanged from normal devise usage.
@@ -110,6 +107,10 @@ In `config/initializers/devise.rb`:
110
107
  # If saml_route_helper_prefix = 'saml' then the new_user_session route becomes new_saml_user_session
111
108
  # config.saml_route_helper_prefix = 'saml'
112
109
 
110
+ # You can add allowance for clock drift between the sp and idp.
111
+ # This is a time in seconds.
112
+ # config.allowed_clock_drift_in_seconds = 0
113
+
113
114
  # Configure with your SAML settings (see ruby-saml's README for more information: https://github.com/onelogin/ruby-saml).
114
115
  config.saml_configure do |settings|
115
116
  # assertion_consumer_service_url is required starting with ruby-saml 1.4.3: https://github.com/onelogin/ruby-saml#updating-from-142-to-143
@@ -126,7 +127,26 @@ In `config/initializers/devise.rb`:
126
127
  end
127
128
  ```
128
129
 
129
- In the config directory, create a YAML file (`attribute-map.yml`) that maps SAML attributes with your model's fields:
130
+ #### Attributes
131
+
132
+ There are two ways to map SAML attributes to User attributes:
133
+
134
+ - [initializer](#attribute-map-initializer)
135
+ - [config file](#attribute-map-config-file)
136
+
137
+ The attribute mappings are very dependent on the way the IdP encodes the attributes.
138
+ In these examples the attributes are given in URN style.
139
+ Other IdPs might provide them as OID's, or by other means.
140
+
141
+ You are now ready to test it against an IdP.
142
+
143
+ When the user visits `/users/saml/sign_in` they will be redirected to the login page of the IdP.
144
+
145
+ Upon successful login the user is redirected to the Devise `user_root_path`.
146
+
147
+ ##### Attribute map config file
148
+
149
+ Create a YAML file (`config/attribute-map.yml`) that maps SAML attributes with your model's fields:
130
150
 
131
151
  ```yaml
132
152
  # attribute-map.yml
@@ -137,15 +157,39 @@ In the config directory, create a YAML file (`attribute-map.yml`) that maps SAML
137
157
  "urn:mace:dir:attribute-def:givenName": "name"
138
158
  ```
139
159
 
140
- The attribute mappings are very dependent on the way the IdP encodes the attributes.
141
- In this example the attributes are given in URN style.
142
- Other IdPs might provide them as OID's, or by other means.
160
+ ##### Attribute map initializer
143
161
 
144
- You are now ready to test it against an IdP.
162
+ In `config/initializers/devise.rb` (see above), add an attribute map resolver.
163
+ The resolver gets the [SAML response from the IdP](https://github.com/onelogin/ruby-saml/blob/master/lib/onelogin/ruby-saml/response.rb) so it can decide which attribute map to load.
164
+ If you only have one IdP, you can use the config file above, or just return a single hash.
145
165
 
146
- When the user visits `/users/saml/sign_in` they will be redirected to the login page of the IdP.
166
+ ```ruby
167
+ # config/initializers/devise.rb
168
+ Devise.setup do |config|
169
+ ...
170
+ # ==> Configuration for :saml_authenticatable
147
171
 
148
- Upon successful login the user is redirected to the Devise `user_root_path`.
172
+ config.saml_attribute_map_resolver = MyAttributeMapResolver
173
+ end
174
+ ```
175
+
176
+ ```ruby
177
+ # app/lib/my_attribute_map_resolver
178
+ class MyAttributeMapResolver < DeviseSamlAuthenticatable::DefaultAttributeMapResolver
179
+ def attribute_map
180
+ issuer = saml_response.issuers.first
181
+ case issuer
182
+ when "idp_entity_id"
183
+ {
184
+ "urn:mace:dir:attribute-def:uid" => "user_name",
185
+ "urn:mace:dir:attribute-def:email" => "email",
186
+ "urn:mace:dir:attribute-def:name" => "last_name",
187
+ "urn:mace:dir:attribute-def:givenName" => "name",
188
+ }
189
+ end
190
+ end
191
+ end
192
+ ```
149
193
 
150
194
  ## Supporting Multiple IdPs
151
195
 
@@ -5,8 +5,10 @@ class Devise::SamlSessionsController < Devise::SessionsController
5
5
  unloadable if Rails::VERSION::MAJOR < 4
6
6
  if Rails::VERSION::MAJOR < 5
7
7
  skip_before_filter :verify_authenticity_token
8
+ prepend_before_filter :store_info_for_sp_initiated_logout, only: :destroy
8
9
  else
9
10
  skip_before_action :verify_authenticity_token, raise: false
11
+ prepend_before_action :store_info_for_sp_initiated_logout, only: :destroy
10
12
  end
11
13
 
12
14
  def new
@@ -30,16 +32,16 @@ class Devise::SamlSessionsController < Devise::SessionsController
30
32
 
31
33
  redirect_to generate_idp_logout_response(saml_config, logout_request.id)
32
34
  elsif params[:SAMLResponse]
33
- #Currently Devise handles the session invalidation when the request is made.
34
- #To support a true SP initiated logout response, the request ID would have to be tracked and session invalidated
35
- #based on that.
35
+ # Currently Devise handles the session invalidation when the request is made.
36
+ # To support a true SP initiated logout response, the request ID would have to be tracked and session invalidated
37
+ # based on that.
36
38
  if Devise.saml_sign_out_success_url
37
39
  redirect_to Devise.saml_sign_out_success_url
38
40
  else
39
41
  redirect_to action: :new
40
42
  end
41
43
  else
42
- head :invalid_request
44
+ head 500
43
45
  end
44
46
  end
45
47
 
@@ -51,14 +53,38 @@ class Devise::SamlSessionsController < Devise::SessionsController
51
53
  end
52
54
  end
53
55
 
56
+ # For non transient name ID, save info to identify user for logout purpose
57
+ # before that user's session got destroyed. These info are used in the
58
+ # `after_sign_out_path_for` method below.
59
+ def store_info_for_sp_initiated_logout
60
+ return if Devise.saml_config.name_identifier_format == "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
61
+ @name_identifier_value_for_sp_initiated_logout = Devise.saml_name_identifier_retriever.call(current_user)
62
+ @sessionindex_for_sp_initiated_logout = current_user.public_send(Devise.saml_session_index_key) if Devise.saml_session_index_key
63
+ end
64
+
54
65
  # Override devise to send user to IdP logout for SLO
55
66
  def after_sign_out_path_for(_)
56
67
  idp_entity_id = get_idp_entity_id(params)
57
68
  request = OneLogin::RubySaml::Logoutrequest.new
58
- request.create(saml_config(idp_entity_id))
69
+ saml_settings = saml_config(idp_entity_id).dup
70
+
71
+ # Add attributes to saml_settings which will later be used to create the SP
72
+ # initiated logout request
73
+ unless Devise.saml_config.name_identifier_format == 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
74
+ saml_settings.name_identifier_value = @name_identifier_value_for_sp_initiated_logout
75
+ saml_settings.sessionindex = @sessionindex_for_sp_initiated_logout
76
+ end
77
+
78
+ request.create(saml_settings)
59
79
  end
60
80
 
61
81
  def generate_idp_logout_response(saml_config, logout_request_id)
62
- OneLogin::RubySaml::SloLogoutresponse.new.create(saml_config, logout_request_id, nil)
82
+
83
+ params = {}
84
+ if relay_state
85
+ params[:RelayState] = relay_state
86
+ end
87
+
88
+ OneLogin::RubySaml::SloLogoutresponse.new.create(saml_config, logout_request_id, nil, params)
63
89
  end
64
90
  end
@@ -5,6 +5,7 @@ require "devise_saml_authenticatable/exception"
5
5
  require "devise_saml_authenticatable/logger"
6
6
  require "devise_saml_authenticatable/routes"
7
7
  require "devise_saml_authenticatable/saml_config"
8
+ require "devise_saml_authenticatable/default_attribute_map_resolver"
8
9
  require "devise_saml_authenticatable/default_idp_entity_id_reader"
9
10
 
10
11
  begin
@@ -66,6 +67,10 @@ module Devise
66
67
  mattr_accessor :saml_relay_state
67
68
  @@saml_relay_state
68
69
 
70
+ # Instead of storing the attribute_map in attribute-map.yml, store it in the database, or set it programatically
71
+ mattr_accessor :saml_attribute_map_resolver
72
+ @@saml_attribute_map_resolver ||= ::DeviseSamlAuthenticatable::DefaultAttributeMapResolver
73
+
69
74
  # Implements a #validate method that takes the retrieved resource and response right after retrieval,
70
75
  # and returns true if it's valid. False will cause authentication to fail.
71
76
  # Only one of saml_resource_validator and saml_resource_validator_hook may be used.
@@ -124,6 +129,14 @@ module Devise
124
129
  # See saml_default_resource_locator above for an example.
125
130
  mattr_accessor :saml_resource_locator
126
131
  @@saml_resource_locator = @@saml_default_resource_locator
132
+
133
+ # Proc that is called to resolve the name identifier to use in a LogoutRequest for the current user.
134
+ # Receives the logged-in user.
135
+ # Is expected to return the identifier the IdP understands for this user, e.g. email address or username.
136
+ mattr_accessor :saml_name_identifier_retriever
137
+ @@saml_name_identifier_retriever = Proc.new do |current_user|
138
+ current_user.public_send(Devise.saml_default_user_key)
139
+ end
127
140
  end
128
141
 
129
142
  # Add saml_authenticatable strategy to defaults.
@@ -0,0 +1,26 @@
1
+ module DeviseSamlAuthenticatable
2
+ class DefaultAttributeMapResolver
3
+ def initialize(saml_response)
4
+ @saml_response = saml_response
5
+ end
6
+
7
+ def attribute_map
8
+ return {} unless File.exist?(attribute_map_path)
9
+
10
+ attribute_map = YAML.load(File.read(attribute_map_path))
11
+ if attribute_map.key?(Rails.env)
12
+ attribute_map[Rails.env]
13
+ else
14
+ attribute_map
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :saml_response
21
+
22
+ def attribute_map_path
23
+ Rails.root.join("config", "attribute-map.yml")
24
+ end
25
+ end
26
+ end
@@ -1,6 +1,6 @@
1
1
  module DeviseSamlAuthenticatable
2
2
 
3
- class SamlException < Exception
3
+ class SamlException < StandardError
4
4
  end
5
5
 
6
6
  end
@@ -33,9 +33,9 @@ module Devise
33
33
  key = Devise.saml_default_user_key
34
34
  decorated_response = ::SamlAuthenticatable::SamlResponse.new(
35
35
  saml_response,
36
- attribute_map
36
+ Devise.saml_attribute_map_resolver.new(saml_response).attribute_map,
37
37
  )
38
- if (Devise.saml_use_subject)
38
+ if Devise.saml_use_subject
39
39
  auth_value = saml_response.name_id
40
40
  else
41
41
  auth_value = decorated_response.attribute_value_by_resource_key(key)
@@ -80,21 +80,6 @@ module Devise
80
80
  def find_for_shibb_authentication(conditions)
81
81
  find_for_authentication(conditions)
82
82
  end
83
-
84
- def attribute_map
85
- @attribute_map ||= attribute_map_for_environment
86
- end
87
-
88
- private
89
-
90
- def attribute_map_for_environment
91
- attribute_map = YAML.load(File.read("#{Rails.root}/config/attribute-map.yml"))
92
- if attribute_map.has_key?(Rails.env)
93
- attribute_map[Rails.env]
94
- else
95
- attribute_map
96
- end
97
- end
98
83
  end
99
84
  end
100
85
  end
@@ -8,7 +8,7 @@ module Devise
8
8
  if params[:SAMLResponse]
9
9
  OneLogin::RubySaml::Response.new(
10
10
  params[:SAMLResponse],
11
- settings: Devise.saml_config,
11
+ settings: saml_config(get_idp_entity_id(params)),
12
12
  allowed_clock_drift: Devise.allowed_clock_drift_in_seconds,
13
13
  )
14
14
  else
@@ -1,3 +1,3 @@
1
1
  module DeviseSamlAuthenticatable
2
- VERSION = "1.5.0"
2
+ VERSION = "1.6.0"
3
3
  end
@@ -1,31 +1,38 @@
1
1
  require 'rails_helper'
2
2
 
3
- class Devise::SessionsController < ActionController::Base
4
- # The important parts from devise
3
+ # The important parts from devise
4
+ class DeviseController < ApplicationController
5
+ attr_accessor :current_user
6
+
5
7
  def resource_class
6
8
  User
7
9
  end
8
10
 
11
+ def require_no_authentication
12
+ end
13
+ end
14
+ class Devise::SessionsController < DeviseController
9
15
  def destroy
10
16
  sign_out
11
17
  redirect_to after_sign_out_path_for(:user)
12
18
  end
13
19
 
14
- def require_no_authentication
20
+ def verify_signed_out_user
21
+ # no-op for these tests
15
22
  end
16
23
  end
17
24
 
18
25
  require_relative '../../../app/controllers/devise/saml_sessions_controller'
19
26
 
20
27
  describe Devise::SamlSessionsController, type: :controller do
21
- let(:saml_config) { Devise.saml_config }
22
28
  let(:idp_providers_adapter) { spy("Stub IDPSettings Adaptor") }
23
29
 
24
30
  before do
31
+ @request.env["devise.mapping"] = Devise.mappings[:user]
25
32
  allow(idp_providers_adapter).to receive(:settings).and_return({
26
33
  assertion_consumer_service_url: "acs_url",
27
34
  assertion_consumer_service_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
28
- name_identifier_format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
35
+ name_identifier_format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
29
36
  issuer: "sp_issuer",
30
37
  idp_entity_id: "http://www.example.com",
31
38
  authn_context: "",
@@ -35,6 +42,20 @@ describe Devise::SamlSessionsController, type: :controller do
35
42
  })
36
43
  end
37
44
 
45
+ before do
46
+ if Rails::VERSION::MAJOR < 5 && Gem::Version.new(RUBY_VERSION) > Gem::Version.new("2.6")
47
+ # we still want to support Rails 4
48
+ # patch tests using snippet from https://github.com/rails/rails/issues/34790#issuecomment-483607370
49
+ class ActionController::TestResponse < ActionDispatch::TestResponse
50
+ def recycle!
51
+ @mon_mutex_owner_object_id = nil
52
+ @mon_mutex = nil
53
+ initialize
54
+ end
55
+ end
56
+ end
57
+ end
58
+
38
59
  describe '#new' do
39
60
  let(:saml_response) { File.read(File.join(File.dirname(__FILE__), '../../support', 'response_encrypted_nameid.xml.base64')) }
40
61
 
@@ -113,6 +134,8 @@ describe Devise::SamlSessionsController, type: :controller do
113
134
  end
114
135
 
115
136
  describe '#metadata' do
137
+ let(:saml_config) { Devise.saml_config.dup }
138
+
116
139
  context "with the default configuration" do
117
140
  it 'generates metadata' do
118
141
  get :metadata
@@ -132,7 +155,7 @@ describe Devise::SamlSessionsController, type: :controller do
132
155
  Devise.saml_configure do |settings|
133
156
  settings.assertion_consumer_service_url = "http://localhost:3000/users/saml/auth"
134
157
  settings.assertion_consumer_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
135
- settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
158
+ settings.name_identifier_format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
136
159
  settings.issuer = "http://localhost:3000"
137
160
  end
138
161
  end
@@ -149,23 +172,47 @@ describe Devise::SamlSessionsController, type: :controller do
149
172
  end
150
173
 
151
174
  describe '#destroy' do
175
+ before do
176
+ allow(controller).to receive(:sign_out)
177
+ end
178
+
152
179
  context "when using the default saml config" do
153
- it 'signs out and redirects to the IdP' do
154
- expect(controller).to receive(:sign_out)
180
+ it "signs out and redirects to the IdP" do
155
181
  delete :destroy
182
+ expect(controller).to have_received(:sign_out)
156
183
  expect(response).to redirect_to(%r(\Ahttp://localhost:8009/saml/logout\?SAMLRequest=))
157
184
  end
158
185
  end
159
186
 
187
+ context "when configured to use a non-transient name identifier" do
188
+ before do
189
+ allow(Devise.saml_config).to receive(:name_identifier_format).and_return("urn:oasis:names:tc:SAML:2.0:nameid-format:persistent")
190
+ end
191
+
192
+ it "includes a LogoutRequest with the name identifier and session index", :aggregate_failures do
193
+ controller.current_user = Struct.new(:email, :session_index).new("user@example.com", "sessionindex")
194
+
195
+ actual_settings = nil
196
+ expect_any_instance_of(OneLogin::RubySaml::Logoutrequest).to receive(:create) do |_, settings|
197
+ actual_settings = settings
198
+ "http://localhost:8009/saml/logout"
199
+ end
200
+
201
+ delete :destroy
202
+ expect(actual_settings.name_identifier_value).to eq("user@example.com")
203
+ expect(actual_settings.sessionindex).to eq("sessionindex")
204
+ end
205
+ end
206
+
160
207
  context "with a specified idp" do
161
208
  before do
162
209
  Devise.idp_settings_adapter = idp_providers_adapter
163
210
  end
164
211
 
165
212
  it "redirects to the associated IdP SSO target url" do
166
- expect(controller).to receive(:sign_out)
167
213
  expect(DeviseSamlAuthenticatable::DefaultIdpEntityIdReader).to receive(:entity_id)
168
214
  delete :destroy
215
+ expect(controller).to have_received(:sign_out)
169
216
  expect(response).to redirect_to(%r(\Ahttp://idp_slo_url\?SAMLRequest=))
170
217
  end
171
218
 
@@ -194,8 +241,8 @@ describe Devise::SamlSessionsController, type: :controller do
194
241
  end
195
242
 
196
243
  it "redirects to the associated IdP SLO target url" do
197
- expect(controller).to receive(:sign_out)
198
244
  do_delete
245
+ expect(controller).to have_received(:sign_out)
199
246
  expect(idp_providers_adapter).to have_received(:settings).with("http://www.example.com")
200
247
  expect(response).to redirect_to(%r(\Ahttp://idp_slo_url\?SAMLRequest=))
201
248
  end
@@ -263,12 +310,13 @@ describe Devise::SamlSessionsController, type: :controller do
263
310
  let(:name_id) { '12312312' }
264
311
  before do
265
312
  allow(OneLogin::RubySaml::SloLogoutrequest).to receive(:new).and_return(saml_request)
313
+ allow(User).to receive(:reset_session_key_for)
266
314
  end
267
315
 
268
316
  it 'direct the resource to reset the session key' do
269
- expect(User).to receive(:reset_session_key_for).with(name_id)
270
317
  do_post
271
318
  expect(response).to redirect_to response_url
319
+ expect(User).to have_received(:reset_session_key_for).with(name_id)
272
320
  end
273
321
 
274
322
  context "with a specified idp" do
@@ -285,6 +333,16 @@ describe Devise::SamlSessionsController, type: :controller do
285
333
  end
286
334
  end
287
335
 
336
+ context "with a relay_state lambda defined" do
337
+ let(:relay_state) { ->(request) { "123" } }
338
+
339
+ it "includes the RelayState param in the request to the IdP" do
340
+ expect(Devise).to receive(:saml_relay_state).at_least(:once).and_return(relay_state)
341
+ do_post
342
+ expect(saml_response).to have_received(:create).with(Devise.saml_config, saml_request.id, nil, {RelayState: "123"})
343
+ end
344
+ end
345
+
288
346
  context 'when saml_session_index_key is not configured' do
289
347
  before do
290
348
  Devise.saml_session_index_key = nil