Skip to content

4. Wrangling The FIDO2.Net library into EF Core

Matt Goldman edited this page Aug 3, 2023 · 8 revisions

The WebAuthN ceremonies have a client-side component and a server-side component. The server side in dotnetflix is IdentityServer, and while it doesn't include FIDO2 or WebAuthN support out of the box, the single greatest benefit of IdentityServer (over say a cloud based IDP or closed-source solution) is that it's just a NuGet package that you include in your code. You can extend or modify its behavior as much as you need to.

In dotnetflix, FIDO2 support is provided via the Fido2 library. This library takes care of things like issuing challenges and validating attestations and assertions. However, the demo code provided uses an in-memory store which means that an authenticator registered for passwordless authentication could not be used after a reboot (or any other loss of memory state). Additionally, it doesn't interact with the user store in any way, just using its own Fido2User type instead, which is not persisted (e.g. with ASP.NET Core Identity).

While this is ok for a demo, I wanted to do something a little more production ready. I wanted to be able to persist the FIDO credentials to enable authentication after a reboot, and I wanted to be able to associate them with my IdentityUser type. The Fido2 library doesn't play particularly well with AspNetCore.Identity or EF Core out of the box, but with the following changes I was able to get it to work.

Adding derived types and configurating relationships

The biggest hurdle is that the Fido2 library is designed for use in any context, not just with EF Core. This meant that there are no relationships defined between the entities, and that some of the properties are not particularly database friendly (for example, the cryptographic blocks used for challenges). The first problem is relatively straightforward to solve - we can use EF Core conventions to add navigation properties and specify relationships.

The first type I added is FidoUser which inherits the Fido2User class that comes from the Fido2.Net library. I then added a navigation property for this type to the ApplicationUser class (that comes from the IdentityServer template):

public class ApplicationUser : IdentityUser
{
    public FidoUser FidoUser { get; set; }
}

I also added a navigation property and foreign key to the FidoUser class which enables EF Core to understand the parent/child nature of the relationship:

public string ApplicationUserId { get; set; }
public ApplicationUser ApplicationUser { get; set; }

This adds a relationship between the ApplicationUser type, which is what represents a user who signs up or signs in, and the FidoUser type, which is used for attestations and assertions.

The Fido2.Net library has a StoredCredential type, which represents a credential registered in an attestation and validated in an assertion. I have added a type called FidoStoredCredential which inherits this, and adds a navigation property for the FidoUser type.

public class FidoStoredCredential : StoredCredential
{
    public int CredentialId { get; set; }
    
    public int FidoUserId { get; set; }
    public FidoUser FidoUser { get; set; }

    public new FidoPublicKeyDescriptor Descriptor { get; set; }
}

A user can register many credentials, so the FidoUser type defines a collection:

public ICollection<FidoStoredCredential> StoredCredentials { get; set; } = new List<FidoStoredCredential>();

As you can see from the definition of the FidoStoredCredential type, it also has a relationship to a FidoPublicKeyDescriptor. This inherits the PublicKeyCredentialDescriptor type from the Fido2.Net library and is used to store the public key created during the attestation.

public class FidoPublicKeyDescriptor : PublicKeyCredentialDescriptor
{
    public int DescriptorId { get; set; }

    public int CredentialId { get; set; }
    public FidoStoredCredential Credential { get; set; }

    public FidoPublicKeyDescriptor(byte[] CredentialId)
    {
        Id = CredentialId;
    }

    public FidoPublicKeyDescriptor()
    {
        
    }
}

This is all the entities required to support FIDO2, but they also need some additional configuration.

Entity Configuration

The first step is to add the new entities to the ApplicationDbContext.

public DbSet<FidoUser> FidoUsers { get; set; }
public DbSet<FidoStoredCredential> FidoStoredCredentials { get; set; }
public DbSet<FidoPublicKeyDescriptor> FidoPublicKeyDescriptors { get; set; }

With the entities added, there are some configuration changes required before you can add a migration:

  1. All three entities need a key explicitly defined and to be set to autogenerate.
  2. The FidoStoredCredential entity has public key and public key list properties, which are based on byte arrays.
  3. The FidoStoredCredential and FidoPublicKeyDescriptor entities have AuthenticatorTransport property which are arrays of enums, and need a converter.

The first step is to create the converter:

public class AuthenticatorTransportArrayConverter : ValueConverter<AuthenticatorTransport[], string>
{
    public AuthenticatorTransportArrayConverter() : base(
        v => string.Join(",", v),
        v => v.Split(',', StringSplitOptions.RemoveEmptyEntries)
               .Select(s => Enum.Parse<AuthenticatorTransport>(s))
               .ToArray())
    {
    }
}

Next we need some methods to convert the list of byte arrays to a byte array (for storage in a column) and back again. I added these methods to the ApplicationDbContext, but they should probably be in a static class in the Converters folder (I may move them):

private static byte[] ConvertToByteArray(List<byte[]> value)
{
    if (value == null || value.Count == 0)
        return null;

    string json = JsonSerializer.Serialize(value);
    return Encoding.UTF8.GetBytes(json);
}

private static List<byte[]> ConvertToByteArrayList(byte[] value)
{
    if (value == null)
        return null;

    string json = Encoding.UTF8.GetString(value);
    return JsonSerializer.Deserialize<List<byte[]>>(json);
}

Finally, with all these parts in place, I added the configuration to the OnModelCreating override method in the ApplicationDbContext class.

protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);
    // Customize the ASP.NET Identity model and override the defaults if needed.
    // For example, you can rename the ASP.NET Identity table names and more.
    // Add your customizations after calling base.OnModelCreating(builder);

    // FidoUser Configuration
    builder.Entity<FidoUser>()
        .HasKey(u => u.UserId);

    builder.Entity<FidoUser>()
        .Property(u => u.UserId)
        .ValueGeneratedOnAdd();

        
        
    // FidoPublicKeyDescriptor configuration
    builder.Entity<FidoPublicKeyDescriptor>()
        .Property(e => e.Transports)
        .HasConversion(new AuthenticatorTransportArrayConverter());

    builder.Entity<FidoPublicKeyDescriptor>()
    .HasKey(d => d.Id);

    builder.Entity<FidoPublicKeyDescriptor>()
        .HasKey(u => u.DescriptorId);

    builder.Entity<FidoPublicKeyDescriptor>()
        .Property(u => u.DescriptorId)
        .ValueGeneratedOnAdd();



    // FidoStoredCredential configuration
    builder.Entity<FidoStoredCredential>()
        .Property(e => e.Transports)
        .HasConversion(new AuthenticatorTransportArrayConverter());

    builder.Entity<FidoStoredCredential>()
    .HasKey(d => d.Id);

    builder.Entity<FidoStoredCredential>()
        .Property(e => e.DevicePublicKeys)
        .HasConversion(
            v => ConvertToByteArray(v),
            v => ConvertToByteArrayList(v));

    builder.Entity<FidoStoredCredential>()
        .HasKey(u => u.CredentialId);

    builder.Entity<FidoStoredCredential>()
        .Property(u => u.CredentialId)
        .ValueGeneratedOnAdd();
}