Added optional two-factor authentication
This commit is contained in:
parent
237cb41ab4
commit
ba192f12e3
16 changed files with 146 additions and 15 deletions
2
Gemfile
2
Gemfile
|
@ -31,8 +31,10 @@ gem 'link_header'
|
||||||
gem 'ostatus2'
|
gem 'ostatus2'
|
||||||
gem 'goldfinger'
|
gem 'goldfinger'
|
||||||
gem 'devise'
|
gem 'devise'
|
||||||
|
gem 'devise-two-factor'
|
||||||
gem 'doorkeeper'
|
gem 'doorkeeper'
|
||||||
gem 'rabl'
|
gem 'rabl'
|
||||||
|
gem 'rqrcode'
|
||||||
gem 'oj'
|
gem 'oj'
|
||||||
gem 'hiredis'
|
gem 'hiredis'
|
||||||
gem 'redis', '~>3.2'
|
gem 'redis', '~>3.2'
|
||||||
|
|
15
Gemfile.lock
15
Gemfile.lock
|
@ -43,6 +43,8 @@ GEM
|
||||||
public_suffix (~> 2.0, >= 2.0.2)
|
public_suffix (~> 2.0, >= 2.0.2)
|
||||||
arel (7.1.4)
|
arel (7.1.4)
|
||||||
ast (2.3.0)
|
ast (2.3.0)
|
||||||
|
attr_encrypted (3.0.3)
|
||||||
|
encryptor (~> 3.0.0)
|
||||||
autoprefixer-rails (6.5.0.2)
|
autoprefixer-rails (6.5.0.2)
|
||||||
execjs
|
execjs
|
||||||
av (0.9.0)
|
av (0.9.0)
|
||||||
|
@ -76,6 +78,7 @@ GEM
|
||||||
bullet (5.3.0)
|
bullet (5.3.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.10.0)
|
uniform_notifier (~> 1.10.0)
|
||||||
|
chunky_png (1.3.8)
|
||||||
climate_control (0.1.0)
|
climate_control (0.1.0)
|
||||||
cocaine (0.5.8)
|
cocaine (0.5.8)
|
||||||
climate_control (>= 0.0.3, < 1.0)
|
climate_control (>= 0.0.3, < 1.0)
|
||||||
|
@ -99,6 +102,12 @@ GEM
|
||||||
railties (>= 4.1.0, < 5.1)
|
railties (>= 4.1.0, < 5.1)
|
||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
|
devise-two-factor (3.0.0)
|
||||||
|
activesupport
|
||||||
|
attr_encrypted (>= 1.3, < 4, != 2)
|
||||||
|
devise (~> 4.0)
|
||||||
|
railties
|
||||||
|
rotp (~> 2.0)
|
||||||
diff-lcs (1.2.5)
|
diff-lcs (1.2.5)
|
||||||
docile (1.1.5)
|
docile (1.1.5)
|
||||||
domain_name (0.5.20161129)
|
domain_name (0.5.20161129)
|
||||||
|
@ -113,6 +122,7 @@ GEM
|
||||||
json
|
json
|
||||||
thread
|
thread
|
||||||
thread_safe
|
thread_safe
|
||||||
|
encryptor (3.0.0)
|
||||||
erubis (2.7.0)
|
erubis (2.7.0)
|
||||||
execjs (2.7.0)
|
execjs (2.7.0)
|
||||||
fabrication (2.15.2)
|
fabrication (2.15.2)
|
||||||
|
@ -304,6 +314,9 @@ GEM
|
||||||
redis (>= 2.2)
|
redis (>= 2.2)
|
||||||
responders (2.3.0)
|
responders (2.3.0)
|
||||||
railties (>= 4.2.0, < 5.1)
|
railties (>= 4.2.0, < 5.1)
|
||||||
|
rotp (2.1.2)
|
||||||
|
rqrcode (0.10.1)
|
||||||
|
chunky_png (~> 1.0)
|
||||||
rspec (3.5.0)
|
rspec (3.5.0)
|
||||||
rspec-core (~> 3.5.0)
|
rspec-core (~> 3.5.0)
|
||||||
rspec-expectations (~> 3.5.0)
|
rspec-expectations (~> 3.5.0)
|
||||||
|
@ -416,6 +429,7 @@ DEPENDENCIES
|
||||||
bullet
|
bullet
|
||||||
coffee-rails (~> 4.1.0)
|
coffee-rails (~> 4.1.0)
|
||||||
devise
|
devise
|
||||||
|
devise-two-factor
|
||||||
doorkeeper
|
doorkeeper
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
fabrication
|
fabrication
|
||||||
|
@ -455,6 +469,7 @@ DEPENDENCIES
|
||||||
react-rails
|
react-rails
|
||||||
redis (~> 3.2)
|
redis (~> 3.2)
|
||||||
redis-rails
|
redis-rails
|
||||||
|
rqrcode
|
||||||
rspec-rails
|
rspec-rails
|
||||||
rspec-sidekiq
|
rspec-sidekiq
|
||||||
rubocop
|
rubocop
|
||||||
|
|
|
@ -7,6 +7,18 @@ code {
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
color: $color2;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: $color5;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.simple_form {
|
.simple_form {
|
||||||
|
@ -118,7 +130,7 @@ code {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button, .block-button {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
@ -128,6 +140,9 @@ code {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: center;
|
||||||
|
box-sizing: border-box;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
|
@ -176,7 +191,7 @@ code {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: white;
|
color: $color5;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -200,3 +215,16 @@ code {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.qr-code {
|
||||||
|
background: #fff;
|
||||||
|
padding: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 0 15px rgba($color8, 0.2);
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
|
|
||||||
layout 'auth'
|
layout 'auth'
|
||||||
|
|
||||||
|
before_action :configure_sign_in_params, only: [:create]
|
||||||
|
|
||||||
def create
|
def create
|
||||||
super do |resource|
|
super do |resource|
|
||||||
remember_me(resource)
|
remember_me(resource)
|
||||||
|
@ -13,6 +15,10 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
|
def configure_sign_in_params
|
||||||
|
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
|
||||||
|
end
|
||||||
|
|
||||||
def after_sign_in_path_for(_resource)
|
def after_sign_in_path_for(_resource)
|
||||||
last_url = stored_location_for(:user)
|
last_url = stored_location_for(:user)
|
||||||
|
|
||||||
|
|
28
app/controllers/settings/two_factor_auths_controller.rb
Normal file
28
app/controllers/settings/two_factor_auths_controller.rb
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Settings::TwoFactorAuthsController < ApplicationController
|
||||||
|
layout 'auth'
|
||||||
|
|
||||||
|
before_action :authenticate_user!
|
||||||
|
|
||||||
|
def show
|
||||||
|
return unless current_user.otp_required_for_login
|
||||||
|
|
||||||
|
@qrcode = RQRCode::QRCode.new(current_user.otp_provisioning_uri(current_user.email, issuer: Rails.configuration.x.local_domain))
|
||||||
|
end
|
||||||
|
|
||||||
|
def enable
|
||||||
|
current_user.otp_required_for_login = true
|
||||||
|
current_user.otp_secret = User.generate_otp_secret
|
||||||
|
current_user.save!
|
||||||
|
|
||||||
|
redirect_to settings_two_factor_auth_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def disable
|
||||||
|
current_user.otp_required_for_login = false
|
||||||
|
current_user.save!
|
||||||
|
|
||||||
|
redirect_to settings_two_factor_auth_path
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,7 +3,9 @@
|
||||||
class User < ApplicationRecord
|
class User < ApplicationRecord
|
||||||
include Settings::Extend
|
include Settings::Extend
|
||||||
|
|
||||||
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable
|
devise :registerable, :recoverable,
|
||||||
|
:rememberable, :trackable, :validatable, :confirmable,
|
||||||
|
:two_factor_authenticatable, otp_secret_encryption_key: ENV['OTP_SECRET']
|
||||||
|
|
||||||
belongs_to :account, inverse_of: :user
|
belongs_to :account, inverse_of: :user
|
||||||
accepts_nested_attributes_for :account
|
accepts_nested_attributes_for :account
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
= simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
|
= simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
|
||||||
= f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
|
= f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
|
||||||
= f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password') }
|
= f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password') }
|
||||||
|
= f.input :otp_attempt, placeholder: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt') }
|
||||||
|
|
||||||
.actions
|
.actions
|
||||||
= f.button :button, t('auth.login'), type: :submit
|
= f.button :button, t('auth.login'), type: :submit
|
||||||
|
|
|
@ -5,4 +5,6 @@
|
||||||
%li= link_to t('settings.preferences'), settings_preferences_path
|
%li= link_to t('settings.preferences'), settings_preferences_path
|
||||||
- if controller_name != 'registrations'
|
- if controller_name != 'registrations'
|
||||||
%li= link_to t('auth.change_password'), edit_user_registration_path
|
%li= link_to t('auth.change_password'), edit_user_registration_path
|
||||||
%li= link_to t('settings.back'), root_path
|
- if controller_name != 'two_factor_auths'
|
||||||
|
%li= link_to t('settings.two_factor_auth'), settings_two_factor_auth_path
|
||||||
|
%li= link_to t('settings.back'), root_path
|
||||||
|
|
17
app/views/settings/two_factor_auths/show.html.haml
Normal file
17
app/views/settings/two_factor_auths/show.html.haml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
- content_for :page_title do
|
||||||
|
= t('settings.two_factor_auth')
|
||||||
|
|
||||||
|
- if current_user.otp_required_for_login
|
||||||
|
%p= t('two_factor_auth.instructions_html')
|
||||||
|
|
||||||
|
.qr-code= raw @qrcode.as_svg(padding: 0, module_size: 5)
|
||||||
|
|
||||||
|
.simple_form
|
||||||
|
= link_to t('two_factor_auth.disable'), disable_settings_two_factor_auth_path, data: { method: 'POST' }, class: 'block-button'
|
||||||
|
- else
|
||||||
|
%p= t('two_factor_auth.description_html')
|
||||||
|
|
||||||
|
.simple_form
|
||||||
|
= link_to t('two_factor_auth.enable'), enable_settings_two_factor_auth_path, data: { method: 'POST' }, class: 'block-button'
|
||||||
|
|
||||||
|
.form-footer= render "settings/shared/links"
|
|
@ -1,6 +1,8 @@
|
||||||
# Use this hook to configure devise mailer, warden hooks and so forth.
|
|
||||||
# Many of these configuration options can be set straight in your model.
|
|
||||||
Devise.setup do |config|
|
Devise.setup do |config|
|
||||||
|
config.warden do |manager|
|
||||||
|
manager.default_strategies(scope: :user).unshift :two_factor_authenticatable
|
||||||
|
end
|
||||||
|
|
||||||
# The secret key used by Devise. Devise uses this key to generate
|
# The secret key used by Devise. Devise uses this key to generate
|
||||||
# random tokens. Changing this key will render invalid all existing
|
# random tokens. Changing this key will render invalid all existing
|
||||||
# confirmation, reset password and unlock tokens in the database.
|
# confirmation, reset password and unlock tokens in the database.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Be sure to restart your server when you modify this file.
|
# Be sure to restart your server when you modify this file.
|
||||||
|
|
||||||
# Configure sensitive parameters which will be filtered from the log file.
|
# Configure sensitive parameters which will be filtered from the log file.
|
||||||
Rails.application.config.filter_parameters += [:password, :private_key, :public_key]
|
Rails.application.config.filter_parameters += [:password, :private_key, :public_key, :otp_attempt]
|
||||||
|
|
|
@ -93,6 +93,7 @@ en:
|
||||||
back: Back to Mastodon
|
back: Back to Mastodon
|
||||||
edit_profile: Edit profile
|
edit_profile: Edit profile
|
||||||
preferences: Preferences
|
preferences: Preferences
|
||||||
|
two_factor_auth: Two-factor Authentication
|
||||||
statuses:
|
statuses:
|
||||||
over_character_limit: character limit of %{max} exceeded
|
over_character_limit: character limit of %{max} exceeded
|
||||||
stream_entries:
|
stream_entries:
|
||||||
|
@ -104,6 +105,11 @@ en:
|
||||||
time:
|
time:
|
||||||
formats:
|
formats:
|
||||||
default: "%b %d, %Y, %H:%M"
|
default: "%b %d, %Y, %H:%M"
|
||||||
|
two_factor_auth:
|
||||||
|
description_html: If you enable <strong>two-factor authentication</strong>, logging in will require you to be in possession of your phone, which will generate tokens for you to enter.
|
||||||
|
disable: Disable
|
||||||
|
enable: Enable
|
||||||
|
instructions_html: "<strong>Scan this QR code into Google Authenticator or a similiar app on your phone</strong>. From now on, that app will generate tokens that you will have to enter when logging in."
|
||||||
users:
|
users:
|
||||||
invalid_email: The e-mail address is invalid
|
invalid_email: The e-mail address is invalid
|
||||||
will_paginate:
|
will_paginate:
|
||||||
|
|
|
@ -17,6 +17,7 @@ en:
|
||||||
locked: Make account private
|
locked: Make account private
|
||||||
new_password: New password
|
new_password: New password
|
||||||
note: Bio
|
note: Bio
|
||||||
|
otp_attempt: If enabled, two-factor token
|
||||||
password: Password
|
password: Password
|
||||||
username: Username
|
username: Username
|
||||||
interactions:
|
interactions:
|
||||||
|
|
|
@ -47,6 +47,13 @@ Rails.application.routes.draw do
|
||||||
namespace :settings do
|
namespace :settings do
|
||||||
resource :profile, only: [:show, :update]
|
resource :profile, only: [:show, :update]
|
||||||
resource :preferences, only: [:show, :update]
|
resource :preferences, only: [:show, :update]
|
||||||
|
|
||||||
|
resource :two_factor_auth, only: [:show] do
|
||||||
|
member do
|
||||||
|
post :enable
|
||||||
|
post :disable
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :media, only: [:show]
|
resources :media, only: [:show]
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
class AddDeviseTwoFactorToUsers < ActiveRecord::Migration[5.0]
|
||||||
|
def change
|
||||||
|
add_column :users, :encrypted_otp_secret, :string
|
||||||
|
add_column :users, :encrypted_otp_secret_iv, :string
|
||||||
|
add_column :users, :encrypted_otp_secret_salt, :string
|
||||||
|
add_column :users, :consumed_timestep, :integer
|
||||||
|
add_column :users, :otp_required_for_login, :boolean
|
||||||
|
end
|
||||||
|
end
|
21
db/schema.rb
21
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 20170125145934) do
|
ActiveRecord::Schema.define(version: 20170127165745) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -240,25 +240,30 @@ ActiveRecord::Schema.define(version: 20170125145934) do
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "users", force: :cascade do |t|
|
create_table "users", force: :cascade do |t|
|
||||||
t.string "email", default: "", null: false
|
t.string "email", default: "", null: false
|
||||||
t.integer "account_id", null: false
|
t.integer "account_id", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.string "encrypted_password", default: "", null: false
|
t.string "encrypted_password", default: "", null: false
|
||||||
t.string "reset_password_token"
|
t.string "reset_password_token"
|
||||||
t.datetime "reset_password_sent_at"
|
t.datetime "reset_password_sent_at"
|
||||||
t.datetime "remember_created_at"
|
t.datetime "remember_created_at"
|
||||||
t.integer "sign_in_count", default: 0, null: false
|
t.integer "sign_in_count", default: 0, null: false
|
||||||
t.datetime "current_sign_in_at"
|
t.datetime "current_sign_in_at"
|
||||||
t.datetime "last_sign_in_at"
|
t.datetime "last_sign_in_at"
|
||||||
t.inet "current_sign_in_ip"
|
t.inet "current_sign_in_ip"
|
||||||
t.inet "last_sign_in_ip"
|
t.inet "last_sign_in_ip"
|
||||||
t.boolean "admin", default: false
|
t.boolean "admin", default: false
|
||||||
t.string "confirmation_token"
|
t.string "confirmation_token"
|
||||||
t.datetime "confirmed_at"
|
t.datetime "confirmed_at"
|
||||||
t.datetime "confirmation_sent_at"
|
t.datetime "confirmation_sent_at"
|
||||||
t.string "unconfirmed_email"
|
t.string "unconfirmed_email"
|
||||||
t.string "locale"
|
t.string "locale"
|
||||||
|
t.string "encrypted_otp_secret"
|
||||||
|
t.string "encrypted_otp_secret_iv"
|
||||||
|
t.string "encrypted_otp_secret_salt"
|
||||||
|
t.integer "consumed_timestep"
|
||||||
|
t.boolean "otp_required_for_login"
|
||||||
t.index ["account_id"], name: "index_users_on_account_id", using: :btree
|
t.index ["account_id"], name: "index_users_on_account_id", using: :btree
|
||||||
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
|
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
|
||||||
t.index ["email"], name: "index_users_on_email", unique: true, using: :btree
|
t.index ["email"], name: "index_users_on_email", unique: true, using: :btree
|
||||||
|
|
Loading…
Reference in a new issue