mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +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 DisplayEuEnvironmentFlag = "display-eu-environment";
|
||||
public const string RegionEnvironment = "regionEnvironment";
|
||||
public const string DuoCallback = "bitwarden://duo-callback";
|
||||
|
||||
/// <summary>
|
||||
/// 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 Spacing="0" Padding="0" IsVisible="{Binding DuoMethod, Mode=OneWay}"
|
||||
VerticalOptions="StartAndExpand">
|
||||
<StackLayout
|
||||
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
|
||||
x:Name="_duoWebView"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="FillAndExpand"
|
||||
HeightRequest="{Binding DuoWebViewHeight, Mode=OneWay}" />
|
||||
<StackLayout StyleClass="box" VerticalOptions="End">
|
||||
HeightRequest="{Binding DuoWebViewHeight, Mode=OneWay}"
|
||||
IsVisible="{Binding IsDuoFrameless, Converter={StaticResource inverseBool}}"/>
|
||||
<StackLayout
|
||||
StyleClass="box"
|
||||
VerticalOptions="End">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n RememberMe}"
|
||||
|
@ -151,6 +163,12 @@
|
|||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<Button Text="{u:I18n LaunchDuo}"
|
||||
Margin="10,21"
|
||||
StyleClass="btn-primary"
|
||||
Command="{Binding AuthenticateWithDuoFramelessCommand}"
|
||||
AutomationId="DuoFramelessButton"
|
||||
IsVisible="{Binding IsDuoFrameless}"/>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
Spacing="0"
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
|
@ -34,6 +35,7 @@ namespace Bit.App.Pages
|
|||
private string _webVaultUrl = "https://vault.bitwarden.com";
|
||||
private bool _enableContinue = false;
|
||||
private bool _showContinue = true;
|
||||
private bool _isDuoFrameless = false;
|
||||
private double _duoWebViewHeight;
|
||||
|
||||
public TwoFactorPageViewModel()
|
||||
|
@ -56,6 +58,7 @@ namespace Bit.App.Pages
|
|||
PageTitle = AppResources.TwoStepLogin;
|
||||
SubmitCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(async () => await SubmitAsync()), allowsMultipleExecutions: false);
|
||||
MoreCommand = CreateDefaultAsyncRelayCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||
AuthenticateWithDuoFramelessCommand = CreateDefaultAsyncRelayCommand(DuoFramelessAuthenticateAsync, allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public string TotpInstruction
|
||||
|
@ -103,6 +106,16 @@ namespace Bit.App.Pages
|
|||
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
|
||||
public string YubikeyInstruction => AppResources.YubiKeyInstructionIos;
|
||||
#else
|
||||
|
@ -125,6 +138,7 @@ namespace Bit.App.Pages
|
|||
}
|
||||
public ICommand SubmitCommand { get; }
|
||||
public ICommand MoreCommand { get; }
|
||||
public ICommand AuthenticateWithDuoFramelessCommand { get; }
|
||||
public Action TwoFactorAuthSuccessAction { get; set; }
|
||||
public Action LockAction { get; set; }
|
||||
public Action StartDeviceApprovalOptionsAction { get; set; }
|
||||
|
@ -179,15 +193,29 @@ namespace Bit.App.Pages
|
|||
break;
|
||||
case TwoFactorProviderType.Duo:
|
||||
case TwoFactorProviderType.OrganizationDuo:
|
||||
SetDuoWebViewHeight();
|
||||
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 =>
|
||||
IsDuoFrameless = providerData.ContainsKey("AuthUrl");
|
||||
if (!IsDuoFrameless)
|
||||
{
|
||||
Token = sig;
|
||||
SubmitCommand.Execute(null);
|
||||
});
|
||||
SetDuoWebViewHeight();
|
||||
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;
|
||||
case TwoFactorProviderType.Email:
|
||||
TotpInstruction = string.Format(AppResources.EnterVerificationCodeEmail,
|
||||
|
@ -211,6 +239,77 @@ namespace Bit.App.Pages
|
|||
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()
|
||||
{
|
||||
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>
|
||||
/// Looks up a localized string similar to Edit.
|
||||
/// </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>
|
||||
/// Looks up a localized string similar to {0} is not correctly formatted..
|
||||
/// </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>
|
||||
/// 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>
|
||||
|
|
|
@ -2877,4 +2877,13 @@ Do you want to switch to this account?</value>
|
|||
<data name="SetUpAnUnlockOptionToChangeYourVaultTimeoutAction" xml:space="preserve">
|
||||
<value>Set up an unlock option to change your vault timeout action.</value>
|
||||
</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>
|
||||
|
|
Loading…
Reference in a new issue