Improved Manage TwoFactorAuthentication

This commit is contained in:
Mia Rose Winter 2024-01-20 20:48:35 +01:00
parent 285a2d173f
commit f910056398
Signed by: miawinter
GPG key ID: 4B6F6A83178F595E
6 changed files with 539 additions and 75 deletions

View file

@ -3,99 +3,114 @@
@using Microsoft.AspNetCore.Http.Features @using Microsoft.AspNetCore.Http.Features
@using Microsoft.AspNetCore.Identity @using Microsoft.AspNetCore.Identity
@using Wave.Data @using Wave.Data
@using Humanizer
@using System.Globalization
@inject UserManager<ApplicationUser> UserManager @inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager @inject SignInManager<ApplicationUser> SignInManager
@inject IdentityUserAccessor UserAccessor @inject IdentityUserAccessor UserAccessor
@inject IdentityRedirectManager RedirectManager @inject IdentityRedirectManager RedirectManager
@inject IStringLocalizer<TwoFactorAuthentication> Localizer
<PageTitle>Two-factor authentication (2FA)</PageTitle> <PageTitle>@Localizer["Title"]</PageTitle>
<StatusMessage /> <StatusMessage />
<h3>Two-factor authentication (2FA)</h3>
@if (canTrack)
{
if (is2faEnabled)
{
if (recoveryCodesLeft == 0)
{
<div class="alert alert-danger">
<strong>You have no recovery codes left.</strong>
<p>You must <a href="Account/Manage/GenerateRecoveryCodes">generate a new set of recovery codes</a> before you can log in with a recovery code.</p>
</div>
}
else if (recoveryCodesLeft == 1)
{
<div class="alert alert-danger">
<strong>You have 1 recovery code left.</strong>
<p>You can <a href="Account/Manage/GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
</div>
}
else if (recoveryCodesLeft <= 3)
{
<div class="alert alert-warning">
<strong>You have @recoveryCodesLeft recovery codes left.</strong>
<p>You should <a href="Account/Manage/GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
</div>
}
if (isMachineRemembered) @* ReSharper disable Html.PathError *@
{ <BoardComponent>
<BoardCardComponent Heading="@Localizer["Title"]">
@if (CanTrack) {
@if (Is2FaEnabled) {
<div class="hyphens-auto" lang="@CultureInfo.CurrentCulture">
@if (RecoveryCodesLeft == 0) {
<Alert Type="Alert.MessageType.Error" CanRemove="false">
<strong>@Localizer["Alert_NoRecoveryCodes_Title"]</strong>
<p>
<a href="Account/Manage/GenerateRecoveryCodes">@Localizer["Alert_NoRecoveryCodes_Message"]</a>
</p>
</Alert>
} else if (RecoveryCodesLeft == 1) {
<Alert Type="Alert.MessageType.Error" CanRemove="false">
<strong>@Localizer["Alert_OneRecoveryCode_Title"]</strong>
<p>
<a href="Account/Manage/GenerateRecoveryCodes">@Localizer["Alert_OneRecoveryCode_Message"]</a>.
</p>
</Alert>
} else if (RecoveryCodesLeft <= 3) {
<Alert Type="Alert.MessageType.Warning" CanRemove="false">
<strong>@string.Format(Localizer["Alert_RecoveryCodes_Title"], RecoveryCodesLeft.ToWords())</strong>
<p>
<a href="Account/Manage/GenerateRecoveryCodes">@Localizer["Alert_RecoveryCodes_Message"]</a>.
</p>
</Alert>
}
</div>
if (IsMachineRemembered) {
<form style="display: inline-block" @formname="forget-browser" @onsubmit="OnSubmitForgetBrowserAsync" method="post"> <form style="display: inline-block" @formname="forget-browser" @onsubmit="OnSubmitForgetBrowserAsync" method="post">
<AntiforgeryToken/> <AntiforgeryToken/>
<button type="submit" class="btn btn-primary">Forget this browser</button> <button type="submit" class="btn btn-primary w-full my-3">
@Localizer["ForgetBrowser_Submit"]
</button>
</form> </form>
} }
<a href="Account/Manage/Disable2fa" class="btn btn-primary">Disable 2FA</a> <div class="flex flex-col gap-2 mt-3">
<a href="Account/Manage/GenerateRecoveryCodes" class="btn btn-primary">Reset recovery codes</a> <a href="Account/Manage/Disable2fa" class="btn btn-primary w-full">
@Localizer["Disable_Label"]
</a>
<a href="Account/Manage/GenerateRecoveryCodes" class="btn btn-primary w-full">
@Localizer["GenerateRecoveryCodes_Label"]
</a>
</div>
} }
<h4>Authenticator app</h4> <div class="flex flex-col gap-2 mt-3">
@if (!hasAuthenticator) <h4 class="text-xl lg:text-2xl font-bold">@Localizer["Authenticator_Title"]</h4>
{ @if (!HasAuthenticator) {
<a href="Account/Manage/EnableAuthenticator" class="btn btn-primary">Add authenticator app</a> <a href="Account/Manage/EnableAuthenticator" class="btn btn-primary w-full">
@Localizer["AuthenticatorEnable_Label"]
</a>
} else {
<a href="Account/Manage/EnableAuthenticator" class="btn btn-primary w-full">
@Localizer["AuthenticatorEnable_Label"]
</a>
<a href="Account/Manage/ResetAuthenticator" class="btn btn-primary w-full">
@Localizer["AuthenticatorReset_Label"]
</a>
} }
else </div>
{ } else {
<a href="Account/Manage/EnableAuthenticator" class="btn btn-primary">Set up authenticator app</a>
<a href="Account/Manage/ResetAuthenticator" class="btn btn-primary">Reset authenticator app</a>
}
}
else
{
<div class="alert alert-danger"> <div class="alert alert-danger">
<strong>Privacy and cookie policy have not been accepted.</strong> <strong>Privacy and cookie policy have not been accepted.</strong>
<p>You must accept the policy before you can enable two factor authentication.</p> <p>You must accept the policy before you can enable two factor authentication.</p>
</div> </div>
} }
</BoardCardComponent>
</BoardComponent>
@* ReSharper restore Html.PathError *@
@code { @code {
private bool canTrack;
private bool hasAuthenticator;
private int recoveryCodesLeft;
private bool is2faEnabled;
private bool isMachineRemembered;
[CascadingParameter] [CascadingParameter]
private HttpContext HttpContext { get; set; } = default!; private HttpContext HttpContext { get; set; } = default!;
protected override async Task OnInitializedAsync() private bool CanTrack { get; set; }
{ private bool HasAuthenticator { get; set; }
private int RecoveryCodesLeft { get; set; }
private bool Is2FaEnabled { get; set; }
private bool IsMachineRemembered { get; set; }
protected override async Task OnInitializedAsync() {
var user = await UserAccessor.GetRequiredUserAsync(HttpContext); var user = await UserAccessor.GetRequiredUserAsync(HttpContext);
canTrack = HttpContext.Features.Get<ITrackingConsentFeature>()?.CanTrack ?? true; CanTrack = HttpContext.Features.Get<ITrackingConsentFeature>()?.CanTrack ?? true;
hasAuthenticator = await UserManager.GetAuthenticatorKeyAsync(user) is not null; HasAuthenticator = await UserManager.GetAuthenticatorKeyAsync(user) is not null;
is2faEnabled = await UserManager.GetTwoFactorEnabledAsync(user); Is2FaEnabled = await UserManager.GetTwoFactorEnabledAsync(user);
isMachineRemembered = await SignInManager.IsTwoFactorClientRememberedAsync(user); IsMachineRemembered = await SignInManager.IsTwoFactorClientRememberedAsync(user);
recoveryCodesLeft = await UserManager.CountRecoveryCodesAsync(user); RecoveryCodesLeft = await UserManager.CountRecoveryCodesAsync(user);
} }
private async Task OnSubmitForgetBrowserAsync() private async Task OnSubmitForgetBrowserAsync() {
{
await SignInManager.ForgetTwoFactorClientAsync(); await SignInManager.ForgetTwoFactorClientAsync();
RedirectManager.RedirectToCurrentPageWithStatus(Localizer["ForgetBrowser_Success"], HttpContext);
RedirectManager.RedirectToCurrentPageWithStatus(
"The current browser has been forgotten. When you login again from this browser you will be prompted for your 2fa code.",
HttpContext);
} }
} }

View file

@ -0,0 +1,62 @@
<div class="alert @GetClass shadow" role="alert">
<div>
@* ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault *@
@switch (Type) {
case MessageType.Information:
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
</svg>
break;
case MessageType.Success:
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
break;
case MessageType.Warning:
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</svg>
break;
case MessageType.Error:
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m0-10.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.75c0 5.592 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.57-.598-3.75h-.152c-3.196 0-6.1-1.25-8.25-3.286Zm0 13.036h.008v.008H12v-.008Z" />
</svg>
break;
}
</div>
<div>
@ChildContent
</div>
@if (CanRemove) {
<button class="btn btn-sm btn-square btn-ghost" onclick="this.parentElement.remove();">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
</svg>
</button>
} else {
<div> </div>
}
</div>
@code {
[Parameter]
public required RenderFragment ChildContent { get; set; }
[Parameter]
public MessageType Type { get; set; } = MessageType.None;
[Parameter]
public bool CanRemove { get; set; } = true;
private string GetClass => Type switch {
MessageType.None => string.Empty,
MessageType.Information => "alert-info",
MessageType.Success => "alert-success",
MessageType.Warning => "alert-warning",
MessageType.Error => "alert-error",
_ => throw new ArgumentOutOfRangeException()
};
public enum MessageType {
None, Information, Success, Warning, Error
}
}

View file

@ -0,0 +1,143 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Title" xml:space="preserve">
<value>Zwei Faktor Authentifizierung (2fa)</value>
</data>
<data name="ForgetBrowser_Submit" xml:space="preserve">
<value>Browser Vergessen</value>
</data>
<data name="ForgetBrowser_Success" xml:space="preserve">
<value>Erfolgreich Browser vergessen, Sie müssen nun ihre 2fa Methode verwenden das nächste mal wenn Sie sich anmelden.</value>
</data>
<data name="GenerateRecoveryCodes_Label" xml:space="preserve">
<value>Neue Wiederherstellungsschlüssel generieren</value>
</data>
<data name="Disable_Label" xml:space="preserve">
<value>2fa Deaktivieren</value>
</data>
<data name="Authenticator_Title" xml:space="preserve">
<value>Authentifizierungsapp</value>
</data>
<data name="AuthenticatorReset_Label" xml:space="preserve">
<value>Authentifizierungsapp zurücksetzen</value>
</data>
<data name="AuthenticatorEnable_Label" xml:space="preserve">
<value>Authentifizierungsapp einrichten</value>
</data>
<data name="Alert_RecoveryCodes_Message" xml:space="preserve">
<value>Sie sollten neue generieren.</value>
</data>
<data name="Alert_RecoveryCodes_Title" xml:space="preserve">
<value>Sie haben {0} Wiederherstellungsschlüssel übrig</value>
</data>
<data name="Alert_OneRecoveryCode_Title" xml:space="preserve">
<value>Sie haben einen Wiederherstellungsschlüssel übrig</value>
</data>
<data name="Alert_NoRecoveryCodes_Title" xml:space="preserve">
<value>Sie haben keine Wiederherstellungsschlüssel übrig</value>
</data>
<data name="Alert_NoRecoveryCodes_Message" xml:space="preserve">
<value>Bevor Sie keine neuen generieren, können Sie sich nicht mit Wiederherstellungsschlüssel anmelden</value>
</data>
<data name="Alert_OneRecoveryCode_Message" xml:space="preserve">
<value>Sie sollten sofot neue generieren</value>
</data>
</root>

View file

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View file

@ -0,0 +1,143 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Title" xml:space="preserve">
<value>Two Factor Authentication (2fa)</value>
</data>
<data name="Alert_NoRecoveryCodes_Title" xml:space="preserve">
<value>You do not have any recovery codes left</value>
</data>
<data name="Alert_NoRecoveryCodes_Message" xml:space="preserve">
<value>Until you generate new ones, you will not be able to log in using recovery codes.</value>
</data>
<data name="Alert_OneRecoveryCode_Message" xml:space="preserve">
<value>Generate new ones immediatly</value>
</data>
<data name="Alert_OneRecoveryCode_Title" xml:space="preserve">
<value>You have one recovery code left</value>
</data>
<data name="Alert_RecoveryCodes_Message" xml:space="preserve">
<value>You should generate new ones.</value>
</data>
<data name="Alert_RecoveryCodes_Title" xml:space="preserve">
<value>You have {0} recovery codes left</value>
</data>
<data name="ForgetBrowser_Submit" xml:space="preserve">
<value>Forget Browser</value>
</data>
<data name="Disable_Label" xml:space="preserve">
<value>Disable 2fa</value>
</data>
<data name="GenerateRecoveryCodes_Label" xml:space="preserve">
<value>Generate new recovery codes</value>
</data>
<data name="Authenticator_Title" xml:space="preserve">
<value>Authenticator App</value>
</data>
<data name="AuthenticatorEnable_Label" xml:space="preserve">
<value>Enable Authenticator</value>
</data>
<data name="AuthenticatorReset_Label" xml:space="preserve">
<value>Reset Authenticator</value>
</data>
<data name="ForgetBrowser_Success" xml:space="preserve">
<value>Successfully forgot Browser, you will need your 2fa to sign in next time.</value>
</data>
</root>

File diff suppressed because one or more lines are too long