Skip to content

Commit 8900eb6

Browse files
committed
Backup codes for 2fa auth (#1237).
Patch by Felix Schäfer. git-svn-id: http://svn.redmine.org/redmine/trunk@19990 e93f8b46-1217-0410-a6f0-8f06a7374b81
1 parent be7f5e2 commit 8900eb6

File tree

15 files changed

+230
-19
lines changed

15 files changed

+230
-19
lines changed

app/controllers/account_controller.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,9 @@ def twofa_setup
265265
# set locale for the twofa user
266266
set_localization(@user)
267267

268+
# set the requesting IP of the twofa user (e.g. for security notifications)
269+
@user.remote_ip = request.remote_ip
270+
268271
@twofa = Redmine::Twofa.for_user(@user)
269272
end
270273

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# frozen_string_literal: true
2+
3+
# Redmine - project management software
4+
# Copyright (C) 2006-2020 Jean-Philippe Lang
5+
#
6+
# This program is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU General Public License
8+
# as published by the Free Software Foundation; either version 2
9+
# of the License, or (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with this program; if not, write to the Free Software
18+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
20+
class TwofaBackupCodesController < ApplicationController
21+
include TwofaHelper
22+
23+
self.main_menu = false
24+
25+
before_action :require_login, :require_active_twofa
26+
27+
before_action :twofa_setup
28+
29+
require_sudo_mode :init
30+
31+
def init
32+
if @twofa.send_code(controller: 'twofa_backup_codes', action: 'create')
33+
flash[:notice] = l('twofa_code_sent')
34+
end
35+
redirect_to action: 'confirm'
36+
end
37+
38+
def confirm
39+
@twofa_view = @twofa.otp_confirm_view_variables
40+
end
41+
42+
def create
43+
if @twofa.verify!(params[:twofa_code].to_s)
44+
if time = @twofa.backup_codes.map(&:created_on).max
45+
flash[:warning] = t('twofa_warning_backup_codes_generated_invalidated', time: format_time(time))
46+
else
47+
flash[:notice] = t('twofa_notice_backup_codes_generated')
48+
end
49+
tokens = @twofa.init_backup_codes!
50+
flash[:twofa_backup_token_ids] = tokens.collect(&:id)
51+
redirect_to action: 'show'
52+
else
53+
flash[:error] = l('twofa_invalid_code')
54+
redirect_to action: 'confirm'
55+
end
56+
end
57+
58+
def show
59+
# make sure we get only the codes that we should show
60+
tokens = @twofa.backup_codes.where(id: flash[:twofa_backup_token_ids])
61+
# Redmine will show all flash contents at the top of the rendered html
62+
# page, so we need to explicitely delete this here
63+
flash.delete(:twofa_backup_token_ids)
64+
65+
if tokens.present? && (@created_at = tokens.collect(&:created_on).max) > 5.minutes.ago
66+
@backup_codes = tokens.collect(&:value)
67+
else
68+
flash[:warning] = l('twofa_backup_codes_already_shown', bc_path: my_twofa_backup_codes_init_path)
69+
redirect_to controller: 'my', action: 'account'
70+
end
71+
end
72+
73+
private
74+
75+
def twofa_setup
76+
@user = User.current
77+
@twofa = Redmine::Twofa.for_user(@user)
78+
end
79+
end

app/controllers/twofa_controller.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919

2020
class TwofaController < ApplicationController
21+
include TwofaHelper
22+
2123
self.main_menu = false
2224

2325
before_action :require_login
@@ -45,7 +47,7 @@ def activate_confirm
4547

4648
def activate
4749
if @twofa.confirm_pairing!(params[:twofa_code].to_s)
48-
flash[:notice] = l('twofa_activated')
50+
flash[:notice] = l('twofa_activated', bc_path: my_twofa_backup_codes_init_path)
4951
redirect_to my_account_path
5052
else
5153
flash[:error] = l('twofa_invalid_code')
@@ -110,8 +112,4 @@ def deactivate_setup
110112
redirect_to my_account_path
111113
end
112114
end
113-
114-
def require_active_twofa
115-
Setting.twofa? ? true : deny_access
116-
end
117115
end

app/helpers/twofa_helper.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
# Redmine - project management software
4+
# Copyright (C) 2006-2020 Jean-Philippe Lang
5+
#
6+
# This program is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU General Public License
8+
# as published by the Free Software Foundation; either version 2
9+
# of the License, or (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with this program; if not, write to the Free Software
18+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
20+
module TwofaHelper
21+
def require_active_twofa
22+
Setting.twofa? ? true : deny_access
23+
end
24+
end

app/models/token.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def add_action(name, options)
4242
add_action :recovery, max_instances: 1, validity_time: Proc.new { Token.validity_time }
4343
add_action :register, max_instances: 1, validity_time: Proc.new { Token.validity_time }
4444
add_action :session, max_instances: 10, validity_time: nil
45+
add_action :twofa_backup_code, max_instances: 10, validity_time: nil
4546

4647
def generate_new_token
4748
self.value = Token.generate_token_value

app/views/my/account.html.erb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
<% if @user.twofa_active? %>
3535
<%=l 'twofa_currently_active', twofa_scheme_name: l("twofa__#{@user.twofa_scheme}__name") -%><br/>
3636
<%= link_to l('button_disable'), { controller: 'twofa', action: 'deactivate_init', scheme: @user.twofa_scheme }, method: :post -%><br/>
37+
<%= link_to l('twofa_generate_backup_codes'), { controller: 'twofa_backup_codes', action: 'init' }, method: :post, data: { confirm: Redmine::Twofa.for_user(User.current).backup_codes.any? ? t('twofa_text_generate_backup_codes_confirmation') : nil } -%>
3738
<% else %>
3839
<% Redmine::Twofa.available_schemes.each do |s| %>
3940
<%= link_to l("twofa__#{s}__label_activate"), { controller: 'twofa', action: 'activate_init', scheme: s }, method: :post -%><br/>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<div class="box">
2+
<p><%=l 'twofa_label_enter_otp' %></p>
3+
<div class="tabular">
4+
<p>
5+
<label for="twofa_code"><%=l 'twofa_label_code' -%></label>
6+
<%= text_field_tag :twofa_code, nil, autocomplete: 'off' -%>
7+
</p>
8+
</div>
9+
</div>

app/views/twofa/deactivate_confirm.html.erb

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,7 @@
55
scheme: @twofa_view[:scheme_name] },
66
{ method: :post,
77
id: 'twofa_form' }) do -%>
8-
<div class="box">
9-
10-
<p><%=l 'twofa_label_enter_otp' %></p>
11-
<div class="tabular">
12-
<p>
13-
<label for="twofa_code"><%=l 'twofa_label_code' -%></label>
14-
<%= text_field_tag :twofa_code, nil, autocomplete: 'off' -%>
15-
</p>
16-
</div>
17-
</div>
8+
<%= render partial: 'twofa_code_form' -%>
189
<%= submit_tag l('button_disable'), name: :submit_otp -%>
1910
<%= link_to l('twofa_resend_code'), { action: 'deactivate_init', scheme: @twofa_view[:scheme_name] }, method: :post if @twofa_view[:resendable] -%>
2011
<% end %>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<h2><%=l 'twofa_generate_backup_codes' -%></h2>
2+
3+
<div class="splitcontentleft">
4+
<%= form_tag({ action: :create },
5+
{ method: :post,
6+
id: 'twofa_form' }) do -%>
7+
<%= render partial: 'twofa/twofa_code_form' -%>
8+
<%= submit_tag l('button_submit'), name: :submit_otp -%>
9+
<%= link_to l('twofa_resend_code'), { action: 'init' }, method: :post if @twofa_view[:resendable] -%>
10+
<% end %>
11+
</div>
12+
13+
<% content_for :sidebar do %>
14+
<%= render :partial => 'my/sidebar' %>
15+
<% end %>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<h2><%=l 'twofa_label_backup_codes' -%></h2>
2+
3+
<div class="splitcontentleft">
4+
<div class="box">
5+
<p><%=l 'twofa_text_backup_codes_hint' -%></p>
6+
<ul class="twofa_backup_codes">
7+
<% @backup_codes.each do |code| -%>
8+
<li><code><%= code.scan(/.{4}/).join(' ') -%></code></li>
9+
<% end -%>
10+
</ul>
11+
<p><em class="info"><%=l 'twofa_text_backup_codes_created_at', datetime: format_time(@created_at) -%></em></p>
12+
</div>
13+
</div>
14+
15+
<% content_for :sidebar do %>
16+
<%= render :partial => 'my/sidebar' %>
17+
<% end %>

0 commit comments

Comments
 (0)