Add Fido2 MFA to an OpenIddict identity provider using ASP.NET Core Identity

This article shows how to add Fido2 multi-factor authentication to an OpenID Connect identity provider using OpenIddict and ASP.NET Core Identity. OpenIddict implements the OpenID Connect standards and ASP.NET Core Identity is used for the user accounting and persistence of the identities.

Code: https://github.com/damienbod/AspNetCoreOpeniddict

I began by creating an OpenIddict web application using ASP.NET Core Identity. See the OpenIddict samples for getting started.

I use the fido2-net-lib Fido2 Nuget package which can be use to add support for Fido2 in .NET Core applications. You can add this to the web application used for the identity provider.

<PackageReference Include="Fido2" Version="3.0.0-beta6" />

Once added, you need to add the API controllers for the webAuthn API calls and the persistence classes using the Fido2 Nuget package. I created a set of classes which you can copy into your project. You need to switch the ApplicationUser class with IdentityUser if you are not extending the ASP.NET Core Identity. I use the ApplicationUser class in this example.

https://github.com/damienbod/AspNetCoreOpeniddict/tree/main/OpeniddictServer/Fido2

In the ApplicationDbContext, the DBSet with the FidoStoredCredential entity is added to persist the Fido2 data.

using Fido2Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace OpeniddictServer.Data;

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    public DbSet<FidoStoredCredential> FidoStoredCredential => Set<FidoStoredCredential>();

    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.Entity<FidoStoredCredential>().HasKey(m => m.Id);

        base.OnModelCreating(builder);
    }
}

The Fido2 Identity services are added to the application. I use an SQL Server to persistent the data. The ApplicationUser is used for the ASP.NET Core services. The Fido2 Fido2UserTwoFactorTokenProvider class is used to add a new Fido2 MFA to the ASP.NET Core Identity. A session is used to store the webAuthn requests and the Fido2 store was added for the persistence.

services.AddDbContext<ApplicationDbContext>(options =>
{
	// Configure the context to use Microsoft SQL Server.
	options.UseSqlServer(Configuration
		.GetConnectionString("DefaultConnection"));

	// Register the entity sets needed by OpenIddict.
	// Note: use the generic overload if you need
	// to replace the default OpenIddict entities.
	options.UseOpenIddict();
});

services.AddIdentity<ApplicationUser, IdentityRole>()
  .AddEntityFrameworkStores<ApplicationDbContext>()
  .AddDefaultTokenProviders()
  .AddDefaultUI()
  .AddTokenProvider<Fido2UserTwoFactorTokenProvider>("FIDO2");

services.Configure<Fido2Configuration>(
	Configuration.GetSection("fido2"));
services.AddScoped<Fido2Store>();

services.AddDistributedMemoryCache();

services.AddSession(options =>
{
	options.IdleTimeout = TimeSpan.FromMinutes(2);
	options.Cookie.HttpOnly = true;
	options.Cookie.SameSite = SameSiteMode.None;
	options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});

The Fido2UserTwoFactorTokenProvider implements the IUserTwoFactorTokenProvider interface which can be used to add additional custom 2FA providers to ASP.NET Core Identity.

using Microsoft.AspNetCore.Identity;
using OpeniddictServer.Data;
using System.Threading.Tasks;

namespace Fido2Identity;

public class Fido2UserTwoFactorTokenProvider 
	: IUserTwoFactorTokenProvider<ApplicationUser>
{
    public Task<bool> CanGenerateTwoFactorTokenAsync(
		UserManager<ApplicationUser> manager, ApplicationUser user)
    {
        return Task.FromResult(true);
    }

    public Task<string> GenerateAsync(string purpose, 
		UserManager<ApplicationUser> manager, 
		ApplicationUser user)
    {
        return Task.FromResult("fido2");
    }

    public Task<bool> ValidateAsync(string purpose, 
		string token, 
		UserManager<ApplicationUser> manager, 
		ApplicationUser user)
    {
        return Task.FromResult(true);
    }
}

The Session is added as middleware as well as the standard packages.

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.UseSession();

app.UseEndpoints(endpoints =>
{
	endpoints.MapControllers();
	endpoints.MapDefaultControllerRoute();
	endpoints.MapRazorPages();
});

The Javascript classes to implement the webAuthn standard API calls and backend API calls need to be added to the wwwroot of the project. The is added here:

https://github.com/damienbod/AspNetCoreOpeniddict/tree/main/OpeniddictServer/wwwroot/js

One js file implements the Fido2 register process and the other implements the login.

Now we need to implement the Fido2 bits in the ASP.NET Core Identity UI and add the Javascript scripts to these pages. I usually scaffold in the required ASP.NET Core pages and extend these with the FIDO2 implementations for the MFA.

The following Identity Pages need to be created or updated:

  • Account/Login
  • Account/LoginFido2Mfa
  • Account/Manage/Disable2fa
  • Account/Manage/Fido2Mfa
  • Account/Manage/TwoFactorAuthentication
  • Account/Manage/ManageNavPages

The Fido2 registration is implemented in the Account/Manage/Fido2Mfa Identity Razor page. The Javascript files are added in this page.

@page "/Fido2Mfa/{handler?}"
@using Microsoft.AspNetCore.Identity
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf
@functions{
    public string? GetAntiXsrfRequestToken()
    {
        return Xsrf.GetAndStoreTokens(this.HttpContext).RequestToken;
    }
}
@model OpeniddictServer.Areas.Identity.Pages.Account.Manage.MfaModel
@{
    Layout = "_Layout.cshtml";
    ViewData["Title"] = "Two-factor authentication (2FA)";
    ViewData["ActivePage"] = ManageNavPages.Fido2Mfa;
}

<h4>@ViewData["Title"]</h4>
<div class="section">
    <div class="container">
        <h1 class="title is-1">2FA/MFA</h1>
        <div class="content"><p>This is scenario where we just want to use FIDO as the MFA. The user register and logins with their username and password. For demo purposes, we trigger the MFA registering on sign up.</p></div>
        <div class="notification is-danger" style="display:none">
            Please note: Your browser does not seem to support WebAuthn yet. <a href="https://caniuse.com/#search=webauthn" target="_blank">Supported browsers</a>
        </div>

        <div class="columns">
            <div class="column is-4">

                <h3 class="title is-3">Add a Fido2 MFA</h3>
                <form action="/Fido2Mfa" method="post" id="register">
                    <input type="hidden" id="RequestVerificationToken" name="RequestVerificationToken" value="@GetAntiXsrfRequestToken()">
                    <div class="field">
                        <label class="label">Username</label>
                        <div class="control has-icons-left has-icons-right">
                            <input class="form-control" type="text" readonly placeholder="email" value="@User.Identity?.Name" name="username" required>
                        </div>
                    </div>

                    <div class="field" style="margin-top:10px;">
                        <div class="control">
                            <button class="btn btn-primary">Add FIDO2 MFA</button>
                        </div>
                    </div>
                </form>
            </div>
        </div>

        <div id="fido2mfadisplay"></div>

    </div>
</div>

<div style="display:none" id="fido2TapYourSecurityKeyToFinishRegistration">FIDO2_TAP_YOUR_SECURITY_KEY_TO_FINISH_REGISTRATION</div>
<div style="display:none" id="fido2RegistrationError">FIDO2_REGISTRATION_ERROR</div>

<script src="~/js/helpers.js"></script>
<script src="~/js/instant.js"></script>
<script src="~/js/mfa.register.js"></script>

The Account/LoginFido2Mfa Razor Page implements the Fido2 login.

@page

@using Microsoft.AspNetCore.Identity
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf
@functions{
    public string? GetAntiXsrfRequestToken()
    {
        return Xsrf.GetAndStoreTokens(this.HttpContext).RequestToken;
    }
}
@model OpeniddictServer.Areas.Identity.Pages.Account.MfaModel
@{
    ViewData["Title"] = "Login with Fido2 MFA";
}

<h4>@ViewData["Title"]</h4>
<div class="section">
    <div class="container">
        <h1 class="title is-1">2FA/MFA</h1>
        <div class="content"><p>This is scenario where we just want to use FIDO as the MFA. The user register and logins with their username and password. For demo purposes, we trigger the MFA registering on sign up.</p></div>
        <div class="notification is-danger" style="display:none">
            Please note: Your browser does not seem to support WebAuthn yet. <a href="https://caniuse.com/#search=webauthn" target="_blank">Supported browsers</a>
        </div>

        <div class="columns">
            <div class="column is-4">

                <h3 class="title is-3">Fido2 2FA</h3>
                <form action="/LoginFido2Mfa" method="post" id="signin">
                    <input type="hidden" id="RequestVerificationToken" name="RequestVerificationToken" value="@GetAntiXsrfRequestToken()">
                    <div class="field">
                        <div class="control">
                            <button class="btn btn-primary">2FA with FIDO2 device</button>
                        </div>
                    </div>
                </form>
            </div>
        </div>

        <div id="fido2logindisplay"></div>

    </div>
</div>

<div style="display:none" id="fido2TapKeyToLogin">FIDO2_TAP_YOUR_SECURITY_KEY_TO_LOGIN</div>
<div style="display:none" id="fido2CouldNotVerifyAssertion">FIDO2_COULD_NOT_VERIFY_ASSERTION</div>
<div style="display:none" id="fido2ReturnUrl">@Model.ReturnUrl</div>

<script src="~/js/helpers.js"></script>
<script src="~/js/instant.js"></script>
<script src="~/js/mfa.login.js"></script>

The other ASP.NET Core Identity files are extended to implement the 2FA providers logic.

I extended the _Layout file to include the sweetalert2 js package used to implement the UI popups as in the FIDO2 demo from the Nuget package passwordless demo. You do not need this and can change the js files to use something else.

<head>

// ...
<script type="text/javascript" 
	src="https://cdn.jsdelivr.net/npm/sweetalert2"></script>
<link rel="stylesheet" 
	href="https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/6.10.1/sweetalert2.min.css" />
	
</head>

I used the Feitian Fido2 keys to test the implementation. I find these keys excellent and robust. I used 2 mini and 2 NFC standard keys to test. You should add at least 2 keys per identity. I usually use three keys for all my accounts.

https://www.ftsafe.com/products/FIDO

Once you login and create an account, you can go to the user settings and setup a 2FA. Choose Fido2. Then you can register a key for you account.

Next time you login, you will be required to authenticate using Fido2 as a second factor.

With Fido2 protecting the accounts against phishing and a solid implementation of OpenID Connect, you have a great start to implementing a professional identity provider.

Links:

https://github.com/abergs/fido2-net-lib

https://webauthn.io/

https://github.com/damienbod/AspNetCoreIdentityFido2Mfa

https://documentation.openiddict.com/

https://github.com/openiddict/openiddict-core

https://github.com/openiddict/openiddict-samples

5 comments

  1. […] Add Fido2 MFA to an OpenIddict identity provider using ASP.NET Core Identity – Damien Bowden […]

  2. […] Add Fido2 MFA to an OpenIddict identity provider using ASP.NET Core Identity (Damien Bowden) […]

  3. Great stuff. Blog post idea: how to implement this with vanilla auth. I’m personally not using all of the Identity stuff that comes with ASP.NET Core, why it would be nice to see how this can be implemented using systems simply using HttpContext.SignInAsync and related methods.

    1. Hi Thomas, thanks if you want to use this without Identity, then there is a great example here:

      https://github.com/passwordless-lib/fido2-net-lib

      But when you are managing identities and persisting them, you will probably require or re-implement most of the Identity features and with probably security holes. If doing accounting and identity management, I want to be safe and use something battle tested and Identity works good for this. Although maybe an IDP with use FIDO2 auth meet be something cool here. You got me thinking 🙂

      Thanks for the feedback.

      Greetings Damien

      1. Thomas Ardal · ·

        Makes sense. Thanks 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: