Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature enable otp #201

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
cbc4802
Update to new ruby syntax and styleguide
Nov 9, 2016
8e5eff3
Update for style guides
Nov 9, 2016
6ead3f0
Add new and create method to add a totp code via QR
Nov 9, 2016
20a4348
Add issuer to config to set issuer via provisioning_uri
Nov 9, 2016
de4e6a1
Add db column for otp enabled status
Nov 10, 2016
a4df525
Change routing by adding verify path and using edit/update to disable…
Nov 10, 2016
c2ab97a
Update creating and updating otp on user
Nov 10, 2016
6a7ec62
Add routing to new_user_two_factor_authentication if otp is not enabl…
Nov 10, 2016
5bb1c3f
Rename set_qr method and remove unused settings
Nov 10, 2016
6cbcf54
Add enable, confirm and disable methods
Nov 10, 2016
f3517e3
Updat readme for new functionalities
Nov 10, 2016
4b71f78
Add view generator and documentate in readme
Nov 10, 2016
7522c8b
Update test for new database field
Nov 10, 2016
157668e
Remove issuer option
Nov 10, 2016
3bdcddd
Update controller spec for verify route instead of update
Nov 10, 2016
f376e55
Fix test for added otp_enabled attribute
Nov 10, 2016
a228c03
Add feature and controller tests
Nov 10, 2016
497f0c3
Refactor routes resources
Nov 11, 2016
f498782
Fix test for new otp_enabled field
Nov 30, 2016
de509bc
Add notice for successful disable of tfa
Nov 30, 2016
db9cb88
Save changes after enabling and disabling otp
Nov 30, 2016
f6e182d
Remove commented code
Nov 30, 2016
965a07b
Fix merge conflict
Nov 30, 2016
3c4ddc9
Merge branch 'Houdini-master' into feature-enable-otp
Nov 30, 2016
17bd00a
Update edit view
Dec 1, 2016
ee49a93
Merge branch 'master' into feature-enable-otp
BookOfGreg Mar 29, 2021
24fe7d9
Update readme
BookOfGreg Mar 29, 2021
30bce03
Remigrate the DB
BookOfGreg Mar 29, 2021
00b273d
Support rails migration versions
BookOfGreg Mar 29, 2021
e935492
Fixes #169 rotp returning url encoded at
BookOfGreg Mar 29, 2021
fff94dc
Merge branch 'fix-169-rotp-v5' into feature-enable-otp
BookOfGreg Mar 29, 2021
e6844bc
Refactor to upgrade specs to rails 4 and 5 compatibility
BookOfGreg Mar 29, 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
2 changes: 2 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
--color
--format documentation
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ gem "rails", rails
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.2.0')
gem "test-unit", "~> 3.0"
end

gem 'sprockets-rails', '~> 2.0'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sprockets 3 requires a manifest, pegged sprockets to 2 for duration of this branch so the manifest can be fixed separately.

group :test, :development do
gem 'sqlite3'
end
Expand Down
91 changes: 49 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
## 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)
* Option to enable or disable otp
* Configurable OTP code digit length
* Configurable max login attempts
* Customizable logic to determine if a user needs two factor authentication
Expand Down Expand Up @@ -43,6 +44,7 @@ Where MODEL is your model name (e.g. User or Admin). This generator will add
`:two_factor_authenticatable` to your model's Devise options and create a
migration in `db/migrate/`, which will add the following columns to your table:

- `:otp_enabled`
- `:second_factor_attempts_count`
- `:encrypted_otp_secret_key`
- `:encrypted_otp_secret_key_iv`
Expand All @@ -63,15 +65,16 @@ devise :database_authenticatable, :registerable, :recoverable, :rememberable,

Then create your migration file using the Rails generator, such as:

```
rails g migration AddTwoFactorFieldsToUsers second_factor_attempts_count:integer encrypted_otp_secret_key:string:index encrypted_otp_secret_key_iv:string encrypted_otp_secret_key_salt:string direct_otp:string direct_otp_sent_at:datetime totp_timestamp:timestamp
```bash
rails g migration AddTwoFactorFieldsToUsers otp_enabled:boolean second_factor_attempts_count:integer encrypted_otp_secret_key:string:index encrypted_otp_secret_key_iv:string encrypted_otp_secret_key_salt:string direct_otp:string direct_otp_sent_at:datetime totp_timestamp:timestamp
```

Open your migration file (it will be in the `db/migrate` directory and will be
named something like `20151230163930_add_two_factor_fields_to_users.rb`), and
add `unique: true` to the `add_index` line so that it looks like this:
add `unique: true` to the `add_index` line and add default: false to otp_enabled, so that it looks like this:

```ruby
add_column :users, :otp_enabled, :boolean, default: false
add_index :users, :encrypted_otp_secret_key, unique: true
```
Save the file.
Expand Down Expand Up @@ -103,16 +106,22 @@ 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
environment variable, and you can generate it with `bundle exec rake secret`.

Override the method in your model in order to send direct OTP codes. This is
automatically called when a user logs in unless they have TOTP enabled (see
below):
#### Enabling two factor authentication
By default when users login and otp is not enabled, the user is asked to enable two factor authentication.

The user has the option to choose between using the app (for example [Google Authenticator](https://support.google.com/accounts/answer/1066447?hl=en)), or receiving direct OTP codes.

Override the method in your model in order to send direct OTP codes:

```ruby
def send_two_factor_authentication_code(code)
# Send code via SMS, etc.
end
```

Once the user has confirmed by entering the code from his app or direct code,
two factor authentication is enabled.

### Customisation and Usage

By default, second factor authentication is required for each user. You can
Expand All @@ -127,36 +136,27 @@ end
In the example above, two factor authentication will not be required for local
users.

This gem is compatible with [Google Authenticator](https://support.google.com/accounts/answer/1066447?hl=en).
To enable this a shared secret must be generated by invoking the following
method on your model:
#### Overriding the views

```ruby
user.generate_totp_secret
```

This must then be shared via a provisioning uri:
The default views that show the forms can be overridden by adding the following files
in either ERB or haml:

```ruby
user.provisioning_uri # This assumes a user model with an email attribute
```
- `new.html.erb` Enabling two factor authentication
- `edit.html.erb` Disabling two factor authentication
- `show.html.erb` Verifying OTP code after login

This provisioning uri can then be turned in to a QR code if desired so that
users may add the app to Google Authenticator easily. Once this is done, they
may retrieve a one-time password directly from the Google Authenticator app.
inside `app/views/devise/two_factor_authentication/` and customizing it.

#### Overriding the view
Or you can use the generator:

The default view that shows the form can be overridden by adding a
file named `show.html.erb` (or `show.html.haml` if you prefer HAML)
inside `app/views/devise/two_factor_authentication/` and customizing it.
Below is an example using ERB:
`bundle exec rails g two_factor_authentication:views`

Below is an example for show using ERB:

```html
<h2>Hi, you received a code by email, please enter it below, thanks!</h2>

<%= form_tag([resource_name, :two_factor_authentication], :method => :put) do %>
<%= form_tag(verify_user_two_factor_authentication_path, method: :put) do %>
<%= text_field_tag :code %>
<%= submit_tag "Log in!" %>
<% end %>
Expand Down Expand Up @@ -189,6 +189,16 @@ User.find_each do |user| do
end
```

#### Disable OTP by users

If you want to give the users to option to disable OTP, you must add a route to edit_#{scope}_two_factor_authentication_path. In this view the user has to confirm with a code to disable his two factor authentication.

#### Filtering sensitive parameters from the logs

To prevent two-factor authentication codes from leaking if your application logs get breached, you'll want to filter sensitive parameters from the Rails logs. Add the following to config/initializers/filter_parameter_logging.rb:

Rails.application.config.filter_parameters += [:totp_secret]

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

If you've already been using this gem, and want to start encrypting the OTP
Expand All @@ -197,7 +207,7 @@ steps:

1. Generate a migration to add the necessary columns to your model's table:

```
```bash
rails g migration AddEncryptionFieldsToUsers encrypted_otp_secret_key:string:index encrypted_otp_secret_key_iv:string encrypted_otp_secret_key_salt:string
```

Expand All @@ -218,7 +228,7 @@ steps:
For example: `has_one_time_password(encrypted: true)`

4. Generate a migration to populate the new encryption fields:
```
```bash
rails g migration PopulateEncryptedOtpFields
```

Expand Down Expand Up @@ -246,7 +256,7 @@ steps:
```

5. Generate a migration to remove the `:otp_secret_key` column:
```
```bash
rails g migration RemoveOtpSecretKeyFromUsers otp_secret_key:string
```

Expand All @@ -259,7 +269,7 @@ use these steps:

2. Roll back the last 3 migrations (assuming you haven't added any new ones
after them):
```
```bash
bundle exec rake db:rollback STEP=3
```

Expand Down Expand Up @@ -289,7 +299,7 @@ Make sure you are passing the 2FA secret codes securely and checking for them up

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

```
```ruby
require 'json'

class AccountController < ApplicationController
Expand Down Expand Up @@ -353,7 +363,7 @@ config.otp_secret_encryption_key = ENV['OTP_SECRET_ENCRYPTION_KEY']

to set up TOTP for Google Authenticator for user:

```
```ruby
current_user.otp_secret_key = current_user.generate_totp_secret
current_user.save!
```
Expand All @@ -364,16 +374,16 @@ 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:

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

additional note:
```

```ruby
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:

Expand All @@ -385,7 +395,7 @@ this returns true or false with an allowed_otp_drift_seconds 'grace period'

to set TOTP to DISABLED for a user account:

```
```ruby
current_user.second_factor_attempts_count=nil
current_user.encrypted_otp_secret_key=nil
current_user.encrypted_otp_secret_key_iv=nil
Expand All @@ -399,6 +409,3 @@ to set TOTP to DISABLED for a user account:
current_user.direct_otp? => false
current_user.totp_enabled? => false
```



49 changes: 46 additions & 3 deletions app/controllers/devise/two_factor_authentication_controller.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,48 @@
require 'rqrcode'
require 'devise/version'

class Devise::TwoFactorAuthenticationController < DeviseController
prepend_before_action :authenticate_scope!
before_action :prepare_and_validate, :handle_two_factor_authentication
before_action :set_qr, only: [:new, :create]

def show
unless resource.otp_enabled
return redirect_to({ action: :new }, notice: I18n.t('devise.two_factor_authentication.totp_not_enabled'))
end
end

def new
if resource.otp_enabled
return redirect_to({ action: :edit }, notice: I18n.t('devise.two_factor_authentication.totp_already_enabled'))
end
end

def edit
end

def create
return render :new if params[:code].nil? || params[:totp_secret].nil?
if resource.confirm_otp(params[:totp_secret], params[:code]) && resource.save
after_two_factor_success_for(resource)
else
set_flash_message :notice, :confirm_failed, now: true
render :new
end
end

def update
render :show and return if params[:code].nil?
return render :edit if params[:code].nil?
if resource.authenticate_otp(params[:code]) && resource.disable_otp
redirect_to after_two_factor_success_path_for(resource), notice: I18n.t('devise.two_factor_authentication.remove_success')
else
set_flash_message :notice, :remove_failed, now: true
render :edit
end
end

def verify
return render :show if params[:code].nil?

if resource.authenticate_otp(params[:code])
after_two_factor_success_for(resource)
Expand All @@ -19,14 +53,23 @@ def update

def resend_code
resource.send_new_otp
redirect_to send("#{resource_name}_two_factor_authentication_path"), notice: I18n.t('devise.two_factor_authentication.code_has_been_sent')

respond_to do |format|
format.html { redirect_to send("#{resource_name}_two_factor_authentication_path"), notice: I18n.t('devise.two_factor_authentication.code_has_been_sent') }
format.json { head :no_content, status: :ok }
end
end

private

def set_qr
@totp_secret = resource.generate_totp_secret
provisioning_uri = resource.provisioning_uri(nil, otp_secret_key: @totp_secret)
@qr = RQRCode::QRCode.new(provisioning_uri).as_png(size: 250).to_data_url
end

def after_two_factor_success_for(resource)
set_remember_two_factor_cookie(resource)

warden.session(resource_name)[TwoFactorAuthentication::NEED_AUTHENTICATION] = false
# For compatability with devise versions below v4.2.0
# https://github.com/plataformatec/devise/commit/2044fffa25d781fcbaf090e7728b48b65c854ccb
Expand Down
12 changes: 12 additions & 0 deletions app/views/devise/two_factor_authentication/edit.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<h2>Disable two-factor authentication</h2>

<p><%= flash[:notice] %></p>

<%= form_tag([resource_name, :two_factor_authentication], method: 'PUT') do %>
<%= text_field_tag :code, nil, placeholder: 'Enter code', autocomplete: 'off' %>

<%= submit_tag 'Confirm and deactivate' %>
<% end %>

<br><br>
<%= link_to 'Send me a code instead', resend_code_user_two_factor_authentication_path, remote: true %>
42 changes: 42 additions & 0 deletions app/views/devise/two_factor_authentication/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<h2>Enable two-factor authentication</h2>

<p><%= flash[:notice] %></p>

<h3>Authentication with an app</h3>

<h5>Get the app</h5>
<p>
Download and install one of the following apps for your phone or table:<br>
- Google Authenticator<br>
- Duo Mobile<br>
- Authy<br>
- Windows Phone Authenticator
</p>

<h4>Scan this barcode</h4>
<%= image_tag @qr %>
<p>
Open the authentication app and:<br>
- Tap the "+" icon in the top-right of the app<br>
- Scan the image to the left, using your phone's camera<br>
<br>
<b>Can't scan this barcode?</b><br>
Instead of scanning, use your authentication app's "Manual entry" or equivalent option and provide the following time-based key.<br>
<br>
<b><i id='totp_secret'><%= @totp_secret %></i></b><br>
<br>
Your app will then generate a 6-digit verification code, which you use below.
</p>

<h3>Authentication via code</h3>

<%= link_to 'Send me a code instead', resend_code_user_two_factor_authentication_path, remote: true %>
<br><br>

<%= form_tag([resource_name, :two_factor_authentication]) do %>
<%= text_field_tag :code, nil, placeholder: 'Enter code', autocomplete: 'off' %>
<%= hidden_field_tag :totp_secret, @totp_secret %>

<%= submit_tag 'Confirm and activate' %>
<% end %>

4 changes: 3 additions & 1 deletion app/views/devise/two_factor_authentication/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<p><%= flash[:notice] %></p>

<%= form_tag([resource_name, :two_factor_authentication], :method => :put) do %>
<%= form_tag(verify_user_two_factor_authentication_path, method: :put) do %>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method name is not generic in this PR, seems to be locked to user for now.

<%= text_field_tag :code, '', autofocus: true %>
<%= submit_tag "Submit" %>
<% end %>
Expand All @@ -16,4 +16,6 @@
<% else %>
<%= link_to "Send me a code instead", send("resend_code_#{resource_name}_two_factor_authentication_path"), action: :get %>
<% end %>

<br>
<%= link_to "Sign out", send("destroy_#{resource_name}_session_path"), :method => :delete %>
5 changes: 5 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ en:
two_factor_authentication:
success: "Two factor authentication successful."
attempt_failed: "Attempt failed."
confirm_failed: "Your code did not match, or expired after scanning. Remove the old barcode from your app, and try again. Since this process is time-sensitive, make sure your device's date and time is set to 'automatic'."
remove_success: "Two factor authentication successful disabled"
remove_failed: "Your code did not match, please try again."
max_login_attempts_reached: "Access completely denied as you have reached your attempts limit"
contact_administrator: "Please contact your system administrator."
code_has_been_sent: "Your authentication code has been sent."
totp_already_enabled: "Two factor authentication is already enabled."
totp_not_enabled: "Two factor authentication is not enabled. Activate first."
Loading