ASP.NET Core 3.1 - Round Trip Challenge
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.
- ASP.NET Core 3.1 - FIDO Utilities Project
- ASP.NET Core 3.1 - SMTP EmailSender
- ASP.NET Core 3.1 - Message Modal
- ASP.NET Core 3.1 - Round Trip Challenge
- ASP.NET Core 3.1 - Cookie Consent
- ASP.NET Core 3.1 - FIDO2 Authenticators
- ASP.NET Core 3.1 - AES Cipher
- ASP.NET Core 5.0 - Bootstrap v5 Offcanvas Demo
- ASP.NET Core 5.0 - Path QR Code
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.
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.
References:
- WebAuthn.io
- WebAuthn
- MDN - Base64
- GitHub - scottbrady91 / Fido2-Poc
- NuGet - IdentityModel.AspNetCore.OAuth2Introspection
- GitHub - IdentityModel/IdentityModel/src/Base64Url.cs
- Introduction to WebAuthn API
- GitHub - damienbod/AspNetCoreIdentityFido2Mfa
- .NET MVC deserialize byte array from JSON Uint8Array
Comments(0)