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");
Window.SetSoftInputMode(SoftInput.StateHidden);
//Window.AddFlags(WindowManagerFlags.Secure);
Window.AddFlags(WindowManagerFlags.Secure);
var appIdService = Resolver.Resolve<IAppIdService>();
var authService = Resolver.Resolve<IAuthService>();

View file

@ -234,6 +234,7 @@ namespace Bit.Android
container.RegisterSingleton<ICipherApiRepository, CipherApiRepository>();
container.RegisterSingleton<ISettingsRepository, SettingsRepository>();
container.RegisterSingleton<ISettingsApiRepository, SettingsApiRepository>();
container.RegisterSingleton<ITwoFactorApiRepository, TwoFactorApiRepository>();
// Other
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>
</PropertyGroup>
<ItemGroup>
<Compile Include="Abstractions\Repositories\ITwoFactorApiRepository.cs" />
<Compile Include="Abstractions\Repositories\ISettingsApiRepository.cs" />
<Compile Include="Abstractions\Repositories\IAccountsApiRepository.cs" />
<Compile Include="Abstractions\Repositories\IDeviceApiRepository.cs" />
@ -97,6 +98,7 @@
<Compile Include="Models\Api\Request\DeviceTokenRequest.cs" />
<Compile Include="Models\Api\Request\FolderRequest.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\LoginRequest.cs" />
<Compile Include="Models\Api\Request\PasswordHintRequest.cs" />
@ -135,7 +137,6 @@
<Compile Include="Pages\HomePage.cs" />
<Compile Include="Pages\Lock\BaseLockPage.cs" />
<Compile Include="Pages\Lock\LockPasswordPage.cs" />
<Compile Include="Pages\TwoFactorMethodsPage.cs" />
<Compile Include="Pages\LoginTwoFactorPage.cs" />
<Compile Include="Pages\PasswordHintPage.cs" />
<Compile Include="Pages\RegisterPage.cs" />
@ -159,6 +160,7 @@
<Compile Include="Pages\Vault\VaultAutofillListLoginsPage.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Abstractions\Repositories\ILoginRepository.cs" />
<Compile Include="Repositories\TwoFactorApiRepository.cs" />
<Compile Include="Repositories\SettingsApiRepository.cs" />
<Compile Include="Repositories\ApiRepository.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.Enums;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FFImageLoading.Forms;
@ -25,12 +24,13 @@ namespace Bit.App.Pages
private ISyncService _syncService;
private IDeviceInfoService _deviceInfoService;
private IGoogleAnalyticsService _googleAnalyticsService;
private ITwoFactorApiRepository _twoFactorApiRepository;
private IPushNotification _pushNotification;
private readonly string _email;
private readonly string _masterPasswordHash;
private readonly SymmetricCryptoKey _key;
private readonly Dictionary<TwoFactorProviderType, Dictionary<string, object>> _providers;
private readonly TwoFactorProviderType? _providerType;
private TwoFactorProviderType? _providerType;
private readonly FullLoginResult _result;
public LoginTwoFactorPage(string email, FullLoginResult result, TwoFactorProviderType? type = null)
@ -49,11 +49,10 @@ namespace Bit.App.Pages
_userDialogs = Resolver.Resolve<IUserDialogs>();
_syncService = Resolver.Resolve<ISyncService>();
_googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
_twoFactorApiRepository = Resolver.Resolve<ITwoFactorApiRepository>();
_pushNotification = Resolver.Resolve<IPushNotification>();
Init();
SubscribeYubiKey(true);
}
public FormEntryCell TokenCell { get; set; }
@ -61,6 +60,13 @@ namespace Bit.App.Pages
private void Init()
{
SubscribeYubiKey(true);
if(_providers.Count > 1)
{
var sendEmailTask = SendEmailAsync(false);
}
ToolbarItems.Clear();
var scrollView = new ScrollView();
var anotherMethodButton = new ExtendedButton
@ -70,7 +76,8 @@ namespace Bit.App.Pages
Margin = new Thickness(15, 0, 15, 25),
Command = new Command(() => AnotherMethodAsync()),
Uppercase = false,
BackgroundColor = Color.Transparent
BackgroundColor = Color.Transparent,
VerticalOptions = LayoutOptions.Start
};
var instruction = new Label
@ -107,7 +114,7 @@ namespace Bit.App.Pages
var continueToolbarItem = new ToolbarItem(AppResources.Continue, null, async () =>
{
var token = TokenCell?.Entry.Text.Trim().Replace(" ", "");
await LogInAsync(token, RememberCell.On);
await LogInAsync(token);
}, ToolbarItemOrder.Default, 0);
var padding = Helpers.OnPlatform(
@ -130,7 +137,7 @@ namespace Bit.App.Pages
var layout = new StackLayout
{
Children = { instruction, table, anotherMethodButton },
Children = { instruction, table },
Spacing = 0
};
@ -140,27 +147,24 @@ namespace Bit.App.Pages
{
case TwoFactorProviderType.Authenticator:
instruction.Text = "Enter the 6 digit verification code from your authenticator app.";
layout.Children.Add(instruction);
layout.Children.Add(table);
layout.Children.Add(anotherMethodButton);
break;
case TwoFactorProviderType.Email:
var emailParams = _providers[TwoFactorProviderType.Email];
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
{
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"],
Margin = new Thickness(15, 0, 15, 25),
Command = new Command(() => SendEmail()),
Margin = new Thickness(15, 0, 15, 0),
Command = new Command(async () => await SendEmailAsync(true)),
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(anotherMethodButton);
break;
@ -184,15 +188,30 @@ namespace Bit.App.Pages
{
Uri = $"http://192.168.1.6:4001/duo-mobile.html?host={host}&request={req}",
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.FillAndExpand
VerticalOptions = LayoutOptions.FillAndExpand,
MinimumHeightRequest = 400
};
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";
Content = webView;
Content = scrollView;
}
else if(_providerType == TwoFactorProviderType.YubiKey)
{
@ -254,28 +273,99 @@ namespace Bit.App.Pages
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/"));
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)
{
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;
}
@ -288,8 +378,8 @@ namespace Bit.App.Pages
return;
}
_userDialogs.ShowLoading(AppResources.ValidatingCode, MaskType.Black);
var response = await _authService.TokenPostTwoFactorAsync(_providerType.Value, token, remember,
_userDialogs.ShowLoading(string.Concat(AppResources.Validating, "..."), MaskType.Black);
var response = await _authService.TokenPostTwoFactorAsync(_providerType.Value, token, RememberCell.On,
_email, _masterPasswordHash, _key);
_userDialogs.HideLoading();
if(!response.Success)
@ -299,7 +389,7 @@ namespace Bit.App.Pages
return;
}
_googleAnalyticsService.TrackAppEvent("LoggedIn From Two-step");
_googleAnalyticsService.TrackAppEvent("LoggedIn From Two-step", _providerType.Value.ToString());
if(Device.RuntimePlatform == Device.Android)
{
@ -320,11 +410,6 @@ namespace Bit.App.Pages
if(_providers != null)
{
if(_providers.Count == 1)
{
return _providers.First().Key;
}
foreach(var p in _providers)
{
switch(p.Key)
@ -348,7 +433,8 @@ namespace Bit.App.Pages
}
break;
case TwoFactorProviderType.YubiKey:
if(!_deviceInfoService.NfcEnabled)
var nfcKey = p.Value.ContainsKey("Nfc") && (bool)p.Value["Nfc"];
if(!_deviceInfoService.NfcEnabled || !nfcKey)
{
continue;
}
@ -364,9 +450,9 @@ namespace Bit.App.Pages
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);
}
@ -391,7 +477,7 @@ namespace Bit.App.Pages
MessagingCenter.Unsubscribe<Application, string>(Application.Current, "GotYubiKeyOTP");
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>
/// Looks up a localized string similar to Validating code....
/// Looks up a localized string similar to Validating.
/// </summary>
public static string ValidatingCode {
public static string Validating {
get {
return ResourceManager.GetString("ValidatingCode", resourceCulture);
return ResourceManager.GetString("Validating", resourceCulture);
}
}

View file

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

View file

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