Skip to contentSkip to navigationSkip to topbar
Rate this Page:

Validating Twilio Authy Callbacks


(warning)

Warning

As of November 2022, Twilio no longer provides support for Authy SMS/Voice-only customers. Customers who were also using Authy TOTP or Push prior to March 1, 2023 are still supported. The Authy API is now closed to new customers and will be fully deprecated in the future.

For new development, we encourage you to use the Verify v2 API.

Existing customers will not be impacted at this time until Authy API has reached End of Life. For more information about migration, see Migrating from Authy to Verify for SMS(link takes you to an external page).

When using Webhooks with push authentications, Twilio will send a callback to your application's exposed URL when a user interacts with your ApprovalRequest. While testing, you can accept all incoming webhooks, but in production, you'll need to verify the authenticity of incoming requests.

Twilio sends an HTTP Header X-Authy-Signature with every outgoing request to your application. X-Authy-Signature is a HMAC signature of the full message body sent from Twilio hashed with your Application API Key (from Authy in the Twilio Console(link takes you to an external page)).

You can find complete code snippets here(link takes you to an external page) on Github.


Verify a Twilio Authy Callback

verify-a-twilio-authy-callback page anchor

Checking the authenticity of the X-Authy-Signature HTTP Header is a 6 step process.

  • Create a string using the Webhook URL without any parameters

Create a Webhook URL String

create-a-webhook-url-string page anchor

Use the webhook URL without any parameters to create a string.

Node.js
Ruby

_63
const qs = require('qs');
_63
const crypto = require('crypto');
_63
_63
/**
_63
* @param {http} req This is an HTTP request from the Express middleware
_63
* @param {!string} apiKey Account Security API key
_63
* @return {Boolean} True if verified
_63
*/
_63
function verifyCallback(req, apiKey) {
_63
const url = req.protocol + '://' + req.get('host') + req.originalUrl;
_63
const method = req.method;
_63
const params = req.body; // needs `npm i body-parser` on Express 4
_63
_63
// Sort the params
_63
const sortedParams = qs
_63
.stringify(params, { arrayFormat: 'brackets' })
_63
.split('&')
_63
.sort(sortByPropertyOnly)
_63
.join('&')
_63
.replace(/%20/g, '+');
_63
_63
// Read the nonce from the request
_63
const nonce = req.headers['x-authy-signature-nonce'];
_63
_63
// concatinate all together and separate by '|'
_63
const data = nonce + '|' + method + '|' + url + '|' + sortedParams;
_63
_63
// compute the signature
_63
const computedSig = crypto
_63
.createHmac('sha256', apiKey)
_63
.update(data)
_63
.digest('base64');
_63
_63
const sig = req.headers['x-authy-signature'];
_63
_63
// compare the message signature with your calculated signature
_63
return sig === computedSig;
_63
}
_63
_63
/**
_63
* Sort by property only.
_63
* Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'
_63
* would be moved after 'events=aaaa'.
_63
*
_63
* For this approach, we split tokenize the string around the '=' value and only sort alphabetically
_63
* by the property.
_63
*
_63
* @param {string} x
_63
* @param {string} y
_63
* @return {number}
_63
*/
_63
function sortByPropertyOnly(x, y) {
_63
const xx = x.split('=')[0];
_63
const yy = y.split('=')[0];
_63
_63
if (xx < yy) {
_63
return -1;
_63
}
_63
if (xx > yy) {
_63
return 1;
_63
}
_63
return 0;
_63
}

  • Flatten the received JSON body and sort this list in case-sensitive order and convert them to URL format

Sort all received parameters in alphabetical, case-sensitive order after converting them to URL format.

Node.js
Ruby

_63
const qs = require('qs');
_63
const crypto = require('crypto');
_63
_63
/**
_63
* @param {http} req This is an HTTP request from the Express middleware
_63
* @param {!string} apiKey Account Security API key
_63
* @return {Boolean} True if verified
_63
*/
_63
function verifyCallback(req, apiKey) {
_63
const url = req.protocol + '://' + req.get('host') + req.originalUrl;
_63
const method = req.method;
_63
const params = req.body; // needs `npm i body-parser` on Express 4
_63
_63
// Sort the params
_63
const sortedParams = qs
_63
.stringify(params, { arrayFormat: 'brackets' })
_63
.split('&')
_63
.sort(sortByPropertyOnly)
_63
.join('&')
_63
.replace(/%20/g, '+');
_63
_63
// Read the nonce from the request
_63
const nonce = req.headers['x-authy-signature-nonce'];
_63
_63
// concatinate all together and separate by '|'
_63
const data = nonce + '|' + method + '|' + url + '|' + sortedParams;
_63
_63
// compute the signature
_63
const computedSig = crypto
_63
.createHmac('sha256', apiKey)
_63
.update(data)
_63
.digest('base64');
_63
_63
const sig = req.headers['x-authy-signature'];
_63
_63
// compare the message signature with your calculated signature
_63
return sig === computedSig;
_63
}
_63
_63
/**
_63
* Sort by property only.
_63
* Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'
_63
* would be moved after 'events=aaaa'.
_63
*
_63
* For this approach, we split tokenize the string around the '=' value and only sort alphabetically
_63
* by the property.
_63
*
_63
* @param {string} x
_63
* @param {string} y
_63
* @return {number}
_63
*/
_63
function sortByPropertyOnly(x, y) {
_63
const xx = x.split('=')[0];
_63
const yy = y.split('=')[0];
_63
_63
if (xx < yy) {
_63
return -1;
_63
}
_63
if (xx > yy) {
_63
return 1;
_63
}
_63
return 0;
_63
}

  • Grab the nonce from the X-Authy-Signature HTTP Header

Grab the nonce from the X-Authy-Signature HTTP Header.

Node.js
Ruby

_63
const qs = require('qs');
_63
const crypto = require('crypto');
_63
_63
/**
_63
* @param {http} req This is an HTTP request from the Express middleware
_63
* @param {!string} apiKey Account Security API key
_63
* @return {Boolean} True if verified
_63
*/
_63
function verifyCallback(req, apiKey) {
_63
const url = req.protocol + '://' + req.get('host') + req.originalUrl;
_63
const method = req.method;
_63
const params = req.body; // needs `npm i body-parser` on Express 4
_63
_63
// Sort the params
_63
const sortedParams = qs
_63
.stringify(params, { arrayFormat: 'brackets' })
_63
.split('&')
_63
.sort(sortByPropertyOnly)
_63
.join('&')
_63
.replace(/%20/g, '+');
_63
_63
// Read the nonce from the request
_63
const nonce = req.headers['x-authy-signature-nonce'];
_63
_63
// concatinate all together and separate by '|'
_63
const data = nonce + '|' + method + '|' + url + '|' + sortedParams;
_63
_63
// compute the signature
_63
const computedSig = crypto
_63
.createHmac('sha256', apiKey)
_63
.update(data)
_63
.digest('base64');
_63
_63
const sig = req.headers['x-authy-signature'];
_63
_63
// compare the message signature with your calculated signature
_63
return sig === computedSig;
_63
}
_63
_63
/**
_63
* Sort by property only.
_63
* Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'
_63
* would be moved after 'events=aaaa'.
_63
*
_63
* For this approach, we split tokenize the string around the '=' value and only sort alphabetically
_63
* by the property.
_63
*
_63
* @param {string} x
_63
* @param {string} y
_63
* @return {number}
_63
*/
_63
function sortByPropertyOnly(x, y) {
_63
const xx = x.split('=')[0];
_63
const yy = y.split('=')[0];
_63
_63
if (xx < yy) {
_63
return -1;
_63
}
_63
if (xx > yy) {
_63
return 1;
_63
}
_63
return 0;
_63
}

  • Join the nonce, HTTP method (' POST '), and the sorted parameters together with the vertical pipe, ('|') character

Join the Nonce, Method, and Params

join-the-nonce-method-and-params page anchor

Using the vertical pipe ('

Node.js
Ruby

_63
const qs = require('qs');
_63
const crypto = require('crypto');
_63
_63
/**
_63
* @param {http} req This is an HTTP request from the Express middleware
_63
* @param {!string} apiKey Account Security API key
_63
* @return {Boolean} True if verified
_63
*/
_63
function verifyCallback(req, apiKey) {
_63
const url = req.protocol + '://' + req.get('host') + req.originalUrl;
_63
const method = req.method;
_63
const params = req.body; // needs `npm i body-parser` on Express 4
_63
_63
// Sort the params
_63
const sortedParams = qs
_63
.stringify(params, { arrayFormat: 'brackets' })
_63
.split('&')
_63
.sort(sortByPropertyOnly)
_63
.join('&')
_63
.replace(/%20/g, '+');
_63
_63
// Read the nonce from the request
_63
const nonce = req.headers['x-authy-signature-nonce'];
_63
_63
// concatinate all together and separate by '|'
_63
const data = nonce + '|' + method + '|' + url + '|' + sortedParams;
_63
_63
// compute the signature
_63
const computedSig = crypto
_63
.createHmac('sha256', apiKey)
_63
.update(data)
_63
.digest('base64');
_63
_63
const sig = req.headers['x-authy-signature'];
_63
_63
// compare the message signature with your calculated signature
_63
return sig === computedSig;
_63
}
_63
_63
/**
_63
* Sort by property only.
_63
* Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'
_63
* would be moved after 'events=aaaa'.
_63
*
_63
* For this approach, we split tokenize the string around the '=' value and only sort alphabetically
_63
* by the property.
_63
*
_63
* @param {string} x
_63
* @param {string} y
_63
* @return {number}
_63
*/
_63
function sortByPropertyOnly(x, y) {
_63
const xx = x.split('=')[0];
_63
const yy = y.split('=')[0];
_63
_63
if (xx < yy) {
_63
return -1;
_63
}
_63
if (xx > yy) {
_63
return 1;
_63
}
_63
return 0;
_63
}

  • Use HMAC-SHA256 to hash the string using your Application API Key

Hash the Combined String with HMAC-SHA256

hash-the-combined-string-with-hmac-sha256 page anchor

Use HMAC-SHA256 to hash the resulting string with your Application API Key from the console.

Node.js
Ruby

_63
const qs = require('qs');
_63
const crypto = require('crypto');
_63
_63
/**
_63
* @param {http} req This is an HTTP request from the Express middleware
_63
* @param {!string} apiKey Account Security API key
_63
* @return {Boolean} True if verified
_63
*/
_63
function verifyCallback(req, apiKey) {
_63
const url = req.protocol + '://' + req.get('host') + req.originalUrl;
_63
const method = req.method;
_63
const params = req.body; // needs `npm i body-parser` on Express 4
_63
_63
// Sort the params
_63
const sortedParams = qs
_63
.stringify(params, { arrayFormat: 'brackets' })
_63
.split('&')
_63
.sort(sortByPropertyOnly)
_63
.join('&')
_63
.replace(/%20/g, '+');
_63
_63
// Read the nonce from the request
_63
const nonce = req.headers['x-authy-signature-nonce'];
_63
_63
// concatinate all together and separate by '|'
_63
const data = nonce + '|' + method + '|' + url + '|' + sortedParams;
_63
_63
// compute the signature
_63
const computedSig = crypto
_63
.createHmac('sha256', apiKey)
_63
.update(data)
_63
.digest('base64');
_63
_63
const sig = req.headers['x-authy-signature'];
_63
_63
// compare the message signature with your calculated signature
_63
return sig === computedSig;
_63
}
_63
_63
/**
_63
* Sort by property only.
_63
* Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'
_63
* would be moved after 'events=aaaa'.
_63
*
_63
* For this approach, we split tokenize the string around the '=' value and only sort alphabetically
_63
* by the property.
_63
*
_63
* @param {string} x
_63
* @param {string} y
_63
* @return {number}
_63
*/
_63
function sortByPropertyOnly(x, y) {
_63
const xx = x.split('=')[0];
_63
const yy = y.split('=')[0];
_63
_63
if (xx < yy) {
_63
return -1;
_63
}
_63
if (xx > yy) {
_63
return 1;
_63
}
_63
return 0;
_63
}

Encode the Digest with Base64

encode-the-digest-with-base64 page anchor

Follow RFC4648 to Base64 encode the digest

Node.js
Ruby

_63
const qs = require('qs');
_63
const crypto = require('crypto');
_63
_63
/**
_63
* @param {http} req This is an HTTP request from the Express middleware
_63
* @param {!string} apiKey Account Security API key
_63
* @return {Boolean} True if verified
_63
*/
_63
function verifyCallback(req, apiKey) {
_63
const url = req.protocol + '://' + req.get('host') + req.originalUrl;
_63
const method = req.method;
_63
const params = req.body; // needs `npm i body-parser` on Express 4
_63
_63
// Sort the params
_63
const sortedParams = qs
_63
.stringify(params, { arrayFormat: 'brackets' })
_63
.split('&')
_63
.sort(sortByPropertyOnly)
_63
.join('&')
_63
.replace(/%20/g, '+');
_63
_63
// Read the nonce from the request
_63
const nonce = req.headers['x-authy-signature-nonce'];
_63
_63
// concatinate all together and separate by '|'
_63
const data = nonce + '|' + method + '|' + url + '|' + sortedParams;
_63
_63
// compute the signature
_63
const computedSig = crypto
_63
.createHmac('sha256', apiKey)
_63
.update(data)
_63
.digest('base64');
_63
_63
const sig = req.headers['x-authy-signature'];
_63
_63
// compare the message signature with your calculated signature
_63
return sig === computedSig;
_63
}
_63
_63
/**
_63
* Sort by property only.
_63
* Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'
_63
* would be moved after 'events=aaaa'.
_63
*
_63
* For this approach, we split tokenize the string around the '=' value and only sort alphabetically
_63
* by the property.
_63
*
_63
* @param {string} x
_63
* @param {string} y
_63
* @return {number}
_63
*/
_63
function sortByPropertyOnly(x, y) {
_63
const xx = x.split('=')[0];
_63
const yy = y.split('=')[0];
_63
_63
if (xx < yy) {
_63
return -1;
_63
}
_63
if (xx > yy) {
_63
return 1;
_63
}
_63
return 0;
_63
}

Here is every step summarized so you can get an idea of the whole process.

Verify an Incoming Two-factor Authentication Webhook

verify-an-incoming-two-factor-authentication-webhook page anchor

Overview of the steps needed to verify an incoming Twilio webhook for Push Notifications.

Node.js
Ruby

_63
const qs = require('qs');
_63
const crypto = require('crypto');
_63
_63
/**
_63
* @param {http} req This is an HTTP request from the Express middleware
_63
* @param {!string} apiKey Account Security API key
_63
* @return {Boolean} True if verified
_63
*/
_63
function verifyCallback(req, apiKey) {
_63
const url = req.protocol + '://' + req.get('host') + req.originalUrl;
_63
const method = req.method;
_63
const params = req.body; // needs `npm i body-parser` on Express 4
_63
_63
// Sort the params
_63
const sortedParams = qs
_63
.stringify(params, { arrayFormat: 'brackets' })
_63
.split('&')
_63
.sort(sortByPropertyOnly)
_63
.join('&')
_63
.replace(/%20/g, '+');
_63
_63
// Read the nonce from the request
_63
const nonce = req.headers['x-authy-signature-nonce'];
_63
_63
// concatinate all together and separate by '|'
_63
const data = nonce + '|' + method + '|' + url + '|' + sortedParams;
_63
_63
// compute the signature
_63
const computedSig = crypto
_63
.createHmac('sha256', apiKey)
_63
.update(data)
_63
.digest('base64');
_63
_63
const sig = req.headers['x-authy-signature'];
_63
_63
// compare the message signature with your calculated signature
_63
return sig === computedSig;
_63
}
_63
_63
/**
_63
* Sort by property only.
_63
* Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'
_63
* would be moved after 'events=aaaa'.
_63
*
_63
* For this approach, we split tokenize the string around the '=' value and only sort alphabetically
_63
* by the property.
_63
*
_63
* @param {string} x
_63
* @param {string} y
_63
* @return {number}
_63
*/
_63
function sortByPropertyOnly(x, y) {
_63
const xx = x.split('=')[0];
_63
const yy = y.split('=')[0];
_63
_63
if (xx < yy) {
_63
return -1;
_63
}
_63
if (xx > yy) {
_63
return 1;
_63
}
_63
return 0;
_63
}

Once you have encoded the digest, you can compare the resulting string with the X-Authy-Signature HTTP Header. If they match, the incoming request is from Twilio. If there is a mismatch, you should reject the request as fraudulent.


Rate this Page: