Skip to content

Account recovery

If passkeys are your primary sign-in method, you still need a recovery path for users who lose access to them. Common examples include a new device that was never synced, a lost security key, or a wiped password manager.

Mailbox challenges give you an email-based recovery step: prove control of the mailbox on the account, then issue a short-lived recovery session that lets the user re-enrol passkeys or complete other recovery-only actions.

backend/recover-account.ts
import { Passlock, isChallengeRateLimitedError } from "@passlock/server";
const passlock = new Passlock({ ... });
// cpatured in a recover account form
const email = "...";
const user = await findUserByEmail(email);
if (!user) {
throw new Error("Account not found");
}
const result = await passlock.createMailboxChallenge({
email: user.email,
userId: user.id,
purpose: "account-recovery",
// any pending challenges for this userId will be nuked
invalidateOthers: true,
});
if (result.success) {
const { challengeId, secret, message } = result.value.challenge;
// save this in the user's session or a secure HTTP only cookie
await savePendingChallenge({
purpose: "account-recovery"
challengeId,
secret,
});
await emailCodeToUser({ email, message });
} else if (isChallengeRateLimitedError(result.error)) {
return showRateLimit(result.error.retryAfterSeconds);
} else {
throw new Error(result.error.message);
}
backend/recover-account.ts
import { Passlock } from "@passlock/server";
const passlock = new Passlock({ ... });
// fetch from the user's session or secure cookie
const pendingChallenge = await loadPendingChallenge({ purpose: "account-recovery" });
const result = await passlock.verifyMailboxChallenge({
challengeId: pendingChallenge.challengeId,
secret: pendingChallenge.secret,
code: form.code, // the user submitted this
});
if (result.success) {
const { challenge } = result.value;
// give the user 10 minutes to further verify
// their identity and create a new passkey
await createRecoverySession({
userId: challenge.userId,
expiresInMinutes: 10,
});
return redirectTo("/settings/account/recover");
} else {
return showVerificationError(result.error);
}