How to Detect a SIM Swap With PHP Before Sending an SMS OTP
Time to read: 7 minutes
One-time passwords (OTPs) sent via SMS are often a great solution for helping people better protect access to their online accounts. However, it's not a perfect solution, because phones are vulnerable to SIM swap attacks.
A SIM swap attack happens when a malicious actor convinces someone working for a cellular phone carrier to link a user's phone number to a new SIM under their control. This SIM can be a physical or a newer embedded SIM or eSIM.
After the phone number is relinked, the genuine user loses control of their number. The malicious user is then able to receive all calls and SMS sent to the phone number, allowing them to receive any OTPs delivered via SMS.
Twilio now offers SIM swap detection through the Lookup API, which reduces the possibility of SIM swap attacks.
In this tutorial you will learn what SIM swap detection is and how it lets you detect attacks using the new SIM Swap package in Twilio's Lookup API.
Prerequisites
To follow along with this tutorial you will need the following:
- PHP 8.0 or above
- Composer installed globally
- A free Twilio account
- Your preferred editor or IDE (I recommend PhpStorm)
What is SIM swap detection?
Lookup SIM Swap (SIM swap detection) provides real-time authoritative data, directly sourced from mobile network operators, telling you if the SIM linked to a mobile phone number has recently changed…It can help you assess the potential risk that a mobile phone number, and the associated user's account, has been potentially compromised.
What is the SIM Swap package?
When requested, Twilio's Lookup API will return an additional element named sim_swap
, which you can see highlighted in the example response below.
Here's are the details of the key fields:
- last_sim_swap_date: This is an ISO-8601 date and timestamp showing when the SIM was last changed for the specified mobile phone number. Be aware that this is only returned for certain countries.
- swapped_period: This is an ISO-8601 duration representing a trailing time period, during which
swapped_in_period
indicates if the SIM was changed for the specified mobile phone number. - swapped_in_period: This is a boolean, indicating whether the SIM was changed for the specified mobile phone number during the trailing duration (
swapped_period
). - error_code: This contains the error code associated with the request, if any was returned.
You can find out more about all returned fields in the documentation.
Create the project directory structure
As always, start off by creating the project's directory structure and switch into it, by running the commands below.
Set the required environment variables
You next need to set three environment variables, to interact with Twilio's Lookup API. These are your Twilio Account SID, Auth Token, and phone number.
To do this, create a new file named .env in the top-level directory of the project, and paste the configuration below.
After that, replace the PHONE_NUMBER
's placeholder with the phone number that you want to check.
Set your Twilio credentials
Next, from the Twilio Console's Dashboard, copy your Account SID and Auth Token and paste them in place of the respective placeholder values (TWILIO_ACCOUNT_SID
, TWILIO_AUTH_TOKEN
) in .env.
Install the dependencies
With the project's directory structure created, it's time to install the three required dependencies; these are:
laminas-hydrator | laminas-hydrator simplifies hydrating objects (or populating an object from a set of data) and extracting data from them. |
PHP Dotenv | PHP Dotenv populates PHP's `$_SERVER` and `$_ENV` superglobals from a configuration file. This encourages keeping sensitive configuration details out of code and version control. |
Twilio’s PHP Helper Library | This package reduces the effort required to interact with Twilio's APIs. |
To install the packages, in the top-level directory of the project, run the command below.
Then, open composer.json in your preferred editor or IDE and add the following after the require
property.
Write the PHP code
Now, it's time to write the code!
LastSimSwap
In the src/SimSwap directory, create a new file named LastSimSwap.php, and in it, paste the code below.
This class will model the last_sim_swap
element in the response from the Lookup API.
The key item to focus on is the swappedPeriodToString()
function. This function returns a human-readable representation of the ISO 8601 duration contained in swappedPeriod
.
It does this by iterating over swappedPeriod
's date and time properties which have a value greater than zero, to build a string from that information. For example, if it was set to P1DT2H29M
then 1 day, 2 hours, and 29 minutes
would be returned.
SimSwap
Next, create another new file, in src/SimSwap, named SimSwap.php, and in it paste the code below.
This class models the sim_swap
element in the response and uses the LastSimSwap
class, via composition, to store the last_sim_swap
information. It has a getErrorReason()
method to simplify determining the cause of an error, should one be returned.
DateIntervalStrategy
Next, in src/SimSwap/Hydrator/Strategy, create a new file named DateIntervalStrategy.php, and in that file, paste the following code.
This class will hydrate an object from an ISO 8601 duration using the hydrate()
method. If the value supplied to the function ($value
) is already a DateInterval
instance, then the value is returned. If $value
is not a string, then an InvalidArgumentException
is thrown. Finally, if $value
is a string, then a DateInterval
object is instantiated with it and returned.
SimSwapFactory
Then, create a third new file in src/SimSwap, named SimSwapFactory.php, and in it paste the code below.
This class defines only one method, factory()
. As the image above attempts to show, it hydrates a new SimSwap
object from the sim_swap
element of the Lookup API's response.
I appreciate that there is a bit going on here – especially if you're new to hydration – and laminas-hydrator in particular. However, in short, the intent of using the package is to avoid writing the hydration code manually. There's no sense in doing that if there is a perfectly good package already available. Right?
factory()
starts off by hydrating a LastSimSwap
object, with the data from sim_swap
's last_sim_swap
element. It does this with a ReflectionHydrator ($lastSimSwapHydrator
).
As the name suggests, ReflectionHydrator
uses PHP's Reflection API to introspect LastSimSwap
and match its properties to the appropriate keys in the provided data array.
While this reduces the code required to hydrate LastSimSwap
, as the data keys and property names differ ReflectionHydrator can't fully determine that LastSimSwap::lastSimSwapDate
should be initialised with the value of the last_sim_swap_date
. It also can't auto-determine that it has to initialise LastSimSwap::lastSimSwapDate
as a DateTimeImmutable instance.
So, a series of Strategies will be employed to help the hydrator make properly informed decisions.
Strategy #1 - Match the data keys to the object's properties
UnderscoreNamingStrategy is used to map the keys in the data array to the properties of the class. It does this by converting each data key from snake case to camel case and assigning the key's value to that property, if available. For example, lastSimSwapDate
would be hydrated from the value of last_sim_swap_date
.
Strategy #2 - Properly initialise the object's properties
Then, it uses the DateTimeFormatterStrategy and DateTimeImmutableFormatterStrategy
to initialise lastSimSwapDate
as a DateTimeImmutable
object from last_sim_swap_date
's value.
After that, it uses DateIntervalStrategy
to initialise swappedPeriod
as a DateInterval
object based on swapped_period
's value, and ScalarTypeStrategy::createToBoolean()
to initialise swappedInPeriod
as a boolean representation of swapped_in_period
's value.
After that, another ReflectionHydrator
is initialised to hydrate a SimSwap
object. It works in almost the same way as the first hydrator. The only additional item is that it uses $lastSimSwapHydrator
to hydrate the lastSimSwap
property.
index.php
Now, there's one final file to create, index.php. Create this file in the project's top-level directory and paste the code below into it.
Similar to my other tutorials, this file uses PHP Dotenv to set environment variables from the variables in .env, which you created earlier in the tutorial. After that, it initialises a new Twilio Client object for brokering the interaction with the Lookup API.
Then, it makes a call to the Lookup API, specifying the phone number to query for and that the response should contain, if available, the Sim Swap information. Following that, it passes the sim_swap
element of the response to SimSwapFactory::factory()
to initialise a SimSwap
object.
After that, it finishes up by printing a message to the terminal, based on the SIM swap data returned in the request.
If no SIM swap data is available, the user is told that. If SIM swap data is available, the last SIM swap date is printed out, if it was provided. If the last SIM swap date was not returned, the user is told if the SIM was swapped within the SIM swap period or not.
There are legitimate reasons for SIMs to be swapped
While SIM swaps can be suspicious, there are also legitimate reasons for a SIM to be swapped. For example, you lost your phone or it was stolen; you want to port your number to a different carrier; you bought a new phone. Please bear these in mind.
Given these reasons, it's hard to programmatically determine if a SIM swap is suspicious or not. The SIM swap data should always be used as part of a larger, more holistic determination.
Test the code
To test the code, run the following command.
What was written to your terminal?
That's how to detect a SIM swap with PHP before sending an SMS OTP
Now – if you're participating in the Private Beta – you can find out when a phone number was last swapped or if it has been swapped within the swapped period; allowing for the information returned by the carrier.
When available, you're in a better position to know if a SIM swap was suspicious. As a result, you can add an additional layer of security to your PHP applications, and greater peace of mind to your users.
Matthew Setter is the PHP Editor in the Twilio Voices team and a PHP and Go developer. He’s also the author of Deploy With Docker Compose and Mezzio Essentials. When he’s not writing PHP code, he’s editing great PHP articles here at Twilio. You can find him at msetter@twilio.com, Twitter, GitHub, and LinkedIn.
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.