feat(auth): allow naming the initial passkey during registration

- Add "Passkey Name" field to the registration form
- Pass passkey_name through start/finish WebAuthn ceremony
- Use user-provided name instead of hardcoded "default"
This commit is contained in:
Jon Seager 2026-02-05 11:24:52 +00:00
parent 16a4704fcf
commit 6a0d94739b
No known key found for this signature in database
3 changed files with 26 additions and 4 deletions

View file

@ -47,6 +47,7 @@ struct CliCallbackTemplate {
pub struct RegisterStartRequest { pub struct RegisterStartRequest {
pub token: String, pub token: String,
pub display_name: String, pub display_name: String,
pub passkey_name: String,
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -58,6 +59,7 @@ pub struct ChallengeResponse<T: Serialize> {
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct RegisterFinishRequest { pub struct RegisterFinishRequest {
pub challenge_id: String, pub challenge_id: String,
pub passkey_name: String,
pub credential: RegisterPublicKeyCredential, pub credential: RegisterPublicKeyCredential,
} }
@ -211,7 +213,7 @@ pub(crate) async fn register_finish(
// Store the credential // Store the credential
let credential_json = let credential_json =
serde_json::to_string(&passkey).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; serde_json::to_string(&passkey).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let new_credential = NewPasskeyCredential::new(user_id, credential_json, "default".to_string()); let new_credential = NewPasskeyCredential::new(user_id, credential_json, payload.passkey_name);
state state
.passkey_repo .passkey_repo
.insert(new_credential) .insert(new_credential)

View file

@ -30,6 +30,17 @@
/> />
</label> </label>
<label class="flex flex-col gap-1 text-sm">
<span class="text-stone-700">Passkey Name</span>
<input
type="text"
id="passkey-name"
required
class="input-field"
placeholder="e.g. MacBook Touch ID, iPhone"
/>
</label>
<button <button
id="register-button" id="register-button"
type="button" type="button"
@ -63,6 +74,7 @@
const token = "{{ token }}"; const token = "{{ token }}";
const button = document.getElementById("register-button"); const button = document.getElementById("register-button");
const displayNameInput = document.getElementById("display-name"); const displayNameInput = document.getElementById("display-name");
const passkeyNameInput = document.getElementById("passkey-name");
const errorDiv = document.getElementById("register-error"); const errorDiv = document.getElementById("register-error");
const loadingDiv = document.getElementById("register-loading"); const loadingDiv = document.getElementById("register-loading");
const unsupportedDiv = document.getElementById("register-unsupported"); const unsupportedDiv = document.getElementById("register-unsupported");
@ -84,12 +96,19 @@
return; return;
} }
const passkeyName = passkeyNameInput.value.trim();
if (!passkeyName) {
errorDiv.textContent = "Please enter a name for this passkey.";
errorDiv.classList.remove("hidden");
return;
}
errorDiv.classList.add("hidden"); errorDiv.classList.add("hidden");
loadingDiv.classList.remove("hidden"); loadingDiv.classList.remove("hidden");
button.disabled = true; button.disabled = true;
try { try {
await startPasskeyRegistration(token, displayName); await startPasskeyRegistration(token, displayName, passkeyName);
formDiv.classList.add("hidden"); formDiv.classList.add("hidden");
successDiv.classList.remove("hidden"); successDiv.classList.remove("hidden");
} catch (err) { } catch (err) {

View file

@ -73,12 +73,12 @@ function serializeAuthenticationCredential(credential) {
} }
// Start passkey registration ceremony // Start passkey registration ceremony
async function startPasskeyRegistration(token, displayName) { async function startPasskeyRegistration(token, displayName, passkeyName) {
// 1. Get challenge from server // 1. Get challenge from server
const startResponse = await fetch("/api/v1/webauthn/register/start", { const startResponse = await fetch("/api/v1/webauthn/register/start", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token, display_name: displayName }), body: JSON.stringify({ token, display_name: displayName, passkey_name: passkeyName }),
}); });
if (!startResponse.ok) { if (!startResponse.ok) {
@ -100,6 +100,7 @@ async function startPasskeyRegistration(token, displayName) {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
challenge_id, challenge_id,
passkey_name: passkeyName,
credential: serializeRegistrationCredential(credential), credential: serializeRegistrationCredential(credential),
}), }),
}); });