code examples
code examples
How to Implement MessageBird OTP & 2FA in Next.js: Complete Tutorial
Learn how to build SMS-based OTP verification and two-factor authentication in Next.js with MessageBird Verify API. Includes working code, security best practices, and step-by-step setup guide.
MessageBird Next.js OTP/2FA Integration: Complete Implementation Guide
Learn how to implement SMS-based One-Time Password (OTP) verification and Two-Factor Authentication (2FA) in your Next.js application using MessageBird's Verify API. This step-by-step tutorial shows you how to build a complete OTP authentication flow—from sending verification codes to validating user input—with production-ready code examples.
By following this guide, you'll build a secure Next.js authentication system that protects user accounts with SMS verification, reduces fraudulent signups, and enhances account security with two-factor authentication.
What You'll Build: SMS OTP Authentication System
Goal: Build SMS-based OTP verification for Next.js using MessageBird's Verify API.
What This Solves: Add a second authentication factor to protect user accounts, verify phone numbers during registration, prevent fraudulent signups, and secure sensitive transactions. Common use cases include user registration verification, login 2FA, password reset confirmation, and transaction authorization.
Time Estimate: 30-45 minutes for basic implementation, plus additional time for production hardening.
Technologies Used:
- Next.js 15.x – React framework with API Routes for server-side logic
- Node.js v20 LTS or v22 LTS – Runtime environment for backend operations
- MessageBird Verify API – SMS and voice OTP delivery service
- MessageBird Node.js SDK – Official npm package (
messagebird) for API integration - React – Frontend UI components and state management
- TypeScript – Type-safe development (recommended)
- Prisma (Optional) – Database ORM for persisting user verification status
- Tailwind CSS (Optional) – Utility-first styling framework
Authentication Flow Architecture:
<table> <tr> <th><strong>Step</strong></th> <th><strong>Component</strong></th> <th><strong>Action</strong></th> </tr> <tr> <td>1</td> <td>Frontend UI</td> <td>User enters phone number</td> </tr> <tr> <td>2</td> <td>API Route</td> <td>Frontend sends number to <code>/api/auth/request-otp</code></td> </tr> <tr> <td>3</td> <td>MessageBird SDK</td> <td>API Route calls MessageBird Verify API to request OTP</td> </tr> <tr> <td>4</td> <td>MessageBird Service</td> <td>Sends OTP via SMS, returns verification <code>id</code></td> </tr> <tr> <td>5</td> <td>API Route</td> <td>Stores <code>id</code> temporarily (session/cookie/server-side) and signals success</td> </tr> <tr> <td>6</td> <td>Frontend UI</td> <td>Prompts user to enter received OTP code</td> </tr> <tr> <td>7</td> <td>Frontend UI</td> <td>User enters code, sends it with verification <code>id</code> to <code>/api/auth/verify-otp</code></td> </tr> <tr> <td>8</td> <td>MessageBird SDK</td> <td>API Route calls Verify API with <code>id</code> and user's <code>token</code> (OTP)</td> </tr> <tr> <td>9</td> <td>MessageBird Service</td> <td>Validates token against <code>id</code>, responds to API Route</td> </tr> <tr> <td>10</td> <td>API Route & Database</td> <td>Processes response, updates user profile if successful (e.g., <code>isTwoFactorEnabled = true</code>)</td> </tr> </table>(Note: An architecture diagram image (e.g., PNG/SVG) would improve visualization across platforms.)
Prerequisites:
- Node.js (v20 LTS or v22 LTS recommended as of 2025) and npm/yarn installed
- A MessageBird account with a Live API Key (Sign up at messagebird.com)
- Basic understanding of React and Next.js
- (Optional) PostgreSQL database and connection string if using Prisma
- (Optional) Familiarity with Tailwind CSS for styling
Step 1: Project Setup and Installation
Initialize your Next.js project and install the MessageBird SDK to get started with OTP authentication.
-
Create Next.js App
Open your terminal and run the following command, choosing options like TypeScript (recommended), Tailwind CSS (optional), and App Router (recommended):
bashnpx create-next-app@latest messagebird-otp-nextjs cd messagebird-otp-nextjsFollow the prompts. This guide assumes you're using the App Router and TypeScript.
-
Install Dependencies
Install the MessageBird Node.js SDK:
bashnpm install messagebird(Optional) If you plan to store user data or 2FA status, install Prisma:
bashnpm install prisma @prisma/client npm install -D prisma npx prisma init --datasource-provider postgresql # Or your preferred DB -
Configure Environment Variables
Create a
.env.localfile in the root of your project. Never commit this file to version control. Add your MessageBird Live API Key and (if using Prisma) your database connection string.ini# .env.local # MessageBird API Key (Get from MessageBird Dashboard > Developers > API access) MESSAGEBIRD_API_KEY="YOUR_LIVE_API_KEY_HERE" # (Optional) Database URL for Prisma # Example for PostgreSQL: postgresql://USER:PASSWORD@HOST:PORT/DATABASE DATABASE_URL="YOUR_DATABASE_CONNECTION_STRING"MESSAGEBIRD_API_KEY: Obtain this from your MessageBird Dashboard under Developers > API access (REST). Use a Live key for actual SMS sending. Test keys won't send real messages.DATABASE_URL: Your database connection string. Format depends on your database provider.
-
(Optional) Prisma Schema Setup
If using Prisma, modify the generated
prisma/schema.prismafile to include relevant user fields for 2FA.prisma// prisma/schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" // Or your chosen provider url = env("DATABASE_URL") } model User { id String @id @default(cuid()) email String? @unique // Example field phoneNumber String? @unique // Store verified phone number isTwoFactorEnabled Boolean @default(false) // Flag for 2FA status createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Add other user fields as needed }Apply the schema to your database:
bashnpx prisma db push # You might also need: npx prisma generateThis creates the
Usertable in your database.
Step 2: Build OTP API Routes with MessageBird Verify API
Create two Next.js API routes: one to generate and send OTP codes via MessageBird, and another to verify user input against the generated code.
-
Create Helper for MessageBird Client
Initialize the MessageBird client once for better performance. Create
lib/messagebird.ts:typescript// lib/messagebird.ts import messagebird from 'messagebird'; const apiKey = process.env.MESSAGEBIRD_API_KEY; if (!apiKey) { throw new Error('MESSAGEBIRD_API_KEY environment variable is not set.'); } // Initialize with your API key export const mbClient = messagebird(apiKey); -
API Route to Request OTP (
/api/auth/request-otp)Create the file
app/api/auth/request-otp/route.ts:typescript// app/api/auth/request-otp/route.ts import { NextRequest, NextResponse } from 'next/server'; import { mbClient } from '@/lib/messagebird'; // Adjust path if needed export async function POST(req: NextRequest) { try { const body = await req.json(); const { phoneNumber } = body; // Basic validation (E.164 format). Consider using a library like // 'google-libphonenumber' for more robust validation in production. if (!phoneNumber || typeof phoneNumber !== 'string' || !/^\+[1-9]\d{1,14}$/.test(phoneNumber)) { return NextResponse.json({ error: 'Invalid phone number format. Use E.164 format (e.g., +1234567890).' }, { status: 400 }); } // Parameters for the MessageBird Verify API const params = { // Sender ID: Alphanumeric (max 11 chars) or phone number. Note: Alphanumeric IDs // are not supported in all countries (e.g., US/Canada). Using a purchased virtual // number from MessageBird is often the most reliable method globally for a custom ID. originator: 'VerifyApp', type: 'sms', // Use 'tts' for voice call OTP template: 'Your verification code is %token.', // Message template // tokenLength: 6, // Default is 6 // timeout: 60, // Default is 30 seconds }; // Note: This uses a Promise wrapper for the callback-based SDK method. // Check if the current 'messagebird' SDK version supports native Promises // (e.g., `await mbClient.verify.create(...)`) for potentially cleaner async/await syntax. return new Promise((resolve) => { mbClient.verify.create(phoneNumber, params, (err: any, response: any) => { if (err) { console.error("MessageBird Verify Create Error:", err); // Provide more specific error messages based on err.errors let errorMessage = 'Failed to send verification code.'; if (err.errors && err.errors[0]) { errorMessage = err.errors[0].description || errorMessage; // Handle specific errors like invalid number format explicitly if needed if (err.errors[0].code === 21) { // Example: Code for invalid parameter errorMessage = 'Invalid phone number provided to MessageBird.'; } } resolve(NextResponse.json({ error: errorMessage }, { status: 500 })); } else { console.log("MessageBird Verify Create Response:", response); // IMPORTANT: Securely handle the response.id // For this example, we return it. In production, consider: // 1. Storing it in a secure, httpOnly cookie associated with the user's session. // 2. Storing it server-side (e.g., Redis, database) linked to the session ID. // Returning it directly to the client is simpler but less secure if not handled carefully frontend. resolve(NextResponse.json({ success: true, verifyId: response.id }, { status: 200 })); } }); }); } catch (error) { console.error("Request OTP Error:", error); return NextResponse.json({ error: 'An unexpected error occurred.' }, { status: 500 }); } }- Validation: Includes basic E.164 format check. For production, use
google-libphonenumberfor robust validation. mbClient.verify.create: Calls the MessageBird API.phoneNumber: The user's number in international E.164 format (e.g.,+14155552671).params: Configuration options.originator: Alphanumeric IDs vary by country (not supported in US/Canada). Use a purchased virtual number from MessageBird for global reliability.template: The message text.%tokenis replaced by the generated OTP.timeout: How long the OTP remains valid (default 30s).
- Error Handling: Logs errors and returns user-friendly messages. Parses MessageBird-specific errors if available.
- Success Response: Returns
success: trueand theverifyId. Handle thisverifyIdsecurely – see comments in code. - Promise Wrapper Note: Check if your SDK version supports native
async/awaitfor cleaner syntax.
- Validation: Includes basic E.164 format check. For production, use
-
API Route to Verify OTP (
/api/auth/verify-otp)Create the file
app/api/auth/verify-otp/route.ts:typescript// app/api/auth/verify-otp/route.ts import { NextRequest, NextResponse } from 'next/server'; import { mbClient } from '@/lib/messagebird'; // Adjust path // import { prisma } from '@/lib/prisma'; // Uncomment if using Prisma export async function POST(req: NextRequest) { try { const body = await req.json(); const { verifyId, token } = body; // Basic validation if (!verifyId || typeof verifyId !== 'string' || verifyId.trim() === '') { return NextResponse.json({ error: 'Verification ID is missing.' }, { status: 400 }); } if (!token || typeof token !== 'string' || !/^\d{6}$/.test(token)) { // Assuming 6-digit token return NextResponse.json({ error: 'Invalid OTP format. Must be 6 digits.' }, { status: 400 }); } // Note: This uses a Promise wrapper for the callback-based SDK method. // Check if the current 'messagebird' SDK version supports native Promises // (e.g., `await mbClient.verify.verify(...)`) for potentially cleaner async/await syntax. return new Promise((resolve) => { mbClient.verify.verify(verifyId, token, (err: any, response: any) => { if (err) { console.error("MessageBird Verify Verify Error:", err); let errorMessage = 'Failed to verify code.'; if (err.errors && err.errors[0]) { errorMessage = err.errors[0].description || errorMessage; // Example: Handle specific error descriptions like expired/invalid ID if (err.errors[0].code === 10) { // Example code often associated with 'not_found' (expired/invalid ID) errorMessage = 'Verification ID is invalid or has expired.'; } // Check err.errors[0].description for more specific details from MessageBird } // Common error: Token is incorrect or expired. resolve(NextResponse.json({ error: errorMessage, verified: false }, { status: 400 })); } else { // Verification successful! console.log("MessageBird Verify Verify Response:", response); // Optional: Update user record in database if verification is for enabling 2FA // const userId = ''; // Get userId from session/token // try { // await prisma.user.update({ // where: { id: userId }, // data: { isTwoFactorEnabled: true, phoneNumber: response.recipient }, // Store verified number // }); // } catch (dbError) { // console.error("Database update error:", dbError); // // Decide how to handle DB error – maybe verification still counts? // resolve(NextResponse.json({ error: 'Verification successful, but failed to update profile.' }, { status: 500 })); // return; // } // Clear the verifyId from session/cookie here if stored server-side resolve(NextResponse.json({ success: true, verified: true }, { status: 200 })); } }); }); } catch (error) { console.error("Verify OTP Error:", error); return NextResponse.json({ error: 'An unexpected error occurred.', verified: false }, { status: 500 }); } }- Validation: Checks for
verifyIdand basic OTP format (assuming 6 digits). mbClient.verify.verify: Calls MessageBird to check the code.verifyId: The ID received from thecreatecall.token: The 6-digit code entered by the user.
- Error Handling: Handles incorrect token, expired token/ID, or other API errors. Check
err.errors[0].descriptionfor specifics. - Success Response: Returns
verified: true. - (Optional) Database Update: Includes commented-out code showing how to update a user record in Prisma upon successful verification (e.g., enabling 2FA or storing the verified phone number). Get the
userIdfrom the user's session or authentication token. - Promise Wrapper Note: Check if your SDK version supports native
async/awaitfor cleaner syntax.
- Validation: Checks for
Step 3: Create the Frontend OTP Verification UI
Build React components to create a user-friendly OTP input interface with proper error handling and loading states.
-
Create the Page
Create a page file, for example,
app/verify/page.tsx:typescript// app/verify/page.tsx 'use client'; // This component interacts with user state and browser APIs import React, { useState } from 'react'; export default function VerifyPage() { const [phoneNumber, setPhoneNumber] = useState(''); const [otpCode, setOtpCode] = useState(''); // SECURITY WARNING: Storing verifyId in client-side state is simple for demos but NOT recommended for production. // See Security Considerations section for better approaches (httpOnly cookies, server sessions). const [verifyId, setVerifyId] = useState<string | null>(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<string | null>(null); const [message, setMessage] = useState<string | null>(null); const [isOtpSent, setIsOtpSent] = useState(false); const [isVerified, setIsVerified] = useState(false); const handleRequestOtp = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); setError(null); setMessage(null); setIsOtpSent(false); setVerifyId(null); // Clear previous ID try { const res = await fetch('/api/auth/request-otp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phoneNumber }), }); const data = await res.json(); if (!res.ok) { throw new Error(data.error || 'Failed to request OTP'); } setVerifyId(data.verifyId); // Store the ID from the backend (see security warning above) setIsOtpSent(true); setMessage('OTP sent successfully! Check your phone.'); } catch (err: any) { setError(err.message || 'An error occurred.'); } finally { setIsLoading(false); } }; const handleVerifyOtp = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); setError(null); setMessage(null); if (!verifyId) { setError("Verification process not initiated correctly. Request OTP again."); setIsLoading(false); return; } try { const res = await fetch('/api/auth/verify-otp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, // Send the stored verifyId and the user's input code body: JSON.stringify({ verifyId: verifyId, token: otpCode }), }); const data = await res.json(); if (!res.ok || !data.verified) { throw new Error(data.error || 'Failed to verify OTP'); } setIsVerified(true); setMessage('Phone number verified successfully!'); // Reset form state after success setIsOtpSent(false); setVerifyId(null); setOtpCode(''); // Optionally redirect or update UI further } catch (err: any) { setError(err.message || 'An error occurred during verification.'); // Keep OTP form visible on verification failure to allow retry } finally { setIsLoading(false); } }; // Basic styling with Tailwind CSS (optional) const inputStyle = "border p-2 rounded w-full mb-4"; const buttonStyle = "bg-blue-500 text-white p-2 rounded hover:bg-blue-600 disabled:opacity-50"; return ( <div className="max-w-md mx-auto mt-10 p-6 border rounded shadow"> <h1 className="text-2xl font-bold mb-6 text-center">Verify Phone Number</h1> {error && <p className="text-red-500 mb-4 bg-red-100 p-3 rounded">{error}</p>} {message && <p className="text-green-500 mb-4 bg-green-100 p-3 rounded">{message}</p>} {!isOtpSent && !isVerified && ( <form onSubmit={handleRequestOtp}> <label htmlFor="phoneNumber" className="block mb-2 font-medium">Phone Number (E.164 format)</label> <input type="tel" id="phoneNumber" value={phoneNumber} onChange={(e) => setPhoneNumber(e.target.value)} placeholder="+1234567890" required className={inputStyle} disabled={isLoading} /> <button type="submit" disabled={isLoading || !phoneNumber} className={`${buttonStyle} w-full`}> {isLoading ? 'Sending…' : 'Send OTP'} </button> </form> )} {isOtpSent && !isVerified && ( <form onSubmit={handleVerifyOtp}> <p className="mb-4">Enter the 6-digit code sent to {phoneNumber}:</p> <label htmlFor="otpCode" className="block mb-2 font-medium">OTP Code</label> <input type="text" id="otpCode" value={otpCode} onChange={(e) => setOtpCode(e.target.value)} placeholder="123456" required maxLength={6} // Assuming 6 digits pattern="\d{6}" // Basic pattern validation className={`${inputStyle} tracking-widest text-center`} // Style for OTP input disabled={isLoading} /> <button type="submit" disabled={isLoading || otpCode.length !== 6} className={`${buttonStyle} w-full`}> {isLoading ? 'Verifying…' : 'Verify Code'} </button> <button type="button" onClick={() => { setIsOtpSent(false); setError(null); setMessage(null); setOtpCode(''); setVerifyId(null); }} className="text-sm text-blue-600 hover:underline mt-4 text-center block w-full disabled:opacity-50" disabled={isLoading} > Entered wrong number? Start over. </button> </form> )} {isVerified && ( <div className="text-center"> <p className="text-xl font-semibold text-green-700">Verification Complete!</p> <button type="button" onClick={() => { setIsVerified(false); setPhoneNumber(''); /* Reset more state if needed */ }} className={`${buttonStyle} mt-6`} > Verify Another Number </button> </div> )} </div> ); }'use client': Required because this component usesuseStateand interacts with browser events/APIs.- State Management: Uses
useStateto manage phone number input, OTP code input, loading state, errors, success messages, theverifyId, and the UI flow (isOtpSent,isVerified). handleRequestOtp: Sends the phone number to/api/auth/request-otp. On success, stores the returnedverifyIdin state and updates the UI to show the OTP input form.handleVerifyOtp: Sends theverifyId(retrieved from state) and the user-enteredotpCodeto/api/auth/verify-otp. Updates UI based on success or failure.- UI Flow: Conditionally renders the phone number form, the OTP form, or the success message based on
isOtpSentandisVerifiedstates. - Error/Message Display: Shows feedback to the user.
- Security Note: Storing
verifyIdin React state is acceptable for simplified examples but not recommended for production. This approach is vulnerable to issues like losing state on page refresh and potential client-side manipulation. A more robust approach involves managing theverifyIdserver-side, typically by storing it in a secure, httpOnly cookie tied to the user's session or in a server-side store (like Redis or a database) linked to a session identifier. The server would then retrieve theverifyIdbased on the session when the verification request arrives, rather than relying on the client to send it back.
Security Best Practices for OTP Authentication
- API Key Security: Your
MESSAGEBIRD_API_KEYis highly sensitive. Keep it in.env.localand ensure this file is listed in your.gitignore. Never expose it client-side. Use environment variables in your deployment environment. - Rate Limiting: Implement rate limiting on your API routes (
/api/auth/request-otp,/api/auth/verify-otp) to prevent abuse (e.g., flooding users with OTPs, brute-forcing codes). Use middleware in Next.js or libraries likerate-limiter-flexible, or leverage platform features like Vercel's built-in IP rate limiting. - Input Validation: Rigorously validate all inputs on the server-side (API routes). Check phone number format (E.164), token format (digits, length), and
verifyIdpresence/format. verifyIdHandling (Crucial): ReturningverifyIddirectly to the client and storing it in frontend state is convenient for demos but insecure for production. An attacker could potentially intercept or manipulate it. Prefer server-side management: Store theverifyIdin a secure, httpOnly cookie (potentially encrypted) or in server-side session storage (e.g., Redis linked to a session ID stored in a secure cookie). When verifying, retrieve theverifyIdon the server based on the user's session, not from the client request body.- Protecting API Routes: Ensure only authorized users can initiate the 2FA setup or verification process if it's tied to user accounts. Implement authentication checks in your API routes before calling MessageBird.
- Token Timeout: MessageBird tokens remain valid for 30 seconds by default. Inform users and handle expired tokens gracefully.
Error Handling Best Practices
- API Route Errors: Use
try...catchblocks in API routes. Log detailed errors server-side (including MessageBird error objects) for debugging. Return clear, user-friendly error messages to the frontend, avoiding exposing sensitive details. - MessageBird Errors: Parse the
err.errorsarray provided by the MessageBird SDK callback for specific error codes and descriptions (e.g., invalid phone number, invalid token, expired token). Check thedescriptionfield for the most accurate information. See examples in API route code. Refer to the official MessageBird API documentation for a list of error codes. - Frontend Errors: Display errors received from the API to the user clearly. Handle network errors during
fetchcalls. - Logging: Implement structured logging (e.g., using Pino or Winston) on the server-side to capture request details, successes, and errors for monitoring and troubleshooting.
How to Test Your OTP Implementation
- Manual Testing:
- Run the app (
npm run dev). - Navigate to
/verify. - Enter your real phone number in E.164 format. Click "Send OTP".
- Check your phone for the SMS.
- Enter the received code. Click "Verify Code".
- Test error cases: invalid phone number format, incorrect OTP, expired OTP (wait > 30s), requesting OTP multiple times quickly (test rate limiting if implemented).
- Run the app (
- Unit Testing (API Routes): Use a testing framework like Jest. Mock the
messagebirdSDK to simulate successful and error responses fromverify.createandverify.verifywithout making actual API calls. Test input validation logic. - Integration Testing (Optional): Use tools like Playwright or Cypress to automate browser interactions, simulating the full user flow from entering the phone number to verifying the OTP. This requires careful setup, potentially involving test MessageBird credentials or mocking the API at the network level.
Deploying Your Next.js OTP Application
- Platform: Deploy your Next.js application to platforms like Vercel, Netlify, AWS Amplify, or a traditional Node.js server.
- Environment Variables: Configure
MESSAGEBIRD_API_KEYandDATABASE_URL(if used) as environment variables in your deployment platform's settings. Never hardcode them. - Build Command: Typically
npm run build. - Database: Ensure your deployed application can connect to your production database. Configure connection pooling appropriately.
- HTTPS: Always use HTTPS in production. Deployment platforms usually handle this automatically.
Common Issues and Troubleshooting
- Invalid API Key: Error messages often indicate authentication failure. Double-check your
MESSAGEBIRD_API_KEYin.env.localand your deployment environment variables. Ensure it's a Live key. - Invalid Phone Number: MessageBird expects E.164 format (
+followed by country code and number, e.g.,+14155552671). Ensure your frontend sends this format and your backend validation checks for it. Check theerr.errors[0].descriptionfrom MessageBird for specifics (error code21often relates to invalid parameters). - Alphanumeric Sender ID Not Working: Support for alphanumeric
originatorvalues varies by country (e.g., not supported in the US/Canada). Test with a purchased virtual number from MessageBird as the originator for better reliability, or stick to the default 'Code' / 'MessageBird' if acceptable. - Token Expired / Invalid: The default timeout is 30 seconds. If the user takes longer,
verify.verifywill fail. Handle this error by checking theerr.errors[0].description(which might indicate an invalid or expired token/ID, sometimes associated with code10) and prompt the user to request a new code. Ensure you're using the correctverifyId(especially relevant if using the less secure client-side storage method). Refer to official MessageBird error documentation for definitive code meanings. - Rate Limits: MessageBird enforces rate limits. If you send too many requests too quickly, you'll receive errors. Implement client-side and server-side rate limiting (see Security Considerations).
- Message Not Received: Check MessageBird dashboard logs for delivery status. Ensure the number is correct and the phone has service. Test with different carriers if possible. Temporary carrier issues can occur.
verifyIdHandling: If you handle theverifyIdinsecurely on the client-side, it can be lost (breaking the flow) or potentially compromised. Server-side session storage is strongly recommended for production environments.- Cost: Sending SMS messages incurs costs. Monitor your MessageBird usage.
Complete Code Repository
Coming Soon: A complete working example repository with all the code from this tutorial will be available for reference. This will include the full Next.js project structure, environment configuration examples, and additional security enhancements.
Next Steps: Enhancing Your OTP System
You've successfully built SMS-based OTP verification in Next.js using MessageBird's Verify API. Your implementation includes API routes for sending and verifying codes, a complete frontend UI, and essential security considerations.
Production Checklist:
- Implement server-side session storage for
verifyIdinstead of client-side state - Add rate limiting to prevent abuse and SMS pumping attacks
- Set up monitoring for failed verification attempts
- Configure database integration to persist verification status
- Test with real phone numbers across different carriers and countries
Enhancement Ideas:
- Add voice OTP support (
type: 'tts') as a fallback for users who don't receive SMS - Implement retry logic with exponential backoff
- Create admin dashboard to monitor verification metrics
- Add support for multiple authentication methods (email OTP, authenticator apps)
Additional Resources:
Frequently Asked Questions
How to set up MessageBird OTP in Next.js?
Start by creating a new Next.js project and installing the required dependencies, including the MessageBird Node.js SDK and optionally Prisma for database interactions. Set up environment variables for your MessageBird API key and database URL in a .env.local file. If using Prisma, modify the schema.prisma file to store user data related to 2FA.
What is the MessageBird Verify API?
The MessageBird Verify API is a service that allows you to send and verify one-time passwords (OTPs) via SMS or voice calls. It's used to implement two-factor authentication (2FA) or verify phone numbers, enhancing security and reducing fraudulent sign-ups.
Why use OTP for user authentication?
OTP adds an extra layer of security by requiring a code sent to the user's phone, making it much harder for unauthorized access even if passwords are compromised. This helps protect against account takeovers and strengthens overall security.
When should I implement 2FA using MessageBird?
Implement 2FA whenever enhanced security is needed, like during login, high-value transactions, or account changes. This is especially important for sensitive data or when regulatory compliance requires stronger authentication methods.
Can I customize the OTP message template?
Yes, the MessageBird Verify API allows message customization. You can set the 'template' parameter in the API request. Use '%token' as a placeholder within the template. For example, 'Your verification code is %token.'
How to handle MessageBird API errors in Next.js?
Use try...catch blocks in your API routes to handle errors during OTP requests and verification. Log the detailed error object from the MessageBird SDK for debugging purposes. Return clear and user-friendly error messages to the frontend.
What is the recommended way to store the verification ID?
For production, store the 'verifyId' securely on the server-side. Options include using secure, HTTPOnly cookies (potentially encrypted) linked to the user's session or storing it in server-side session storage like Redis, linked to a session identifier in a secure cookie.
How to verify the OTP code entered by the user?
The frontend sends the 'verifyId' and the user-entered OTP code to the /api/auth/verify-otp route. This route uses the MessageBird SDK to call the verify.verify API method, which checks if the code is valid and responds accordingly. Ensure 'verifyId' is handled securely.
What are the security best practices for MessageBird OTP integration?
Key security measures include protecting your MessageBird API key, implementing rate limiting, rigorous input validation, secure verifyId handling, and protecting API routes with authentication. Ensure only authorized users can initiate or verify 2FA.
How to test the MessageBird OTP implementation effectively?
Testing methods include manual testing of the complete user flow, unit testing API routes with mocked MessageBird responses, and optionally automated integration testing using tools like Playwright or Cypress to simulate browser interactions.
What to do if the user doesn't receive the SMS message?
Check the MessageBird dashboard logs for delivery status. Verify the phone number's correctness and the user's network connectivity. Consider potential temporary carrier issues and test with different carriers if necessary.
What is the E.164 phone number format?
E.164 is an international standard for phone number formatting. It begins with a '+' sign followed by the country code and then the national subscriber number. Example: +1234567890.
Where can I find the complete code example for the MessageBird OTP tutorial?
The tutorial mentions a complete code repository. If not found within the tutorial, contact the tutorial provider or search for relevant repositories online (e.g., on GitHub).
How to handle rate limiting for MessageBird API calls?
Implement rate limiting in your API routes to prevent abuse. This can be done with middleware in Next.js using libraries like 'rate-limiter-flexible', or through platform features (Vercel's IP rate limiting).
What are the common troubleshooting steps for MessageBird OTP issues?
Common issues include incorrect or expired tokens, invalid phone numbers, and API key errors. Make sure your MessageBird API key is valid and stored correctly. Also verify phone number format. Check MessageBird error logs for detailed information and potential carrier-side issues.