Verify POST request signatures
When Canva sends a POST
request to an app, it includes a comma-separated list of request signatures in the X-Canva-Signatures
header.
A request signature is a unique string that identifies the request:
e03c80881a48bb730cee12c7e842301b0b116b970a03068a5f5263358926e897
Before you can submit an app for review, the app must:
- Calculate a signature for each request.
- Check if the calculated signature is included in the comma-separated list of signatures.
- Reject the request with a
401
status code if the calculated signature is not included in the list of signatures.
This protects the app from a variety of attacks.
Canva provides a list of signatures—rather than just one signature—to support key rotation. This lets you regenerate client secrets without causing downtime.
Step 1: Get your app's client secret
Every app has a client secret. This is a sensitive value that's shared between Canva and your app. You must use the secret to calculate a request signature.
To get your app's client secret:
- Navigate to an app via the Developer Portal.
- Click Verification.
- Under the Client secret heading, click Copy.
Never share a client secret or commit it to source control.
We recommend loading the client secret via an environment variable.
Step 2: Decode your app's client secret
Canva provides the Client secret as a base64-encoded string. Your app must decode this string into a byte array. The following snippet demonstrates how to do this in Node.js:
const secret = process.env.CLIENT_SECRET;const key = Buffer.from(secret, "base64");console.log(key);
To learn how to decode a client secret in different programming languages, refer to Decoding a client secret.
Step 3: Create a message
To verify that a request signature was generated by Canva, an app must calculate the signature itself and compare it to the provided signatures. This requires two ingredients: a key and a message.
The key is the decoded client secret.
In a POST
request, the message is a colon-separated string that contains the following values:
Version
The version of Canva's API that's sending the request. You must set this value to v1
.
Timestamp
The UNIX timestamp (in seconds) of when Canva sent the request. This timestamp is provided in the X-Canva-Timestamp
header.
The names of the HTTP headers are sometimes lowercase (for example, x-canva-timestamp
).
Path
The path that Canva appends to the extension's Base URL, such as:
/content/resources/find
/publish/resources/find
/publish/resources/get
/publish/resources/upload
If the Base URL includes additional path segments, do not include these when calculating a request signature. For example, if the Base URL is example.com/api
, omit /api
.
These are examples of invalid paths:
/api/content/resources/find
/api/publish/resources/upload
Body
The raw, unserialized body of the request. This is the body of the request before it's parsed as JSON.
Some web frameworks, such as Express.js, automatically deserialize incoming request bodies. You need to bypass this functionality to access the raw body of the request.
This snippet demonstrates how to construct a message for a POST
request:
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) => {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}`;console.log(message);});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));};app.listen(process.env.PORT || 3000);
This is an example of a message for a POST
request:
v1:1586167939:/content/resources/find:{"user":"AXqAwpfw2GuMaXL9-zBB8LKhViH6JTO068_8XTXjaJE=","brand":"AXqAwpfm9BvNmaakx13Cz_r13DTeRea9hWZt09b_u7s=","label":"CONTENT","limit":8,"query":"","locale":"en-GB","type":"EMBED"}
Step 4: Calculate the request signature
When you have a key and a message, use these values to calculate a SHA-256 hash and convert that hash into a hex-encoded string. The result is the signature of the request.
This snippet demonstrates how to calculate a signature in Node.js:
const { createHmac } = require("crypto");const signature = createHmac("sha256", key).update(message).digest("hex");console.log(signature);
You can refactor this logic into a calculateSignature
function that accepts a secret and a message and returns a signature:
function calculateSignature(secret, message) {// Decode the client secretconst key = Buffer.from(secret, "base64");// Generate the signaturereturn createHmac("sha256", key).update(message).digest("hex");}
You can then use this function to calculate the signature for POST
and GET
requests.
Step 5: Compare the signatures
When Canva sends a POST
request to an app, it includes a comma-separated list of request signatures in the X-Canva-Signatures
header.
If the calculated signature is not included in the list of signatures, the request did not originate from Canva and the app must reject the request with a 401
status code:
// Load the client secret from an environment variableconst secret = process.env.CLIENT_SECRET;// Construct the messageconst 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 signatureconst signature = calculateSignature(secret, message);// Reject requests with invalid signaturesif (!request.header("X-Canva-Signatures").includes(signature)) {response.sendStatus(401);return;}
Step 6: Verify the timestamp
Even if an app verifies request signatures, it's still vulnerable to replay attacks(opens in a new tab or window). To protect itself against these types of attacks, an app must:
- Compare the timestamp of when the request was sent with when it was received.
- Verify that the timestamps are within 5 minutes (300 seconds) of one another.
When the timestamps are not within 5 minutes of one another, the app must reject the request by returning a 401
status code.
In a POST
request, an app can access the UNIX timestamp (in seconds) of when Canva sent the request via the X-Canva-Timestamp
HTTP header.
The following snippet demonstrates how to create an isValidTimestamp
function that checks if two timestamps are within 300 seconds of each other and rejects the request if they're not:
function isValidTimestamp(sentAtSeconds,receivedAtSeconds,leniencyInSeconds = 300) {return (Math.abs(Number(sentAtSeconds) - Number(receivedAtSeconds)) <Number(leniencyInSeconds));}const sentAtSeconds = request.header("X-Canva-Timestamp");const receivedAtSeconds = new Date().getTime() / 1000;// Verify the timestamp of a POST requestif (!isValidTimestamp(sentAtSeconds, receivedAtSeconds)) {response.sendStatus(401);return;}
The leniency of 5 minutes accounts for the fact that requests are not instantaneous and server clocks may not be perfectly synchronized.
Example
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: [],});});const isValidPostRequest = (secret, request) => {// Verify the timestampconst sentAtSeconds = request.header("X-Canva-Timestamp");const receivedAtSeconds = new Date().getTime() / 1000;if (!isValidTimestamp(sentAtSeconds, receivedAtSeconds)) {return false;}// Construct the messageconst 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 signatureconst signature = calculateSignature(secret, message);// Reject requests with invalid signaturesif (!request.header("X-Canva-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 secretconst key = Buffer.from(secret, "base64");// Calculate the signaturereturn createHmac("sha256", key).update(message).digest("hex");};app.listen(process.env.PORT || 3000);