fido2-net-lib

repository·main·Indexed 23 days ago

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

A .NET library for implementing FIDO2 and WebAuthn (Passkeys), supporting passwordless authentication and registration for both roaming and platform authenticators. It provides core functionality via the Fido2 NuGet package (requiring .NET 8.0 or later) and offers specialized integrations for ASP.NET Core and Blazor WebAssembly. The library supports advanced features such as attestation and includes a FIDO2 Metadata Service (MDS) with a two-layer architecture consisting of IMetadataRepository and IMetadataService.

Tokens
10.4K
Snippets
26
Records
48
Agent score
80%

What's inside fido2-net-lib

  1. Compare Fido2-net-lib with .NET 10 Passkeys

    main

    When deciding between this library and the built-in .NET 10 passkey support (available in ASP.NET Identity), consider the following:

    Use .NET 10 Passkeys if:

    • You are using the standard ASP.NET Identity stack and do not need advanced features like attestation.

    Use Fido2-net-lib if:

    • You want to avoid being tied to ASP.NET Identity (e.g., building a SPA or custom auth).
    • You need advanced features like attestation.
    • You want faster access to new parts of the evolving passkey specification.
    • You want to combine advanced features with ASP.NET Identity (the library is designed to be compatible via a plugin interface).
  2. How FIDO2 Metadata Service (MDS) components work together

    main

    The FIDO2 Metadata Service (MDS) uses a two-layer architecture to separate data retrieval from caching and access logic:

    1. IMetadataRepository (Data Source Layer): Responsible for the heavy lifting of fetching, validating, and parsing metadata from sources like the FIDO Alliance, local files, or conformance endpoints.
    2. IMetadataService (Caching/Access Layer): Acts as a wrapper around one or more repositories. It provides a simplified API for retrieving metadata entries and manages caching strategies (e.g., multi-level caching) to optimize performance.

    This separation allows you to source attestation data from multiple repositories while maintaining a single, efficient access point for your application.

  3. Implement a custom IMetadataRepository

    main

    To create a custom metadata source (e.g., a database), implement the IMetadataRepository interface. You must implement GetBLOBAsync to fetch the payload and GetMetadataStatementAsync to retrieve specific statements from that payload.

    public class DatabaseMetadataRepository : IMetadataRepository
    {
        private readonly IDbContext _context;
        private readonly ILogger<DatabaseMetadataRepository> _logger;
    
        public DatabaseMetadataRepository(IDbContext context, ILogger<DatabaseMetadataRepository> logger)
        {
            _context = context;
            _logger = logger;
        }
    
        public async Task<MetadataBLOBPayload> GetBLOBAsync(CancellationToken cancellationToken = default)
        {
            _logger.LogInformation("Loading metadata BLOB from database");
            // TODO: Implement
        }
    
        public Task<MetadataStatement?> GetMetadataStatementAsync(
            MetadataBLOBPayload blob, 
            MetadataBLOBPayloadEntry entry, 
            CancellationToken cancellationToken = default)
        {
            // Statement is already loaded in the entry from GetBLOBAsync
            return Task.FromResult(entry.MetadataStatement);
        }
    }
  4. Register custom Metadata Repository or Service

    main

    Use the following methods to register your custom implementations in the DI container:

    Register both custom service and repository:

    services
        .AddFido2(config => { /* ... */ })
        .AddMetadataRepository<DatabaseMetadataRepository>()  // Custom repository
        .AddMetadataService<SimpleMetadataService>();         // Custom service

    Register only a custom service (using built-in repository):

    services
        .AddFido2(config => { /* ... */ })
        .AddFidoMetadataRepository()  // FIDO Alliance repository
        .AddMetadataService<SimpleMetadataService>();         // Custom service

    Register a custom repository with built-in caching service:

    services
        .AddFido2(config => { /* ... */ })
        .AddMetadataRepository<DatabaseMetadataRepository>()  // Custom repository
        .AddCachedMetadataService();                          // Built-in caching
  5. Configure Fido2 services in ASP.NET Core

    main

    Register the Fido2 services in your dependency injection container using AddFido2. You must provide a ServerDomain, ServerName, and a set of allowed Origins.

    services.AddFido2(options =>
    {
        options.ServerDomain = "example.com";
        options.ServerName = "Example App";
        options.Origins = new HashSet<string> { "https://example.com" };
    });
  6. Migrate from v3.0.1 to v4.0.0

    main

    Version 4.0.0 introduces breaking changes to improve API ergonomics. Key changes include:

    • API Method Signatures: Main API methods now use parameter wrapper classes instead of multiple overloads.
    • Return Types: Some return types have been renamed or restructured (e.g., MakeNewCredentialAsync now returns RegisteredPublicKeyCredential).
    • Framework Target: The library now targets .NET 8.0.
    • Extension Support: Expanded WebAuthn extensions support.
    • Nullable Reference Types: Comprehensive nullable annotations are now used.
  7. Implement a custom IMetadataService

    main

    To implement a custom caching strategy, implement IMetadataService. The service typically receives an IEnumerable<IMetadataRepository> via dependency injection and iterates through them to refresh its internal cache.

    public class SimpleMetadataService : IMetadataService
    {
        private readonly IEnumerable<IMetadataRepository> _repositories;
        private readonly ILogger<SimpleMetadataService> _logger;
        private readonly ConcurrentDictionary<Guid, MetadataBLOBPayloadEntry?> _cache = new();
        private DateTime _lastRefresh = DateTime.MinValue;
        private readonly TimeSpan _refreshInterval = TimeSpan.FromHours(6);
    
        public SimpleMetadataService(
            IEnumerable<IMetadataRepository> repositories, 
            ILogger<SimpleMetadataService> logger)
        {
            _repositories = repositories;
            _logger = logger;
        }
    
        public async Task<MetadataBLOBPayloadEntry?> GetEntryAsync(Guid aaguid, CancellationToken cancellationToken = default)
        {
            await RefreshIfNeededAsync(cancellationToken);
            return _cache.TryGetValue(aaguid, out var entry) ? entry : null;
        }
    
        public bool ConformanceTesting() => false;
    
        private async Task RefreshIfNeededAsync(CancellationToken cancellationToken)
        {
            foreach (var repository in _repositories)
            {
                try
                {
                    var blob = await repository.GetBLOBAsync(cancellationToken);
                    foreach (var entry in blob.Entries)
                    {
                        // Cache it
                    }
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "Failed to refresh from repository {Repository}", repository.GetType().Name);
                }
            }
        }
    }