Add e-mail-based sign in challenge for users with disabled 2FA (#14013)
parent
8b6d97fb7c
commit
72a7cfaa39
@ -0,0 +1,49 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
module SignInTokenAuthenticationConcern |
||||
extend ActiveSupport::Concern |
||||
|
||||
included do |
||||
prepend_before_action :authenticate_with_sign_in_token, if: :sign_in_token_required?, only: [:create] |
||||
end |
||||
|
||||
def sign_in_token_required? |
||||
find_user&.suspicious_sign_in?(request.remote_ip) |
||||
end |
||||
|
||||
def valid_sign_in_token_attempt?(user) |
||||
Devise.secure_compare(user.sign_in_token, user_params[:sign_in_token_attempt]) |
||||
end |
||||
|
||||
def authenticate_with_sign_in_token |
||||
user = self.resource = find_user |
||||
|
||||
if user_params[:sign_in_token_attempt].present? && session[:attempt_user_id] |
||||
authenticate_with_sign_in_token_attempt(user) |
||||
elsif user.present? && user.external_or_valid_password?(user_params[:password]) |
||||
prompt_for_sign_in_token(user) |
||||
end |
||||
end |
||||
|
||||
def authenticate_with_sign_in_token_attempt(user) |
||||
if valid_sign_in_token_attempt?(user) |
||||
session.delete(:attempt_user_id) |
||||
remember_me(user) |
||||
sign_in(user) |
||||
else |
||||
flash.now[:alert] = I18n.t('users.invalid_sign_in_token') |
||||
prompt_for_sign_in_token(user) |
||||
end |
||||
end |
||||
|
||||
def prompt_for_sign_in_token(user) |
||||
if user.sign_in_token_expired? |
||||
user.generate_sign_in_token && user.save |
||||
UserMailer.sign_in_token(user, request.remote_ip, request.user_agent, Time.now.utc.to_s).deliver_later! |
||||
end |
||||
|
||||
session[:attempt_user_id] = user.id |
||||
@body_classes = 'lighter' |
||||
render :sign_in_token |
||||
end |
||||
end |
@ -0,0 +1,47 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
module TwoFactorAuthenticationConcern |
||||
extend ActiveSupport::Concern |
||||
|
||||
included do |
||||
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create] |
||||
end |
||||
|
||||
def two_factor_enabled? |
||||
find_user&.otp_required_for_login? |
||||
end |
||||
|
||||
def valid_otp_attempt?(user) |
||||
user.validate_and_consume_otp!(user_params[:otp_attempt]) || |
||||
user.invalidate_otp_backup_code!(user_params[:otp_attempt]) |
||||
rescue OpenSSL::Cipher::CipherError |
||||
false |
||||
end |
||||
|
||||
def authenticate_with_two_factor |
||||
user = self.resource = find_user |
||||
|
||||
if user_params[:otp_attempt].present? && session[:attempt_user_id] |
||||
authenticate_with_two_factor_attempt(user) |
||||
elsif user.present? && user.external_or_valid_password?(user_params[:password]) |
||||
prompt_for_two_factor(user) |
||||
end |
||||
end |
||||
|
||||
def authenticate_with_two_factor_attempt(user) |
||||
if valid_otp_attempt?(user) |
||||
session.delete(:attempt_user_id) |
||||
remember_me(user) |
||||
sign_in(user) |
||||
else |
||||
flash.now[:alert] = I18n.t('users.invalid_otp_token') |
||||
prompt_for_two_factor(user) |
||||
end |
||||
end |
||||
|
||||
def prompt_for_two_factor(user) |
||||
session[:attempt_user_id] = user.id |
||||
@body_classes = 'lighter' |
||||
render :two_factor |
||||
end |
||||
end |
@ -0,0 +1,14 @@ |
||||
- content_for :page_title do |
||||
= t('auth.login') |
||||
|
||||
= simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| |
||||
%p.hint.otp-hint= t('users.suspicious_sign_in_confirmation') |
||||
|
||||
.fields-group |
||||
= f.input :sign_in_token_attempt, type: :number, wrapper: :with_label, label: t('simple_form.labels.defaults.sign_in_token_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.sign_in_token_attempt'), :autocomplete => 'off' }, autofocus: true |
||||
|
||||
.actions |
||||
= f.button :button, t('auth.login'), type: :submit |
||||
|
||||
- if Setting.site_contact_email.present? |
||||
%p.hint.subtle-hint= t('users.generic_access_help_html', email: mail_to(Setting.site_contact_email, nil)) |
@ -0,0 +1,105 @@ |
||||
%table.email-table{ cellspacing: 0, cellpadding: 0 } |
||||
%tbody |
||||
%tr |
||||
%td.email-body |
||||
.email-container |
||||
%table.content-section{ cellspacing: 0, cellpadding: 0 } |
||||
%tbody |
||||
%tr |
||||
%td.content-cell.hero |
||||
.email-row |
||||
.col-6 |
||||
%table.column{ cellspacing: 0, cellpadding: 0 } |
||||
%tbody |
||||
%tr |
||||
%td.column-cell.text-center.padded |
||||
%table.hero-icon.alert-icon{ align: 'center', cellspacing: 0, cellpadding: 0 } |
||||
%tbody |
||||
%tr |
||||
%td |
||||
= image_tag full_pack_url('media/images/mailer/icon_email.png'), alt: '' |
||||
|
||||
%h1= t 'user_mailer.sign_in_token.title' |
||||
%p.lead= t 'user_mailer.sign_in_token.explanation' |
||||
|
||||
%table.email-table{ cellspacing: 0, cellpadding: 0 } |
||||
%tbody |
||||
%tr |
||||
%td.email-body |
||||
.email-container |
||||
%table.content-section{ cellspacing: 0, cellpadding: 0 } |
||||
%tbody |
||||
%tr |
||||
%td.content-cell.content-start |
||||
%table.column{ cellspacing: 0, cellpadding: 0 } |
||||
%tbody |
||||
%tr |
||||
%td.column-cell.input-cell |
||||
%table.input{ align: 'center', cellspacing: 0, cellpadding: 0 } |
||||
%tbody |
||||
%tr |
||||
%td= @resource.sign_in_token |
||||
|
||||
%table.email-table{ cellspacing: 0, cellpadding: 0 } |
||||
%tbody |
||||
%tr |
||||
%td.email-body |
||||
.email-container |
||||
%table.content-section{ cellspacing: 0, cellpadding: 0 } |
||||
%tbody |
||||
%tr |
||||
%td.content-cell |
||||
.email-row |
||||
.col-6 |
||||
%table.column{ cellspacing: 0, cellpadding: 0 } |
||||
%tbody |
||||
%tr |
||||
%td.column-cell.text-center |
||||
%p= t 'user_mailer.sign_in_token.details' |
||||
%tr |
||||
%td.column-cell.text-center |
||||
%p |
||||
%strong= "#{t('sessions.ip')}:" |
||||
= @remote_ip |
||||
%br/ |
||||
%strong= "#{t('sessions.browser')}:" |
||||
%span{ title: @user_agent }= t 'sessions.description', browser: t("sessions.browsers.#{@detection.id}", default: "#{@detection.id}"), platform: t("sessions.platforms.#{@detection.platform.id}", default: "#{@detection.platform.id}") |
||||
%br/ |
||||
= l(@timestamp) |
||||
|
||||
%table.email-table{ cellspacing: 0, cellpadding: 0 } |
||||
%tbody |
||||
%tr |
||||
%td.email-body |
||||
.email-container |
||||
%table.content-section{ cellspacing: 0, cellpadding: 0 } |
||||
%tbody |
||||
%tr |
||||
%td.content-cell |
||||
.email-row |
||||
.col-6 |
||||
%table.column{ cellspacing: 0, cellpadding: 0 } |
||||
%tbody |
||||
%tr |
||||
%td.column-cell.text-center |
||||
%p= t 'user_mailer.sign_in_token.further_actions' |
||||
|
||||
%table.email-table{ cellspacing: 0, cellpadding: 0 } |
||||
%tbody |
||||
%tr |
||||
%td.email-body |
||||
.email-container |
||||
%table.content-section{ cellspacing: 0, cellpadding: 0 } |
||||
%tbody |
||||
%tr |
||||
%td.content-cell |
||||
%table.column{ cellspacing: 0, cellpadding: 0 } |
||||
%tbody |
||||
%tr |
||||
%td.column-cell.button-cell |
||||
%table.button{ align: 'center', cellspacing: 0, cellpadding: 0 } |
||||
%tbody |
||||
%tr |
||||
%td.button-primary |
||||
= link_to edit_user_registration_url do |
||||
%span= t 'settings.account_settings' |
@ -0,0 +1,17 @@ |
||||
<%= t 'user_mailer.sign_in_token.title' %> |
||||
|
||||
=== |
||||
|
||||
<%= t 'user_mailer.sign_in_token.explanation' %> |
||||
|
||||
=> <%= @resource.sign_in_token %> |
||||
|
||||
<%= t 'user_mailer.sign_in_token.details' %> |
||||
|
||||
<%= t('sessions.ip') %>: <%= @remote_ip %> |
||||
<%= t('sessions.browser') %>: <%= t('sessions.description', browser: t("sessions.browsers.#{@detection.id}", default: "#{@detection.id}"), platform: t("sessions.platforms.#{@detection.platform.id}", default: "#{@detection.platform.id}")) %> |
||||
<%= l(@timestamp) %> |
||||
|
||||
<%= t 'user_mailer.sign_in_token.further_actions' %> |
||||
|
||||
=> <%= edit_user_registration_url %> |
@ -0,0 +1,6 @@ |
||||
class AddSignInTokenToUsers < ActiveRecord::Migration[5.2] |
||||
def change |
||||
add_column :users, :sign_in_token, :string |
||||
add_column :users, :sign_in_token_sent_at, :datetime |
||||
end |
||||
end |
Loading…
Reference in new issue