# FIDO2 .NET Library (WebAuthn) The FIDO2 .NET Library is a fully working, battle-tested implementation for passkeys (FIDO2 and WebAuthn) on .NET. It provides a developer-friendly FIDO2 Server / WebAuthn relying party library for validating registration (attestation) and authentication (assertion) of FIDO2/WebAuthn credentials. The library enables passwordless sign-in for all .NET applications including ASP.NET Core, Blazor WebAssembly, and native apps, with the ultimate goal of defeating phishing attacks through modern authentication standards. This library supports all current attestation formats (packed, tpm, android-key, android-safetynet, fido-u2f, apple, apple-appattest, none), comprehensive WebAuthn extensions (PRF, Large Blob, Credential Protection), FIDO2 Metadata Service V3, and achieves 100% pass rate in FIDO Alliance conformance testing. It works with security keys (YubiKey, SoloKeys), platform authenticators (Windows Hello, Face ID, Touch ID, Android Key), and has full backward compatibility with FIDO U2F. The library requires .NET 8.0 or later. ## Installation Install the core package: ```bash dotnet add package Fido2 ``` For ASP.NET Core helpers: ```bash dotnet add package Fido2.AspNet ``` For Blazor WebAssembly support: ```bash dotnet add package Fido2.BlazorWebAssembly ``` ## AddFido2 - Configure FIDO2 Services Registers FIDO2 services in the ASP.NET Core dependency injection container. This is the entry point for configuring the library with your relying party settings and optional metadata service. ```csharp using Fido2NetLib; using Microsoft.Extensions.DependencyInjection; var builder = WebApplication.CreateBuilder(args); // Basic configuration builder.Services.AddFido2(options => { options.ServerDomain = "example.com"; options.ServerName = "My Application"; options.Origins = new HashSet { "https://example.com" }; options.Timeout = 60000; // 60 seconds options.ChallengeSize = 32; }); // With FIDO Alliance Metadata Service for attestation verification builder.Services .AddFido2(options => { options.ServerDomain = "example.com"; options.ServerName = "My Application"; options.Origins = new HashSet { "https://example.com" }; options.BackupEligibleCredentialPolicy = Fido2Configuration.CredentialBackupPolicy.Allowed; options.BackedUpCredentialPolicy = Fido2Configuration.CredentialBackupPolicy.Allowed; }) .AddFidoMetadataRepository() // Official FIDO Alliance MDS .AddCachedMetadataService(); // Multi-tier caching var app = builder.Build(); ``` ## RequestNewCredential - Create Registration Options Generates credential creation options to be sent to the client's browser/authenticator for registering a new passkey. This is the first step in the registration ceremony. ```csharp using Fido2NetLib; using Fido2NetLib.Objects; using Microsoft.AspNetCore.Mvc; [ApiController] [Route("api/[controller]")] public class AuthController : ControllerBase { private readonly IFido2 _fido2; private readonly IUserStore _userStore; // Your user storage public AuthController(IFido2 fido2, IUserStore userStore) { _fido2 = fido2; _userStore = userStore; } [HttpPost("register/options")] public async Task CreateRegistrationOptions([FromBody] RegisterRequest request) { // Get or create user var user = new Fido2User { DisplayName = request.DisplayName, Name = request.Username, Id = Encoding.UTF8.GetBytes(request.Username) }; // Get existing credentials to exclude (prevents re-registration) var existingCredentials = await _userStore.GetCredentialsForUser(user.Id); var excludeCredentials = existingCredentials .Select(c => new PublicKeyCredentialDescriptor(c.Id)) .ToList(); // Create options with full configuration var options = _fido2.RequestNewCredential(new RequestNewCredentialParams { User = user, ExcludeCredentials = excludeCredentials, AuthenticatorSelection = new AuthenticatorSelection { ResidentKey = ResidentKeyRequirement.Preferred, UserVerification = UserVerificationRequirement.Preferred, AuthenticatorAttachment = AuthenticatorAttachment.Platform // or CrossPlatform }, AttestationPreference = AttestationConveyancePreference.Direct, Extensions = new AuthenticationExtensionsClientInputs { CredProps = true, // Get credential properties CredentialProtectionPolicy = CredentialProtectionPolicy.UserVerificationRequired, EnforceCredentialProtectionPolicy = true } }); // Store options for verification (use session, cache, or database) HttpContext.Session.SetString("fido2.attestationOptions", options.ToJson()); return Ok(options); } } // Request DTO public class RegisterRequest { public string Username { get; set; } public string DisplayName { get; set; } } ``` ## MakeNewCredentialAsync - Verify Registration Response Verifies the attestation response from the authenticator and creates a new credential. This completes the registration ceremony after the client has created a passkey. ```csharp [HttpPost("register/verify")] public async Task VerifyRegistration( [FromBody] AuthenticatorAttestationRawResponse attestationResponse, CancellationToken cancellationToken) { try { // Retrieve the original options var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions"); if (string.IsNullOrEmpty(jsonOptions)) return BadRequest(new { error = "Session expired. Please restart registration." }); var options = CredentialCreateOptions.FromJson(jsonOptions); HttpContext.Session.Remove("fido2.attestationOptions"); // Callback to verify credential ID uniqueness IsCredentialIdUniqueToUserAsyncDelegate uniqueCallback = async (args, ct) => { var existingUsers = await _userStore.GetUsersByCredentialId(args.CredentialId); return existingUsers.Count == 0; }; // Verify and create the credential var credential = await _fido2.MakeNewCredentialAsync( new MakeNewCredentialParams { AttestationResponse = attestationResponse, OriginalOptions = options, IsCredentialIdUniqueToUserCallback = uniqueCallback }, cancellationToken); // Store the credential in your database await _userStore.AddCredential(new StoredCredential { Id = credential.Id, PublicKey = credential.PublicKey, UserHandle = credential.User.Id, SignCount = credential.SignCount, AttestationFormat = credential.AttestationFormat, AaGuid = credential.AaGuid, Transports = credential.Transports, IsBackupEligible = credential.IsBackupEligible, IsBackedUp = credential.IsBackedUp, AttestationObject = credential.AttestationObject, AttestationClientDataJson = credential.AttestationClientDataJson, RegDate = DateTimeOffset.UtcNow }); return Ok(new { success = true, credentialId = Convert.ToBase64String(credential.Id), attestationFormat = credential.AttestationFormat }); } catch (Fido2VerificationException ex) { return BadRequest(new { error = ex.Message }); } } ``` ## GetAssertionOptions - Create Authentication Options Generates assertion options to be sent to the client for authenticating with an existing passkey. This is the first step in the authentication ceremony. ```csharp [HttpPost("login/options")] public async Task CreateLoginOptions([FromBody] LoginRequest request) { List allowedCredentials = new(); // For identified users, provide their registered credentials if (!string.IsNullOrEmpty(request.Username)) { var user = await _userStore.GetUserByUsername(request.Username); if (user == null) return NotFound(new { error = "User not found" }); var credentials = await _userStore.GetCredentialsForUser(user.Id); allowedCredentials = credentials .Select(c => new PublicKeyCredentialDescriptor( PublicKeyCredentialType.PublicKey, c.Id, c.Transports)) .ToList(); } // If username is empty, allows discoverable credentials (usernameless flow) var options = _fido2.GetAssertionOptions(new GetAssertionOptionsParams { AllowedCredentials = allowedCredentials, UserVerification = UserVerificationRequirement.Preferred, Extensions = new AuthenticationExtensionsClientInputs { Extensions = true } }); // Store options for verification HttpContext.Session.SetString("fido2.assertionOptions", options.ToJson()); return Ok(options); } public class LoginRequest { public string? Username { get; set; } // Optional for discoverable credentials } ``` ## MakeAssertionAsync - Verify Authentication Response Verifies the assertion response from the authenticator to complete user authentication. Returns the verified result with updated signature counter. ```csharp [HttpPost("login/verify")] public async Task VerifyLogin( [FromBody] AuthenticatorAssertionRawResponse clientResponse, CancellationToken cancellationToken) { try { // Retrieve the original options var jsonOptions = HttpContext.Session.GetString("fido2.assertionOptions"); if (string.IsNullOrEmpty(jsonOptions)) return BadRequest(new { error = "Session expired. Please restart login." }); var options = AssertionOptions.FromJson(jsonOptions); HttpContext.Session.Remove("fido2.assertionOptions"); // Get the stored credential var credential = await _userStore.GetCredentialById(clientResponse.RawId); if (credential == null) return BadRequest(new { error = "Unknown credential" }); // Callback to verify user handle ownership IsUserHandleOwnerOfCredentialIdAsync ownershipCallback = async (args, ct) => { var userCredentials = await _userStore.GetCredentialsByUserHandle(args.UserHandle); return userCredentials.Any(c => c.Id.SequenceEqual(args.CredentialId)); }; // Verify the assertion var result = await _fido2.MakeAssertionAsync( new MakeAssertionParams { AssertionResponse = clientResponse, OriginalOptions = options, StoredPublicKey = credential.PublicKey, StoredSignatureCounter = credential.SignCount, IsUserHandleOwnerOfCredentialIdCallback = ownershipCallback }, cancellationToken); // Update the signature counter (important for clone detection) await _userStore.UpdateCredentialCounter(result.CredentialId, result.SignCount); // Get user information for the response var user = await _userStore.GetUserByCredentialId(result.CredentialId); return Ok(new { success = true, userId = user?.Id, username = user?.Name, signCount = result.SignCount }); } catch (Fido2VerificationException ex) { return BadRequest(new { error = ex.Message }); } } ``` ## Fido2Configuration - Configuration Options Configures the FIDO2 library behavior including server identity, origins, timeouts, and security policies for credential backup and attestation verification. ```csharp var config = new Fido2Configuration { // Required: Server identity ServerDomain = "example.com", ServerName = "My Application", Origins = new HashSet { "https://example.com", "https://www.example.com", "https://app.example.com" }, // Optional: Timing Timeout = 60000, // 60 seconds for user interaction ChallengeSize = 32, // Bytes for challenge generation TimestampDriftTolerance = 0, // Milliseconds for attestation timestamps // Optional: Credential backup policies BackupEligibleCredentialPolicy = Fido2Configuration.CredentialBackupPolicy.Allowed, BackedUpCredentialPolicy = Fido2Configuration.CredentialBackupPolicy.Allowed, // Optional: Metadata service cache location MDSCacheDirPath = "/var/cache/fido2mds", // Optional: Reject authenticators with these security issues UndesiredAuthenticatorMetadataStatuses = new[] { AuthenticatorStatus.ATTESTATION_KEY_COMPROMISE, AuthenticatorStatus.USER_VERIFICATION_BYPASS, AuthenticatorStatus.USER_KEY_REMOTE_COMPROMISE, AuthenticatorStatus.USER_KEY_PHYSICAL_COMPROMISE, AuthenticatorStatus.REVOKED } }; // Use with DI services.AddFido2(options => { options.ServerDomain = config.ServerDomain; options.ServerName = config.ServerName; options.Origins = config.Origins; options.BackupEligibleCredentialPolicy = config.BackupEligibleCredentialPolicy; options.BackedUpCredentialPolicy = config.BackedUpCredentialPolicy; }); ``` ## AuthenticationExtensionsClientInputs - WebAuthn Extensions Configures WebAuthn extensions for enhanced functionality including credential properties, PRF (pseudo-random function), large blob storage, and credential protection policies. ```csharp // Registration extensions var registrationExtensions = new AuthenticationExtensionsClientInputs { // Request credential properties (discoverable, authenticator info) CredProps = true, // Credential protection - control when credential can be used CredentialProtectionPolicy = CredentialProtectionPolicy.UserVerificationRequired, EnforceCredentialProtectionPolicy = true, // Large blob storage (for storing additional data with credential) LargeBlob = new AuthenticationExtensionsLargeBlobInputs { Support = LargeBlobSupport.Required }, // PRF extension for deriving encryption keys PRF = new AuthenticationExtensionsPRFInputs { Eval = new AuthenticationExtensionsPRFValues { First = Convert.FromBase64String("c2FsdDEyMzQ1Njc4") // Salt value } } }; // Use in registration var options = _fido2.RequestNewCredential(new RequestNewCredentialParams { User = user, ExcludeCredentials = excludeCredentials, Extensions = registrationExtensions }); // Authentication extensions var authenticationExtensions = new AuthenticationExtensionsClientInputs { Extensions = true, // Get supported extensions list // Read large blob data LargeBlob = new AuthenticationExtensionsLargeBlobInputs { Read = true }, // PRF for key derivation during authentication PRF = new AuthenticationExtensionsPRFInputs { Eval = new AuthenticationExtensionsPRFValues { First = Convert.FromBase64String("c2FsdDEyMzQ1Njc4") } } }; var assertionOptions = _fido2.GetAssertionOptions(new GetAssertionOptionsParams { AllowedCredentials = allowedCredentials, UserVerification = UserVerificationRequirement.Required, Extensions = authenticationExtensions }); ``` ## Metadata Service - Attestation Verification Configures the FIDO Alliance Metadata Service for validating authenticator attestations. This enables verification of authenticator security status and certification. ```csharp // Basic setup with FIDO Alliance MDS services .AddFido2(options => { options.ServerDomain = "example.com"; options.ServerName = "My App"; options.Origins = new HashSet { "https://example.com" }; }) .AddFidoMetadataRepository() // Official FIDO Alliance MDS v3 .AddCachedMetadataService(); // Multi-tier caching (Memory + Distributed) // With custom HTTP client configuration services .AddFido2(options => { /* ... */ }) .AddFidoMetadataRepository(httpBuilder => { httpBuilder.ConfigureHttpClient(client => { client.Timeout = TimeSpan.FromSeconds(30); }); // Add Polly retry policy if using Microsoft.Extensions.Http.Polly // httpBuilder.AddTransientHttpErrorPolicy(p => p.RetryAsync(3)); }) .AddCachedMetadataService(); // Multiple repositories for fallback services .AddFido2(options => { /* ... */ }) .AddFidoMetadataRepository() // Primary: FIDO Alliance .AddFileSystemMetadataRepository("/path/to/local/mds") // Fallback: Local files .AddCachedMetadataService(); // Custom metadata service implementation public class CustomMetadataService : IMetadataService { private readonly IEnumerable _repositories; public CustomMetadataService(IEnumerable repositories) { _repositories = repositories; } public async Task GetEntryAsync( Guid aaguid, CancellationToken cancellationToken = default) { foreach (var repo in _repositories) { var blob = await repo.GetBLOBAsync(cancellationToken); var entry = blob.Entries.FirstOrDefault(e => e.AaGuid == aaguid); if (entry != null) return entry; } return null; } public bool ConformanceTesting() => false; } // Register custom service services .AddFido2(options => { /* ... */ }) .AddFidoMetadataRepository() .AddMetadataService(); ``` ## DevelopmentInMemoryStore - Development Credential Storage Provides an in-memory credential store for development and testing. This is not suitable for production - implement your own persistent storage. ```csharp using Fido2NetLib.Development; using Fido2NetLib.Objects; // In-memory store for development/demo purposes public static class DemoStorage { public static readonly DevelopmentInMemoryStore Store = new(); } // Using the development store public class DemoController : ControllerBase { private readonly IFido2 _fido2; [HttpPost("register/options")] public IActionResult CreateOptions([FromBody] RegisterRequest request) { // Get or create user var user = DemoStorage.Store.GetOrAddUser(request.Username, () => new Fido2User { DisplayName = request.DisplayName, Name = request.Username, Id = Encoding.UTF8.GetBytes(request.Username) }); // Get existing credentials var existingKeys = DemoStorage.Store.GetCredentialsByUser(user) .Select(c => c.Descriptor) .ToList(); var options = _fido2.RequestNewCredential(new RequestNewCredentialParams { User = user, ExcludeCredentials = existingKeys }); HttpContext.Session.SetString("fido2.options", options.ToJson()); return Ok(options); } [HttpPost("register/verify")] public async Task Verify( [FromBody] AuthenticatorAttestationRawResponse response, CancellationToken ct) { var options = CredentialCreateOptions.FromJson( HttpContext.Session.GetString("fido2.options")); var credential = await _fido2.MakeNewCredentialAsync( new MakeNewCredentialParams { AttestationResponse = response, OriginalOptions = options, IsCredentialIdUniqueToUserCallback = async (args, ct) => { var users = await DemoStorage.Store.GetUsersByCredentialIdAsync(args.CredentialId, ct); return users.Count == 0; } }, ct); // Store credential DemoStorage.Store.AddCredentialToUser(options.User, new StoredCredential { Id = credential.Id, PublicKey = credential.PublicKey, UserHandle = credential.User.Id, SignCount = credential.SignCount, AttestationFormat = credential.AttestationFormat, AaGuid = credential.AaGuid, Transports = credential.Transports, IsBackupEligible = credential.IsBackupEligible, IsBackedUp = credential.IsBackedUp, RegDate = DateTimeOffset.UtcNow }); return Ok(new { success = true }); } [HttpPost("login/verify")] public async Task VerifyLogin( [FromBody] AuthenticatorAssertionRawResponse response, CancellationToken ct) { var options = AssertionOptions.FromJson( HttpContext.Session.GetString("fido2.assertionOptions")); var cred = DemoStorage.Store.GetCredentialById(response.RawId); var result = await _fido2.MakeAssertionAsync( new MakeAssertionParams { AssertionResponse = response, OriginalOptions = options, StoredPublicKey = cred.PublicKey, StoredSignatureCounter = cred.SignCount, IsUserHandleOwnerOfCredentialIdCallback = async (args, ct) => { var creds = await DemoStorage.Store.GetCredentialsByUserHandleAsync(args.UserHandle, ct); return creds.Exists(c => c.Descriptor.Id.SequenceEqual(args.CredentialId)); } }, ct); // Update counter DemoStorage.Store.UpdateCounter(result.CredentialId, result.SignCount); return Ok(new { success = true }); } } ``` ## Client-Side JavaScript Integration Example JavaScript code for interacting with the FIDO2 API endpoints from the browser using the WebAuthn API. ```javascript // Registration flow async function register(username, displayName) { // 1. Get options from server const optionsResponse = await fetch('/api/auth/register/options', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, displayName }) }); const options = await optionsResponse.json(); // 2. Convert base64url to ArrayBuffer options.challenge = base64urlToArrayBuffer(options.challenge); options.user.id = base64urlToArrayBuffer(options.user.id); if (options.excludeCredentials) { options.excludeCredentials = options.excludeCredentials.map(c => ({ ...c, id: base64urlToArrayBuffer(c.id) })); } // 3. Create credential using WebAuthn API const credential = await navigator.credentials.create({ publicKey: options }); // 4. Prepare response for server const attestationResponse = { id: credential.id, rawId: arrayBufferToBase64url(credential.rawId), type: credential.type, response: { clientDataJSON: arrayBufferToBase64url(credential.response.clientDataJSON), attestationObject: arrayBufferToBase64url(credential.response.attestationObject) } }; // 5. Send to server for verification const verifyResponse = await fetch('/api/auth/register/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(attestationResponse) }); return await verifyResponse.json(); } // Authentication flow async function login(username = null) { // 1. Get assertion options const optionsResponse = await fetch('/api/auth/login/options', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username }) }); const options = await optionsResponse.json(); // 2. Convert base64url to ArrayBuffer options.challenge = base64urlToArrayBuffer(options.challenge); if (options.allowCredentials) { options.allowCredentials = options.allowCredentials.map(c => ({ ...c, id: base64urlToArrayBuffer(c.id) })); } // 3. Get assertion using WebAuthn API const assertion = await navigator.credentials.get({ publicKey: options }); // 4. Prepare response for server const assertionResponse = { id: assertion.id, rawId: arrayBufferToBase64url(assertion.rawId), type: assertion.type, response: { clientDataJSON: arrayBufferToBase64url(assertion.response.clientDataJSON), authenticatorData: arrayBufferToBase64url(assertion.response.authenticatorData), signature: arrayBufferToBase64url(assertion.response.signature), userHandle: assertion.response.userHandle ? arrayBufferToBase64url(assertion.response.userHandle) : null } }; // 5. Send to server for verification const verifyResponse = await fetch('/api/auth/login/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(assertionResponse) }); return await verifyResponse.json(); } // Utility functions function base64urlToArrayBuffer(base64url) { const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/'); const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes.buffer; } function arrayBufferToBase64url(buffer) { const bytes = new Uint8Array(buffer); let binary = ''; for (let i = 0; i < bytes.length; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); } ``` ## Summary The FIDO2 .NET Library provides a complete solution for implementing passwordless authentication in .NET applications. The primary use cases include: adding passkey support to existing user authentication systems, implementing strong multi-factor authentication (MFA), building usernameless/discoverable credential flows, and integrating with platform authenticators like Windows Hello, Face ID, and Touch ID. The library handles all the complex cryptographic verification, attestation format parsing, and security validations required by the WebAuthn specification. Integration follows a clean pattern: configure services with `AddFido2()`, inject `IFido2` into controllers, use `RequestNewCredential` and `MakeNewCredentialAsync` for registration, and `GetAssertionOptions` and `MakeAssertionAsync` for authentication. For production deployments, implement a persistent credential store (database), configure the FIDO Alliance Metadata Service for attestation verification, and ensure proper session/cache management for storing temporary options between API calls. The library seamlessly integrates with ASP.NET Core's dependency injection and can be combined with existing authentication systems like ASP.NET Identity or custom JWT-based solutions.