Verifying requests
When the frontend of an app sends an HTTP request, it must use the getCanvaUserToken
method to generate a JSON Web Token (JWT) and include that JWT with the request:
const token = await getCanvaUserToken();const response = await("http://localhost:3001/custom-route", {headers: {Authorization: `Bearer ${token}`,},});
The backend must then verify that the JWT is valid and reject the request if it's not. This is a security mechanism that reduces the risk of requests from malicious third-parties.
The starter kit includes a backend that automatically validates the JWT, but if you have an existing backend or are working with a different tech stack, you'll need to implement the logic yourself.
This guide explains everything you need to know about setting up a backend that can verify requests.
Step 1: Run the mock server for Canva's backend
When an app calls the getCanvaUserToken
method, the planned behavior is for Canva to generate a JWT via its backend. At this stage though, the backend for Canva has not been implemented.
As a stop-gap, we've provided a mock server in the starter kit that mimics the planned behavior.
To run the mock server:
-
Copy the ID of an app from the Your apps page.
-
In the starter kit's
.env
file, setCANVA_APP_ID
to the ID of the app. -
Run the following command:
npm run start:mock-canva-backend
The mock server must run concurrently with the app's frontend and backend servers.
When Canva's actual backend is released, the app can be updated to point to Canva's server, rather than the local server. No additional changes will be required.
Step 2: Get a list of public keys from Canva's backend
Canva exposes a JSON file that contains a list of public keys. This is an example of the JSON file:
{"auth_key": {"app": "YOUR_APP_ID","public_keys": [{"key_id": "FAKE_KEY_ID_1","activation_time_ms": 123456789,"jwk": "-----BEGIN PUBLIC KEY-----\n ..."},{"key_id": "FAKE_KEY_ID_2","activation_time_ms": 234567891,"jwk": "-----BEGIN PUBLIC KEY-----\n ..."},{"key_id": "FAKE_KEY_ID_3","activation_time_ms": 345678912,"jwk": "-----BEGIN PUBLIC KEY-----\n ..."}]}}
An app's backend must use one of these keys — only one of them is active at any point in time — to verify that a JWT is legitimate.
When the server starts up, download the JSON file from the following endpoint:
/v0/apps/YOUR_APP_ID/jwks
Be sure to replace YOUR_APP_ID
with the ID of the app, as the JWTs are unique to each app.
Until the actual backend is released, the endpoint is available via the mock server:
http://localhost:3002/v0/apps/YOUR_APP_ID/jwks
In the future, the endpoint will be available via the public-api.canva.com
domain:
https://public-api.canva.com/v0/apps/YOUR_APP_ID/jwks
The backend must refresh the file on a regular basis to ensure that the app has access to the latest public keys. We recommend updating the file every 60 minutes.
Step 3: Extract the JWT from the Authorization
header
When an app sends a request to its backend, it includes a JWT in the Authorization
header. Upon receiving the request, the backend should extract the token from the header:
import { Request, Response } from "express";function getTokenFromHeader(request: Request) {const header = request.headers["Authorization"];if (!header) {throw new Error("Authorization header is missing");}const parts = header.split(" ");if (parts.length !== 2 || parts[0] !== "Bearer") {throw new Error("Invalid Authorization header format");}const [_, token] = parts;return token;}
Be sure to remove the Bearer
prefix and any whitespace.
Step 4: Decode the JWT (without verifying it)
JWTs are made up of three parts:
- Header
- Payload
- Signature
After extracting the token from the Authorization
header, use a JWT library to:
- Decode the JWT
- Extract the header from the JWT
By default, some libraries don't extract the header. For example, if you're using the jsonwebtoken library for Node.js, you must pass an object into the decode
method and set complete
to true
:
import jwt from "jsonwebtoken";const decodedToken = jwt.decode("TOKEN GOES HERE", {complete: true,});console.log(decodedToken);
Step 5: Extract the kid
property from the JWT header
The JWT header object contains a kid
property:
import jwt from "jsonwebtoken";const decodedToken = jwt.decode("TOKEN GOES HERE", {complete: true,});const kid = decodedToken.header.kid;console.log(kid);
This is known as the key id.
Extract this value from the decoded JWT, as it's required for the next step.
Step 6: Get a public key
Loop through the public keys that were downloaded from Canva's backend and check if the key_id
of any of them is equal to the kid
property from the JWT header:
interface AuthKeyJson {auth_key: AuthKey;}interface AuthKey {app: string;public_keys: PublicKey[];}interface PublicKey {key_id: string;activation_time_ms: number;jwk: string;}function findPublicKeyById(json: AuthKeyJson,kid: string): PublicKey | undefined {return json.auth_key.public_keys.find((key) => key.key_id === kid);}
If there's no match, the JWT is invalid and the request should be rejected:
const publicKey = findPublicKeyById(json, kid);if (!publicKey) {console.log("Public key not found");response.sendStatus(401);return;}
Step 7: Check that the public key is active
Each object in the public_keys
array has an activation_time_ms
property. This property is a UNIX timestamp, in milliseconds, that identifies when the key is active from.
The backend should check if the key is active, and if it's not, reject the request:
const currentTime = new Date().getTime();if (publicKey.activation_time_ms > currentTime) {console.log("Public key is not active");response.sendStatus(401);return;}
Step 8: Verify the JWT
The final step is use a JWT library to verify that the JWT is valid.
This requires two ingredients:
- The JWT itself
- The
jwk
property from the public key object
The following snippet demonstrates how to use the verify
method from the jsonwebtoken library:
const verifiedToken = jwt.verify("TOKEN", "PUBLIC_KEY_JWK");
If the token is valid, the return value will be an object containing the following properties:
aud
- The ID of the app.userId
- The ID of the user.brandId
- The ID of the user's team.
If these properties are available, the JWT is valid and the request can be accepted:
const isValidToken =verifiedToken["userId"] && verifiedToken["brandId"] && verifiedToken["aud"];if (isValidToken) {console.log("The request was successful!");response.sendStatus(200);} else {console.log("The request was not successful.");response.sendStatus(401);}
Otherwise, the JWT is invalid and the request should be rejected.