mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
[SG-174] Login with Device Request - Mobile (#2167)
* [SG-174] Add new login request services to Api * [SG-174] Fix typo * [SG-174] Enable login with device button. * [SG-174] Add new login request page and viewmodel * [SG-174] Add new text resources * [SG-174] Add new RSA Decrypt method with string param * [SG-174] Change create login request method * [SG-174] Add new method to auth service to login passwordless * [SG-174] Refactor login helper method to work with passwordless * [SG-174] Fix service registration * [SG-174] Update token request to support passwordless * [SG-174] Update Api service with passwordless methods * [SG-174] Fix App csproj references * [SG-174] Remove unnecessary argument * [SG-174] dotnet format * [SG-174] Fixed iOS Extensions * [SG-174] Change Command to ICommand * [SG-174] Change Gesture Recognizer to Command * [SG-174] Fix close action * [SG-174] Code format * [SG-174] Fix android frame shadow bug * [SG-174] PR fixes
This commit is contained in:
parent
04ed47d545
commit
9ae269dd57
25 changed files with 618 additions and 15 deletions
|
@ -125,6 +125,9 @@
|
||||||
<Compile Update="Pages\Accounts\LoginPasswordlessPage.xaml.cs">
|
<Compile Update="Pages\Accounts\LoginPasswordlessPage.xaml.cs">
|
||||||
<DependentUpon>LoginPasswordlessPage.xaml</DependentUpon>
|
<DependentUpon>LoginPasswordlessPage.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Update="Pages\Accounts\LoginPasswordlessRequestPage.xaml.cs">
|
||||||
|
<DependentUpon>LoginPasswordlessRequestPage.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
Padding="1"
|
Padding="1"
|
||||||
StyleClass="btn-icon-secondary"
|
StyleClass="btn-icon-secondary"
|
||||||
BackgroundColor="{Binding IconLabelBorderColor, Source={x:Reference _iconLabelButton}}"
|
BackgroundColor="{Binding IconLabelBorderColor, Source={x:Reference _iconLabelButton}}"
|
||||||
|
BorderColor="Transparent"
|
||||||
HasShadow="False">
|
HasShadow="False">
|
||||||
<Frame.GestureRecognizers>
|
<Frame.GestureRecognizers>
|
||||||
<TapGestureRecognizer Command="{Binding ButtonCommand, Source={x:Reference _iconLabelButton}}" />
|
<TapGestureRecognizer Command="{Binding ButtonCommand, Source={x:Reference _iconLabelButton}}" />
|
||||||
|
@ -17,6 +18,7 @@
|
||||||
Padding="0"
|
Padding="0"
|
||||||
CornerRadius="{Binding CornerRadius, Source={x:Reference _iconLabelButton}}"
|
CornerRadius="{Binding CornerRadius, Source={x:Reference _iconLabelButton}}"
|
||||||
BackgroundColor="{Binding IconLabelBackgroundColor, Source={x:Reference _iconLabelButton}}"
|
BackgroundColor="{Binding IconLabelBackgroundColor, Source={x:Reference _iconLabelButton}}"
|
||||||
|
BorderColor="Transparent"
|
||||||
IsClippedToBounds="True"
|
IsClippedToBounds="True"
|
||||||
HasShadow="False">
|
HasShadow="False">
|
||||||
<StackLayout
|
<StackLayout
|
||||||
|
|
|
@ -19,7 +19,7 @@ namespace Bit.App.Controls
|
||||||
nameof(Label), typeof(string), typeof(IconLabelButton));
|
nameof(Label), typeof(string), typeof(IconLabelButton));
|
||||||
|
|
||||||
public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create(
|
public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create(
|
||||||
nameof(ButtonCommand), typeof(Command), typeof(IconLabelButton));
|
nameof(ButtonCommand), typeof(ICommand), typeof(IconLabelButton));
|
||||||
|
|
||||||
public static readonly BindableProperty IconLabelColorProperty = BindableProperty.Create(
|
public static readonly BindableProperty IconLabelColorProperty = BindableProperty.Create(
|
||||||
nameof(IconLabelColor), typeof(Color), typeof(IconLabelButton), Color.White);
|
nameof(IconLabelColor), typeof(Color), typeof(IconLabelButton), Color.White);
|
||||||
|
|
|
@ -110,8 +110,8 @@
|
||||||
VerticalOptions="CenterAndExpand"
|
VerticalOptions="CenterAndExpand"
|
||||||
Icon="{Binding Source={x:Static core:BitwardenIcons.Device}}"
|
Icon="{Binding Source={x:Static core:BitwardenIcons.Device}}"
|
||||||
Label="{u:I18n LogInWithAnotherDevice}"
|
Label="{u:I18n LogInWithAnotherDevice}"
|
||||||
ButtonCommand="{Binding LogInCommand}"
|
ButtonCommand="{Binding LogInWithDeviceCommand}"
|
||||||
IsVisible="False"/>
|
IsVisible="{Binding IsKnownDevice}"/>
|
||||||
<controls:IconLabelButton
|
<controls:IconLabelButton
|
||||||
HorizontalOptions="Fill"
|
HorizontalOptions="Fill"
|
||||||
VerticalOptions="CenterAndExpand"
|
VerticalOptions="CenterAndExpand"
|
||||||
|
|
|
@ -26,9 +26,9 @@ namespace Bit.App.Pages
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
||||||
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
|
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
|
||||||
|
_vm.LogInWithDeviceAction = () => StartLoginWithDeviceAsync().FireAndForget();
|
||||||
_vm.StartSsoLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartSsoLoginAsync());
|
_vm.StartSsoLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartSsoLoginAsync());
|
||||||
_vm.UpdateTempPasswordAction =
|
_vm.UpdateTempPasswordAction = () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||||
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
|
||||||
_vm.CloseAction = async () =>
|
_vm.CloseAction = async () =>
|
||||||
{
|
{
|
||||||
await _accountListOverlay.HideAsync();
|
await _accountListOverlay.HideAsync();
|
||||||
|
@ -122,6 +122,12 @@ namespace Bit.App.Pages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task StartLoginWithDeviceAsync()
|
||||||
|
{
|
||||||
|
var page = new LoginPasswordlessRequestPage(_vm.Email, _appOptions);
|
||||||
|
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||||
|
}
|
||||||
|
|
||||||
private async Task StartSsoLoginAsync()
|
private async Task StartSsoLoginAsync()
|
||||||
{
|
{
|
||||||
var page = new LoginSsoPage(_appOptions);
|
var page = new LoginSsoPage(_appOptions);
|
||||||
|
|
|
@ -54,6 +54,7 @@ namespace Bit.App.Pages
|
||||||
TogglePasswordCommand = new Command(TogglePassword);
|
TogglePasswordCommand = new Command(TogglePassword);
|
||||||
LogInCommand = new Command(async () => await LogInAsync());
|
LogInCommand = new Command(async () => await LogInAsync());
|
||||||
MoreCommand = new AsyncCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
|
MoreCommand = new AsyncCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||||
|
LogInWithDeviceCommand = new AsyncCommand(() => Device.InvokeOnMainThreadAsync(LogInWithDeviceAction), onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||||
|
|
||||||
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
||||||
{
|
{
|
||||||
|
@ -114,11 +115,13 @@ namespace Bit.App.Pages
|
||||||
public Command LogInCommand { get; }
|
public Command LogInCommand { get; }
|
||||||
public Command TogglePasswordCommand { get; }
|
public Command TogglePasswordCommand { get; }
|
||||||
public ICommand MoreCommand { get; internal set; }
|
public ICommand MoreCommand { get; internal set; }
|
||||||
|
public ICommand LogInWithDeviceCommand { get; }
|
||||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||||
public string LoggingInAsText => string.Format(AppResources.LoggingInAsX, Email);
|
public string LoggingInAsText => string.Format(AppResources.LoggingInAsX, Email);
|
||||||
public Action StartTwoFactorAction { get; set; }
|
public Action StartTwoFactorAction { get; set; }
|
||||||
public Action LogInSuccessAction { get; set; }
|
public Action LogInSuccessAction { get; set; }
|
||||||
|
public Action LogInWithDeviceAction { get; set; }
|
||||||
public Action UpdateTempPasswordAction { get; set; }
|
public Action UpdateTempPasswordAction { get; set; }
|
||||||
public Action StartSsoLoginAction { get; set; }
|
public Action StartSsoLoginAction { get; set; }
|
||||||
public Action CloseAction { get; set; }
|
public Action CloseAction { get; set; }
|
||||||
|
|
76
src/App/Pages/Accounts/LoginPasswordlessRequestPage.xaml
Normal file
76
src/App/Pages/Accounts/LoginPasswordlessRequestPage.xaml
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<pages:BaseContentPage
|
||||||
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
x:Class="Bit.App.Pages.LoginPasswordlessRequestPage"
|
||||||
|
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
|
x:DataType="pages:LoginPasswordlessRequestViewModel"
|
||||||
|
Title="{Binding PageTitle}">
|
||||||
|
|
||||||
|
<ContentPage.BindingContext>
|
||||||
|
<pages:LoginPasswordlessRequestViewModel />
|
||||||
|
</ContentPage.BindingContext>
|
||||||
|
|
||||||
|
<ContentPage.ToolbarItems>
|
||||||
|
<ToolbarItem Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1" x:Name="_closeItem"/>
|
||||||
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
|
<ScrollView>
|
||||||
|
<StackLayout
|
||||||
|
Padding="7, 0, 7, 20">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n LogInInitiated}"
|
||||||
|
FontSize="Title"
|
||||||
|
FontAttributes="Bold"
|
||||||
|
Margin="0,14,0,21"/>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n ANotificationHasBeenSentToYourDevice}"
|
||||||
|
FontSize="Small"
|
||||||
|
Margin="0,0,0,10"/>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice}"
|
||||||
|
FontSize="Small"
|
||||||
|
Margin="0,0,0,24"/>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n FingerprintPhrase}"
|
||||||
|
FontSize="Small"
|
||||||
|
FontAttributes="Bold"/>
|
||||||
|
<controls:MonoLabel
|
||||||
|
FormattedText="{Binding FingerprintPhrase}"
|
||||||
|
FontSize="Medium"
|
||||||
|
TextColor="{DynamicResource FingerprintPhrase}"/>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n ResendNotification}"
|
||||||
|
StyleClass="text-md"
|
||||||
|
HorizontalOptions="Start"
|
||||||
|
Margin="0,40,0,0"
|
||||||
|
TextColor="{DynamicResource HyperlinkColor}">
|
||||||
|
<Label.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Command="{Binding CreatePasswordlessLoginCommand}" />
|
||||||
|
</Label.GestureRecognizers>
|
||||||
|
</Label>
|
||||||
|
<StackLayout
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Margin="0,30,0,0">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n NeedAnotherOption}"
|
||||||
|
FontSize="Small"
|
||||||
|
VerticalTextAlignment="End"/>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n ViewAllLoginOptions}"
|
||||||
|
StyleClass="text-md"
|
||||||
|
VerticalTextAlignment="End"
|
||||||
|
VerticalOptions="CenterAndExpand"
|
||||||
|
Margin="5, 0"
|
||||||
|
TextColor="{DynamicResource HyperlinkColor}">
|
||||||
|
<Label.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Command="{Binding CloseCommand}" />
|
||||||
|
</Label.GestureRecognizers>
|
||||||
|
</Label>
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
|
</StackLayout>
|
||||||
|
</ScrollView>
|
||||||
|
</pages:BaseContentPage>
|
65
src/App/Pages/Accounts/LoginPasswordlessRequestPage.xaml.cs
Normal file
65
src/App/Pages/Accounts/LoginPasswordlessRequestPage.xaml.cs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.App.Models;
|
||||||
|
using Bit.App.Utilities;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public partial class LoginPasswordlessRequestPage : BaseContentPage
|
||||||
|
{
|
||||||
|
private LoginPasswordlessRequestViewModel _vm;
|
||||||
|
private readonly AppOptions _appOptions;
|
||||||
|
|
||||||
|
public LoginPasswordlessRequestPage(string email, AppOptions appOptions = null)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_appOptions = appOptions;
|
||||||
|
_vm = BindingContext as LoginPasswordlessRequestViewModel;
|
||||||
|
_vm.Page = this;
|
||||||
|
_vm.Email = email;
|
||||||
|
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
||||||
|
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
|
||||||
|
_vm.UpdateTempPasswordAction = () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||||
|
_vm.CloseAction = () => { Navigation.PopModalAsync(); };
|
||||||
|
|
||||||
|
_vm.CreatePasswordlessLoginCommand.Execute(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnAppearing()
|
||||||
|
{
|
||||||
|
base.OnAppearing();
|
||||||
|
_vm.StartCheckLoginRequestStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDisappearing()
|
||||||
|
{
|
||||||
|
base.OnDisappearing();
|
||||||
|
_vm.StopCheckLoginRequestStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartTwoFactorAsync()
|
||||||
|
{
|
||||||
|
var page = new TwoFactorPage(false, _appOptions);
|
||||||
|
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LogInSuccessAsync()
|
||||||
|
{
|
||||||
|
if (AppHelpers.SetAlternateMainPage(_appOptions))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||||
|
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateTempPasswordAsync()
|
||||||
|
{
|
||||||
|
var page = new UpdateTempPasswordPage();
|
||||||
|
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
194
src/App/Pages/Accounts/LoginPasswordlessRequestViewModel.cs
Normal file
194
src/App/Pages/Accounts/LoginPasswordlessRequestViewModel.cs
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.App.Resources;
|
||||||
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Domain;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
|
using Xamarin.Essentials;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public class LoginPasswordlessRequestViewModel : CaptchaProtectedViewModel
|
||||||
|
{
|
||||||
|
private const int REQUEST_TIME_UPDATE_PERIOD_IN_SECONDS = 4;
|
||||||
|
|
||||||
|
private IDeviceActionService _deviceActionService;
|
||||||
|
private IAuthService _authService;
|
||||||
|
private ISyncService _syncService;
|
||||||
|
private II18nService _i18nService;
|
||||||
|
private IStateService _stateService;
|
||||||
|
private IPlatformUtilsService _platformUtilsService;
|
||||||
|
private IEnvironmentService _environmentService;
|
||||||
|
private ILogger _logger;
|
||||||
|
|
||||||
|
protected override II18nService i18nService => _i18nService;
|
||||||
|
protected override IEnvironmentService environmentService => _environmentService;
|
||||||
|
protected override IDeviceActionService deviceActionService => _deviceActionService;
|
||||||
|
protected override IPlatformUtilsService platformUtilsService => _platformUtilsService;
|
||||||
|
|
||||||
|
private CancellationTokenSource _checkLoginRequestStatusCts;
|
||||||
|
private Task _checkLoginRequestStatusTask;
|
||||||
|
private string _fingerprintPhrase;
|
||||||
|
private string _email;
|
||||||
|
private string _requestId;
|
||||||
|
private string _requestAccessCode;
|
||||||
|
// Item1 publicKey, Item2 privateKey
|
||||||
|
private Tuple<byte[], byte[]> _requestKeyPair;
|
||||||
|
|
||||||
|
public LoginPasswordlessRequestViewModel()
|
||||||
|
{
|
||||||
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
||||||
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||||
|
_environmentService = ServiceContainer.Resolve<IEnvironmentService>();
|
||||||
|
_authService = ServiceContainer.Resolve<IAuthService>();
|
||||||
|
_syncService = ServiceContainer.Resolve<ISyncService>();
|
||||||
|
_i18nService = ServiceContainer.Resolve<II18nService>();
|
||||||
|
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||||
|
_logger = ServiceContainer.Resolve<ILogger>();
|
||||||
|
|
||||||
|
PageTitle = AppResources.LogInWithAnotherDevice;
|
||||||
|
|
||||||
|
CreatePasswordlessLoginCommand = new AsyncCommand(CreatePasswordlessLoginAsync,
|
||||||
|
onException: ex => HandleException(ex),
|
||||||
|
allowsMultipleExecutions: false);
|
||||||
|
|
||||||
|
CloseCommand = new AsyncCommand(() => Device.InvokeOnMainThreadAsync(CloseAction),
|
||||||
|
onException: _logger.Exception,
|
||||||
|
allowsMultipleExecutions: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Action StartTwoFactorAction { get; set; }
|
||||||
|
public Action LogInSuccessAction { get; set; }
|
||||||
|
public Action UpdateTempPasswordAction { get; set; }
|
||||||
|
public Action CloseAction { get; set; }
|
||||||
|
|
||||||
|
public ICommand CreatePasswordlessLoginCommand { get; }
|
||||||
|
public ICommand CloseCommand { get; }
|
||||||
|
|
||||||
|
public string FingerprintPhrase
|
||||||
|
{
|
||||||
|
get => _fingerprintPhrase;
|
||||||
|
set => SetProperty(ref _fingerprintPhrase, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Email
|
||||||
|
{
|
||||||
|
get => _email;
|
||||||
|
set => SetProperty(ref _email, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartCheckLoginRequestStatus()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_checkLoginRequestStatusCts?.Cancel();
|
||||||
|
_checkLoginRequestStatusCts = new CancellationTokenSource();
|
||||||
|
_checkLoginRequestStatusTask = new TimerTask(_logger, CheckLoginRequestStatus, _checkLoginRequestStatusCts).RunPeriodic(TimeSpan.FromSeconds(REQUEST_TIME_UPDATE_PERIOD_IN_SECONDS));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Exception(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopCheckLoginRequestStatus()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_checkLoginRequestStatusCts?.Cancel();
|
||||||
|
_checkLoginRequestStatusCts?.Dispose();
|
||||||
|
_checkLoginRequestStatusCts = null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Exception(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CheckLoginRequestStatus()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_requestId) || string.IsNullOrEmpty(_requestAccessCode))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await _authService.GetPasswordlessLoginResponseAsync(_requestId, _requestAccessCode);
|
||||||
|
|
||||||
|
if (!response.RequestApproved)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StopCheckLoginRequestStatus();
|
||||||
|
|
||||||
|
var authResult = await _authService.LogInPasswordlessAsync(Email, _requestAccessCode, _requestId, _requestKeyPair.Item2, response.Key, response.MasterPasswordHash);
|
||||||
|
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||||
|
|
||||||
|
if (await HandleCaptchaAsync(authResult.CaptchaSiteKey, authResult.CaptchaNeeded, CheckLoginRequestStatus))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authResult.TwoFactor)
|
||||||
|
{
|
||||||
|
StartTwoFactorAction?.Invoke();
|
||||||
|
}
|
||||||
|
else if (authResult.ForcePasswordReset)
|
||||||
|
{
|
||||||
|
UpdateTempPasswordAction?.Invoke();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_syncService.FullSyncAsync(true).FireAndForget();
|
||||||
|
LogInSuccessAction?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StartCheckLoginRequestStatus();
|
||||||
|
HandleException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreatePasswordlessLoginAsync()
|
||||||
|
{
|
||||||
|
await Device.InvokeOnMainThreadAsync(() => _deviceActionService.ShowLoadingAsync(AppResources.Loading));
|
||||||
|
|
||||||
|
var response = await _authService.PasswordlessCreateLoginRequestAsync(_email);
|
||||||
|
if (response != null)
|
||||||
|
{
|
||||||
|
FingerprintPhrase = response.RequestFingerprint;
|
||||||
|
_requestId = response.Id;
|
||||||
|
_requestAccessCode = response.RequestAccessCode;
|
||||||
|
_requestKeyPair = response.RequestKeyPair;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleException(Exception ex)
|
||||||
|
{
|
||||||
|
Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () =>
|
||||||
|
{
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage);
|
||||||
|
}).FireAndForget();
|
||||||
|
_logger.Exception(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -16,6 +16,22 @@ namespace Bit.App.Pages
|
||||||
protected abstract IPlatformUtilsService platformUtilsService { get; }
|
protected abstract IPlatformUtilsService platformUtilsService { get; }
|
||||||
protected string _captchaToken = null;
|
protected string _captchaToken = null;
|
||||||
|
|
||||||
|
protected async Task<bool> HandleCaptchaAsync(string captchaSiteKey, bool needsCaptcha, Func<Task> onSuccess)
|
||||||
|
{
|
||||||
|
if (!needsCaptcha)
|
||||||
|
{
|
||||||
|
_captchaToken = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await HandleCaptchaAsync(captchaSiteKey))
|
||||||
|
{
|
||||||
|
await onSuccess();
|
||||||
|
_captchaToken = null;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
protected async Task<bool> HandleCaptchaAsync(string CaptchaSiteKey)
|
protected async Task<bool> HandleCaptchaAsync(string CaptchaSiteKey)
|
||||||
{
|
{
|
||||||
var callbackUri = "bitwarden://captcha-callback";
|
var callbackUri = "bitwarden://captcha-callback";
|
||||||
|
|
54
src/App/Resources/AppResources.Designer.cs
generated
54
src/App/Resources/AppResources.Designer.cs
generated
|
@ -481,6 +481,15 @@ namespace Bit.App.Resources {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to A notification has been sent to your device..
|
||||||
|
/// </summary>
|
||||||
|
public static string ANotificationHasBeenSentToYourDevice {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ANotificationHasBeenSentToYourDevice", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to API access token.
|
/// Looks up a localized string similar to API access token.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -3480,6 +3489,15 @@ namespace Bit.App.Resources {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Log in initiated.
|
||||||
|
/// </summary>
|
||||||
|
public static string LogInInitiated {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("LogInInitiated", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Login.
|
/// Looks up a localized string similar to Login.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -3966,6 +3984,15 @@ namespace Bit.App.Resources {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Need another option?.
|
||||||
|
/// </summary>
|
||||||
|
public static string NeedAnotherOption {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("NeedAnotherOption", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Never.
|
/// Looks up a localized string similar to Never.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -4705,6 +4732,15 @@ namespace Bit.App.Resources {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Please make sure your vault is unlocked and the Fingerprint phrase matches on the other device..
|
||||||
|
/// </summary>
|
||||||
|
public static string PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Plus addressed email.
|
/// Looks up a localized string similar to Plus addressed email.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -5003,6 +5039,15 @@ namespace Bit.App.Resources {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Resend notification.
|
||||||
|
/// </summary>
|
||||||
|
public static string ResendNotification {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ResendNotification", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to This organization has an enterprise policy that will automatically enroll you in password reset. Enrollment will allow organization administrators to change your master password..
|
/// Looks up a localized string similar to This organization has an enterprise policy that will automatically enroll you in password reset. Enrollment will allow organization administrators to change your master password..
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -6542,6 +6587,15 @@ namespace Bit.App.Resources {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to View all log in options.
|
||||||
|
/// </summary>
|
||||||
|
public static string ViewAllLoginOptions {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ViewAllLoginOptions", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to View item.
|
/// Looks up a localized string similar to View item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -2491,4 +2491,22 @@ Do you want to switch to this account?</value>
|
||||||
<data name="LogInWithAnotherDevice" xml:space="preserve">
|
<data name="LogInWithAnotherDevice" xml:space="preserve">
|
||||||
<value>Log In with another device</value>
|
<value>Log In with another device</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="LogInInitiated" xml:space="preserve">
|
||||||
|
<value>Log in initiated</value>
|
||||||
|
</data>
|
||||||
|
<data name="ANotificationHasBeenSentToYourDevice" xml:space="preserve">
|
||||||
|
<value>A notification has been sent to your device.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice" xml:space="preserve">
|
||||||
|
<value>Please make sure your vault is unlocked and the Fingerprint phrase matches on the other device.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ResendNotification" xml:space="preserve">
|
||||||
|
<value>Resend notification</value>
|
||||||
|
</data>
|
||||||
|
<data name="NeedAnotherOption" xml:space="preserve">
|
||||||
|
<value>Need another option?</value>
|
||||||
|
</data>
|
||||||
|
<data name="ViewAllLoginOptions" xml:space="preserve">
|
||||||
|
<value>View all log in options</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
|
@ -84,7 +84,9 @@ namespace Bit.Core.Abstractions
|
||||||
Task<SendResponse> PutSendRemovePasswordAsync(string id);
|
Task<SendResponse> PutSendRemovePasswordAsync(string id);
|
||||||
Task DeleteSendAsync(string id);
|
Task DeleteSendAsync(string id);
|
||||||
Task<PasswordlessLoginResponse> GetAuthRequestAsync(string id);
|
Task<PasswordlessLoginResponse> GetAuthRequestAsync(string id);
|
||||||
|
Task<PasswordlessLoginResponse> GetAuthResponseAsync(string id, string accessCode);
|
||||||
Task<PasswordlessLoginResponse> PutAuthRequestAsync(string id, string key, string masterPasswordHash, string deviceIdentifier, bool requestApproved);
|
Task<PasswordlessLoginResponse> PutAuthRequestAsync(string id, string key, string masterPasswordHash, string deviceIdentifier, bool requestApproved);
|
||||||
|
Task<PasswordlessLoginResponse> PostCreateRequestAsync(PasswordlessCreateLoginRequest passwordlessCreateLoginRequest);
|
||||||
Task<string> GetUsernameFromAsync(ForwardedEmailServiceType service, UsernameGeneratorConfig config);
|
Task<string> GetUsernameFromAsync(ForwardedEmailServiceType service, UsernameGeneratorConfig config);
|
||||||
Task<bool> GetKnownDeviceAsync(string email, string deviceIdentifier);
|
Task<bool> GetKnownDeviceAsync(string email, string deviceIdentifier);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
|
using Bit.Core.Models.Request;
|
||||||
using Bit.Core.Models.Response;
|
using Bit.Core.Models.Response;
|
||||||
|
|
||||||
namespace Bit.Core.Abstractions
|
namespace Bit.Core.Abstractions
|
||||||
|
@ -26,9 +27,12 @@ namespace Bit.Core.Abstractions
|
||||||
Task<AuthResult> LogInSsoAsync(string code, string codeVerifier, string redirectUrl, string orgId);
|
Task<AuthResult> LogInSsoAsync(string code, string codeVerifier, string redirectUrl, string orgId);
|
||||||
Task<AuthResult> LogInCompleteAsync(string email, string masterPassword, TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null);
|
Task<AuthResult> LogInCompleteAsync(string email, string masterPassword, TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null);
|
||||||
Task<AuthResult> LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, string captchaToken, bool? remember = null);
|
Task<AuthResult> LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, string captchaToken, bool? remember = null);
|
||||||
|
Task<AuthResult> LogInPasswordlessAsync(string email, string accessCode, string authRequestId, byte[] decryptionKey, string userKeyCiphered, string localHashedPasswordCiphered);
|
||||||
|
|
||||||
Task<PasswordlessLoginResponse> GetPasswordlessLoginRequestByIdAsync(string id);
|
Task<PasswordlessLoginResponse> GetPasswordlessLoginRequestByIdAsync(string id);
|
||||||
|
Task<PasswordlessLoginResponse> GetPasswordlessLoginResponseAsync(string id, string accessCode);
|
||||||
Task<PasswordlessLoginResponse> PasswordlessLoginAsync(string id, string pubKey, bool requestApproved);
|
Task<PasswordlessLoginResponse> PasswordlessLoginAsync(string id, string pubKey, bool requestApproved);
|
||||||
|
Task<PasswordlessLoginResponse> PasswordlessCreateLoginRequestAsync(string email);
|
||||||
|
|
||||||
void LogOut(Action callback);
|
void LogOut(Action callback);
|
||||||
void Init();
|
void Init();
|
||||||
|
|
|
@ -46,6 +46,7 @@ namespace Bit.Core.Abstractions
|
||||||
Task<int> RandomNumberAsync(int min, int max);
|
Task<int> RandomNumberAsync(int min, int max);
|
||||||
Task<Tuple<SymmetricCryptoKey, EncString>> RemakeEncKeyAsync(SymmetricCryptoKey key);
|
Task<Tuple<SymmetricCryptoKey, EncString>> RemakeEncKeyAsync(SymmetricCryptoKey key);
|
||||||
Task<EncString> RsaEncryptAsync(byte[] data, byte[] publicKey = null);
|
Task<EncString> RsaEncryptAsync(byte[] data, byte[] publicKey = null);
|
||||||
|
Task<byte[]> RsaDecryptAsync(string encValue, byte[] privateKey = null);
|
||||||
Task SetEncKeyAsync(string encKey);
|
Task SetEncKeyAsync(string encKey);
|
||||||
Task SetEncPrivateKeyAsync(string encPrivateKey);
|
Task SetEncPrivateKeyAsync(string encPrivateKey);
|
||||||
Task SetKeyAsync(SymmetricCryptoKey key);
|
Task SetKeyAsync(SymmetricCryptoKey key);
|
||||||
|
|
34
src/Core/Models/Request/PasswordlessCreateLoginRequest.cs
Normal file
34
src/Core/Models/Request/PasswordlessCreateLoginRequest.cs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
using System;
|
||||||
|
namespace Bit.Core.Models.Request
|
||||||
|
{
|
||||||
|
public class PasswordlessCreateLoginRequest
|
||||||
|
{
|
||||||
|
public PasswordlessCreateLoginRequest(string email, string publicKey, string deviceIdentifier, string accessCode, AuthRequestType? type, string fingerprintPhrase)
|
||||||
|
{
|
||||||
|
Email = email ?? throw new ArgumentNullException(nameof(email));
|
||||||
|
PublicKey = publicKey ?? throw new ArgumentNullException(nameof(publicKey));
|
||||||
|
DeviceIdentifier = deviceIdentifier ?? throw new ArgumentNullException(nameof(deviceIdentifier));
|
||||||
|
AccessCode = accessCode ?? throw new ArgumentNullException(nameof(accessCode));
|
||||||
|
Type = type;
|
||||||
|
FingerprintPhrase = fingerprintPhrase ?? throw new ArgumentNullException(nameof(fingerprintPhrase));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
public string PublicKey { get; set; }
|
||||||
|
|
||||||
|
public string DeviceIdentifier { get; set; }
|
||||||
|
|
||||||
|
public string AccessCode { get; set; }
|
||||||
|
|
||||||
|
public AuthRequestType? Type { get; set; }
|
||||||
|
|
||||||
|
public string FingerprintPhrase { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AuthRequestType : byte
|
||||||
|
{
|
||||||
|
AuthenticateAndUnlock = 0,
|
||||||
|
Unlock = 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,13 +15,14 @@ namespace Bit.Core.Models.Request
|
||||||
public string CodeVerifier { get; set; }
|
public string CodeVerifier { get; set; }
|
||||||
public string RedirectUri { get; set; }
|
public string RedirectUri { get; set; }
|
||||||
public string Token { get; set; }
|
public string Token { get; set; }
|
||||||
|
public string AuthRequestId { get; set; }
|
||||||
public TwoFactorProviderType? Provider { get; set; }
|
public TwoFactorProviderType? Provider { get; set; }
|
||||||
public bool? Remember { get; set; }
|
public bool? Remember { get; set; }
|
||||||
public string CaptchaToken { get; set; }
|
public string CaptchaToken { get; set; }
|
||||||
public DeviceRequest Device { get; set; }
|
public DeviceRequest Device { get; set; }
|
||||||
|
|
||||||
public TokenRequest(string[] credentials, string[] codes, TwoFactorProviderType? provider, string token,
|
public TokenRequest(string[] credentials, string[] codes, TwoFactorProviderType? provider, string token,
|
||||||
bool? remember, string captchaToken, DeviceRequest device = null)
|
bool? remember, string captchaToken, DeviceRequest device = null, string authRequestId = null)
|
||||||
{
|
{
|
||||||
if (credentials != null && credentials.Length > 1)
|
if (credentials != null && credentials.Length > 1)
|
||||||
{
|
{
|
||||||
|
@ -39,6 +40,7 @@ namespace Bit.Core.Models.Request
|
||||||
Remember = remember;
|
Remember = remember;
|
||||||
Device = device;
|
Device = device;
|
||||||
CaptchaToken = captchaToken;
|
CaptchaToken = captchaToken;
|
||||||
|
AuthRequestId = authRequestId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<string, string> ToIdentityToken(string clientId)
|
public Dictionary<string, string> ToIdentityToken(string clientId)
|
||||||
|
@ -67,6 +69,11 @@ namespace Bit.Core.Models.Request
|
||||||
throw new Exception("must provide credentials or codes");
|
throw new Exception("must provide credentials or codes");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (AuthRequestId != null)
|
||||||
|
{
|
||||||
|
obj.Add("authRequest", AuthRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
if (Device != null)
|
if (Device != null)
|
||||||
{
|
{
|
||||||
obj.Add("deviceType", ((int)Device.Type).ToString());
|
obj.Add("deviceType", ((int)Device.Type).ToString());
|
||||||
|
|
|
@ -15,5 +15,7 @@ namespace Bit.Core.Models.Response
|
||||||
public DateTime CreationDate { get; set; }
|
public DateTime CreationDate { get; set; }
|
||||||
public bool RequestApproved { get; set; }
|
public bool RequestApproved { get; set; }
|
||||||
public string Origin { get; set; }
|
public string Origin { get; set; }
|
||||||
|
public string RequestAccessCode { get; set; }
|
||||||
|
public Tuple<byte[], byte[]> RequestKeyPair { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -541,6 +541,16 @@ namespace Bit.Core.Services
|
||||||
return SendAsync<object, PasswordlessLoginResponse>(HttpMethod.Get, $"/auth-requests/{id}", null, true, true);
|
return SendAsync<object, PasswordlessLoginResponse>(HttpMethod.Get, $"/auth-requests/{id}", null, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<PasswordlessLoginResponse> GetAuthResponseAsync(string id, string accessCode)
|
||||||
|
{
|
||||||
|
return SendAsync<object, PasswordlessLoginResponse>(HttpMethod.Get, $"/auth-requests/{id}/response?code={accessCode}", null, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<PasswordlessLoginResponse> PostCreateRequestAsync(PasswordlessCreateLoginRequest passwordlessCreateLoginRequest)
|
||||||
|
{
|
||||||
|
return SendAsync<object, PasswordlessLoginResponse>(HttpMethod.Post, $"/auth-requests", passwordlessCreateLoginRequest, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
public Task<PasswordlessLoginResponse> PutAuthRequestAsync(string id, string encKey, string encMasterPasswordHash, string deviceIdentifier, bool requestApproved)
|
public Task<PasswordlessLoginResponse> PutAuthRequestAsync(string id, string encKey, string encMasterPasswordHash, string deviceIdentifier, bool requestApproved)
|
||||||
{
|
{
|
||||||
var request = new PasswordlessLoginRequest(encKey, encMasterPasswordHash, deviceIdentifier, requestApproved);
|
var request = new PasswordlessLoginRequest(encKey, encMasterPasswordHash, deviceIdentifier, requestApproved);
|
||||||
|
|
|
@ -24,8 +24,8 @@ namespace Bit.Core.Services
|
||||||
private readonly IPlatformUtilsService _platformUtilsService;
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
private readonly IMessagingService _messagingService;
|
private readonly IMessagingService _messagingService;
|
||||||
private readonly IKeyConnectorService _keyConnectorService;
|
private readonly IKeyConnectorService _keyConnectorService;
|
||||||
|
private readonly IPasswordGenerationService _passwordGenerationService;
|
||||||
private readonly bool _setCryptoKeys;
|
private readonly bool _setCryptoKeys;
|
||||||
|
|
||||||
private SymmetricCryptoKey _key;
|
private SymmetricCryptoKey _key;
|
||||||
|
|
||||||
public AuthService(
|
public AuthService(
|
||||||
|
@ -40,6 +40,7 @@ namespace Bit.Core.Services
|
||||||
IMessagingService messagingService,
|
IMessagingService messagingService,
|
||||||
IVaultTimeoutService vaultTimeoutService,
|
IVaultTimeoutService vaultTimeoutService,
|
||||||
IKeyConnectorService keyConnectorService,
|
IKeyConnectorService keyConnectorService,
|
||||||
|
IPasswordGenerationService passwordGenerationService,
|
||||||
bool setCryptoKeys = true)
|
bool setCryptoKeys = true)
|
||||||
{
|
{
|
||||||
_cryptoService = cryptoService;
|
_cryptoService = cryptoService;
|
||||||
|
@ -52,6 +53,7 @@ namespace Bit.Core.Services
|
||||||
_platformUtilsService = platformUtilsService;
|
_platformUtilsService = platformUtilsService;
|
||||||
_messagingService = messagingService;
|
_messagingService = messagingService;
|
||||||
_keyConnectorService = keyConnectorService;
|
_keyConnectorService = keyConnectorService;
|
||||||
|
_passwordGenerationService = passwordGenerationService;
|
||||||
_setCryptoKeys = setCryptoKeys;
|
_setCryptoKeys = setCryptoKeys;
|
||||||
|
|
||||||
TwoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
|
TwoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
|
||||||
|
@ -137,6 +139,14 @@ namespace Bit.Core.Services
|
||||||
null, captchaToken);
|
null, captchaToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<AuthResult> LogInPasswordlessAsync(string email, string accessCode, string authRequestId, byte[] decryptionKey, string userKeyCiphered, string localHashedPasswordCiphered)
|
||||||
|
{
|
||||||
|
var decKey = await _cryptoService.RsaDecryptAsync(userKeyCiphered, decryptionKey);
|
||||||
|
var decPasswordHash = await _cryptoService.RsaDecryptAsync(localHashedPasswordCiphered, decryptionKey);
|
||||||
|
return await LogInHelperAsync(email, accessCode, Encoding.UTF8.GetString(decPasswordHash), null, null, null, new SymmetricCryptoKey(decKey), null, null,
|
||||||
|
null, null, authRequestId: authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<AuthResult> LogInSsoAsync(string code, string codeVerifier, string redirectUrl, string orgId)
|
public async Task<AuthResult> LogInSsoAsync(string code, string codeVerifier, string redirectUrl, string orgId)
|
||||||
{
|
{
|
||||||
SelectedTwoFactorProviderType = null;
|
SelectedTwoFactorProviderType = null;
|
||||||
|
@ -286,7 +296,7 @@ namespace Bit.Core.Services
|
||||||
private async Task<AuthResult> LogInHelperAsync(string email, string hashedPassword, string localHashedPassword,
|
private async Task<AuthResult> LogInHelperAsync(string email, string hashedPassword, string localHashedPassword,
|
||||||
string code, string codeVerifier, string redirectUrl, SymmetricCryptoKey key,
|
string code, string codeVerifier, string redirectUrl, SymmetricCryptoKey key,
|
||||||
TwoFactorProviderType? twoFactorProvider = null, string twoFactorToken = null, bool? remember = null,
|
TwoFactorProviderType? twoFactorProvider = null, string twoFactorToken = null, bool? remember = null,
|
||||||
string captchaToken = null, string orgId = null)
|
string captchaToken = null, string orgId = null, string authRequestId = null)
|
||||||
{
|
{
|
||||||
var storedTwoFactorToken = await _tokenService.GetTwoFactorTokenAsync(email);
|
var storedTwoFactorToken = await _tokenService.GetTwoFactorTokenAsync(email);
|
||||||
var appId = await _appIdService.GetAppIdAsync();
|
var appId = await _appIdService.GetAppIdAsync();
|
||||||
|
@ -322,6 +332,10 @@ namespace Bit.Core.Services
|
||||||
request = new TokenRequest(emailPassword, codeCodeVerifier, TwoFactorProviderType.Remember,
|
request = new TokenRequest(emailPassword, codeCodeVerifier, TwoFactorProviderType.Remember,
|
||||||
storedTwoFactorToken, false, captchaToken, deviceRequest);
|
storedTwoFactorToken, false, captchaToken, deviceRequest);
|
||||||
}
|
}
|
||||||
|
else if (authRequestId != null)
|
||||||
|
{
|
||||||
|
request = new TokenRequest(emailPassword, null, null, null, false, null, deviceRequest, authRequestId);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
request = new TokenRequest(emailPassword, codeCodeVerifier, null, null, false, captchaToken, deviceRequest);
|
request = new TokenRequest(emailPassword, codeCodeVerifier, null, null, false, captchaToken, deviceRequest);
|
||||||
|
@ -476,6 +490,11 @@ namespace Bit.Core.Services
|
||||||
return await _apiService.GetAuthRequestAsync(id);
|
return await _apiService.GetAuthRequestAsync(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<PasswordlessLoginResponse> GetPasswordlessLoginResponseAsync(string id, string accessCode)
|
||||||
|
{
|
||||||
|
return await _apiService.GetAuthResponseAsync(id, accessCode);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<PasswordlessLoginResponse> PasswordlessLoginAsync(string id, string pubKey, bool requestApproved)
|
public async Task<PasswordlessLoginResponse> PasswordlessLoginAsync(string id, string pubKey, bool requestApproved)
|
||||||
{
|
{
|
||||||
var publicKey = CoreHelpers.Base64UrlDecode(pubKey);
|
var publicKey = CoreHelpers.Base64UrlDecode(pubKey);
|
||||||
|
@ -485,5 +504,25 @@ namespace Bit.Core.Services
|
||||||
var deviceId = await _appIdService.GetAppIdAsync();
|
var deviceId = await _appIdService.GetAppIdAsync();
|
||||||
return await _apiService.PutAuthRequestAsync(id, encryptedKey.EncryptedString, encryptedMasterPassword.EncryptedString, deviceId, requestApproved);
|
return await _apiService.PutAuthRequestAsync(id, encryptedKey.EncryptedString, encryptedMasterPassword.EncryptedString, deviceId, requestApproved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<PasswordlessLoginResponse> PasswordlessCreateLoginRequestAsync(string email)
|
||||||
|
{
|
||||||
|
var deviceId = await _appIdService.GetAppIdAsync();
|
||||||
|
var keyPair = await _cryptoFunctionService.RsaGenerateKeyPairAsync(2048);
|
||||||
|
var generatedFingerprintPhrase = await _cryptoService.GetFingerprintAsync(email, keyPair.Item1);
|
||||||
|
var fingerprintPhrase = string.Join("-", generatedFingerprintPhrase);
|
||||||
|
var publicB64 = Convert.ToBase64String(keyPair.Item1);
|
||||||
|
var accessCode = await _passwordGenerationService.GeneratePasswordAsync(new PasswordGenerationOptions(true) { Length = 25 });
|
||||||
|
var passwordlessCreateLoginRequest = new PasswordlessCreateLoginRequest(email, publicB64, deviceId, accessCode, AuthRequestType.AuthenticateAndUnlock, fingerprintPhrase);
|
||||||
|
var response = await _apiService.PostCreateRequestAsync(passwordlessCreateLoginRequest);
|
||||||
|
|
||||||
|
if (response != null)
|
||||||
|
{
|
||||||
|
response.RequestKeyPair = keyPair;
|
||||||
|
response.RequestAccessCode = accessCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -723,7 +723,7 @@ namespace Bit.Core.Services
|
||||||
return await _cryptoFunctionService.AesDecryptAsync(data, iv, theKey.EncKey);
|
return await _cryptoFunctionService.AesDecryptAsync(data, iv, theKey.EncKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<byte[]> RsaDecryptAsync(string encValue)
|
public async Task<byte[]> RsaDecryptAsync(string encValue, byte[] privateKey = null)
|
||||||
{
|
{
|
||||||
var headerPieces = encValue.Split('.');
|
var headerPieces = encValue.Split('.');
|
||||||
EncryptionType? encType = null;
|
EncryptionType? encType = null;
|
||||||
|
@ -750,7 +750,12 @@ namespace Bit.Core.Services
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = Convert.FromBase64String(encPieces[0]);
|
var data = Convert.FromBase64String(encPieces[0]);
|
||||||
var privateKey = await GetPrivateKeyAsync();
|
|
||||||
|
if (privateKey is null)
|
||||||
|
{
|
||||||
|
privateKey = await GetPrivateKeyAsync();
|
||||||
|
}
|
||||||
|
|
||||||
if (privateKey == null)
|
if (privateKey == null)
|
||||||
{
|
{
|
||||||
throw new Exception("No private key.");
|
throw new Exception("No private key.");
|
||||||
|
|
|
@ -77,7 +77,7 @@ namespace Bit.Core.Utilities
|
||||||
var totpService = new TotpService(cryptoFunctionService);
|
var totpService = new TotpService(cryptoFunctionService);
|
||||||
var authService = new AuthService(cryptoService, cryptoFunctionService, apiService, stateService,
|
var authService = new AuthService(cryptoService, cryptoFunctionService, apiService, stateService,
|
||||||
tokenService, appIdService, i18nService, platformUtilsService, messagingService, vaultTimeoutService,
|
tokenService, appIdService, i18nService, platformUtilsService, messagingService, vaultTimeoutService,
|
||||||
keyConnectorService);
|
keyConnectorService, passwordGenerationService);
|
||||||
var exportService = new ExportService(folderService, cipherService, cryptoService);
|
var exportService = new ExportService(folderService, cipherService, cryptoService);
|
||||||
var auditService = new AuditService(cryptoFunctionService, apiService);
|
var auditService = new AuditService(cryptoFunctionService, apiService);
|
||||||
var environmentService = new EnvironmentService(apiService, stateService);
|
var environmentService = new EnvironmentService(apiService, stateService);
|
||||||
|
|
|
@ -497,6 +497,7 @@ namespace Bit.iOS.Autofill
|
||||||
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false));
|
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false));
|
||||||
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
|
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
|
||||||
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
|
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
|
||||||
|
vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(email));
|
||||||
vm.LogInSuccessAction = () => DismissLockAndContinue();
|
vm.LogInSuccessAction = () => DismissLockAndContinue();
|
||||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage(checkRememberedEmail: false));
|
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage(checkRememberedEmail: false));
|
||||||
}
|
}
|
||||||
|
@ -509,6 +510,29 @@ namespace Bit.iOS.Autofill
|
||||||
LogoutIfAuthed();
|
LogoutIfAuthed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LaunchLoginWithDevice(string email = null)
|
||||||
|
{
|
||||||
|
var appOptions = new AppOptions { IosExtension = true };
|
||||||
|
var app = new App.App(appOptions);
|
||||||
|
var loginWithDevicePage = new LoginPasswordlessRequestPage(email, appOptions);
|
||||||
|
ThemeManager.SetTheme(app.Resources);
|
||||||
|
ThemeManager.ApplyResourcesTo(loginWithDevicePage);
|
||||||
|
if (loginWithDevicePage.BindingContext is LoginPasswordlessRequestViewModel vm)
|
||||||
|
{
|
||||||
|
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false));
|
||||||
|
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
|
||||||
|
vm.LogInSuccessAction = () => DismissLockAndContinue();
|
||||||
|
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||||
|
}
|
||||||
|
|
||||||
|
var navigationPage = new NavigationPage(loginWithDevicePage);
|
||||||
|
var loginController = navigationPage.CreateViewController();
|
||||||
|
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||||
|
PresentViewController(loginController, true, null);
|
||||||
|
|
||||||
|
LogoutIfAuthed();
|
||||||
|
}
|
||||||
|
|
||||||
private void LaunchLoginSsoFlow()
|
private void LaunchLoginSsoFlow()
|
||||||
{
|
{
|
||||||
var loginPage = new LoginSsoPage();
|
var loginPage = new LoginSsoPage();
|
||||||
|
|
|
@ -518,6 +518,7 @@ namespace Bit.iOS.Extension
|
||||||
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false));
|
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false));
|
||||||
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
|
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
|
||||||
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
|
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
|
||||||
|
vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(email));
|
||||||
vm.LogInSuccessAction = () => DismissLockAndContinue();
|
vm.LogInSuccessAction = () => DismissLockAndContinue();
|
||||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage(checkRememberedEmail: false));
|
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage(checkRememberedEmail: false));
|
||||||
}
|
}
|
||||||
|
@ -530,6 +531,29 @@ namespace Bit.iOS.Extension
|
||||||
LogoutIfAuthed();
|
LogoutIfAuthed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LaunchLoginWithDevice(string email = null)
|
||||||
|
{
|
||||||
|
var appOptions = new AppOptions { IosExtension = true };
|
||||||
|
var app = new App.App(appOptions);
|
||||||
|
var loginWithDevicePage = new LoginPasswordlessRequestPage(email, appOptions);
|
||||||
|
ThemeManager.SetTheme(app.Resources);
|
||||||
|
ThemeManager.ApplyResourcesTo(loginWithDevicePage);
|
||||||
|
if (loginWithDevicePage.BindingContext is LoginPasswordlessRequestViewModel vm)
|
||||||
|
{
|
||||||
|
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false));
|
||||||
|
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
|
||||||
|
vm.LogInSuccessAction = () => DismissLockAndContinue();
|
||||||
|
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||||
|
}
|
||||||
|
|
||||||
|
var navigationPage = new NavigationPage(loginWithDevicePage);
|
||||||
|
var loginController = navigationPage.CreateViewController();
|
||||||
|
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||||
|
PresentViewController(loginController, true, null);
|
||||||
|
|
||||||
|
LogoutIfAuthed();
|
||||||
|
}
|
||||||
|
|
||||||
private void LaunchLoginSsoFlow()
|
private void LaunchLoginSsoFlow()
|
||||||
{
|
{
|
||||||
var loginPage = new LoginSsoPage();
|
var loginPage = new LoginSsoPage();
|
||||||
|
|
|
@ -339,10 +339,8 @@ namespace Bit.iOS.ShareExtension
|
||||||
vm.StartTwoFactorAction = () => DismissAndLaunch(() => LaunchTwoFactorFlow(false));
|
vm.StartTwoFactorAction = () => DismissAndLaunch(() => LaunchTwoFactorFlow(false));
|
||||||
vm.UpdateTempPasswordAction = () => DismissAndLaunch(() => LaunchUpdateTempPasswordFlow());
|
vm.UpdateTempPasswordAction = () => DismissAndLaunch(() => LaunchUpdateTempPasswordFlow());
|
||||||
vm.StartSsoLoginAction = () => DismissAndLaunch(() => LaunchLoginSsoFlow());
|
vm.StartSsoLoginAction = () => DismissAndLaunch(() => LaunchLoginSsoFlow());
|
||||||
vm.LogInSuccessAction = () =>
|
vm.LogInWithDeviceAction = () => DismissAndLaunch(() => LaunchLoginWithDevice(email));
|
||||||
{
|
vm.LogInSuccessAction = () => { DismissLockAndContinue(); };
|
||||||
DismissLockAndContinue();
|
|
||||||
};
|
|
||||||
vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage(checkRememberedEmail: false));
|
vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage(checkRememberedEmail: false));
|
||||||
}
|
}
|
||||||
NavigateToPage(loginPage);
|
NavigateToPage(loginPage);
|
||||||
|
@ -350,6 +348,22 @@ namespace Bit.iOS.ShareExtension
|
||||||
LogoutIfAuthed();
|
LogoutIfAuthed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LaunchLoginWithDevice(string email = null)
|
||||||
|
{
|
||||||
|
var loginWithDevicePage = new LoginPasswordlessRequestPage(email, _appOptions.Value);
|
||||||
|
SetupAppAndApplyResources(loginWithDevicePage);
|
||||||
|
if (loginWithDevicePage.BindingContext is LoginPasswordlessRequestViewModel vm)
|
||||||
|
{
|
||||||
|
vm.StartTwoFactorAction = () => DismissAndLaunch(() => LaunchTwoFactorFlow(false));
|
||||||
|
vm.UpdateTempPasswordAction = () => DismissAndLaunch(() => LaunchUpdateTempPasswordFlow());
|
||||||
|
vm.LogInSuccessAction = () => { DismissLockAndContinue(); };
|
||||||
|
vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage());
|
||||||
|
}
|
||||||
|
NavigateToPage(loginWithDevicePage);
|
||||||
|
|
||||||
|
LogoutIfAuthed();
|
||||||
|
}
|
||||||
|
|
||||||
private void LaunchLoginSsoFlow()
|
private void LaunchLoginSsoFlow()
|
||||||
{
|
{
|
||||||
var loginPage = new LoginSsoPage();
|
var loginPage = new LoginSsoPage();
|
||||||
|
|
Loading…
Reference in a new issue