devise_saml_authenticatable 1.3.1 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
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