parent
7bfef64877
commit
1781358bd9
@ -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
|
@ -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
|
@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Form::Challenge
|
||||||
|
include ActiveModel::Model
|
||||||
|
|
||||||
|
attr_accessor :current_password, :current_username,
|
||||||
|
:return_to
|
||||||
|
end
|
@ -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'
|
@ -0,0 +1,43 @@
|
|||||||
|
%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_lock_open.png'), alt: ''
|
||||||
|
|
||||||
|
%h1= t 'devise.mailer.two_factor_disabled.title'
|
||||||
|
%p.lead= t 'devise.mailer.two_factor_disabled.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.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,7 @@
|
|||||||
|
<%= t 'devise.mailer.two_factor_disabled.title' %>
|
||||||
|
|
||||||
|
===
|
||||||
|
|
||||||
|
<%= t 'devise.mailer.two_factor_disabled.explanation' %>
|
||||||
|
|
||||||
|
=> <%= edit_user_registration_url %>
|
@ -0,0 +1,43 @@
|
|||||||
|
%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{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td
|
||||||
|
= image_tag full_pack_url('media/images/mailer/icon_lock_open.png'), alt: ''
|
||||||
|
|
||||||
|
%h1= t 'devise.mailer.two_factor_enabled.title'
|
||||||
|
%p.lead= t 'devise.mailer.two_factor_enabled.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.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,7 @@
|
|||||||
|
<%= t 'devise.mailer.two_factor_enabled.title' %>
|
||||||
|
|
||||||
|
===
|
||||||
|
|
||||||
|
<%= t 'devise.mailer.two_factor_enabled.explanation' %>
|
||||||
|
|
||||||
|
=> <%= edit_user_registration_url %>
|
@ -0,0 +1,43 @@
|
|||||||
|
%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_lock_open.png'), alt: ''
|
||||||
|
|
||||||
|
%h1= t 'devise.mailer.two_factor_recovery_codes_changed.title'
|
||||||
|
%p.lead= t 'devise.mailer.two_factor_recovery_codes_changed.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.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,7 @@
|
|||||||
|
<%= t 'devise.mailer.two_factor_recovery_codes_changed.title' %>
|
||||||
|
|
||||||
|
===
|
||||||
|
|
||||||
|
<%= t 'devise.mailer.two_factor_recovery_codes_changed.explanation' %>
|
||||||
|
|
||||||
|
=> <%= edit_user_registration_url %>
|
@ -0,0 +1,46 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Auth::ChallengesController, type: :controller do
|
||||||
|
render_views
|
||||||
|
|
||||||
|
let(:password) { 'foobar12345' }
|
||||||
|
let(:user) { Fabricate(:user, password: password) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in user
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST #create' do
|
||||||
|
let(:return_to) { edit_user_registration_path }
|
||||||
|
|
||||||
|
context 'with correct password' do
|
||||||
|
before { post :create, params: { form_challenge: { return_to: return_to, current_password: password } } }
|
||||||
|
|
||||||
|
it 'redirects back' do
|
||||||
|
expect(response).to redirect_to(return_to)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets session' do
|
||||||
|
expect(session[:challenge_passed_at]).to_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with incorrect password' do
|
||||||
|
before { post :create, params: { form_challenge: { return_to: return_to, current_password: 'hhfggjjd562' } } }
|
||||||
|
|
||||||
|
it 'renders challenge' do
|
||||||
|
expect(response).to render_template('auth/challenges/new')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'displays error' do
|
||||||
|
expect(response.body).to include 'Invalid password'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not set session' do
|
||||||
|
expect(session[:challenge_passed_at]).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,114 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe ChallengableConcern, type: :controller do
|
||||||
|
controller(ApplicationController) do
|
||||||
|
include ChallengableConcern
|
||||||
|
|
||||||
|
before_action :require_challenge!
|
||||||
|
|
||||||
|
def foo
|
||||||
|
render plain: 'foo'
|
||||||
|
end
|
||||||
|
|
||||||
|
def bar
|
||||||
|
render plain: 'bar'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
routes.draw do
|
||||||
|
get 'foo' => 'anonymous#foo'
|
||||||
|
post 'bar' => 'anonymous#bar'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a no-password user' do
|
||||||
|
let(:user) { Fabricate(:user, external: true, password: nil) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in user
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for GET requests' do
|
||||||
|
before { get :foo }
|
||||||
|
|
||||||
|
it 'does not ask for password' do
|
||||||
|
expect(response.body).to eq 'foo'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for POST requests' do
|
||||||
|
before { post :bar }
|
||||||
|
|
||||||
|
it 'does not ask for password' do
|
||||||
|
expect(response.body).to eq 'bar'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with recent challenge in session' do
|
||||||
|
let(:password) { 'foobar12345' }
|
||||||
|
let(:user) { Fabricate(:user, password: password) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in user
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for GET requests' do
|
||||||
|
before { get :foo, session: { challenge_passed_at: Time.now.utc } }
|
||||||
|
|
||||||
|
it 'does not ask for password' do
|
||||||
|
expect(response.body).to eq 'foo'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for POST requests' do
|
||||||
|
before { post :bar, session: { challenge_passed_at: Time.now.utc } }
|
||||||
|
|
||||||
|
it 'does not ask for password' do
|
||||||
|
expect(response.body).to eq 'bar'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a password user' do
|
||||||
|
let(:password) { 'foobar12345' }
|
||||||
|
let(:user) { Fabricate(:user, password: password) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in user
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for GET requests' do
|
||||||
|
before { get :foo }
|
||||||
|
|
||||||
|
it 'renders challenge' do
|
||||||
|
expect(response).to render_template('auth/challenges/new')
|
||||||
|
end
|
||||||
|
|
||||||
|
# See Auth::ChallengesControllerSpec
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for POST requests' do
|
||||||
|
before { post :bar }
|
||||||
|
|
||||||
|
it 'renders challenge' do
|
||||||
|
expect(response).to render_template('auth/challenges/new')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'accepts correct password' do
|
||||||
|
post :bar, params: { form_challenge: { current_password: password } }
|
||||||
|
expect(response.body).to eq 'bar'
|
||||||
|
expect(session[:challenge_passed_at]).to_not be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rejects wrong password' do
|
||||||
|
post :bar, params: { form_challenge: { current_password: 'dddfff888123' } }
|
||||||
|
expect(response.body).to render_template('auth/challenges/new')
|
||||||
|
expect(session[:challenge_passed_at]).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in new issue