diff --git a/src/App/Abstractions/Services/IAuthService.cs b/src/App/Abstractions/Services/IAuthService.cs index 39cb636a6..2dc25591e 100644 --- a/src/App/Abstractions/Services/IAuthService.cs +++ b/src/App/Abstractions/Services/IAuthService.cs @@ -1,4 +1,5 @@ -using Bit.App.Models; +using Bit.App.Enums; +using Bit.App.Models; using System.Threading.Tasks; namespace Bit.App.Abstractions @@ -15,6 +16,7 @@ namespace Bit.App.Abstractions bool BelongsToOrganization(string orgId); void LogOut(); Task TokenPostAsync(string email, string masterPassword); - Task TokenPostTwoFactorAsync(string token, string email, string masterPasswordHash, SymmetricCryptoKey key); + Task TokenPostTwoFactorAsync(TwoFactorProviderType type, string token, bool remember, string email, + string masterPasswordHash, SymmetricCryptoKey key); } } diff --git a/src/App/App.csproj b/src/App/App.csproj index 0051c31df..118b1b7ee 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -81,6 +81,7 @@ + diff --git a/src/App/Enums/TwoFactorProviderType.cs b/src/App/Enums/TwoFactorProviderType.cs new file mode 100644 index 000000000..10a741a32 --- /dev/null +++ b/src/App/Enums/TwoFactorProviderType.cs @@ -0,0 +1,12 @@ +namespace Bit.App.Enums +{ + public enum TwoFactorProviderType : byte + { + Authenticator = 0, + Email = 1, + Duo = 2, + YubiKey = 3, + U2f = 4, + Remember = 5 + } +} diff --git a/src/App/Models/Api/Request/TokenRequest.cs b/src/App/Models/Api/Request/TokenRequest.cs index 96ba1329b..d07a26003 100644 --- a/src/App/Models/Api/Request/TokenRequest.cs +++ b/src/App/Models/Api/Request/TokenRequest.cs @@ -1,4 +1,5 @@ -using System; +using Bit.App.Enums; +using System; using System.Collections.Generic; namespace Bit.App.Models.Api @@ -8,10 +9,11 @@ namespace Bit.App.Models.Api public string Email { get; set; } public string MasterPasswordHash { get; set; } public string Token { get; set; } - public int? Provider { get; set; } + public TwoFactorProviderType? Provider { get; set; } [Obsolete] public string OldAuthBearer { get; set; } public DeviceRequest Device { get; set; } + public bool Remember { get; set; } public IDictionary ToIdentityTokenRequest() { @@ -40,7 +42,8 @@ namespace Bit.App.Models.Api if(Token != null && Provider.HasValue) { 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; diff --git a/src/App/Models/Api/Response/TokenResponse.cs b/src/App/Models/Api/Response/TokenResponse.cs index 450f053b3..84e7b01e9 100644 --- a/src/App/Models/Api/Response/TokenResponse.cs +++ b/src/App/Models/Api/Response/TokenResponse.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using Bit.App.Enums; +using Newtonsoft.Json; using System.Collections.Generic; namespace Bit.App.Models.Api @@ -13,7 +14,7 @@ namespace Bit.App.Models.Api public string RefreshToken { get; set; } [JsonProperty("token_type")] public string TokenType { get; set; } - public List TwoFactorProviders { get; set; } + public Dictionary> TwoFactorProviders2 { get; set; } public string PrivateKey { get; set; } public string Key { get; set; } } diff --git a/src/App/Models/LoginResult.cs b/src/App/Models/LoginResult.cs index be9209ceb..4b1fe9ae0 100644 --- a/src/App/Models/LoginResult.cs +++ b/src/App/Models/LoginResult.cs @@ -1,4 +1,7 @@ -namespace Bit.App.Models +using Bit.App.Enums; +using System.Collections.Generic; + +namespace Bit.App.Models { public class LoginResult { @@ -8,7 +11,8 @@ public class FullLoginResult : LoginResult { - public bool TwoFactorRequired { get; set; } + public bool TwoFactorRequired => TwoFactorProviders != null && TwoFactorProviders.Count > 0; + public Dictionary> TwoFactorProviders { get; set; } public SymmetricCryptoKey Key { get; set; } public string MasterPasswordHash { get; set; } } diff --git a/src/App/Pages/LoginPage.cs b/src/App/Pages/LoginPage.cs index d1788b0b8..3a17e7680 100644 --- a/src/App/Pages/LoginPage.cs +++ b/src/App/Pages/LoginPage.cs @@ -192,7 +192,7 @@ namespace Bit.App.Pages if(result.TwoFactorRequired) { _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; } diff --git a/src/App/Pages/LoginTwoFactorPage.cs b/src/App/Pages/LoginTwoFactorPage.cs index 4abfe12b3..2b1c3207b 100644 --- a/src/App/Pages/LoginTwoFactorPage.cs +++ b/src/App/Pages/LoginTwoFactorPage.cs @@ -9,6 +9,9 @@ using System.Threading.Tasks; using PushNotification.Plugin.Abstractions; using Bit.App.Models; using Bit.App.Utilities; +using Bit.App.Enums; +using System.Collections.Generic; +using System.Linq; namespace Bit.App.Pages { @@ -22,13 +25,17 @@ namespace Bit.App.Pages private readonly string _email; private readonly string _masterPasswordHash; private readonly SymmetricCryptoKey _key; + private readonly Dictionary> _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) { _email = email; - _masterPasswordHash = masterPasswordHash; - _key = key; + _masterPasswordHash = result.MasterPasswordHash; + _key = result.Key; + _providers = result.TwoFactorProviders; + _providerType = type ?? GetDefaultProvider(); _authService = Resolver.Resolve(); _userDialogs = Resolver.Resolve(); @@ -39,109 +46,192 @@ namespace Bit.App.Pages Init(); } - public FormEntryCell CodeCell { get; set; } + public FormEntryCell TokenCell { get; set; } + public ExtendedSwitchCell RememberCell { get; set; } private void Init() { - var padding = Helpers.OnPlatform( - 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 scrollView = new ScrollView(); var continueToolbarItem = new ToolbarItem(AppResources.Continue, null, async () => { - await LogInAsync(); + var token = TokenCell?.Entry.Text.Trim().Replace(" ", ""); + await LogInAsync(token); }, ToolbarItemOrder.Default, 0); - ToolbarItems.Add(continueToolbarItem); - Title = AppResources.VerificationCode; + if(!_providerType.HasValue) + { + 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; } protected override void OnAppearing() { base.OnAppearing(); - CodeCell.InitEvents(); - CodeCell.Entry.FocusWithDelay(); - CodeCell.Entry.Completed += Entry_Completed; + + if(TokenCell != null) + { + TokenCell.InitEvents(); + TokenCell.Entry.FocusWithDelay(); + TokenCell.Entry.Completed += Entry_Completed; + } } protected override void 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/")); } 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, AppResources.VerificationCode), AppResources.Ok); @@ -149,7 +239,8 @@ namespace Bit.App.Pages } _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(); if(!response.Success) { @@ -167,5 +258,45 @@ namespace Bit.App.Pages var task = Task.Run(async () => await _syncService.FullSyncAsync(true)); 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; + } } } diff --git a/src/App/Repositories/ConnectApiRepository.cs b/src/App/Repositories/ConnectApiRepository.cs index 74811c1d6..cc60c1cf3 100644 --- a/src/App/Repositories/ConnectApiRepository.cs +++ b/src/App/Repositories/ConnectApiRepository.cs @@ -8,6 +8,7 @@ using Plugin.Connectivity.Abstractions; using System.Net; using Newtonsoft.Json.Linq; using System.Collections.Generic; +using Bit.App.Enums; namespace Bit.App.Repositories { @@ -46,11 +47,13 @@ namespace Bit.App.Repositories if(!response.IsSuccessStatusCode) { var errorResponse = JObject.Parse(responseContent); - if(errorResponse["TwoFactorProviders"] != null) + if(errorResponse["TwoFactorProviders2"] != null) { return ApiResult.Success(new TokenResponse { - TwoFactorProviders = errorResponse["TwoFactorProviders"].ToObject>() + TwoFactorProviders2 = + errorResponse["TwoFactorProviders2"] + .ToObject>>() }, response.StatusCode); } diff --git a/src/App/Services/AuthService.cs b/src/App/Services/AuthService.cs index 51327dc43..c38d3c4b2 100644 --- a/src/App/Services/AuthService.cs +++ b/src/App/Services/AuthService.cs @@ -6,6 +6,7 @@ using Bit.App.Models.Api; using Plugin.Settings.Abstractions; using Bit.App.Models; using System.Linq; +using Bit.App.Enums; namespace Bit.App.Services { @@ -230,11 +231,11 @@ namespace Bit.App.Services } 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.MasterPasswordHash = request.MasterPasswordHash; - result.TwoFactorRequired = true; + result.TwoFactorProviders = response.Result.TwoFactorProviders2; return result; } @@ -242,17 +243,18 @@ namespace Bit.App.Services return result; } - public async Task TokenPostTwoFactorAsync(string token, string email, string masterPasswordHash, - SymmetricCryptoKey key) + public async Task TokenPostTwoFactorAsync(TwoFactorProviderType type, string token, bool remember, + string email, string masterPasswordHash, SymmetricCryptoKey key) { var result = new LoginResult(); var request = new TokenRequest { + Remember = remember, Email = email.Trim().ToLower(), MasterPasswordHash = masterPasswordHash, - Token = token.Trim().Replace(" ", ""), - Provider = 0, // Authenticator app (only 1 provider for now, so hard coded) + Token = token, + Provider = type, Device = new DeviceRequest(_appIdService, _deviceInfoService) };