At Twilio we're fans of using a second factor to protect user accounts, but that doesn't mean we've forgotten the first factor. Encouraging users to pick strong passwords is still the first line of defence for their accounts.
After spending years collecting lists of passwords from publicly available data breaches at HaveIBeenPwned, Troy Hunt has made available an API to check whether a password has been used before. This post will show you how to encourage your users to use stronger passwords by checking against the pwned passwords API.
The Pwned Passwords API
In 2017 NIST (National Institute of Standards and Technology) as part of their digital identity guidelines recommended that user passwords are checked against existing public breaches of data. The idea is that if a password has appeared in a data breach before then it is deemed compromised and should not be used. Of course, the recommendations include the use of two factor authentication to protect user accounts too.
The Pwned Passwords API allows you to check whether a potential password has been exposed as part of a number of data breaches across the web. There is an online version of the API where you can enter a password and see if it's been used before. If it has, it’ll also show how many times it appeared. The data has more than 500,000,000 unique passwords that have been used before.
While you're at it, check the main haveibeenpwned service with your email address to see if your credentials have been in any of those data breaches. Spoiler alert, it probably has!
The API
The Pwned Passwords API allows us to check a password against the database of passwords. With the results, we can advise users to choose better passwords when they sign up for a service, when they log in or when they change their password.
Your security senses might be tingling at the prospect of sending all your users' passwords to a third-party. Thankfully you needn't worry.
Instead of sending the whole password, you only need to hash the password using SHA-1 and send the first 5 characters of the result. This returns all the hashes that are in the data set beginning with those 5 characters and if the remained of the hash is present, the password was in the list. You can read more about this technique, the dump of passwords and the API in this article.
Let's take a look at how to use this in a Ruby application using a couple of gems that abstract that process away for you.
Pwned Passwords in Ruby
If you want to use the Pwned Passwords API in any Ruby application then do I have the gem for you. It's called pwned and it makes checking passwords against the API really easy.
You can check out all the documentation for pwned on GitHub, but here's how you get started.
Install the gem:
gem install pwned
Open up an irb session to test it out.
irb -r pwned
> password = Pwned::Password.new("password")
> password.pwned?
#=> true
> password.pwned_count
#=> 3303003
Well, how about that. The password "password" has been seen more than 3 million times in public data breaches. 😱
There are also shortcut methods you can use:
> Pwned.pwned?("password1")
#=> true
> Pwned.pwned_count("password1")
#=> 2310111
Wow. "password1" is almost as bad! It's fun to play with the data like this, but what if you want to use this in a real application? You can use the gem directly, but if you're using Rails you're in for a treat.
Pwned Passwords and Rails
We looked at how to write ActiveModel validators on this blog before. The pwned gem comes with one out of the box. If you're using Rails you can use this validator by adding the gem to your Gemfile
:
gem "pwned", "~> 1.2.1"
Install the gem by running bundle install
. Now you have access to a :not_pwned
validator in your models. For example:
class User < ApplicationRecord
has_secure_password
validates :password, not_pwned: true
end
Now, validating user objects against the Pwned Passwords API is as easy as:
user = User.new(email: "<a class="c1" href="mailto:philnash@twilio.com">philnash@twilio.com</a>", password: "password!")
user.valid?
#=> false
user.errors.messages
#=> {:password=>["has previously appeared in a data breach and should not be used"]}
Let's try a strong passphrase instead (but not "correct horse battery staple" —that appears in the breached data twice):
user.password = "wet koalas are terrifying"
user.valid?
#=> true
There are other options you can use in the validator, such as setting the threshold for the number of times a password can appear in the data or what to do if the API returns an error. The details are in the documentation.
If you are using Rails with Devise, there's an even easier way to use the API.
Pwned Passwords and Devise
To use the API with Devise there is a different gem available: devise-pwned_password. To use it, add the gem to your Gemfile
:
gem "devise-pwned_password", "~> 0.1.4"
Run bundle install
to install the gem. All you need to do now is add the :pwned_password
option to the devise method for your User
model:
class User < ApplicationRecord
devise :database_authenticatable,
:recoverable, :rememberable, :trackable, :validatable, :pwned_password
end
Now Devise will check against the Pwned Passwords API when your users try to sign up.
You can also use the Devise plugin to warn existing users about their passwords when they sign in. To do this, you need to override after_sign_in_path_for(resource)
in your ApplicationController
:
def after_sign_in_path_for(resource)
set_flash_message! :alert, :warn_pwned if resource.respond_to?(:pwned?) && resource.pwned?
super
end
Advanced options
Both the Devise plugin and the pwned gem will mark a password as valid if the API fails in the background. With the pwned gem you can change this network failure behaviour. You can either set the model to be invalid or run your own proc so that you can log errors.
If you don't want to mark a password as invalid if it has only been used once or twice, both gems provide a way to set a threshold. With pwned you set a threshold in the validation:
validates :password, not_pwned: { threshold: 1 }
With devise-pwned_password, open config/initializers/devise.rb
and add the following config:
config.min_password_matches = 2
For more options, check the documentation for pwned and devise-pwned_password.
Safer users with safer passwords
Using the Pwned Passwords API you can ensure or encourage your users to use better passwords when they sign up for accounts, as they log in or when they update their password.
As I said at the start, I also recommend implementing 2FA in your Rails applications to keep your user accounts safe.
Are you pursuing other methods to get your users to use better passwords? Do you see the Pwned Passwords API as a good tool for this? Let me know in the comments or on Twitter at @philnash.
Just finally, while I wrote the initial version of the pwned gem I want to give a shout out to Dmytro Shteflyuk who contributed a number of improvements, including the ActiveModel validator. Thanks!
Better passwords in Ruby applications with the Pwned Passwords API was originally published on the Twilio blog on March 20, 2018.
Top comments (8)
I was about to write this gem myself right now! I just would have used a different name for Devise integration, maybe
:unpwnable
(hehe) in order to match the other devise modules.Great work though, gonna add it to my project at work. Thanks!
Ha, that would be more fun! Though no password is unpwnable in reality.
Glad you found this before writing another gem, hope I've saved you some time.
True!
There's the coolest gem ever!
I don't know about cool*, I just hope it can help keep users' accounts a bit more safe.
.
.
*Alright, I was pretty delighted that the gem name "pwned" was still available. That's cool!
I love it, I'll definitely try this approach :)
Awesome, let me know how it goes.
What a cool gem. Great article!