parent
d0c2c52783
commit
e1066cd431
@ -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