devise_saml_authenticatable 1.2.2 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +8 -0
- data/README.md +56 -1
- data/app/controllers/devise/saml_sessions_controller.rb +5 -4
- data/devise_saml_authenticatable.gemspec +1 -1
- data/lib/devise_saml_authenticatable.rb +15 -0
- data/lib/devise_saml_authenticatable/default_idp_entity_id_reader.rb +11 -0
- data/lib/devise_saml_authenticatable/model.rb +0 -14
- data/lib/devise_saml_authenticatable/saml_config.rb +28 -5
- data/lib/devise_saml_authenticatable/strategy.rb +3 -3
- data/lib/devise_saml_authenticatable/version.rb +1 -1
- data/spec/controllers/devise/saml_sessions_controller_spec.rb +113 -18
- data/spec/devise_saml_authenticatable/default_idp_entity_id_reader_spec.rb +23 -0
- data/spec/devise_saml_authenticatable/saml_config_spec.rb +62 -10
- data/spec/devise_saml_authenticatable/strategy_spec.rb +58 -16
- data/spec/features/saml_authentication_spec.rb +56 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/support/idp_settings_adapter.rb.erb +19 -0
- data/spec/support/idp_template.rb +14 -0
- data/spec/support/response_encrypted_nameid.xml.base64 +1 -0
- data/spec/support/saml_idp_controller.rb.erb +12 -3
- data/spec/support/sp_template.rb +46 -0
- metadata +11 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 811f4c9c0306a1c5ba28080ac773ba88022b79cc
|
4
|
+
data.tar.gz: f2571561eb9c3ba05832e65d1b8ab15d3c9c408c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c21a2ac40153fcb51c843fcdfa081ba2caab761c94a43cf1ebe297d1260021fc54409fe6cdac9cc3c6340db077ba3c8d2cb15e2f83a9af8886cff90ac128cb58
|
7
|
+
data.tar.gz: dc0c2d6f2174e0f5bef4a6b4590ed90558741a8a86af2754bb5c1094d039c29cd9da726f804bbd78a1693023ac4f50ad81d095956ad728272144d4977bb25eec
|
data/Gemfile
CHANGED
@@ -10,4 +10,12 @@ group :test do
|
|
10
10
|
gem 'rspec-rails'
|
11
11
|
gem 'sqlite3'
|
12
12
|
gem 'capybara-webkit'
|
13
|
+
|
14
|
+
# Lock down versions of gems for older versions of Ruby
|
15
|
+
if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.0")
|
16
|
+
gem 'mime-types', '~> 2.99'
|
17
|
+
end
|
18
|
+
if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
|
19
|
+
gem 'devise', '~> 3.5'
|
20
|
+
end
|
13
21
|
end
|
data/README.md
CHANGED
@@ -56,6 +56,18 @@ In config/initializers/devise.rb
|
|
56
56
|
# If you don't set it then email will be extracted from SAML assertation attributes
|
57
57
|
config.saml_use_subject = true
|
58
58
|
|
59
|
+
# You can support multiple IdPs by setting this value to a class that implements a #settings method which takes
|
60
|
+
# an IdP entity id as an argument and returns a hash of idp settings for the corresponding IdP.
|
61
|
+
config.idp_settings_adapter = nil
|
62
|
+
|
63
|
+
# You provide you own method to find the idp_entity_id in a SAML message in the case of multiple IdPs
|
64
|
+
# by setting this to a custom reader class, or use the default.
|
65
|
+
# config.idp_entity_id_reader = DeviseSamlAuthenticatable::DefaultIdpEntityIdReader
|
66
|
+
|
67
|
+
# You can set a handler object that takes the response for a failed SAML request and the strategy,
|
68
|
+
# and implements a #handle method. This method can then redirect the user, return error messages, etc.
|
69
|
+
# config.saml_failed_callback = nil
|
70
|
+
|
59
71
|
# Configure with your SAML settings (see [ruby-saml][] for more information).
|
60
72
|
config.saml_configure do |settings|
|
61
73
|
settings.assertion_consumer_service_url = "http://localhost:3000/users/saml/auth"
|
@@ -106,12 +118,55 @@ You are now ready to test it against an IdP.
|
|
106
118
|
When the user goes to `/users/saml/sign_in` he will be redirected to the login page of the IdP.
|
107
119
|
Upon successful login the user is redirected to devise `user_root_path`.
|
108
120
|
|
121
|
+
## Supporting Multiple IdPs
|
122
|
+
|
123
|
+
If you must support multiple Identity Providers you can implement an adapter class with a `#settings` method that takes an IdP entity id and returns a hash of settings for the corresponding IdP. The `config.idp_settings_adapter` then must be set to point to your adapter in config/initializers/devise.rb. The implementation of the adapter is up to you. A simple example may look like this:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
class IdPSettingsAdapter
|
127
|
+
def self.settings(idp_entity_id)
|
128
|
+
case idp_entity_id
|
129
|
+
when "http://www.example_idp_entity_id.com"
|
130
|
+
{
|
131
|
+
assertion_consumer_service_url: "http://localhost:3000/users/saml/auth",
|
132
|
+
assertion_consumer_service_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
133
|
+
name_identifier_format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
|
134
|
+
issuer: "http://localhost:3000/saml/metadata",
|
135
|
+
idp_entity_id: "http://www.example_idp_entity_id.com",
|
136
|
+
authn_context: "",
|
137
|
+
idp_slo_target_url: "http://example_idp_slo_target_url.com",
|
138
|
+
idp_sso_target_url: "http://example_idp_sso_target_url.com",
|
139
|
+
idp_cert: "example_idp_cert"
|
140
|
+
}
|
141
|
+
when "http://www.another_idp_entity_id.biz"
|
142
|
+
{
|
143
|
+
assertion_consumer_service_url: "http://localhost:3000/users/saml/auth",
|
144
|
+
assertion_consumer_service_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
145
|
+
name_identifier_format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
|
146
|
+
issuer: "http://localhost:3000/saml/metadata",
|
147
|
+
idp_entity_id: "http://www.another_idp_entity_id.biz",
|
148
|
+
authn_context: "",
|
149
|
+
idp_slo_target_url: "http://another_idp_slo_target_url.com",
|
150
|
+
idp_sso_target_url: "http://another_idp_sso_target_url.com",
|
151
|
+
idp_cert: "another_idp_cert"
|
152
|
+
}
|
153
|
+
else
|
154
|
+
{}
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
Detecting the entity ID passed to the `settings` method is done by `config.idp_entity_id_reader`.
|
161
|
+
By default this will find the `Issuer` in the SAML request.
|
162
|
+
You can support more use cases by writing your own and implementing the `.entity_id` method.
|
163
|
+
|
109
164
|
## Identity Provider
|
110
165
|
|
111
166
|
If you don't have an identity provider an you would like to test the authentication against your app there are some options:
|
112
167
|
|
113
168
|
1. Use [ruby-saml-idp](https://github.com/lawrencepit/ruby-saml-idp). You can add your own logic to your IdP, or you can also set it as a dummy IdP that always sends a valid authentication response to your app.
|
114
|
-
2. Use an online service that can act as an IdP. Onelogin, Salesforce and some others provide you with this functionality
|
169
|
+
2. Use an online service that can act as an IdP. Onelogin, Salesforce, Okta and some others provide you with this functionality
|
115
170
|
3. Install your own IdP.
|
116
171
|
|
117
172
|
There are numerous IdPs that support SAML 2.0, there are propietary (like Microsoft ADFS 2.0 or Ping federate) and there are also open source solutions like Shibboleth and simplesamlphp.
|
@@ -6,8 +6,9 @@ class Devise::SamlSessionsController < Devise::SessionsController
|
|
6
6
|
skip_before_filter :verify_authenticity_token
|
7
7
|
|
8
8
|
def new
|
9
|
+
idp_entity_id = get_idp_entity_id(params)
|
9
10
|
request = OneLogin::RubySaml::Authrequest.new
|
10
|
-
action = request.create(saml_config)
|
11
|
+
action = request.create(saml_config(idp_entity_id))
|
11
12
|
redirect_to action
|
12
13
|
end
|
13
14
|
|
@@ -18,10 +19,11 @@ class Devise::SamlSessionsController < Devise::SessionsController
|
|
18
19
|
|
19
20
|
def idp_sign_out
|
20
21
|
if params[:SAMLRequest] && Devise.saml_session_index_key
|
22
|
+
saml_config = saml_config(get_idp_entity_id(params))
|
21
23
|
logout_request = OneLogin::RubySaml::SloLogoutrequest.new(params[:SAMLRequest], settings: saml_config)
|
22
24
|
resource_class.reset_session_key_for(logout_request.name_id)
|
23
25
|
|
24
|
-
redirect_to generate_idp_logout_response(logout_request)
|
26
|
+
redirect_to generate_idp_logout_response(saml_config, logout_request.id)
|
25
27
|
elsif params[:SAMLResponse]
|
26
28
|
#Currently Devise handles the session invalidation when the request is made.
|
27
29
|
#To support a true SP initiated logout response, the request ID would have to be tracked and session invalidated
|
@@ -44,8 +46,7 @@ class Devise::SamlSessionsController < Devise::SessionsController
|
|
44
46
|
request.create(saml_config)
|
45
47
|
end
|
46
48
|
|
47
|
-
def generate_idp_logout_response(
|
48
|
-
logout_request_id = logout_request.id
|
49
|
+
def generate_idp_logout_response(saml_config, logout_request_id)
|
49
50
|
OneLogin::RubySaml::SloLogoutresponse.new.create(saml_config, logout_request_id, nil)
|
50
51
|
end
|
51
52
|
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_idp_entity_id_reader"
|
8
9
|
|
9
10
|
begin
|
10
11
|
Rails::Engine
|
@@ -36,12 +37,26 @@ module Devise
|
|
36
37
|
mattr_accessor :saml_use_subject
|
37
38
|
@@saml_use_subject
|
38
39
|
|
40
|
+
# Key used to index sessions for later retrieval
|
39
41
|
mattr_accessor :saml_session_index_key
|
40
42
|
@@saml_session_index_key
|
41
43
|
|
44
|
+
# Redirect after signout (redirects to 'users/saml/sign_in' by default)
|
42
45
|
mattr_accessor :saml_sign_out_success_url
|
43
46
|
@@saml_sign_out_success_url
|
44
47
|
|
48
|
+
# Adapter for multiple IdP support
|
49
|
+
mattr_accessor :idp_settings_adapter
|
50
|
+
@@idp_settings_adapter
|
51
|
+
|
52
|
+
# Reader that can parse entity id from a SAMLMessage
|
53
|
+
mattr_accessor :idp_entity_id_reader
|
54
|
+
@@idp_entity_id_reader ||= ::DeviseSamlAuthenticatable::DefaultIdpEntityIdReader
|
55
|
+
|
56
|
+
# Implements a #handle method that takes the response and strategy as an argument
|
57
|
+
mattr_accessor :saml_failed_callback
|
58
|
+
@@saml_failed_callback
|
59
|
+
|
45
60
|
mattr_accessor :saml_config
|
46
61
|
@@saml_config = OneLogin::RubySaml::Settings.new
|
47
62
|
def self.saml_configure
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module DeviseSamlAuthenticatable
|
2
|
+
class DefaultIdpEntityIdReader
|
3
|
+
def self.entity_id(params)
|
4
|
+
if params[:SAMLRequest]
|
5
|
+
OneLogin::RubySaml::SloLogoutrequest.new(params[:SAMLRequest]).issuer
|
6
|
+
elsif params[:SAMLResponse]
|
7
|
+
OneLogin::RubySaml::Response.new(params[:SAMLResponse]).issuers.first
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -11,19 +11,6 @@ module Devise
|
|
11
11
|
attr_accessor :password_confirmation
|
12
12
|
end
|
13
13
|
|
14
|
-
def update_with_password(params={})
|
15
|
-
params.delete(:current_password)
|
16
|
-
self.update_without_password(params)
|
17
|
-
end
|
18
|
-
|
19
|
-
def update_without_password(params={})
|
20
|
-
params.delete(:password)
|
21
|
-
params.delete(:password_confirmation)
|
22
|
-
|
23
|
-
result = update_attributes(params)
|
24
|
-
result
|
25
|
-
end
|
26
|
-
|
27
14
|
def after_saml_authentication(session_index)
|
28
15
|
if Devise.saml_session_index_key && self.respond_to?(Devise.saml_session_index_key)
|
29
16
|
self.update_attribute(Devise.saml_session_index_key, session_index)
|
@@ -41,7 +28,6 @@ module Devise
|
|
41
28
|
end
|
42
29
|
|
43
30
|
module ClassMethods
|
44
|
-
include DeviseSamlAuthenticatable::SamlConfig
|
45
31
|
def authenticate_with_saml(saml_response)
|
46
32
|
key = Devise.saml_default_user_key
|
47
33
|
attributes = saml_response.attributes
|
@@ -1,16 +1,39 @@
|
|
1
1
|
require 'ruby-saml'
|
2
2
|
module DeviseSamlAuthenticatable
|
3
3
|
module SamlConfig
|
4
|
-
def saml_config
|
5
|
-
return
|
4
|
+
def saml_config(idp_entity_id = nil)
|
5
|
+
return file_based_config if file_based_config
|
6
|
+
return adapter_based_config(idp_entity_id) if Devise.idp_settings_adapter
|
6
7
|
|
8
|
+
Devise.saml_config
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def file_based_config
|
14
|
+
return @file_based_config if @file_based_config
|
7
15
|
idp_config_path = "#{Rails.root}/config/idp.yml"
|
8
|
-
|
16
|
+
|
9
17
|
if File.exists?(idp_config_path)
|
10
|
-
|
18
|
+
@file_based_config ||= OneLogin::RubySaml::Settings.new(YAML.load(File.read(idp_config_path))[Rails.env])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def adapter_based_config(idp_entity_id)
|
23
|
+
config = Marshal.load(Marshal.dump(Devise.saml_config))
|
24
|
+
|
25
|
+
Devise.idp_settings_adapter.settings(idp_entity_id).each do |k,v|
|
26
|
+
acc = "#{k.to_s}=".to_sym
|
27
|
+
|
28
|
+
if config.respond_to? acc
|
29
|
+
config.send(acc, v)
|
30
|
+
end
|
11
31
|
end
|
32
|
+
config
|
33
|
+
end
|
12
34
|
|
13
|
-
|
35
|
+
def get_idp_entity_id(params)
|
36
|
+
Devise.idp_entity_id_reader.entity_id(params)
|
14
37
|
end
|
15
38
|
end
|
16
39
|
end
|
@@ -6,21 +6,21 @@ module Devise
|
|
6
6
|
include DeviseSamlAuthenticatable::SamlConfig
|
7
7
|
def valid?
|
8
8
|
if params[:SAMLResponse]
|
9
|
-
|
10
|
-
!(response.response.include? 'LogoutResponse')
|
9
|
+
OneLogin::RubySaml::Response.new(params[:SAMLResponse])
|
11
10
|
else
|
12
11
|
false
|
13
12
|
end
|
14
13
|
end
|
15
14
|
|
16
15
|
def authenticate!
|
17
|
-
@response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], settings: saml_config)
|
16
|
+
@response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], settings: saml_config(get_idp_entity_id(params)))
|
18
17
|
resource = mapping.to.authenticate_with_saml(@response)
|
19
18
|
if @response.is_valid? && resource
|
20
19
|
resource.after_saml_authentication(@response.sessionindex)
|
21
20
|
success!(resource)
|
22
21
|
else
|
23
22
|
fail!(:invalid)
|
23
|
+
Devise.saml_failed_callback.new.handle(@response, self) if Devise.saml_failed_callback
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
@@ -17,25 +17,106 @@ end
|
|
17
17
|
|
18
18
|
require_relative '../../../app/controllers/devise/saml_sessions_controller'
|
19
19
|
|
20
|
-
|
21
20
|
describe Devise::SamlSessionsController, type: :controller do
|
22
21
|
let(:saml_config) { Devise.saml_config }
|
22
|
+
let(:idp_providers_adapter) { spy("Stub IDPSettings Adaptor") }
|
23
|
+
|
24
|
+
before do
|
25
|
+
allow(idp_providers_adapter).to receive(:settings).and_return({
|
26
|
+
assertion_consumer_service_url: "acs_url",
|
27
|
+
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",
|
29
|
+
issuer: "sp_issuer",
|
30
|
+
idp_entity_id: "http://www.example.com",
|
31
|
+
authn_context: "",
|
32
|
+
idp_slo_target_url: "http://idp_slo_url",
|
33
|
+
idp_sso_target_url: "http://idp_sso_url",
|
34
|
+
idp_cert: "idp_cert"
|
35
|
+
})
|
36
|
+
end
|
23
37
|
|
24
38
|
describe '#new' do
|
25
|
-
|
26
|
-
|
27
|
-
|
39
|
+
let(:saml_response) { File.read(File.join(File.dirname(__FILE__), '../../support', 'response_encrypted_nameid.xml.base64')) }
|
40
|
+
|
41
|
+
context "when using the default saml config" do
|
42
|
+
it "redirects to the IdP SSO target url" do
|
43
|
+
get :new, "SAMLResponse" => saml_response
|
44
|
+
expect(response).to redirect_to(%r(\Ahttp://localhost:8009/saml/auth\?SAMLRequest=))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "with a specified idp" do
|
49
|
+
before do
|
50
|
+
Devise.idp_settings_adapter = idp_providers_adapter
|
51
|
+
end
|
52
|
+
|
53
|
+
it "redirects to the associated IdP SSO target url" do
|
54
|
+
get :new, "SAMLResponse" => saml_response
|
55
|
+
expect(response).to redirect_to(%r(\Ahttp://idp_sso_url\?SAMLRequest=))
|
56
|
+
end
|
57
|
+
|
58
|
+
it "uses the DefaultIdpEntityIdReader" do
|
59
|
+
expect(DeviseSamlAuthenticatable::DefaultIdpEntityIdReader).to receive(:entity_id)
|
60
|
+
get :new, "SAMLResponse" => saml_response
|
61
|
+
end
|
62
|
+
|
63
|
+
context "with a specified idp entity id reader" do
|
64
|
+
class OurIdpEntityIdReader
|
65
|
+
def self.entity_id(params)
|
66
|
+
params[:entity_id]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
before do
|
71
|
+
@default_reader = Devise.idp_entity_id_reader
|
72
|
+
Devise.idp_entity_id_reader = OurIdpEntityIdReader # which will have some different behavior
|
73
|
+
end
|
74
|
+
|
75
|
+
after do
|
76
|
+
Devise.idp_entity_id_reader = @default_reader
|
77
|
+
end
|
78
|
+
|
79
|
+
it "redirects to the associated IdP SSO target url" do
|
80
|
+
get :new, entity_id: "http://www.example.com"
|
81
|
+
expect(response).to redirect_to(%r(\Ahttp://idp_sso_url\?SAMLRequest=))
|
82
|
+
end
|
83
|
+
end
|
28
84
|
end
|
29
85
|
end
|
30
86
|
|
31
87
|
describe '#metadata' do
|
32
|
-
|
33
|
-
|
88
|
+
context "with the default configuration" do
|
89
|
+
it 'generates metadata' do
|
90
|
+
get :metadata
|
91
|
+
|
92
|
+
# Remove ID that can vary across requests
|
93
|
+
expected_metadata = OneLogin::RubySaml::Metadata.new.generate(saml_config)
|
94
|
+
metadata_pattern = Regexp.escape(expected_metadata).gsub(/ ID='[^']+'/, " ID='[\\w-]+'")
|
95
|
+
expect(response.body).to match(Regexp.new(metadata_pattern))
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context "with a specified IDP" do
|
100
|
+
let(:saml_config) { controller.saml_config("anything") }
|
101
|
+
|
102
|
+
before do
|
103
|
+
Devise.idp_settings_adapter = idp_providers_adapter
|
104
|
+
Devise.saml_configure do |settings|
|
105
|
+
settings.assertion_consumer_service_url = "http://localhost:3000/users/saml/auth"
|
106
|
+
settings.assertion_consumer_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
107
|
+
settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
|
108
|
+
settings.issuer = "http://localhost:3000"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
it "generates the same service metadata" do
|
113
|
+
get :metadata
|
34
114
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
115
|
+
# Remove ID that can vary across requests
|
116
|
+
expected_metadata = OneLogin::RubySaml::Metadata.new.generate(saml_config)
|
117
|
+
metadata_pattern = Regexp.escape(expected_metadata).gsub(/ ID='[^']+'/, " ID='[\\w-]+'")
|
118
|
+
expect(response.body).to match(Regexp.new(metadata_pattern))
|
119
|
+
end
|
39
120
|
end
|
40
121
|
end
|
41
122
|
|
@@ -49,18 +130,18 @@ describe Devise::SamlSessionsController, type: :controller do
|
|
49
130
|
|
50
131
|
describe '#idp_sign_out' do
|
51
132
|
let(:name_id) { '12312312' }
|
52
|
-
let(:saml_request) { double(:
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
133
|
+
let(:saml_request) { double(:slo_logoutrequest, {
|
134
|
+
id: 42,
|
135
|
+
name_id: name_id,
|
136
|
+
issuer: "http://www.example.com"
|
137
|
+
}) }
|
138
|
+
let(:saml_response) { double(:slo_logoutresponse) }
|
57
139
|
let(:response_url) { 'http://localhost/logout_response' }
|
58
140
|
|
59
|
-
|
60
141
|
before do
|
61
142
|
allow(OneLogin::RubySaml::SloLogoutrequest).to receive(:new).and_return(saml_request)
|
62
|
-
allow(OneLogin::RubySaml::SloLogoutresponse).to receive(:new).and_return(
|
63
|
-
allow(
|
143
|
+
allow(OneLogin::RubySaml::SloLogoutresponse).to receive(:new).and_return(saml_response)
|
144
|
+
allow(saml_response).to receive(:create).and_return(response_url)
|
64
145
|
end
|
65
146
|
|
66
147
|
it 'returns invalid request if SAMLRequest is not passed' do
|
@@ -75,6 +156,20 @@ describe Devise::SamlSessionsController, type: :controller do
|
|
75
156
|
expect(response).to redirect_to '/users/saml/sign_in'
|
76
157
|
end
|
77
158
|
|
159
|
+
context "with a specified idp" do
|
160
|
+
let(:idp_entity_id) { "http://www.example.com" }
|
161
|
+
before do
|
162
|
+
Devise.idp_settings_adapter = idp_providers_adapter
|
163
|
+
end
|
164
|
+
|
165
|
+
it "accepts a LogoutResponse for the associated slo_target_url and redirects to sign_in" do
|
166
|
+
post :idp_sign_out, SAMLRequest: "stubbed_logout_request"
|
167
|
+
expect(response.status).to eq 302
|
168
|
+
expect(idp_providers_adapter).to have_received(:settings).with(idp_entity_id)
|
169
|
+
expect(response).to redirect_to "http://localhost/logout_response"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
78
173
|
context 'when saml_sign_out_success_url is configured' do
|
79
174
|
let(:test_url) { '/test/url' }
|
80
175
|
before do
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe DeviseSamlAuthenticatable::DefaultIdpEntityIdReader do
|
4
|
+
describe ".entity_id" do
|
5
|
+
context "when there is a SAMLRequest in the params" do
|
6
|
+
let(:params) { {SAMLRequest: "logout request"} }
|
7
|
+
let(:slo_logout_request) { double('slo_logout_request', issuer: 'meow')}
|
8
|
+
it "uses an OneLogin::RubySaml::SloLogoutrequest to get the idp_entity_id" do
|
9
|
+
expect(OneLogin::RubySaml::SloLogoutrequest).to receive(:new).and_return(slo_logout_request)
|
10
|
+
described_class.entity_id(params)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context "when there is a SAMLResponse in the params" do
|
15
|
+
let(:params) { {SAMLResponse: "auth response"} }
|
16
|
+
let(:response) { double('response', issuers: ['meow'] )}
|
17
|
+
it "uses an OneLogin::RubySaml::Response to get the idp_entity_id" do
|
18
|
+
expect(OneLogin::RubySaml::Response).to receive(:new).and_return(response)
|
19
|
+
described_class.entity_id(params)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,17 +1,9 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe DeviseSamlAuthenticatable::SamlConfig do
|
4
|
-
|
4
|
+
let(:saml_config) { controller.saml_config }
|
5
5
|
let(:controller) { Class.new { include DeviseSamlAuthenticatable::SamlConfig }.new }
|
6
6
|
|
7
|
-
# Replace global config since this test changes it
|
8
|
-
before do
|
9
|
-
@original_saml_config = Devise.saml_config
|
10
|
-
end
|
11
|
-
after do
|
12
|
-
Devise.saml_config = @original_saml_config
|
13
|
-
end
|
14
|
-
|
15
7
|
context "when config/idp.yml does not exist" do
|
16
8
|
before do
|
17
9
|
allow(Rails).to receive(:root).and_return("/railsroot")
|
@@ -25,6 +17,66 @@ describe DeviseSamlAuthenticatable::SamlConfig do
|
|
25
17
|
expect(saml_config).to be(Devise.saml_config)
|
26
18
|
expect(saml_config.assertion_consumer_logout_service_binding).to eq('test')
|
27
19
|
end
|
20
|
+
|
21
|
+
context "when the idp_providers_adapter key exists" do
|
22
|
+
before do
|
23
|
+
Devise.idp_settings_adapter = idp_providers_adapter
|
24
|
+
end
|
25
|
+
|
26
|
+
let(:saml_config) { controller.saml_config(idp_entity_id) }
|
27
|
+
let(:idp_providers_adapter) {
|
28
|
+
Class.new {
|
29
|
+
def self.settings(idp_entity_id)
|
30
|
+
#some hash of stuff (by doing a fetch, in our case, but could also be a giant hash keyed by idp_entity_id)
|
31
|
+
if idp_entity_id == "http://www.example.com"
|
32
|
+
{
|
33
|
+
assertion_consumer_service_url: "acs_url",
|
34
|
+
assertion_consumer_service_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
35
|
+
name_identifier_format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
36
|
+
issuer: "sp_issuer",
|
37
|
+
idp_entity_id: "http://www.example.com",
|
38
|
+
authn_context: "",
|
39
|
+
idp_slo_target_url: "idp_slo_url",
|
40
|
+
idp_sso_target_url: "idp_sso_url",
|
41
|
+
idp_cert: "idp_cert"
|
42
|
+
}
|
43
|
+
elsif idp_entity_id == "http://www.example.com_other"
|
44
|
+
{
|
45
|
+
assertion_consumer_service_url: "acs_url_other",
|
46
|
+
assertion_consumer_service_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST_other",
|
47
|
+
name_identifier_format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress_other",
|
48
|
+
issuer: "sp_issuer_other",
|
49
|
+
idp_entity_id: "http://www.example.com_other",
|
50
|
+
authn_context: "_other",
|
51
|
+
idp_slo_target_url: "idp_slo_url_other",
|
52
|
+
idp_sso_target_url: "idp_sso_url_other",
|
53
|
+
idp_cert: "idp_cert_other"
|
54
|
+
}
|
55
|
+
else
|
56
|
+
{}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
context "when a specific idp_entity_id is requested" do
|
63
|
+
let(:idp_entity_id) { "http://www.example.com" }
|
64
|
+
it "uses the settings from the adapter for that idp" do
|
65
|
+
expect(saml_config.idp_entity_id).to eq (idp_entity_id)
|
66
|
+
expect(saml_config.idp_sso_target_url).to eq ("idp_sso_url")
|
67
|
+
expect(saml_config.class).to eq OneLogin::RubySaml::Settings
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "when another idp_entity_id is requested" do
|
72
|
+
let(:idp_entity_id) { "http://www.example.com_other" }
|
73
|
+
it "returns the other idp settings" do
|
74
|
+
expect(saml_config.idp_entity_id).to eq (idp_entity_id)
|
75
|
+
expect(saml_config.idp_sso_target_url).to eq ("idp_sso_url_other")
|
76
|
+
expect(saml_config.class).to eq OneLogin::RubySaml::Settings
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
28
80
|
end
|
29
81
|
|
30
82
|
context "when config/idp.yml exists" do
|
@@ -66,7 +118,7 @@ environment:
|
|
66
118
|
IDP
|
67
119
|
end
|
68
120
|
|
69
|
-
it "
|
121
|
+
it "uses that file's contents" do
|
70
122
|
expect(saml_config.assertion_consumer_logout_service_binding).to eq('assertion_consumer_logout_service_binding')
|
71
123
|
expect(saml_config.assertion_consumer_logout_service_url).to eq('assertion_consumer_logout_service_url')
|
72
124
|
expect(saml_config.assertion_consumer_service_binding).to eq('assertion_consumer_service_binding')
|
@@ -4,16 +4,12 @@ describe Devise::Strategies::SamlAuthenticatable do
|
|
4
4
|
subject(:strategy) { described_class.new(env, :user) }
|
5
5
|
let(:env) { {} }
|
6
6
|
|
7
|
-
let(:response) { double(:response, :settings= => nil, is_valid?: true, sessionindex: '123123123') }
|
7
|
+
let(:response) { double(:response, issuers: [idp_entity_id], :settings= => nil, is_valid?: true, sessionindex: '123123123') }
|
8
|
+
let(:idp_entity_id) { "https://test/saml/metadata/123123" }
|
8
9
|
before do
|
9
10
|
allow(OneLogin::RubySaml::Response).to receive(:new).and_return(response)
|
10
11
|
end
|
11
12
|
|
12
|
-
let(:saml_config) { OneLogin::RubySaml::Settings.new }
|
13
|
-
before do
|
14
|
-
allow(strategy).to receive(:saml_config).and_return(saml_config)
|
15
|
-
end
|
16
|
-
|
17
13
|
let(:mapping) { double(:mapping, to: user_class) }
|
18
14
|
let(:user_class) { double(:user_class, authenticate_with_saml: user) }
|
19
15
|
let(:user) { double(:user) }
|
@@ -35,7 +31,7 @@ describe Devise::Strategies::SamlAuthenticatable do
|
|
35
31
|
end
|
36
32
|
|
37
33
|
it "authenticates with the response" do
|
38
|
-
expect(OneLogin::RubySaml::Response).to receive(:new).with(params[:SAMLResponse],
|
34
|
+
expect(OneLogin::RubySaml::Response).to receive(:new).with(params[:SAMLResponse], anything)
|
39
35
|
expect(user_class).to receive(:authenticate_with_saml).with(response)
|
40
36
|
expect(user).to receive(:after_saml_authentication).with(response.sessionindex)
|
41
37
|
|
@@ -43,7 +39,46 @@ describe Devise::Strategies::SamlAuthenticatable do
|
|
43
39
|
strategy.authenticate!
|
44
40
|
end
|
45
41
|
|
46
|
-
context "
|
42
|
+
context "when saml config uses an idp_adapter" do
|
43
|
+
let(:idp_providers_adapter) {
|
44
|
+
Class.new {
|
45
|
+
def self.settings(idp_entity_id)
|
46
|
+
{
|
47
|
+
assertion_consumer_service_url: "acs_url",
|
48
|
+
assertion_consumer_service_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
49
|
+
name_identifier_format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
50
|
+
issuer: "sp_issuer",
|
51
|
+
idp_entity_id: "http://www.example.com",
|
52
|
+
authn_context: "",
|
53
|
+
idp_slo_target_url: "idp_slo_url",
|
54
|
+
idp_sso_target_url: "http://idp_sso_url",
|
55
|
+
idp_cert: "idp_cert"
|
56
|
+
}
|
57
|
+
end
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
before do
|
62
|
+
Devise.idp_settings_adapter = idp_providers_adapter
|
63
|
+
allow(idp_providers_adapter).to receive(:settings).and_return({})
|
64
|
+
end
|
65
|
+
|
66
|
+
it "is valid" do
|
67
|
+
expect(strategy).to be_valid
|
68
|
+
end
|
69
|
+
|
70
|
+
it "authenticates with the response for the corresponding idp" do
|
71
|
+
expect(OneLogin::RubySaml::Response).to receive(:new).with(params[:SAMLResponse], anything)
|
72
|
+
expect(idp_providers_adapter).to receive(:settings).with(idp_entity_id)
|
73
|
+
expect(user_class).to receive(:authenticate_with_saml).with(response)
|
74
|
+
expect(user).to receive(:after_saml_authentication).with(response.sessionindex)
|
75
|
+
|
76
|
+
expect(strategy).to receive(:success!).with(user)
|
77
|
+
strategy.authenticate!
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context "and the resource does not exist" do
|
47
82
|
let(:user) { nil }
|
48
83
|
|
49
84
|
it "fails to authenticate" do
|
@@ -53,26 +88,33 @@ describe Devise::Strategies::SamlAuthenticatable do
|
|
53
88
|
end
|
54
89
|
|
55
90
|
context "and the SAML response is not valid" do
|
91
|
+
class CallbackHandler
|
92
|
+
def handle(response, strategy)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
56
96
|
before do
|
57
97
|
allow(response).to receive(:is_valid?).and_return(false)
|
98
|
+
@saml_failed_login = Devise.saml_failed_callback
|
99
|
+
Devise.saml_failed_callback = CallbackHandler
|
100
|
+
end
|
101
|
+
|
102
|
+
after do
|
103
|
+
Devise.saml_failed_callback = @saml_failed_login
|
58
104
|
end
|
59
105
|
|
60
106
|
it "fails to authenticate" do
|
61
107
|
expect(strategy).to receive(:fail!).with(:invalid)
|
62
108
|
strategy.authenticate!
|
63
109
|
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
context "with a logout SAMLResponse parameter" do
|
68
|
-
let(:params) { {SAMLResponse: "PHNhbWxwOkxvZ291dFJlc3BvbnNlIFZlcnNpb249JzIuMCcgSW5SZXNwb25zZVRvPSdfMTM0MjQzMjQzMjQzMicgeG1sbnM6c2FtbHA9J3VybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCcgSXNzdWVJbnN0YW50PScyMDE1LTA2LTMwVDE0OjQzOjQ0JyBJRD0nXzY5OTc2OTc5Nzk4Nzk4Nzk3OTg3Jz48c2FtbDpJc3N1ZXIgeG1sbnM6c2FtbD0ndXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbic+aHR0cHM6Ly90ZXN0L3NhbWwvbWV0YWRhdGEvMTQzMjQzMjwvc2FtbDpJc3N1ZXI+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0ndXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzJy8+PHNhbWxwOlN0YXR1c01lc3NhZ2U+U3VjY2Vzc2Z1bGx5IGxvZ2dlZCBvdXQgZnJvbSBzZXJ2aWNlIDwvc2FtbHA6U3RhdHVzTWVzc2FnZT48L3NhbWxwOlN0YXR1cz48L3NhbWxwOkxvZ291dFJlc3BvbnNlPg=="} }
|
69
110
|
|
70
|
-
|
71
|
-
|
111
|
+
it "redirects" do
|
112
|
+
expect_any_instance_of(CallbackHandler).to receive(:handle).with(response, strategy)
|
113
|
+
strategy.authenticate!
|
114
|
+
end
|
72
115
|
end
|
73
116
|
end
|
74
117
|
|
75
|
-
|
76
118
|
it "is not valid without a SAMLResponse parameter" do
|
77
119
|
expect(strategy).not_to be_valid
|
78
120
|
end
|
@@ -137,6 +137,62 @@ describe "SAML Authentication", type: :feature do
|
|
137
137
|
it_behaves_like "it authenticates and creates users"
|
138
138
|
end
|
139
139
|
|
140
|
+
context "when the idp_settings_adapter key is set" do
|
141
|
+
|
142
|
+
before(:each) do
|
143
|
+
create_app('idp', 'INCLUDE_SUBJECT_IN_ATTRIBUTES' => "false")
|
144
|
+
create_app('sp', 'USE_SUBJECT_TO_AUTHENTICATE' => "true", 'IDP_SETTINGS_ADAPTER' => "IdpSettingsAdapter", 'IDP_ENTITY_ID_READER' => "OurEntityIdReader")
|
145
|
+
|
146
|
+
@idp_pid = start_app('idp', idp_port)
|
147
|
+
@sp_pid = start_app('sp', sp_port)
|
148
|
+
end
|
149
|
+
|
150
|
+
after(:each) do
|
151
|
+
stop_app(@idp_pid)
|
152
|
+
stop_app(@sp_pid)
|
153
|
+
end
|
154
|
+
|
155
|
+
it "authenticates an existing user on a SP via an IdP" do
|
156
|
+
create_user("you@example.com")
|
157
|
+
|
158
|
+
visit 'http://localhost:8020/users/saml/sign_in/?entity_id=http%3A%2F%2Flocalhost%3A8020%2Fsaml%2Fmetadata'
|
159
|
+
expect(current_url).to match(%r(\Ahttp://www.example.com/\?SAMLRequest=))
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context "when the saml_failed_callback is set" do
|
164
|
+
let(:valid_destination) { "true" }
|
165
|
+
before(:each) do
|
166
|
+
create_app('idp', 'INCLUDE_SUBJECT_IN_ATTRIBUTES' => "false", 'VALID_DESTINATION' => valid_destination)
|
167
|
+
create_app('sp', 'USE_SUBJECT_TO_AUTHENTICATE' => "true", 'SAML_FAILED_CALLBACK' => "OurSamlFailedCallbackHandler")
|
168
|
+
|
169
|
+
@idp_pid = start_app('idp', idp_port)
|
170
|
+
@sp_pid = start_app('sp', sp_port)
|
171
|
+
end
|
172
|
+
|
173
|
+
after(:each) do
|
174
|
+
stop_app(@idp_pid)
|
175
|
+
stop_app(@sp_pid)
|
176
|
+
end
|
177
|
+
|
178
|
+
it_behaves_like "it authenticates and creates users"
|
179
|
+
|
180
|
+
context "a bad SAML Request" do
|
181
|
+
let(:valid_destination) { "false" }
|
182
|
+
it "redirects to the callback handler's redirect destination" do
|
183
|
+
create_user("you@example.com")
|
184
|
+
|
185
|
+
visit 'http://localhost:8020/'
|
186
|
+
expect(current_url).to match(%r(\Ahttp://localhost:8009/saml/auth\?SAMLRequest=))
|
187
|
+
fill_in "Email", with: "you@example.com"
|
188
|
+
fill_in "Password", with: "asdf"
|
189
|
+
click_on "Sign in"
|
190
|
+
expect(page).to have_content("Example Domain This domain is established to be used for illustrative examples in documents. You may use this domain in examples without prior coordination or asking for permission.")
|
191
|
+
expect(current_url).to eq("http://www.example.com/")
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
140
196
|
def create_user(email)
|
141
197
|
response = Net::HTTP.post_form(URI('http://localhost:8020/users'), email: email)
|
142
198
|
expect(response.code).to eq('201')
|
data/spec/spec_helper.rb
CHANGED
@@ -15,6 +15,19 @@ RSpec.configure do |config|
|
|
15
15
|
# `true` in RSpec 4.
|
16
16
|
mocks.verify_partial_doubles = true
|
17
17
|
end
|
18
|
+
|
19
|
+
config.before :each do
|
20
|
+
@original_saml_config = Devise.saml_config
|
21
|
+
@original_sign_out_success_url = Devise.saml_sign_out_success_url
|
22
|
+
@original_saml_session_index_key = Devise.saml_session_index_key
|
23
|
+
end
|
24
|
+
|
25
|
+
config.after :each do
|
26
|
+
Devise.saml_config = @original_saml_config
|
27
|
+
Devise.saml_sign_out_success_url = @original_sign_out_success_url
|
28
|
+
Devise.saml_session_index_key = @original_saml_session_index_key
|
29
|
+
Devise.idp_settings_adapter = nil
|
30
|
+
end
|
18
31
|
end
|
19
32
|
|
20
33
|
require 'support/rails_app'
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class IdpSettingsAdapter
|
2
|
+
def self.settings(idp_entity_id)
|
3
|
+
if idp_entity_id == "http://localhost:8020/saml/metadata"
|
4
|
+
{
|
5
|
+
assertion_consumer_service_url: "acs_url",
|
6
|
+
assertion_consumer_service_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
7
|
+
name_identifier_format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
8
|
+
issuer: "sp_issuer",
|
9
|
+
idp_entity_id: "http://localhost:8020/saml/metadata",
|
10
|
+
authn_context: "",
|
11
|
+
idp_slo_target_url: "http://www.example.com",
|
12
|
+
idp_sso_target_url: "http://www.example.com",
|
13
|
+
idp_cert: "idp_cert"
|
14
|
+
}
|
15
|
+
else
|
16
|
+
{}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -1,16 +1,30 @@
|
|
1
1
|
# Set up a SAML IdP
|
2
2
|
|
3
3
|
@include_subject_in_attributes = ENV.fetch('INCLUDE_SUBJECT_IN_ATTRIBUTES')
|
4
|
+
@valid_destination = ENV.fetch('VALID_DESTINATION', "true")
|
4
5
|
|
5
6
|
gem 'ruby-saml-idp'
|
6
7
|
gem 'thin'
|
7
8
|
|
9
|
+
insert_into_file('Gemfile', after: /\z/) {
|
10
|
+
<<-GEMFILE
|
11
|
+
# Lock down versions of gems for older versions of Ruby
|
12
|
+
if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.0")
|
13
|
+
gem 'mime-types', '~> 2.99'
|
14
|
+
end
|
15
|
+
if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
|
16
|
+
gem 'devise', '~> 3.5'
|
17
|
+
end
|
18
|
+
GEMFILE
|
19
|
+
}
|
20
|
+
|
8
21
|
route "get '/saml/auth' => 'saml_idp#new'"
|
9
22
|
route "post '/saml/auth' => 'saml_idp#create'"
|
10
23
|
route "get '/saml/logout' => 'saml_idp#logout'"
|
11
24
|
route "get '/saml/sp_sign_out' => 'saml_idp#sp_sign_out'"
|
12
25
|
|
13
26
|
template File.expand_path('../saml_idp_controller.rb.erb', __FILE__), 'app/controllers/saml_idp_controller.rb'
|
27
|
+
|
14
28
|
copy_file File.expand_path('../saml_idp-saml_slo_post.html.erb', __FILE__), 'app/views/saml_idp/saml_slo_post.html.erb'
|
15
29
|
create_file 'public/stylesheets/application.css', ''
|
16
30
|
|
@@ -0,0 +1 @@
|
|
1
|
+
PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgSUQ9InBmeDUxOWI1Y2JiLWNiNmYtOTQzNS0xNjNmLWJkMzVjZTM1YzNmMCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDYtMDRUMDI6MjI6MDJaIiBEZXN0aW5hdGlvbj0iaHR0cDovL2FwcC5tdWRhLm5vL3Nzby9jb25zdW1lIiBJblJlc3BvbnNlVG89Il9mYzRhMzRiMC03ZWZiLTAxMmUtY2FhZS03ODJiY2IxM2JiMzgiPjxzYW1sOklzc3Vlcj5odHRwczovL2FwcC5vbmVsb2dpbi5jb20vc2FtbDI8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng1MTliNWNiYi1jYjZmLTk0MzUtMTYzZi1iZDM1Y2UzNWMzZjAiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPjBTRFRrNXNYWjdoMW9YUWVRMm5YY3BLZnZoTT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+WENPVmk4U2c0MllRS25oMWpNTWYvV0dVcDh5Q1dFQWV4UE5taVNWT0M2dUFBUGc5WWwySUt4Um1SeGczcHpVK0o5SzlTRUVEOEJWenJERTZ4VDlxV1JUbXZ1WExqemE0TndvRmFGWllIc3ZzN0FPR3l5UEJjT3Z2R3JoM2RGWmVTUzF5U2tVc3FBWW5Wck54emRkRVFZa2trRmNxQkNqZ3dnd0Z5Vlpvbkc4PTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDR3pDQ0FZUUNDUUNOTmNRWG9tMzJWREFOQmdrcWhraUc5dzBCQVFVRkFEQlNNUXN3Q1FZRFZRUUdFd0pWVXpFTE1Ba0dBMVVFQ0JNQ1NVNHhGVEFUQmdOVkJBY1RERWx1WkdsaGJtRndiMnhwY3pFUk1BOEdBMVVFQ2hNSVQyNWxURzluYVc0eEREQUtCZ05WQkFzVEEwVnVaekFlRncweE5EQTBNak14T0RReE1ERmFGdzB4TlRBME1qTXhPRFF4TURGYU1GSXhDekFKQmdOVkJBWVRBbFZUTVFzd0NRWURWUVFJRXdKSlRqRVZNQk1HQTFVRUJ4TU1TVzVrYVdGdVlYQnZiR2x6TVJFd0R3WURWUVFLRXdoUGJtVk1iMmRwYmpFTU1Bb0dBMVVFQ3hNRFJXNW5NSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURvNm0rUVp2WVEveEwwRWxMZ3VwSzFRRGNZTDRmNVBja3dzTmdTOXBVdlY3ZnpUcUNIazhUaEx4VGs0Mk1RMk1jSnNPZVVKVlA3MjhLaHltakZDcXhnUDRWdXdSazlycEFsMCttaHk2TVBkeWp5QTZHMTRqckRXUzY1eXNMY2hLNHQvdndwRUR6MFNRbEVvRzFrTXpsbFNtN3paUzNYcmVnQTdEak5hVVlRcXdJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBTE0ydkdDaVEvdm0rYTZ2NDArVlgyemRxSEEyUS8xdkYxaWJReko1NE1KQ09WV3ZzK3ZRWGZaRmhkbTBPUE0ySXJEVTdvcXZLUHFQNnhPQWVKSzZIMHlQN000WUwzZmF0U3ZJWW1tZnlYQzlrdDNTdnovTnlySHpQaFVuSjB5ZS9zVVNYeG56UXh3Y20vOVB3QXFyUWFBM1FwUWtINTd5YkYvT29yeVBlKzJoPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgVmVyc2lvbj0iMi4wIiBJRD0icGZ4OTUxNmIwZjMtNDUzNi0xMGY2LWM2ZmEtOWRkNTIzZTE0OThjIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDYtMDRUMDI6MjI6MDJaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9hcHAub25lbG9naW4uY29tL3NhbWwyPC9zYW1sOklzc3Vlcj48c2FtbDpTdWJqZWN0PjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMzAtMDYtMDRUMDI6Mjc6MDJaIiBSZWNpcGllbnQ9InJlY2lwaWVudCIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjxzYW1sOkVuY3J5cHRlZElEPjx4ZW5jOkVuY3J5cHRlZERhdGEgeG1sbnM6eGVuYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjIiB4bWxuczpkc2lnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIiBUeXBlPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNFbGVtZW50Ij48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjYWVzMTI4LWNiYyIvPjxkc2lnOktleUluZm8geG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PHhlbmM6RW5jcnlwdGVkS2V5Pjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNyc2EtMV81Ii8+PHhlbmM6Q2lwaGVyRGF0YT48eGVuYzpDaXBoZXJWYWx1ZT5ZUkdFZGF2dWpSNlYwNUZsWERHbmxCK1VUWTFjak9DallkYlhKN2JBZHFURWxDTyt1eHl0aytnMWVTTGVuczhJcjlZaVBNNUorUWU5cXo0TkdORXdyNjV6aDM5L0ZJVXNMQ3BhaXQ3QjZXM2lFcmR4aVUrSUN1cUw3TCtNSmlGVHZiVG90NVdleWZvVkFnSE94Z1BodDRONlZSL3BhYzRDdFZEQ0ZBbDlEMjA9PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWRLZXk+PC9kc2lnOktleUluZm8+DQogICA8eGVuYzpDaXBoZXJEYXRhPg0KICAgICAgPHhlbmM6Q2lwaGVyVmFsdWU+dFdQZEV1dXZmSjh3WVBhOFVUQTRvR2htRENQTjFhQzVkUUFEN0g5SkhWQm5VS3Y0UkljNEQ3SnVJem12bXlyalZGWmRGNW15K3cvUGd3dWlOVGdpOUxid01iSW5adW1HbDhlSndFblBaVXBPQ0w1dDNXbEdKbU85OVVNejZQUVNLeGlGSU1DYzcrQXlRQmpjdTEzaUxWeU5TbFQyWDMxRXBOaW5jQ3FzSldvPTwveGVuYzpDaXBoZXJWYWx1ZT4NCiAgIDwveGVuYzpDaXBoZXJEYXRhPg0KPC94ZW5jOkVuY3J5cHRlZERhdGE+PC9zYW1sOkVuY3J5cHRlZElEPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDExLTA2LTA0VDAyOjE3OjAyWiIgTm90T25PckFmdGVyPSIyMDMwLTA2LTA0VDAyOjI3OjAyWiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL3NvbWVvbmUuZXhhbXBsZS5jb20vYXVkaWVuY2U8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTA2LTA0VDAyOjIyOjAyWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAzMC0wNi0wNVQwMjoyMjowMloiIFNlc3Npb25JbmRleD0iXzE2ZjU3MGZiYzAzMTUwMDdhMDM1NWRmZWE2YjNjNDZjIj48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmRQcm90ZWN0ZWRUcmFuc3BvcnQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==
|
@@ -62,7 +62,7 @@ class SamlIdpController < SamlIdp::IdpController
|
|
62
62
|
|
63
63
|
assertion_and_signature = assertion.sub(/Issuer\>\<Subject/, "Issuer>#{signature}<Subject")
|
64
64
|
|
65
|
-
xml = %[<samlp:Response ID="_#{response_id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{@saml_acs_url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="#{@saml_request_id}" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">#{issuer_uri}</Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status>#{assertion_and_signature}</samlp:Response>]
|
65
|
+
xml = %[<samlp:Response ID="_#{response_id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{destination(@saml_acs_url)}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="#{@saml_request_id}" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">#{issuer_uri}</Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status>#{assertion_and_signature}</samlp:Response>]
|
66
66
|
|
67
67
|
Base64.encode64(xml)
|
68
68
|
end
|
@@ -97,6 +97,7 @@ class SamlIdpController < SamlIdp::IdpController
|
|
97
97
|
idp_slo_authenticate(params[:name_id])
|
98
98
|
saml_slo_request = encode_SAML_SLO_Request("you@example.com")
|
99
99
|
uri = URI.parse("http://localhost:8020/users/saml/idp_sign_out")
|
100
|
+
require 'net/http'
|
100
101
|
Net::HTTP.post_form(uri, {"SAMLRequest" => saml_slo_request})
|
101
102
|
head :no_content
|
102
103
|
end
|
@@ -116,6 +117,14 @@ class SamlIdpController < SamlIdp::IdpController
|
|
116
117
|
|
117
118
|
private
|
118
119
|
|
120
|
+
def destination(destination)
|
121
|
+
<% if @valid_destination == "false" %>
|
122
|
+
"{recipient}"
|
123
|
+
<% else %>
|
124
|
+
destination
|
125
|
+
<% end %>
|
126
|
+
end
|
127
|
+
|
119
128
|
def validate_saml_slo_request(saml_request = params[:SAMLRequest])
|
120
129
|
decode_SAML_SLO_Request(saml_request)
|
121
130
|
end
|
@@ -150,7 +159,7 @@ class SamlIdpController < SamlIdp::IdpController
|
|
150
159
|
ID="_#{response_id}"
|
151
160
|
Version="2.0"
|
152
161
|
IssueInstant="#{now.iso8601}"
|
153
|
-
Destination="#{@saml_slo_acs_url}"
|
162
|
+
Destination="#{destination(@saml_slo_acs_url)}"
|
154
163
|
Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified"
|
155
164
|
InResponseTo="#{@saml_slo_request_id}"
|
156
165
|
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
|
@@ -172,7 +181,7 @@ class SamlIdpController < SamlIdp::IdpController
|
|
172
181
|
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
173
182
|
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
|
174
183
|
ID="_#{response_id}" Version="2.0"
|
175
|
-
Destination="#{@saml_slo_acs_url}"
|
184
|
+
Destination="#{destination(@saml_slo_acs_url)}"
|
176
185
|
IssueInstant="#{now.iso8601}">
|
177
186
|
<saml:Issuer >#{issuer_uri}</saml:Issuer>
|
178
187
|
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">#{nameID}</saml:NameID>
|
data/spec/support/sp_template.rb
CHANGED
@@ -2,16 +2,59 @@
|
|
2
2
|
|
3
3
|
saml_session_index_key = ENV.fetch('SAML_SESSION_INDEX_KEY', ":session_index")
|
4
4
|
use_subject_to_authenticate = ENV.fetch('USE_SUBJECT_TO_AUTHENTICATE')
|
5
|
+
idp_settings_adapter = ENV.fetch('IDP_SETTINGS_ADAPTER', "nil")
|
6
|
+
idp_entity_id_reader = ENV.fetch('IDP_ENTITY_ID_READER', "DeviseSamlAuthenticatable::DefaultIdpEntityIdReader")
|
7
|
+
saml_failed_callback = ENV.fetch('SAML_FAILED_CALLBACK', "nil")
|
5
8
|
|
6
9
|
gem 'devise_saml_authenticatable', path: '../../..'
|
7
10
|
gem 'thin'
|
8
11
|
|
12
|
+
insert_into_file('Gemfile', after: /\z/) {
|
13
|
+
<<-GEMFILE
|
14
|
+
# Lock down versions of gems for older versions of Ruby
|
15
|
+
if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.0")
|
16
|
+
gem 'mime-types', '~> 2.99'
|
17
|
+
end
|
18
|
+
if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
|
19
|
+
gem 'devise', '~> 3.5'
|
20
|
+
end
|
21
|
+
GEMFILE
|
22
|
+
}
|
23
|
+
|
24
|
+
template File.expand_path('../idp_settings_adapter.rb.erb', __FILE__), 'app/lib/idp_settings_adapter.rb'
|
25
|
+
|
9
26
|
create_file 'config/attribute-map.yml', <<-ATTRIBUTES
|
10
27
|
---
|
11
28
|
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": email
|
12
29
|
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": name
|
13
30
|
ATTRIBUTES
|
14
31
|
|
32
|
+
create_file('app/lib/our_saml_failed_callback_handler.rb', <<-CALLBACKHANDLER)
|
33
|
+
|
34
|
+
class OurSamlFailedCallbackHandler
|
35
|
+
def handle(response, strategy)
|
36
|
+
strategy.redirect! "http://www.example.com"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
CALLBACKHANDLER
|
40
|
+
|
41
|
+
create_file('app/lib/our_entity_id_reader.rb', <<-READER)
|
42
|
+
|
43
|
+
class OurEntityIdReader
|
44
|
+
def self.entity_id(params)
|
45
|
+
if params[:entity_id]
|
46
|
+
params[:entity_id]
|
47
|
+
elsif params[:SAMLRequest]
|
48
|
+
OneLogin::RubySaml::SloLogoutrequest.new(params[:SAMLRequest]).issuer
|
49
|
+
elsif params[:SAMLResponse]
|
50
|
+
OneLogin::RubySaml::Response.new(params[:SAMLResponse]).issuers.first
|
51
|
+
else
|
52
|
+
"http://www.cats.com"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
READER
|
57
|
+
|
15
58
|
after_bundle do
|
16
59
|
generate :controller, 'home', 'index'
|
17
60
|
insert_into_file('app/controllers/home_controller.rb', after: "class HomeController < ApplicationController\n") {
|
@@ -38,6 +81,9 @@ after_bundle do
|
|
38
81
|
config.saml_use_subject = #{use_subject_to_authenticate}
|
39
82
|
config.saml_create_user = true
|
40
83
|
config.saml_update_user = true
|
84
|
+
config.idp_settings_adapter = #{idp_settings_adapter}
|
85
|
+
config.idp_entity_id_reader = #{idp_entity_id_reader}
|
86
|
+
config.saml_failed_callback = #{saml_failed_callback}
|
41
87
|
|
42
88
|
config.saml_configure do |settings|
|
43
89
|
settings.assertion_consumer_service_url = "http://localhost:8020/users/saml/auth"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: devise_saml_authenticatable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josef Sauter
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-07-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: devise
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '1.
|
33
|
+
version: '1.3'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '1.
|
40
|
+
version: '1.3'
|
41
41
|
description: SAML Authentication for devise
|
42
42
|
email:
|
43
43
|
- Josef.Sauter@gmail.com
|
@@ -55,6 +55,7 @@ files:
|
|
55
55
|
- app/controllers/devise/saml_sessions_controller.rb
|
56
56
|
- devise_saml_authenticatable.gemspec
|
57
57
|
- lib/devise_saml_authenticatable.rb
|
58
|
+
- lib/devise_saml_authenticatable/default_idp_entity_id_reader.rb
|
58
59
|
- lib/devise_saml_authenticatable/exception.rb
|
59
60
|
- lib/devise_saml_authenticatable/logger.rb
|
60
61
|
- lib/devise_saml_authenticatable/model.rb
|
@@ -64,14 +65,17 @@ files:
|
|
64
65
|
- lib/devise_saml_authenticatable/version.rb
|
65
66
|
- rails/init.rb
|
66
67
|
- spec/controllers/devise/saml_sessions_controller_spec.rb
|
68
|
+
- spec/devise_saml_authenticatable/default_idp_entity_id_reader_spec.rb
|
67
69
|
- spec/devise_saml_authenticatable/model_spec.rb
|
68
70
|
- spec/devise_saml_authenticatable/saml_config_spec.rb
|
69
71
|
- spec/devise_saml_authenticatable/strategy_spec.rb
|
70
72
|
- spec/features/saml_authentication_spec.rb
|
71
73
|
- spec/rails_helper.rb
|
72
74
|
- spec/spec_helper.rb
|
75
|
+
- spec/support/idp_settings_adapter.rb.erb
|
73
76
|
- spec/support/idp_template.rb
|
74
77
|
- spec/support/rails_app.rb
|
78
|
+
- spec/support/response_encrypted_nameid.xml.base64
|
75
79
|
- spec/support/saml_idp-saml_slo_post.html.erb
|
76
80
|
- spec/support/saml_idp_controller.rb.erb
|
77
81
|
- spec/support/sp_template.rb
|
@@ -100,14 +104,17 @@ specification_version: 4
|
|
100
104
|
summary: SAML Authentication for devise
|
101
105
|
test_files:
|
102
106
|
- spec/controllers/devise/saml_sessions_controller_spec.rb
|
107
|
+
- spec/devise_saml_authenticatable/default_idp_entity_id_reader_spec.rb
|
103
108
|
- spec/devise_saml_authenticatable/model_spec.rb
|
104
109
|
- spec/devise_saml_authenticatable/saml_config_spec.rb
|
105
110
|
- spec/devise_saml_authenticatable/strategy_spec.rb
|
106
111
|
- spec/features/saml_authentication_spec.rb
|
107
112
|
- spec/rails_helper.rb
|
108
113
|
- spec/spec_helper.rb
|
114
|
+
- spec/support/idp_settings_adapter.rb.erb
|
109
115
|
- spec/support/idp_template.rb
|
110
116
|
- spec/support/rails_app.rb
|
117
|
+
- spec/support/response_encrypted_nameid.xml.base64
|
111
118
|
- spec/support/saml_idp-saml_slo_post.html.erb
|
112
119
|
- spec/support/saml_idp_controller.rb.erb
|
113
120
|
- spec/support/sp_template.rb
|