Skip to content

Passwordless logins

Mailbox challenges also work well for passwordless login flows. The user enters their email address, your backend creates a challenge, and the submitted code is verified against the stored secret.

import { createMailboxChallenge } from "@passlock/server";
const user = await findUserByEmail("jdoe@example.com");
if (!user) {
return showUnknownAccount();
}
const result = await createMailboxChallenge({
tenancyId: "myTenancyId",
apiKey: "myApiKey",
email: user.email,
purpose: "login",
userId: String(user.id),
invalidateOthers: true,
});
// message contains rendered HTML and plain-text email content.
// result.challenge.code is also available if you prefer to render your own email.
const { challengeId, secret, message, email } = result.challenge;
// save this in the user's session or a secure HTTP only cookie
await savePendingChallenge({
flow: "login",
challengeId,
secret,
userId: user.id,
email,
});
await emailCodeToUser({ email, message });
import { verifyMailboxChallenge } from "@passlock/server";
// fetch from the user's session or secure cookie
const pending = await loadPendingChallenge({ flow: "login" });
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");
}
if (verified.challenge.userId !== String(pending.userId)) {
throw new Error("Unexpected user");
}
await clearPendingChallenge({ flow: "login" });
await createSession(pending.userId);

If you need to restore an in-progress login screen, getMailboxChallenge can read the current challenge without exposing the secret or code. The raw HTTP endpoints are documented in the REST API mailbox challenge reference.