|
@@ -1,5 +1,6 @@
|
|
|
using System.Text;
|
|
using System.Text;
|
|
|
using System.Text.Json;
|
|
using System.Text.Json;
|
|
|
|
|
+using System.Text.Json.Nodes;
|
|
|
using Fido2NetLib;
|
|
using Fido2NetLib;
|
|
|
using Fido2NetLib.Objects;
|
|
using Fido2NetLib.Objects;
|
|
|
|
|
|
|
@@ -47,7 +48,7 @@ public class CredentialManager
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
var options = _fido2.RequestNewCredential(user, new List<PublicKeyCredentialDescriptor>(), authenticatorSelection, AttestationConveyancePreference.None, extensions);
|
|
var options = _fido2.RequestNewCredential(user, new List<PublicKeyCredentialDescriptor>(), authenticatorSelection, AttestationConveyancePreference.None, extensions);
|
|
|
- _optionsCache.Set(options);
|
|
|
|
|
|
|
+ _optionsCache.Set(options.User.Name, options);
|
|
|
|
|
|
|
|
return options;
|
|
return options;
|
|
|
}
|
|
}
|
|
@@ -57,12 +58,12 @@ public class CredentialManager
|
|
|
// 2. Create callback so that lib can verify credential id is unique to this user
|
|
// 2. Create callback so that lib can verify credential id is unique to this user
|
|
|
static Task<bool> Callback(IsCredentialIdUniqueToUserParams args, CancellationToken cancellationToken)
|
|
static Task<bool> Callback(IsCredentialIdUniqueToUserParams args, CancellationToken cancellationToken)
|
|
|
{
|
|
{
|
|
|
- return Task.FromResult(!File.Exists($"./data/{args.User.Name}"));
|
|
|
|
|
|
|
+ return Task.FromResult(!File.Exists($"./data/{args.User.Name}.json"));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
var loginDisplay = Encoding.UTF8.GetString(Base64UrlConverter.Decode(login));
|
|
var loginDisplay = Encoding.UTF8.GetString(Base64UrlConverter.Decode(login));
|
|
|
var loginName = NameTransform.ToFileName(loginDisplay);
|
|
var loginName = NameTransform.ToFileName(loginDisplay);
|
|
|
- var options = _optionsCache.Get(loginName);
|
|
|
|
|
|
|
+ var options = _optionsCache.Get<CredentialCreateOptions>(loginName);
|
|
|
|
|
|
|
|
var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, Callback);
|
|
var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, Callback);
|
|
|
|
|
|
|
@@ -74,4 +75,90 @@ public class CredentialManager
|
|
|
|
|
|
|
|
return success;
|
|
return success;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ public async Task<AssertionOptions> 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<string>());
|
|
|
|
|
+ }
|
|
|
|
|
+ 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<AssertionOptions>(loginName, options));
|
|
|
|
|
+ return options;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public async Task<AssertionVerificationResult> VerifyCredential(AuthenticatorAssertionRawResponse assertionResponse)
|
|
|
|
|
+ {
|
|
|
|
|
+ // 1. Get the assertion options we sent the client
|
|
|
|
|
+ var optionsWithName = _optionsCache.Get<OptionsWithName<AssertionOptions>>(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<bool> 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;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|