mirror of
https://github.com/bitwarden/android.git
synced 2024-12-25 18:38:27 +03:00
[PM-4615] [PM-6217] Add new DUO frameless 2fa flow (#2956)
* [PM-4615] Update DUO 2FA screen to support DUO frameless flow.
This commit is contained in:
parent
f1c20e03bc
commit
4c88524f0e
5 changed files with 166 additions and 12 deletions
|
@ -47,6 +47,7 @@ namespace Bit.Core
|
||||||
public const string ConfigsKey = "configsKey";
|
public const string ConfigsKey = "configsKey";
|
||||||
public const string DisplayEuEnvironmentFlag = "display-eu-environment";
|
public const string DisplayEuEnvironmentFlag = "display-eu-environment";
|
||||||
public const string RegionEnvironment = "regionEnvironment";
|
public const string RegionEnvironment = "regionEnvironment";
|
||||||
|
public const string DuoCallback = "bitwarden://duo-callback";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This key is used to store the value of "ShouldConnectToWatch" of the last user that had logged in
|
/// This key is used to store the value of "ShouldConnectToWatch" of the last user that had logged in
|
||||||
|
|
|
@ -132,14 +132,26 @@
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<StackLayout Spacing="0" Padding="0" IsVisible="{Binding DuoMethod, Mode=OneWay}"
|
<StackLayout
|
||||||
VerticalOptions="StartAndExpand">
|
Spacing="0"
|
||||||
|
Padding="0"
|
||||||
|
IsVisible="{Binding DuoMethod, Mode=OneWay}"
|
||||||
|
VerticalOptions="FillAndExpand">
|
||||||
|
<Label
|
||||||
|
StyleClass="box"
|
||||||
|
Text="{Binding DuoFramelessLabel}"
|
||||||
|
HorizontalOptions="StartAndExpand"
|
||||||
|
Margin="10,21"
|
||||||
|
IsVisible="{Binding IsDuoFrameless}"/>
|
||||||
<controls:HybridWebView
|
<controls:HybridWebView
|
||||||
x:Name="_duoWebView"
|
x:Name="_duoWebView"
|
||||||
HorizontalOptions="FillAndExpand"
|
HorizontalOptions="FillAndExpand"
|
||||||
VerticalOptions="FillAndExpand"
|
VerticalOptions="FillAndExpand"
|
||||||
HeightRequest="{Binding DuoWebViewHeight, Mode=OneWay}" />
|
HeightRequest="{Binding DuoWebViewHeight, Mode=OneWay}"
|
||||||
<StackLayout StyleClass="box" VerticalOptions="End">
|
IsVisible="{Binding IsDuoFrameless, Converter={StaticResource inverseBool}}"/>
|
||||||
|
<StackLayout
|
||||||
|
StyleClass="box"
|
||||||
|
VerticalOptions="End">
|
||||||
<StackLayout StyleClass="box-row, box-row-switch">
|
<StackLayout StyleClass="box-row, box-row-switch">
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n RememberMe}"
|
Text="{u:I18n RememberMe}"
|
||||||
|
@ -151,6 +163,12 @@
|
||||||
HorizontalOptions="End" />
|
HorizontalOptions="End" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
|
<Button Text="{u:I18n LaunchDuo}"
|
||||||
|
Margin="10,21"
|
||||||
|
StyleClass="btn-primary"
|
||||||
|
Command="{Binding AuthenticateWithDuoFramelessCommand}"
|
||||||
|
AutomationId="DuoFramelessButton"
|
||||||
|
IsVisible="{Binding IsDuoFrameless}"/>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<StackLayout
|
<StackLayout
|
||||||
Spacing="0"
|
Spacing="0"
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
@ -34,6 +35,7 @@ namespace Bit.App.Pages
|
||||||
private string _webVaultUrl = "https://vault.bitwarden.com";
|
private string _webVaultUrl = "https://vault.bitwarden.com";
|
||||||
private bool _enableContinue = false;
|
private bool _enableContinue = false;
|
||||||
private bool _showContinue = true;
|
private bool _showContinue = true;
|
||||||
|
private bool _isDuoFrameless = false;
|
||||||
private double _duoWebViewHeight;
|
private double _duoWebViewHeight;
|
||||||
|
|
||||||
public TwoFactorPageViewModel()
|
public TwoFactorPageViewModel()
|
||||||
|
@ -56,6 +58,7 @@ namespace Bit.App.Pages
|
||||||
PageTitle = AppResources.TwoStepLogin;
|
PageTitle = AppResources.TwoStepLogin;
|
||||||
SubmitCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(async () => await SubmitAsync()), allowsMultipleExecutions: false);
|
SubmitCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(async () => await SubmitAsync()), allowsMultipleExecutions: false);
|
||||||
MoreCommand = CreateDefaultAsyncRelayCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
|
MoreCommand = CreateDefaultAsyncRelayCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||||
|
AuthenticateWithDuoFramelessCommand = CreateDefaultAsyncRelayCommand(DuoFramelessAuthenticateAsync, allowsMultipleExecutions: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string TotpInstruction
|
public string TotpInstruction
|
||||||
|
@ -103,6 +106,16 @@ namespace Bit.App.Pages
|
||||||
set => SetProperty(ref _enableContinue, value);
|
set => SetProperty(ref _enableContinue, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsDuoFrameless
|
||||||
|
{
|
||||||
|
get => _isDuoFrameless;
|
||||||
|
set => SetProperty(ref _isDuoFrameless, value, additionalPropertyNames: new string[] { nameof(DuoFramelessLabel) });
|
||||||
|
}
|
||||||
|
|
||||||
|
public string DuoFramelessLabel => SelectedProviderType == TwoFactorProviderType.OrganizationDuo ?
|
||||||
|
$"{AppResources.DuoTwoStepLoginIsRequiredForYourAccount} {AppResources.FollowTheStepsFromDuoToFinishLoggingIn}" :
|
||||||
|
AppResources.FollowTheStepsFromDuoToFinishLoggingIn;
|
||||||
|
|
||||||
#if IOS
|
#if IOS
|
||||||
public string YubikeyInstruction => AppResources.YubiKeyInstructionIos;
|
public string YubikeyInstruction => AppResources.YubiKeyInstructionIos;
|
||||||
#else
|
#else
|
||||||
|
@ -125,6 +138,7 @@ namespace Bit.App.Pages
|
||||||
}
|
}
|
||||||
public ICommand SubmitCommand { get; }
|
public ICommand SubmitCommand { get; }
|
||||||
public ICommand MoreCommand { get; }
|
public ICommand MoreCommand { get; }
|
||||||
|
public ICommand AuthenticateWithDuoFramelessCommand { get; }
|
||||||
public Action TwoFactorAuthSuccessAction { get; set; }
|
public Action TwoFactorAuthSuccessAction { get; set; }
|
||||||
public Action LockAction { get; set; }
|
public Action LockAction { get; set; }
|
||||||
public Action StartDeviceApprovalOptionsAction { get; set; }
|
public Action StartDeviceApprovalOptionsAction { get; set; }
|
||||||
|
@ -179,15 +193,29 @@ namespace Bit.App.Pages
|
||||||
break;
|
break;
|
||||||
case TwoFactorProviderType.Duo:
|
case TwoFactorProviderType.Duo:
|
||||||
case TwoFactorProviderType.OrganizationDuo:
|
case TwoFactorProviderType.OrganizationDuo:
|
||||||
SetDuoWebViewHeight();
|
IsDuoFrameless = providerData.ContainsKey("AuthUrl");
|
||||||
var host = WebUtility.UrlEncode(providerData["Host"] as string);
|
if (!IsDuoFrameless)
|
||||||
var req = WebUtility.UrlEncode(providerData["Signature"] as string);
|
|
||||||
page.DuoWebView.Uri = $"{_webVaultUrl}/duo-connector.html?host={host}&request={req}";
|
|
||||||
page.DuoWebView.RegisterAction(sig =>
|
|
||||||
{
|
{
|
||||||
Token = sig;
|
SetDuoWebViewHeight();
|
||||||
SubmitCommand.Execute(null);
|
var host = WebUtility.UrlEncode(providerData["Host"] as string);
|
||||||
});
|
var req = WebUtility.UrlEncode(providerData["Signature"] as string);
|
||||||
|
page.DuoWebView.Uri = $"{_webVaultUrl}/duo-connector.html?host={host}&request={req}";
|
||||||
|
page.DuoWebView.RegisterAction(sig =>
|
||||||
|
{
|
||||||
|
Token = sig;
|
||||||
|
MainThread.BeginInvokeOnMainThread(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await SubmitAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
HandleException(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case TwoFactorProviderType.Email:
|
case TwoFactorProviderType.Email:
|
||||||
TotpInstruction = string.Format(AppResources.EnterVerificationCodeEmail,
|
TotpInstruction = string.Format(AppResources.EnterVerificationCodeEmail,
|
||||||
|
@ -211,6 +239,77 @@ namespace Bit.App.Pages
|
||||||
ShowContinue = !(SelectedProviderType == null || DuoMethod || Fido2Method);
|
ShowContinue = !(SelectedProviderType == null || DuoMethod || Fido2Method);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task DuoFramelessAuthenticateAsync()
|
||||||
|
{
|
||||||
|
await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
|
||||||
|
|
||||||
|
if (!_authService.TwoFactorProvidersData.TryGetValue(SelectedProviderType.Value, out var providerData) ||
|
||||||
|
!providerData.TryGetValue("AuthUrl", out var urlObject))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Duo authentication error: Could not get ProviderData or AuthUrl");
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = urlObject as string;
|
||||||
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("Duo authentication error: Could not get valid auth url");
|
||||||
|
}
|
||||||
|
|
||||||
|
WebAuthenticatorResult authResult;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
authResult = await WebAuthenticator.AuthenticateAsync(new WebAuthenticatorOptions
|
||||||
|
{
|
||||||
|
Url = new Uri(url),
|
||||||
|
CallbackUrl = new Uri(Constants.DuoCallback)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
// user canceled
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
if (authResult == null || authResult.Properties == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Duo authentication error: Could not get result from authentication");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authResult.Properties.TryGetValue("error", out var resultError))
|
||||||
|
{
|
||||||
|
_logger.Error(resultError);
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string code = null;
|
||||||
|
if (authResult.Properties.TryGetValue("code", out var resultCodeData))
|
||||||
|
{
|
||||||
|
code = Uri.UnescapeDataString(resultCodeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(code))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Duo authentication error: response code is null or empty/whitespace");
|
||||||
|
}
|
||||||
|
|
||||||
|
string state = null;
|
||||||
|
if (authResult.Properties.TryGetValue("state", out var resultStateData))
|
||||||
|
{
|
||||||
|
state = Uri.UnescapeDataString(resultStateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(state))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Duo authentication error: response state is null or empty/whitespace");
|
||||||
|
}
|
||||||
|
|
||||||
|
Token = $"{code}|{state}";
|
||||||
|
await SubmitAsync(true);
|
||||||
|
}
|
||||||
|
|
||||||
public void SetDuoWebViewHeight()
|
public void SetDuoWebViewHeight()
|
||||||
{
|
{
|
||||||
var screenHeight = DeviceDisplay.MainDisplayInfo.Height / DeviceDisplay.MainDisplayInfo.Density;
|
var screenHeight = DeviceDisplay.MainDisplayInfo.Height / DeviceDisplay.MainDisplayInfo.Density;
|
||||||
|
|
|
@ -2335,6 +2335,15 @@ namespace Bit.Core.Resources.Localization {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Duo two-step login is required for your account. .
|
||||||
|
/// </summary>
|
||||||
|
public static string DuoTwoStepLoginIsRequiredForYourAccount {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DuoTwoStepLoginIsRequiredForYourAccount", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Edit.
|
/// Looks up a localized string similar to Edit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -3199,6 +3208,15 @@ namespace Bit.Core.Resources.Localization {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Follow the steps from Duo to finish logging in..
|
||||||
|
/// </summary>
|
||||||
|
public static string FollowTheStepsFromDuoToFinishLoggingIn {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("FollowTheStepsFromDuoToFinishLoggingIn", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to {0} is not correctly formatted..
|
/// Looks up a localized string similar to {0} is not correctly formatted..
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -3793,6 +3811,15 @@ namespace Bit.Core.Resources.Localization {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Launch Duo.
|
||||||
|
/// </summary>
|
||||||
|
public static string LaunchDuo {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("LaunchDuo", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Bitwarden allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website..
|
/// Looks up a localized string similar to Bitwarden allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website..
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -2877,4 +2877,13 @@ Do you want to switch to this account?</value>
|
||||||
<data name="SetUpAnUnlockOptionToChangeYourVaultTimeoutAction" xml:space="preserve">
|
<data name="SetUpAnUnlockOptionToChangeYourVaultTimeoutAction" xml:space="preserve">
|
||||||
<value>Set up an unlock option to change your vault timeout action.</value>
|
<value>Set up an unlock option to change your vault timeout action.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DuoTwoStepLoginIsRequiredForYourAccount" xml:space="preserve">
|
||||||
|
<value>Duo two-step login is required for your account. </value>
|
||||||
|
</data>
|
||||||
|
<data name="FollowTheStepsFromDuoToFinishLoggingIn" xml:space="preserve">
|
||||||
|
<value>Follow the steps from Duo to finish logging in.</value>
|
||||||
|
</data>
|
||||||
|
<data name="LaunchDuo" xml:space="preserve">
|
||||||
|
<value>Launch Duo</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
Loading…
Reference in a new issue