Verifying Request Signatures

Verifying request signatures from FreeClimb helps keep your applications extra secure

If your application deals with sensitive data or may be threatened by bad actors, you can add a level of security to your applications by verifying that webhook events are from FreeClimb and not a third party. FreeClimb does this by adding a FreeClimb-Signature HTTP header on each HTTP request. This header is created by combining a signing secret unique to your account with the body of the request that we send using a standard HMAC-SHA256 keyed hash. You authenticate the HTTP request by comparing a signature you generate using your signing secret against the signature(s) found in the header. If they match, the request from FreeClimb is authentic. If verification fails, the SDK returns an error message and your application should not trust the request.


How to verify requests using a FreeClimb SDK

The following FreeClimb SDKs have built-in verification:

If using one of these SDKs, you can perform verification quickly on all requests by including your signing secret in your application configuration, similar to API key handling.

First, add your signing secret to environment variables:

ACCOUNT_ID="YOUR-ACCOUNT-TOKEN"
API_KEY="YOUR-API-KEY"
SIGNING_SECRET="YOUR-SIGNING-SECRET"

Then, add your signing secret to your configuration. The examples below build on our Voice Quickstarts:

// We will need to parse the request's body in order to verify the request, so we will be using the 'body-parser' library.

const express = require('express')
const bodyParser = require('body-parser')               // <-- Added for verifying request signature
const app = express()
app.use(bodyParser.raw({ type: '*/*' }))                // <-- Added for verifying request signature
const freeclimbSDK = require('@freeclimb/sdk')
const port = process.env.PORT || 80
const signingSecret = process.env.SIGNING_SECRET        // <-- Added for verifying request signature
 
// your freeclimb API key (available in the Dashboard) - be sure to set up environment variables to store these values
const accountId = process.env.ACCOUNT_ID
const apiKey = process.env.API_KEY
const freeclimb = freeclimbSDK(accountId, apiKey)
 
// Handles incoming requests on the /incomingCall endpoint
app.post('/incomingCall', (req, res) => {
 
    // Verify Request                                                               // <-- Added for verifying request signature
    const signatureHeader = req.headers['freeclimb-signature'];                     // <-- Added for verifying request signature
    const requestBody = req.body.toString();                                        // <-- Added for verifying request signature
    freeclimb.utils.verifyRequest(requestBody, signatureHeader, signingSecret);     // <-- Added for verifying request signature
 
    // Create Say script to greet caller
    const hello = freeclimb.percl.say("Hello world!")
 
    // Add greeting to PerCL script and append to response
    res.status(200).json(freeclimb.percl.build(hello))
})
 
app.listen(port)app.listen(port)
// We will need to parse the request's header and body in order to verify the request, so we will be using spring-framework's 'RequestHeader' and 'RequestBody' classes.

package main.java.hello;
 
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestHeader;   // <-- Added for verifying request signature
import org.springframework.web.bind.annotation.RequestBody;     // <-- Added for verifying request signature
 
import com.vailsys.freeclimb.percl.PerCLScript;
import com.vailsys.freeclimb.percl.Say;
import com.vailsys.freeclimb.api.FreeClimbException;            // <-- Added for verifying request signature
import com.vailsys.freeclimb.Utils;                             // <-- Added for verifying request signature
 
@RestController
public class HelloFreeClimbController {
  // Get signing secret from environment variables              // <-- Added for verifying request signature
  private String signingSecret = System.getenv("SECRET");       // <-- Added for verifying request signature
  @RequestMapping("/voice")
  public String index(@RequestHeader(value="freeclimb-signature") String signatureHeader, @RequestBody String body) {  // <-- Updated for verifying request signature
    try {                                                           // <-- Added for verifying request signature
      // Verify Request                                             // <-- Added for verifying request signature
      Utils.verifyRequest(body, signatureHeader, signingSecret);    // <-- Added for verifying request signature
       
      // Create PerCL script
      PerCLScript script = new PerCLScript();
      // Create Say script to greet caller
      Say sayHelloWorld = new Say("Hello world!");
      // Add Say script to PerCL script
      script.add(sayHelloWorld);
 
      // Return PerCL script as response after converting to JSON
      return script.toJson();
    } catch (FreeClimbException e) {                            // <-- Added for verifying request signature
      throw new Error(e);                                       // <-- Added for verifying request signature
    }                                                           // <-- Added for verifying request signature
  }
}
import freeclimb
from flask import Flask, request
from freeclimb import percl_to_json
from freeclimb import Utils # <-- Added for verifying request signature
import json
import os                   # <-- Added for verifying request signature
 
app = Flask(__name__)
 
signingSecret = os.environ['SECRET']    # <-- Added for verifying request signature
 
# Specify this route with 'Voice URL' in App Config
@app.route('/incomingCall', methods=['POST'])
def incomingCall():
    if request.method == 'POST':
        # Verify request                                            # <-- Added for verifying request signature
        body = json.dumps(request.json).replace(' ', '')            # <-- Added for verifying request signature
        signatureHeader = request.headers['freeclimb-signature']    # <-- Added for verifying request signature
        Utils.verify_request(body, signatureHeader, signingSecret)  # <-- Added for verifying request signature
         
        # Create Say script to greet caller
        message = "Hello, World!"
        say = freeclimb.Say(message)
        # Create PerCL script and add Say script to PerCL script
        script = freeclimb.PerclScript(commands=[say])
        # Return PerCL script as response after converting to JSON
        return percl_to_json(script)
 
# Specify this route with 'STATUS CALLBACK URL' in App Config
@app.route('/status', methods=['POST'])
def status():
return json.dumps({'success': True}), 200, {'ContentType': 'application/json'}
require 'sinatra'
require 'freeclimb'
require 'json'

signingSecret = ENV['SIGNING_SECRET']

post '/voice' do
  FreeClimb::Utils.verify_request(request.body.string, request.env["HTTP_FREECLIMB_SIGNATURE"], signingSecret)
  say = Freeclimb::Say.new(text:'Hello, World!')
  script = Freeclimb::PerclScript.new(commands:[say])
  Freeclimb::percl_to_json(script)
end

Once signing secrets are included in your application configuration, FreeClimb will make sure to use this secret to authenticate all requests. If verification fails, the SDK will return an error message.

If you are using another language not currently supported by a FreeClimb SDK but would still like to verify requests, skip to How to verify requests manually. We are currently working on adding signing secret support to our C# and PHP SDKs.


Getting your signing secret

You can get your most recently created signing secret from the API Credentials page in your dashboard.


Generating a new signing secret

Sign in to your Dashboard and go to Account > API Credentials. You will see signing secret below your API key(s).

1694

You can find your signing secret on the API Credentials page below your API key

If you only have one signing secret, you can use the Add Secret button to get a new one.

1604

The Add Secret button allows you to add a second signing secret

Once you've clicked Add in the resulting pop-up modal, a new signing secret will be generated. At this time, both secrets will be live. This allows you to update your applications without any downtime.

🚧

Once you've created a new secret and have successfully updated all your applications, we highly recommend deleting your old secret for security purposes.

If you already have two signing secrets, you'll need to delete a secret before you can generate a new one. Accounts are limited to only two signing secrets at one time to limit the possibility of bad actors finding and misusing your secrets.


Deleting a signing secret

FreeClimb accounts must have at least one signing secret at all times. Therefore, if you only have one signing secret and wish to delete it, you must first generate a new one and then delete the other.

Click the Delete button on the secret you wish to delete.

1656

The Delete button will permanently delete the secret and remove it from your account

Once clicked, you will be prompted to provide your password. Entering your password and clicking the Delete button will permanently delete your secret. Once secrets are deleted, they cannot be returned to your account.


How to verify requests manually

If you are using a language not currently supported by a FreeClimb SDK but would still like to verify requests, you can do so manually.

Step 1: Extract the timestamp and signatures from the header

Break down the header's value into a list of items by using the , character. Then, split each list item using the = character in order to create key-value pairs.

Once you've done this, extract the timestamp from key t, and the signature from key v1. If you have two signing secrets, make sure that you extract both and keep like versions together and unlike versions apart.

Step 2: Verify the timestamp

Compute the difference between the current timestamp and the received timestamp and decide if the difference is sufficient. We suggest five minutes or less as an acceptable time difference.

Step 3: Construct the expected signature

Concatenate the header's timestamp with a . and the contents of the raw request body. Using your signing secret as a key, use HMAC-SHA256 to hash the data from the concatenated header. Output the hash using a hexadecimal encoding for comparison with the request header.

Compare this newly generated signature to the signature (or signatures if you have two secrets) extracted from the header. The signature(s) is / are the value(s) of the v1 key in the request header.

If they match, the request is valid. If they do not match, the request is not valid.

Example manual verification

We have provided an example to help you test your implementation. Given you have the following header:

FreeClimb-Signature: t=1617735085,v1=1d798c86e977ff734dec3a8b8d67fe8621dcc1df46ef4212e0bfe2e122b01bfd,v1=1ba18712726898fbbe48cd862dd096a709f7ad761a5bab14bda9ac24d963a6a8

And you have the following request body:

'{"accountId":"AC1334ffb694cd8d969f51cddf5f7c9b478546d50c","callId":"CAccb0b00506553cda09b51c5477f672a49e0b2213","callStatus":"ringing","conferenceId":null,"direction":"inbound","from":"+13121000109","parentCallId":null,"queueId":null,"requestType":"inboundCall","to":"+13121000096"}'

And the following signing secret:

sigsec_ead6d3b6904196c60835d039e91b3341c77a7793

Using the above three elements, you would calculate the signature as follows:

const signedTime = 1617735085
const requestBody = '{"accountId":"AC1334ffb694cd8d969f51cddf5f7c9b478546d50c","callId":"CAccb0b00506553cda09b51c5477f672a49e0b2213","callStatus":"ringing","conferenceId":null,"direction":"inbound","from":"+13121000109","parentCallId":null,"queueId":null,"requestType":"inboundCall","to":"+13121000096"}'
const data = signedTime + '.' + requestBody
const signingSecret = 'sigsec_ead6d3b6904196c60835d039e91b3341c77a7793'
const signature = HMAC-SHA256(data, key).toString(Encoding.Hex)
// signature === 1d798c86e977ff734dec3a8b8d67fe8621dcc1df46ef4212e0bfe2e122b01bfd