CredentialManager.cs 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. using System.Text;
  2. using System.Text.Json;
  3. using System.Text.Json.Nodes;
  4. using Fido2NetLib;
  5. using Fido2NetLib.Objects;
  6. namespace Passwordless;
  7. public class CredentialManager
  8. {
  9. private readonly IFido2 _fido2;
  10. private readonly OptionsCache _optionsCache;
  11. private readonly JsonSerializerOptions _jsonOptions;
  12. public CredentialManager(IFido2 fido2, OptionsCache optionsCache)
  13. {
  14. _fido2 = fido2;
  15. _optionsCache = optionsCache;
  16. _jsonOptions = new JsonSerializerOptions()
  17. {
  18. WriteIndented = true,
  19. };
  20. }
  21. public CredentialCreateOptions BuildCredentialOptions(string login)
  22. {
  23. var loginDisplay = Encoding.UTF8.GetString(Base64UrlConverter.Decode(login));
  24. var loginName = NameTransform.ToFileName(loginDisplay);
  25. var user = new Fido2User
  26. {
  27. DisplayName = loginDisplay,
  28. Id = Base64UrlConverter.Decode(login),
  29. Name = loginName,
  30. };
  31. var authenticatorSelection = new AuthenticatorSelection
  32. {
  33. UserVerification = UserVerificationRequirement.Discouraged,
  34. RequireResidentKey = false,
  35. };
  36. var extensions = new AuthenticationExtensionsClientInputs
  37. {
  38. Extensions = true,
  39. UserVerificationMethod = false,
  40. };
  41. var options = _fido2.RequestNewCredential(user, new List<PublicKeyCredentialDescriptor>(), authenticatorSelection, AttestationConveyancePreference.None, extensions);
  42. _optionsCache.Set(options.User.Name, options);
  43. return options;
  44. }
  45. public async Task<Fido2.CredentialMakeResult> RegisterCredential(string login, AuthenticatorAttestationRawResponse attestationResponse)
  46. {
  47. // 2. Create callback so that lib can verify credential id is unique to this user
  48. static Task<bool> Callback(IsCredentialIdUniqueToUserParams args, CancellationToken cancellationToken)
  49. {
  50. return Task.FromResult(!File.Exists($"./data/{args.User.Name}.json"));
  51. }
  52. var loginDisplay = Encoding.UTF8.GetString(Base64UrlConverter.Decode(login));
  53. var loginName = NameTransform.ToFileName(loginDisplay);
  54. var options = _optionsCache.Get<CredentialCreateOptions>(loginName);
  55. var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, Callback);
  56. if (success.Status == "ok")
  57. {
  58. await using var fileStream = File.OpenWrite($"./data/{success.Result!.User.Name}.json");
  59. await JsonSerializer.SerializeAsync(fileStream, success.Result, _jsonOptions);
  60. }
  61. return success;
  62. }
  63. public async Task<AssertionOptions> BuildAssertionOptions(string login)
  64. {
  65. var loginDisplay = Encoding.UTF8.GetString(Base64UrlConverter.Decode(login));
  66. var loginName = NameTransform.ToFileName(loginDisplay);
  67. byte[] credentialId;
  68. if (File.Exists($"./data/{loginName}.json"))
  69. {
  70. await using var fileStream = File.OpenRead($"./data/{loginName}.json");
  71. var accountInfo =
  72. (JsonObject)(await JsonSerializer.DeserializeAsync(fileStream, typeof(JsonObject), _jsonOptions))!;
  73. credentialId = Convert.FromBase64String(accountInfo["CredentialId"]?.GetValue<string>());
  74. }
  75. else
  76. {
  77. throw new ArgumentException("Username was not registered");
  78. }
  79. var extensions = new AuthenticationExtensionsClientInputs()
  80. {
  81. Extensions = true,
  82. UserVerificationMethod = false,
  83. };
  84. var descriptor = new PublicKeyCredentialDescriptor()
  85. {
  86. Id = credentialId,
  87. Type = PublicKeyCredentialType.PublicKey,
  88. };
  89. var options = _fido2.GetAssertionOptions(
  90. new[] { descriptor },
  91. UserVerificationRequirement.Discouraged,
  92. extensions
  93. );
  94. _optionsCache.Set(Convert.ToBase64String(credentialId), new OptionsWithName<AssertionOptions>(loginName, options));
  95. return options;
  96. }
  97. public async Task<AssertionVerificationResult> VerifyCredential(AuthenticatorAssertionRawResponse assertionResponse)
  98. {
  99. // 1. Get the assertion options we sent the client
  100. var optionsWithName = _optionsCache.Get<OptionsWithName<AssertionOptions>>(Convert.ToBase64String(assertionResponse.Id));
  101. AttestationVerificationSuccess assertionVerification;
  102. if (File.Exists($"./data/{optionsWithName.Name}.json"))
  103. {
  104. await using var fileStream = File.OpenRead($"./data/{optionsWithName.Name}.json");
  105. assertionVerification =
  106. (AttestationVerificationSuccess)(await JsonSerializer.DeserializeAsync(fileStream,
  107. typeof(AttestationVerificationSuccess), _jsonOptions))!;
  108. }
  109. else
  110. {
  111. throw new ArgumentException("Username was not registered");
  112. }
  113. // 3. Get credential counter from database
  114. var storedCounter = assertionVerification.Counter;
  115. // 4. Create callback to check if the user handle owns the credentialId
  116. Task<bool> Callback(IsUserHandleOwnerOfCredentialIdParams args, CancellationToken cancellationToken)
  117. {
  118. return Task.FromResult(assertionVerification.CredentialId.SequenceEqual(args.CredentialId));
  119. }
  120. // 5. Make the assertion
  121. var result = await _fido2.MakeAssertionAsync(assertionResponse, optionsWithName.Options, assertionVerification.PublicKey, storedCounter, Callback);
  122. if (result.Status == "ok")
  123. {
  124. assertionVerification.Counter = result.Counter;
  125. await using var fileStream = File.OpenWrite($"./data/{optionsWithName.Name}.json");
  126. await JsonSerializer.SerializeAsync(fileStream, assertionVerification, _jsonOptions);
  127. }
  128. // TODO: ???
  129. // if (result.DevicePublicKey is not null)
  130. // creds.DevicePublicKeys.Add(result.DevicePublicKey);
  131. // 7. return OK to client
  132. return result;
  133. }
  134. }