Verifying requests

How to verify that an HTTP request is valid.

When the frontend of an app sends an HTTP request, it must use the getCanvaUserToken method to generate a JSON Web Token (JWT) and include that JWT with the request:

const token = await getCanvaUserToken();
const response = await("http://localhost:3001/custom-route", {
headers: {
Authorization: `Bearer ${token}`,
},
});

The backend must then verify that the JWT is valid and reject the request if it's not. This is a security mechanism that reduces the risk of requests from malicious third-parties.

The starter kit includes a backend that automatically validates the JWT, but if you have an existing backend or are working with a different tech stack, you'll need to implement the logic yourself.

This guide explains everything you need to know about setting up a backend that can verify requests.

When an app calls the getCanvaUserToken method, the planned behavior is for Canva to generate a JWT via its backend. At this stage though, the backend for Canva has not been implemented.

As a stop-gap, we've provided a mock server in the starter kit that mimics the planned behavior.

To run the mock server:

  1. Copy the ID of an app from the Your apps page.

  2. In the starter kit's .env file, set CANVA_APP_ID to the ID of the app.

  3. Run the following command:

    npm run start:mock-canva-backend

The mock server must run concurrently with the app's frontend and backend servers.

When Canva's actual backend is released, the app can be updated to point to Canva's server, rather than the local server. No additional changes will be required.

Canva exposes a JSON file that contains a list of public keys. This is an example of the JSON file:

{
"auth_key": {
"app": "YOUR_APP_ID",
"public_keys": [
{
"key_id": "FAKE_KEY_ID_1",
"activation_time_ms": 123456789,
"jwk": "-----BEGIN PUBLIC KEY-----\n ..."
},
{
"key_id": "FAKE_KEY_ID_2",
"activation_time_ms": 234567891,
"jwk": "-----BEGIN PUBLIC KEY-----\n ..."
},
{
"key_id": "FAKE_KEY_ID_3",
"activation_time_ms": 345678912,
"jwk": "-----BEGIN PUBLIC KEY-----\n ..."
}
]
}
}

An app's backend must use one of these keys — only one of them is active at any point in time — to verify that a JWT is legitimate.

When the server starts up, download the JSON file from the following endpoint:

/v0/apps/YOUR_APP_ID/jwks

Be sure to replace YOUR_APP_ID with the ID of the app, as the JWTs are unique to each app.

Until the actual backend is released, the endpoint is available via the mock server:

http://localhost:3002/v0/apps/YOUR_APP_ID/jwks

In the future, the endpoint will be available via the public-api.canva.com domain:

https://public-api.canva.com/v0/apps/YOUR_APP_ID/jwks

The backend must refresh the file on a regular basis to ensure that the app has access to the latest public keys. We recommend updating the file every 60 minutes.

When an app sends a request to its backend, it includes a JWT in the Authorization header. Upon receiving the request, the backend should extract the token from the header:

import { Request, Response } from "express";
function getTokenFromHeader(request: Request) {
const header = request.headers["Authorization"];
if (!header) {
throw new Error("Authorization header is missing");
}
const parts = header.split(" ");
if (parts.length !== 2 || parts[0] !== "Bearer") {
throw new Error("Invalid Authorization header format");
}
const [_, token] = parts;
return token;
}

Be sure to remove the Bearer prefix and any whitespace.

JWTs are made up of three parts:

  • Header
  • Payload
  • Signature

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

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

By default, some libraries don't extract the header. For example, if you're using the jsonwebtoken library for Node.js, you must pass an object into the decode method and set complete to true:

import jwt from "jsonwebtoken";
const decodedToken = jwt.decode("TOKEN GOES HERE", {
complete: true,
});
console.log(decodedToken);

The JWT header object contains a kid property:

import jwt from "jsonwebtoken";
const decodedToken = jwt.decode("TOKEN GOES HERE", {
complete: true,
});
const kid = decodedToken.header.kid;
console.log(kid);

This is known as the key id.

Extract this value from the decoded JWT, as it's required for the next step.

Loop through the public keys that were downloaded from Canva's backend and check if the key_id of any of them is equal to the kid property from the JWT header:

interface AuthKeyJson {
auth_key: AuthKey;
}
interface AuthKey {
app: string;
public_keys: PublicKey[];
}
interface PublicKey {
key_id: string;
activation_time_ms: number;
jwk: string;
}
function findPublicKeyById(
json: AuthKeyJson,
kid: string
): PublicKey | undefined {
return json.auth_key.public_keys.find((key) => key.key_id === kid);
}

If there's no match, the JWT is invalid and the request should be rejected:

const publicKey = findPublicKeyById(json, kid);
if (!publicKey) {
console.log("Public key not found");
response.sendStatus(401);
return;
}

Each object in the public_keys array has an activation_time_ms property. This property is a UNIX timestamp, in milliseconds, that identifies when the key is active from.

The backend should check if the key is active, and if it's not, reject the request:

const currentTime = new Date().getTime();
if (publicKey.activation_time_ms > currentTime) {
console.log("Public key is not active");
response.sendStatus(401);
return;
}

The final step is use a JWT library to verify that the JWT is valid.

This requires two ingredients:

  • The JWT itself
  • The jwk property from the public key object

The following snippet demonstrates how to use the verify method from the jsonwebtoken library:

const verifiedToken = jwt.verify("TOKEN", "PUBLIC_KEY_JWK");

If the token is valid, the return value will be an object containing the following properties:

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

If these properties are available, the JWT is valid and the request can be accepted:

const isValidToken =
verifiedToken["userId"] && verifiedToken["brandId"] && verifiedToken["aud"];
if (isValidToken) {
console.log("The request was successful!");
response.sendStatus(200);
} else {
console.log("The request was not successful.");
response.sendStatus(401);
}

Otherwise, the JWT is invalid and the request should be rejected.