ASP.NET Core 5.0 - FIDO2 Challenge Options
This article will describe the options used to implement authentication with a FIDO2 device. I will assume you have downloaded the ASP.NET Core 5.0 - Users Without Passwords Project.
Users Without Passwords Project and Article Series
I have developed two separate projects in the Users Without Passwords Project (UWPP) solution. The Users Without Passwords v4 project supports Bootstrap v4 and the new Users Without Passwords project supports Bootstrap v5. The new version is published at Fido.KenHaggerty.Com. You can register a new user with Windows Hello or a FIDO2 security key. Details, screenshots, and related articles can be found at ASP.NET Core 5.0 - Users Without Passwords Project. The details page includes the version change log.
- ASP.NET Core 5.0 - Users Without Passwords
- ASP.NET Core 5.0 - Migrate To Bootstrap v5
- ASP.NET Core 5.0 - The TempData Challenge
- ASP.NET Core 5.0 - FIDO2 Challenge Options
- ASP.NET Core 5.0 - FIDO2 Credential Devices
- ASP.NET Core 5.0 - FIDO2 Credential Create
- ASP.NET Core 5.0 - FIDO2 Credential Get
- ASP.NET Core 5.0 - FIDO2 Attestation Trust
- ASP.NET Core 5.0 - Multiple FIDO2 Credentials
The ceremonies to create or authenticate a FIDO2 authenticator implement options which must match property names and types. The CredentialCreationOptions are used with the navigator. credentials. create({ publicKey: createOptions }) method to register a new device.
5.4. Options for Credential Creation
dictionary PublicKeyCredentialCreationOptions { required PublicKeyCredentialRpEntity rp; required PublicKeyCredentialUserEntity user; required BufferSource challenge; required sequence<PublicKeyCredentialParameters> pubKeyCredParams; unsigned long timeout; sequence<PublicKeyCredentialDescriptor> excludeCredentials = []; AuthenticatorSelectionCriteria authenticatorSelection; DOMString attestation = "none"; AuthenticationExtensionsClientInputs extensions; };
The CredentialRequestOptions are used with the navigator. credentials. get({ publicKey: getOptions }) method to authenticate a device.
5.5. Options for Assertion Generation
dictionary PublicKeyCredentialRequestOptions { required BufferSource challenge; unsigned long timeout; USVString rpId; sequence<PublicKeyCredentialDescriptor> allowCredentials = []; DOMString userVerification = "preferred"; AuthenticationExtensionsClientInputs extensions; };
The UWPP implements ChallengeOptions and AuthenticatorSelection classes and a UserVerificationRequirement enum. The ChallengeOptions are configured by appsettings.json and added to the service container in Startup > ConfigureServices.
public class ChallengeOptions { public string CredentialType { get; set; } public string RelyingPartyName { get; set; } public List<int> PubKeyCredParamsAlgs { get; set; } public AuthenticatorSelection AuthenticatorSelection { get; set; } public bool UvmExtension { get; set; } public bool CredPropsExtension { get; set; } public List<string> AllowCredentialTransports { get; set; } public string Attestation { get; set; } public uint AttestationTimeout { get; set; } public uint AssertionTimeout { get; set; } }
public class AuthenticatorSelection { public string AuthenticatorAttachment { get; set; } public bool RequireResidentKey { get; set; } public UserVerificationRequirement UserVerification { get; set; } }
public enum UserVerificationRequirement { required = 0, preferred = 1, discouraged = 2 }
appsettings.json:
"ChallengeOptions": { "CredentialType": "public-key", "RelyingPartyName": "Users Without Passwords", "PubKeyCredParamsAlgs": [ -7, // ECDSA w/ SHA-256 - External authenticators implements "ES256" -35, // ECDSA w/ SHA-384 -36, // ECDSA w/ SHA-512 -257, // RSASSA-PKCS1-v1_5 w/ SHA-256 - Windows Hello implements "RS256" -258, // RSASSA-PKCS1-v1_5 w/ SHA-384 -259 // RSASSA-PKCS1-v1_5 w/ SHA-512 ], "AuthenticatorSelection": { "AuthenticatorAttachment": "cross-platform", // platform, cross-platform "RequireResidentKey": false, // username-less flows = true "UserVerification": "preferred" // required, preferred, discouraged }, "UvmExtension": true, "CredPropsExtension": true, "AllowCredentialTransports": [ "usb", "nfc", "ble", "internal" ], "Attestation": "direct", // none, indirect, direct, enterprise "AttestationTimeout": 90000, // 1.5 minutes "AssertionTimeout": 120000 // 2 minutes }
Startup > ConfigureServices:
services.Configure<ChallengeOptions>(Configuration.GetSection("ChallengeOptions"));
The UWPP implements the complete ceremony on a single page. The RegisterChallenge page injects the ChallengeOptions. The OnGet() method generates and serializes the CredentialCreationOptions.
RegisterChallenge.cshtml.cs:
public class RegisterChallengeModel : PageModel { private readonly IUserService _userService; private readonly ICredentialService _credentialService; private readonly ChallengeOptions _challengeOptions; public RegisterChallengeModel(IUserService userService, ICredentialService credentialService, IOptions<ChallengeOptions> challengeOptions) { _userService = userService; _credentialService = credentialService; _challengeOptions = challengeOptions?.Value; } [TempData] public string LoginName { get; set; } [TempData] public Guid ChallengeGuid { get; set; } public string CreateOptions { get; set; } /// <summary> /// 7.1 https://www.w3.org/TR/webauthn-2/#sctn-registering-a-new-credential /// </summary> public IActionResult OnGet() { if (User.Identity.IsAuthenticated) return RedirectToPage("/Account/Manage/Credentials"); if (TempData.Peek("LoginName") == null) return RedirectToPage("./Register"); TempData.Keep(); // Generate challenge ChallengeGuid = Guid.NewGuid(); var challenge = ChallengeGuid.ToByteArray(); // 16 bytes var currentHost = HttpContext.Request.Host.ToString(); var relyingPartyId = currentHost.Split(":")[0]; var rp = new Rp { Name = _challengeOptions.RelyingPartyName, Id = relyingPartyId }; var user = new { name = LoginName.ToUpper(), displayName = LoginName, id = challenge // new UserHandle }; var pubKeyCredParams = new List<PubKeyCredParams>(); var pubKeyCredParamsAlgs = _challengeOptions.PubKeyCredParamsAlgs; foreach (var alg in pubKeyCredParamsAlgs) pubKeyCredParams.Add(new PubKeyCredParams() { Type = _challengeOptions.CredentialType, Alg = alg }); var authenticatorSelection = _challengeOptions.AuthenticatorSelection; var attestation = _challengeOptions.Attestation; var timeout = _challengeOptions.AttestationTimeout; var extensions = new { uvm = _challengeOptions.UvmExtension, credProps = _challengeOptions.CredPropsExtension }; // 7.1.1 Let options be a new PublicKeyCredentialCreationOptions structure configured to the Relying Party's // needs for the ceremony. var createOptions = new { challenge, rp, user, pubKeyCredParams, authenticatorSelection, attestation, timeout, extensions }; var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }, WriteIndented = true }; CreateOptions = JsonSerializer.Serialize(createOptions, jsonSerializerOptions); return Page(); } }
The FIDO2 authenticators expect the Challenge, UserHandle, and CredentialId properties to be a JavaScript Uint8Array type. The UWPP generates and compiles these properties as Base64 string. The UWPP implements a conversion function in site.js.
function getUint8Array(byteString) { let binaryValue = window.atob(byteString); let len = binaryValue.length; let uint8Array = new Uint8Array(len); let i; for (i = 0; i < len; i++) uint8Array[i] = binaryValue.charCodeAt(i); return uint8Array; }
The RegisterChallenge page's JavaScript updates the appropriate properties to Uint8Array in a createCredential() function.
RegisterChallenge.cshtml - JavaScript:
let createOptions = @Html.Raw(Model.CreateOptions);
// Update byte string to expected Uint8Array try { if (typeof (createOptions.challenge) != Uint8Array) createOptions.challenge = getUint8Array(createOptions.challenge); if (typeof (createOptions.user.id) != Uint8Array) createOptions.user.id = getUint8Array(createOptions.user.id); } catch (e) { showMessageModal(e, 'alert-warning'); return; }
Comments(0)