Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
56f099d
Bump version 1.1.5 -> 2.0.0
sbc100 Aug 26, 2016
ffbad70
Add compat with older versions of devise
gaurish Nov 8, 2016
348c2a1
[PR Feedback] Use Devise::Version over respond_to?
gaurish Nov 12, 2016
0f40c79
Merge pull request #107 from gaurish/master
Houdini Nov 14, 2016
41a01ac
Fix merge conflict
Dec 1, 2016
4cc5762
Add test for deleting cookie on logout
Dec 1, 2016
a97b3f6
Update readme
Dec 1, 2016
d1e1a66
Access config via Devise instead user.class
Dec 1, 2016
d0d42e5
Merge pull request #95 from sbc100/bump_version
Houdini Dec 2, 2016
d87b7b3
README typo fix[ci skip]
shaunakpp Feb 24, 2017
7d99a6c
Merge pull request #110 from shaunakpp/docs
Houdini Feb 27, 2017
a32f71a
Update two_factor_authenticatable.rb
apoyan Mar 1, 2017
40fb11b
Merge pull request #111 from apoyan/master
Houdini Mar 6, 2017
68f407c
Fix OpenSSL deprecation warning
Mar 14, 2017
57517e5
Add new encryption algorithm to Encryptor test
Mar 14, 2017
0d9bc8d
Check and use if newer bypass_sign_in method exists in devise
Mar 18, 2017
82fb6b1
Merge pull request #115 from pstaender/master
Houdini Mar 26, 2017
4419777
Switch badges to vector in README
rmm5t May 1, 2017
6cf659a
Merge pull request #120 from rmm5t/patch-1
Houdini May 1, 2017
96abae0
fix test in models/two_factor_authenticatable_spec.rb
Houdini May 11, 2017
2123660
Doc change re otp_secret_key for version 1 to 2 upgrade
edsimpson May 29, 2017
13ea083
Merge pull request #122 from edsimpson/otp-secret-key-not-needed-doc-…
Houdini May 30, 2017
20c9d14
Merge pull request #114 from newtrat/fix-test-and-deprecations
Houdini May 30, 2017
93674d2
dynamically generate path based on resource scope
jskirst Jul 5, 2017
38803d8
Makes encrypt/decrypt method names unique:
leanucci Jul 14, 2017
0df4c17
Merge pull request #126 from leanucci/encryption_methods_unique_names
Houdini Jul 18, 2017
9c1af25
Bump to version 2.0.1
Houdini Jul 18, 2017
d43c7d8
Merge pull request #125 from jskirst/scopeable-view
Houdini Jul 31, 2017
5865994
Update README.md
poctek Nov 3, 2017
f59001c
Merge pull request #130 from poctek/patch-3
Houdini Nov 6, 2017
9bb3e65
Merge pull request #109 from benjaminwols/delete-cookie-on-logout
Houdini Dec 2, 2017
8c37495
Fix README markdown formatting
rmm5t Jan 19, 2018
ea27bcd
Remove duplicate pry entry in Gemfile
rmm5t Jan 29, 2018
bf37c45
Enhance travis build matrix for supported Ruby and Rails versions
rmm5t Jan 29, 2018
ce054cd
Upgrade capybara for Rails 5 support
rmm5t Jan 29, 2018
f6b011d
Add Rails 5 support to specs
rmm5t Jan 29, 2018
5bf0aaf
Only run Ruby 2.2 against Rails 4
rmm5t Jan 29, 2018
6050115
Made Rails 5.1 the "default" version of rails in Gemfile
rmm5t Jan 29, 2018
f677f1f
Normalize migrations between Rails 4 and Rails 5
rmm5t Jan 29, 2018
91e8eb4
Delegate logic of send_new_otp to user#send_new_otp_after_login?
rmm5t Jan 29, 2018
6e24299
Merge pull request #141 from rmm5t/delegate-send-new-otp-logic-to-use…
Houdini Jan 30, 2018
a9e9093
Return JSON with 'redirect_to' when handle_failed_second_factor
Kevinrob Feb 8, 2018
0cad30d
Bump to version 2.1.0
Houdini Mar 29, 2018
f448cc7
Replace `render :nothing` with `head`
billkirtley Jun 7, 2018
8b3c424
Fix integer to seconds in remember_otp_session_for_seconds
Laykou Jul 10, 2018
8546040
Merge pull request #149 from Laykou/patch-1
Houdini Jul 10, 2018
e5aaf16
Bump to version 2.1.1
Houdini Jul 10, 2018
b57edd5
Load ActiveRecord functionality only if ActiveRecord itself is loaded
cesarizu Aug 27, 2018
05ee43c
Merge pull request #142 from rmm5t/improve-travis-matrix
Houdini Aug 27, 2018
dd2ccc8
Merge pull request #153 from GovSciences/active_record_fixes
Houdini Aug 27, 2018
5cb982a
Merge pull request #148 from actblue/rails_51
Houdini Aug 27, 2018
8887bab
Strip Spaces from TOTP Code
trianglegrrl Oct 25, 2018
972e455
Merge pull request #156 from PrecisionNutrition/remove-spaces-from-ot…
Houdini Nov 1, 2018
d462080
fix rotp 4 breaking authenticate totp
Nov 27, 2018
edcbc38
rotp 4.0.0 dependency
Nov 27, 2018
dbd14d6
Update README.md
Tectract Nov 30, 2018
3365885
default param padded removed
Jan 21, 2019
5fd39c1
16 to 32 lenght
Jan 21, 2019
afd2003
bundler 2 and rails 4.2 conflict
Jan 21, 2019
06bfb50
Update README.md
MarkFChavez Jan 26, 2019
1d6c978
Merge pull request #159 from resitcl/master
Houdini Jan 29, 2019
d6a050a
Merge pull request #165 from MarkFChavez/patch-1
Houdini Jan 29, 2019
81762fe
Merge pull request #160 from Tectract/master
Houdini Jan 29, 2019
dd72a24
update spec schema.rb
Houdini Jan 29, 2019
f904248
bump version to 2.2.0
Houdini Jan 29, 2019
153b016
Add german translations
JanBussieck Feb 1, 2019
90e31a4
Merge pull request #166 from JanBussieck/add_german_translations
Houdini Feb 7, 2019
859cec9
Merge pull request #139 from rmm5t/patch-1
Houdini Feb 7, 2019
8170ec0
Update README
Feb 21, 2019
cab2aaf
Merge pull request #168 from MarkFChavez/master
Houdini Apr 19, 2019
7d49400
Merge pull request #143 from Kevinrob/patch-1
Houdini May 23, 2019
8f9377b
Use ROTP::Base32.random instead of ROTP::Base32.random_base32 if avai…
Jun 14, 2019
20544c7
Merge pull request #171 from jaspervandenberg/newer_rotp_support
Houdini Jul 4, 2019
82e5aff
Additional comments in generate_totp_secret method
Houdini Jul 4, 2019
2d3650d
Autofocus MFA code text field
gustavokitman Jul 23, 2019
2229ead
Merge pull request #174 from gustavokitman/patch-1
Houdini Jul 25, 2019
c21a935
Make current migration safe using index concurrently
Sep 17, 2020
7e0faa6
Merge pull request #197 from Lackoftactics/index_concurrently
Houdini Sep 18, 2020
a0b2ed0
Added license tag (MIT) to gemspec
alessio-signorini Feb 25, 2021
c68d9ea
Merge pull request #199 from alessio-signorini/patch-1
Houdini Mar 12, 2021
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
21 changes: 10 additions & 11 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
language: ruby

env:
- "RAILS_VERSION=4.0"
- "RAILS_VERSION=4.1"
- "RAILS_VERSION=4.2"
- "RAILS_VERSION=5.2"
- "RAILS_VERSION=master"

rvm:
- 2.1
- 2.2
- 2.3.1
- 2.3.8
- 2.4.5
- 2.5.3

matrix:
fast_finish: true
allow_failures:
- env: "RAILS_VERSION=master"
exclude:
- rvm: 2.1
env: RAILS_VERSION=master
include:
- rvm: 2.2
env: RAILS_VERSION=master
env: RAILS_VERSION=4.2

before_install:
- gem update bundler
- gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true
- gem install bundler -v '< 2'

before_script:
- bundle exec rake app:db:migrate
- bundle exec rake app:db:setup

script: bundle exec rake spec
3 changes: 1 addition & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ rails = case rails_version
when "master"
{github: "rails/rails"}
when "default"
"~> 4.1"
"~> 5.2"
else
"~> #{rails_version}"
end
Expand All @@ -27,5 +27,4 @@ end
group :test do
gem 'rack_session_access'
gem 'ammeter'
gem 'pry'
end
210 changes: 177 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Houdini/two_factor_authentication?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

[![Build Status](https://travis-ci.org/Houdini/two_factor_authentication.svg?branch=master)](https://travis-ci.org/Houdini/two_factor_authentication)
[![Code Climate](https://codeclimate.com/github/Houdini/two_factor_authentication.png)](https://codeclimate.com/github/Houdini/two_factor_authentication)
[![Code Climate](https://codeclimate.com/github/Houdini/two_factor_authentication.svg)](https://codeclimate.com/github/Houdini/two_factor_authentication)

## Features

* Support for 2 types of OTP codes
1. Codes delivered directly to the user
2. TOTP (Google Authenticator) codes based on a shared secret (HMAC)
1. Codes delivered directly to the user
2. TOTP (Google Authenticator) codes based on a shared secret (HMAC)
* Configurable OTP code digit length
* Configurable max login attempts
* Customizable logic to determine if a user needs two factor authentication
Expand Down Expand Up @@ -54,7 +54,7 @@ migration in `db/migrate/`, which will add the following columns to your table:
#### Manual initial setup

If you prefer to set up the model and migration manually, add the
`:two_factor_authentication` option to your existing devise options, such as:
`:two_factor_authenticatable` option to your existing devise options, such as:

```ruby
devise :database_authenticatable, :registerable, :recoverable, :rememberable,
Expand Down Expand Up @@ -97,6 +97,7 @@ config.direct_otp_length = 6 # Direct OTP code length
config.remember_otp_session_for_seconds = 30.days # Time before browser has to perform 2fA again. Default is 0.
config.otp_secret_encryption_key = ENV['OTP_SECRET_ENCRYPTION_KEY']
config.second_factor_resource_id = 'id' # Field or method name used to set value for 2fA remember cookie
config.delete_cookie_on_logout = false # Delete cookie when user signs out, to force 2fA again on login
```
The `otp_secret_encryption_key` must be a random key that is not stored in the
DB, and is not checked in to your repo. It is recommended to store it in an
Expand Down Expand Up @@ -161,25 +162,32 @@ Below is an example using ERB:
<% end %>

<%= link_to "Sign out", destroy_user_session_path, :method => :delete %>

```

#### Enable TOTP support for existing users
#### Upgrading from version 1.X to 2.X

The following database fields are new in version 2.

- `direct_otp`
- `direct_otp_sent_at`
- `totp_timestamp`

If you have existing users that need to be provided with a OTP secret key, so
they can use TOTP, create a rake task. It could look like this one below:
To add them, generate a migration such as:

$ rails g migration AddTwoFactorFieldsToUsers direct_otp:string direct_otp_sent_at:datetime totp_timestamp:timestamp

The `otp_secret_key` is only required for users who use TOTP (Google Authenticator) codes,
so unless it has been shared with the user it should be set to `nil`. The
following pseudo-code is an example of how this might be done:

```ruby
desc 'rake task to update users with otp secret key'
task :update_users_with_otp_secret_key => :environment do
User.find_each do |user|
user.generate_totp_secret
User.find_each do |user| do
if !uses_authenticator_app(user)
user.otp_secret_key = nil
user.save!
puts "Rake[:update_users_with_otp_secret_key] => OTP secret key set to '#{key}' for User '#{user.email}'"
end
end
```
Then run the task with `bundle exec rake update_users_with_otp_secret_key`

#### Adding the TOTP encryption option to an existing app

Expand Down Expand Up @@ -217,25 +225,25 @@ steps:
Open the generated file, and replace its contents with the following:
```ruby
class PopulateEncryptedOtpFields < ActiveRecord::Migration
def up
User.reset_column_information

User.find_each do |user|
user.otp_secret_key = user.read_attribute('otp_secret_key')
user.save!
end
end

def down
User.reset_column_information

User.find_each do |user|
user.otp_secret_key = ROTP::Base32.random_base32
user.save!
end
end
end
```
def up
User.reset_column_information

User.find_each do |user|
user.otp_secret_key = user.read_attribute('otp_secret_key')
user.save!
end
end

def down
User.reset_column_information

User.find_each do |user|
user.otp_secret_key = ROTP::Base32.random_base32
user.save!
end
end
end
```

5. Generate a migration to remove the `:otp_secret_key` column:
```
Expand All @@ -255,6 +263,142 @@ after them):
bundle exec rake db:rollback STEP=3
```

#### Critical Security Note! Add before_action to your user registration controllers

You should have a file registrations_controller.rb in your controllers folder
to overwrite/customize user registrations. It should include the lines below, for 2FA protection of user model updates, meaning that users can only access the users/edit page after confirming 2FA fully, not simply by logging in. Otherwise the entire 2FA system can be bypassed!

```ruby
class RegistrationsController < Devise::RegistrationsController
before_action :confirm_two_factor_authenticated, except: [:new, :create, :cancel]

protected

def confirm_two_factor_authenticated
return if is_fully_authenticated?

flash[:error] = t('devise.errors.messages.user_not_authenticated')
redirect_to user_two_factor_authentication_url
end
end
```

#### Critical Security Note! Add 2FA validation to your custom user actions

Make sure you are passing the 2FA secret codes securely and checking for them upon critical user actions, such as API key updates, user email or pgp pubkey updates, or any other changess to private/secure account-related details. Validate the secret during the initial 2FA key/secret verification by the user also, of course.

For example, a simple account_controller.rb may look something like this:

```
require 'json'

class AccountController < ApplicationController
before_action :require_signed_in!
before_action :authenticate_user!
respond_to :html, :json

def account_API
resp = {}
begin
if(account_params["twoFAKey"] && account_params["twoFASecret"])
current_user.otp_secret_key = account_params["twoFAKey"]
if(current_user.authenticate_totp(account_params["twoFASecret"]))
# user has validated their temporary 2FA code, save it to their account, enable 2FA on this account
current_user.save!
resp['success'] = "passed 2FA validation!"
else
resp['error'] = "failed 2FA validation!"
end
elsif(param[:userAccountStuff] && param[:userAccountWidget])
#before updating important user account stuff and widgets,
#check to see that the 2FA secret has also been passed in, and verify it...
if(account_params["twoFASecret"] && current_user.totp_enabled? && current_user.authenticate_totp(account_params["twoFASecret"]))
# user has passed 2FA checks, do cool user account stuff here
...
else
# user failed 2FA check! No cool user stuff happens!
resp[error] = 'You failed 2FA validation!'
end

...
end
else
resp['error'] = 'unknown format error, not saved!'
end
rescue Exception => e
puts "WARNING: account api threw error : '#{e}' for user #{current_user.username}"
#print "error trace: #{e.backtrace}\n"
resp['error'] = "unanticipated server response"
end
render json: resp.to_json
end

def account_params
params.require(:twoFA).permit(:userAccountStuff, :userAcountWidget, :twoFAKey, :twoFASecret)
end
end
```


### Example App

[TwoFactorAuthenticationExample](https://github.com/Houdini/TwoFactorAuthenticationExample)


### Example user actions

to use an ENV VAR for the 2FA encryption key:

config.otp_secret_encryption_key = ENV['OTP_SECRET_ENCRYPTION_KEY']

to set up TOTP for Google Authenticator for user:

```
current_user.otp_secret_key = current_user.generate_totp_secret
current_user.save!
```

( encrypted db fields are set upon user model save action,
rails c access relies on setting env var: OTP_SECRET_ENCRYPTION_KEY )

to check if user has input the correct code (from the QR display page)
before saving the user model:

```
current_user.authenticate_totp('123456')
```

additional note:

```
current_user.otp_secret_key
```

This returns the OTP secret key in plaintext for the user (if you have set the env var) in the console
the string used for generating the QR given to the user for their Google Auth is something like:

otpauth://totp/LABEL?secret=p6wwetjnkjnrcmpd (example secret used here)

where LABEL should be something like "example.com (Username)", which shows up in their GA app to remind them the code is for example.com

this returns true or false with an allowed_otp_drift_seconds 'grace period'

to set TOTP to DISABLED for a user account:

```
current_user.second_factor_attempts_count=nil
current_user.encrypted_otp_secret_key=nil
current_user.encrypted_otp_secret_key_iv=nil
current_user.encrypted_otp_secret_key_salt=nil
current_user.direct_otp=nil
current_user.direct_otp_sent_at=nil
current_user.totp_timestamp=nil
current_user.direct_otp=nil
current_user.otp_secret_key=nil
current_user.save! (if in ruby code instead of console)
current_user.direct_otp? => false
current_user.totp_enabled? => false
```



12 changes: 10 additions & 2 deletions app/controllers/devise/two_factor_authentication_controller.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'devise/version'

class Devise::TwoFactorAuthenticationController < DeviseController
prepend_before_action :authenticate_scope!
before_action :prepare_and_validate, :handle_two_factor_authentication
Expand Down Expand Up @@ -26,7 +28,13 @@ def after_two_factor_success_for(resource)
set_remember_two_factor_cookie(resource)

warden.session(resource_name)[TwoFactorAuthentication::NEED_AUTHENTICATION] = false
bypass_sign_in(resource, scope: resource_name)
# For compatability with devise versions below v4.2.0
# https://github.com/plataformatec/devise/commit/2044fffa25d781fcbaf090e7728b48b65c854ccb
if respond_to?(:bypass_sign_in)
bypass_sign_in(resource, scope: resource_name)
else
sign_in(resource_name, resource, bypass: true)
end
set_flash_message :notice, :success
resource.update_attribute(:second_factor_attempts_count, 0)

Expand All @@ -39,7 +47,7 @@ def set_remember_two_factor_cookie(resource)
if expires_seconds && expires_seconds > 0
cookies.signed[TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME] = {
value: "#{resource.class}-#{resource.public_send(Devise.second_factor_resource_id)}",
expires: expires_seconds.from_now
expires: expires_seconds.seconds.from_now
}
end
end
Expand Down
8 changes: 4 additions & 4 deletions app/views/devise/two_factor_authentication/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
<p><%= flash[:notice] %></p>

<%= form_tag([resource_name, :two_factor_authentication], :method => :put) do %>
<%= text_field_tag :code %>
<%= text_field_tag :code, '', autofocus: true %>
<%= submit_tag "Submit" %>
<% end %>

<% if resource.direct_otp %>
<%= link_to "Resend Code", resend_code_user_two_factor_authentication_path, action: :get %>
<%= link_to "Resend Code", send("resend_code_#{resource_name}_two_factor_authentication_path"), action: :get %>
<% else %>
<%= link_to "Send me a code instead", resend_code_user_two_factor_authentication_path, action: :get %>
<%= link_to "Send me a code instead", send("resend_code_#{resource_name}_two_factor_authentication_path"), action: :get %>
<% end %>
<%= link_to "Sign out", destroy_user_session_path, :method => :delete %>
<%= link_to "Sign out", send("destroy_#{resource_name}_session_path"), :method => :delete %>
8 changes: 8 additions & 0 deletions config/locales/de.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
de:
devise:
two_factor_authentication:
success: "Ihre Zwei-Faktor-Authentifizierung war erfolgreich."
attempt_failed: "Authentifizierungsversuch fehlgeschlagen."
max_login_attempts_reached: "Ihr Zugang wurde ganz verweigert, da Sie Ihr Versuchslimit erreicht haben."
contact_administrator: "Kontaktieren Sie bitte einen Ihrer Administratoren."
code_has_been_sent: "Ihr Einmal-Passwort wurde verschickt."
4 changes: 3 additions & 1 deletion lib/generators/active_record/templates/migration.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class TwoFactorAuthenticationAddTo<%= table_name.camelize %> < ActiveRecord::Migration
disable_ddl_transaction!

def change
add_column :<%= table_name %>, :second_factor_attempts_count, :integer, default: 0
add_column :<%= table_name %>, :encrypted_otp_secret_key, :string
Expand All @@ -8,6 +10,6 @@ def change
add_column :<%= table_name %>, :direct_otp_sent_at, :datetime
add_column :<%= table_name %>, :totp_timestamp, :timestamp

add_index :<%= table_name %>, :encrypted_otp_secret_key, unique: true
add_index :<%= table_name %>, :encrypted_otp_secret_key, unique: true, algorithm: :concurrently
end
end
Loading