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:
parent
16a4704fcf
commit
6a0d94739b
3 changed files with 26 additions and 4 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue