├── .rspec
├── demo
├── log
│ └── .keep
├── app
│ ├── mailers
│ │ └── .keep
│ ├── models
│ │ ├── .keep
│ │ ├── concerns
│ │ │ └── .keep
│ │ └── user.rb
│ ├── assets
│ │ ├── images
│ │ │ └── .keep
│ │ ├── stylesheets
│ │ │ ├── home.css.scss
│ │ │ └── application.css
│ │ └── javascripts
│ │ │ ├── home.js.coffee
│ │ │ └── application.js
│ ├── controllers
│ │ ├── concerns
│ │ │ └── .keep
│ │ ├── home_controller.rb
│ │ ├── application_controller.rb
│ │ └── users_controller.rb
│ ├── helpers
│ │ ├── home_helper.rb
│ │ └── application_helper.rb
│ └── views
│ │ ├── devise
│ │ ├── mailer
│ │ │ ├── confirmation_instructions.html.erb
│ │ │ ├── unlock_instructions.html.erb
│ │ │ └── reset_password_instructions.html.erb
│ │ ├── unlocks
│ │ │ └── new.html.erb
│ │ ├── passwords
│ │ │ ├── new.html.erb
│ │ │ └── edit.html.erb
│ │ ├── confirmations
│ │ │ └── new.html.erb
│ │ ├── registrations
│ │ │ ├── new.html.erb
│ │ │ └── edit.html.erb
│ │ ├── sessions
│ │ │ └── new.html.erb
│ │ └── shared
│ │ │ └── _links.erb
│ │ ├── layouts
│ │ └── application.html.erb
│ │ └── home
│ │ └── index.html.erb
├── lib
│ ├── assets
│ │ └── .keep
│ └── tasks
│ │ └── .keep
├── public
│ ├── favicon.ico
│ ├── robots.txt
│ ├── 500.html
│ ├── 422.html
│ └── 404.html
├── test
│ ├── helpers
│ │ ├── .keep
│ │ └── home_helper_test.rb
│ ├── mailers
│ │ └── .keep
│ ├── models
│ │ ├── .keep
│ │ └── user_test.rb
│ ├── controllers
│ │ ├── .keep
│ │ └── home_controller_test.rb
│ ├── fixtures
│ │ ├── .keep
│ │ └── users.yml
│ ├── integration
│ │ └── .keep
│ └── test_helper.rb
├── vendor
│ └── assets
│ │ ├── javascripts
│ │ └── .keep
│ │ └── stylesheets
│ │ └── .keep
├── bin
│ ├── rake
│ ├── bundle
│ └── rails
├── config
│ ├── initializers
│ │ ├── cookies_serializer.rb
│ │ ├── session_store.rb
│ │ ├── mime_types.rb
│ │ ├── filter_parameter_logging.rb
│ │ ├── backtrace_silencers.rb
│ │ ├── wrap_parameters.rb
│ │ ├── devise.rb
│ │ └── inflections.rb
│ ├── environment.rb
│ ├── boot.rb
│ ├── routes.rb
│ ├── locales
│ │ ├── en.yml
│ │ └── devise.en.yml
│ ├── secrets.yml
│ ├── application.rb
│ ├── environments
│ │ ├── development.rb
│ │ ├── test.rb
│ │ └── production.rb
│ └── database.yml
├── config.ru
├── Rakefile
├── db
│ ├── seeds.rb
│ ├── migrate
│ │ ├── 20140516191259_add_devise_two_factor_to_users.rb
│ │ └── 20140515190128_devise_create_users.rb
│ └── schema.rb
├── .gitignore
├── README.rdoc
└── Gemfile
├── Gemfile
├── lib
├── devise_two_factor
│ ├── version.rb
│ ├── models.rb
│ ├── strategies.rb
│ ├── spec_helpers.rb
│ ├── strategies
│ │ ├── two_factor_backupable.rb
│ │ └── two_factor_authenticatable.rb
│ ├── models
│ │ ├── two_factor_authenticatable.rb
│ │ └── two_factor_backupable.rb
│ └── spec_helpers
│ │ ├── two_factor_authenticatable_shared_examples.rb
│ │ └── two_factor_backupable_shared_examples.rb
├── devise-two-factor.rb
└── generators
│ └── devise_two_factor
│ └── devise_two_factor_generator.rb
├── .travis.yml
├── spec
├── devise
│ └── models
│ │ ├── two_factor_authenticatable_spec.rb
│ │ └── two_factor_backupable_spec.rb
└── spec_helper.rb
├── Rakefile
├── .gitignore
├── LICENSE
├── CONTRIBUTING.md
├── devise-two-factor.gemspec
├── certs
├── tinfoilsecurity-gems-cert.pem
└── tinfoil-cacert.pem
└── README.md
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 |
--------------------------------------------------------------------------------
/demo/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/app/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/app/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/lib/tasks/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/test/helpers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/test/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/test/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/test/controllers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/test/fixtures/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/test/integration/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/vendor/assets/javascripts/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/vendor/assets/stylesheets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | gemspec
3 |
--------------------------------------------------------------------------------
/demo/app/helpers/home_helper.rb:
--------------------------------------------------------------------------------
1 | module HomeHelper
2 | end
3 |
--------------------------------------------------------------------------------
/demo/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/lib/devise_two_factor/version.rb:
--------------------------------------------------------------------------------
1 | module DeviseTwoFactor
2 | VERSION = '1.0.0'.freeze
3 | end
4 |
--------------------------------------------------------------------------------
/demo/app/controllers/home_controller.rb:
--------------------------------------------------------------------------------
1 | class HomeController < ApplicationController
2 | def index
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/demo/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative '../config/boot'
3 | require 'rake'
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/demo/test/helpers/home_helper_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class HomeHelperTest < ActionView::TestCase
4 | end
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | cache: bundler
3 | rvm:
4 | - "1.9.3"
5 | - "2.0.0"
6 | - jruby-19mode # JRuby in 1.9 mode
7 |
--------------------------------------------------------------------------------
/demo/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/lib/devise_two_factor/models.rb:
--------------------------------------------------------------------------------
1 | require 'devise_two_factor/models/two_factor_authenticatable'
2 | require 'devise_two_factor/models/two_factor_backupable'
3 |
--------------------------------------------------------------------------------
/lib/devise_two_factor/strategies.rb:
--------------------------------------------------------------------------------
1 | require 'devise_two_factor/strategies/two_factor_authenticatable'
2 | require 'devise_two_factor/strategies/two_factor_backupable'
3 |
--------------------------------------------------------------------------------
/demo/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path('../../config/application', __FILE__)
3 | require_relative '../config/boot'
4 | require 'rails/commands'
5 |
--------------------------------------------------------------------------------
/demo/test/models/user_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class UserTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/demo/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.action_dispatch.cookies_serializer = :json
--------------------------------------------------------------------------------
/demo/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 | run Rails.application
5 |
--------------------------------------------------------------------------------
/demo/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/demo/config/boot.rb:
--------------------------------------------------------------------------------
1 | # Set up gems listed in the Gemfile.
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 |
4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
5 |
--------------------------------------------------------------------------------
/demo/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.session_store :cookie_store, key: '_DeviseTwoFactorDemo_session'
4 |
--------------------------------------------------------------------------------
/lib/devise_two_factor/spec_helpers.rb:
--------------------------------------------------------------------------------
1 | require 'devise_two_factor/spec_helpers/two_factor_authenticatable_shared_examples'
2 | require 'devise_two_factor/spec_helpers/two_factor_backupable_shared_examples'
3 |
--------------------------------------------------------------------------------
/demo/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/demo/app/assets/stylesheets/home.css.scss:
--------------------------------------------------------------------------------
1 | // Place all the styles related to the home controller here.
2 | // They will automatically be included in application.css.
3 | // You can use Sass (SCSS) here: http://sass-lang.com/
4 |
--------------------------------------------------------------------------------
/demo/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | get 'home/index'
3 | post 'users/enable_otp'
4 | post 'users/disable_otp'
5 |
6 | devise_for :users
7 |
8 | root to: "home#index", via: [:get, :post]
9 | end
10 |
--------------------------------------------------------------------------------
/demo/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/demo/test/controllers/home_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class HomeControllerTest < ActionController::TestCase
4 | test "should get index" do
5 | get :index
6 | assert_response :success
7 | end
8 |
9 | end
10 |
--------------------------------------------------------------------------------
/demo/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/demo/app/assets/javascripts/home.js.coffee:
--------------------------------------------------------------------------------
1 | # Place all the behaviors and hooks related to the matching controller here.
2 | # All this logic will automatically be available in application.js.
3 | # You can use CoffeeScript in this file: http://coffeescript.org/
4 |
--------------------------------------------------------------------------------
/demo/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | # Prevent CSRF attacks by raising an exception.
3 | # For APIs, you may want to use :null_session instead.
4 | protect_from_forgery with: :exception
5 | end
6 |
--------------------------------------------------------------------------------
/demo/app/views/devise/mailer/confirmation_instructions.html.erb:
--------------------------------------------------------------------------------
1 |
Welcome <%= @email %>!
2 |
3 | You can confirm your account email through the link below:
4 |
5 | <%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>
6 |
--------------------------------------------------------------------------------
/demo/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require File.expand_path('../config/application', __FILE__)
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/demo/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ActiveRecord::Base
2 | devise :two_factor_authenticatable,
3 | :otp_secret_encryption_key => ENV['your_encryption_key_here']
4 |
5 | devise :registerable,
6 | :recoverable, :rememberable, :trackable, :validatable
7 | end
8 |
--------------------------------------------------------------------------------
/demo/app/views/devise/mailer/unlock_instructions.html.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @resource.email %>!
2 |
3 | Your account has been locked due to an excessive number of unsuccessful sign in attempts.
4 |
5 | Click the link below to unlock your account:
6 |
7 | <%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>
8 |
--------------------------------------------------------------------------------
/demo/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
7 | # Mayor.create(name: 'Emanuel', city: cities.first)
8 |
--------------------------------------------------------------------------------
/demo/db/migrate/20140516191259_add_devise_two_factor_to_users.rb:
--------------------------------------------------------------------------------
1 | class AddDeviseTwoFactorToUsers < ActiveRecord::Migration
2 | def change
3 | add_column :users, :encrypted_otp_secret, :string
4 | add_column :users, :encrypted_otp_secret_iv, :string
5 | add_column :users, :encrypted_otp_secret_salt, :string
6 | add_column :users, :otp_required_for_login, :boolean
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/demo/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | DeviseTwoFactorDemo
5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
6 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
7 | <%= csrf_meta_tags %>
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/demo/test/fixtures/users.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | # This model initially had no columns defined. If you add columns to the
4 | # model remove the '{}' from the fixture names and add the columns immediately
5 | # below each fixture, per the syntax in the comments below
6 | #
7 | one: {}
8 | # column: value
9 | #
10 | two: {}
11 | # column: value
12 |
--------------------------------------------------------------------------------
/demo/app/controllers/users_controller.rb:
--------------------------------------------------------------------------------
1 | class UsersController < ApplicationController
2 | def disable_otp
3 | current_user.otp_required_for_login = false
4 | current_user.save!
5 | redirect_to home_index_path
6 | end
7 |
8 | def enable_otp
9 | current_user.otp_secret = User.generate_otp_secret
10 | current_user.otp_required_for_login = true
11 | current_user.save!
12 | redirect_to home_index_path
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/demo/app/views/devise/unlocks/new.html.erb:
--------------------------------------------------------------------------------
1 | Resend unlock instructions
2 |
3 | <%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %>
4 | <%= devise_error_messages! %>
5 |
6 | <%= f.label :email %>
7 | <%= f.email_field :email, autofocus: true %>
8 |
9 | <%= f.submit "Resend unlock instructions" %>
10 | <% end %>
11 |
12 | <%= render "devise/shared/links" %>
13 |
--------------------------------------------------------------------------------
/demo/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/demo/app/views/devise/passwords/new.html.erb:
--------------------------------------------------------------------------------
1 | Forgot your password?
2 |
3 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
4 | <%= devise_error_messages! %>
5 |
6 | <%= f.label :email %>
7 | <%= f.email_field :email, autofocus: true %>
8 |
9 | <%= f.submit "Send me reset password instructions" %>
10 | <% end %>
11 |
12 | <%= render "devise/shared/links" %>
13 |
--------------------------------------------------------------------------------
/demo/app/views/devise/mailer/reset_password_instructions.html.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @resource.email %>!
2 |
3 | Someone has requested a link to change your password. You can do this through the link below.
4 |
5 | <%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>
6 |
7 | If you didn't request this, please ignore this email.
8 | Your password won't change until you access the link above and create a new one.
9 |
--------------------------------------------------------------------------------
/demo/app/views/devise/confirmations/new.html.erb:
--------------------------------------------------------------------------------
1 | Resend confirmation instructions
2 |
3 | <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
4 | <%= devise_error_messages! %>
5 |
6 | <%= f.label :email %>
7 | <%= f.email_field :email, autofocus: true %>
8 |
9 | <%= f.submit "Resend confirmation instructions" %>
10 | <% end %>
11 |
12 | <%= render "devise/shared/links" %>
13 |
--------------------------------------------------------------------------------
/demo/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | ENV['RAILS_ENV'] ||= 'test'
2 | require File.expand_path('../../config/environment', __FILE__)
3 | require 'rails/test_help'
4 |
5 | class ActiveSupport::TestCase
6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
7 | #
8 | # Note: You'll currently still have to declare fixtures explicitly in integration tests
9 | # -- they do not yet inherit this setting
10 | fixtures :all
11 |
12 | # Add more helper methods to be used by all tests here...
13 | end
14 |
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | # Ignore bundler config.
8 | /.bundle
9 |
10 | # Ignore the default SQLite database.
11 | /db/*.sqlite3
12 | /db/*.sqlite3-journal
13 |
14 | # Ignore all logfiles and tempfiles.
15 | /log/*.log
16 | /tmp
17 |
--------------------------------------------------------------------------------
/spec/devise/models/two_factor_authenticatable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | class TwoFactorAuthenticatableDouble
4 | include ::ActiveModel::Validations::Callbacks
5 | extend ::Devise::Models
6 |
7 | devise :two_factor_authenticatable, :otp_secret_encryption_key => 'test-key'
8 | end
9 |
10 | describe ::Devise::Models::TwoFactorAuthenticatable do
11 | context 'When included in a class' do
12 | subject { TwoFactorAuthenticatableDouble.new }
13 |
14 | it_behaves_like 'two_factor_authenticatable'
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/devise/models/two_factor_backupable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | class TwoFactorBackupableDouble
4 | include ::ActiveModel::Validations::Callbacks
5 | extend ::Devise::Models
6 |
7 | devise :two_factor_authenticatable, :two_factor_backupable,
8 | :otp_secret_encryption_key => 'test-key'
9 |
10 | attr_accessor :otp_backup_codes
11 | end
12 |
13 | describe ::Devise::Models::TwoFactorBackupable do
14 | context 'When included in a class' do
15 | subject { TwoFactorBackupableDouble.new }
16 |
17 | it_behaves_like 'two_factor_backupable'
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/demo/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/demo/README.rdoc:
--------------------------------------------------------------------------------
1 | == README
2 |
3 | This README would normally document whatever steps are necessary to get the
4 | application up and running.
5 |
6 | Things you may want to cover:
7 |
8 | * Ruby version
9 |
10 | * System dependencies
11 |
12 | * Configuration
13 |
14 | * Database creation
15 |
16 | * Database initialization
17 |
18 | * How to run the test suite
19 |
20 | * Services (job queues, cache servers, search engines, etc.)
21 |
22 | * Deployment instructions
23 |
24 | * ...
25 |
26 |
27 | Please feel free to use a different markup language if you do not plan to run
28 | rake doc:app.
29 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | require 'rubygems'
4 | require 'bundler'
5 | begin
6 | Bundler.setup(:default, :development)
7 | rescue Bundler::BundlerError => e
8 | $stderr.puts e.message
9 | $stderr.puts "Run `bundle install` to install missing gems"
10 | exit e.status_code
11 | end
12 | require 'rake'
13 |
14 | require 'rspec/core'
15 | require 'rspec/core/rake_task'
16 | RSpec::Core::RakeTask.new(:spec) do |spec|
17 | spec.pattern = FileList['spec/**/*_spec.rb']
18 | end
19 |
20 | desc "Code coverage detail"
21 | task :simplecov do
22 | ENV['COVERAGE'] = "true"
23 | Rake::Task['spec'].execute
24 | end
25 |
26 | task :default => :spec
27 |
--------------------------------------------------------------------------------
/demo/app/views/devise/registrations/new.html.erb:
--------------------------------------------------------------------------------
1 | Sign up
2 |
3 | <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
4 | <%= devise_error_messages! %>
5 |
6 | <%= f.label :email %>
7 | <%= f.email_field :email, autofocus: true %>
8 |
9 | <%= f.label :password %>
10 | <%= f.password_field :password, autocomplete: "off" %>
11 |
12 | <%= f.label :password_confirmation %>
13 | <%= f.password_field :password_confirmation, autocomplete: "off" %>
14 |
15 | <%= f.submit "Sign up" %>
16 | <% end %>
17 |
18 | <%= render "devise/shared/links" %>
19 |
--------------------------------------------------------------------------------
/demo/config/initializers/devise.rb:
--------------------------------------------------------------------------------
1 | Devise.setup do |config|
2 | config.warden do |manager|
3 | manager.default_strategies(:scope => :user).unshift :two_factor_authenticatable
4 | end
5 |
6 | config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com'
7 |
8 | require 'devise/orm/active_record'
9 |
10 | config.case_insensitive_keys = [ :email ]
11 | config.strip_whitespace_keys = [ :email ]
12 | config.skip_session_storage = [:http_auth]
13 | config.stretches = Rails.env.test? ? 1 : 10
14 | config.reconfirmable = true
15 | config.password_length = 8..128
16 | config.reset_password_within = 6.hours
17 | config.sign_out_via = :delete
18 | end
19 |
--------------------------------------------------------------------------------
/demo/app/views/devise/passwords/edit.html.erb:
--------------------------------------------------------------------------------
1 | Change your password
2 |
3 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %>
4 | <%= devise_error_messages! %>
5 | <%= f.hidden_field :reset_password_token %>
6 |
7 | <%= f.label :password, "New password" %>
8 | <%= f.password_field :password, autofocus: true, autocomplete: "off" %>
9 |
10 | <%= f.label :password_confirmation, "Confirm new password" %>
11 | <%= f.password_field :password_confirmation, autocomplete: "off" %>
12 |
13 | <%= f.submit "Change my password" %>
14 | <% end %>
15 |
16 | <%= render "devise/shared/links" %>
17 |
--------------------------------------------------------------------------------
/demo/app/views/devise/sessions/new.html.erb:
--------------------------------------------------------------------------------
1 | Sign in
2 |
3 | <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
4 | <%= f.label :email %>
5 | <%= f.email_field :email, autofocus: true %>
6 |
7 | <%= f.label :password %>
8 | <%= f.password_field :password, autocomplete: "off" %>
9 |
10 | <%= f.label :otp_attempt %>
11 | <%= f.text_field :otp_attempt %>
12 |
13 | <% if devise_mapping.rememberable? -%>
14 | <%= f.check_box :remember_me %> <%= f.label :remember_me %>
15 | <% end -%>
16 |
17 | <%= f.submit "Sign in" %>
18 | <% end %>
19 |
20 | <%= render "devise/shared/links" %>
21 |
--------------------------------------------------------------------------------
/demo/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/demo/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # To learn more, please read the Rails Internationalization guide
20 | # available at http://guides.rubyonrails.org/i18n.html.
21 |
22 | en:
23 | hello: "Hello world"
24 |
--------------------------------------------------------------------------------
/demo/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file.
9 | //
10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require jquery
14 | //= require jquery_ujs
15 | //= require turbolinks
16 | //= require_tree .
17 |
--------------------------------------------------------------------------------
/demo/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any styles
10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11 | * file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'simplecov'
2 |
3 | module SimpleCov::Configuration
4 | def clean_filters
5 | @filters = []
6 | end
7 | end
8 |
9 | SimpleCov.configure do
10 | clean_filters
11 | load_profile 'test_frameworks'
12 | end
13 |
14 | ENV["COVERAGE"] && SimpleCov.start do
15 | add_filter "/.rvm/"
16 | end
17 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
18 | $LOAD_PATH.unshift(File.dirname(__FILE__))
19 |
20 | require 'rspec'
21 | require 'faker'
22 | require 'timecop'
23 | require 'devise-two-factor'
24 | require 'devise_two_factor/spec_helpers'
25 |
26 | # Requires supporting files with custom matchers and macros, etc,
27 | # in ./support/ and its subdirectories.
28 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
29 |
30 | RSpec.configure do |config|
31 | config.order = 'random'
32 | end
33 |
--------------------------------------------------------------------------------
/demo/app/views/home/index.html.erb:
--------------------------------------------------------------------------------
1 | <% if !current_user %>
2 | <%= link_to "Sign up", new_user_registration_path %>
3 | <%= link_to "Login", new_user_session_path %>
4 | <% end %>
5 |
6 | <% if current_user %>
7 | <% if !current_user.otp_required_for_login %>
8 | <%= button_to "Enable 2FA", users_enable_otp_path, :method => :post %>
9 | <% end %>
10 |
11 | <% if current_user.otp_required_for_login %>
12 | <%= button_to "Disable 2FA", users_disable_otp_path, :method => :post %>
13 | <%= raw RQRCode::render_qrcode(current_user.otp_provisioning_uri(current_user.email, issuer: "Devise-Two-Factor-Demo"),
14 | :svg,
15 | :level => :l,
16 | :unit => 2) %>
17 |
18 | <% end %>
19 | <%= link_to "Log out", destroy_user_session_path, :method => :delete %>
20 | <% end %>
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | Gemfile.lock
2 | *.gem
3 |
4 | # rcov generated
5 | coverage
6 | coverage.data
7 |
8 | # yard generated
9 | doc
10 | .yardoc
11 |
12 | # bundler
13 | .bundle
14 |
15 | # jeweler generated
16 | pkg
17 |
18 | # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
19 | #
20 | # * Create a file at ~/.gitignore
21 | # * Include files you want ignored
22 | # * Run: git config --global core.excludesfile ~/.gitignore
23 | #
24 | # After doing this, these files will be ignored in all your git projects,
25 | # saving you from having to 'pollute' every project you touch with them
26 | #
27 | # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
28 | #
29 | # For MacOS:
30 | #
31 | #.DS_Store
32 |
33 | # For TextMate
34 | #*.tmproj
35 | #tmtags
36 |
37 | # For emacs:
38 | #*~
39 | #\#*
40 | #.\#*
41 |
42 | # For vim:
43 | #*.swp
44 |
45 | # For redcar:
46 | #.redcar
47 |
48 | # For rubinius:
49 | #*.rbc
50 |
--------------------------------------------------------------------------------
/lib/devise_two_factor/strategies/two_factor_backupable.rb:
--------------------------------------------------------------------------------
1 | module Devise
2 | module Strategies
3 | class TwoFactorBackupable < Devise::Strategies::DatabaseAuthenticatable
4 |
5 | def authenticate!
6 | resource = mapping.to.find_for_database_authentication(authentication_hash)
7 |
8 | if validate(resource) { resource.invalidate_otp_backup_code!(params[scope]['otp_attempt']) }
9 | # Devise fails to authenticate invalidated resources, but if we've
10 | # gotten here, the object changed (Since we deleted a recovery code)
11 | resource.save!
12 | super
13 | end
14 |
15 | fail(:not_found_in_database) unless resource
16 |
17 | # We want to cascade to the next strategy if this one fails,
18 | # but database authenticatable automatically halts on a bad password
19 | @halted = false if @result == :failure
20 | end
21 | end
22 | end
23 | end
24 |
25 | Warden::Strategies.add(:two_factor_backupable, Devise::Strategies::TwoFactorBackupable)
26 |
--------------------------------------------------------------------------------
/demo/config/secrets.yml:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rake secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | development:
14 | secret_key_base: c8c845b85a69d2b67ec99c1912ddcde63f0fcd05a74052f396daf68a2688d576c203bdc239c9423a0cf88218e201d30616959d5eeceea00eedf915677ecd373b
15 |
16 | test:
17 | secret_key_base: df468dd66c4b0f143354d216d96cdb8542ffe215afd78ebf977f7a8bfdb93010db134320cb738d27385bd7884cd73f3f22f90b147d4d176c3c8d0d63d5427fa9
18 |
19 | # Do not keep production secrets in the repository,
20 | # instead read values from the environment.
21 | production:
22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
23 |
--------------------------------------------------------------------------------
/demo/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require 'rails/all'
4 |
5 | # Require the gems listed in Gemfile, including any gems
6 | # you've limited to :test, :development, or :production.
7 | Bundler.require(*Rails.groups)
8 |
9 | module DeviseTwoFactorDemo
10 | class Application < Rails::Application
11 | # Settings in config/environments/* take precedence over those specified here.
12 | # Application configuration should go into files in config/initializers
13 | # -- all .rb files in that directory are automatically loaded.
14 |
15 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
16 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
17 | # config.time_zone = 'Central Time (US & Canada)'
18 |
19 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
20 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
21 | # config.i18n.default_locale = :de
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Tinfoil Security, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/lib/devise_two_factor/strategies/two_factor_authenticatable.rb:
--------------------------------------------------------------------------------
1 | module Devise
2 | module Strategies
3 | class TwoFactorAuthenticatable < Devise::Strategies::DatabaseAuthenticatable
4 |
5 | def authenticate!
6 | resource = mapping.to.find_for_database_authentication(authentication_hash)
7 | # We authenticate in two cases:
8 | # 1. The password and the OTP are correct
9 | # 2. The password is correct, and OTP is not required for login
10 | # We check the OTP, then defer to DatabaseAuthenticatable
11 | if validate(resource) { !resource.otp_required_for_login ||
12 | resource.valid_otp?(params[scope]['otp_attempt']) }
13 | super
14 | end
15 |
16 | fail(:not_found_in_database) unless resource
17 |
18 | # We want to cascade to the next strategy if this one fails,
19 | # but database authenticatable automatically halts on a bad password
20 | @halted = false if @result == :failure
21 | end
22 | end
23 | end
24 | end
25 |
26 | Warden::Strategies.add(:two_factor_authenticatable, Devise::Strategies::TwoFactorAuthenticatable)
27 |
--------------------------------------------------------------------------------
/lib/devise-two-factor.rb:
--------------------------------------------------------------------------------
1 | require 'devise'
2 | require 'devise_two_factor/models'
3 | require 'devise_two_factor/strategies'
4 |
5 | module Devise
6 | # The length of generated OTP secrets
7 | mattr_accessor :otp_secret_length
8 | @@otp_secret_length = 128
9 |
10 | # The number of seconds before and after the current
11 | # time for which codes will be accepted
12 | mattr_accessor :otp_allowed_drift
13 | @@otp_allowed_drift = 30
14 |
15 | # The key used to encrypt OTP secrets in the database
16 | mattr_accessor :otp_secret_encryption_key
17 | @@otp_secret_encryption_key = nil
18 |
19 | # The length of all generated OTP backup codes
20 | mattr_accessor :otp_backup_code_length
21 | @@otp_backup_code_length = 16
22 |
23 | # The number of backup codes generated by a call to
24 | # generate_otp_backup_codes!
25 | mattr_accessor :otp_number_of_backup_codes
26 | @@otp_number_of_backup_codes = 5
27 | end
28 |
29 | Devise.add_module(:two_factor_authenticatable, :route => :session, :strategy => true,
30 | :controller => :sessions, :model => true)
31 |
32 | Devise.add_module(:two_factor_backupable, :route => :session, :strategy => true,
33 | :controller => :sessions, :model => true)
34 |
--------------------------------------------------------------------------------
/demo/app/views/devise/shared/_links.erb:
--------------------------------------------------------------------------------
1 | <%- if controller_name != 'sessions' %>
2 | <%= link_to "Sign in", new_session_path(resource_name) %>
3 | <% end -%>
4 |
5 | <%- if devise_mapping.registerable? && controller_name != 'registrations' %>
6 | <%= link_to "Sign up", new_registration_path(resource_name) %>
7 | <% end -%>
8 |
9 | <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
10 | <%= link_to "Forgot your password?", new_password_path(resource_name) %>
11 | <% end -%>
12 |
13 | <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
14 | <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
15 | <% end -%>
16 |
17 | <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
18 | <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
19 | <% end -%>
20 |
21 | <%- if devise_mapping.omniauthable? %>
22 | <%- resource_class.omniauth_providers.each do |provider| %>
23 | <%= link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider) %>
24 | <% end -%>
25 | <% end -%>
26 |
--------------------------------------------------------------------------------
/demo/app/views/devise/registrations/edit.html.erb:
--------------------------------------------------------------------------------
1 | Edit <%= resource_name.to_s.humanize %>
2 |
3 | <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
4 | <%= devise_error_messages! %>
5 |
6 | <%= f.label :email %>
7 | <%= f.email_field :email, autofocus: true %>
8 |
9 | <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
10 | Currently waiting confirmation for: <%= resource.unconfirmed_email %>
11 | <% end %>
12 |
13 | <%= f.label :password %> (leave blank if you don't want to change it)
14 | <%= f.password_field :password, autocomplete: "off" %>
15 |
16 | <%= f.label :password_confirmation %>
17 | <%= f.password_field :password_confirmation, autocomplete: "off" %>
18 |
19 | <%= f.label :current_password %> (we need your current password to confirm your changes)
20 | <%= f.password_field :current_password, autocomplete: "off" %>
21 |
22 | <%= f.submit "Update" %>
23 | <% end %>
24 |
25 | Cancel my account
26 |
27 | Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>
28 |
29 | <%= link_to "Back", :back %>
30 |
--------------------------------------------------------------------------------
/demo/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 |
4 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
5 | gem 'rails', '4.1.0'
6 | # Use postgresql as the database for Active Record
7 | gem 'pg'
8 | # Use SCSS for stylesheets
9 | gem 'sass-rails', '~> 4.0.3'
10 | # Use Uglifier as compressor for JavaScript assets
11 | gem 'uglifier', '>= 1.3.0'
12 | # Use CoffeeScript for .js.coffee assets and views
13 | gem 'coffee-rails', '~> 4.0.0'
14 | # See https://github.com/sstephenson/execjs#readme for more supported runtimes
15 | # gem 'therubyracer', platforms: :ruby
16 |
17 | # Use jquery as the JavaScript library
18 | gem 'jquery-rails'
19 | # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
20 | gem 'turbolinks'
21 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
22 | gem 'jbuilder', '~> 2.0'
23 | # bundle exec rake doc:rails generates the API under doc/api.
24 | gem 'sdoc', '~> 0.4.0', group: :doc
25 |
26 | gem 'devise'
27 | gem 'devise-two-factor', :path => '../'
28 | gem 'rqrcode-rails3'
29 |
30 | # Use ActiveModel has_secure_password
31 | # gem 'bcrypt', '~> 3.1.7'
32 |
33 | # Use unicorn as the app server
34 | # gem 'unicorn'
35 |
36 | # Use Capistrano for deployment
37 | # gem 'capistrano-rails', group: :development
38 |
39 | # Use debugger
40 | # gem 'debugger', group: [:development, :test]
41 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | We love pull requests. Here's a quick guide:
2 |
3 | 1. Fork the repo.
4 |
5 | 2. Run the tests. We only take pull requests with passing tests, and it's great
6 | to know that you have a clean slate: `bundle && rake`
7 |
8 | 3. Add a test for your change. Only refactoring and documentation changes
9 | require no new tests. If you are adding functionality or fixing a bug, we need
10 | a test!
11 |
12 | 4. Make the test pass.
13 |
14 | 5. Push to your fork and submit a pull request.
15 |
16 |
17 | At this point you're waiting on us. We like to at least comment on, if not
18 | accept, pull requests within three business days (and, typically, one business
19 | day). We may suggest some changes or improvements or alternatives.
20 |
21 | Some things that will increase the chance that your pull request is accepted,
22 | taken straight from the Ruby on Rails guide:
23 |
24 | * Use Rails idioms and helpers
25 | * Include tests that fail without your code, and pass with it
26 | * Update the documentation, the surrounding one, examples elsewhere, guides,
27 | whatever is affected by your contribution
28 |
29 | Syntax:
30 |
31 | * Two spaces, no tabs.
32 | * No trailing whitespace. Blank lines should not have any space.
33 | * Prefer &&/|| over and/or.
34 | * MyClass.my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
35 | * a = b and not a=b.
36 | * Follow the conventions you see used in the source already.
37 |
38 | And in case we didn't emphasize it enough: we love tests!
39 |
--------------------------------------------------------------------------------
/demo/db/migrate/20140515190128_devise_create_users.rb:
--------------------------------------------------------------------------------
1 | class DeviseCreateUsers < ActiveRecord::Migration
2 | def change
3 | create_table(:users) do |t|
4 | ## Database authenticatable
5 | t.string :email, null: false, default: ""
6 | t.string :encrypted_password, null: false, default: ""
7 |
8 | ## Recoverable
9 | t.string :reset_password_token
10 | t.datetime :reset_password_sent_at
11 |
12 | ## Rememberable
13 | t.datetime :remember_created_at
14 |
15 | ## Trackable
16 | t.integer :sign_in_count, default: 0, null: false
17 | t.datetime :current_sign_in_at
18 | t.datetime :last_sign_in_at
19 | t.string :current_sign_in_ip
20 | t.string :last_sign_in_ip
21 |
22 | ## Confirmable
23 | # t.string :confirmation_token
24 | # t.datetime :confirmed_at
25 | # t.datetime :confirmation_sent_at
26 | # t.string :unconfirmed_email # Only if using reconfirmable
27 |
28 | ## Lockable
29 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
30 | # t.string :unlock_token # Only if unlock strategy is :email or :both
31 | # t.datetime :locked_at
32 |
33 |
34 | t.timestamps
35 | end
36 |
37 | add_index :users, :email, unique: true
38 | add_index :users, :reset_password_token, unique: true
39 | # add_index :users, :confirmation_token, unique: true
40 | # add_index :users, :unlock_token, unique: true
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/demo/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports and disable caching.
13 | config.consider_all_requests_local = true
14 | config.action_controller.perform_caching = false
15 |
16 | # Don't care if the mailer can't send.
17 | config.action_mailer.raise_delivery_errors = false
18 |
19 | # Print deprecation notices to the Rails logger.
20 | config.active_support.deprecation = :log
21 |
22 | # Raise an error on page load if there are pending migrations.
23 | config.active_record.migration_error = :page_load
24 |
25 | # Debug mode disables concatenation and preprocessing of assets.
26 | # This option may cause significant delays in view rendering with a large
27 | # number of complex assets.
28 | config.assets.debug = true
29 |
30 | # Adds additional error checking when serving assets at runtime.
31 | # Checks for improperly declared sprockets dependencies.
32 | # Raises helpful error messages.
33 | config.assets.raise_runtime_errors = true
34 |
35 | # Raises error for missing translations
36 | # config.action_view.raise_on_missing_translations = true
37 | end
38 |
--------------------------------------------------------------------------------
/devise-two-factor.gemspec:
--------------------------------------------------------------------------------
1 | $:.push File.expand_path('../lib', __FILE__)
2 | require 'devise_two_factor/version'
3 |
4 | Gem::Specification.new do |s|
5 | s.name = 'devise-two-factor'
6 | s.version = DeviseTwoFactor::VERSION.dup
7 | s.platform = Gem::Platform::RUBY
8 | s.licenses = ['MIT']
9 | s.summary = 'Barebones two-factor authentication with Devise'
10 | s.email = 'engineers@tinfoilsecurity.com'
11 | s.homepage = 'https://github.com/tinfoil/devise-two-factor'
12 | s.description = 'Barebones two-factor authentication with Devise'
13 | s.authors = ['Shane Wilton']
14 |
15 | s.cert_chain = [
16 | 'certs/tinfoil-cacert.pem',
17 | 'certs/tinfoilsecurity-gems-cert.pem'
18 | ]
19 | s.signing_key = File.expand_path("~/.ssh/tinfoilsecurity-gems-key.pem") if $0 =~ /gem\z/
20 |
21 | s.rubyforge_project = 'devise-two-factor'
22 |
23 | s.files = `git ls-files`.split("\n").delete_if { |x| x.match('demo/*') }
24 | s.test_files = `git ls-files -- spec/*`.split("\n")
25 | s.require_paths = ['lib']
26 |
27 | s.add_runtime_dependency 'rails' # For generators
28 | s.add_runtime_dependency 'activesupport'
29 | s.add_runtime_dependency 'activemodel'
30 | s.add_runtime_dependency 'attr_encrypted'
31 | s.add_runtime_dependency 'devise'
32 | s.add_runtime_dependency 'rotp'
33 |
34 | s.add_development_dependency 'bundler', '> 1.0'
35 | s.add_development_dependency 'rspec', '~> 2.8'
36 | s.add_development_dependency 'simplecov'
37 | s.add_development_dependency 'faker'
38 | s.add_development_dependency 'timecop'
39 | end
40 |
--------------------------------------------------------------------------------
/demo/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/demo/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure static asset server for tests with Cache-Control for performance.
16 | config.serve_static_assets = true
17 | config.static_cache_control = 'public, max-age=3600'
18 |
19 | # Show full error reports and disable caching.
20 | config.consider_all_requests_local = true
21 | config.action_controller.perform_caching = false
22 |
23 | # Raise exceptions instead of rendering exception templates.
24 | config.action_dispatch.show_exceptions = false
25 |
26 | # Disable request forgery protection in test environment.
27 | config.action_controller.allow_forgery_protection = false
28 |
29 | # Tell Action Mailer not to deliver emails to the real world.
30 | # The :test delivery method accumulates sent emails in the
31 | # ActionMailer::Base.deliveries array.
32 | config.action_mailer.delivery_method = :test
33 |
34 | # Print deprecation notices to the stderr.
35 | config.active_support.deprecation = :stderr
36 |
37 | # Raises error for missing translations
38 | # config.action_view.raise_on_missing_translations = true
39 | end
40 |
--------------------------------------------------------------------------------
/demo/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/demo/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/demo/db/schema.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | # This file is auto-generated from the current state of the database. Instead
3 | # of editing this file, please use the migrations feature of Active Record to
4 | # incrementally modify your database, and then regenerate this schema definition.
5 | #
6 | # Note that this schema.rb definition is the authoritative source for your
7 | # database schema. If you need to create the application database on another
8 | # system, you should be using db:schema:load, not running all the migrations
9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 | # you'll amass, the slower it'll run and the greater likelihood for issues).
11 | #
12 | # It's strongly recommended that you check this file into your version control system.
13 |
14 | ActiveRecord::Schema.define(version: 20140516191259) do
15 |
16 | # These are extensions that must be enabled in order to support this database
17 | enable_extension "plpgsql"
18 |
19 | create_table "users", force: true do |t|
20 | t.string "email", default: "", null: false
21 | t.string "encrypted_password", default: "", null: false
22 | t.string "reset_password_token"
23 | t.datetime "reset_password_sent_at"
24 | t.datetime "remember_created_at"
25 | t.integer "sign_in_count", default: 0, null: false
26 | t.datetime "current_sign_in_at"
27 | t.datetime "last_sign_in_at"
28 | t.string "current_sign_in_ip"
29 | t.string "last_sign_in_ip"
30 | t.datetime "created_at"
31 | t.datetime "updated_at"
32 | t.string "encrypted_otp_secret"
33 | t.string "encrypted_otp_secret_iv"
34 | t.string "encrypted_otp_secret_salt"
35 | t.boolean "otp_required_for_login"
36 | end
37 |
38 | add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
39 | add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
40 |
41 | end
42 |
--------------------------------------------------------------------------------
/lib/devise_two_factor/models/two_factor_authenticatable.rb:
--------------------------------------------------------------------------------
1 | require 'active_model'
2 | require 'attr_encrypted'
3 | require 'rotp'
4 |
5 | module Devise
6 | module Models
7 | module TwoFactorAuthenticatable
8 | extend ActiveSupport::Concern
9 | include Devise::Models::DatabaseAuthenticatable
10 |
11 | included do
12 | attr_encrypted :otp_secret, :key => self.otp_secret_encryption_key,
13 | :mode => :per_attribute_iv_and_salt
14 |
15 | attr_accessor :otp_attempt
16 | end
17 |
18 | def self.required_fields(klass)
19 | [:encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt]
20 | end
21 |
22 | # This defaults to the model's otp_secret
23 | # If this hasn't been generated yet, pass a secret as an option
24 | def valid_otp?(code, options = {})
25 | otp_secret = options[:otp_secret] || self.otp_secret
26 | return false unless otp_secret.present?
27 |
28 | totp = self.otp(otp_secret)
29 | totp.verify_with_drift(code, self.class.otp_allowed_drift)
30 | end
31 |
32 | def otp(otp_secret = self.otp_secret)
33 | ROTP::TOTP.new(otp_secret)
34 | end
35 |
36 | def current_otp
37 | otp.at(Time.now)
38 | end
39 |
40 | def otp_provisioning_uri(account, options = {})
41 | otp_secret = options[:otp_secret] || self.otp_secret
42 | ROTP::TOTP.new(otp_secret, options).provisioning_uri(account)
43 | end
44 |
45 | def clean_up_passwords
46 | self.otp_attempt = nil
47 | end
48 |
49 | protected
50 |
51 | module ClassMethods
52 | Devise::Models.config(self, :otp_secret_length,
53 | :otp_allowed_drift,
54 | :otp_secret_encryption_key)
55 |
56 | def generate_otp_secret(otp_secret_length = self.otp_secret_length)
57 | ROTP::Base32.random_base32(otp_secret_length)
58 | end
59 | end
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/certs/tinfoilsecurity-gems-cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIFyjCCA7KgAwIBAgIJAK2u0LojMCNtMA0GCSqGSIb3DQEBDQUAMIGcMQswCQYD
3 | VQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVBhbG8gQWx0bzEfMB0GA1UE
4 | ChMWVGluZm9pbCBTZWN1cml0eSwgSW5jLjEfMB0GA1UEAxMWVGluZm9pbCBTZWN1
5 | cml0eSwgSW5jLjEqMCgGCSqGSIb3DQEJARYbc3VwcG9ydEB0aW5mb2lsc2VjdXJp
6 | dHkuY29tMB4XDTE0MDUyMDIxMTAwMFoXDTE2MDUyMDIxMTAwMFowgZwxCzAJBgNV
7 | BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJUGFsbyBBbHRvMR8wHQYDVQQK
8 | ExZUaW5mb2lsIFNlY3VyaXR5LCBJbmMuMR0wGwYDVQQDExR0aW5mb2lsc2VjdXJp
9 | dHktZ2VtczEsMCoGCSqGSIb3DQEJARYdZW5naW5lZXJzQHRpbmZvaWxzZWN1cml0
10 | eS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDNJYNH8D+8lACL
11 | t3KzjEIPs3XVBCPaMm2eD/Xk9OOTuDV/NqgMK0icD9MRxMUtS3SCrC9QcPocXT76
12 | f2LQ3yVJuK+rBUasymEES47PIx2czC4n4Hga0xPPuBpioO26oaRFsobyzh9RPOIb
13 | nYfpjyqtdrbm+YyM3sPR4XzFirv9xomT4E9T4RCLgOQHTcLKL9K9m+EN7PeVdVUX
14 | V0Pa7cVs2vJUKedsd7vnr6Lzbn8ToPk/7J/4W931PbaeI5yg9ZuaRa9K2IaY1TkP
15 | I67NW4qKitBVepRlXw6Sb7TYcUncWEQ/eC5CpnOmqUrG5tfGD8cc5aGZOkitW/VX
16 | ZgVj81xgCv1hk4HjErrqq4FBNAaCSNyBfwR0TUYqg1lN1nbNjOKwfb6YRn06R2ov
17 | cFJG0tmGhsQULCr6fW8u2TfSM+U9WFSIJx2griureY7EZPwg/MgsUiWUWMFemz3G
18 | VYXWJR3dN2pW9Uqr3rkjKZbA0bstGWahJO9HuFdDakQxoaTPYPtTQDC+kskkO6lK
19 | G1KLIoZ1iLZzB1Ks1vEeyE7lp1imWgpUq+q23PFkt1gIBi/4tGvzsLZye25QU2Y+
20 | XLzldCNm+DyRFXZ+Q+bK33IveUeUWEOv4T1qTXHAOypyzmgodVRG/PrlsSMOBfE5
21 | 15kG1mDMGjRcCpEtlskgxUbf7qM7hQIDAQABow0wCzAJBgNVHRMEAjAAMA0GCSqG
22 | SIb3DQEBDQUAA4ICAQCLXzGJOr0isGHscTvm73ReEAOv4g0IOSXjfHfHAOWKhzdM
23 | g+D8nrzhy8wnARqAt2Ob4I+R9scEIfI5MPp/C5HHqWed4m0W0Uygx3V3qyixavkc
24 | nVUJMZ4TPS6W48IHdGGVD45hopx7ycFy+iPm7QUFk4sg044dO53mkScTetm2AvIE
25 | xDTotsFNpn/hrAnzXlH4MQQ7LKXAedPmFi8Jv5nJwv9BnEGJQJhvzQtdx7le4S8J
26 | a78vwAq429N2gVnfSdeU9v9QqdHOF1215lC6qaDI4bk8hVE4RyMxSBZmWKhnPLAm
27 | jFXGc6Wj2yF7HQ2YtEGAAEyjH+tAwF35STv+J3eweUFhyGZPH8Sf+b+UKZc9UF1D
28 | J/VmzQd9D9RaB+pOclDYR3Uiji5EBq1La2FPg48hA0uCd4KJ9dBGwNrxvjDNSBW0
29 | FiM1vjR6AVxIAhOwICeWG6QTvH5Jrnq/UDBnnK/KALCbFw3YbbhOyy+295jta7xf
30 | r32d4cJAvbDF1C6t2JRjNwi0ANgPw0cytl+8yvCyXpXMZpT0YpDk76XICo97SOHI
31 | 5C31v4YyRBnNCp0pN66nxYX2avEiQ8riTBP5mlkPPOhsIoYQHHe2Uj75aVpu0LZ3
32 | cdFzuO4GC1dV0Wv+dsDm+MyF7DT5E9pUPXpnMJuPvPrFpCb+wrFlszW9hGjXbQ==
33 | -----END CERTIFICATE-----
34 |
--------------------------------------------------------------------------------
/lib/devise_two_factor/models/two_factor_backupable.rb:
--------------------------------------------------------------------------------
1 | require 'active_model'
2 |
3 | module Devise
4 | module Models
5 | # TwoFactorBackupable allows a user to generate backup codes which
6 | # provide one-time access to their account in the event that they have
7 | # lost access to their two-factor device
8 | module TwoFactorBackupable
9 | extend ActiveSupport::Concern
10 |
11 | def self.required_fields(klass)
12 | [:otp_backup_codes]
13 | end
14 |
15 | # 1) Invalidates all existing backup codes
16 | # 2) Generates otp_number_of_backup_codes backup codes
17 | # 3) Stores the hashed backup codes in the database
18 | # 4) Returns a plaintext array of the generated backup codes
19 | def generate_otp_backup_codes!
20 | codes = []
21 | number_of_codes = self.class.otp_number_of_backup_codes
22 | code_length = self.class.otp_backup_code_length
23 |
24 | number_of_codes.times do
25 | codes << SecureRandom.hex(code_length / 2) # Hexstring has length 2*n
26 | end
27 |
28 | hashed_codes = codes.map { |code| Devise.bcrypt self.class, code }
29 | self.otp_backup_codes = hashed_codes
30 |
31 | codes
32 | end
33 |
34 | # Returns true and invalidates the given code
35 | # iff that code is a valid backup code.
36 | def invalidate_otp_backup_code!(code)
37 | codes = self.otp_backup_codes || []
38 |
39 | codes.each do |backup_code|
40 | # We hashed the code with Devise.bcrypt, so if Devise changes that
41 | # method, we'll have to adjust our comparison here to match it
42 | # TODO Fork Devise and encapsulate this logic in a helper
43 | bcrypt = ::BCrypt::Password.new(backup_code)
44 | hashed_code = ::BCrypt::Engine.hash_secret("#{code}#{self.class.pepper}",
45 | bcrypt.salt)
46 |
47 | next unless Devise.secure_compare(hashed_code, backup_code)
48 |
49 | codes.delete(backup_code)
50 | self.otp_backup_codes = codes
51 | return true
52 | end
53 |
54 | false
55 | end
56 |
57 | protected
58 |
59 | module ClassMethods
60 | Devise::Models.config(self, :otp_backup_code_length,
61 | :otp_number_of_backup_codes,
62 | :pepper)
63 | end
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/certs/tinfoil-cacert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIHSjCCBTKgAwIBAgIJAK2u0LojMCNgMA0GCSqGSIb3DQEBBQUAMIGcMQswCQYD
3 | VQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVBhbG8gQWx0bzEfMB0GA1UE
4 | ChMWVGluZm9pbCBTZWN1cml0eSwgSW5jLjEfMB0GA1UEAxMWVGluZm9pbCBTZWN1
5 | cml0eSwgSW5jLjEqMCgGCSqGSIb3DQEJARYbc3VwcG9ydEB0aW5mb2lsc2VjdXJp
6 | dHkuY29tMB4XDTExMTIyNzA1MDc0N1oXDTIxMTIyNDA1MDc0N1owgZwxCzAJBgNV
7 | BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJUGFsbyBBbHRvMR8wHQYDVQQK
8 | ExZUaW5mb2lsIFNlY3VyaXR5LCBJbmMuMR8wHQYDVQQDExZUaW5mb2lsIFNlY3Vy
9 | aXR5LCBJbmMuMSowKAYJKoZIhvcNAQkBFhtzdXBwb3J0QHRpbmZvaWxzZWN1cml0
10 | eS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCqbHvsSj0H0FB1
11 | 0gLYoDK1BKugkSB2DZeZZHP6B1UdWRahJXJP9oT1lhfQxx8iX4cgEi7JU3NqA6NR
12 | cIRFQ50eH/qlmgs7909gaf8pDaeC0vR3wd0GeRg6qr1eDEnkzIyr/D1AMiX6H1eP
13 | Y7J3SfrdaL3gft2iPRKGkgqsXR7oBNLA3n/ShiNgPXqRDl1CCj6aMY0cn5ROFScz
14 | vT2FUB4DEwPD2l18m1p99OnXqsOLL2J65qA2+cI8FtgFmlwIi5oSf+URvIdNx+cH
15 | lInlAtVHCvAKYLY0dlQ7czMQBcRpYjp2rwPt9f2ksq9b/voMTBABYHFV+IVn8svv
16 | GZ5e1+icjtr/R7dCGmCdEdFLXVxafmZhukymG9USv9DKuv1qh7r4q8KaPIE8n7nQ
17 | m97jENFfsgnwv+nUmIJ3tzuW5ZxO7A0tIIYdwzt0UjrO3ya4R5bTFXr4bnzZ/g/s
18 | CLknWqg1BCRlPd6LnpVGPT0gNDV1pEO25wE3A3Yy0Ujxudcgay/CgUhnlU11qOAc
19 | xmar2fhNZsviUhndd/220Ad5QMV2XzcAiopJIeu0juIVGRQM7x2h19Hsp0m6sOEF
20 | jfhvbdUa4nvmIFeYFY+hr/YkTmG9ZjyBa8YaZXhwjhSmKCQ374J7mn5e0Cryuvi5
21 | tYhwJn8rdwYZF/h2qqfEu8vaLoD09QIDAQABo4IBizCCAYcwHQYDVR0OBBYEFMmT
22 | /x412UH+5OHqgleeTjLOv6iHMIHRBgNVHSMEgckwgcaAFMmT/x412UH+5OHqglee
23 | TjLOv6iHoYGipIGfMIGcMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNV
24 | BAcTCVBhbG8gQWx0bzEfMB0GA1UEChMWVGluZm9pbCBTZWN1cml0eSwgSW5jLjEf
25 | MB0GA1UEAxMWVGluZm9pbCBTZWN1cml0eSwgSW5jLjEqMCgGCSqGSIb3DQEJARYb
26 | c3VwcG9ydEB0aW5mb2lsc2VjdXJpdHkuY29tggkAra7QuiMwI2AwDwYDVR0TAQH/
27 | BAUwAwEB/zARBglghkgBhvhCAQEEBAMCAQYwCQYDVR0SBAIwADArBglghkgBhvhC
28 | AQ0EHhYcVGlueUNBIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAmBgNVHREEHzAdgRtz
29 | dXBwb3J0QHRpbmZvaWxzZWN1cml0eS5jb20wDgYDVR0PAQH/BAQDAgEGMA0GCSqG
30 | SIb3DQEBBQUAA4ICAQAD7nsmdg1vStFTi8/P2rgSFxlXxZT0aaVVB1bFBe/m5phb
31 | MjvKQ7VAuiFZxEp3oBNdXTi4FzT1QjhRKdlYMgKZQnU+XDLLIYuoi+atxr5qGD4B
32 | m58eCGO6ZEutVs3Z7s63UOm5rG0zJ+IEWh8VHMvxgSwiX88QyJuhOtqeiKhIeSGZ
33 | 2/qGGMWgsScnPg3J/ZVOIKUn/4ljEDlC64Gh5Zz5PZUbGSXPMhdYbSD3EknDvEGA
34 | omYW4jlPMeK3GJgwAZu9yWC8hHGFpiMca/6W0W622cg7MX+CByOd+24dvWFnOHur
35 | NHBqI+kZo/7Sjdm8x7TWEOz9Rfh5RPMeVNRTj4iq0B6GzfaecT3Yn8y7HTRRiWns
36 | IYpP+iHCFYnZhDZsFi4ccKqxKtj6BGmhLf00FuNpgkvrsU3cXrhidkCaYGYj1SME
37 | 1CMfy0PPKVDpDKeFb6y0NvLf4d57vi99dZAvSJEO18rrNEHN2VUfCKRPA/mBSMLY
38 | RxKWAby1YVT/8iC9JWix9yvgsEUtTLyOFxLGtgj3PRiQSvbNe/jK4G9WAIFe6R9E
39 | 9+HUO2owcmyFXyU3rC/z/lBfDP+2pIRFdUVRGlYCMeUqR08PXpfva5+NQz21fC69
40 | FPRMZvXh70ntnFaWAq+j6NCss+AauC8ckECiQsTgbzJvJd6C3mJXYHkNCQODhg==
41 | -----END CERTIFICATE-----
42 |
--------------------------------------------------------------------------------
/lib/devise_two_factor/spec_helpers/two_factor_authenticatable_shared_examples.rb:
--------------------------------------------------------------------------------
1 | shared_examples 'two_factor_authenticatable' do
2 | before :each do
3 | subject.otp_secret = subject.class.generate_otp_secret
4 | end
5 |
6 | describe 'required_fields' do
7 | it 'should have the attr_encrypted fields for otp_secret' do
8 | Devise::Models::TwoFactorAuthenticatable.required_fields(subject.class).should =~ ([:encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt])
9 | end
10 | end
11 |
12 | describe '#otp_secret' do
13 | it 'should be of the configured length' do
14 | subject.otp_secret.length.should eq(subject.class.otp_secret_length)
15 | end
16 |
17 | it 'stores the encrypted otp_secret' do
18 | subject.encrypted_otp_secret.should_not be_nil
19 | end
20 |
21 | it 'stores an iv for otp_secret' do
22 | subject.encrypted_otp_secret_iv.should_not be_nil
23 | end
24 |
25 | it 'stores a salt for otp_secret' do
26 | subject.encrypted_otp_secret_salt.should_not be_nil
27 | end
28 | end
29 |
30 | describe '#valid_otp?' do
31 | let(:otp_secret) { '2z6hxkdwi3uvrnpn' }
32 |
33 | before :each do
34 | Timecop.freeze(Time.current)
35 | subject.otp_secret = otp_secret
36 | end
37 |
38 | after :each do
39 | Timecop.return
40 | end
41 |
42 | it 'validates a precisely correct OTP' do
43 | otp = ROTP::TOTP.new(otp_secret).at(Time.now)
44 | subject.valid_otp?(otp).should be_true
45 | end
46 |
47 | it 'validates an OTP within the allowed drift' do
48 | otp = ROTP::TOTP.new(otp_secret).at(Time.now + subject.class.otp_allowed_drift, true)
49 | subject.valid_otp?(otp).should be_true
50 | end
51 |
52 | it 'does not validate an OTP above the allowed drift' do
53 | otp = ROTP::TOTP.new(otp_secret).at(Time.now + subject.class.otp_allowed_drift * 2, true)
54 | subject.valid_otp?(otp).should be_false
55 | end
56 |
57 | it 'does not validate an OTP below the allowed drift' do
58 | otp = ROTP::TOTP.new(otp_secret).at(Time.now - subject.class.otp_allowed_drift * 2, true)
59 | subject.valid_otp?(otp).should be_false
60 | end
61 | end
62 |
63 | describe '#otp_provisioning_uri' do
64 | let(:otp_secret_length) { subject.class.otp_secret_length }
65 | let(:account) { Faker::Internet.email }
66 | let(:issuer) { "Tinfoil" }
67 |
68 | it "should return uri with specified account" do
69 | subject.otp_provisioning_uri(account).should match(%r{otpauth://totp/#{account}\?secret=\w{#{otp_secret_length}}})
70 | end
71 |
72 | it 'should return uri with issuer option' do
73 | subject.otp_provisioning_uri(account, issuer: issuer).should match(%r{otpauth://totp/#{account}\?issuer=#{issuer}&secret=\w{#{otp_secret_length}}$})
74 | end
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/lib/generators/devise_two_factor/devise_two_factor_generator.rb:
--------------------------------------------------------------------------------
1 | require 'rails/generators'
2 |
3 | module DeviseTwoFactor
4 | module Generators
5 | class DeviseTwoFactorGenerator < Rails::Generators::NamedBase
6 | argument :encryption_key_env, :type => :string, :required => true
7 |
8 | desc 'Creates a migration to add the required attributes to NAME, and ' \
9 | 'adds the necessary Devise directives to the model'
10 |
11 | def install_devise_two_factor
12 | create_devise_two_factor_migration
13 | inject_strategies_into_warden_config
14 | inject_devise_directives_into_model
15 | end
16 |
17 | private
18 |
19 | def create_devise_two_factor_migration
20 | migration_arguments = [
21 | "add_devise_two_factor_to_#{plural_name}",
22 | "encrypted_otp_secret:string",
23 | "encrypted_otp_secret_iv:string",
24 | "encrypted_otp_secret_salt:string",
25 | "otp_required_for_login:boolean"
26 | ]
27 |
28 | Rails::Generators.invoke('active_record:migration', migration_arguments)
29 | end
30 |
31 | def inject_strategies_into_warden_config
32 | config_path = File.join('config', 'initializers', 'devise.rb')
33 |
34 | content = " config.warden do |manager|\n" \
35 | " manager.default_strategies(:scope => :#{singular_table_name}).unshift :two_factor_authenticatable\n" \
36 | " end\n\n"
37 |
38 | inject_into_file(config_path, content, after: "Devise.setup do |config|\n")
39 | end
40 |
41 | def inject_devise_directives_into_model
42 | model_path = File.join('app', 'models', "#{file_path}.rb")
43 |
44 | class_path = if namespaced?
45 | class_name.to_s.split("::")
46 | else
47 | [class_name]
48 | end
49 |
50 | indent_depth = class_path.size
51 |
52 | content = [
53 | "devise :two_factor_authenticatable,",
54 | " :otp_secret_encryption_key => ENV['#{encryption_key_env}']\n"
55 | ]
56 |
57 | content << "attr_accessible :otp_attempt\n" if needs_attr_accessible?
58 | content = content.map { |line| " " * indent_depth + line }.join("\n") << "\n"
59 |
60 | inject_into_class(model_path, class_path.last, content)
61 |
62 | # Remove :database_authenticatable from the list of loaded models
63 | gsub_file(model_path, /(devise.*):(, )?database_authenticatable(, )?/, '\1\2')
64 | end
65 |
66 | def needs_attr_accessible?
67 | !strong_parameters_enabled? && mass_assignment_security_enabled?
68 | end
69 |
70 | def strong_parameters_enabled?
71 | defined?(ActionController::StrongParameters)
72 | end
73 |
74 | def mass_assignment_security_enabled?
75 | defined?(ActiveModel::MassAssignmentSecurity)
76 | end
77 | end
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/lib/devise_two_factor/spec_helpers/two_factor_backupable_shared_examples.rb:
--------------------------------------------------------------------------------
1 | shared_examples 'two_factor_backupable' do
2 | describe 'required_fields' do
3 | it 'has the attr_encrypted fields for otp_backup_codes' do
4 | Devise::Models::TwoFactorBackupable.required_fields(subject.class).should =~ [:otp_backup_codes]
5 | end
6 | end
7 |
8 | describe '#generate_otp_backup_codes!' do
9 | context 'with no existing recovery codes' do
10 | before do
11 | @plaintext_codes = subject.generate_otp_backup_codes!
12 | end
13 |
14 | it 'generates the correct number of new recovery codes' do
15 | subject.otp_backup_codes.length.should
16 | eq(subject.class.otp_number_of_backup_codes)
17 | end
18 |
19 | it 'generates recovery codes of the correct length' do
20 | @plaintext_codes.each do |code|
21 | code.length.should eq(subject.class.otp_backup_code_length)
22 | end
23 | end
24 |
25 | it 'generates distinct recovery codes' do
26 | @plaintext_codes.uniq.should =~ @plaintext_codes
27 | end
28 |
29 | it 'stores the codes as BCrypt hashes' do
30 | subject.otp_backup_codes.each do |code|
31 | # $algorithm$cost$(22 character salt + 31 character hash)
32 | code.should =~ /\A\$[0-9a-z]{2}\$[0-9]{2}\$[A-Za-z0-9\.\/]{53}\z/
33 | end
34 | end
35 | end
36 |
37 | context 'with existing recovery codes' do
38 | let(:old_codes) { Faker::Lorem.words }
39 | let(:old_codes_hashed) { old_codes.map { |x| Devise.bcrypt subject.class, x } }
40 |
41 | before do
42 | subject.otp_backup_codes = old_codes_hashed
43 | @plaintext_codes = subject.generate_otp_backup_codes!
44 | end
45 |
46 | it 'invalidates the existing recovery codes' do
47 | (subject.otp_backup_codes & old_codes_hashed).should =~ []
48 | end
49 | end
50 | end
51 |
52 | describe '#invalidate_otp_backup_code!' do
53 | before do
54 | @plaintext_codes = subject.generate_otp_backup_codes!
55 | end
56 |
57 | context 'given an invalid recovery code' do
58 | it 'returns false' do
59 | subject.invalidate_otp_backup_code!('password').should be_false
60 | end
61 | end
62 |
63 | context 'given a valid recovery code' do
64 | it 'returns true' do
65 | @plaintext_codes.each do |code|
66 | subject.invalidate_otp_backup_code!(code).should be_true
67 | end
68 | end
69 |
70 | it 'invalidates that recovery code' do
71 | code = @plaintext_codes.sample
72 |
73 | subject.invalidate_otp_backup_code!(code)
74 | subject.invalidate_otp_backup_code!(code).should be_false
75 | end
76 |
77 | it 'does not invalidate the other recovery codes' do
78 | code = @plaintext_codes.sample
79 | subject.invalidate_otp_backup_code!(code)
80 |
81 | @plaintext_codes.delete(code)
82 |
83 | @plaintext_codes.each do |code|
84 | subject.invalidate_otp_backup_code!(code).should be_true
85 | end
86 | end
87 | end
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/demo/config/database.yml:
--------------------------------------------------------------------------------
1 | # PostgreSQL. Versions 8.2 and up are supported.
2 | #
3 | # Install the pg driver:
4 | # gem install pg
5 | # On OS X with Homebrew:
6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config
7 | # On OS X with MacPorts:
8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
9 | # On Windows:
10 | # gem install pg
11 | # Choose the win32 build.
12 | # Install PostgreSQL and put its /bin directory on your path.
13 | #
14 | # Configure Using Gemfile
15 | # gem 'pg'
16 | #
17 | default: &default
18 | adapter: postgresql
19 | encoding: unicode
20 | # For details on connection pooling, see rails configuration guide
21 | # http://guides.rubyonrails.org/configuring.html#database-pooling
22 | pool: 5
23 |
24 | development:
25 | <<: *default
26 | database: DeviseTwoFactorDemo_development
27 |
28 | # The specified database role being used to connect to postgres.
29 | # To create additional roles in postgres see `$ createuser --help`.
30 | # When left blank, postgres will use the default role. This is
31 | # the same name as the operating system user that initialized the database.
32 | #username: DeviseTwoFactorDemo
33 |
34 | # The password associated with the postgres role (username).
35 | #password:
36 |
37 | # Connect on a TCP socket. Omitted by default since the client uses a
38 | # domain socket that doesn't need configuration. Windows does not have
39 | # domain sockets, so uncomment these lines.
40 | #host: localhost
41 |
42 | # The TCP port the server listens on. Defaults to 5432.
43 | # If your server runs on a different port number, change accordingly.
44 | #port: 5432
45 |
46 | # Schema search path. The server defaults to $user,public
47 | #schema_search_path: myapp,sharedapp,public
48 |
49 | # Minimum log levels, in increasing order:
50 | # debug5, debug4, debug3, debug2, debug1,
51 | # log, notice, warning, error, fatal, and panic
52 | # Defaults to warning.
53 | #min_messages: notice
54 |
55 | # Warning: The database defined as "test" will be erased and
56 | # re-generated from your development database when you run "rake".
57 | # Do not set this db to the same as development or production.
58 | test:
59 | <<: *default
60 | database: DeviseTwoFactorDemo_test
61 |
62 | # As with config/secrets.yml, you never want to store sensitive information,
63 | # like your database password, in your source code. If your source code is
64 | # ever seen by anyone, they now have access to your database.
65 | #
66 | # Instead, provide the password as a unix environment variable when you boot
67 | # the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
68 | # for a full rundown on how to provide these environment variables in a
69 | # production deployment.
70 | #
71 | # On Heroku and other platform providers, you may have a full connection URL
72 | # available as an environment variable. For example:
73 | #
74 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
75 | #
76 | # You can use this database configuration with:
77 | #
78 | # production:
79 | # url: <%= ENV['DATABASE_URL'] %>
80 | #
81 | production:
82 | <<: *default
83 | database: DeviseTwoFactorDemo_production
84 | username: DeviseTwoFactorDemo
85 | password: <%= ENV['DEVISETWOFACTORDEMO_DATABASE_PASSWORD'] %>
86 |
--------------------------------------------------------------------------------
/demo/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application
18 | # Add `rack-cache` to your Gemfile before enabling this.
19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid.
20 | # config.action_dispatch.rack_cache = true
21 |
22 | # Disable Rails's static asset server (Apache or nginx will already do this).
23 | config.serve_static_assets = false
24 |
25 | # Compress JavaScripts and CSS.
26 | config.assets.js_compressor = :uglifier
27 | # config.assets.css_compressor = :sass
28 |
29 | # Do not fallback to assets pipeline if a precompiled asset is missed.
30 | config.assets.compile = false
31 |
32 | # Generate digests for assets URLs.
33 | config.assets.digest = true
34 |
35 | # Version of your assets, change this if you want to expire all your assets.
36 | config.assets.version = '1.0'
37 |
38 | # Specifies the header that your server uses for sending files.
39 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
40 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
41 |
42 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
43 | # config.force_ssl = true
44 |
45 | # Set to :debug to see everything in the log.
46 | config.log_level = :info
47 |
48 | # Prepend all log lines with the following tags.
49 | # config.log_tags = [ :subdomain, :uuid ]
50 |
51 | # Use a different logger for distributed setups.
52 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
53 |
54 | # Use a different cache store in production.
55 | # config.cache_store = :mem_cache_store
56 |
57 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
58 | # config.action_controller.asset_host = "http://assets.example.com"
59 |
60 | # Precompile additional assets.
61 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
62 | # config.assets.precompile += %w( search.js )
63 |
64 | # Ignore bad email addresses and do not raise email delivery errors.
65 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
66 | # config.action_mailer.raise_delivery_errors = false
67 |
68 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
69 | # the I18n.default_locale when a translation cannot be found).
70 | config.i18n.fallbacks = true
71 |
72 | # Send deprecation notices to registered listeners.
73 | config.active_support.deprecation = :notify
74 |
75 | # Disable automatic flushing of the log to improve performance.
76 | # config.autoflush_log = false
77 |
78 | # Use default logging formatter so that PID and timestamp are not suppressed.
79 | config.log_formatter = ::Logger::Formatter.new
80 |
81 | # Do not dump schema after migrations.
82 | config.active_record.dump_schema_after_migration = false
83 | end
84 |
--------------------------------------------------------------------------------
/demo/config/locales/devise.en.yml:
--------------------------------------------------------------------------------
1 | # Additional translations at https://github.com/plataformatec/devise/wiki/I18n
2 |
3 | en:
4 | devise:
5 | confirmations:
6 | confirmed: "Your account was successfully confirmed."
7 | send_instructions: "You will receive an email with instructions about how to confirm your account in a few minutes."
8 | send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions about how to confirm your account in a few minutes."
9 | failure:
10 | already_authenticated: "You are already signed in."
11 | inactive: "Your account is not activated yet."
12 | invalid: "Invalid email or password."
13 | locked: "Your account is locked."
14 | last_attempt: "You have one more attempt before your account will be locked."
15 | not_found_in_database: "Invalid email or password."
16 | timeout: "Your session expired. Please sign in again to continue."
17 | unauthenticated: "You need to sign in or sign up before continuing."
18 | unconfirmed: "You have to confirm your account before continuing."
19 | mailer:
20 | confirmation_instructions:
21 | subject: "Confirmation instructions"
22 | reset_password_instructions:
23 | subject: "Reset password instructions"
24 | unlock_instructions:
25 | subject: "Unlock Instructions"
26 | omniauth_callbacks:
27 | failure: "Could not authenticate you from %{kind} because \"%{reason}\"."
28 | success: "Successfully authenticated from %{kind} account."
29 | passwords:
30 | no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
31 | send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."
32 | send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
33 | updated: "Your password was changed successfully. You are now signed in."
34 | updated_not_active: "Your password was changed successfully."
35 | registrations:
36 | destroyed: "Bye! Your account was successfully cancelled. We hope to see you again soon."
37 | signed_up: "Welcome! You have signed up successfully."
38 | signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated."
39 | signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked."
40 | signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please open the link to activate your account."
41 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and click on the confirm link to finalize confirming your new email address."
42 | updated: "You updated your account successfully."
43 | sessions:
44 | signed_in: "Signed in successfully."
45 | signed_out: "Signed out successfully."
46 | unlocks:
47 | send_instructions: "You will receive an email with instructions about how to unlock your account in a few minutes."
48 | send_paranoid_instructions: "If your account exists, you will receive an email with instructions about how to unlock it in a few minutes."
49 | unlocked: "Your account has been unlocked successfully. Please sign in to continue."
50 | errors:
51 | messages:
52 | already_confirmed: "was already confirmed, please try signing in"
53 | confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one"
54 | expired: "has expired, please request a new one"
55 | not_found: "not found"
56 | not_locked: "was not locked"
57 | not_saved:
58 | one: "1 error prohibited this %{resource} from being saved:"
59 | other: "%{count} errors prohibited this %{resource} from being saved:"
60 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Devise-Two-Factor Authentication
2 | By [Tinfoil Security](http://tinfoilsecurity.com/)
3 |
4 | [](https://travis-ci.org/tinfoil/devise-two-factor)
5 |
6 | Devise-two-factor is a minimalist extension to Devise which offers support for two-factor authentication. It:
7 |
8 | * Allows you to incorporate two-factor authentication into your existing models
9 | * Is opinionated about security, so you don't have to be
10 | * Integrates easily with two-factor applications like Google Authenticator and Authy
11 | * Is extensible, and includes two-factor backup codes as an example of how plugins can be structured
12 |
13 | ## Example App
14 | An example Rails 4 application is provided in demo/
15 |
16 | It showcases a minimal example of devise-two-factor in action, and can act as a reference for integrating the gem into your own application.
17 |
18 |
19 | ## Getting Started
20 | Devise-two-factor doesn't require much to get started, but there are a few prerequisites before you can start using it in your application.
21 |
22 | First, you'll need a Rails application setup with Devise. Visit the Devise [homepage](https://github.com/plataformatec/devise) for instructions.
23 |
24 | Next, since devise-two-factor encrypts its secrets before storing them in the database, you'll need to generate an encryption key, and store it in an environment variable of your choice.
25 |
26 | Finally, you can automate all of the required setup by simply running:
27 |
28 | ```ruby
29 | rails generate devise_two_factor MODEL ENVIRONMENT_VARIABLE
30 | ```
31 |
32 | Where MODEL is the name of the model you wish to add two-factor functionality to, and ENVIRONMENT_VARIABLE is the name of the variable you're storing your encryption key in.
33 |
34 | This generator will add a few columns to the specified model:
35 |
36 | * encrypted_otp_secret
37 | * encrypted_otp_secret_iv
38 | * encrypted_otp_secret_salt
39 | * otp_required_for_login
40 |
41 | It also adds the :two_factor_authenticatable directive to your model, and sets up your encryption key. If present, it will remove :database_authenticatable from the model, as the two strategies are incompatible. Lastly, the generator will add a Warden config block to your Devise initializer, which enables the strategies required for two-factor authenticatation.
42 |
43 | If you're running Rails 3, or do not have strong parameters enabled, the generator will also setup the required mass-assignment security options in your model.
44 |
45 | **After running the generator, verify that :database_authenticatable is not being loaded by your model. The generator will try to remove it, but if you have a non-standard Devise setup, this step may fail. Loading both :database_authenticatable and :two_factor_authenticatable in a model will allow users to bypass two-factor authenticatable due to the way Warden handles cascading strategies.**
46 |
47 | ## Designing Your Workflow
48 | Devise-two-factor only worries about the backend, leaving the details of the integration up to you. This means that you're responsible for building the UI that drives the gem. While there is an example Rails application included in the gem, it is important to remember that this gem is intentionally very open-ended, and you should build a user experience which fits your individual application.
49 |
50 | There are two key workflows you'll have to think about:
51 |
52 | 1. Logging in with two-factor authentication
53 | 2. Enabling two-factor authentication for a given user
54 |
55 | We chose to keep things as simple as possible, and our implemention can be found by registering at [Tinfoil Security](https://tinfoilsecurity.com/), and enabling two-factor authentication from the [security settings page](https://www.tinfoilsecurity.com/account/security).
56 |
57 |
58 | ### Logging In
59 | Logging in with two-factor authentication works extremely similarly to regular database authentication in Devise. The TwoFactorAuthenticatable strategy accepts three parameters:
60 |
61 | 1. email
62 | 2. password
63 | 3. otp_attempt (Their one-time password for this session)
64 |
65 | These parameters can be submitted to the standard Devise login route, and the strategy will handle the authentication of the user for you.
66 |
67 | ### Enabling Two-Factor Authentication
68 | Enabling two-factor authentication for a user is easy. For example, if my user model were named User, I could do the following:
69 |
70 | ```ruby
71 | current_user.otp_required_for_login = true
72 | current_user.otp_secret = User.generate_otp_secret
73 | current_user.save!
74 | ```
75 |
76 | Before you can do this however, you need to decide how you're going to transmit two-factor tokens to a user. Common strategies include sending an SMS, or using a mobile application such as Google Authenticator.
77 |
78 | At Tinfoil Security, we opted to use the excellent [rqrcode-rails3](https://github.com/samvincent/rqrcode-rails3) gem to generate a QR-code representing the user's secret key, which can then be scanned by any mobile two-factor authentication client.
79 |
80 | However you decide to handle enrollment, there are a few important considerations to be made:
81 |
82 | * Whether you'll force the use of two-factor authentication, and if so, how you'll migrate existing users to system, and what your onboarding experience will look like
83 | * If you authenticate using SMS, you'll want to verify the user's ownership of the phone, in much the same way you're probably verifying their email address
84 | * How you'll handle device revocation in the event that a user loses access to their device, or that device is rendered temporarily unavailable (This gem includes TwoFactorBackupable as an example extension meant to solve this problem)
85 |
86 | It sounds like a lot of work, but most of these problems have been very elegantly solved by other people. We recommend taking a look at the excellent workflows used by Heroku and Google for inspiration.
87 |
88 | ## Backup Codes
89 | Devise-two-factor is designed with extensibility in mind. One such extension, TwoFactorBackupable, is included and serves as a good example of how to extend this gem. This plugin allows you to add the ability to generate single-use backup codes for a user, which they may use to bypass two-factor authentication, in the event that they lose access to their device.
90 |
91 | To install it, you need to add the :two_factor_backupable directive to your model.
92 |
93 | ```ruby
94 | devise :two_factor_backupable
95 | ```
96 |
97 | You'll also be required to enable the :two_factor_backupable strategy, by adding the following line to your Warden config in your Devise initializer, substituting :user for the name of your Devise scope.
98 |
99 | ```ruby
100 | manager.default_strategies(:scope => :user).unshift :two_factor_backupable
101 | ```
102 |
103 | The final installation step is dependent on your version of Rails. If you're not running Rails 4, skip to the next section. Otherwise, create the following migration:
104 |
105 | ```ruby
106 | class AddDeviseTwoFactorBackupableToUsers < ActiveRecord::Migration
107 | def change
108 | add_column :users, :otp_backup_codes, :string, array: true
109 | end
110 | end
111 | ```
112 |
113 | You can then generate backup codes for a user:
114 |
115 | ```ruby
116 | codes = current_user.generate_otp_backup_codes!
117 | current_user.save!
118 | # Display codes to the user somehow!
119 | ```
120 |
121 | The backup codes are stored in the database as bcrypt hashes, so be sure to display them to the user at this point. If all went well, the user should be able to login using each of the generated codes in place of their two-factor token. Each code is single-use, and generating a new set of backup codes for that user will invalidate all of the old ones.
122 |
123 | You can customize the length of each code, and the number of codes generated by passing the options into `:two_factor_backupable` in the Devise directive:
124 |
125 | ```ruby
126 | devise :two_factor_backupable, otp_backup_code_length: 32,
127 | otp_number_of_backup_codes: 10
128 | ```
129 |
130 | ### Help! I'm not using Rails 4.0!
131 | Don't worry! TwoFactorBackupable stores the backup codes as an array of strings in the database. In Rails 4.0 this is supported natively, but in earlier versions you can use a gem to emulate this behaviour: we recommend [activerecord-postgres-array](https://github.com/tlconnor/activerecord-postgres-array).
132 |
133 | You'll then simply have to create a migration to add an array named `otp_backup_codes` to your model. If you use the above gem, this migration might look like:
134 |
135 | ```ruby
136 | class AddTwoFactorBackupCodesToUsers < ActiveRecord::Migration
137 | def change
138 | add_column :users, :otp_backup_codes, :string_array
139 | end
140 | end
141 | ```
142 |
143 | Now just continue with the setup in the previous section, skipping the generator step.
144 |
145 | ## Testing
146 | Devise-two-factor includes shared-examples for both TwoFactorAuthenticatable and TwoFactorBackupable. Adding the following two lines to the specs for your two-factor enabled models will allow you to test your models for two-factor functionality:
147 |
148 | ```ruby
149 | require 'devise_two_factor/spec_helpers'
150 |
151 | it_behaves_like "two_factor_authenticatable"
152 | it_behaves_like "two_factor_backupable"
153 | ```
154 |
--------------------------------------------------------------------------------