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.
- checksums.yaml +5 -5
- data/.gitignore +0 -2
- data/.travis.yml +37 -22
- data/Gemfile +2 -10
- data/README.md +127 -44
- data/app/controllers/devise/saml_sessions_controller.rb +38 -7
- data/devise_saml_authenticatable.gemspec +2 -1
- data/lib/devise_saml_authenticatable.rb +70 -0
- data/lib/devise_saml_authenticatable/default_attribute_map_resolver.rb +26 -0
- data/lib/devise_saml_authenticatable/default_idp_entity_id_reader.rb +10 -2
- data/lib/devise_saml_authenticatable/exception.rb +1 -1
- data/lib/devise_saml_authenticatable/model.rb +20 -32
- data/lib/devise_saml_authenticatable/routes.rb +17 -6
- data/lib/devise_saml_authenticatable/saml_mapped_attributes.rb +38 -0
- data/lib/devise_saml_authenticatable/saml_response.rb +16 -0
- data/lib/devise_saml_authenticatable/strategy.rb +10 -2
- data/lib/devise_saml_authenticatable/version.rb +1 -1
- data/spec/controllers/devise/saml_sessions_controller_spec.rb +118 -11
- data/spec/devise_saml_authenticatable/default_attribute_map_resolver_spec.rb +58 -0
- data/spec/devise_saml_authenticatable/default_idp_entity_id_reader_spec.rb +34 -4
- data/spec/devise_saml_authenticatable/model_spec.rb +199 -5
- data/spec/devise_saml_authenticatable/saml_mapped_attributes_spec.rb +50 -0
- data/spec/devise_saml_authenticatable/strategy_spec.rb +18 -0
- data/spec/features/saml_authentication_spec.rb +45 -21
- data/spec/rails_helper.rb +6 -2
- data/spec/routes/routes_spec.rb +102 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/support/Gemfile.rails4 +24 -6
- data/spec/support/Gemfile.rails5 +25 -0
- data/spec/support/Gemfile.rails5.1 +25 -0
- data/spec/support/Gemfile.rails5.2 +25 -0
- data/spec/support/attribute-map.yml +12 -0
- data/spec/support/attribute_map_resolver.rb.erb +14 -0
- data/spec/support/idp_settings_adapter.rb.erb +5 -5
- data/spec/support/idp_template.rb +8 -1
- data/spec/support/rails_app.rb +110 -16
- data/spec/support/saml_idp_controller.rb.erb +22 -10
- data/spec/support/sp_template.rb +52 -21
- metadata +26 -10
- 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.
|
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(
|
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(
|
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,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
|
-
|
34
|
-
|
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
|
-
|
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 =
|
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
|
-
|
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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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(
|
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(
|
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,31 +1,38 @@
|
|
1
1
|
require 'rails_helper'
|
2
2
|
|
3
|
-
|
4
|
-
|
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
|
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:
|
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:
|
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
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|