OTP Module
Accessed via auth.otp. Handles generating, delivering, and verifying one-time
passwords.
otp.send(opts)
async send(opts: SendOtpOptions): Promise<SendOtpResult>Generates a fresh OTP, stores it hashed, and delivers it via the configured
notifier. Calling again before expiry replaces the previous code and resets the
attempt counter. Throws RateLimitError if the send rate
limit is exceeded.
const result = await auth.otp.send({
identifier: "user@example.com",
channel: "email", // or "sms" - default "email"
ip: req.ip, // optional - enables per-IP rate limiting
});
// → { expiresInSeconds: 300 }SendOtpOptions:
| Field | Type | Description |
|---|---|---|
identifier | string | The user’s email address or phone number. |
channel | "email" | "sms" | Delivery method. Defaults to "email". |
ip | string? | Client IP. When provided, per-IP rate limiting is enforced too. |
SendOtpResult:
| Field | Type | Description |
|---|---|---|
expiresInSeconds | number | Seconds until the sent OTP expires. |
code | string? | The plaintext code - only present when otp.exposeCode is enabled in createAuth(). |
Reading the code in tests
By default the code is never returned - it only reaches the configured notifier.
For automated tests, enable otp.exposeCode so send() returns the plaintext
code directly instead of having to scrape notifier output:
const auth = createAuth({
jwt: { secret: process.env.JWT_SECRET! },
otp: { exposeCode: true }, // TEST/DEV ONLY - never enable in production
});
const { code } = await auth.otp.send({ identifier: "user@example.com" });
await auth.auth.loginWithOtp({ identifier: "user@example.com", otp: code! });otp.verify(opts)
async verify(opts: VerifyOtpOptions): Promise<VerifyOtpResult>Verifies the user-entered OTP. Consumes it on success (one-time use). After 5
failed attempts the OTP is automatically invalidated. Unlike loginWithOtp,
this method does not throw - it returns a result object.
const result = await auth.otp.verify({
identifier: "user@example.com",
otp: "123456",
});
// Success:
// { success: true, verified: true }
// Failure (note the machine-readable `code`):
// { success: false, verified: false, error: "Invalid OTP. 4 attempt(s) remaining.", code: "OTP_INVALID" }
// { success: false, verified: false, error: "OTP expired or not found. Request a new code.", code: "OTP_EXPIRED" }
// { success: false, verified: false, error: "Too many failed attempts. Please request a new OTP.", code: "OTP_MAX_ATTEMPTS" }VerifyOtpResult:
| Field | Type | Description |
|---|---|---|
success | boolean | Whether the operation completed without error. |
verified | boolean | Whether the OTP was correct. |
error | string? | Human-readable message on failure. |
code | GuardoErrorCode? | Machine-readable code - see Error Handling. |
Verification uses crypto.timingSafeEqual against the stored SHA-256 hash, so
comparison time doesn’t leak whether digits matched.
otp.exists(identifier)
async exists(identifier: string): Promise<boolean>Check whether a valid (unexpired) OTP exists for an identifier. Useful for showing “resend” UI.
const pending = await auth.otp.exists("user@example.com");
// → true or false