Skip to content

Introducing mailbox challenges

New feature Passlock now supports mailbox challenges, a server-side feature for building email one-time code flows without rebuilding the awkward security and lifecycle pieces yourself.

This is aimed at the cases teams keep needing even when passkeys are the long-term goal:

  • verifying email ownership during signup
  • passwordless login when passkeys are not available
  • confirming a new email address before updating an account

A mailbox challenge is an email verification flow with a little more structure than “generate a six-digit code and hope for the best”.

When you create a challenge, Passlock returns:

  • challengeId to identify the challenge
  • secret for your app to keep server-side
  • code for you to deliver by email
  • message.html and message.text if you want ready-made email content

The user receives the code, enters it into your app, and your backend verifies the attempt using all three parts: challengeId, secret, and code.

That detail matters. The emailed code alone is not enough.

Email one-time codes look simple until you try to ship them properly.

You need challenge expiry, retry handling, invalidation of older outstanding codes, rate limiting, consistent verification, and a way to reduce the risk of treating an intercepted code as sufficient proof on its own.

Mailbox challenges package those moving parts into a small API so you can focus on your product flow instead of rebuilding verification plumbing.

The flow is straightforward:

  1. Your backend creates a mailbox challenge for a purpose like signup, login, or email-change.
  2. Your app stores challengeId and secret in a server-side session or HTTP-only cookie.
  3. Your mailer sends the code to the user, using Passlock’s rendered message or your own template.
  4. The user submits the code.
  5. Your backend verifies the challenge and checks the returned purpose, email, and any expected local user context before completing the action.

If you need to restore an in-progress flow, you can also read a challenge without exposing the secret or code.

If you are already using the Passlock server library, the basic shape looks like this:

import {
createMailboxChallenge,
verifyMailboxChallenge,
} from "@passlock/server";
const created = await createMailboxChallenge({
tenancyId: "myTenancyId",
apiKey: "myApiKey",
email: "jdoe@example.com",
purpose: "login",
invalidateOthers: true,
});
await savePendingLoginSession({
challengeId: created.challenge.challengeId,
secret: created.challenge.secret,
});
await sendEmail({
to: created.challenge.email,
html: created.challenge.message.html,
text: created.challenge.message.text,
});
const pending = await readPendingLoginSession();
const verified = await verifyMailboxChallenge({
tenancyId: "myTenancyId",
apiKey: "myApiKey",
challengeId: pending.challengeId,
secret: pending.secret,
code: form.code,
});
if (verified.challenge.purpose !== "login") {
throw new Error("Unexpected challenge purpose");
}
await completeLoginForEmail(verified.challenge.email);

The same feature is also available over raw HTTP if you are not using Node.js.

Why Passlock does not send the email for you

Section titled “Why Passlock does not send the email for you”

Passlock creates the challenge and renders the message content, but the email still comes from your system.

That is deliberate:

  • the email should come from your domain
  • you keep control over deliverability and branding
  • you can either send the rendered HTML/text directly or generate your own template from the raw code

Mailbox challenges are not a replacement for passkeys. They cover a different part of the authentication surface.

Passkeys remain the stronger default for ongoing authentication. Mailbox challenges are useful when you need to prove mailbox ownership, offer a passwordless fallback, or verify account changes that are naturally email-centric.

In practice, many applications will use both:

  • passkeys for primary authentication
  • mailbox challenges for onboarding, recovery, or account-change verification