fix(core): Trust BE method on register and hint authenticator class

- Registration verify endpoint now returns the actual `method`
  classified from the WebAuthn response (`transports` /
  `credentialDeviceType`). The frontend uses this instead of the
  attachment hint it sent, so when the OS routes a "security key"
  request through iCloud Keychain (or any other platform
  authenticator), the UI reflects what was actually registered and
  the state survives a page refresh.
- Add `hints: ['security-key' | 'client-device']` to registration
  options so Chrome skips its platform-passkey dialog and goes
  straight to the matching prompt.
- Rename "Switch to this" → "Set up" for the inactive 2FA method
  card.
This commit is contained in:
Ricardo Espinoza 2026-05-12 08:46:26 -04:00
parent 3df9905815
commit 824c1cc663
No known key found for this signature in database
6 changed files with 16 additions and 7 deletions

View File

@ -323,6 +323,10 @@ export class MFAController {
id: savedCredential.id,
credentialId: savedCredential.credentialId,
label: savedCredential.label,
// The OS picker may route a cross-platform request through a platform
// authenticator (e.g. iCloud Keychain), so the actual method is
// derived from the response — not the client's attachment hint.
method,
};
}

View File

@ -81,6 +81,13 @@ export class WebAuthnService {
authenticatorSelection,
});
// Hint the browser at the intended authenticator class. Chrome respects
// this and skips its platform-passkey dialog when `security-key` is
// hinted, going straight to the "insert your key" prompt. @simplewebauthn
// doesn't type the field yet, so attach it post-generation.
const hint = attachment === 'platform' ? 'client-device' : 'security-key';
(options as typeof options & { hints: string[] }).hints = [hint];
await this.cacheService.set(
`webauthn:challenge:reg:${userId}`,
options.challenge,

View File

@ -4864,7 +4864,6 @@
"settings.personal.passkey.remove.button": "Remove passkey",
"settings.personal.method.status.notSetUp": "Not set up",
"settings.personal.method.button.setUp": "Set up",
"settings.personal.method.button.switchTo": "Switch to this",
"settings.personal.method.button.remove": "Remove",
"settings.personal.method.button.disable": "Disable",
"settings.personal.twoFactor.section.title": "Two-factor authentication (2FA)",

View File

@ -24,6 +24,7 @@ export async function verifyRegistration(
id: string;
credentialId: string;
label: string;
method: 'passkey' | 'security_key';
}> {
return await makeRestApiRequest(context, 'POST', '/mfa/webauthn/registration-verify', data);
}

View File

@ -595,11 +595,7 @@ onBeforeUnmount(() => {
v-else
variant="subtle"
size="small"
:label="
has2fa
? i18n.baseText('settings.personal.method.button.switchTo')
: i18n.baseText('settings.personal.method.button.setUp')
"
:label="i18n.baseText('settings.personal.method.button.setUp')"
:data-test-id="`mfa-method-${option.method}-setup`"
@click="onTwoFactorMethodClick(option.method)"
/>

View File

@ -444,7 +444,9 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
});
if (currentUser.value) {
currentUser.value.mfaEnabled = true;
currentUser.value.mfaMethod = attachment === 'platform' ? 'passkey' : 'security_key';
// Use the BE-classified method — the OS picker may route a
// cross-platform request through a platform authenticator.
currentUser.value.mfaMethod = result.method;
}
return result;
};