Skip to content

Commit

Permalink
Merge pull request #62 from monfresh/fix/51-encrypt-otp-secret
Browse files Browse the repository at this point in the history
Add support for OTP secret key encryption
  • Loading branch information
Houdini committed Jan 11, 2016
2 parents d350b69 + d2b6d39 commit b4a589d
Show file tree
Hide file tree
Showing 24 changed files with 718 additions and 275 deletions.
9 changes: 7 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
source "http://rubygems.org"
source 'https://rubygems.org'

# Specify your gem's dependencies in devise_ip_filter.gemspec
gemspec
Expand All @@ -20,6 +20,11 @@ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.2.0')
gem "test-unit", "~> 3.0"
end

group :test, :development do
gem 'sqlite3'
end

group :test do
gem "sqlite3"
gem 'rack_session_access'
gem 'ammeter'
end
201 changes: 147 additions & 54 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@

## Features

* control sms code pattern
* configure max login attempts
* per user level control if he really need two factor authentication
* your own sms logic
* configurable OTP code digit length
* configurable max login attempts
* customizable logic to determine if a user needs two factor authentication
* customizable logic for sending the OTP code to the user
* configurable period where users won't be asked for 2FA again
* option to encrypt the OTP secret key in the database, with iv and salt

## Configuration

Expand All @@ -23,93 +24,112 @@ Once that's done, run:

bundle install

### Automatic installation
### Installation

In order to add two factor authentication to a model, run the command:
#### Automatic initial setup
To set up the model and database migration file automatically, run the
following command:

bundle exec rails g two_factor_authentication MODEL

Where MODEL is your model name (e.g. User or Admin). This generator will add `:two_factor_authenticatable` to your model
and create a migration in `db/migrate/`, which will add `:otp_secret_key` and `:second_factor_attempts_count` to your table.
Finally, run the migration with:
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:

bundle exec rake db:migrate

Add the following line to your model to fully enable two-factor auth:
- `:second_factor_attempts_count`
- `:encrypted_otp_secret_key`
- `:encrypted_otp_secret_key_iv`
- `:encrypted_otp_secret_key_salt`

has_one_time_password

Set config values, if desired:
#### 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:

```ruby
config.max_login_attempts = 3 # Maximum second factor attempts count
config.allowed_otp_drift_seconds = 30 # Allowed time drift
config.otp_length = 6 # OTP code length
config.remember_otp_session_for_seconds = 30.days # Time before browser has to enter OTP code again
devise :database_authenticatable, :registerable, :recoverable, :rememberable,
:trackable, :validatable, :two_factor_authenticatable
```

Override the method to send one-time passwords in your model, this is automatically called when a user logs in:
Then create your migration file using the Rails generator, such as:

```ruby
def send_two_factor_authentication_code
# use Model#otp_code and send via SMS, etc.
end
```
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
```

### Manual installation

To manually enable two factor authentication for the User model, you should add two_factor_authentication to your devise line, like:
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:

```ruby
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :two_factor_authenticatable
add_index :users, :encrypted_otp_secret_key, unique: true
```
Save the file.

#### Complete the setup
Run the migration with:

bundle exec rake db:migrate

Add the following line to your model to fully enable two-factor auth:

has_one_time_password
has_one_time_password(encrypted: true)

Set config values to devise.rb, if desired:
Set config values in `config/initializers/devise.rb`:

```ruby
config.max_login_attempts = 3 # Maximum second factor attempts count
config.allowed_otp_drift_seconds = 30 # Allowed time drift
config.max_login_attempts = 3 # Maximum second factor attempts count.
config.allowed_otp_drift_seconds = 30 # Allowed time drift between client and server.
config.otp_length = 6 # OTP code length
config.remember_otp_session_for_seconds = 30.days # Time before browser has to enter OTP code again
config.remember_otp_session_for_seconds = 30.days # Time before browser has to enter OTP code again. Default is 0.
config.otp_secret_encryption_key = ENV['OTP_SECRET_ENCRYPTION_KEY']
```
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 to send one-time passwords in your model, this is automatically called when a user logs in:
Override the method to send one-time passwords in your model. This is
automatically called when a user logs in:

```ruby
def send_two_factor_authentication_code
# use Model#otp_code and send via SMS, etc.
end
```


### Customisation and Usage

By default second factor authentication enabled for each user, you can change it with this method in your User model:
By default, second factor authentication is required for each user. You can
change that by overriding the following method in your model:

```ruby
def need_two_factor_authentication?(request)
request.ip != '127.0.0.1'
end
```

this will disable two factor authentication for local users
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). You can generate provisioning uris by invoking the following method on your model:
This gem is compatible with [Google Authenticator](https://support.google.com/accounts/answer/1066447?hl=en).
You can generate provisioning uris by invoking the following method on your model:

user.provisioning_uri #This assumes a user model with an email attributes
```ruby
user.provisioning_uri # This assumes a user model with an email attribute
```

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 as well as through whatever method you define in `send_two_factor_authentication_code`
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 as
well as through whatever method you define in
`send_two_factor_authentication_code`.

#### Overriding the view

The default view that shows the form can be overridden by first adding a folder named: "two_factor_authentication" inside "app/views/devise", in here you want to create a "show.html.erb" view.
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:

The full path should be "app/views/devise/two_factor_authentication/show.html.erb"

```html
<h2>Hi, you received a code by email, please enter it below, thanks!</h2>
Expand All @@ -125,21 +145,94 @@ The full path should be "app/views/devise/two_factor_authentication/show.html.er

#### Updating existing users with OTP secret key

If you have existing users that needs to be provided with a OTP secret key, so they can take benefit of the two factor authentication, create a rake. It could look like this one below:
If you have existing users that need to be provided with a OTP secret key, so
they can use two factor authentication, create a rake task. It could look like this one below:

```ruby
desc "rake task to update users with otp secret key"
desc 'rake task to update users with otp secret key'
task :update_users_with_otp_secret_key => :environment do
users = User.all

users.each do |user|
key = ROTP::Base32.random_base32
user.update_attributes(:otp_secret_key => key)
user.save
puts "Rake[:update_users_with_otp_secret_key] => User '#{user.email}' OTP secret key set to '#{key}'"
end
User.find_each do |user|
user.otp_secret_key = ROTP::Base32.random_base32
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 OTP encryption option to an existing app

If you've already been using this gem, and want to start encrypting the OTP
secret key in the database (recommended), you'll need to perform the following
steps:

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

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

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

```ruby
add_index :users, :encrypted_otp_secret_key, unique: true
```
Save the file.

2. Run the migration: `bundle exec rake db:migrate`

2. Update the gem: `bundle update two_factor_authentication`

3. Add `encrypted: true` to `has_one_time_password` in your model.
For example: `has_one_time_password(encrypted: true)`

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

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

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

6. Run the migrations: `bundle exec rake db:migrate`

If, for some reason, you want to switch back to the old non-encrypted version,
use these steps:

1. Remove `(encrypted: true)` from `has_one_time_password`

2. Roll back the last 3 migrations (assuming you haven't added any new ones
after them):
```
bundle exec rake db:rollback STEP=3
```
#### Executing some code after the user signs in and before they sign out
Expand Down Expand Up @@ -174,6 +267,6 @@ and you need different logic for each type of user, create a second class for
your admin user, such as `AdminOtpSender`, with its own logic for
`#reset_otp_state`.
### Example
### Example App
[TwoFactorAuthenticationExample](https://github.com/Houdini/TwoFactorAuthenticationExample)
17 changes: 6 additions & 11 deletions lib/generators/active_record/templates/migration.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
class TwoFactorAuthenticationAddTo<%= table_name.camelize %> < ActiveRecord::Migration
def up
change_table :<%= table_name %> do |t|
t.string :otp_secret_key
t.integer :second_factor_attempts_count, :default => 0
end
def change
add_column :<%= table_name %>, :second_factor_attempts_count, :integer, default: 0
add_column :<%= table_name %>, :encrypted_otp_secret_key, :string
add_column :<%= table_name %>, :encrypted_otp_secret_key_iv, :string
add_column :<%= table_name %>, :encrypted_otp_secret_key_salt, :string

add_index :<%= table_name %>, :otp_secret_key, :unique => true
end

def down
remove_column :<%= table_name %>, :otp_secret_key
remove_column :<%= table_name %>, :second_factor_attempts_count
add_index :<%= table_name %>, :encrypted_otp_secret_key, unique: true
end
end
3 changes: 3 additions & 0 deletions lib/two_factor_authentication.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ module Devise

mattr_accessor :remember_otp_session_for_seconds
@@remember_otp_session_for_seconds = 0

mattr_accessor :otp_secret_encryption_key
@@otp_secret_encryption_key = ''
end

module TwoFactorAuthentication
Expand Down
Loading

0 comments on commit b4a589d

Please sign in to comment.