Skip to content

Commit

Permalink
FIDO2 WebAuthn support for mobile (#1519)
Browse files Browse the repository at this point in the history
* FIDO2 / WebAuthn support for mobile

* fixes
  • Loading branch information
mpbw2 committed Aug 30, 2021
1 parent d050215 commit 307a5a5
Show file tree
Hide file tree
Showing 24 changed files with 272 additions and 151 deletions.
5 changes: 5 additions & 0 deletions src/Android/Services/DeviceActionService.cs
Expand Up @@ -776,6 +776,11 @@ public void CloseMainApp()
_messagingService.Send("finishMainActivity");
}

public bool SupportsFido2()
{
return true;
}

private bool DeleteDir(Java.IO.File dir)
{
if (dir != null && dir.IsDirectory)
Expand Down
1 change: 1 addition & 0 deletions src/App/Abstractions/IDeviceActionService.cs
Expand Up @@ -45,5 +45,6 @@ public interface IDeviceActionService
bool UsingDarkTheme();
long GetActiveTime();
void CloseMainApp();
bool SupportsFido2();
}
}
2 changes: 1 addition & 1 deletion src/App/Pages/Accounts/LoginPage.xaml
Expand Up @@ -82,7 +82,7 @@
</Grid>
</StackLayout>
<StackLayout Padding="10, 0">
<Button Text="{u:I18n LogIn}" Clicked="LogIn_Clicked" IsEnabled="{Binding LoginEnabled}"/>
<Button Text="{u:I18n LogIn}" Clicked="LogIn_Clicked" />
</StackLayout>
</StackLayout>
</ScrollView>
Expand Down
11 changes: 5 additions & 6 deletions src/App/Pages/Accounts/LoginPage.xaml.cs
Expand Up @@ -16,6 +16,8 @@ public partial class LoginPage : BaseContentPage
private readonly LoginPageViewModel _vm;
private readonly AppOptions _appOptions;

private bool _inputFocused;

public LoginPage(string email = null, AppOptions appOptions = null)
{
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
Expand Down Expand Up @@ -58,13 +60,10 @@ protected override async void OnAppearing()
{
base.OnAppearing();
await _vm.InitAsync();
if (string.IsNullOrWhiteSpace(_vm.Email))
{
RequestFocus(_email);
}
else
if (!_inputFocused)
{
RequestFocus(_masterPassword);
RequestFocus(string.IsNullOrWhiteSpace(_vm.Email) ? _email : _masterPassword);
_inputFocused = true;
}
}

Expand Down
29 changes: 6 additions & 23 deletions src/App/Pages/Accounts/LoginPageViewModel.cs
Expand Up @@ -33,7 +33,6 @@ public class LoginPageViewModel : CaptchaProtectedViewModel
private bool _showPassword;
private string _email;
private string _masterPassword;
private bool _loginEnabled = true;

public LoginPageViewModel()
{
Expand Down Expand Up @@ -73,16 +72,6 @@ public string MasterPassword
set => SetProperty(ref _masterPassword, value);
}

public bool LoginEnabled {
get => _loginEnabled;
set => SetProperty(ref _loginEnabled, value);
}
public bool Loading
{
get => !LoginEnabled;
set => LoginEnabled = !value;
}

public Command LogInCommand { get; }
public Command TogglePasswordCommand { get; }
public string ShowPasswordIcon => ShowPassword ? "" : "";
Expand All @@ -106,7 +95,7 @@ public async Task InitAsync()
RememberEmail = rememberEmail.GetValueOrDefault(true);
}

public async Task LogInAsync()
public async Task LogInAsync(bool showLoading = true)
{
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
{
Expand Down Expand Up @@ -140,10 +129,9 @@ public async Task LogInAsync()
ShowPassword = false;
try
{
if (!Loading)
if (showLoading)
{
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
Loading = true;
}

var response = await _authService.LogInAsync(Email, MasterPassword, _captchaToken);
Expand All @@ -156,25 +144,21 @@ public async Task LogInAsync()
await _storageService.RemoveAsync(Keys_RememberedEmail);
}
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
await _deviceActionService.HideLoadingAsync();

if (response.CaptchaNeeded)
{
if (await HandleCaptchaAsync(response.CaptchaSiteKey))
{
await LogInAsync();
await LogInAsync(false);
_captchaToken = null;
return;
}
else
{
Loading = false;
return;
}
return;
}
MasterPassword = string.Empty;
_captchaToken = null;

await _deviceActionService.HideLoadingAsync();

if (response.TwoFactor)
{
StartTwoFactorAction?.Invoke();
Expand All @@ -198,7 +182,6 @@ public async Task LogInAsync()
AppResources.AnErrorHasOccurred);
}
}
Loading = false;
}

public void TogglePassword()
Expand Down
37 changes: 11 additions & 26 deletions src/App/Pages/Accounts/LoginSsoPageViewModel.cs
Expand Up @@ -123,43 +123,28 @@ public async Task LogInAsync()
"domain_hint=" + Uri.EscapeDataString(OrgIdentifier);

WebAuthenticatorResult authResult = null;
bool cancelled = false;
try
{
authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
new Uri(redirectUri));
}
catch (TaskCanceledException taskCanceledException)
catch (TaskCanceledException)
{
// user canceled
await _deviceActionService.HideLoadingAsync();
cancelled = true;
return;
}
catch (Exception e)

var code = GetResultCode(authResult, state);
if (!string.IsNullOrEmpty(code))
{
// WebAuthenticator throws NSErrorException if iOS flow is cancelled - by setting cancelled to true
// here we maintain the appearance of a clean cancellation (we don't want to do this across the board
// because we still want to present legitimate errors). If/when this is fixed, we can remove this
// particular catch block (catching taskCanceledException above must remain)
// https://github.com/xamarin/Essentials/issues/1240
if (Device.RuntimePlatform == Device.iOS)
{
await _deviceActionService.HideLoadingAsync();
cancelled = true;
}
await LogIn(code, codeVerifier, redirectUri);
}
if (!cancelled)
else
{
var code = GetResultCode(authResult, state);
if (!string.IsNullOrEmpty(code))
{
await LogIn(code, codeVerifier, redirectUri);
}
else
{
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
AppResources.AnErrorHasOccurred);
}
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
AppResources.AnErrorHasOccurred);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/App/Pages/Accounts/RegisterPage.xaml
Expand Up @@ -21,7 +21,7 @@

<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" IsEnabled="{Binding SubmitEnabled}"/>
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" />
</ContentPage.ToolbarItems>

<ScrollView>
Expand Down
8 changes: 7 additions & 1 deletion src/App/Pages/Accounts/RegisterPage.xaml.cs
Expand Up @@ -11,6 +11,8 @@ public partial class RegisterPage : BaseContentPage
private readonly IMessagingService _messagingService;
private readonly RegisterPageViewModel _vm;

private bool _inputFocused;

public RegisterPage(HomePage homePage)
{
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
Expand Down Expand Up @@ -45,7 +47,11 @@ public RegisterPage(HomePage homePage)
protected override void OnAppearing()
{
base.OnAppearing();
RequestFocus(_email);
if (!_inputFocused)
{
RequestFocus(_email);
_inputFocused = true;
}
}

private async void Submit_Clicked(object sender, EventArgs e)
Expand Down
35 changes: 8 additions & 27 deletions src/App/Pages/Accounts/RegisterPageViewModel.cs
Expand Up @@ -22,7 +22,6 @@ public class RegisterPageViewModel : CaptchaProtectedViewModel
private readonly IPlatformUtilsService _platformUtilsService;
private bool _showPassword;
private bool _acceptPolicies;
private bool _submitEnabled = true;

public RegisterPageViewModel()
{
Expand Down Expand Up @@ -60,16 +59,6 @@ public bool AcceptPolicies
get => _acceptPolicies;
set => SetProperty(ref _acceptPolicies, value);
}
public bool SubmitEnabled
{
get => _submitEnabled;
set => SetProperty(ref _submitEnabled, value);
}
public bool Loading
{
get => !SubmitEnabled;
set => SubmitEnabled = !value;
}

public Thickness SwitchMargin
{
Expand All @@ -96,7 +85,7 @@ public Thickness SwitchMargin
protected override IDeviceActionService deviceActionService => _deviceActionService;
protected override IPlatformUtilsService platformUtilsService => _platformUtilsService;

public async Task SubmitAsync()
public async Task SubmitAsync(bool showLoading = true)
{
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
{
Expand Down Expand Up @@ -143,6 +132,11 @@ public async Task SubmitAsync()
}

// TODO: Password strength check?

if (showLoading)
{
await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
}

Name = string.IsNullOrWhiteSpace(Name) ? null : Name;
Email = Email.Trim().ToLower();
Expand Down Expand Up @@ -172,14 +166,8 @@ public async Task SubmitAsync()

try
{
if (!Loading)
{
await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
Loading = true;
}
await _apiService.PostRegisterAsync(request);
await _deviceActionService.HideLoadingAsync();
Loading = false;
_platformUtilsService.ShowToast("success", null, AppResources.AccountCreated,
new System.Collections.Generic.Dictionary<string, object>
{
Expand All @@ -193,19 +181,12 @@ public async Task SubmitAsync()
{
if (await HandleCaptchaAsync(e.Error.CaptchaSiteKey))
{
await SubmitAsync();
await SubmitAsync(false);
_captchaToken = null;
return;
}
else
{
await _deviceActionService.HideLoadingAsync();
Loading = false;
return;
};
return;
}
await _deviceActionService.HideLoadingAsync();
Loading = false;
if (e?.Error != null)
{
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
Expand Down
24 changes: 24 additions & 0 deletions src/App/Pages/Accounts/TwoFactorPage.xaml
Expand Up @@ -102,6 +102,30 @@
</StackLayout>
</StackLayout>
</StackLayout>
<StackLayout Spacing="20" Padding="0" IsVisible="{Binding Fido2Method, Mode=OneWay}">
<Label
Text="{u:I18n Fido2Instruction}"
Margin="10, 20, 10, 0"
HorizontalTextAlignment="Center" />
<Image
Source="yubikey.png"
Margin="10, 0"
WidthRequest="266"
HeightRequest="160"
HorizontalOptions="Center" />
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="{u:I18n RememberMe}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding Remember}"
StyleClass="box-value"
HorizontalOptions="End" />
</StackLayout>
</StackLayout>
</StackLayout>
<StackLayout Spacing="0" Padding="0" IsVisible="{Binding DuoMethod, Mode=OneWay}"
VerticalOptions="FillAndExpand">
<controls:HybridWebView
Expand Down
8 changes: 6 additions & 2 deletions src/App/Pages/Accounts/TwoFactorPage.xaml.cs
Expand Up @@ -168,11 +168,15 @@ private void Close_Clicked(object sender, System.EventArgs e)
}
}

private void TryAgain_Clicked(object sender, EventArgs e)
private async void TryAgain_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
if (_vm.YubikeyMethod)
if (_vm.Fido2Method)
{
await _vm.Fido2AuthenticateAsync();
}
else if (_vm.YubikeyMethod)
{
_messagingService.Send("listenYubiKeyOTP", true);
}
Expand Down

0 comments on commit 307a5a5

Please sign in to comment.