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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d2e501ba6f2facaaa55601864a66fbf5799f2b93
4
- data.tar.gz: 644a41f1ef4dafdac806600b8a5646e3a00acbf6
3
+ metadata.gz: 811f4c9c0306a1c5ba28080ac773ba88022b79cc
4
+ data.tar.gz: f2571561eb9c3ba05832e65d1b8ab15d3c9c408c
5
5
  SHA512:
6
- metadata.gz: a4096feb553134033f04079e7c10a1ace113fc048f79dbf712ee0fe5b478ce6696ccd4434c5c05f4aecced1c55e79f13415027bfa613ca2937a216e8bb6949cd
7
- data.tar.gz: 20c0f29acc30f4db788fa8ada4bfbff4df88421d48403f52b39cdb9876f1907be732a3641a0d9cdd7e3997f63809f71894769e8791014183a226333bedc2353e
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(logout_request)
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
@@ -17,5 +17,5 @@ Gem::Specification.new do |gem|
17
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
18
 
19
19
  gem.add_dependency("devise","> 2.0.0")
20
- gem.add_dependency("ruby-saml","~> 1.0")
20
+ gem.add_dependency("ruby-saml","~> 1.3")
21
21
  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 @saml_config if @saml_config
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
- # Support 0.0.x-style configuration via a YAML file
16
+
9
17
  if File.exists?(idp_config_path)
10
- Devise.saml_config = OneLogin::RubySaml::Settings.new(YAML.load(File.read(idp_config_path))[Rails.env])
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
- @saml_config = Devise.saml_config
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
- response = OneLogin::RubySaml::Logoutresponse.new(params[:SAMLResponse], saml_config)
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
 
@@ -1,3 +1,3 @@
1
1
  module DeviseSamlAuthenticatable
2
- VERSION = "1.2.2"
2
+ VERSION = "1.3.0"
3
3
  end
@@ -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
- it 'redirects to the SAML Auth Request endpoint' do
26
- get :new
27
- expect(response).to redirect_to(%r(\Ahttp://localhost:8009/saml/auth\?SAMLRequest=))
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
- it 'generates metadata' do
33
- get :metadata
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
- # Remove ID that can vary across requests
36
- expected_metadata = OneLogin::RubySaml::Metadata.new.generate(saml_config)
37
- metadata_pattern = Regexp.escape(expected_metadata).gsub(/ ID='[^']+'/, " ID='[\\w-]+'")
38
- expect(response.body).to match(Regexp.new(metadata_pattern))
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(:logout_request, {
53
- id: 42,
54
- name_id: name_id
55
- }) }
56
- let(:sam_response) { double(:logout_response)}
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(sam_response)
63
- allow(sam_response).to receive(:create).and_return(response_url)
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
- subject(:saml_config) { controller.saml_config }
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 "stores the configured IdP settings" do
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], settings: saml_config)
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 "and the resource cannot does not exist" do
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
- it "is valid" do
71
- expect(strategy).not_to be_valid
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>
@@ -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.2.2
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-01-12 00:00:00.000000000 Z
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.0'
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.0'
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