| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164 |
- 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<PublicKeyCredentialDescriptor>(), authenticatorSelection, AttestationConveyancePreference.None, extensions);
- _optionsCache.Set(options.User.Name, options);
-
- return options;
- }
- public async Task<Fido2.CredentialMakeResult> RegisterCredential(string login, AuthenticatorAttestationRawResponse attestationResponse)
- {
- // 2. Create callback so that lib can verify credential id is unique to this user
- static Task<bool> 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<CredentialCreateOptions>(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<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;
- }
- }
|