This guide provides a complete OTP authentication system for your Vite React app using 2Factor.in API with proper backend/frontend separation to avoid CORS issues.
skillbench-webapp/
βββ backend-example/ # Node.js + Express backend
β βββ services/
β β βββ OTPService.js # 2Factor.in API integration
β βββ routes/
β β βββ otp.js # OTP API endpoints
β βββ middleware/
β β βββ errorHandler.js # Error handling
β βββ server.js # Express server setup
β βββ package.json # Dependencies
β βββ .env.example # Environment variables
β βββ README.md # Backend documentation
βββ src/
β βββ services/
β β βββ OTPService.clean.js # Frontend OTP service
β β βββ FirebaseOTPService.js # Firebase integration
β βββ components/auth/
β β βββ OTPLogin.jsx # Basic OTP UI component
β β βββ FirebaseOTPLogin.jsx # Complete Firebase OTP component
β β βββ hooks/
β β βββ useOTPAuth.js # OTP authentication hook
β β βββ useFirebaseOTPAuth.js # Firebase OTP authentication hook
β βββ pages/
β βββ LoginExample.jsx # Basic usage example
β βββ LoginFirebaseExample.jsx # Firebase usage example
βββ IMPLEMENTATION_GUIDE.md # This guide
cd backend-example
npm installcp .env.example .envEdit .env:
TWOFACTOR_API_KEY=your-2factor-api-key-here
PORT=3001
ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000# Development
npm run dev
# Production
npm start# Health check
curl http://localhost:3001/health
# Get API key
curl http://localhost:3001/otpget
# Send OTP
curl -X POST http://localhost:3001/send-otp \
-H "Content-Type: application/json" \
-d '{"phone":"9876543210"}'
# Verify OTP
curl -X POST http://localhost:3001/verify-otp \
-H "Content-Type: application/json" \
-d '{"sessionId":"session-id-here","otp":"123456"}'Remove test mode and configure backend URL in your React app .env:
# Backend configuration
VITE_BACKEND_URL=http://localhost:3001
# Remove test mode
# VITE_FORCE_TEST_MODE=trueUse the basic OTP service for simple phone verification:
import React from 'react';
import OTPLogin from '../components/auth/OTPLogin';
const LoginPage = () => {
const handleSuccess = (result) => {
console.log('OTP verified:', result);
// Handle successful verification
};
const handleError = (error) => {
console.error('OTP error:', error);
// Handle errors
};
return (
<OTPLogin
onSuccess={handleSuccess}
onError={handleError}
/>
);
};Use the Firebase OTP service for complete authentication:
import React from 'react';
import FirebaseOTPLogin from '../components/auth/FirebaseOTPLogin';
const LoginPage = () => {
const handleSuccess = (result) => {
if (result.userExists) {
// Existing user signed in
console.log('User signed in:', result.firebaseUser);
} else {
// New user created
console.log('New user created:', result.firebaseUser);
}
};
const handleError = (error, step) => {
console.error(`Error at ${step}:`, error);
};
return (
<FirebaseOTPLogin
onSuccess={handleSuccess}
onError={handleError}
options={{
redirectToRegistration: '/complete-profile',
redirectToHome: '/dashboard'
}}
/>
);
};Create your own UI using the authentication hooks:
import React, { useState } from 'react';
import { useFirebaseOTPAuth } from '../components/auth/hooks/useFirebaseOTPAuth';
const CustomLoginForm = () => {
const [phone, setPhone] = useState('');
const [otp, setOtp] = useState('');
const {
loading,
error,
message,
step,
sendOTP,
authenticateWithOTP
} = useFirebaseOTPAuth();
const handleSendOTP = async () => {
await sendOTP(phone);
};
const handleVerifyOTP = async () => {
await authenticateWithOTP(`+91${phone}`, otp);
};
return (
<div>
{step === 'phone' && (
<div>
<input
value={phone}
onChange={(e) => setPhone(e.target.value)}
placeholder="Enter phone number"
/>
<button onClick={handleSendOTP} disabled={loading}>
Send OTP
</button>
</div>
)}
{step === 'otp' && (
<div>
<input
value={otp}
onChange={(e) => setOtp(e.target.value)}
placeholder="Enter OTP"
/>
<button onClick={handleVerifyOTP} disabled={loading}>
Verify OTP
</button>
</div>
)}
{error && <p style={{color: 'red'}}>{error}</p>}
{message && <p style={{color: 'green'}}>{message}</p>}
</div>
);
};The system uses this Firestore structure (matching your Flutter app):
skillbench/
βββ phone_to_uid/
β βββ mappings/
β βββ {phone_number}/ # e.g., "+919876543210"
β βββ uid: "firebase_uid"
β βββ phoneNumber: "+919876543210"
β βββ createdAt: timestamp
β βββ lastUpdated: timestamp
βββ users/
βββ users/
βββ {firebase_uid}/ # User profile data
βββ phoneNumber: "+919876543210"
βββ username: "user_name"
βββ college: "college_name"
βββ currentFirebaseUid: "firebase_uid"
- Phone Input β User enters phone number
- Send OTP β Backend calls 2Factor.in API
- OTP Verification β Backend verifies OTP with 2Factor.in
- User Lookup β Check Firestore for existing phone-to-UID mapping
- Firebase Auth:
- Existing user: Sign in with
{phone}@skillbench.temp/temp_{phone} - New user: Create Firebase account and phone-to-UID mapping
- Existing user: Sign in with
- Redirect:
- Existing user: Redirect to home/dashboard
- New user: Redirect to registration/profile completion
- CORS protection with configurable origins
- Helmet.js security headers
- Input validation and sanitization
- Error message sanitization (no sensitive data exposure)
- Rate limiting (built into 2Factor.in API)
- No API keys exposed in frontend code
- Session management with localStorage
- Input validation for phone numbers and OTP codes
- Automatic session cleanup on verification success/failure
# Install backend dependencies
cd backend-example && npm install
# Start backend server
npm run dev
# Test endpoints manually or with automated tests# Remove test mode from .env
# VITE_FORCE_TEST_MODE=true
# Start frontend
npm run dev
# Test with real phone numbers and OTP codes- Real testing: Use your actual phone number
- 2Factor.in test numbers: Check 2Factor.in documentation for test numbers
- Deploy to your preferred platform (Railway, Render, Vercel, etc.)
- Set environment variables:
TWOFACTOR_API_KEY=your-api-key PORT=3001 ALLOWED_ORIGINS=https://your-frontend-domain.com - Update frontend
VITE_BACKEND_URLto point to deployed backend
- Update
.envwith production backend URL:VITE_BACKEND_URL=https://your-backend-domain.com
- Deploy to your preferred platform (Vercel, Netlify, etc.)
If you're currently using test mode, follow these steps:
- Setup Backend: Follow backend setup instructions above
- Update Frontend: Remove
VITE_FORCE_TEST_MODE=truefrom.env - Configure Backend URL: Set
VITE_BACKEND_URLin.env - Test Integration: Verify OTP flow works with real phone numbers
- Deploy: Deploy both backend and frontend
Health check endpoint.
Returns 2Factor.in API key (legacy compatibility).
Send OTP to phone number.
// Request
{
"phone": "9876543210"
}
// Response
{
"success": true,
"sessionId": "session-id",
"message": "OTP sent successfully",
"phone": "+919876543210"
}Verify OTP code.
// Request
{
"sessionId": "session-id",
"otp": "123456"
}
// Response
{
"success": true,
"message": "OTP verified successfully"
}Basic OTP operations without Firebase integration.
Methods:
sendOTP(phoneNumber)- Send OTPverifyOTP(otp, sessionId)- Verify OTPresendOTP()- Resend OTPgetSessionInfo()- Get session dataclearSessionId()- Clear session
Complete OTP + Firebase authentication.
Methods:
sendOTP(phoneNumber)- Send OTPauthenticateWithOTP(phoneNumber, otp)- Complete auth flowgetUidByPhoneNumber(phoneNumber)- Check existing usersignOut()- Sign out user
If you encounter issues:
- Check logs: Backend logs show detailed error information
- Verify configuration: Ensure all environment variables are set
- Test endpoints: Use curl or Postman to test backend directly
- Check CORS: Ensure frontend URL is in
ALLOWED_ORIGINS - Verify 2Factor.in: Check your 2Factor.in account balance and API key
- Environment Variables: Never commit API keys to version control
- Error Handling: Always handle errors gracefully with user-friendly messages
- Validation: Validate inputs on both frontend and backend
- Logging: Log important events for debugging (without sensitive data)
- Session Management: Clear sessions on sign-out and errors
- User Experience: Provide clear feedback during the authentication flow
This implementation provides a production-ready OTP authentication system that mirrors your Flutter app architecture while avoiding CORS issues through proper backend/frontend separation.