devise_saml_authenticatable 1.5.0 → 1.6.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 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