Using design IDs
Sometimes, apps need to associate data with a user's design. For example, an app could present the user with settings that persist on a per-design basis. To allow for this, apps can use the Apps SDK to access the ID of the current design.
If your app uses design IDs, we strongly encourage you to follow our Security guidelines.
Step 1: Get a design and user token
For security reasons, Canva uses JSON Web Tokens(opens in a new tab or window) (JWTs) to encode certain information. 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.
Your app needs to use both tokens to securely store data against a user's design.
To get a user token:
-
Import the
authconstant from the@canva/userpackage:import { auth } from "@canva/user";TSX -
Call the
getCanvaUserTokenmethod:const userToken = await auth.getCanvaUserToken();TSX
To get a design token:
-
Import the
getDesignTokenmethod from the@canva/designpackage:import { getDesignToken } from "@canva/design";TSX -
Call the
getDesignTokenmethod:const designToken = await getDesignToken();TSX
Step 2: Send the tokens to the app's backend
For security reasons, apps must decode and verify tokens through their backend — never through the frontend.
In the same request, the app should also send whatever data it wants to store against the user's design, such as any settings the user has configured through the app's frontend. The shape of this data is highly dependent on the behavior of the app and isn't specific to the Apps SDK, so it's not demonstrated here.
The following code snippet demonstrates how an app can send tokens to a backend:
const response = await fetch(`http://localhost:3001/my/api/endpoint?designToken=${designToken}`,{method: "POST",headers: {Authorization: `Bearer ${userToken}`,},});
In this case, the design token is sent as a query parameter, but this isn't a strict requirement. You could send the token in some other way, such as in the body of a POST request.
The user token must always be sent as an Authorization header. Learn more about HTTP request verification.
Step 3: Verify the tokens
After the backend receives the tokens, it must decode and verify them before it can access the encoded data. For a step-by-step walkthrough of how to do this, see JSON Web Tokens.
For Node.js backends: We recommend using the @canva/app-middleware(opens in a new tab or window) package, which provides built-in JWT verification, JWKS caching, and error handling. This package simplifies the verification process and follows security best practices.
Installation
npm install @canva/app-middleware
Express.js middleware
For Express.js applications, use the built-in middleware:
import express from "express";import { design, tokenExtractors } from "@canva/app-middleware/express";const app = express();// Design token middleware requires explicit token extractionapp.post("/my/api/design",design.verifyToken({appId: process.env.CANVA_APP_ID,tokenExtractor: tokenExtractors.fromQuery("designToken"),}),(req, res) => {// Access verified design informationconst { designId, appId } = req.canva.design;// Your application logic hereres.sendStatus(200);});app.listen(process.env.PORT || 3000);
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 { initDesignTokenVerifier } from "@canva/app-middleware";// Initialize once at app startupconst designTokenVerifier = initDesignTokenVerifier({appId: process.env.CANVA_APP_ID,});// In your request handlerasync function handleRequest(request) {// Extract design token from query parameterconst 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 herereturn { status: 200, body: "Success" };} catch (error) {return { status: 401, body: "Unauthorized" };}}
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);
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);
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"}]}
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:
- Download the JWKS file.
- 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 minutesconst TIMEOUT_MS = 30 * 1_000; // 30 secondsasync 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();}
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",});
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 tokens
Use the public key to verify both the user token and design token:
const verifiedUserToken = jwt.verify("USER_JWT_GOES_HERE", publicKey, {audience: "YOUR_APP_ID",});const verifiedDesignToken = jwt.verify("DESIGN_JWT_GOES_HERE", publicKey, {audience: "YOUR_APP_ID",});
The exact syntax will depend on the library you're using.
If the tokens are valid, the verified tokens will be dictionaries. The user token 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.
The design token 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 tokens or public key are invalid:
if (!verifiedUserToken.aud || !verifiedUserToken.brandId || !verifiedUserToken.userId) {throw new Error("The user token is not valid");}if (!verifiedDesignToken.aud || !verifiedDesignToken.designId) {throw new Error("The design token is not valid");}
Step 4: Store data against the design
After the app's backend verifies the tokens, it will have access to:
- The ID of the design
- The ID of the user
- The ID of the user's team
The backend can then use the combination of these properties to store data that's linked to the design. The key word here is combination, as it's important to note that:
- A design may have multiple users collaborating on it.
- A user may belong to multiple teams.
Therefore, any data shouldn't only be linked with the ID of the design, as this would allow data to be leaked between users or between teams. The data should be linked with the ID of the design, the user, and the user's team.
This means a database table containing data linked to a design would likely have the following columns:
design_idteam_id
But the exact implementation details may be different.
Step 5: Retrieve data for the design
To retrieve data linked with a design, repeat the previous steps but for an endpoint that performs a read operation instead of a write operation. Be sure the data is scoped to the ID of the design, the user, and the user's team.
Security guidelines
To ensure that data is always linked with the correct design, user, and team, follow these guidelines:
- Decode and verify tokens through the backend. Your app should never attempt to decode and verify tokens through its frontend. To learn more, see JSON Web Tokens.
- Get fresh tokens from Canva before sending the tokens to the app's backend. Don't attempt to cache or reuse tokens across multiple HTTP requests.
- Send tokens to an app's backend with any data relevant to the request, such as data required to perform a read or write operation related to a particular design. Don't send tokens and relevant data in separate requests.
The following code sample demonstrates what these guidelines look like in practice:
import { auth } from "@canva/user";import { getDesignToken } from "@canva/design";// Get fresh tokens before every requestconst designToken = await getDesignToken();const userToken = await auth.getCanvaUserToken();// Send tokens to the app's backendconst response = await fetch(`http://localhost:3001/my/api/endpoint?designToken=${designToken}`,{method: "POST",headers: {Authorization: `Bearer ${userToken}`,"Content-Type": "application/json",},// Include relevant data in the same request as the tokensbody: JSON.stringify({name: "David",age: 33,location: "Australia",}),});