1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-05 23:53:33 +00:00

FIDO2 implementation using Google Play Services on Android

This commit is contained in:
Matt Portune
2021-08-30 13:38:03 -04:00
parent 307a5a5843
commit c1461ab16b
19 changed files with 713 additions and 4 deletions

View File

@@ -92,6 +92,7 @@
</PackageReference>
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.3.0.1" />
<PackageReference Include="Xamarin.Google.Dagger" Version="2.37.0" />
<PackageReference Include="Xamarin.GooglePlayServices.Fido" Version="118.1.0" />
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet">
<Version>117.0.0</Version>
</PackageReference>
@@ -115,6 +116,8 @@
<Compile Include="Effects\FixedSizeEffect.cs" />
<Compile Include="Effects\SelectableLabelEffect.cs" />
<Compile Include="Effects\TabBarEffect.cs" />
<Compile Include="Fido2System\Fido2BuilderObject.cs" />
<Compile Include="Fido2System\Fido2Service.cs" />
<Compile Include="Push\FirebaseMessagingService.cs" />
<Compile Include="Receivers\ClearClipboardAlarmReceiver.cs" />
<Compile Include="Receivers\RestrictionsChangedReceiver.cs" />

View File

@@ -0,0 +1,106 @@
#if !FDROID
using System.Collections.Generic;
using Android.Gms.Fido.Common;
using Android.Gms.Fido.Fido2.Api.Common;
using Bit.Core.Models.Data;
using Bit.Core.Models.Response;
using Bit.Core.Utilities;
using Java.Lang;
using Newtonsoft.Json.Linq;
namespace Bit.Droid.Fido2System
{
class Fido2BuilderObject
{
public static PublicKeyCredentialRequestOptions ParsePublicKeyCredentialRequestOptions(
Fido2AuthenticationChallengeResponse data)
{
if (data == null)
{
return null;
}
var builder = new PublicKeyCredentialRequestOptions.Builder();
if (!string.IsNullOrEmpty(data.Challenge))
{
builder.SetChallenge(CoreHelpers.Base64UrlDecode(data.Challenge));
}
if (data.AllowCredentials != null && data.AllowCredentials.Count > 0)
{
builder.SetAllowList(ParseCredentialDescriptors(data.AllowCredentials));
}
if (!string.IsNullOrEmpty(data.RpId))
{
builder.SetRpId(data.RpId);
}
if (data.Timeout > 0)
{
builder.SetTimeoutSeconds((Double)(data.Timeout / 1000));
}
if (data.Extensions != null)
{
builder.SetAuthenticationExtensions(ParseExtensions((JObject)data.Extensions));
}
return builder.Build();
}
private static List<PublicKeyCredentialDescriptor> ParseCredentialDescriptors(
List<Fido2CredentialDescriptor> listData)
{
if (listData == null || listData.Count == 0)
{
return new List<PublicKeyCredentialDescriptor>();
}
var credentials = new List<PublicKeyCredentialDescriptor>();
foreach (var data in listData)
{
string id = null;
string type = null;
var transports = new List<Transport>();
if (!string.IsNullOrEmpty(data.Id))
{
id = data.Id;
}
if (!string.IsNullOrEmpty(data.Type))
{
type = data.Type;
}
if (data.Transports != null && data.Transports.Count > 0)
{
foreach (var transport in data.Transports)
{
transports.Add(Transport.FromString(transport));
}
}
credentials.Add(new PublicKeyCredentialDescriptor(type, CoreHelpers.Base64UrlDecode(id), transports));
}
return credentials;
}
private static AuthenticationExtensions ParseExtensions(JObject extensions)
{
var builder = new AuthenticationExtensions.Builder();
if (extensions.ContainsKey("appid"))
{
var appId = new FidoAppIdExtension((string)extensions.GetValue("appid"));
builder.SetFido2Extension(appId);
}
if (extensions.ContainsKey("uvm"))
{
var uvm = new UserVerificationMethodExtension((bool)extensions.GetValue("uvm"));
builder.SetUserVerificationMethodExtension(uvm);
}
return builder.Build();
}
}
}
#endif

View File

@@ -0,0 +1,249 @@
#if !FDROID
using Android.App;
using Android.Content;
using Android.Gms.Fido;
using Android.Gms.Fido.Fido2;
using Android.Gms.Fido.Fido2.Api.Common;
using Android.Gms.Tasks;
using Android.Util;
using AndroidX.AppCompat.App;
using Bit.App.Services;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Request;
using Bit.Core.Models.Response;
using Bit.Core.Utilities;
using Java.Lang;
using Newtonsoft.Json;
using Xamarin.Forms;
using Enum = System.Enum;
namespace Bit.Droid.Fido2System
{
public class Fido2Service
{
public static readonly string _tag_log = "Fido2Service";
public static Fido2Service INSTANCE = new Fido2Service();
private readonly MobileI18nService _i18nService;
private readonly IPlatformUtilsService _platformUtilsService;
private AppCompatActivity _activity;
private Fido2ApiClient _fido2ApiClient;
private Fido2CodesTypes _fido2CodesType;
public Fido2Service()
{
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService") as MobileI18nService;
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
}
public void Start(AppCompatActivity activity)
{
_activity = activity;
_fido2ApiClient = Fido.GetFido2ApiClient(_activity);
}
public void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
if (resultCode == Result.Ok && Enum.IsDefined(typeof(Fido2CodesTypes), requestCode))
{
switch ((Fido2CodesTypes)requestCode)
{
case Fido2CodesTypes.RequestSignInUser:
var errorExtra = data?.GetByteArrayExtra(Fido.Fido2KeyErrorExtra);
if (errorExtra != null)
{
HandleErrorCode(errorExtra);
}
else
{
if (data != null)
{
SignInUserResponse(data);
}
}
break;
// TODO: Key registration, should we ever choose to implement client-side
/*case Fido2CodesTypes.RequestRegisterNewKey:
errorExtra = data?.GetByteArrayExtra(Fido.Fido2KeyErrorExtra);
if (errorExtra != null)
{
HandleErrorCode(errorExtra);
}
else
{
if (data != null)
{
// begin registration flow
}
}
break;*/
}
}
else if (resultCode == Result.Canceled && Enum.IsDefined(typeof(Fido2CodesTypes), requestCode))
{
Log.Info(_tag_log, "cancelled");
_platformUtilsService.ShowDialogAsync(_i18nService.T("Fido2AbortError"),
_i18nService.T("Fido2Title"));
}
}
public void OnSuccess(Object result)
{
if (result != null && Enum.IsDefined(typeof(Fido2CodesTypes), _fido2CodesType))
{
try
{
_activity.StartIntentSenderForResult(((PendingIntent)result).IntentSender, (int)_fido2CodesType,
null, 0, 0, 0);
}
catch (System.Exception e)
{
Log.Error(_tag_log, e.Message);
_platformUtilsService.ShowDialogAsync(_i18nService.T("Fido2SomethingWentWrong"),
_i18nService.T("Fido2Title"));
}
}
}
public void OnFailure(Exception e)
{
Log.Error(_tag_log, e.Message ?? "OnFailure: No error message returned");
_platformUtilsService.ShowDialogAsync(_i18nService.T("Fido2SomethingWentWrong"),
_i18nService.T("Fido2Title"));
}
public void OnComplete(Task task)
{
Log.Debug(_tag_log, "OnComplete");
}
public async System.Threading.Tasks.Task SignInUserRequestAsync(string dataJson)
{
try
{
var dataObject = JsonConvert.DeserializeObject<Fido2AuthenticationChallengeResponse>(dataJson);
_fido2CodesType = Fido2CodesTypes.RequestSignInUser;
var options = Fido2BuilderObject.ParsePublicKeyCredentialRequestOptions(dataObject);
var task = _fido2ApiClient.GetSignPendingIntent(options);
task.AddOnSuccessListener((IOnSuccessListener)_activity)
.AddOnFailureListener((IOnFailureListener)_activity)
.AddOnCompleteListener((IOnCompleteListener)_activity);
}
catch (System.Exception e)
{
Log.Error(_tag_log, e.StackTrace);
await _platformUtilsService.ShowDialogAsync(_i18nService.T("Fido2SomethingWentWrong"),
_i18nService.T("Fido2Title"));
}
finally
{
Log.Info(_tag_log, "SignInUserRequest() -> finally()");
}
}
private void SignInUserResponse(Intent data)
{
try
{
var response =
AuthenticatorAssertionResponse.DeserializeFromBytes(
data.GetByteArrayExtra(Fido.Fido2KeyResponseExtra));
var responseJson = JsonConvert.SerializeObject(new Fido2AuthenticationChallengeRequest
{
Id = CoreHelpers.Base64UrlEncode(response.GetKeyHandle()),
RawId = CoreHelpers.Base64UrlEncode(response.GetKeyHandle()),
Type = "public-key",
Response = new Fido2AssertionResponse
{
AuthenticatorData = CoreHelpers.Base64UrlEncode(response.GetAuthenticatorData()),
ClientDataJson = CoreHelpers.Base64UrlEncode(response.GetClientDataJSON()),
Signature = CoreHelpers.Base64UrlEncode(response.GetSignature()),
UserHandle = (response.GetUserHandle() != null
? CoreHelpers.Base64UrlEncode(response.GetUserHandle()) : null),
},
Extensions = null
}
);
Device.BeginInvokeOnMainThread(() => ((MainActivity)_activity).Fido2Submission(responseJson));
}
catch (System.Exception e)
{
Log.Error(_tag_log, e.Message);
_platformUtilsService.ShowDialogAsync(_i18nService.T("Fido2SomethingWentWrong"),
_i18nService.T("Fido2Title"));
}
finally
{
Log.Info(_tag_log, "SignInUserResponse() -> finally()");
}
}
public void HandleErrorCode(byte[] errorExtra)
{
var error = AuthenticatorErrorResponse.DeserializeFromBytes(errorExtra);
if (error.ErrorMessage.Length > 0)
{
Log.Info(_tag_log, error.ErrorMessage);
}
string message = "";
if (error.ErrorCode == ErrorCode.AbortErr)
{
message = "Fido2AbortError";
}
else if (error.ErrorCode == ErrorCode.TimeoutErr)
{
message = "Fido2TimeoutError";
}
else if (error.ErrorCode == ErrorCode.AttestationNotPrivateErr)
{
message = "Fido2PrivacyError";
}
else if (error.ErrorCode == ErrorCode.ConstraintErr)
{
message = "Fido2SomethingWentWrong";
}
else if (error.ErrorCode == ErrorCode.DataErr)
{
message = "Fido2ServerDataFail";
}
else if (error.ErrorCode == ErrorCode.EncodingErr)
{
message = "Fido2SomethingWentWrong";
}
else if (error.ErrorCode == ErrorCode.InvalidStateErr)
{
message = "Fido2SomethingWentWrong";
}
else if (error.ErrorCode == ErrorCode.NetworkErr)
{
message = "Fido2NetworkFail";
}
else if (error.ErrorCode == ErrorCode.NotAllowedErr)
{
message = "Fido2NoPermission";
}
else if (error.ErrorCode == ErrorCode.NotSupportedErr)
{
message = "Fido2NotSupportedError";
}
else if (error.ErrorCode == ErrorCode.SecurityErr)
{
message = "Fido2SecurityError";
}
else if (error.ErrorCode == ErrorCode.UnknownErr)
{
message = "Fido2SomethingWentWrong";
}
else
{
message = "Fido2SomethingWentWrong";
}
_platformUtilsService.ShowDialogAsync(_i18nService.T(message), _i18nService.T("Fido2Title"));
}
}
}
#endif

View File

@@ -9,6 +9,7 @@ using Bit.Core.Utilities;
using Bit.Core.Abstractions;
using System.IO;
using System;
using System.Collections.Generic;
using Android.Content;
using Bit.Droid.Utilities;
using Bit.Droid.Receivers;
@@ -17,7 +18,11 @@ using Bit.Core.Enums;
using Android.Nfc;
using Bit.App.Utilities;
using System.Threading.Tasks;
using Android.Util;
using AndroidX.Core.Content;
#if !FDROID
using Bit.Droid.Fido2System;
#endif
using ZXing.Net.Mobile.Android;
namespace Bit.Droid
@@ -42,7 +47,10 @@ namespace Bit.Droid
@"text/*"
})]
[Register("com.x8bit.bitwarden.MainActivity")]
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity,
Android.Gms.Tasks.IOnSuccessListener,
Android.Gms.Tasks.IOnCompleteListener,
Android.Gms.Tasks.IOnFailureListener
{
private IDeviceActionService _deviceActionService;
private IMessagingService _messagingService;
@@ -57,6 +65,7 @@ namespace Bit.Droid
private string _activityKey = $"{nameof(MainActivity)}_{Java.Lang.JavaSystem.CurrentTimeMillis().ToString()}";
private Java.Util.Regex.Pattern _otpPattern =
Java.Util.Regex.Pattern.Compile("^.*?([cbdefghijklnrtuv]{32,64})$");
private string _fidoDataJson;
protected override void OnCreate(Bundle savedInstanceState)
{
@@ -91,6 +100,7 @@ namespace Bit.Droid
#if !FDROID
var appCenterHelper = new AppCenterHelper(_appIdService, _userService);
var appCenterTask = appCenterHelper.InitAsync();
Fido2Service.INSTANCE.Start(this);
#endif
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
@@ -112,6 +122,14 @@ namespace Bit.Droid
{
Xamarin.Forms.Device.BeginInvokeOnMainThread(() => Finish());
}
else if (message.Command == "listenFido2")
{
ListenFido2((Dictionary<string, object>)message.Data);
}
else if (message.Command == "listenFido2TryAgain")
{
ListenFido2();
}
else if (message.Command == "listenYubiKeyOTP")
{
ListenYubiKey((bool)message.Data);
@@ -260,6 +278,34 @@ namespace Bit.Droid
return;
}
}
else if (resultCode == Result.Ok &&
Enum.IsDefined(typeof(Fido2CodesTypes), requestCode))
{
#if !FDROID
Fido2Service.INSTANCE.OnActivityResult(requestCode, resultCode, data);
#endif
}
}
public void OnSuccess(Java.Lang.Object result)
{
#if !FDROID
Fido2Service.INSTANCE.OnSuccess(result);
#endif
}
public void OnComplete(Android.Gms.Tasks.Task task)
{
#if !FDROID
Fido2Service.INSTANCE.OnComplete(task);
#endif
}
public void OnFailure(Java.Lang.Exception e)
{
#if !FDROID
Fido2Service.INSTANCE.OnFailure(e);
#endif
}
protected override void OnDestroy()
@@ -268,6 +314,41 @@ namespace Bit.Droid
_broadcasterService.Unsubscribe(_activityKey);
}
private void ListenFido2(Dictionary<string, object> data = null)
{
if (!_deviceActionService.SupportsFido2())
{
return;
}
#if !FDROID
RunOnUiThread(async () =>
{
try
{
if (data != null)
{
_fidoDataJson = Newtonsoft.Json.JsonConvert.SerializeObject(data);
await Fido2Service.INSTANCE.SignInUserRequestAsync(_fidoDataJson);
}
else
{
await Fido2Service.INSTANCE.SignInUserRequestAsync(_fidoDataJson);
}
}
catch (Exception e)
{
Log.Error(Fido2Service._tag_log, e.Message);
}
});
#endif
}
public void Fido2Submission(string token)
{
_messagingService.Send("gotFido2Token", token);
}
private void ListenYubiKey(bool listen)
{
if (!_deviceActionService.SupportsNfc())

View File

@@ -778,7 +778,13 @@ namespace Bit.Droid.Services
public bool SupportsFido2()
{
return true;
#if !FDROID
if ((int)Build.VERSION.SdkInt >= 21)
{
return true;
}
#endif
return false;
}
private bool DeleteDir(Java.IO.File dir)

View File

@@ -75,6 +75,18 @@ namespace Bit.App.Pages
});
}
}
else if (message.Command == "gotFido2Token")
{
var token = (string)message.Data;
if (!string.IsNullOrWhiteSpace(token))
{
Device.BeginInvokeOnMainThread(async () =>
{
_vm.Token = token;
await _vm.SubmitAsync();
});
}
}
else if (message.Command == "resumeYubiKey")
{
if (_vm.YubikeyMethod)
@@ -174,7 +186,14 @@ namespace Bit.App.Pages
{
if (_vm.Fido2Method)
{
await _vm.Fido2AuthenticateAsync();
if (Device.RuntimePlatform == Device.Android)
{
_messagingService.Send("listenFido2TryAgain", true);
}
else
{
await _vm.Fido2AuthenticateAsync();
}
}
else if (_vm.YubikeyMethod)
{

View File

@@ -10,6 +10,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Bit.App.Utilities;
using Newtonsoft.Json;
@@ -151,7 +153,14 @@ namespace Bit.App.Pages
switch (SelectedProviderType.Value)
{
case TwoFactorProviderType.Fido2WebAuthn:
Fido2AuthenticateAsync(providerData);
if (Device.RuntimePlatform == Device.Android)
{
_messagingService.Send("listenFido2", providerData);
}
else
{
Fido2AuthenticateAsync(providerData);
}
break;
case TwoFactorProviderType.YubiKey:
_messagingService.Send("listenYubiKeyOTP", true);

View File

@@ -3590,5 +3590,53 @@ namespace Bit.App.Resources {
return ResourceManager.GetString("Fido2SomethingWentWrong", resourceCulture);
}
}
public static string Fido2AbortError {
get {
return ResourceManager.GetString("Fido2AbortError", resourceCulture);
}
}
public static string Fido2NetworkFail {
get {
return ResourceManager.GetString("Fido2NetworkFail", resourceCulture);
}
}
public static string Fido2NoPermission {
get {
return ResourceManager.GetString("Fido2NoPermission", resourceCulture);
}
}
public static string Fido2NotSupportedError {
get {
return ResourceManager.GetString("Fido2NotSupportedError", resourceCulture);
}
}
public static string Fido2PrivacyError {
get {
return ResourceManager.GetString("Fido2PrivacyError", resourceCulture);
}
}
public static string Fido2SecurityError {
get {
return ResourceManager.GetString("Fido2SecurityError", resourceCulture);
}
}
public static string Fido2ServerDataFail {
get {
return ResourceManager.GetString("Fido2ServerDataFail", resourceCulture);
}
}
public static string Fido2TimeoutError {
get {
return ResourceManager.GetString("Fido2TimeoutError", resourceCulture);
}
}
}
}

View File

@@ -2031,4 +2031,28 @@
<data name="Fido2SomethingWentWrong" xml:space="preserve">
<value>Something Went Wrong. Try again.</value>
</data>
<data name="Fido2AbortError" xml:space="preserve">
<value>Aborted FIDO2 operation. Try again.</value>
</data>
<data name="Fido2NetworkFail" xml:space="preserve">
<value>No internet connection. Try again.</value>
</data>
<data name="Fido2NoPermission" xml:space="preserve">
<value>Permission was not given. Try again.</value>
</data>
<data name="Fido2NotSupportedError" xml:space="preserve">
<value>Unsupported device.</value>
</data>
<data name="Fido2PrivacyError" xml:space="preserve">
<value>Privacy issues encountered. Try again.</value>
</data>
<data name="Fido2SecurityError" xml:space="preserve">
<value>Security issues encountered.</value>
</data>
<data name="Fido2ServerDataFail" xml:space="preserve">
<value>The server returned invalid data. Try again.</value>
</data>
<data name="Fido2TimeoutError" xml:space="preserve">
<value>Timeout for FIDO2. Try again</value>
</data>
</root>

View File

@@ -0,0 +1,8 @@
namespace Bit.Core.Enums
{
public enum Fido2CodesTypes
{
RequestSignInUser = 994,
RequestRegisterNewKey = 995,
}
}

View File

@@ -0,0 +1,16 @@
using Newtonsoft.Json;
namespace Bit.Core.Models.Data
{
public class Fido2AssertionResponse : Data
{
[JsonProperty("authenticatorData")]
public string AuthenticatorData { get; set; }
[JsonProperty("signature")]
public string Signature { get; set; }
[JsonProperty("clientDataJson")]
public string ClientDataJson { get; set; }
[JsonProperty("userHandle")]
public string UserHandle { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using Newtonsoft.Json;
namespace Bit.Core.Models.Data
{
public class Fido2AuthenticatorSelection : Data
{
[JsonProperty("authenticatorAttachment")]
public string AuthenticatorAttachment { get; set; }
[JsonProperty("userVerification")]
public string UserVerification { get; set; }
[JsonProperty("requireResidentKey")]
public string RequireResidentKey { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Bit.Core.Models.Data
{
public class Fido2CredentialDescriptor : Data
{
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("transports", NullValueHandling = NullValueHandling.Ignore)]
public List<string> Transports { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
using Newtonsoft.Json;
namespace Bit.Core.Models.Data
{
public class Fido2PubKeyCredParam : Data
{
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("alg")]
public int Alg { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using Newtonsoft.Json;
namespace Bit.Core.Models.Data
{
public class Fido2RP : Data
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("icon")]
public string Icon { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
using Newtonsoft.Json;
namespace Bit.Core.Models.Data
{
public class Fido2User : Data
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("displayName")]
public string DisplayName { get; set; }
[JsonProperty("icon")]
public string Icon { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
using Bit.Core.Models.Data;
using Newtonsoft.Json;
namespace Bit.Core.Models.Request
{
public class Fido2AuthenticationChallengeRequest
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("rawId")]
public string RawId { get; set; }
[JsonProperty("response")]
public Fido2AssertionResponse Response { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("extensions", NullValueHandling = NullValueHandling.Ignore)]
public string Extensions { get; set; }
}
}

View File

@@ -0,0 +1,22 @@
using System.Collections.Generic;
using Bit.Core.Models.Data;
using Newtonsoft.Json;
namespace Bit.Core.Models.Response
{
public class Fido2AuthenticationChallengeResponse
{
[JsonProperty("challenge")]
public string Challenge { get; set; }
[JsonProperty("rpId")]
public string RpId { get; set; }
[JsonProperty("timeout")]
public double Timeout { get; set; }
[JsonProperty("allowCredentials")]
public List<Fido2CredentialDescriptor> AllowCredentials { get; set; }
[JsonProperty("userVerification")]
public string UserVerification { get; set; }
[JsonProperty("extensions", NullValueHandling = NullValueHandling.Ignore)]
public object Extensions { get; set; }
}
}

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
using Bit.Core.Models.Data;
using Newtonsoft.Json;
namespace Bit.Core.Models.Response
{
public class Fido2RegistrationChallengeResponse
{
[JsonProperty("challenge")]
public string Challenge { get; set; }
[JsonProperty("timeout")]
public double Timeout { get; set; }
[JsonProperty("rp")]
public Fido2RP Rp { get; set; }
[JsonProperty("user")]
public Fido2User User { get; set; }
[JsonProperty("pubKeyCredParams")]
public List<Fido2PubKeyCredParam> PubKeyCredParams { get; set; }
[JsonProperty("excludeCredentials")]
public List<Fido2CredentialDescriptor> ExcludeCredentials { get; set; }
[JsonProperty("authenticatorSelection")]
public Fido2AuthenticatorSelection AuthenticatorSelection { get; set; }
[JsonProperty("attestation", NullValueHandling = NullValueHandling.Ignore)]
public object Attestation { get; set; }
[JsonProperty("extensions", NullValueHandling = NullValueHandling.Ignore)]
public object Extensions { get; set; }
}
}