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 token: String,
pub display_name: String,
pub passkey_name: String,
}
#[derive(Serialize)]
@ -58,6 +59,7 @@ pub struct ChallengeResponse<T: Serialize> {
#[derive(Deserialize)]
pub struct RegisterFinishRequest {
pub challenge_id: String,
pub passkey_name: String,
pub credential: RegisterPublicKeyCredential,
}
@ -211,7 +213,7 @@ pub(crate) async fn register_finish(
// Store the credential
let credential_json =
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
.passkey_repo
.insert(new_credential)

View file

@ -30,6 +30,17 @@
/>
</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
id="register-button"
type="button"
@ -63,6 +74,7 @@
const token = "{{ token }}";
const button = document.getElementById("register-button");
const displayNameInput = document.getElementById("display-name");
const passkeyNameInput = document.getElementById("passkey-name");
const errorDiv = document.getElementById("register-error");
const loadingDiv = document.getElementById("register-loading");
const unsupportedDiv = document.getElementById("register-unsupported");
@ -84,12 +96,19 @@
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");
loadingDiv.classList.remove("hidden");
button.disabled = true;
try {
await startPasskeyRegistration(token, displayName);
await startPasskeyRegistration(token, displayName, passkeyName);
formDiv.classList.add("hidden");
successDiv.classList.remove("hidden");
} catch (err) {

View file

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