devise-pwned_password 0.1.4 → 0.1.9

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
- SHA1:
3
- metadata.gz: 9034d6a6d49339f92023d27ef40b8633a2332ded
4
- data.tar.gz: 1e429adc52ffb2aff28985d9a1636aec657b686a
2
+ SHA256:
3
+ metadata.gz: 366fc1a14fb5abd102a74ce7a814219815c3ab764ae7c2d3c14296fb72679f76
4
+ data.tar.gz: 682e24df4e1a6cbf674c58c34cac5a16f4a233d351f52f7400aa8f35a83e7f72
5
5
  SHA512:
6
- metadata.gz: 75aede6ba1404dc2065db2d404467071d1d00397da09ad141f760edb8d74617e750d8707e287ab2ebf088bb85cc1038acb36c7b6099c5dc73bc8372ebb66b067
7
- data.tar.gz: 0ca55ec7326ed19bde60f5bc4d5cc55ffaf09b68a65a7162372078641668f743134b1267940e5ecfe5a5263cbb3b39078f93a3a0b9fa912b4fee6dca8bceb4fc
6
+ metadata.gz: 89f437434e88bc07874002f0baed66f210a15e4a7b9fa3d4377d4c9005c63d3b5a9f5476bf8ccb63ced1d34e81c1f8d3ab7d9f2137d332e6d3557fe4dad53456
7
+ data.tar.gz: de00ba890a8edad1bdac2281332f72570521794616666bd0efa1b50d0f138dbec1d852ba8de1639822caf7a8743130efe5318a8c94db57a2b48d97fac75e95aa
data/README.md CHANGED
@@ -1,17 +1,23 @@
1
1
  # Devise::PwnedPassword
2
- Devise extension that checks user passwords against the PwnedPasswords dataset https://haveibeenpwned.com/Passwords
2
+ Devise extension that checks user passwords against the PwnedPasswords dataset (https://haveibeenpwned.com/Passwords).
3
3
 
4
- Based on
4
+ Checks for compromised ("pwned") passwords in 2 different places/ways:
5
+ 1. As a standard model validation using [pwned](https://github.com/philnash/pwned). This:
6
+ - prevents new users from being created (signing up) with a compromised password
7
+ - prevents existing users from changing their password to a password that is known to be compromised
8
+ 2. (Optionally) Whenever a user signs in, checks if their current password is compromised and shows a warning if it is.
5
9
 
6
- https://github.com/HCLarsen/devise-uncommon_password
10
+ Based on [devise-uncommon_password](https://github.com/HCLarsen/devise-uncommon_password).
11
+
12
+ Recently the HaveIBeenPwned API has moved to an [authenticated/paid model](https://www.troyhunt.com/authentication-and-the-have-i-been-pwned-api/), but this does not affect the PwnedPasswords API; no payment or authentication is required.
7
13
 
8
14
 
9
15
  ## Usage
10
- Add the :pwned_password module to your existing Devise model.
16
+ Add the `:pwned_password` module to your existing Devise model.
11
17
 
12
18
  ```ruby
13
19
  class AdminUser < ApplicationRecord
14
- devise :database_authenticatable,
20
+ devise :database_authenticatable,
15
21
  :recoverable, :rememberable, :trackable, :validatable, :pwned_password
16
22
  end
17
23
  ```
@@ -23,6 +29,8 @@ PwnedPasswords dataset:
23
29
  Password has previously appeared in a data breach and should never be used. Please choose something harder to guess.
24
30
  ```
25
31
 
32
+ ## Configuration
33
+
26
34
  You can customize this error message by modifying the `devise` YAML file.
27
35
 
28
36
  ```yml
@@ -33,22 +41,6 @@ en:
33
41
  pwned_password: "has previously appeared in a data breach and should never be used. If you've ever used it anywhere before, change it immediately!"
34
42
  ```
35
43
 
36
- You can optionally warn existing users when they sign in if they are using a password from the PwnedPasswords dataset. The default message is:
37
-
38
- ```
39
- Your password has previously appeared in a data breach and should never be used. We strongly recommend you change your password.
40
- ```
41
-
42
- You can customize this message by modifying the `devise` YAML file.
43
-
44
- ```yml
45
- # config/locales/devise.en.yml
46
- en:
47
- devise:
48
- sessions:
49
- warn_pwned: "Your password has previously appeared in a data breach and should never be used. We strongly recommend you change your password everywhere you have used it."
50
- ```
51
-
52
44
  By default passwords are rejected if they appear at all in the data set.
53
45
  Optionally, you can add the following snippet to `config/initializers/devise.rb`
54
46
  if you want the error message to be displayed only when the password is present
@@ -70,38 +62,103 @@ config.pwned_password_open_timeout = 1
70
62
  config.pwned_password_read_timeout = 2
71
63
  ```
72
64
 
73
- ## Installation
74
- Add this line to your application's Gemfile:
75
65
 
76
- ```ruby
77
- gem 'devise-pwned_password'
78
- ```
66
+ ### How to warn existing users when they sign in
79
67
 
80
- And then execute:
81
- ```bash
82
- $ bundle install
83
- ```
68
+ You can optionally warn existing users when they sign in if they are using a password from the PwnedPasswords dataset.
69
+
70
+ To enable this, you _must_ override `after_sign_in_path_for`, like this:
84
71
 
85
- Optionally, if you also want to warn existing users when they sign in, override `after_sign_in_path_for`
86
72
  ```ruby
87
- def after_sign_in_path_for(resource)
88
- set_flash_message! :alert, :warn_pwned if resource.respond_to?(:pwned?) && resource.pwned?
89
- super
90
- end
73
+ # app/controllers/application_controller.rb
74
+
75
+ def after_sign_in_path_for(resource)
76
+ set_flash_message! :alert, :warn_pwned if resource.respond_to?(:pwned?) && resource.pwned?
77
+ super
78
+ end
91
79
  ```
92
80
 
93
- This should generally be added in ```app/controllers/application_controller.rb``` for a rails app. For an Active Admin application the following monkey patch is needed.
81
+ For an [Active Admin](https://github.com/activeadmin/activeadmin) application the following monkey patch is needed:
94
82
 
95
83
  ```ruby
96
84
  # config/initializers/active_admin_devise_sessions_controller.rb
97
85
  class ActiveAdmin::Devise::SessionsController
98
86
  def after_sign_in_path_for(resource)
99
- set_flash_message! :alert, :warn_pwned if resource.respond_to?(:pwned?) && resource.pwned?
100
- super
87
+ set_flash_message! :alert, :warn_pwned if resource.respond_to?(:pwned?) && resource.pwned?
88
+ super
101
89
  end
102
90
  end
103
91
  ```
104
92
 
93
+ To prevent the default call to the HaveIBeenPwned API on user sign-in (only
94
+ really useful if you're going to check `pwned?` after sign-in as used above),
95
+ add the following to `config/initializers/devise.rb`:
96
+
97
+ ```ruby
98
+ config.pwned_password_check_on_sign_in = false
99
+ ```
100
+
101
+ #### Customize warning message
102
+
103
+ The default message is:
104
+ ```
105
+ Your password has previously appeared in a data breach and should never be used. We strongly recommend you change your password.
106
+ ```
107
+
108
+ You can customize this message by modifying the `devise.en.yml` locale file.
109
+
110
+ ```yml
111
+ # config/locales/devise.en.yml
112
+ en:
113
+ devise:
114
+ sessions:
115
+ warn_pwned: "Your password has previously appeared in a data breach and should never be used. We strongly recommend you change your password everywhere you have used it."
116
+ ```
117
+
118
+ #### Customize the warning threshold
119
+
120
+ By default the same value, `config.min_password_matches` is used as the threshold for rejecting a passwords for _new_ user sign-ups and for warning existing users.
121
+
122
+ If you want to use different thresholds for rejecting the password and warning
123
+ the user (for example you may only want to reject passwords that are common but
124
+ warn if the password occurs at all in the list), you can set a different value for each.
125
+
126
+ To change the threshold used for the warning _only_, add to `config/initializers/devise.rb`
127
+
128
+ ```ruby
129
+ # Minimum number of times a pwned password must exist in the data set in order
130
+ # to warn the user.
131
+ config.min_password_matches_warn = 1
132
+ ```
133
+
134
+ Note: If you do have a different warning threshold, that threshold will also be used
135
+ when a user changes their password (added as an _error_!) so that they don't
136
+ continue to be warned if they choose another password that is in the pwned list
137
+ but occurs with a frequency below the main threshold that is used for *new*
138
+ user registrations (`config.min_password_matches`).
139
+
140
+ ### Disabling in test environments
141
+
142
+ Currently this module cannot be mocked out for test environments. Because an API call is made this can slow down tests, or make test fixtures needlessly complex (dynamically generated passwords). The module can be disabled in test environments like this.
143
+
144
+ ```ruby
145
+ class User < ApplicationRecord
146
+ devise :invitable ... :validatable, :lockable
147
+ devise :pwned_password unless Rails.env.test?
148
+ end
149
+ ```
150
+
151
+ ## Installation
152
+ Add this line to your application's Gemfile:
153
+
154
+ ```ruby
155
+ gem 'devise-pwned_password'
156
+ ```
157
+
158
+ And then execute:
159
+ ```bash
160
+ $ bundle install
161
+ ```
105
162
 
106
163
  ## Considerations
107
164
 
@@ -114,18 +171,19 @@ A few things to consider/understand when using this gem:
114
171
  to a third party. More implementation details and important caveats can be
115
172
  found in https://blog.cloudflare.com/validating-leaked-passwords-with-k-anonymity/
116
173
 
117
- * This puts an external API in the request path of users signing up to your
118
- application. This could potentially add some latency to this operation. The
119
- gem is designed to fail silently if the PwnedPasswords service is unavailable.
174
+ * This puts an external API in the request path of users signing up to your application. This could
175
+ potentially add some latency to this operation. The gem is designed to silently swallows errors if
176
+ the PwnedPasswords service is unavailable, allowing users to use compromised passwords during the
177
+ time when it is unavailable.
120
178
 
121
179
  ## Contributing
122
180
 
123
- To contribute
181
+ To contribute:
124
182
 
125
183
  * Check the [issue tracker](https://github.com/michaelbanfield/devise-pwned_password/issues) and [pull requests](https://github.com/michaelbanfield/devise-pwned_password/pulls) for anything similar
126
184
  * Fork the repository
127
185
  * Make your changes
128
- * Run bin/test to make sure the unit tests still run
186
+ * Run `bin/test` to make sure the unit tests still run
129
187
  * Send a pull request
130
188
 
131
189
  ## License
@@ -4,8 +4,11 @@ require "devise"
4
4
  require "devise/pwned_password/model"
5
5
 
6
6
  module Devise
7
- mattr_accessor :min_password_matches, :pwned_password_open_timeout, :pwned_password_read_timeout
7
+ mattr_accessor :min_password_matches, :min_password_matches_warn, :pwned_password_check_on_sign_in,
8
+ :pwned_password_open_timeout, :pwned_password_read_timeout
8
9
  @@min_password_matches = 1
10
+ @@min_password_matches_warn = nil
11
+ @@pwned_password_check_on_sign_in = true
9
12
  @@pwned_password_open_timeout = 5
10
13
  @@pwned_password_read_timeout = 5
11
14
 
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Warden::Manager.after_set_user except: :fetch do |user, auth, opts|
4
- password = auth.request.params.fetch(opts[:scope], {}).fetch(:password, nil)
5
- password && auth.authenticated?(opts[:scope]) && user.respond_to?(:password_pwned?) && user.password_pwned?(password)
4
+ if user.class.respond_to?(:pwned_password_check_on_sign_in) && user.class.pwned_password_check_on_sign_in
5
+ password = auth.request.params.fetch(opts[:scope], {}).fetch(:password, nil)
6
+ password && auth.authenticated?(opts[:scope]) && user.respond_to?(:password_pwned?) && user.password_pwned?(password)
7
+ end
6
8
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "net/http"
3
+ require "pwned"
4
4
  require "devise/pwned_password/hooks/pwned_password"
5
5
 
6
6
  module Devise
@@ -14,11 +14,14 @@ module Devise
14
14
  extend ActiveSupport::Concern
15
15
 
16
16
  included do
17
- validate :not_pwned_password, if: :password_required?
17
+ validate :not_pwned_password,
18
+ if: Devise.activerecord51? ? :will_save_change_to_encrypted_password? : :encrypted_password_changed?
18
19
  end
19
20
 
20
21
  module ClassMethods
21
22
  Devise::Models.config(self, :min_password_matches)
23
+ Devise::Models.config(self, :min_password_matches_warn)
24
+ Devise::Models.config(self, :pwned_password_check_on_sign_in)
22
25
  Devise::Models.config(self, :pwned_password_open_timeout)
23
26
  Devise::Models.config(self, :pwned_password_read_timeout)
24
27
  end
@@ -27,26 +30,37 @@ module Devise
27
30
  @pwned ||= false
28
31
  end
29
32
 
33
+ def pwned_count
34
+ @pwned_count ||= 0
35
+ end
36
+
30
37
  # Returns true if password is present in the PwnedPasswords dataset
31
- # Implement retry behaviour described here https://haveibeenpwned.com/API/v2#RateLimiting
32
38
  def password_pwned?(password)
33
39
  @pwned = false
34
- hash = Digest::SHA1.hexdigest(password.to_s).upcase
35
- prefix, suffix = hash.slice!(0..4), hash
36
-
37
- userAgent = "devise_pwned_password"
38
-
39
- uri = URI.parse("https://api.pwnedpasswords.com/range/#{prefix}")
40
+ @pwned_count = 0
40
41
 
42
+ options = {
43
+ headers: { "User-Agent" => "devise_pwned_password" },
44
+ read_timeout: self.class.pwned_password_read_timeout,
45
+ open_timeout: self.class.pwned_password_open_timeout
46
+ }
47
+ pwned_password = Pwned::Password.new(password.to_s, options)
41
48
  begin
42
- Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: self.class.pwned_password_open_timeout, read_timeout: self.class.pwned_password_read_timeout) do |http|
43
- request = Net::HTTP::Get.new(uri.request_uri, "User-Agent" => userAgent)
44
- response = http.request request
45
- return false unless response.is_a?(Net::HTTPSuccess)
46
- @pwned = usage_count(response.read_body, suffix) >= self.class.min_password_matches
47
- return @pwned
48
- end
49
- rescue StandardError
49
+ @pwned_count = pwned_password.pwned_count
50
+ @pwned = @pwned_count >= (
51
+ if persisted?
52
+ # If you do have a different warning threshold, that threshold will also be used
53
+ # when a user changes their password so that they don't continue to be warned if they
54
+ # choose another password that is in the pwned list but occurs with a frequency below
55
+ # the main threshold that is used for *new* user registrations.
56
+ self.class.min_password_matches_warn || self.class.min_password_matches
57
+ else
58
+ self.class.min_password_matches
59
+ end
60
+ )
61
+ return @pwned
62
+ rescue Pwned::Error
63
+ # This deliberately silently swallows errors and returns false (valid) if there was an error. Most apps won't want to tie the ability to sign up users to the availability of a third-party API.
50
64
  return false
51
65
  end
52
66
 
@@ -55,21 +69,9 @@ module Devise
55
69
 
56
70
  private
57
71
 
58
- def usage_count(response, suffix)
59
- count = 0
60
- response.each_line do |line|
61
- if line.start_with? suffix
62
- count = line.strip.split(":").last.to_i
63
- break
64
- end
65
- end
66
- count
67
- end
68
-
69
72
  def not_pwned_password
70
- # This deliberately fails silently on 500's etc. Most apps wont want to tie the ability to sign up customers to the availability of a third party API
71
73
  if password_pwned?(password)
72
- errors.add(:password, :pwned_password)
74
+ errors.add(:password, :pwned_password, count: @pwned_count)
73
75
  end
74
76
  end
75
77
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Devise
4
4
  module PwnedPassword
5
- VERSION = "0.1.4"
5
+ VERSION = "0.1.9"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,45 +1,59 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devise-pwned_password
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Banfield
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-03-12 00:00:00.000000000 Z
11
+ date: 2020-08-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rails
14
+ name: devise
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 5.1.2
19
+ version: '4'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 5.1.2
26
+ version: '4'
27
27
  - !ruby/object:Gem::Dependency
28
- name: devise
28
+ name: pwned
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '4'
33
+ version: 2.0.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '4'
40
+ version: 2.0.0
41
41
  - !ruby/object:Gem::Dependency
42
- name: sqlite3
42
+ name: byebug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: capybara
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
59
  - - ">="
@@ -52,6 +66,20 @@ dependencies:
52
66
  - - ">="
53
67
  - !ruby/object:Gem::Version
54
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 5.1.2
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 5.1.2
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: rubocop
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +94,20 @@ dependencies:
66
94
  - - "~>"
67
95
  - !ruby/object:Gem::Version
68
96
  version: 0.52.1
97
+ - !ruby/object:Gem::Dependency
98
+ name: sqlite3
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
69
111
  description: Devise extension that checks user passwords against the PwnedPasswords
70
112
  dataset https://haveibeenpwned.com/Passwords.
71
113
  email:
@@ -102,8 +144,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
102
144
  - !ruby/object:Gem::Version
103
145
  version: '0'
104
146
  requirements: []
105
- rubyforge_project:
106
- rubygems_version: 2.5.2
147
+ rubygems_version: 3.0.3
107
148
  signing_key:
108
149
  specification_version: 4
109
150
  summary: Devise extension that checks user passwords against the PwnedPasswords dataset.