add TOTP-based two-factor authentication option
This commit is contained in:
parent
1ac81cbe2a
commit
fd1148dc6b
2
Gemfile
2
Gemfile
|
@ -21,6 +21,8 @@ gem "dynamic_form"
|
|||
gem "exception_notification"
|
||||
|
||||
gem "bcrypt", "~> 3.1.2"
|
||||
gem "rotp"
|
||||
gem "rqrcode"
|
||||
|
||||
gem "nokogiri", "= 1.6.1"
|
||||
gem "htmlentities"
|
||||
|
|
|
@ -41,13 +41,18 @@ class LoginController < ApplicationController
|
|||
"unmoderated comments have been undeleted."
|
||||
end
|
||||
|
||||
session[:u] = user.session_token
|
||||
|
||||
if !user.password_digest.to_s.match(/^\$2a\$#{BCrypt::Engine::DEFAULT_COST}\$/)
|
||||
user.password = user.password_confirmation = params[:password].to_s
|
||||
user.save!
|
||||
end
|
||||
|
||||
if user.has_2fa?
|
||||
session[:twofa_u] = user.session_token
|
||||
return redirect_to "/login/2fa"
|
||||
end
|
||||
|
||||
session[:u] = user.session_token
|
||||
|
||||
if (rd = session[:redirect_to]).present?
|
||||
session.delete(:redirect_to)
|
||||
return redirect_to rd
|
||||
|
@ -126,4 +131,32 @@ class LoginController < ApplicationController
|
|||
return redirect_to forgot_password_path
|
||||
end
|
||||
end
|
||||
|
||||
def twofa
|
||||
if tmpu = find_twofa_user
|
||||
Rails.logger.info " Authenticated as user #{tmpu.id} " <<
|
||||
"(#{tmpu.username}), verifying TOTP"
|
||||
else
|
||||
reset_session
|
||||
return redirect_to "/login"
|
||||
end
|
||||
end
|
||||
|
||||
def twofa_verify
|
||||
if (tmpu = find_twofa_user) && tmpu.authenticate_totp(params[:totp_code])
|
||||
session[:u] = tmpu.session_token
|
||||
session.delete(:twofa_u)
|
||||
return redirect_to "/"
|
||||
else
|
||||
flash[:error] = "Your TOTP code did not match. Please try again."
|
||||
return redirect_to "/login/2fa"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def find_twofa_user
|
||||
if session[:twofa_u].present?
|
||||
return User.where(:session_token => session[:twofa_u]).first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
class SettingsController < ApplicationController
|
||||
before_filter :require_logged_in_user
|
||||
|
||||
TOTP_SESSION_TIMEOUT = (60 * 15)
|
||||
|
||||
def index
|
||||
@title = t('.accountsettings')
|
||||
|
||||
|
@ -73,6 +75,88 @@ class SettingsController < ApplicationController
|
|||
render :action => "index"
|
||||
end
|
||||
|
||||
def twofa
|
||||
@title = "Two-Factor Authentication"
|
||||
end
|
||||
|
||||
def twofa_auth
|
||||
if @user.authenticate(params[:user][:password].to_s)
|
||||
session[:last_authed] = Time.now.to_i
|
||||
session.delete(:totp_secret)
|
||||
|
||||
if @user.has_2fa?
|
||||
@user.disable_2fa!
|
||||
flash[:success] = "Two-Factor Authentication has been disabled."
|
||||
return redirect_to "/settings"
|
||||
else
|
||||
return redirect_to twofa_enroll_url
|
||||
end
|
||||
else
|
||||
flash[:error] = "Your password was not correct."
|
||||
return redirect_to twofa_url
|
||||
end
|
||||
end
|
||||
|
||||
def twofa_enroll
|
||||
@title = "Two-Factor Authentication"
|
||||
|
||||
if (Time.now.to_i - session[:last_authed].to_i) > TOTP_SESSION_TIMEOUT
|
||||
flash[:error] = "Your enrollment period timed out."
|
||||
return redirect_to twofa_url
|
||||
end
|
||||
|
||||
if !session[:totp_secret]
|
||||
session[:totp_secret] = ROTP::Base32.random_base32
|
||||
end
|
||||
|
||||
totp = ROTP::TOTP.new(session[:totp_secret],
|
||||
:issuer => Rails.application.name)
|
||||
totp_url = totp.provisioning_uri(@user.email)
|
||||
|
||||
# no option for inline svg, so just strip off leading <?xml> tag
|
||||
qrcode = RQRCode::QRCode.new(totp_url)
|
||||
qr = qrcode.as_svg(:offset => 0, color: "000", :module_size => 5,
|
||||
:shape_rendering => "crispEdges").gsub(/^<\?xml.*>/, "")
|
||||
|
||||
@qr_svg = "<a href=\"#{totp_url}\">#{qr}</a>"
|
||||
end
|
||||
|
||||
def twofa_verify
|
||||
@title = "Two-Factor Authentication"
|
||||
|
||||
if ((Time.now.to_i - session[:last_authed].to_i) > TOTP_SESSION_TIMEOUT) ||
|
||||
!session[:totp_secret]
|
||||
flash[:error] = "Your enrollment period timed out."
|
||||
return redirect_to twofa_url
|
||||
end
|
||||
end
|
||||
|
||||
def twofa_update
|
||||
if ((Time.now.to_i - session[:last_authed].to_i) > TOTP_SESSION_TIMEOUT) ||
|
||||
!session[:totp_secret]
|
||||
flash[:error] = "Your enrollment period timed out."
|
||||
return redirect_to twofa_url
|
||||
end
|
||||
|
||||
@user.totp_secret = session[:totp_secret]
|
||||
if @user.authenticate_totp(params[:totp_code])
|
||||
# re-roll, just in case
|
||||
@user.session_token = nil
|
||||
@user.save!
|
||||
|
||||
session[:u] = @user.session_token
|
||||
|
||||
flash[:success] = "Two-Factor Authentication has been enabled on " <<
|
||||
"your account."
|
||||
session.delete(:totp_secret)
|
||||
return redirect_to "/settings"
|
||||
else
|
||||
flash[:error] = "Your TOTP code was invalid, please verify the " <<
|
||||
"current code in your TOTP application."
|
||||
return redirect_to twofa_verify_url
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_params
|
||||
|
|
|
@ -45,6 +45,7 @@ class User < ActiveRecord::Base
|
|||
s.boolean :show_story_previews, :default => false
|
||||
s.boolean :show_submitted_story_threads, :default => false
|
||||
s.boolean :hide_dragons, :default => false
|
||||
s.string :totp_secret
|
||||
end
|
||||
|
||||
validates :email, :format => { :with => /\A[^@ ]+@[^@ ]+\.[^@ ]+\Z/ },
|
||||
|
@ -116,6 +117,11 @@ class User < ActiveRecord::Base
|
|||
h
|
||||
end
|
||||
|
||||
def authenticate_totp(code)
|
||||
totp = ROTP::TOTP.new(self.totp_secret)
|
||||
totp.verify(code)
|
||||
end
|
||||
|
||||
def avatar_url(size = 100)
|
||||
"https://secure.gravatar.com/avatar/" +
|
||||
Digest::MD5.hexdigest(self.email.strip.downcase) +
|
||||
|
@ -276,6 +282,11 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def disable_2fa!
|
||||
self.totp_secret = nil
|
||||
self.save!
|
||||
end
|
||||
|
||||
def grant_moderatorship_by_user!(user)
|
||||
User.transaction do
|
||||
self.is_moderator = true
|
||||
|
@ -304,6 +315,10 @@ class User < ActiveRecord::Base
|
|||
PasswordReset.password_reset_link(self, ip).deliver
|
||||
end
|
||||
|
||||
def has_2fa?
|
||||
self.totp_secret.present?
|
||||
end
|
||||
|
||||
def is_active?
|
||||
!(deleted_at? || is_banned?)
|
||||
end
|
||||
|
|
23
app/views/login/twofa.html.erb
Normal file
23
app/views/login/twofa.html.erb
Normal file
|
@ -0,0 +1,23 @@
|
|||
<div class="box wide">
|
||||
<div class="legend">
|
||||
Login - Two Factor Authentication
|
||||
</div>
|
||||
|
||||
<%= form_tag twofa_login_url do %>
|
||||
<p>
|
||||
Enter the current TOTP code from your TOTP application:
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= label_tag :totp_code, "TOTP Code:" %>
|
||||
<%= text_field_tag :totp_code, "", :size => 10, :type => "number",
|
||||
:autofocus => "autofocus" %>
|
||||
<br />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= submit_tag "Login" %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -62,6 +62,25 @@
|
|||
|
||||
<br>
|
||||
|
||||
<div class="legend">
|
||||
Security Settings
|
||||
</div>
|
||||
|
||||
<div class="boxline">
|
||||
<%= f.label :twofa, "Two-Factor Auth:", :class => "required" %>
|
||||
<span>
|
||||
<% if @edit_user.totp_secret.present? %>
|
||||
<span style="color: green; font-weight: bold;">
|
||||
Enabled
|
||||
</span> (<a href="/settings/2fa">Disable</a>)
|
||||
<% else %>
|
||||
Disabled (<a href="/settings/2fa">Enroll</a>)
|
||||
<% end %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="legend">
|
||||
<%= t('.notificationsettings') %>
|
||||
</div>
|
||||
|
|
32
app/views/settings/twofa.html.erb
Normal file
32
app/views/settings/twofa.html.erb
Normal file
|
@ -0,0 +1,32 @@
|
|||
<div class="box wide">
|
||||
<div class="legend right">
|
||||
<a href="/settings">Back to Settings</a>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<%= @title %>
|
||||
</div>
|
||||
|
||||
<%= form_for @user, :url => twofa_auth_url, :method => :post do |f| %>
|
||||
<p>
|
||||
<% if @user.has_2fa? %>
|
||||
To turn off two-factor authentication for your account, enter your
|
||||
current password:
|
||||
<% else %>
|
||||
To begin the two-factor authentication enrollment for your account,
|
||||
enter your current password:
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<div class="boxline">
|
||||
<%= f.label :password, "Current Password:", :class => "required" %>
|
||||
<%= f.password_field :password, :size => 40, :autocomplete => "off" %>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<% if @user.has_2fa? %>
|
||||
<%= submit_tag "Disable Two-Factor Authentication" %>
|
||||
<% else %>
|
||||
<%= submit_tag "Continue" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
25
app/views/settings/twofa_enroll.html.erb
Normal file
25
app/views/settings/twofa_enroll.html.erb
Normal file
|
@ -0,0 +1,25 @@
|
|||
<div class="box wide">
|
||||
<div class="legend right">
|
||||
<a href="/settings">Back to Settings</a>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<%= @title %>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Scan the QR code below or click on it to open in your <a
|
||||
href="https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm"
|
||||
target="_blank">TOTP</a> application of choice:
|
||||
</p>
|
||||
|
||||
<%= raw @qr_svg %>
|
||||
|
||||
<p>
|
||||
Once you have finished registering with your TOTP application, proceed to
|
||||
the next screen to verify your current TOTP code and actually enable
|
||||
Two-Factor Authentication on your account.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= button_to "Verify and Enable", twofa_verify_url, :method => :get %>
|
||||
</div>
|
24
app/views/settings/twofa_verify.html.erb
Normal file
24
app/views/settings/twofa_verify.html.erb
Normal file
|
@ -0,0 +1,24 @@
|
|||
<div class="box wide">
|
||||
<div class="legend right">
|
||||
<a href="/settings">Back to Settings</a>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<%= @title %>
|
||||
</div>
|
||||
|
||||
<%= form_tag twofa_update_url do %>
|
||||
<p>
|
||||
To enable Two-Factor Authentication on your account using your new TOTP
|
||||
secret, enter the six-digit code from your TOTP application:
|
||||
</p>
|
||||
|
||||
<div class="boxline">
|
||||
<%= label_tag :totp_code, "TOTP Code:", :class => "required" %>
|
||||
<%= text_field_tag :totp_code, "", :size => 10, :autocomplete => "off",
|
||||
:type => "number", :autofocus => "autofocus" %>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<%= submit_tag "Verify and Enable" %>
|
||||
<% end %>
|
||||
</div>
|
|
@ -32,6 +32,8 @@ Lobsters::Application.routes.draw do
|
|||
get "/login" => "login#index"
|
||||
post "/login" => "login#login"
|
||||
post "/logout" => "login#logout"
|
||||
get "/login/2fa" => "login#twofa"
|
||||
post "/login/2fa_verify" => "login#twofa_verify", :as => "twofa_login"
|
||||
|
||||
get "/signup" => "signup#index"
|
||||
post "/signup" => "signup#signup"
|
||||
|
@ -107,6 +109,14 @@ Lobsters::Application.routes.draw do
|
|||
get "/settings/pushover_callback" => "settings#pushover_callback"
|
||||
post "/settings/delete_account" => "settings#delete_account",
|
||||
:as => "delete_account"
|
||||
get "/settings/2fa" => "settings#twofa", :as => "twofa"
|
||||
post "/settings/2fa_auth" => "settings#twofa_auth", :as => "twofa_auth"
|
||||
get "/settings/2fa_enroll" => "settings#twofa_enroll",
|
||||
:as => "twofa_enroll"
|
||||
get "/settings/2fa_verify" => "settings#twofa_verify",
|
||||
:as => "twofa_verify"
|
||||
post "/settings/2fa_update" => "settings#twofa_update",
|
||||
:as => "twofa_update"
|
||||
|
||||
get "/filters" => "filters#index"
|
||||
post "/filters" => "filters#update"
|
||||
|
|
Loading…
Reference in a new issue