devise-security 0.14.0.rc1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +116 -60
  3. data/app/controllers/devise/password_expired_controller.rb +10 -1
  4. data/app/views/devise/paranoid_verification_code/show.html.erb +3 -3
  5. data/app/views/devise/password_expired/show.html.erb +5 -5
  6. data/config/locales/by.yml +48 -0
  7. data/config/locales/cs.yml +40 -0
  8. data/config/locales/de.yml +12 -2
  9. data/config/locales/en.yml +12 -1
  10. data/config/locales/es.yml +9 -9
  11. data/config/locales/fa.yml +40 -0
  12. data/config/locales/hi.yml +41 -0
  13. data/config/locales/it.yml +34 -4
  14. data/config/locales/ja.yml +1 -1
  15. data/config/locales/nl.yml +40 -0
  16. data/config/locales/pt.yml +40 -0
  17. data/config/locales/ru.yml +48 -0
  18. data/config/locales/uk.yml +48 -0
  19. data/config/locales/zh_CN.yml +40 -0
  20. data/config/locales/zh_TW.yml +40 -0
  21. data/lib/devise-security.rb +1 -0
  22. data/lib/devise-security/controllers/helpers.rb +59 -50
  23. data/lib/devise-security/hooks/password_expirable.rb +2 -0
  24. data/lib/devise-security/hooks/session_limitable.rb +21 -10
  25. data/lib/devise-security/models/compatibility.rb +2 -2
  26. data/lib/devise-security/models/compatibility/{active_record.rb → active_record_patch.rb} +12 -1
  27. data/lib/devise-security/models/compatibility/{mongoid.rb → mongoid_patch.rb} +11 -1
  28. data/lib/devise-security/models/mongoid/old_password.rb +1 -1
  29. data/lib/devise-security/models/password_expirable.rb +5 -1
  30. data/lib/devise-security/models/session_limitable.rb +17 -2
  31. data/lib/devise-security/schema.rb +1 -1
  32. data/lib/devise-security/validators/password_complexity_validator.rb +4 -2
  33. data/lib/devise-security/version.rb +1 -1
  34. data/lib/generators/devise_security/install_generator.rb +2 -2
  35. data/test/{test_captcha_controller.rb → controllers/test_captcha_controller.rb} +0 -0
  36. data/test/controllers/test_password_expired_controller.rb +141 -0
  37. data/test/{test_security_question_controller.rb → controllers/test_security_question_controller.rb} +0 -0
  38. data/test/dummy/app/assets/config/manifest.js +3 -0
  39. data/test/dummy/app/controllers/widgets_controller.rb +6 -0
  40. data/test/dummy/app/models/user.rb +8 -0
  41. data/test/dummy/config/application.rb +1 -0
  42. data/test/dummy/config/routes.rb +4 -3
  43. data/test/dummy/db/migrate/20120508165529_create_tables.rb +11 -2
  44. data/test/dummy/log/test.log +1799 -0
  45. data/test/integration/test_password_expirable_workflow.rb +57 -0
  46. data/test/integration/test_session_limitable_workflow.rb +67 -0
  47. data/test/orm/active_record.rb +4 -1
  48. data/test/support/integration_helpers.rb +47 -0
  49. data/test/test_compatibility.rb +13 -0
  50. data/test/test_complexity_validator.rb +12 -0
  51. data/test/test_helper.rb +21 -6
  52. data/test/test_install_generator.rb +10 -0
  53. data/test/test_session_limitable.rb +57 -0
  54. data/test/tmp/config/initializers/devise-security.rb +44 -0
  55. data/test/tmp/config/locales/devise.security_extension.de.yml +38 -0
  56. data/test/tmp/config/locales/devise.security_extension.en.yml +40 -0
  57. data/test/tmp/config/locales/devise.security_extension.es.yml +29 -0
  58. data/test/tmp/config/locales/devise.security_extension.fa.yml +40 -0
  59. data/test/tmp/config/locales/devise.security_extension.fr.yml +29 -0
  60. data/test/tmp/config/locales/devise.security_extension.it.yml +40 -0
  61. data/test/tmp/config/locales/devise.security_extension.ja.yml +29 -0
  62. data/test/tmp/config/locales/devise.security_extension.nl.yml +40 -0
  63. data/test/tmp/config/locales/devise.security_extension.pt.yml +40 -0
  64. data/test/tmp/config/locales/devise.security_extension.ru.yml +48 -0
  65. data/test/tmp/config/locales/devise.security_extension.tr.yml +17 -0
  66. data/test/tmp/config/locales/devise.security_extension.uk.yml +48 -0
  67. data/test/tmp/config/locales/devise.security_extension.zh_CN.yml +40 -0
  68. metadata +165 -121
  69. data/.codeclimate.yml +0 -63
  70. data/.document +0 -5
  71. data/.gitignore +0 -43
  72. data/.mdlrc +0 -1
  73. data/.rubocop.yml +0 -64
  74. data/.ruby-version +0 -1
  75. data/.travis.yml +0 -41
  76. data/Appraisals +0 -35
  77. data/Gemfile +0 -10
  78. data/Rakefile +0 -28
  79. data/devise-security.gemspec +0 -50
  80. data/gemfiles/rails_4.2_stable.gemfile +0 -16
  81. data/gemfiles/rails_5.0_stable.gemfile +0 -15
  82. data/gemfiles/rails_5.1_stable.gemfile +0 -15
  83. data/gemfiles/rails_5.2_stable.gemfile +0 -15
  84. data/gemfiles/rails_6.0_beta.gemfile +0 -15
  85. data/test/dummy/app/models/.gitkeep +0 -0
  86. data/test/test_password_expired_controller.rb +0 -46
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestPasswordExpirableWorkflow < ActionDispatch::IntegrationTest
6
+ include IntegrationHelpers
7
+
8
+ setup do
9
+ @user = User.create!(password: 'passWord1',
10
+ password_confirmation: 'passWord1',
11
+ email: 'bob@microsoft.com',
12
+ password_changed_at: 4.months.ago) # the default expiration time is 3.months.ago
13
+ @user.confirm
14
+
15
+ assert @user.valid?
16
+ assert @user.need_change_password?
17
+ end
18
+
19
+ test 'sign in and change expired password' do
20
+ skip("Does not work in Rails < 5.0") if Rails.gem_version < Gem::Version.new('5.0')
21
+
22
+ sign_in(@user)
23
+ assert_redirected_to(root_path)
24
+ follow_redirect!
25
+ assert_redirected_to(user_password_expired_path)
26
+ # @note This is not the same controller used by Devise for password changes
27
+ put '/users/password_expired', params: {
28
+ user: {
29
+ current_password: 'passWord1',
30
+ password: 'Password12345!',
31
+ password_confirmation: 'Password12345!',
32
+ },
33
+ }
34
+ assert_redirected_to(root_path)
35
+ @user.reload
36
+ assert_not @user.need_change_password?
37
+ end
38
+
39
+ test 'sign in and password is updated before redirect completes' do
40
+ skip("Does not work in Rails < 5.0") if Rails.gem_version < Gem::Version.new('5.0')
41
+
42
+ sign_in(@user)
43
+ assert_redirected_to(root_path)
44
+
45
+ # simulates an external process updating the password
46
+ @user.update(password_changed_at: Time.zone.now)
47
+ assert_not @user.need_change_password?
48
+
49
+ follow_redirect!
50
+ assert_response :success
51
+
52
+ # if the password is expired at this point they will be redirected to the
53
+ # password change controller.
54
+ get root_path
55
+ assert_response :success
56
+ end
57
+ end
@@ -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
@@ -2,7 +2,10 @@ require 'active_record'
2
2
 
3
3
  ActiveRecord::Migration.verbose = false
4
4
  ActiveRecord::Base.logger = Logger.new(nil)
5
- if Rails.gem_version >= Gem::Version.new('5.2.0')
5
+ case
6
+ when Rails.gem_version >= Gem::Version.new('6.0.0')
7
+ ActiveRecord::MigrationContext.new(File.expand_path('../../dummy/db/migrate', __FILE__), ActiveRecord::SchemaMigration).migrate
8
+ when Rails.gem_version >= Gem::Version.new('5.2.0')
6
9
  ActiveRecord::MigrationContext.new(File.expand_path('../../dummy/db/migrate', __FILE__)).migrate
7
10
  else
8
11
  ActiveRecord::Migrator.migrate(File.expand_path('../../dummy/db/migrate', __FILE__))
@@ -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 = integration_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
@@ -37,6 +37,12 @@ class PasswordComplexityValidatorTest < Minitest::Test
37
37
  assert(ModelWithPassword.new('aaa1').valid?)
38
38
  end
39
39
 
40
+ def test_enforces_digits
41
+ ModelWithPassword.validates :password, 'devise_security/password_complexity': { digits: 2 }
42
+ refute(ModelWithPassword.new('aaa1').valid?)
43
+ assert(ModelWithPassword.new('aa12').valid?)
44
+ end
45
+
40
46
  def test_enforces_lower
41
47
  ModelWithPassword.validates :password, 'devise_security/password_complexity': { lower: 1 }
42
48
  refute(ModelWithPassword.new('AAAA').valid?)
@@ -49,6 +55,12 @@ class PasswordComplexityValidatorTest < Minitest::Test
49
55
  assert(ModelWithPassword.new('aaa!').valid?)
50
56
  end
51
57
 
58
+ def test_enforces_symbols
59
+ ModelWithPassword.validates :password, 'devise_security/password_complexity': { symbols: 2 }
60
+ refute(ModelWithPassword.new('aaa!').valid?)
61
+ assert(ModelWithPassword.new('aa!?').valid?)
62
+ end
63
+
52
64
  def test_enforces_combination
53
65
  ModelWithPassword.validates :password, 'devise_security/password_complexity': { lower: 1, upper: 1, digit: 1, symbol: 1 }
54
66
  refute(ModelWithPassword.new('abcd').valid?)
@@ -1,20 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  ENV['RAILS_ENV'] ||= 'test'
4
- DEVISE_ORM = ENV.fetch('DEVISE_ORM', 'active_record').to_sym
5
4
 
6
5
  require 'simplecov'
7
6
  SimpleCov.start do
8
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'
9
18
  add_group 'Tests', 'test'
10
- add_group 'Password Archivable', 'password_archivable'
11
- add_group 'Password Expirable', 'password_expirable'
12
19
  end
13
20
 
14
21
  if ENV['CI']
15
- require 'coveralls'
16
- SimpleCov.formatter = Coveralls::SimpleCov::Formatter
17
- Coveralls.wear!
22
+ require 'simplecov'
23
+ require 'simplecov-lcov'
24
+ SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter
25
+ SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true
26
+ SimpleCov.start
18
27
  end
19
28
 
20
29
  require 'pry'
@@ -25,6 +34,12 @@ require 'devise-security'
25
34
  require 'database_cleaner'
26
35
  require "orm/#{DEVISE_ORM}"
27
36
 
37
+ if Rails.gem_version >= Gem::Version.new('5.0.0')
38
+ require 'rails-controller-testing'
39
+ Rails::Controller::Testing.install
40
+ end
41
+ require 'support/integration_helpers'
42
+
28
43
  class Minitest::Test
29
44
  def before_setup
30
45
  DatabaseCleaner.start
@@ -12,12 +12,22 @@ class TestInstallGenerator < Rails::Generators::TestCase
12
12
  test 'Assert all files are properly created' do
13
13
  run_generator
14
14
  assert_file 'config/initializers/devise-security.rb'
15
+ assert_file 'config/locales/devise.security_extension.by.yml'
16
+ assert_file 'config/locales/devise.security_extension.cs.yml'
15
17
  assert_file 'config/locales/devise.security_extension.de.yml'
16
18
  assert_file 'config/locales/devise.security_extension.en.yml'
17
19
  assert_file 'config/locales/devise.security_extension.es.yml'
20
+ assert_file 'config/locales/devise.security_extension.fa.yml'
18
21
  assert_file 'config/locales/devise.security_extension.fr.yml'
22
+ assert_file 'config/locales/devise.security_extension.hi.yml'
19
23
  assert_file 'config/locales/devise.security_extension.it.yml'
20
24
  assert_file 'config/locales/devise.security_extension.ja.yml'
25
+ assert_file 'config/locales/devise.security_extension.nl.yml'
26
+ assert_file 'config/locales/devise.security_extension.pt.yml'
27
+ assert_file 'config/locales/devise.security_extension.ru.yml'
21
28
  assert_file 'config/locales/devise.security_extension.tr.yml'
29
+ assert_file 'config/locales/devise.security_extension.uk.yml'
30
+ assert_file 'config/locales/devise.security_extension.zh_CN.yml'
31
+ assert_file 'config/locales/devise.security_extension.zh_TW.yml'
22
32
  end
23
33
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestSessionLimitable < ActiveSupport::TestCase
6
+ class ModifiedUser < User
7
+ def skip_session_limitable?
8
+ true
9
+ end
10
+ end
11
+
12
+ test 'check is not skipped by default' do
13
+ user = User.create email: 'bob@microsoft.com', password: 'password1', password_confirmation: 'password1'
14
+ assert_equal(false, user.skip_session_limitable?)
15
+ end
16
+
17
+ test 'default check can be overridden by record instance' do
18
+ modified_user = ModifiedUser.create email: 'bob2@microsoft.com', password: 'password1', password_confirmation: 'password1'
19
+ assert_equal(true, modified_user.skip_session_limitable?)
20
+ end
21
+
22
+ class SessionLimitableUser < User
23
+ devise :session_limitable
24
+ include ::Mongoid::Mappings if DEVISE_ORM == :mongoid
25
+ end
26
+
27
+ test 'includes Devise::Models::Compatibility' do
28
+ assert_kind_of(Devise::Models::Compatibility, SessionLimitableUser.new)
29
+ end
30
+
31
+ test '#update_unique_session_id!(value) updates valid record' do
32
+ user = User.create! password: 'passWord1', password_confirmation: 'passWord1', email: 'bob@microsoft.com'
33
+ assert user.persisted?
34
+ assert_nil user.unique_session_id
35
+ user.update_unique_session_id!('unique_value')
36
+ user.reload
37
+ assert_equal user.unique_session_id, 'unique_value'
38
+ end
39
+
40
+ test '#update_unique_session_id!(value) updates invalid record atomically' do
41
+ user = User.create! password: 'passWord1', password_confirmation: 'passWord1', email: 'bob@microsoft.com'
42
+ assert user.persisted?
43
+ user.email = ''
44
+ assert user.invalid?
45
+ assert_nil user.unique_session_id
46
+ user.update_unique_session_id!('unique_value')
47
+ user.reload
48
+ assert_equal user.email, 'bob@microsoft.com'
49
+ assert_equal user.unique_session_id, 'unique_value'
50
+ end
51
+
52
+ test '#update_unique_session_id!(value) raises an exception on an unpersisted record' do
53
+ user = User.create
54
+ assert !user.persisted?
55
+ assert_raises(Devise::Models::Compatibility::NotPersistedError) { user.update_unique_session_id!('unique_value') }
56
+ end
57
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ Devise.setup do |config|
4
+ # ==> Security Extension
5
+ # Configure security extension for devise
6
+
7
+ # Should the password expire (e.g 3.months)
8
+ # config.expire_password_after = false
9
+
10
+ # Need 1 char of A-Z, a-z and 0-9
11
+ # config.password_complexity = { digit: 1, lower: 1, symbol: 1, upper: 1 }
12
+
13
+ # How many passwords to keep in archive
14
+ # config.password_archiving_count = 5
15
+
16
+ # Deny old passwords (true, false, number_of_old_passwords_to_check)
17
+ # Examples:
18
+ # config.deny_old_passwords = false # allow old passwords
19
+ # config.deny_old_passwords = true # will deny all the old passwords
20
+ # config.deny_old_passwords = 3 # will deny new passwords that matches with the last 3 passwords
21
+ # config.deny_old_passwords = true
22
+
23
+ # enable email validation for :secure_validatable. (true, false, validation_options)
24
+ # dependency: see https://github.com/devise-security/devise-security/blob/master/README.md#e-mail-validation
25
+ # config.email_validation = true
26
+
27
+ # captcha integration for recover form
28
+ # config.captcha_for_recover = true
29
+
30
+ # captcha integration for sign up form
31
+ # config.captcha_for_sign_up = true
32
+
33
+ # captcha integration for sign in form
34
+ # config.captcha_for_sign_in = true
35
+
36
+ # captcha integration for unlock form
37
+ # config.captcha_for_unlock = true
38
+
39
+ # captcha integration for confirmation form
40
+ # config.captcha_for_confirmation = true
41
+
42
+ # Time period for account expiry from last_activity_at
43
+ # config.expire_after = 90.days
44
+ end
@@ -0,0 +1,38 @@
1
+ de:
2
+ errors:
3
+ messages:
4
+ taken_in_past: 'wurde bereits in der Vergangenheit verwendet.'
5
+ equal_to_current_password: 'darf nicht dem aktuellen Passwort entsprechen.'
6
+ password_complexity:
7
+ digit:
8
+ one: muss mindestens eine Ziffer enthalten
9
+ other: muss mindestens %{count} Ziffern enthalten
10
+ lower:
11
+ one: muss mindestens einen Kleinbuchstaben enthalten
12
+ other: muss mindestens %{count} Kleinbuchstaben enthalten
13
+ symbol:
14
+ one: muss mindestens ein Sonderzeichen enthalten
15
+ other: muss mindestens %{count} Sonderzeichen enthalten
16
+ upper:
17
+ one: muss mindestens einen Großbuchstaben enthalten
18
+ other: muss mindestens %{count} Großbuchstaben enthalten
19
+ devise:
20
+ invalid_captcha: 'Die Captcha-Eingabe ist nicht gültig.'
21
+ paranoid_verify:
22
+ code_required: 'Bitte geben Sie den Code ein, den unser Support-Team zur Verfügung gestellt hat.'
23
+ show:
24
+ submit_verification_code: Bestätigungscode eingeben
25
+ verification_code: Bestätigungscode
26
+ submit: Bestätigen
27
+ password_expired:
28
+ updated: 'Das neue Passwort wurde übernommen.'
29
+ change_required: 'Ihr Passwort ist abgelaufen. Bitte vergeben Sie ein neues Passwort.'
30
+ show:
31
+ renew_your_password: Vergeben Sie ein neues Passwort
32
+ current_password: Aktuelles Passwort
33
+ new_password: Neues Passwort
34
+ new_password_confirmation: Passwort bestätigen
35
+ change_my_password: Passwort ändern
36
+ failure:
37
+ session_limited: 'Ihre Anmeldedaten wurden in einem anderen Browser genutzt. Bitte melden Sie sich erneut an, um in diesem Browser fortzufahren.'
38
+ expired: 'Ihr Account ist aufgrund zu langer Inaktivität abgelaufen. Bitte kontaktieren Sie den Administrator.'
@@ -0,0 +1,40 @@
1
+ en:
2
+ errors:
3
+ messages:
4
+ taken_in_past: 'was used previously.'
5
+ equal_to_current_password: 'must be different than the current password.'
6
+ password_complexity:
7
+ digit:
8
+ one: must contain at least one digit
9
+ other: must contain at least %{count} numerals
10
+ lower:
11
+ one: must contain at least one lower-case letter
12
+ other: must contain at least %{count} lower-case letters
13
+ symbol:
14
+ one: must contain at least one punctuation mark or symbol
15
+ other: must contain at least %{count} punctuation marks or symbols
16
+ upper:
17
+ one: must contain at least one upper-case letter
18
+ other: must contain at least %{count} upper-case letters
19
+ devise:
20
+ invalid_captcha: 'The captcha input was invalid.'
21
+ invalid_security_question: 'The security question answer was invalid.'
22
+ paranoid_verify:
23
+ code_required: 'Please enter the code our support team provided'
24
+ paranoid_verification_code:
25
+ show:
26
+ submit_verification_code: Submit verification code
27
+ verification_code: Verification code
28
+ submit: Submit
29
+ password_expired:
30
+ updated: 'Your new password is saved.'
31
+ change_required: 'Your password is expired. Please renew your password.'
32
+ show:
33
+ renew_your_password: Renew your password
34
+ current_password: Current password
35
+ new_password: New password
36
+ new_password_confirmation: Confirm new password
37
+ change_my_password: Change my password
38
+ failure:
39
+ session_limited: 'Your login credentials were used in another browser. Please sign in again to continue in this browser.'
40
+ expired: 'Your account has expired due to inactivity. Please contact the site administrator.'