Signature verification
What is signature verification? Why verify request signatures?
If your app has extensions that receive HTTP requests, it must 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.
You can’t submit an app for review until it verifies requests. This applies to both public apps and team apps.

What requests need to be verified?

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.
If an app supports authentication, it must also verify the authenticity of the authentication flow. This is a little different from verifying requests. To learn more, see Authentication.

How Canva confirms that requests are verified

Before an app can be submitted for review, it must pass a signature verification test. This 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.

How to verify a request

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

Example

This example demonstrates how to verify POST and GET requests in Express.js.
1
const { createHmac } = require("crypto");
2
const express = require("express");
3
4
const app = express();
5
6
app.use(
7
express.json({
8
verify: (request, response, buffer) => {
9
request.rawBody = buffer.toString();
10
},
11
})
12
);
13
14
app.post("/content/resources/find", async (request, response) => {
15
if (!isValidPostRequest(process.env.CLIENT_SECRET, request)) {
16
response.sendStatus(401);
17
return;
18
}
19
20
response.send({
21
type: "SUCCESS",
22
resources: [],
23
});
24
});
25
26
app.get("/my-redirect-url", async (request, response) => {
27
if (!isValidGetRequest(process.env.CLIENT_SECRET, request)) {
28
response.sendStatus(401);
29
return;
30
}
31
32
response.sendStatus(200);
33
});
34
35
const isValidPostRequest = (secret, request) => {
36
// Verify the timestamp
37
const sentAtSeconds = request.header("X-Canva-Timestamp");
38
const receivedAtSeconds = new Date().getTime() / 1000;
39
40
if (!isValidTimestamp(sentAtSeconds, receivedAtSeconds)) {
41
return false;
42
}
43
44
// Construct the message
45
const version = "v1";
46
const timestamp = request.header("X-Canva-Timestamp");
47
const path = getPathForSignatureVerification(request.path);
48
const body = request.rawBody;
49
const message = `${version}:${timestamp}:${path}:${body}`;
50
51
// Calculate a signature
52
const signature = calculateSignature(secret, message);
53
54
// Reject requests with invalid signatures
55
if (!request.header("X-Canva-Signatures").includes(signature)) {
56
return false;
57
}
58
59
return true;
60
};
61
62
const isValidGetRequest = (secret, request) => {
63
// Verify the timestamp
64
const sentAtSeconds = request.query.time;
65
const receivedAtSeconds = new Date().getTime() / 1000;
66
67
if (!isValidTimestamp(sentAtSeconds, receivedAtSeconds)) {
68
return false;
69
}
70
71
// Construct the message
72
const version = "v1";
73
const { time, user, brand, extensions, state } = request.query;
74
const message = `${version}:${time}:${user}:${brand}:${extensions}:${state}`;
75
76
// Calculate a signature
77
const signature = calculateSignature(secret, message);
78
79
// Reject requests with invalid signatures
80
if (!request.query.signatures.includes(signature)) {
81
return false;
82
}
83
84
return true;
85
};
86
87
const isValidTimestamp = (
88
sentAtSeconds,
89
receivedAtSeconds,
90
leniencyInSeconds = 300
91
) => {
92
return (
93
Math.abs(Number(sentAtSeconds) - Number(receivedAtSeconds)) <
94
Number(leniencyInSeconds)
95
);
96
};
97
98
const getPathForSignatureVerification = (input) => {
99
const paths = [
100
"/configuration",
101
"/configuration/delete",
102
"/content/resources/find",
103
"/editing/image/process",
104
"/editing/image/process/get",
105
"/publish/resources/find",
106
"/publish/resources/get",
107
"/publish/resources/upload",
108
];
109
110
return paths.find((path) => input.endsWith(path));
111
};
112
113
const calculateSignature = (secret, message) => {
114
// Decode the client secret
115
const key = Buffer.from(secret, "base64");
116
117
// Calculate the signature
118
return createHmac("sha256", key).update(message).digest("hex");
119
};
120
121
app.listen(process.env.PORT || 3000);
Copied!
Last modified 14d ago