Validating Twilio Authy Callbacks

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.

Verify a Twilio Authy Callback

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
Loading Code Samples...
Language
SDK Version:
  • 5.x
SDK Version:
  • 7.x
SDK Version:
  • 3.x
SDK Version:
  • 5.x
SDK Version:
  • 6.x
SDK Version:
  • 5.x
const qs = require('qs');
const crypto = require('crypto');

/**
 * @param {http} req This is an HTTP request from the Express middleware
 * @param {!string} apiKey  Account Security API key
 * @return {Boolean} True if verified
 */
function verifyCallback(req, apiKey) {
  const url = req.protocol + '://' + req.get('host') + req.originalUrl;
  const method = req.method;
  const params = req.body;	// needs `npm i body-parser` on Express 4

  // Sort the params
  const sortedParams = qs
    .stringify(params, { arrayFormat: 'brackets' })
    .split('&')
    .sort(sortByPropertyOnly)
    .join('&')
    .replace(/%20/g, '+');

  // Read the nonce from the request
  const nonce = req.headers['x-authy-signature-nonce'];

  // concatinate all together and separate by '|'
  const data = nonce + '|' + method + '|' + url + '|' + sortedParams;

  // compute the signature
  const computedSig = crypto
    .createHmac('sha256', apiKey)
    .update(data)
    .digest('base64');

  const sig = req.headers['x-authy-signature'];

  // compare the message signature with your calculated signature
  return sig === computedSig;
}

/**
 * Sort by property only.
 *  Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'
 *  would be moved after 'events=aaaa'.
 *
 *  For this approach, we split tokenize the string around the '=' value and only sort alphabetically
 *  by the property.
 *
 * @param {string} x
 * @param {string} y
 * @return {number}
 */
function sortByPropertyOnly(x, y) {
  const xx = x.split('=');
  const yy = y.split('=');

  if (xx < yy) {
    return -1;
  }
  if (xx > yy) {
    return 1;
  }
  return 0;
}
using System.Security.Cryptography;
using Microsoft.AspNetCore.Http.Extensions;
using Newtonsoft.Json.Linq;
using System.Text.RegularExpressions;

namespace Test
{
    public class AuthyWebhook
    {
        private bool Verify(Microsoft.AspNetCore.Http.HttpRequest Request,
          Newtonsoft.Json.Linq.JObject JsonContent,
          string ApiKey)
        {
            // Read the nonce from the request
            var nonce = Request.Headers["x-authy-signature-nonce"];
            var method = Request.Method;
            var url = Request.GetDisplayUrl();
            List<String> bodyRequest = new List<String>();

            // Flatten
            var props = GetPropPaths(string.Empty, JsonContent);
            foreach (var kvp in props)
            {
                var encodedKey = System.Web.HttpUtility.UrlEncode(kvp.Item1);
                var encodedValue = System.Web.HttpUtility.UrlEncode(kvp.Item2);
                // uppercase all escaped url-encoded characters
                Regex reg = new Regex(@"%[a-f0-9]{2}");
                encodedKey = reg.Replace(encodedKey, m => m.Value.ToUpperInvariant());
                encodedValue = reg.Replace(encodedValue, m => m.Value.ToUpperInvariant());
                bodyRequest.Add(encodedKey + "=" + encodedValue);
            }

            // Sort the params
            bodyRequest.Sort();
            var parameters = String.Join("&", bodyRequest);

            // concatenate all together and separate by '|'
            var data = $"{nonce}|{method}|{url}|{parameters}";

            // compute the signature
            var encoding = new System.Text.ASCIIEncoding();
            byte[] ApiKeyBytes = encoding.GetBytes(ApiKey);
            byte[] DataBytes = encoding.GetBytes(data);
            using (var hmacsha256 = new HMACSHA256(ApiKeyBytes))
            {
                // Read the Authy Signature from the request
                var sig = Request.Headers["x-authy-signature"];

                // compare the message signature with your calculated signature
                byte[] hashmessage = hmacsha256.ComputeHash(DataBytes);
                var hash = Convert.ToBase64String(hashmessage);
                return hash == sig;
            }
        }

        private IEnumerable<Tuple<string, string>> GetPropPaths(string currPath, JObject obj)
        {
            foreach (var prop in obj.Properties())
            {
                var propPath = string.IsNullOrWhiteSpace(currPath) ? prop.Name : currPath + "[" + prop.Name + "]";

                if (prop.Value.Type == JTokenType.Object)
                {
                    foreach (var subProp in GetPropPaths(propPath, prop.Value as JObject))
                        yield return subProp;
                }
                else if (prop.Value.Type == JTokenType.Boolean)
                {
                    // Fix boolean type conversion to lowercase string
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString().ToLower());
                }
                else
                {
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString());
                }
            }
        }
    }
}
<?php
/**
 *
 * Verify Authy signature
 *
 * @param    string  $apiKey Authy app API KEY
 * @return   bool
 *
 */
function verifyWebhook($apiKey) {
    // Read the nonce from the request
    $nonce = $_SERVER['HTTP_X_AUTHY_SIGNATURE_NONCE'];
    $method = $_SERVER['REQUEST_METHOD'];
    $proto = isset($_SERVER['HTTPS']) ? "https" : "http";
    $url = "{$proto}://{$_SERVER[HTTP_HOST]}{$_SERVER[REQUEST_URI]}";

    // get the Json string sent by Authy
    $json = file_get_contents('php://input');
    $decoded = json_decode($json, true);
    $normalized = normalizeArray($decoded);

    //  sort the parameters by keys
    $query = http_build_query($normalized);
    $exploded = explode('&', $query);
    sort($exploded);
    $params = implode('&', $exploded);

    // concatenate all together and separate them by '|'
    $data = "$nonce|$method|$url|$params";

    // compute the signature
    $computedSig = base64_encode(hash_hmac('sha256', $data, $apiKey, true));

    // get the authy signature
    $sig = $_SERVER['HTTP_X_AUTHY_SIGNATURE'];

    // compare the message signature with your calculated signature
    return hash_equals($computedSig, $sig);
}

/**
 *
 * Replace boolean values with strings.
 *   In PHP methods like http_build_query converts values from an array
 *   to 0 and 1 integers instead of 'false' and 'true'.
 *
 * @param    array  $array Multidimensional array with boolean values
 * @return   array
 *
 */
function normalizeArray($array) {
  foreach ($array as &$el) {
      if (is_bool($el)) {
        $el = ($el) ? "true" : "false";
      } elseif (is_array($el)) {
        $el = normalizeArray($el);
      }
  }
  return $array;
}
require 'uri'

class CallbackVerifier

  # @param [request] A Rails request object
  # @param [api_key] The API key used to sign the request
  # @return [boolean] True if verified
  def verify_callback(request, api_key)
    url = url_for(:only_path => false, :overwrite_params=>nil)

    # Sort and join the parameters on Rails
    json_params = JSON.parse(request.body.read)
    parameter_string = json_params.to_query

    # Read the nonce from the request
    nonce = request.headers['x-authy-signature-nonce']

    # Join all request parts using '|'
    data = "#{nonce}|#{request.method}|#{url}|#{parameter_string}"

    # Compute the signature
    digest = OpenSSL::Digest.new('sha256')
    hmac = OpenSSL::HMAC.digest(digest, api_key, data)
    hash = Base64.strict_encode64(hmac)

    # Extract the actual request signature
    signature = request.headers['x-authy-signature']

    # Compare the computed signature with the actual signature
    hash == signature
  end
end
import base64
import hashlib
import hmac
import json
import re

try:
    from urllib.parse import urlencode, quote
except ImportError:
    from urllib import urlencode, quote


def validate_authy_request(request,api_key):
    """Validates that incoming requests genuinely originated from Twilio"""
    url = request.url
    method = request.method

    # Read the Flask request parameters
    params = json.loads(request.data)

    # Convert the JSON dict to query params
    query_params = __make_http_query(params)
    sorted_params = '&'.join(sorted(query_params.replace('/', '%2F').replace('%20', '+').split('&')))
    sorted_params = re.sub("\\%5B([0-9])*\\%5D","%5B%5D",sorted_params)
    sorted_params = re.sub("\\=None", "=", sorted_params)

    # Read the nonce from the request
    nonce = request.headers['X-Authy-Signature-Nonce']

    # Concatenate all together and separate by '|'
    data = '|'.join([nonce, method, url, sorted_params])

    # Compute the signature
    computed_dig = hmac.new(
        api_key.encode(),
        msg=data.encode(),
        digestmod=hashlib.sha256
    ).digest()
    computed_sig = base64.b64encode(computed_dig)

    sig = request.headers['X-Authy-Signature']

    # Compare the message signature with your calculated signature
    # Continue processing the request if it's valid, return a 403 error if
    # it's not
    return sig == computed_sig


def __make_http_query(params, topkey=''):
    """
    Function to covert params into url encoded query string
    :param dict params: Json string sent  by Authy.
    :param string topkey: params key
    :return string: url encoded Query.
    """
    if len(params) == 0:
        return ""
    result = ""
    # is a dictionary?
    if type(params) is dict:
        for key in params.keys():
            newkey = quote(key)
            if topkey != '':
                newkey = topkey + quote('[' + key + ']')
            if type(params[key]) is dict:
                result += __make_http_query(params[key], newkey)
            elif type(params[key]) is list:
                i = 0
                for val in params[key]:
                    if type(val) is dict:
                        result +=   __make_http_query(val, newkey + quote('['+str(i)+']'))
                    else:
                        result += newkey + quote('['+str(i)+']') + "=" + quote(str(val)) + "&"
                    i = i + 1
            # boolean should have special treatment as well
            elif type(params[key]) is bool:
                result += newkey + "=" + quote(str(params[key]).lower()) + "&"
            # assume string (integers and floats work well)
            else:
                result += newkey + "=" + quote(str(params[key])) + "&"
    # remove the last '&'
    if (result) and (topkey == '') and (result[-1] == '&'):
        result = result[:-1]
    return result
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

public class CallbackVerifier {
  
    private boolean verify(HttpServletRequest request, String authyApiKey)
            throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        String forwardedProtocol = request.getHeader("X-Forwarded-Proto");
        String protocol = forwardedProtocol != null ? forwardedProtocol :
                request.getScheme();
        String url = String.format("%s://%s%s", protocol,
                request.getServerName(), request.getServletPath());

        // Fetch Json body
        String body = IOUtils.toString(request.getReader());
        Map<String, Object> bodyJson = objectMapper.readValue(body,
                new TypeReference<Map<String, Object>>() {
                });

        // Flatten and sort JSON elements
        String flattenJson = bodyJson.entrySet()
                .stream()
                .flatMap(FlatMap::flatten)
                .map(e -> e.getKey() + "=" + e.getValue())
                .sorted()
                .collect(Collectors.joining("&"));

        // Read the nonce from the request
        String nonce = request.getHeader("X-Authy-Signature-Nonce");

        // Join all the request bits using '|'
        String data = String.format("%s|%s|%s|%s",
                nonce,
                request.getMethod(),
                url,
                flattenJson);

        try {
            // Compute the signature
            Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
            SecretKeySpec secret_key = new SecretKeySpec(authyApiKey.getBytes(),
                    "HmacSHA256");
            sha256_HMAC.init(secret_key);
            String hash = Base64
                    .getEncoder()
                    .encodeToString(sha256_HMAC.doFinal(data.getBytes()));

            // Extract the actual request signature
            String signature = request.getHeader("X-Authy-Signature");

            // Compare the request signature with your computed signature
            return hash.equals(signature);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            e.printStackTrace();
            return false;
        }
    }
}

class FlatMap {

    private static String openBracket;
    private static String closeBracket;

    static {
        try {
            openBracket = encode("[", "UTF-8");
            closeBracket = encode("]", "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private static String prependKeyName(String keyName, String prefix) {
        StringBuffer result = new StringBuffer();
        result.append(prefix);
        int indexOfOpenBracket = keyName.indexOf(openBracket);
        if(indexOfOpenBracket == -1) {
            indexOfOpenBracket = keyName.length();
        }
        result.append(openBracket);
        result.append(keyName.substring(0, indexOfOpenBracket));
        result.append(closeBracket);
        result.append(keyName.substring(indexOfOpenBracket));
        return result.toString();

    }

    public static Stream<Map.Entry<?, ?>> flatten(Map.Entry<?, ?> e) {
        Object value = e.getValue();
        if (value instanceof Map<?, ?>) {
            return ((Map<?, ?>) value).entrySet()
                    .stream()
                    .flatMap(FlatMap::flatten)
                    .map(el -> {
                        String newKeyName = FlatMap.prependKeyName(
                                (String) el.getKey(), (String) e.getKey());
                        return new AbstractMap.SimpleEntry<>(newKeyName, el.getValue());
                    });
        } else if(value == null) {
            return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(), ""));
        } else if(value instanceof String) {
            try {
                String encodedValue = URLEncoder.encode((String) value, "UTF-8");
                return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(),
                        encodedValue.replace(" ", "+")));
            } catch (UnsupportedEncodingException exception) {
                throw new RuntimeException(exception);
            }
        }
        return Stream.of(e);
    }
}
testable: false
Use the webhook URL without any parameters to create a string.
Create a Webhook URL String

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

  • Flatten the received JSON body and sort this list in case-sensitive order and convert them to URL format
Loading Code Samples...
Language
SDK Version:
  • 5.x
SDK Version:
  • 7.x
SDK Version:
  • 3.x
SDK Version:
  • 5.x
SDK Version:
  • 6.x
SDK Version:
  • 5.x
const qs = require('qs');
const crypto = require('crypto');

/**
 * @param {http} req This is an HTTP request from the Express middleware
 * @param {!string} apiKey  Account Security API key
 * @return {Boolean} True if verified
 */
function verifyCallback(req, apiKey) {
  const url = req.protocol + '://' + req.get('host') + req.originalUrl;
  const method = req.method;
  const params = req.body;	// needs `npm i body-parser` on Express 4

  // Sort the params
  const sortedParams = qs
    .stringify(params, { arrayFormat: 'brackets' })
    .split('&')
    .sort(sortByPropertyOnly)
    .join('&')
    .replace(/%20/g, '+');

  // Read the nonce from the request
  const nonce = req.headers['x-authy-signature-nonce'];

  // concatinate all together and separate by '|'
  const data = nonce + '|' + method + '|' + url + '|' + sortedParams;

  // compute the signature
  const computedSig = crypto
    .createHmac('sha256', apiKey)
    .update(data)
    .digest('base64');

  const sig = req.headers['x-authy-signature'];

  // compare the message signature with your calculated signature
  return sig === computedSig;
}

/**
 * Sort by property only.
 *  Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'
 *  would be moved after 'events=aaaa'.
 *
 *  For this approach, we split tokenize the string around the '=' value and only sort alphabetically
 *  by the property.
 *
 * @param {string} x
 * @param {string} y
 * @return {number}
 */
function sortByPropertyOnly(x, y) {
  const xx = x.split('=');
  const yy = y.split('=');

  if (xx < yy) {
    return -1;
  }
  if (xx > yy) {
    return 1;
  }
  return 0;
}
using System.Security.Cryptography;
using Microsoft.AspNetCore.Http.Extensions;
using Newtonsoft.Json.Linq;
using System.Text.RegularExpressions;

namespace Test
{
    public class AuthyWebhook
    {
        private bool Verify(Microsoft.AspNetCore.Http.HttpRequest Request,
          Newtonsoft.Json.Linq.JObject JsonContent,
          string ApiKey)
        {
            // Read the nonce from the request
            var nonce = Request.Headers["x-authy-signature-nonce"];
            var method = Request.Method;
            var url = Request.GetDisplayUrl();
            List<String> bodyRequest = new List<String>();

            // Flatten
            var props = GetPropPaths(string.Empty, JsonContent);
            foreach (var kvp in props)
            {
                var encodedKey = System.Web.HttpUtility.UrlEncode(kvp.Item1);
                var encodedValue = System.Web.HttpUtility.UrlEncode(kvp.Item2);
                // uppercase all escaped url-encoded characters
                Regex reg = new Regex(@"%[a-f0-9]{2}");
                encodedKey = reg.Replace(encodedKey, m => m.Value.ToUpperInvariant());
                encodedValue = reg.Replace(encodedValue, m => m.Value.ToUpperInvariant());
                bodyRequest.Add(encodedKey + "=" + encodedValue);
            }

            // Sort the params
            bodyRequest.Sort();
            var parameters = String.Join("&", bodyRequest);

            // concatenate all together and separate by '|'
            var data = $"{nonce}|{method}|{url}|{parameters}";

            // compute the signature
            var encoding = new System.Text.ASCIIEncoding();
            byte[] ApiKeyBytes = encoding.GetBytes(ApiKey);
            byte[] DataBytes = encoding.GetBytes(data);
            using (var hmacsha256 = new HMACSHA256(ApiKeyBytes))
            {
                // Read the Authy Signature from the request
                var sig = Request.Headers["x-authy-signature"];

                // compare the message signature with your calculated signature
                byte[] hashmessage = hmacsha256.ComputeHash(DataBytes);
                var hash = Convert.ToBase64String(hashmessage);
                return hash == sig;
            }
        }

        private IEnumerable<Tuple<string, string>> GetPropPaths(string currPath, JObject obj)
        {
            foreach (var prop in obj.Properties())
            {
                var propPath = string.IsNullOrWhiteSpace(currPath) ? prop.Name : currPath + "[" + prop.Name + "]";

                if (prop.Value.Type == JTokenType.Object)
                {
                    foreach (var subProp in GetPropPaths(propPath, prop.Value as JObject))
                        yield return subProp;
                }
                else if (prop.Value.Type == JTokenType.Boolean)
                {
                    // Fix boolean type conversion to lowercase string
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString().ToLower());
                }
                else
                {
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString());
                }
            }
        }
    }
}
<?php
/**
 *
 * Verify Authy signature
 *
 * @param    string  $apiKey Authy app API KEY
 * @return   bool
 *
 */
function verifyWebhook($apiKey) {
    // Read the nonce from the request
    $nonce = $_SERVER['HTTP_X_AUTHY_SIGNATURE_NONCE'];
    $method = $_SERVER['REQUEST_METHOD'];
    $proto = isset($_SERVER['HTTPS']) ? "https" : "http";
    $url = "{$proto}://{$_SERVER[HTTP_HOST]}{$_SERVER[REQUEST_URI]}";

    // get the Json string sent by Authy
    $json = file_get_contents('php://input');
    $decoded = json_decode($json, true);
    $normalized = normalizeArray($decoded);

    //  sort the parameters by keys
    $query = http_build_query($normalized);
    $exploded = explode('&', $query);
    sort($exploded);
    $params = implode('&', $exploded);

    // concatenate all together and separate them by '|'
    $data = "$nonce|$method|$url|$params";

    // compute the signature
    $computedSig = base64_encode(hash_hmac('sha256', $data, $apiKey, true));

    // get the authy signature
    $sig = $_SERVER['HTTP_X_AUTHY_SIGNATURE'];

    // compare the message signature with your calculated signature
    return hash_equals($computedSig, $sig);
}

/**
 *
 * Replace boolean values with strings.
 *   In PHP methods like http_build_query converts values from an array
 *   to 0 and 1 integers instead of 'false' and 'true'.
 *
 * @param    array  $array Multidimensional array with boolean values
 * @return   array
 *
 */
function normalizeArray($array) {
  foreach ($array as &$el) {
      if (is_bool($el)) {
        $el = ($el) ? "true" : "false";
      } elseif (is_array($el)) {
        $el = normalizeArray($el);
      }
  }
  return $array;
}
require 'uri'

class CallbackVerifier

  # @param [request] A Rails request object
  # @param [api_key] The API key used to sign the request
  # @return [boolean] True if verified
  def verify_callback(request, api_key)
    url = url_for(:only_path => false, :overwrite_params=>nil)

    # Sort and join the parameters on Rails
    json_params = JSON.parse(request.body.read)
    parameter_string = json_params.to_query

    # Read the nonce from the request
    nonce = request.headers['x-authy-signature-nonce']

    # Join all request parts using '|'
    data = "#{nonce}|#{request.method}|#{url}|#{parameter_string}"

    # Compute the signature
    digest = OpenSSL::Digest.new('sha256')
    hmac = OpenSSL::HMAC.digest(digest, api_key, data)
    hash = Base64.strict_encode64(hmac)

    # Extract the actual request signature
    signature = request.headers['x-authy-signature']

    # Compare the computed signature with the actual signature
    hash == signature
  end
end
import base64
import hashlib
import hmac
import json
import re

try:
    from urllib.parse import urlencode, quote
except ImportError:
    from urllib import urlencode, quote


def validate_authy_request(request,api_key):
    """Validates that incoming requests genuinely originated from Twilio"""
    url = request.url
    method = request.method

    # Read the Flask request parameters
    params = json.loads(request.data)

    # Convert the JSON dict to query params
    query_params = __make_http_query(params)
    sorted_params = '&'.join(sorted(query_params.replace('/', '%2F').replace('%20', '+').split('&')))
    sorted_params = re.sub("\\%5B([0-9])*\\%5D","%5B%5D",sorted_params)
    sorted_params = re.sub("\\=None", "=", sorted_params)

    # Read the nonce from the request
    nonce = request.headers['X-Authy-Signature-Nonce']

    # Concatenate all together and separate by '|'
    data = '|'.join([nonce, method, url, sorted_params])

    # Compute the signature
    computed_dig = hmac.new(
        api_key.encode(),
        msg=data.encode(),
        digestmod=hashlib.sha256
    ).digest()
    computed_sig = base64.b64encode(computed_dig)

    sig = request.headers['X-Authy-Signature']

    # Compare the message signature with your calculated signature
    # Continue processing the request if it's valid, return a 403 error if
    # it's not
    return sig == computed_sig


def __make_http_query(params, topkey=''):
    """
    Function to covert params into url encoded query string
    :param dict params: Json string sent  by Authy.
    :param string topkey: params key
    :return string: url encoded Query.
    """
    if len(params) == 0:
        return ""
    result = ""
    # is a dictionary?
    if type(params) is dict:
        for key in params.keys():
            newkey = quote(key)
            if topkey != '':
                newkey = topkey + quote('[' + key + ']')
            if type(params[key]) is dict:
                result += __make_http_query(params[key], newkey)
            elif type(params[key]) is list:
                i = 0
                for val in params[key]:
                    if type(val) is dict:
                        result +=   __make_http_query(val, newkey + quote('['+str(i)+']'))
                    else:
                        result += newkey + quote('['+str(i)+']') + "=" + quote(str(val)) + "&"
                    i = i + 1
            # boolean should have special treatment as well
            elif type(params[key]) is bool:
                result += newkey + "=" + quote(str(params[key]).lower()) + "&"
            # assume string (integers and floats work well)
            else:
                result += newkey + "=" + quote(str(params[key])) + "&"
    # remove the last '&'
    if (result) and (topkey == '') and (result[-1] == '&'):
        result = result[:-1]
    return result
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

public class CallbackVerifier {
  
    private boolean verify(HttpServletRequest request, String authyApiKey)
            throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        String forwardedProtocol = request.getHeader("X-Forwarded-Proto");
        String protocol = forwardedProtocol != null ? forwardedProtocol :
                request.getScheme();
        String url = String.format("%s://%s%s", protocol,
                request.getServerName(), request.getServletPath());

        // Fetch Json body
        String body = IOUtils.toString(request.getReader());
        Map<String, Object> bodyJson = objectMapper.readValue(body,
                new TypeReference<Map<String, Object>>() {
                });

        // Flatten and sort JSON elements
        String flattenJson = bodyJson.entrySet()
                .stream()
                .flatMap(FlatMap::flatten)
                .map(e -> e.getKey() + "=" + e.getValue())
                .sorted()
                .collect(Collectors.joining("&"));

        // Read the nonce from the request
        String nonce = request.getHeader("X-Authy-Signature-Nonce");

        // Join all the request bits using '|'
        String data = String.format("%s|%s|%s|%s",
                nonce,
                request.getMethod(),
                url,
                flattenJson);

        try {
            // Compute the signature
            Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
            SecretKeySpec secret_key = new SecretKeySpec(authyApiKey.getBytes(),
                    "HmacSHA256");
            sha256_HMAC.init(secret_key);
            String hash = Base64
                    .getEncoder()
                    .encodeToString(sha256_HMAC.doFinal(data.getBytes()));

            // Extract the actual request signature
            String signature = request.getHeader("X-Authy-Signature");

            // Compare the request signature with your computed signature
            return hash.equals(signature);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            e.printStackTrace();
            return false;
        }
    }
}

class FlatMap {

    private static String openBracket;
    private static String closeBracket;

    static {
        try {
            openBracket = encode("[", "UTF-8");
            closeBracket = encode("]", "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private static String prependKeyName(String keyName, String prefix) {
        StringBuffer result = new StringBuffer();
        result.append(prefix);
        int indexOfOpenBracket = keyName.indexOf(openBracket);
        if(indexOfOpenBracket == -1) {
            indexOfOpenBracket = keyName.length();
        }
        result.append(openBracket);
        result.append(keyName.substring(0, indexOfOpenBracket));
        result.append(closeBracket);
        result.append(keyName.substring(indexOfOpenBracket));
        return result.toString();

    }

    public static Stream<Map.Entry<?, ?>> flatten(Map.Entry<?, ?> e) {
        Object value = e.getValue();
        if (value instanceof Map<?, ?>) {
            return ((Map<?, ?>) value).entrySet()
                    .stream()
                    .flatMap(FlatMap::flatten)
                    .map(el -> {
                        String newKeyName = FlatMap.prependKeyName(
                                (String) el.getKey(), (String) e.getKey());
                        return new AbstractMap.SimpleEntry<>(newKeyName, el.getValue());
                    });
        } else if(value == null) {
            return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(), ""));
        } else if(value instanceof String) {
            try {
                String encodedValue = URLEncoder.encode((String) value, "UTF-8");
                return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(),
                        encodedValue.replace(" ", "+")));
            } catch (UnsupportedEncodingException exception) {
                throw new RuntimeException(exception);
            }
        }
        return Stream.of(e);
    }
}
testable: false
Sort all received parameters in alphabetical, case-sensitive order after converting them to URL format.
Sort the Parameters

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

  • Grab the nonce from the X-Authy-Signature HTTP Header
Loading Code Samples...
Language
SDK Version:
  • 5.x
SDK Version:
  • 7.x
SDK Version:
  • 3.x
SDK Version:
  • 5.x
SDK Version:
  • 6.x
SDK Version:
  • 5.x
const qs = require('qs');
const crypto = require('crypto');

/**
 * @param {http} req This is an HTTP request from the Express middleware
 * @param {!string} apiKey  Account Security API key
 * @return {Boolean} True if verified
 */
function verifyCallback(req, apiKey) {
  const url = req.protocol + '://' + req.get('host') + req.originalUrl;
  const method = req.method;
  const params = req.body;	// needs `npm i body-parser` on Express 4

  // Sort the params
  const sortedParams = qs
    .stringify(params, { arrayFormat: 'brackets' })
    .split('&')
    .sort(sortByPropertyOnly)
    .join('&')
    .replace(/%20/g, '+');

  // Read the nonce from the request
  const nonce = req.headers['x-authy-signature-nonce'];

  // concatinate all together and separate by '|'
  const data = nonce + '|' + method + '|' + url + '|' + sortedParams;

  // compute the signature
  const computedSig = crypto
    .createHmac('sha256', apiKey)
    .update(data)
    .digest('base64');

  const sig = req.headers['x-authy-signature'];

  // compare the message signature with your calculated signature
  return sig === computedSig;
}

/**
 * Sort by property only.
 *  Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'
 *  would be moved after 'events=aaaa'.
 *
 *  For this approach, we split tokenize the string around the '=' value and only sort alphabetically
 *  by the property.
 *
 * @param {string} x
 * @param {string} y
 * @return {number}
 */
function sortByPropertyOnly(x, y) {
  const xx = x.split('=');
  const yy = y.split('=');

  if (xx < yy) {
    return -1;
  }
  if (xx > yy) {
    return 1;
  }
  return 0;
}
using System.Security.Cryptography;
using Microsoft.AspNetCore.Http.Extensions;
using Newtonsoft.Json.Linq;
using System.Text.RegularExpressions;

namespace Test
{
    public class AuthyWebhook
    {
        private bool Verify(Microsoft.AspNetCore.Http.HttpRequest Request,
          Newtonsoft.Json.Linq.JObject JsonContent,
          string ApiKey)
        {
            // Read the nonce from the request
            var nonce = Request.Headers["x-authy-signature-nonce"];
            var method = Request.Method;
            var url = Request.GetDisplayUrl();
            List<String> bodyRequest = new List<String>();

            // Flatten
            var props = GetPropPaths(string.Empty, JsonContent);
            foreach (var kvp in props)
            {
                var encodedKey = System.Web.HttpUtility.UrlEncode(kvp.Item1);
                var encodedValue = System.Web.HttpUtility.UrlEncode(kvp.Item2);
                // uppercase all escaped url-encoded characters
                Regex reg = new Regex(@"%[a-f0-9]{2}");
                encodedKey = reg.Replace(encodedKey, m => m.Value.ToUpperInvariant());
                encodedValue = reg.Replace(encodedValue, m => m.Value.ToUpperInvariant());
                bodyRequest.Add(encodedKey + "=" + encodedValue);
            }

            // Sort the params
            bodyRequest.Sort();
            var parameters = String.Join("&", bodyRequest);

            // concatenate all together and separate by '|'
            var data = $"{nonce}|{method}|{url}|{parameters}";

            // compute the signature
            var encoding = new System.Text.ASCIIEncoding();
            byte[] ApiKeyBytes = encoding.GetBytes(ApiKey);
            byte[] DataBytes = encoding.GetBytes(data);
            using (var hmacsha256 = new HMACSHA256(ApiKeyBytes))
            {
                // Read the Authy Signature from the request
                var sig = Request.Headers["x-authy-signature"];

                // compare the message signature with your calculated signature
                byte[] hashmessage = hmacsha256.ComputeHash(DataBytes);
                var hash = Convert.ToBase64String(hashmessage);
                return hash == sig;
            }
        }

        private IEnumerable<Tuple<string, string>> GetPropPaths(string currPath, JObject obj)
        {
            foreach (var prop in obj.Properties())
            {
                var propPath = string.IsNullOrWhiteSpace(currPath) ? prop.Name : currPath + "[" + prop.Name + "]";

                if (prop.Value.Type == JTokenType.Object)
                {
                    foreach (var subProp in GetPropPaths(propPath, prop.Value as JObject))
                        yield return subProp;
                }
                else if (prop.Value.Type == JTokenType.Boolean)
                {
                    // Fix boolean type conversion to lowercase string
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString().ToLower());
                }
                else
                {
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString());
                }
            }
        }
    }
}
<?php
/**
 *
 * Verify Authy signature
 *
 * @param    string  $apiKey Authy app API KEY
 * @return   bool
 *
 */
function verifyWebhook($apiKey) {
    // Read the nonce from the request
    $nonce = $_SERVER['HTTP_X_AUTHY_SIGNATURE_NONCE'];
    $method = $_SERVER['REQUEST_METHOD'];
    $proto = isset($_SERVER['HTTPS']) ? "https" : "http";
    $url = "{$proto}://{$_SERVER[HTTP_HOST]}{$_SERVER[REQUEST_URI]}";

    // get the Json string sent by Authy
    $json = file_get_contents('php://input');
    $decoded = json_decode($json, true);
    $normalized = normalizeArray($decoded);

    //  sort the parameters by keys
    $query = http_build_query($normalized);
    $exploded = explode('&', $query);
    sort($exploded);
    $params = implode('&', $exploded);

    // concatenate all together and separate them by '|'
    $data = "$nonce|$method|$url|$params";

    // compute the signature
    $computedSig = base64_encode(hash_hmac('sha256', $data, $apiKey, true));

    // get the authy signature
    $sig = $_SERVER['HTTP_X_AUTHY_SIGNATURE'];

    // compare the message signature with your calculated signature
    return hash_equals($computedSig, $sig);
}

/**
 *
 * Replace boolean values with strings.
 *   In PHP methods like http_build_query converts values from an array
 *   to 0 and 1 integers instead of 'false' and 'true'.
 *
 * @param    array  $array Multidimensional array with boolean values
 * @return   array
 *
 */
function normalizeArray($array) {
  foreach ($array as &$el) {
      if (is_bool($el)) {
        $el = ($el) ? "true" : "false";
      } elseif (is_array($el)) {
        $el = normalizeArray($el);
      }
  }
  return $array;
}
require 'uri'

class CallbackVerifier

  # @param [request] A Rails request object
  # @param [api_key] The API key used to sign the request
  # @return [boolean] True if verified
  def verify_callback(request, api_key)
    url = url_for(:only_path => false, :overwrite_params=>nil)

    # Sort and join the parameters on Rails
    json_params = JSON.parse(request.body.read)
    parameter_string = json_params.to_query

    # Read the nonce from the request
    nonce = request.headers['x-authy-signature-nonce']

    # Join all request parts using '|'
    data = "#{nonce}|#{request.method}|#{url}|#{parameter_string}"

    # Compute the signature
    digest = OpenSSL::Digest.new('sha256')
    hmac = OpenSSL::HMAC.digest(digest, api_key, data)
    hash = Base64.strict_encode64(hmac)

    # Extract the actual request signature
    signature = request.headers['x-authy-signature']

    # Compare the computed signature with the actual signature
    hash == signature
  end
end
import base64
import hashlib
import hmac
import json
import re

try:
    from urllib.parse import urlencode, quote
except ImportError:
    from urllib import urlencode, quote


def validate_authy_request(request,api_key):
    """Validates that incoming requests genuinely originated from Twilio"""
    url = request.url
    method = request.method

    # Read the Flask request parameters
    params = json.loads(request.data)

    # Convert the JSON dict to query params
    query_params = __make_http_query(params)
    sorted_params = '&'.join(sorted(query_params.replace('/', '%2F').replace('%20', '+').split('&')))
    sorted_params = re.sub("\\%5B([0-9])*\\%5D","%5B%5D",sorted_params)
    sorted_params = re.sub("\\=None", "=", sorted_params)

    # Read the nonce from the request
    nonce = request.headers['X-Authy-Signature-Nonce']

    # Concatenate all together and separate by '|'
    data = '|'.join([nonce, method, url, sorted_params])

    # Compute the signature
    computed_dig = hmac.new(
        api_key.encode(),
        msg=data.encode(),
        digestmod=hashlib.sha256
    ).digest()
    computed_sig = base64.b64encode(computed_dig)

    sig = request.headers['X-Authy-Signature']

    # Compare the message signature with your calculated signature
    # Continue processing the request if it's valid, return a 403 error if
    # it's not
    return sig == computed_sig


def __make_http_query(params, topkey=''):
    """
    Function to covert params into url encoded query string
    :param dict params: Json string sent  by Authy.
    :param string topkey: params key
    :return string: url encoded Query.
    """
    if len(params) == 0:
        return ""
    result = ""
    # is a dictionary?
    if type(params) is dict:
        for key in params.keys():
            newkey = quote(key)
            if topkey != '':
                newkey = topkey + quote('[' + key + ']')
            if type(params[key]) is dict:
                result += __make_http_query(params[key], newkey)
            elif type(params[key]) is list:
                i = 0
                for val in params[key]:
                    if type(val) is dict:
                        result +=   __make_http_query(val, newkey + quote('['+str(i)+']'))
                    else:
                        result += newkey + quote('['+str(i)+']') + "=" + quote(str(val)) + "&"
                    i = i + 1
            # boolean should have special treatment as well
            elif type(params[key]) is bool:
                result += newkey + "=" + quote(str(params[key]).lower()) + "&"
            # assume string (integers and floats work well)
            else:
                result += newkey + "=" + quote(str(params[key])) + "&"
    # remove the last '&'
    if (result) and (topkey == '') and (result[-1] == '&'):
        result = result[:-1]
    return result
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

public class CallbackVerifier {
  
    private boolean verify(HttpServletRequest request, String authyApiKey)
            throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        String forwardedProtocol = request.getHeader("X-Forwarded-Proto");
        String protocol = forwardedProtocol != null ? forwardedProtocol :
                request.getScheme();
        String url = String.format("%s://%s%s", protocol,
                request.getServerName(), request.getServletPath());

        // Fetch Json body
        String body = IOUtils.toString(request.getReader());
        Map<String, Object> bodyJson = objectMapper.readValue(body,
                new TypeReference<Map<String, Object>>() {
                });

        // Flatten and sort JSON elements
        String flattenJson = bodyJson.entrySet()
                .stream()
                .flatMap(FlatMap::flatten)
                .map(e -> e.getKey() + "=" + e.getValue())
                .sorted()
                .collect(Collectors.joining("&"));

        // Read the nonce from the request
        String nonce = request.getHeader("X-Authy-Signature-Nonce");

        // Join all the request bits using '|'
        String data = String.format("%s|%s|%s|%s",
                nonce,
                request.getMethod(),
                url,
                flattenJson);

        try {
            // Compute the signature
            Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
            SecretKeySpec secret_key = new SecretKeySpec(authyApiKey.getBytes(),
                    "HmacSHA256");
            sha256_HMAC.init(secret_key);
            String hash = Base64
                    .getEncoder()
                    .encodeToString(sha256_HMAC.doFinal(data.getBytes()));

            // Extract the actual request signature
            String signature = request.getHeader("X-Authy-Signature");

            // Compare the request signature with your computed signature
            return hash.equals(signature);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            e.printStackTrace();
            return false;
        }
    }
}

class FlatMap {

    private static String openBracket;
    private static String closeBracket;

    static {
        try {
            openBracket = encode("[", "UTF-8");
            closeBracket = encode("]", "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private static String prependKeyName(String keyName, String prefix) {
        StringBuffer result = new StringBuffer();
        result.append(prefix);
        int indexOfOpenBracket = keyName.indexOf(openBracket);
        if(indexOfOpenBracket == -1) {
            indexOfOpenBracket = keyName.length();
        }
        result.append(openBracket);
        result.append(keyName.substring(0, indexOfOpenBracket));
        result.append(closeBracket);
        result.append(keyName.substring(indexOfOpenBracket));
        return result.toString();

    }

    public static Stream<Map.Entry<?, ?>> flatten(Map.Entry<?, ?> e) {
        Object value = e.getValue();
        if (value instanceof Map<?, ?>) {
            return ((Map<?, ?>) value).entrySet()
                    .stream()
                    .flatMap(FlatMap::flatten)
                    .map(el -> {
                        String newKeyName = FlatMap.prependKeyName(
                                (String) el.getKey(), (String) e.getKey());
                        return new AbstractMap.SimpleEntry<>(newKeyName, el.getValue());
                    });
        } else if(value == null) {
            return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(), ""));
        } else if(value instanceof String) {
            try {
                String encodedValue = URLEncoder.encode((String) value, "UTF-8");
                return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(),
                        encodedValue.replace(" ", "+")));
            } catch (UnsupportedEncodingException exception) {
                throw new RuntimeException(exception);
            }
        }
        return Stream.of(e);
    }
}
testable: false
Grab the nonce from the X-Authy-Signature HTTP Header.
Get the Nonce

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

  • Join the nonce, HTTP method ('POST'), and the sorted parameters together with the vertical pipe, ('|') character
Loading Code Samples...
Language
SDK Version:
  • 5.x
SDK Version:
  • 7.x
SDK Version:
  • 3.x
SDK Version:
  • 5.x
SDK Version:
  • 6.x
SDK Version:
  • 5.x
const qs = require('qs');
const crypto = require('crypto');

/**
 * @param {http} req This is an HTTP request from the Express middleware
 * @param {!string} apiKey  Account Security API key
 * @return {Boolean} True if verified
 */
function verifyCallback(req, apiKey) {
  const url = req.protocol + '://' + req.get('host') + req.originalUrl;
  const method = req.method;
  const params = req.body;	// needs `npm i body-parser` on Express 4

  // Sort the params
  const sortedParams = qs
    .stringify(params, { arrayFormat: 'brackets' })
    .split('&')
    .sort(sortByPropertyOnly)
    .join('&')
    .replace(/%20/g, '+');

  // Read the nonce from the request
  const nonce = req.headers['x-authy-signature-nonce'];

  // concatinate all together and separate by '|'
  const data = nonce + '|' + method + '|' + url + '|' + sortedParams;

  // compute the signature
  const computedSig = crypto
    .createHmac('sha256', apiKey)
    .update(data)
    .digest('base64');

  const sig = req.headers['x-authy-signature'];

  // compare the message signature with your calculated signature
  return sig === computedSig;
}

/**
 * Sort by property only.
 *  Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'
 *  would be moved after 'events=aaaa'.
 *
 *  For this approach, we split tokenize the string around the '=' value and only sort alphabetically
 *  by the property.
 *
 * @param {string} x
 * @param {string} y
 * @return {number}
 */
function sortByPropertyOnly(x, y) {
  const xx = x.split('=');
  const yy = y.split('=');

  if (xx < yy) {
    return -1;
  }
  if (xx > yy) {
    return 1;
  }
  return 0;
}
using System.Security.Cryptography;
using Microsoft.AspNetCore.Http.Extensions;
using Newtonsoft.Json.Linq;
using System.Text.RegularExpressions;

namespace Test
{
    public class AuthyWebhook
    {
        private bool Verify(Microsoft.AspNetCore.Http.HttpRequest Request,
          Newtonsoft.Json.Linq.JObject JsonContent,
          string ApiKey)
        {
            // Read the nonce from the request
            var nonce = Request.Headers["x-authy-signature-nonce"];
            var method = Request.Method;
            var url = Request.GetDisplayUrl();
            List<String> bodyRequest = new List<String>();

            // Flatten
            var props = GetPropPaths(string.Empty, JsonContent);
            foreach (var kvp in props)
            {
                var encodedKey = System.Web.HttpUtility.UrlEncode(kvp.Item1);
                var encodedValue = System.Web.HttpUtility.UrlEncode(kvp.Item2);
                // uppercase all escaped url-encoded characters
                Regex reg = new Regex(@"%[a-f0-9]{2}");
                encodedKey = reg.Replace(encodedKey, m => m.Value.ToUpperInvariant());
                encodedValue = reg.Replace(encodedValue, m => m.Value.ToUpperInvariant());
                bodyRequest.Add(encodedKey + "=" + encodedValue);
            }

            // Sort the params
            bodyRequest.Sort();
            var parameters = String.Join("&", bodyRequest);

            // concatenate all together and separate by '|'
            var data = $"{nonce}|{method}|{url}|{parameters}";

            // compute the signature
            var encoding = new System.Text.ASCIIEncoding();
            byte[] ApiKeyBytes = encoding.GetBytes(ApiKey);
            byte[] DataBytes = encoding.GetBytes(data);
            using (var hmacsha256 = new HMACSHA256(ApiKeyBytes))
            {
                // Read the Authy Signature from the request
                var sig = Request.Headers["x-authy-signature"];

                // compare the message signature with your calculated signature
                byte[] hashmessage = hmacsha256.ComputeHash(DataBytes);
                var hash = Convert.ToBase64String(hashmessage);
                return hash == sig;
            }
        }

        private IEnumerable<Tuple<string, string>> GetPropPaths(string currPath, JObject obj)
        {
            foreach (var prop in obj.Properties())
            {
                var propPath = string.IsNullOrWhiteSpace(currPath) ? prop.Name : currPath + "[" + prop.Name + "]";

                if (prop.Value.Type == JTokenType.Object)
                {
                    foreach (var subProp in GetPropPaths(propPath, prop.Value as JObject))
                        yield return subProp;
                }
                else if (prop.Value.Type == JTokenType.Boolean)
                {
                    // Fix boolean type conversion to lowercase string
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString().ToLower());
                }
                else
                {
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString());
                }
            }
        }
    }
}
<?php
/**
 *
 * Verify Authy signature
 *
 * @param    string  $apiKey Authy app API KEY
 * @return   bool
 *
 */
function verifyWebhook($apiKey) {
    // Read the nonce from the request
    $nonce = $_SERVER['HTTP_X_AUTHY_SIGNATURE_NONCE'];
    $method = $_SERVER['REQUEST_METHOD'];
    $proto = isset($_SERVER['HTTPS']) ? "https" : "http";
    $url = "{$proto}://{$_SERVER[HTTP_HOST]}{$_SERVER[REQUEST_URI]}";

    // get the Json string sent by Authy
    $json = file_get_contents('php://input');
    $decoded = json_decode($json, true);
    $normalized = normalizeArray($decoded);

    //  sort the parameters by keys
    $query = http_build_query($normalized);
    $exploded = explode('&', $query);
    sort($exploded);
    $params = implode('&', $exploded);

    // concatenate all together and separate them by '|'
    $data = "$nonce|$method|$url|$params";

    // compute the signature
    $computedSig = base64_encode(hash_hmac('sha256', $data, $apiKey, true));

    // get the authy signature
    $sig = $_SERVER['HTTP_X_AUTHY_SIGNATURE'];

    // compare the message signature with your calculated signature
    return hash_equals($computedSig, $sig);
}

/**
 *
 * Replace boolean values with strings.
 *   In PHP methods like http_build_query converts values from an array
 *   to 0 and 1 integers instead of 'false' and 'true'.
 *
 * @param    array  $array Multidimensional array with boolean values
 * @return   array
 *
 */
function normalizeArray($array) {
  foreach ($array as &$el) {
      if (is_bool($el)) {
        $el = ($el) ? "true" : "false";
      } elseif (is_array($el)) {
        $el = normalizeArray($el);
      }
  }
  return $array;
}
require 'uri'

class CallbackVerifier

  # @param [request] A Rails request object
  # @param [api_key] The API key used to sign the request
  # @return [boolean] True if verified
  def verify_callback(request, api_key)
    url = url_for(:only_path => false, :overwrite_params=>nil)

    # Sort and join the parameters on Rails
    json_params = JSON.parse(request.body.read)
    parameter_string = json_params.to_query

    # Read the nonce from the request
    nonce = request.headers['x-authy-signature-nonce']

    # Join all request parts using '|'
    data = "#{nonce}|#{request.method}|#{url}|#{parameter_string}"

    # Compute the signature
    digest = OpenSSL::Digest.new('sha256')
    hmac = OpenSSL::HMAC.digest(digest, api_key, data)
    hash = Base64.strict_encode64(hmac)

    # Extract the actual request signature
    signature = request.headers['x-authy-signature']

    # Compare the computed signature with the actual signature
    hash == signature
  end
end
import base64
import hashlib
import hmac
import json
import re

try:
    from urllib.parse import urlencode, quote
except ImportError:
    from urllib import urlencode, quote


def validate_authy_request(request,api_key):
    """Validates that incoming requests genuinely originated from Twilio"""
    url = request.url
    method = request.method

    # Read the Flask request parameters
    params = json.loads(request.data)

    # Convert the JSON dict to query params
    query_params = __make_http_query(params)
    sorted_params = '&'.join(sorted(query_params.replace('/', '%2F').replace('%20', '+').split('&')))
    sorted_params = re.sub("\\%5B([0-9])*\\%5D","%5B%5D",sorted_params)
    sorted_params = re.sub("\\=None", "=", sorted_params)

    # Read the nonce from the request
    nonce = request.headers['X-Authy-Signature-Nonce']

    # Concatenate all together and separate by '|'
    data = '|'.join([nonce, method, url, sorted_params])

    # Compute the signature
    computed_dig = hmac.new(
        api_key.encode(),
        msg=data.encode(),
        digestmod=hashlib.sha256
    ).digest()
    computed_sig = base64.b64encode(computed_dig)

    sig = request.headers['X-Authy-Signature']

    # Compare the message signature with your calculated signature
    # Continue processing the request if it's valid, return a 403 error if
    # it's not
    return sig == computed_sig


def __make_http_query(params, topkey=''):
    """
    Function to covert params into url encoded query string
    :param dict params: Json string sent  by Authy.
    :param string topkey: params key
    :return string: url encoded Query.
    """
    if len(params) == 0:
        return ""
    result = ""
    # is a dictionary?
    if type(params) is dict:
        for key in params.keys():
            newkey = quote(key)
            if topkey != '':
                newkey = topkey + quote('[' + key + ']')
            if type(params[key]) is dict:
                result += __make_http_query(params[key], newkey)
            elif type(params[key]) is list:
                i = 0
                for val in params[key]:
                    if type(val) is dict:
                        result +=   __make_http_query(val, newkey + quote('['+str(i)+']'))
                    else:
                        result += newkey + quote('['+str(i)+']') + "=" + quote(str(val)) + "&"
                    i = i + 1
            # boolean should have special treatment as well
            elif type(params[key]) is bool:
                result += newkey + "=" + quote(str(params[key]).lower()) + "&"
            # assume string (integers and floats work well)
            else:
                result += newkey + "=" + quote(str(params[key])) + "&"
    # remove the last '&'
    if (result) and (topkey == '') and (result[-1] == '&'):
        result = result[:-1]
    return result
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

public class CallbackVerifier {
  
    private boolean verify(HttpServletRequest request, String authyApiKey)
            throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        String forwardedProtocol = request.getHeader("X-Forwarded-Proto");
        String protocol = forwardedProtocol != null ? forwardedProtocol :
                request.getScheme();
        String url = String.format("%s://%s%s", protocol,
                request.getServerName(), request.getServletPath());

        // Fetch Json body
        String body = IOUtils.toString(request.getReader());
        Map<String, Object> bodyJson = objectMapper.readValue(body,
                new TypeReference<Map<String, Object>>() {
                });

        // Flatten and sort JSON elements
        String flattenJson = bodyJson.entrySet()
                .stream()
                .flatMap(FlatMap::flatten)
                .map(e -> e.getKey() + "=" + e.getValue())
                .sorted()
                .collect(Collectors.joining("&"));

        // Read the nonce from the request
        String nonce = request.getHeader("X-Authy-Signature-Nonce");

        // Join all the request bits using '|'
        String data = String.format("%s|%s|%s|%s",
                nonce,
                request.getMethod(),
                url,
                flattenJson);

        try {
            // Compute the signature
            Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
            SecretKeySpec secret_key = new SecretKeySpec(authyApiKey.getBytes(),
                    "HmacSHA256");
            sha256_HMAC.init(secret_key);
            String hash = Base64
                    .getEncoder()
                    .encodeToString(sha256_HMAC.doFinal(data.getBytes()));

            // Extract the actual request signature
            String signature = request.getHeader("X-Authy-Signature");

            // Compare the request signature with your computed signature
            return hash.equals(signature);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            e.printStackTrace();
            return false;
        }
    }
}

class FlatMap {

    private static String openBracket;
    private static String closeBracket;

    static {
        try {
            openBracket = encode("[", "UTF-8");
            closeBracket = encode("]", "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private static String prependKeyName(String keyName, String prefix) {
        StringBuffer result = new StringBuffer();
        result.append(prefix);
        int indexOfOpenBracket = keyName.indexOf(openBracket);
        if(indexOfOpenBracket == -1) {
            indexOfOpenBracket = keyName.length();
        }
        result.append(openBracket);
        result.append(keyName.substring(0, indexOfOpenBracket));
        result.append(closeBracket);
        result.append(keyName.substring(indexOfOpenBracket));
        return result.toString();

    }

    public static Stream<Map.Entry<?, ?>> flatten(Map.Entry<?, ?> e) {
        Object value = e.getValue();
        if (value instanceof Map<?, ?>) {
            return ((Map<?, ?>) value).entrySet()
                    .stream()
                    .flatMap(FlatMap::flatten)
                    .map(el -> {
                        String newKeyName = FlatMap.prependKeyName(
                                (String) el.getKey(), (String) e.getKey());
                        return new AbstractMap.SimpleEntry<>(newKeyName, el.getValue());
                    });
        } else if(value == null) {
            return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(), ""));
        } else if(value instanceof String) {
            try {
                String encodedValue = URLEncoder.encode((String) value, "UTF-8");
                return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(),
                        encodedValue.replace(" ", "+")));
            } catch (UnsupportedEncodingException exception) {
                throw new RuntimeException(exception);
            }
        }
        return Stream.of(e);
    }
}
testable: false
Using the vertical pipe ('\') character, join the nonce, HTTP Method, and sorted parameters.
Join the Nonce, Method, and Params

Using the vertical pipe ('\') character, join the nonce, HTTP Method, and sorted parameters.

  • Use HMAC-SHA256 to hash the string using your Application API Key
Loading Code Samples...
Language
SDK Version:
  • 5.x
SDK Version:
  • 7.x
SDK Version:
  • 3.x
SDK Version:
  • 5.x
SDK Version:
  • 6.x
SDK Version:
  • 5.x
const qs = require('qs');
const crypto = require('crypto');

/**
 * @param {http} req This is an HTTP request from the Express middleware
 * @param {!string} apiKey  Account Security API key
 * @return {Boolean} True if verified
 */
function verifyCallback(req, apiKey) {
  const url = req.protocol + '://' + req.get('host') + req.originalUrl;
  const method = req.method;
  const params = req.body;	// needs `npm i body-parser` on Express 4

  // Sort the params
  const sortedParams = qs
    .stringify(params, { arrayFormat: 'brackets' })
    .split('&')
    .sort(sortByPropertyOnly)
    .join('&')
    .replace(/%20/g, '+');

  // Read the nonce from the request
  const nonce = req.headers['x-authy-signature-nonce'];

  // concatinate all together and separate by '|'
  const data = nonce + '|' + method + '|' + url + '|' + sortedParams;

  // compute the signature
  const computedSig = crypto
    .createHmac('sha256', apiKey)
    .update(data)
    .digest('base64');

  const sig = req.headers['x-authy-signature'];

  // compare the message signature with your calculated signature
  return sig === computedSig;
}

/**
 * Sort by property only.
 *  Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'
 *  would be moved after 'events=aaaa'.
 *
 *  For this approach, we split tokenize the string around the '=' value and only sort alphabetically
 *  by the property.
 *
 * @param {string} x
 * @param {string} y
 * @return {number}
 */
function sortByPropertyOnly(x, y) {
  const xx = x.split('=');
  const yy = y.split('=');

  if (xx < yy) {
    return -1;
  }
  if (xx > yy) {
    return 1;
  }
  return 0;
}
using System.Security.Cryptography;
using Microsoft.AspNetCore.Http.Extensions;
using Newtonsoft.Json.Linq;
using System.Text.RegularExpressions;

namespace Test
{
    public class AuthyWebhook
    {
        private bool Verify(Microsoft.AspNetCore.Http.HttpRequest Request,
          Newtonsoft.Json.Linq.JObject JsonContent,
          string ApiKey)
        {
            // Read the nonce from the request
            var nonce = Request.Headers["x-authy-signature-nonce"];
            var method = Request.Method;
            var url = Request.GetDisplayUrl();
            List<String> bodyRequest = new List<String>();

            // Flatten
            var props = GetPropPaths(string.Empty, JsonContent);
            foreach (var kvp in props)
            {
                var encodedKey = System.Web.HttpUtility.UrlEncode(kvp.Item1);
                var encodedValue = System.Web.HttpUtility.UrlEncode(kvp.Item2);
                // uppercase all escaped url-encoded characters
                Regex reg = new Regex(@"%[a-f0-9]{2}");
                encodedKey = reg.Replace(encodedKey, m => m.Value.ToUpperInvariant());
                encodedValue = reg.Replace(encodedValue, m => m.Value.ToUpperInvariant());
                bodyRequest.Add(encodedKey + "=" + encodedValue);
            }

            // Sort the params
            bodyRequest.Sort();
            var parameters = String.Join("&", bodyRequest);

            // concatenate all together and separate by '|'
            var data = $"{nonce}|{method}|{url}|{parameters}";

            // compute the signature
            var encoding = new System.Text.ASCIIEncoding();
            byte[] ApiKeyBytes = encoding.GetBytes(ApiKey);
            byte[] DataBytes = encoding.GetBytes(data);
            using (var hmacsha256 = new HMACSHA256(ApiKeyBytes))
            {
                // Read the Authy Signature from the request
                var sig = Request.Headers["x-authy-signature"];

                // compare the message signature with your calculated signature
                byte[] hashmessage = hmacsha256.ComputeHash(DataBytes);
                var hash = Convert.ToBase64String(hashmessage);
                return hash == sig;
            }
        }

        private IEnumerable<Tuple<string, string>> GetPropPaths(string currPath, JObject obj)
        {
            foreach (var prop in obj.Properties())
            {
                var propPath = string.IsNullOrWhiteSpace(currPath) ? prop.Name : currPath + "[" + prop.Name + "]";

                if (prop.Value.Type == JTokenType.Object)
                {
                    foreach (var subProp in GetPropPaths(propPath, prop.Value as JObject))
                        yield return subProp;
                }
                else if (prop.Value.Type == JTokenType.Boolean)
                {
                    // Fix boolean type conversion to lowercase string
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString().ToLower());
                }
                else
                {
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString());
                }
            }
        }
    }
}
<?php
/**
 *
 * Verify Authy signature
 *
 * @param    string  $apiKey Authy app API KEY
 * @return   bool
 *
 */
function verifyWebhook($apiKey) {
    // Read the nonce from the request
    $nonce = $_SERVER['HTTP_X_AUTHY_SIGNATURE_NONCE'];
    $method = $_SERVER['REQUEST_METHOD'];
    $proto = isset($_SERVER['HTTPS']) ? "https" : "http";
    $url = "{$proto}://{$_SERVER[HTTP_HOST]}{$_SERVER[REQUEST_URI]}";

    // get the Json string sent by Authy
    $json = file_get_contents('php://input');
    $decoded = json_decode($json, true);
    $normalized = normalizeArray($decoded);

    //  sort the parameters by keys
    $query = http_build_query($normalized);
    $exploded = explode('&', $query);
    sort($exploded);
    $params = implode('&', $exploded);

    // concatenate all together and separate them by '|'
    $data = "$nonce|$method|$url|$params";

    // compute the signature
    $computedSig = base64_encode(hash_hmac('sha256', $data, $apiKey, true));

    // get the authy signature
    $sig = $_SERVER['HTTP_X_AUTHY_SIGNATURE'];

    // compare the message signature with your calculated signature
    return hash_equals($computedSig, $sig);
}

/**
 *
 * Replace boolean values with strings.
 *   In PHP methods like http_build_query converts values from an array
 *   to 0 and 1 integers instead of 'false' and 'true'.
 *
 * @param    array  $array Multidimensional array with boolean values
 * @return   array
 *
 */
function normalizeArray($array) {
  foreach ($array as &$el) {
      if (is_bool($el)) {
        $el = ($el) ? "true" : "false";
      } elseif (is_array($el)) {
        $el = normalizeArray($el);
      }
  }
  return $array;
}
require 'uri'

class CallbackVerifier

  # @param [request] A Rails request object
  # @param [api_key] The API key used to sign the request
  # @return [boolean] True if verified
  def verify_callback(request, api_key)
    url = url_for(:only_path => false, :overwrite_params=>nil)

    # Sort and join the parameters on Rails
    json_params = JSON.parse(request.body.read)
    parameter_string = json_params.to_query

    # Read the nonce from the request
    nonce = request.headers['x-authy-signature-nonce']

    # Join all request parts using '|'
    data = "#{nonce}|#{request.method}|#{url}|#{parameter_string}"

    # Compute the signature
    digest = OpenSSL::Digest.new('sha256')
    hmac = OpenSSL::HMAC.digest(digest, api_key, data)
    hash = Base64.strict_encode64(hmac)

    # Extract the actual request signature
    signature = request.headers['x-authy-signature']

    # Compare the computed signature with the actual signature
    hash == signature
  end
end
import base64
import hashlib
import hmac
import json
import re

try:
    from urllib.parse import urlencode, quote
except ImportError:
    from urllib import urlencode, quote


def validate_authy_request(request,api_key):
    """Validates that incoming requests genuinely originated from Twilio"""
    url = request.url
    method = request.method

    # Read the Flask request parameters
    params = json.loads(request.data)

    # Convert the JSON dict to query params
    query_params = __make_http_query(params)
    sorted_params = '&'.join(sorted(query_params.replace('/', '%2F').replace('%20', '+').split('&')))
    sorted_params = re.sub("\\%5B([0-9])*\\%5D","%5B%5D",sorted_params)
    sorted_params = re.sub("\\=None", "=", sorted_params)

    # Read the nonce from the request
    nonce = request.headers['X-Authy-Signature-Nonce']

    # Concatenate all together and separate by '|'
    data = '|'.join([nonce, method, url, sorted_params])

    # Compute the signature
    computed_dig = hmac.new(
        api_key.encode(),
        msg=data.encode(),
        digestmod=hashlib.sha256
    ).digest()
    computed_sig = base64.b64encode(computed_dig)

    sig = request.headers['X-Authy-Signature']

    # Compare the message signature with your calculated signature
    # Continue processing the request if it's valid, return a 403 error if
    # it's not
    return sig == computed_sig


def __make_http_query(params, topkey=''):
    """
    Function to covert params into url encoded query string
    :param dict params: Json string sent  by Authy.
    :param string topkey: params key
    :return string: url encoded Query.
    """
    if len(params) == 0:
        return ""
    result = ""
    # is a dictionary?
    if type(params) is dict:
        for key in params.keys():
            newkey = quote(key)
            if topkey != '':
                newkey = topkey + quote('[' + key + ']')
            if type(params[key]) is dict:
                result += __make_http_query(params[key], newkey)
            elif type(params[key]) is list:
                i = 0
                for val in params[key]:
                    if type(val) is dict:
                        result +=   __make_http_query(val, newkey + quote('['+str(i)+']'))
                    else:
                        result += newkey + quote('['+str(i)+']') + "=" + quote(str(val)) + "&"
                    i = i + 1
            # boolean should have special treatment as well
            elif type(params[key]) is bool:
                result += newkey + "=" + quote(str(params[key]).lower()) + "&"
            # assume string (integers and floats work well)
            else:
                result += newkey + "=" + quote(str(params[key])) + "&"
    # remove the last '&'
    if (result) and (topkey == '') and (result[-1] == '&'):
        result = result[:-1]
    return result
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

public class CallbackVerifier {
  
    private boolean verify(HttpServletRequest request, String authyApiKey)
            throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        String forwardedProtocol = request.getHeader("X-Forwarded-Proto");
        String protocol = forwardedProtocol != null ? forwardedProtocol :
                request.getScheme();
        String url = String.format("%s://%s%s", protocol,
                request.getServerName(), request.getServletPath());

        // Fetch Json body
        String body = IOUtils.toString(request.getReader());
        Map<String, Object> bodyJson = objectMapper.readValue(body,
                new TypeReference<Map<String, Object>>() {
                });

        // Flatten and sort JSON elements
        String flattenJson = bodyJson.entrySet()
                .stream()
                .flatMap(FlatMap::flatten)
                .map(e -> e.getKey() + "=" + e.getValue())
                .sorted()
                .collect(Collectors.joining("&"));

        // Read the nonce from the request
        String nonce = request.getHeader("X-Authy-Signature-Nonce");

        // Join all the request bits using '|'
        String data = String.format("%s|%s|%s|%s",
                nonce,
                request.getMethod(),
                url,
                flattenJson);

        try {
            // Compute the signature
            Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
            SecretKeySpec secret_key = new SecretKeySpec(authyApiKey.getBytes(),
                    "HmacSHA256");
            sha256_HMAC.init(secret_key);
            String hash = Base64
                    .getEncoder()
                    .encodeToString(sha256_HMAC.doFinal(data.getBytes()));

            // Extract the actual request signature
            String signature = request.getHeader("X-Authy-Signature");

            // Compare the request signature with your computed signature
            return hash.equals(signature);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            e.printStackTrace();
            return false;
        }
    }
}

class FlatMap {

    private static String openBracket;
    private static String closeBracket;

    static {
        try {
            openBracket = encode("[", "UTF-8");
            closeBracket = encode("]", "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private static String prependKeyName(String keyName, String prefix) {
        StringBuffer result = new StringBuffer();
        result.append(prefix);
        int indexOfOpenBracket = keyName.indexOf(openBracket);
        if(indexOfOpenBracket == -1) {
            indexOfOpenBracket = keyName.length();
        }
        result.append(openBracket);
        result.append(keyName.substring(0, indexOfOpenBracket));
        result.append(closeBracket);
        result.append(keyName.substring(indexOfOpenBracket));
        return result.toString();

    }

    public static Stream<Map.Entry<?, ?>> flatten(Map.Entry<?, ?> e) {
        Object value = e.getValue();
        if (value instanceof Map<?, ?>) {
            return ((Map<?, ?>) value).entrySet()
                    .stream()
                    .flatMap(FlatMap::flatten)
                    .map(el -> {
                        String newKeyName = FlatMap.prependKeyName(
                                (String) el.getKey(), (String) e.getKey());
                        return new AbstractMap.SimpleEntry<>(newKeyName, el.getValue());
                    });
        } else if(value == null) {
            return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(), ""));
        } else if(value instanceof String) {
            try {
                String encodedValue = URLEncoder.encode((String) value, "UTF-8");
                return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(),
                        encodedValue.replace(" ", "+")));
            } catch (UnsupportedEncodingException exception) {
                throw new RuntimeException(exception);
            }
        }
        return Stream.of(e);
    }
}
testable: false
Use HMAC-SHA256 to hash the resulting string with your Application API Key from the console.
Hash the Combined String with HMAC-SHA256

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

  • Base64 Encode the digest (as described in RFC 4648 - do not include line breaks)
Loading Code Samples...
Language
SDK Version:
  • 5.x
SDK Version:
  • 7.x
SDK Version:
  • 3.x
SDK Version:
  • 5.x
SDK Version:
  • 6.x
SDK Version:
  • 5.x
const qs = require('qs');
const crypto = require('crypto');

/**
 * @param {http} req This is an HTTP request from the Express middleware
 * @param {!string} apiKey  Account Security API key
 * @return {Boolean} True if verified
 */
function verifyCallback(req, apiKey) {
  const url = req.protocol + '://' + req.get('host') + req.originalUrl;
  const method = req.method;
  const params = req.body;	// needs `npm i body-parser` on Express 4

  // Sort the params
  const sortedParams = qs
    .stringify(params, { arrayFormat: 'brackets' })
    .split('&')
    .sort(sortByPropertyOnly)
    .join('&')
    .replace(/%20/g, '+');

  // Read the nonce from the request
  const nonce = req.headers['x-authy-signature-nonce'];

  // concatinate all together and separate by '|'
  const data = nonce + '|' + method + '|' + url + '|' + sortedParams;

  // compute the signature
  const computedSig = crypto
    .createHmac('sha256', apiKey)
    .update(data)
    .digest('base64');

  const sig = req.headers['x-authy-signature'];

  // compare the message signature with your calculated signature
  return sig === computedSig;
}

/**
 * Sort by property only.
 *  Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'
 *  would be moved after 'events=aaaa'.
 *
 *  For this approach, we split tokenize the string around the '=' value and only sort alphabetically
 *  by the property.
 *
 * @param {string} x
 * @param {string} y
 * @return {number}
 */
function sortByPropertyOnly(x, y) {
  const xx = x.split('=');
  const yy = y.split('=');

  if (xx < yy) {
    return -1;
  }
  if (xx > yy) {
    return 1;
  }
  return 0;
}
using System.Security.Cryptography;
using Microsoft.AspNetCore.Http.Extensions;
using Newtonsoft.Json.Linq;
using System.Text.RegularExpressions;

namespace Test
{
    public class AuthyWebhook
    {
        private bool Verify(Microsoft.AspNetCore.Http.HttpRequest Request,
          Newtonsoft.Json.Linq.JObject JsonContent,
          string ApiKey)
        {
            // Read the nonce from the request
            var nonce = Request.Headers["x-authy-signature-nonce"];
            var method = Request.Method;
            var url = Request.GetDisplayUrl();
            List<String> bodyRequest = new List<String>();

            // Flatten
            var props = GetPropPaths(string.Empty, JsonContent);
            foreach (var kvp in props)
            {
                var encodedKey = System.Web.HttpUtility.UrlEncode(kvp.Item1);
                var encodedValue = System.Web.HttpUtility.UrlEncode(kvp.Item2);
                // uppercase all escaped url-encoded characters
                Regex reg = new Regex(@"%[a-f0-9]{2}");
                encodedKey = reg.Replace(encodedKey, m => m.Value.ToUpperInvariant());
                encodedValue = reg.Replace(encodedValue, m => m.Value.ToUpperInvariant());
                bodyRequest.Add(encodedKey + "=" + encodedValue);
            }

            // Sort the params
            bodyRequest.Sort();
            var parameters = String.Join("&", bodyRequest);

            // concatenate all together and separate by '|'
            var data = $"{nonce}|{method}|{url}|{parameters}";

            // compute the signature
            var encoding = new System.Text.ASCIIEncoding();
            byte[] ApiKeyBytes = encoding.GetBytes(ApiKey);
            byte[] DataBytes = encoding.GetBytes(data);
            using (var hmacsha256 = new HMACSHA256(ApiKeyBytes))
            {
                // Read the Authy Signature from the request
                var sig = Request.Headers["x-authy-signature"];

                // compare the message signature with your calculated signature
                byte[] hashmessage = hmacsha256.ComputeHash(DataBytes);
                var hash = Convert.ToBase64String(hashmessage);
                return hash == sig;
            }
        }

        private IEnumerable<Tuple<string, string>> GetPropPaths(string currPath, JObject obj)
        {
            foreach (var prop in obj.Properties())
            {
                var propPath = string.IsNullOrWhiteSpace(currPath) ? prop.Name : currPath + "[" + prop.Name + "]";

                if (prop.Value.Type == JTokenType.Object)
                {
                    foreach (var subProp in GetPropPaths(propPath, prop.Value as JObject))
                        yield return subProp;
                }
                else if (prop.Value.Type == JTokenType.Boolean)
                {
                    // Fix boolean type conversion to lowercase string
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString().ToLower());
                }
                else
                {
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString());
                }
            }
        }
    }
}
<?php
/**
 *
 * Verify Authy signature
 *
 * @param    string  $apiKey Authy app API KEY
 * @return   bool
 *
 */
function verifyWebhook($apiKey) {
    // Read the nonce from the request
    $nonce = $_SERVER['HTTP_X_AUTHY_SIGNATURE_NONCE'];
    $method = $_SERVER['REQUEST_METHOD'];
    $proto = isset($_SERVER['HTTPS']) ? "https" : "http";
    $url = "{$proto}://{$_SERVER[HTTP_HOST]}{$_SERVER[REQUEST_URI]}";

    // get the Json string sent by Authy
    $json = file_get_contents('php://input');
    $decoded = json_decode($json, true);
    $normalized = normalizeArray($decoded);

    //  sort the parameters by keys
    $query = http_build_query($normalized);
    $exploded = explode('&', $query);
    sort($exploded);
    $params = implode('&', $exploded);

    // concatenate all together and separate them by '|'
    $data = "$nonce|$method|$url|$params";

    // compute the signature
    $computedSig = base64_encode(hash_hmac('sha256', $data, $apiKey, true));

    // get the authy signature
    $sig = $_SERVER['HTTP_X_AUTHY_SIGNATURE'];

    // compare the message signature with your calculated signature
    return hash_equals($computedSig, $sig);
}

/**
 *
 * Replace boolean values with strings.
 *   In PHP methods like http_build_query converts values from an array
 *   to 0 and 1 integers instead of 'false' and 'true'.
 *
 * @param    array  $array Multidimensional array with boolean values
 * @return   array
 *
 */
function normalizeArray($array) {
  foreach ($array as &$el) {
      if (is_bool($el)) {
        $el = ($el) ? "true" : "false";
      } elseif (is_array($el)) {
        $el = normalizeArray($el);
      }
  }
  return $array;
}
require 'uri'

class CallbackVerifier

  # @param [request] A Rails request object
  # @param [api_key] The API key used to sign the request
  # @return [boolean] True if verified
  def verify_callback(request, api_key)
    url = url_for(:only_path => false, :overwrite_params=>nil)

    # Sort and join the parameters on Rails
    json_params = JSON.parse(request.body.read)
    parameter_string = json_params.to_query

    # Read the nonce from the request
    nonce = request.headers['x-authy-signature-nonce']

    # Join all request parts using '|'
    data = "#{nonce}|#{request.method}|#{url}|#{parameter_string}"

    # Compute the signature
    digest = OpenSSL::Digest.new('sha256')
    hmac = OpenSSL::HMAC.digest(digest, api_key, data)
    hash = Base64.strict_encode64(hmac)

    # Extract the actual request signature
    signature = request.headers['x-authy-signature']

    # Compare the computed signature with the actual signature
    hash == signature
  end
end
import base64
import hashlib
import hmac
import json
import re

try:
    from urllib.parse import urlencode, quote
except ImportError:
    from urllib import urlencode, quote


def validate_authy_request(request,api_key):
    """Validates that incoming requests genuinely originated from Twilio"""
    url = request.url
    method = request.method

    # Read the Flask request parameters
    params = json.loads(request.data)

    # Convert the JSON dict to query params
    query_params = __make_http_query(params)
    sorted_params = '&'.join(sorted(query_params.replace('/', '%2F').replace('%20', '+').split('&')))
    sorted_params = re.sub("\\%5B([0-9])*\\%5D","%5B%5D",sorted_params)
    sorted_params = re.sub("\\=None", "=", sorted_params)

    # Read the nonce from the request
    nonce = request.headers['X-Authy-Signature-Nonce']

    # Concatenate all together and separate by '|'
    data = '|'.join([nonce, method, url, sorted_params])

    # Compute the signature
    computed_dig = hmac.new(
        api_key.encode(),
        msg=data.encode(),
        digestmod=hashlib.sha256
    ).digest()
    computed_sig = base64.b64encode(computed_dig)

    sig = request.headers['X-Authy-Signature']

    # Compare the message signature with your calculated signature
    # Continue processing the request if it's valid, return a 403 error if
    # it's not
    return sig == computed_sig


def __make_http_query(params, topkey=''):
    """
    Function to covert params into url encoded query string
    :param dict params: Json string sent  by Authy.
    :param string topkey: params key
    :return string: url encoded Query.
    """
    if len(params) == 0:
        return ""
    result = ""
    # is a dictionary?
    if type(params) is dict:
        for key in params.keys():
            newkey = quote(key)
            if topkey != '':
                newkey = topkey + quote('[' + key + ']')
            if type(params[key]) is dict:
                result += __make_http_query(params[key], newkey)
            elif type(params[key]) is list:
                i = 0
                for val in params[key]:
                    if type(val) is dict:
                        result +=   __make_http_query(val, newkey + quote('['+str(i)+']'))
                    else:
                        result += newkey + quote('['+str(i)+']') + "=" + quote(str(val)) + "&"
                    i = i + 1
            # boolean should have special treatment as well
            elif type(params[key]) is bool:
                result += newkey + "=" + quote(str(params[key]).lower()) + "&"
            # assume string (integers and floats work well)
            else:
                result += newkey + "=" + quote(str(params[key])) + "&"
    # remove the last '&'
    if (result) and (topkey == '') and (result[-1] == '&'):
        result = result[:-1]
    return result
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

public class CallbackVerifier {
  
    private boolean verify(HttpServletRequest request, String authyApiKey)
            throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        String forwardedProtocol = request.getHeader("X-Forwarded-Proto");
        String protocol = forwardedProtocol != null ? forwardedProtocol :
                request.getScheme();
        String url = String.format("%s://%s%s", protocol,
                request.getServerName(), request.getServletPath());

        // Fetch Json body
        String body = IOUtils.toString(request.getReader());
        Map<String, Object> bodyJson = objectMapper.readValue(body,
                new TypeReference<Map<String, Object>>() {
                });

        // Flatten and sort JSON elements
        String flattenJson = bodyJson.entrySet()
                .stream()
                .flatMap(FlatMap::flatten)
                .map(e -> e.getKey() + "=" + e.getValue())
                .sorted()
                .collect(Collectors.joining("&"));

        // Read the nonce from the request
        String nonce = request.getHeader("X-Authy-Signature-Nonce");

        // Join all the request bits using '|'
        String data = String.format("%s|%s|%s|%s",
                nonce,
                request.getMethod(),
                url,
                flattenJson);

        try {
            // Compute the signature
            Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
            SecretKeySpec secret_key = new SecretKeySpec(authyApiKey.getBytes(),
                    "HmacSHA256");
            sha256_HMAC.init(secret_key);
            String hash = Base64
                    .getEncoder()
                    .encodeToString(sha256_HMAC.doFinal(data.getBytes()));

            // Extract the actual request signature
            String signature = request.getHeader("X-Authy-Signature");

            // Compare the request signature with your computed signature
            return hash.equals(signature);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            e.printStackTrace();
            return false;
        }
    }
}

class FlatMap {

    private static String openBracket;
    private static String closeBracket;

    static {
        try {
            openBracket = encode("[", "UTF-8");
            closeBracket = encode("]", "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private static String prependKeyName(String keyName, String prefix) {
        StringBuffer result = new StringBuffer();
        result.append(prefix);
        int indexOfOpenBracket = keyName.indexOf(openBracket);
        if(indexOfOpenBracket == -1) {
            indexOfOpenBracket = keyName.length();
        }
        result.append(openBracket);
        result.append(keyName.substring(0, indexOfOpenBracket));
        result.append(closeBracket);
        result.append(keyName.substring(indexOfOpenBracket));
        return result.toString();

    }

    public static Stream<Map.Entry<?, ?>> flatten(Map.Entry<?, ?> e) {
        Object value = e.getValue();
        if (value instanceof Map<?, ?>) {
            return ((Map<?, ?>) value).entrySet()
                    .stream()
                    .flatMap(FlatMap::flatten)
                    .map(el -> {
                        String newKeyName = FlatMap.prependKeyName(
                                (String) el.getKey(), (String) e.getKey());
                        return new AbstractMap.SimpleEntry<>(newKeyName, el.getValue());
                    });
        } else if(value == null) {
            return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(), ""));
        } else if(value instanceof String) {
            try {
                String encodedValue = URLEncoder.encode((String) value, "UTF-8");
                return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(),
                        encodedValue.replace(" ", "+")));
            } catch (UnsupportedEncodingException exception) {
                throw new RuntimeException(exception);
            }
        }
        return Stream.of(e);
    }
}
testable: false
Follow RFC4648 to Base64 encode the digest
Encode the Digest with Base64

Follow RFC4648 to Base64 encode the digest

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

Loading Code Samples...
Language
SDK Version:
  • 5.x
SDK Version:
  • 7.x
SDK Version:
  • 3.x
SDK Version:
  • 5.x
SDK Version:
  • 6.x
SDK Version:
  • 5.x
const qs = require('qs');
const crypto = require('crypto');

/**
 * @param {http} req This is an HTTP request from the Express middleware
 * @param {!string} apiKey  Account Security API key
 * @return {Boolean} True if verified
 */
function verifyCallback(req, apiKey) {
  const url = req.protocol + '://' + req.get('host') + req.originalUrl;
  const method = req.method;
  const params = req.body;	// needs `npm i body-parser` on Express 4

  // Sort the params
  const sortedParams = qs
    .stringify(params, { arrayFormat: 'brackets' })
    .split('&')
    .sort(sortByPropertyOnly)
    .join('&')
    .replace(/%20/g, '+');

  // Read the nonce from the request
  const nonce = req.headers['x-authy-signature-nonce'];

  // concatinate all together and separate by '|'
  const data = nonce + '|' + method + '|' + url + '|' + sortedParams;

  // compute the signature
  const computedSig = crypto
    .createHmac('sha256', apiKey)
    .update(data)
    .digest('base64');

  const sig = req.headers['x-authy-signature'];

  // compare the message signature with your calculated signature
  return sig === computedSig;
}

/**
 * Sort by property only.
 *  Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'
 *  would be moved after 'events=aaaa'.
 *
 *  For this approach, we split tokenize the string around the '=' value and only sort alphabetically
 *  by the property.
 *
 * @param {string} x
 * @param {string} y
 * @return {number}
 */
function sortByPropertyOnly(x, y) {
  const xx = x.split('=');
  const yy = y.split('=');

  if (xx < yy) {
    return -1;
  }
  if (xx > yy) {
    return 1;
  }
  return 0;
}
using System.Security.Cryptography;
using Microsoft.AspNetCore.Http.Extensions;
using Newtonsoft.Json.Linq;
using System.Text.RegularExpressions;

namespace Test
{
    public class AuthyWebhook
    {
        private bool Verify(Microsoft.AspNetCore.Http.HttpRequest Request,
          Newtonsoft.Json.Linq.JObject JsonContent,
          string ApiKey)
        {
            // Read the nonce from the request
            var nonce = Request.Headers["x-authy-signature-nonce"];
            var method = Request.Method;
            var url = Request.GetDisplayUrl();
            List<String> bodyRequest = new List<String>();

            // Flatten
            var props = GetPropPaths(string.Empty, JsonContent);
            foreach (var kvp in props)
            {
                var encodedKey = System.Web.HttpUtility.UrlEncode(kvp.Item1);
                var encodedValue = System.Web.HttpUtility.UrlEncode(kvp.Item2);
                // uppercase all escaped url-encoded characters
                Regex reg = new Regex(@"%[a-f0-9]{2}");
                encodedKey = reg.Replace(encodedKey, m => m.Value.ToUpperInvariant());
                encodedValue = reg.Replace(encodedValue, m => m.Value.ToUpperInvariant());
                bodyRequest.Add(encodedKey + "=" + encodedValue);
            }

            // Sort the params
            bodyRequest.Sort();
            var parameters = String.Join("&", bodyRequest);

            // concatenate all together and separate by '|'
            var data = $"{nonce}|{method}|{url}|{parameters}";

            // compute the signature
            var encoding = new System.Text.ASCIIEncoding();
            byte[] ApiKeyBytes = encoding.GetBytes(ApiKey);
            byte[] DataBytes = encoding.GetBytes(data);
            using (var hmacsha256 = new HMACSHA256(ApiKeyBytes))
            {
                // Read the Authy Signature from the request
                var sig = Request.Headers["x-authy-signature"];

                // compare the message signature with your calculated signature
                byte[] hashmessage = hmacsha256.ComputeHash(DataBytes);
                var hash = Convert.ToBase64String(hashmessage);
                return hash == sig;
            }
        }

        private IEnumerable<Tuple<string, string>> GetPropPaths(string currPath, JObject obj)
        {
            foreach (var prop in obj.Properties())
            {
                var propPath = string.IsNullOrWhiteSpace(currPath) ? prop.Name : currPath + "[" + prop.Name + "]";

                if (prop.Value.Type == JTokenType.Object)
                {
                    foreach (var subProp in GetPropPaths(propPath, prop.Value as JObject))
                        yield return subProp;
                }
                else if (prop.Value.Type == JTokenType.Boolean)
                {
                    // Fix boolean type conversion to lowercase string
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString().ToLower());
                }
                else
                {
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString());
                }
            }
        }
    }
}
<?php
/**
 *
 * Verify Authy signature
 *
 * @param    string  $apiKey Authy app API KEY
 * @return   bool
 *
 */
function verifyWebhook($apiKey) {
    // Read the nonce from the request
    $nonce = $_SERVER['HTTP_X_AUTHY_SIGNATURE_NONCE'];
    $method = $_SERVER['REQUEST_METHOD'];
    $proto = isset($_SERVER['HTTPS']) ? "https" : "http";
    $url = "{$proto}://{$_SERVER[HTTP_HOST]}{$_SERVER[REQUEST_URI]}";

    // get the Json string sent by Authy
    $json = file_get_contents('php://input');
    $decoded = json_decode($json, true);
    $normalized = normalizeArray($decoded);

    //  sort the parameters by keys
    $query = http_build_query($normalized);
    $exploded = explode('&', $query);
    sort($exploded);
    $params = implode('&', $exploded);

    // concatenate all together and separate them by '|'
    $data = "$nonce|$method|$url|$params";

    // compute the signature
    $computedSig = base64_encode(hash_hmac('sha256', $data, $apiKey, true));

    // get the authy signature
    $sig = $_SERVER['HTTP_X_AUTHY_SIGNATURE'];

    // compare the message signature with your calculated signature
    return hash_equals($computedSig, $sig);
}

/**
 *
 * Replace boolean values with strings.
 *   In PHP methods like http_build_query converts values from an array
 *   to 0 and 1 integers instead of 'false' and 'true'.
 *
 * @param    array  $array Multidimensional array with boolean values
 * @return   array
 *
 */
function normalizeArray($array) {
  foreach ($array as &$el) {
      if (is_bool($el)) {
        $el = ($el) ? "true" : "false";
      } elseif (is_array($el)) {
        $el = normalizeArray($el);
      }
  }
  return $array;
}
require 'uri'

class CallbackVerifier

  # @param [request] A Rails request object
  # @param [api_key] The API key used to sign the request
  # @return [boolean] True if verified
  def verify_callback(request, api_key)
    url = url_for(:only_path => false, :overwrite_params=>nil)

    # Sort and join the parameters on Rails
    json_params = JSON.parse(request.body.read)
    parameter_string = json_params.to_query

    # Read the nonce from the request
    nonce = request.headers['x-authy-signature-nonce']

    # Join all request parts using '|'
    data = "#{nonce}|#{request.method}|#{url}|#{parameter_string}"

    # Compute the signature
    digest = OpenSSL::Digest.new('sha256')
    hmac = OpenSSL::HMAC.digest(digest, api_key, data)
    hash = Base64.strict_encode64(hmac)

    # Extract the actual request signature
    signature = request.headers['x-authy-signature']

    # Compare the computed signature with the actual signature
    hash == signature
  end
end
import base64
import hashlib
import hmac
import json
import re

try:
    from urllib.parse import urlencode, quote
except ImportError:
    from urllib import urlencode, quote


def validate_authy_request(request,api_key):
    """Validates that incoming requests genuinely originated from Twilio"""
    url = request.url
    method = request.method

    # Read the Flask request parameters
    params = json.loads(request.data)

    # Convert the JSON dict to query params
    query_params = __make_http_query(params)
    sorted_params = '&'.join(sorted(query_params.replace('/', '%2F').replace('%20', '+').split('&')))
    sorted_params = re.sub("\\%5B([0-9])*\\%5D","%5B%5D",sorted_params)
    sorted_params = re.sub("\\=None", "=", sorted_params)

    # Read the nonce from the request
    nonce = request.headers['X-Authy-Signature-Nonce']

    # Concatenate all together and separate by '|'
    data = '|'.join([nonce, method, url, sorted_params])

    # Compute the signature
    computed_dig = hmac.new(
        api_key.encode(),
        msg=data.encode(),
        digestmod=hashlib.sha256
    ).digest()
    computed_sig = base64.b64encode(computed_dig)

    sig = request.headers['X-Authy-Signature']

    # Compare the message signature with your calculated signature
    # Continue processing the request if it's valid, return a 403 error if
    # it's not
    return sig == computed_sig


def __make_http_query(params, topkey=''):
    """
    Function to covert params into url encoded query string
    :param dict params: Json string sent  by Authy.
    :param string topkey: params key
    :return string: url encoded Query.
    """
    if len(params) == 0:
        return ""
    result = ""
    # is a dictionary?
    if type(params) is dict:
        for key in params.keys():
            newkey = quote(key)
            if topkey != '':
                newkey = topkey + quote('[' + key + ']')
            if type(params[key]) is dict:
                result += __make_http_query(params[key], newkey)
            elif type(params[key]) is list:
                i = 0
                for val in params[key]:
                    if type(val) is dict:
                        result +=   __make_http_query(val, newkey + quote('['+str(i)+']'))
                    else:
                        result += newkey + quote('['+str(i)+']') + "=" + quote(str(val)) + "&"
                    i = i + 1
            # boolean should have special treatment as well
            elif type(params[key]) is bool:
                result += newkey + "=" + quote(str(params[key]).lower()) + "&"
            # assume string (integers and floats work well)
            else:
                result += newkey + "=" + quote(str(params[key])) + "&"
    # remove the last '&'
    if (result) and (topkey == '') and (result[-1] == '&'):
        result = result[:-1]
    return result
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

public class CallbackVerifier {
  
    private boolean verify(HttpServletRequest request, String authyApiKey)
            throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        String forwardedProtocol = request.getHeader("X-Forwarded-Proto");
        String protocol = forwardedProtocol != null ? forwardedProtocol :
                request.getScheme();
        String url = String.format("%s://%s%s", protocol,
                request.getServerName(), request.getServletPath());

        // Fetch Json body
        String body = IOUtils.toString(request.getReader());
        Map<String, Object> bodyJson = objectMapper.readValue(body,
                new TypeReference<Map<String, Object>>() {
                });

        // Flatten and sort JSON elements
        String flattenJson = bodyJson.entrySet()
                .stream()
                .flatMap(FlatMap::flatten)
                .map(e -> e.getKey() + "=" + e.getValue())
                .sorted()
                .collect(Collectors.joining("&"));

        // Read the nonce from the request
        String nonce = request.getHeader("X-Authy-Signature-Nonce");

        // Join all the request bits using '|'
        String data = String.format("%s|%s|%s|%s",
                nonce,
                request.getMethod(),
                url,
                flattenJson);

        try {
            // Compute the signature
            Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
            SecretKeySpec secret_key = new SecretKeySpec(authyApiKey.getBytes(),
                    "HmacSHA256");
            sha256_HMAC.init(secret_key);
            String hash = Base64
                    .getEncoder()
                    .encodeToString(sha256_HMAC.doFinal(data.getBytes()));

            // Extract the actual request signature
            String signature = request.getHeader("X-Authy-Signature");

            // Compare the request signature with your computed signature
            return hash.equals(signature);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            e.printStackTrace();
            return false;
        }
    }
}

class FlatMap {

    private static String openBracket;
    private static String closeBracket;

    static {
        try {
            openBracket = encode("[", "UTF-8");
            closeBracket = encode("]", "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private static String prependKeyName(String keyName, String prefix) {
        StringBuffer result = new StringBuffer();
        result.append(prefix);
        int indexOfOpenBracket = keyName.indexOf(openBracket);
        if(indexOfOpenBracket == -1) {
            indexOfOpenBracket = keyName.length();
        }
        result.append(openBracket);
        result.append(keyName.substring(0, indexOfOpenBracket));
        result.append(closeBracket);
        result.append(keyName.substring(indexOfOpenBracket));
        return result.toString();

    }

    public static Stream<Map.Entry<?, ?>> flatten(Map.Entry<?, ?> e) {
        Object value = e.getValue();
        if (value instanceof Map<?, ?>) {
            return ((Map<?, ?>) value).entrySet()
                    .stream()
                    .flatMap(FlatMap::flatten)
                    .map(el -> {
                        String newKeyName = FlatMap.prependKeyName(
                                (String) el.getKey(), (String) e.getKey());
                        return new AbstractMap.SimpleEntry<>(newKeyName, el.getValue());
                    });
        } else if(value == null) {
            return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(), ""));
        } else if(value instanceof String) {
            try {
                String encodedValue = URLEncoder.encode((String) value, "UTF-8");
                return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(),
                        encodedValue.replace(" ", "+")));
            } catch (UnsupportedEncodingException exception) {
                throw new RuntimeException(exception);
            }
        }
        return Stream.of(e);
    }
}
testable: false
Overview of the steps needed to verify an incoming Twilio webhook for Push Notifications.
Verify an Incoming Two-factor Authentication Webhook

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

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.

Need some help?

We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd browsing the Twilio tag on Stack Overflow.

Loading Code Samples...
SDK Version:
  • 5.x
SDK Version:
  • 7.x
SDK Version:
  • 3.x
SDK Version:
  • 5.x
SDK Version:
  • 6.x
SDK Version:
  • 5.x
const qs = require('qs');
const crypto = require('crypto');

/**
 * @param {http} req This is an HTTP request from the Express middleware
 * @param {!string} apiKey  Account Security API key
 * @return {Boolean} True if verified
 */
function verifyCallback(req, apiKey) {
  const url = req.protocol + '://' + req.get('host') + req.originalUrl;
  const method = req.method;
  const params = req.body;	// needs `npm i body-parser` on Express 4

  // Sort the params
  const sortedParams = qs
    .stringify(params, { arrayFormat: 'brackets' })
    .split('&')
    .sort(sortByPropertyOnly)
    .join('&')
    .replace(/%20/g, '+');

  // Read the nonce from the request
  const nonce = req.headers['x-authy-signature-nonce'];

  // concatinate all together and separate by '|'
  const data = nonce + '|' + method + '|' + url + '|' + sortedParams;

  // compute the signature
  const computedSig = crypto
    .createHmac('sha256', apiKey)
    .update(data)
    .digest('base64');

  const sig = req.headers['x-authy-signature'];

  // compare the message signature with your calculated signature
  return sig === computedSig;
}

/**
 * Sort by property only.
 *  Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'
 *  would be moved after 'events=aaaa'.
 *
 *  For this approach, we split tokenize the string around the '=' value and only sort alphabetically
 *  by the property.
 *
 * @param {string} x
 * @param {string} y
 * @return {number}
 */
function sortByPropertyOnly(x, y) {
  const xx = x.split('=');
  const yy = y.split('=');

  if (xx < yy) {
    return -1;
  }
  if (xx > yy) {
    return 1;
  }
  return 0;
}
using System.Security.Cryptography;
using Microsoft.AspNetCore.Http.Extensions;
using Newtonsoft.Json.Linq;
using System.Text.RegularExpressions;

namespace Test
{
    public class AuthyWebhook
    {
        private bool Verify(Microsoft.AspNetCore.Http.HttpRequest Request,
          Newtonsoft.Json.Linq.JObject JsonContent,
          string ApiKey)
        {
            // Read the nonce from the request
            var nonce = Request.Headers["x-authy-signature-nonce"];
            var method = Request.Method;
            var url = Request.GetDisplayUrl();
            List<String> bodyRequest = new List<String>();

            // Flatten
            var props = GetPropPaths(string.Empty, JsonContent);
            foreach (var kvp in props)
            {
                var encodedKey = System.Web.HttpUtility.UrlEncode(kvp.Item1);
                var encodedValue = System.Web.HttpUtility.UrlEncode(kvp.Item2);
                // uppercase all escaped url-encoded characters
                Regex reg = new Regex(@"%[a-f0-9]{2}");
                encodedKey = reg.Replace(encodedKey, m => m.Value.ToUpperInvariant());
                encodedValue = reg.Replace(encodedValue, m => m.Value.ToUpperInvariant());
                bodyRequest.Add(encodedKey + "=" + encodedValue);
            }

            // Sort the params
            bodyRequest.Sort();
            var parameters = String.Join("&", bodyRequest);

            // concatenate all together and separate by '|'
            var data = $"{nonce}|{method}|{url}|{parameters}";

            // compute the signature
            var encoding = new System.Text.ASCIIEncoding();
            byte[] ApiKeyBytes = encoding.GetBytes(ApiKey);
            byte[] DataBytes = encoding.GetBytes(data);
            using (var hmacsha256 = new HMACSHA256(ApiKeyBytes))
            {
                // Read the Authy Signature from the request
                var sig = Request.Headers["x-authy-signature"];

                // compare the message signature with your calculated signature
                byte[] hashmessage = hmacsha256.ComputeHash(DataBytes);
                var hash = Convert.ToBase64String(hashmessage);
                return hash == sig;
            }
        }

        private IEnumerable<Tuple<string, string>> GetPropPaths(string currPath, JObject obj)
        {
            foreach (var prop in obj.Properties())
            {
                var propPath = string.IsNullOrWhiteSpace(currPath) ? prop.Name : currPath + "[" + prop.Name + "]";

                if (prop.Value.Type == JTokenType.Object)
                {
                    foreach (var subProp in GetPropPaths(propPath, prop.Value as JObject))
                        yield return subProp;
                }
                else if (prop.Value.Type == JTokenType.Boolean)
                {
                    // Fix boolean type conversion to lowercase string
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString().ToLower());
                }
                else
                {
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString());
                }
            }
        }
    }
}
<?php
/**
 *
 * Verify Authy signature
 *
 * @param    string  $apiKey Authy app API KEY
 * @return   bool
 *
 */
function verifyWebhook($apiKey) {
    // Read the nonce from the request
    $nonce = $_SERVER['HTTP_X_AUTHY_SIGNATURE_NONCE'];
    $method = $_SERVER['REQUEST_METHOD'];
    $proto = isset($_SERVER['HTTPS']) ? "https" : "http";
    $url = "{$proto}://{$_SERVER[HTTP_HOST]}{$_SERVER[REQUEST_URI]}";

    // get the Json string sent by Authy
    $json = file_get_contents('php://input');
    $decoded = json_decode($json, true);
    $normalized = normalizeArray($decoded);

    //  sort the parameters by keys
    $query = http_build_query($normalized);
    $exploded = explode('&', $query);
    sort($exploded);
    $params = implode('&', $exploded);

    // concatenate all together and separate them by '|'
    $data = "$nonce|$method|$url|$params";

    // compute the signature
    $computedSig = base64_encode(hash_hmac('sha256', $data, $apiKey, true));

    // get the authy signature
    $sig = $_SERVER['HTTP_X_AUTHY_SIGNATURE'];

    // compare the message signature with your calculated signature
    return hash_equals($computedSig, $sig);
}

/**
 *
 * Replace boolean values with strings.
 *   In PHP methods like http_build_query converts values from an array
 *   to 0 and 1 integers instead of 'false' and 'true'.
 *
 * @param    array  $array Multidimensional array with boolean values
 * @return   array
 *
 */
function normalizeArray($array) {
  foreach ($array as &$el) {
      if (is_bool($el)) {
        $el = ($el) ? "true" : "false";
      } elseif (is_array($el)) {
        $el = normalizeArray($el);
      }
  }
  return $array;
}
require 'uri'

class CallbackVerifier

  # @param [request] A Rails request object
  # @param [api_key] The API key used to sign the request
  # @return [boolean] True if verified
  def verify_callback(request, api_key)
    url = url_for(:only_path => false, :overwrite_params=>nil)

    # Sort and join the parameters on Rails
    json_params = JSON.parse(request.body.read)
    parameter_string = json_params.to_query

    # Read the nonce from the request
    nonce = request.headers['x-authy-signature-nonce']

    # Join all request parts using '|'
    data = "#{nonce}|#{request.method}|#{url}|#{parameter_string}"

    # Compute the signature
    digest = OpenSSL::Digest.new('sha256')
    hmac = OpenSSL::HMAC.digest(digest, api_key, data)
    hash = Base64.strict_encode64(hmac)

    # Extract the actual request signature
    signature = request.headers['x-authy-signature']

    # Compare the computed signature with the actual signature
    hash == signature
  end
end
import base64
import hashlib
import hmac
import json
import re

try:
    from urllib.parse import urlencode, quote
except ImportError:
    from urllib import urlencode, quote


def validate_authy_request(request,api_key):
    """Validates that incoming requests genuinely originated from Twilio"""
    url = request.url
    method = request.method

    # Read the Flask request parameters
    params = json.loads(request.data)

    # Convert the JSON dict to query params
    query_params = __make_http_query(params)
    sorted_params = '&'.join(sorted(query_params.replace('/', '%2F').replace('%20', '+').split('&')))
    sorted_params = re.sub("\\%5B([0-9])*\\%5D","%5B%5D",sorted_params)
    sorted_params = re.sub("\\=None", "=", sorted_params)

    # Read the nonce from the request
    nonce = request.headers['X-Authy-Signature-Nonce']

    # Concatenate all together and separate by '|'
    data = '|'.join([nonce, method, url, sorted_params])

    # Compute the signature
    computed_dig = hmac.new(
        api_key.encode(),
        msg=data.encode(),
        digestmod=hashlib.sha256
    ).digest()
    computed_sig = base64.b64encode(computed_dig)

    sig = request.headers['X-Authy-Signature']

    # Compare the message signature with your calculated signature
    # Continue processing the request if it's valid, return a 403 error if
    # it's not
    return sig == computed_sig


def __make_http_query(params, topkey=''):
    """
    Function to covert params into url encoded query string
    :param dict params: Json string sent  by Authy.
    :param string topkey: params key
    :return string: url encoded Query.
    """
    if len(params) == 0:
        return ""
    result = ""
    # is a dictionary?
    if type(params) is dict:
        for key in params.keys():
            newkey = quote(key)
            if topkey != '':
                newkey = topkey + quote('[' + key + ']')
            if type(params[key]) is dict:
                result += __make_http_query(params[key], newkey)
            elif type(params[key]) is list:
                i = 0
                for val in params[key]:
                    if type(val) is dict:
                        result +=   __make_http_query(val, newkey + quote('['+str(i)+']'))
                    else:
                        result += newkey + quote('['+str(i)+']') + "=" + quote(str(val)) + "&"
                    i = i + 1
            # boolean should have special treatment as well
            elif type(params[key]) is bool:
                result += newkey + "=" + quote(str(params[key]).lower()) + "&"
            # assume string (integers and floats work well)
            else:
                result += newkey + "=" + quote(str(params[key])) + "&"
    # remove the last '&'
    if (result) and (topkey == '') and (result[-1] == '&'):
        result = result[:-1]
    return result
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

public class CallbackVerifier {
  
    private boolean verify(HttpServletRequest request, String authyApiKey)
            throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        String forwardedProtocol = request.getHeader("X-Forwarded-Proto");
        String protocol = forwardedProtocol != null ? forwardedProtocol :
                request.getScheme();
        String url = String.format("%s://%s%s", protocol,
                request.getServerName(), request.getServletPath());

        // Fetch Json body
        String body = IOUtils.toString(request.getReader());
        Map<String, Object> bodyJson = objectMapper.readValue(body,
                new TypeReference<Map<String, Object>>() {
                });

        // Flatten and sort JSON elements
        String flattenJson = bodyJson.entrySet()
                .stream()
                .flatMap(FlatMap::flatten)
                .map(e -> e.getKey() + "=" + e.getValue())
                .sorted()
                .collect(Collectors.joining("&"));

        // Read the nonce from the request
        String nonce = request.getHeader("X-Authy-Signature-Nonce");

        // Join all the request bits using '|'
        String data = String.format("%s|%s|%s|%s",
                nonce,
                request.getMethod(),
                url,
                flattenJson);

        try {
            // Compute the signature
            Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
            SecretKeySpec secret_key = new SecretKeySpec(authyApiKey.getBytes(),
                    "HmacSHA256");
            sha256_HMAC.init(secret_key);
            String hash = Base64
                    .getEncoder()
                    .encodeToString(sha256_HMAC.doFinal(data.getBytes()));

            // Extract the actual request signature
            String signature = request.getHeader("X-Authy-Signature");

            // Compare the request signature with your computed signature
            return hash.equals(signature);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            e.printStackTrace();
            return false;
        }
    }
}

class FlatMap {

    private static String openBracket;
    private static String closeBracket;

    static {
        try {
            openBracket = encode("[", "UTF-8");
            closeBracket = encode("]", "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private static String prependKeyName(String keyName, String prefix) {
        StringBuffer result = new StringBuffer();
        result.append(prefix);
        int indexOfOpenBracket = keyName.indexOf(openBracket);
        if(indexOfOpenBracket == -1) {
            indexOfOpenBracket = keyName.length();
        }
        result.append(openBracket);
        result.append(keyName.substring(0, indexOfOpenBracket));
        result.append(closeBracket);
        result.append(keyName.substring(indexOfOpenBracket));
        return result.toString();

    }

    public static Stream<Map.Entry<?, ?>> flatten(Map.Entry<?, ?> e) {
        Object value = e.getValue();
        if (value instanceof Map<?, ?>) {
            return ((Map<?, ?>) value).entrySet()
                    .stream()
                    .flatMap(FlatMap::flatten)
                    .map(el -> {
                        String newKeyName = FlatMap.prependKeyName(
                                (String) el.getKey(), (String) e.getKey());
                        return new AbstractMap.SimpleEntry<>(newKeyName, el.getValue());
                    });
        } else if(value == null) {
            return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(), ""));
        } else if(value instanceof String) {
            try {
                String encodedValue = URLEncoder.encode((String) value, "UTF-8");
                return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(),
                        encodedValue.replace(" ", "+")));
            } catch (UnsupportedEncodingException exception) {
                throw new RuntimeException(exception);
            }
        }
        return Stream.of(e);
    }
}
testable: false
SDK Version:
  • 5.x
SDK Version:
  • 7.x
SDK Version:
  • 3.x
SDK Version:
  • 5.x
SDK Version:
  • 6.x
SDK Version:
  • 5.x
const qs = require('qs');
const crypto = require('crypto');

/**
 * @param {http} req This is an HTTP request from the Express middleware
 * @param {!string} apiKey  Account Security API key
 * @return {Boolean} True if verified
 */
function verifyCallback(req, apiKey) {
  const url = req.protocol + '://' + req.get('host') + req.originalUrl;
  const method = req.method;
  const params = req.body;	// needs `npm i body-parser` on Express 4

  // Sort the params
  const sortedParams = qs
    .stringify(params, { arrayFormat: 'brackets' })
    .split('&')
    .sort(sortByPropertyOnly)
    .join('&')
    .replace(/%20/g, '+');

  // Read the nonce from the request
  const nonce = req.headers['x-authy-signature-nonce'];

  // concatinate all together and separate by '|'
  const data = nonce + '|' + method + '|' + url + '|' + sortedParams;

  // compute the signature
  const computedSig = crypto
    .createHmac('sha256', apiKey)
    .update(data)
    .digest('base64');

  const sig = req.headers['x-authy-signature'];

  // compare the message signature with your calculated signature
  return sig === computedSig;
}

/**
 * Sort by property only.
 *  Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'
 *  would be moved after 'events=aaaa'.
 *
 *  For this approach, we split tokenize the string around the '=' value and only sort alphabetically
 *  by the property.
 *
 * @param {string} x
 * @param {string} y
 * @return {number}
 */
function sortByPropertyOnly(x, y) {
  const xx = x.split('=');
  const yy = y.split('=');

  if (xx < yy) {
    return -1;
  }
  if (xx > yy) {
    return 1;
  }
  return 0;
}
using System.Security.Cryptography;
using Microsoft.AspNetCore.Http.Extensions;
using Newtonsoft.Json.Linq;
using System.Text.RegularExpressions;

namespace Test
{
    public class AuthyWebhook
    {
        private bool Verify(Microsoft.AspNetCore.Http.HttpRequest Request,
          Newtonsoft.Json.Linq.JObject JsonContent,
          string ApiKey)
        {
            // Read the nonce from the request
            var nonce = Request.Headers["x-authy-signature-nonce"];
            var method = Request.Method;
            var url = Request.GetDisplayUrl();
            List<String> bodyRequest = new List<String>();

            // Flatten
            var props = GetPropPaths(string.Empty, JsonContent);
            foreach (var kvp in props)
            {
                var encodedKey = System.Web.HttpUtility.UrlEncode(kvp.Item1);
                var encodedValue = System.Web.HttpUtility.UrlEncode(kvp.Item2);
                // uppercase all escaped url-encoded characters
                Regex reg = new Regex(@"%[a-f0-9]{2}");
                encodedKey = reg.Replace(encodedKey, m => m.Value.ToUpperInvariant());
                encodedValue = reg.Replace(encodedValue, m => m.Value.ToUpperInvariant());
                bodyRequest.Add(encodedKey + "=" + encodedValue);
            }

            // Sort the params
            bodyRequest.Sort();
            var parameters = String.Join("&", bodyRequest);

            // concatenate all together and separate by '|'
            var data = $"{nonce}|{method}|{url}|{parameters}";

            // compute the signature
            var encoding = new System.Text.ASCIIEncoding();
            byte[] ApiKeyBytes = encoding.GetBytes(ApiKey);
            byte[] DataBytes = encoding.GetBytes(data);
            using (var hmacsha256 = new HMACSHA256(ApiKeyBytes))
            {
                // Read the Authy Signature from the request
                var sig = Request.Headers["x-authy-signature"];

                // compare the message signature with your calculated signature
                byte[] hashmessage = hmacsha256.ComputeHash(DataBytes);
                var hash = Convert.ToBase64String(hashmessage);
                return hash == sig;
            }
        }

        private IEnumerable<Tuple<string, string>> GetPropPaths(string currPath, JObject obj)
        {
            foreach (var prop in obj.Properties())
            {
                var propPath = string.IsNullOrWhiteSpace(currPath) ? prop.Name : currPath + "[" + prop.Name + "]";

                if (prop.Value.Type == JTokenType.Object)
                {
                    foreach (var subProp in GetPropPaths(propPath, prop.Value as JObject))
                        yield return subProp;
                }
                else if (prop.Value.Type == JTokenType.Boolean)
                {
                    // Fix boolean type conversion to lowercase string
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString().ToLower());
                }
                else
                {
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString());
                }
            }
        }
    }
}
<?php
/**
 *
 * Verify Authy signature
 *
 * @param    string  $apiKey Authy app API KEY
 * @return   bool
 *
 */
function verifyWebhook($apiKey) {
    // Read the nonce from the request
    $nonce = $_SERVER['HTTP_X_AUTHY_SIGNATURE_NONCE'];
    $method = $_SERVER['REQUEST_METHOD'];
    $proto = isset($_SERVER['HTTPS']) ? "https" : "http";
    $url = "{$proto}://{$_SERVER[HTTP_HOST]}{$_SERVER[REQUEST_URI]}";

    // get the Json string sent by Authy
    $json = file_get_contents('php://input');
    $decoded = json_decode($json, true);
    $normalized = normalizeArray($decoded);

    //  sort the parameters by keys
    $query = http_build_query($normalized);
    $exploded = explode('&', $query);
    sort($exploded);
    $params = implode('&', $exploded);

    // concatenate all together and separate them by '|'
    $data = "$nonce|$method|$url|$params";

    // compute the signature
    $computedSig = base64_encode(hash_hmac('sha256', $data, $apiKey, true));

    // get the authy signature
    $sig = $_SERVER['HTTP_X_AUTHY_SIGNATURE'];

    // compare the message signature with your calculated signature
    return hash_equals($computedSig, $sig);
}

/**
 *
 * Replace boolean values with strings.
 *   In PHP methods like http_build_query converts values from an array
 *   to 0 and 1 integers instead of 'false' and 'true'.
 *
 * @param    array  $array Multidimensional array with boolean values
 * @return   array
 *
 */
function normalizeArray($array) {
  foreach ($array as &$el) {
      if (is_bool($el)) {
        $el = ($el) ? "true" : "false";
      } elseif (is_array($el)) {
        $el = normalizeArray($el);
      }
  }
  return $array;
}
require 'uri'

class CallbackVerifier

  # @param [request] A Rails request object
  # @param [api_key] The API key used to sign the request
  # @return [boolean] True if verified
  def verify_callback(request, api_key)
    url = url_for(:only_path => false, :overwrite_params=>nil)

    # Sort and join the parameters on Rails
    json_params = JSON.parse(request.body.read)
    parameter_string = json_params.to_query

    # Read the nonce from the request
    nonce = request.headers['x-authy-signature-nonce']

    # Join all request parts using '|'
    data = "#{nonce}|#{request.method}|#{url}|#{parameter_string}"

    # Compute the signature
    digest = OpenSSL::Digest.new('sha256')
    hmac = OpenSSL::HMAC.digest(digest, api_key, data)
    hash = Base64.strict_encode64(hmac)

    # Extract the actual request signature
    signature = request.headers['x-authy-signature']

    # Compare the computed signature with the actual signature
    hash == signature
  end
end
import base64
import hashlib
import hmac
import json
import re

try:
    from urllib.parse import urlencode, quote
except ImportError:
    from urllib import urlencode, quote


def validate_authy_request(request,api_key):
    """Validates that incoming requests genuinely originated from Twilio"""
    url = request.url
    method = request.method

    # Read the Flask request parameters
    params = json.loads(request.data)

    # Convert the JSON dict to query params
    query_params = __make_http_query(params)
    sorted_params = '&'.join(sorted(query_params.replace('/', '%2F').replace('%20', '+').split('&')))
    sorted_params = re.sub("\\%5B([0-9])*\\%5D","%5B%5D",sorted_params)
    sorted_params = re.sub("\\=None", "=", sorted_params)

    # Read the nonce from the request
    nonce = request.headers['X-Authy-Signature-Nonce']

    # Concatenate all together and separate by '|'
    data = '|'.join([nonce, method, url, sorted_params])

    # Compute the signature
    computed_dig = hmac.new(
        api_key.encode(),
        msg=data.encode(),
        digestmod=hashlib.sha256
    ).digest()
    computed_sig = base64.b64encode(computed_dig)

    sig = request.headers['X-Authy-Signature']

    # Compare the message signature with your calculated signature
    # Continue processing the request if it's valid, return a 403 error if
    # it's not
    return sig == computed_sig


def __make_http_query(params, topkey=''):
    """
    Function to covert params into url encoded query string
    :param dict params: Json string sent  by Authy.
    :param string topkey: params key
    :return string: url encoded Query.
    """
    if len(params) == 0:
        return ""
    result = ""
    # is a dictionary?
    if type(params) is dict:
        for key in params.keys():
            newkey = quote(key)
            if topkey != '':
                newkey = topkey + quote('[' + key + ']')
            if type(params[key]) is dict:
                result += __make_http_query(params[key], newkey)
            elif type(params[key]) is list:
                i = 0
                for val in params[key]:
                    if type(val) is dict:
                        result +=   __make_http_query(val, newkey + quote('['+str(i)+']'))
                    else:
                        result += newkey + quote('['+str(i)+']') + "=" + quote(str(val)) + "&"
                    i = i + 1
            # boolean should have special treatment as well
            elif type(params[key]) is bool:
                result += newkey + "=" + quote(str(params[key]).lower()) + "&"
            # assume string (integers and floats work well)
            else:
                result += newkey + "=" + quote(str(params[key])) + "&"
    # remove the last '&'
    if (result) and (topkey == '') and (result[-1] == '&'):
        result = result[:-1]
    return result
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

public class CallbackVerifier {
  
    private boolean verify(HttpServletRequest request, String authyApiKey)
            throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        String forwardedProtocol = request.getHeader("X-Forwarded-Proto");
        String protocol = forwardedProtocol != null ? forwardedProtocol :
                request.getScheme();
        String url = String.format("%s://%s%s", protocol,
                request.getServerName(), request.getServletPath());

        // Fetch Json body
        String body = IOUtils.toString(request.getReader());
        Map<String, Object> bodyJson = objectMapper.readValue(body,
                new TypeReference<Map<String, Object>>() {
                });

        // Flatten and sort JSON elements
        String flattenJson = bodyJson.entrySet()
                .stream()
                .flatMap(FlatMap::flatten)
                .map(e -> e.getKey() + "=" + e.getValue())
                .sorted()
                .collect(Collectors.joining("&"));

        // Read the nonce from the request
        String nonce = request.getHeader("X-Authy-Signature-Nonce");

        // Join all the request bits using '|'
        String data = String.format("%s|%s|%s|%s",
                nonce,
                request.getMethod(),
                url,
                flattenJson);

        try {
            // Compute the signature
            Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
            SecretKeySpec secret_key = new SecretKeySpec(authyApiKey.getBytes(),
                    "HmacSHA256");
            sha256_HMAC.init(secret_key);
            String hash = Base64
                    .getEncoder()
                    .encodeToString(sha256_HMAC.doFinal(data.getBytes()));

            // Extract the actual request signature
            String signature = request.getHeader("X-Authy-Signature");

            // Compare the request signature with your computed signature
            return hash.equals(signature);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            e.printStackTrace();
            return false;
        }
    }
}

class FlatMap {

    private static String openBracket;
    private static String closeBracket;

    static {
        try {
            openBracket = encode("[", "UTF-8");
            closeBracket = encode("]", "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private static String prependKeyName(String keyName, String prefix) {
        StringBuffer result = new StringBuffer();
        result.append(prefix);
        int indexOfOpenBracket = keyName.indexOf(openBracket);
        if(indexOfOpenBracket == -1) {
            indexOfOpenBracket = keyName.length();
        }
        result.append(openBracket);
        result.append(keyName.substring(0, indexOfOpenBracket));
        result.append(closeBracket);
        result.append(keyName.substring(indexOfOpenBracket));
        return result.toString();

    }

    public static Stream<Map.Entry<?, ?>> flatten(Map.Entry<?, ?> e) {
        Object value = e.getValue();
        if (value instanceof Map<?, ?>) {
            return ((Map<?, ?>) value).entrySet()
                    .stream()
                    .flatMap(FlatMap::flatten)
                    .map(el -> {
                        String newKeyName = FlatMap.prependKeyName(
                                (String) el.getKey(), (String) e.getKey());
                        return new AbstractMap.SimpleEntry<>(newKeyName, el.getValue());
                    });
        } else if(value == null) {
            return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(), ""));
        } else if(value instanceof String) {
            try {
                String encodedValue = URLEncoder.encode((String) value, "UTF-8");
                return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(),
                        encodedValue.replace(" ", "+")));
            } catch (UnsupportedEncodingException exception) {
                throw new RuntimeException(exception);
            }
        }
        return Stream.of(e);
    }
}
testable: false
SDK Version:
  • 5.x
SDK Version:
  • 7.x
SDK Version:
  • 3.x
SDK Version:
  • 5.x
SDK Version:
  • 6.x
SDK Version:
  • 5.x
const qs = require('qs');
const crypto = require('crypto');

/**
 * @param {http} req This is an HTTP request from the Express middleware
 * @param {!string} apiKey  Account Security API key
 * @return {Boolean} True if verified
 */
function verifyCallback(req, apiKey) {
  const url = req.protocol + '://' + req.get('host') + req.originalUrl;
  const method = req.method;
  const params = req.body;	// needs `npm i body-parser` on Express 4

  // Sort the params
  const sortedParams = qs
    .stringify(params, { arrayFormat: 'brackets' })
    .split('&')
    .sort(sortByPropertyOnly)
    .join('&')
    .replace(/%20/g, '+');

  // Read the nonce from the request
  const nonce = req.headers['x-authy-signature-nonce'];

  // concatinate all together and separate by '|'
  const data = nonce + '|' + method + '|' + url + '|' + sortedParams;

  // compute the signature
  const computedSig = crypto
    .createHmac('sha256', apiKey)
    .update(data)
    .digest('base64');

  const sig = req.headers['x-authy-signature'];

  // compare the message signature with your calculated signature
  return sig === computedSig;
}

/**
 * Sort by property only.
 *  Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'
 *  would be moved after 'events=aaaa'.
 *
 *  For this approach, we split tokenize the string around the '=' value and only sort alphabetically
 *  by the property.
 *
 * @param {string} x
 * @param {string} y
 * @return {number}
 */
function sortByPropertyOnly(x, y) {
  const xx = x.split('=');
  const yy = y.split('=');

  if (xx < yy) {
    return -1;
  }
  if (xx > yy) {
    return 1;
  }
  return 0;
}
using System.Security.Cryptography;
using Microsoft.AspNetCore.Http.Extensions;
using Newtonsoft.Json.Linq;
using System.Text.RegularExpressions;

namespace Test
{
    public class AuthyWebhook
    {
        private bool Verify(Microsoft.AspNetCore.Http.HttpRequest Request,
          Newtonsoft.Json.Linq.JObject JsonContent,
          string ApiKey)
        {
            // Read the nonce from the request
            var nonce = Request.Headers["x-authy-signature-nonce"];
            var method = Request.Method;
            var url = Request.GetDisplayUrl();
            List<String> bodyRequest = new List<String>();

            // Flatten
            var props = GetPropPaths(string.Empty, JsonContent);
            foreach (var kvp in props)
            {
                var encodedKey = System.Web.HttpUtility.UrlEncode(kvp.Item1);
                var encodedValue = System.Web.HttpUtility.UrlEncode(kvp.Item2);
                // uppercase all escaped url-encoded characters
                Regex reg = new Regex(@"%[a-f0-9]{2}");
                encodedKey = reg.Replace(encodedKey, m => m.Value.ToUpperInvariant());
                encodedValue = reg.Replace(encodedValue, m => m.Value.ToUpperInvariant());
                bodyRequest.Add(encodedKey + "=" + encodedValue);
            }

            // Sort the params
            bodyRequest.Sort();
            var parameters = String.Join("&", bodyRequest);

            // concatenate all together and separate by '|'
            var data = $"{nonce}|{method}|{url}|{parameters}";

            // compute the signature
            var encoding = new System.Text.ASCIIEncoding();
            byte[] ApiKeyBytes = encoding.GetBytes(ApiKey);
            byte[] DataBytes = encoding.GetBytes(data);
            using (var hmacsha256 = new HMACSHA256(ApiKeyBytes))
            {
                // Read the Authy Signature from the request
                var sig = Request.Headers["x-authy-signature"];

                // compare the message signature with your calculated signature
                byte[] hashmessage = hmacsha256.ComputeHash(DataBytes);
                var hash = Convert.ToBase64String(hashmessage);
                return hash == sig;
            }
        }

        private IEnumerable<Tuple<string, string>> GetPropPaths(string currPath, JObject obj)
        {
            foreach (var prop in obj.Properties())
            {
                var propPath = string.IsNullOrWhiteSpace(currPath) ? prop.Name : currPath + "[" + prop.Name + "]";

                if (prop.Value.Type == JTokenType.Object)
                {
                    foreach (var subProp in GetPropPaths(propPath, prop.Value as JObject))
                        yield return subProp;
                }
                else if (prop.Value.Type == JTokenType.Boolean)
                {
                    // Fix boolean type conversion to lowercase string
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString().ToLower());
                }
                else
                {
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString());
                }
            }
        }
    }
}
<?php
/**
 *
 * Verify Authy signature
 *
 * @param    string  $apiKey Authy app API KEY
 * @return   bool
 *
 */
function verifyWebhook($apiKey) {
    // Read the nonce from the request
    $nonce = $_SERVER['HTTP_X_AUTHY_SIGNATURE_NONCE'];
    $method = $_SERVER['REQUEST_METHOD'];
    $proto = isset($_SERVER['HTTPS']) ? "https" : "http";
    $url = "{$proto}://{$_SERVER[HTTP_HOST]}{$_SERVER[REQUEST_URI]}";

    // get the Json string sent by Authy
    $json = file_get_contents('php://input');
    $decoded = json_decode($json, true);
    $normalized = normalizeArray($decoded);

    //  sort the parameters by keys
    $query = http_build_query($normalized);
    $exploded = explode('&', $query);
    sort($exploded);
    $params = implode('&', $exploded);

    // concatenate all together and separate them by '|'
    $data = "$nonce|$method|$url|$params";

    // compute the signature
    $computedSig = base64_encode(hash_hmac('sha256', $data, $apiKey, true));

    // get the authy signature
    $sig = $_SERVER['HTTP_X_AUTHY_SIGNATURE'];

    // compare the message signature with your calculated signature
    return hash_equals($computedSig, $sig);
}

/**
 *
 * Replace boolean values with strings.
 *   In PHP methods like http_build_query converts values from an array
 *   to 0 and 1 integers instead of 'false' and 'true'.
 *
 * @param    array  $array Multidimensional array with boolean values
 * @return   array
 *
 */
function normalizeArray($array) {
  foreach ($array as &$el) {
      if (is_bool($el)) {
        $el = ($el) ? "true" : "false";
      } elseif (is_array($el)) {
        $el = normalizeArray($el);
      }
  }
  return $array;
}
require 'uri'

class CallbackVerifier

  # @param [request] A Rails request object
  # @param [api_key] The API key used to sign the request
  # @return [boolean] True if verified
  def verify_callback(request, api_key)
    url = url_for(:only_path => false, :overwrite_params=>nil)

    # Sort and join the parameters on Rails
    json_params = JSON.parse(request.body.read)
    parameter_string = json_params.to_query

    # Read the nonce from the request
    nonce = request.headers['x-authy-signature-nonce']

    # Join all request parts using '|'
    data = "#{nonce}|#{request.method}|#{url}|#{parameter_string}"

    # Compute the signature
    digest = OpenSSL::Digest.new('sha256')
    hmac = OpenSSL::HMAC.digest(digest, api_key, data)
    hash = Base64.strict_encode64(hmac)

    # Extract the actual request signature
    signature = request.headers['x-authy-signature']

    # Compare the computed signature with the actual signature
    hash == signature
  end
end
import base64
import hashlib
import hmac
import json
import re

try:
    from urllib.parse import urlencode, quote
except ImportError:
    from urllib import urlencode, quote


def validate_authy_request(request,api_key):
    """Validates that incoming requests genuinely originated from Twilio"""
    url = request.url
    method = request.method

    # Read the Flask request parameters
    params = json.loads(request.data)

    # Convert the JSON dict to query params
    query_params = __make_http_query(params)
    sorted_params = '&'.join(sorted(query_params.replace('/', '%2F').replace('%20', '+').split('&')))
    sorted_params = re.sub("\\%5B([0-9])*\\%5D","%5B%5D",sorted_params)
    sorted_params = re.sub("\\=None", "=", sorted_params)

    # Read the nonce from the request
    nonce = request.headers['X-Authy-Signature-Nonce']

    # Concatenate all together and separate by '|'
    data = '|'.join([nonce, method, url, sorted_params])

    # Compute the signature
    computed_dig = hmac.new(
        api_key.encode(),
        msg=data.encode(),
        digestmod=hashlib.sha256
    ).digest()
    computed_sig = base64.b64encode(computed_dig)

    sig = request.headers['X-Authy-Signature']

    # Compare the message signature with your calculated signature
    # Continue processing the request if it's valid, return a 403 error if
    # it's not
    return sig == computed_sig


def __make_http_query(params, topkey=''):
    """
    Function to covert params into url encoded query string
    :param dict params: Json string sent  by Authy.
    :param string topkey: params key
    :return string: url encoded Query.
    """
    if len(params) == 0:
        return ""
    result = ""
    # is a dictionary?
    if type(params) is dict:
        for key in params.keys():
            newkey = quote(key)
            if topkey != '':
                newkey = topkey + quote('[' + key + ']')
            if type(params[key]) is dict:
                result += __make_http_query(params[key], newkey)
            elif type(params[key]) is list:
                i = 0
                for val in params[key]:
                    if type(val) is dict:
                        result +=   __make_http_query(val, newkey + quote('['+str(i)+']'))
                    else:
                        result += newkey + quote('['+str(i)+']') + "=" + quote(str(val)) + "&"
                    i = i + 1
            # boolean should have special treatment as well
            elif type(params[key]) is bool:
                result += newkey + "=" + quote(str(params[key]).lower()) + "&"
            # assume string (integers and floats work well)
            else:
                result += newkey + "=" + quote(str(params[key])) + "&"
    # remove the last '&'
    if (result) and (topkey == '') and (result[-1] == '&'):
        result = result[:-1]
    return result
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

public class CallbackVerifier {
  
    private boolean verify(HttpServletRequest request, String authyApiKey)
            throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        String forwardedProtocol = request.getHeader("X-Forwarded-Proto");
        String protocol = forwardedProtocol != null ? forwardedProtocol :
                request.getScheme();
        String url = String.format("%s://%s%s", protocol,
                request.getServerName(), request.getServletPath());

        // Fetch Json body
        String body = IOUtils.toString(request.getReader());
        Map<String, Object> bodyJson = objectMapper.readValue(body,
                new TypeReference<Map<String, Object>>() {
                });

        // Flatten and sort JSON elements
        String flattenJson = bodyJson.entrySet()
                .stream()
                .flatMap(FlatMap::flatten)
                .map(e -> e.getKey() + "=" + e.getValue())
                .sorted()
                .collect(Collectors.joining("&"));

        // Read the nonce from the request
        String nonce = request.getHeader("X-Authy-Signature-Nonce");

        // Join all the request bits using '|'
        String data = String.format("%s|%s|%s|%s",
                nonce,
                request.getMethod(),
                url,
                flattenJson);

        try {
            // Compute the signature
            Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
            SecretKeySpec secret_key = new SecretKeySpec(authyApiKey.getBytes(),
                    "HmacSHA256");
            sha256_HMAC.init(secret_key);
            String hash = Base64
                    .getEncoder()
                    .encodeToString(sha256_HMAC.doFinal(data.getBytes()));

            // Extract the actual request signature
            String signature = request.getHeader("X-Authy-Signature");

            // Compare the request signature with your computed signature
            return hash.equals(signature);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            e.printStackTrace();
            return false;
        }
    }
}

class FlatMap {

    private static String openBracket;
    private static String closeBracket;

    static {
        try {
            openBracket = encode("[", "UTF-8");
            closeBracket = encode("]", "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private static String prependKeyName(String keyName, String prefix) {
        StringBuffer result = new StringBuffer();
        result.append(prefix);
        int indexOfOpenBracket = keyName.indexOf(openBracket);
        if(indexOfOpenBracket == -1) {
            indexOfOpenBracket = keyName.length();
        }
        result.append(openBracket);
        result.append(keyName.substring(0, indexOfOpenBracket));
        result.append(closeBracket);
        result.append(keyName.substring(indexOfOpenBracket));
        return result.toString();

    }

    public static Stream<Map.Entry<?, ?>> flatten(Map.Entry<?, ?> e) {
        Object value = e.getValue();
        if (value instanceof Map<?, ?>) {
            return ((Map<?, ?>) value).entrySet()
                    .stream()
                    .flatMap(FlatMap::flatten)
                    .map(el -> {
                        String newKeyName = FlatMap.prependKeyName(
                                (String) el.getKey(), (String) e.getKey());
                        return new AbstractMap.SimpleEntry<>(newKeyName, el.getValue());
                    });
        } else if(value == null) {
            return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(), ""));
        } else if(value instanceof String) {
            try {
                String encodedValue = URLEncoder.encode((String) value, "UTF-8");
                return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(),
                        encodedValue.replace(" ", "+")));
            } catch (UnsupportedEncodingException exception) {
                throw new RuntimeException(exception);
            }
        }
        return Stream.of(e);
    }
}
testable: false
SDK Version:
  • 5.x
SDK Version:
  • 7.x
SDK Version:
  • 3.x
SDK Version:
  • 5.x
SDK Version:
  • 6.x
SDK Version:
  • 5.x
const qs = require('qs');
const crypto = require('crypto');

/**
 * @param {http} req This is an HTTP request from the Express middleware
 * @param {!string} apiKey  Account Security API key
 * @return {Boolean} True if verified
 */
function verifyCallback(req, apiKey) {
  const url = req.protocol + '://' + req.get('host') + req.originalUrl;
  const method = req.method;
  const params = req.body;	// needs `npm i body-parser` on Express 4

  // Sort the params
  const sortedParams = qs
    .stringify(params, { arrayFormat: 'brackets' })
    .split('&')
    .sort(sortByPropertyOnly)
    .join('&')
    .replace(/%20/g, '+');

  // Read the nonce from the request
  const nonce = req.headers['x-authy-signature-nonce'];

  // concatinate all together and separate by '|'
  const data = nonce + '|' + method + '|' + url + '|' + sortedParams;

  // compute the signature
  const computedSig = crypto
    .createHmac('sha256', apiKey)
    .update(data)
    .digest('base64');

  const sig = req.headers['x-authy-signature'];

  // compare the message signature with your calculated signature
  return sig === computedSig;
}

/**
 * Sort by property only.
 *  Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'
 *  would be moved after 'events=aaaa'.
 *
 *  For this approach, we split tokenize the string around the '=' value and only sort alphabetically
 *  by the property.
 *
 * @param {string} x
 * @param {string} y
 * @return {number}
 */
function sortByPropertyOnly(x, y) {
  const xx = x.split('=');
  const yy = y.split('=');

  if (xx < yy) {
    return -1;
  }
  if (xx > yy) {
    return 1;
  }
  return 0;
}
using System.Security.Cryptography;
using Microsoft.AspNetCore.Http.Extensions;
using Newtonsoft.Json.Linq;
using System.Text.RegularExpressions;

namespace Test
{
    public class AuthyWebhook
    {
        private bool Verify(Microsoft.AspNetCore.Http.HttpRequest Request,
          Newtonsoft.Json.Linq.JObject JsonContent,
          string ApiKey)
        {
            // Read the nonce from the request
            var nonce = Request.Headers["x-authy-signature-nonce"];
            var method = Request.Method;
            var url = Request.GetDisplayUrl();
            List<String> bodyRequest = new List<String>();

            // Flatten
            var props = GetPropPaths(string.Empty, JsonContent);
            foreach (var kvp in props)
            {
                var encodedKey = System.Web.HttpUtility.UrlEncode(kvp.Item1);
                var encodedValue = System.Web.HttpUtility.UrlEncode(kvp.Item2);
                // uppercase all escaped url-encoded characters
                Regex reg = new Regex(@"%[a-f0-9]{2}");
                encodedKey = reg.Replace(encodedKey, m => m.Value.ToUpperInvariant());
                encodedValue = reg.Replace(encodedValue, m => m.Value.ToUpperInvariant());
                bodyRequest.Add(encodedKey + "=" + encodedValue);
            }

            // Sort the params
            bodyRequest.Sort();
            var parameters = String.Join("&", bodyRequest);

            // concatenate all together and separate by '|'
            var data = $"{nonce}|{method}|{url}|{parameters}";

            // compute the signature
            var encoding = new System.Text.ASCIIEncoding();
            byte[] ApiKeyBytes = encoding.GetBytes(ApiKey);
            byte[] DataBytes = encoding.GetBytes(data);
            using (var hmacsha256 = new HMACSHA256(ApiKeyBytes))
            {
                // Read the Authy Signature from the request
                var sig = Request.Headers["x-authy-signature"];

                // compare the message signature with your calculated signature
                byte[] hashmessage = hmacsha256.ComputeHash(DataBytes);
                var hash = Convert.ToBase64String(hashmessage);
                return hash == sig;
            }
        }

        private IEnumerable<Tuple<string, string>> GetPropPaths(string currPath, JObject obj)
        {
            foreach (var prop in obj.Properties())
            {
                var propPath = string.IsNullOrWhiteSpace(currPath) ? prop.Name : currPath + "[" + prop.Name + "]";

                if (prop.Value.Type == JTokenType.Object)
                {
                    foreach (var subProp in GetPropPaths(propPath, prop.Value as JObject))
                        yield return subProp;
                }
                else if (prop.Value.Type == JTokenType.Boolean)
                {
                    // Fix boolean type conversion to lowercase string
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString().ToLower());
                }
                else
                {
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString());
                }
            }
        }
    }
}
<?php
/**
 *
 * Verify Authy signature
 *
 * @param    string  $apiKey Authy app API KEY
 * @return   bool
 *
 */
function verifyWebhook($apiKey) {
    // Read the nonce from the request
    $nonce = $_SERVER['HTTP_X_AUTHY_SIGNATURE_NONCE'];
    $method = $_SERVER['REQUEST_METHOD'];
    $proto = isset($_SERVER['HTTPS']) ? "https" : "http";
    $url = "{$proto}://{$_SERVER[HTTP_HOST]}{$_SERVER[REQUEST_URI]}";

    // get the Json string sent by Authy
    $json = file_get_contents('php://input');
    $decoded = json_decode($json, true);
    $normalized = normalizeArray($decoded);

    //  sort the parameters by keys
    $query = http_build_query($normalized);
    $exploded = explode('&', $query);
    sort($exploded);
    $params = implode('&', $exploded);

    // concatenate all together and separate them by '|'
    $data = "$nonce|$method|$url|$params";

    // compute the signature
    $computedSig = base64_encode(hash_hmac('sha256', $data, $apiKey, true));

    // get the authy signature
    $sig = $_SERVER['HTTP_X_AUTHY_SIGNATURE'];

    // compare the message signature with your calculated signature
    return hash_equals($computedSig, $sig);
}

/**
 *
 * Replace boolean values with strings.
 *   In PHP methods like http_build_query converts values from an array
 *   to 0 and 1 integers instead of 'false' and 'true'.
 *
 * @param    array  $array Multidimensional array with boolean values
 * @return   array
 *
 */
function normalizeArray($array) {
  foreach ($array as &$el) {
      if (is_bool($el)) {
        $el = ($el) ? "true" : "false";
      } elseif (is_array($el)) {
        $el = normalizeArray($el);
      }
  }
  return $array;
}
require 'uri'

class CallbackVerifier

  # @param [request] A Rails request object
  # @param [api_key] The API key used to sign the request
  # @return [boolean] True if verified
  def verify_callback(request, api_key)
    url = url_for(:only_path => false, :overwrite_params=>nil)

    # Sort and join the parameters on Rails
    json_params = JSON.parse(request.body.read)
    parameter_string = json_params.to_query

    # Read the nonce from the request
    nonce = request.headers['x-authy-signature-nonce']

    # Join all request parts using '|'
    data = "#{nonce}|#{request.method}|#{url}|#{parameter_string}"

    # Compute the signature
    digest = OpenSSL::Digest.new('sha256')
    hmac = OpenSSL::HMAC.digest(digest, api_key, data)
    hash = Base64.strict_encode64(hmac)

    # Extract the actual request signature
    signature = request.headers['x-authy-signature']

    # Compare the computed signature with the actual signature
    hash == signature
  end
end
import base64
import hashlib
import hmac
import json
import re

try:
    from urllib.parse import urlencode, quote
except ImportError:
    from urllib import urlencode, quote


def validate_authy_request(request,api_key):
    """Validates that incoming requests genuinely originated from Twilio"""
    url = request.url
    method = request.method

    # Read the Flask request parameters
    params = json.loads(request.data)

    # Convert the JSON dict to query params
    query_params = __make_http_query(params)
    sorted_params = '&'.join(sorted(query_params.replace('/', '%2F').replace('%20', '+').split('&')))
    sorted_params = re.sub("\\%5B([0-9])*\\%5D","%5B%5D",sorted_params)
    sorted_params = re.sub("\\=None", "=", sorted_params)

    # Read the nonce from the request
    nonce = request.headers['X-Authy-Signature-Nonce']

    # Concatenate all together and separate by '|'
    data = '|'.join([nonce, method, url, sorted_params])

    # Compute the signature
    computed_dig = hmac.new(
        api_key.encode(),
        msg=data.encode(),
        digestmod=hashlib.sha256
    ).digest()
    computed_sig = base64.b64encode(computed_dig)

    sig = request.headers['X-Authy-Signature']

    # Compare the message signature with your calculated signature
    # Continue processing the request if it's valid, return a 403 error if
    # it's not
    return sig == computed_sig


def __make_http_query(params, topkey=''):
    """
    Function to covert params into url encoded query string
    :param dict params: Json string sent  by Authy.
    :param string topkey: params key
    :return string: url encoded Query.
    """
    if len(params) == 0:
        return ""
    result = ""
    # is a dictionary?
    if type(params) is dict:
        for key in params.keys():
            newkey = quote(key)
            if topkey != '':
                newkey = topkey + quote('[' + key + ']')
            if type(params[key]) is dict:
                result += __make_http_query(params[key], newkey)
            elif type(params[key]) is list:
                i = 0
                for val in params[key]:
                    if type(val) is dict:
                        result +=   __make_http_query(val, newkey + quote('['+str(i)+']'))
                    else:
                        result += newkey + quote('['+str(i)+']') + "=" + quote(str(val)) + "&"
                    i = i + 1
            # boolean should have special treatment as well
            elif type(params[key]) is bool:
                result += newkey + "=" + quote(str(params[key]).lower()) + "&"
            # assume string (integers and floats work well)
            else:
                result += newkey + "=" + quote(str(params[key])) + "&"
    # remove the last '&'
    if (result) and (topkey == '') and (result[-1] == '&'):
        result = result[:-1]
    return result
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

public class CallbackVerifier {
  
    private boolean verify(HttpServletRequest request, String authyApiKey)
            throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        String forwardedProtocol = request.getHeader("X-Forwarded-Proto");
        String protocol = forwardedProtocol != null ? forwardedProtocol :
                request.getScheme();
        String url = String.format("%s://%s%s", protocol,
                request.getServerName(), request.getServletPath());

        // Fetch Json body
        String body = IOUtils.toString(request.getReader());
        Map<String, Object> bodyJson = objectMapper.readValue(body,
                new TypeReference<Map<String, Object>>() {
                });

        // Flatten and sort JSON elements
        String flattenJson = bodyJson.entrySet()
                .stream()
                .flatMap(FlatMap::flatten)
                .map(e -> e.getKey() + "=" + e.getValue())
                .sorted()
                .collect(Collectors.joining("&"));

        // Read the nonce from the request
        String nonce = request.getHeader("X-Authy-Signature-Nonce");

        // Join all the request bits using '|'
        String data = String.format("%s|%s|%s|%s",
                nonce,
                request.getMethod(),
                url,
                flattenJson);

        try {
            // Compute the signature
            Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
            SecretKeySpec secret_key = new SecretKeySpec(authyApiKey.getBytes(),
                    "HmacSHA256");
            sha256_HMAC.init(secret_key);
            String hash = Base64
                    .getEncoder()
                    .encodeToString(sha256_HMAC.doFinal(data.getBytes()));

            // Extract the actual request signature
            String signature = request.getHeader("X-Authy-Signature");

            // Compare the request signature with your computed signature
            return hash.equals(signature);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            e.printStackTrace();
            return false;
        }
    }
}

class FlatMap {

    private static String openBracket;
    private static String closeBracket;

    static {
        try {
            openBracket = encode("[", "UTF-8");
            closeBracket = encode("]", "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private static String prependKeyName(String keyName, String prefix) {
        StringBuffer result = new StringBuffer();
        result.append(prefix);
        int indexOfOpenBracket = keyName.indexOf(openBracket);
        if(indexOfOpenBracket == -1) {
            indexOfOpenBracket = keyName.length();
        }
        result.append(openBracket);
        result.append(keyName.substring(0, indexOfOpenBracket));
        result.append(closeBracket);
        result.append(keyName.substring(indexOfOpenBracket));
        return result.toString();

    }

    public static Stream<Map.Entry<?, ?>> flatten(Map.Entry<?, ?> e) {
        Object value = e.getValue();
        if (value instanceof Map<?, ?>) {
            return ((Map<?, ?>) value).entrySet()
                    .stream()
                    .flatMap(FlatMap::flatten)
                    .map(el -> {
                        String newKeyName = FlatMap.prependKeyName(
                                (String) el.getKey(), (String) e.getKey());
                        return new AbstractMap.SimpleEntry<>(newKeyName, el.getValue());
                    });
        } else if(value == null) {
            return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(), ""));
        } else if(value instanceof String) {
            try {
                String encodedValue = URLEncoder.encode((String) value, "UTF-8");
                return Stream.of(new AbstractMap.SimpleEntry<>(e.getKey(),
                        encodedValue.replace(" ", "+")));
            } catch (UnsupportedEncodingException exception) {
                throw new RuntimeException(exception);
            }
        }
        return Stream.of(e);
    }
}
testable: false
SDK Version:
  • 5.x
SDK Version:
  • 7.x
SDK Version:
  • 3.x
SDK Version:
  • 5.x
SDK Version:
  • 6.x
SDK Version:
  • 5.x
const qs = require('qs');
const crypto = require('crypto');

/**
 * @param {http} req This is an HTTP request from the Express middleware
 * @param {!string} apiKey  Account Security API key
 * @return {Boolean} True if verified
 */
function verifyCallback(req, apiKey) {
  const url = req.protocol + '://' + req.get('host') + req.originalUrl;
  const method = req.method;
  const params = req.body;	// needs `npm i body-parser` on Express 4

  // Sort the params
  const sortedParams = qs
    .stringify(params, { arrayFormat: 'brackets' })
    .split('&')
    .sort(sortByPropertyOnly)
    .join('&')
    .replace(/%20/g, '+');

  // Read the nonce from the request
  const nonce = req.headers['x-authy-signature-nonce'];

  // concatinate all together and separate by '|'
  const data = nonce + '|' + method + '|' + url + '|' + sortedParams;

  // compute the signature
  const computedSig = crypto
    .createHmac('sha256', apiKey)
    .update(data)
    .digest('base64');

  const sig = req.headers['x-authy-signature'];

  // compare the message signature with your calculated signature
  return sig === computedSig;
}

/**
 * Sort by property only.
 *  Normal JS sort parses the entire string so a stringified array value like 'events=zzzz'
 *  would be moved after 'events=aaaa'.
 *
 *  For this approach, we split tokenize the string around the '=' value and only sort alphabetically
 *  by the property.
 *
 * @param {string} x
 * @param {string} y
 * @return {number}
 */
function sortByPropertyOnly(x, y) {
  const xx = x.split('=');
  const yy = y.split('=');

  if (xx < yy) {
    return -1;
  }
  if (xx > yy) {
    return 1;
  }
  return 0;
}
using System.Security.Cryptography;
using Microsoft.AspNetCore.Http.Extensions;
using Newtonsoft.Json.Linq;
using System.Text.RegularExpressions;

namespace Test
{
    public class AuthyWebhook
    {
        private bool Verify(Microsoft.AspNetCore.Http.HttpRequest Request,
          Newtonsoft.Json.Linq.JObject JsonContent,
          string ApiKey)
        {
            // Read the nonce from the request
            var nonce = Request.Headers["x-authy-signature-nonce"];
            var method = Request.Method;
            var url = Request.GetDisplayUrl();
            List<String> bodyRequest = new List<String>();

            // Flatten
            var props = GetPropPaths(string.Empty, JsonContent);
            foreach (var kvp in props)
            {
                var encodedKey = System.Web.HttpUtility.UrlEncode(kvp.Item1);
                var encodedValue = System.Web.HttpUtility.UrlEncode(kvp.Item2);
                // uppercase all escaped url-encoded characters
                Regex reg = new Regex(@"%[a-f0-9]{2}");
                encodedKey = reg.Replace(encodedKey, m => m.Value.ToUpperInvariant());
                encodedValue = reg.Replace(encodedValue, m => m.Value.ToUpperInvariant());
                bodyRequest.Add(encodedKey + "=" + encodedValue);
            }

            // Sort the params
            bodyRequest.Sort();
            var parameters = String.Join("&", bodyRequest);

            // concatenate all together and separate by '|'
            var data = $"{nonce}|{method}|{url}|{parameters}";

            // compute the signature
            var encoding = new System.Text.ASCIIEncoding();
            byte[] ApiKeyBytes = encoding.GetBytes(ApiKey);
            byte[] DataBytes = encoding.GetBytes(data);
            using (var hmacsha256 = new HMACSHA256(ApiKeyBytes))
            {
                // Read the Authy Signature from the request
                var sig = Request.Headers["x-authy-signature"];

                // compare the message signature with your calculated signature
                byte[] hashmessage = hmacsha256.ComputeHash(DataBytes);
                var hash = Convert.ToBase64String(hashmessage);
                return hash == sig;
            }
        }

        private IEnumerable<Tuple<string, string>> GetPropPaths(string currPath, JObject obj)
        {
            foreach (var prop in obj.Properties())
            {
                var propPath = string.IsNullOrWhiteSpace(currPath) ? prop.Name : currPath + "[" + prop.Name + "]";

                if (prop.Value.Type == JTokenType.Object)
                {
                    foreach (var subProp in GetPropPaths(propPath, prop.Value as JObject))
                        yield return subProp;
                }
                else if (prop.Value.Type == JTokenType.Boolean)
                {
                    // Fix boolean type conversion to lowercase string
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString().ToLower());
                }
                else
                {
                    yield return new Tuple<string, string>(propPath, prop.Value.ToString());
                }
            }
        }
    }
}
<?php
/**
 *
 * Verify Authy signature
 *
 * @param    string  $apiKey Authy app API KEY
 * @return   bool
 *
 */
function verifyWebhook($apiKey) {
    // Read the nonce from the request
    $nonce = $_SERVER['HTTP_X_AUTHY_SIGNATURE_NONCE'];
    $method = $_SERVER['REQUEST_METHOD'];
    $proto = isset($_SERVER['HTTPS']) ? "https" : "http";
    $url = "{$proto}://{$_SERVER[HTTP_HOST]}{$_SERVER[REQUEST_URI]}";

    // get the Json string sent by Authy
    $json = file_get_contents('php://input');
    $decoded = json_decode($json, true);
    $normalized = normalizeArray($decoded);

    //  sort the parameters by keys
    $query = http_build_query($normalized);
    $exploded = explode('&', $query);
    sort($exploded);
    $params = implode('&', $exploded);

    // concatenate all together and separate them by '|'
    $data = "$nonce|$method|$url|$params";

    // compute the signature
    $computedSig = base64_encode(hash_hmac('sha256', $data, $apiKey, true));

    // get the authy signature
    $sig = $_SERVER['HTTP_X_AUTHY_SIGNATURE'];

    // compare the message signature with your calculated signature
    return hash_equals($computedSig, $sig);
}

/**
 *
 * Replace boolean values with strings.
 *   In PHP methods like http_build_query converts values from an array
 *   to 0 and 1 integers instead of 'false' and 'true'.
 *
 * @param    array  $array Multidimensional array with boolean values
 * @return   array
 *
 */
function normalizeArray($array) {
  foreach ($array as &$el) {
      if (is_bool($el)) {
        $el = ($el) ? "true" : "false";
      } elseif (is_array($el)) {
        $el = normalizeArray($el);
      }
  }
  return $array;
}
require 'uri'

class CallbackVerifier

  # @param [request] A Rails request object
  # @param [api_key] The API key used to sign the request
  # @return [boolean] True if verified
  def verify_callback(request, api_key)
    url = url_for(:only_path => false, :overwrite_params=>nil)

    # Sort and join the parameters on Rails
    json_params = JSON.parse(request.body.read)
    parameter_string = json_params.to_query

    # Read the nonce from the request
    nonce = request.headers['x-authy-signature-nonce']

    # Join all request parts using '|'
    data = "#{nonce}|#{request.method}|#{url}|#{parameter_string}"

    # Compute the signature
    digest = OpenSSL::Digest.new('sha256')
    hmac = OpenSSL::HMAC.digest(digest, api_key, data)
    hash = Base64.strict_encode64(hmac)

    # Extract the actual request signature
    signature = request.headers['x-authy-signature']

    # Compare the computed signature with the actual signature
    hash == signature
  end
end
import base64
import hashlib
import hmac
import json
import re

try:
    from urllib.parse import urlencode, quote
except ImportError:
    from urllib import urlencode, quote


def validate_authy_request(request,api_key):
    """Validates that incoming requests genuinely originated from Twilio"""
    url = request.url
    method = request.method

    # Read the Flask request parameters
    params = json.loads(request.data)

    # Convert the JSON dict to query params
    query_params = __make_http_query(params)
    sorted_params = '&'.join(sorted(query_params.replace('/', '%2F').replace('%20', '+').split('&')))
    sorted_params = re.sub("\\%5B([0-9])*\\%5D","%5B%5D",sorted_params)
    sorted_params = re.sub("\\=None", "=", sorted_params)

    # Read the nonce from the request
    nonce = request.headers['X-Authy-Signature-Nonce']

    # Concatenate all together and separate by '|'
    data = '|'.join([nonce, method,