In today's digital landscape, where virtual interactions have become the norm, ensuring trust and security has become paramount. One of the most effective ways to achieve this is through phone number verification.
One time passwords (OTPs), whether delivered by SMS, email, or an authenticator app, have been widely used as a form of two-factor authentication in various industries. However, there is a risk of OTPs being intercepted through social engineering, malware, and other fraud techniques. And while OTPs have their place, there is room for an alternative that mitigates those issues.
Enter Number Verify.
What is Number Verify?
Number Verify is a service that checks if the phone number being used in a mobile website or app matches the phone number provided by the customer or stored on their account. It does this in real-time, meaning it happens instantly.
When a customer uses their personal device to log in or use an app, Number Verify quietly checks if the phone number they entered or have associated with their account is correct. If someone tries to log in from a different device with a different phone number, it could be a sign of fraudulent activity, and Number Verify helps identify and prevent that.
In this tutorial, you will:
- Sign up for and configure Number Verify
- Create a Next.js app
- Integrate Number Verify API calls into its front-and backend
- Verify phone numbers
Real-world use cases
The verification of a user's phone number serves as a powerful mechanism to enhance security across a wide range of scenarios. Let's explore a few practical examples:
Financial services
Institutions such as banks or investment platforms may use Number Verify as a way to verify a user's identity, and to provide an extra layer of security when logging in, changing a password, or making transactions.
Online marketplaces
An online marketplace that connects buyers and sellers may require phone number verification for new user registration to ensure that users are legitimate and can be reached if necessary. Number Verify can also help to reduce fraud and promotion by preventing users from creating multiple accounts or sharing account log-in details.
Social media app
A social media app might use Number Verify as a way to validate new user accounts, and to prevent users from creating fake accounts. Similarly, Number Verify can help identify bots, sockpuppet accounts, and shadow accounts used to spam or harass other users.
Apartment listings/viewing
Using Number Verify, a platform can improve the overall user experience, reduce fraudulent activities, and foster a safer and more trustworthy environment for both landlords and renters.
Tutorial requirements
- This tutorial was written with Node.js 18 LTS
- Client ID and Secret from the Developer portal
Simple Number Verify app
We’ll be using Next.js for this application. We’ll be able to leverage its router API to create a backend for our phone number verification to take place, as well as offer a frontend for the end-user to enter their number for verification.
In your terminal, run the following:
npx create-next-app@latest number-verify-app
Answer the prompts accordingly:
Use Typescript: no
Use ESLint: yes
Use Tailwind: yes
Use the `src` directory: no
Use the App Router: yes
Customise default import alias: no
Once this is done, our project is ready to be run! Navigate into it by running:
cd number-verify-app
Then start up the application:
npm run dev
You’ll then find your app running at http://localhost:3000:
We’ll then set up our environment variables. Create the file .env.local and enter the following environment variables:
NEXT_PUBLIC_NV_CLIENT_ID=<Integrate from portal>
NEXT_PUBLIC_API_HOST=<Integrate from portal>
NV_CLIENT_SECRET=<Integrate from portal>
Environment variables prefixed with `NEXT_PUBLIC_` will be made available in the frontend.
Frontend Code
Now we’re able to start writing code. First off, let’s replace the contents in ./app/page.js
:
import VerifyNumber from './_components/VerifyNumber';
export default function Home() {
return (
<main className="flex min-h-screen flex-row items-center justify-center p-24">
<div className="z-10 max-w-5xl font-mono text-sm">
<h1 className="text-xl mb-6">Verify your phone number!</h1>
<VerifyNumber />
</div>
</main>
);
}
This will display a `VerifyNumber`
below the heading “Verify your phone number!”
.
Since our `VerifyNumber`
component file does not exist yet, we’ll create it at ./app/_components/VerifyNumber.js
:
'use client';
import { useState } from 'react';
export default function VerifyNumber() {
const [phoneNumber, setPhoneNumber] = useState('');
const [verifiedStatus, setNumberVerifiedStatus] = useState('');
const handleInputChange = (event) => {
setPhoneNumber(event.target.value);
};
return (
<>
<form className="flex flex-col" onSubmit={handleSubmit}>
<label className="mb-6">
Phone Number:
<input
className="ml-2 border border-indigo-600 p-2"
type="text"
value={phoneNumber}
onChange={handleInputChange}
/>
</label>
<button
className="rounded-full text-white py-4 bg-gradient-to-r from-cyan-500 to-blue-500"
type="submit"
>
Submit
</button>
</form>
{verifiedStatus.length > 0 && <h2>{verifiedStatus}</h2>}
</>
);
}
This is a client-side component, as denoted by “use client”
at the top. It will render a form, where we statefully control the `phoneNumber`
, as well as the `verifiedStatus`
.
One thing you may have noticed is an unimplemented `onSubmit`
callback on the form element. Let's implement handleSubmit()
:
export default function VerifyNumber() {
// useState declarations
const handleSubmit = async (event) => {
event.preventDefault();
try {
const verified = await verify(phoneNumber);
setNumberVerifiedStatus(
`${phoneNumber}: ${verified ? 'verified' :'invalid'}!`,
);
} catch {
setNumberVerifiedStatus('Error validating phone number.');
}
};
}
Our `handleSubmit`
function will call upon our `verify(phoneNumber)`
function to check whether the phone number is verified.
Next, we will implement the verify(number)
function above our component function:
async function verify(phoneNumber) {
const number = hashNumber(phoneNumber);
const code = clientAuth();
const response = await fetch('/api', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ code, number }),
});
const { verified } = await response.json();
return verified;
}
First you’ll see we’re calling `hashNumber`
with our phone number as required by Number Verify API.
Let’s implement it:
import crypto from 'crypto';
//... Other imports
function hashNumber(number) {
return crypto.createHash('sha256').update(number).digest('hex');
}
With that implemented, we’re next going to look at authentication with the Number Verify API.
Three-legged OAuth
Let’s take a moment to cover how authentication with the Number Verify API works:
NOTE: Step 1 needs to be done on a device in the Vodafone network!
-
Client-side authentication: The device sends the provided
`client_id`
to the`/authorize`
endpoint and through a series of redirects also provides the mobile number (MSISDN). The response includes an authorization`code`
. -
Server-side authentication: The server uses the
`client_id`
and`client_secret`
as basic authentication with the`/token`
endpoint, sending along the provided`code`
from step 1. The response includes an`auth_token`
. -
Server-side number verification:
We’re ready to verify our phone number! The server sends the hashed phone number along with the Bearer token authentication from the last step, and in the body passing the hashed phone number.
Response includes
device_msisdn_verified
as a boolean.
Back to the frontend
Let's implement clientAuth():
import { v4 as uuidv4 } from 'uuid';
// Other import code
const api = `${process.env.NEXT_PUBLIC_API_HOST}/deviceinitiated/v1/authorize?`;
async function clientAuth() {
const redirect_uri = 'https://httpbin.org/get';
const params = {
client_id: process.env.NEXT_PUBLIC_NV_CLIENT_ID,
redirect_uri,
scope: 'openid mc_vm_match_hash',
response_type: 'code',
state: uuidv4(),
acr_values: 2,
version: 'mc_di_r2_v2.3',
nonce: uuidv4(),
};
const requestUrl = api + new URLSearchParams(params).toString();
let response = await fetch(requestUrl);
if (response.status != 302) {
throw new Error();
}
let attempts = 1;
while (attempts <= 4 || !response.headers.get('Location').includes(redirect_uri)) {
attempts++;
response = await fetch(response.headers.get('Location'));
}
const locationParams = new URLSearchParams(response.headers.get('Location').split("?")[1])
const code = params.get("code");
if (!code) {
throw new Error();
}
}
First, we’re building our endpoint URL with our environment variable >NEXT_PUBLIC_API_HOST
.
Then in `clientAuth()`
, we’re building our search params out of, amongst others, our `client_id`
from our NEXT_PUBLIC_NV_CLIENT_ID
environment variable.
Our nonce will be generated with the `uuid`
package.
Let’s install it in the terminal:
npm install uuid
We’ll use the `fetch`
API to send a GET request to the `/authorize`
endpoint, and parse the `code`
from the reply, which we return.
Back to verify()
, we see a call to an /api endpoint:
const response = await fetch('/api', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ code, number }),
});
We’re sending this endpoint our auth code and hashed phone number. The endpoint itself is going to be implemented on our backend, powered by Next.js’ Route Handlers.
Next.js’ backend
Create the file ./app/api/route.js
with the following content:
export async function POST(req) {
try {
const { code, number } = await req.json();
const token = accessToken(code);
const verified = verifyNumber(number, token);
return new Response({ verified });
} catch {
new Response({ error: 'Something went wrong' }, { status: 500 });
}
}
Here, we’re grabbing the code from our request and passing it to `accessToken`
to get our auth token.
Here’s the implementation of accessToken()
:
const API = process.env.NEXT_PUBLIC_API_HOST;
async function accessToken(code) {
const tokenHost = `${API}/deviceinitiated/v1/token?`;
const encodedBasicAuth = Buffer.from(`${process.env.NEXT_PUBLIC_NV_CLIENT_ID}:${process.env.NV_CLIENT_SECRET}`).toString('base64')
const Authorization = `Basic ${encodedBasicAuth}`;
const tokenSearchParams = new URLSearchParams({
grant_type: 'authorization_code',
redirect_uri: 'https://httpbin.org/get',
code,
}).toString();
const tokenResponse = await fetch(tokenHost + tokenSearchParams, {
method: 'POST',
headers: { Authorization },
});
const { access_token } = await tokenResponse.json();
return access_token;
}
This is the second step of the three-legged OAuth process we outlined earlier.
We’re passing the `Authorization`
header to the `/token`
endpoint, along with our code in the search params. This will give us a JSON response with an `access_token`
, which we return.
Finally, let's implement verifyNumber(number, token)
:
async function verifyNumber(number, token) {
const verifyHost = `${API}/premiuminfo/v1/`;
const Authorization = `Bearer ${token}`;
const params = {
mc_claims: {
device_msisdn_hash: number,
},
};
const response = await fetch(verifyHost, {
headers: {
Authorization,
'Content-Type': 'application/json',
},
body: JSON.stringify(params),
});
const { device_msisdn_verified } = response.json();
return device_msisdn_verified;
}
We’re passing the `Authorization`
header with our `token`
over to the `/premiumInfo/v1`
endpoint, with the JSON body containing our hashed phone number.
This will respond with a boolean `device_msisdn_verified`
, telling us whether this was successful or not.
With this implemented, we’re done coding and ready to verify! Let’s head over to `localhost:3000`
in our browser from a device in the Vodafone network and try it out by entering our phone number:
Once the authentication is set up, the verification process is highly straightforward.
Expand your two-factor authentication with Number Verify
Two-factor authentication (2FA) is a widely adopted security measure that adds an extra layer of protection to user accounts. It typically involves verifying the user's identity through a combination of something they know (e.g., a password) and something they have (e.g., a keyfob).
However, relying solely on a single device for 2FA can be problematic if said device is lost, stolen, or inaccessible.
To address this issue, an alternative mechanism can be implemented using Number Verify. The above sample app can be used as a standalone mechanism for doing so you could integrate into your existing application.
Start verifying today
Integrating Number Verify into your mobile or web app can simplify phone number verification, enhance user experiences, and strengthen platform security. Take the step to enhance trust and security on your platform by signing up for Number Verify today. Empower your authentication process and provide a smoother, more secure experience for your users.