Skip to content

Commit

Permalink
Add password challenge to 2FA settings, e-mail notifications (mastodo…
Browse files Browse the repository at this point in the history
  • Loading branch information
Gargron authored and hiyuki2578 committed Oct 2, 2019
1 parent ef0bf7d commit 16ba81a
Show file tree
Hide file tree
Showing 32 changed files with 567 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ def destroy
authorize @user, :disable_2fa?
@user.disable_two_factor!
log_action :disable_2fa, @user
UserMailer.two_factor_disabled(@user).deliver_later!
redirect_to admin_accounts_path
end

Expand Down
22 changes: 22 additions & 0 deletions app/controllers/auth/challenges_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

class Auth::ChallengesController < ApplicationController
include ChallengableConcern

layout 'auth'

before_action :authenticate_user!

skip_before_action :require_functional!

def create
if challenge_passed?
session[:challenge_passed_at] = Time.now.utc
redirect_to challenge_params[:return_to]
else
@challenge = Form::Challenge.new(return_to: challenge_params[:return_to])
flash.now[:alert] = I18n.t('challenge.invalid_password')
render_challenge
end
end
end
1 change: 1 addition & 0 deletions app/controllers/auth/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def create
def destroy
tmp_stored_location = stored_location_for(:user)
super
session.delete(:challenge_passed_at)
flash.delete(:notice)
store_location_for(:user, tmp_stored_location) if continue_after?
end
Expand Down
65 changes: 65 additions & 0 deletions app/controllers/concerns/challengable_concern.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

# This concern is inspired by "sudo mode" on GitHub. It
# is a way to re-authenticate a user before allowing them
# to see or perform an action.
#
# Add `before_action :require_challenge!` to actions you
# want to protect.
#
# The user will be shown a page to enter the challenge (which
# is either the password, or just the username when no
# password exists). Upon passing, there is a grace period
# during which no challenge will be asked from the user.
#
# Accessing challenge-protected resources during the grace
# period will refresh the grace period.
module ChallengableConcern
extend ActiveSupport::Concern

CHALLENGE_TIMEOUT = 1.hour.freeze

def require_challenge!
return if skip_challenge?

if challenge_passed_recently?
session[:challenge_passed_at] = Time.now.utc
return
end

@challenge = Form::Challenge.new(return_to: request.url)

if params.key?(:form_challenge)
if challenge_passed?
session[:challenge_passed_at] = Time.now.utc
return
else
flash.now[:alert] = I18n.t('challenge.invalid_password')
render_challenge
end
else
render_challenge
end
end

def render_challenge
@body_classes = 'lighter'
render template: 'auth/challenges/new', layout: 'auth'
end

def challenge_passed?
current_user.valid_password?(challenge_params[:current_password])
end

def skip_challenge?
current_user.encrypted_password.blank?
end

def challenge_passed_recently?
session[:challenge_passed_at].present? && session[:challenge_passed_at] >= CHALLENGE_TIMEOUT.ago
end

def challenge_params
params.require(:form_challenge).permit(:current_password, :return_to)
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
module Settings
module TwoFactorAuthentication
class ConfirmationsController < BaseController
include ChallengableConcern

layout 'admin'

before_action :authenticate_user!
before_action :require_challenge!
before_action :ensure_otp_secret

skip_before_action :require_functional!
Expand All @@ -22,6 +25,8 @@ def create
@recovery_codes = current_user.generate_otp_backup_codes!
current_user.save!

UserMailer.two_factor_enabled(current_user).deliver_later!

render 'settings/two_factor_authentication/recovery_codes/index'
else
flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,22 @@
module Settings
module TwoFactorAuthentication
class RecoveryCodesController < BaseController
include ChallengableConcern

layout 'admin'

before_action :authenticate_user!
before_action :require_challenge!, on: :create

skip_before_action :require_functional!

def create
@recovery_codes = current_user.generate_otp_backup_codes!
current_user.save!

UserMailer.two_factor_recovery_codes_changed(current_user).deliver_later!
flash.now[:notice] = I18n.t('two_factor_authentication.recovery_codes_regenerated')

render :index
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

module Settings
class TwoFactorAuthenticationsController < BaseController
include ChallengableConcern

layout 'admin'

before_action :authenticate_user!
before_action :verify_otp_required, only: [:create]
before_action :require_challenge!, only: [:create]

skip_before_action :require_functional!

Expand All @@ -23,6 +26,7 @@ def destroy
if acceptable_code?
current_user.otp_required_for_login = false
current_user.save!
UserMailer.two_factor_disabled(current_user).deliver_later!
redirect_to settings_two_factor_authentication_path
else
flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
Expand Down
43 changes: 23 additions & 20 deletions app/javascript/styles/mastodon/admin.scss
Original file line number Diff line number Diff line change
Expand Up @@ -233,32 +233,35 @@ hr.spacer {
height: 1px;
}

.muted-hint {
color: $darker-text-color;
body,
.admin-wrapper .content {
.muted-hint {
color: $darker-text-color;

a {
color: $highlight-text-color;
a {
color: $highlight-text-color;
}
}
}

.positive-hint {
color: $valid-value-color;
font-weight: 500;
}
.positive-hint {
color: $valid-value-color;
font-weight: 500;
}

.negative-hint {
color: $error-value-color;
font-weight: 500;
}
.negative-hint {
color: $error-value-color;
font-weight: 500;
}

.neutral-hint {
color: $dark-text-color;
font-weight: 500;
}
.neutral-hint {
color: $dark-text-color;
font-weight: 500;
}

.warning-hint {
color: $gold-star;
font-weight: 500;
.warning-hint {
color: $gold-star;
font-weight: 500;
}
}

.filters {
Expand Down
4 changes: 4 additions & 0 deletions app/javascript/styles/mastodon/forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,10 @@ code {
&-6 {
max-width: 50%;
}

.actions {
margin-top: 27px;
}
}

.fields-group:last-child,
Expand Down
33 changes: 33 additions & 0 deletions app/mailers/user_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,39 @@ def email_changed(user, **)
end
end

def two_factor_enabled(user, **)
@resource = user
@instance = Rails.configuration.x.local_domain

return if @resource.disabled?

I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_enabled.subject')
end
end

def two_factor_disabled(user, **)
@resource = user
@instance = Rails.configuration.x.local_domain

return if @resource.disabled?

I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_disabled.subject')
end
end

def two_factor_recovery_codes_changed(user, **)
@resource = user
@instance = Rails.configuration.x.local_domain

return if @resource.disabled?

I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_recovery_codes_changed.subject')
end
end

def welcome(user)
@resource = user
@instance = Rails.configuration.x.local_domain
Expand Down
8 changes: 8 additions & 0 deletions app/models/form/challenge.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

class Form::Challenge
include ActiveModel::Model

attr_accessor :current_password, :current_username,
:return_to
end
9 changes: 6 additions & 3 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -264,17 +264,20 @@ def invite_code=(code)
end

def password_required?
return false if Devise.pam_authentication || Devise.ldap_authentication
return false if external?

super
end

def send_reset_password_instructions
return false if encrypted_password.blank? && (Devise.pam_authentication || Devise.ldap_authentication)
return false if encrypted_password.blank?

super
end

def reset_password!(new_password, new_password_confirmation)
return false if encrypted_password.blank? && (Devise.pam_authentication || Devise.ldap_authentication)
return false if encrypted_password.blank?

super
end

Expand Down
15 changes: 15 additions & 0 deletions app/views/auth/challenges/new.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
- content_for :page_title do
= t('challenge.prompt')

= simple_form_for @challenge, url: request.get? ? auth_challenge_path : '' do |f|
= f.input :return_to, as: :hidden

.field-group
= f.input :current_password, wrapper: :with_block_label, input_html: { :autocomplete => 'off', :autofocus => true }, label: t('challenge.prompt'), required: true

.actions
= f.button :button, t('challenge.confirm'), type: :submit

%p.hint.subtle-hint= t('challenge.hint_html')

.form-footer= render 'auth/shared/links'
2 changes: 1 addition & 1 deletion app/views/auth/shared/_links.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
- if controller_name != 'passwords' && controller_name != 'registrations'
%li= link_to t('auth.forgot_password'), new_user_password_path

- if controller_name != 'confirmations'
- if controller_name != 'confirmations' && (!user_signed_in? || !current_user.confirmed? || current_user.unconfirmed_email.present?)
%li= link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path

- if user_signed_in? && controller_name != 'setup'
Expand Down
38 changes: 20 additions & 18 deletions app/views/settings/two_factor_authentications/show.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,35 @@
= t('settings.two_factor_authentication')

- if current_user.otp_required_for_login
%p.positive-hint
= fa_icon 'check'
= ' '
= t 'two_factor_authentication.enabled'
%p.hint
%span.positive-hint
= fa_icon 'check'
= ' '
= t 'two_factor_authentication.enabled'

%hr/
%hr.spacer/

= simple_form_for @confirmation, url: settings_two_factor_authentication_path, method: :delete do |f|
= f.input :otp_attempt, wrapper: :with_label, hint: t('two_factor_authentication.code_hint'), label: t('simple_form.labels.defaults.otp_attempt'), input_html: { :autocomplete => 'off' }, required: true
.fields-group
= f.input :otp_attempt, wrapper: :with_block_label, hint: t('two_factor_authentication.code_hint'), label: t('simple_form.labels.defaults.otp_attempt'), input_html: { :autocomplete => 'off' }, required: true

.actions
= f.button :button, t('two_factor_authentication.disable'), type: :submit
= f.button :button, t('two_factor_authentication.disable'), type: :submit, class: 'negative'

%hr/
%hr.spacer/

%h6= t('two_factor_authentication.recovery_codes')
%p.muted-hint
= t('two_factor_authentication.lost_recovery_codes')
= link_to t('two_factor_authentication.generate_recovery_codes'),
settings_two_factor_authentication_recovery_codes_path,
data: { method: :post }
%h3= t('two_factor_authentication.recovery_codes')
%p.muted-hint= t('two_factor_authentication.lost_recovery_codes')

%hr.spacer/

.simple_form
= link_to t('two_factor_authentication.generate_recovery_codes'), settings_two_factor_authentication_recovery_codes_path, data: { method: :post }, class: 'block-button'

- else
.simple_form
%p.hint= t('two_factor_authentication.description_html')

= link_to t('two_factor_authentication.setup'),
settings_two_factor_authentication_path,
data: { method: :post },
class: 'block-button'
%hr.spacer/

= link_to t('two_factor_authentication.setup'), settings_two_factor_authentication_path, data: { method: :post }, class: 'block-button'
Loading

0 comments on commit 16ba81a

Please sign in to comment.