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.
Step 1: Get a design and user token
For security reasons, Canva uses JSON Web Tokens (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
auth
constant from the@canva/user
package:import { auth } from "@canva/user";tsx -
Call the
getCanvaUserToken
method:const userToken = await auth.getCanvaUserToken();tsx
To get a design token:
-
Import the
getDesignToken
method from the@canva/design
package:import { getDesignToken } from "@canva/design";tsx -
Call the
getDesignToken
method:const designToken = await getDesignToken();tsx
Step 2: Send the tokens to the app's backend
For security reasons, apps must decode and verify tokens via their backend — never via 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 via the app's frontend. The shape of this data is highly dependent on the behavior of the app and is not 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}`,{method: "POST",headers: {Authorization: `Bearer ${userToken}`,},});
In this case, the design token is sent as a path parameter, but this is not a strict requirement. You could send the token in some other way, such as in the body of a POST
request or as a query string parameter.
The user token must always be sent as an Authorization
header.
The following snippet demonstrates how a backend could handle the incoming request:
import express from "express";const app = express();app.post("/my/api/endpoint/:designToken", async (request, response) => {const userToken = getTokenFromHeader(request);const designToken = request.params.designToken;});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;}
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.
The following snippet demonstrates how an app could decode and verify the tokens:
import express from "express";import { JwksClient } from "jwks-rsa";import jwt from "jsonwebtoken";const { CANVA_APP_ID } = process.env;const CACHE_EXPIRY_MS = 60 * 60 * 1_000; // 60 minutesconst TIMEOUT_MS = 30 * 1_000; // 30 secondsconst app = express();app.post("/my/api/endpoint/:designToken", async (request, response) => {const userToken = getTokenFromHeader(request);const designToken = request.params.designToken;// If the user token is not available, reject the requestif (!userToken) {return response.sendStatus(401);}const verifiedUserToken = await verifyUserToken(CANVA_APP_ID, userToken);// If the user token is not valid, reject the requestif (!verifiedUserToken.aud ||!verifiedUserToken.brandId ||!verifiedUserToken.userId) {return response.sendStatus(401);}// If the design token is not available, reject the requestif (!designToken) {return response.sendStatus(401);}const verifiedDesignToken = await verifyDesignToken(CANVA_APP_ID,designToken);// If the design token is not valid, reject the requestif (!verifiedDesignToken.aud || !verifiedDesignToken.designId) {return response.sendStatus(401);}});app.listen(process.env.PORT || 3000);async function verifyUserToken(appId: string, token: string) {const publicKey = await getPublicKey({ appId, token });return jwt.verify(token, publicKey, {audience: appId,});}async function verifyDesignToken(appId: string, token: string) {const publicKey = await getPublicKey({ appId, token });return jwt.verify(token, publicKey, {audience: appId,});}async function getPublicKey({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();}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;}
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 should not 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_id
team_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 via the backend. Your app should never attempt to decode and verify tokens via 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}`,{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",}),});