ASP.NET Core 3.1 - Round Trip Challenge


Ken Haggerty
Created 05/08/2020 - Updated 05/23/2020 15:21

This article will describe the round trip of the challenge, a unique code used by FIDO, from the server to the client-js to the postback verification. I will assume you have created a new ASP.NET Core 3.1 Razor Pages project and have reviewed the previous articles of the series. I won't use Identity or Individual User Accounts. See Tutorial: Get started with Razor Pages in ASP.NET Core.

FIDO Utilties Project and Article Series

This project helps mitigate some of the issues implementing the challenge only. The Users Without Passwords Project implements the challenge with users and authenticators.


Access to the research project source code is free to registered users on KenHaggerty.Com at Manage > Assets.

I will publish the FIDO Utilities Project at fido.kenhaggerty.com until I publish the Users Without Passwords Project.

I enjoy writing these articles. It often enhances and clarifies my coding. The research project is a result of a lot of refactoring and hopefully provides logical segues for the articles. Thank you for supporting my efforts.

The Users Without Passwords Project's registration and login processes involve communication between the server, client-js, authenticator, and user. The server provides a unique code called a challenge to the client. The client transforms the challenge to a UInt8Array expected by the authenticator. The client requests the user's login name. The challenge and username are used to register or verify a public key with the authenticator. The client sends the response from the authenticator to the server. The response is decoded and verified. The response includes the challenge which is decoded from the UInt8Array to verify a match to the server's original code. If the response and challenge are verified the response is stored and action is implemented like create or login the user.

You should detect if Credentials and PublicKeyCredential are supported. Most but not all browsers provide support which is required by FIDO2 processes. I developed a getWebAuthnError() function in site.js which also notifies the lack of https on hosts other than localhost.

site.js - getWebAuthnError
function getWebAuthnError() {
    if (!('credentials' in navigator)) return 'This browser does not currently support Credentials.';
    if (window.PublicKeyCredential === undefined || typeof window.PublicKeyCredential !== 'function') {
        let errorMessage = 'This browser does not currently support WebAuthn.';
        if (window.location.protocol === 'http:' && (window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1'))
            errorMessage = 'WebAuthn only supports secure connections. For testing over HTTP, you can use the origin "localhost".';
        return errorMessage;
    }
    return '';
}

This function returns an error or empty string. If an error is detected you can disable buttons for FIDO functions or inform and redirect the user with the message modal.

document.addEventListener('DOMContentLoaded', function () {
    let waError = getWebAuthnError();
    if (waError != '') {
        disableFidoButtons();
        showMessageModal(waError, 'alert-danger', false, '', '', true, '/', '_self', 'Home');
    };
});

The server provides a unique code called the challenge which is sent to the client-js. The server must persist the code between the initial request and the callback. I use the Cookie TempDataProvider in the FIDO Challenges Project for the proof of concept. I am using the Application Session State in the Users Without Passwords Project. See Session and state management in ASP.NET Core

Add the CookieTempDataProvider to the RazorPages service.

Startup.cs > ConfigureServices
services.AddRazorPages().AddCookieTempDataProvider();

To access the TempDataProvider, inject the ITempDataProvider to the PageModel.

Index.cshtml.cs
public class IndexModel : PageModel
{
    private readonly ITempDataProvider _tempData;
    public IndexModel(ITempDataProvider tempData)
    {
        _tempData = tempData ?? throw new ArgumentNullException(nameof(tempData));
    }

    public void OnGet()
    {
        _tempData.SaveTempData(HttpContext, new Dictionary<string, object> { { "challenge", guidString } });
    }
}

I figured out random number generators researching ASP.NET Core 3.1 - Password Hasher. The first working challenges in the Users Without Passwords Project were 16 byte arrays populated by a rng. I found a FIDO web app example which used a new guid for the unique code. I save the Guid. ToString() in TempData and I use it as a transaction id when I store the creation or verification of the credential. I use the 16 byte Guid. ToByteArray() for the challenge.

var challengeGuid = Guid.NewGuid();
var guidString = challengeGuid.ToString();
var guidByteArray = challengeGuid.ToByteArray(); // 16 bytes
var guidASCIIBytes = Encoding.ASCII.GetBytes(guidString); // 36 bytes

I am not sure Guid. ToByteArray() is compatible with JavaScript's btoa (binary to ASCII) and atob (ASCII to binary) functions. I am sure the 36 byte Encoding. ASCII. GetBytes(Guid. ToString()) is compatible. I continue to evaluate the smaller Guid. ToByteArray() with no issue. See MDN - Base64

GitHub - scottbrady91 / Fido2-Poc uses NuGet - IdentityModel. AspNetCore. OAuth2Introspection which includes a CryptoRandom class to create unique strings and byte arrays.

var uniqueId = CryptoRandom.CreateUniqueId(); // wcUsfGAgExAwC6w4NBlGTS2T6E3QjRQrDJspGHZyp3o
var uidBytes = Base64Url.Decode(uniqueId); // 32 bytes
var randomKey = CryptoRandom.CreateRandomKey(16);

I am not using the IdentityModel Nuget package but I am using a copy of Base64Url.cs. Base64 encoding uses the = char to pad the string to a multiple of four. The Users Without Passwords Project implements Base64Url encoding required to properly decode the authenticator's response. See GitHub - IdentityModel/ src/ Base64Url. cs

The challenge must be properly encoded and decoded to JavaScript's UInt8Array which is required by the authenticator.

TypeError: Failed to execute 'create' on 'CredentialsContainer': The provided value is not of type '(ArrayBuffer or ArrayBufferView)'

The challenge demo in the project simulates an authenticator by converting the challenge byte array from the server to a UInt8Array before posting the serialized challenge array to the callback. There are many more FIDO property values which require this conversion. I developed a JavaScript function getUint8Array (byteString) 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;
}

I implement getUint8Array (byteString) in the Users Without Passwords Project before navigator. credentials. create({ publicKey }) and navigator. credentials. get({ publicKey }) often within a loop.

// Updates for expected Uint8Array
try {
    publicKey.challenge = getUint8Array(publicKey.challenge);
    publicKey.user.id = getUint8Array(publicKey.user.id);
    let allowCredLen = publicKey.excludeCredentials.length;
    for (let ci = 0; ci < allowCredLen; ci++) {
        publicKey.excludeCredentials[ci].id = getUint8Array(publicKey.excludeCredentials[ci].id);
    }
} catch (e) {
    showMessageModal(e, 'alert-warning');
    return;
}

I implement getUint8Array (byteString) in the FIDO Utilities Project's Challenge demo with a Validate UInt8Array for Authenticator checkbox.

// Authenticators expect and return Uint8Array
if (document.querySelector('#ValidateUInt8ArrayCheckboxId').checked) {
    try
    {
        postData.Challenge = getUint8Array(postData.Challenge);
    }
    catch (e) {
        showMessageModal('Challenge Uint8Array Failed. ' + e.toString(), 'alert-danger');
        return;
    }
    // An authenticator will get and return the challenge
    postData.Challenge = window.btoa(String.fromCharCode.apply(null, postData.Challenge));
}

The callback decodes the posted challenge and compares it to the original Guid. ToString() we stored in TempData.

var tempDataDictionary = _tempData.LoadTempData(HttpContext);
// Verify TempData
if (!tempDataDictionary.ContainsKey("GuidString"))
    return new JsonResult(new { Status = "Failed", Error = "Temp Data GuidString not found." });

var tempDataGuidString = tempDataDictionary["GuidString"].ToString();
var tempDataGuid = new Guid(tempDataGuidString);
var tempDataChallengeBytes = tempDataGuid.ToByteArray();
var postChallengeBytes = Base64Url.Decode(postChallengeString);
if (!tempDataChallengeBytes.SequenceEqual(postChallengeBytes))
    return new JsonResult(new { Status = "Failed", Error = "The challenge did not verify." });

The FIDO Utilities Project's challenge demo page generates new guids then compares and validates the Guid. ToByteArray() and Encoding. ASCII. GetBytes(Guid. ToString()). Help me find a Guid. ToByteArray() which is not ASCII compatible.

Challenge Demo
Update 05/10/2020

I updated the article links.

Update 05/23/2020

I added the AES Cipher article link and updated the screenshot.


Article Tags:

FIDO JavaScript Json Modal

Comment Count = 0

Please log in to comment or follow.

Login Register
Follow to get web notifications when new comments are posted to this article.
Logged in users receive web notifications for new articles, topics and assets.
Web Notifications