Agent Wranglers

Sign-in methods Recommended

Offer passkeys first

Offer passkeys as the main way to sign in, through autofill. Invite people to create one right after they sign in another way.

How to do it

Follow WebAuthn Level 3, a W3C Recommendation since 25 August 2026. On the sign-in page, check PublicKeyCredential.isConditionalMediationAvailable(), then start a request with mediation: 'conditional'. Give the username field autocomplete="username webauthn". After a password sign-in, offer a passkey. Where PublicKeyCredential.getClientCapabilities() reports conditionalCreate, call navigator.credentials.create() with mediation: 'conditional', and the password manager can add one without a prompt. Set residentKey to required and userVerification to preferred. Make user.id random bytes, at most 64, with no email or name in it. Store the credential ID, public key, counter, transports, backup flags and a name. Allow several passkeys per account. The passkey addresses rule points password managers to your pages.

Example

if (await PublicKeyCredential.isConditionalMediationAvailable?.()) {
  const response = await fetch('/passkeys/sign-in-options');
  const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(await response.json());
  const credential = await navigator.credentials.get({ publicKey, mediation: 'conditional' });
  await fetch('/passkeys/sign-in', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(credential.toJSON()),
  });
}
from webauthn import generate_registration_options, options_to_json
from webauthn.helpers.structs import (
    AuthenticatorSelectionCriteria, PublicKeyCredentialDescriptor, ResidentKeyRequirement, UserVerificationRequirement,
)

def passkey_registration_options(user) -> str:
    options = generate_registration_options(
        rp_id="example.com",
        rp_name="Example Bakery",
        user_id=user.webauthn_handle,  # 32 random bytes from secrets.token_bytes, made once per user
        user_name=user.email,
        authenticator_selection=AuthenticatorSelectionCriteria(
            resident_key=ResidentKeyRequirement.REQUIRED,
            user_verification=UserVerificationRequirement.PREFERRED,
        ),
        exclude_credentials=[PublicKeyCredentialDescriptor(id=p.credential_id) for p in user.passkeys],
    )
    return options_to_json(options)

How to check

In a browser test with a virtual authenticator, such as Playwright over the Chrome DevTools Protocol, register a passkey and sign in through autofill. Both succeed. The stored user handle holds no email or name.

For agents: if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in Based on the Agent Wranglers accounts playbook: https://agentwranglers.se/playbook/accounts/sign-in-methods/passkeys-first.