05/18 Enhanced codes and functionalities 11
This commit is contained in:
+14
-1
@@ -78,11 +78,24 @@ def register_begin():
|
|||||||
"""
|
"""
|
||||||
Generate PublicKeyCredentialCreationOptions for passkey registration.
|
Generate PublicKeyCredentialCreationOptions for passkey registration.
|
||||||
Requires an active JWT session (user must be logged in).
|
Requires an active JWT session (user must be logged in).
|
||||||
|
|
||||||
|
Optional JSON body:
|
||||||
|
attachment: "platform" (default) | "cross-platform"
|
||||||
|
"platform" → device biometrics (Touch ID, Face ID, Windows Hello)
|
||||||
|
"cross-platform" → roaming authenticators (YubiKey, phone-as-key via QR)
|
||||||
"""
|
"""
|
||||||
user = db.session.get(User, g.current_user_id)
|
user = db.session.get(User, g.current_user_id)
|
||||||
if not user:
|
if not user:
|
||||||
return jsonify({'error': 'User not found'}), 404
|
return jsonify({'error': 'User not found'}), 404
|
||||||
|
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
attachment_str = (data.get('attachment') or 'platform').strip().lower()
|
||||||
|
|
||||||
|
if attachment_str == 'cross-platform':
|
||||||
|
authenticator_attachment = AuthenticatorAttachment.CROSS_PLATFORM
|
||||||
|
else:
|
||||||
|
authenticator_attachment = AuthenticatorAttachment.PLATFORM
|
||||||
|
|
||||||
# Collect existing credential IDs to exclude (prevent re-registering same key).
|
# Collect existing credential IDs to exclude (prevent re-registering same key).
|
||||||
existing = WebAuthnCredential.query.filter_by(user_id=user.id).all()
|
existing = WebAuthnCredential.query.filter_by(user_id=user.id).all()
|
||||||
exclude_credentials = [
|
exclude_credentials = [
|
||||||
@@ -102,7 +115,7 @@ def register_begin():
|
|||||||
authenticator_selection=AuthenticatorSelectionCriteria(
|
authenticator_selection=AuthenticatorSelectionCriteria(
|
||||||
resident_key=ResidentKeyRequirement.PREFERRED,
|
resident_key=ResidentKeyRequirement.PREFERRED,
|
||||||
user_verification=UserVerificationRequirement.PREFERRED,
|
user_verification=UserVerificationRequirement.PREFERRED,
|
||||||
authenticator_attachment=AuthenticatorAttachment.PLATFORM,
|
authenticator_attachment=authenticator_attachment,
|
||||||
),
|
),
|
||||||
exclude_credentials=exclude_credentials,
|
exclude_credentials=exclude_credentials,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2550,3 +2550,21 @@ html.sidebar-open {
|
|||||||
.health-banner-dismiss:hover {
|
.health-banner-dismiss:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Passkey attachment type selector */
|
||||||
|
.passkey-attachment-select {
|
||||||
|
padding: 7px 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--surface);
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passkey-attachment-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(192, 57, 43, 0.15);
|
||||||
|
}
|
||||||
|
|||||||
@@ -523,9 +523,10 @@ const PasskeyAuth = (() => {
|
|||||||
/**
|
/**
|
||||||
* Register a new passkey for the currently logged-in user.
|
* Register a new passkey for the currently logged-in user.
|
||||||
* Requires an active access_token in sessionStorage (set by vault.js on login).
|
* Requires an active access_token in sessionStorage (set by vault.js on login).
|
||||||
* @param {string} name User-friendly name for the passkey (e.g. "iPhone 15").
|
* @param {string} name User-friendly name (e.g. "iPhone 15").
|
||||||
|
* @param {string} attachment "platform" (default) | "cross-platform"
|
||||||
*/
|
*/
|
||||||
async function registerPasskey(name) {
|
async function registerPasskey(name, attachment = "platform") {
|
||||||
if (!window.PublicKeyCredential) {
|
if (!window.PublicKeyCredential) {
|
||||||
return { error: 'Passkeys are not supported in this browser.' };
|
return { error: 'Passkeys are not supported in this browser.' };
|
||||||
}
|
}
|
||||||
@@ -540,6 +541,7 @@ const PasskeyAuth = (() => {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
|
body: JSON.stringify({ attachment }),
|
||||||
});
|
});
|
||||||
if (!beginRes.ok) {
|
if (!beginRes.ok) {
|
||||||
const d = await beginRes.json().catch(() => ({}));
|
const d = await beginRes.json().catch(() => ({}));
|
||||||
|
|||||||
+10
-2
@@ -2442,10 +2442,16 @@ const Vault = (() => {
|
|||||||
const lastUsed = c.last_used_at
|
const lastUsed = c.last_used_at
|
||||||
? `Last used ${new Date(c.last_used_at).toLocaleDateString()}`
|
? `Last used ${new Date(c.last_used_at).toLocaleDateString()}`
|
||||||
: "Never used";
|
: "Never used";
|
||||||
|
const transports = c.transports || [];
|
||||||
|
const typeLabel = transports.includes("internal")
|
||||||
|
? "📱 Device"
|
||||||
|
: transports.some((t) => ["usb", "nfc", "ble", "smart-card"].includes(t))
|
||||||
|
? "🔑 Security key"
|
||||||
|
: "🔑 Passkey";
|
||||||
return `<div class="passkey-item" data-cred-id="${c.id}">
|
return `<div class="passkey-item" data-cred-id="${c.id}">
|
||||||
<div class="passkey-info">
|
<div class="passkey-info">
|
||||||
<span class="passkey-name">${escHtml(c.name)}</span>
|
<span class="passkey-name">${escHtml(c.name)}</span>
|
||||||
<span class="passkey-meta">${lastUsed} · Added ${escHtml(created)}</span>
|
<span class="passkey-meta">${typeLabel} · ${lastUsed} · Added ${escHtml(created)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="passkey-actions">
|
<div class="passkey-actions">
|
||||||
<button class="btn-text btn-sm btn-rename-passkey" data-id="${c.id}" data-name="${escHtml(c.name)}">Rename</button>
|
<button class="btn-text btn-sm btn-rename-passkey" data-id="${c.id}" data-name="${escHtml(c.name)}">Rename</button>
|
||||||
@@ -2498,13 +2504,15 @@ const Vault = (() => {
|
|||||||
registerBtn.dataset.bound = "1";
|
registerBtn.dataset.bound = "1";
|
||||||
registerBtn.addEventListener("click", async () => {
|
registerBtn.addEventListener("click", async () => {
|
||||||
const nameInput = document.getElementById("passkey-name-input");
|
const nameInput = document.getElementById("passkey-name-input");
|
||||||
|
const attachSel = document.getElementById("passkey-attachment-select");
|
||||||
const name = (nameInput?.value || "").trim() || "Passkey";
|
const name = (nameInput?.value || "").trim() || "Passkey";
|
||||||
|
const attachment = attachSel?.value || "platform";
|
||||||
const errEl = document.getElementById("passkeys-error");
|
const errEl = document.getElementById("passkeys-error");
|
||||||
errEl?.classList.add("hidden");
|
errEl?.classList.add("hidden");
|
||||||
registerBtn.disabled = true;
|
registerBtn.disabled = true;
|
||||||
registerBtn.textContent = "Waiting…";
|
registerBtn.textContent = "Waiting…";
|
||||||
|
|
||||||
const result = await PasskeyAuth.registerPasskey(name);
|
const result = await PasskeyAuth.registerPasskey(name, attachment);
|
||||||
|
|
||||||
registerBtn.disabled = false;
|
registerBtn.disabled = false;
|
||||||
registerBtn.textContent = "+ Add passkey";
|
registerBtn.textContent = "+ Add passkey";
|
||||||
|
|||||||
@@ -977,6 +977,10 @@
|
|||||||
maxlength="128"
|
maxlength="128"
|
||||||
class="passkey-name-input"
|
class="passkey-name-input"
|
||||||
/>
|
/>
|
||||||
|
<select id="passkey-attachment-select" class="passkey-attachment-select">
|
||||||
|
<option value="platform">Device (biometric)</option>
|
||||||
|
<option value="cross-platform">Security key / cross-device</option>
|
||||||
|
</select>
|
||||||
<button class="btn-secondary" id="btn-register-passkey">
|
<button class="btn-secondary" id="btn-register-passkey">
|
||||||
+ Add passkey
|
+ Add passkey
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user