Project

General

Profile

Feature #1237 » 0003-Backup-codes-for-2-factor-authentication.patch

Felix Schäfer, 2018-01-02 18:42

View differences:

app/controllers/account_controller.rb
257 257
    # set locale for the twofa user
258 258
    set_localization(@user)
259 259

  
260
    # set the requesting IP of the twofa user (e.g. for security notifications)
261
    @user.remote_ip = request.remote_ip
262

  
260 263
    @twofa = Redmine::Twofa.for_user(@user)
261 264
  end
262 265

  
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
40 40
  add_action :recovery,  max_instances: 1,  validity_time: Proc.new { Token.validity_time }
41 41
  add_action :register,  max_instances: 1,  validity_time: Proc.new { Token.validity_time }
42 42
  add_action :session,   max_instances: 10, validity_time: nil
43
  add_action :twofa_backup_code, max_instances: 10, validity_time: nil
43 44

  
44 45
  def generate_new_token
45 46
    self.value = Token.generate_token_value
app/views/my/account.html.erb
33 33
    <% if @user.twofa_active? %>
34 34
      <%=l 'twofa_currently_active', twofa_scheme_name: l("twofa__#{@user.twofa_scheme}__name") -%><br/>
35 35
      <%= link_to l('button_disable'), { controller: 'twofa', action: 'deactivate_init', scheme: @user.twofa_scheme }, method: :post -%><br/>
36
      <%= 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 } -%>
36 37
    <% else %>
37 38
      <% Redmine::Twofa.available_schemes.each do |s| %>
38 39
        <%= 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
1251 1251
  twofa_label_deactivation_confirmation: Zwei-Faktor-Authentifizierung abschalten
1252 1252
  twofa_notice_select: "Bitte wählen Sie Ihr gewünschtes Schema für die Zwei-Faktor-Authentifizierung:"
1253 1253
  twofa_warning_require: Der Administrator fordert Sie dazu auf Zwei-Faktor-Authentifizierung einzurichten.
1254
  twofa_activated: Zwei-Faktor-Authentifizierung erfolgreich eingerichtet.
1254
  twofa_activated: Zwei-Faktor-Authentifizierung erfolgreich eingerichtet. Es ist empfohlen hierzu <a data-method="post" href="%{bc_path}">Backup-Codes zu generieren</a>.
1255 1255
  twofa_deactivated: Zwei-Faktor-Authentifizierung abgeschaltet.
1256 1256
  twofa_mail_body_security_notification_paired: "Zwei-Faktor-Authentifizierung per %{field} eingerichtet."
1257 1257
  twofa_mail_body_security_notification_unpaired: "Zwei-Faktor-Authentifizierung für Ihr Konto abgeschaltet."
1258
  twofa_mail_body_backup_codes_generated: "Neue Backup-Codes für Zwei-Faktor-Authentifizierung generiert."
1259
  twofa_mail_body_backup_code_used: "Ein Backup-Code für Zwei-Faktor-Authentifizierung ist verwendet worden."
1258 1260
  twofa_invalid_code: Der eingegebene Code ist ungültig oder abgelaufen.
1259 1261
  twofa_label_enter_otp: Bitte geben Sie Ihren Code für die Zwei-Faktor-Authentifizierung ein.
1260 1262
  twofa_too_many_tries: Zu viele Versuche.
1261 1263
  twofa_resend_code: Code erneut senden
1262 1264
  twofa_code_sent: Ein Code für die Zwei-Faktor-Authentifizierung wurde Ihnen zugesendet.
1265
  twofa_generate_backup_codes: Backup-Codes generieren
1266
  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?
1267
  twofa_notice_backup_codes_generated: Ihre Backup-Codes wurden generiert.
1268
  twofa_warning_backup_codes_generated_invalidated: Es wurden neue Backup-Codes generiert. Die bestehenden Codes vom %{time} sind nicht mehr gültig.
1269
  twofa_label_backup_codes: Zwei-Faktor-Authentifizierung Backup-Codes
1270
  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.
1271
  twofa_text_backup_codes_created_at: Backup-Codes generiert am %{datetime}.
1272
  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
1233 1233
  twofa_label_deactivation_confirmation: Disable two-factor authentication
1234 1234
  twofa_notice_select: "Please select the two-factor scheme you would like to use:"
1235 1235
  twofa_warning_require: The administrator requires you to enable two-factor authentication.
1236
  twofa_activated: Two-factor authentication successfully enabled.
1236
  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.
1237 1237
  twofa_deactivated: Two-factor authentication disabled.
1238 1238
  twofa_mail_body_security_notification_paired: "Two-factor authentication successfully enabled using %{field}."
1239 1239
  twofa_mail_body_security_notification_unpaired: "Two-factor authentication disabled for your account."
1240
  twofa_mail_body_backup_codes_generated: "New two-factor authentication backup codes generated."
1241
  twofa_mail_body_backup_code_used: "A two-factor authentication backup code has been used."
1240 1242
  twofa_invalid_code: Code is invalid or outdated.
1241 1243
  twofa_label_enter_otp: Please enter your two-factor authentication code.
1242 1244
  twofa_too_many_tries: Too many tries.
1243 1245
  twofa_resend_code: Resend code
1244 1246
  twofa_code_sent: An authentication code has been sent to you.
1247
  twofa_generate_backup_codes: Generate backup codes
1248
  twofa_text_generate_backup_codes_confirmation: This will invalidate all existing backup codes and generate new ones. Would you like to continue?
1249
  twofa_notice_backup_codes_generated: Your backup codes have been generated.
1250
  twofa_warning_backup_codes_generated_invalidated: New backup codes have been generated. Your existing codes from %{time} are now invalid.
1251
  twofa_label_backup_codes: Two-factor authentication backup codes
1252
  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.
1253
  twofa_text_backup_codes_created_at: Backup codes generated %{datetime}.
1254
  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
93 93
  match 'my/twofa/:scheme/deactivate/confirm', :controller => 'twofa', :action => 'deactivate_confirm', :via => :get
94 94
  match 'my/twofa/:scheme/deactivate', :controller => 'twofa', :action => 'deactivate', :via => [:get, :post]
95 95
  match 'my/twofa/select_scheme', :controller => 'twofa', :action => 'select_scheme', :via => :get
96
  match 'my/twofa/backup_codes/init', :controller => 'twofa_backup_codes', :action => 'init', :via => :post
97
  match 'my/twofa/backup_codes/confirm', :controller => 'twofa_backup_codes', :action => 'confirm', :via => :get
98
  match 'my/twofa/backup_codes/create', :controller => 'twofa_backup_codes', :action => 'create', :via => [:get, :post]
99
  match 'my/twofa/backup_codes', :controller => 'twofa_backup_codes', :action => 'show', :via => [:get]
96 100
  match 'users/:user_id/twofa/deactivate', :controller => 'twofa', :action => 'admin_deactivate', :via => :post
97 101

  
98 102
  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
......
57 57

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

  
......
77 78
      end
78 79

  
79 80
      def verify!(code)
80
        verify_otp!(code)
81
        verify_otp!(code) || verify_backup_code!(code)
81 82
      end
82 83

  
83 84
      def verify_otp!(code)
84 85
        raise 'not implemented'
85 86
      end
86 87

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

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

  
127
      def backup_codes
128
        Token.where(user_id: @user.id, action: 'twofa_backup_code')
129
      end
130

  
87 131
      # this will only be used on pairing initialization
88 132
      def init_pairing_view_variables
89 133
        otp_confirm_view_variables
public/stylesheets/application.css
672 672
.tabular input, .tabular select {max-width:95%}
673 673
.tabular textarea {width:95%; resize:vertical;}
674 674
input#twofa_code, img#twofa_code { width: 140px; }
675
ul.twofa_backup_codes { list-style-type: none; padding: 0; display: inline-block; }
676
ul.twofa_backup_codes li { float: left; }
677
ul.twofa_backup_codes li:nth-child(odd) { float: left; clear: left; padding-right: 4em; }
675 678

  
676 679
.tabular label{
677 680
  font-weight: bold;
(3-3/22)