Authenticating users

How to authenticate users.

Authentication in Canva is about creating a seamless experience by ensuring that your app remembers your users. How you create this experience in your app can depend on the type of app and how you want to store user data.

Check out the Authentication design guidelines

Our design guidelines help you create a high-quality app that easily passes app review.

Apps can handle users with two forms of authentication:

  • Frictionless authentication (preferred): This ensures that user data or credentials are automatically remembered to help users retain data, even without them passing through a login screen.
  • Manual authentication: This requires your users to authenticate with a third-party platform to access certain content or features within an app, including their own data.

Users confronted with a login prompt could consider it a hurdle. Any hurdles in the onboarding process come with a chance for them to drop out of your funnel, maybe to never return again. Because of this, we encourage you to consider frictionless authentication.

Frictionless authentication is when you use the auth.getCanvaUserToken method to retain a user's data without them having to sign-in via a login screen. With this, you can do away with traditional, manual authentication entirely or delay it until a user tests your app to see the value it provides.

Using the auth.getCanvaUserToken method you can:

  • Retrieve a token that uniquely identifies the Canva user and their associated brand
  • Store the user ID on your end
  • Learn about how your users use your app.

We've seen that if a user experiences most of an app's features, they're less likely to abandon the flow than if they're required to authenticate up front.

Manual authentication is the more traditional approach when a user logs in via a third-party so they can access certain content or features within the app.

By doing this, you can:

  • Give users access to their personal content, such as photos or videos
  • Give users access to certain features based on their specific privileges
  • Monetize apps by offering features or content to paying customers.

Some examples of apps that support authentication include:

For more examples, see the Apps Marketplace.

The workflow looks like the following:

  1. The user interacts with your app. Canva assigns a unique user ID to the user.
  2. You track how the user uses your app. This interaction, as well as the associated user and app ID, is stored in the app backend.
  3. Your app backend stores the user ID in a database.

As well as the above, you, as the app developer exploring metrics, can use the stored data to know more about your users, such as:

  • Track usage: You can monitor how much a user has used your services in Canva, regardless of whether they have interacted with the design. This is especially useful for AI apps where there's a real cost to serve for running the model. Here, you do all the tracking using the data in your app's backend.
  • Track time in app: You can save the date the user initially created their account (When the user token was created), then compare that date to today's date to see how long they've been using your app.

To implement frictionless authentication, an app needs the ID of the user and their team, which you can get using a JSON Web Token (JWT).

To get a JWT for the current user:

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

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

    const token = await auth.getCanvaUserToken();
    ts

    This method returns a JWT as a string.

By itself, the JWT is a meaningless string of characters. To get the ID of the user and their team from the JWT, an app must send the JWT to the app's backend then verify that JWT.

To send the JWT to the app's backend, use the Fetch API, or a library such as axios:

const response = await fetch("http://localhost:3001/my/api/endpoint", {
headers: {
Authorization: `Bearer ${token}`,
},
});
ts

When sending the request, include an Authorization header that contains the word Bearer and the JWT, separated by a space.

When the backend receives the request, extract the JWT from the Authorization header. The following code demonstrates how to extract a JWT from an Express.js Request object:

import express from "express";
const app = express();
app.post("/my/api/endpoint", async (request, response) => {
const token = getTokenFromHeader(request);
if (!token) {
return response.sendStatus(401);
}
return response.sendStatus(200);
});
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;
}
ts

To verify the JWT, follow the steps in Verifying JWTs. If the JWT is valid, you will end up with an object that contains the ID of the user and their team.

When the user opens the app for the first time, use the ID of the user and their team to create a user record in the backend's database. This is essentially a registration process, except that it's invisible to the user — an account is created for them in the app's backend when all they've done is access the app.

As the user interacts with the app, or when they return to the app at a later time, send additional HTTP requests to create, read, or update data associated with their account. With each request, the app will need to send and verify a JWT in order to get the ID of the user and their team.

This section explains how to set up authentication with a third-party platform.

The diagram below illustrates the lifecycle of an authentication flow that integrates with a third-party platform. Refer to the instructions below to understand how to implement each of the steps.

By default, authentication is disabled.

To enable authentication:

  1. Log in to the Developer Portal.
  2. Navigate to an app via the Your apps page.
  3. Click Add authentication.
  4. Enable This app requires authentication.

When authentication is enabled, the following fields become active:

  • Redirect URL
  • Authentication base URL

Both of these fields must contain a URL and, in the coming steps, we'll set up those URLs.

Apps are responsible for triggering the start of an authentication flow. When an app triggers the start of an authentication flow depends on the desired user experience. For example, you might want to start an authentication flow when a user tries to access restricted content.

To trigger the start of an authentication flow:

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

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

    const result = await auth.requestAuthentication();
    ts

The requestAuthentication method opens a popup window and, within this window, redirects the user to the following URL:

<authentication_base_url>/configuration/start

<authentication_base_url> is a placeholder that will be replaced with the app's Authentication base URL. You can configure this URL in the Developer Portal, via the Add authentication page.

For example, if the Authentication base URL is:

https://www.example.com

Then the complete URL would be:

https://www.example.com/configuration/start

This URL doesn't exist yet though, so we'll set it up in the next step.

The Authentication base URL must be available to Canva's backend. This means it must be exposed via the public internet. If it's not, Canva won't be able to send requests to it.

To do this, we recommend either of the following options:

  • Deploy the app's backend to a non-production environment.
  • Use an SSH tunneling service, such as ngrok, to expose a local server.

In the app's backend, set up an endpoint that handle GET requests sent to /configuration/start:

import * as express from "express";
import * as crypto from "crypto";
const app = express();
app.get("/configuration/start", (req, res) => {
// TODO: Handle the request
});
// Start the server
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
ts

This endpoint must:

  • Extract the state parameter from the request's query parameters
  • Generate a unique nonce for each request
  • Store the nonce and an expiry time in a cookie
  • Sign the cookie to avoid cookie tampering
  • Redirect the user back to Canva

Canva appends a state query parameter to the /configuration/start endpoint. This parameter contains a random string of characters that must be returned to Canva for security reasons.

For the time being, extract the parameter from the request object:

const { state } = req.query;
ts

In the /configuration/start endpoint, generate a unique nonce for each request.

A nonce is a random, single-use value that's impossible to guess or enumerate. We recommended using a Version 4 UUID that is cryptographically secure, such as one generated with randomUUID:

import * as crypto from "crypto";
const nonce = crypto.randomUUID();
ts

After generating a nonce, set a cookie that contains the nonce and an expiry time.

The cookie should be:

It should also have an expiry time in addition to the expiry time being included in the cookie itself.

By signing a cookie, the app's backend can verify that the user hasn't modified the cookie. This prevents bad actors from tampering with the cookie in the browser.

The steps for setting and signing cookies depend on the framework. Some frameworks automatically sign cookies, while some don't have built-in support for cookie signing and require custom code.

It's beyond the scope of this documentation to explain all of the possible approaches, but refer to the following links to learn more about cookie signing in some popular frameworks:

The following code sample demonstrates how to set cookies with Express.js and cookieParser:

import * as express from "express";
import * as crypto from "crypto";
import * as cookieParser from "cookie-parser";
const app = express();
// TODO: Load a cryptographically secure secret from an environment variable
app.use(cookieParser("SECRET GOES HERE"));
// The expiry time for the nonce, in milliseconds
const NONCE_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
app.get("/configuration/start", (req, res) => {
// Generate a nonce
const nonce = crypto.randomUUID();
// Create an expiry time for the nonce
const nonceExpiry = Date.now() + NONCE_EXPIRY_MS;
// Store the nonce and expiry time in a stringified JSON array
const nonceWithExpiry = JSON.stringify([nonce, nonceExpiry]);
// Store the nonce and expiry time in a cookie
res.cookie("nonceWithExpiry", nonceWithExpiry, {
httpOnly: true,
secure: true,
signed: true,
maxAge: NONCE_EXPIRY_MS,
});
});
ts

It's worth nothing that:

  • To sign a cookie, you need a cryptographically secure secret, such as one generated by the randomUUID method. This secret should:
    • be loaded via an environment variable
    • not change between server restarts
    • not be committed to source control
  • To store the nonce and an expiry time in a cookie, the above code sample stringifies a JSON array that contains both of the values, but there are other ways to accomplish the same outcome.
  • You should be mindful of framework-specific nuances. For example, in Express.js, the maxAge property is set in milliseconds, but the Max-Age attribute value must be set in seconds.

After setting the cookie, redirect the user to the following URL with a 302 redirect:

https://www.canva.com/apps/configure/link?state=<state>&nonce=<nonce>

You'll need to:

  • Replace <state> with the state query parameter that Canva included with the request.
  • Replace <nonce> with the nonce that was stored in the cookie.

For example:

// Extract state from query parameters
const { state } = req.query;
// Create query parameters
const params = new URLSearchParams({
state,
nonce,
});
// Redirect the user
res.redirect(
302,
`https://www.canva.com/apps/configure/link?${params.toString()}`
);
ts

When the redirect completes, Canva redirects the user to the app's Redirect URL. We'll see how to set up the Redirect URL in the next step.

To authenticate users, apps need a Redirect URL — a page, hosted on the app's infrastructure, that allows a user to authenticate with the app's platform.

You can configure the app's Redirect URL in the Developer Portal, via the Add authentication page.

How a user authenticates isn't strictly important. The Redirect URL could point to a login form with a username and password, an OAuth 2.0 authorization flow, or something else altogether. The key is that the platform can identify the user and then link that identity to the user's Canva account.

When Canva redirects to the Redirect URL, it appends the following query parameters to the URL:

  • canva_user_token
  • nonce
  • state

Your app's backend can use these parameters to securely authenticate a user.

When a user arrives at the Redirect URL, the app needs to validate the nonce that was generated in the /configuration/start endpoint. This ensures that the user who started the authentication flow is the same one who is continuing it (and not some attacker trying to impersonate the user).

To validate the nonce:

  1. Get the nonce query parameter:

    const nonceQuery = req.query.nonce;
    ts
  2. Get the cookie that contains the nonce and its expiry time:

    const nonceWithExpiryCookie = req.signedCookies.nonceWithExpiry;
    ts
  3. Clear the cookie:

    res.clearCookie("nonceWithExpiry");
    ts
  4. Parse the value of the cookie:

    try {
    const [nonceCookie, nonceExpiry] = JSON.parse(nonceWithExpiryCookie);
    } catch (e) {
    // TODO: Handle errors
    }
    ts

    (This code uses a try/catch block to account for the possibility of JSON parsing exceptions.)

  5. Verify that:

    • The nonce from the cookie has not expired.
    • The nonces are not nullish.
    • The nonces are not empty strings.
    • The nonces are equal to one another.

    For example:

    if (
    Date.now() > nonceExpiry || // The nonce has expired
    typeof nonceCookie !== "string" || // The nonce in the cookie is not a string
    typeof nonceQuery !== "string" || // The nonce in the query parameter is not a string
    nonceCookie.length < 1 || // The nonce in the cookie is an empty string
    nonceQuery.length < 1 || // The nonce in the query parameter is an empty string
    nonceCookie !== nonceQuery // The nonce in the cookie does not match the nonce in the query parameter
    ) {
    // The nonce is NOT valid
    }
    ts

If the nonce validation fails, redirect the user to the following URL with a 302 redirect:

https://www.canva.com/apps/configured?success=false&state=<state>&errors=<errors>

You'll need to:

  • Replace <state> with the state query parameter that Canva included with the request.
  • Replace <errors> with a comma-separated list of one or more error codes. You can define the error codes yourself. They will be passed as-is to the app's frontend.

For example:

// Get the nonce from the query parameter
const nonceQuery = req.query.nonce;
// Get the nonce with expiry time from the cookie
const nonceWithExpiryCookie = req.signedCookies.nonceWithExpiry;
try {
// Parse the JSON that contains the nonce and expiry time
const nonceWithExpiry = JSON.parse(nonceWithExpiryCookie);
// Extract the nonce and expiry time
const [nonceCookie, nonceExpiry] = nonceWithExpiry;
// Clear the cookie
res.clearCookie("nonceWithExpiry");
// If the nonces are invalid, terminate the authentication flow
if (
Date.now() > nonceExpiry || // The nonce has expired
typeof nonceCookie !== "string" || // The nonce in the cookie is not a string
typeof nonceQuery !== "string" || // The nonce in the query parameter is not a string
nonceCookie.length < 1 || // The nonce in the cookie is an empty string
nonceQuery.length < 1 || // The nonce in the query parameter is an empty string
nonceCookie !== nonceQuery // The nonce in the cookie does not match the nonce in the query parameter
) {
const params = new URLSearchParams({
success: "false",
state: req.query.state,
errors: "invalid_nonce",
});
return res.redirect(
302,
`https://www.canva.com/apps/configured?${params.toString()}`
);
}
} catch (e) {
// An unexpected error has occurred (e.g. JSON parsing error)
const params = new URLSearchParams({
success: "false",
state: req.query.state,
errors: "invalid_nonce",
});
return res.redirect(
302,
`https://www.canva.com/apps/configured?${params.toString()}`
);
}
ts

We also recommend logging a security alert, as invalid nonces suggest a potential threat.

If the nonce validation succeeds, allow the user to authenticate. This is the part of the process that is highly dependent on the third-party platform, as there are many authentication strategies available.

After the user authenticates — for example, by entering a username and password — the next step is to create a link between the user's account on the platform and the user's account with Canva.

To link a user's accounts:

  1. Grab the canva_user_token query parameter that Canva includes with the request. This parameter is a JSON Web Token (JWT) that is unique to the user.
  2. Verify the JWT to get the ID of the user and their team.
  3. Use the IDs to create a mapping between the user in Canva and the user in the app's backend.

There are many ways to create a mapping, so it's not practical to document all of the possible options, but we can provide an example:

If your app's backend has a users table in its database, you could add an canvaId column to the table. This column could contain a composite key made up of the IDs. For example, if the user's ID is 123 and the ID of their team is 456, the composite key would be something like 123:456.

When a user authenticates via the Redirect URL, the app could save the composite key to the database. The app could then check for the existence of this key to determine if the user is authenticated.

At the end of an authentication flow, the app needs to:

  • Know that the authentication flow has ended.
  • Know the outcome of the authentication flow.
  • Close the popup window.

If the user is able to successfully authenticate with the third-party platform, redirect them to the following URL from within the popup window:

https://www.canva.com/apps/configured?success=true&state=<state>

Replace <state> with the value of the state token from the start of the authentication flow.

If the user is not able to authenticate — for example, they have too many failed login attempts — redirect them to the following URL:

https://www.canva.com/apps/configured?success=false&state=<state>&errors=<errors>

Be sure to:

  • Replace <state> with the value of the state token from the start of the authentication flow.
  • Replace <errors> with a comma-separated list of one or more error codes. You can define the error codes yourself. They will be passed as-is to the app's frontend.

If you're performing the redirect via the app's backend, use a 302 redirect:

const params = new URLSearchParams({
success: "true",
state: req.query.state,
});
response.redirect(
302,
`https://www.canva.com/apps/configured?${params.toString()}`
);
ts

If you're performing the redirect via the app's frontend, use the window.location.replace method:

const params = new URLSearchParams({
success: "true",
state: req.query.state,
});
window.location.replace(
`https://www.canva.com/apps/configured?${params.toString()}`
);
ts

When the authentication flow ends, successfully or otherwise, the requestAuthentication method returns an object with a status property that describes the outcome of the flow:

const result = await auth.requestAuthentication();
console.log(result.status);
ts

You can use the status property to render an appropriate user interface based on the result of the flow — for example, rendering an error when the authentication fails, or rendering certain content or features when authentication succeeds.

The possible values of the status property are:

  • "COMPLETED" - The user was able to authenticate.
  • "DENIED" - The user was not able to authenticate.
  • "ABORTED" - The user closed the popup window.

When the status property is "DENIED", the object has a details property that contains an array of error codes. These are the error codes provided by the app (if any) when it redirected back to Canva.

When a user opens or interacts with an app, the app will need to check if the user is authenticated and, in some cases, render privileged content or features for that user.

To accomplish this, generate a JWT for the current user in the app's frontend:

const token = await auth.getCanvaUserToken();
ts

Then send an HTTP request to the app's backend with the JWT in the Authorization header:

const response = await fetch("https://localhost:3001/authenticated-endpoint", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
});
ts

When the backend receives the request:

  1. Extract the JWT from the Authorization header.
  2. Verify the JWT to get the ID of the user and their team.
  3. Use the IDs to check if the user is authenticated. How the app does this depends on how the app created a mapping between the user in their backend and the user on Canva.

If the user is authenticated, respond with whatever the frontend needs to render the authenticated content or features — for example, the user's photos or specific privileges.

If the user is not authenticated, respond to the request with an error. The frontend can use this error to restrict what the user has access to and to prompt them to authenticate.

After a user connects (installs) an app, they have the option of disconnecting (uninstalling) it.

If a user has authenticated with a third-party platform, disconnecting the app in Canva won't automatically remove the connection between Canva and the platform — as far as the platform is concerned, the user will still be authenticated.

This is a subpar user experience for a couple of reasons:

  • Disconnecting an app signals that the user's intent is to no longer be associated with the app and the app should respect that intent.
  • If the user reconnects the app at a later time, they'll already be authenticated. This is a confusing user experience that's inconsistent with how the the rest of Canva works.

To address this issue, Canva sends a POST to the following endpoint when a user disconnects an app:

<authentication_base_url>/configuration/delete

<authentication_base_url> is a placeholder that will be replaced with the app's Authentication base URL. You can configure this URL in the Developer Portal, via the Add authentication page.

In the request's headers, Canva includes an Authorization header that contains a JWT for the current user. When the app receives this request, it should:

  1. Extract the JWT from the Authorization header.
  2. Verify the JWT to get the ID of the user and their team.
  3. Use the IDs to find the user in the app's backend.
  4. Remove any connection between the user and Canva.

When the disconnection is complete, respond with a 200 status code and the following object:

{
"type": "SUCCESS"
}
json

To confirm that the disconnection flow is working:

  1. Navigate through the authentication flow.
  2. Disconnect (uninstall) the app.
  3. Reconnect (install) the app.

You should have to re-authenticate to access any privileged content or features. If you don't have to re-authenticate, double-check the disconnection logic.

How you create an app and create ways for users to authenticate can shape the user experience. The following are workflows you can consider to create a delightful authentication flow.

If a Canva user has an account with your app (for example, if they have previously accessed your app with Canva) fetching the user token should be enough for you to identify if they do. Then you can either:

  • Continue allowing them access to the content associated with that user token.
  • Prompt them to log in to their account using manual authentication.

A user might have an account with your app, but have never accessed your app through Canva, so you don't have a user token associated to them.

In this case we recommend adding non-blocking text such as 'Already have an account? Click here to sign in.' at the bottom of your app to provide them the opportunity to initiate the authentication process.

If the trial is time-based, you can save the date of initial account creation and then check if that date + [trial time period] is before today's date. If it is, then allow the user to continue accessing the app. If it is not, you can restrict the UI in the app until the user authenticates and subscribes to an appropriate plan.

If the trial is usage-based, track usage as above and then restrict usage of the app when the tracked usage is the same as your defined usage limit.

If your app is free, you can use tracking usage for your analytics to understand how users are using your app. Free apps typically see higher usage, which count towards your app usage award tier - so you can make money without ever needing to set up subscriptions or authenticating users!

If you implement a freemium model for your app, which allows a certain amount of free usage per month (same as free trial), get them to upgrade, but reset at the end of the month.

If they can only use certain features, consider:

  • Allowing access to those features within the free tier
  • When they want to try a paid feature, give them some access to trial it (and track as you would any other usage and then require users authenticate and then subscribe to continue using that feature).