two-factor other methods switching and send email

This commit is contained in:
Kyle Spearrin 2017-06-29 11:22:06 -04:00
parent 56075cb7d9
commit 74fba486bd
11 changed files with 208 additions and 188 deletions

View file

@ -51,7 +51,7 @@ namespace Bit.Android
Console.WriteLine("A OnCreate"); Console.WriteLine("A OnCreate");
Window.SetSoftInputMode(SoftInput.StateHidden); Window.SetSoftInputMode(SoftInput.StateHidden);
//Window.AddFlags(WindowManagerFlags.Secure); Window.AddFlags(WindowManagerFlags.Secure);
var appIdService = Resolver.Resolve<IAppIdService>(); var appIdService = Resolver.Resolve<IAppIdService>();
var authService = Resolver.Resolve<IAuthService>(); var authService = Resolver.Resolve<IAuthService>();

View file

@ -234,6 +234,7 @@ namespace Bit.Android
container.RegisterSingleton<ICipherApiRepository, CipherApiRepository>(); container.RegisterSingleton<ICipherApiRepository, CipherApiRepository>();
container.RegisterSingleton<ISettingsRepository, SettingsRepository>(); container.RegisterSingleton<ISettingsRepository, SettingsRepository>();
container.RegisterSingleton<ISettingsApiRepository, SettingsApiRepository>(); container.RegisterSingleton<ISettingsApiRepository, SettingsApiRepository>();
container.RegisterSingleton<ITwoFactorApiRepository, TwoFactorApiRepository>();
// Other // Other
container.RegisterSingleton(CrossSettings.Current); container.RegisterSingleton(CrossSettings.Current);

View file

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Bit.App.Models.Api;
namespace Bit.App.Abstractions
{
public interface ITwoFactorApiRepository
{
Task<ApiResult> PostSendEmailLoginAsync(TwoFactorEmailRequest requestObj);
}
}

View file

@ -35,6 +35,7 @@
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Abstractions\Repositories\ITwoFactorApiRepository.cs" />
<Compile Include="Abstractions\Repositories\ISettingsApiRepository.cs" /> <Compile Include="Abstractions\Repositories\ISettingsApiRepository.cs" />
<Compile Include="Abstractions\Repositories\IAccountsApiRepository.cs" /> <Compile Include="Abstractions\Repositories\IAccountsApiRepository.cs" />
<Compile Include="Abstractions\Repositories\IDeviceApiRepository.cs" /> <Compile Include="Abstractions\Repositories\IDeviceApiRepository.cs" />
@ -97,6 +98,7 @@
<Compile Include="Models\Api\Request\DeviceTokenRequest.cs" /> <Compile Include="Models\Api\Request\DeviceTokenRequest.cs" />
<Compile Include="Models\Api\Request\FolderRequest.cs" /> <Compile Include="Models\Api\Request\FolderRequest.cs" />
<Compile Include="Models\Api\Request\DeviceRequest.cs" /> <Compile Include="Models\Api\Request\DeviceRequest.cs" />
<Compile Include="Models\Api\Request\TwoFactorEmailRequest.cs" />
<Compile Include="Models\Api\Request\RegisterRequest.cs" /> <Compile Include="Models\Api\Request\RegisterRequest.cs" />
<Compile Include="Models\Api\Request\LoginRequest.cs" /> <Compile Include="Models\Api\Request\LoginRequest.cs" />
<Compile Include="Models\Api\Request\PasswordHintRequest.cs" /> <Compile Include="Models\Api\Request\PasswordHintRequest.cs" />
@ -135,7 +137,6 @@
<Compile Include="Pages\HomePage.cs" /> <Compile Include="Pages\HomePage.cs" />
<Compile Include="Pages\Lock\BaseLockPage.cs" /> <Compile Include="Pages\Lock\BaseLockPage.cs" />
<Compile Include="Pages\Lock\LockPasswordPage.cs" /> <Compile Include="Pages\Lock\LockPasswordPage.cs" />
<Compile Include="Pages\TwoFactorMethodsPage.cs" />
<Compile Include="Pages\LoginTwoFactorPage.cs" /> <Compile Include="Pages\LoginTwoFactorPage.cs" />
<Compile Include="Pages\PasswordHintPage.cs" /> <Compile Include="Pages\PasswordHintPage.cs" />
<Compile Include="Pages\RegisterPage.cs" /> <Compile Include="Pages\RegisterPage.cs" />
@ -159,6 +160,7 @@
<Compile Include="Pages\Vault\VaultAutofillListLoginsPage.cs" /> <Compile Include="Pages\Vault\VaultAutofillListLoginsPage.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Abstractions\Repositories\ILoginRepository.cs" /> <Compile Include="Abstractions\Repositories\ILoginRepository.cs" />
<Compile Include="Repositories\TwoFactorApiRepository.cs" />
<Compile Include="Repositories\SettingsApiRepository.cs" /> <Compile Include="Repositories\SettingsApiRepository.cs" />
<Compile Include="Repositories\ApiRepository.cs" /> <Compile Include="Repositories\ApiRepository.cs" />
<Compile Include="Repositories\AccountsApiRepository.cs" /> <Compile Include="Repositories\AccountsApiRepository.cs" />

View file

@ -0,0 +1,8 @@
namespace Bit.App.Models.Api
{
public class TwoFactorEmailRequest
{
public string Email { get; set; }
public string MasterPasswordHash { get; set; }
}
}

View file

@ -11,7 +11,6 @@ using Bit.App.Models;
using Bit.App.Utilities; using Bit.App.Utilities;
using Bit.App.Enums; using Bit.App.Enums;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net; using System.Net;
using FFImageLoading.Forms; using FFImageLoading.Forms;
@ -25,12 +24,13 @@ namespace Bit.App.Pages
private ISyncService _syncService; private ISyncService _syncService;
private IDeviceInfoService _deviceInfoService; private IDeviceInfoService _deviceInfoService;
private IGoogleAnalyticsService _googleAnalyticsService; private IGoogleAnalyticsService _googleAnalyticsService;
private ITwoFactorApiRepository _twoFactorApiRepository;
private IPushNotification _pushNotification; private IPushNotification _pushNotification;
private readonly string _email; private readonly string _email;
private readonly string _masterPasswordHash; private readonly string _masterPasswordHash;
private readonly SymmetricCryptoKey _key; private readonly SymmetricCryptoKey _key;
private readonly Dictionary<TwoFactorProviderType, Dictionary<string, object>> _providers; private readonly Dictionary<TwoFactorProviderType, Dictionary<string, object>> _providers;
private readonly TwoFactorProviderType? _providerType; private TwoFactorProviderType? _providerType;
private readonly FullLoginResult _result; private readonly FullLoginResult _result;
public LoginTwoFactorPage(string email, FullLoginResult result, TwoFactorProviderType? type = null) public LoginTwoFactorPage(string email, FullLoginResult result, TwoFactorProviderType? type = null)
@ -49,11 +49,10 @@ namespace Bit.App.Pages
_userDialogs = Resolver.Resolve<IUserDialogs>(); _userDialogs = Resolver.Resolve<IUserDialogs>();
_syncService = Resolver.Resolve<ISyncService>(); _syncService = Resolver.Resolve<ISyncService>();
_googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>(); _googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
_twoFactorApiRepository = Resolver.Resolve<ITwoFactorApiRepository>();
_pushNotification = Resolver.Resolve<IPushNotification>(); _pushNotification = Resolver.Resolve<IPushNotification>();
Init(); Init();
SubscribeYubiKey(true);
} }
public FormEntryCell TokenCell { get; set; } public FormEntryCell TokenCell { get; set; }
@ -61,6 +60,13 @@ namespace Bit.App.Pages
private void Init() private void Init()
{ {
SubscribeYubiKey(true);
if(_providers.Count > 1)
{
var sendEmailTask = SendEmailAsync(false);
}
ToolbarItems.Clear();
var scrollView = new ScrollView(); var scrollView = new ScrollView();
var anotherMethodButton = new ExtendedButton var anotherMethodButton = new ExtendedButton
@ -70,7 +76,8 @@ namespace Bit.App.Pages
Margin = new Thickness(15, 0, 15, 25), Margin = new Thickness(15, 0, 15, 25),
Command = new Command(() => AnotherMethodAsync()), Command = new Command(() => AnotherMethodAsync()),
Uppercase = false, Uppercase = false,
BackgroundColor = Color.Transparent BackgroundColor = Color.Transparent,
VerticalOptions = LayoutOptions.Start
}; };
var instruction = new Label var instruction = new Label
@ -107,7 +114,7 @@ namespace Bit.App.Pages
var continueToolbarItem = new ToolbarItem(AppResources.Continue, null, async () => var continueToolbarItem = new ToolbarItem(AppResources.Continue, null, async () =>
{ {
var token = TokenCell?.Entry.Text.Trim().Replace(" ", ""); var token = TokenCell?.Entry.Text.Trim().Replace(" ", "");
await LogInAsync(token, RememberCell.On); await LogInAsync(token);
}, ToolbarItemOrder.Default, 0); }, ToolbarItemOrder.Default, 0);
var padding = Helpers.OnPlatform( var padding = Helpers.OnPlatform(
@ -130,7 +137,7 @@ namespace Bit.App.Pages
var layout = new StackLayout var layout = new StackLayout
{ {
Children = { instruction, table, anotherMethodButton }, Children = { instruction, table },
Spacing = 0 Spacing = 0
}; };
@ -140,27 +147,24 @@ namespace Bit.App.Pages
{ {
case TwoFactorProviderType.Authenticator: case TwoFactorProviderType.Authenticator:
instruction.Text = "Enter the 6 digit verification code from your authenticator app."; instruction.Text = "Enter the 6 digit verification code from your authenticator app.";
layout.Children.Add(instruction);
layout.Children.Add(table);
layout.Children.Add(anotherMethodButton); layout.Children.Add(anotherMethodButton);
break; break;
case TwoFactorProviderType.Email: case TwoFactorProviderType.Email:
var emailParams = _providers[TwoFactorProviderType.Email]; var emailParams = _providers[TwoFactorProviderType.Email];
var redactedEmail = emailParams["Email"].ToString(); var redactedEmail = emailParams["Email"].ToString();
instruction.Text = "Enter the 6 digit verification code from your authenticator app."; instruction.Text = $"Enter the 6 digit verification code that was emailed to {redactedEmail}.";
var resendEmailButton = new ExtendedButton var resendEmailButton = new ExtendedButton
{ {
Text = $"Enter the 6 digit verification code that was emailed to {redactedEmail}.", Text = "Send verification code email again",
Style = (Style)Application.Current.Resources["btn-primaryAccent"], Style = (Style)Application.Current.Resources["btn-primaryAccent"],
Margin = new Thickness(15, 0, 15, 25), Margin = new Thickness(15, 0, 15, 0),
Command = new Command(() => SendEmail()), Command = new Command(async () => await SendEmailAsync(true)),
Uppercase = false, Uppercase = false,
BackgroundColor = Color.Transparent BackgroundColor = Color.Transparent,
VerticalOptions = LayoutOptions.Start
}; };
layout.Children.Add(instruction);
layout.Children.Add(table);
layout.Children.Add(resendEmailButton); layout.Children.Add(resendEmailButton);
layout.Children.Add(anotherMethodButton); layout.Children.Add(anotherMethodButton);
break; break;
@ -184,15 +188,30 @@ namespace Bit.App.Pages
{ {
Uri = $"http://192.168.1.6:4001/duo-mobile.html?host={host}&request={req}", Uri = $"http://192.168.1.6:4001/duo-mobile.html?host={host}&request={req}",
HorizontalOptions = LayoutOptions.FillAndExpand, HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.FillAndExpand VerticalOptions = LayoutOptions.FillAndExpand,
MinimumHeightRequest = 400
}; };
webView.RegisterAction(async (sig) => webView.RegisterAction(async (sig) =>
{ {
await LogInAsync(sig, false); await LogInAsync(sig);
}); });
var table = new TwoFactorTable(
new TableSection(" ")
{
RememberCell
});
var layout = new StackLayout
{
Children = { webView, table, anotherMethodButton },
Spacing = 0
};
scrollView.Content = layout;
Title = "Duo"; Title = "Duo";
Content = webView; Content = scrollView;
} }
else if(_providerType == TwoFactorProviderType.YubiKey) else if(_providerType == TwoFactorProviderType.YubiKey)
{ {
@ -254,28 +273,99 @@ namespace Bit.App.Pages
private async void AnotherMethodAsync() private async void AnotherMethodAsync()
{ {
await Navigation.PushForDeviceAsync(new TwoFactorMethodsPage(_email, _result)); var beforeProviderType = _providerType;
}
private void SendEmail() var options = new List<string>();
if(_providers.ContainsKey(TwoFactorProviderType.Authenticator))
{ {
options.Add("Authenticator App");
} }
private void Recover() if(_providers.ContainsKey(TwoFactorProviderType.Duo))
{
options.Add("Duo");
}
if(_providers.ContainsKey(TwoFactorProviderType.YubiKey))
{
var nfcKey = _providers[TwoFactorProviderType.YubiKey].ContainsKey("Nfc") &&
(bool)_providers[TwoFactorProviderType.YubiKey]["Nfc"];
if(_deviceInfoService.NfcEnabled || nfcKey)
{
options.Add("YubiKey NFC Security Key");
}
}
if(_providers.ContainsKey(TwoFactorProviderType.Email))
{
options.Add("Email");
}
options.Add("Recovery Code");
var selection = await DisplayActionSheet("Two-step Login Options", AppResources.Cancel, null, options.ToArray());
if(selection == "Authenticator App")
{
_providerType = TwoFactorProviderType.Authenticator;
}
else if(selection == "Duo")
{
_providerType = TwoFactorProviderType.Duo;
}
else if(selection == "YubiKey NFC Security Key")
{
_providerType = TwoFactorProviderType.YubiKey;
}
else if(selection == "Email")
{
_providerType = TwoFactorProviderType.Email;
}
else if(selection == "Recovery Code")
{ {
Device.OpenUri(new Uri("https://help.bitwarden.com/article/lost-two-step-device/")); Device.OpenUri(new Uri("https://help.bitwarden.com/article/lost-two-step-device/"));
return;
}
if(beforeProviderType != _providerType)
{
Init();
ListenYubiKey(false, beforeProviderType == TwoFactorProviderType.YubiKey);
ListenYubiKey(true);
}
}
private async Task SendEmailAsync(bool doToast)
{
if(_providerType != TwoFactorProviderType.Email)
{
return;
}
var response = await _twoFactorApiRepository.PostSendEmailLoginAsync(new Models.Api.TwoFactorEmailRequest
{
Email = _email,
MasterPasswordHash = _masterPasswordHash
});
if(response.Succeeded && doToast)
{
_userDialogs.Toast("Verification email sent.");
}
else if(!response.Succeeded)
{
_userDialogs.Alert("Could not send verification email. Try again.");
}
} }
private async void Entry_Completed(object sender, EventArgs e) private async void Entry_Completed(object sender, EventArgs e)
{ {
var token = TokenCell.Entry.Text.Trim().Replace(" ", ""); var token = TokenCell.Entry.Text.Trim().Replace(" ", "");
await LogInAsync(token, RememberCell.On); await LogInAsync(token);
} }
private async Task LogInAsync(string token, bool remember) private async Task LogInAsync(string token)
{ {
if(_lastAction.LastActionWasRecent()) if(!_providerType.HasValue || _lastAction.LastActionWasRecent())
{ {
return; return;
} }
@ -288,8 +378,8 @@ namespace Bit.App.Pages
return; return;
} }
_userDialogs.ShowLoading(AppResources.ValidatingCode, MaskType.Black); _userDialogs.ShowLoading(string.Concat(AppResources.Validating, "..."), MaskType.Black);
var response = await _authService.TokenPostTwoFactorAsync(_providerType.Value, token, remember, var response = await _authService.TokenPostTwoFactorAsync(_providerType.Value, token, RememberCell.On,
_email, _masterPasswordHash, _key); _email, _masterPasswordHash, _key);
_userDialogs.HideLoading(); _userDialogs.HideLoading();
if(!response.Success) if(!response.Success)
@ -299,7 +389,7 @@ namespace Bit.App.Pages
return; return;
} }
_googleAnalyticsService.TrackAppEvent("LoggedIn From Two-step"); _googleAnalyticsService.TrackAppEvent("LoggedIn From Two-step", _providerType.Value.ToString());
if(Device.RuntimePlatform == Device.Android) if(Device.RuntimePlatform == Device.Android)
{ {
@ -320,11 +410,6 @@ namespace Bit.App.Pages
if(_providers != null) if(_providers != null)
{ {
if(_providers.Count == 1)
{
return _providers.First().Key;
}
foreach(var p in _providers) foreach(var p in _providers)
{ {
switch(p.Key) switch(p.Key)
@ -348,7 +433,8 @@ namespace Bit.App.Pages
} }
break; break;
case TwoFactorProviderType.YubiKey: case TwoFactorProviderType.YubiKey:
if(!_deviceInfoService.NfcEnabled) var nfcKey = p.Value.ContainsKey("Nfc") && (bool)p.Value["Nfc"];
if(!_deviceInfoService.NfcEnabled || !nfcKey)
{ {
continue; continue;
} }
@ -364,9 +450,9 @@ namespace Bit.App.Pages
return provider; return provider;
} }
private void ListenYubiKey(bool listen) private void ListenYubiKey(bool listen, bool overrideCheck = false)
{ {
if(_providerType == TwoFactorProviderType.YubiKey) if(_providerType == TwoFactorProviderType.YubiKey || overrideCheck)
{ {
MessagingCenter.Send(Application.Current, "ListenYubiKeyOTP", listen); MessagingCenter.Send(Application.Current, "ListenYubiKeyOTP", listen);
} }
@ -391,7 +477,7 @@ namespace Bit.App.Pages
MessagingCenter.Unsubscribe<Application, string>(Application.Current, "GotYubiKeyOTP"); MessagingCenter.Unsubscribe<Application, string>(Application.Current, "GotYubiKeyOTP");
if(_providerType == TwoFactorProviderType.YubiKey) if(_providerType == TwoFactorProviderType.YubiKey)
{ {
await LogInAsync(otp, RememberCell.On); await LogInAsync(otp);
} }
}); });

View file

@ -1,141 +0,0 @@
using System;
using Bit.App.Controls;
using Xamarin.Forms;
using Bit.App.Models;
namespace Bit.App.Pages
{
public class TwoFactorMethodsPage : ExtendedContentPage
{
private readonly string _email;
private readonly FullLoginResult _result;
public TwoFactorMethodsPage(string email, FullLoginResult result)
: base(updateActivity: false)
{
_email = email;
_result = result;
Init();
}
public ExtendedTextCell AuthenticatorCell { get; set; }
public ExtendedTextCell EmailCell { get; set; }
public ExtendedTextCell DuoCell { get; set; }
public ExtendedTextCell RecoveryCell { get; set; }
private void Init()
{
var section = new TableSection(" ");
if(_result.TwoFactorProviders.ContainsKey(Enums.TwoFactorProviderType.Authenticator))
{
AuthenticatorCell = new ExtendedTextCell
{
Text = "Authenticator App",
Detail = "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes."
};
section.Add(AuthenticatorCell);
}
if(_result.TwoFactorProviders.ContainsKey(Enums.TwoFactorProviderType.Duo))
{
DuoCell = new ExtendedTextCell
{
Text = "Duo",
Detail = "Use duo."
};
section.Add(DuoCell);
}
if(_result.TwoFactorProviders.ContainsKey(Enums.TwoFactorProviderType.Email))
{
EmailCell = new ExtendedTextCell
{
Text = "Email",
Detail = "Verification codes will be emailed to you."
};
section.Add(EmailCell);
}
RecoveryCell = new ExtendedTextCell
{
Text = "Recovery Code",
Detail = "Lost access to all of your two-factor providers? Use your recovery code to disable all two-factor providers from your account."
};
section.Add(RecoveryCell);
var table = new ExtendedTableView
{
EnableScrolling = true,
Intent = TableIntent.Settings,
HasUnevenRows = true,
Root = new TableRoot
{
section
}
};
if(Device.RuntimePlatform == Device.iOS)
{
table.RowHeight = -1;
table.EstimatedRowHeight = 100;
}
Title = "Two-step Login Options";
Content = table;
}
protected override void OnAppearing()
{
base.OnAppearing();
if(AuthenticatorCell != null)
{
AuthenticatorCell.Tapped += AuthenticatorCell_Tapped;
}
if(DuoCell != null)
{
DuoCell.Tapped += DuoCell_Tapped;
}
if(EmailCell != null)
{
EmailCell.Tapped += EmailCell_Tapped;
}
RecoveryCell.Tapped += RecoveryCell_Tapped;
}
protected override void OnDisappearing()
{
base.OnDisappearing();
if(AuthenticatorCell != null)
{
AuthenticatorCell.Tapped -= AuthenticatorCell_Tapped;
}
if(DuoCell != null)
{
DuoCell.Tapped -= DuoCell_Tapped;
}
if(EmailCell != null)
{
EmailCell.Tapped -= EmailCell_Tapped;
}
RecoveryCell.Tapped -= RecoveryCell_Tapped;
}
private void AuthenticatorCell_Tapped(object sender, EventArgs e)
{
}
private void RecoveryCell_Tapped(object sender, EventArgs e)
{
}
private void EmailCell_Tapped(object sender, EventArgs e)
{
}
private void DuoCell_Tapped(object sender, EventArgs e)
{
}
}
}

View file

@ -0,0 +1,53 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Models.Api;
using Plugin.Connectivity.Abstractions;
namespace Bit.App.Repositories
{
public class TwoFactorApiRepository : BaseApiRepository, ITwoFactorApiRepository
{
public TwoFactorApiRepository(
IConnectivity connectivity,
IHttpService httpService,
ITokenService tokenService)
: base(connectivity, httpService, tokenService)
{ }
protected override string ApiRoute => "two-factor";
public virtual async Task<ApiResult> PostSendEmailLoginAsync(TwoFactorEmailRequest requestObj)
{
if(!Connectivity.IsConnected)
{
return HandledNotConnected();
}
using(var client = HttpService.ApiClient)
{
var requestMessage = new TokenHttpRequestMessage(requestObj)
{
Method = HttpMethod.Post,
RequestUri = new Uri(client.BaseAddress, string.Concat(ApiRoute, "/send-email-login")),
};
try
{
var response = await client.SendAsync(requestMessage).ConfigureAwait(false);
if(!response.IsSuccessStatusCode)
{
return await HandleErrorAsync(response).ConfigureAwait(false);
}
return ApiResult.Success(response.StatusCode);
}
catch
{
return HandledWebException();
}
}
}
}
}

View file

@ -1943,11 +1943,11 @@ namespace Bit.App.Resources {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Validating code.... /// Looks up a localized string similar to Validating.
/// </summary> /// </summary>
public static string ValidatingCode { public static string Validating {
get { get {
return ResourceManager.GetString("ValidatingCode", resourceCulture); return ResourceManager.GetString("Validating", resourceCulture);
} }
} }

View file

@ -728,8 +728,8 @@
<data name="UnlockWithPIN" xml:space="preserve"> <data name="UnlockWithPIN" xml:space="preserve">
<value>Unlock with PIN Code</value> <value>Unlock with PIN Code</value>
</data> </data>
<data name="ValidatingCode" xml:space="preserve"> <data name="Validating" xml:space="preserve">
<value>Validating code...</value> <value>Validating</value>
<comment>Message shown when interacting with the server</comment> <comment>Message shown when interacting with the server</comment>
</data> </data>
<data name="VerificationCode" xml:space="preserve"> <data name="VerificationCode" xml:space="preserve">

View file

@ -280,6 +280,7 @@ namespace Bit.iOS
container.RegisterSingleton<ICipherApiRepository, CipherApiRepository>(); container.RegisterSingleton<ICipherApiRepository, CipherApiRepository>();
container.RegisterSingleton<ISettingsRepository, SettingsRepository>(); container.RegisterSingleton<ISettingsRepository, SettingsRepository>();
container.RegisterSingleton<ISettingsApiRepository, SettingsApiRepository>(); container.RegisterSingleton<ISettingsApiRepository, SettingsApiRepository>();
container.RegisterSingleton<ITwoFactorApiRepository, TwoFactorApiRepository>();
// Other // Other
container.RegisterSingleton(CrossConnectivity.Current); container.RegisterSingleton(CrossConnectivity.Current);