Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ rails = case rails_version

gem "rails", rails

gem "devise", ENV["DEVISE_VERSION"] if ENV["DEVISE_VERSION"]
gem "rotp", ENV["ROTP_VERSION"] if ENV["ROTP_VERSION"]

if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.2.0')
gem "test-unit", "~> 3.0"
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def authenticate_totp(code, options = {})
drift_ahead: drift, drift_behind: drift, after: totp_timestamp
)
return false unless new_timestamp
self.totp_timestamp = new_timestamp
self.totp_timestamp = Time.at(new_timestamp).utc
true
end

Expand Down Expand Up @@ -98,7 +98,7 @@ def generate_totp_secret
def create_direct_otp(options = {})
# Create a new random OTP and store it in the database
digits = options[:length] || self.class.direct_otp_length || 6
update_attributes(
update(
direct_otp: random_base10(digits),
direct_otp_sent_at: Time.now.utc
)
Expand All @@ -119,7 +119,7 @@ def direct_otp_expired?
end

def clear_direct_otp
update_attributes(direct_otp: nil, direct_otp_sent_at: nil)
update(direct_otp: nil, direct_otp_sent_at: nil)
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/two_factor_authentication/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ class Mapper
protected

def devise_two_factor_authentication(mapping, controllers)
resource :two_factor_authentication, :only => [:show, :update, :resend_code], :path => mapping.path_names[:two_factor_authentication], :controller => controllers[:two_factor_authentication] do
resource :two_factor_authentication, :only => [:show, :update], :path => mapping.path_names[:two_factor_authentication], :controller => controllers[:two_factor_authentication] do
collection { get "resend_code" }
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
describe 'the migration' do
subject { migration_file('db/migrate/two_factor_authentication_add_to_users.rb') }

it { is_expected.to exist }
it { is_expected.to satisfy { |path| File.exist?(path) } }
it { is_expected.to be_a_migration }
it { is_expected.to contain /def change/ }
it { is_expected.to contain /add_column :users, :second_factor_attempts_count, :integer, default: 0/ }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,41 @@ def do_invoke(code, user)

it_behaves_like 'authenticate_totp', GuestUser.new
it_behaves_like 'authenticate_totp', EncryptedUser.new

it 'keeps a legacy encrypted secret and replay protection across persistence' do
allow(Devise).to receive(:otp_secret_encryption_key).and_return('a' * 32)
ActiveRecord::Migration.suppress_messages do
ActiveRecord::Schema.define do
create_table :persisted_encrypted_users, force: :cascade do |table|
table.string :encrypted_otp_secret_key
table.string :encrypted_otp_secret_key_iv
table.string :encrypted_otp_secret_key_salt
table.timestamp :totp_timestamp
end
end
end

user_class = Class.new(ActiveRecord::Base) do
self.table_name = 'persisted_encrypted_users'
include Devise::Models::TwoFactorAuthenticatable
has_one_time_password encrypted: true
end
user = user_class.create!(
encrypted_otp_secret_key: "qqtceBScHArOXNFRTZfNyDih+kzYDujh7emlkGi4V6A=\n",
encrypted_otp_secret_key_iv: "ezgScHq7FcShFtQ2WYPP2g==\n",
encrypted_otp_secret_key_salt: "_NemuOAzhuv7qvoPP3RVyBA==\n"
)
secret = 'JBSWY3DPEHPK3PXP'
code = TotpHelper.new(secret, user.class.otp_length).totp_code

expect(user.otp_secret_key).to eq(secret)
expect(user.authenticate_totp(code)).to eq(true)
user.save!

user.reload
expect(user.totp_timestamp).to be_a(Time)
expect(user.authenticate_totp(code)).to eq(false)
end
end

describe '#send_two_factor_authentication_code' do
Expand Down Expand Up @@ -137,8 +172,11 @@ def instance.send_two_factor_authentication_code(code)
end

it "returns uri with user's email" do
expect(instance.provisioning_uri).
to match(%r{otpauth://totp/houdini@example.com\?secret=\w{32}})
uri = URI.parse(instance.provisioning_uri)
params = URI.decode_www_form(uri.query).to_h

expect(URI.decode_www_form_component(uri.path)).to eq('/houdini@example.com')
expect(params['secret']).to match(/\w{32}/)
end

it 'returns uri with issuer option' do
Expand Down
10 changes: 10 additions & 0 deletions spec/lib/two_factor_authentication/routes_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
require 'spec_helper'

describe 'two-factor authentication routes', type: :routing do
it 'routes resend_code through the collection endpoint' do
expect(get: '/users/two_factor_authentication/resend_code').to route_to(
controller: 'devise/two_factor_authentication',
action: 'resend_code'
)
end
end
2 changes: 1 addition & 1 deletion spec/rails_app/app/models/guest_user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class GuestUser
attr_accessor :direct_otp, :direct_otp_sent_at, :otp_secret_key, :email,
:second_factor_attempts_count, :totp_timestamp

def update_attributes(attrs)
def update(attrs)
attrs.each do |key, value|
send(key.to_s + '=', value)
end
Expand Down
8 changes: 0 additions & 8 deletions spec/rails_app/config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
require "active_record/railtie"
require "action_controller/railtie"
require "action_mailer/railtie"
require "sprockets/railtie"

Bundler.require(*Rails.groups)
require "two_factor_authentication"
Expand Down Expand Up @@ -47,17 +46,10 @@ class Application < Rails::Application
# like if you have constraints or database-specific column types
# config.active_record.schema_format = :sql

# Enable the asset pipeline
config.assets.enabled = true

# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'

config.action_mailer.default_url_options = { host: 'localhost:3000' }

config.i18n.enforce_available_locales = false

config.secret_key_base = 'secretvalue'
end
end

5 changes: 0 additions & 5 deletions spec/rails_app/config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,4 @@
# Only use best-standards-support built into browsers
config.action_dispatch.best_standards_support = :builtin

# Do not compress assets
config.assets.compress = false

# Expands the lines which load the assets
config.assets.debug = true
end
12 changes: 0 additions & 12 deletions spec/rails_app/config/environments/production.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,6 @@
config.consider_all_requests_local = false
config.action_controller.perform_caching = true

# Disable Rails's static asset server (Apache or nginx will already do this)
config.serve_static_assets = false

# Compress JavaScripts and CSS
config.assets.compress = true

# Don't fallback to assets pipeline if a precompiled asset is missed
config.assets.compile = false

# Generate digests for assets URLs
config.assets.digest = true

# Defaults to nil and saved in location specified by config.assets.prefix
# config.assets.manifest = YOUR_PATH

Expand Down
4 changes: 4 additions & 0 deletions spec/rails_app/config/initializers/inflections.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@
# ActiveSupport::Inflector.inflections do |inflect|
# inflect.acronym 'RESTful'
# end

ActiveSupport::Inflector.inflections do |inflect|
inflect.acronym 'SMS'
end
2 changes: 1 addition & 1 deletion two_factor_authentication.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Gem::Specification.new do |s|
s.add_development_dependency 'bundler'
s.add_development_dependency 'rake'
s.add_development_dependency 'rspec-rails', '>= 3.0.1'
s.add_development_dependency 'capybara', '~> 2.5'
s.add_development_dependency 'capybara', '>= 2.5', '< 4'
s.add_development_dependency 'pry'
s.add_development_dependency 'timecop'
end