Feature #1237 » 0003-Backup-codes-for-2fa-auth-1237.patch
app/controllers/account_controller.rb | ||
---|---|---|
265 | 265 |
# set locale for the twofa user |
266 | 266 |
set_localization(@user) |
267 | 267 | |
268 |
# set the requesting IP of the twofa user (e.g. for security notifications) |
|
269 |
@user.remote_ip = request.remote_ip |
|
270 | ||
268 | 271 |
@twofa = Redmine::Twofa.for_user(@user) |
269 | 272 |
end |
270 | 273 |
app/controllers/twofa_backup_codes_controller.rb | ||
---|---|---|
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 | ||
---|---|---|
18 | 18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
19 | 19 | |
20 | 20 |
class TwofaController < ApplicationController |
21 |
include TwofaHelper |
|
22 | ||
21 | 23 |
self.main_menu = false |
22 | 24 | |
23 | 25 |
before_action :require_login |
... | ... | |
45 | 47 | |
46 | 48 |
def activate |
47 | 49 |
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)
|
|
49 | 51 |
redirect_to my_account_path |
50 | 52 |
else |
51 | 53 |
flash[:error] = l('twofa_invalid_code') |
... | ... | |
107 | 109 |
redirect_to my_account_path |
108 | 110 |
end |
109 | 111 |
end |
110 | ||
111 |
def require_active_twofa |
|
112 |
Setting.twofa? ? true : deny_access |
|
113 |
end |
|
114 | 112 |
end |
app/helpers/twofa_helper.rb | ||
---|---|---|
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 | ||
---|---|---|
42 | 42 |
add_action :recovery, max_instances: 1, validity_time: Proc.new { Token.validity_time } |
43 | 43 |
add_action :register, max_instances: 1, validity_time: Proc.new { Token.validity_time } |
44 | 44 |
add_action :session, max_instances: 10, validity_time: nil |
45 |
add_action :twofa_backup_code, max_instances: 10, validity_time: nil |
|
45 | 46 | |
46 | 47 |
def generate_new_token |
47 | 48 |
self.value = Token.generate_token_value |
app/views/my/account.html.erb | ||
---|---|---|
34 | 34 |
<% if @user.twofa_active? %> |
35 | 35 |
<%=l 'twofa_currently_active', twofa_scheme_name: l("twofa__#{@user.twofa_scheme}__name") -%><br/> |
36 | 36 |
<%= 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 } -%> |
|
37 | 38 |
<% else %> |
38 | 39 |
<% Redmine::Twofa.available_schemes.each do |s| %> |
39 | 40 |
<%= link_to l("twofa__#{s}__label_activate"), { controller: 'twofa', action: 'activate_init', scheme: s }, method: :post -%><br/> |
app/views/twofa/_twofa_code_form.html.erb | ||
---|---|---|
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 | ||
---|---|---|
5 | 5 |
scheme: @twofa_view[:scheme_name] }, |
6 | 6 |
{ method: :post, |
7 | 7 |
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' -%> |
|
18 | 9 |
<%= submit_tag l('button_disable'), name: :submit_otp -%> |
19 | 10 |
<%= link_to l('twofa_resend_code'), { action: 'deactivate_init', scheme: @twofa_view[:scheme_name] }, method: :post if @twofa_view[:resendable] -%> |
20 | 11 |
<% end %> |
app/views/twofa_backup_codes/confirm.html.erb | ||
---|---|---|
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 %> |
app/views/twofa_backup_codes/show.html.erb | ||
---|---|---|
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 %> |
config/locales/de.yml | ||
---|---|---|
1337 | 1337 |
twofa_label_deactivation_confirmation: Zwei-Faktor-Authentifizierung abschalten |
1338 | 1338 |
twofa_notice_select: "Bitte wählen Sie Ihr gewünschtes Schema für die Zwei-Faktor-Authentifizierung:" |
1339 | 1339 |
twofa_warning_require: Der Administrator fordert Sie dazu auf Zwei-Faktor-Authentifizierung einzurichten. |
1340 |
twofa_activated: Zwei-Faktor-Authentifizierung erfolgreich eingerichtet. |
|
1340 |
twofa_activated: Zwei-Faktor-Authentifizierung erfolgreich eingerichtet. Es ist empfohlen hierzu <a data-method="post" href="%{bc_path}">Backup-Codes zu generieren</a>.
|
|
1341 | 1341 |
twofa_deactivated: Zwei-Faktor-Authentifizierung abgeschaltet. |
1342 | 1342 |
twofa_mail_body_security_notification_paired: "Zwei-Faktor-Authentifizierung per %{field} eingerichtet." |
1343 | 1343 |
twofa_mail_body_security_notification_unpaired: "Zwei-Faktor-Authentifizierung für Ihr Konto abgeschaltet." |
1344 |
twofa_mail_body_backup_codes_generated: "Neue Backup-Codes für Zwei-Faktor-Authentifizierung generiert." |
|
1345 |
twofa_mail_body_backup_code_used: "Ein Backup-Code für Zwei-Faktor-Authentifizierung ist verwendet worden." |
|
1344 | 1346 |
twofa_invalid_code: Der eingegebene Code ist ungültig oder abgelaufen. |
1345 | 1347 |
twofa_label_enter_otp: Bitte geben Sie Ihren Code für die Zwei-Faktor-Authentifizierung ein. |
1346 | 1348 |
twofa_too_many_tries: Zu viele Versuche. |
1347 | 1349 |
twofa_resend_code: Code erneut senden |
1348 | 1350 |
twofa_code_sent: Ein Code für die Zwei-Faktor-Authentifizierung wurde Ihnen zugesendet. |
1351 |
twofa_generate_backup_codes: Backup-Codes generieren |
|
1352 |
twofa_text_generate_backup_codes_confirmation: Im nächsten Schritt werden alle bestehenden Backup-Codes ungültig gemacht und neue generiert. Möchten Sie fortfahren? |
|
1353 |
twofa_notice_backup_codes_generated: Ihre Backup-Codes wurden generiert. |
|
1354 |
twofa_warning_backup_codes_generated_invalidated: Es wurden neue Backup-Codes generiert. Die bestehenden Codes vom %{time} sind nicht mehr gültig. |
|
1355 |
twofa_label_backup_codes: Zwei-Faktor-Authentifizierung Backup-Codes |
|
1356 |
twofa_text_backup_codes_hint: Sie können einen dieser Codes benutzen wenn Sie vorübergehend keinen Zugriff auf Ihren zweiten Faktor haben. Jeder Code kann nur ein Mal verwendet werden. Es wird empfohlen, diese Codes auszudrucken und sie an einem sicheren Ort zu verwahren. |
|
1357 |
twofa_text_backup_codes_created_at: Backup-Codes generiert am %{datetime}. |
|
1358 |
twofa_backup_codes_already_shown: Aus Sicherheitsgründen können Backup-Codes nicht erneut angezeigt werden. Bitte <a data-method="post" href="%{bc_path}">generieren Sie neue Codes</a> falls nötig. |
config/locales/en.yml | ||
---|---|---|
1315 | 1315 |
twofa_label_deactivation_confirmation: Disable two-factor authentication |
1316 | 1316 |
twofa_notice_select: "Please select the two-factor scheme you would like to use:" |
1317 | 1317 |
twofa_warning_require: The administrator requires you to enable two-factor authentication. |
1318 |
twofa_activated: Two-factor authentication successfully enabled. |
|
1318 |
twofa_activated: Two-factor authentication successfully enabled. It is recommended to <a data-method="post" href="%{bc_path}">generate backup codes</a> for your account.
|
|
1319 | 1319 |
twofa_deactivated: Two-factor authentication disabled. |
1320 | 1320 |
twofa_mail_body_security_notification_paired: "Two-factor authentication successfully enabled using %{field}." |
1321 | 1321 |
twofa_mail_body_security_notification_unpaired: "Two-factor authentication disabled for your account." |
1322 |
twofa_mail_body_backup_codes_generated: "New two-factor authentication backup codes generated." |
|
1323 |
twofa_mail_body_backup_code_used: "A two-factor authentication backup code has been used." |
|
1322 | 1324 |
twofa_invalid_code: Code is invalid or outdated. |
1323 | 1325 |
twofa_label_enter_otp: Please enter your two-factor authentication code. |
1324 | 1326 |
twofa_too_many_tries: Too many tries. |
1325 | 1327 |
twofa_resend_code: Resend code |
1326 | 1328 |
twofa_code_sent: An authentication code has been sent to you. |
1329 |
twofa_generate_backup_codes: Generate backup codes |
|
1330 |
twofa_text_generate_backup_codes_confirmation: This will invalidate all existing backup codes and generate new ones. Would you like to continue? |
|
1331 |
twofa_notice_backup_codes_generated: Your backup codes have been generated. |
|
1332 |
twofa_warning_backup_codes_generated_invalidated: New backup codes have been generated. Your existing codes from %{time} are now invalid. |
|
1333 |
twofa_label_backup_codes: Two-factor authentication backup codes |
|
1334 |
twofa_text_backup_codes_hint: Use these codes instead of a one-time password should you not have access to your second factor. Each code can only be used once. It is recommended to print and store them in a safe place. |
|
1335 |
twofa_text_backup_codes_created_at: Backup codes generated %{datetime}. |
|
1336 |
twofa_backup_codes_already_shown: Backup codes cannot be shown again, please <a data-method="post" href="%{bc_path}">generate new backup codes</a> if required. |
config/routes.rb | ||
---|---|---|
96 | 96 |
match 'my/twofa/:scheme/deactivate/confirm', :controller => 'twofa', :action => 'deactivate_confirm', :via => :get |
97 | 97 |
match 'my/twofa/:scheme/deactivate', :controller => 'twofa', :action => 'deactivate', :via => [:get, :post] |
98 | 98 |
match 'my/twofa/select_scheme', :controller => 'twofa', :action => 'select_scheme', :via => :get |
99 |
match 'my/twofa/backup_codes/init', :controller => 'twofa_backup_codes', :action => 'init', :via => :post |
|
100 |
match 'my/twofa/backup_codes/confirm', :controller => 'twofa_backup_codes', :action => 'confirm', :via => :get |
|
101 |
match 'my/twofa/backup_codes/create', :controller => 'twofa_backup_codes', :action => 'create', :via => [:get, :post] |
|
102 |
match 'my/twofa/backup_codes', :controller => 'twofa_backup_codes', :action => 'show', :via => [:get] |
|
99 | 103 |
match 'users/:user_id/twofa/deactivate', :controller => 'twofa', :action => 'admin_deactivate', :via => :post |
100 | 104 | |
101 | 105 |
resources :users do |
lib/redmine/twofa/base.rb | ||
---|---|---|
42 | 42 |
end |
43 | 43 | |
44 | 44 |
def confirm_pairing!(code) |
45 |
# make sure an otp is used |
|
45 |
# make sure an otp and not a backup code is used
|
|
46 | 46 |
if verify_otp!(code) |
47 | 47 |
@user.update!(twofa_scheme: scheme_name) |
48 | 48 |
deliver_twofa_paired |
... | ... | |
77 | 77 | |
78 | 78 |
def destroy_pairing_without_verify! |
79 | 79 |
@user.update!(twofa_scheme: nil) |
80 |
backup_codes.delete_all |
|
80 | 81 |
deliver_twofa_unpaired |
81 | 82 |
end |
82 | 83 | |
... | ... | |
98 | 99 |
end |
99 | 100 | |
100 | 101 |
def verify!(code) |
101 |
verify_otp!(code) |
|
102 |
verify_otp!(code) || verify_backup_code!(code)
|
|
102 | 103 |
end |
103 | 104 | |
104 | 105 |
def verify_otp!(code) |
105 | 106 |
raise 'not implemented' |
106 | 107 |
end |
107 | 108 | |
109 |
def verify_backup_code!(code) |
|
110 |
# backup codes are case-insensitive and white-space-insensitive |
|
111 |
code = code.to_s.remove(/[[:space:]]/).downcase |
|
112 |
user_from_code = Token.find_active_user('twofa_backup_code', code) |
|
113 |
# invalidate backup code after usage |
|
114 |
Token.where(user_id: @user.id).find_token('twofa_backup_code', code).try(:delete) |
|
115 |
# make sure the user using the backup code is the same it's been issued to |
|
116 |
return false unless @user.present? && @user == user_from_code |
|
117 |
Mailer.security_notification( |
|
118 |
@user, |
|
119 |
User.current, |
|
120 |
{ |
|
121 |
originator: @user, |
|
122 |
title: :label_my_account, |
|
123 |
message: 'twofa_mail_body_backup_code_used', |
|
124 |
url: { controller: 'my', action: 'account' } |
|
125 |
} |
|
126 |
).deliver |
|
127 |
return true |
|
128 |
end |
|
129 | ||
130 |
def init_backup_codes! |
|
131 |
backup_codes.delete_all |
|
132 |
tokens = [] |
|
133 |
10.times do |
|
134 |
token = Token.create(user_id: @user.id, action: 'twofa_backup_code') |
|
135 |
token.update_columns value: Redmine::Utils.random_hex(6) |
|
136 |
tokens << token |
|
137 |
end |
|
138 |
Mailer.security_notification( |
|
139 |
@user, |
|
140 |
User.current, |
|
141 |
{ |
|
142 |
title: :label_my_account, |
|
143 |
message: 'twofa_mail_body_backup_codes_generated', |
|
144 |
url: { controller: 'my', action: 'account' } |
|
145 |
} |
|
146 |
).deliver |
|
147 |
tokens |
|
148 |
end |
|
149 | ||
150 |
def backup_codes |
|
151 |
Token.where(user_id: @user.id, action: 'twofa_backup_code') |
|
152 |
end |
|
153 | ||
108 | 154 |
# this will only be used on pairing initialization |
109 | 155 |
def init_pairing_view_variables |
110 | 156 |
otp_confirm_view_variables |
public/stylesheets/application.css | ||
---|---|---|
795 | 795 |
.tabular input, .tabular select {max-width:95%} |
796 | 796 |
.tabular textarea {width:95%; resize:vertical;} |
797 | 797 |
input#twofa_code, img#twofa_code { width: 140px; } |
798 |
ul.twofa_backup_codes { list-style-type: none; padding: 0; display: inline-block; } |
|
799 |
ul.twofa_backup_codes li { float: left; } |
|
800 |
ul.twofa_backup_codes li:nth-child(odd) { float: left; clear: left; padding-right: 4em; } |
|
798 | 801 | |
799 | 802 |
.tabular label{ |
800 | 803 |
font-weight: bold; |