Passwordless SMS Authentication with Plivo and Directus Automate
Published May 20th, 2024
In this tutorial, we will be setting up Passwordless Authentication using Directus Automate and the Plivo Verify API.
This will allow users to sign in to your Directus project by sending a one-time password (OTP) to their phone number, and then validating that it is correct before returning a token for further requests.
This solution can be used once a user already exists in your project and has a unique, correctly-formatted, phone number. Formatting numbers can be hard and the Plivo Lookup API can be used to validate a phone number.
Before You Start
You will need...
- A Directus project - follow our quickstart guide if you don't already have one.
- A Plivo account.
- A phone number that you can use to test receiving SMS messages (such as your own mobile phone number).
- The
directus_users
table should have aphone
field that is aString
. - The
directus_users
table should have aotp_session_uuid
field that is aString
. - A Directus user account with a valid mobile phone number added to the
phone
field.
Setting Up Plivo
In the Plivo Verify Overview, take note of your Auth ID and Auth Token. Create a new application and also take note of it's UUID.
Plivo uses BasicAuth to authenticate requests, but Directus Automate does not support this natively. Fortunately, you can use BasicAuth through headers by encoding the username and password as a Base64 string.
The encoded value must be in the format auth_id:auth_token
. You can use this web tool to encode the string. Take note of the string.
The Login Flow
Using Directus Flows, we will accept a phone number and country code from the user, clean up the number, create a Plivo session saving session ID against the Directus user account. We will then return the session ID to the user ready for the verification flow.
Create a new Flow from your project settings with a Webhook trigger and caching disabled. Your application will make a request to this URL when starting a login.
Number Cleanup
Numbers must be formatted in E.164 format to be accepted by Plivo. That means a format such as +447123456789
(a +
, a country code, and a subscriber number).
Create a Run Script operation with the following code:
module.exports = async function(data) {
const countryCode = `+${data.$trigger.query.country_code}`;
const phoneNumber = data.$trigger.query.phone_number;
let fullPhoneNumber = phoneNumber.replace(/[^\d+]/g, ''); // remove all non-numeric characters except "+"
if (phoneNumber.startsWith('00')) {
fullPhoneNumber = phoneNumber.replace(/^00/g, '+')
} else if (!phoneNumber.startsWith('+')) {
fullPhoneNumber = `${countryCode}${phoneNumber.replace(/^[0-9]/g, '')}`;
}
return {
phone_number: fullPhoneNumber
};
}
module.exports = async function(data) {
const countryCode = `+${data.$trigger.query.country_code}`;
const phoneNumber = data.$trigger.query.phone_number;
let fullPhoneNumber = phoneNumber.replace(/[^\d+]/g, ''); // remove all non-numeric characters except "+"
if (phoneNumber.startsWith('00')) {
fullPhoneNumber = phoneNumber.replace(/^00/g, '+')
} else if (!phoneNumber.startsWith('+')) {
fullPhoneNumber = `${countryCode}${phoneNumber.replace(/^[0-9]/g, '')}`;
}
return {
phone_number: fullPhoneNumber
};
}
Save the flow, open your browser and navigate to your Trigger URL appended with ?phone_number=07123456789&country_code=44
to the end. This will trigger the flow, clean up the phone number and return the following JSON: {"phone_number":"+447123456789"}
If this is your response, then the Number Cleanup operation works.
Creating the Plivo OTP Session
Plivo's Verify API has two stages - creating a session will send the user a OTP and return a Session UUID. To verify the OTP later, a Session UUID and OTP must be sent to Plivo who will validate whether it was correct.
To create a session, create a Webhook / Request URL operation with the following options:
- Set the method to POST
- In the URL field, enter
https://api.plivo.com/v1/Account/{PLIVO_AUTH_ID}/Verify/Session/
. Replace{PLIVO_AUTH_ID}
with your Plivo Auth ID. - In the headers section, add the following header replacing
{AUTH_HEADER}
with the encoded Authentication Header we created earlier:- Header: Authorization
- Value: Basic
- In the body section, add the following JSON replacing
{PLIVO_APP_UUID}
with your Plivo App UUID:
{
"app_uuid": "{PLIVO_APP_UUID}",
"recipient": "{{$last.phone_number}}",
"channel": "sms",
"method": "POST"
}
{
"app_uuid": "{PLIVO_APP_UUID}",
"recipient": "{{$last.phone_number}}",
"channel": "sms",
"method": "POST"
}
A typical response from Plivo will look like this:
{
"api_id": "8ad839d3-34ab-4549-945d-33ed4f350ef5",
"message": "Session initiated",
"session_uuid": "b3d06b0c-d1cb-47cd-ab4b-64b579f72f93"
}
{
"api_id": "8ad839d3-34ab-4549-945d-33ed4f350ef5",
"message": "Session initiated",
"session_uuid": "b3d06b0c-d1cb-47cd-ab4b-64b579f72f93"
}
Saving the Session UUID
Create a new Update Data operation. In the collection field, edit the raw value and set the value to directus_users
. Set full access permissions and the following payload:
{
"otp_session_uuid": "{{$last.session_uuid}}"
}
{
"otp_session_uuid": "{{$last.session_uuid}}"
}
Set the following query:
{
"filter": {
"phone": {
"_eq": "{{number_cleanup.phone_number}}"
}
}
}
{
"filter": {
"phone": {
"_eq": "{{number_cleanup.phone_number}}"
}
}
}
Returning the OTP Session
Your application will need the Session UUID. Create a Run Script operation:
module.exports = async function(data) {
return {
otp_session_uuid: data.create_otp_session.data.session_uuid
};
}
module.exports = async function(data) {
return {
otp_session_uuid: data.create_otp_session.data.session_uuid
};
}
Testing the Flow
Open your browser to your trigger URL appended with ?phone_number={YOUR_NUMBER}&country_code={YOUR_COUNTRY_CODE}
, replacing the values with your real number.
Directus will respond with a otp_session_uuid
. This UUID is the OTP session ID that we will use to verify the OTP code. You should also receive an OTP code via SMS to the phone number you provided. Make a note of the OTP code. The otp_session_uuid
has also been stored against the user account.
The Verification Flow
The second flow will accept a Session UUID and a One Time Password. If correct, it will generate and save a new static token against the user and deliver it. The token can then be used to authenticate requests.
Create a new Flow from your project settings with a Webhook trigger and caching disabled. Your application will make a request to this URL when verifying a OTP.
To create a session, create a Webhook / Request URL operation with the following options:
- Set the method to POST
- In the URL field, enter
https://api.plivo.com/v1/Account/{PLIVO_AUTH_ID}/Verify/Session/{{$trigger.query.session_uuid}}/
. Replace{PLIVO_AUTH_ID}
with your Plivo Auth ID. - In the headers section, add the following header replacing
{AUTH_HEADER}
with the encoded Authentication Header we created earlier:- Header: Authorization
- Value: Basic
- In the body section, add the following JSON replacing
{PLIVO_APP_UUID}
with your Plivo App UUID:
{
"otp": "{{$trigger.query.otp}}"
}
{
"otp": "{{$trigger.query.otp}}"
}
Saving and Sending the Static Token
Create a new Update Data operation. In the collection field, edit the raw value and set the value to directus_users
. Set full access permissions and the following payload:
{
"token": "{{$last.token}}"
}
{
"token": "{{$last.token}}"
}
Set the following query:
{
"filter": {
"otp_session_uuid": {
"_eq": "{{$trigger.query.session_uuid}}"
}
}
}
{
"filter": {
"otp_session_uuid": {
"_eq": "{{$trigger.query.session_uuid}}"
}
}
}
Your application will need the new static token. Create a Run Script operation:
module.exports = async function(data) {
return data.generate_session_token;
}
module.exports = async function(data) {
return data.generate_session_token;
}
Testing the Flow
Open your browser to your trigger URL appended with ?otp={YOUR_OTP_CODE}&session_uuid={YOUR_OTP_SESSION}
, replacing the values from the first flow run. If it works, Directus will respond with a token
.
Summary
You have now successfully set up passwordless authentication in Directus using the Plivo Verify API. This will allow users to sign in to your Directus project by sending a one time password (OTP) to their phone number.
This same general workflow could be used for emails and magic links could also work the same way. It's important to note that static tokens do not expire and are stored in plaintext, so you should consider a strategy for invalidating tokens.