ASP.NET Core 3.1 - Round Trip Challenge

Ken Haggerty
Created 05/08/2020 - Updated 09/02/2021 01:50

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 downloaded the FREE ASP.NET Core 3.1 - FIDO Utilities Project or created a new ASP.NET Core 3.1 Razor Pages project. I won't use Identity or Individual User Accounts. See Tutorial: Get started with Razor Pages in ASP.NET Core.

FIDO Utilities Project and Article Series

The ASP.NET Core 3.1 - FIDO Utilities Project (FUP) is a collection of utilities I use in the ASP.NET Core 5.0 - Users Without Passwords Project (UWPP). I have upgraded the FUP with Bootstrap v5 and Bootstrap Native v4. FUP version 2.1 features SingleUser Authentication, Admin Page with Send Email Tests, ExceptionEmailerMiddleware, Bootstrap v5 Detection JavaScript, Copy and Paste Demo, Offcanvas Partial Demo, and Path QR Code Demo. The SMTP Settings Tester is updated and now located in the authorized Pages > Admin folder. The EmailSender and AES Cipher Demo are also updated. Registered users can download the source code for free on KenHaggerty. Com at Manage > Assets. The UWPP is published at Fido. KenHaggerty. Com.

Update 09/01/2021

The FUP v2.1 removes the Base64Url.cs and updates to Microsoft. AspNetCore. WebUtilities. Base64UrlTextEncoder. I found injecting the ITempDataProvider to the PageModel is redundant. The UWPP implements the PageModel's ITempDataDictionary. See ASP.NET Core 5.0 - The TempData Challenge. The new FUP includes the TempData Demo. I updated the article for Base64UrlTextEncoder and CookieTempDataProvider options.

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. The UWPP implements the PageModel's ITempDataDictionary. See ASP.NET Core 5.0 - The TempData Challenge. Add the CookieTempDataProvider to the RazorPages service.

Startup.cs > ConfigureServices
services.AddRazorPages(options => { options.Conventions.AuthorizeFolder("/Admin"); })
    .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = null; })
    .AddCookieTempDataProvider(options =>
    {
        options.Cookie.IsEssential = true;
        options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
    });

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);

The new FUP updates the Base64Url.cs to Microsoft. AspNetCore. WebUtilities. Base64UrlTextEncoder. 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 Verified
Challenge Demo Verified Mobile

Article Tags:

FIDO JavaScript Json Modal

Comment Count = 0

Please log in to comment.

Login Register
Logged in users receive web notifications.
Web Notifications