{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreifpqwbx4hilh65vme3rdozzwud4cc7rlmn664touteakhlwjymxe4",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mokql33wjjh2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreig3evutjglbdhcbjojmkbqvcealthlo4ex4gyyt7ddlulgkglmcp4"
},
"mimeType": "image/webp",
"size": 188936
},
"path": "/tomdevbrown/how-to-make-production-ready-otp-handling-system-1g5e",
"publishedAt": "2026-06-18T11:24:19.000Z",
"site": "https://dev.to",
"tags": [
"javascript",
"node",
"security",
"tutorial"
],
"textContent": "Handling an OTP (One-Time Password) flow requires a clean sequence so you don't run into race conditions, like a user trying to verify a token before it's securely saved in your state or database.\n\nHere is how you structure a production-ready OTP management lifecycle using `auth-verify`.\n\n## Installation\n\n\n npm install auth-verify\n\n\n## Initialize the library:\n\n### Step 1.\n\nFirst, bring in the library and configure the token storage. For development, memory works fine, but use redis or a database token store in production so your OTPs survive server restarts.\n\n\n\n const AuthVerify = require(\"auth-verify\");\n const auth = new AuthVerify({\n storeTokens: \"memory\", // 'redis' is highly recommended for production\n expiresIn: \"5m\" // OTP automatically expires after 5 minutes\n });\n\n\n## Configure your transport channel:\n\n### Step 2.\n\nSet up how the OTP actually gets to the user. You need to provide your service credentials (like SMTP for emails or API keys for SMS).\n\n\n\n auth.otp.sender({\n via: 'email',\n sender: 'app@yourdomain.com',\n pass: process.env.EMAIL_APP_PASSWORD,\n service: 'gmail' // or custom SMTP settings\n });\n\n\n## Generate and send the OTP:\n\n### Step 3.\n\nTrigger this inside your `login/registration` route. The library automatically generates a secure crypto-random numeric code, maps it to the identifier (email/phone), and sends it out.\n\n\n\n app.post(\"/api/auth/request-otp\", async (req, res) => {\n const { email } = req.body;\n\n try {\n const success = await auth.otp.send(email);\n if (!success) return res.status(500).json({ error: \"Failed to send OTP\" });\n\n res.json({ message: \"OTP sent successfully!\" });\n } catch (err) {\n res.status(500).json({ error: err.message });\n }\n });\n\n\n## Verify the user's input:\n\n### Step 4.\n\nWhen the user submits the code from their screen, pass their identifier and the code to `.verify()`. The library automatically checks if the code matches, handles the expiration window, and destroys the OTP on success to prevent reuse.\n\n\n\n app.post(\"/api/auth/verify-otp\", async (req, res) => {\n const { email, code } = req.body;\n\n try {\n const isValid = await auth.otp.verify(email, code);\n\n if (!isValid) {\n return res.status(400).json({ error: \"Invalid or expired OTP\" });\n }\n\n // OTP is valid! Mint your JWT or log the user in here\n res.json({ success: true, message: \"Authenticated!\" });\n } catch (err) {\n res.status(500).json({ error: err.message });\n }\n });\n\n\n## 💡 Production Gotchas to Keep in Mind\n\n**Rate Limiting:** `auth-verify` handles the generation and verification, but it won't stop a malicious actor from hitting your /request-otp endpoint 10,000 times to blow up your email/SMS billing. Always wrap your OTP routes in a rate-limiter middleware like express-rate-limit.\n\n**Replay Attacks:** The library automatically handles deleting the OTP token storage space upon a successful validation so that the exact same code cannot be used twice.",
"title": "How to make production ready OTP handling system"
}