devise-unpwn 0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +125 -0
- data/Rakefile +30 -0
- data/lib/devise/unpwn/hooks/unpwn.rb +6 -0
- data/lib/devise/unpwn/locales/en.yml +7 -0
- data/lib/devise/unpwn/model.rb +77 -0
- data/lib/devise/unpwn/version.rb +7 -0
- data/lib/devise/unpwn.rb +20 -0
- metadata +66 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ad6db676f32e9c18ae0589c9d774ebfd8bb17c456f1f65bf7ec715d090127ceb
|
4
|
+
data.tar.gz: de2f820bc97b83efaed6d5a9e59e319c3cf9b85a2762735e49f71b9c98421620
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a70ad60f8892de118e70f45eb52fc8a568b21c835a1fdccbf1a5106fad80db998d02a8c4eabff879a2e86704116ac7e5e39a002af0b875ea1915d830443bb67e
|
7
|
+
data.tar.gz: 81d0c716c7e3238a5eb1d57ed8b4d580e8baa1da19d7eab76c7c1e8328ebd2390d3af6001d917ca5e1e61a95098a492b27549a7613bbfb52de786de5f1bfbb47
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2017 Michael
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
# Devise::Unpwn
|
2
|
+
Devise extension that checks user passwords against the HaveIBeenPwned dataset https://haveibeenpwned.com/Passwords
|
3
|
+
|
4
|
+
Based on
|
5
|
+
|
6
|
+
https://github.com/michaelbanfield/devise-pwned_password
|
7
|
+
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
Add the :unpwned module to your existing Devise model.
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
class AdminUser < ApplicationRecord
|
14
|
+
devise :database_authenticatable,
|
15
|
+
:recoverable, :rememberable, :trackable, :validatable, :unpwned
|
16
|
+
end
|
17
|
+
```
|
18
|
+
|
19
|
+
Users will receive the following error message if they use a password from the
|
20
|
+
HaveIBeenPwned dataset:
|
21
|
+
|
22
|
+
```
|
23
|
+
Password has previously appeared in a data breach and should never be used. Please choose something harder to guess.
|
24
|
+
```
|
25
|
+
|
26
|
+
You can customize this error message by modifying the `devise` YAML file.
|
27
|
+
|
28
|
+
```yml
|
29
|
+
# config/locales/devise.en.yml
|
30
|
+
en:
|
31
|
+
errors:
|
32
|
+
messages:
|
33
|
+
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
|
+
```
|
35
|
+
|
36
|
+
You can optionally warn existing users when they sign in if they are using a password from the HaveIBeenPwned 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
|
+
By default passwords are rejected if they appear at all in the data set.
|
53
|
+
Optionally, you can add the following snippet to `config/initializers/devise.rb`
|
54
|
+
if you want the error message to be displayed only when the password is present
|
55
|
+
a certain number of times in the data set:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
# Minimum number of times a pwned password must exist in the data set in order
|
59
|
+
# to be reject.
|
60
|
+
config.min_password_matches = 10
|
61
|
+
```
|
62
|
+
|
63
|
+
By default responses from the HaveIBeenPwned API are timed out after 5 seconds
|
64
|
+
to reduce potential latency problems.
|
65
|
+
Optionally, you can add the following snippet to `config/initializers/devise.rb`
|
66
|
+
to control the timeout settings:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
config.unpwn_open_timeout = 1
|
70
|
+
config.unpwn_read_timeout = 2
|
71
|
+
```
|
72
|
+
|
73
|
+
## Installation
|
74
|
+
|
75
|
+
```bash
|
76
|
+
bundle add devise-unpwn
|
77
|
+
```
|
78
|
+
|
79
|
+
Optionally, if you also want to warn existing users when they sign in, override `after_sign_in_path_for`
|
80
|
+
```ruby
|
81
|
+
def after_sign_in_path_for(resource)
|
82
|
+
set_flash_message! :alert, :warn_pwned if resource.respond_to?(:pwned?) && resource.pwned?
|
83
|
+
super
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
87
|
+
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.
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
# config/initializers/active_admin_devise_sessions_controller.rb
|
91
|
+
class ActiveAdmin::Devise::SessionsController
|
92
|
+
def after_sign_in_path_for(resource)
|
93
|
+
set_flash_message! :alert, :warn_pwned if resource.respond_to?(:pwned?) && resource.pwned?
|
94
|
+
super
|
95
|
+
end
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
|
100
|
+
## Considerations
|
101
|
+
|
102
|
+
A few things to consider/understand when using this gem:
|
103
|
+
|
104
|
+
* User passwords are hashed using SHA-1 and then truncated to 5 characters,
|
105
|
+
implementing the k-Anonymity model described in
|
106
|
+
https://haveibeenpwned.com/API/v2#SearchingPwnedPasswordsByRange
|
107
|
+
Neither the clear-text password nor the full password hash is ever transmitted
|
108
|
+
to a third party. More implementation details and important caveats can be
|
109
|
+
found in https://blog.cloudflare.com/validating-leaked-passwords-with-k-anonymity/
|
110
|
+
|
111
|
+
* This puts an external API in the request path of users signing up to your
|
112
|
+
application. This could potentially add some latency to this operation. The
|
113
|
+
gem is designed to fail silently if the HaveIBeenPwned service is unavailable.
|
114
|
+
|
115
|
+
## Contributing
|
116
|
+
|
117
|
+
To contribute
|
118
|
+
|
119
|
+
* Fork the repository
|
120
|
+
* Make your changes
|
121
|
+
* Run bin/test to make sure the unit tests still run
|
122
|
+
* Send a pull request
|
123
|
+
|
124
|
+
## License
|
125
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "bundler/setup"
|
5
|
+
require "bundler/gem_tasks"
|
6
|
+
rescue LoadError
|
7
|
+
puts "You must `gem install bundler` and `bundle install` to run rake tasks"
|
8
|
+
end
|
9
|
+
|
10
|
+
require "rdoc/task"
|
11
|
+
|
12
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
13
|
+
rdoc.rdoc_dir = "rdoc"
|
14
|
+
rdoc.title = "Devise::Unpwn"
|
15
|
+
rdoc.options << "--line-numbers"
|
16
|
+
rdoc.rdoc_files.include("README.md")
|
17
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
18
|
+
end
|
19
|
+
|
20
|
+
require "bundler/gem_tasks"
|
21
|
+
|
22
|
+
require "rake/testtask"
|
23
|
+
|
24
|
+
Rake::TestTask.new(:test) do |t|
|
25
|
+
t.libs << "test"
|
26
|
+
t.pattern = "test/**/*_test.rb"
|
27
|
+
t.verbose = false
|
28
|
+
end
|
29
|
+
|
30
|
+
task default: :test
|
@@ -0,0 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
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)
|
6
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
en:
|
2
|
+
devise:
|
3
|
+
sessions:
|
4
|
+
warn_pwned: "Your password has previously appeared in a data breach and should never be used. We strongly recommend you change your password."
|
5
|
+
errors:
|
6
|
+
messages:
|
7
|
+
pwned_password: "has previously appeared in a data breach and should never be used. Please choose something harder to guess."
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "devise/unpwn/hooks/unpwn"
|
5
|
+
|
6
|
+
module Devise
|
7
|
+
module Models
|
8
|
+
# The Unpwn module adds a new validation for Devise Models.
|
9
|
+
# No modifications to routes or controllers needed.
|
10
|
+
# Simply add :unpwned to the list of included modules in your
|
11
|
+
# devise module, and all new registrations will be blocked if they use
|
12
|
+
# a password in this dataset https://haveibeenpwned.com/Passwords.
|
13
|
+
module Unpwned
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
included do
|
17
|
+
validate :unpwned_password, if: :password_required?
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
Devise::Models.config(self, :min_password_matches)
|
22
|
+
Devise::Models.config(self, :unpwn_open_timeout)
|
23
|
+
Devise::Models.config(self, :unpwn_read_timeout)
|
24
|
+
end
|
25
|
+
|
26
|
+
def pwned?
|
27
|
+
@pwned ||= false
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns true if password is present in the HaveIBeenPwned dataset
|
31
|
+
# Implement retry behaviour described here https://haveibeenpwned.com/API/v2#RateLimiting
|
32
|
+
def password_pwned?(password)
|
33
|
+
@pwned = false
|
34
|
+
hash = Digest::SHA1.hexdigest(password.to_s).upcase
|
35
|
+
prefix, suffix = hash.slice!(0..4), hash
|
36
|
+
|
37
|
+
user_agent = "devise_unpwn"
|
38
|
+
|
39
|
+
uri = URI.parse("https://api.pwnedpasswords.com/range/#{prefix}")
|
40
|
+
|
41
|
+
begin
|
42
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: self.class.unpwn_open_timeout, read_timeout: self.class.unpwn_read_timeout) do |http|
|
43
|
+
request = Net::HTTP::Get.new(uri.request_uri, "User-Agent" => user_agent)
|
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
|
50
|
+
return false
|
51
|
+
end
|
52
|
+
|
53
|
+
false
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
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
|
+
def unpwned_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
|
+
if password_pwned?(password)
|
72
|
+
errors.add(:password, :pwned_password)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/lib/devise/unpwn.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "devise"
|
4
|
+
require "devise/unpwn/model"
|
5
|
+
|
6
|
+
module Devise
|
7
|
+
mattr_accessor :min_password_matches, :unpwn_open_timeout, :unpwn_read_timeout
|
8
|
+
@@min_password_matches = 1
|
9
|
+
@@unpwn_open_timeout = 5
|
10
|
+
@@unpwn_read_timeout = 5
|
11
|
+
|
12
|
+
module Unpwn
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Load default I18n
|
17
|
+
#
|
18
|
+
I18n.load_path.unshift File.join(File.dirname(__FILE__), *%w[unpwn locales en.yml])
|
19
|
+
|
20
|
+
Devise.add_module :unpwn, model: "devise/unpwn/model"
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: devise-unpwn
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.9'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- André Arko
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-11-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: devise
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4'
|
27
|
+
description: Devise extension that checks user passwords against the HaveIBeenPwned
|
28
|
+
dataset.
|
29
|
+
email:
|
30
|
+
- andre@arko.net
|
31
|
+
executables: []
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- MIT-LICENSE
|
36
|
+
- README.md
|
37
|
+
- Rakefile
|
38
|
+
- lib/devise/unpwn.rb
|
39
|
+
- lib/devise/unpwn/hooks/unpwn.rb
|
40
|
+
- lib/devise/unpwn/locales/en.yml
|
41
|
+
- lib/devise/unpwn/model.rb
|
42
|
+
- lib/devise/unpwn/version.rb
|
43
|
+
homepage: https://github.com/indirect/devise-unpwn
|
44
|
+
licenses:
|
45
|
+
- MIT
|
46
|
+
metadata: {}
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
requirements: []
|
62
|
+
rubygems_version: 3.3.7
|
63
|
+
signing_key:
|
64
|
+
specification_version: 4
|
65
|
+
summary: Devise extension that checks user passwords against the HaveIBeenPwned dataset.
|
66
|
+
test_files: []
|