wip passkey registration day 2

This commit is contained in:
2026-04-12 16:59:40 -04:00
parent 6c2305cac9
commit 8ff4fa74e4
8 changed files with 192 additions and 45 deletions
+18 -18
View File
@@ -24,8 +24,10 @@ dependencies = [
"dioxus", "dioxus",
"dioxus-primitives", "dioxus-primitives",
"dotenv", "dotenv",
"js-sys",
"prost", "prost",
"serde", "serde",
"serde-wasm-bindgen",
"serde_repr", "serde_repr",
"tonic", "tonic",
"tonic-build", "tonic-build",
@@ -1669,10 +1671,12 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.91" version = "0.3.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
dependencies = [ dependencies = [
"cfg-if",
"futures-util",
"once_cell", "once_cell",
"wasm-bindgen", "wasm-bindgen",
] ]
@@ -3340,9 +3344,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.114" version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -3353,23 +3357,19 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.64" version = "0.4.68"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8"
dependencies = [ dependencies = [
"cfg-if",
"futures-util",
"js-sys", "js-sys",
"once_cell",
"wasm-bindgen", "wasm-bindgen",
"web-sys",
] ]
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.114" version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -3377,9 +3377,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.114" version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@@ -3390,9 +3390,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.114" version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -3412,9 +3412,9 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.91" version = "0.3.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
+4 -2
View File
@@ -7,7 +7,7 @@ edition = "2024"
[dependencies] [dependencies]
dioxus = { version = "0.7.5", features = ["router"] } dioxus = { version = "0.7.5", features = ["router"] }
serde = "1.0.228" serde = { version = "1.0.228", features = ["derive"] }
serde_repr = "0.1.20" serde_repr = "0.1.20"
tonic = { version = "*", default-features = false, features = [ tonic = { version = "*", default-features = false, features = [
"codegen", "codegen",
@@ -15,8 +15,10 @@ tonic = { version = "*", default-features = false, features = [
] } ] }
prost = "0.13" prost = "0.13"
tonic-web-wasm-client = "0.7" tonic-web-wasm-client = "0.7"
web-sys = { version = "0.3.91", features = ["Storage", "Window", "Navigator", "CredentialsContainer", "CredentialCreationOptions"] } web-sys = { version = "0.3.91", features = ["Storage", "Window", "Navigator", "CredentialsContainer", "CredentialCreationOptions", "AttestationConveyancePreference", "PublicKeyCredentialCreationOptions", "PublicKeyCredentialRpEntity", "PublicKeyCredentialUserEntity"] }
dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false } dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false }
serde-wasm-bindgen = "0.6.5"
js-sys = "0.3.95"
[build-dependencies] [build-dependencies]
tonic-build = { version = "*", default-features = false, features = ["prost"] } tonic-build = { version = "*", default-features = false, features = ["prost"] }
+9 -4
View File
@@ -3,13 +3,15 @@ use std::env;
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>>
{
tonic_build::configure() tonic_build::configure()
.build_server(false) .build_server(false)
.build_client(true) .build_client(true)
.compile_protos( .compile_protos(
&[ &[
"../AobaServer/Proto/Aoba.proto", "../AobaServer/Proto/Aoba.proto",
"../AobaServer/Proto/Account.proto",
"../AobaServer/Proto/Auth.proto", "../AobaServer/Proto/Auth.proto",
"../AobaServer/Proto/Metrics.proto", "../AobaServer/Proto/Metrics.proto",
"../AobaServer/Proto/Types.proto", "../AobaServer/Proto/Types.proto",
@@ -20,15 +22,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(()) Ok(())
} }
fn forward_env() { fn forward_env()
{
let dest_path = "./src/env.rs"; let dest_path = "./src/env.rs";
let mut f = File::create(&dest_path).unwrap(); let mut f = File::create(&dest_path).unwrap();
f.write_all(b"// This file is automatically generated by build.rs\n\n") f.write_all(b"// This file is automatically generated by build.rs\n\n")
.unwrap(); .unwrap();
dotenv().ok(); dotenv().ok();
for (key, value) in env::vars() { for (key, value) in env::vars()
if key.starts_with("APP_") { {
if key.starts_with("APP_")
{
f.write_all("#[allow(dead_code)]\n".as_bytes()).unwrap(); f.write_all("#[allow(dead_code)]\n".as_bytes()).unwrap();
let line = format!( let line = format!(
"pub const {}: &'static str = \"{}\";\n", "pub const {}: &'static str = \"{}\";\n",
+50 -12
View File
@@ -1,10 +1,15 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use web_sys::{CredentialCreationOptions, window}; use js_sys::{Uint8Array, wasm_bindgen::JsValue};
use web_sys::{
CredentialCreationOptions, PublicKeyCredentialCreationOptions, PublicKeyCredentialRpEntity,
PublicKeyCredentialUserEntity, window,
};
use crate::components::basic::Button; use crate::{components::basic::Button, rpc::aoba::PasskeyCredentialCreateOptions};
#[component] #[component]
pub fn PasskeyRegistrationButton() -> Element { pub fn PasskeyRegistrationButton() -> Element
{
rsx! { rsx! {
Button{ Button{
text: "Register Passkey", text: "Register Passkey",
@@ -15,22 +20,55 @@ pub fn PasskeyRegistrationButton() -> Element {
} }
} }
fn start_passkey_registration() { fn start_passkey_registration()
create_credential(); {
create_credential(todo!());
} }
fn create_credential() { fn create_credential(req_opts: PasskeyCredentialCreateOptions)
let credentials = window() {
.expect("Failed to get window") let window = window().expect("Window does not exist");
.navigator() let credentaials = window.navigator().credentials();
.credentials();
let opts = opts_from_rpc(req_opts);
let result = credentaials.create_with_options(&opts);
todo!()
}
fn opts_from_rpc(rpc_opts: PasskeyCredentialCreateOptions) -> CredentialCreationOptions
{
let opt_user = &rpc_opts.user.expect("user is missing");
let opt_rp = &rpc_opts.rp.expect("rp is missing");
let opts = CredentialCreationOptions::new(); let opts = CredentialCreationOptions::new();
let _result = credentials.create_with_options(&opts); let rp = PublicKeyCredentialRpEntity::new(&opt_rp.name);
rp.set_id(&opt_rp.id);
let user = PublicKeyCredentialUserEntity::new_with_u8_array(
&opt_user.name,
&opt_user.display_name,
&to_u8_array(&opt_user.id),
);
let pub_key_opts = PublicKeyCredentialCreationOptions::new_with_u8_array(
&to_u8_array(&rpc_opts.challenge),
&JsValue::undefined(),
&rp,
&user,
);
//pub_key_opts.set_exclude_credentials(val);
opts.set_public_key(&pub_key_opts);
return opts;
}
fn to_u8_array(value: &String) -> Uint8Array
{
todo!()
} }
#[component] #[component]
pub fn PasskeyLoginButton() -> Element { pub fn PasskeyLoginButton() -> Element
{
rsx! { rsx! {
Button{ Button{
text: "Login with Passkey" text: "Login with Passkey"
+18 -1
View File
@@ -6,7 +6,9 @@ use tonic_web_wasm_client::Client;
use crate::{ use crate::{
RPC_HOST, RPC_HOST,
rpc::aoba::{auth_rpc_client::AuthRpcClient, metrics_rpc_client::MetricsRpcClient}, rpc::aoba::{
account_rpc_client::AccountRpcClient, auth_rpc_client::AuthRpcClient, metrics_rpc_client::MetricsRpcClient,
},
}; };
pub mod aoba pub mod aoba
@@ -17,6 +19,7 @@ pub mod aoba
static RPC_CLIENT: RpcConnection = RpcConnection { static RPC_CLIENT: RpcConnection = RpcConnection {
aoba: RwLock::new(None), aoba: RwLock::new(None),
auth: RwLock::new(None), auth: RwLock::new(None),
account: RwLock::new(None),
metrics: RwLock::new(None), metrics: RwLock::new(None),
jwt: RwLock::new(None), jwt: RwLock::new(None),
}; };
@@ -26,6 +29,7 @@ pub struct RpcConnection
{ {
aoba: RwLock<Option<AobaRpcClient<InterceptedService<Client, AuthInterceptor>>>>, aoba: RwLock<Option<AobaRpcClient<InterceptedService<Client, AuthInterceptor>>>>,
auth: RwLock<Option<AuthRpcClient<Client>>>, auth: RwLock<Option<AuthRpcClient<Client>>>,
account: RwLock<Option<AccountRpcClient<InterceptedService<Client, AuthInterceptor>>>>,
metrics: RwLock<Option<MetricsRpcClient<InterceptedService<Client, AuthInterceptor>>>>, metrics: RwLock<Option<MetricsRpcClient<InterceptedService<Client, AuthInterceptor>>>>,
jwt: RwLock<Option<String>>, jwt: RwLock<Option<String>>,
} }
@@ -38,6 +42,12 @@ impl RpcConnection
return self.aoba.read().unwrap().clone().unwrap(); return self.aoba.read().unwrap().clone().unwrap();
} }
pub fn get_account_client(&self) -> AccountRpcClient<InterceptedService<Client, AuthInterceptor>>
{
self.ensure_client();
return self.account.read().unwrap().clone().unwrap();
}
pub fn get_auth_client(&self) -> AuthRpcClient<Client> pub fn get_auth_client(&self) -> AuthRpcClient<Client>
{ {
self.ensure_client(); self.ensure_client();
@@ -58,6 +68,8 @@ impl RpcConnection
let aoba_client = AobaRpcClient::with_interceptor(wasm_client.clone(), AuthInterceptor); let aoba_client = AobaRpcClient::with_interceptor(wasm_client.clone(), AuthInterceptor);
*self.aoba.write().unwrap() = Some(aoba_client); *self.aoba.write().unwrap() = Some(aoba_client);
*self.auth.write().unwrap() = Some(AuthRpcClient::new(wasm_client.clone())); *self.auth.write().unwrap() = Some(AuthRpcClient::new(wasm_client.clone()));
*self.account.write().unwrap() =
Some(AccountRpcClient::with_interceptor(wasm_client.clone(), AuthInterceptor));
*self.metrics.write().unwrap() = *self.metrics.write().unwrap() =
Some(MetricsRpcClient::with_interceptor(wasm_client.clone(), AuthInterceptor)); Some(MetricsRpcClient::with_interceptor(wasm_client.clone(), AuthInterceptor));
} }
@@ -90,6 +102,11 @@ pub fn get_auth_rpc_client() -> AuthRpcClient<Client>
return RPC_CLIENT.get_auth_client(); return RPC_CLIENT.get_auth_client();
} }
pub fn get_account_rpc_client() -> AccountRpcClient<InterceptedService<Client, AuthInterceptor>>
{
return RPC_CLIENT.get_account_client();
}
pub fn get_metrics_rpc_client() -> MetricsRpcClient<InterceptedService<Client, AuthInterceptor>> pub fn get_metrics_rpc_client() -> MetricsRpcClient<InterceptedService<Client, AuthInterceptor>>
{ {
return RPC_CLIENT.get_metrics_client(); return RPC_CLIENT.get_metrics_client();
+28 -1
View File
@@ -123,8 +123,28 @@ message PasskeyPayload {
message PasskeyCredentialCreateOptions{ message PasskeyCredentialCreateOptions{
string challenge = 1; string challenge = 1;
string userId = 2; PublicKeyCredentialUser user = 2;
PublicKeyCredentialRpEntity rp = 3;
repeated PubKeyCredParam pubKeyParams = 4;
} }
message PubKeyCredParam{
string alg = 1;
string type = 2;
}
message PublicKeyCredentialRpEntity{
string id = 1;
string icon = 2;
string name = 3;
}
message PublicKeyCredentialUser{
string id = 1;
string name = 2;
string displayName = 3;
}
message PasskeyRegistrationCredentials{ message PasskeyRegistrationCredentials{
string id = 1; string id = 1;
string rawId = 2; string rawId = 2;
@@ -137,3 +157,10 @@ message CredentialsClientResponse{
string attestationObject = 2; string attestationObject = 2;
string authenticatorData = 3; string authenticatorData = 3;
} }
message PublicKeyCredentialDescriptor{
string type = 1;
string id = 2;
repeated string transports = 3;
}
+9 -6
View File
@@ -32,13 +32,16 @@ public class AccountRpcService(IFido2 fido2, AccountsService accounts) : Account
var credOptions = fido2.RequestNewCredential(new RequestNewCredentialParams var credOptions = fido2.RequestNewCredential(new RequestNewCredentialParams
{ {
User = user, User = user,
ExcludeCredentials = curUser.CredentialDescriptors ExcludeCredentials = curUser.CredentialDescriptors,
AuthenticatorSelection = new AuthenticatorSelection
{
ResidentKey = Fido2NetLib.Objects.ResidentKeyRequirement.Required,
UserVerification = Fido2NetLib.Objects.UserVerificationRequirement.Preferred
}
}); });
return new PasskeyCredentialCreateOptions
{
Challenge = credOptions.Challenge.ToB64String().Replace('+', '-').Replace('/', '_'), return credOptions.ToRPC();
UserId = credOptions.User.Id.ToB64String().Replace('+', '-').Replace('/', '_')
};
} }
public override Task<Empty> CompletePasskeyRegistration(PasskeyRegistrationCredentials request, ServerCallContext context) public override Task<Empty> CompletePasskeyRegistration(PasskeyRegistrationCredentials request, ServerCallContext context)
+55
View File
@@ -0,0 +1,55 @@
using Aoba.RPC;
using Isopoh.Cryptography.Argon2;
namespace AobaServer.Utils;
public static class PasskeyExtensions
{
public static PublicKeyCredentialRpEntity ToRPC(this Fido2NetLib.PublicKeyCredentialRpEntity value)
{
return new PublicKeyCredentialRpEntity
{
Id = value.Id,
Icon = value.Icon,
Name = value.Name,
};
}
public static PublicKeyCredentialUser ToRPC(this Fido2NetLib.Fido2User value)
{
return new PublicKeyCredentialUser
{
Id = value.Id.ToB64String(),
DisplayName = value.DisplayName,
Name = value.Name,
};
}
public static PubKeyCredParam ToRPC(this Fido2NetLib.PubKeyCredParam value)
{
return new PubKeyCredParam
{
Alg = value.Alg.ToString(),
Type = value.Type.ToString(),
};
}
public static IEnumerable<PubKeyCredParam> ToRPC(this IEnumerable<Fido2NetLib.PubKeyCredParam> value)
{
return value.Select(x => x.ToRPC());
}
public static PasskeyCredentialCreateOptions ToRPC(this Fido2NetLib.CredentialCreateOptions value)
{
var opts = new PasskeyCredentialCreateOptions
{
Challenge = value.Challenge.ToB64String(),
Rp = value.Rp.ToRPC(),
User = value.User.ToRPC()
};
//todo: excluded credentials
opts.PubKeyParams.AddRange(value.PubKeyCredParams.ToRPC());
return opts;
}
}