In Progress
Unit 1, Lesson 1
In Progress

Two Factor Authentication – Frank Rietta

Not only do we want to deliver useful and enjoyable software, we also want to ensure our software never compromises our users’ privacy. In order to do that, we need to make our applications secure.

Many security breaches start with the compromise of a single user account. That’s why it’s of crucial importance to make sure that user accounts – especially accounts with elevated privileges — are strongly protected. One easy and highly recommended way to increase account protection is with two factor authentication. Here to show you how to add two-factor support to a Ruby on Rails application is guest chef Frank Rietta. Enjoy!

Video transcript & code

(Demo code is available here.)

Why

Sixty three percent of the big data breaches involving web apps in recent years have been perpetrated via compromised credentials! That is the bad guys simply log in using a valid username and password.

 

Despite other technical ways that apps are routinely popped, many that are covered by the OWASP Top 10, as developers we cannot ignore the plight of the username and password.It's bad when any user's credentials are compromised, but it's worse it's a staff user who has access to OTHER people's data. That's a recipe for a data breach. So what are some ways these credentials are stolen? It could be

  • A user chooses passwords that are truly weak and easily broken by password cracking.
  • Or one who reuses a password among multiple websites. If any of those other third parties are compromised, an attacker can use those credentials to target the site we're trying to protect.
  • Or a user is lured to use a faked login page that looks like ours. This is known as a "spearphishing attack".

The problem is so prevalent that U.S. National Institute for Science and Technology adopted the NIST 800–63b Federal Standard for password verifiers.It challenges many of the recommendations you might have come across for password security.

For example making users change their password every 90 days is specifically not recommended.

One thing that is strongly recommended is to use multi-factor authentication.

Today we're talking about a subset of multi-factor authentication. Two factor authentication!

What is Two Factor Authentication?

That means the user must have something more than their username and password.

So "two-factor" really means having three pieces of information to authenticate yourself with a system:

  • Something you are: Your username.
  • Something you know: Your password.
  • Something you have: The "second factor".
  • A good way to do this is to require users, especially staff users, to use a smartphone app to generate a time-based two factor code.

Now, you’ve probably seen these time-based code implementations in some of the big name services that you use online, like Gmail and Dropbox, and you might be surprised how easy it is to add it to your Ruby application!

How Does TOTP Work?

The time-based one-time password algorithm is an open standard.It works by hashing a shared secret with current timestamp to compute a six digit code that changes each minute and can only be used once.

There are multiple good smartphone apps for your users to choose from.

There are three aspects that you have to handle to add TOTP to your web application. Enrollment, verification, and backup codes.

Enrollment

On enrollment, we present our user with a QR barcode that is an encoding of a secret string. But they can also be shown the raw code itself as you see here.

The user scans or enters the secret and then types the current six digit code into our web form to confirm enrollment.

Verification

When a user account has 2FA enabled, enforce the required current valid 6 digit TOTP for authentication.

Backup Codes

You need to provide backup codes in case your user looses their smartphone or deletes the app.

Code Example!

I’m showing an example of a Ruby on Rails that shows DELICIOUS tapas to authenticated users.

We're adding TOTP via the devise-two-factor gem.

How is this Done

Gemfile


source 'https://rubygems.org'

git_source(:github) do |repo_name|
  repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?('/')
  "https://github.com/#{repo_name}.git"
end

##############################################################################
# Specific gems for the Two Factor Tapa Demo

# The user authentication system and our 2FA library
gem 'devise'
gem 'devise-two-factor'

# Despite the name, this works quite fine in Rails 5. It generates QR codes
# without a dependency on 3rd party services which is bad for security because
# your application would be leaking shared secret codes with another
# service that you do not control.
gem 'rqrcode-rails3'

# Useful for using environment variables in a local test environment. You
# do not want to store server secrets in the codebase.
gem 'dotenv-rails', groups: [:development, :test]


##############################################################################
# Useful, but not specifically part of the demo or rails defaults

gem 'annotate'
gem 'pry-rails'
gem 'jquery-rails'
gem 'auto_strip_attributes'

##############################################################################
# Default Stuff from Rails New Below

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.1.5'
# Use sqlite3 as the database for Active Record
gem 'sqlite3'
# Use Puma as the app server
gem 'puma', '~> 3.7'
# Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# See https://github.com/rails/execjs#readme for more supported runtimes
# gem 'therubyracer', platforms: :ruby

# Use CoffeeScript for .coffee assets and views
gem 'coffee-rails', '~> 4.2'
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.5'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'

# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: %i[mri mingw x64_mingw]
  # Adds support for Capybara system testing and selenium driver
  gem 'capybara', '~> 2.13'
  gem 'selenium-webdriver'
end

group :development do
  # Access an IRB console on exception pages or by using <%= console %> anywhere in the code.
  gem 'listen', '>= 3.0.5', '< 3.2' gem 'web-console', '>= 3.3.0'
  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]

I've added a few gems to Gemfile:

  • devise for user authentication
  • devise-two-factor
  • a library to natively generate QR codes without leaking secrets to some web API

Because we'll load the encryption secret from an environment variable, I've added a gem to support that in local too. User model / migration to show fields needed for the 2FA implementation


# frozen_string_literal: true

class AddOtpToUser < ActiveRecord::Migration[5.1]
  def change
    add_column :users, :encrypted_otp_secret, :string
    add_column :users, :encrypted_otp_secret_iv, :string
    add_column :users, :encrypted_otp_secret_salt, :string
    add_column :users, :consumed_timestep, :integer
    add_column :users, :otp_required_for_login, :boolean, default: false
  end
end

The migration adds a 5 fields to the user model.

The first three manage the symmetric encryption of the shared secret so that its not stored in the database in plaintext, which would be bad!

The next keeps a record of the most recently used code so replays are rejected.

And finally if 2FA is enabled for the user.


class AddOtpToUser < ActiveRecord::Migration[5.1]
  def change
    add_column :users, :encrypted_otp_secret, :string
    add_column :users, :encrypted_otp_secret_iv, :string
    add_column :users, :encrypted_otp_secret_salt, :string
    add_column :users, :consumed_timestep, :integer
    add_column :users, :otp_required_for_login, :boolean, default: false
  end
end

class User < ApplicationRecord
  auto_strip_attributes :email

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :registerable,
         :recoverable,
         :rememberable,
         :trackable,
         :validatable,
         :two_factor_authenticatable, # Remember to remove database_authenticatable
         # :two_factor_backupable,    # For production use, you'll want backup codes
         otp_secret_encryption_key: ENV['OTP_SECRET_ENCRYPTION_KEY']

  ###############################
  # OTP Enrollment Support
  #
  # You'll probably want to use a service object for a production-grade
  # implementation.
  #
  attr_accessor :enrolling_otp_secret
  before_validation :setup_enrolling_otp, on: :create
  validates :enrolling_otp_secret, presence: true, format: {with: /\A[a-zA-Z0-9]*\z/ }, on: :create
  validate :validate_otp_secret_matches, on: :create

  private

  def setup_enrolling_otp
    return if persisted? || enrolling_otp_secret.nil?
    self.otp_required_for_login = true
    self.otp_secret = enrolling_otp_secret
  end

  def validate_otp_secret_matches
    return if validate_and_consume_otp!(otp_attempt)
    errors.add(:otp_attempt, 'Does not match expected code. Please check again.')
    return false
  end
end

In the user model, you see that we enabled the two_factor_authenticatable auth strategy and loaded the OTP encryption secret.

Conclusion

Implementing two factor authentication is a necessary step to providing good web application security. You could spend tons of time and money on other security measures to harden your system, but without two-factor, a user who makes a poor choice of password or falls victim to a spearphishing attack could render all of those safeguards ineffective.The next step will be to ensure that as many users as possible make use of the feature. You should consider requiring those with administrative access to use 2FA. And finally, don't just implement but be a user. Many of the websites and services that we Rubyists use in our daily work, such as Heroku, Github, and AWS support 2FA. Use it everywhere for your and your customers' protection.

[su_note note_color="#eeeeee" radius="0"]The fully functional demo at its Github location is https://github.com/rietta/TwoFactorTapa. Those interested can fork it and really dig into the example.[/su_note]

Responses