Better passwords in Ruby applications with the Pwned Passwords API

Phil Nash - Apr 4 '18 - - Dev Community

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
Enter fullscreen mode Exit fullscreen mode

Open up an irb session to test it out.

irb -r pwned
> password = Pwned::Password.new("password")
> password.pwned?
#=> true
> password.pwned_count
#=> 3303003
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now Devise will check against the Pwned Passwords API when your users try to sign up.

A sign up form showing one error that reads 'Password has previously appeared in a data breach and should never be used. Please choose something harder to guess.'

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
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

With devise-pwned_password, open config/initializers/devise.rb and add the following config:

config.min_password_matches = 2
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .