Using design IDs

How to store and retrieve data against a user's design.

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.

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:

  1. Import the auth constant from the @canva/user package:

    import { auth } from "@canva/user";
    tsx
  2. Call the getCanvaUserToken method:

    const userToken = await auth.getCanvaUserToken();
    tsx

To get a design token:

  1. Import the getDesignToken method from the @canva/design package:

    import { getDesignToken } from "@canva/design";
    tsx
  2. Call the getDesignToken method:

    const designToken = await getDesignToken();
    tsx

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}`,
},
}
);
tsx

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;
}
tsx

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 Verifying JWTs.

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 minutes
const TIMEOUT_MS = 30 * 1_000; // 30 seconds
const 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 request
if (!userToken) {
return response.sendStatus(401);
}
const verifiedUserToken = await verifyUserToken(CANVA_APP_ID, userToken);
// If the user token is not valid, reject the request
if (
!verifiedUserToken.aud ||
!verifiedUserToken.brandId ||
!verifiedUserToken.userId
) {
return response.sendStatus(401);
}
// If the design token is not available, reject the request
if (!designToken) {
return response.sendStatus(401);
}
const verifiedDesignToken = await verifyDesignToken(
CANVA_APP_ID,
designToken
);
// If the design token is not valid, reject the request
if (!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;
}
tsx

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.

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.

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 Verifying JWTs.
  • 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 request
const designToken = await getDesignToken();
const userToken = await auth.getCanvaUserToken();
// Send tokens to the app's backend
const 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 tokens
body: JSON.stringify({
name: "David",
age: 33,
location: "Australia ",
}),
}
);
tsx