using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using Fido2NetLib; using Fido2NetLib.Objects; namespace Passwordless; public class CredentialManager { private readonly IFido2 _fido2; private readonly OptionsCache _optionsCache; private readonly JsonSerializerOptions _jsonOptions; public CredentialManager(IFido2 fido2, OptionsCache optionsCache) { _fido2 = fido2; _optionsCache = optionsCache; _jsonOptions = new JsonSerializerOptions() { WriteIndented = true, }; } public CredentialCreateOptions BuildCredentialOptions(string login) { var loginDisplay = Encoding.UTF8.GetString(Base64UrlConverter.Decode(login)); var loginName = NameTransform.ToFileName(loginDisplay); var user = new Fido2User { DisplayName = loginDisplay, Id = Base64UrlConverter.Decode(login), Name = loginName, }; var authenticatorSelection = new AuthenticatorSelection { UserVerification = UserVerificationRequirement.Discouraged, RequireResidentKey = false, }; var extensions = new AuthenticationExtensionsClientInputs { Extensions = true, UserVerificationMethod = false, }; var options = _fido2.RequestNewCredential(user, new List(), authenticatorSelection, AttestationConveyancePreference.None, extensions); _optionsCache.Set(options.User.Name, options); return options; } public async Task RegisterCredential(string login, AuthenticatorAttestationRawResponse attestationResponse) { // 2. Create callback so that lib can verify credential id is unique to this user static Task Callback(IsCredentialIdUniqueToUserParams args, CancellationToken cancellationToken) { return Task.FromResult(!File.Exists($"./data/{args.User.Name}.json")); } var loginDisplay = Encoding.UTF8.GetString(Base64UrlConverter.Decode(login)); var loginName = NameTransform.ToFileName(loginDisplay); var options = _optionsCache.Get(loginName); var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, Callback); if (success.Status == "ok") { await using var fileStream = File.OpenWrite($"./data/{success.Result!.User.Name}.json"); await JsonSerializer.SerializeAsync(fileStream, success.Result, _jsonOptions); } return success; } public async Task BuildAssertionOptions(string login) { var loginDisplay = Encoding.UTF8.GetString(Base64UrlConverter.Decode(login)); var loginName = NameTransform.ToFileName(loginDisplay); byte[] credentialId; if (File.Exists($"./data/{loginName}.json")) { await using var fileStream = File.OpenRead($"./data/{loginName}.json"); var accountInfo = (JsonObject)(await JsonSerializer.DeserializeAsync(fileStream, typeof(JsonObject), _jsonOptions))!; credentialId = Convert.FromBase64String(accountInfo["CredentialId"]?.GetValue()); } else { throw new ArgumentException("Username was not registered"); } var extensions = new AuthenticationExtensionsClientInputs() { Extensions = true, UserVerificationMethod = false, }; var descriptor = new PublicKeyCredentialDescriptor() { Id = credentialId, Type = PublicKeyCredentialType.PublicKey, }; var options = _fido2.GetAssertionOptions( new[] { descriptor }, UserVerificationRequirement.Discouraged, extensions ); _optionsCache.Set(Convert.ToBase64String(credentialId), new OptionsWithName(loginName, options)); return options; } public async Task VerifyCredential(AuthenticatorAssertionRawResponse assertionResponse) { // 1. Get the assertion options we sent the client var optionsWithName = _optionsCache.Get>(Convert.ToBase64String(assertionResponse.Id)); AttestationVerificationSuccess assertionVerification; if (File.Exists($"./data/{optionsWithName.Name}.json")) { await using var fileStream = File.OpenRead($"./data/{optionsWithName.Name}.json"); assertionVerification = (AttestationVerificationSuccess)(await JsonSerializer.DeserializeAsync(fileStream, typeof(AttestationVerificationSuccess), _jsonOptions))!; } else { throw new ArgumentException("Username was not registered"); } // 3. Get credential counter from database var storedCounter = assertionVerification.Counter; // 4. Create callback to check if the user handle owns the credentialId Task Callback(IsUserHandleOwnerOfCredentialIdParams args, CancellationToken cancellationToken) { return Task.FromResult(assertionVerification.CredentialId.SequenceEqual(args.CredentialId)); } // 5. Make the assertion var result = await _fido2.MakeAssertionAsync(assertionResponse, optionsWithName.Options, assertionVerification.PublicKey, storedCounter, Callback); if (result.Status == "ok") { assertionVerification.Counter = result.Counter; await using var fileStream = File.OpenWrite($"./data/{optionsWithName.Name}.json"); await JsonSerializer.SerializeAsync(fileStream, assertionVerification, _jsonOptions); } // TODO: ??? // if (result.DevicePublicKey is not null) // creds.DevicePublicKeys.Add(result.DevicePublicKey); // 7. return OK to client return result; } }