Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Handles are public

A handle names an account in public. It is the account’s login name, relying parties receive it as preferred_username, and published profiles will live at sourceorigin.dev/@handle (#101). Auth doesn’t hide which handles have an account:

  • Handle sign-in refuses a handle without an active account at once: “No active account has this handle.” (start_login in apps/auth-service/src/passkey/mod.rs).
  • Signup and renames say why a handle can’t be taken, naming the look-alike that blocks it (Handles::claimable in apps/auth-service/src/handles/mod.rs).

This was decided in #255.

Why handles aren’t hidden

  • Uniqueness gives handles away. Signup has to refuse a handle that’s taken, and so do renames. Anyone who can sign up, or who holds an account, can therefore check any handle. A vaguer refusal would only stop naming the look-alike; it would still say the typed handle is unavailable. Production requires invitations, which limits signup to people with one, but open signup would extend this to everyone.
  • Handles are already published: to relying parties, and in profiles once #101 ships.
  • Decoys would hide little. Answering handle sign-in for an unknown handle with stable fake credentials, derived from a server key, gives them fixed shapes, so accounts with other passkeys stand out. A disabled account would visibly switch to decoys, and every change of the key changes them, so anyone comparing responses from before and after learns which handles exist (#181).

What a handle reveals

WhoLearns
Anyone, through handle sign-inWhether the handle belongs to an account that can sign in with a passkey by handle: one that is active, or scheduled for deletion, and has an unrevoked passkey. If so, that account’s credential IDs too, which WebAuthn lists for the browser: how many passkeys it has and, from their lengths, often which provider holds them.
Anyone admitted to signup, and every account holder through a renameWhether the handle, or a look-alike, is held or was held, and which look-alike.
Relying parties the user signs in toThe handle, as preferred_username.
Anyone, once a profile is published (#101)The profile. #101 has an unpublished profile answer 404 like an unknown handle, so it reveals nothing beyond what sign-in does.

The account picker (signing in without a handle) reveals nothing: the authenticator names the account.

Threat model

Knowing that a handle has an account gives an attacker little:

  • Nothing to guess. Sign-in needs a passkey. A handle doesn’t help forge one, and passkeys resist phishing.
  • No lockout. Rate limits count requests per client IP, never per account (apps/auth-service/src/security/mod.rs), so naming an account can’t lock its owner out.
  • Not evidence. Support’s evidence policy never counts knowing a handle (account recovery).
  • Credential IDs were never hidden. Handle sign-in lists a real account’s credential IDs with or without decoys. They only work for auth’s RP ID, and are useless without the passkey’s private key.

The cost is privacy: anyone can check whether a person, by the username they use elsewhere, has an Origin Account. For the sign-in of SourceOrigin’s own apps, that is accepted.

Limits

  • Anyone can add failed sign-ins to an account’s history. Handle sign-in binds its ceremony to the account, so a failed assertion, even from a passkey that was never registered, is recorded against it as SignInFailed. The owner’s history hides these, but support reads them in the account’s events. Per-IP rate limits slow a single source, but nothing bounds the failures across sources. The failures change nothing: they are noise, not progress.
  • Credential IDs link a handle across renames. Anyone who recorded an account’s credential IDs before a rename can find its new handle by trying candidates. #297 decides what to do about it.