Verifying JWTs

How to verify a JSON Web Token (JWT).

For security reasons, Canva uses JSON Web Tokens (JWTs) to encode certain information, such as the ID of the user or the ID of a user's design. To access this information, apps must decode and verify the JWTs.

In the Apps SDK, there are two types of tokens:

  • Design tokens
  • User tokens

Design tokens encode information about the current design, such as the ID of the design, while user tokens encode information about the current user, such as the ID of the user and their team.

The steps for verifying each type of token are the same, but the tokens encode different information and apps access them in different ways:

  • An app can call the getDesignToken method. This method returns a design token, which the app can use to get the ID of a design and associate data with that design. To learn more, see Using design IDs.
  • An app can start an authentication flow. This redirects the user to the app's Redirect URL. Canva appends a user token to the URL. The app's backend can use this token to authenticate the user via a third-party platform.
  • An app can call the auth.getCanvaUserToken method. This method returns a user token, which the app can use to verify HTTP requests or identify users (for example, to check if they're authenticated or not).

JWTs are made up of three parts:

  • Header
  • Payload
  • Signature

The part we're interested in is the header.

To extract the header, we recommend using a library. By default though, some libraries don't extract the header. When using the jsonwebtoken library, for instance, you must enable the behavior:

import jwt from "jsonwebtoken";
const decoded = jwt.decode("JWT_GOES_HERE", { complete: true });
console.log(decoded.header);
ts

The JWT header is an object. This object has a kid property, which is short for key ID. You need to grab this property from the header object as it's required in a later step:

const { kid } = decoded.header;
console.log(kid);
ts

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

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

The endpoint returns 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"
}
]
}
json

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.

The active public key is the key with a kid property that matches the kid property from the JWT header.

If possible, we recommend using a library to handle these steps and wrapping the logic in a function that's compatible with all of Canva's JWTs. The following code sample demonstrates how to do this:

import { JwksClient } from "jwks-rsa";
import jwt from "jsonwebtoken";
const CACHE_EXPIRY_MS = 60 * 60 * 1_000; // 60 minutes
const TIMEOUT_MS = 30 * 1_000; // 30 seconds
async function getActivePublicKey({
appId,
token,
cacheExpiryMs = CACHE_EXPIRY_MS,
timeoutMs = TIMEOUT_MS,
}: {
appId: string;
token: string;
cacheExpiryMs?: number;
timeoutMs?: number;
}) {
const decoded = jwt.decode(token, {
complete: true,
});
const { kid } = decoded.header;
const jwks = new JwksClient({
cache: true,
cacheMaxAge: cacheExpiryMs,
timeout: timeoutMs,
rateLimit: true,
jwksUri: `https://api.canva.com/rest/v1/apps/${appId}/jwks`,
});
const key = await jwksClient.getSigningKey(decoded.header.kid);
return key.getPublicKey();
}
ts

When calling getActivePublicKey, pass in the ID of the app and the JWT to be verified:

const publicKey = await getActivePublicKey({
appId: "YOUR_APP_ID",
token: "JWT_GOES_HERE",
});
tsx

In the above example, the jwks-rsa library caches the JSON file for the specified amount of time — in this case, 60 minutes. This means the app doesn't have to repeatedly download the file, which:

  • Improves the performance of the app
  • Reduces the risk of DDOS attacks

If you don't use a library, either cache the file or download it on a schedule. Do not download the file every time the backend receives a request, as this is inefficient and makes the backend vulnerable to DDOS attacks.

Use the public key to verify the JWT:

const verified = jwt.verify("JWT_GOES_HERE", publicKey, {
audience: "YOUR_APP_ID",
});
ts

The exact syntax will depend on the library you're using.

If the token is valid, the verified token will be an object. The properties in this object will depend on whether the token is a design token or a user token.

If the token is a valid design token, the object will contain the following properties:

  • aud - The ID of the app.
  • designId - The ID of the current design.

If these properties are not available, it means the token or public key are invalid:

if (!verified.aud || !verified.designId) {
throw new Error("The design token is not valid");
}
tsx

If the token is a valid user token, the object 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, it means the token or public key are invalid:

if (!verified.aud || !verified.brandId || !verified.userId) {
throw new Error("The user token is not valid");
}
tsx