Skip to main content

Passkeys and Authenticator Apps

Overview​

The API stores up to five TOTP apps and ten passkeys per profile in user_mfa_factors. The passkey user handle is random and kept separately in user_webauthn_handles until profile deletion. A passkey can sign in without a password, complete a pending 2FA challenge or confirm a session. REST (routes/security.rs) and GraphQL (graphql/security.rs) call the same security service. Both use an interactive session for method management and the same second-factor validation for removal, disable and recovery-code replacement. The first method gives ten recovery codes; a first-passkey registration is committed only after recoveryCodesStored: true (REST recovery_codes_stored).

[webauthn] pins the RP ID, name and exact browser origins; see Installation. Registration requests attestation: none, accepts ES256 and RS256, and sends the profile display name rather than its email address. It does not query FIDO MDS. The ID app forbids embedding with frame-ancestors 'none' and X-Frame-Options: DENY. WebAuthn registration and assertion challenges are short-lived, single-use Redis records.

Provider names and logos​

apps/api/src/services/security/passkey_aaguids.json embeds provider names and AAGUIDs. The dashboard shows the user's label with the provider name and its logo, where one is available. Unknown or browser-suppressed AAGUIDs produce Passkey and the generic passkey symbol. Providers with no icon in the list or an excluded logo also show the generic symbol. Where the browser supplies a YubiKey AAGUID, the API shows its model name and the dashboard uses the Yubico logo. No provider list or logo is fetched from a third party at runtime. A trademark notice appears under the user's passkey list whenever a logo is visible.

The logo files in apps/web/public/passkey-providers/ are light and dark SVG pairs. For entries from the AAGUID list, each file is the icon_light or icon_dark value from the same upstream commit as the API's provider names, decoded from base64 byte for byte; do not redraw, optimize or recolor it. sources.json records that upstream commit, the provider-name and AAGUID mapping, the commit and submitter domain for each icon, excluded providers and entries without an icon. The Yubico pair is the documented Simple Icons CC0 exception in sources.json. The server reads this metadata and passes only the name-to-logo mapping to the dashboard. The UI embeds third-party SVGs only through <img> with empty alt text because the provider name is adjacent; never inline them or use <object>, <embed> or <iframe>. The light and dark variants switch with the theme without changing brand colors. Logos belong only in the user's passkey list and its dialogs. Public documentation screenshots must use the generic symbol or crop provider logos out.

To refresh the provider list and logos:

  1. Review the selected upstream AAGUID-list commit. For every new or changed icon, inspect the commit that supplied that icon and who submitted it, then check the vendor's trademark guidance for restrictions on this use. Keep excluded providers in sources.json without logo files. If a vendor objects, remove its logo mapping and both SVG files; its row then uses the generic symbol.
  2. In one repository commit, update passkey_aaguids.json, sources.json and the matching <slug>-light.svg / <slug>-dark.svg files from that same upstream commit. Record the commit, provider names, AAGUIDs and icon submitter evidence in sources.json. Check that every API provider name maps to a logo, an excluded provider or an entry without an icon. If the upstream list is emptied or retired, keep the embedded copy until a reviewed replacement exists.
  3. Run the SVG safety test in apps/web/__tests__/passkey-providers.test.ts. Its allowlist accepts only the SVG elements and namespaces already needed by the files, local #id references, and restricted CSS. It rejects active markup, external references, entities and unsafe CSS. An SVG that fails needs review; do not widen the allowlist merely to accept it. The static path also has an enforcing sandbox CSP and nosniff; see Web Content-Security-Policy.

Privacy and exports​

The JSON export uses an explicit factors projection: kind, label, created_at, last_used_at, plus provider and backup_state for passkeys. Do not broaden it to a row-wide serializer: secrets, credentials, IDs, counters and the user handle must stay out. Audit entries and security notices identify the method type and context, never the user's free-text label. Removing a passkey invalidates it on Lumio and returns its credential ID only to the signed-in browser for the WebAuthn Signal API; the user may still need to delete it in their password manager.