devise_saml_authenticatable 1.3.1 → 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.
Files changed (40) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +0 -2
  3. data/.travis.yml +37 -22
  4. data/Gemfile +2 -10
  5. data/README.md +127 -44
  6. data/app/controllers/devise/saml_sessions_controller.rb +38 -7
  7. data/devise_saml_authenticatable.gemspec +2 -1
  8. data/lib/devise_saml_authenticatable.rb +70 -0
  9. data/lib/devise_saml_authenticatable/default_attribute_map_resolver.rb +26 -0
  10. data/lib/devise_saml_authenticatable/default_idp_entity_id_reader.rb +10 -2
  11. data/lib/devise_saml_authenticatable/exception.rb +1 -1
  12. data/lib/devise_saml_authenticatable/model.rb +20 -32
  13. data/lib/devise_saml_authenticatable/routes.rb +17 -6
  14. data/lib/devise_saml_authenticatable/saml_mapped_attributes.rb +38 -0
  15. data/lib/devise_saml_authenticatable/saml_response.rb +16 -0
  16. data/lib/devise_saml_authenticatable/strategy.rb +10 -2
  17. data/lib/devise_saml_authenticatable/version.rb +1 -1
  18. data/spec/controllers/devise/saml_sessions_controller_spec.rb +118 -11
  19. data/spec/devise_saml_authenticatable/default_attribute_map_resolver_spec.rb +58 -0
  20. data/spec/devise_saml_authenticatable/default_idp_entity_id_reader_spec.rb +34 -4
  21. data/spec/devise_saml_authenticatable/model_spec.rb +199 -5
  22. data/spec/devise_saml_authenticatable/saml_mapped_attributes_spec.rb +50 -0
  23. data/spec/devise_saml_authenticatable/strategy_spec.rb +18 -0
  24. data/spec/features/saml_authentication_spec.rb +45 -21
  25. data/spec/rails_helper.rb +6 -2
  26. data/spec/routes/routes_spec.rb +102 -0
  27. data/spec/spec_helper.rb +7 -0
  28. data/spec/support/Gemfile.rails4 +24 -6
  29. data/spec/support/Gemfile.rails5 +25 -0
  30. data/spec/support/Gemfile.rails5.1 +25 -0
  31. data/spec/support/Gemfile.rails5.2 +25 -0
  32. data/spec/support/attribute-map.yml +12 -0
  33. data/spec/support/attribute_map_resolver.rb.erb +14 -0
  34. data/spec/support/idp_settings_adapter.rb.erb +5 -5
  35. data/spec/support/idp_template.rb +8 -1
  36. data/spec/support/rails_app.rb +110 -16
  37. data/spec/support/saml_idp_controller.rb.erb +22 -10
  38. data/spec/support/sp_template.rb +52 -21
  39. metadata +26 -10
  40. data/spec/support/Gemfile.ruby-saml-1.3 +0 -23
@@ -7,6 +7,7 @@ Gem::Specification.new do |gem|
7
7
  gem.description = %q{SAML Authentication for devise}
8
8
  gem.summary = %q{SAML Authentication for devise }
9
9
  gem.homepage = ""
10
+ gem.license = "MIT"
10
11
 
11
12
  gem.files = `git ls-files`.split($\)
12
13
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
@@ -17,5 +18,5 @@ Gem::Specification.new do |gem|
17
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
19
 
19
20
  gem.add_dependency("devise","> 2.0.0")
20
- gem.add_dependency("ruby-saml","~> 1.3")
21
+ gem.add_dependency("ruby-saml","~> 1.7")
21
22
  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
@@ -19,6 +20,10 @@ end
19
20
 
20
21
  # Get saml information from config/saml.yml now
21
22
  module Devise
23
+ # Allow route customization to avoid collision
24
+ mattr_accessor :saml_route_helper_prefix
25
+ @@saml_route_helper_prefix
26
+
22
27
  # Allow logging
23
28
  mattr_accessor :saml_logger
24
29
  @@saml_logger = true
@@ -62,11 +67,76 @@ module Devise
62
67
  mattr_accessor :saml_relay_state
63
68
  @@saml_relay_state
64
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
+
74
+ # Implements a #validate method that takes the retrieved resource and response right after retrieval,
75
+ # and returns true if it's valid. False will cause authentication to fail.
76
+ # Only one of saml_resource_validator and saml_resource_validator_hook may be used.
77
+ mattr_accessor :saml_resource_validator
78
+ @@saml_resource_validator
79
+
80
+ # Proc that determines whether a technically correct SAML response is valid per some custom logic.
81
+ # Receives the user object (or nil, if no match was found), decorated saml_response and
82
+ # auth_value, inspects the combination for acceptability of login (or create+login, if enabled),
83
+ # and returns true if it's valid. False will cause authentication to fail.
84
+ mattr_accessor :saml_resource_validator_hook
85
+ @@saml_resource_validator_hook
86
+
87
+ # Custom value for ruby-saml allowed_clock_drift
88
+ mattr_accessor :allowed_clock_drift_in_seconds
89
+ @@allowed_clock_drift_in_seconds
90
+
65
91
  mattr_accessor :saml_config
66
92
  @@saml_config = OneLogin::RubySaml::Settings.new
67
93
  def self.saml_configure
68
94
  yield saml_config
69
95
  end
96
+
97
+ # Default update resource hook. Updates each attribute on the model that is mapped, updates the
98
+ # saml_default_user_key if saml_use_subject is true and saves the user model.
99
+ # See saml_update_resource_hook for more information.
100
+ mattr_reader :saml_default_update_resource_hook
101
+ @@saml_default_update_resource_hook = Proc.new do |user, saml_response, auth_value|
102
+ saml_response.attributes.resource_keys.each do |key|
103
+ user.send "#{key}=", saml_response.attribute_value_by_resource_key(key)
104
+ end
105
+
106
+ if (Devise.saml_use_subject)
107
+ user.send "#{Devise.saml_default_user_key}=", auth_value
108
+ end
109
+
110
+ user.save!
111
+ end
112
+
113
+ # Proc that is called if Devise.saml_update_user and/or Devise.saml_create_user are true.
114
+ # Receives the user object, saml_response and auth_value, and defines how the object's values are
115
+ # updated with regards to the SAML response. See saml_default_update_resource_hook for an example.
116
+ mattr_accessor :saml_update_resource_hook
117
+ @@saml_update_resource_hook = @@saml_default_update_resource_hook
118
+
119
+ # Default resource locator. Uses saml_default_user_key and auth_value to resolve user.
120
+ # See saml_resource_locator for more information.
121
+ mattr_reader :saml_default_resource_locator
122
+ @@saml_default_resource_locator = Proc.new do |model, saml_response, auth_value|
123
+ model.where(Devise.saml_default_user_key => auth_value).first
124
+ end
125
+
126
+ # Proc that is called to resolve the saml_response and auth_value into the correct user object.
127
+ # Receives a copy of the ActiveRecord::Model, saml_response and auth_value. Is expected to return
128
+ # one instance of the provided model that is the matched account, or nil if none exists.
129
+ # See saml_default_resource_locator above for an example.
130
+ mattr_accessor :saml_resource_locator
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
70
140
  end
71
141
 
72
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
@@ -2,9 +2,17 @@ module DeviseSamlAuthenticatable
2
2
  class DefaultIdpEntityIdReader
3
3
  def self.entity_id(params)
4
4
  if params[:SAMLRequest]
5
- OneLogin::RubySaml::SloLogoutrequest.new(params[:SAMLRequest]).issuer
5
+ OneLogin::RubySaml::SloLogoutrequest.new(
6
+ params[:SAMLRequest],
7
+ settings: Devise.saml_config,
8
+ allowed_clock_drift: Devise.allowed_clock_drift_in_seconds,
9
+ ).issuer
6
10
  elsif params[:SAMLResponse]
7
- OneLogin::RubySaml::Response.new(params[:SAMLResponse]).issuers.first
11
+ OneLogin::RubySaml::Response.new(
12
+ params[:SAMLResponse],
13
+ settings: Devise.saml_config,
14
+ allowed_clock_drift: Devise.allowed_clock_drift_in_seconds,
15
+ ).issuers.first
8
16
  end
9
17
  end
10
18
  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
@@ -1,4 +1,5 @@
1
1
  require 'devise_saml_authenticatable/strategy'
2
+ require 'devise_saml_authenticatable/saml_response'
2
3
 
3
4
  module Devise
4
5
  module Models
@@ -30,16 +31,29 @@ module Devise
30
31
  module ClassMethods
31
32
  def authenticate_with_saml(saml_response, relay_state)
32
33
  key = Devise.saml_default_user_key
33
- attributes = saml_response.attributes
34
- if (Devise.saml_use_subject)
34
+ decorated_response = ::SamlAuthenticatable::SamlResponse.new(
35
+ saml_response,
36
+ Devise.saml_attribute_map_resolver.new(saml_response).attribute_map,
37
+ )
38
+ if Devise.saml_use_subject
35
39
  auth_value = saml_response.name_id
36
40
  else
37
- inv_attr = attribute_map.invert
38
- auth_value = attributes[inv_attr[key.to_s]]
41
+ auth_value = decorated_response.attribute_value_by_resource_key(key)
39
42
  end
40
43
  auth_value.try(:downcase!) if Devise.case_insensitive_keys.include?(key)
41
44
 
42
- resource = where(key => auth_value).first
45
+ resource = Devise.saml_resource_locator.call(self, decorated_response, auth_value)
46
+
47
+ raise "Only one validator configuration can be used at a time" if Devise.saml_resource_validator && Devise.saml_resource_validator_hook
48
+ if Devise.saml_resource_validator || Devise.saml_resource_validator_hook
49
+ valid = if Devise.saml_resource_validator then Devise.saml_resource_validator.new.validate(resource, saml_response)
50
+ else Devise.saml_resource_validator_hook.call(resource, decorated_response, auth_value)
51
+ end
52
+ if !valid
53
+ logger.info("User(#{auth_value}) did not pass custom validation.")
54
+ return nil
55
+ end
56
+ end
43
57
 
44
58
  if resource.nil?
45
59
  if Devise.saml_create_user
@@ -52,11 +66,7 @@ module Devise
52
66
  end
53
67
 
54
68
  if Devise.saml_update_user || (resource.new_record? && Devise.saml_create_user)
55
- set_user_saml_attributes(resource, attributes)
56
- if (Devise.saml_use_subject)
57
- resource.send "#{key}=", auth_value
58
- end
59
- resource.save!
69
+ Devise.saml_update_resource_hook.call(resource, decorated_response, auth_value)
60
70
  end
61
71
 
62
72
  resource
@@ -70,28 +80,6 @@ module Devise
70
80
  def find_for_shibb_authentication(conditions)
71
81
  find_for_authentication(conditions)
72
82
  end
73
-
74
- def attribute_map
75
- @attribute_map ||= attribute_map_for_environment
76
- end
77
-
78
- private
79
-
80
- def set_user_saml_attributes(user,attributes)
81
- attribute_map.each do |k,v|
82
- Rails.logger.info "Setting: #{v}, #{attributes[k]}"
83
- user.send "#{v}=", attributes[k]
84
- end
85
- end
86
-
87
- def attribute_map_for_environment
88
- attribute_map = YAML.load(File.read("#{Rails.root}/config/attribute-map.yml"))
89
- if attribute_map.has_key?(Rails.env)
90
- attribute_map[Rails.env]
91
- else
92
- attribute_map
93
- end
94
- end
95
83
  end
96
84
  end
97
85
  end
@@ -1,12 +1,23 @@
1
1
  ActionDispatch::Routing::Mapper.class_eval do
2
2
  protected
3
3
  def devise_saml_authenticatable(mapping, controllers)
4
- resource :session, :only => [], :controller => controllers[:saml_sessions], :path => "" do
5
- get :new, :path => "saml/sign_in", :as => "new"
6
- post :create, :path=>"saml/auth"
7
- match :destroy, :path => mapping.path_names[:sign_out], :as => "destroy", :via => mapping.sign_out_via
8
- get :metadata, :path=>"saml/metadata"
9
- match :idp_sign_out, :path=>"saml/idp_sign_out", via: [:get, :post]
4
+ if ::Devise.saml_route_helper_prefix
5
+ prefix = ::Devise.saml_route_helper_prefix
6
+ resource :session, only: [], controller: controllers[:saml_sessions], path: '' do
7
+ get :new, path: 'saml/sign_in', as: "new_#{prefix}"
8
+ post :create, path: 'saml/auth', as: prefix
9
+ match :destroy, path: mapping.path_names[:sign_out], as: "destroy_#{prefix}", via: mapping.sign_out_via
10
+ get :metadata, path: 'saml/metadata'
11
+ match :idp_sign_out, path: 'saml/idp_sign_out', as: "idp_destroy_#{prefix}", via: [:get, :post]
12
+ end
13
+ else
14
+ resource :session, only: [], controller: controllers[:saml_sessions], path: '' do
15
+ get :new, path: 'saml/sign_in', as: 'new'
16
+ post :create, path: 'saml/auth'
17
+ match :destroy, path: mapping.path_names[:sign_out], as: 'destroy', via: mapping.sign_out_via
18
+ get :metadata, path: 'saml/metadata'
19
+ match :idp_sign_out, path: 'saml/idp_sign_out', via: [:get, :post]
20
+ end
10
21
  end
11
22
  end
12
23
  end
@@ -0,0 +1,38 @@
1
+ module SamlAuthenticatable
2
+ class SamlMappedAttributes
3
+ def initialize(attributes, attribute_map)
4
+ @attributes = attributes
5
+ @attribute_map = attribute_map
6
+ end
7
+
8
+ def saml_attribute_keys
9
+ @attribute_map.keys
10
+ end
11
+
12
+ def resource_keys
13
+ @attribute_map.values
14
+ end
15
+
16
+ def value_by_resource_key(key)
17
+ str_key = String(key)
18
+
19
+ # Find all of the SAML attributes that map to the resource key
20
+ attribute_map_for_key = @attribute_map.select { |_, resource_key| String(resource_key) == str_key }
21
+
22
+ saml_value = nil
23
+
24
+ # Find the first non-nil value
25
+ attribute_map_for_key.each_key do |saml_key|
26
+ saml_value = value_by_saml_attribute_key(saml_key)
27
+
28
+ break unless saml_value.nil?
29
+ end
30
+
31
+ saml_value
32
+ end
33
+
34
+ def value_by_saml_attribute_key(key)
35
+ @attributes[String(key)]
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,16 @@
1
+ require 'devise_saml_authenticatable/saml_mapped_attributes'
2
+
3
+ module SamlAuthenticatable
4
+ class SamlResponse
5
+ attr_reader :raw_response, :attributes
6
+
7
+ def initialize(saml_response, attribute_map)
8
+ @attributes = ::SamlAuthenticatable::SamlMappedAttributes.new(saml_response.attributes, attribute_map)
9
+ @raw_response = saml_response
10
+ end
11
+
12
+ def attribute_value_by_resource_key(key)
13
+ attributes.value_by_resource_key(key)
14
+ end
15
+ end
16
+ end
@@ -6,7 +6,11 @@ module Devise
6
6
  include DeviseSamlAuthenticatable::SamlConfig
7
7
  def valid?
8
8
  if params[:SAMLResponse]
9
- OneLogin::RubySaml::Response.new(params[:SAMLResponse])
9
+ OneLogin::RubySaml::Response.new(
10
+ params[:SAMLResponse],
11
+ settings: saml_config(get_idp_entity_id(params)),
12
+ allowed_clock_drift: Devise.allowed_clock_drift_in_seconds,
13
+ )
10
14
  else
11
15
  false
12
16
  end
@@ -30,7 +34,11 @@ module Devise
30
34
 
31
35
  private
32
36
  def parse_saml_response
33
- @response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], settings: saml_config(get_idp_entity_id(params)))
37
+ @response = OneLogin::RubySaml::Response.new(
38
+ params[:SAMLResponse],
39
+ settings: saml_config(get_idp_entity_id(params)),
40
+ allowed_clock_drift: Devise.allowed_clock_drift_in_seconds,
41
+ )
34
42
  unless @response.is_valid?
35
43
  failed_auth("Auth errors: #{@response.errors.join(', ')}")
36
44
  end
@@ -1,3 +1,3 @@
1
1
  module DeviseSamlAuthenticatable
2
- VERSION = "1.3.1"
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
 
@@ -66,6 +87,7 @@ describe Devise::SamlSessionsController, type: :controller do
66
87
  it "uses the DefaultIdpEntityIdReader" do
67
88
  expect(DeviseSamlAuthenticatable::DefaultIdpEntityIdReader).to receive(:entity_id)
68
89
  do_get
90
+ expect(idp_providers_adapter).to have_received(:settings).with(nil)
69
91
  end
70
92
 
71
93
  context "with a relay_state lambda defined" do
@@ -104,6 +126,7 @@ describe Devise::SamlSessionsController, type: :controller do
104
126
 
105
127
  it "redirects to the associated IdP SSO target url" do
106
128
  do_get
129
+ expect(idp_providers_adapter).to have_received(:settings).with("http://www.example.com")
107
130
  expect(response).to redirect_to(%r(\Ahttp://idp_sso_url\?SAMLRequest=))
108
131
  end
109
132
  end
@@ -111,6 +134,8 @@ describe Devise::SamlSessionsController, type: :controller do
111
134
  end
112
135
 
113
136
  describe '#metadata' do
137
+ let(:saml_config) { Devise.saml_config.dup }
138
+
114
139
  context "with the default configuration" do
115
140
  it 'generates metadata' do
116
141
  get :metadata
@@ -130,7 +155,7 @@ describe Devise::SamlSessionsController, type: :controller do
130
155
  Devise.saml_configure do |settings|
131
156
  settings.assertion_consumer_service_url = "http://localhost:3000/users/saml/auth"
132
157
  settings.assertion_consumer_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
133
- 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"
134
159
  settings.issuer = "http://localhost:3000"
135
160
  end
136
161
  end
@@ -147,10 +172,81 @@ describe Devise::SamlSessionsController, type: :controller do
147
172
  end
148
173
 
149
174
  describe '#destroy' do
150
- it 'signs out and redirects to the IdP' do
151
- expect(controller).to receive(:sign_out)
152
- delete :destroy
153
- expect(response).to redirect_to(%r(\Ahttp://localhost:8009/saml/logout\?SAMLRequest=))
175
+ before do
176
+ allow(controller).to receive(:sign_out)
177
+ end
178
+
179
+ context "when using the default saml config" do
180
+ it "signs out and redirects to the IdP" do
181
+ delete :destroy
182
+ expect(controller).to have_received(:sign_out)
183
+ expect(response).to redirect_to(%r(\Ahttp://localhost:8009/saml/logout\?SAMLRequest=))
184
+ end
185
+ end
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
+
207
+ context "with a specified idp" do
208
+ before do
209
+ Devise.idp_settings_adapter = idp_providers_adapter
210
+ end
211
+
212
+ it "redirects to the associated IdP SSO target url" do
213
+ expect(DeviseSamlAuthenticatable::DefaultIdpEntityIdReader).to receive(:entity_id)
214
+ delete :destroy
215
+ expect(controller).to have_received(:sign_out)
216
+ expect(response).to redirect_to(%r(\Ahttp://idp_slo_url\?SAMLRequest=))
217
+ end
218
+
219
+ context "with a specified idp entity id reader" do
220
+ class OurIdpEntityIdReader
221
+ def self.entity_id(params)
222
+ params[:entity_id]
223
+ end
224
+ end
225
+
226
+ subject(:do_delete) {
227
+ if Rails::VERSION::MAJOR > 4
228
+ delete :destroy, params: {entity_id: "http://www.example.com"}
229
+ else
230
+ delete :destroy, entity_id: "http://www.example.com"
231
+ end
232
+ }
233
+
234
+ before do
235
+ @default_reader = Devise.idp_entity_id_reader
236
+ Devise.idp_entity_id_reader = OurIdpEntityIdReader # which will have some different behavior
237
+ end
238
+
239
+ after do
240
+ Devise.idp_entity_id_reader = @default_reader
241
+ end
242
+
243
+ it "redirects to the associated IdP SLO target url" do
244
+ do_delete
245
+ expect(controller).to have_received(:sign_out)
246
+ expect(idp_providers_adapter).to have_received(:settings).with("http://www.example.com")
247
+ expect(response).to redirect_to(%r(\Ahttp://idp_slo_url\?SAMLRequest=))
248
+ end
249
+ end
154
250
  end
155
251
  end
156
252
 
@@ -214,12 +310,13 @@ describe Devise::SamlSessionsController, type: :controller do
214
310
  let(:name_id) { '12312312' }
215
311
  before do
216
312
  allow(OneLogin::RubySaml::SloLogoutrequest).to receive(:new).and_return(saml_request)
313
+ allow(User).to receive(:reset_session_key_for)
217
314
  end
218
315
 
219
316
  it 'direct the resource to reset the session key' do
220
- expect(User).to receive(:reset_session_key_for).with(name_id)
221
317
  do_post
222
318
  expect(response).to redirect_to response_url
319
+ expect(User).to have_received(:reset_session_key_for).with(name_id)
223
320
  end
224
321
 
225
322
  context "with a specified idp" do
@@ -236,6 +333,16 @@ describe Devise::SamlSessionsController, type: :controller do
236
333
  end
237
334
  end
238
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
+
239
346
  context 'when saml_session_index_key is not configured' do
240
347
  before do
241
348
  Devise.saml_session_index_key = nil