Authentication
VirtualID is passwordless. You sign in with a passkey (WebAuthn/FIDO2), and log in on new devices with a join code approved from a device you already trust. There is no password to phish, reuse, or leak.
Principles
- No email required to create an account — your identity is a username you pick.
- Fewest clicks possible — signup is pick username → create passkey → logged in.
- Passkey is the credential — WebAuthn discoverable (resident) credentials, so returning users are offered their identity by the device.
- Recovery is explicit, never an email password-reset bypass: recovery phrase / linked methods / optional TOTP.
Flow A — New account (signup)
- Pick a username. Single input with live validity hints.
- Next → client calls
POST /auth/username/check. Available → green, and a Create passkey button appears. Taken/invalid → red with a reason. - Create passkey → the browser's WebAuthn create prompt. The client also generates a non-exportable DPoP key (WebCrypto) at this step.
POST /auth/register/finishwith the attestation + DPoP proof → the server creates the account, stores the credential anddpop_jkt, and issues tokens. You're logged in instantly.
Usernames use casefolded uniqueness, a reserved-word list, and a confusable/homoglyph guard (anti-impersonation). Uniqueness is enforced atomically at register-finish — the availability check reserves nothing (prevents squatting via check spam).
Flow B — Returning user, this device has a passkey
- The login page immediately attempts discoverable login (conditional UI):
navigator.credentials.get()with an empty allowlist, so the platform offers the identities stored on this device. - You pick an identity, authenticate (biometric/PIN), and the client sends the assertion + a fresh DPoP key & proof to
POST /auth/login/finish. Zero typing in the common case.
Flow C — New device, no passkey → cross-device join
To log in on a device that has no passkey yet, VirtualID uses a short join code generated on a device you're already logged into — then approved on that trusted device.
On the new device
- Choose Other and type your username.
- The server sees no credential here and offers Confirm with a join code.
- Enter the 6-digit join code from a device where you're already logged in.
On the trusted device
- Open Log in another device → the portal shows a 6-digit code (single-use, ~2-minute TTL) via
POST /auth/join/issue. - The new device submits
{username, code}toPOST /auth/join/redeem. This only moves the attempt to awaiting approval — it never logs anyone in. - The trusted device shows an approval prompt with the new device's approximate location/UA. You must Approve (
POST /auth/join/approve).
join/finish returns 403 JOIN_NOT_APPROVED unless the request is approved on the trusted device. This is enforced server-side (proven by TestAuth_Join_RequiresApproval). If you have no trusted device, a recovery phrase or TOTP replaces the trusted-device approval.Permanent-device opt-in
After approval, a deliberate checkbox — "This is a permanent device — trust it for future logins" — is unchecked by default:
- Unchecked (default): Proceed gives you a DPoP-bound session for this session only. No passkey is created — right for a borrowed or shared machine.
- Checked: Create passkey & proceed runs a WebAuthn create ceremony and registers a passkey for this device, so next time it's Flow B.
This maps to POST /auth/join/finish with persist_device: false issues an ephemeral session with no stored credential; true requires the WebAuthn ceremony and registers one credential.
Sessions & token binding (DPoP)
Sessions are opaque-token backed and cryptographically sender-bound with DPoP: a stolen bearer token is useless without the device's non-exportable DPoP private key. Tokens are short-lived and renewed via POST /auth/token/renew. The join-code path produces DPoP-bound sessions like any other.
Multiple methods, 2FA & recovery (MVP)
- Multiple passkeys per account (phone + laptop + hardware key); list, rename, remove — but you cannot remove the last method.
- A downloadable recovery/backup key, generated once (step-up to download).
- Optional TOTP 2FA, with multiple authenticators (any valid code passes).
- Recovery binding rule: if you have ≥1 TOTP authenticator, recovery needs the recovery key AND a valid TOTP code — the key alone is rejected. With no authenticator, the recovery key alone can recover (your explicit choice).