Login verification
Add phone-based 2FA to your login flow. After the user enters their password, send a verification code to their registered phone number before granting access.Sequence diagram
Server implementation
- Node.js (Express)
- Python (Flask)
Report incorrect code
Copy
Ask AI
import express from 'express';
import Telnyx from 'telnyx';
import jwt from 'jsonwebtoken';
const app = express();
app.use(express.json());
const telnyx = new Telnyx({ apiKey: process.env.TELNYX_API_KEY });
const VERIFY_PROFILE_ID = process.env.TELNYX_VERIFY_PROFILE_ID;
const JWT_SECRET = process.env.JWT_SECRET;
// Step 1: User submits credentials
app.post('/login', async (req, res) => {
const { email, password } = req.body;
// Validate credentials against your database
const user = await db.users.findByEmail(email);
if (!user || !await bcrypt.compare(password, user.password_hash)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Check if user has 2FA enabled
if (!user.phone_number) {
// No 2FA — issue token directly
const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '24h' });
return res.json({ token });
}
try {
// Send verification code
const verification = await telnyx.verifications.sms({
phone_number: user.phone_number,
verify_profile_id: VERIFY_PROFILE_ID,
});
// Store pending verification in session
req.session.pendingLogin = {
userId: user.id,
phoneNumber: user.phone_number,
verificationId: verification.data.id,
expiresAt: Date.now() + 300_000, // 5 minutes
};
// Mask phone number for display
const masked = user.phone_number.replace(/(\+\d{1})\d{6}(\d{4})/, '$1******$2');
res.json({
requires_2fa: true,
phone_hint: masked, // "+1******3456"
timeout_secs: 300,
});
} catch (error) {
console.error('Verification send failed:', error);
res.status(500).json({ error: 'Failed to send verification code' });
}
});
// Step 2: User submits verification code
app.post('/login/verify', async (req, res) => {
const { code } = req.body;
const pending = req.session.pendingLogin;
if (!pending) {
return res.status(400).json({ error: 'No pending verification. Start login again.' });
}
if (Date.now() > pending.expiresAt) {
delete req.session.pendingLogin;
return res.status(410).json({ error: 'Verification expired. Start login again.' });
}
try {
const result = await telnyx.verifications.verify(pending.phoneNumber, {
code,
verify_profile_id: VERIFY_PROFILE_ID,
});
if (result.data.response_code === 'accepted') {
delete req.session.pendingLogin;
const token = jwt.sign({ userId: pending.userId }, JWT_SECRET, { expiresIn: '24h' });
return res.json({ token });
}
res.status(401).json({
error: 'Invalid code',
response_code: result.data.response_code,
});
} catch (error) {
console.error('Verification check failed:', error);
res.status(500).json({ error: 'Verification failed' });
}
});
// Resend code
app.post('/login/resend', async (req, res) => {
const pending = req.session.pendingLogin;
if (!pending) {
return res.status(400).json({ error: 'No pending verification' });
}
try {
const verification = await telnyx.verifications.sms({
phone_number: pending.phoneNumber,
verify_profile_id: VERIFY_PROFILE_ID,
});
pending.verificationId = verification.data.id;
pending.expiresAt = Date.now() + 300_000;
res.json({ message: 'Code resent', timeout_secs: 300 });
} catch (error) {
res.status(500).json({ error: 'Failed to resend code' });
}
});
app.listen(3000);
Report incorrect code
Copy
Ask AI
import os
import time
import jwt
from flask import Flask, request, jsonify, session
from telnyx import Telnyx
app = Flask(__name__)
app.secret_key = os.environ["FLASK_SECRET_KEY"]
telnyx_client = Telnyx(api_key=os.environ["TELNYX_API_KEY"])
VERIFY_PROFILE_ID = os.environ["TELNYX_VERIFY_PROFILE_ID"]
JWT_SECRET = os.environ["JWT_SECRET"]
@app.route("/login", methods=["POST"])
def login():
data = request.json
email, password = data["email"], data["password"]
# Validate credentials
user = db.users.find_by_email(email)
if not user or not bcrypt.check_password(password, user.password_hash):
return jsonify({"error": "Invalid credentials"}), 401
if not user.phone_number:
token = jwt.encode({"user_id": user.id}, JWT_SECRET, algorithm="HS256")
return jsonify({"token": token})
try:
verification = telnyx_client.verifications.sms(
phone_number=user.phone_number,
verify_profile_id=VERIFY_PROFILE_ID,
)
session["pending_login"] = {
"user_id": user.id,
"phone_number": user.phone_number,
"verification_id": verification.data.id,
"expires_at": time.time() + 300,
}
masked = user.phone_number[:2] + "******" + user.phone_number[-4:]
return jsonify({
"requires_2fa": True,
"phone_hint": masked,
"timeout_secs": 300,
})
except Exception as e:
return jsonify({"error": "Failed to send verification code"}), 500
@app.route("/login/verify", methods=["POST"])
def login_verify():
code = request.json["code"]
pending = session.get("pending_login")
if not pending:
return jsonify({"error": "No pending verification"}), 400
if time.time() > pending["expires_at"]:
session.pop("pending_login", None)
return jsonify({"error": "Verification expired"}), 410
try:
result = telnyx_client.verifications.verify(
phone_number=pending["phone_number"],
code=code,
verify_profile_id=VERIFY_PROFILE_ID,
)
if result.data.response_code == "accepted":
session.pop("pending_login", None)
token = jwt.encode({"user_id": pending["user_id"]}, JWT_SECRET, algorithm="HS256")
return jsonify({"token": token})
return jsonify({
"error": "Invalid code",
"response_code": result.data.response_code,
}), 401
except Exception as e:
return jsonify({"error": "Verification failed"}), 500
if __name__ == "__main__":
app.run(port=3000)
Frontend UX guidance
Code input design
Code input design
- Use a single input field or separate digit boxes (4–6 boxes depending on code length)
- Auto-focus the input when the 2FA screen appears
- Auto-submit when the full code length is entered (no submit button needed)
- Accept numeric input only — set
inputmode="numeric"andpattern="[0-9]*" - Enable paste support for users copying from SMS notifications
Countdown timer
Countdown timer
- Show a countdown timer (e.g., “Code expires in 4:32”)
- Disable the Resend button for 30 seconds after sending to prevent rapid resends
- After timeout, show a clear “Code expired” message with a Resend button
Error states
Error states
- Wrong code: “Invalid code. X attempts remaining.” Keep the input active for retry.
- Expired code: “Code expired. Click Resend to get a new code.”
- Max attempts: “Too many attempts. Please start the login process again.”
- Network error: “Could not verify. Check your connection and try again.”
Registration verification
Verify phone number ownership during account creation. This confirms the user has access to the phone number before your application creates the account.Sequence diagram
Server implementation
- Node.js (Express)
- Python (Flask)
Report incorrect code
Copy
Ask AI
import express from 'express';
import Telnyx from 'telnyx';
const app = express();
app.use(express.json());
const telnyx = new Telnyx({ apiKey: process.env.TELNYX_API_KEY });
const VERIFY_PROFILE_ID = process.env.TELNYX_VERIFY_PROFILE_ID;
// Step 1: Start registration — validate and send code
app.post('/register/start', async (req, res) => {
const { name, email, phone_number, password } = req.body;
// Validate input
if (!name || !email || !phone_number || !password) {
return res.status(400).json({ error: 'All fields required' });
}
// Check for existing account
const existing = await db.users.findByEmailOrPhone(email, phone_number);
if (existing) {
return res.status(409).json({
error: existing.email === email
? 'Email already registered'
: 'Phone number already registered'
});
}
try {
// Send verification code
const verification = await telnyx.verifications.sms({
phone_number,
verify_profile_id: VERIFY_PROFILE_ID,
});
// Store pending registration (use Redis/DB in production)
req.session.pendingRegistration = {
name,
email,
phone_number,
password_hash: await bcrypt.hash(password, 12),
verificationId: verification.data.id,
expiresAt: Date.now() + 600_000, // 10 minutes
};
res.json({ verification_sent: true, timeout_secs: 600 });
} catch (error) {
console.error('Verification failed:', error);
res.status(500).json({ error: 'Failed to send verification code' });
}
});
// Step 2: Verify code and create account
app.post('/register/verify', async (req, res) => {
const { code } = req.body;
const pending = req.session.pendingRegistration;
if (!pending) {
return res.status(400).json({ error: 'No pending registration' });
}
if (Date.now() > pending.expiresAt) {
delete req.session.pendingRegistration;
return res.status(410).json({ error: 'Registration expired. Please start again.' });
}
try {
const result = await telnyx.verifications.verify(pending.phone_number, {
code,
verify_profile_id: VERIFY_PROFILE_ID,
});
if (result.data.response_code !== 'accepted') {
return res.status(401).json({
error: 'Invalid verification code',
response_code: result.data.response_code,
});
}
// Phone verified — create the account
const user = await db.users.create({
name: pending.name,
email: pending.email,
phone_number: pending.phone_number,
password_hash: pending.password_hash,
phone_verified: true,
phone_verified_at: new Date(),
});
delete req.session.pendingRegistration;
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '24h' });
res.status(201).json({ user_id: user.id, token });
} catch (error) {
console.error('Registration verification failed:', error);
res.status(500).json({ error: 'Verification failed' });
}
});
app.listen(3000);
Report incorrect code
Copy
Ask AI
import os
import time
import jwt
from flask import Flask, request, jsonify, session
from telnyx import Telnyx
app = Flask(__name__)
app.secret_key = os.environ["FLASK_SECRET_KEY"]
telnyx_client = Telnyx(api_key=os.environ["TELNYX_API_KEY"])
VERIFY_PROFILE_ID = os.environ["TELNYX_VERIFY_PROFILE_ID"]
@app.route("/register/start", methods=["POST"])
def register_start():
data = request.json
name = data.get("name")
email = data.get("email")
phone_number = data.get("phone_number")
password = data.get("password")
if not all([name, email, phone_number, password]):
return jsonify({"error": "All fields required"}), 400
existing = db.users.find_by_email_or_phone(email, phone_number)
if existing:
field = "Email" if existing.email == email else "Phone number"
return jsonify({"error": f"{field} already registered"}), 409
try:
verification = telnyx_client.verifications.sms(
phone_number=phone_number,
verify_profile_id=VERIFY_PROFILE_ID,
)
session["pending_registration"] = {
"name": name,
"email": email,
"phone_number": phone_number,
"password_hash": bcrypt.hash_password(password),
"verification_id": verification.data.id,
"expires_at": time.time() + 600,
}
return jsonify({"verification_sent": True, "timeout_secs": 600})
except Exception as e:
return jsonify({"error": "Failed to send verification code"}), 500
@app.route("/register/verify", methods=["POST"])
def register_verify():
code = request.json["code"]
pending = session.get("pending_registration")
if not pending:
return jsonify({"error": "No pending registration"}), 400
if time.time() > pending["expires_at"]:
session.pop("pending_registration", None)
return jsonify({"error": "Registration expired"}), 410
try:
result = telnyx_client.verifications.verify(
phone_number=pending["phone_number"],
code=code,
verify_profile_id=VERIFY_PROFILE_ID,
)
if result.data.response_code != "accepted":
return jsonify({
"error": "Invalid code",
"response_code": result.data.response_code,
}), 401
user = db.users.create(
name=pending["name"],
email=pending["email"],
phone_number=pending["phone_number"],
password_hash=pending["password_hash"],
phone_verified=True,
)
session.pop("pending_registration", None)
token = jwt.encode({"user_id": user.id}, os.environ["JWT_SECRET"], algorithm="HS256")
return jsonify({"user_id": user.id, "token": token}), 201
except Exception as e:
return jsonify({"error": "Verification failed"}), 500
if __name__ == "__main__":
app.run(port=3000)
Payment verification
Add step-up authentication for high-value transactions. When a user initiates a payment above a threshold, require phone verification before processing.Sequence diagram
Server implementation
- Node.js (Express)
- Python (Flask)
Report incorrect code
Copy
Ask AI
import express from 'express';
import Telnyx from 'telnyx';
const app = express();
app.use(express.json());
const telnyx = new Telnyx({ apiKey: process.env.TELNYX_API_KEY });
const VERIFY_PROFILE_ID = process.env.TELNYX_VERIFY_PROFILE_ID;
const VERIFICATION_THRESHOLD = 100.00; // Require verification above $100
// Middleware: require authenticated user
function requireAuth(req, res, next) {
// Your auth middleware here
next();
}
// Step 1: Initiate payment
app.post('/payments/initiate', requireAuth, async (req, res) => {
const { amount, currency, recipient, description } = req.body;
const user = req.user;
if (amount <= 0) {
return res.status(400).json({ error: 'Invalid amount' });
}
// Create pending payment record
const payment = await db.payments.create({
user_id: user.id,
amount,
currency: currency || 'USD',
recipient,
description,
status: 'pending',
});
// Check if verification is needed
if (amount >= VERIFICATION_THRESHOLD) {
try {
const verification = await telnyx.verifications.sms({
phone_number: user.phone_number,
verify_profile_id: VERIFY_PROFILE_ID,
});
await db.payments.update(payment.id, {
verification_id: verification.data.id,
verification_required: true,
verification_expires_at: new Date(Date.now() + 300_000),
});
return res.json({
payment_id: payment.id,
requires_verification: true,
amount,
currency: currency || 'USD',
timeout_secs: 300,
});
} catch (error) {
await db.payments.update(payment.id, { status: 'failed' });
return res.status(500).json({ error: 'Failed to send verification' });
}
}
// Low-value payment — process directly
const result = await processPayment(payment);
res.json({ payment_id: payment.id, status: result.status });
});
// Step 2: Verify and process payment
app.post('/payments/confirm', requireAuth, async (req, res) => {
const { payment_id, code } = req.body;
const user = req.user;
const payment = await db.payments.findById(payment_id);
if (!payment || payment.user_id !== user.id) {
return res.status(404).json({ error: 'Payment not found' });
}
if (payment.status !== 'pending') {
return res.status(400).json({ error: `Payment already ${payment.status}` });
}
if (new Date() > payment.verification_expires_at) {
await db.payments.update(payment.id, { status: 'expired' });
return res.status(410).json({ error: 'Verification expired. Initiate a new payment.' });
}
try {
const result = await telnyx.verifications.verify(user.phone_number, {
code,
verify_profile_id: VERIFY_PROFILE_ID,
});
if (result.data.response_code !== 'accepted') {
return res.status(401).json({
error: 'Invalid verification code',
response_code: result.data.response_code,
});
}
// Verification passed — process payment
await db.payments.update(payment.id, {
verified_at: new Date(),
status: 'processing',
});
const paymentResult = await processPayment(payment);
res.json({
payment_id: payment.id,
status: paymentResult.status,
transaction_id: paymentResult.transaction_id,
});
} catch (error) {
console.error('Payment verification failed:', error);
res.status(500).json({ error: 'Verification failed' });
}
});
async function processPayment(payment) {
// Integrate with your payment processor (Stripe, etc.)
// This is a placeholder
await db.payments.update(payment.id, { status: 'completed' });
return { status: 'completed', transaction_id: `txn_${Date.now()}` };
}
app.listen(3000);
Report incorrect code
Copy
Ask AI
import os
import time
from flask import Flask, request, jsonify, g
from telnyx import Telnyx
app = Flask(__name__)
telnyx_client = Telnyx(api_key=os.environ["TELNYX_API_KEY"])
VERIFY_PROFILE_ID = os.environ["TELNYX_VERIFY_PROFILE_ID"]
VERIFICATION_THRESHOLD = 100.00
@app.route("/payments/initiate", methods=["POST"])
@require_auth
def initiate_payment():
data = request.json
amount = data["amount"]
user = g.user
payment = db.payments.create(
user_id=user.id,
amount=amount,
currency=data.get("currency", "USD"),
recipient=data["recipient"],
description=data.get("description", ""),
status="pending",
)
if amount >= VERIFICATION_THRESHOLD:
try:
verification = telnyx_client.verifications.sms(
phone_number=user.phone_number,
verify_profile_id=VERIFY_PROFILE_ID,
)
db.payments.update(payment.id, {
"verification_id": verification.data.id,
"verification_required": True,
"verification_expires_at": time.time() + 300,
})
return jsonify({
"payment_id": payment.id,
"requires_verification": True,
"amount": amount,
"timeout_secs": 300,
})
except Exception:
db.payments.update(payment.id, {"status": "failed"})
return jsonify({"error": "Failed to send verification"}), 500
result = process_payment(payment)
return jsonify({"payment_id": payment.id, "status": result["status"]})
@app.route("/payments/confirm", methods=["POST"])
@require_auth
def confirm_payment():
data = request.json
payment = db.payments.find_by_id(data["payment_id"])
user = g.user
if not payment or payment.user_id != user.id:
return jsonify({"error": "Payment not found"}), 404
if payment.status != "pending":
return jsonify({"error": f"Payment already {payment.status}"}), 400
if time.time() > payment.verification_expires_at:
db.payments.update(payment.id, {"status": "expired"})
return jsonify({"error": "Verification expired"}), 410
try:
result = telnyx_client.verifications.verify(
phone_number=user.phone_number,
code=data["code"],
verify_profile_id=VERIFY_PROFILE_ID,
)
if result.data.response_code != "accepted":
return jsonify({
"error": "Invalid code",
"response_code": result.data.response_code,
}), 401
db.payments.update(payment.id, {"status": "processing", "verified_at": time.time()})
payment_result = process_payment(payment)
return jsonify({
"payment_id": payment.id,
"status": payment_result["status"],
})
except Exception:
return jsonify({"error": "Verification failed"}), 500
if __name__ == "__main__":
app.run(port=3000)
Security considerations for payment verification:
- Always verify the payment amount hasn’t been tampered with between initiation and confirmation
- Set short timeouts (5 minutes) for payment verification codes
- Log all verification attempts for audit purposes
- Never expose payment details in verification messages — the SMS should only contain the code
Common patterns across all flows
Rate limiting resend requests
Prevent abuse by limiting how often users can request new codes:Report incorrect code
Copy
Ask AI
// In-memory rate limiter (use Redis in production)
const resendLimits = new Map();
function canResend(phoneNumber) {
const key = phoneNumber;
const now = Date.now();
const limit = resendLimits.get(key);
if (limit && now - limit.lastSent < 30_000) {
return { allowed: false, retryAfter: Math.ceil((30_000 - (now - limit.lastSent)) / 1000) };
}
if (limit && limit.count >= 5 && now - limit.firstSent < 3600_000) {
return { allowed: false, retryAfter: Math.ceil((3600_000 - (now - limit.firstSent)) / 1000) };
}
return { allowed: true };
}
function recordResend(phoneNumber) {
const key = phoneNumber;
const now = Date.now();
const existing = resendLimits.get(key);
resendLimits.set(key, {
lastSent: now,
firstSent: existing?.firstSent || now,
count: (existing?.count || 0) + 1,
});
}
Handling user cancellation
Always clean up pending verifications when users navigate away:Report incorrect code
Copy
Ask AI
// Frontend: clean up on unmount
useEffect(() => {
return () => {
if (pendingVerification) {
fetch('/verify/cancel', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ verification_id: pendingVerification }),
keepalive: true, // Ensures the request completes even during navigation
});
}
};
}, [pendingVerification]);
Webhook-driven verification
Instead of polling, use webhooks for real-time verification status updates:Report incorrect code
Copy
Ask AI
app.post('/webhooks/verify', (req, res) => {
const event = req.body.data;
if (event.event_type === 'verification.completed') {
const { phone_number, status } = event.payload;
// Update your UI via WebSocket or SSE
notifyClient(phone_number, { verified: status === 'accepted' });
}
res.sendStatus(200);
});
For more on webhooks, see Receiving Webhooks for Telnyx Verify.