devise_saml_authenticatable 1.3.1 → 1.3.2

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: eeabad37a5473557499db0b8de604afb7740608d
4
- data.tar.gz: 952108992254613cdf27f72aedbf6e2a772f95a3
3
+ metadata.gz: 269c0505d92b49c3b9a79d1b8aece87533018b10
4
+ data.tar.gz: 259f7e012808aabe3ab06e854a01deab8959843a
5
5
  SHA512:
6
- metadata.gz: 720d7b21c40596b762a85c5c0df2f9dc3d1af7fdce86a7c8562428b866e79eab6a768e070c648fd4c657415bca175102d4eaa0dff6866c7cc8b14025d1bf5101
7
- data.tar.gz: c3368984264f54a99a37ce9e4b546b7bbd2460a834bc57300dfaf242232619bad50f51bb66adc4d2e1e41d287ce4f3f7f85923697835a06bf14f73968a31f1ed
6
+ metadata.gz: 83a89cbe087accbcf239e047027188a35793c9cdcb3198fd207edeb95652bb6e8957ef8e0aeaa9b919f01cbb91677bf4c5ade49fae0a85b86cf95edc4b09592b
7
+ data.tar.gz: 230f5490186a0853cd63f401b390afa29129b8f103de04cbb64a734274edd88de9467030784122cccdc743fbd5eb7babf75ad3635be5b51d2a5af467d4e17b1f
@@ -2,26 +2,34 @@ language: ruby
2
2
  rvm:
3
3
  - "1.9.3"
4
4
  - "2.0.0"
5
- - "2.1.9"
6
- - "2.2.5"
7
- - "2.3.1"
5
+ - "2.1.10"
6
+ - "2.2.7"
7
+ - "2.3.4"
8
+ - "2.4.1"
8
9
  gemfile:
9
10
  - Gemfile
11
+ - spec/support/Gemfile.rails5
10
12
  - spec/support/Gemfile.rails4
11
13
  - spec/support/Gemfile.ruby-saml-1.3
12
14
  matrix:
13
15
  allow_failures:
14
16
  - rvm: "1.9.3"
15
17
  gemfile: Gemfile
18
+ - rvm: "1.9.3"
19
+ gemfile: spec/support/Gemfile.rails5
16
20
  - rvm: "1.9.3"
17
21
  gemfile: spec/support/Gemfile.ruby-saml-1.3
18
22
  - rvm: "2.0.0"
19
23
  gemfile: Gemfile
24
+ - rvm: "2.0.0"
25
+ gemfile: spec/support/Gemfile.rails5
20
26
  - rvm: "2.0.0"
21
27
  gemfile: spec/support/Gemfile.ruby-saml-1.3
22
- - rvm: "2.1.9"
28
+ - rvm: "2.1.10"
23
29
  gemfile: Gemfile
24
- - rvm: "2.1.9"
30
+ - rvm: "2.1.10"
31
+ gemfile: spec/support/Gemfile.rails5
32
+ - rvm: "2.1.10"
25
33
  gemfile: spec/support/Gemfile.ruby-saml-1.3
26
34
 
27
35
  script:
data/Gemfile CHANGED
@@ -6,17 +6,9 @@ gemspec
6
6
  group :test do
7
7
  gem 'rake'
8
8
  gem 'rspec', '~> 3.0'
9
- gem 'rails', '~> 5.0'
9
+ gem 'rails', '~> 5.1'
10
10
  gem 'rspec-rails'
11
11
  gem 'sqlite3'
12
12
  gem 'capybara'
13
13
  gem 'poltergeist'
14
-
15
- # Lock down versions of gems for older versions of Ruby
16
- if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.0")
17
- gem 'mime-types', '~> 2.99'
18
- end
19
- if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
20
- gem 'devise', '~> 3.5'
21
- end
22
14
  end
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  # DeviseSamlAuthenticatable
3
3
 
4
4
  Devise Saml Authenticatable is a Single-Sign-On authentication strategy for devise that relies on SAML.
5
- It uses [ruby-saml][] to handle all SAML related stuff.
5
+ It uses [ruby-saml][] to handle all SAML-related stuff.
6
6
 
7
7
  ## Installation
8
8
 
@@ -21,6 +21,7 @@ Or install it yourself as:
21
21
  ## Usage
22
22
 
23
23
  In `app/models/<YOUR_MODEL>.rb` set the `:saml_authenticatable` strategy.
24
+
24
25
  In the example the model is `user.rb`:
25
26
 
26
27
  ```ruby
@@ -31,7 +32,7 @@ In the example the model is `user.rb`:
31
32
  end
32
33
  ```
33
34
 
34
- In config/initializers/devise.rb
35
+ In `config/initializers/devise.rb`:
35
36
 
36
37
  ```ruby
37
38
  Devise.setup do |config|
@@ -52,8 +53,8 @@ In config/initializers/devise.rb
52
53
  # for the user's session to facilitate an IDP initiated logout request.
53
54
  config.saml_session_index_key = :session_index
54
55
 
55
- # You can set this value to use Subject or SAML assertation as info to which email will be compared
56
- # If you don't set it then email will be extracted from SAML assertation attributes
56
+ # You can set this value to use Subject or SAML assertation as info to which email will be compared.
57
+ # If you don't set it then email will be extracted from SAML assertation attributes.
57
58
  config.saml_use_subject = true
58
59
 
59
60
  # You can support multiple IdPs by setting this value to a class that implements a #settings method which takes
@@ -99,7 +100,7 @@ In config/initializers/devise.rb
99
100
  end
100
101
  ```
101
102
 
102
- In config directory create a YAML file (`attribute-map.yml`) that maps SAML attributes with your model's fields:
103
+ In the config directory, create a YAML file (`attribute-map.yml`) that maps SAML attributes with your model's fields:
103
104
 
104
105
  ```yaml
105
106
  # attribute-map.yml
@@ -112,15 +113,17 @@ In config directory create a YAML file (`attribute-map.yml`) that maps SAML attr
112
113
 
113
114
  The attribute mappings are very dependent on the way the IdP encodes the attributes.
114
115
  In this example the attributes are given in URN style.
115
- Other IdPs might provide them as OID's or other means.
116
+ Other IdPs might provide them as OID's, or by other means.
116
117
 
117
118
  You are now ready to test it against an IdP.
118
- When the user goes to `/users/saml/sign_in` he will be redirected to the login page of the IdP.
119
- Upon successful login the user is redirected to devise `user_root_path`.
119
+
120
+ When the user visits `/users/saml/sign_in` they will be redirected to the login page of the IdP.
121
+
122
+ Upon successful login the user is redirected to the Devise `user_root_path`.
120
123
 
121
124
  ## Supporting Multiple IdPs
122
125
 
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:
126
+ 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
127
 
125
128
  ```ruby
126
129
  class IdPSettingsAdapter
@@ -158,21 +161,26 @@ end
158
161
  ```
159
162
 
160
163
  Detecting the entity ID passed to the `settings` method is done by `config.idp_entity_id_reader`.
164
+
161
165
  By default this will find the `Issuer` in the SAML request.
166
+
162
167
  You can support more use cases by writing your own and implementing the `.entity_id` method.
168
+
163
169
  If you use encrypted assertions, your entity ID reader will need to understand how to decrypt the response from each of the possible IdPs.
164
170
 
165
171
  ## Identity Provider
166
172
 
167
- If you don't have an identity provider an you would like to test the authentication against your app there are some options:
173
+ If you don't have an identity provider and you would like to test the authentication against your app, there are some options:
168
174
 
169
- 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.
170
- 2. Use an online service that can act as an IdP. Onelogin, Salesforce, Okta and some others provide you with this functionality
175
+ 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 up as a dummy IdP that always sends a valid authentication response to your app.
176
+ 2. Use an online service that can act as an IdP. OneLogin, Salesforce, Okta and some others provide you with this functionality.
171
177
  3. Install your own IdP.
172
178
 
173
- 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.
179
+ 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].
180
+
181
+ [SimpleSAMLphp] was my choice for development since it is a production-ready SAML solution, that is also really easy to install, configure and use.
174
182
 
175
- [SimpleSAMLphp](http://simplesamlphp.org/) was my choice for development since it is a production-ready SAML solution, that is also really easy to install, configure and use.
183
+ [SimpleSAMLphp]: http://simplesamlphp.org/
176
184
 
177
185
  ## Logout
178
186
 
@@ -180,18 +188,20 @@ Logout support is included by immediately terminating the local session and then
180
188
 
181
189
  ## Logout Request
182
190
 
183
- Logout requests from the IDP are supported by the `idp_sign_out` end point. Directing logout requests to `users/saml/idp_sign_out` will logout the respective user by invalidating their current sessions.
191
+ Logout requests from the IDP are supported by the `idp_sign_out` endpoint. Directing logout requests to `users/saml/idp_sign_out` will log out the respective user by invalidating their current sessions.
192
+
184
193
  `saml_session_index_key` must be configured to support this feature.
185
194
 
186
195
  ## Signing and Encrypting Authentication Requests
187
196
 
188
- ruby-saml 1.0.0 supports signature and decrypt. Teh only requirement is to place the public certificate and the private key. Please reffer to these features in the ruby-saml documentation [here](https://github.com/onelogin/ruby-saml#signing)
197
+ ruby-saml 1.0.0 supports signature and decrypt. The only requirement is to set the public certificate and the private key. For more information, see [the ruby-saml documentation](https://github.com/onelogin/ruby-saml#signing).
189
198
 
190
199
  ## Thanks
191
200
 
192
201
  The continued maintenance of this gem could not have been possible without the hard work of [Adam Stegman](https://github.com/adamstegman) and [Mitch Lindsay](https://github.com/mitch-lindsay). Thank you guys for keeping this project alive.
193
202
 
194
203
  Thanks to all other contributors that have also helped us make this software better.
204
+
195
205
  ## Contributing
196
206
 
197
207
  1. Fork it
@@ -3,7 +3,11 @@ require "ruby-saml"
3
3
  class Devise::SamlSessionsController < Devise::SessionsController
4
4
  include DeviseSamlAuthenticatable::SamlConfig
5
5
  unloadable if Rails::VERSION::MAJOR < 4
6
- skip_before_filter :verify_authenticity_token, raise: false
6
+ if Rails::VERSION::MAJOR < 5
7
+ skip_before_filter :verify_authenticity_token
8
+ else
9
+ skip_before_action :verify_authenticity_token, raise: false
10
+ end
7
11
 
8
12
  def new
9
13
  idp_entity_id = get_idp_entity_id(params)
@@ -62,11 +62,56 @@ module Devise
62
62
  mattr_accessor :saml_relay_state
63
63
  @@saml_relay_state
64
64
 
65
+ # Implements a #validate method that takes the retrieved resource and response right after retrieval,
66
+ # and returns true if it's valid. False will cause authentication to fail.
67
+ mattr_accessor :saml_resource_validator
68
+ @@saml_resource_validator
69
+
70
+ # Custom value for ruby-saml allowed_clock_drift
71
+ mattr_accessor :allowed_clock_drift_in_seconds
72
+ @@allowed_clock_drift_in_seconds
73
+
65
74
  mattr_accessor :saml_config
66
75
  @@saml_config = OneLogin::RubySaml::Settings.new
67
76
  def self.saml_configure
68
77
  yield saml_config
69
78
  end
79
+
80
+ # Default update resource hook. Updates each attribute on the model that is mapped, updates the
81
+ # saml_default_user_key if saml_use_subject is true and saves the user model.
82
+ # See saml_update_resource_hook for more information.
83
+ mattr_reader :saml_default_update_resource_hook
84
+ @@saml_default_update_resource_hook = Proc.new do |user, saml_response, auth_value|
85
+ saml_response.attributes.resource_keys.each do |key|
86
+ user.send "#{key}=", saml_response.attribute_value_by_resource_key(key)
87
+ end
88
+
89
+ if (Devise.saml_use_subject)
90
+ user.send "#{Devise.saml_default_user_key}=", auth_value
91
+ end
92
+
93
+ user.save!
94
+ end
95
+
96
+ # Proc that is called if Devise.saml_update_user and/or Devise.saml_create_user are true.
97
+ # Recieves the user object, saml_response and auth_value, and defines how the object's values are
98
+ # updated with regards to the SAML response. See saml_default_update_resource_hook for an example.
99
+ mattr_accessor :saml_update_resource_hook
100
+ @@saml_update_resource_hook = @@saml_default_update_resource_hook
101
+
102
+ # Default resource locator. Uses saml_default_user_key and auth_value to resolve user.
103
+ # See saml_resource_locator for more information.
104
+ mattr_reader :saml_default_resource_locator
105
+ @@saml_default_resource_locator = Proc.new do |model, saml_response, auth_value|
106
+ model.where(Devise.saml_default_user_key => auth_value).first
107
+ end
108
+
109
+ # Proc that is called to resolve the saml_response and auth_value into the correct user object.
110
+ # Recieves a copy of the ActiveRecord::Model, saml_response and auth_value. Is expected to return
111
+ # one instance of the provided model that is the matched account, or nil if none exists.
112
+ # See saml_default_resource_locator above for an example.
113
+ mattr_accessor :saml_resource_locator
114
+ @@saml_resource_locator = @@saml_default_resource_locator
70
115
  end
71
116
 
72
117
  # Add saml_authenticatable strategy to defaults.
@@ -2,9 +2,15 @@ module DeviseSamlAuthenticatable
2
2
  class DefaultIdpEntityIdReader
3
3
  def self.entity_id(params)
4
4
  if params[:SAMLRequest]
5
- OneLogin::RubySaml::SloLogoutrequest.new(params[:SAMLRequest]).issuer
5
+ OneLogin::RubySaml::SloLogoutrequest.new(
6
+ params[:SAMLRequest],
7
+ allowed_clock_drift: Devise.allowed_clock_drift_in_seconds,
8
+ ).issuer
6
9
  elsif params[:SAMLResponse]
7
- OneLogin::RubySaml::Response.new(params[:SAMLResponse]).issuers.first
10
+ OneLogin::RubySaml::Response.new(
11
+ params[:SAMLResponse],
12
+ allowed_clock_drift: Devise.allowed_clock_drift_in_seconds,
13
+ ).issuers.first
8
14
  end
9
15
  end
10
16
  end
@@ -1,4 +1,5 @@
1
1
  require 'devise_saml_authenticatable/strategy'
2
+ require 'devise_saml_authenticatable/saml_response'
2
3
 
3
4
  module Devise
4
5
  module Models
@@ -30,16 +31,25 @@ module Devise
30
31
  module ClassMethods
31
32
  def authenticate_with_saml(saml_response, relay_state)
32
33
  key = Devise.saml_default_user_key
33
- attributes = saml_response.attributes
34
+ decorated_response = ::SamlAuthenticatable::SamlResponse.new(
35
+ saml_response,
36
+ attribute_map
37
+ )
34
38
  if (Devise.saml_use_subject)
35
39
  auth_value = saml_response.name_id
36
40
  else
37
- inv_attr = attribute_map.invert
38
- auth_value = attributes[inv_attr[key.to_s]]
41
+ auth_value = decorated_response.attribute_value_by_resource_key(key)
39
42
  end
40
43
  auth_value.try(:downcase!) if Devise.case_insensitive_keys.include?(key)
41
44
 
42
- resource = where(key => auth_value).first
45
+ resource = Devise.saml_resource_locator.call(self, decorated_response, auth_value)
46
+
47
+ if Devise.saml_resource_validator
48
+ if not Devise.saml_resource_validator.new.validate(resource, saml_response)
49
+ logger.info("User(#{auth_value}) did not pass custom validation.")
50
+ return nil
51
+ end
52
+ end
43
53
 
44
54
  if resource.nil?
45
55
  if Devise.saml_create_user
@@ -52,11 +62,7 @@ module Devise
52
62
  end
53
63
 
54
64
  if Devise.saml_update_user || (resource.new_record? && Devise.saml_create_user)
55
- set_user_saml_attributes(resource, attributes)
56
- if (Devise.saml_use_subject)
57
- resource.send "#{key}=", auth_value
58
- end
59
- resource.save!
65
+ Devise.saml_update_resource_hook.call(resource, decorated_response, auth_value)
60
66
  end
61
67
 
62
68
  resource
@@ -77,13 +83,6 @@ module Devise
77
83
 
78
84
  private
79
85
 
80
- def set_user_saml_attributes(user,attributes)
81
- attribute_map.each do |k,v|
82
- Rails.logger.info "Setting: #{v}, #{attributes[k]}"
83
- user.send "#{v}=", attributes[k]
84
- end
85
- end
86
-
87
86
  def attribute_map_for_environment
88
87
  attribute_map = YAML.load(File.read("#{Rails.root}/config/attribute-map.yml"))
89
88
  if attribute_map.has_key?(Rails.env)
@@ -0,0 +1,25 @@
1
+ module SamlAuthenticatable
2
+ class SamlMappedAttributes
3
+ def initialize(attributes, attribute_map)
4
+ @attributes = attributes
5
+ @attribute_map = attribute_map
6
+ @inverted_attribute_map = @attribute_map.invert
7
+ end
8
+
9
+ def saml_attribute_keys
10
+ @attribute_map.keys
11
+ end
12
+
13
+ def resource_keys
14
+ @attribute_map.values
15
+ end
16
+
17
+ def value_by_resource_key(key)
18
+ value_by_saml_attribute_key(@inverted_attribute_map.fetch(String(key)))
19
+ end
20
+
21
+ def value_by_saml_attribute_key(key)
22
+ @attributes[String(key)]
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,16 @@
1
+ require 'devise_saml_authenticatable/saml_mapped_attributes'
2
+
3
+ module SamlAuthenticatable
4
+ class SamlResponse
5
+ attr_reader :raw_response, :attributes
6
+
7
+ def initialize(saml_response, attribute_map)
8
+ @attributes = ::SamlAuthenticatable::SamlMappedAttributes.new(saml_response.attributes, attribute_map)
9
+ @raw_response = saml_response
10
+ end
11
+
12
+ def attribute_value_by_resource_key(key)
13
+ attributes.value_by_resource_key(key)
14
+ end
15
+ end
16
+ end
@@ -6,7 +6,10 @@ module Devise
6
6
  include DeviseSamlAuthenticatable::SamlConfig
7
7
  def valid?
8
8
  if params[:SAMLResponse]
9
- OneLogin::RubySaml::Response.new(params[:SAMLResponse])
9
+ OneLogin::RubySaml::Response.new(
10
+ params[:SAMLResponse],
11
+ allowed_clock_drift: Devise.allowed_clock_drift_in_seconds,
12
+ )
10
13
  else
11
14
  false
12
15
  end
@@ -30,7 +33,11 @@ module Devise
30
33
 
31
34
  private
32
35
  def parse_saml_response
33
- @response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], settings: saml_config(get_idp_entity_id(params)))
36
+ @response = OneLogin::RubySaml::Response.new(
37
+ params[:SAMLResponse],
38
+ settings: saml_config(get_idp_entity_id(params)),
39
+ allowed_clock_drift: Devise.allowed_clock_drift_in_seconds,
40
+ )
34
41
  unless @response.is_valid?
35
42
  failed_auth("Auth errors: #{@response.errors.join(', ')}")
36
43
  end
@@ -1,3 +1,3 @@
1
1
  module DeviseSamlAuthenticatable
2
- VERSION = "1.3.1"
2
+ VERSION = "1.3.2"
3
3
  end
@@ -5,18 +5,48 @@ describe DeviseSamlAuthenticatable::DefaultIdpEntityIdReader do
5
5
  context "when there is a SAMLRequest in the params" do
6
6
  let(:params) { {SAMLRequest: "logout request"} }
7
7
  let(:slo_logout_request) { double('slo_logout_request', issuer: 'meow')}
8
+ before do
9
+ allow(OneLogin::RubySaml::SloLogoutrequest).to receive(:new).and_return(slo_logout_request)
10
+ end
11
+
8
12
  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)
13
+ expect(OneLogin::RubySaml::SloLogoutrequest).to receive(:new).with("logout request", hash_including)
14
+ expect(described_class.entity_id(params)).to eq("meow")
15
+ end
16
+
17
+ context "and allowed_clock_drift is configured" do
18
+ before do
19
+ allow(Devise).to receive(:allowed_clock_drift_in_seconds).and_return(30)
20
+ end
21
+
22
+ it "allows the configured clock drift" do
23
+ expect(OneLogin::RubySaml::SloLogoutrequest).to receive(:new).with("logout request", hash_including(allowed_clock_drift: 30))
24
+ expect(described_class.entity_id(params)).to eq("meow")
25
+ end
11
26
  end
12
27
  end
13
28
 
14
29
  context "when there is a SAMLResponse in the params" do
15
30
  let(:params) { {SAMLResponse: "auth response"} }
16
31
  let(:response) { double('response', issuers: ['meow'] )}
32
+ before do
33
+ allow(OneLogin::RubySaml::Response).to receive(:new).and_return(response)
34
+ end
35
+
17
36
  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)
37
+ expect(OneLogin::RubySaml::Response).to receive(:new).with("auth response", hash_including)
38
+ expect(described_class.entity_id(params)).to eq("meow")
39
+ end
40
+
41
+ context "and allowed_clock_drift is configured" do
42
+ before do
43
+ allow(Devise).to receive(:allowed_clock_drift_in_seconds).and_return(30)
44
+ end
45
+
46
+ it "allows the configured clock drift" do
47
+ expect(OneLogin::RubySaml::Response).to receive(:new).with("auth response", hash_including(allowed_clock_drift: 30))
48
+ expect(described_class.entity_id(params)).to eq("meow")
49
+ end
20
50
  end
21
51
  end
22
52
  end
@@ -178,4 +178,156 @@ describe Devise::Models::SamlAuthenticatable do
178
178
  include_examples "correct downcasing"
179
179
  end
180
180
  end
181
+
182
+ context "when configured with a resource validator" do
183
+ let(:validator_class) { double("validator_class") }
184
+ let(:validator) { double("validator") }
185
+ let(:user) { Model.new(new_record: false) }
186
+
187
+ before do
188
+ allow(Devise).to receive(:saml_resource_validator).and_return(validator_class)
189
+ allow(validator_class).to receive(:new).and_return(validator)
190
+ end
191
+
192
+ context "and sent a valid value" do
193
+ before do
194
+ allow(validator).to receive(:validate).with(user, response).and_return(true)
195
+ end
196
+
197
+ it "returns the user" do
198
+ expect(Model).to receive(:where).with(email: 'user@example.com').and_return([user])
199
+ expect(Model.authenticate_with_saml(response, nil)).to eq(user)
200
+ end
201
+ end
202
+
203
+ context "and sent an invalid value" do
204
+ before do
205
+ allow(validator).to receive(:validate).with(user, response).and_return(false)
206
+ end
207
+
208
+ it "returns nil" do
209
+ expect(Model).to receive(:where).with(email: 'user@example.com').and_return([user])
210
+ expect(Model.authenticate_with_saml(response, nil)).to be_nil
211
+ end
212
+ end
213
+ end
214
+
215
+ context "when configured to use a custom update hook" do
216
+ it "can replicate the default behaviour in a custom hook" do
217
+ configure_hook do |user, saml_response|
218
+ Devise.saml_default_update_resource_hook.call(user, saml_response)
219
+ end
220
+
221
+ new_user = Model.authenticate_with_saml(response, nil)
222
+
223
+ expect(new_user.name).to eq(attributes['saml-name-format'])
224
+ expect(new_user.email).to eq(attributes['saml-email-format'])
225
+ end
226
+
227
+ it "can extend the default behaviour with custom transformations" do
228
+ configure_hook do |user, saml_response|
229
+ Devise.saml_default_update_resource_hook.call(user, saml_response)
230
+
231
+ user.email = "ext+#{user.email}"
232
+ end
233
+
234
+ new_user = Model.authenticate_with_saml(response, nil)
235
+
236
+ expect(new_user.name).to eq(attributes['saml-name-format'])
237
+ expect(new_user.email).to eq("ext+#{attributes['saml-email-format']}")
238
+ end
239
+
240
+ it "can extend the default behaviour using information from the saml response" do
241
+ configure_hook do |user, saml_response|
242
+ Devise.saml_default_update_resource_hook.call(user, saml_response)
243
+
244
+ name_id = saml_response.raw_response.name_id
245
+ user.name += "@#{name_id}"
246
+ end
247
+
248
+ new_user = Model.authenticate_with_saml(response, nil)
249
+
250
+ expect(new_user.name).to eq("#{attributes['saml-name-format']}@#{response.name_id}")
251
+ expect(new_user.email).to eq(attributes['saml-email-format'])
252
+ end
253
+
254
+ def configure_hook(&block)
255
+ allow(Model).to receive(:where).with(email: 'user@example.com').and_return([])
256
+ allow(Devise).to receive(:saml_default_user_key).and_return(:email)
257
+ allow(Devise).to receive(:saml_create_user).and_return(true)
258
+ allow(Devise).to receive(:saml_update_resource_hook).and_return(block)
259
+ end
260
+ end
261
+
262
+ context "when configured to use a custom user locator" do
263
+ let(:name_id) { 'SomeUsername' }
264
+
265
+ it "can replicate the default behaviour for a new user in a custom locator" do
266
+ allow(Model).to receive(:where).with(email: attributes['saml-email-format']).and_return([])
267
+
268
+ configure_hook do |model, saml_response, auth_value|
269
+ Devise.saml_default_resource_locator.call(model, saml_response, auth_value)
270
+ end
271
+
272
+ new_user = Model.authenticate_with_saml(response, nil)
273
+
274
+ expect(new_user.name).to eq(attributes['saml-name-format'])
275
+ expect(new_user.email).to eq(attributes['saml-email-format'])
276
+ end
277
+
278
+ it "can replicate the default behaviour for an existing user in a custom locator" do
279
+ user = Model.new(email: attributes['saml-email-format'], name: attributes['saml-name-format'])
280
+ user.save!
281
+
282
+ allow(Model).to receive(:where).with(email: attributes['saml-email-format']).and_return([user])
283
+
284
+ configure_hook do |model, saml_response, auth_value|
285
+ Devise.saml_default_resource_locator.call(model, saml_response, auth_value)
286
+ end
287
+
288
+ new_user = Model.authenticate_with_saml(response, nil)
289
+
290
+ expect(new_user).to eq(user)
291
+ expect(new_user.name).to eq(attributes['saml-name-format'])
292
+ expect(new_user.email).to eq(attributes['saml-email-format'])
293
+ end
294
+
295
+ it "can change the default behaviour for a new user from the saml response" do
296
+ allow(Model).to receive(:where).with(foo: attributes['saml-email-format'], bar: name_id).and_return([])
297
+
298
+ configure_hook do |model, saml_response, auth_value|
299
+ name_id = saml_response.raw_response.name_id
300
+ model.where(foo: auth_value, bar: name_id).first
301
+ end
302
+
303
+ new_user = Model.authenticate_with_saml(response, nil)
304
+
305
+ expect(new_user.name).to eq(attributes['saml-name-format'])
306
+ expect(new_user.email).to eq(attributes['saml-email-format'])
307
+ end
308
+
309
+ it "can change the default behaviour for an existing user from the saml response" do
310
+ user = Model.new(email: attributes['saml-email-format'], name: attributes['saml-name-format'])
311
+ user.save!
312
+
313
+ allow(Model).to receive(:where).with(foo: attributes['saml-email-format'], bar: name_id).and_return([user])
314
+
315
+ configure_hook do |model, saml_response, auth_value|
316
+ name_id = saml_response.raw_response.name_id
317
+ model.where(foo: auth_value, bar: name_id).first
318
+ end
319
+
320
+ new_user = Model.authenticate_with_saml(response, nil)
321
+
322
+ expect(new_user).to eq(user)
323
+ expect(new_user.name).to eq(attributes['saml-name-format'])
324
+ expect(new_user.email).to eq(attributes['saml-email-format'])
325
+ end
326
+
327
+ def configure_hook(&block)
328
+ allow(Devise).to receive(:saml_default_user_key).and_return(:email)
329
+ allow(Devise).to receive(:saml_create_user).and_return(true)
330
+ allow(Devise).to receive(:saml_resource_locator).and_return(block)
331
+ end
332
+ end
181
333
  end
@@ -134,6 +134,24 @@ describe Devise::Strategies::SamlAuthenticatable do
134
134
  strategy.authenticate!
135
135
  end
136
136
  end
137
+
138
+ context "when allowed_clock_drift is configured" do
139
+ before do
140
+ allow(Devise).to receive(:allowed_clock_drift_in_seconds).and_return(30)
141
+ end
142
+
143
+ it "is valid with the configured clock drift" do
144
+ expect(OneLogin::RubySaml::Response).to receive(:new).with(params[:SAMLResponse], hash_including(allowed_clock_drift: 30))
145
+ expect(strategy).to be_valid
146
+ end
147
+
148
+ it "authenticates with the configured clock drift" do
149
+ expect(OneLogin::RubySaml::Response).to receive(:new).with(params[:SAMLResponse], hash_including(allowed_clock_drift: 30))
150
+
151
+ expect(strategy).to receive(:success!).with(user)
152
+ strategy.authenticate!
153
+ end
154
+ end
137
155
  end
138
156
 
139
157
  it "is not valid without a SAMLResponse parameter" do
@@ -19,5 +19,6 @@ group :test do
19
19
  end
20
20
  if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
21
21
  gem 'devise', '~> 3.5'
22
+ gem 'nokogiri', '~> 1.6.8'
22
23
  end
23
24
  end
@@ -0,0 +1,14 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in devise_saml_authenticatable.gemspec
4
+ gemspec path: '../..'
5
+
6
+ group :test do
7
+ gem 'rake'
8
+ gem 'rspec', '~> 3.0'
9
+ gem 'rails', '~> 5.0.0'
10
+ gem 'rspec-rails'
11
+ gem 'sqlite3'
12
+ gem 'capybara'
13
+ gem 'poltergeist'
14
+ end
@@ -19,5 +19,6 @@ group :test do
19
19
  end
20
20
  if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
21
21
  gem 'devise', '~> 3.5'
22
+ gem 'nokogiri', '~> 1.6.8'
22
23
  end
23
24
  end
@@ -3,7 +3,9 @@
3
3
  @include_subject_in_attributes = ENV.fetch('INCLUDE_SUBJECT_IN_ATTRIBUTES')
4
4
  @valid_destination = ENV.fetch('VALID_DESTINATION', "true")
5
5
 
6
- gem 'ruby-saml-idp'
6
+ gsub_file 'config/secrets.yml', /secret_key_base:.*$/, 'secret_key_base: "34814fd41f91c493b89aa01ac73c44d241a31245b5bc5542fa4b7317525e1dcfa60ba947b3d085e4e229456fdee0d8af6aac6a63cf750d807ea6fe5d853dff4a"'
7
+
8
+ gem 'ruby-saml-idp', git: "https://github.com/lawrencepit/ruby-saml-idp.git", ref: "ec715b252e849105c7a96df27b731c6e7f725a51"
7
9
  gem 'thin'
8
10
 
9
11
  insert_into_file('Gemfile', after: /\z/) {
@@ -11,6 +13,7 @@ insert_into_file('Gemfile', after: /\z/) {
11
13
  # Lock down versions of gems for older versions of Ruby
12
14
  if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
13
15
  gem 'devise', '~> 3.5'
16
+ gem 'nokogiri', '~> 1.6.8'
14
17
  end
15
18
  GEMFILE
16
19
  }
@@ -1,4 +1,8 @@
1
1
  require 'open3'
2
+ require 'socket'
3
+ require 'timeout'
4
+
5
+ APP_READY_TIMEOUT ||= 30
2
6
 
3
7
  def sh!(cmd)
4
8
  unless system(cmd)
@@ -7,8 +11,9 @@ def sh!(cmd)
7
11
  end
8
12
 
9
13
  def app_ready?(pid, port)
10
- Process.getpgid(pid) &&
11
- system("lsof -i:#{port}", out: '/dev/null')
14
+ Process.getpgid(pid) && port_open?(port)
15
+ rescue Errno::ESRCH
16
+ false
12
17
  end
13
18
 
14
19
  def create_app(name, env = {})
@@ -24,16 +29,26 @@ def start_app(name, port, options = {})
24
29
  pid = nil
25
30
  Bundler.with_clean_env do
26
31
  Dir.chdir(File.expand_path("../../support/#{name}", __FILE__)) do
27
- pid = Process.spawn("bundle exec rails server -p #{port}")
28
- sleep 1 until app_ready?(pid, port)
29
- if app_ready?(pid, port)
30
- puts "Launched #{name} on port #{port} (pid #{pid})..."
31
- else
32
+ pid = Process.spawn({"RAILS_ENV" => "production"}, "bundle exec rails server -p #{port} -e production", out: "log/#{name}.log", err: "log/#{name}.err.log")
33
+ begin
34
+ Timeout::timeout(APP_READY_TIMEOUT) do
35
+ sleep 1 until app_ready?(pid, port)
36
+ end
37
+ if app_ready?(pid, port)
38
+ puts "Launched #{name} on port #{port} (pid #{pid})..."
39
+ else
40
+ raise "#{name} failed after starting"
41
+ end
42
+ rescue Timeout::Error
32
43
  raise "#{name} failed to start"
33
44
  end
34
45
  end
35
46
  end
36
47
  pid
48
+ rescue RuntimeError => e
49
+ $stdout.puts "#{File.read(File.expand_path("../../support/#{name}/log/#{name}.log", __FILE__))}"
50
+ $stderr.puts "#{File.read(File.expand_path("../../support/#{name}/log/#{name}.err.log", __FILE__))}"
51
+ raise e
37
52
  end
38
53
 
39
54
  def stop_app(pid)
@@ -42,3 +57,24 @@ def stop_app(pid)
42
57
  Process.wait(pid)
43
58
  end
44
59
  end
60
+
61
+ def port_open?(port)
62
+ Timeout::timeout(1) do
63
+ begin
64
+ s = TCPSocket.new('localhost', port)
65
+ s.close
66
+ return true
67
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
68
+ # try 127.0.0.1
69
+ end
70
+ begin
71
+ s = TCPSocket.new('127.0.0.1', port)
72
+ s.close
73
+ return true
74
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
75
+ return false
76
+ end
77
+ end
78
+ rescue Timeout::Error
79
+ false
80
+ end
@@ -29,14 +29,14 @@ class SamlIdpController < SamlIdp::IdpController
29
29
 
30
30
  def session_index
31
31
  Rails.cache.fetch('session_key') {
32
- UUID.generate
32
+ SecureRandom.uuid
33
33
  }
34
34
  end
35
35
 
36
36
 
37
37
  def encode_SAMLResponse(nameID, opts = {})
38
38
  now = Time.now.utc
39
- response_id = UUID.generate
39
+ response_id = SecureRandom.uuid
40
40
  audience_uri = opts[:audience_uri] || "#{saml_acs_url[/^(.*?\/\/.*?\/)/, 1]}saml/metadata"
41
41
  issuer_uri = opts[:issuer_uri] || (defined?(request) && request.url) || "http://example.com"
42
42
 
@@ -72,8 +72,13 @@ class SamlIdpController < SamlIdp::IdpController
72
72
  end
73
73
 
74
74
  # == SLO functionality, see https://github.com/lawrencepit/ruby-saml-idp/pull/10
75
+ <% if Rails::VERSION::MAJOR < 5 %>
75
76
  skip_before_filter :validate_saml_request, :only => [:logout, :sp_sign_out]
76
77
  before_filter :validate_saml_slo_request, :only => [:logout]
78
+ <% else %>
79
+ skip_before_action :validate_saml_request, :only => [:logout, :sp_sign_out]
80
+ before_action :validate_saml_slo_request, :only => [:logout]
81
+ <% end %>
77
82
 
78
83
  public
79
84
 
@@ -139,7 +144,7 @@ class SamlIdpController < SamlIdp::IdpController
139
144
 
140
145
  def encode_SAML_SLO_Response(nameID, opts = {})
141
146
  now = Time.now.utc
142
- response_id = UUID.generate
147
+ response_id = SecureRandom.uuid
143
148
  audience_uri = opts[:audience_uri] || (@saml_slo_acs_url && @saml_slo_acs_url[/^(.*?\/\/.*?\/)/, 1])
144
149
  issuer_uri = opts[:issuer_uri] || (defined?(request) && request.url.split("?")[0]) || "http://example.com"
145
150
 
@@ -175,7 +180,7 @@ class SamlIdpController < SamlIdp::IdpController
175
180
 
176
181
  def encode_SAML_SLO_Request(nameID, opts = {})
177
182
  now = Time.now.utc
178
- response_id = UUID.generate
183
+ response_id = SecureRandom.uuid
179
184
  issuer_uri = opts[:issuer_uri] || (defined?(request) && request.url.split("?")[0]) || "http://example.com"
180
185
  xml = %[<samlp:LogoutRequest
181
186
  xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
@@ -8,6 +8,8 @@ idp_settings_adapter = ENV.fetch('IDP_SETTINGS_ADAPTER', "nil")
8
8
  idp_entity_id_reader = ENV.fetch('IDP_ENTITY_ID_READER', "DeviseSamlAuthenticatable::DefaultIdpEntityIdReader")
9
9
  saml_failed_callback = ENV.fetch('SAML_FAILED_CALLBACK', "nil")
10
10
 
11
+ gsub_file 'config/secrets.yml', /secret_key_base:.*$/, 'secret_key_base: "8b5889df1fcf03f76c7d66da02d8776bcc85b06bed7d9c592f076d9c8a5455ee6d4beae45986c3c030b40208db5e612f2a6ef8283036a352e3fae83c5eda36be"'
12
+
11
13
  gem 'devise_saml_authenticatable', path: '../../..'
12
14
  gem 'ruby-saml', OneLogin::RubySaml::VERSION
13
15
  gem 'thin'
@@ -17,6 +19,7 @@ insert_into_file('Gemfile', after: /\z/) {
17
19
  # Lock down versions of gems for older versions of Ruby
18
20
  if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
19
21
  gem 'devise', '~> 3.5'
22
+ gem 'nokogiri', '~> 1.6.8'
20
23
  end
21
24
  GEMFILE
22
25
  }
@@ -75,6 +78,8 @@ after_bundle do
75
78
  # Configure for our SAML IdP
76
79
  generate 'devise:install'
77
80
  gsub_file 'config/initializers/devise.rb', /^end$/, <<-CONFIG
81
+ config.secret_key = 'adc7cd73792f5d20055a0ac749ce8cdddb2e0f0d3ea7fe7855eec3d0f81833b9a4ac31d12e05f232d40ae86ca492826a6fc5a65228c6e16752815316e2d5b38d'
82
+
78
83
  config.saml_default_user_key = :email
79
84
  config.saml_session_index_key = #{saml_session_index_key}
80
85
 
@@ -103,13 +108,15 @@ class UsersController < ApplicationController
103
108
  skip_before_action :verify_authenticity_token
104
109
  def create
105
110
  User.create!(email: params[:email])
106
- render nothing: true, status: 201
111
+ head 201
107
112
  end
108
113
  end
109
114
  USERS
110
115
 
111
116
  rake "db:create"
112
117
  rake "db:migrate"
118
+ rake "db:create", env: "production"
119
+ rake "db:migrate", env: "production"
113
120
  end
114
121
 
115
122
  create_file 'public/stylesheets/application.css', ''
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.3.1
4
+ version: 1.3.2
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-30 00:00:00.000000000 Z
11
+ date: 2017-05-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: devise
@@ -61,6 +61,8 @@ files:
61
61
  - lib/devise_saml_authenticatable/model.rb
62
62
  - lib/devise_saml_authenticatable/routes.rb
63
63
  - lib/devise_saml_authenticatable/saml_config.rb
64
+ - lib/devise_saml_authenticatable/saml_mapped_attributes.rb
65
+ - lib/devise_saml_authenticatable/saml_response.rb
64
66
  - lib/devise_saml_authenticatable/strategy.rb
65
67
  - lib/devise_saml_authenticatable/version.rb
66
68
  - rails/init.rb
@@ -73,6 +75,7 @@ files:
73
75
  - spec/rails_helper.rb
74
76
  - spec/spec_helper.rb
75
77
  - spec/support/Gemfile.rails4
78
+ - spec/support/Gemfile.rails5
76
79
  - spec/support/Gemfile.ruby-saml-1.3
77
80
  - spec/support/idp_settings_adapter.rb.erb
78
81
  - spec/support/idp_template.rb
@@ -114,6 +117,7 @@ test_files:
114
117
  - spec/rails_helper.rb
115
118
  - spec/spec_helper.rb
116
119
  - spec/support/Gemfile.rails4
120
+ - spec/support/Gemfile.rails5
117
121
  - spec/support/Gemfile.ruby-saml-1.3
118
122
  - spec/support/idp_settings_adapter.rb.erb
119
123
  - spec/support/idp_template.rb