/omniauth-onetime

An OmniAuth strategy using secure onetime passwords.

Primary LanguageRubyGNU Lesser General Public License v3.0LGPL-3.0

OmniAuth One-Time

Code Climate Maintainability Code Climate Issue Count Gitlab Pipeline Status Gitlab Code Coverage

An OmniAuth strategy using secure onetime passwords.

I released this code on Dec 14, 2016 after having used it in production for some time. Coincidentally, this is the same day that Yahoo disclosed a breach of 1 billion accounts which may have included MD5 hashed passwords. In the wake of this and numerous other password breaches every web development team needs to ask:

Is it worth storing long term user generated passwords?

I suggest that it very rarely is, unless you are an email provider or a financial institution.

Vision

Strong passwords are difficult to remember. Rememberable passwords are usually weak. Thus asking users to create and remember strong passwords is a limited proposition.

The purpose of this gem is to provide a way for developers to quickly add a secure authentication system to Rack based projects. This system is based on one-time passwords which are emailed to the user at sign in time. These passwords quickly expire (5 minute default, expiration time is configurable) in order to thwart brute-force attacks.

Installation

Add this line to your application's Gemfile:

gem 'omniauth-onetime'

And then execute:

$ bundle

Or install it yourself as:

$ gem install omniauth-onetime

Usage

Enable omniauth-onetime using a Rails initializer at config/initializers/omniauth.rb:

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :onetime
end

config/routes.rb file something like this:

match '/auth/:provider/callback', to: 'sessions#create', via: [:get, :post]

app/controllers/sessions_controller.rb file something like this:

class SessionsController < ApplicationController
  def create
    @user = User.omniauth(auth_hash)
    session[:user_id] = user.id
    redirect_to request.env['omniauth.origin'] || root_path,
      notice: "Signed in!"
  end

  def destroy
    session[:user_id] = nil
    redirect_to root_url, notice: "Signed out!"
  end

  protected

  def auth_hash
    request.env['omniauth.auth']
  end
end

app/models/user.rb file something like this:

class User < ActiveRecord::Base
  def self.omniauth(auth)
    User.find_or_create_by!(provider: auth['provider'], email: auth['email'])
  end
end

Configuration

These settings can be passed as a hash in the initializer.

  • password_length - length of generated random passwords (default: 8)
  • password_time - time in seconds that a generated password is valid (default: 300)
  • password_cost - bcrypt cost/rounds (default: 12), be sure you understand the implications of changing this
  • email_options - a hash to be sent to ActionMailer::Base.mail (default: { subject: 'Sign In Details' })
  • password_cache - a cache to store the passwords (default: Rails.cache for Rails apps, none otherwise), expected to function like ActiveSupport::Cache::Store, make sure this cache is appropriate for your deployment
    • must implement: write, read, delete, exist?

Details

Reading List:

The nomenclature has been settling on calling this approach "passwordless". That makes sense if the email sent the user contains a link such that the user never has to enter a password. However, this gem assumes that the device receiving the password and the device being used to sign in may not be the same and thus a password that can be read and entered is also a requirement. This password can be used as a token for a "passwordless" authentication link. This gem is "passwordless" but I believe more accurately it uses out-of-band transmission of quickly expiring one-time passwords.

This approach may sound counter-intuitive, especially with a default password length of 8 characters. However, the real key to the security is that the passwords are sufficiently random and the window of opportunity to crack them is very short. A traditional username / password system fails because the passwords are not sufficiently random, as truly random passwords are difficult to remember, and because the password lifetime is often very long. An issue which compounds these traditional systems is password reuse which puts accounts at risk whenever any system containing a user's password becomes compromised and subject to a brute force attack.

Benefits:

  • No passwords to create - Users are generally very bad at creating passwords unless they are somewhat knowledgeable and highly disciplined. The requirement to remember the password is directly at odds with the strength of the password. See also: xkcd: Password Strength
  • No passwords to remember - Passwords can be random and arbitrarily strong by increasing password length.
  • Passwords are short lived - Brute-force attacks are thwarted even with shorter passwords such as 8 random letters.
  • Passwords are not reused - A system using this gem cannot divulge useful password secrets if compromised. It is also immune from secrets divulged from other systems, whether they use this gem or not. It also does not require user trust that the website will not use passwords submitted by users for nefarious ends. See also: xkcd: Password Reuse
  • Easy for users - To Sign In:
    1. enter your email
    2. enter the password that has been emailed to you or click a link in the email.

Limitations:

  • A compromised email account will compromise the user account. However, this is also true of any traditional password system that allows for automated password reset or recovery via email. The only way to circumvent this attack vector is to handle password resets in a way that verifies a person's identity manually, in person, and with identification. Since most websites are probably not willing to take that step (though financial institutions should at the very least consider it) then emailed one-time passwords are just as secure as any website employing an automated email password reset system.
  • Users must divulge an email account under their control to sign in. This does not seem like a huge hurdle. If people are concerned with their privacy they would likely have to create an anonymous/pseudonymous email for use with these systems. Many websites require divulging an email even with a traditional password system.
  • Password emails can potentially create a log of usage. The existence of the email cannot prove a user signed in or was trying to sign in since such emails can be triggered by anyone. However, this is still a notable difference from traditional systems.
  • Relies on external email systems to deliver passwords. Any downtime in either the email service used by the system or that used by the user will disrupt the user's ability to sign in.

Brute-force attacks:

Let's assume a malicious agent wants to brute-force a user's password on a system using this gem. Using the default settings of an 8 letter password:

26^8 = 208,827,064,576 permutations

In order to compromise a password in 5 minutes an adversary will have to hash nearly 700 million passwords per second to ensure the password is cracked within the time the password is valid.

26^8 permutations / 300 seconds = 696,090,216 hashes per second

This is far beyond what computing power can deliver for the bcrypt with a default cost of 12. This scenario also assumes instantaneous access to the stored crypted passwords which is highly unlikely without a greater breach of security having already occurred.

On my development system the speed of bcrypt at cost 12 is roughly 4 hashes per second per core. A 2015 attempt to crack bcrypt passwords with a cost of 12 using a GPU was able to achieve 156 hashes per second per GPU. A Zynq 7045 FPGA device was able to achieve 226 hashes per second at bcrypt cost 12. In 2020, claims were made that 18 ZTEX 1.15y boards could achieve 2.1M hashes per second at cost 5. That would be about 16.5K hashes per second at cost 12.

The above scenario also assumes that the attack is either not through the web app itself (ie there has already been a security breach) or that the web app is not a limiting factor on the number of attempts. If a web app also employed a solution like Rack::Attack to limit sign-in attempts to 1 per second per ip then the chance of cracking a random 8 letter password is 300 (approximate attempts) in 26^8 (permutations) or 1 in 696,090,216.

It's probably wise to keep this in mind:

xlcd: Security

Comparisons to similar solutions

  • nopassword - I don't prefer this implementation: it's an engine, it's very opinionated about database structure, it always stores geographical location. I like OmniAuth because it allows authentication mechanisms to be changed without requiring many, if any, application changes. Storing geographic information may be something that people want but I don't want to couple that with my solution.
  • omniauth-passwordless - This does not appear maintained or developed and is not comparable - it simply asks for an email address and passes that straight through - much like the OmniAuth Developer strategy. To quote from that code, "It has zero security and should never be used in a production setting."
  • omniauth-email - This does not appear maintained or developed. To quote the README, "this code does not work, don't use it."

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/thoughtafter/omniauth-onetime. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

omniauth-onetime is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

omniauth-onetime is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along with omniauth-onetime. If not, see http://www.gnu.org/licenses/.