Added optional two-factor authentication
This commit is contained in:
parent
237cb41ab4
commit
ba192f12e3
2
Gemfile
2
Gemfile
|
@ -31,8 +31,10 @@ gem 'link_header'
|
|||
gem 'ostatus2'
|
||||
gem 'goldfinger'
|
||||
gem 'devise'
|
||||
gem 'devise-two-factor'
|
||||
gem 'doorkeeper'
|
||||
gem 'rabl'
|
||||
gem 'rqrcode'
|
||||
gem 'oj'
|
||||
gem 'hiredis'
|
||||
gem 'redis', '~>3.2'
|
||||
|
|
15
Gemfile.lock
15
Gemfile.lock
|
@ -43,6 +43,8 @@ GEM
|
|||
public_suffix (~> 2.0, >= 2.0.2)
|
||||
arel (7.1.4)
|
||||
ast (2.3.0)
|
||||
attr_encrypted (3.0.3)
|
||||
encryptor (~> 3.0.0)
|
||||
autoprefixer-rails (6.5.0.2)
|
||||
execjs
|
||||
av (0.9.0)
|
||||
|
@ -76,6 +78,7 @@ GEM
|
|||
bullet (5.3.0)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.10.0)
|
||||
chunky_png (1.3.8)
|
||||
climate_control (0.1.0)
|
||||
cocaine (0.5.8)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
|
@ -99,6 +102,12 @@ GEM
|
|||
railties (>= 4.1.0, < 5.1)
|
||||
responders
|
||||
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)
|
||||
docile (1.1.5)
|
||||
domain_name (0.5.20161129)
|
||||
|
@ -113,6 +122,7 @@ GEM
|
|||
json
|
||||
thread
|
||||
thread_safe
|
||||
encryptor (3.0.0)
|
||||
erubis (2.7.0)
|
||||
execjs (2.7.0)
|
||||
fabrication (2.15.2)
|
||||
|
@ -304,6 +314,9 @@ GEM
|
|||
redis (>= 2.2)
|
||||
responders (2.3.0)
|
||||
railties (>= 4.2.0, < 5.1)
|
||||
rotp (2.1.2)
|
||||
rqrcode (0.10.1)
|
||||
chunky_png (~> 1.0)
|
||||
rspec (3.5.0)
|
||||
rspec-core (~> 3.5.0)
|
||||
rspec-expectations (~> 3.5.0)
|
||||
|
@ -416,6 +429,7 @@ DEPENDENCIES
|
|||
bullet
|
||||
coffee-rails (~> 4.1.0)
|
||||
devise
|
||||
devise-two-factor
|
||||
doorkeeper
|
||||
dotenv-rails
|
||||
fabrication
|
||||
|
@ -455,6 +469,7 @@ DEPENDENCIES
|
|||
react-rails
|
||||
redis (~> 3.2)
|
||||
redis-rails
|
||||
rqrcode
|
||||
rspec-rails
|
||||
rspec-sidekiq
|
||||
rubocop
|
||||
|
|
|
@ -7,6 +7,18 @@ code {
|
|||
max-width: 400px;
|
||||
padding: 20px;
|
||||
margin: 0 auto;
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
color: $color2;
|
||||
margin-bottom: 20px;
|
||||
|
||||
strong {
|
||||
color: $color5;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.simple_form {
|
||||
|
@ -118,7 +130,7 @@ code {
|
|||
margin-top: 30px;
|
||||
}
|
||||
|
||||
button {
|
||||
button, .block-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
|
@ -128,6 +140,9 @@ code {
|
|||
font-size: 18px;
|
||||
padding: 10px;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
outline: 0;
|
||||
|
@ -176,7 +191,7 @@ code {
|
|||
text-align: center;
|
||||
|
||||
a {
|
||||
color: white;
|
||||
color: $color5;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
|
@ -200,3 +215,16 @@ code {
|
|||
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'
|
||||
|
||||
before_action :configure_sign_in_params, only: [:create]
|
||||
|
||||
def create
|
||||
super do |resource|
|
||||
remember_me(resource)
|
||||
|
@ -13,6 +15,10 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
|
||||
protected
|
||||
|
||||
def configure_sign_in_params
|
||||
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
|
||||
end
|
||||
|
||||
def after_sign_in_path_for(_resource)
|
||||
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
|
||||
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
|
||||
accepts_nested_attributes_for :account
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
= 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 :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
|
||||
= f.button :button, t('auth.login'), type: :submit
|
||||
|
|
|
@ -5,4 +5,6 @@
|
|||
%li= link_to t('settings.preferences'), settings_preferences_path
|
||||
- if controller_name != 'registrations'
|
||||
%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|
|
||||
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
|
||||
# random tokens. Changing this key will render invalid all existing
|
||||
# confirmation, reset password and unlock tokens in the database.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Be sure to restart your server when you modify this 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
|
||||
edit_profile: Edit profile
|
||||
preferences: Preferences
|
||||
two_factor_auth: Two-factor Authentication
|
||||
statuses:
|
||||
over_character_limit: character limit of %{max} exceeded
|
||||
stream_entries:
|
||||
|
@ -104,6 +105,11 @@ en:
|
|||
time:
|
||||
formats:
|
||||
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:
|
||||
invalid_email: The e-mail address is invalid
|
||||
will_paginate:
|
||||
|
|
|
@ -17,6 +17,7 @@ en:
|
|||
locked: Make account private
|
||||
new_password: New password
|
||||
note: Bio
|
||||
otp_attempt: If enabled, two-factor token
|
||||
password: Password
|
||||
username: Username
|
||||
interactions:
|
||||
|
|
|
@ -47,6 +47,13 @@ Rails.application.routes.draw do
|
|||
namespace :settings do
|
||||
resource :profile, only: [:show, :update]
|
||||
resource :preferences, only: [:show, :update]
|
||||
|
||||
resource :two_factor_auth, only: [:show] do
|
||||
member do
|
||||
post :enable
|
||||
post :disable
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
enable_extension "plpgsql"
|
||||
|
@ -240,25 +240,30 @@ ActiveRecord::Schema.define(version: 20170125145934) do
|
|||
end
|
||||
|
||||
create_table "users", force: :cascade do |t|
|
||||
t.string "email", default: "", null: false
|
||||
t.integer "account_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "encrypted_password", default: "", null: false
|
||||
t.string "email", default: "", null: false
|
||||
t.integer "account_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "encrypted_password", default: "", null: false
|
||||
t.string "reset_password_token"
|
||||
t.datetime "reset_password_sent_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 "last_sign_in_at"
|
||||
t.inet "current_sign_in_ip"
|
||||
t.inet "last_sign_in_ip"
|
||||
t.boolean "admin", default: false
|
||||
t.boolean "admin", default: false
|
||||
t.string "confirmation_token"
|
||||
t.datetime "confirmed_at"
|
||||
t.datetime "confirmation_sent_at"
|
||||
t.string "unconfirmed_email"
|
||||
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 ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
|
||||
t.index ["email"], name: "index_users_on_email", unique: true, using: :btree
|
||||
|
|
Reference in a new issue