refactors for new 2fa flows

This commit is contained in:
Kyle Spearrin 2017-06-27 16:18:32 -04:00
parent 35ae2b783f
commit 4116d95a3e
10 changed files with 254 additions and 95 deletions

View file

@ -1,4 +1,5 @@
using Bit.App.Models; using Bit.App.Enums;
using Bit.App.Models;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Bit.App.Abstractions namespace Bit.App.Abstractions
@ -15,6 +16,7 @@ namespace Bit.App.Abstractions
bool BelongsToOrganization(string orgId); bool BelongsToOrganization(string orgId);
void LogOut(); void LogOut();
Task<FullLoginResult> TokenPostAsync(string email, string masterPassword); Task<FullLoginResult> TokenPostAsync(string email, string masterPassword);
Task<LoginResult> TokenPostTwoFactorAsync(string token, string email, string masterPasswordHash, SymmetricCryptoKey key); Task<LoginResult> TokenPostTwoFactorAsync(TwoFactorProviderType type, string token, bool remember, string email,
string masterPasswordHash, SymmetricCryptoKey key);
} }
} }

View file

@ -81,6 +81,7 @@
<Compile Include="Controls\FormEntryCell.cs" /> <Compile Include="Controls\FormEntryCell.cs" />
<Compile Include="Controls\PinControl.cs" /> <Compile Include="Controls\PinControl.cs" />
<Compile Include="Controls\VaultListViewCell.cs" /> <Compile Include="Controls\VaultListViewCell.cs" />
<Compile Include="Enums\TwoFactorProviderType.cs" />
<Compile Include="Enums\EncryptionType.cs" /> <Compile Include="Enums\EncryptionType.cs" />
<Compile Include="Enums\OrganizationUserType.cs" /> <Compile Include="Enums\OrganizationUserType.cs" />
<Compile Include="Enums\LockType.cs" /> <Compile Include="Enums\LockType.cs" />

View file

@ -0,0 +1,12 @@
namespace Bit.App.Enums
{
public enum TwoFactorProviderType : byte
{
Authenticator = 0,
Email = 1,
Duo = 2,
YubiKey = 3,
U2f = 4,
Remember = 5
}
}

View file

@ -1,4 +1,5 @@
using System; using Bit.App.Enums;
using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Bit.App.Models.Api namespace Bit.App.Models.Api
@ -8,10 +9,11 @@ namespace Bit.App.Models.Api
public string Email { get; set; } public string Email { get; set; }
public string MasterPasswordHash { get; set; } public string MasterPasswordHash { get; set; }
public string Token { get; set; } public string Token { get; set; }
public int? Provider { get; set; } public TwoFactorProviderType? Provider { get; set; }
[Obsolete] [Obsolete]
public string OldAuthBearer { get; set; } public string OldAuthBearer { get; set; }
public DeviceRequest Device { get; set; } public DeviceRequest Device { get; set; }
public bool Remember { get; set; }
public IDictionary<string, string> ToIdentityTokenRequest() public IDictionary<string, string> ToIdentityTokenRequest()
{ {
@ -40,7 +42,8 @@ namespace Bit.App.Models.Api
if(Token != null && Provider.HasValue) if(Token != null && Provider.HasValue)
{ {
dict.Add("TwoFactorToken", Token); dict.Add("TwoFactorToken", Token);
dict.Add("TwoFactorProvider", Provider.Value.ToString()); dict.Add("TwoFactorProvider", ((byte)(Provider.Value)).ToString());
dict.Add("TwoFactorRemember", Remember ? "1" : "0");
} }
return dict; return dict;

View file

@ -1,4 +1,5 @@
using Newtonsoft.Json; using Bit.App.Enums;
using Newtonsoft.Json;
using System.Collections.Generic; using System.Collections.Generic;
namespace Bit.App.Models.Api namespace Bit.App.Models.Api
@ -13,7 +14,7 @@ namespace Bit.App.Models.Api
public string RefreshToken { get; set; } public string RefreshToken { get; set; }
[JsonProperty("token_type")] [JsonProperty("token_type")]
public string TokenType { get; set; } public string TokenType { get; set; }
public List<int> TwoFactorProviders { get; set; } public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProviders2 { get; set; }
public string PrivateKey { get; set; } public string PrivateKey { get; set; }
public string Key { get; set; } public string Key { get; set; }
} }

View file

@ -1,4 +1,7 @@
namespace Bit.App.Models using Bit.App.Enums;
using System.Collections.Generic;
namespace Bit.App.Models
{ {
public class LoginResult public class LoginResult
{ {
@ -8,7 +11,8 @@
public class FullLoginResult : LoginResult public class FullLoginResult : LoginResult
{ {
public bool TwoFactorRequired { get; set; } public bool TwoFactorRequired => TwoFactorProviders != null && TwoFactorProviders.Count > 0;
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProviders { get; set; }
public SymmetricCryptoKey Key { get; set; } public SymmetricCryptoKey Key { get; set; }
public string MasterPasswordHash { get; set; } public string MasterPasswordHash { get; set; }
} }

View file

@ -192,7 +192,7 @@ namespace Bit.App.Pages
if(result.TwoFactorRequired) if(result.TwoFactorRequired)
{ {
_googleAnalyticsService.TrackAppEvent("LoggedIn To Two-step"); _googleAnalyticsService.TrackAppEvent("LoggedIn To Two-step");
await Navigation.PushAsync(new LoginTwoFactorPage(EmailCell.Entry.Text, result.MasterPasswordHash, result.Key)); await Navigation.PushAsync(new LoginTwoFactorPage(EmailCell.Entry.Text, result));
return; return;
} }

View file

@ -9,6 +9,9 @@ using System.Threading.Tasks;
using PushNotification.Plugin.Abstractions; using PushNotification.Plugin.Abstractions;
using Bit.App.Models; using Bit.App.Models;
using Bit.App.Utilities; using Bit.App.Utilities;
using Bit.App.Enums;
using System.Collections.Generic;
using System.Linq;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
@ -22,13 +25,17 @@ namespace Bit.App.Pages
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 TwoFactorProviderType? _providerType;
public LoginTwoFactorPage(string email, string masterPasswordHash, SymmetricCryptoKey key) public LoginTwoFactorPage(string email, FullLoginResult result, TwoFactorProviderType? type = null)
: base(updateActivity: false) : base(updateActivity: false)
{ {
_email = email; _email = email;
_masterPasswordHash = masterPasswordHash; _masterPasswordHash = result.MasterPasswordHash;
_key = key; _key = result.Key;
_providers = result.TwoFactorProviders;
_providerType = type ?? GetDefaultProvider();
_authService = Resolver.Resolve<IAuthService>(); _authService = Resolver.Resolve<IAuthService>();
_userDialogs = Resolver.Resolve<IUserDialogs>(); _userDialogs = Resolver.Resolve<IUserDialogs>();
@ -39,109 +46,192 @@ namespace Bit.App.Pages
Init(); Init();
} }
public FormEntryCell CodeCell { get; set; } public FormEntryCell TokenCell { get; set; }
public ExtendedSwitchCell RememberCell { get; set; }
private void Init() private void Init()
{ {
var padding = Helpers.OnPlatform( var scrollView = new ScrollView();
iOS: new Thickness(15, 20),
Android: new Thickness(15, 8),
WinPhone: new Thickness(15, 20));
CodeCell = new FormEntryCell(AppResources.VerificationCode, useLabelAsPlaceholder: true,
imageSource: "lock", containerPadding: padding);
CodeCell.Entry.Keyboard = Keyboard.Numeric;
CodeCell.Entry.ReturnType = Enums.ReturnType.Go;
var table = new ExtendedTableView
{
Intent = TableIntent.Settings,
EnableScrolling = false,
HasUnevenRows = true,
EnableSelection = true,
NoFooter = true,
VerticalOptions = LayoutOptions.Start,
Root = new TableRoot
{
new TableSection(" ")
{
CodeCell
}
}
};
var codeLabel = new Label
{
Text = AppResources.EnterVerificationCode,
LineBreakMode = LineBreakMode.WordWrap,
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
Style = (Style)Application.Current.Resources["text-muted"],
Margin = new Thickness(15, (this.IsLandscape() ? 5 : 0), 15, 25)
};
var lostAppButton = new ExtendedButton
{
Text = AppResources.Lost2FAApp,
Style = (Style)Application.Current.Resources["btn-primaryAccent"],
Margin = new Thickness(15, 0, 15, 25),
Command = new Command(() => Lost2FAApp()),
Uppercase = false,
BackgroundColor = Color.Transparent
};
var layout = new StackLayout
{
Children = { table, codeLabel, lostAppButton },
Spacing = 0
};
var scrollView = new ScrollView { Content = layout };
if(Device.RuntimePlatform == Device.iOS)
{
table.RowHeight = -1;
table.EstimatedRowHeight = 70;
}
var continueToolbarItem = new ToolbarItem(AppResources.Continue, null, async () => var continueToolbarItem = new ToolbarItem(AppResources.Continue, null, async () =>
{ {
await LogInAsync(); var token = TokenCell?.Entry.Text.Trim().Replace(" ", "");
await LogInAsync(token);
}, ToolbarItemOrder.Default, 0); }, ToolbarItemOrder.Default, 0);
ToolbarItems.Add(continueToolbarItem); if(!_providerType.HasValue)
Title = AppResources.VerificationCode; {
var noProviderLabel = new Label
{
Text = "No provider.",
LineBreakMode = LineBreakMode.WordWrap,
Margin = new Thickness(15),
HorizontalTextAlignment = TextAlignment.Center
};
scrollView.Content = noProviderLabel;
}
else
{
var padding = Helpers.OnPlatform(
iOS: new Thickness(15, 20),
Android: new Thickness(15, 8),
WinPhone: new Thickness(15, 20));
TokenCell = new FormEntryCell(AppResources.VerificationCode, useLabelAsPlaceholder: true,
imageSource: "lock", containerPadding: padding);
TokenCell.Entry.Keyboard = Keyboard.Numeric;
TokenCell.Entry.ReturnType = ReturnType.Go;
RememberCell = new ExtendedSwitchCell
{
Text = "Remember me",
On = false
};
var table = new ExtendedTableView
{
Intent = TableIntent.Settings,
EnableScrolling = false,
HasUnevenRows = true,
EnableSelection = true,
NoFooter = true,
NoHeader = true,
VerticalOptions = LayoutOptions.Start,
Root = new TableRoot
{
new TableSection(" ")
{
TokenCell,
RememberCell
}
}
};
if(Device.RuntimePlatform == Device.iOS)
{
table.RowHeight = -1;
table.EstimatedRowHeight = 70;
}
var instruction = new Label
{
Text = AppResources.EnterVerificationCode,
LineBreakMode = LineBreakMode.WordWrap,
Margin = new Thickness(15),
HorizontalTextAlignment = TextAlignment.Center
};
var anotherMethodButton = new ExtendedButton
{
Text = "Use another two-step login method",
Style = (Style)Application.Current.Resources["btn-primaryAccent"],
Margin = new Thickness(15, 0, 15, 25),
Command = new Command(() => AnotherMethod()),
Uppercase = false,
BackgroundColor = Color.Transparent
};
var layout = new StackLayout
{
Children = { instruction, table, anotherMethodButton },
Spacing = 0
};
scrollView.Content = layout;
switch(_providerType.Value)
{
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);
ToolbarItems.Add(continueToolbarItem);
Title = AppResources.VerificationCode;
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.";
var resendEmailButton = new ExtendedButton
{
Text = $"Enter the 6 digit verification code that was emailed to {redactedEmail}.",
Style = (Style)Application.Current.Resources["btn-primaryAccent"],
Margin = new Thickness(15, 0, 15, 25),
Command = new Command(() => SendEmail()),
Uppercase = false,
BackgroundColor = Color.Transparent
};
layout.Children.Add(instruction);
layout.Children.Add(table);
layout.Children.Add(resendEmailButton);
layout.Children.Add(anotherMethodButton);
ToolbarItems.Add(continueToolbarItem);
Title = AppResources.VerificationCode;
break;
case TwoFactorProviderType.Duo:
break;
default:
break;
}
}
Content = scrollView; Content = scrollView;
} }
protected override void OnAppearing() protected override void OnAppearing()
{ {
base.OnAppearing(); base.OnAppearing();
CodeCell.InitEvents();
CodeCell.Entry.FocusWithDelay(); if(TokenCell != null)
CodeCell.Entry.Completed += Entry_Completed; {
TokenCell.InitEvents();
TokenCell.Entry.FocusWithDelay();
TokenCell.Entry.Completed += Entry_Completed;
}
} }
protected override void OnDisappearing() protected override void OnDisappearing()
{ {
base.OnDisappearing(); base.OnDisappearing();
CodeCell.Dispose();
CodeCell.Entry.Completed -= Entry_Completed; if(TokenCell != null)
{
TokenCell.Dispose();
TokenCell.Entry.Completed -= Entry_Completed;
}
} }
private void Lost2FAApp() private void AnotherMethod()
{
}
private void SendEmail()
{
}
private void Recover()
{ {
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/"));
} }
private async void Entry_Completed(object sender, EventArgs e) private async void Entry_Completed(object sender, EventArgs e)
{ {
await LogInAsync(); var token = TokenCell.Entry.Text.Trim().Replace(" ", "");
await LogInAsync(token);
} }
private async Task LogInAsync() private async Task LogInAsync(string token)
{ {
if(string.IsNullOrWhiteSpace(CodeCell.Entry.Text)) if(string.IsNullOrWhiteSpace(token))
{ {
await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired,
AppResources.VerificationCode), AppResources.Ok); AppResources.VerificationCode), AppResources.Ok);
@ -149,7 +239,8 @@ namespace Bit.App.Pages
} }
_userDialogs.ShowLoading(AppResources.ValidatingCode, MaskType.Black); _userDialogs.ShowLoading(AppResources.ValidatingCode, MaskType.Black);
var response = await _authService.TokenPostTwoFactorAsync(CodeCell.Entry.Text, _email, _masterPasswordHash, _key); var response = await _authService.TokenPostTwoFactorAsync(_providerType.Value, token, RememberCell.On,
_email, _masterPasswordHash, _key);
_userDialogs.HideLoading(); _userDialogs.HideLoading();
if(!response.Success) if(!response.Success)
{ {
@ -167,5 +258,45 @@ namespace Bit.App.Pages
var task = Task.Run(async () => await _syncService.FullSyncAsync(true)); var task = Task.Run(async () => await _syncService.FullSyncAsync(true));
Application.Current.MainPage = new MainPage(); Application.Current.MainPage = new MainPage();
} }
private TwoFactorProviderType? GetDefaultProvider()
{
TwoFactorProviderType? provider = null;
if(_providers != null)
{
if(_providers.Count == 1)
{
return _providers.First().Key;
}
foreach(var p in _providers)
{
switch(p.Key)
{
case TwoFactorProviderType.Authenticator:
if(provider == TwoFactorProviderType.Duo)
{
continue;
}
break;
case TwoFactorProviderType.Email:
if(provider.HasValue)
{
continue;
}
break;
case TwoFactorProviderType.Duo:
break;
default:
continue;
}
provider = p.Key;
}
}
return provider;
}
} }
} }

View file

@ -8,6 +8,7 @@ using Plugin.Connectivity.Abstractions;
using System.Net; using System.Net;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using Bit.App.Enums;
namespace Bit.App.Repositories namespace Bit.App.Repositories
{ {
@ -46,11 +47,13 @@ namespace Bit.App.Repositories
if(!response.IsSuccessStatusCode) if(!response.IsSuccessStatusCode)
{ {
var errorResponse = JObject.Parse(responseContent); var errorResponse = JObject.Parse(responseContent);
if(errorResponse["TwoFactorProviders"] != null) if(errorResponse["TwoFactorProviders2"] != null)
{ {
return ApiResult<TokenResponse>.Success(new TokenResponse return ApiResult<TokenResponse>.Success(new TokenResponse
{ {
TwoFactorProviders = errorResponse["TwoFactorProviders"].ToObject<List<int>>() TwoFactorProviders2 =
errorResponse["TwoFactorProviders2"]
.ToObject<Dictionary<TwoFactorProviderType, Dictionary<string, object>>>()
}, response.StatusCode); }, response.StatusCode);
} }

View file

@ -6,6 +6,7 @@ using Bit.App.Models.Api;
using Plugin.Settings.Abstractions; using Plugin.Settings.Abstractions;
using Bit.App.Models; using Bit.App.Models;
using System.Linq; using System.Linq;
using Bit.App.Enums;
namespace Bit.App.Services namespace Bit.App.Services
{ {
@ -230,11 +231,11 @@ namespace Bit.App.Services
} }
result.Success = true; result.Success = true;
if(response.Result.TwoFactorProviders != null && response.Result.TwoFactorProviders.Count > 0) if(response.Result.TwoFactorProviders2 != null && response.Result.TwoFactorProviders2.Count > 0)
{ {
result.Key = key; result.Key = key;
result.MasterPasswordHash = request.MasterPasswordHash; result.MasterPasswordHash = request.MasterPasswordHash;
result.TwoFactorRequired = true; result.TwoFactorProviders = response.Result.TwoFactorProviders2;
return result; return result;
} }
@ -242,17 +243,18 @@ namespace Bit.App.Services
return result; return result;
} }
public async Task<LoginResult> TokenPostTwoFactorAsync(string token, string email, string masterPasswordHash, public async Task<LoginResult> TokenPostTwoFactorAsync(TwoFactorProviderType type, string token, bool remember,
SymmetricCryptoKey key) string email, string masterPasswordHash, SymmetricCryptoKey key)
{ {
var result = new LoginResult(); var result = new LoginResult();
var request = new TokenRequest var request = new TokenRequest
{ {
Remember = remember,
Email = email.Trim().ToLower(), Email = email.Trim().ToLower(),
MasterPasswordHash = masterPasswordHash, MasterPasswordHash = masterPasswordHash,
Token = token.Trim().Replace(" ", ""), Token = token,
Provider = 0, // Authenticator app (only 1 provider for now, so hard coded) Provider = type,
Device = new DeviceRequest(_appIdService, _deviceInfoService) Device = new DeviceRequest(_appIdService, _deviceInfoService)
}; };