Verify GET request signatures
When Canva sends a GET
request to an app, it includes a comma-separated list of request signatures in the signatures
query parameter.
A request signature is a unique string of characters 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 found in the comma-separated list of signatures.
- Return a
401
status code if the calculated signature is not in the list of signatures.
This protects the app from a variety of attacks.
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.
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);
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 GET
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. You can access this timestamp via the time
query parameter.
User
The ID of the user. You can access this ID via the user
query parameter.
Brand
The ID of the user's brand. You can access this ID via the brand
query parameter.
Extensions
A comma-separated list of extension types, such as CONTENT
or PUBLISH
. You can access this list via the extensions
query parameter.
State
A unique token for protecting an app against CSRF attacks. You can access this token via the state
query parameter.
This snippet demonstrates how to construct a message for a GET
request:
const express = require("express");const app = express();app.get("/my-redirect-url", async (request, response) => {const version = "v1";const { time, user, brand, extensions, state } = request.query;const message = `${version}:${time}:${user}:${brand}:${extensions}:${state}`;console.log(message);});app.listen(process.env.PORT || 3000);
This is an example of a message for a GET
request:
v1:1586167939:AQy_Xvglh9cbgHk97BqOiRscRk98Vm-Fjytfs9X-68s=:AQy_XvgNXCsnKeFtcD5-L-VBg_ngJepbEhGYBVmCo6E=:CONTENT:95a5aa62-0713-4ae4-b99f-8efa57e7def0
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 GET
and POST
requests.
Step 5: Compare the signatures
When Canva sends a GET
request to an app, it includes a comma-separated list of request signatures in the signatures
query parameter.
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 { time, user, brand, extensions, state } = request.query;const message = `${version}:${time}:${user}:${brand}:${extensions}:${state}`;// Calculate a signatureconst signature = calculateSignature(secret, message);// Reject requests with invalid signaturesif (!request.query.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. 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 GET
request, an app can access the UNIX timestamp (in seconds) of when Canva sent the request via the time
query parameter.
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.query.time;const receivedAtSeconds = new Date().getTime() / 1000;// Verify the timestamp of a POST requestif (!isValidTimestamp(sentAtSeconds, receivedAtSeconds)) {response.sendStatus(401);return;}
Example
const { createHmac } = require("crypto");const express = require("express");const app = express();app.get("/my-redirect-url", async (request, response) => {if (!isValidGetRequest(process.env.CLIENT_SECRET, request)) {response.sendStatus(401);return;}response.sendStatus(200);});const isValidGetRequest = (secret, request) => {// Verify the timestampconst sentAtSeconds = request.query.time;const receivedAtSeconds = new Date().getTime() / 1000;if (!isValidTimestamp(sentAtSeconds, receivedAtSeconds)) {return false;}// Construct the messageconst version = "v1";const { time, user, brand, extensions, state } = request.query;const message = `${version}:${time}:${user}:${brand}:${extensions}:${state}`;// Calculate a signatureconst signature = calculateSignature(secret, message);// Reject requests with invalid signaturesif (!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 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);