WebAuthn backend setup
- Install nuget packages (use version 4):
dotnet add package Fido2 --version 4.0.0-beta.13 dotnet add package Fido2.AspNet --version 4.0.0-beta.13
- Add table in database to store passkeys
public class UserPasskey { public Guid UserId { get; set; } public byte[] Id { get; set; } public byte[] UserHandle { get; set; } public uint SignCount { get; set; } public DateTime RegistrationDate { get; set; } public Guid AaGuid { get; set; } public byte[] PublicKey { get; set; } public string AttestationFormat { get; set; } public AuthenticatorTransport[] Transports { get; set; } public bool IsBackupEligible { get; set; } public bool IsBackedUp { get; set; } public byte[] AttestationObject { get; set; } public byte[] AttestationClientDataJson { get; set; } public List<byte[]> DevicePublicKeys { get; set; } }
- Configure fido service, in
Program.cs
builder.Services.AddFido2(options => { options.ServerDomain = "localhost"; options.ServerName = "WebAuth test"; options.Origins = new HashSet<string>() { "https://localhost:44365/" }; options.TimestampDriftTolerance = 300000; options.MDSCacheDirPath = ""; }).AddCachedMetadataService(config => { config.AddFidoMetadataRepository(httpClientBuilder => { }); });
- Add session cookies to site, in
Program.cs
builder.Services.AddSession(options => { options.Cookie.HttpOnly = true; options.IdleTimeout = TimeSpan.FromMinutes(5); options.Cookie.SameSite = SameSiteMode.Strict; });
app.UseSession();
How to register a passkey
Create settings for registering a passkey
- Create a fido user from logged-in user
var fidoUser = new Fido2User { Id = user.Id.ToByteArray(), DisplayName = user.UserName, Name = user.UserName };
- Get existing keys for user
var existingKeys = await _context.UserPasskeys .Where(x => x.UserId == user.Id) .Select(x => new PublicKeyCredentialDescriptor(PublicKeyCredentialType.PublicKey, x.Id, x.Transports)) .ToListAsync();
- Create settings used for registrations
var authenticatorSelection = new AuthenticatorSelection { UserVerification = UserVerificationRequirement.Preferred, ResidentKey = ResidentKeyRequirement.Discouraged, AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform }; var exts = new AuthenticationExtensionsClientInputs() { Extensions = false, };
- Create the credential create options
var options = _fido.RequestNewCredential(fidoUser, existingKeys, authenticatorSelection, AttestationConveyancePreference.None, exts);
- Save options in session
HttpContext.Session.SetString("fido2.attestationOptions", options.ToJson());
- Return options to client
Register passkey
- Get options from session
var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions"); var options = CredentialCreateOptions.FromJson(jsonOptions);
- Create callback to check if a credential has been used before
IsCredentialIdUniqueToUserAsyncDelegate callback = async (args, cancellationToken) => { var passkey = await _context.UserPasskeys.FirstOrDefaultAsync(x => x.Id == args.CredentialId, cancellationToken); if (passkey is not null) { return false; } return true; };
- Create credential
var credential = await _fido.MakeNewCredentialAsync(attestationResponse, options, callback, cancellationToken: ct);
- Save credential to database
var newPasskey = new UserPasskey() { UserId = user.Id, Id = credential.Result.Id, PublicKey = credential.Result.PublicKey, UserHandle = credential.Result.User.Id, SignCount = credential.Result.SignCount, AttestationFormat = credential.Result.AttestationFormat, RegistrationDate = DateTime.UtcNow, AaGuid = credential.Result.AaGuid, Transports = credential.Result.Transports, IsBackupEligible = credential.Result.IsBackupEligible, IsBackedUp = credential.Result.IsBackedUp, AttestationObject = credential.Result.AttestationObject, AttestationClientDataJson = credential.Result.AttestationClientDataJson, DevicePublicKeys = new List<byte[]> {credential.Result.DevicePublicKey} }; _context.Add(newPasskey); await _context.SaveChangesAsync(ct);
- Return credential to client
How to verify a passkey
Create settings for verifying a passkey
- Get existing credentials for user
var existingCredentials = await _context.UserPasskeys .Where(x => x.UserId == user.Id) .Select(x => new PublicKeyCredentialDescriptor(PublicKeyCredentialType.PublicKey, x.Id, x.Transports)) .ToListAsync();
- Set verification settings
var exts = new AuthenticationExtensionsClientInputs() { Extensions = false, }; var uv = UserVerificationRequirement.Preferred;
- Create assertion options
var options = _fido.GetAssertionOptions(existingCredentials, uv, exts);
- Save options in session
HttpContext.Session.SetString("fido2.assertionOptions", options.ToJson());
- Return options to client
Verify passkey
- Get options from session
var jsonOptions = HttpContext.Session.GetString("fido2.assertionOptions"); var options = CredentialCreateOptions.FromJson(jsonOptions);
- Get credential from database
var credential = await _context.UserPasskeys.FirstOrDefaultAsync(x => x.Id == clientResponse.Id, ct);
- Create callback to check that the credential belongs to user
IsUserHandleOwnerOfCredentialIdAsync callback = async (args, cancellationToken) => { var storedCreds = await _context.UserPasskeys.Where(x => x.UserHandle == args.UserHandle).ToListAsync(cancellationToken); return storedCreds.Exists(c => c.Id.SequenceEqual(args.CredentialId)); };
- Verify the public key
var res = await _fido.MakeAssertionAsync(clientResponse, options, credential.PublicKey, credential.DevicePublicKeys, storedCounter, callback, cancellationToken: ct);
- Update the sign count and device public keys on the database credential
if (res.ErrorMessage is null) { credential.SignCount = res.SignCount; credential.DevicePublicKeys.Add(res.DevicePublicKey); _context.Update(credential); await _context.SaveChangesAsync(ct); }
- Return the result to client