wip passkey registration

This commit is contained in:
2026-04-11 18:56:04 -04:00
parent 4325280020
commit 7a43d5c11f
6 changed files with 70 additions and 13 deletions
+4 -1
View File
@@ -1,4 +1,6 @@
using Microsoft.IdentityModel.Tokens; using Fido2NetLib.Objects;
using Microsoft.IdentityModel.Tokens;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
@@ -19,6 +21,7 @@ public class User
public bool IsArgon { get; set; } public bool IsArgon { get; set; }
public ObjectId[] ApiKeys { get; set; } = []; public ObjectId[] ApiKeys { get; set; } = [];
public List<ObjectId> RegTokens { get; set; } = []; public List<ObjectId> RegTokens { get; set; } = [];
public List<PublicKeyCredentialDescriptor> CredentialDescriptors { get; set; } = [];
public ClaimsIdentity GetIdentity() public ClaimsIdentity GetIdentity()
{ {
+10 -3
View File
@@ -1,5 +1,7 @@
using AobaCore.Models; using AobaCore.Models;
using Fido2NetLib.Objects;
using Isopoh.Cryptography.Argon2; using Isopoh.Cryptography.Argon2;
using MongoDB.Bson; using MongoDB.Bson;
@@ -54,13 +56,18 @@ public class AccountsService(IMongoDatabase db)
/* Get the salt */ /* Get the salt */
byte[] salt = new byte[16]; byte[] salt = new byte[16];
Array.Copy(hashBytes, 0, salt, 0, 16); Array.Copy(hashBytes, 0, salt, 0, 16);
/* Compute the hash on the password the user entered */
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 10000, HashAlgorithmName.SHA1); var hash= Rfc2898DeriveBytes.Pbkdf2(password, salt, 10000, HashAlgorithmName.SHA1, 20);
byte[] hash = pbkdf2.GetBytes(20);
/* Compare the results */ /* Compare the results */
for (int i = 0; i < 20; i++) for (int i = 0; i < 20; i++)
if (hashBytes[i + 16] != hash[i]) if (hashBytes[i + 16] != hash[i])
return false; return false;
return true; return true;
} }
public async Task<List<PublicKeyCredentialDescriptor>> GetPublicKeyCredentialDescriptorsAsync(ObjectId id, CancellationToken cancellationToken = default)
{
var creds = await _users.Find(u => u.Id == id).Project(u => u.CredentialDescriptors).FirstOrDefaultAsync(cancellationToken);
return creds ?? [];
}
} }
+10
View File
@@ -121,6 +121,16 @@ builder.Services.AddAuthentication(options =>
builder.Services.AddAoba(); builder.Services.AddAoba();
builder.Services.AddFido2(opts =>
{
opts.ServerName = "Aoba";
opts.ServerDomain = "aoba.app";
#if DEBUG
opts.Origins = new HashSet<string> { "http://localhost:8081", "http://127.0.0.1:8080" };
#else
opts.Origins = new HashSet<string> { "https://aoba.app" };
#endif
});
#if DEBUG #if DEBUG
builder.Services.AddHostedService<DebugService>(); builder.Services.AddHostedService<DebugService>();
#endif #endif
+2 -2
View File
@@ -7,7 +7,7 @@ import "google/protobuf/empty.proto";
import "Proto/Types.proto"; import "Proto/Types.proto";
service AccountRpc { service AccountRpc {
rpc RegisterPasskey(google.protobuf.Empty) returns (PasskeyRegistrationCreds); rpc RegisterPasskey(google.protobuf.Empty) returns (PasskeyCredentialCreateOptions);
rpc CompletePasskeyRegistration(PasskeyPublicKey) returns (google.protobuf.Empty); rpc CompletePasskeyRegistration(PasskeyRegistrationCredentials) returns (google.protobuf.Empty);
} }
+14 -4
View File
@@ -121,9 +121,19 @@ message PasskeyPayload {
} }
message PasskeyRegistrationCreds{ message PasskeyCredentialCreateOptions{
string challenge = 1;
string userId = 2;
} }
message PasskeyPublicKey{ message PasskeyRegistrationCredentials{
string id = 1;
string rawId = 2;
string type = 3;
CredentialsClientResponse response = 4;
}
message CredentialsClientResponse{
string clientDataJSON = 1;
string attestationObject = 2;
string authenticatorData = 3;
} }
+31 -4
View File
@@ -1,20 +1,47 @@
using Aoba.RPC; using Aoba.RPC;
using Aoba.RPC.Account; using Aoba.RPC.Account;
using AobaCore.Services;
using AobaServer.Utils;
using Fido2NetLib;
using Google.Protobuf.WellKnownTypes; using Google.Protobuf.WellKnownTypes;
using Grpc.Core; using Grpc.Core;
using Isopoh.Cryptography.Argon2;
namespace AobaServer.Services; namespace AobaServer.Services;
public class AccountRpcService : AccountRpc.AccountRpcBase public class AccountRpcService(IFido2 fido2, AccountsService accounts) : AccountRpc.AccountRpcBase
{ {
public override Task<PasskeyRegistrationCreds> RegisterPasskey(Empty request, ServerCallContext context) public override async Task<PasskeyCredentialCreateOptions> RegisterPasskey(Empty request, ServerCallContext context)
{ {
return base.RegisterPasskey(request, context); var curUser = await accounts.GetUserAsync(context.GetUserId(), context.CancellationToken);
if (curUser == null)
throw new Exception($"Logged in user does not exist somehow. Id: {context.GetUserId()}");
var user = new Fido2User
{
DisplayName = curUser.Username,
Id = curUser.Id.ToByteArray(),
Name = curUser.Username
};
var credOptions = fido2.RequestNewCredential(new RequestNewCredentialParams
{
User = user,
ExcludeCredentials = curUser.CredentialDescriptors
});
return new PasskeyCredentialCreateOptions
{
Challenge = credOptions.Challenge.ToB64String().Replace('+', '-').Replace('/', '_'),
UserId = credOptions.User.Id.ToB64String().Replace('+', '-').Replace('/', '_')
};
} }
public override Task<Empty> CompletePasskeyRegistration(PasskeyPublicKey request, ServerCallContext context) public override Task<Empty> CompletePasskeyRegistration(PasskeyRegistrationCredentials request, ServerCallContext context)
{ {
return base.CompletePasskeyRegistration(request, context); return base.CompletePasskeyRegistration(request, context);
} }