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;
}
}
}