Sending HTTP requests

How to send HTTP requests from an app.

At their most basic, apps are web pages embedded in an iframe. As a result, they have access to many standard web APIs, including the Fetch API.

Apps can use the Fetch API for a variety of purposes, such as fetching data from an API or offloading tasks to a server, such as machine learning or image processing tasks.

For the most part, using the Fetch API in an app is identical to using it anywhere else. Canva doesn't monkey-patch the API or otherwise modify its behavior.

The one caveat is that, before an app is approved for release, its backend must verify any incoming requests. This verification needs to confirm that requests are arriving from the app's frontend and not from some other, potentially malicious source. This is a security precaution that protects your app and our users from a variety of threats.

This page explains everything you need to know about how to verify requests.

Before an app sends an HTTP request to its backend, it must request a JWT from Canva. The app's backend can use this JWT to verify that the request is legitimate.

To get a JWT, initialize the Authentication capability:

import { getAuthentication } from "@canva/authentication";
const auth = getAuthentication();

Then call the getCanvaUserToken method:

const token = await auth.getCanvaUserToken();

The method sends a request to Canva and returns a JWT.

  1. Call the fetch method:

    const response = await fetch("http://localhost:3001/my/api/endpoint");
  2. In the request's headers, include an Authorization header that contains the JWT:

    const response = await fetch("http://localhost:3001/my/api/endpoint", {
    headers: {
    Authorization: `Bearer ${token}`,
    },
    });

    Be sure to include the Bearer prefix.

When an app's backend receives a request, it must:

  • Verify that the JWT is valid
  • Reject the request if it's not valid

In the starter kit, the example backend handles this for you. You only need to add the ID of the app to the .env file. The rest of this document explains how to set up the logic in your own backend.

When the backend receives the request, extract the JWT from the Authorization header. The following snippet demonstrates how to extract a JWT from the Express.js Request object:

import express from "express";
const app = express();
app.post("/my/api/endpoint", async (request, response) => {
const token = getTokenFromHeader(request);
if (!token) {
return response.sendStatus(401);
}
console.log(token);
return response.sendStatus(200);
});
app.listen(process.env.PORT || 3000);
function getTokenFromHeader(request: express.Request) {
const header = request.headers["authorization"];
if (!header) {
return;
}
const parts = header.split(" ");
if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer") {
return;
}
const [, token] = parts;
return token;
}

If the request doesn't contain a token, reject it with a 401 error.

JWTs are made up of three parts:

  • Header
  • Payload
  • Signature

After extracting the JWT from the Authorization header, use a library to:

  1. Decode the JWT
  2. Extract the kid property from the JWT header

By default, some libraries don't extract the header. For example, if you're using the jsonwebtoken library for Node.js, you must enable this behavior with the complete property:

import express from "express";
import jwt from "jsonwebtoken";
const app = express();
app.post("/my/api/endpoint", async (request, response) => {
const token = getTokenFromHeader(request);
if (!token) {
return response.sendStatus(401);
}
const decoded = jwt.decode(token, { complete: true });
const { kid } = decoded.header;
console.log(kid);
return response.sendStatus(200);
});

Canva exposes a JSON Web Key Set (JWKS) via the following endpoint:

https://api.canva.com/rest/v1/apps/YOUR_APP_ID/jwks

This is a JSON file that looks something like this:

{
"keys": [
{
"kid": "292e133c-2afe-4cb6-8e8d-43468affa32a",
"kty": "RSA",
"n": "39fdyga5zNmwBhc0Hsdpd_u5DrJa8-OS8KkyoD_sipY4rbD6yyBSr1kqJa3n8qG1K2d96OEVZH-_BdpeLMHmP3NkhCacT1dkzpM_b0mWLCYA-xKt-eAFVIAxiVjorjQHtX6qD-UtborDwMKMm0ul3TFJPU2LVNmLePZrfPkb3jMkzYQPixprmdh5XfR-r853RhphhkscvbLJIcSdz56_6gQZrp6peGOn_7XSxiOSDbFdEgPMAxaFP1vHStp8yj09K_UKGOFQye06Dz26DIN8U8F8_QFafLuIp0fl-2eehfUT8f_iFUE3kuOkzJsXL3Wg4kjmsVoSlVIFhM0KPVs_hw",
"e": "AQAB"
}
]
}

Each object in the keys array is a JSON Web Key (JWK) — also known as a public key. The array may contain multiple keys, but only one key is active at any point in time.

Your app's backend must:

  1. Download the JWKS file
  2. Get the active public key — that is, the key with a kid property that matches the kid from the JWT header

To do this, we recommend using a JWKS library, such as jwks-rsa:

import express from "express";
import { JwksClient } from "jwks-rsa";
import jwt from "jsonwebtoken";
const { APP_ID } = process.env;
const CACHE_EXPIRY_MS = 60 * 60 * 1_000; // 60 minutes
const TIMEOUT_MS = 30 * 1_000; // 30 seconds
const app = express();
app.post("/my/api/endpoint", async (request, response) => {
// Decode the token
const token = getTokenFromHeader(request);
if (!token) {
return response.sendStatus(401);
}
const decoded = jwt.decode(token, { complete: true });
const { kid } = decoded.header;
// Get the public key
const jwks = new JwksClient({
cache: true,
cacheMaxAge: CACHE_EXPIRY_MS,
timeout: TIMEOUT_MS,
rateLimit: true,
jwksUri: `https://api.canva.com/rest/v1/apps/${APP_ID}/jwks`,
});
const publicKey = await jwks.getSigningKey(kid).getPublicKey();
return response.sendStatus(200);
});

Under the hood, this library:

  1. Uses the kid property from the JWT header to find a key with a matching kid property
  2. Caches the JSON file for the specified amount of time — in this case, 60 minutes

By caching the JSON file, the backend doesn't have to download the file every time an endpoint receives a request.

An alternative to caching is to:

  1. Download the JWKS file when the server starts up (rather than when an endpoint is hit)
  2. Redownload the JWKS file on a set schedule — for example, with the setInterval function

The exact mechanism isn't important. The key is to avoid downloading the JWKS file more times than is necessary, as this is more performant and reduces the risk of DDOS attacks.

Use a JWT library to verify that the JWT is valid.

This requires two ingredients:

  • The JWT
  • The public key

The following snippet demonstrates how to verify the JWT with the jsonwebtoken library:

app.post("/my/api/endpoint", async (request, response) => {
// Decode the token
const token = getTokenFromHeader(request);
if (!token) {
return response.sendStatus(401);
}
const decoded = jwt.decode(token, { complete: true });
// Get the public key
const jwks = new JwksClient({
cache: true,
cacheMaxAge: CACHE_EXPIRY_MS,
timeout: TIMEOUT_MS,
rateLimit: true,
jwksUri: `https://api.canva.com/rest/v1/apps/${APP_ID}/jwks`,
});
const { kid } = decoded.header;
const publicKey = await jwks.getSigningKey(kid).getPublicKey();
// Verify the token
const verified = jwt.verify(token, publicKey, {
audience: APP_ID,
});
return response.sendStatus(200);
});

If the verified token is valid, it will contain the following properties:

  • aud - The ID of the app
  • brandId - The ID of the user's team
  • userId - The ID of the user

If these properties are not available, reject the request:

if (!verified.aud || !verified.brandId || !verified.userId) {
return response.sendStatus(401);
}