This quickstart will walk you through a basic implementation of FIDO2, where we are using FIDO2 for single-factor authentication (something you have). This quickstart is designed to get you familiar with our FIDO2 APIs and WebAuthn. The completed sample should not be deployed into production.
You will be writing some JavaScript in this quickstart in order to use the WebAuthn API. The code for this is meant to be as unopinionated as possible, solely for the purpose of getting you familiar with WebAuthn and the FIDO2 authentication process. The quickstart does use jQuery and Bootstrap, but this is not necessary for component or WebAuthn usage. This example works well on top of the Visual Studio .NET Core or .NET 6 MVC template.
For production, we recommend checking out the work GitHub is doing in webauthn-json.
To run this quickstart, you will need a FIDO U2F or FIDO2 authenticator that can communicate with your browser. This authenticator could be a physical key such as a Yubikey or a platform authenticator such as Windows Hello.
You can find completed source code for this quickstart on GitHub.
NuGet Installation
To start, you’ll need to install our FIDO2 component from NuGet:
install-package Rsk.AspNetCore.Fido
This component requires a license which you can get by signing up for a demo or purchasing via sales@identityserver.com.
Initial Configuration
The FIDO component requires the AddFido
registration in your IServiceCollection
. You also need a store for adding user credentials to, so for now, let’s use an in-memory store.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddFido(options => {
options.Licensee = "<your licensee>";
options.LicenseKey = "<you license key>";
}).AddInMemoryKeyStore();
}
public void Configure(IApplicationBuilder app)
{
app.UseDeveloperExceptionPage();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
}
Registration
For registration, you will need three actions and two views:
- Start Registration, taking in the user’s identifier and a friendly name for the authenticator they are about to register. The view for this will simply be a form that gathers these values
- Registration, using the user’s identifier to start the FIDO registration process (e.g. generating a cryptographic challenge value). The view for this will handle the interaction with the FIDO authenticator
- Complete Registration, for receiving the authenticator’s credential creation response and submitting it to the FIDO library.
You will also need access to IFidoAuthentication
, which will be your entry point into our FIDO component. This can be injected into your controller.
When registering a new credential, you will need to call two methods on this class: one to start registration, and one to complete it. Initiation will create a cookie, and completion will use that cookie and the response from the authenticator to verify and create the new public key credential.
Your code should look something like this:
public class HomeController : Controller
{
private readonly IFidoAuthentication fido;
public HomeController(IFidoAuthentication fido)
{
this.fido = fido ?? throw new ArgumentNullException(nameof(fido));
}
public IActionResult Index() => View();
public IActionResult StartRegistration() => View();
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegistrationModel model)
{
var challenge = await fido.InitiateRegistration(model.UserId, model.DeviceName);
return View(challenge.ToBase64Dto());
}
[HttpPost]
public async Task<IActionResult> CompleteRegistration([FromBody] Base64FidoRegistrationResponse registrationResponse)
{
var result = await fido.CompleteRegistration(registrationResponse.ToFidoResponse());
if (result.IsError) return BadRequest(result.ErrorDescription);
return Ok();
}
}
public class RegistrationModel
{
public string UserId { get; set; }
public string DeviceName { get; set; }
}
With a basic view for StartRegistration
, like so:
@model Core.Models.RegistrationModel
<h2>Register</h2>
<form asp-action="Register">
<div class="form-group">
<label asp-for="UserId"></label>
<input asp-for="UserId" class="form-control" />
</div>
<div class="form-group">
<label asp-for="DeviceName"></label>
<input asp-for="DeviceName" class="form-control" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
If you get lost at any point during this preliminary set up, feel free to copy across the controllers, models, and views from the completed quickstart on GitHub.
Registration View and WebAuthn
Once you have the user flow in place, you will need to interact with the WebAuthn API in your view for Registration. For now, let’s do things inline (bad) in order to keep things simple:
@model Rsk.AspNetCore.Fido.Models.FidoRegistrationChallenge
<h2>Please use your authenticator</h2>
<script>
// TODO: WebAuthn interaction
</script>
First, you’ll need to take the base64 encoded challenge value provided by the server, decode it, and convert it into a byte array. This value must be created by the server (the FIDO component), not client-side.
// Challenge
let challengeBytesAsString = atob("@Html.Raw(Model.Base64Challenge)");
let challenge = new Uint8Array(challengeBytesAsString.length);
for (let i = 0; i < challengeBytesAsString.length; i++) {
challenge[i] = challengeBytesAsString.charCodeAt(i);
}
The server can also define its own identifier (the RP ID), and human-friendly display name so that the user can recognize which site the credential is for.
The RP ID must be a valid “domain string”. If not provided, then WebAuthn will use the origin’s effective domain. An authenticator will only use credentials that match both the stated RP ID and the current browser origin. See the WebAuthn specification for more details.
// Relying party details
let rp = {
id: "@Model.RelyingPartyId",
name: "RSK FIDO Quickstart - Core"
};
Next, we have the user’s details. The user ID is a value that is unique to your user. This is how you will match keys to a user stored elsewhere (e.g. in ASP.NET Identity). The user handle is slightly different. This value must not be personally identifying and will be used during the authentication process after registration. Our default implementation of the user handle generates a different value for every registration request, no matter the user. This addresses account enumeration concerns detailed in the WebAuthn specification.
// User handle
let userHandleBytesAsString = atob("@Html.Raw(Model.Base64UserHandle)");
let userHandle = new Uint8Array(userHandleBytesAsString.length);
for (let i = 0; i < userHandleBytesAsString.length; i++) {
userHandle[i] = userHandleBytesAsString.charCodeAt(i);
}
let user = {
name: "@Model.UserId",
displayName: "@Model.UserId",
id: userHandle
};
Finally, we have pubKeyCredParams
. This is an array of what kinds of public key credentials we are willing to accept. This is ordered from most preferred to least preferred. In this case we are asking for a credential that uses ES256 or RS256 (currently the most common algorithms). You can find a full list of algorithms on GitHub.
// Request ES256 algorithm
let pubKeyCredParams = [
{
type: "public-key",
alg: -7
},
{
type: "public-key",
alg: -257
}
];
This is the minimum required data to create a new public key credential using WebAuthn.
With these parameters, we can now call WebAuthn’s create method:
navigator.credentials.create({ publicKey: {challenge, rp, user, pubKeyCredParams} })
.then((credentials) => {
// TODO: Credential handling
})
.catch((error) => {
console.error(error);
});
Now that you have the credentials, you need to get them back to the server for validation and storage. For this demo, we’re going to base64 encode some of the values that are otherwise array buffers, post the credentials to our CompleteRegistration
endpoint, and then redirect back to the home page. As an alternative, you may want to use a form post approach.
navigator.credentials.create({ publicKey: {challenge, rp, user, pubKeyCredParams} })
.then((credentials) => {
// base64 encode array buffers
let encodedCredentials = {
id: credentials.id,
rawId: btoa(String.fromCharCode.apply(null, new Uint8Array(credentials.rawId))),
type: credentials.type,
response: {
attestationObject:
btoa(String.fromCharCode.apply(null, new Uint8Array(credentials.response.attestationObject))),
clientDataJSON:
btoa(String.fromCharCode.apply(null, new Uint8Array(credentials.response.clientDataJSON)))
}
};
// post to register callback endpoint and redirect to homepage
$.ajax({
url: '/Home/CompleteRegistration',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(encodedCredentials),
success: function() {
window.location.href = "/";
},
error: function() {
console.error("Error from server...");
}
});
})
.catch((error) => {
console.error(error);
});
With this in place, you should now be able to register a FIDO authenticator.
Authentication
Now, to authenticate our registered user. First, let’s add authentication using a cookie to our Startup.cs, with a LoginPath pointing at an action that we are about to create.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
/* existing registrations */
services.AddAuthentication("cookie")
.AddCookie("cookie", options => { options.LoginPath = "/Home/StartLogin"; });
}
public void Configure(IApplicationBuilder app)
{
/* existing registrations */
app.UseAuthentication();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
}
And now, let’s create that action, along with one to handle login initiation, and another for login completion. This will be a similar approach to registration, with an initial action to collect the user ID, initiate authentication with the FIDO component, and then use WebAuthn to facilitate the authentication process.
public class HomeController : Controller
{
/* Existing registraiton methods */
public IActionResult StartLogin() => View();
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginModel model)
{
var challenge = await fido.InitiateAuthentication(model.UserId);
return View(challenge.ToBase64Dto());
}
[HttpPost]
public async Task<IActionResult> CompleteLogin([FromBody] Base64FidoAuthenticationResponse authenticationResponse)
{
var result = await fido.CompleteAuthentication(authenticationResponse.ToFidoResponse());
if (result.IsSuccess)
{
await HttpContext.SignInAsync("cookie", new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
{
new Claim("sub", result.UserId)
}, "cookie")));
}
if (result.IsError) return BadRequest(result.ErrorDescription);
return Ok();
}
}
public class LoginModel
{
public string UserId { get; set; }
}
Your StartLogin view will again be a simple form:
@model Core.Models.LoginModel
<h2>Login</h2>
<form asp-action="Login">
<div class="form-group">
<label asp-for="UserId"></label>
<input asp-for="UserId" class="form-control" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
Login View and WebAuthn
In your login view, you will again need to interact with the WebAuthn API. You’ll need to use the challenge generated by the FIDO component, and RP ID, so let’s add them straight away:
@model Rsk.AspNetCore.Fido.Dtos.Base64FidoRegistrationChallenge
<h2>Please use your authenticator</h2>
<script>
// Challenge
let challengeBytesAsString = atob("@Html.Raw(Model.Base64Challenge)");
let challenge = new Uint8Array(challengeBytesAsString.length);
for (let i = 0; i < challengeBytesAsString.length; i++) {
challenge[i] = challengeBytesAsString.charCodeAt(i);
}
// RP ID
let rpId = "@Model.RelyingPartyId";
// TODO: WebAuthn interaction
</script>
Since you know who the user is, you’ll also know what credentials have been registered for them. As a result, you can ensure that the user can only use a credential that you recognize, using allowCredentials
.
The FIDO library will provide a collection of base64 encoded allowed credential IDs, which we will need to decode and then parse into a collection of WebAuthn PublicKeyCredentialDescriptor
s.
// Allowed credentials
let keys = JSON.parse('@Html.Raw(JsonConvert.SerializeObject(Model.Base64KeyIds))');
let allowCredentials = [];
for (let i = 0; i < keys.length; i++) {
let keyIdBytesAsString = window.atob(keys[i]);
let key = new Uint8Array(keyIdBytesAsString.length);
for (let i = 0; i < keyIdBytesAsString.length; i++) {
key[i] = keyIdBytesAsString.charCodeAt(i);
}
allowCredentials.push({
type: "public-key",
id: key
});
}
With these values, you can now call the WebAuthn’s get method, once again base64 encoding the response and posting it back to the server’s CompleteLogin endpoint:
navigator.credentials.get({ publicKey: { challenge, rpId, allowCredentials } })
.then((result) => {
// base64 encode array buffers
var encodedResult = {
id: result.id,
rawId: btoa(String.fromCharCode.apply(null, new Uint8Array(result.rawId))),
type: result.type,
response: {
authenticatorData:
btoa(String.fromCharCode.apply(null, new Uint8Array(result.response.authenticatorData))),
signature:
btoa(String.fromCharCode.apply(null, new Uint8Array(result.response.signature))),
userHandle:
btoa(String.fromCharCode.apply(null, new Uint8Array(result.response.userHandle))),
clientDataJSON:
btoa(String.fromCharCode.apply(null, new Uint8Array(result.response.clientDataJSON)))
}
};
// post to login callback endpoint and redirect to homepage
$.ajax({
url: '/Home/CompleteLogin',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(encodedResult),
success:function() {
window.location.href = "/";
}
});
})
.catch((error) => {
console.error(error);
});
Triggering Authentication
So, assuming the user has registered, you can now trigger authentication when they try and access a protected area of the website. To test this, let’s spin up a new action that simply has the AuthorizeAttribute on it, display the index page, and say hello to the current user.
Action
[Authorize]
public IActionResult Secure() => View("Index");
View
Updated Index.cshtml
:
@using System.Security.Claims
@{
ViewData["Title"] = "Home Page";
}
@if (User.Identity.IsAuthenticated)
{
<h1>Authenticated!</h1>
<p>
Hello, @User.FindFirstValue("sub")
</p>
}
else
{
<h1>Unauthenticated</h1>
}
You should now be able to run the application, register a FIDO authenticator, and authenticate by trying to access the Secure action.