@ -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 |