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:
- Node.js SDK 2.0.0 or later
- Java SDK 4.0.0 or later
- Python SDK 3.0.0 or later
- Ruby SDK 3.0.0 or later
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:
const express = require('express')
const bodyParser = require('body-parser')
const {
createConfiguration,
DefaultApi,
PerclScript,
Say
} = require('@freeclimb/sdk')
const { RequestVerifier } = require('@freeclimb/sdk/utils/RequestVerifier')
const app = express()
app.use(bodyParser.raw({ type: '*/*' }))
const port = process.env.PORT || 80
const signingSecret = process.env.SIGNING_SECRET
const accountId = process.env.ACCOUNT_ID
const apiKey = process.env.API_KEY
const freeclimb = new DefaultApi(createConfiguration({ accountId, apiKey }))
app.post('/incomingCall', (req, res) => {
const signatureHeader = req.headers['freeclimb-signature'];
const requestBody = req.body.toString();
const tolerance = 5 * 60;
RequestVerifier.verifyRequestSignature(
requestBody,
signatureHeader,
signingSecret,
tolerance
)
const script = new PerclScript({
commands: [new Say({ text: "Hello, World! " })],
});
res.status(200).json(script.build())
})
app.listen(port, () => {
console.log(`Started server on port ${ 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.utils.request_verifier import RequestVerifier
import json
import os
app = Flask(__name__)
# Specify this route with 'Voice URL' in App Config
@app.route('/incomingCall', methods=['POST'])
def incomingCall():
if request.method == 'POST':
body = json.dumps(request.json).replace(' ', '')
request_header = request.headers['freeclimb-signature']
signing_secret = os.environ('SECRET')
tolernace = 5 * 60
RequestVerifier.verify_request_signature(body, request_header, signing_secret, tolerance)
say = freeclimb.Say(text='Hello, World!')
percl_script = freeclimb.PerclScript(commands=[say])
return percl_script.tojson()
# 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).
If you only have one signing secret, you can use the Add Secret button to get a new one.
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.
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
Updated 9 days ago