Signature verification

What is signature verification? Why verify request signatures?

If your app has extensions that receive HTTP requests, it is recommended that you verify that any requests it receives are actually arriving from Canva (and not from some nefarious third-party). This protects your app and our users from a variety of attacks.

Once you enable signature verification checks, all HTTP requests that Canva sends to an app must be verified. This includes POST requests, such as requests sent to the /content/resources/find endpoint, and GET requests, such as the request sent to the app's Redirect URL.

The signature verification test is available via the Developer Portal. The test sends a combination of valid and invalid requests to the app. The app must accept the valid requests and reject the invalid requests.

To learn more, see Signature verification test.

The steps for verifying a request depend on whether it's a GET or POST request. To learn more, see the following guides:

This example demonstrates how to verify POST and GET requests in Express.js.

const { createHmac } = require("crypto");
const express = require("express");
const app = express();
app.use(
express.json({
verify: (request, response, buffer) => {
request.rawBody = buffer.toString();
},
})
);
app.post("/content/resources/find", async (request, response) => {
if (!isValidPostRequest(process.env.CLIENT_SECRET, request)) {
response.sendStatus(401);
return;
}
response.send({
type: "SUCCESS",
resources: [],
});
});
app.get("/my-redirect-url", async (request, response) => {
if (!isValidGetRequest(process.env.CLIENT_SECRET, request)) {
response.sendStatus(401);
return;
}
response.sendStatus(200);
});
const isValidPostRequest = (secret, request) => {
// Verify the timestamp
const sentAtSeconds = request.header("X-Canva-Timestamp");
const receivedAtSeconds = new Date().getTime() / 1000;
if (!isValidTimestamp(sentAtSeconds, receivedAtSeconds)) {
return false;
}
// Construct the message
const version = "v1";
const timestamp = request.header("X-Canva-Timestamp");
const path = getPathForSignatureVerification(request.path);
const body = request.rawBody;
const message = `${version}:${timestamp}:${path}:${body}`;
// Calculate a signature
const signature = calculateSignature(secret, message);
// Reject requests with invalid signatures
if (!request.header("X-Canva-Signatures").includes(signature)) {
return false;
}
return true;
};
const isValidGetRequest = (secret, request) => {
// Verify the timestamp
const sentAtSeconds = request.query.time;
const receivedAtSeconds = new Date().getTime() / 1000;
if (!isValidTimestamp(sentAtSeconds, receivedAtSeconds)) {
return false;
}
// Construct the message
const version = "v1";
const { time, user, brand, extensions, state } = request.query;
const message = `${version}:${time}:${user}:${brand}:${extensions}:${state}`;
// Calculate a signature
const signature = calculateSignature(secret, message);
// Reject requests with invalid signatures
if (!request.query.signatures.includes(signature)) {
return false;
}
return true;
};
const isValidTimestamp = (
sentAtSeconds,
receivedAtSeconds,
leniencyInSeconds = 300
) => {
return (
Math.abs(Number(sentAtSeconds) - Number(receivedAtSeconds)) <
Number(leniencyInSeconds)
);
};
const getPathForSignatureVerification = (input) => {
const paths = [
"/configuration",
"/configuration/delete",
"/content/resources/find",
"/publish/resources/find",
"/publish/resources/get",
"/publish/resources/upload",
];
return paths.find((path) => input.endsWith(path));
};
const calculateSignature = (secret, message) => {
// Decode the client secret
const key = Buffer.from(secret, "base64");
// Calculate the signature
return createHmac("sha256", key).update(message).digest("hex");
};
app.listen(process.env.PORT || 3000);
javascript