JSON Web Tokens

How to verify a JSON Web Token (JWT).

For security reasons, Canva uses JSON Web Tokens(opens in a new tab or window) (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.

Apps must only verify JWTs through the app's backend — never through the frontend.

For Node.js backends: We recommend using the @canva/app-middleware(opens in a new tab or window) package, which provides built-in verification for both user tokens and design tokens, along with JWKS caching and error handling. This package simplifies the verification process and follows security best practices.

Types of tokens

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

  • Design tokens - Encode information about the current design, such as the ID of the design
  • 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 using 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).

Verifying JWTs with @canva/app-middleware

For Node.js backends, the @canva/app-middleware package provides a simple and secure way to verify JWT tokens.

Installation

npm install @canva/app-middleware
SHELL

Express.js middleware

For Express.js applications, use the built-in middleware:

import express from "express";
import { user } from "@canva/app-middleware/express";
const app = express();
// Apply middleware to verify all requests
app.use("/my/api", user.verifyToken({ appId: process.env.CANVA_APP_ID }));
app.post("/my/api/endpoint", (req, res) => {
// Access verified user information
const { userId, brandId } = req.canva.user;
// Your application logic here
res.sendStatus(200);
});
app.listen(process.env.PORT || 3000);
TYPESCRIPT

For more information, see user.verifyToken.

import express from "express";
import { design, tokenExtractors } from "@canva/app-middleware/express";
const app = express();
// Design token middleware requires explicit token extraction
app.post(
"/my/api/design",
design.verifyToken({
appId: process.env.CANVA_APP_ID,
tokenExtractor: tokenExtractors.fromQuery("designToken"),
}),
(req, res) => {
// Access verified design information
const { designId, appId } = req.canva.design;
// Your application logic here
res.sendStatus(200);
}
);
app.listen(process.env.PORT || 3000);
TYPESCRIPT

Design tokens require an explicit tokenExtractor parameter. The available extractors are tokenExtractors.fromQuery(), tokenExtractors.fromCookie(), and tokenExtractors.fromBearerAuth().

Framework-agnostic function

For other Node.js environments (Next.js, AWS Lambda, Cloudflare Workers, and so on):

import { initUserTokenVerifier } from "@canva/app-middleware";
// Initialize once at app startup
const userTokenVerifier = initUserTokenVerifier({
appId: process.env.CANVA_APP_ID,
});
// In your request handler
async function handleRequest(request) {
const authHeader = request.headers.authorization;
const token = authHeader?.replace("Bearer ", "");
if (!token) {
return { status: 401, body: "Unauthorized" };
}
try {
const { userId, brandId } = await userTokenVerifier.verify(token);
// Your application logic here
return { status: 200, body: "Success" };
} catch (error) {
return { status: 401, body: "Unauthorized" };
}
}
TYPESCRIPT

For more information, see initUserTokenVerifier.

import { initDesignTokenVerifier } from "@canva/app-middleware";
// Initialize once at app startup
const designTokenVerifier = initDesignTokenVerifier({
appId: process.env.CANVA_APP_ID,
});
// In your request handler
async function handleRequest(request) {
// Extract design token from query parameter
const url = new URL(request.url);
const designToken = url.searchParams.get("designToken");
if (!designToken) {
return { status: 401, body: "Unauthorized" };
}
try {
const { designId, appId } = await designTokenVerifier.verify(designToken);
// Your application logic here
return { status: 200, body: "Success" };
} catch (error) {
return { status: 401, body: "Unauthorized" };
}
}
TYPESCRIPT

Manually verifying JWTs

If you're using a non-Node.js backend, you can manually verify JWTs. The following steps show how to implement JWT verification from scratch in TypeScript, which you can then adapt to your preferred language or framework.

Step 1: Extract the JWT header

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, 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

To find a JWT library for your preferred language or framework, see jwt.io/libraries(opens in a new tab or window).

Step 2: Get the kid property

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

Step 3: Get the active public key

Canva exposes a JSON Web Key Set(opens in a new tab or window) (JWKS) through the following endpoint:

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

YOUR_APP_ID is a placeholder for the ID of the app. You can find the ID of the app in the Developer Portal, using the Your apps(opens in a new tab or window) page.

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 jwks.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

Caching the JWKS

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. Don't download the file every time the backend receives a request, as this is inefficient and makes the backend vulnerable to DDOS attacks.

Your app's backend can safely overwrite the JWKS file. You don't need to store multiple versions of it. Your app only needs the latest version of the file.

Step 4: Verify the token

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 a dictionary. The properties in this dictionary will depend on whether the token is a design token or a user token.

Design tokens

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 aren't 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

User tokens

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 aren't 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