Project

General

Profile

Feature #1237 » 0003-Backup-codes-for-2fa-auth-1237.patch

Go MAEDA, 2020-08-26 18:22

View differences:

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;
(21-21/22)