diff --git a/src/App/App.csproj b/src/App/App.csproj
index c60580e2d..8dbcc000f 100644
--- a/src/App/App.csproj
+++ b/src/App/App.csproj
@@ -125,6 +125,9 @@
LoginPasswordlessPage.xaml
+
+ LoginPasswordlessRequestPage.xaml
+
diff --git a/src/App/Controls/IconLabelButton/IconLabelButton.xaml b/src/App/Controls/IconLabelButton/IconLabelButton.xaml
index 5b88dcd91..73af0751e 100644
--- a/src/App/Controls/IconLabelButton/IconLabelButton.xaml
+++ b/src/App/Controls/IconLabelButton/IconLabelButton.xaml
@@ -8,6 +8,7 @@
Padding="1"
StyleClass="btn-icon-secondary"
BackgroundColor="{Binding IconLabelBorderColor, Source={x:Reference _iconLabelButton}}"
+ BorderColor="Transparent"
HasShadow="False">
@@ -17,6 +18,7 @@
Padding="0"
CornerRadius="{Binding CornerRadius, Source={x:Reference _iconLabelButton}}"
BackgroundColor="{Binding IconLabelBackgroundColor, Source={x:Reference _iconLabelButton}}"
+ BorderColor="Transparent"
IsClippedToBounds="True"
HasShadow="False">
+ ButtonCommand="{Binding LogInWithDeviceCommand}"
+ IsVisible="{Binding IsKnownDevice}"/>
Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
+ _vm.LogInWithDeviceAction = () => StartLoginWithDeviceAsync().FireAndForget();
_vm.StartSsoLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartSsoLoginAsync());
- _vm.UpdateTempPasswordAction =
- () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
+ _vm.UpdateTempPasswordAction = () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
_vm.CloseAction = async () =>
{
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()
{
var page = new LoginSsoPage(_appOptions);
diff --git a/src/App/Pages/Accounts/LoginPageViewModel.cs b/src/App/Pages/Accounts/LoginPageViewModel.cs
index 9bf0ebe2a..0dbcd07c1 100644
--- a/src/App/Pages/Accounts/LoginPageViewModel.cs
+++ b/src/App/Pages/Accounts/LoginPageViewModel.cs
@@ -54,6 +54,7 @@ namespace Bit.App.Pages
TogglePasswordCommand = new Command(TogglePassword);
LogInCommand = new Command(async () => await LogInAsync());
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)
{
@@ -114,11 +115,13 @@ namespace Bit.App.Pages
public Command LogInCommand { get; }
public Command TogglePasswordCommand { get; }
public ICommand MoreCommand { get; internal set; }
+ public ICommand LogInWithDeviceCommand { get; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string LoggingInAsText => string.Format(AppResources.LoggingInAsX, Email);
public Action StartTwoFactorAction { get; set; }
public Action LogInSuccessAction { get; set; }
+ public Action LogInWithDeviceAction { get; set; }
public Action UpdateTempPasswordAction { get; set; }
public Action StartSsoLoginAction { get; set; }
public Action CloseAction { get; set; }
diff --git a/src/App/Pages/Accounts/LoginPasswordlessRequestPage.xaml b/src/App/Pages/Accounts/LoginPasswordlessRequestPage.xaml
new file mode 100644
index 000000000..d6ea864f0
--- /dev/null
+++ b/src/App/Pages/Accounts/LoginPasswordlessRequestPage.xaml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/App/Pages/Accounts/LoginPasswordlessRequestPage.xaml.cs b/src/App/Pages/Accounts/LoginPasswordlessRequestPage.xaml.cs
new file mode 100644
index 000000000..383161c7c
--- /dev/null
+++ b/src/App/Pages/Accounts/LoginPasswordlessRequestPage.xaml.cs
@@ -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));
+ }
+ }
+}
+
diff --git a/src/App/Pages/Accounts/LoginPasswordlessRequestViewModel.cs b/src/App/Pages/Accounts/LoginPasswordlessRequestViewModel.cs
new file mode 100644
index 000000000..b21437c7a
--- /dev/null
+++ b/src/App/Pages/Accounts/LoginPasswordlessRequestViewModel.cs
@@ -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 _requestKeyPair;
+
+ public LoginPasswordlessRequestViewModel()
+ {
+ _deviceActionService = ServiceContainer.Resolve();
+ _platformUtilsService = ServiceContainer.Resolve();
+ _environmentService = ServiceContainer.Resolve();
+ _authService = ServiceContainer.Resolve();
+ _syncService = ServiceContainer.Resolve();
+ _i18nService = ServiceContainer.Resolve();
+ _stateService = ServiceContainer.Resolve();
+ _logger = ServiceContainer.Resolve();
+
+ 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);
+ }
+ }
+}
+
diff --git a/src/App/Pages/CaptchaProtectedViewModel.cs b/src/App/Pages/CaptchaProtectedViewModel.cs
index 042d1423c..2f9969080 100644
--- a/src/App/Pages/CaptchaProtectedViewModel.cs
+++ b/src/App/Pages/CaptchaProtectedViewModel.cs
@@ -16,6 +16,22 @@ namespace Bit.App.Pages
protected abstract IPlatformUtilsService platformUtilsService { get; }
protected string _captchaToken = null;
+ protected async Task HandleCaptchaAsync(string captchaSiteKey, bool needsCaptcha, Func onSuccess)
+ {
+ if (!needsCaptcha)
+ {
+ _captchaToken = null;
+ return false;
+ }
+
+ if (await HandleCaptchaAsync(captchaSiteKey))
+ {
+ await onSuccess();
+ _captchaToken = null;
+ }
+ return true;
+ }
+
protected async Task HandleCaptchaAsync(string CaptchaSiteKey)
{
var callbackUri = "bitwarden://captcha-callback";
diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs
index a98953e55..7da43e892 100644
--- a/src/App/Resources/AppResources.Designer.cs
+++ b/src/App/Resources/AppResources.Designer.cs
@@ -481,6 +481,15 @@ namespace Bit.App.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to A notification has been sent to your device..
+ ///
+ public static string ANotificationHasBeenSentToYourDevice {
+ get {
+ return ResourceManager.GetString("ANotificationHasBeenSentToYourDevice", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to API access token.
///
@@ -3480,6 +3489,15 @@ namespace Bit.App.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Log in initiated.
+ ///
+ public static string LogInInitiated {
+ get {
+ return ResourceManager.GetString("LogInInitiated", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Login.
///
@@ -3966,6 +3984,15 @@ namespace Bit.App.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Need another option?.
+ ///
+ public static string NeedAnotherOption {
+ get {
+ return ResourceManager.GetString("NeedAnotherOption", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Never.
///
@@ -4705,6 +4732,15 @@ namespace Bit.App.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Please make sure your vault is unlocked and the Fingerprint phrase matches on the other device..
+ ///
+ public static string PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice {
+ get {
+ return ResourceManager.GetString("PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Plus addressed email.
///
@@ -5003,6 +5039,15 @@ namespace Bit.App.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Resend notification.
+ ///
+ public static string ResendNotification {
+ get {
+ return ResourceManager.GetString("ResendNotification", resourceCulture);
+ }
+ }
+
///
/// 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..
///
@@ -6542,6 +6587,15 @@ namespace Bit.App.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to View all log in options.
+ ///
+ public static string ViewAllLoginOptions {
+ get {
+ return ResourceManager.GetString("ViewAllLoginOptions", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to View item.
///
diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx
index 03f1c7aba..610ad0597 100644
--- a/src/App/Resources/AppResources.resx
+++ b/src/App/Resources/AppResources.resx
@@ -2491,4 +2491,22 @@ Do you want to switch to this account?
Log In with another device
+
+ Log in initiated
+
+
+ A notification has been sent to your device.
+
+
+ Please make sure your vault is unlocked and the Fingerprint phrase matches on the other device.
+
+
+ Resend notification
+
+
+ Need another option?
+
+
+ View all log in options
+
diff --git a/src/Core/Abstractions/IApiService.cs b/src/Core/Abstractions/IApiService.cs
index 208d17c0c..5c85e41ea 100644
--- a/src/Core/Abstractions/IApiService.cs
+++ b/src/Core/Abstractions/IApiService.cs
@@ -84,7 +84,9 @@ namespace Bit.Core.Abstractions
Task PutSendRemovePasswordAsync(string id);
Task DeleteSendAsync(string id);
Task GetAuthRequestAsync(string id);
+ Task GetAuthResponseAsync(string id, string accessCode);
Task PutAuthRequestAsync(string id, string key, string masterPasswordHash, string deviceIdentifier, bool requestApproved);
+ Task PostCreateRequestAsync(PasswordlessCreateLoginRequest passwordlessCreateLoginRequest);
Task GetUsernameFromAsync(ForwardedEmailServiceType service, UsernameGeneratorConfig config);
Task GetKnownDeviceAsync(string email, string deviceIdentifier);
}
diff --git a/src/Core/Abstractions/IAuthService.cs b/src/Core/Abstractions/IAuthService.cs
index e151daabd..bebd290d5 100644
--- a/src/Core/Abstractions/IAuthService.cs
+++ b/src/Core/Abstractions/IAuthService.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
+using Bit.Core.Models.Request;
using Bit.Core.Models.Response;
namespace Bit.Core.Abstractions
@@ -26,9 +27,12 @@ namespace Bit.Core.Abstractions
Task LogInSsoAsync(string code, string codeVerifier, string redirectUrl, string orgId);
Task LogInCompleteAsync(string email, string masterPassword, TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null);
Task LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, string captchaToken, bool? remember = null);
+ Task LogInPasswordlessAsync(string email, string accessCode, string authRequestId, byte[] decryptionKey, string userKeyCiphered, string localHashedPasswordCiphered);
Task GetPasswordlessLoginRequestByIdAsync(string id);
+ Task GetPasswordlessLoginResponseAsync(string id, string accessCode);
Task PasswordlessLoginAsync(string id, string pubKey, bool requestApproved);
+ Task PasswordlessCreateLoginRequestAsync(string email);
void LogOut(Action callback);
void Init();
diff --git a/src/Core/Abstractions/ICryptoService.cs b/src/Core/Abstractions/ICryptoService.cs
index c06b5b272..b628f3ac1 100644
--- a/src/Core/Abstractions/ICryptoService.cs
+++ b/src/Core/Abstractions/ICryptoService.cs
@@ -46,6 +46,7 @@ namespace Bit.Core.Abstractions
Task RandomNumberAsync(int min, int max);
Task> RemakeEncKeyAsync(SymmetricCryptoKey key);
Task RsaEncryptAsync(byte[] data, byte[] publicKey = null);
+ Task RsaDecryptAsync(string encValue, byte[] privateKey = null);
Task SetEncKeyAsync(string encKey);
Task SetEncPrivateKeyAsync(string encPrivateKey);
Task SetKeyAsync(SymmetricCryptoKey key);
diff --git a/src/Core/Models/Request/PasswordlessCreateLoginRequest.cs b/src/Core/Models/Request/PasswordlessCreateLoginRequest.cs
new file mode 100644
index 000000000..aeaff5f1f
--- /dev/null
+++ b/src/Core/Models/Request/PasswordlessCreateLoginRequest.cs
@@ -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
+ }
+}
diff --git a/src/Core/Models/Request/TokenRequest.cs b/src/Core/Models/Request/TokenRequest.cs
index fe299e455..75d14fdb9 100644
--- a/src/Core/Models/Request/TokenRequest.cs
+++ b/src/Core/Models/Request/TokenRequest.cs
@@ -15,13 +15,14 @@ namespace Bit.Core.Models.Request
public string CodeVerifier { get; set; }
public string RedirectUri { get; set; }
public string Token { get; set; }
+ public string AuthRequestId { get; set; }
public TwoFactorProviderType? Provider { get; set; }
public bool? Remember { get; set; }
public string CaptchaToken { get; set; }
public DeviceRequest Device { get; set; }
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)
{
@@ -39,6 +40,7 @@ namespace Bit.Core.Models.Request
Remember = remember;
Device = device;
CaptchaToken = captchaToken;
+ AuthRequestId = authRequestId;
}
public Dictionary ToIdentityToken(string clientId)
@@ -67,6 +69,11 @@ namespace Bit.Core.Models.Request
throw new Exception("must provide credentials or codes");
}
+ if (AuthRequestId != null)
+ {
+ obj.Add("authRequest", AuthRequestId);
+ }
+
if (Device != null)
{
obj.Add("deviceType", ((int)Device.Type).ToString());
diff --git a/src/Core/Models/Response/PasswordlessLoginResponse.cs b/src/Core/Models/Response/PasswordlessLoginResponse.cs
index 20113de8e..ca830e75f 100644
--- a/src/Core/Models/Response/PasswordlessLoginResponse.cs
+++ b/src/Core/Models/Response/PasswordlessLoginResponse.cs
@@ -15,5 +15,7 @@ namespace Bit.Core.Models.Response
public DateTime CreationDate { get; set; }
public bool RequestApproved { get; set; }
public string Origin { get; set; }
+ public string RequestAccessCode { get; set; }
+ public Tuple RequestKeyPair { get; set; }
}
}
diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs
index 256135310..938cbecfa 100644
--- a/src/Core/Services/ApiService.cs
+++ b/src/Core/Services/ApiService.cs
@@ -541,6 +541,16 @@ namespace Bit.Core.Services
return SendAsync