devise_saml_authenticatable 1.2.2 → 1.3.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 +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
|