Commit b71e80df authored by mh's avatar mh
Browse files

totp refactor

parent dddbd9b6
......@@ -54,6 +54,7 @@ group :development do
end
group :test do
gem "i18n-tasks"
# Adds support for Capybara system testing and selenium driver
gem 'capybara', '>= 2.15', '< 4.0'
gem 'selenium-webdriver'
......
......@@ -47,6 +47,7 @@ GEM
archive-zip (0.12.0)
io-like (~> 0.3.0)
arel (9.0.0)
ast (2.4.0)
bindex (0.7.0)
bootsnap (1.4.4)
msgpack (~> 1.0)
......@@ -82,10 +83,21 @@ GEM
ffi (1.10.0)
globalid (0.4.2)
activesupport (>= 4.2.0)
highline (2.0.2)
http-cookie (1.0.3)
domain_name (~> 0.5)
i18n (1.6.0)
concurrent-ruby (~> 1.0)
i18n-tasks (0.9.29)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
erubi
highline (>= 2.0.0)
i18n
parser (>= 2.2.3.0)
rails-i18n
rainbow (>= 2.2.2, < 4.0)
terminal-table (>= 1.5.1)
io-like (0.3.0)
jbuilder (2.8.0)
activesupport (>= 4.2.0)
......@@ -115,6 +127,8 @@ GEM
nio4r (2.3.1)
nokogiri (1.10.3)
mini_portile2 (~> 2.4.0)
parser (2.6.3.0)
ast (~> 2.4.0)
public_suffix (3.0.3)
puma (3.12.1)
rack (2.0.7)
......@@ -138,12 +152,16 @@ GEM
nokogiri (>= 1.6)
rails-html-sanitizer (1.0.4)
loofah (~> 2.2, >= 2.2.2)
rails-i18n (5.1.3)
i18n (>= 0.7, < 2)
railties (>= 5.0, < 6)
railties (5.2.3)
actionpack (= 5.2.3)
activesupport (= 5.2.3)
method_source
rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0)
rainbow (3.0.0)
rake (12.3.2)
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
......@@ -188,6 +206,8 @@ GEM
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sqlite3 (1.3.13)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thor (0.20.3)
thread_safe (0.3.6)
tilt (2.0.9)
......@@ -198,6 +218,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.6)
unicode-display_width (1.6.0)
web-console (3.7.0)
actionview (>= 5.0)
activemodel (>= 5.0)
......@@ -219,6 +240,7 @@ DEPENDENCIES
capybara (>= 2.15, < 4.0)
chromedriver-helper
coffee-rails (~> 4.2)
i18n-tasks
jbuilder (~> 2.5)
listen (>= 3.0.5, < 3.2)
puma (~> 3.11)
......
......@@ -221,20 +221,20 @@ module ApiBackend
post(['users', 'enable_mail_crypt'], {"password" => pw})
end
def create_totp(name, password)
post(['users', 'create_totp'], {'name' => name, 'password' => password })
def create_totp(name, password, secret)
post(['users', 'create_totp'], {
'name' => name,
'password' => password,
'secret' => secret
})
end
def get_totp_names
get(['users', 'totp_names'])
end
def verify_totp(name, totp)
post(['users', 'verify_totp'], {'name' => name, 'totp' => totp })
end
def delete_totp(name, pw)
post(['users', 'delete_totp'], {'name' => name, 'password' => pw })
def delete_totp(name, password)
post(['users', 'delete_totp'], {'name' => name, 'password' => password })
end
def mail_crypt_token(pw)
......
......@@ -4,32 +4,31 @@ class TfaController < ApplicationController
include ApiBackend
def show
fetch_existing_2fa
end
def create
if params[:name]
def create_totp
if [:name, :password, :secret].all?{|e| params[e].present? }
begin
res = api.create_totp(params[:name], params[:password])
totp = ROTP::TOTP.new(res['secret'], issuer: "immerda.ch")
url = totp.provisioning_uri(current_user)
@new_totp_qr = RQRCode::QRCode.new(url, :size => 12, :level => :h)
@new_totp_secret = res['secret']
@new_totp_name = res['name']
flash[:notice] = nil
api.create_totp(params[:name], params[:password], params[:secret])
flash[:notice] = :success
rescue ApiBackend::ApiError
flash[:notice] = :failed
return render 'new_totp'
end
else
flash[:notice] = t(:name_must_be_set)
flash[:notice] = t(:all_fields_required)
return render 'new_totp'
end
fetch_existing_2fa
render 'show'
redirect_to tfa_path
end
def new_totp
render 'new_totp'
end
def delete_totp
if params['name'].present?
if params[:name].present?
begin
api.delete_totp(params['name'], params['password'])
api.delete_totp(params[:name], params[:password])
flash[:notice] = :success
rescue ApiBackend::ApiError
flash[:notice] = :failed
......@@ -40,23 +39,27 @@ class TfaController < ApplicationController
redirect_to tfa_path
end
def verify_totp
if params['name'].present?
begin
api.verify_totp(params[:name], params[:totp])
rescue ApiBackend::ApiError
flash[:notice] = :failed
end
else
flash[:notice] = t(:name_must_be_set)
end
redirect_to tfa_path
protected
helper_method :tfa_activated?, :totp_qr, :totp_secret, :existing_totps
def tfa_activated?
existing_totps.present?
end
private
def fetch_existing_2fa
res = api.get_totp_names
@existing_totps = res['totps']
def totp_qr
totp = ROTP::TOTP.new(totp_secret, issuer: "immerda.ch")
url = totp.provisioning_uri(current_user)
RQRCode::QRCode.new(url, :size => 12, :level => :h)
end
def totp_secret
@totp_secret ||= (params[:secret] || ROTP::Base32.random_base32)
end
def existing_totps
@existing_totps ||= begin
res = api.get_totp_names
res['totps']
end
rescue ApiBackend::ApiError
flash[:notice] = :failed
end
......
<p>
<h4><%= t(:your_new_totp_token) %></h4>
<div class="qr-field"><%= raw(@new_totp_qr.as_html) %></div>
<br/>
Secret: <%= @new_totp_secret %>
</p>
<p>
<h4><%= t(:activate_your_totp) %></h4>
<%= t(:activate_your_totp_long) %>
<br/>
<%= form_tag("/tfa/#{URI.encode(@new_totp_name)}/verify_totp", method: "post") do %>
<%= label_tag(:totp, t(:totp)) %><%= text_field_tag(:totp, nil, autofocus: true, autocomplete: :off) %>
<%= submit_tag t(:verify) %>
<% end -%>
</p>
<h3><%= @page_title = t(:add_totp) %></h3>
<p>
<%= t(:totp_description) %> <%= t(:add_your_totp_long) %> <%= link_to 'FreeOTP', 'https://freeotp.github.io/', target: '_new' %>
</p>
<h4><%= t(:your_new_totp_token) %></h4>
<p>
<%= t(:your_new_totp_token_long) %>
<div class="qr-field"><%= raw(totp_qr.as_html) %></div>
<br/>
Secret: <%= totp_secret %>
</p>
<p>
<h4><%= t(:finalize_your_totp) %></h4>
<p>
<%= t(:finalize_your_totp_long) %>
</p>
<%= form_tag(tfa_totp_path, method: "post") do %>
<%= hidden_field('', :kind, :value => 'totp') %>
<%= hidden_field('', :secret, :value => totp_secret) %>
<table>
<tr><td>
<%= label_tag(:name, (t :name)) %>
</td><td>
<%= text_field_tag(:name, '', value: (existing_totps.any?{|t| t == 'default' } ? "default-#{Time.now.strftime("%Y%m%d%H%M")}" : 'default'), maxlength: 255, autocomplete: 'off') %>
</td></tr>
<tr><td>
<%= label_tag(:pass, t(:your_main_pw)) %>
</td><td>
<%= password_field_tag(:password, nil, placeholder: t(:enter_your_current_pw)) %>
</td></tr>
<tr><td>
<%= label_tag(:totp, t(:current_totp)) %>
</td><td>
<%= text_field_tag(:totp, nil, autofocus: true, autocomplete: :off) %>
</td></tr>
</table>
<%= submit_tag(t(:create)) %>
<%= link_to t(:back), tfa_path %>
<% end -%>
<h3><%= @page_title = t(:tfa_enable) %></h3>
<p>
<%= t(:status) %>: <%= @existing_totps.values.any? ? "<b>#{t(:activated)}</b>".html_safe : t(:disabled) %>
<%= t(:status) %>: <%= tfa_activated? ? "<b>#{t(:activated)}</b>".html_safe : t(:disabled) %> | <%= link_to(t(:back), root_path) %>
</p>
<% if @new_totp_qr -%>
<%= render 'new_totp' %>
<%= link_to t(:back), tfa_path %>
<% else %>
<p>
<%= t :tfa_enable_help %>
</p>
<h4><%= t(:totp_enable) %></h4>
<p>
<%= t :totp_enable_help %> <%= link_to 'FreeOTP', 'https://freeotp.github.io/', target: '_new' %>
</p>
<% has_default = false if @existing_totps.present? -%>
<hr />
<h4><%= t(:existing_totps) %></h4>
<p>
<%= t(:tfa_enable_help) %>
</p>
<h4><%= t(:totp) %></h4>
<p>
<%= t(:totp_description) %>
</p>
<h5><%= t(:my_totps) %></h5>
<% if existing_totps.present? -%>
<ul>
<% @existing_totps.each do |name, verified|
has_default = true if name == 'default' -%>
<% existing_totps.each do |name| -%>
<li>
<%= name %>&nbsp;
<% if verified -%>
<%= t(:active) %>
<% else -%>
<%= t(:not_active) %> - <a href="#" onclick="toggle_visibility('activate_form_<%= URI.encode(name) %>')"> <%= t(:verify_to_activate) %></a>&nbsp;
<div id="activate_form_<%= URI.encode(name) %>" style="display:none">
<%= t(:activate_your_totp_long) %>
<br/>
<%= form_tag("/tfa/#{URI.encode(name)}/verify_totp", method: "post") do %>
<%= label_tag(:totp, t(:totp)) %><%= text_field_tag(:totp, nil, autofocus: true, autocomplete: :off) %>
<%= submit_tag t(:verify) %>
<% end -%>
</div>
<% end -%>
<a href="#" onclick="toggle_visibility('delete_form_<%= URI.encode(name) %>')"/><%= t(:delete) %></a>
<div id="delete_form_<%= URI.encode(name) %>" style="display:none">
<br />
<%= form_tag("/tfa/#{URI.encode(name)}/delete_totp", method: "post") do %>
<%= form_tag("/tfa/totp/#{URI.encode(name)}/delete", method: "post") do %>
<%= label_tag(:pass, t(:your_main_pw)) %><%= password_field_tag(:password, nil, placeholder: t(:enter_your_current_pw)) %>
<%= submit_tag t(:delete) %>
<% end -%>
......@@ -50,26 +27,11 @@
</li>
<% end -%>
</ul>
<hr />
<p>
<b> <%= t(:totp_enable) %>:</b>
</p>
<%= form_tag(tfa_path, method: "post") do %>
<table>
<tr><td>
<%= label_tag(:name, (t :name)) %>
</td><td>
<%= text_field_tag(:name, '', value: (has_default ? "default-#{Time.now.strftime("%Y%m%d%H%M")}" : 'default'), maxlength: 255, autocomplete: 'off') %>
</td></tr>
<tr><td>
<%= label_tag(:pass, t(:your_main_pw)) %>
</td><td>
<%= password_field_tag(:password, nil, placeholder: t(:enter_your_current_pw)) %>
</td></tr>
</table>
<%= hidden_field('', :kind, :value => 'totp') %>
<%= submit_tag(t(:create)) %>
<%= link_to t(:back), root_path %>
<% end %>
<% else -%>
<ul>
<li><%= t(:no_existing_totps) %></li>
</ul>
<% end -%>
<p>
<%= link_to(t(:add_totp), tfa_totp_path) %>
</p>
# i18n-tasks finds and manages missing and unused translations: https://github.com/glebm/i18n-tasks
# The "main" locale.
base_locale: en
## All available locales are inferred from the data by default. Alternatively, specify them explicitly:
# locales: [es, fr]
## Reporting locale, default: en. Available: en, ru.
# internal_locale: en
# Read and write translations.
data:
## Translations are read from the file system. Supported format: YAML, JSON.
## Provide a custom adapter:
# adapter: I18n::Tasks::Data::FileSystem
# Locale files or `File.find` patterns where translations are read from:
read:
## Default:
# - config/locales/%{locale}.yml
## More files:
# - config/locales/**/*.%{locale}.yml
# Locale files to write new keys to, based on a list of key pattern => file rules. Matched from top to bottom:
# `i18n-tasks normalize -p` will force move the keys according to these rules
write:
## For example, write devise and simple form keys to their respective files:
# - ['{devise, simple_form}.*', 'config/locales/\1.%{locale}.yml']
## Catch-all default:
# - config/locales/%{locale}.yml
# External locale data (e.g. gems).
# This data is not considered unused and is never written to.
external:
## Example (replace %#= with %=):
# - "<%#= %x[bundle show vagrant].chomp %>/templates/locales/%{locale}.yml"
## Specify the router (see Readme for details). Valid values: conservative_router, pattern_router, or a custom class.
# router: conservative_router
yaml:
write:
# do not wrap lines at 80 characters
line_width: -1
## Pretty-print JSON:
# json:
# write:
# indent: ' '
# space: ' '
# object_nl: "\n"
# array_nl: "\n"
# Find translate calls
search:
## Paths or `File.find` patterns to search in:
# paths:
# - app/
## Root directories for relative keys resolution.
# relative_roots:
# - app/controllers
# - app/helpers
# - app/mailers
# - app/presenters
# - app/views
## Files or `File.fnmatch` patterns to exclude from search. Some files are always excluded regardless of this setting:
## %w(*.jpg *.png *.gif *.svg *.ico *.eot *.otf *.ttf *.woff *.woff2 *.pdf *.css *.sass *.scss *.less *.yml *.json)
exclude:
- app/assets/images
- app/assets/fonts
- app/assets/videos
## Alternatively, the only files or `File.fnmatch patterns` to search in `paths`:
## If specified, this settings takes priority over `exclude`, but `exclude` still applies.
# only: ["*.rb", "*.html.slim"]
## If `strict` is `false`, guess usages such as t("categories.#{category}.title"). The default is `true`.
# strict: true
## Multiple scanners can be used. Their results are merged.
## The options specified above are passed down to each scanner. Per-scanner options can be specified as well.
## See this example of a custom scanner: https://github.com/glebm/i18n-tasks/wiki/A-custom-scanner-example
## Translation Services
# translation:
# # Google Translate
# # Get an API key and set billing info at https://code.google.com/apis/console to use Google Translate
# google_translate_api_key: "AbC-dEf5"
# # DeepL Pro Translate
# # Get an API key and subscription at https://www.deepl.com/pro to use DeepL Pro
# deepl_api_key: "48E92789-57A3-466A-9959-1A1A1A1A1A1A"
## Do not consider these keys missing:
# ignore_missing:
# - 'errors.messages.{accepted,blank,invalid,too_short,too_long}'
# - '{devise,simple_form}.*'
## Consider these keys used:
# ignore_unused:
# - 'activerecord.attributes.*'
# - '{devise,kaminari,will_paginate}.*'
# - 'simple_form.{yes,no}'
# - 'simple_form.{placeholders,hints,labels}.*'
# - 'simple_form.{error_notification,required}.:'
## Exclude these keys from the `i18n-tasks eq-base' report:
# ignore_eq_base:
# all:
# - common.ok
# fr,es:
# - common.brand
## Exclude these keys from the `i18n-tasks check-consistent-interpolations` report:
# ignore_inconsistent_interpolations:
# - 'activerecord.attributes.*'
## Ignore these keys completely:
# ignore:
# - kaminari.*
## Sometimes, it isn't possible for i18n-tasks to match the key correctly,
## e.g. in case of a relative key defined in a helper method.
## In these cases you can use the built-in PatternMapper to map patterns to keys, e.g.:
#
# <%# I18n::Tasks.add_scanner 'I18n::Tasks::Scanners::PatternMapper',
# only: %w(*.html.haml *.html.slim),
# patterns: [['= title\b', '.page_title']] %>
#
# The PatternMapper can also match key literals via a special %{key} interpolation, e.g.:
#
# <%# I18n::Tasks.add_scanner 'I18n::Tasks::Scanners::PatternMapper',
# patterns: [['\bSpree\.t[( ]\s*%{key}', 'spree.%{key}']] %>
......@@ -14,6 +14,14 @@ de:
edit: "Bearbeiten"
deleted: "Gelöscht"
verify: "Verifizieren"
action: "Aktion"
admin: 'Admin'
administration: 'administration'
account: 'Konto'
accounts: 'Konti'
acl: 'Acl'
alias: 'Alias'
mail_crypt_settings: "Geschützte Mailablage"
mail_crypt_short_help: "Bestimme wie deine Mails auf unserem Server gespeichert werden."
......@@ -60,7 +68,7 @@ de:
undelete: "wiederherstellen"
app_name: "Verwendungszweck"
your_app_pw_is: "Dein neues Applikations Passwort lautet: "
create: "erstellen"
create: "Erstellen"
app_passwords: "Applikations Passwörter"
app_passwords_short_help: "Wenn du dein Konto mit einem Programm verwendest (z.B. Mailprogramm), kannst du mehrere Applikations Passwörter erstellen."
app_passwords_help: "Um mit externen Programmen einzuloggen, kannst du ein zusätzliches Applikations Passwort erstellen. Damit du nicht dein Hauptpasswort in Thunderbird, oder dem Phone speichern musst, kannst du hier ein spezifisches Passwort generieren. Gib dazu den Verwendungszweck, sowie dein Hauptpasswort ein und klicke auf generieren. Das Passwort wird dir danach angezeigt und du kannst es kopieren. Ebenfalls kannst du hier bestehende Applikations Passwörter löschen."
......@@ -106,16 +114,24 @@ de:
tfa: '2fa'
status: "Status"
activated: 'aktiviert'
deactived: 'deaktiviert'
disabled: 'deaktiviert'
tfa_short_help: 'Zwei-Faktor Authentifzierung verwalten'
tfa_enable: 'Zwei-Faktor Authentifzierung aktivieren'
tfa_enable_help: "Du kannst einen zweiten Faktor für eine erfolgreiche Authentifizierung an deinem Account festlegen. Dies stellt einen zusätzlichen Schutz für deinen Account dar. BEACHTE: Da die Zwei-Faktor-Authentifizierung nur bei der Authentifizierung über unser Portal funktioniert, musst du bspw. für die Authentifizierung von Email-Programmen, wie Thunderbird, Applikations Passwörter erstellen. Sobald du Zwei-Faktor-Authentifizierung aktivierst, wird dein Hauptpasswort nur noch auf dem Portallogin funktionieren. Wir empfehlen dir eine Wiederherstellungs Email festzulegen, so dass wir bspw. bei Verlust deines zweiten Faktors diese Anforderung wieder abschalten können. Du kannst für deinen Account mehrere Zweit-Faktoren festlegen, benötigt wird jeweils ein richtiger."
totp_enable: 'Zeitbasierte OneTimePassword'
totp_enable_help: 'Zeitbasierte OneTimePassword (TOTP) sind ein zweiter Faktor, der auf der aktuellen Zeit und einem geteilten Passwort basiert. Üblicherweise kannst du eine Mobile App verwenden, welche dir das aktuell gültige Passwort anzeigt. Unsere TOTPs sind kompatibel mit RFC6263. Eine populäre und freie mobile App für TOTPs ist FreeOTP, welche in den Appstores von Android und iOS verfügbar ist. Wie auch auf der folgenden Webseite:'
your_new_totp_token: 'Dein neuer TOTP Token'
tfa_enable_help: "Du kannst einen zweiten Faktor für eine erfolgreiche Authentifizierung an deinem Account festlegen. Dies stellt einen zusätzlichen Schutz für deinen Account dar. BEACHTE: Da die Zwei-Faktor-Authentifizierung nur bei der Authentifizierung über unser Portal funktioniert, musst du bspw. für die Authentifizierung von Email-Programmen, wie Thunderbird, Applikations Passwörter erstellen. Sobald du Zwei-Faktor-Authentifizierung aktivierst, wird dein Hauptpasswort nur noch auf dem Portallogin funktionieren. Um Zwei-Faktor Authentifzierung zu nutzen musst du eine Wiederherstellungs Email festgelegt haben, so dass wir bspw. bei Verlust deines zweiten Faktors diese Anforderung wieder abschalten können. Du kannst für deinen Account mehrere Zweit-Faktoren festlegen, benötigt wird jeweils ein richtiger."
totp: 'Zeitbasierte OneTimePasswords'
my_totps: 'Meine TOTPs'
totp_description: 'Zeitbasierte OneTimePassword (TOTP) sind ein zweiter Faktor, der auf der aktuellen Zeit und einem geteilten Passwort basiert. Üblicherweise kannst du eine Mobile App verwenden, welche dir das aktuell gültige Passwort anzeigt.'
add_totp: 'Zeitbasiertes OneTimePassword hinzufügen'
add_your_totp_long: 'Unsere TOTPs sind kompatibel mit RFC6263. Eine populäre und freie mobile App für TOTPs ist FreeOTP, welche in den Appstores von Android und iOS verfügbar ist. Wie auch auf der folgenden Webseite:'
your_new_totp_token: 'Dein neues zeitbasiertes OneTimePassword Token'
your_new_totp_token_long: 'Scanne den folgenden QR Code mit deiner TOTP Applikation oder konfiguriere das untenstehende Secret in deiner Applikation.'
existing_totps: 'Bestehende TOTPs'
activate_your_totp: 'Aktiviere dein TOTP Token'
activate_your_totp_long: 'Damit wir sicherstellen können, dass dein neuer TOTP auch richtig funktioniert, musst du ihn ein erstes mal verifizieren. Erst nach einer erfolgreichen Verifikation aktivieren wir diesen TOTP Token. Um die Verifikation durchzuführen musst du den den aktuellen Wert des TOTP Tokens eingeben'
no_existing_totps: 'Keine bestehende zeitbasierten OneTimePasswords'
add_a_totp: 'Ein zeitbasiertes OneTimePassword Token hinzufügen'
finalize_your_totp: 'Zeitbasiertes OneTimePassword fertigstellen'
finalize_your_totp_long: 'Damit wir sicherstellen können, dass dein neuer TOTP auch richtig funktioniert, musst du ihn ein erstes mal verifizieren. Erst nach einer erfolgreichen Verifikation aktivieren wir diesen TOTP Token. Um die Verifikation durchzuführen musst du den den aktuellen Wert des TOTP Tokens eingeben'
current_totp: 'Aktueller TOTP Wert'
all_fields_required: 'All Felder werden benötigt'
auth_wait: "Bitte warte während wir deine Eingabe überprüfen..."
enter_captcha: "Bitte kopiere diese Buchstaben und Zahlen in das untere Feld."
......@@ -343,6 +359,8 @@ de:
kb: KB
mb: MB
tb: TB
eb: EB
pb: PB
percentage:
format:
delimiter: ''
......
......@@ -14,6 +14,14 @@ en:
edit: "Edit"
deleted: "Deleted"
verify: "verify"
action: 'action'
admin: 'Admin'
administration: 'administration'
account: 'Account'
accounts: 'Accounts'
acl: 'Acl'
alias: 'Alias'
mail_crypt_settings: "Secured Mailstorage"
mail_crypt_short_help: "Decide how your mails are stored on our servers."
......@@ -106,16 +114,24 @@ en:
tfa: '2fa'
status: 'Status'
activated: 'activated'
deactived: 'deactivated'
tfa_short_help: 'Manage TwoFactor Authentication'
tfa_enable: 'Enable TwoFactor Authentication'
tfa_enable_help: "You can define a second factor that is required to successfully authenticate your account. This provides an additional layer of protection for your account. NOTE: As two factor authentication only works when authenticating against our user portal, you will need to create application passwords e.g. for your mail programm like thunderbird. Once you enable TwoFactor Authentication your main password will only work on the portal login. We recommend you to set a recovery email address for your account, so in case of loosing your second factor, we can remove the two factor authentication. You can define multiple second factors, though to successfully login only one valid is required."
totp_enable: 'Timebased OneTimePasswords'
totp_enable_help: "Timebased onetimepasswords (TOTP) are a second factor that is based on the current time and a shared secret. Usually you can use a mobile app, which will show you the current valid password. We are providing TOTPs which are compatible with RFC6263. A popular and free mobile application for TOTPs is FreeOTP, which is available on Android's and iOS' appstores or on the following website:"
your_new_totp_token: 'Your new TOTP token'
disabled: 'disabled'
tfa_short_help: 'Manage Two-Factor Authentication'
tfa_enable: 'Enable Two-Factor Authentication'
tfa_enable_help: "You can define a second factor that is required to successfully authenticate your account. This provides an additional layer of protection for your account. NOTE: As two factor authentication only works when authenticating against our user portal, you will need to create application passwords e.g. for your mail programm like thunderbird. Once you enable Two-Factor Authentication your main password will only work on the portal login. We require you to set a recovery email address for your account, so in case of loosing your second factor, we can remove the two factor authentication. You can define multiple second factors, though to successfully login only one valid is required."
totp: 'Timebased OneTimePasswords'
my_totps: 'My TOTPs'
totp_description: "Timebased OneTimePasswords (TOTP) are a second factor that is based on the current time and a shared secret. Usually you can use a mobile app, which will show you the current valid password."
add_totp: 'Add a Timebased OneTimePassword'
add_your_totp_long: "We are providing TOTPs which are compatible with RFC6263. A popular and free mobile application for TOTPs is FreeOTP, which is available on Android's and iOS' appstores or on the following website:"
your_new_totp_token: 'Your new Timebased OneTimePassword token'
your_new_totp_token_long: 'Either scan the following QR code with your Two-Factor application or add the shown secret to your application.'