ASP.NET Core 5.0 - FIDO2 Challenge Options

Ken Haggerty
Created 07/04/2021 - Updated 08/16/2021 04:00

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.

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;
}
RegisterChallenge Page
RegisterChallenge Page Mobile

Article Tags:

FIDO JavaScript Json Model

Comment Count = 0

Please log in to comment.

Login Register
Logged in users receive web notifications.
Web Notifications