Web and mobile applications that allow users to sign up must balance ease of onboarding with signup spam. Often the signup spam comes in waves over a few hours or days, rather than in a constant stream. In this tutorial, we’ll build an ExpressJS demo app that has the flexibility to toggle extra verification layers on during spam attacks.
This demo combines Twilio Verify, a fraud verification API that can be used across many communications channels, with LaunchDarkly feature flags. Take that, spammers.
Development configuration and dependencies
To complete this tutorial you'll need:
- A LaunchDarkly account. Sign up for a free one here
- A Twilio account. You can use a free trial account, although some features will show up slightly differently
- A developer environment with git, npm, and Node.js installed
- A cell phone that can receive SMS messages
What is verification?
When a user adds new credentials to their account, that’s verification. Verification can happen during sign-up or when updating an account to add a new phone number or email address.
Verification is different from authentication (although you can use the Verify API for both.) Authentication happens on an ongoing basis during potentially risky interactions, such as:
- when a user logs in to their account
- high-dollar transactions
- password reset flows
- deleting data
In our verification flow, the user enters their phone number during signup. They will be sent a verification code. Once they receive the code, they enter it into a form on the app to prove that the phone number belongs to them.
Although we’re only using SMS here to keep things simple, Twilio Verify supports many other channels such as email and WhatsApp. To learn more about verification, read Twilio’s docs.
This tutorial starts with an example app that includes a signup flow. This app was inspired by my pet rabbit, Hester, who cares deeply about optimizing carrot acquisition processes.
First we’ll get the example app up and running. Then we’ll add Twilio credentials and test the verification flow. Finally, let’s add a LaunchDarkly feature flag to enable and disable the extra verification step at will.
Getting started with an ExpressJS and Twilio Verify example application
Run the following commands in your terminal to clone the repository, install dependencies, and start the server.
git clone https://github.com/annthurium/expressjs-twilio-verify-starter
cd expressjs-twilio-verify-starter
npm install
npm start
Open http://localhost:3000/signup.html in a browser and you’ll see this form:
To sign up, the user will input their phone number and a password.
This form isn’t fully plumbed yet. When the full tutorial has been completed, Twilio will send the user a verification code via SMS. On the next page of the form, the user will input that verification code to complete the signup process.
Creating a Twilio Verify service
Log in to your Twilio account or create a new one.
Open the env.example file in your editor. Grab your Twilio account SID and auth token from the console.
If you’re using a free trial account, you’ll find these right in the center of your screen:
Paste these credentials into your .env.example file. Rename the file to .env and save it. This step ensures that you don’t accidentally commit credentials to source control and compromise security.
Go to this link to create a new Verify Service so we can configure our verification flow.
Name your service “launchdarkly-signup-form.” When Twilio sends your users the verification code, it will show them the service name. Select the “SMS” verification channel. Check the box that says “Authorize the use of friendly name” to ensure that you have the right to this name and that it can be used in SMS communication.
Optional but recommended: enable Fraud Guard. On the next screen, you’ll see a Verify service SID. Copy and paste that value into your .env file and save it.
Open index.js in your code editor. Uncomment line 12 to initialize the Twilio server.
const twilioClient = twilio(accountSid, authToken);
Restart the server just in case it didn’t automatically pick up the new variables. Reload http://localhost:3000/signup.html in your browser. Type your cell phone number and a password into the form. Click “Submit.”
On your cell phone, you should receive a SMS verification code from Twilio: “Your launchdarkly-signup-form verification code is: 12345”. If your Twilio account is in free trial mode, this message will say SAMPLE APP instead of the Verify Service name.
Part of Verify's magic is that you don’t need to provision a phone number—the API automatically handles that for you.
This app uses Twilio Lookup’s phone number formatting capabilities to gracefully handle imperfectly formatted user input. It also passes the user’s phone number back through to the verification form, so they don’t need to retype it.
Type the verification code into the form and click “Submit.” You’ll see a confirmation page.
Adding a LaunchDarkly flag to conditionally enable SMS verification
Head over to the LaunchDarkly app. Click on either of the “Create flag” buttons.
Create a flag using this configuration:
- Name: extra-verification-new-users
- Description: When flag is enabled, require verification of the user’s phone number in order to complete the signup flow.
- Configuration: Custom
- Type: boolean
Set up the variations like so:
- extra verification on: true
- extra verification off: false
Click “Create flag” at the bottom of the page. Turn your flag On. Click "Review and save."
If LaunchDarkly is configured to require confirmation, you'll need to add a comment to explain why you're enabling the flag. In this case it's a spam attack. Type the comment. Click “Save changes.”
You’ll need the LaunchDarkly SDK key to evaluate a feature flag in the app. Click the menu with 3 dots. Select “SDK Key” from the dropdown menu to copy it. Paste the SDK key into your .env file. Save the .env file, which is the last configuration step.
Next, we’ll tweak this code to add a LaunchDarkly feature flag. Open index.js. You can copy the whole file, or modify the commented functions and lines below for a more detailed understanding.
const express = require("express");
const path = require("path");
const serveStatic = require("serve-static");
const bodyParser = require("body-parser");
require("dotenv").config();
const app = express();
const twilio = require("twilio");
// add the following line to import the LaunchDarkly Server SDK
const LaunchDarkly = require("@launchdarkly/node-server-sdk");
const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const twilioClient = twilio(accountSid, authToken);
const verifyServiceSid = process.env.VERIFY_SERVICE_SID;
async function createSMSVerification(phoneNumber) {
const verification = await twilioClient.verify.v2
.services(verifyServiceSid)
.verifications.create({
channel: "sms",
to: phoneNumber,
});
console.log("Twilio verification created:", verification.sid);
}
// add the following line to initialize the LaunchDarkly client
const ldClient = LaunchDarkly.init(process.env.LAUNCHDARKLY_SDK_KEY);
app.use(serveStatic(path.join(__dirname, "public")));
app.use(bodyParser.urlencoded({ extended: false }));
async function formatPhoneNumber(phoneNumber) {
try {
const lookup = await twilioClient.lookups.v2
.phoneNumbers(phoneNumber)
.fetch();
return lookup.phoneNumber;
} catch (error) {
console.error("Error formatting phone number:", error);
return phoneNumber; // Return original number if lookup fails
}
}
async function checkVerificationCode(phoneNumber, verificationCode) {
try {
const verificationCheck = await twilioClient.verify.v2
.services(verifyServiceSid)
.verificationChecks.create({
to: phoneNumber,
code: verificationCode,
});
console.log(`Verification status: ${verificationCheck.status}`);
return verificationCheck.status === "approved";
} catch (error) {
console.error("Error checking verification code:", error);
return false;
}
}
// modify this function to add a LaunchDarkly flag evaluation
// and logic to use the flag to determine whether to show the extra verification step
app.post("/verify", async (req, res) => {
const context = {
kind: "user",
anonymous: true,
key: "anonymous-123",
};
const extraVerification = await ldClient.variation(
"extra-verification-new-users",
context,
false
);
console.log("extraVerification", extraVerification);
if (extraVerification === false) {
return res.redirect("/success.html");
}
const phoneNumber = req.body["phone"];
// Use Twilio Lookup to format the phone number as E.164
const e164PhoneNumber = await formatPhoneNumber(phoneNumber);
await createSMSVerification(e164PhoneNumber);
// Redirect to the verification form page with the phone number as a query parameter
// so the user doesn't have to re-enter it
const encodedPhoneNumber = encodeURIComponent(e164PhoneNumber);
res.redirect(`/verification-form.html?phone=${encodedPhoneNumber}`);
});
app.post("/submit-verification-code", async (req, res) => {
const verificationCode = req.body["verification-code"];
const phoneNumber = req.body["phone"];
if (!phoneNumber || !verificationCode) {
return res
.status(400)
.send("Phone number and verification code are required.");
}
const isVerified = await checkVerificationCode(phoneNumber, verificationCode);
if (isVerified) {
res.redirect("/success.html");
} else {
res.status(400).send("Invalid verification code. Please try again.");
}
});
// Add the waitForInitialization function to ensure the client is ready before starting the server
const timeoutInSeconds = 5;
ldClient.waitForInitialization({ timeout: timeoutInSeconds }).then(() => {
const port = 3000;
const server = app.listen(port, function (err) {
if (err) console.log("Error in server setup");
console.log(`Server listening on http://localhost:${port}`);
});
});
// Add the following new function to gracefully close the connection to the LaunchDarkly server.
process.on("SIGTERM", () => {
debug("SIGTERM signal received: closing HTTP server");
ld.close();
server.close(() => {
debug("HTTP server closed");
ldClient.close();
});
});
If you go through the signup flow at http://localhost:3000/signup.html, SMS verification should be enabled by default. 💥
Try turning off the feature flag. In the LaunchDarkly app, toggle the extra-verification-new-users flag off.
If you try again, the signup flow should take you straight through to the success page rather than requiring SMS verification.
Conclusion: using LaunchDarkly feature flags to stop fraudulent signup requests
If you’ve been following along, you’ve integrated LaunchDarkly feature flags into your signup flow to conditionally enable Twilio Verify during spam spikes. Nice job!
As engineers, we often wrangle two opposing forces such as ease of use and fraud. Tooling that is flexible enough to let us make adjustments based on local conditions is hugely helpful, especially when dealing with a mission-critical path like signups.
If you enjoyed this post, you might be interested in:
- Using LaunchDarkly with anonymous contexts
- Upgrade your APIs safely with Progressive Rollouts in an ExpressJS Application
- Creating customized user experiences using Express JS and LaunchDarkly segment targeting
Thanks for reading! If you have any questions, or you want to see some cute rabbit pics, you can reach me via email (tthurium@launchdarkly.com), X/Twitter, Mastodon, Discord or LinkedIn.