devise-security 0.14.1 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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