devise-security 0.14.1 → 0.14.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
  SHA256:
3
- metadata.gz: fc16bc5c313bb9834b1ab7aedb0e84ff51b9e2f9a2574cea1191bd9c9ac59f81
4
- data.tar.gz: 0b2129cc1557cf83338be9cf88792bdb60dca838c06b9903ec8b189289389572
3
+ metadata.gz: 5aa65f547b5acbc4207678cda6b4af888f94f3c18a7ebb1ccd80888433cf350f
4
+ data.tar.gz: ce3b96b475d683ba121c3db9477915c44c52ac27c9244a54f5c613e76a0e86e4
5
5
  SHA512:
6
- metadata.gz: fe84191b2373f752b0de75b0d6072dfffe52ca2f76a332dde63db79e39d1ae9e80c7878f011e586080e8b9a7b8fbd488c0f02a3a47a2924a322ab188c9cbb17f
7
- data.tar.gz: 2eece1cf95ebbbca4f1f0a6f03e780d5f24b2e8ba1134fe381d6cd2966e43aee7ef1d6f0d1bdedeb683bd634af7544143fb60e7b2b2c53585719dcfd390b76e6
6
+ metadata.gz: 540d6e0534dd9c37d28e7281ada99959d73f6afb0de030f406305e689c237afda6ea94fc3f84f4392c4a5cea2243191aa1aaae4bf0f9e77efb41a41cff2e8497
7
+ data.tar.gz: 6064b218564eb7c9b3d8b849185d4fe3bbd49c466e47716a6ee666dcbc5f02f27d7015dde4f32238c95f6189b32516afcf3101c2dea0cb0bd78e9f9bd2d70cfd
data/README.md CHANGED
@@ -195,7 +195,7 @@ add_index :old_passwords, [:password_archivable_type, :password_archivable_id],
195
195
  create_table :the_resources do |t|
196
196
  # other devise fields
197
197
 
198
- t.string :unique_session_id, limit: 20
198
+ t.string :unique_session_id
199
199
  end
200
200
  ```
201
201
 
data/Rakefile CHANGED
@@ -12,7 +12,7 @@ task default: :test
12
12
  Rake::TestTask.new(:test) do |t|
13
13
  t.libs << 'lib'
14
14
  t.libs << 'test'
15
- t.test_files = FileList['test/*test*.rb']
15
+ t.test_files = FileList['test/*test*.rb', 'test/**/*test*.rb']
16
16
  t.verbose = true
17
17
  t.warning = false
18
18
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rails", "~> 6.0.0.beta1"
5
+ gem "rails", "~> 6.0.0.rc1"
6
6
 
7
7
  group :active_record do
8
8
  gem "sqlite3", "~> 1.3.0"
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # After each sign in, update unique_session_id.
4
- # This is only triggered when the user is explicitly set (with set_user)
5
- # and on authentication. Retrieving the user from session (:fetch) does
6
- # not trigger it.
3
+ # After each sign in, update unique_session_id. This is only triggered when the
4
+ # user is explicitly set (with set_user) and on authentication. Retrieving the
5
+ # user from session (:fetch) does not trigger it.
7
6
  Warden::Manager.after_set_user except: :fetch do |record, warden, options|
8
7
  if record.respond_to?(:update_unique_session_id!) && warden.authenticated?(options[:scope])
9
8
  unique_session_id = Devise.friendly_token
@@ -12,15 +11,21 @@ Warden::Manager.after_set_user except: :fetch do |record, warden, options|
12
11
  end
13
12
  end
14
13
 
15
- # Each time a record is fetched from session we check if a new session from another
16
- # browser was opened for the record or not, based on a unique session identifier.
17
- # If so, the old account is logged out and redirected to the sign in page on the next request.
14
+ # Each time a record is fetched from session we check if a new session from
15
+ # another browser was opened for the record or not, based on a unique session
16
+ # identifier. If so, the old account is logged out and redirected to the sign in
17
+ # page on the next request.
18
18
  Warden::Manager.after_set_user only: :fetch do |record, warden, options|
19
19
  scope = options[:scope]
20
20
  env = warden.request.env
21
21
 
22
22
  if record.respond_to?(:unique_session_id) && warden.authenticated?(scope) && options[:store] != false
23
23
  if record.unique_session_id != warden.session(scope)['unique_session_id'] && !env['devise.skip_session_limitable']
24
+ Rails.logger.warn {
25
+ "[devise-security][session_limitable] session id mismatch: "\
26
+ "expected=#{record.unique_session_id.inspect} "\
27
+ "actual=#{warden.session(scope)['unique_session_id'].inspect}"
28
+ }
24
29
  warden.raw_session.clear
25
30
  warden.logout(scope)
26
31
  throw :warden, scope: scope, message: :session_limited
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "compatibility/#{DEVISE_ORM}"
3
+ require_relative "compatibility/#{DEVISE_ORM}_patch"
4
4
 
5
5
  module Devise
6
6
  module Models
@@ -9,7 +9,7 @@ module Devise
9
9
  # and/or older versions of ORMs.
10
10
  module Compatibility
11
11
  extend ActiveSupport::Concern
12
- include "Devise::Models::Compatibility::#{DEVISE_ORM.to_s.classify}".constantize
12
+ include "Devise::Models::Compatibility::#{DEVISE_ORM.to_s.classify}Patch".constantize
13
13
  end
14
14
  end
15
15
  end
@@ -1,7 +1,10 @@
1
1
  module Devise
2
2
  module Models
3
3
  module Compatibility
4
- module ActiveRecord
4
+
5
+ class NotPersistedError < ActiveRecord::ActiveRecordError; end
6
+
7
+ module ActiveRecordPatch
5
8
  extend ActiveSupport::Concern
6
9
  unless Devise.activerecord51?
7
10
  # When the record was saved, was the +encrypted_password+ changed?
@@ -23,6 +26,14 @@ module Devise
23
26
  changed_attributes['encrypted_password'].present?
24
27
  end
25
28
  end
29
+
30
+ # Updates the record with the value and does not trigger validations or callbacks
31
+ # @param name [Symbol] attribute to update
32
+ # @param value [String] value to set
33
+ def update_attribute_without_validatons_or_callbacks(name, value)
34
+ update_column(name, value)
35
+ end
36
+
26
37
  end
27
38
  end
28
39
  end
@@ -1,7 +1,10 @@
1
1
  module Devise
2
2
  module Models
3
3
  module Compatibility
4
- module Mongoid
4
+
5
+ class NotPersistedError < Mongoid::Errors::MongoidError; end
6
+
7
+ module MongoidPatch
5
8
  extend ActiveSupport::Concern
6
9
 
7
10
  # Will saving this record change the +email+ attribute?
@@ -15,6 +18,13 @@ module Devise
15
18
  def will_save_change_to_encrypted_password?
16
19
  changed.include? 'encrypted_password'
17
20
  end
21
+
22
+ # Updates the document with the value and does not trigger validations or callbacks
23
+ # @param name [Symbol] attribute to update
24
+ # @param value [String] value to set
25
+ def update_attribute_without_validatons_or_callbacks(name, value)
26
+ set(Hash[*[name, value]])
27
+ end
18
28
  end
19
29
  end
20
30
  end
@@ -12,10 +12,16 @@ module Devise
12
12
  module SessionLimitable
13
13
  extend ActiveSupport::Concern
14
14
 
15
+ # Update the unique_session_id on the model. This will be checked in
16
+ # the Warden after_set_user hook in {file:devise-security/hooks/session_limitable}
17
+ # @param unique_session_id [String]
18
+ # @return [void]
19
+ # @raise [Devise::Models::Compatibility::NotPersistedError] if record is unsaved
15
20
  def update_unique_session_id!(unique_session_id)
16
- self.unique_session_id = unique_session_id
17
-
18
- save(validate: false)
21
+ raise Devise::Models::Compatibility::NotPersistedError, 'cannot update a new record' unless persisted?
22
+ update_attribute_without_validatons_or_callbacks(:unique_session_id, unique_session_id).tap do
23
+ Rails.logger.debug { "[devise-security][session_limitable] unique_session_id=#{unique_session_id}"}
24
+ end
19
25
  end
20
26
 
21
27
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeviseSecurity
4
- VERSION = '0.14.1'
4
+ VERSION = '0.14.2'
5
5
  end
@@ -0,0 +1,6 @@
1
+ class WidgetsController < ApplicationController
2
+ before_action :authenticate_user!
3
+ def show
4
+ render plain: 'success'
5
+ end
6
+ end
@@ -25,5 +25,13 @@ class User < ApplicationRecord
25
25
  if DEVISE_ORM == :mongoid
26
26
  require './test/dummy/app/models/mongoid/mappings'
27
27
  include ::Mongoid::Mappings
28
+
29
+ def some_method_calling_mongoid
30
+ Mongoid.logger
31
+ end
32
+ elsif DEVISE_ORM == :active_record
33
+ def some_method_calling_active_record
34
+ ActiveRecord::Base.transaction {}
35
+ end
28
36
  end
29
37
  end
@@ -7,6 +7,7 @@ RailsApp::Application.routes.draw do
7
7
  devise_for :security_question_users, only: [:sessions, :unlocks], controllers: { unlocks: "security_question/unlocks" }
8
8
 
9
9
  resources :foos
10
+ resource :widgets
10
11
 
11
12
  root to: 'foos#index'
12
13
  end
@@ -5,13 +5,22 @@ class CreateTables < MIGRATION_CLASS
5
5
  create_table :users do |t|
6
6
  t.string :username
7
7
  t.string :facebook_token
8
- t.string :unique_session_id, :limit => 20
8
+
9
+ # session_limitable
10
+ t.string :unique_session_id
9
11
 
10
12
  ## Database authenticatable
11
13
  t.string :email, null: false, default: ''
12
14
  t.string :encrypted_password, null: false, default: ''
13
15
 
14
16
  t.datetime :password_changed_at
17
+
18
+ t.datetime :current_sign_in_at
19
+ t.datetime :last_sign_in_at
20
+ t.string :current_sign_in_ip
21
+ t.string :last_sign_in_ip
22
+ t.integer :sign_in_count, default: 0
23
+ t.integer :failed_attempts, default: 0
15
24
  t.timestamps null: false
16
25
  end
17
26
  add_index :users, :password_changed_at
@@ -0,0 +1,67 @@
1
+ require 'test_helper'
2
+
3
+ class TestSessionLimitableWorkflow < ActionDispatch::IntegrationTest
4
+ include IntegrationHelpers
5
+
6
+ setup do
7
+ @user = User.create!(password: 'passWord1',
8
+ password_confirmation: 'passWord1',
9
+ email: 'bob@microsoft.com')
10
+ @user.confirm
11
+ end
12
+
13
+ test 'failed login' do
14
+ assert_nil @user.unique_session_id
15
+
16
+ open_session do |session|
17
+ failed_sign_in(@user, session)
18
+ session.assert_response(:success)
19
+ assert_equal session.flash[:alert], I18n.t('devise.failure.invalid', authentication_keys: 'Email')
20
+ assert_nil @user.reload.unique_session_id
21
+ end
22
+ end
23
+
24
+ test 'successful login' do
25
+ assert_nil @user.unique_session_id
26
+
27
+ open_session do |session|
28
+ sign_in(@user, session)
29
+ session.assert_redirected_to '/'
30
+ session.get widgets_path
31
+ session.assert_response(:success)
32
+ assert_equal session.response.body, 'success'
33
+ assert_not_nil @user.reload.unique_session_id
34
+ end
35
+ end
36
+
37
+ test 'session is logged out when another session is created' do
38
+ first_session = open_session
39
+ second_session = open_session
40
+ unique_session_id = nil
41
+
42
+ first_session.tap do |session|
43
+ sign_in(@user, session)
44
+ session.assert_redirected_to '/'
45
+ session.get widgets_path
46
+ session.assert_response(:success)
47
+ assert_equal session.response.body, 'success'
48
+ unique_session_id = @user.reload.unique_session_id
49
+ assert_not_nil unique_session_id
50
+ end
51
+
52
+ second_session.tap do |session|
53
+ sign_in(@user, session)
54
+ session.assert_redirected_to '/'
55
+ session.get widgets_path
56
+ session.assert_response(:success)
57
+ assert_equal session.response.body, 'success'
58
+ assert_not_equal unique_session_id, @user.reload.unique_session_id
59
+ end
60
+
61
+ first_session.tap do |session|
62
+ session.get widgets_path
63
+ session.assert_redirected_to new_user_session_path
64
+ assert_equal session.flash[:alert], I18n.t('devise.failure.session_limited')
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,47 @@
1
+ module IntegrationHelpers
2
+ # login the user. This will exercise all the Warden Hooks
3
+ # @param user [User]
4
+ # @param session [ActionDispatch::Integration::Session]
5
+ # @return [void]
6
+ # @note accounts for differences in the integration test API between rails versions
7
+ def sign_in(user, session)
8
+ if Rails.gem_version > Gem::Version.new('5.0')
9
+ session.post new_user_session_path, params: {
10
+ user: {
11
+ email: user.email,
12
+ password: user.password
13
+ }
14
+ }
15
+ else
16
+ session.post new_user_session_path, {
17
+ user: {
18
+ email: user.email,
19
+ password: user.password
20
+ }
21
+ }
22
+ end
23
+ end
24
+
25
+ # attempt to login the user with a bad password. This will exercise all the Warden Hooks
26
+ # @param user [User]
27
+ # @param session [ActionDispatch::Integration::Session]
28
+ # @return [void]
29
+ # @note accounts for differences in the integration test API between rails versions
30
+ def failed_sign_in(user, session)
31
+ if Rails.gem_version > Gem::Version.new('5.0')
32
+ session.post new_user_session_path, params: {
33
+ user: {
34
+ email: user.email,
35
+ password: 'bad-password'
36
+ }
37
+ }
38
+ else
39
+ session.post new_user_session_path, {
40
+ user: {
41
+ email: user.email,
42
+ password: 'bad-password'
43
+ }
44
+ }
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,13 @@
1
+ require 'test_helper'
2
+
3
+ class TestCompatibility < ActiveSupport::TestCase
4
+ test 'can access ActiveRecord namespace' do
5
+ skip unless DEVISE_ORM == :active_record
6
+ assert_nothing_raised { User.new.some_method_calling_active_record }
7
+ end
8
+
9
+ test 'can access Mongoid namespace' do
10
+ skip unless DEVISE_ORM == :mongoid
11
+ assert_nothing_raised { User.new.some_method_calling_mongoid }
12
+ end
13
+ end
@@ -5,9 +5,17 @@ ENV['RAILS_ENV'] ||= 'test'
5
5
  require 'simplecov'
6
6
  SimpleCov.start do
7
7
  add_filter 'gemfiles'
8
+ add_filter 'test/dummy/db'
9
+ add_group 'ActiveRecord', 'active_record'
10
+ add_group 'Expirable', /(?<!password_)expirable/
11
+ add_group 'Mongoid', 'mongoid'
12
+ add_group 'Paranoid Verifiable', 'paranoid_verification'
13
+ add_group 'Password Archivable', /password_archivable|old_password/
14
+ add_group 'Password Expirable', /password_expirable|password_expired/
15
+ add_group 'Secure Validateable', 'secure_validatable'
16
+ add_group 'Security Questionable', 'security_question'
17
+ add_group 'Session Limitable', 'session_limitable'
8
18
  add_group 'Tests', 'test'
9
- add_group 'Password Archivable', 'password_archivable'
10
- add_group 'Password Expirable', 'password_expirable'
11
19
  end
12
20
 
13
21
  if ENV['CI']
@@ -23,6 +31,7 @@ require 'rails/test_help'
23
31
  require 'devise-security'
24
32
  require 'database_cleaner'
25
33
  require "orm/#{DEVISE_ORM}"
34
+ require 'support/integration_helpers'
26
35
 
27
36
  class Minitest::Test
28
37
  def before_setup
@@ -0,0 +1,31 @@
1
+ require 'test_helper'
2
+
3
+ class TestSessionLimitable < ActiveSupport::TestCase
4
+
5
+ test '#update_unique_session_id!(value) updates valid record' do
6
+ user = User.create! password: 'passWord1', password_confirmation: 'passWord1', email: 'bob@microsoft.com'
7
+ assert user.persisted?
8
+ assert_nil user.unique_session_id
9
+ user.update_unique_session_id!('unique_value')
10
+ user.reload
11
+ assert_equal user.unique_session_id, 'unique_value'
12
+ end
13
+
14
+ test '#update_unique_session_id!(value) updates invalid record atomically' do
15
+ user = User.create! password: 'passWord1', password_confirmation: 'passWord1', email: 'bob@microsoft.com'
16
+ assert user.persisted?
17
+ user.email = ''
18
+ assert user.invalid?
19
+ assert_nil user.unique_session_id
20
+ user.update_unique_session_id!('unique_value')
21
+ user.reload
22
+ assert_equal user.email, 'bob@microsoft.com'
23
+ assert_equal user.unique_session_id, 'unique_value'
24
+ end
25
+
26
+ test '#update_unique_session_id!(value) raises an exception on an unpersisted record' do
27
+ user = User.create
28
+ assert !user.persisted?
29
+ assert_raises(Devise::Models::Compatibility::NotPersistedError) { user.update_unique_session_id!('unique_value') }
30
+ end
31
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devise-security
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.1
4
+ version: 0.14.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marco Scholl
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2019-04-26 00:00:00.000000000 Z
15
+ date: 2019-05-21 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: rails
@@ -293,8 +293,8 @@ files:
293
293
  - lib/devise-security/hooks/session_limitable.rb
294
294
  - lib/devise-security/models/active_record/old_password.rb
295
295
  - lib/devise-security/models/compatibility.rb
296
- - lib/devise-security/models/compatibility/active_record.rb
297
- - lib/devise-security/models/compatibility/mongoid.rb
296
+ - lib/devise-security/models/compatibility/active_record_patch.rb
297
+ - lib/devise-security/models/compatibility/mongoid_patch.rb
298
298
  - lib/devise-security/models/database_authenticatable_patch.rb
299
299
  - lib/devise-security/models/expirable.rb
300
300
  - lib/devise-security/models/mongoid/old_password.rb
@@ -324,11 +324,15 @@ files:
324
324
  - lib/devise-security/version.rb
325
325
  - lib/generators/devise_security/install_generator.rb
326
326
  - lib/generators/templates/devise-security.rb
327
+ - test/controllers/test_captcha_controller.rb
328
+ - test/controllers/test_password_expired_controller.rb
329
+ - test/controllers/test_security_question_controller.rb
327
330
  - test/dummy/Rakefile
328
331
  - test/dummy/app/controllers/application_controller.rb
329
332
  - test/dummy/app/controllers/captcha/sessions_controller.rb
330
333
  - test/dummy/app/controllers/foos_controller.rb
331
334
  - test/dummy/app/controllers/security_question/unlocks_controller.rb
335
+ - test/dummy/app/controllers/widgets_controller.rb
332
336
  - test/dummy/app/models/.gitkeep
333
337
  - test/dummy/app/models/application_record.rb
334
338
  - test/dummy/app/models/application_user_record.rb
@@ -390,19 +394,20 @@ files:
390
394
  - test/dummy/lib/shared_user_without_email.rb
391
395
  - test/dummy/lib/shared_user_without_omniauth.rb
392
396
  - test/dummy/lib/shared_verification_fields.rb
397
+ - test/integration/test_session_limitable_workflow.rb
393
398
  - test/orm/active_record.rb
394
399
  - test/orm/mongoid.rb
400
+ - test/support/integration_helpers.rb
395
401
  - test/support/mongoid.yml
396
- - test/test_captcha_controller.rb
402
+ - test/test_compatibility.rb
397
403
  - test/test_complexity_validator.rb
398
404
  - test/test_helper.rb
399
405
  - test/test_install_generator.rb
400
406
  - test/test_paranoid_verification.rb
401
407
  - test/test_password_archivable.rb
402
408
  - test/test_password_expirable.rb
403
- - test/test_password_expired_controller.rb
404
409
  - test/test_secure_validatable.rb
405
- - test/test_security_question_controller.rb
410
+ - test/test_session_limitable.rb
406
411
  homepage: https://github.com/devise-security/devise-security
407
412
  licenses:
408
413
  - MIT
@@ -428,11 +433,15 @@ signing_key:
428
433
  specification_version: 4
429
434
  summary: Security extension for devise
430
435
  test_files:
436
+ - test/controllers/test_captcha_controller.rb
437
+ - test/controllers/test_password_expired_controller.rb
438
+ - test/controllers/test_security_question_controller.rb
431
439
  - test/dummy/Rakefile
432
440
  - test/dummy/app/controllers/application_controller.rb
433
441
  - test/dummy/app/controllers/captcha/sessions_controller.rb
434
442
  - test/dummy/app/controllers/foos_controller.rb
435
443
  - test/dummy/app/controllers/security_question/unlocks_controller.rb
444
+ - test/dummy/app/controllers/widgets_controller.rb
436
445
  - test/dummy/app/models/.gitkeep
437
446
  - test/dummy/app/models/application_record.rb
438
447
  - test/dummy/app/models/application_user_record.rb
@@ -494,16 +503,17 @@ test_files:
494
503
  - test/dummy/lib/shared_user_without_email.rb
495
504
  - test/dummy/lib/shared_user_without_omniauth.rb
496
505
  - test/dummy/lib/shared_verification_fields.rb
506
+ - test/integration/test_session_limitable_workflow.rb
497
507
  - test/orm/active_record.rb
498
508
  - test/orm/mongoid.rb
509
+ - test/support/integration_helpers.rb
499
510
  - test/support/mongoid.yml
500
- - test/test_captcha_controller.rb
511
+ - test/test_compatibility.rb
501
512
  - test/test_complexity_validator.rb
502
513
  - test/test_helper.rb
503
514
  - test/test_install_generator.rb
504
515
  - test/test_paranoid_verification.rb
505
516
  - test/test_password_archivable.rb
506
517
  - test/test_password_expirable.rb
507
- - test/test_password_expired_controller.rb
508
518
  - test/test_secure_validatable.rb
509
- - test/test_security_question_controller.rb
519
+ - test/test_session_limitable.rb