Project

General

Profile

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

Jens Krämer, 2019-08-17 16:05

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
class TwofaBackupCodesController < ApplicationController
2
  include TwofaHelper
3

  
4
  self.main_menu = false
5

  
6
  before_action :require_login, :require_active_twofa
7

  
8
  before_action :twofa_setup
9

  
10
  require_sudo_mode :init
11

  
12
  def init
13
    if @twofa.send_code(controller: 'twofa_backup_codes', action: 'create')
14
      flash[:notice] = l('twofa_code_sent')
15
    end
16
    redirect_to action: 'confirm'
17
  end
18

  
19
  def confirm
20
    @twofa_view = @twofa.otp_confirm_view_variables
21
  end
22

  
23
  def create
24
    if @twofa.verify!(params[:twofa_code].to_s)
25
      if time = @twofa.backup_codes.map(&:created_on).max
26
        flash[:warning] = t('twofa_warning_backup_codes_generated_invalidated', time: format_time(time))
27
      else
28
        flash[:notice] = t('twofa_notice_backup_codes_generated')
29
      end
30
      tokens = @twofa.init_backup_codes!
31
      flash[:twofa_backup_token_ids] = tokens.collect(&:id)
32
      redirect_to action: 'show'
33
    else
34
      flash[:error] = l('twofa_invalid_code')
35
      redirect_to action: 'confirm'
36
    end
37
  end
38

  
39
  def show
40
    # make sure we get only the codes that we should show
41
    tokens = @twofa.backup_codes.where(id: flash[:twofa_backup_token_ids])
42
    # Redmine will show all flash contents at the top of the rendered html
43
    # page, so we need to explicitely delete this here
44
    flash.delete(:twofa_backup_token_ids)
45

  
46
    if tokens.present? && (@created_at = tokens.collect(&:created_on).max) > 5.minutes.ago
47
      @backup_codes = tokens.collect(&:value)
48
    else
49
      flash[:warning] = l('twofa_backup_codes_already_shown', bc_path: my_twofa_backup_codes_init_path)
50
      redirect_to controller: 'my', action: 'account'
51
    end
52
  end
53

  
54
  private
55

  
56
  def twofa_setup
57
    @user = User.current
58
    @twofa = Redmine::Twofa.for_user(@user)
59
  end
60
end
app/controllers/twofa_controller.rb
1 1
class TwofaController < ApplicationController
2
  include TwofaHelper
3

  
2 4
  self.main_menu = false
3 5

  
4 6
  before_action :require_login
......
26 28

  
27 29
  def activate
28 30
    if @twofa.confirm_pairing!(params[:twofa_code].to_s)
29
      flash[:notice] = l('twofa_activated')
31
      flash[:notice] = l('twofa_activated', bc_path: my_twofa_backup_codes_init_path)
30 32
      redirect_to my_account_path
31 33
    else
32 34
      flash[:error] = l('twofa_invalid_code')
......
88 90
      redirect_to my_account_path
89 91
    end
90 92
  end
91

  
92
  def require_active_twofa
93
    Setting.twofa? ? true : deny_access
94
  end
95 93
end
app/helpers/twofa_helper.rb
1
module TwofaHelper
2
  def require_active_twofa
3
    Setting.twofa? ? true : deny_access
4
  end
5
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
1304 1304
  twofa_label_deactivation_confirmation: Zwei-Faktor-Authentifizierung abschalten
1305 1305
  twofa_notice_select: "Bitte wählen Sie Ihr gewünschtes Schema für die Zwei-Faktor-Authentifizierung:"
1306 1306
  twofa_warning_require: Der Administrator fordert Sie dazu auf Zwei-Faktor-Authentifizierung einzurichten.
1307
  twofa_activated: Zwei-Faktor-Authentifizierung erfolgreich eingerichtet.
1307
  twofa_activated: Zwei-Faktor-Authentifizierung erfolgreich eingerichtet. Es ist empfohlen hierzu <a data-method="post" href="%{bc_path}">Backup-Codes zu generieren</a>.
1308 1308
  twofa_deactivated: Zwei-Faktor-Authentifizierung abgeschaltet.
1309 1309
  twofa_mail_body_security_notification_paired: "Zwei-Faktor-Authentifizierung per %{field} eingerichtet."
1310 1310
  twofa_mail_body_security_notification_unpaired: "Zwei-Faktor-Authentifizierung für Ihr Konto abgeschaltet."
1311
  twofa_mail_body_backup_codes_generated: "Neue Backup-Codes für Zwei-Faktor-Authentifizierung generiert."
1312
  twofa_mail_body_backup_code_used: "Ein Backup-Code für Zwei-Faktor-Authentifizierung ist verwendet worden."
1311 1313
  twofa_invalid_code: Der eingegebene Code ist ungültig oder abgelaufen.
1312 1314
  twofa_label_enter_otp: Bitte geben Sie Ihren Code für die Zwei-Faktor-Authentifizierung ein.
1313 1315
  twofa_too_many_tries: Zu viele Versuche.
1314 1316
  twofa_resend_code: Code erneut senden
1315 1317
  twofa_code_sent: Ein Code für die Zwei-Faktor-Authentifizierung wurde Ihnen zugesendet.
1318
  twofa_generate_backup_codes: Backup-Codes generieren
1319
  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?
1320
  twofa_notice_backup_codes_generated: Ihre Backup-Codes wurden generiert.
1321
  twofa_warning_backup_codes_generated_invalidated: Es wurden neue Backup-Codes generiert. Die bestehenden Codes vom %{time} sind nicht mehr gültig.
1322
  twofa_label_backup_codes: Zwei-Faktor-Authentifizierung Backup-Codes
1323
  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.
1324
  twofa_text_backup_codes_created_at: Backup-Codes generiert am %{datetime}.
1325
  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
1284 1284
  twofa_label_deactivation_confirmation: Disable two-factor authentication
1285 1285
  twofa_notice_select: "Please select the two-factor scheme you would like to use:"
1286 1286
  twofa_warning_require: The administrator requires you to enable two-factor authentication.
1287
  twofa_activated: Two-factor authentication successfully enabled.
1287
  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.
1288 1288
  twofa_deactivated: Two-factor authentication disabled.
1289 1289
  twofa_mail_body_security_notification_paired: "Two-factor authentication successfully enabled using %{field}."
1290 1290
  twofa_mail_body_security_notification_unpaired: "Two-factor authentication disabled for your account."
1291
  twofa_mail_body_backup_codes_generated: "New two-factor authentication backup codes generated."
1292
  twofa_mail_body_backup_code_used: "A two-factor authentication backup code has been used."
1291 1293
  twofa_invalid_code: Code is invalid or outdated.
1292 1294
  twofa_label_enter_otp: Please enter your two-factor authentication code.
1293 1295
  twofa_too_many_tries: Too many tries.
1294 1296
  twofa_resend_code: Resend code
1295 1297
  twofa_code_sent: An authentication code has been sent to you.
1298
  twofa_generate_backup_codes: Generate backup codes
1299
  twofa_text_generate_backup_codes_confirmation: This will invalidate all existing backup codes and generate new ones. Would you like to continue?
1300
  twofa_notice_backup_codes_generated: Your backup codes have been generated.
1301
  twofa_warning_backup_codes_generated_invalidated: New backup codes have been generated. Your existing codes from %{time} are now invalid.
1302
  twofa_label_backup_codes: Two-factor authentication backup codes
1303
  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.
1304
  twofa_text_backup_codes_created_at: Backup codes generated %{datetime}.
1305
  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
95 95
  match 'my/twofa/:scheme/deactivate/confirm', :controller => 'twofa', :action => 'deactivate_confirm', :via => :get
96 96
  match 'my/twofa/:scheme/deactivate', :controller => 'twofa', :action => 'deactivate', :via => [:get, :post]
97 97
  match 'my/twofa/select_scheme', :controller => 'twofa', :action => 'select_scheme', :via => :get
98
  match 'my/twofa/backup_codes/init', :controller => 'twofa_backup_codes', :action => 'init', :via => :post
99
  match 'my/twofa/backup_codes/confirm', :controller => 'twofa_backup_codes', :action => 'confirm', :via => :get
100
  match 'my/twofa/backup_codes/create', :controller => 'twofa_backup_codes', :action => 'create', :via => [:get, :post]
101
  match 'my/twofa/backup_codes', :controller => 'twofa_backup_codes', :action => 'show', :via => [:get]
98 102
  match 'users/:user_id/twofa/deactivate', :controller => 'twofa', :action => 'admin_deactivate', :via => :post
99 103

  
100 104
  resources :users do
lib/redmine/twofa/base.rb
23 23
      end
24 24

  
25 25
      def confirm_pairing!(code)
26
        # make sure an otp is used
26
        # make sure an otp and not a backup code is used
27 27
        if verify_otp!(code)
28 28
          @user.update!(twofa_scheme: scheme_name)
29 29
          deliver_twofa_paired
......
58 58

  
59 59
      def destroy_pairing_without_verify!
60 60
        @user.update!(twofa_scheme: nil)
61
        backup_codes.delete_all
61 62
        deliver_twofa_unpaired
62 63
      end
63 64

  
......
79 80
      end
80 81

  
81 82
      def verify!(code)
82
        verify_otp!(code)
83
        verify_otp!(code) || verify_backup_code!(code)
83 84
      end
84 85

  
85 86
      def verify_otp!(code)
86 87
        raise 'not implemented'
87 88
      end
88 89

  
90
      def verify_backup_code!(code)
91
        # backup codes are case-insensitive and white-space-insensitive
92
        code = code.to_s.remove(/[[:space:]]/).downcase
93
        user_from_code = Token.find_active_user('twofa_backup_code', code)
94
        # invalidate backup code after usage
95
        Token.where(user_id: @user.id).find_token('twofa_backup_code', code).try(:delete)
96
        # make sure the user using the backup code is the same it's been issued to
97
        return false unless (@user.present? && @user == user_from_code)
98
        Mailer.security_notification(
99
          @user,
100
          User.current,
101
          {
102
            originator: @user,
103
            title: :label_my_account,
104
            message: 'twofa_mail_body_backup_code_used',
105
            url: { controller: 'my', action: 'account' }
106
          }
107
        ).deliver
108
        return true
109
      end
110

  
111
      def init_backup_codes!
112
        backup_codes.delete_all
113
        tokens = []
114
        10.times do
115
          token = Token.create(user_id: @user.id, action: 'twofa_backup_code')
116
          token.update_columns value: Redmine::Utils.random_hex(6)
117
          tokens << token
118
        end
119
        Mailer.security_notification(
120
          @user,
121
          User.current,
122
          {
123
            title: :label_my_account,
124
            message: 'twofa_mail_body_backup_codes_generated',
125
            url: { controller: 'my', action: 'account' }
126
          }
127
        ).deliver
128
        tokens
129
      end
130

  
131
      def backup_codes
132
        Token.where(user_id: @user.id, action: 'twofa_backup_code')
133
      end
134

  
89 135
      # this will only be used on pairing initialization
90 136
      def init_pairing_view_variables
91 137
        otp_confirm_view_variables
public/stylesheets/application.css
750 750
.tabular input, .tabular select {max-width:95%}
751 751
.tabular textarea {width:95%; resize:vertical;}
752 752
input#twofa_code, img#twofa_code { width: 140px; }
753
ul.twofa_backup_codes { list-style-type: none; padding: 0; display: inline-block; }
754
ul.twofa_backup_codes li { float: left; }
755
ul.twofa_backup_codes li:nth-child(odd) { float: left; clear: left; padding-right: 4em; }
753 756

  
754 757
.tabular label{
755 758
  font-weight: bold;
(17-17/22)