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.
Start the login challenge
Section titled “Start the login challenge”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 cookieawait savePendingChallenge({ flow: "login", challengeId, secret, userId: user.id, email,});
await emailCodeToUser({ email, message });import { createMailboxChallenge, isChallengeRateLimitedError,} from "@passlock/server/safe";
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,});
if (result.success) { // message contains rendered HTML and plain-text email content. // result.value.challenge.code is also available if you prefer to render your own email. const { challengeId, secret, message, email } = result.value.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 });} else if (isChallengeRateLimitedError(result.error)) { return showRateLimit(result.error.retryAfterSeconds);} else { throw new Error(result.error.message);}Verify the code and create a session
Section titled “Verify the code and create a session”import { verifyMailboxChallenge } from "@passlock/server";
// fetch from the user's session or secure cookieconst 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);import { verifyMailboxChallenge } from "@passlock/server/safe";
// fetch from the user's session or secure cookieconst pending = await loadPendingChallenge({ flow: "login" });
const result = await verifyMailboxChallenge({ tenancyId: "myTenancyId", apiKey: "myApiKey", challengeId: pending.challengeId, secret: pending.secret, code: form.code,});
if (result.success) { const { challenge } = result.value;
if (challenge.purpose !== "login") { throw new Error("Unexpected challenge purpose"); }
if (challenge.userId !== String(pending.userId)) { throw new Error("Unexpected user"); }
await clearPendingChallenge({ flow: "login" }); await createSession(pending.userId);} else { return showVerificationError(result.error);}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.