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.
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).
You can find complete code snippets here on Github.
Checking the authenticity of the X-Authy-Signature
HTTP Header is a 6 step process.
Webhook
URL
without any parameters1const qs = require('qs');2const crypto = require('crypto');34/**5* @param {http} req This is an HTTP request from the Express middleware6* @param {!string} apiKey Account Security API key7* @return {Boolean} True if verified8*/9function verifyCallback(req, apiKey) {10const url = req.protocol + '://' + req.get('host') + req.originalUrl;11const method = req.method;12const params = req.body; // needs `npm i body-parser` on Express 41314// Sort the params15const sortedParams = qs16.stringify(params, { arrayFormat: 'brackets' })17.split('&')18.sort(sortByPropertyOnly)19.join('&')20.replace(/%20/g, '+');2122// Read the nonce from the request23const nonce = req.headers['x-authy-signature-nonce'];2425// concatinate all together and separate by '|'26const data = nonce + '|' + method + '|' + url + '|' + sortedParams;2728// compute the signature29const computedSig = crypto30.createHmac('sha256', apiKey)31.update(data)32.digest('base64');3334const sig = req.headers['x-authy-signature'];3536// compare the message signature with your calculated signature37return sig === computedSig;38}3940/**41* Sort by property only.42* Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'43* would be moved after 'events=aaaa'.44*45* For this approach, we split tokenize the string around the '=' value and only sort alphabetically46* by the property.47*48* @param {string} x49* @param {string} y50* @return {number}51*/52function sortByPropertyOnly(x, y) {53const xx = x.split('=')[0];54const yy = y.split('=')[0];5556if (xx < yy) {57return -1;58}59if (xx > yy) {60return 1;61}62return 0;63}
1const qs = require('qs');2const crypto = require('crypto');34/**5* @param {http} req This is an HTTP request from the Express middleware6* @param {!string} apiKey Account Security API key7* @return {Boolean} True if verified8*/9function verifyCallback(req, apiKey) {10const url = req.protocol + '://' + req.get('host') + req.originalUrl;11const method = req.method;12const params = req.body; // needs `npm i body-parser` on Express 41314// Sort the params15const sortedParams = qs16.stringify(params, { arrayFormat: 'brackets' })17.split('&')18.sort(sortByPropertyOnly)19.join('&')20.replace(/%20/g, '+');2122// Read the nonce from the request23const nonce = req.headers['x-authy-signature-nonce'];2425// concatinate all together and separate by '|'26const data = nonce + '|' + method + '|' + url + '|' + sortedParams;2728// compute the signature29const computedSig = crypto30.createHmac('sha256', apiKey)31.update(data)32.digest('base64');3334const sig = req.headers['x-authy-signature'];3536// compare the message signature with your calculated signature37return sig === computedSig;38}3940/**41* Sort by property only.42* Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'43* would be moved after 'events=aaaa'.44*45* For this approach, we split tokenize the string around the '=' value and only sort alphabetically46* by the property.47*48* @param {string} x49* @param {string} y50* @return {number}51*/52function sortByPropertyOnly(x, y) {53const xx = x.split('=')[0];54const yy = y.split('=')[0];5556if (xx < yy) {57return -1;58}59if (xx > yy) {60return 1;61}62return 0;63}
X-Authy-Signature
HTTP Header1const qs = require('qs');2const crypto = require('crypto');34/**5* @param {http} req This is an HTTP request from the Express middleware6* @param {!string} apiKey Account Security API key7* @return {Boolean} True if verified8*/9function verifyCallback(req, apiKey) {10const url = req.protocol + '://' + req.get('host') + req.originalUrl;11const method = req.method;12const params = req.body; // needs `npm i body-parser` on Express 41314// Sort the params15const sortedParams = qs16.stringify(params, { arrayFormat: 'brackets' })17.split('&')18.sort(sortByPropertyOnly)19.join('&')20.replace(/%20/g, '+');2122// Read the nonce from the request23const nonce = req.headers['x-authy-signature-nonce'];2425// concatinate all together and separate by '|'26const data = nonce + '|' + method + '|' + url + '|' + sortedParams;2728// compute the signature29const computedSig = crypto30.createHmac('sha256', apiKey)31.update(data)32.digest('base64');3334const sig = req.headers['x-authy-signature'];3536// compare the message signature with your calculated signature37return sig === computedSig;38}3940/**41* Sort by property only.42* Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'43* would be moved after 'events=aaaa'.44*45* For this approach, we split tokenize the string around the '=' value and only sort alphabetically46* by the property.47*48* @param {string} x49* @param {string} y50* @return {number}51*/52function sortByPropertyOnly(x, y) {53const xx = x.split('=')[0];54const yy = y.split('=')[0];5556if (xx < yy) {57return -1;58}59if (xx > yy) {60return 1;61}62return 0;63}
POST
'), and the sorted parameters together with the vertical pipe, ('|') character1const qs = require('qs');2const crypto = require('crypto');34/**5* @param {http} req This is an HTTP request from the Express middleware6* @param {!string} apiKey Account Security API key7* @return {Boolean} True if verified8*/9function verifyCallback(req, apiKey) {10const url = req.protocol + '://' + req.get('host') + req.originalUrl;11const method = req.method;12const params = req.body; // needs `npm i body-parser` on Express 41314// Sort the params15const sortedParams = qs16.stringify(params, { arrayFormat: 'brackets' })17.split('&')18.sort(sortByPropertyOnly)19.join('&')20.replace(/%20/g, '+');2122// Read the nonce from the request23const nonce = req.headers['x-authy-signature-nonce'];2425// concatinate all together and separate by '|'26const data = nonce + '|' + method + '|' + url + '|' + sortedParams;2728// compute the signature29const computedSig = crypto30.createHmac('sha256', apiKey)31.update(data)32.digest('base64');3334const sig = req.headers['x-authy-signature'];3536// compare the message signature with your calculated signature37return sig === computedSig;38}3940/**41* Sort by property only.42* Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'43* would be moved after 'events=aaaa'.44*45* For this approach, we split tokenize the string around the '=' value and only sort alphabetically46* by the property.47*48* @param {string} x49* @param {string} y50* @return {number}51*/52function sortByPropertyOnly(x, y) {53const xx = x.split('=')[0];54const yy = y.split('=')[0];5556if (xx < yy) {57return -1;58}59if (xx > yy) {60return 1;61}62return 0;63}
1const qs = require('qs');2const crypto = require('crypto');34/**5* @param {http} req This is an HTTP request from the Express middleware6* @param {!string} apiKey Account Security API key7* @return {Boolean} True if verified8*/9function verifyCallback(req, apiKey) {10const url = req.protocol + '://' + req.get('host') + req.originalUrl;11const method = req.method;12const params = req.body; // needs `npm i body-parser` on Express 41314// Sort the params15const sortedParams = qs16.stringify(params, { arrayFormat: 'brackets' })17.split('&')18.sort(sortByPropertyOnly)19.join('&')20.replace(/%20/g, '+');2122// Read the nonce from the request23const nonce = req.headers['x-authy-signature-nonce'];2425// concatinate all together and separate by '|'26const data = nonce + '|' + method + '|' + url + '|' + sortedParams;2728// compute the signature29const computedSig = crypto30.createHmac('sha256', apiKey)31.update(data)32.digest('base64');3334const sig = req.headers['x-authy-signature'];3536// compare the message signature with your calculated signature37return sig === computedSig;38}3940/**41* Sort by property only.42* Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'43* would be moved after 'events=aaaa'.44*45* For this approach, we split tokenize the string around the '=' value and only sort alphabetically46* by the property.47*48* @param {string} x49* @param {string} y50* @return {number}51*/52function sortByPropertyOnly(x, y) {53const xx = x.split('=')[0];54const yy = y.split('=')[0];5556if (xx < yy) {57return -1;58}59if (xx > yy) {60return 1;61}62return 0;63}
1const qs = require('qs');2const crypto = require('crypto');34/**5* @param {http} req This is an HTTP request from the Express middleware6* @param {!string} apiKey Account Security API key7* @return {Boolean} True if verified8*/9function verifyCallback(req, apiKey) {10const url = req.protocol + '://' + req.get('host') + req.originalUrl;11const method = req.method;12const params = req.body; // needs `npm i body-parser` on Express 41314// Sort the params15const sortedParams = qs16.stringify(params, { arrayFormat: 'brackets' })17.split('&')18.sort(sortByPropertyOnly)19.join('&')20.replace(/%20/g, '+');2122// Read the nonce from the request23const nonce = req.headers['x-authy-signature-nonce'];2425// concatinate all together and separate by '|'26const data = nonce + '|' + method + '|' + url + '|' + sortedParams;2728// compute the signature29const computedSig = crypto30.createHmac('sha256', apiKey)31.update(data)32.digest('base64');3334const sig = req.headers['x-authy-signature'];3536// compare the message signature with your calculated signature37return sig === computedSig;38}3940/**41* Sort by property only.42* Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'43* would be moved after 'events=aaaa'.44*45* For this approach, we split tokenize the string around the '=' value and only sort alphabetically46* by the property.47*48* @param {string} x49* @param {string} y50* @return {number}51*/52function sortByPropertyOnly(x, y) {53const xx = x.split('=')[0];54const yy = y.split('=')[0];5556if (xx < yy) {57return -1;58}59if (xx > yy) {60return 1;61}62return 0;63}
Here is every step summarized so you can get an idea of the whole process.
1const qs = require('qs');2const crypto = require('crypto');34/**5* @param {http} req This is an HTTP request from the Express middleware6* @param {!string} apiKey Account Security API key7* @return {Boolean} True if verified8*/9function verifyCallback(req, apiKey) {10const url = req.protocol + '://' + req.get('host') + req.originalUrl;11const method = req.method;12const params = req.body; // needs `npm i body-parser` on Express 41314// Sort the params15const sortedParams = qs16.stringify(params, { arrayFormat: 'brackets' })17.split('&')18.sort(sortByPropertyOnly)19.join('&')20.replace(/%20/g, '+');2122// Read the nonce from the request23const nonce = req.headers['x-authy-signature-nonce'];2425// concatinate all together and separate by '|'26const data = nonce + '|' + method + '|' + url + '|' + sortedParams;2728// compute the signature29const computedSig = crypto30.createHmac('sha256', apiKey)31.update(data)32.digest('base64');3334const sig = req.headers['x-authy-signature'];3536// compare the message signature with your calculated signature37return sig === computedSig;38}3940/**41* Sort by property only.42* Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'43* would be moved after 'events=aaaa'.44*45* For this approach, we split tokenize the string around the '=' value and only sort alphabetically46* by the property.47*48* @param {string} x49* @param {string} y50* @return {number}51*/52function sortByPropertyOnly(x, y) {53const xx = x.split('=')[0];54const yy = y.split('=')[0];5556if (xx < yy) {57return -1;58}59if (xx > yy) {60return 1;61}62return 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.