using CloudBuilder.Core.Application.Options; using CloudBuilder.Security.Data; using Fido2NetLib; using Fido2NetLib.Objects; using Microsoft.Extensions.Options; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace CloudBuilder.Security.Service { /// /// Wraps the Fido2 ceremony primitives (challenge generation, /// attestation verification, assertion verification) so the process /// layer can stay focused on the workflow. The frontend drives both /// registration and login through this service via webmethods on /// UserProfileProcess. /// public class WebAuthnService : IWebAuthnService { private readonly IFido2 _fido2; private readonly WebAuthnOptions _options; public WebAuthnService(IOptions options, IFido2 fido2) { _options = options.Value; _fido2 = fido2; } public string Origin => _options.Origin; public string RpId => _options.RpId; /// /// Build the PublicKeyCredentialCreationOptions that the browser /// will hand to navigator.credentials.create(). /// public CredentialCreateOptions BuildCreationOptions(string username, byte[] userId, IReadOnlyList existingCredentialIds) { var user = new Fido2User { Name = username, Id = userId, DisplayName = username }; var exclude = existingCredentialIds .Select(id => new PublicKeyCredentialDescriptor(id)) .ToList(); var authSel = new AuthenticatorSelection { // Prefer the platform authenticator (Touch ID / Windows Hello / fingerprint) AuthenticatorAttachment = AuthenticatorAttachment.Platform, UserVerification = UserVerificationRequirement.Preferred }; return _fido2.RequestNewCredential(new RequestNewCredentialParams { User = user, ExcludeCredentials = exclude, AuthenticatorSelection = authSel, AttestationPreference = AttestationConveyancePreference.None }); } /// /// Verify the attestation returned by the browser and return the /// stored credential fields. Throws on validation failure. /// public async Task FinishRegistrationAsync( AuthenticatorAttestationRawResponse attestationResponse, CredentialCreateOptions originalOptions, Func> isCredentialIdUnique) { var makeResult = await _fido2.MakeNewCredentialAsync(new MakeNewCredentialParams { AttestationResponse = attestationResponse, OriginalOptions = originalOptions, IsCredentialIdUniqueToUserCallback = (args, ct) => Task.FromResult(true) }); var isUnique = await isCredentialIdUnique(makeResult.Id); if (!isUnique) { throw new InvalidOperationException("Credential id duplicate"); } return new StoredBiometricCredential { CredentialId = makeResult.Id, PublicKey = makeResult.PublicKey }; } /// /// Build the PublicKeyCredentialRequestOptions that the browser will /// hand to navigator.credentials.get(). Only the stored /// credential for this user is allowed. /// public AssertionOptions BuildRequestOptions(byte[] storedCredentialId) { var allowed = new List { new PublicKeyCredentialDescriptor(storedCredentialId) }; return _fido2.GetAssertionOptions(new GetAssertionOptionsParams { AllowedCredentials = allowed, UserVerification = UserVerificationRequirement.Preferred }); } /// /// Same as but returns a /// browser-ready JSON string directly. /// public string BuildRequestOptionsJson(byte[] storedCredentialId) { var options = _fido2.GetAssertionOptions(new GetAssertionOptionsParams { AllowedCredentials = new List { new PublicKeyCredentialDescriptor(storedCredentialId) }, UserVerification = UserVerificationRequirement.Preferred }); var result = new { challenge = options.Challenge, rpId = options.RpId, allowCredentials = new[] { new { id = MyBase64Url.Encode(storedCredentialId), type = "public-key", } }, userVerification = options.UserVerification?.ToString().ToLower() ?? "preferred", timeout = options.Timeout, }; return JsonConvert.SerializeObject(result); } /// /// Verify the assertion returned by the browser. Returns the new /// signature counter to persist. Throws on validation failure. /// public async Task FinishAssertionAsync( AuthenticatorAssertionRawResponse assertionResponse, AssertionOptions originalOptions, byte[] storedPublicKey, uint storedCounter) { var result = await _fido2.MakeAssertionAsync(new MakeAssertionParams { AssertionResponse = assertionResponse, OriginalOptions = originalOptions, StoredPublicKey = storedPublicKey, StoredSignatureCounter = storedCounter, IsUserHandleOwnerOfCredentialIdCallback = (args, ct) => Task.FromResult(true) }); uint newCounter = result.SignCount; if (newCounter <= storedCounter) { throw new InvalidOperationException( $"WebAuthn counter regression: stored={storedCounter}, new={newCounter}"); } return newCounter; } } }