[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:
André Bispo 2024-02-20 18:46:47 +00:00 committed by GitHub
parent f1c20e03bc
commit 4c88524f0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 166 additions and 12 deletions

View file

@ -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

View file

@ -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"

View file

@ -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;

View file

@ -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>

View file

@ -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>